Java事务与并发控制实战指南

一、乐观锁实现方案

乐观锁假设并发冲突概率低,通过版本控制实现无锁并发,适合读多写少场景。

1. 版本号机制(@Version)

@Entity
public class Product {
    @Id
    private Long id;
    private String name;
    private Integer stock;
    
    @Version  // 关键注解
    private Integer version;
    // getters/setters...
}

工作原理

  1. 读取数据时获取当前版本号
  2. 更新时检查版本号是否变化
  3. 若版本一致则更新成功并递增版本号
  4. 若版本不一致抛出OptimisticLockException

实践建议

  • 结合Spring Data JPA使用最简便
  • 版本字段建议用包装类型(允许null)
  • 捕获异常后应提供友好重试机制

2. CAS操作

AtomicInteger counter = new AtomicInteger(0);

// 典型CAS操作
boolean success = counter.compareAndSet(expect, update);

适用场景

  • 简单计数器场景
  • 无状态并发控制
  • JDK原子类底层实现

性能对比

图1

二、悲观锁应用场景

悲观锁假定并发冲突必然发生,适合写多读少或强一致性要求的场景。

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) { ... }
}

解决方案

  1. 统一资源获取顺序
  2. 设置锁超时时间(tryLock
  3. 使用jstack分析线程转储

实践建议

  • 生产环境建议添加innodb_deadlock_detect = ON
  • 事务应尽量短小精悍
  • 避免在事务中包含RPC调用

三、分布式锁与事务结合

1. Redis RedLock算法

实现步骤

  1. 获取当前毫秒级时间戳T1
  2. 轮流在N个Redis节点执行SETNX命令
  3. 计算获取锁总耗时(T2-T1)
  4. 当且仅当在多数节点获取成功且耗时小于锁有效期时认为成功
// Redisson实现示例
RLock lock = redisson.getLock("orderLock");
try {
    if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
        // 业务处理
    }
} finally {
    lock.unlock();
}

2. Zookeeper分布式锁

临时顺序节点方案

图2

实践建议

  • 推荐使用Curator框架的InterProcessMutex
  • 注意处理session过期导致的临时节点消失
  • 设置合理的重试策略和超时时间

四、混合方案最佳实践

电商库存扣减示例

  1. 前端:令牌防重+滑动窗口限流
  2. 网关层:Redis漏桶算法限流
  3. 服务层:本地锁+Redis分布式锁双重校验
  4. 数据层:最终通过数据库乐观锁保证一致性

性能优化技巧

  • 热点数据采用缓存+异步刷盘
  • 合并短事务为批量操作
  • 对锁进行分段处理(如ConcurrentHashMap的分段锁思想)

总结对比

方案类型适用场景性能一致性强度实现复杂度
乐观锁读多写少最终一致
悲观锁写多读少强一致
分布式锁跨服务调用强一致

技术选型建议:根据业务场景的CAP需求进行权衡,通常可以组合使用多种方案形成多级防护。

添加新评论