CC 咖啡猫的工作空间 Coding Space

应用锁与数据库锁

在并发编程和分布式系统中,锁是保证数据一致性和线程安全的核心机制。正确选择和使用锁对于系统性能和可靠性至关重要。


1. 应用层锁(JVM 内锁)

1.1 synchronized 关键字

// 方法级别锁
public synchronized void method() {
    // 同步方法,锁住当前对象实例
}

// 代码块级别锁
public void method() {
    synchronized(this) {
        // 锁住当前对象实例
    }
    
    synchronized(SomeClass.class) {
        // 锁住类对象(静态锁)
    }
}

特点

  • JVM 内置,自动获取和释放
  • 可重入:同一个线程可以多次获取同一把锁
  • 非公平锁:不保证等待时间最长的线程优先获得锁
  • 性能开销相对较小

1.2 ReentrantLock

private final ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock();
    try {
        // 临界区代码
    } finally {
        lock.unlock(); // 必须在finally中释放
    }
}

// 公平锁
private final ReentrantLock fairLock = new ReentrantLock(true);

优势

  • 可中断:lockInterruptibly() 支持响应中断
  • 可超时:tryLock(timeout, unit) 支持超时获取
  • 可查询:isLocked()getHoldCount() 等方法
  • 公平性:可选择公平或非公平锁

1.3 ReadWriteLock

private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();

public void read() {
    readLock.lock();
    try {
        // 多个读线程可以同时访问
    } finally {
        readLock.unlock();
    }
}

public void write() {
    writeLock.lock();
    try {
        // 写操作独占访问
    } finally {
        writeLock.unlock();
    }
}

适用场景:读多写少的场景,如缓存、配置管理等。

1.4 StampedLock(Java 8+)

private final StampedLock stampedLock = new StampedLock();

