009 mysql事务分析

一、InnoDB的事务分析

        数据库事务具有ACID四大特性;

  • 原子性(atomicity) :事务最小工作单元,要么全成功,要么全失败 。
  • 一致性(consistency): 事务开始和结束后,数据库的完整性不会被破坏 。
  • 隔离性(isolation) :不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)。
  • 持久性(durability) :事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失 。

        事务的隔离性由多版本控制机制和锁实现,而原子性、一致性和持久性通过InnoDB的redo log、undo log和ForceLog at Commit机制来实现;

1.原子性,持久性和一致性

        原子性,持久性和一致性主要是通过redo log、undo log和Force Log at Commit机制机制实现。redo log用于在崩溃时恢复数据;undo log用于对事务的影响进行撤销,也可以用于多版本控制;而Force Log at Commit机制保证事务提交后redo log日志都已经持久化。

1.1 RedoLog数据库日志和数据落盘机制

        redo log写入磁盘时,必须进行一次操作系统的fsync操作,防止redo log只是写入了操作系统的磁盘缓存中。参数innodb_flush_log_at_trx_commit可以控制redo log日志刷新到磁盘的策略;

1.2 UndoLog

        在事务执行的过程中,除了记录redo log,还会记录一定量的undo log,undo log记录数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作;

        数据库崩溃重启后需要从redo log中把未落盘的脏页数据恢复出来,重新写入磁盘,保证用户的数据不丢失,并且,在崩溃恢复中还需要回滚没有提交的事务,由于回滚操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo恢复数据,然后做undo回滚。

如上图所示,表空间中有回滚段(新增、修改、删除会产生,查询不产生)和叶节点段和非叶节点段,而三者都有对应的页结构。

回滚段

        通过Roll Pointer找到row_id(历史版本),完成回滚;

记录(ROW)

  • Trx id 事务id;

  • Roll Pointer 回滚指针,指向上一条记录结果结果的id;

        undo log的存储不同于redo log,它存放在数据库内部的一个特殊的段(segment)中,这个段称为回滚段。回滚段位于共享表空间中(ibdata1),undo段中的以undo page为更小的组织单位,undo page和存储数据库数据和索引的页类似,因为redo log是物理日志,记录的是数据库页的物理修改操作,所以undo log(也看成数据库数据)的写入也会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护

        事务进行过程中,每次sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页,然后redo log按照时间或者空间等条件进行落盘,undo log和脏页按照checkpoint进行落盘,落盘后相应的redo log就可以删除了。此时,事务还未COMMIT,如果发生崩溃,则首先检查checkpoint记录,使用相应的redo log进行数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,就使用undo log进行事务回滚;事务执行COMMIT操作时,会将本事务相关的所有redo log都进行落盘,只有所有redo log落盘成功,才算COMMIT成功;然后内存中的数据脏页继续按照checkpoint进行落盘,如果此时发生了崩溃,则只使用redo log恢复数据。

2.隔离性

2.1 事务并发问题

在事务的并发操作中可能会出现一些问题:

  • 丢失更新:两个事务针对同一数据都发生修改操作时,会存在丢失更新问题。

  • 脏读:一个事务读取到另一个事务未提交的数据。

  • 不可重复读(允许发生):一个事务因读取到另一个事务已提交的update或者delete数据。导致对同一条记录读取两次以上的结果不一致。

  • 幻读:一个事务因读取到另一个事务已提交的insert数据。导致对同一张表读取两次以上的结果不 一致。

2.2 事务隔离级别(SQL92标准)

MySQL数据库提供的四种隔离级别(由低到高):

  • ① Read uncommitted (读未提交):最低级别,任何情况都无法保证。

  • ② Read committed (RC,读已提交):可避免脏读的发生。

  • ③ Repeatable read (RR,可重复读):可避免脏读、不可重复读的发生。(注意事项:InnoDB的RR还可以解决幻读,主要原因是Next-Key锁,只有RR才能使用Next-Key锁)

  • ④ Serializable (串行化):可避免脏读、不可重复读、幻读的发生。 (由MVCC降级为Locking-Base CC(锁方式),性能差,有死锁问题)

