CC 咖啡猫的工作空间 Coding Space

分布式锁实践

审校日期:2026-09-05。分布式锁协调遵循相同协议的参与者;租约失效、故障转移和旧持有者继续执行都需要额外处理。Java 片段未在完整依赖工程编译,API 用法按下方官方文档核对。


1. 分布式锁要素

要素 说明 必须
互斥 任意时刻只有一个客户端能持有锁
防死锁 锁到期自动释放或持有者异常时能释放
可重入 同一线程可多次获取同一锁 通常
高性能 加锁/解锁快,高并发下无瓶颈
高可用 在选定容错假设内可用;依赖多数派的系统失去多数后通常不能继续授予锁

2. Redis 分布式锁

2.1 基础实现(SETNX)

public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 简单实现(有问题,不推荐直接用)
    public boolean lock(String key, String value, long expireTime) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(key, value, Duration.ofSeconds(expireTime))
        );
    }

    public void unlock(String key, String value) {
        // 问题:不是原子操作,可能误删他人的锁
        if (value.equals(redisTemplate.opsForValue().get(key))) {
            redisTemplate.delete(key);
        }
    }
}

2.2 Lua 原子解锁

// 解锁 Lua 脚本(原子操作)
private static final String UNLOCK_SCRIPT =
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "    return redis.call('del', KEYS[1]) " +
    "else " +
    "    return 0 " +
    "end";

public void unlock(String key, String value) {
    redisTemplate.execute(
        new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
        Collections.singletonList(key),
        value
    );
}

2.3 Redisson API 用法

@Service
public class DistLockService {

    @Autowired
    private RedissonClient redisson;

    // 简单获取锁
    public void lock(String resourceCode) {
        RLock lock = redisson.getLock(resourceCode);
        lock.lock();  // 未指定租约时由 watchdog 续期,超时配置默认 30 秒
    }

    // 带过期时间
    public boolean tryLock(String resourceCode, long waitTime, long leaseTime) throws InterruptedException {
        RLock lock = redisson.getLock(resourceCode);
        return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
    }

    // 释放锁
    public void unlock(String resourceCode) {
        RLock lock = redisson.getLock(resourceCode);
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }

    // 公平锁(按请求顺序获取)
    public void fairLock(String resourceCode) {
        RLock lock = redisson.getFairLock(resourceCode);
        lock.lock();
    }

    // 读锁(多线程可并发读)
    public void readLock(String resourceCode) {
        RLock lock = redisson.getReadLock(resourceCode);
        lock.lock();
    }

    // 写锁(独占)
    public void writeLock(String resourceCode) {
        RLock lock = redisson.getWriteLock(resourceCode);
        lock.lock();
    }
}

2.4 Redisson 使用示例

@Service
public class OrderService {

    @Autowired
    private DistLockService distLockService;

    public void createOrder(Order order) throws InterruptedException {
        String lockKey = "order:create:" + order.getUserId();

        // 尝试获取锁,最多等3秒,锁自动10秒后过期
        boolean locked = distLockService.tryLock(lockKey, 3, 10);
        if (!locked) {
            throw new BusinessException("系统繁忙,请稍后重试");
        }

        try {
            // 业务逻辑
            checkInventory(order);
            deductInventory(order);
            saveOrder(order);
        } finally {
            // 与加锁在同一线程释放;固定租约可能已过期
            distLockService.unlock(lockKey);
        }
    }
}

3. ZooKeeper 分布式锁

3.1 Curator 实现

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.5.0</version>
</dependency>
// 调用方启动 Curator client,并按资源创建、复用 InterProcessMutex。
// 应用关闭时关闭 client;同一线程、同一对象完成 acquire/release。
public void withLock(InterProcessMutex lock) throws Exception {
    boolean acquired = lock.acquire(30, TimeUnit.SECONDS);
    if (!acquired) {
        throw new IllegalStateException("获取锁超时");
    }
    try {
        // 执行业务;同时处理连接 SUSPENDED/LOST 对所有权的影响。
    } finally {
        lock.release();
    }
}

不能在 unlock 时重新 new 一个 InterProcessMutex;新对象没有原对象的线程持有状态。应用还需监听连接状态:连接挂起时不能假定仍安全持锁,会话丢失时应停止依赖旧锁的操作。


