背景知识
好多开发同学都知道事务有四大特性ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),但对隔离性的具体内容却不太清楚。
SQL标准的事务隔离级别有4种:读未提交(read uncommitted,简写ru)、读提交(read committed,简写rc)、可重复读(repeatable read,简写rr)、串行化(serializable)。
事务隔离其实是一种并发保护机制。
MySQL的事务隔离是引擎层实现的,InnoDB默认的事务隔离级别是可重复读(reaptable read)。
可重复读意思是一旦我的事务开启了,其他事务的修改我就不管了(读不到了)。
问题
如果我们不了解MySQL事务隔离级别和相应事务隔离级别对应的含义,那么我们写的业务代码可能会遇到一些奇怪的问题。一个常见的问题是依赖的数据读不到,程序逻辑出现异常。
举例
下面是一个网友向我寻求帮助的一个的问题。
网友的业务,推测是电商的。
场景
一个账户在指定的时间段内消费有最高限额,达到限额就不允许继续消费。
网友的实现
1、账户消费会保存记录到消费记录表。
2、当用户发起消费时,业务流程如下,
①获取用户账户信息(select for update语句,加锁。)
②获取用户指定时间段消费记录表所有记录
③汇总判断是否超过限额,不超过则允许消费,生成消费记录;超过则不允许消费。整个流程在一个事务里。
问题
高并发情况下,消费记录会超过限额。网友疑问,整个流程已经用了数据库行锁,为什么锁不住还是超过限额?
排查
1、通过仔细分析他发给我的日志,我确定是行锁是有生效的。因为日志时间戳清楚地显示并发时2个线程执行事务,在获取账户信息(select for update,数据库行锁)操作上是有先后顺序的。
2、问题关键在②。MySQL事务隔离级别是rr。假设现在有2个事务,事务A执行①就已开启事务,此时事务B执行到①也开启了事务,但因为行锁阻塞了,事务A继续执行②③之后插入消费记录,然后事务A提交;事务B继续执行②,因为事务隔离级别rr,事务B读不到事务A插入的消费记录,也执行③插入消费记录,然后就超限额了。
解决方案
方法1、修改数据库事务隔离级别为成读提交(read commited)。这样事务A提交后,事务B就能读到它插入的记录。
方法2、修改实现。不采用汇总消费记录来控制限额,而是对每个账户每个消费时间段记录一条消费限额记录,记录剩余金额。流程改为先加锁读取限额记录,然后判断限额,插入记录并更新限额记录剩余金额。这样即使在rr隔离级别下,利用行锁,上面事务A和B例子,事务B能读取到最新剩余金额,完成限额控制逻辑。
结果
最终网友使用方法1在自己开发环境验证了问题,但生成环境不能随便更改事务隔离级别,所以采用方法2解决了问题。
结论
一般情况下,不可重复读和幻读是可以接受的,我们的项目使用读提交(read committed)就可以了。可重复读(reaptable read)为了解决幻读问题,引入了间隙锁,会加大死锁的概率,降低系统并发效率。