乐观锁和悲观锁的理解
参考解答
1. 首先看一个著名的 丢失更新 问题:
两个用户同时修改一条数据,原数据为:
id name age
1 张三 18
程序设计时采用下列sql来完成更新
update stu set name=?, age=? where id=?
时间 | 执行用户 | sql | 解析 |
---|---|---|---|
t1 | 用户1 | select * from stu where id=1 | 用户1看到的数据为1,张三,18并显示在表单页面,准备更新age 18->20 |
t2 | 用户2 | select * from stu where id=1 | 用户2看到的数据为1,张三,18并显示在表单页面,准备更新name 张三->张小三 |
t3 | 用户2 | update stu set name=张小三, age=18 where id=1 | 以自己看到的数据,执行更新 |
t4 | 用户1 | update stu set name=张三, age=20 where id=1 | 以自己看到的数据,执行更新 |
t5 | 用户2 | select * from stu where id=1 | 用户2看到的数据是1,张三,20,用户2对姓名的修改丢失了 |
简单的讲,就是用户1查询出来的数据被别人修改过了,自己看到的还是旧的数据,以旧数据去更新,就会将别人的修改覆盖掉。
2. 解决方法
解法1: 悲观锁
在t3处改为:
开始事务
select * from stu where id=1 for update; // 查出最新的数据1,张三,18
update stu set name=张小三(用户2要修改的值), age=18(查出来的最新值) where id=1;
提交事务
在t4处改为:
开始事务
select * from stu where id=1 for update; // 查出最新的数据1,张小三,18
update stu set name=张小三(查出来的最新值), age=20(用户1要修改的值) where id=1;
提交事务
注意 事务和select ... for update是必须的
- 普通的select是非阻塞的,不能保证查询的同时别人对数据做修改
- select ... for update必须配合事务使用,查询时加锁,事务结束才会释放for update 加的锁
解法2: 乐观锁
修改表结构,添加一个version(版本)列,每次更新时版本列加1,并且where条件中也要加入版本列条件
id name age version
1 张三 18 1
在t3处改为:
最早在t1时查出的数据为:1,张三,18,版本1
update stu set name=张小三(用户2要修改的值), age=18, version=2 where id=1 and version=1;
检查更新的影响行数,结果影响行数为1,更新成功
在t4处改为:
最早在t2时查出的数据为:1,张三,18,版本1
update stu set name=张三(自己拿到的值,已经旧了), age=20(用户1要修改的值), version=2 where id=1 and version=1;
检查更新的影响行数,结果影响行数为0,没有更新到
重新查询数据,重复以上逻辑,直至成功
悲观锁和乐观锁比较
悲观锁的缺点,在于在修改前加锁,并发性比较低。即使冲突的可能性比较小,还要每次都加锁,太悲观了。
乐观锁的思想在于,我每次修改完检查一下,看是否有别人做了修改,如果有,那么我刚才做的修改不算,重新尝试一次吧,如果第二次还是有人修改了,那么再尝试第三次... 以乐观的态度面对修改失败。
对于读多写少的业务场景,可以采用乐观锁。 如果写比较频繁,乐观锁会导致重试较多,不太合适。