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

分布式锁是构建分布式系统时确保数据一致性的关键组件。本文将深入分析四种主流实现方案的技术细节、适用场景及最佳实践。

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

1. 唯一索引/主键冲突

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

CREATE TABLE distributed_lock (
    lock_name VARCHAR(64) PRIMARY KEY,
    expire_time DATETIME
);

加锁操作

public boolean tryLock(String lockName, long expireSeconds) {
    try {
        return jdbcTemplate.update(
            "INSERT INTO distributed_lock VALUES (?, DATE_ADD(NOW(), INTERVAL ? SECOND))",
            lockName, expireSeconds) > 0;
    } catch (DuplicateKeyException e) {
        return false;
    }
}

实践建议

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

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

实现模式

图1

典型代码

public boolean optimisticLock(String resourceId) {
    int version = getCurrentVersion(resourceId);
    return jdbcTemplate.update(
        "UPDATE resources SET version = version + 1 WHERE id = ? AND version = ?",
        resourceId, version) > 0;
}

3. 悲观锁(SELECT FOR UPDATE)

事务中使用方式

@Transactional
public void processWithPessimisticLock(Long id) {
    // 获取行锁
    Resource res = jdbcTemplate.queryForObject(
        "SELECT * FROM resources WHERE id = ? FOR UPDATE", 
        rowMapper, id);
    // 业务处理
    updateResource(res);
} // 事务提交自动释放锁

注意事项

  • MySQL需要InnoDB引擎支持
  • 可能引发死锁,需设置事务超时
  • 高并发下性能较差

二、基于Redis的实现方案

1. SETNX + 过期时间

基础实现

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

关键问题

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

2. RedLock算法

实现步骤

  1. 获取当前毫秒级时间戳
  2. 依次尝试从N个独立Redis实例获取锁
  3. 计算获取锁总耗时,小于锁过期时间且成功获取多数节点锁时视为成功
  4. 若失败,向所有节点发起释放锁请求

图2

3. Redisson最佳实践

典型用法

RLock lock = redisson.getLock("orderLock");
try {
    // 支持看门狗自动续期
    if (lock.tryLock(10, 60, TimeUnit.SECONDS)) {
        // 业务处理
    }
} finally {
    lock.unlock();
}

核心特性

  • 默认30秒过期,每10秒自动续期(看门狗机制)
  • 提供可重入锁、公平锁等多种锁类型
  • 支持联锁(MultiLock)和红锁(RedLock)

三、基于ZooKeeper的实现方案

1. 临时顺序节点实现

Curator框架实现

InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
public void process() throws Exception {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        try {
            // 临界区代码
        } finally {
            lock.release();
        }
    }
}

ZK节点结构

/locks
  /order
    /_c_8623f816-634c-45d1-8a8f-27f9c4e8e314-lock-0000000000
    /_c_9237h291-283x-56k2-9d3f-38f7d5c2e1f6-lock-0000000001

2. Watcher监听机制

工作原理

  1. 客户端创建临时顺序节点
  2. 检查自己是否是最小序号节点
  3. 如果不是,则监听前一个节点的删除事件
  4. 前驱节点删除后重新检查序号

图3

四、其他中间件方案

1. Etcd实现方案

基于租约的锁

// 创建租约(TTL 30秒)
resp, err := client.Grant(context.TODO(), 30)
// 创建事务比较
cmp := v3.Compare(v3.CreateRevision(key), "=", 0)
put := v3.OpPut(key, "", v3.WithLease(resp.ID))
get := v3.OpGet(key)
// 执行事务
txnResp, err := client.Txn(context.TODO()).If(cmp).Then(put, get).Else(get).Commit()

2. Consul实现方案

Session机制关键步骤

# 创建Session(TTL 30s)
SESSION_ID=$(curl -X PUT -d '{"Name": "order_lock", "TTL": "30s"}' \
  http://localhost:8500/v1/session/create | jq -r '.ID')

# 获取锁(KV存储)
curl -X PUT -d "locked" \
  http://localhost:8500/v1/kv/locks/order?acquire=$SESSION_ID

五、方案选型对比

特性数据库RedisZooKeeperEtcd
性能低(100-500QPS)高(10,000+QPS)中(1000-5000QPS)高(5000+QPS)
一致性保证强一致最终一致强一致强一致
实现复杂度简单中等复杂中等
典型适用场景传统数据库系统高并发缓存系统配置中心服务发现

实践建议

  1. 已有Redis集群时优先考虑Redisson方案
  2. 强一致性要求场景选择ZooKeeper或Etcd
  3. 数据库方案仅作为兜底方案使用
  4. 跨机房部署需特别注意网络分区问题

六、常见问题解决方案

锁续期问题

  • 实现模式:

    private void scheduleExpirationRenewal(final String threadId) {
      if (expirationRenewalMap.containsKey(getEntryName())) {
          return;
      }
      
      Timeout task = commandExecutor.getConnectionManager()
          .newTimeout(new TimerTask() {
              public void run(Timeout timeout) {
                  // 执行Lua脚本续期
                  renewExpirationAsync(threadId);
              }
          }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    }

锁重入实现

-- Redisson重入锁Lua脚本
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);

通过深入理解各种分布式锁的实现原理和适用场景,开发者可以根据实际业务需求选择最适合的技术方案,确保分布式系统的数据一致性和高可用性。

添加新评论