前言
大家对 乐观锁 这三个字眼应该不陌生吧?
为什么今天我想谈谈乐观锁的设计呢?
关于数据库的乐观锁使用, 是不是很多人一看到乐观锁就会想到 Version 字段 (版本标识)。
ps: 其实不是非要新增版本字段
正文
乐观锁 , Optimistic Concurrency Control (乐观并发控制),简称 OCC 。
乐观锁不是一种真正的 ‘ 锁 ’,而是一种实现锁效果的 设计思想:
乐观地 认为 并发的操作对数据 不会产生冲突,所以没有使用 真正的 ‘锁’ 去对数据加锁;
而是选择在提交数据的时候,去检测数据是否冲突了?
发现冲突就采取 处理操作,例如报错、重试、停止等等。
设计
基于数据库使用展开介绍
使用版本标识 version 字段
也就是在 表内 增加一个字段 version 。
每次写操作如果时成功的,都需要 将 version 版本值 +1 ,
例如原来 某条数据的 version值为 1, 如果修改了,那么 version就需要变成 version+ 1 , 也就是 2.
然而在并发场景,大量的写操作不免会发生冲突。
所以当我们 读取 数据 且 需要做更新操作时。 我们的 设计流程时这样的:
1. 读取数据,把数据里的version值 取出作为 更新前标识 值 version-before。
2.做业务逻辑计算等等 ....
3. 更新数据操作 ,更新时, 将之前的 标识 值 version-before 与 数据库里面的 version值 做匹对, 检测是否一致。
如果一致, 那么意味着 这时段内,没有其他写操作修改过数据, 那么我们可以提交成功。
如果不一致,那么意味着 发生了写写冲突, 也就是我们此刻需要更新的数据,已经被修改过了。那么我们可以根据业务场景,采取处理措施 (报错记录、重试流程、停止等等)。
ps: 注意了,这里的读取,检测,更新 这些操作都是务必保证 操作的原子性 ,连贯执行,也就是处于同个事务内。
mysql语句的写法举例:
update proinfo set proNum = proNum + 10 , version = version + 1 where version = #{version} and proId = #{proId}
只要where 后的 version 条件不成立,那么就是更新不成功,也就是 检测到了 ‘冲突’ 。
那么前言里,我提到 使用乐观锁,不一定非要新增版本 version字段。
我们还可以使用 updateTimestamp 这种字段值。
我想,大家接触过很多项目,是不是看到很多老项目的表内都会有个 更新时间(时间戳)的字段,
但是好像业务里又没有用。
其实,这种字段,可以用来实现 乐观锁。
精确到毫秒或者更细, 每次操作,读取数据前把 时间戳的值 保存,然后更新提交的时候, 将这时间戳和数据库内的时间戳 值做匹对,原理也是一样的。时间戳字段可以自己传入,也可以是通过mysql函数默认获取更新。
update proinfo set proNum = proNum + 10 , updateTimestamp = unix_timestamp(now()) where updateTimestamp = #{updateTimestamp} and proId = #{proId}
(可能有人会反驳,如果时间戳一模一样呢? 我不多说、)
为什么需要提这个呢。 因为我想传达的是, 乐观锁,要理解这种 乐观控制的设计思想,灵活去运用。
而不是固化,看到千篇一律地加字段 version,就跟着加。
有时候有些老项目,不是说加个字段那么回事,也许会引发一些杂七杂八的问题。
那么我们大可去根据实际情况去设计乐观锁。
那么最后也简单地说下这个数据库使用乐观锁设计,
1.最好是 读多写少的场景下使用,因为写的操作少了,也就更乐观了。
2. 在发现冲突时, 咱们的处理操作要慎重设计, 特别是写操作并发特别多的情况,采取 无限制地重试? 短时间会不会适得其反?
思想很重要,不是只顾着去套模式,因为掌握了这种思想,也许你不单单在使用乐观锁的时候你才用得上。
最后给大家留个小话题, CAS 无锁算法 大家了解过么?
可以去了解下,再回来 看看 文中说的 数据库里乐观锁的设计思想。
该篇浅谈就到这,神神叨叨习惯了,说的东西可能没营养可能有营养,就到这吧。