分布式锁
分布式锁详解:实现方案与对比分析
在分布式系统中,多个节点竞争同一资源(如库存扣减、订单创建)时,需通过分布式锁保证操作的原子性,避免数据不一致。常见实现方式包括 Redis、ZooKeeper 和数据库,每种方案各有优劣,需根据场景选择。以下详细解析各方案的原理、代码实现及适用场景。
Redis 分布式锁:高性能的选择
Redis 凭借其高性能和原子操作特性,成为分布式锁的主流实现方案。核心思路是通过 set 命令的原子性实现加锁,结合过期时间和唯一标识避免死锁。
基础实现(setnx + expire 的问题与优化)
原始方案:使用
setnx加锁(仅当 key 不存在时成功),expire设置过期时间(避免死锁)。// 加锁:若 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命令扩展参数,实现 “加锁 + 过期时间” 原子操作:// NX:仅 key 不存在时设置;EX:过期时间单位为秒 String result = jedis.set("lock:order", "requestId123", "NX", "EX", 30); if ("OK".equals(result)) { // 加锁成功,执行业务 // ... }
释放锁的原子性(避免误删)
若直接使用 del 释放锁,可能删除其他节点的锁(如当前节点业务超时,锁已过期并被其他节点获取)。解决方案:通过唯一标识(如 UUID)验证锁归属,并使用 Lua 脚本保证释放操作的原子性。
// 释放锁的 Lua 脚本:仅当 value 匹配时删除 key
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
// 执行脚本:KEYS 为锁 key,ARGV 为唯一标识
Long result = (Long) jedis.eval(
luaScript,
Collections.singletonList("lock:order"),
Collections.singletonList("requestId123")
);
if (result == 1) {
System.out.println("释放锁成功");
}
Redisson 框架:生产级实现
手动实现存在锁续约(避免业务未完成锁过期)、集群一致性等问题,推荐使用 Redisson 框架,其封装了完整的分布式锁逻辑:
依赖引入:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.20.0</version> </dependency>代码示例:
// 初始化客户端(支持单机、集群等模式) 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 模块封装了分布式锁实现:
依赖引入:
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.4.0</version> </dependency>代码示例:
// 初始化客户端 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 锁定特定记录:
// 加锁:锁定 id=1 的记录(行锁)
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
// for update 会阻塞其他事务的更新操作
PreparedStatement ps = conn.prepareStatement(
"select * from lock_table where id = 1 for update"
);
ps.executeQuery();
// 执行业务逻辑
// ...
conn.commit(); // 释放锁
} catch (SQLException e) {
conn.rollback();
}
基于锁表
创建专门的锁表,通过插入 / 删除记录实现锁控制:
-- 锁表结构
CREATE TABLE distributed_lock (
id INT PRIMARY KEY,
lock_name VARCHAR(50) UNIQUE, -- 锁名称
holder VARCHAR(50), -- 持有节点标识
expire_time TIMESTAMP -- 过期时间
);
-- 加锁:插入记录(唯一索引保证原子性)
INSERT INTO distributed_lock (lock_name, holder, expire_time)
VALUES ('order_lock', 'node1', NOW() + INTERVAL 30 SECOND);
-- 释放锁:删除记录
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND holder = 'node1';
缺点
- 性能差:数据库 IO 开销大,不适合高并发场景;
- 可能死锁:若事务未提交,行锁 / 表锁会长期持有;
- 无自动续约:需手动处理锁过期问题。
三种方案对比与选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis 锁 | 高性能,支持高并发;部署简单 | 弱一致性(主从同步可能延迟);需处理续约 | 互联网高并发场景(如秒杀、电商) |
| ZooKeeper 锁 | 强一致性;天然公平锁;自动释放 | 性能较低(创建节点开销大);部署复杂 | 金融、支付等强一致性场景 |
| 数据库锁 | 实现简单,无需额外组件 | 性能差,不支持高并发;易死锁 | 低并发、架构简单的内部系统 |