首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >MySql避坑指南:可重复读隔离级别下,并发情况下更新丢失问题避坑

MySql避坑指南:可重复读隔离级别下,并发情况下更新丢失问题避坑

作者头像
崔认知
发布2023-06-20 11:17:16
发布2023-06-20 11:17:16
4.2K2
举报
文章被收录于专栏:nobodynobody

简介


在MySql中,我们也要处理三种类型的并发关系:

  • 读-读 并发; 这种情况下,不存在更新数据问题,只有读取数据,不存在并发问题
  • 读-写 并发: 有线程安全问题,可能导致脏读、不可重复读、幻读数据一致性读问题,必须由数据库提供的事务隔离机制来解决。
  • 写-写 并发: 有线程安全问题,可能存在更新丢失问题,比如第一类更新丢失问题(A事务撤销时,把已经提交的B事务的更新数据覆盖了)第二类更新丢失问题(A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失)。

MySql是如何解决并发问题的


解决并发问题,当然锁最靠谱,所以MySql也提了共享锁、排它锁等。但是一个并发性能良好的系统一旦加锁,不可避免的造成访问的串行化,影响并发性能。所以MySql提供了一种乐观锁的实现:MVCC(多版本并发控制),来解决读-写并发不加锁

InnoDB引擎通过MVCC,解决了脏读、不可重复读,通过MVCC + Next-Key Lock(临键锁)来解决幻读,实现了事务的隔离级别Repeatable Read(可重复读)。

但是可重复读隔离级别下,写-写并发问题只能靠开发者显示或隐士使用锁来自行解决

InnoDB引擎MVCC机制下的当前读和快照读


  • 当前读 当前读,则读取当前最新的数据,并且通过数据库锁机制来保证其它并发事务不能修改当前数据。 select lock in share mode 使用共享锁; select for update; update; insert; delete 使用排他锁; 这些操作都是一种当前读。
  • 快照读 即不加锁的非阻塞读。 普通的select读就是快照读快照读的实现是基于多版本并发控制,即 MVCC ,既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

快照读


快照读会生成一个ReadView(读视图),InnoDB引擎在默认隔离级别可重复读情况下,第一次普通的select生成ReadView,在事务提交之前一直不变

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

  • m_low_limit_id 为当前系统要分配的最新的事务ID,事务ID是递增分配的,m_low_limit_id即为当前系统已分配的事务ID + 1

如果当前数据版本的事务ID 大于等于m_low_limit_id,表明生成该数据版本的事务在当前事务生成ReadView后才开启的,所以该版本不可以被当前事务访问

  • m_up_limit_id

当前活跃事务中的最小事务ID,如果当前无活跃事务,为m_low_limit_id值。

如果当前数据版本的事务ID 小于m_up_limit_id,表明生成该数据版本的事务在当前事务生成ReadView前就已经提交了,所以该版本可以被当前事务访问。

  • m_creator_trx_id

为当前生成ReadView的事务ID,表明生成该数据版本为当前事务,可以访问它自己修改的数据,所以该版本可以被当前自己的事务访问。

代码语言:javascript
复制
m_ids
为当前生成ReadView的事务时,当前活跃的读写事务ID列表。如果当前数据版本的事务ID在m_ids中,表明生成ReadView的事务时,此版本的数据还没被事务提交,当前事务不可访问。如果当前数据版本的事务ID不在m_ids中,表明生成ReadView的事务时,此版本的数据是被提交,当前事务可以访问。
判断可见性的源码:
代码语言:javascript
复制
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排它锁:

代码语言:javascript
复制
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,在事务提交之前一直不变。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 认知科技技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档