示例:

        管理者要查询所有用户的存款总额,假设除了用户A和用户B之外,其他用户的存款总额都为0,A、B用户各有存款1000,所以所有用户的存款总额为2000,但是在查询过程中,用户A会向用户B进行转账操作,转账操作和查询总额操作的时序图如下图所示。

        如果没有任何并发控制机制,查询总额事务先读取了用户A账户存款,然后转账事务改变了用户A和用户B的账户存款,最后查询总额事务继续读取了转账后用户B的账号存款,导致最终统计的存款总额多了100元,发生错误。

--创建账户表并初始化数据
create table tacount(id int , aname varchar(100),acount int , primary key(id));
alter table tacount add index idx_name(aname);
insert into tacount values(1,'a',1000);
insert into tacount values(2,'b',1000);
--设置隔离级读未提交,只在当前连接生效(read-uncommitted)
set session transaction isolation level read uncommitted;
--session 1
start transaction ; select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
--session 2
start transaction; update tacount set acount=1100 where aname='b';
--session 1
select * from tacount where aname='b';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 2 | b | 1100 |
+----+-------+--------+

使用锁机制(LBCC)解决(存在死锁)

        查询总额事务对读取的行加锁,等到操作结束后再释放所有行上的锁,因为用户A的存款被锁,导致转账操作被阻塞,直到查询总额事务提交并将所有锁都释放才可执行。

        但是可能会引入新的问题,当转账操作是从用户B向用户A进行转账时会导致死锁,转账事务会先锁住用户B的数据,等待用户A数据上的锁,但是查询总额的事务却先锁住了用户A数据,等待用户B数据上的锁。

--设置隔离级别为串行化(serializable) 死锁演示
--此级别会使所有sql都加锁
 set session transaction isolation level serializable;
--session 1
 start transaction;select * from tacount where aname='a';
--session 2
 start transaction ; update tacount set acount=900 where aname='b';
-- session 1
 select * from tacount where aname='b';
-- session 2
 update tacount set acount=1100 where aname='a';
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting
transaction

应用MVCC机制解决

        查询总额事务先读取了用户A账户存款,然后转账事务会修改用户A和用户B账户存款,查询总额事务读取用户B存款时不会读取转账事务修改后的数据,而是读取本事务开始时的数据副本(在REPEATABLE READ隔离等级下)。

        MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力;借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)。

-- 显示当前隔离级别为 REPEATABLE-READ MySQL默认隔离级别
 select @@tx_isolation;
-- session 1
 start transaction ; select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2
 start transaction; update tacount set acount=1100 where aname='a';
-- session 1
 select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2 提交事务
 commit;
-- session 1 显示在session 1 事务开始时的数据
 select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- 设置事务隔离级别为REPEATABLE-COMMITTED 读已提交
-- session 1
 set session transaction isolation level read committed;
 start transaction ; select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2
 set session transaction isolation level read committed;
 start transaction; update tacount set acount=1100 where aname='a';
-- session 1
 select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2 提交事务
 commit;
-- session 1 显示最新事务提交后的数据
 select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1100 |
+----+-------+--------+

2.3 InnoDB的MVCC实现

        Multiversion concurrency control (MCC or MVCC), is a concurrency control methodcommonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory.

        多版本并发控制(MCC或MVCC)是数据库管理系统常用的一种并发控制方法,用于提供对并发访问数据库和在编程语言中实现事务性内存.

2.3.1 当前读和快照读

        在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。

  • 快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。

    简单的select操作,属于快照读,不加读锁;(有例外)

  • 当前读,读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录;

    特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁;(加行写锁读当前版本)

2.3.2 一致性非锁定读(MVCC核心)

        一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制(MVCC)读取当前数据库中行数据的方式;如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放,相反地,InnoDB会去读取行的一个最新可见快照(历史版本)。

 

        如上图所示,当会话B提交事务后,会话A再次运行SELECT * FROM test WHERE id = 1 的SQL语句时,两个事务隔离级别下得到的结果就不一样了; MVCC 在mysql 中的实现依赖的是 undo log 与 read view ;

