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
}

实现原理

  1. 首次读取时获取version值(如version=1)
  2. 更新时自动附加条件WHERE id=? AND version=1
  3. 若version已被修改,抛出OptimisticLockException

实践建议

  • 版本字段推荐使用IntegerLong类型
  • 结合@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. 死锁检测与避免

常见死锁场景

图1

解决方案

  1. 统一资源获取顺序(如按ID排序后加锁)
  2. 设置锁超时时间:

    SET innodb_lock_wait_timeout = 5;  -- 超时5秒自动释放
  3. 使用死锁检测工具:

    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();
}

算法要点

  1. 获取当前毫秒级时间戳T1
  2. 依次尝试从N/2+1个独立Redis节点获取锁
  3. 计算获取锁总耗时,若小于锁有效期且获得多数锁则成功
  4. 实际持有时间 = 初始设置时间 - 获取锁耗时

2. Zookeeper分布式锁

// Curator框架实现
InterProcessMutex lock = new InterProcessMutex(client, "/locks/account1");
try {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        // 执行业务逻辑
    }
} finally {
    lock.release();
}

实现原理

  1. 创建临时顺序节点(如/locks/account1/_c_123
  2. 检查自己是否是最小序号节点
  3. 如果不是,监听前一个节点的删除事件
  4. 获得锁后执行业务,完成后自动删除节点

对比分析

特性Redis RedLockZookeeper
性能高(内存操作)中(需要节点通信)
可靠性依赖时钟一致性基于CP模型更可靠
实现复杂度中等(需处理多个实例)高(需处理Watcher事件)
适用场景高性能要求的短期锁需要高可靠性的长期锁

四、最佳实践总结

  1. 选型建议

    • 单机环境优先考虑JVM锁(synchronized/ReentrantLock)
    • 集群环境根据CAP需求选择:

      • 高性能选Redis(AP)
      • 强一致选Zookeeper(CP)
  2. 事务与锁的配合

    @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);
        }
    }
  3. 监控指标

    • 锁等待时间
    • 锁持有时间
    • 死锁发生次数
    • 锁获取失败率

通过合理选择锁策略和事务隔离级别,可以在并发性能和数据一致性之间取得最佳平衡。建议在预发布环境进行充分的并发测试,使用Arthas等工具观察锁竞争情况。

添加新评论