Java并发控制:乐观锁与悲观锁深度解析

一、乐观锁实现方案

乐观锁假设并发冲突概率低,采用"先修改再验证"的策略,适合读多写少场景。

1. 版本号机制(@Version)

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

实现原理

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

图1

实践建议

  • 适合电商库存、订单状态等低频修改场景
  • 结合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;

解决方案

  1. 统一资源访问顺序
  2. 设置锁超时:innodb_lock_wait_timeout
  3. 死锁检测:SHOW ENGINE INNODB STATUS
  4. 应用层实现全局锁排序

图2

三、分布式锁与事务结合

1. Redis RedLock算法

实现步骤

  1. 获取当前毫秒时间戳T1
  2. 依次向N个Redis节点请求锁(SET NX PX)
  3. 计算获取锁耗时=当前时间T2-T1
  4. 当且仅当获得多数节点(N/2+1)认可且耗时小于锁有效期时才认为成功
  5. 实际有效时间 = 原设置时间 - 获取锁耗时
// 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分布式锁

临时顺序节点实现

图3

实践建议

  • 使用Curator的InterProcessMutex
  • 设置合理的sessionTimeout(建议10-30秒)
  • 实现锁的自动续期机制
// Curator实现示例
InterProcessMutex lock = new InterProcessMutex(client, "/transaction/lock");
try {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        // 执行业务逻辑
    }
} finally {
    lock.release();
}

四、选型决策树

图4

性能对比指标

方案TPS延迟一致性实现复杂度
乐观锁10k+1-5ms最终
SELECT FOR UPDATE3k10-50ms
Redis锁8k5-20ms
Zookeeper锁2k50-100ms

五、最佳实践

  1. 混合锁策略:读操作用乐观锁,写操作用悲观锁
  2. 锁粒度控制:库存系统按SKU维度加锁而非整个仓库
  3. 事务拆分:长事务拆分为多个短事务+本地状态
  4. 降级方案:分布式锁失败时降级为本地锁+补偿机制
  5. 监控指标

    • 锁等待时间
    • 死锁次数
    • 锁获取成功率
// 混合锁示例
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();
        }
    }
}

添加新评论