抛出一个面试问题
Mysql如何实现乐观锁与悲观锁?
关于悲观锁已经探讨过了,传送门:Myql悲观锁
乐观锁
再次看下用户转账的场景,可能涉及多个步骤
- 首先查询用户账户信息:select操作
- 检测是否能进行转账:业务检查
- 进行转账,更新账户余额等信息
假定这个操作就在一个本地事物中,看起来是不会有问题的。但是如果出现并发,就有可能出现丢失更新,如下
上图为丢失更新的一种,这种情况下是很可怕的,产生了资损。
前面已经介绍过了悲观锁的解决办法,下面看看乐观锁怎么解决这个问题
乐观锁的设计
悲观锁,顾名思义,就是每次操作的时候都要获取唯一的一把锁。Mysql里面悲观锁都是X锁,独占的,简而言之就是操作变成了串行。如果并发比较高,每次都要等待前面的操作释放,恰巧业务操作比较耗时,就可能会造成许多失败。
相对的,另一种假设每次操作都不会产生业务冲突,所以在操作中也就不加锁。不加锁的操作就为乐观锁。
那么问题来了,不加锁怎么保证操作的正确呢?
在每次更新时,都带上一个版本号!
大致就是
- 以Mysql数据库为例,在设计表结构的时候,加上一个version字段,
CREATE TABLE `t1` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, `version` int(11) NOT NULL, `age` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='测试表1';
-
每次更新的时候条件带上version,每次更新对version+1
// 获取账户信息 select * from account where user_id = 'XX' // 更新账户信息,同时version+1,条件里面带上前一个version update account set balance = yy, version = version + 1 where user_id = 'XX' and version = ''
-
比较更新结果是否为1,如果为1,表示更新成功,反之更新失败,根据业务规则决定是重新还是报出异常
注意上面更新的数据库字段最好是主键或者唯一索引,不然可能会造成锁表
流程图
借用网上的经典图:如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。
看到这里不知道大家有没有一个疑问,就是如果A,B同一时刻(无线接近,趋于相等)执行数据库更新操作,怎么能保证呢只有一个成功,一个失败?
个人理解是这样的,在系统时钟上来看,A,B操作肯定有一个在前,一个在后。说的同一时刻,可能是在A执行的过程中,B也开始执行了,那么为什么A执行了,B乐观更新失败呢?因为Mysql在RR隔离级别下,对update默认是加X锁的,这个时候B根本拿不到锁,也就执行不了。而B能执行时,是A已经更新完成,此时version已经发生变化,自然就失败了。
所以说的乐观锁不加锁,一般指的在业务整个过程中,不用对业务记录一直加锁,减少加锁的时间而已
应用场景
JVM中的CAS就用到了version机制来解决CAS存在ABA问题,可以视为一种乐观锁应用。