2.3.3 Undo Log

        InnoDB行记录有三个隐藏字段:分别对应该行的rowid、事务号db_trx_id和回滚指针db_roll_ptr,其中db_trx_id表示最近修改的事务的id,db_roll_ptr指向回滚段中的undo log;根据行为的不同,undo log分为insert undo log和update undo log; insert undo log ​ 在 insert 操作中产生的 undo log;因为 insert 操作的记录只对事务本身可见,rollback 在该事务中直接删除 ,不需要进行 purge 操作(purge线程,mysql的维护线程)。

        update undo log update 或 delete 操作中产生的 undo log,因为会对已经存在的记录产生影响,rollback时应用MVCC机制会找他的历史版本进行恢复;为了提供 MVCC机制,因此 update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作。

        当事务2使用UPDATE语句修改该行数据时,会首先使用排他锁锁定改行,将该行当前的值复制到undolog中,然后再真正地修改当前行的值,最后填写事务ID,使用回滚指针指向undo log中修改前的行。

        当事务3进行修改与事务2的处理过程类似;

2.3.4 事务链表

        MySQL中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中,这是一个基本的链表结构: ​ ct-trx --> trx11 --> trx9 --> trx6 --> trx5 --> trx3; 事务链表中保存的都是还未提交的事务,事务一旦被提交,则会被从事务链表中摘除。 ​         RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(readview); ​ RC隔离级别下,在每个语句开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read1view)

--查看事务列表
show engine innodb status;

2.3.5 ReadView

        当前事务(读)能读哪个历史版本? Read View是事务开启时当前所有事务的一个集合,这个类中存储了当前Read View中最大事务ID及最小事务ID。 这就是当前活跃的事务列表。如下所示,

 ct-trx --> trx11 --> trx9 --> trx6 --> trx5 --> trx3;

ct-trx 表示当前事务的id,对应上面的read_view数据结构如下,

read_view->creator_trx_id = ct-trx;
read_view->up_limit_id = trx3; --低水位
read_view->low_limit_id = trx11; --高水位
read_view->trx_ids = [trx11, trx9, trx6, trx5, trx3];

        low_limit_id是“高水位”,即当时活跃事务的最大id,如果读到row的db_trx_id>=low_limit_id,说明这些id在此之前的数据都没有提交,如注释中的描述,这些数据都不可见。

if (trx_id >= view->low_limit_id) {
   return(FALSE);
}
/*注:readview 部分源码

        up_limit_id是“低水位”,即当时活跃事务列表的最小事务id,如果row的db_trx_id<up_limit_id,说明这些数据在事务创建的id时都已经提交,如注释中的描述,这些数据均可见。

if (trx_id < view->up_limit_id) {
  return(TRUE);
}

        row的db_trx_id在low_limit_id和up_limit_id之间,则查找该记录的db_trx_id是否在自己事务的read_view->trx_ids列表中,如果在则该记录的当前版本不可见,否则该记录的当前版本可见。

不同隔离级别ReadView实现方式:

read-commited

RC级别下能读到最新的数据,每次执行语句都创建新read_view;

//函数:ha_innobase::external_lock
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED && trx->global_read_view) {
/* At low transaction isolation levels we leteach consistent read set its own snapshot */
  read_view_close_for_mysql(trx);
}

即:在每次语句执行的过程中,都关闭read_view, 重新在row_search_for_mysql函数中创建当前的一份read_view。这样就会产生不可重复读现象发生。

RR级别下不能读到最新,只能读到事务开始时的版本,一次事务中共用一个read_view;

        在repeatable read的隔离级别下,创建事务trx结构的时候,就生成了当前的global read view(全局)。使用trx_assign_read_view函数创建,一直维持到事务结束。在事务结束这段时间内每一次查询都不会重新重建Read View , 从而实现了可重复读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值