分布式锁详解:实现方案与对比分析
在分布式系统中,多个节点竞争同一资源(如库存扣减、订单创建)时,需通过分布式锁保证操作的原子性,避免数据不一致。常见实现方式包括 Redis、ZooKeeper 和数据库,每种方案各有优劣,需根据场景选择。以下详细解析各方案的原理、代码实现及适用场景。
Redis 分布式锁:高性能的选择
Redis 凭借其高性能和原子操作特性,成为分布式锁的主流实现方案。核心思路是通过 set 命令的原子性实现加锁,结合过期时间和唯一标识避免死锁。
基础实现(setnx + expire 的问题与优化)
原始方案:使用
setnx加锁(仅当 key 不存在时成功),expire设置过期时间(避免死锁)。1
2
3
4
5
6
7
8
9
10// 加锁:若 key 不存在则设置,返回 1 表示成功
Long setnx = jedis.setnx("lock:order", "1");
if (setnx == 1) {
// 设置过期时间(30 秒)
jedis.expire("lock:order", 30);
// 执行业务逻辑
// ...
// 释放锁
jedis.del("lock:order");
}问题:
setnx和expire是非原子操作,若加锁后未设置过期时间就宕机,会导致锁永久有效。优化方案:使用 Redis 2.6.12+ 支持的
set命令扩展参数,实现 “加锁 + 过期时间” 原子操作:1
2
3
4
5
6// NX:仅 key 不存在时设置;EX:过期时间单位为秒
String result = jedis.set("lock:order", "requestId123", "NX", "EX", 30);
if ("OK".equals(result)) {
// 加锁成功,执行业务
// ...
}
释放锁的原子性(避免误删)
若直接使用 del 释放锁,可能删除其他节点的锁(如当前节点业务超时,锁已过期并被其他节点获取)。解决方案:通过唯一标识(如 UUID)验证锁归属,并使用 Lua 脚本保证释放操作的原子性。
1 | // 释放锁的 Lua 脚本:仅当 value 匹配时删除 key |
Redisson 框架:生产级实现
手动实现存在锁续约(避免业务未完成锁过期)、集群一致性等问题,推荐使用 Redisson 框架,其封装了完整的分布式锁逻辑:
依赖引入:
1
2
3
4
5<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version>
</dependency>代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 初始化客户端(支持单机、集群等模式)
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 获取锁对象
RLock lock = redisson.getLock("lock:order");
try {
// 加锁(默认 30 秒过期,自动续约)
lock.lock();
// 执行业务逻辑(如扣减库存)
// ...
} finally {
// 释放锁
lock.unlock();
}核心特性:
- 自动续约(Watch Dog 机制):若业务未完成,自动延长锁过期时间;
- 支持集群模式(Redis Cluster),解决单点故障问题;
- 提供公平锁、读写锁等高级功能。
ZooKeeper 分布式锁:强一致性的选择
ZooKeeper 基于临时有序节点和监听机制实现分布式锁,天然支持公平锁,适合对一致性要求高的场景。
实现原理
- 加锁:所有节点在指定路径(如
/locks)下创建临时有序节点(如/locks/lock-0000000001)。 - 竞争锁:
- 节点创建后,获取
/locks下的所有子节点,若自身是序号最小的节点,则获得锁; - 若不是,则监听前一个节点(如
/locks/lock-0000000000)的删除事件。
- 节点创建后,获取
- 释放锁:完成业务后删除自身节点,或会话失效时临时节点自动删除,触发下一个节点的监听事件。
Curator 框架:简化实现
Curator 是 ZooKeeper 官方推荐的客户端,其 recipes 模块封装了分布式锁实现:
依赖引入:
1
2
3
4
5<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.4.0</version>
</dependency>代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 初始化客户端
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory
.newClient("localhost:2181", retryPolicy);
client.start();
// 创建分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
try {
// 尝试加锁(最多等待 10 秒)
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 执行业务逻辑
// ...
}
} finally {
// 释放锁
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
}核心特性:
- 公平锁:通过有序节点保证 “先到先得”,避免饥饿问题;
- 自动释放:临时节点随会话失效而删除,避免死锁;
- 强一致性:基于 ZooKeeper 的 CP 特性,适合金融等对一致性敏感的场景。
数据库分布式锁:简单但性能有限
基于数据库的行锁或表锁实现分布式锁,适合并发量低、架构简单的场景。
基于行锁(select for update)
利用 InnoDB 的行锁机制,通过 select ... for update 锁定特定记录:
1 | // 加锁:锁定 id=1 的记录(行锁) |
基于锁表
创建专门的锁表,通过插入 / 删除记录实现锁控制:
1 | -- 锁表结构 |
缺点
- 性能差:数据库 IO 开销大,不适合高并发场景;
- 可能死锁:若事务未提交,行锁 / 表锁会长期持有;
- 无自动续约:需手动处理锁过期问题。
三种方案对比与选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis 锁 | 高性能,支持高并发;部署简单 | 弱一致性(主从同步可能延迟);需处理续约 | 互联网高并发场景(如秒杀、电商) |
| ZooKeeper 锁 | 强一致性;天然公平锁;自动释放 | 性能较低(创建节点开销大);部署复杂 | 金融、支付等强一致性场景 |
| 数据库锁 | 实现简单,无需额外组件 | 性能差,不支持高并发;易死锁 | 低并发、架构简单的内部系统 |