Java事务与并发控制:乐观锁到分布式锁实践
Java事务与并发控制深度解析:从乐观锁到分布式锁实践
一、乐观锁实现方案
乐观锁假设并发冲突概率低,通过版本控制实现无锁并发,适合读多写少场景。
1. 版本号机制(@Version)
@Entity
public class Product {
@Id
private Long id;
private String name;
private Integer stock;
@Version // 关键注解
private Integer version;
// getters/setters...
}
// 使用示例
@Transactional
public void updateProduct(Long id, String newName) {
Product product = entityManager.find(Product.class, id);
product.setName(newName); // 提交时会自动检查version
}
实现原理:
- 首次读取时获取version值(如version=1)
- 更新时自动附加条件
WHERE id=? AND version=1
- 若version已被修改,抛出
OptimisticLockException
实践建议:
- 版本字段推荐使用
Integer
或Long
类型 - 结合
@Retryable
注解实现自动重试 - 避免在长事务中使用(version可能过期)
2. CAS操作
Compare-And-Swap是CPU原子指令,Java通过Unsafe类提供支持:
// AtomicInteger内部实现
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 手动实现库存扣减
public boolean deductStock(Long productId, int quantity) {
Product product = productDao.selectById(productId);
int oldStock = product.getStock();
if (oldStock < quantity) {
return false;
}
int newStock = oldStock - quantity;
// CAS操作:比较并交换
return productDao.updateStock(productId, oldStock, newStock) > 0;
}
适用场景对比:
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
版本号机制 | 实现简单,与ORM集成好 | 需额外字段 | 数据库实体更新 |
CAS操作 | 无额外字段,性能高 | 需自行处理ABA问题 | 简单数值类操作 |
二、悲观锁应用场景
悲观锁假定并发冲突高,提前获取锁保证独占访问。
1. SELECT FOR UPDATE
-- MySQL行锁示例
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 执行余额操作...
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
Java实现方式:
// JPA实现
@Lock(LockModeType.PESSIMISTIC_WRITE) // 相当于FOR UPDATE
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdForUpdate(@Param("id") Long id);
// MyBatis实现
@Select("SELECT * FROM account WHERE id = #{id} FOR UPDATE")
Account selectForUpdate(Long id);
锁升级问题:
- InnoDB在无索引字段上锁会升级为表锁
- 推荐始终通过主键或唯一索引锁定
2. 死锁检测与避免
常见死锁场景:
解决方案:
- 统一资源获取顺序(如按ID排序后加锁)
设置锁超时时间:
SET innodb_lock_wait_timeout = 5; -- 超时5秒自动释放
使用死锁检测工具:
SHOW ENGINE INNODB STATUS; -- 查看最近死锁信息
实践建议:
- 事务尽可能短小
- 避免交叉申请多个锁
- 考虑使用
SELECT ... FOR UPDATE NOWAIT
(Oracle/PostgreSQL支持)
三、分布式锁与事务结合
1. Redis RedLock算法
RedLock是Redis官方推荐的分布式锁算法:
// Redisson实现
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
// 尝试获取锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
boolean locked = redLock.tryLock(
10, // 等待时间
30, // 锁持有时间
TimeUnit.SECONDS);
if (locked) {
// 执行业务逻辑
}
} finally {
redLock.unlock();
}
算法要点:
- 获取当前毫秒级时间戳T1
- 依次尝试从N/2+1个独立Redis节点获取锁
- 计算获取锁总耗时,若小于锁有效期且获得多数锁则成功
- 实际持有时间 = 初始设置时间 - 获取锁耗时
2. Zookeeper分布式锁
// Curator框架实现
InterProcessMutex lock = new InterProcessMutex(client, "/locks/account1");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
lock.release();
}
实现原理:
- 创建临时顺序节点(如
/locks/account1/_c_123
) - 检查自己是否是最小序号节点
- 如果不是,监听前一个节点的删除事件
- 获得锁后执行业务,完成后自动删除节点
对比分析:
特性 | Redis RedLock | Zookeeper |
---|---|---|
性能 | 高(内存操作) | 中(需要节点通信) |
可靠性 | 依赖时钟一致性 | 基于CP模型更可靠 |
实现复杂度 | 中等(需处理多个实例) | 高(需处理Watcher事件) |
适用场景 | 高性能要求的短期锁 | 需要高可靠性的长期锁 |
四、最佳实践总结
选型建议:
- 单机环境优先考虑JVM锁(synchronized/ReentrantLock)
集群环境根据CAP需求选择:
- 高性能选Redis(AP)
- 强一致选Zookeeper(CP)
事务与锁的配合:
@Transactional public void businessMethod() { // 先获取分布式锁 String lockKey = "order_" + orderId; try { boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS); if (!locked) throw new BusinessException("操作频繁"); // 再查询数据库(避免幻读) Order order = orderDao.selectForUpdate(orderId); // 业务处理... } finally { redisLock.unlock(lockKey); } }
监控指标:
- 锁等待时间
- 锁持有时间
- 死锁发生次数
- 锁获取失败率
通过合理选择锁策略和事务隔离级别,可以在并发性能和数据一致性之间取得最佳平衡。建议在预发布环境进行充分的并发测试,使用Arthas等工具观察锁竞争情况。