基于innodb引擎
1、MySQL并发控制
并发事务的类型
并发事务即多个事务同时执行,而在事务间执行操作的方面可以分为三种:
读、读:不存在任何问题,也不需要并发控制。
读、写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,不可重复读,幻读。
写、写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失(回滚丢失,A事务撤销时,把已经提交的B事务的更新数据覆盖了),第二类更新丢失(更新覆盖,A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失)。
写-写并发问题靠开发者使用锁来自行解决
读-写中产生的问题
**脏读:**指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,这些数据可能最终不会存到数据库中,也就是读到了不存在的数据。
不可重复读:事务A在执行读取操作,由于事务A执行的时间较长,在事务A第一次读取数据后,事务B执行了更新操作并进行了提交,事务A第二次读该数据时,数据与第一次读的内容不一致。
针对数据更新(UPDATE)操作。
幻读:事务A在执行读取操作,需要两次统计数据的总量,第一次查询数据总量后,事务B执行了新增数据的操作并提交后,这事务A第二次统计数据总量后,就像产生了幻觉一样,多了几条数据,称为幻读。
针对数据插入(INSERT)操作来说的。
隔离级别
本次分享主要介绍RR隔离级别下快照读、当前是如何解决脏读、不可重复读、幻读的。
RR读数据的方式
当前读
select...lock in share mode (共享读锁)
select...for update
update , delete , insert
采用的是当前读,当前读就是读取最新的数据,为了保证读取的是最新且准确的数据,所以它在读取的时候会加锁,防止其它事物操作。
快照读
单纯的select操作
采用的是快照读,快照读是不加锁的方式,当一个事物要操作数据库的时候,会在这个事物的基础上形成一个快照,后续对数据的读取都是基于这个快照。
**快照读和当前读如何实现的?**MVCC、加锁机制
2、MVCC
MVCC全称 Multi-Version Concurrency Control ,即多版本并发控制,是InnoDB引擎中用来解决读-写冲突的无锁并发控制方式。
MVCC通过快照读获取历史版本的数据,避免脏读、不可重复读、幻读,从而提高并发性能。
实现原理
MVCC 模型是由隐藏字段、undo_log日志、Read View 实现的。
(1)隐藏字段
一张表,里面有两个字段,name、age,但实际上我们表里的数据是这样的
-
DB_ROW_ID 隐藏主键id:6byte,隐含的自增ID(隐藏主键)。
-
DB_TRX_ID 事物id:记录这条记录最后一次操作的事物id
-
DB_ROLL_PTR 回滚指针:回滚指针,指向这条记录的上一个版本,用于配合下面的 undo log。
(2)undo_log
- 事务未提交的时候,数据修改前的旧版本会存到undo日志里。以便事务回滚时,恢复旧版本数据,撤销未提交事务数据对数据库的影响。
- undo日志是逻辑日志。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。
- 多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(DB_ROLL_PTR)连成一条Undo日志链。
事务1插入数据
事务2修改name
事务3修改age
(3)read view
Read View就是事务执行快照读时,产生的读视图。事务执行快照读时,会记录当前系统中还有哪些活跃的读写事务,把它们放到一个列表里。Read View主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据.
查询出来的每一行记录,都会用readview来判断一下当前这行是否可以被当前事务看到,如果可以,则输出,否则就利用undolog来构建历史版本,再进行判断,直到可见性条件满足。
read view中的主要字段:
- m_ids:记录当前系统中那些活跃的读写事务ID。
- min_trx:m_ids事务列表中,最小的事务ID
- max_trx:m_ids事务列表中,最大的事务ID
- creator_trx_id:当前事务的id
事务A访问某版本数据X
如果X. DB_TRX_ID< min_trx,表明生成X数据的事务在生成ReadView前已经提交(因为事务ID是递增的),所以当前版本的数据X可以被事务A访问。
如果X. DB_TRX_ID > max_trx,表明生成X数据的事务在生成ReadView后才生成,所以当前版本的数据X不可以被事务A访问。
如果 min_trx =<X. DB_TRX_ID<= max_trx,需要判断m_ids.contains(X. DB_TRX_ID),如果存在,则代表Read View生成时,生成当前版本的数据X的事务还在活跃,还没有Commit,对数据X的修改,事务A也是看不见的;如果不在,则说明,生成当前版本的数据X的事务在Read View生成之前就已经Commit了,对数据X的修改,事务A是能看见的。
Read Repeatable隔离级别:同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
示例
现在数据有一条数据,如下原始值,上一个已经提交事务的事务txr_id=30
此时两个事务并发执行, 事务A(txr_id= 40),事务B(txr_id=50),事务A要读取这行数据,事务B要更新这行数据。
此时事务A在读取时开启ReadView,此时的ReadView的值如下:
m_ids:=[40,50]
min_trx_id=40
max_trx_id=50
creator_trx_id=40
事务A在第一次查询发起一个判断,判断当前数据的txr_id是否小于ReadView中的min_trx_id(此时txr_id (30)< min_trx_id(40)),说明在A事务开启前,修改这行数据的事务已经提交了,所以可以查询这条数据
接着事务B开始修改数据为B值,然后将这行数据的txr_id设置为自己的txr_id(50),然后将roll_pointer指向之修改前生成的undo log备份,然后提交事务
此时A再重复查询,当读取B修改的数据时,发现在ReadView中min_txr_id(40)<txr_id(50)<max_trx_id(51),同时txr_id=50在m_ids,可以确定修改数据的事务和自己处于同一时间并发提交的,为了保证可重复读,这行数据是不能查询的。所以需要通过roll_pointer顺着undo log链表找到最近的一条undo log,即修改前的值,找到txr_id = 30(B未修改的值) 发现此时原始值的 txr_id < min_trx_id,说明这条数据肯定是在事务A开启钱前提交的,所以可以查询,就查询txr_id=30的值,即查询的的还是原始的值,这样就保证了可重复读。
如果C事务插入一条数据也是如此,A是读取不到的,从而也解决了幻读问题
总结
快照读通过mvcc的原理,开启事务后第一次select获取快照,后续每次select都是基于这个快照,从而避免了脏读、不可重复读、幻读。
3、快照读避免了幻读吗?
快照读中的幻读
事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来。
然后事务 B 插入一条 id = 5 的记录,并且提交了事务。
此时,事务 A 更新 id = 5 这条记录,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了,幻读就是发生在这种违和的场景。
在可重复读的隔离级别下,事务A第一次执行普通select语句生成了一条readview,之后事务B向表中插入了一条id=5的记录并提交。接着,事务A对id=5的这条记录进行了更新操作,在这个时刻这条记录的trx_id隐藏列的值就变成了事务A 的事务id,之后事务A再使用普通select语句去查询这条记录就能看到这条记录,就发生了幻读。
因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。
总结
RR隔离级别下的快照读,规避了常规情况下的幻读,但并没有完全规避幻读