在并发编程和数据库操作中,锁机制是确保数据一致性的重要手段。乐观锁和悲观锁作为两种主流的并发控制策略,各自适用于不同的业务场景。本文将剖析这两种锁机制的核心思想、技术实现和典型应用,帮助开发者做出合理的技术选型。
基本概念与核心思想
1. 悲观锁:保守的独占策略
悲观锁基于"最坏情况"假设,认为并发操作中数据冲突必然发生。其核心机制是:访问数据前先加锁,确保整个操作过程中资源被独占。
典型特征:
- 获取锁后才能操作数据
- 其他线程必须阻塞等待
- 适合写操作频繁的场景
- 实现简单但性能开销大
2. 乐观锁:乐观的冲突检测
乐观锁则持相反观点,假设多数情况下不会发生冲突。它不提前加锁,只在数据提交时检查是否被其他线程修改。
典型特征:
- 读取数据时不加锁
- 提交时通过版本号/CAS检测冲突
- 冲突时回滚或重试
- 适合读多写少的场景
技术实现对比
悲观锁实现方式
在Java中,synchronize 和 ReentrantLock都是悲观锁的实现
// synchronized关键字
public synchronized void update() {
// 临界区代码
// ReentrantLock显式锁
private Lock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// 临界区代码
finally {
lock.unlock();
}
乐观锁实现方式
(1) 版本号机制
-- 数据库表添加version字段
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=1 AND version=old_version;
(2) CAS原子操作
// Java Atomic类
AtomicInteger counter = new AtomicInteger(0);
public void increment() {
while(true) {
int current = counter.get();
int next = current + 1;
if(counter.compareAndSet(current, next)) {
break;
}
}
}
(3) 技术对比表
特性 |
悲观锁 |
乐观锁 |
加锁时机 |
操作前加锁 |
提交时验证 |
实现复杂度 |
简单 |
需处理冲突重试 |
性能开销 |
高(锁竞争) |
低(无阻塞) |
数据一致性 |
强一致性 |
最终一致性 |
适用场景 |
写多读少 |
读多写少 |
典型实现 |
synchronized ReentrantLock |
版本号、CAS |
应用场景分析
悲观锁适用场景
- 金融交易系统:如账户余额修改,要求强一致性
- 库存实时扣减:避免超卖现象
- 长事务处理:操作过程需要保持数据状态
- 临界资源保护:如全局配置修改
乐观锁适用场景
- 商品详情页:大量并发读取,少量评价更新
- 文档协作编辑:多人同时编辑不同部分
- 统计计数器:如文章阅读量统计
- 版本控制系统:如Git、SVN的合并机制
经典案例对比
电商库存管理:
- 悲观锁方案:
BEGIN;
SELECT stock FROM products WHERE id=1 FOR UPDATE;
-- 检查库存
UPDATE products SET stock=stock-1 WHERE id=1;
COMMIT;
优点:能够尽可能的避免超卖
缺点:高并发时性能急剧下降
- 乐观锁方案:
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=1 AND version=old_version AND stock>0;
优点:支持更高并发
缺点:失败需前端重试
进阶问题与解决方案
1. 乐观锁的ABA问题
问题描述:
变量值从A改为B又改回A,CAS检查会误判无修改。
解决方案:
- 使用AtomicStampedReference添加版本戳
- 数据库使用自增版本号而非时间戳
2. 高冲突下的优化策略
- 指数退避:冲突后延迟重试,避免活锁
- 熔断机制:冲突超过阈值转为悲观锁
- 批量合并:累计多次更新一次性提交
3. 分布式锁实现
3.1. 基于Redis的悲观锁(Redisson)
核心机制:
// 加锁逻辑
SET lock_key client_id NX PX 30000
// 看门狗自动续期(默认30秒续期一次)
高级特性:
- 可重入锁:同一线程可重复获取锁
- 红锁算法:跨多个Redis节点实现容错
- 公平锁:按请求顺序分配锁
代码示例:
RLock lock = redissonClient.getLock("payment_lock");
try {
if(lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.unlock();
}
3.2. 分布式乐观锁实现
- Redis WATCH/MULTI/EXEC指令
- 实现流程:
WATCH resource_key
current_value = GET resource_key
MULTI
SET resource_key new_value
EXEC # 若key被修改则返回nil
- 适用场景:简单数据结构的原子更新
- 增强版实现方案
- 版本号机制:
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=1 AND version=old_version
- Redis Lua脚本:
local key = KEYS[1]
local version = ARGV[1]
if redis.call("get", key.."_version") == version then
redis.call("set", key.."_version", version+1)
return redis.call("incrby", key, -1)
end
return false
实践建议
在选择使用时需要注意事项如下
- 评估冲突概率:低冲突(<20%)优先乐观锁,高冲突选择悲观锁
- 监控关键指标:包括锁等待时间、冲突重试次数等
- 合理设置超时:避免线程长时间阻塞
- 结合业务特点:如金融系统倾向悲观锁,社交 feed 流倾向乐观锁
小结
乐观锁和悲观锁各有优劣,没有绝对的好坏之分。技术选型的核心在于理解业务场景:对数据一致性要求极高的场景适合悲观锁;追求高吞吐、能容忍偶尔重试的场景则乐观锁更优。分布式系统中,往往需要两者结合使用,如用乐观锁处理大部分请求,对特定关键操作采用悲观锁保护。