我们首先创建一张user表,id为主键
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`money` int DEFAULT NULL,
`version` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
第一类丢失更新(回滚丢失)
A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:
时间 | 取款事务A | 转账事务B |
T1 | begin; |
|
T2 | begin; 开始事务 | |
T3 | select money from user where id = 1; | |
T4 | select money from user where id = 1; | |
T5 | money = 1000 + 100; | |
T6 | commit; | |
T7 | money = 1000 - 100; | |
T8 | rollback; | |
T9 | 余额恢复为1000 元(丢失更新) |
A事务在回滚时,“不小心”将B事务已经转入账户的金额给抹去了。
在MySQL数据库,任何隔离级别不允许第一类更新丢失
第二类丢失更新(覆盖丢失/两次更新问题)
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:
时间 | 转账事务A | 取款事务B |
T1 | begin; | begin; |
T2 | select money from user where id = 1; | |
T3 | select money from user where id = 1; |
|
T4 | money = 1000 + 100; update user set money=1100 where id = 1; 汇入100元 | |
T5 | commit; | |
T6 | select money from user where id = 1; |
|
T7 | money = 1000 - 100; update user set money=900 where id = 1; 取出100元把余额改为900元 | |
T8 | commit; | |
T9 | 把余额改为900 元(丢失更新) |
上面的例子里由于取款覆盖了转账对存款余额所做的更新,导致用户最后损失了100元,相反如果取款事务先提交,那么用银行将损失100元。
第二类丢失更新原因:MySQL可重复读默认采用的是快照读。快照读的一个问题也就是没有办法获取最新的数据。所以快照读是第二类更新丢失的一个主要原因。
解决快照读:基本两种思路,一种是悲观锁,另外一种是乐观锁;
简单的说就是一种假定这样的问题是高概率的,最好一开始就锁住,免得更新老是失败;另外一种假定这样的问题是小概率的,最后一步做更新的时候再锁住,免得锁住时间太长影响其他人做有关操作。
悲观锁
读取数据的时候加锁,这样能够保证读到的数据都是最新的数据,并且会将记录锁住,其他事物如果想要获取锁会阻塞。所以整体业务流程变成:在开启事物后要获取锁,然后根据业务场景不同进行不同操作
begin; 开启事务 | begin; 开启事务 |
select money from user where id = 1 for update; 查询账户余额为1000元 | |
select money from user where id = 1 for update; 阻塞... | |
money = 1000 - 100; update user set money=900 where id = 1; 取出100元把余额改为900元 | 阻塞... |
commit; | 阻塞... |
查到数据 :money = 900 | |
其他操作.... |
乐观锁
添加version字段,记录每条记录的更新版本,每次更新后版本号加一。
在每次事物开启之前获取行的版本号,在更新的时候带上版本号进行判断。update money set money = money + 100 where id = 1 and version = 查到的版本号
select version from user where id = 1; | |
select version from user where id = 1; | |
begin; | begin; |
money = 1000 - 100; | |
更新成功,更新条数为1 | money = 1000 + 100; update user set money=1100,version = version +1 where id = 1 and version = 0; 阻塞... |
阻塞... | |
commit | |
更新成功,更新条数为0(因为另一个事物修改了版本号现在版本号为1) |
总结:
悲观锁b方法是通过select..for update方式,这个可能会导致其他会话的阻塞,而乐观锁b方法需要多一个版本列的维护。
个人建议:在用户并发数比较少且冲突比较严重的应用系统中选择悲观锁方法,其他情况首先乐观锁版本列法。