在并发编程和数据库设计中,乐观锁(Optimistic Locking) 的核心思想是:假设冲突不会发生。它在读取数据时不加锁,只有在最后“提交更新”的一瞬间,才会检查这段时间内数据是否被别人改过。
如果数据没变,则更新成功;如果数据变了,则更新失败(通常报错或重试)。
常见的实现方式有以下三种:
1. 版本号机制(最常用)
这是数据库实现中最标准的方法。在表中新增一个字段 version。
-
步骤:
-
读取记录,获取当前
version(假设为1)。 -
在内存中修改业务数据。
-
更新时,执行带有条件的 SQL:
UPDATE table SET score = 100, version = version + 1 WHERE id = 1 AND version = 1; -
判断结果:如果影响行数为 1,表示成功;如果为 0,表示这期间有人改了数据,版本号已不再是 1,此时触发重试逻辑。
-
2. CAS (Compare And Swap) 机制
这是 Java 并发包(java.util.concurrent.atomic)底层的核心原理,依赖于 CPU 的硬件指令。
-
原理:包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
-
逻辑:如果内存位置 V 的值等于预期原值 A,则将 V 修改为 B。否则,什么都不做。
-
Java 示例:
JavaAtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 内部通过 CAS 自旋实现 -
缺点:ABA 问题。如果一个值从 A 变成 B 又变回 A,CAS 会认为它没变。
-
解决办法:加时间戳或版本号(如 Java 中的
AtomicStampedReference)。
-
3. 时间戳(Timestamp)
逻辑与版本号类似,只是把 version 换成了 update_time。
-
逻辑:提交更新时比较最后修改时间是否一致。
-
缺点:在高并发下,如果系统时钟精度不够(比如两个请求在同一毫秒内到达),可能会出现误判,安全性略低于递增的版本号。
乐观锁 vs 悲观锁:怎么选?
| 维度 | 乐观锁 | 悲观锁 (SELECT … FOR UPDATE) |
| 核心假设 | 冲突极少发生。 | 冲突经常发生。 |
| 开销 | 低(不涉及内核态锁竞争)。 | 高(会造成线程阻塞、上下文切换)。 |
| 吞吐量 | 高。 | 低。 |
| 适用场景 | 读多写少(如商城修改个人信息)。 | 写多读少(如秒杀抢购、库存强一致性)。 |
结合你的业务:KeyGeneratorService
如果你在生成比特币私钥后需要把“发放记录”存入数据库(防止同一个私钥发给两个人),建议使用版本号乐观锁:
-- 伪代码
UPDATE private_keys
SET status = 'USED', user_id = 9527, version = version + 1
WHERE key_id = 100 AND version = 5;
这样做可以保证在高并发发放私钥时,数据库不会因为大量的排他锁(Pessimistic Lock)而卡死。
既然聊到了并发控制,你想了解一下在高并发场景下,如何通过“重试策略(Retry Policy)”来处理乐观锁更新失败的情况吗?