西安网站建设网络公司网络广告
文章目录
- 1. innodb引擎结构
- 1.1 逻辑存储结构
- 1.2 存储架构
- 1.2.1 内存结构
- 1.2.2 磁盘结构
- 1.2.3 mysql服务端有哪些线程
- 2. 事务
- 2.1 事务并发可能存在的问题
- 2.1.1 脏读
- 2.1.2 不可重复读
- 2.1.3 幻读
- 2.1.4 小结
- 2.2 事务的隔离性等级
- 2.3 事务隔离等级的实现
- 2.4 事务的例子
- 2.4.1 存储过程中使用事务
- 2.4.2 异常处理中回滚到保存点
- 3. ACID的实现
- 3.1 持久性
- 3.2 一致性
- 3.3 原子性
- 3.4 隔离性
- 3. 4.1 前置概念
- 当前读
- 快照读
- 3.4.2 MVCC
- 记录的三个隐式字段
- undo log
- read view
- 使用read view 访问undo log版本链的规则
1. innodb引擎结构
1.1 逻辑存储结构
表空间(.idb文件) -> 段(数据段,索引段,回滚段) -> 区(1MB) -> page(16KB)-> row
1.2 存储架构
1.2.1 内存结构
buffer pool(page为单位,free page(空闲页),clean page(数据没有被修改),dirty page(数据修改过,需要写回磁盘)),change buffer(缓存DML操作结果,下次查询则合并它和buffer pool中的数据),log buffer(缓存日志数据),adaptive hash index(自动对适合建立哈希索引的字段建立hash索引)
1.2.2 磁盘结构
各种表空间
system tablesapce:change buffer在其中,也可是表数据的存放地(表数据可以存放在system tablespaces,file-per-table tablespaces,general tablespaces中)。变量innodb_data_filepath记录了system tablespaces文件地址。/var/lib/mysql/iddata1
file-pre-table tablespaces:存放单个innodb表的数据和索引,一个表一个 .ibd 文件。变量innodb_file_per_table记录是否开启一个文件一个表的功能,默认开启。
general tablespaces:用户自己创建tablespace,并加入表。
-- create tablesapce
create tablespace myTableSpace add datafile 'myTableSpace.ibd' engine = innodb;-- adhere table to tablespaces
create table userTbl (id int auto_increment primary key, name varchar(10) not null default '', gender varchar(10) not null default '') tablespace myTableSpace;
undo tablespaces:用户存储undo日志,默认两个文件(初始大小均为16MB):undo_001,undo_001
Temporary tablesapces:存放会话临时表和全局临时表。
doublewrite buffer files:为保证数据安全,数据从buffer pool中刷新到磁盘时,需要先放到doublewrite buffer files中,以便故障时恢复数据。默认两个doublewrite buffer files:‘#ib_16384_0.dblwr’, ‘#ib_16384_1.dblwr’
redo log:用于实现事务的持久性。分为两部分:位于内存的redo log buffer,位于磁盘的redo log。事务结束后,将改动写入redo log,刷新脏页到磁盘发生错误时,用redo log来恢复。
1.2.3 mysql服务端有哪些线程
Master Thread:调度管理其他线程。将buffer pool中的数据刷新到磁盘。脏页刷新,合并数据后插入buffer pool,回收undo页。
IO Thread:异步处理IO请求,调用4种回调函数来处理4中类型的IO请求:Read Thread(读操作,默认4个),Write Thread(写操作,默认4个),Log Thread(日志缓冲区刷到磁盘,默认1个),Insert buffer thread(刷新写缓冲区到磁盘,默认1个)。show engin innodb status
可以查看有哪些IO Thread。
Puege Thread:事务提交后,undo log页就不需要了,需要回收它们。回收事务已经提交的undo log
Page Cleaner Thread:协助master thread进行脏页刷新,缓解master thread压力。
2. 事务
什么是事务:使用事务来确保成批的sql语句或者全部执行或者全部不执行,以此来保证数据的完整性。如果事务在执行过程中有错误则要回退到某个已知的安全状态 。可以回退的语句:INSERT,UPDATE,DELETE。
事务的特性(ACID):原子性(atomicity,要么都执行,要么都不执行);一致性(Consistency,数据保持一致);隔离性(Isolation,不同事务独立执行);持久性(Duration,事务提交成功,对数据库的变动永久保存)。
实现ACID的机制:通过redo log undo log实现原子性,一致性和持久性。通过MVCC和锁机制实现隔离性。
2.1 事务并发可能存在的问题
2.1.1 脏读
事务A读取了事务B未提交的数据,结果事务B执行出错发生回滚,那么事务A读取的数据就是无效的。即,读了非稳定状态的数据。
-- 准备数据
create table account(id int auto_increment primary key, name varchar(10) not null default '', balance int not null default 0);
insert into account values(1, 'aa', 2000), (2, 'bb', 3000), (3, 'cc', 4500);+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | aa | 2000 |
| 2 | bb | 3000 |
| 3 | cc | 4500 |
+----+------+---------+-- 设置当前会话隔离级别为read uncommited
set session transaction isolation level read uncommitted;-- 执行时序如下
-- client2
start transaction;
update account set balance = balance - 1000 where name = 'aa';-- client1
select balance from account where name ='aa'; -- 1000,脏数据-- client2: 出错发生回滚
update account set balance = balance - 1000 where name = 'dd';
rollback;-- client1
select balance from account where name ='aa'; -- 2000
2.1.2 不可重复读
在一个事务看来,它没有改动某条数据,那么这条数据就是对于该事务而言是稳定的,两次读取该条数据的结果应该是不变的(幂等性)。不可重复读相当于数据不具备这种幂等性,比如事务A中需要两次读取某数据,改数据在事务A两次读取的间隙被事务B修改。那么事务A两次读取的数据是不一致的。即,两次读的不一致。
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | aa | 2000 |
| 2 | bb | 3000 |
| 3 | cc | 4500 |
+----+------+---------+-- 执行时序如下
-- client1
start transaction;
select balance from account where name ='aa'; -- 2000-- client2
start transaction;
update account set balance = balance - 1000 where name = 'aa';
commit;-- client1
select balance from account where name ='aa'; -- 1000
commit;
2.1.3 幻读
事务A使用相同的筛选条件两次检索数据,第二次查询的数据比第一次查询的数据多。这可能是因为事务B在两次查询间隙插入了符合相同筛选条件的新数据。即,插入不存在的数据失败。
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | aa | 2000 |
| 2 | bb | 3000 |
| 3 | cc | 4500 |
+----+------+---------+-- 执行时序如下
-- client1
start transaction;
select balance from account where id = 4; -- 确认id=4的记录不存在-- client2
start transaction;
insert into account values(4, 'dd', 3500);
commit;-- client1
insert into account values(4, 'dd', 3500); -- 插入失败,client2捷足先登
-- ERROR 1062 (23000): Duplicate entry '4' for key 'account.PRIMARY'
commit;
2.1.4 小结
脏读:读一次,数据可能没用。
不可重复读:读两次,前后不一。
幻读:读无插有,似无中生有。
2.2 事务的隔离性等级
读未提交(READ UNCOMMITTED),无法避免脏读,不可重复读,幻读。
读已提交(READ COMMITTED),可以避免脏读,但不可避免不可重复读和幻读.
可重复读(REPEATABLE READ,MySQL InnoDB 存储引擎的默认隔离级别),可以避免脏读,不可重复读问题,无法避免幻读
串行化(SERIALIZABLE):可以避免脏读,不可重复读,幻读。
2.3 事务隔离等级的实现
由多版本并发控制(MVCC) 和 锁机制 共同实现。
read uncommited:没有使用MVCC快照,写加X锁,写完就释放(允许脏读)。
read commited:每次查询生成快照,写加X锁,事务结束后释放(整个事务期间其他人读不了)。
repeatable read:事务开始时生成唯一快照,整个事务都是用这个快照,写加X锁,事务结束后释放。
serializable:完全使用锁机制,读加S锁,写加X锁。
2.4 事务的例子
2.4.1 存储过程中使用事务
-- transfer_funds.sql
DELIMITER //CREATE PROCEDURE transfer_funds()
BEGIN-- 开始事务START TRANSACTION;-- 尝试从账户 1 扣除 100 元,并锁定该行UPDATE accounts SET balance = balance - 100 WHERE id = 1;-- 检查账户 1 的余额是否足够SELECT balance INTO @balance FROM accounts WHERE id = 1;IF @balance < 0 THEN-- 如果余额不足,回滚事务ROLLBACK;SELECT '余额不足,事务回滚';ELSE-- 如果余额足够,向账户 2 存入 100 元UPDATE accounts SET balance = balance + 100 WHERE id = 2;-- 提交事务COMMIT;SELECT '转账成功,事务提交';END IF;
END //DELIMITER ;-- 调用存储过程
CALL transfer_funds();
2.4.2 异常处理中回滚到保存点
新顾客下订单。操作1:先往Customers中插入新顾客信息,操作2:再往Orders中插入这个新顾客的订单信息,操作3:最后往OrderItems中插入该订单的详情。如果错误发生在操作1之后,操作2之前,则状态是安全的(允许不产生订单的顾客存在);如果错误发生在操作2之后操作3之前,则数据发生不一致(不允许有订单记录却没有订单详情)。
-- AddOrder.sql
DELIMITER //CREATE PROCEDURE AddOrder()
BEGINDECLARE EXIT HANDLER FOR SQLEXCEPTIONBEGINROLLBACK TO SAVEPOINT StartOrder;select 'Adding order failed! Rollback to save point!';COMMIT;END;START TRANSACTION;INSERT INTO Customers(cust_id, cust_name) VALUES(1000000010, 'Toys Emporium');SAVEPOINT StartOrder; -- 在安全状态设置检查点INSERT INTO Orders(order_num, order_date, cust_id) VALUES(20100, '2001-12-01 00:00:00', 1000000010);-- 出错语句INSERT INTO Orders(order_num, order_date) VALUES(20100, '2001-12-01 00:00:00', 1000000010);INSERT INTO OrderItems(order_num, order_item, prod_id, quantity, item_price) VALUES(20100, 1, 'BR01', 100, 5.49);INSERT INTO OrderItems(order_num, order_item, prod_id, quantity, item_price) VALUES(20100, 2, 'BR03', 100, 10.99);COMMIT;
END //DELIMITER ;-- 调用存储过程
CALL AddOrder();
3. ACID的实现
3.1 持久性
MySQL对用户DML操作的处理:操作的目标数据对象如果在buffer pool则直接按照DML进行操作;如果没有在buffer pool中,从磁盘调入目标数据对象到buffer pool,再对buffer pool按照DML进行操作。对buffer pool操作完毕后,内存的数据和磁盘的数据不一致,产生了脏页,这些脏页要按照一定频率或者指定实际刷新到磁盘中从而持久化用户的操作结果。
3.2 一致性
问题:如果从buffer pool刷新脏页到磁盘的操作失败,那么会导致数据不一致。
解决:引入redo log机制(在内存的redo log buffer,在磁盘中循环写入的两个redo log file),按照用户DML对buffer pool的目标数据改动后,随后立即记录改动结果到redo log buffer,然后事务提交后,redo log buffer内容将被追加到磁盘的redo log中。如果刷新buffer pool中的脏页到磁盘失败,则根据可以磁盘中的redo log内容重新刷新。——WAL(Write-ahead logging)。
为什么直接刷新脏页到磁盘效率低于刷新redo log buffer到redo log:前者涉及随机访问磁盘块(磁臂移动距离远),而后者是磁盘块的顺序访问(相邻的磁盘块,磁臂移动距离短)。
3.3 原子性
允许事务回滚,一旦事务执行失败,则回滚到事务开始前的状态,保证了原子性。回滚借助undo log机制实现。undo log 除了用于回滚,还用于MVCC判定哪个版本对事务可见。因此事务提交后,不会立即删除undo log,MVCC也可能用到undo log。undo log存放在回滚段中,有1024个回滚段。
3.4 隔离性
3. 4.1 前置概念
当前读
事务中查询的是数据对象最新版本。加S锁的sql:select … in share mode; 加X锁的sql:select … for update, insert, update。都采取当前读方式(操作总是基于数据对象最新的版本)。
快照读
事务中,简单的select查询就是快照读,即读取操作总是基于数据对象的某一个快照/版本。在read commited隔离等级中,事务中的每个select都生成一个快照,基于该快照再读,可以读到其他事务已经提交的后的数据版本。在repeatable read中,快照读基于开始事务之前的快照。serializable隔离等级中,快照读退化为当前读。快照读操作将生成一个read view。
3.4.2 MVCC
多版本并发控制。事务执行过程中保存数据的多个版本,避免冲突。MVCC依赖于快照读技术,3个隐式字段,undo log,readView。
记录的三个隐式字段
row_id:唯一标识行,无主键时自动生成。
trx_id:记录最新修改事务 ID,用于 MVCC 可见性判断。
roll_ptr:指向 undo 日志,支持事务回滚和版本回溯。
-- 查看隐式字段
SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'users';
undo log
多个并发事务场景下修改记录的过程:
- 事务2请求修改id=30的一行数据,先将该行数据记录追加到undo log,称其为条目1。条目1的DB_TRX_ID 字段值为2,DN_ROLL_PTR字段为null。
- 修改这行数据。这行数据的DB_TRX_ID字段值为2,DB_ROLL_PTR指向undo log中的条目1。
- 事务3请求修改id=30的一行数据,先将该行数据记录追加到undo log,称其为条目2。条目2的DB_TRX_ID字段值为3,DB_ROLL_PTR指向条目1。
- 修改这行数据。这行数据的DB_TRX_ID字段值为3,DB_ROLL_PTR指向undo log中的条目2。
并发事务先后修改同一行数据产生了多版本的undo log链,根据这个undo log链可以回溯到这些改动而回退到任意一次修改之前的状态。
read view
事务依照递增序依次创建。MVCC依据read view来确认访问版本链的规则。read view只存在于read commited,repeatable read这两个隔离等级。对于read commited,每次快照读(普通的select语句)都会创建一个read view,然后用read view从undo log链中返回某个版本的数据。对于repeatable read,进入事务第一次快照读时创建一个read view,事务期间快照读都基于这个read view。
read view由事务创建,它包含4个信息:m_ids(当前活跃事务ID的集合),min_trx_id(m_ids中值最小的ID),max_trx_id(分配给下一个将要创建的事务的ID,即max(m_ids) + 1),creator_trx_id(创建本read view的事务ID)。
使用read view 访问undo log版本链的规则
假设迭代某行数据的undo log链中版本时,迭代的数据版本的DB_TRX_ID为trx_id。
- trx_id == creator_trx_id,意味着创建当前版本的事务就是创建该read view的事务,事务可以访问自己创建的版本。
- trx_id < min_trx_id,意味着创建当前版本的事务已经提交。可以访问已提交的版本。
- trx_id > max_trx_id,意味着read view创建后才有事务trx_id产生这个版本。这个版本的数据对于创建read view的事务/当前事务不可见。
- min_trx_id <= trx_id <= max_id。如果trx_id 不在m_ids集合中(事务已经提交),则可以访问该行数据当前版本。如果在m_ids集合中(其他并发的事务最近修改了该行数据),则不可以访问该行数据当前版本。