总结下:简单的事务操作,例如快照读,通过系统提供的mvcc机制——创建版本号、删除版
本号,避免幻读。而复杂的事务操作,例如当前度,则需要通过加锁进行控制。
什么是幻读
事务不是独立执行造成的一种现象。一个事务对数据表进行了全面的修改,而后另一个事务增加了一行。这时候第一个事务中的另一个操作,发现表中有一个数据没有更新,以为产生了幻觉,这就是幻读。
共享锁和排它锁
贡献锁
共享用于不更改或不更新数据的操作,如select语句。如果不同的事务都是读操作,可以共享获取这把锁。
排它锁
用于数据修改操作,例如INSERT、UPDATE或DELETE。确保不会同时对同一资源进行多重更新。
当前读和快照读的区别
当前读
每次读取的都是当前最新的数据,但是读的时候不允许写,写的时候不允许读。基于共享锁+排它锁+Next-Key Lock实现。每次读取行时添加共享锁,读取范围时添加范围共享锁。每次修改行时添加排它锁,而范围行进行修改时,添加范围排它锁。保障每个事务读取的内容最新。
快照读
读写不冲突,每次读取的都是快照数据。这是基于MVCC+undolog实现的,可以保证读写不冲突,需要读取老版本的数据是,可以使用undolog中保存的内容。在Repeatable read(默认)下,有可能读取的不是最新的数据。但是在Read Committed隔离级别下,快照读和当前读读取的数据都一样,都是最新的。
mysql如何实现避免幻读
- 在快照读的情况下,使用mysql使用mvcc来避免幻读
- 在当前读的情况下,使用next-key来避免幻读
什么是MVCC,全称是多版本并发控制。mysql把每个操作定义为一个事务,每开启一个事务,系统的事务版本号自动递增。每行记录都有两个隐藏列:创建版本号和删除版本号。
- select事务:事务每次只能读到创建版本号小于等于此次系统版本号的记录,同时删除版本号不存在或者大于系统版本号。
- update事务:将当前系统版本号作为行记录的版本号,并将当前系统版本号作为原来行记录的删除版本号。
- delete:将系统版本号作为行记录的删除版本号
- insert:把当前系统版本号作为行记录的版本号。
什么是next-key锁(record locks+gap locks:行锁+间隙锁)
什么是快照读和当前度:
- 快照读:简单的select操作,属于快照读,不加锁。
- select * from table where ?;
- 当前读:特殊的读操作,插入/更新/删除操作,属于当前度,需要加锁。
- select * from table where ? lock in share mode;
- select * from table where ? for update;
- insert into table values (…);
- update table set ? where ?;
- delete from table where ?;
事务的隔离级别概念
存在4中不同的事务隔离级别,不同的隔离级别下会产生脏读、幻读、不可重复读等相关问题。因此在选择事务的隔离级别的时候要根据应用场景来决定,使用合适的隔离界别。
在InnoDB存储引擎中提供了SQL标准
Read-uncommited 读未提交
存在脏读、幻读的问题。
支持行锁+表锁。
幻读:事务A执行期间,其他事物有写入操作,因为本级别没有快照,更没有MVCC机制,其他事务修改数据后即便不做提交,事务A使用select也会因为直接为当前读的模式,前后得到不同的结果导致幻读。
同时,其他事务最终可能会执行回滚
操作,导致了事务A的脏读。
Read-commited 读已提交
存在幻读,
支持行锁 + 表锁。
在事务A执行期间,其他事务做一次数据插入并提交事务,事务A就会select出不同的结果导致幻读。在这种隔离界别下,快照读和当前读读取的内容是一样的。因为select每次都会去读取最新的快照数据。
而脏读问题被快照(ReadView)机制解决。
Repeatable-read 可重复读
可重复读存在幻读,但也可以避免幻读。
支持:行锁+间隙锁(gap)+ next-key lock(行锁+间隙锁)+ 表锁。
由于本级别支持的netx-key lock机制,才有了解决幻读的可能。行锁锁定等值记录,阻塞其他事务的即使读操作,间隙锁根据不同的查询条件,锁住与行相关的一个索引区间,阻塞其他事务对此索引区间做即使读操作(比如最直接引发幻读的insert操作)。
RR下幻读的场景模拟
在事务A中,先使用快照读,后使用当前读,导致事务A没有next-key lock锁定索引区间,其他事务对事务A所覆盖的查询范围中,元数据做了增删改,而后事务A又切换为当前读而非继续使用快照读,导致事务内前后查询到的数据集合不一致,出现了幻读。
逻辑如下所示:
-- 事务A
begin;
-- step1 快照读 不加锁 空结果集
select * from users where id >= 1;
-- step4 快照读 不会导致幻读 空结果集
select * from users where id >= 1;
-- step5 即时读 查询出事务B插入的数据 发生幻读
select * from users where id >= 1 for update;
-- step6
commit;
-- 事务B
begin;
-- step2 直接插入成功
insert into users value(1, "sqrt", 18, 1), (2, "cat", 19, 1);
-- step3
commit;
RR下杜绝幻读的方法:
不使用快照读,始终使用当前读,触发next-key lock锁机制,阻塞其他事务对当前事务查询条件所能覆盖的索引区间的数据当前读
操作。(其实就相当于SERIALIZE级别了,该级别下没有快照读,会默认地给select也追加上lock in share mode的读锁)。
-- 事务A
begin;
-- step1 加上了间隙锁 [1, +无穷)
select * from users where id >= 1 for update;
-- step3 快照读/即时读 都不会出现幻读
select * from users where id >= 1;
select * from users where id >= 1 for update;
-- step4
commit;
-- 事务B
begin;
-- step2 会因为事务A step1 的间隙锁被阻塞
insert into users value(1, "sqrt", 18, 1), (2, "cat", 19, 1);
-- step5 事务A提交后 锁释放 才会执行插入
commit;
Serializable 串行化
序列化模式,解决了幻读的问题。
该模式下不存在快照读一说,因为所有的操作都做了锁机制,对某数据的访存操作转为了串行化。该模式下对select也隐式添加了lock in share mode的读锁。
支持行锁+间隙锁+next-key lock+表锁。
加锁实例
1、对主键的等值查询,数据不存在,会对相邻数据的主键区间加间隙锁。
若原表中的数据为[1, 2, 3, 5, 20],在查询主键为10的事务操作中,区间[5,20]会被锁定。
2、对主键等值查询,且数据存在,则为行锁,不影响其他事务对其他数据行的操作。
3、 若对主键范围查询,锁定范围区间。
总结
1、RR级别下的确存在幻读的可能
2、RR级别下通过合理的加锁,来启用next-key lock,即便是当前读,也可以防止幻读的
3、Serialize级别则是自动对select做lock in share mode的加锁模式,防止其他事务的当前读来修改数据,杜绝幻读。