分布式锁主流实现方案深度解析

分布式锁是分布式系统中保证资源互斥访问的核心机制。本文将深入分析四种主流实现方案的技术原理、适用场景及最佳实践。

一、基于数据库的实现方案

1. 唯一索引/主键冲突

实现原理:利用数据库唯一约束特性,通过插入相同键值实现锁竞争。

CREATE TABLE distributed_lock (
    lock_key VARCHAR(64) PRIMARY KEY,
    expire_time DATETIME NOT NULL
);

加锁操作

INSERT INTO distributed_lock(lock_key, expire_time) 
VALUES ('order_lock', NOW() + INTERVAL 30 SECOND);

解锁操作

DELETE FROM distributed_lock WHERE lock_key = 'order_lock';

实践建议

  • 必须设置过期时间避免死锁
  • 添加定时任务清理过期锁
  • 适合并发量较低的场景(QPS < 500)

2. 乐观锁(版本号机制)

实现原理:通过版本号或时间戳实现CAS操作。

UPDATE resource_table 
SET resource = new_value, version = version + 1 
WHERE id = #{id} AND version = #{old_version};

适用场景

  • 读多写少环境
  • 冲突概率低的业务场景

3. 悲观锁(SELECT FOR UPDATE)

实现原理:利用数据库行锁机制阻塞其他事务。

// 事务方法
@Transactional
public void updateWithLock(Long id) {
    // 获取行锁
    Resource res = jdbcTemplate.queryForObject(
        "SELECT * FROM resources WHERE id = ? FOR UPDATE", 
        rowMapper, id);
    // 业务处理
    updateResource(res);
}

注意事项

  • 必须与事务注解@Transactional配合使用
  • 可能导致死锁,需设置超时时间
  • 高并发场景下性能较差

二、基于Redis的实现方案

1. SETNX + 过期时间

基础实现

public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    return "OK".equals(result);
}

关键问题

  • 非原子性操作风险(SETNX + EXPIRE应使用Lua脚本)
  • 误删其他客户端锁(需验证requestId)

2. RedLock算法

实现步骤

  1. 获取当前毫秒级时间戳
  2. 依次向N个Redis节点申请锁
  3. 当获得多数节点锁时,计算获取锁总耗时
  4. 若总耗时小于锁有效期,则获取成功

图1

实践建议

  • 至少部署5个Redis主节点
  • 网络延迟需稳定在锁有效期的1/3以内
  • 官方建议仅在没有其他方案时使用

3. Redisson实现

核心特性

  • Watchdog自动续期机制
  • 可重入锁支持
  • 多种锁类型(公平锁、联锁等)
// 获取锁
RLock lock = redisson.getLock("orderLock");
try {
    // 尝试加锁,最多等待100秒,上锁后30秒自动解锁
    boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
    if (res) {
        // 业务逻辑
    }
} finally {
    lock.unlock();
}

三、基于ZooKeeper的实现方案

1. 临时顺序节点

实现原理

  1. 在锁节点下创建临时顺序节点
  2. 获取所有子节点并排序
  3. 判断自己是否是最小节点
  4. 若不是则监听前一个节点

图2

Curator实现示例

InterProcessMutex lock = new InterProcessMutex(client, "/order_lock");
try {
    if (lock.acquire(30, TimeUnit.SECONDS)) {
        // 业务处理
    }
} finally {
    lock.release();
}

2. 对比优势

特性Redis方案ZK方案
性能
可靠性依赖持久化
实现复杂度简单复杂
锁自动释放依赖TTL会话结束自动释放
公平锁需额外实现原生支持

四、其他中间件方案

1. Etcd实现

核心机制

# 获取锁(租约10秒)
etcdctl lease grant 10
# 写入键值(关联租约ID)
etcdctl put --lease=1234abcd /lock/resource1 client1

特点

  • 基于Raft协议强一致性
  • 租约自动过期
  • 适合Kubernetes环境

2. Consul实现

Session机制

// 创建Session(TTL 10秒)
String sessionId = consulClient.sessionCreate(
    NewSession.request().ttl("10s"));

// 获取锁
boolean locked = consulClient.setKVValue(
    "lock/resource1", "client1", 
    new QueryParams().acquire(sessionId));

五、选型建议

  1. 简单场景:Redis SETNX(需处理续期问题)
  2. 高一致性要求:ZooKeeper/Etcd
  3. 云原生环境:Etcd/Consul
  4. 已有技术栈:优先选择团队熟悉的方案

性能对比

  • Redis:10,000+ QPS
  • ZooKeeper:1,000-5,000 QPS
  • 数据库:500-1,000 QPS

六、常见陷阱

  1. 锁续期问题:业务执行时间超过锁有效期

    • 解决方案:Watchdog机制或合理设置超时
  2. 锁误释放:客户端A释放了客户端B的锁

    • 解决方案:每个客户端设置唯一标识
  3. GC停顿导致锁失效

    • 解决方案:ZK通过会话检测,Redis需额外处理
  4. 时钟漂移问题

    • 解决方案:避免依赖多节点时间,使用TTL机制

最佳实践

  1. 始终设置锁超时时间
  2. 实现锁的可重入性
  3. 添加锁获取失败的重试机制
  4. 记录锁竞争情况监控
  5. 考虑锁降级策略(如从分布式锁降级到本地锁)
// 锁模板示例
public <T> T executeWithLock(String lockKey, int waitTime, int leaseTime, Supplier<T> supplier) {
    RLock lock = redisson.getLock(lockKey);
    try {
        if (lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) {
            return supplier.get();
        }
        throw new LockAcquireException("Get lock failed");
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

通过深入理解各方案的实现原理和适用场景,开发者可以构建出既可靠又高效的分布式锁体系。

添加新评论