

简介
在MySql中,我们也要处理三种类型的并发关系:
MySql是如何解决并发问题的
解决并发问题,当然锁最靠谱,所以MySql也提了共享锁、排它锁等。但是一个并发性能良好的系统一旦加锁,不可避免的造成访问的串行化,影响并发性能。所以MySql提供了一种乐观锁的实现:MVCC(多版本并发控制),来解决读-写并发不加锁。
InnoDB引擎通过MVCC,解决了脏读、不可重复读,通过MVCC + Next-Key Lock(临键锁)来解决幻读,实现了事务的隔离级别Repeatable Read(可重复读)。
但是可重复读隔离级别下,写-写并发问题只能靠开发者显示或隐士使用锁来自行解决。
InnoDB引擎MVCC机制下的当前读和快照读
快照读
快照读会生成一个ReadView(读视图),InnoDB引擎在默认隔离级别可重复读情况下,第一次普通的select生成ReadView,在事务提交之前一直不变。

ReadView中记录了当前系统几个事务ID:


如果当前数据版本的事务ID 大于等于m_low_limit_id,表明生成该数据版本的事务在当前事务生成ReadView后才开启的,所以该版本不可以被当前事务访问。
为当前活跃事务中的最小事务ID,如果当前无活跃事务,为m_low_limit_id值。

如果当前数据版本的事务ID 小于m_up_limit_id,表明生成该数据版本的事务在当前事务生成ReadView前就已经提交了,所以该版本可以被当前事务访问。
为当前生成ReadView的事务ID,表明生成该数据版本为当前事务,可以访问它自己修改的数据,所以该版本可以被当前自己的事务访问。
m_ids
为当前生成ReadView的事务时,当前活跃的读写事务ID列表。如果当前数据版本的事务ID在m_ids中,表明生成ReadView的事务时,此版本的数据还没被事务提交,当前事务不可访问。如果当前数据版本的事务ID不在m_ids中,表明生成ReadView的事务时,此版本的数据是被提交,当前事务可以访问。
判断可见性的源码:1、id < m_up_limit_id ||id == m_creator_trx_id当前版本数据可见;
当前版本数据是在ReadView生成之提交的或当前ReadView事务修改的--可见。
2、id >= m_low_limit_id当前版本数据不可见;
当前版本数据是在ReadView生成之后的事务的--不可见。3、m_ids.empty() ||
(!std::binary_search(p, p + m_ids.size(), id))
当前版本数据可见;
如果当前版本数据在活跃事务列表中,在ReadView生成时,当前版本数据所在的事务还没提交,不可见。如果当前版本数据不在活跃事务列表中,在ReadView生成时,当前版本的数据已经被事务提交,可见。
举例说明:

事务1~6的执行时间顺序按序号依次开始,当事务2执行普通select查询,生成快照读视图,在事务2结束之前不变(InnoDB引擎在默认隔离级别可重复读情况)。
此时,m_low_limit_id = 6,m_up_limit_id = 2,m_creator_trx_id = 2,
m_ids = 【2,3,4】
根据以上可见性判断:事务1、事务2、事务5版本的数据在事务2快照读视图生成后是可见的。

MySql写-写并发问题避坑:更新丢失问题
虽然InnoDB通过MVCC + Next-Key Lock(临键锁)来解决幻读,实现了事务的隔离级别Repeatable Read(可重复读),但是写-写 并发问题依然靠开发者使用锁来自行解决。
避坑:更新失败示例:
当前表中数据:

以当前事务顺序执行age+1更新操作:

两个事务执行完毕,结果:

最终结果为右边的事务更新成功,左边的事务更新失败,写-写并发导致其中一个失败。
因为左边的事务在时刻3生成了读视图,右边的事务所更新的数据对左边的事务是不可见的,此时左边的事务读取的数据是历史版本,如果左边的事务以当前查询的年龄为基准+1,即2去更新,update操作失败了。
如果age+1 操作不以当前事务读取的值为准,而是用update排它锁:
update table_renzhi
set age = age + 1
where id = 1;则可以避免。
避坑:丢失更新示例:
当前表数据:

两个事务随机更新年龄。

两个事务执行完毕,结果:

左边的事务更新覆盖了右边事务的更新。这种情况下丢失更新很难发现,两个事务都执行成功了,也不报错。
为了解决写-写并发问题,我们只能通过悲观锁或乐观锁来解决。
1、乐观锁解决写-写并发
我们可以通过增加版本号机制,利用CAS的思想来对比版本号更新。
如果更新失败报错回滚,或者自旋,新开启事务重新查询当前最新版本号,再重复利用CAS的思想来对比版本号更新。
自旋记得要开启新事务,因为MVCC读视图一旦成功,同一个事务就不会变,读取的数据永远是一样的。
2、悲观锁解决写-写并发
使用select for update悲观锁,其他事务操作阻塞。
示例:

小结
InnoDB引擎在默认隔离级别可重复读情况下通过MVCC,解决了脏读、不可重复读,通过MVCC + Next-Key Lock(临键锁)来解决幻读,实现了事务的隔离级别Repeatable Read(可重复读)。
但是可重复读隔离级别下,写-写并发问题只能靠开发者使用锁来自行解决。为了避免写-写并发导致更新失败或丢失更新问题,可以使用乐观锁或悲观锁来解决。
select lock in share mode 使用共享锁;
select for update; update; insert; delete 使用排他锁;
这些操作都是一种当前读。
普通的select读就是快照读,非阻塞。InnoDB引擎在默认隔离级别可重复读情况下,第一次普通的select生成ReadView,在事务提交之前一直不变。