一、事务处理思路
1.1 排队
排队处理是事务管理最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。
1.2 排它锁
引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。
1.3 读写锁
读和写操作:读读、写写、读写、写读。
读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务就可以同时被执行了。
读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。
1.4 乐观锁
通过版本进行判断,MVCC 就是其中一种实现方式
二、 MVCC
2.1 基础概念
MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。
MVCC最大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突是非常重要的,极大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持 MVCC 的原因,不过目前MVCC只在** Read Commited** 和 **Repeatable Read **两种隔离级别下工作。
2.2 Undo Log 多版本链
在前面我们介绍过,每条数据都有两个隐藏的字段,事务 Id (trx_id),回滚指针 (roll_pointer)
- trx_id : 记录最近一次更新这条数据的事务 Id
- roll_pointer : 指向之前生成的 undo log
生成流程
接着又有一个事务B (trx_id=58) 过来,对同一条数据进行修改,将值改为B,事务B的id是58,在更新之前会生成一个undo log来记录之前的值.然后会让roll_pointer指向这个实际的undo log回滚日志:
如果再有一个事务C (trx_id=69) 继续更新该条记录值为C,则会跟第二步的步骤一样
总结: 每一条数据都有多个版本,版本之间通过 undo log 链来进行连接
好处: 每个事务提交的时候一旦需要回滚操作,可以保证同一个事务只能读到比当前版本更早提交的值
2.3 Read View
核心解决问题: 需要判断一下版本链中的哪个版本是当前事务可见的
如上是一个 Read View 所存储的信息
- m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的 事务id 列表。
- min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是 m_ids 中的最 小值。
- creator_trx_id :表示生成该 ReadView 的事务的 事务id 。
- max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三 个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,mi n_trx_id的值就是1,max_trx_id的值就是4。
我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会 为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
核心流程
- 假设数据库有一行数据,很早就有事务操作过,事务id 是32. 此时两个事务并发过来执行了, 一个事务A (id=45),一个事务B (id=59). 事务A需要读取数据,而事务B需要更新数据,如下图:
如果不加任何限制,这里会出现脏读的情况,也就是一个事务可能会读到一个没有提交的值.
- 现在事务A直接开启一个ReadView,这个ReadView里的
m_ids
就包含了事务A和事务B的两个id,45和59,然后min_trx_id
就是45,max_trx_id
就是60.creator_trx_id
就是45,是事务A自己。
- 此时事务A第一次查询这行数据,首先会判断一下当前这行数据的
txr_id
是否小于ReadView中的min_trx_id
,此时发现txr_id=32,是小于ReadView里的min_trx_id
就是45的,说明你事务开启之前,修改这行数据的事务已经提交了,所以此时可以查到这行数据,如下图所示:
- 接下来事务B开始操作该条数据,他把这行数据的值修改为了值B,然后将这行数据的
txr_id
设置为自己的id,也就是59,同时roll_pointer
指向了修改之前生成的一个undo log,然后事务B提交,如下图所示
- 这时事务A再次执行了查询,但是却发现数据行里的txr_id=59
- max_trx_id(60)>trxid (59) > mintrxid(45): 说明当前事务的数据是可能在自己同一时刻开启的
- m_ids(45,59)包含了当前事务,说明就是同一时刻
所以当前记录无法查询
- 事务A根据roll_point顺着undo log版本链向下找,找到最近的一条undo log,trx_id是32。由于trx_id=32小于ReadView里的min_trx_id(45),说明这个undo log版本是在事务A开启之前就执行且提交的。因此,事务A可以查询最近的这个undo log里的值,这时undo log版本链的作用就体现出来了,它保存了一条快照链条,而事务A读取到的数据就是之前的快照数据。
Read View总结
- 通过Read View判断记录的某个版本是否可见的方式总结:
- trx_id = creator_trx_id
如果被访问版本的trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,该版本可以被当前事务访问. - trx_id < min_trx_id
如果被访问版本的trx_id,小于readview中的min_trx_id值,表明生成该版本的事务在当前事务生成readview前已经提交,该版本可以被当前事务访问. - trx_id >= max_trx_id
如果被访问版本的trx_id,大于或等于readview中的max_trx_id值,表明生成该版本的事务在当前事务生成readview后才开启,该版本不可以被当前事务访问. - trx_id > min_trx_id && trx_id < max_trx_id
如果被访问版本的trx_id,值在readview的min_trx_id和max_trx_id之间,就需要判断trx_id属性值是不是在m_ids列表中?- 在:说明创建readview时生成该版本的事务还是活跃的,该版本不可以被访问
- 不在:说明创建readview时生成该版本的事务已经被提交,该版本可以被访问
- trx_id = creator_trx_id
- 生成readview时机
- RC隔离级别:每次读取数据前,都生成一个readview.
- RR隔离级别:在第一次读取数据前,生成一个readview,之后read view不再更新.
2.4 MVCC 在 MySQL 中的具体实现
2.4.1 简介
MySQL中实现MVCC(多版本并发控制)的机制主要基于undo log多版本链和ReadView机制。下面是MySQL中MVCC的实现方式的详细说明:
2.4.2 数据表隐藏字段
MySQL为了实现MVCC,在每个表中添加了一些隐藏字段:
- DATA_TRX_ID(6字节): 记录最新更新该行记录的事务ID,每个事务处理时会自动设置为当前事务ID。但值得注意的是,DATA_TRX_ID只有在事务提交之后才会更新。
- DATA_ROLL_PTR(7字节): 一个rollback指针,指向当前行数据的上一个版本。通过这个指针将数据的多个版本连接在一起构成一个undo log版本链。
- DB_ROW_ID(6字节): 隐含的自增ID,用来唯一标识每一行的字段。
- DELETE BIT位: 标识当前记录是否被删除,实际上并不是真正的删除数据,而是一个标志。真正的删除操作是在事务提交的时候执行。
2.4.3 场景示例
查询 SELECT
- 在查询时,InnoDB只查找版本早于当前事务版本的数据行,即数据行的版本必须小于等于当前事务的版本。这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行。
- 对于被删除的行,其删除操作的版本一定是未定义的或者大于当前事务的版本号,这意味着在当前事务开始之前,行没有被删除。只有符合以上两点,才会返回查询结果。
删除 DELETE
- 删除操作会修改DATA_TRX_ID的值为当前执行删除操作的事务ID,并将DELETE BIT设置为True,表示被删除。
增加 INSERT
- 新记录的DATA_TRX_ID会被设置为当前事务ID。
修改 UPDATE
- 修改操作会使用排它锁锁定该行,以保证写操作的一致性。
- 同时,会记录redo log,将更新之后的数据记录到redo log中,以便日后使用。
- 也会记录undo log,将更新之前的数据记录到undo log中,以便回滚操作。
2.4.4 总结
通过undo log多版本链和ReadView机制的结合,MySQL实现了MVCC机制,确保了数据库在并发操作下的一致性和隔离性。
三、MVCC 读操作分类
在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read
)与当前读 (Current Read
)。
- 快照读
快照读是指读取数据时不是读取最新版本的数据,而是基于历史版本读取的一个快照信息(mysql读取undo log历史版本) ,快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题- 解决了因加锁导致的修改数据时无法对数据读取问题.
- 解决了因加锁导致读取数据时无法对数据进行修改的问题.
- 当前读
当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的Update delete insert select ....lock in share mode
select for update
为当前读
MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,可以采用下面两种方案:
- 乐观锁
- 悲观锁