4. 对比与选型

方案 优点 缺点 适用场景
Redis SETNX 性能高、部署简单 过期难精确、集群脑裂 短暂锁、并发量大的场景
Redisson 功能完善、可重入、续期 需引入 client 需要 Redis 锁 API 与续期管理的场景
ZooKeeper 有序锁、临时节点、自动释放 性能略低、运维成本 需要严格顺序的场景
Etcd Raft 一致性、 Lease 机制 生态不如 ZK Kubernetes 生态

5. 常见问题

5.1 锁过期,业务未完成

问题:线程A获取锁后业务执行过慢,锁过期自动释放,线程B获取同一把锁,两个线程同时执行。

缓解:未显式指定 leaseTime 时使用 watchdog 续期;指定固定租约时到期会释放。续期受进程暂停、网络和调度影响,不能保证业务不会越过租约继续运行。

lock.lock(); // 由 watchdog 管理续期
try {
    // 业务操作
} finally {
    if (lock.isHeldByCurrentThread()) lock.unlock();
}

如果旧持有者可能写外部资源,应由资源侧原子校验 fencing token 或业务版本,拒绝落后的持有者。仅检查锁归属再写资源仍有竞态窗口。

5.2 主从切换丢锁

异步复制可能使新主节点缺失锁记录。不能通过同一 Redisson 客户端上的三个不同 key 模拟三个独立 Redis 实例。

Redlock 的多数条件是至少 floor(N / 2) + 1 个独立实例成功,还需检查获取耗时与剩余有效期;并非只数成功次数。Redisson 当前文档将 RedLock 标为弃用,不应把旧版 getRedLock 示例作为通用解决办法。应结合部署拓扑、复制确认与资源侧 fencing 设计故障处理。

5.3 锁重入问题

问题:同一线程多次获取同一把锁,需要记录持有次数。

解决:Redis 用计数器 + Lua 脚本;Redisson 内置可重入。

// Redis 可重入实现
private static final String REENTRANT_LOCK_SCRIPT =
    "if redis.call('exists', KEYS[1]) == 0 then " +
    "    redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    "    redis.call('pexpire', KEYS[1], ARGV[2]) " +
    "    return 1 " +
    "elseif redis.call('hget', KEYS[1], ARGV[1]) ~= false then " +
    "    redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    "    redis.call('pexpire', KEYS[1], ARGV[2]) " +
    "    return 1 " +
    "else " +
    "    return 0 " +
    "end";

5.4 数据库约束与条件更新

唯一索引用于业务身份去重;下例条件更新用于防止库存不足时扣减,两者职责不同。count 必须先校验为正整数。

// 原子条件更新(库存扣减),没有显式版本号
@Update("UPDATE product SET stock = stock - #{count} " +
       "WHERE id = #{id} AND stock >= #{count}")
int deductStock(@Param("id") Long id, @Param("count") int count);
// 影响行数 0 表示商品不存在或库存不足;不证明本次请求已经执行

// 或者悲观锁(SELECT FOR UPDATE)
@Transactional
public void createOrder(Long productId) {
    Product p = productMapper.selectByIdForUpdate(productId);
    if (p.getStock() < 1) throw new BusinessException("库存不足");
    p.setStock(p.getStock() - 1);
    productMapper.updateById(p);
}

6. 使用原则

  1. 锁覆盖完整临界区:若正确性依赖数据库提交,释放必须在提交之后;在此约束下缩小范围
  2. 加锁失败处理:必须考虑超时,快速失败,不要无限等待
  3. 必须解锁:finally 中解锁,确保无论成功失败都释放
  4. 锁的粒度:按业务 ID 锁(userId、orderId),不要按通用 key
  5. 避免嵌套锁:跨服务调用时不要在内部锁住,易死锁

7. 可复现案例与来源

运行 node --test examples/consistency.test.js,其中两个测试展示旧租约持有者的迟到写入,以及资源侧 fencing 拒绝旧 token。案例是确定性内存模型,不代表已完成 Redis/ZooKeeper 故障演练。详见 并发一致性实验