Java乐观锁与悲观锁实现方案对比
Java并发控制:乐观锁与悲观锁深度解析
一、乐观锁实现方案
乐观锁假设并发冲突概率低,采用"先修改再验证"的策略,适合读多写少场景。
1. 版本号机制(@Version)
@Entity
public class Product {
@Id
private Long id;
private String name;
private int stock;
@Version // 关键注解
private int version;
// getters/setters...
}
实现原理:
- 读取数据时获取版本号
- 更新时检查版本号是否变化
- 若版本一致则更新成功并递增版本号
- 若版本不一致抛出
OptimisticLockException
实践建议:
- 适合电商库存、订单状态等低频修改场景
- 结合Spring的
@Retryable
实现自动重试 - 版本号建议使用长整型避免溢出
2. CAS操作
Compare-And-Swap是CPU原子指令,Java通过Unsafe
类和原子包装类提供支持。
// AtomicInteger实现示例
public class AtomicInteger {
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
典型应用场景:
- 计数器(如
AtomicLong
) - 状态标志位(如
AtomicBoolean
) - 累加器(如
LongAdder
)
ABA问题解决方案:
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
int stamp = ref.getStamp();
ref.compareAndSet(100, 101, stamp, stamp+1);
二、悲观锁应用场景
悲观锁假设并发冲突概率高,采用"先锁定再修改"策略,适合写多读少场景。
1. SELECT FOR UPDATE
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 获取行锁
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
锁升级情况:
- 无索引查询→表锁
- 唯一索引查询→行锁
- 非唯一索引查询→间隙锁
实践建议:
- 明确指定索引列避免全表锁定
- 设置合理的锁超时时间:
SET innodb_lock_wait_timeout=3
- 配合
NOWAIT
选项快速失败:SELECT ... FOR UPDATE NOWAIT
2. 死锁检测与避免
常见死锁场景:
-- 事务1
UPDATE user SET name='A' WHERE id=1;
UPDATE user SET name='B' WHERE id=2;
-- 事务2(相反顺序)
UPDATE user SET name='B' WHERE id=2;
UPDATE user SET name='A' WHERE id=1;
解决方案:
- 统一资源访问顺序
- 设置锁超时:
innodb_lock_wait_timeout
- 死锁检测:
SHOW ENGINE INNODB STATUS
- 应用层实现全局锁排序
三、分布式锁与事务结合
1. Redis RedLock算法
实现步骤:
- 获取当前毫秒时间戳T1
- 依次向N个Redis节点请求锁(SET NX PX)
- 计算获取锁耗时=当前时间T2-T1
- 当且仅当获得多数节点(N/2+1)认可且耗时小于锁有效期时才认为成功
- 实际有效时间 = 原设置时间 - 获取锁耗时
// Redisson实现示例
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}
2. Zookeeper分布式锁
临时顺序节点实现:
实践建议:
- 使用Curator的
InterProcessMutex
- 设置合理的sessionTimeout(建议10-30秒)
- 实现锁的自动续期机制
// Curator实现示例
InterProcessMutex lock = new InterProcessMutex(client, "/transaction/lock");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
lock.release();
}
四、选型决策树
性能对比指标:
方案 | TPS | 延迟 | 一致性 | 实现复杂度 |
---|---|---|---|---|
乐观锁 | 10k+ | 1-5ms | 最终 | 低 |
SELECT FOR UPDATE | 3k | 10-50ms | 强 | 中 |
Redis锁 | 8k | 5-20ms | 弱 | 中 |
Zookeeper锁 | 2k | 50-100ms | 强 | 高 |
五、最佳实践
- 混合锁策略:读操作用乐观锁,写操作用悲观锁
- 锁粒度控制:库存系统按SKU维度加锁而非整个仓库
- 事务拆分:长事务拆分为多个短事务+本地状态
- 降级方案:分布式锁失败时降级为本地锁+补偿机制
监控指标:
- 锁等待时间
- 死锁次数
- 锁获取成功率
// 混合锁示例
public void updateProduct(Long id) {
Product product = productDao.findById(id); // 无锁读取
// 业务校验...
int retry = 0;
while (retry++ < 3) {
try {
productDao.updateWithVersion(product); // 乐观锁更新
break;
} catch (OptimisticLockException e) {
product = productDao.findById(id); // 重新加载
// 重新计算业务逻辑...
}
}
if (retry >= 3) {
lock.lock(); // 退化为悲观锁
try {
// 最终处理...
} finally {
lock.unlock();
}
}
}