Java事务与并发控制实战:乐观锁、悲观锁与分布式锁
Java事务与并发控制实战指南
一、乐观锁实现方案
乐观锁假设并发冲突概率低,通过版本控制实现无锁并发,适合读多写少场景。
1. 版本号机制(@Version)
@Entity
public class Product {
@Id
private Long id;
private String name;
private Integer stock;
@Version // 关键注解
private Integer version;
// getters/setters...
}
工作原理:
- 读取数据时获取当前版本号
- 更新时检查版本号是否变化
- 若版本一致则更新成功并递增版本号
- 若版本不一致抛出
OptimisticLockException
实践建议:
- 结合Spring Data JPA使用最简便
- 版本字段建议用包装类型(允许null)
- 捕获异常后应提供友好重试机制
2. CAS操作
AtomicInteger counter = new AtomicInteger(0);
// 典型CAS操作
boolean success = counter.compareAndSet(expect, update);
适用场景:
- 简单计数器场景
- 无状态并发控制
- JDK原子类底层实现
性能对比:
二、悲观锁应用场景
悲观锁假定并发冲突必然发生,适合写多读少或强一致性要求的场景。
1. SELECT FOR UPDATE
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 获取行锁
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
锁升级问题:
- MySQL可能从行锁升级为表锁
- 索引失效会导致锁全表
- 建议通过
EXPLAIN
确认索引使用情况
2. 死锁检测与避免
常见死锁场景:
// 线程1
synchronized(resourceA) {
synchronized(resourceB) { ... }
}
// 线程2
synchronized(resourceB) {
synchronized(resourceA) { ... }
}
解决方案:
- 统一资源获取顺序
- 设置锁超时时间(
tryLock
) - 使用
jstack
分析线程转储
实践建议:
- 生产环境建议添加
innodb_deadlock_detect = ON
- 事务应尽量短小精悍
- 避免在事务中包含RPC调用
三、分布式锁与事务结合
1. Redis RedLock算法
实现步骤:
- 获取当前毫秒级时间戳T1
- 轮流在N个Redis节点执行SETNX命令
- 计算获取锁总耗时(T2-T1)
- 当且仅当在多数节点获取成功且耗时小于锁有效期时认为成功
// Redisson实现示例
RLock lock = redisson.getLock("orderLock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.unlock();
}
2. Zookeeper分布式锁
临时顺序节点方案:
实践建议:
- 推荐使用Curator框架的
InterProcessMutex
- 注意处理session过期导致的临时节点消失
- 设置合理的重试策略和超时时间
四、混合方案最佳实践
电商库存扣减示例:
- 前端:令牌防重+滑动窗口限流
- 网关层:Redis漏桶算法限流
- 服务层:本地锁+Redis分布式锁双重校验
- 数据层:最终通过数据库乐观锁保证一致性
性能优化技巧:
- 热点数据采用缓存+异步刷盘
- 合并短事务为批量操作
- 对锁进行分段处理(如ConcurrentHashMap的分段锁思想)
总结对比
方案类型 | 适用场景 | 性能 | 一致性强度 | 实现复杂度 |
---|---|---|---|---|
乐观锁 | 读多写少 | 高 | 最终一致 | 低 |
悲观锁 | 写多读少 | 中 | 强一致 | 中 |
分布式锁 | 跨服务调用 | 低 | 强一致 | 高 |
技术选型建议:根据业务场景的CAP需求进行权衡,通常可以组合使用多种方案形成多级防护。