1. 并发控制的本质矛盾
在数据库和编程领域,乐观锁和悲观锁是解决并发冲突的两种经典策略。它们的核心差异源于对"并发冲突发生概率"的基本假设不同——这就像两种性格的人对待同一件事的态度:悲观者总是做最坏打算,乐观者则相信事情会顺利发展。
我处理过的一个电商库存系统案例就很典型。当多个用户同时抢购同一件商品时,如果采用悲观锁策略,系统会默认"肯定会有人跟我抢",所以从一开始就锁住数据;而乐观锁则认为"冲突应该不常发生",只在最后提交时检查是否有冲突。这两种思路在性能、复杂度上有着截然不同的表现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 悲观锁:先保护再操作
2.1 工作机制与实现方式
悲观锁的核心逻辑是"先获取锁,再操作数据"。在MySQL中,典型的实现是通过SELECT ... FOR UPDATE语句:
sql复制BEGIN;
-- 锁定id=123的记录
SELECT * FROM products WHERE id=123 FOR UPDATE;
-- 执行业务逻辑
UPDATE products SET stock=stock-1 WHERE id=123;
COMMIT;
这个过程中,第一个事务获取锁后,其他事务如果尝试锁定同一行,会被阻塞直到锁释放。Java中的synchronized关键字也是类似的机制:
java复制public synchronized void updateInventory() {
// 线程安全的代码块
}
2.2 适用场景与性能代价
悲观锁最适合冲突频繁的场景。比如银行账户转账:几乎每次操作都可能涉及相同账户的并发修改。但它的代价也很明显:
- 锁获取和释放需要额外开销
- 可能引起死锁(两个事务互相等待对方释放锁)
- 高并发下大量线程阻塞会导致吞吐量下降
我在金融系统项目中就遇到过这样的案例:夜间批量处理时,错误的锁粒度设计导致上千个事务串行执行,原本1小时的任务跑了整晚。后来通过缩小锁范围(从表锁改为行锁)和设置锁超时才解决问题。
3. 乐观锁:先操作再验证
3.1 版本控制机制
乐观锁通常通过版本号或时间戳实现。以Hibernate为例:
java复制@Entity
public class Product {
@Id
private Long id;
@Version
private int version; // 乐观锁版本字段
private int stock;
}
// 更新时会自动检查版本
product.setStock(newStock);
session.update(product);
对应的SQL会包含版本检查:
sql复制UPDATE products
SET stock=10, version=version+1
WHERE id=123 AND version=5
如果更新行数为0,说明版本已变更,会抛出OptimisticLockException。
3.2 CAS算法的硬件支持
CPU层面的CAS(Compare-And-Swap)指令是乐观锁的硬件基础。Java中的AtomicInteger就是典型实现:
java复制AtomicInteger counter = new AtomicInteger(0);
// 类似乐观锁的CAS操作
boolean success = counter.compareAndSet(0, 1);
这个操作在底层是一条CPU指令完成的,比传统锁更高效。但要注意ABA问题——如果一个值从A变B又变回A,简单的CAS会误判没有变化。解决方案是使用带标记的引用(如AtomicStampedReference)。
4. 实现原理深度对比
4.1 数据库层面的实现差异
在MySQL InnoDB中,悲观锁通过锁管理器实现:
- 意向锁(Intention Locks):表明事务想在某个粒度上加锁
- 记录锁(Record Locks):锁定索引记录
- 间隙锁(Gap Locks):锁定索引记录间的间隙
- 临键锁(Next-Key Locks):记录锁+间隙锁的组合
而乐观锁在数据库层面就是普通的UPDATE语句加上版本检查,依赖的是事务的隔离性(通常是READ COMMITTED或REPEATABLE READ级别)。
4.2 并发数据结构中的应用
ConcurrentHashMap是两种策略结合的典型案例:
- 分段锁(Segment):底层是悲观锁思路,不同段可以并行修改
- CAS操作:在具体槽位上使用乐观锁进行无锁化更新
这种混合设计在高并发场景下表现优异。我在一个日活千万的系统中测试发现,相比Hashtable的全表锁,ConcurrentHashMap的吞吐量提升了近20倍。
5. 选型决策的关键因素
5.1 冲突频率的评估方法
通过数据库监控可以统计:
- 锁等待时间(lock_wait_time)
- 死锁发生率(innodb_deadlocks)
- 更新冲突率(update冲突数/总更新数)
我曾经通过slow query log分析出一个系统的更新冲突率只有0.3%,这种情况下从悲观锁切换到乐观锁后,TPS直接从1200提升到5800。
5.2 业务语义的影响
有些业务天然适合某种策略:
- 悲观锁:账户扣款、座位锁定
- 乐观锁:购物车合并、计数器更新
但要注意混合场景。比如电商下单流程:
- 库存检查用乐观锁(冲突率低)
- 支付处理用悲观锁(必须强一致性)
6. 实战中的进阶技巧
6.1 乐观锁的重试策略
简单的"失败即报错"体验很差。更好的做法是:
java复制int retries = 3;
while(retries-- > 0) {
try {
// 尝试业务操作
return doBusiness();
} catch(OptimisticLockException e) {
// 刷新数据并重试
refreshState();
}
}
throw new BusyException("操作过于频繁");
但要注意:
- 设置合理的重试次数(通常3-5次)
- 重试间增加随机延迟(避免活锁)
- 记录重试日志用于监控
6.2 悲观锁的优化手段
- 缩小锁粒度:从表锁→行锁→只锁必要字段
- 控制锁时长:将耗时操作移到锁外
- 使用锁超时:
SET innodb_lock_wait_timeout=3
一个实际案例:我们将用户画像更新的锁范围从整个用户对象缩小到仅锁定积分字段后,系统延迟降低了65%。
7. 特殊场景处理方案
7.1 分布式环境下的挑战
单机锁在分布式系统中会失效。解决方案包括:
- 分布式锁(Redis/ZooKeeper实现)
- 数据库唯一约束
- 乐观锁+版本号全局唯一
我曾经实现过一个基于Redis的分布式乐观锁:
python复制def update_with_optimistic_lock(key, modify_func):
with redis.pipeline() as pipe:
while True:
try:
pipe.watch(key)
current_version = pipe.get(f"{key}:version")
# 执行业务逻辑
new_value = modify_func(pipe.get(key))
pipe.multi()
pipe.set(key, new_value)
pipe.incr(f"{key}:version")
pipe.execute()
return True
except WatchError:
continue
7.2 批量操作的锁策略
批量更新如果逐条检查版本会导致性能问题。解决方案:
- 使用批量CAS(如MySQL的WHERE version IN(...))
- 采用最终一致性(先执行后补偿)
- 分批次处理+并行流
在数据迁移项目中,我们通过分批+并行处理将1000万条数据的乐观锁更新从8小时缩短到23分钟。
