两类更新丢失及解决办法

我们首先创建一张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; 
查询账户余额为1000元    

T4

select money from user where id = 1;
查询账户余额为1000元

T5

money = 1000 + 100;
update user set money=1100 where id = 1;
汇入100元把余额改为1100元

T6

commit;
提交事务

T7

money = 1000 - 100;
update user set money=900 where id = 1;
取出100元把余额改为900元

T8

rollback;
回滚事务

T9

余额恢复为1000 元(丢失更新)

A事务在回滚时,“不小心”将B事务已经转入账户的金额给抹去了。

在MySQL数据库,任何隔离级别不允许第一类更新丢失


第二类丢失更新(覆盖丢失/两次更新问题) 

A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失: 

时间

转账事务A

取款事务B

T1

begin; 
开始事务

begin;
开始事务

T2

select money from user where id = 1;
查询账户余额为1000元                

T3

select money from user where id = 1;
查询账户余额为1000元

  

T4

money = 1000 + 100;
update user set money=1100 where id = 1;
汇入100元

T5

commit;
提交事务   

T6

select money from user where id = 1;
查询账户余额为1000元

        

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;
获取版本号为0

select version from user where id = 1;
获取版本号为0

begin;
开启事物

begin;
开启事物

money = 1000 - 100;
update user set money=900,version = version +1 where id = 1 and version = 0;
取出100元把余额改为900元

更新成功,更新条数为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方法需要多一个版本列的维护。

个人建议:在用户并发数比较少且冲突比较严重的应用系统中选择悲观锁方法,其他情况首先乐观锁版本列法。 

你可能感兴趣的:(#,MySQL,核心技术,大数据)