分布式锁方案对比
约 2067 字大约 7 分钟
distributedlock
2025-06-07
概述
分布式锁是分布式系统中控制对共享资源互斥访问的基本工具。与单机锁(如Java的synchronized或ReentrantLock)不同,分布式锁需要跨进程、跨机器协调。本文对比分析基于Redis、ZooKeeper、etcd和MySQL的分布式锁方案,包括各自的实现原理、优缺点和适用场景。
分布式锁的基本要求
Redis分布式锁
基础方案:SETNX + 过期时间
public class RedisDistributedLock {
private final StringRedisTemplate redis;
private final String lockKey;
private final String lockValue; // 唯一标识(UUID),防止误删
private final long expireTime;
public boolean tryLock() {
// SET key value NX PX milliseconds(原子操作)
Boolean result = redis.opsForValue().setIfAbsent(
lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock() {
// 使用Lua脚本保证原子性:检查值匹配后再删除
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redis.execute(new DefaultRedisScript<>(luaScript, Long.class),
List.of(lockKey), lockValue);
}
}为什么需要Lua脚本解锁?
Redisson实现
Redisson是Redis的Java客户端,提供了功能完善的分布式锁实现。
// Redisson分布式锁
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 可重入锁
RLock lock = redisson.getLock("orderLock");
try {
// 尝试获取锁,等待10秒,锁自动释放时间30秒
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
// 执行业务逻辑
processOrder();
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}Redisson的看门狗(Watchdog)机制会自动续期锁的过期时间,防止业务执行时间超过锁有效期:
RedLock算法
Redis单节点锁在主从切换时可能失效。Redisson提供了RedLock算法(由Redis作者Antirez提出),使用多个独立的Redis主节点:
// RedLock:需要获取多数节点的锁
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// 需要获取 3/2+1 = 2 个节点的锁才算成功
redLock.lock();
try {
// 执行业务
} finally {
redLock.unlock();
}注意: Martin Kleppmann对RedLock的安全性提出了质疑(文章《How to do distributed locking》),认为在GC暂停、时钟漂移等情况下RedLock可能失效。详见Fencing Token部分。
ZooKeeper分布式锁
利用ZooKeeper的临时顺序节点(Ephemeral Sequential Node)实现分布式锁。
// 使用Apache Curator实现(推荐)
public class ZookeeperLockExample {
private final CuratorFramework client;
public void executeWithLock() throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
processOrder();
} finally {
lock.release();
}
}
} catch (Exception e) {
throw new RuntimeException("Lock acquisition failed", e);
}
}
// 可重入读写锁
public void executeWithReadWriteLock() throws Exception {
InterProcessReadWriteLock rwLock =
new InterProcessReadWriteLock(client, "/locks/config");
// 读锁(多个客户端可同时获取)
InterProcessMutex readLock = rwLock.readLock();
readLock.acquire();
try {
readConfig();
} finally {
readLock.release();
}
// 写锁(独占)
InterProcessMutex writeLock = rwLock.writeLock();
writeLock.acquire();
try {
updateConfig();
} finally {
writeLock.release();
}
}
}优点
- 天然公平锁:顺序节点保证了先到先得
- 无死锁:临时节点在会话断开后自动删除
- 可重入:Curator的InterProcessMutex支持可重入
- 强一致性:基于ZAB协议
缺点
- 性能较低:每次加锁/解锁需要创建/删除ZNode + 通知Watcher
- 运维复杂:需要维护ZooKeeper集群
etcd分布式锁
etcd使用Lease(租约)和Revision(版本号)实现分布式锁。
import (
"context"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
)
func acquireLock() {
client, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"etcd1:2379", "etcd2:2379"},
})
defer client.Close()
// 创建Session(类似ZK的临时节点,基于Lease自动续期)
session, _ := concurrency.NewSession(client, concurrency.WithTTL(10))
defer session.Close()
// 创建互斥锁
mutex := concurrency.NewMutex(session, "/locks/order/")
// 获取锁(阻塞直到获取成功)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := mutex.Lock(ctx); err != nil {
log.Fatal("Failed to acquire lock:", err)
}
// 执行业务逻辑
processOrder()
// 释放锁
mutex.Unlock(context.Background())
}原理:
- 创建一个Lease(类似ZK的Session)
- 使用
PUT /locks/order/<lease-id>创建Key(绑定Lease) - 通过比较所有
/locks/order/前缀Key的Revision,Revision最小的获得锁 - 其他客户端Watch前一个Revision的Key,实现公平等待
MySQL分布式锁
利用数据库的唯一约束或FOR UPDATE实现。
-- 方案1:唯一约束
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) PRIMARY KEY,
lock_owner VARCHAR(128) NOT NULL,
expire_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 加锁(INSERT成功即获取到锁)
INSERT INTO distributed_lock (lock_name, lock_owner, expire_at)
VALUES ('order_lock', 'host1-thread1-uuid', NOW() + INTERVAL 30 SECOND);
-- 解锁
DELETE FROM distributed_lock
WHERE lock_name = 'order_lock' AND lock_owner = 'host1-thread1-uuid';
-- 方案2:FOR UPDATE(悲观锁)
BEGIN;
SELECT * FROM distributed_lock WHERE lock_name = 'order_lock' FOR UPDATE;
-- 获取到锁后执行业务逻辑
COMMIT; -- 提交事务,释放锁方案对比
| 特性 | Redis (Redisson) | ZooKeeper (Curator) | etcd | MySQL |
|---|---|---|---|---|
| 性能 | 极高(~10万/s) | 中(~1万/s) | 高(~5万/s) | 低(~1千/s) |
| 可靠性 | 中(主从切换风险) | 高(ZAB协议) | 高(Raft协议) | 中(单点) |
| 公平性 | 无(需额外实现) | 天然公平 | 天然公平 | 取决于实现 |
| 可重入 | 支持 | 支持 | 支持 | 需自实现 |
| 自动释放 | 过期时间+Watchdog | 临时节点 | Lease | 需定时清理 |
| 实现复杂度 | 低(Redisson封装好) | 低(Curator封装好) | 低 | 中 |
| 额外依赖 | Redis | ZooKeeper集群 | etcd集群 | 数据库 |
Fencing Token
Martin Kleppmann提出的安全模型:即使分布式锁失效(如GC暂停导致锁过期),也能通过Fencing Token保证资源安全。
// Fencing Token实现
public class FencedLock {
private final AtomicLong tokenGenerator = new AtomicLong(0);
public LockResult tryLock(String lockKey) {
boolean acquired = redis.opsForValue().setIfAbsent(lockKey, lockValue, ttl);
if (acquired) {
long token = tokenGenerator.incrementAndGet();
return new LockResult(true, token);
}
return LockResult.FAILED;
}
}
// 存储服务端校验
public class FencedStorage {
private final Map<String, Long> maxTokens = new ConcurrentHashMap<>();
public boolean write(String resource, Object data, long fencingToken) {
Long currentMax = maxTokens.get(resource);
if (currentMax != null && fencingToken <= currentMax) {
return false; // 拒绝过期的token
}
maxTokens.put(resource, fencingToken);
doWrite(resource, data);
return true;
}
}总结
分布式锁方案的选择取决于性能需求、可靠性要求和已有的基础设施。Redis锁性能最高但在主从切换时有安全风险;ZooKeeper和etcd基于强一致性协议,可靠性更高但性能略低;MySQL锁最简单但性能最差。无论选择哪种方案,都应考虑引入Fencing Token机制来防止锁失效导致的数据不一致问题。在实际工程中,推荐使用Redisson或Curator等成熟的客户端库,避免自行实现锁逻辑中的各种边界情况。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于