public void optimisticRead() {
    long stamp = stampedLock.tryOptimisticRead();
    // 乐观读,假设没有写操作
    if (!stampedLock.validate(stamp)) {
        // 验证失败,升级为悲观读
        stamp = stampedLock.readLock();
        try {
            // 重新读取
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }
}

优势:支持乐观读,性能更好;避免了读写锁的写饥饿问题。


2. 数据库锁

2.1 行级锁(Row-Level Locking)

InnoDB 行锁

  • 基于索引实现,只有通过索引条件检索数据才会使用行锁
  • 如果不走索引,会升级为表锁
  • 支持共享锁(S锁)和排他锁(X锁)
-- 共享锁(读锁)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

-- 排他锁(写锁)
SELECT * FROM users WHERE id = 1 FOR UPDATE;

注意事项

  • 行锁只在事务中生效
  • 范围查询可能导致间隙锁(Gap Lock)
  • 死锁检测和处理需要应用层配合

2.2 表级锁(Table-Level Locking)

MyISAM 表锁

  • 读操作加读锁,写操作加写锁
  • 写锁优先级高于读锁
  • 并发性能较差,但开销小
-- 手动加表锁
LOCK TABLES users READ;
LOCK TABLES users WRITE;
UNLOCK TABLES;

2.3 意向锁(Intention Locks)

InnoDB 的意向锁用于表级和行级锁的协调:

意向锁类型 说明
IS(Intention Shared) 表示事务准备给某些行加共享锁
IX(Intention Exclusive) 表示事务准备给某些行加排他锁

意向锁是表级锁,与行锁兼容,主要用于快速判断表中是否有行被锁定。

2.4 间隙锁(Gap Lock)

防止幻读的锁机制:

-- 假设 users 表中有 id: 1, 3, 5
SELECT * FROM users WHERE id BETWEEN 2 AND 4 FOR UPDATE;
-- 会锁定 (1,3] 和 (3,5) 的间隙,防止插入 id=2,4 的记录

影响

  • REPEATABLE READ 隔离级别下默认启用
  • 可能导致死锁概率增加
  • 可通过设置隔离级别为 READ COMMITTED 来避免

3. 分布式锁

3.1 Redis 分布式锁

基础实现(有问题)

// ❌ 不安全的实现
public boolean tryLock(String key, String value) {
    return redisTemplate.opsForValue().setIfAbsent(key, value);
}

安全实现(Redisson)

// ✅ 使用 Redisson 的 RLock
RLock lock = redisson.getLock("myLock");
try {
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (acquired) {
        // 执行业务逻辑
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redisson 实现原理

  • Lua 脚本保证原子性
  • WatchDog 机制自动续期
  • 可重入锁支持
  • 支持 RedLock 算法(多 Redis 实例)

3.2 ZooKeeper 分布式锁

基于临时顺序节点实现:

// 使用 Curator Framework
InterProcessMutex lock = new InterProcessMutex(client, "/locks/my-lock");
try {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        // 执行业务逻辑
    }
} finally {
    lock.release();
}

实现原理

  • 创建临时顺序节点
  • 判断自己是否是最小序号节点
  • 不是最小则监听前一个节点的删除事件
  • 临时节点保证客户端断开时自动释放锁

3.3 数据库分布式锁

基于唯一约束

-- 创建锁表
CREATE TABLE distributed_lock (
    lock_name VARCHAR(100) PRIMARY KEY,
    lock_value VARCHAR(100) NOT NULL
);

-- 获取锁
INSERT INTO distributed_lock(lock_name, lock_value) VALUES('order_lock', 'client_1');

-- 释放锁  
DELETE FROM distributed_lock WHERE lock_name = 'order_lock';

优点:简单可靠,利用数据库 ACID 特性 缺点:性能较差,无法自动过期

基于版本号

-- 获取锁(带超时)
UPDATE distributed_lock 
SET lock_value = 'client_1', expire_time = NOW() + INTERVAL 30 SECOND 
WHERE lock_name = 'order_lock' AND (expire_time < NOW() OR lock_name IS NULL);

-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND lock_value = 'client_1';

4. 锁的选择策略

4.1 单机应用

  • 简单场景:synchronized
  • 需要高级特性:ReentrantLock
  • 读多写少:ReadWriteLock 或 StampedLock

4.2 分布式应用

场景 推荐方案 理由
高性能要求 Redis 分布式锁 性能好,支持自动过期
强一致性要求 ZooKeeper 分布式锁 顺序一致性,可靠性高
已有数据库 数据库分布式锁 无需引入新组件
跨语言环境 ETCD 分布式锁 gRPC 接口,多语言支持

4.2 性能对比

  • 应用层锁:纳秒级,最快
  • Redis 锁:毫秒级,网络延迟影响
  • ZooKeeper 锁:几十毫秒级,强一致性代价
  • 数据库锁:几十毫秒级,受数据库性能影响

5. 常见问题与最佳实践

5.1 死锁预防

应用层死锁

// ❌ 可能死锁
synchronized(lockA) {
    synchronized(lockB) {
        // ...
    }
}

synchronized(lockB) {
    synchronized(lockA) {
        // ...
    }
}

// ✅ 按固定顺序获取锁
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

// 总是先获取 lock1,再获取 lock2

数据库死锁

  • 尽量按相同顺序访问表和行
  • 减少事务持有锁的时间
  • 使用合适的隔离级别
  • 设置合理的死锁超时时间

5.2 锁粒度控制

  • 粗粒度锁:简单但并发性能差
  • 细粒度锁:并发性能好但复杂度高
  • 分段锁:折中方案,如 ConcurrentHashMap
// 分段锁示例
private final Map<String, Object> segmentLocks = new ConcurrentHashMap<>();

public void process(String key) {
    Object segmentLock = segmentLocks.computeIfAbsent(
        key.substring(0, Math.min(2, key.length())), 
        k -> new Object()
    );
    synchronized(segmentLock) {
        // 处理逻辑
    }
}

5.3 超时与重试

public boolean acquireLockWithRetry(String lockKey, int maxRetries, long retryInterval) {
    for (int i = 0; i < maxRetries; i++) {
        if (tryAcquireLock(lockKey)) {
            return true;
        }
        try {
            Thread.sleep(retryInterval);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    return false;
}

5.4 监控与告警

  • 锁等待时间:监控获取锁的平均等待时间
  • 锁持有时间:监控临界区执行时间
  • 死锁检测:定期检查死锁情况
  • 锁竞争:监控锁的竞争激烈程度

6. 实际应用场景

6.1 库存扣减

// Redis 分布式锁 + 数据库乐观锁
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
    String lockKey = "stock_lock:" + productId;
    RLock lock = redisson.getLock(lockKey);
    
    try {
        if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            // 查询当前库存
            Product product = productMapper.selectById(productId);
            if (product.getStock() >= quantity) {
                // 乐观锁更新
                int updated = productMapper.updateStockWithVersion(
                    productId, quantity, product.getVersion()
                );
                return updated > 0;
            }
        }
    } finally {
        lock.unlock();
    }
    return false;
}

6.2 订单号生成

// 数据库自增 + 应用层缓存
@Component
public class OrderIdGenerator {
    private final AtomicInteger counter = new AtomicInteger(0);
    private volatile long currentBatch = 0L;
    private final Object batchLock = new Object();
    
    public long nextId() {
        int count = counter.getAndIncrement();
        if (count % 1000 == 0) { // 每1000个ID重新获取批次
            synchronized(batchLock) {
                if (counter.get() % 1000 == 0) {
                    currentBatch = fetchNextBatchFromDB();
                    counter.set(1);
                }
            }
        }
        return currentBatch + count % 1000;
    }
}

6.3 缓存更新

// 双重检查锁 + 本地缓存
private final Map<String, Object> localCache = new ConcurrentHashMap<>();
private final Map<String, Object> cacheLocks = new ConcurrentHashMap<>();

public Object getData(String key) {
    Object data = localCache.get(key);
    if (data == null) {
        Object lock = cacheLocks.computeIfAbsent(key, k -> new Object());
        synchronized(lock) {
            data = localCache.get(key);
            if (data == null) {
                data = loadFromRemote(key);
                localCache.put(key, data);
            }
        }
    }
    return data;
}

总结:锁的选择需要根据具体场景权衡性能、一致性和复杂度。单机场景优先考虑 JVM 内置锁,分布式场景根据一致性要求选择合适的分布式锁实现。无论哪种锁,都要注意死锁预防、超时处理和监控告警。