分布式知识梳理(4)
作者:没有四次元口袋的蓝胖
日期:2026-08-09
标签:Java, 分布式锁, Redis, Zookeeper, 数据库
分布式锁是分布式系统的核心组件。核心掌握:数据库锁、Redis 锁、Zookeeper 锁的实现与对比。
一、为什么需要分布式锁?
1.1 场景
多台服务器访问共享资源,需要互斥访问。
服务器 A → 扣减库存(同时访问)→ 数据库
服务器 B → 扣减库存(同时访问)→ 数据库
需要保证:同一时刻只有一个服务器能扣减库存
1.2 分布式锁要求
| 特性 | 说明 |
|---|---|
| 互斥性 | 同一时刻只有一个线程能获取锁 |
| 可重入 | 同一线程可以多次获取同一把锁 |
| 超时释放 | 防止死锁(锁超时自动释放) |
| 高可用 | 锁服务不能单点故障 |
| 高性能 | 获取和释放锁要快 |
二、基于数据库实现
2.1 唯一索引锁
原理:利用数据库唯一索引的冲突机制。
java
// 创建锁表
CREATE TABLE distributed_lock (
lock_name VARCHAR(100) PRIMARY KEY,
expire_time DATETIME
);
// 获取锁
public boolean tryLock(String lockName) {
try {
// 插入锁记录
lockMapper.insert(lockName, new Date());
return true;
} catch (DuplicateKeyException e) {
// 唯一索引冲突,获取锁失败
return false;
}
}
// 释放锁
public void unlock(String lockName) {
lockMapper.delete(lockName);
}
问题:
- 锁没有过期时间,可能死锁
- 非阻塞,获取失败直接返回
2.2 悲观锁(for update)
原理:利用数据库的排他锁。
java
// 获取锁(开启事务)
@Transactional
public boolean tryLock(String lockName) {
// SELECT ... FOR UPDATE 会阻塞等待
Lock lock = lockMapper.selectForUpdate(lockName);
if (lock == null) {
// 创建锁记录
lockMapper.insert(lockName, new Date());
}
return true;
}
// 释放锁(事务结束自动释放)
// 不需要显式 unlock,事务提交/回滚后锁自动释放
问题:
- 依赖数据库,性能差
- 单点故障风险
2.3 优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单 | 性能差(依赖数据库) |
| 无需额外组件 | 单点故障 |
| 支持可重入 | 可能死锁(需要设置过期时间) |
三、基于 Redis 实现
3.1 基础版本
原理:利用 Redis 的 SETNX(SET if Not eXists)。
java
// 获取锁
public boolean tryLock(String lockName, long expireTime) {
// SETNX + 过期时间(原子操作)
Long result = jedis.setnx(lockName, String.valueOf(System.currentTimeMillis() + expireTime));
return result == 1;
}
// 释放锁
public void unlock(String lockName) {
jedis.del(lockName);
}
问题:
- 释放锁时可能误删别人的锁
- 非原子操作(判断+删除)
3.2 改进版本(解决误删问题)
java
// 获取锁(带唯一标识)
public boolean tryLock(String lockName, String requestId, long expireTime) {
// value 存储 requestId
Long result = jedis.setnx(lockName, requestId);
if (result == 1) {
jedis.pexpire(lockName, expireTime);
return true;
}
return false;
}
// 释放锁(Lua 脚本,原子操作)
public void unlock(String lockName, String requestId) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(luaScript, Arrays.asList(lockName), Arrays.asList(requestId));
}
3.3 完善版本(解决超时问题)
java
// 看门狗机制(自动续期)
public class RedisLock {
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
public boolean tryLock(String lockName, String requestId, long expireTime) {
Long result = jedis.setnx(lockName, requestId);
if (result == 1) {
jedis.pexpire(lockName, expireTime);
// 启动看门狗,自动续期
startWatchDog(lockName, requestId, expireTime);
return true;
}
return false;
}
private void startWatchDog(String lockName, String requestId, long expireTime) {
executor.scheduleAtFixedRate(() -> {
// 检查锁是否还属于自己
if (requestId.equals(jedis.get(lockName))) {
jedis.pexpire(lockName, expireTime);
}
}, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
}
public void unlock(String lockName, String requestId) {
// 停止看门狗
executor.shutdown();
// Lua 脚本释放锁
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(luaScript, Arrays.asList(lockName), Arrays.asList(requestId));
}
}
3.4 Redisson 实现(推荐)
java
// 引入 Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 获取锁
RLock lock = redisson.getLock("myLock");
try {
// 尝试获取锁,等待 10 秒,锁定 30 秒后自动释放
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}
3.5 优缺点
| 优点 | 缺点 |
|---|---|
| 性能好(内存操作) | Redis 宕机可能丢失锁 |
| 支持集群(Redis Cluster) | 主从切换可能丢锁 |
| 实现相对简单 | 需要处理超时、续期 |
四、基于 Zookeeper 实现
4.1 原理
利用 ZK 的临时顺序节点 和Watcher 机制。
1. 在 /locks 下创建临时顺序节点
2. 判断自己是否是序号最小的节点
- 是:获取锁成功
- 否:监听前一个节点
3. 前一个节点删除(释放锁),触发 Watcher
4. 再次判断自己是否是最小节点
4.2 代码示例
java
public class ZkLock {
private ZooKeeper zk;
private String lockPath = "/locks/";
private String currentNode;
public boolean tryLock() throws Exception {
// 创建临时顺序节点
currentNode = zk.create(lockPath + "lock-", null,
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点
List<String> children = zk.getChildren(lockPath, false);
Collections.sort(children);
// 判断自己是否是最小节点
int index = children.indexOf(currentNode.substring(lockPath.length()));
if (index == 0) {
return true; // 获取锁成功
}
// 监听前一个节点
CountDownLatch latch = new CountDownLatch(1);
String prevNode = children.get(index - 1);
zk.exists(lockPath + prevNode, event -> {
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
latch.countDown();
}
});
// 等待前一个节点删除
latch.await();
return true;
}
public void unlock() throws Exception {
zk.delete(currentNode, -1);
}
}
4.3 Curator 实现(推荐)
java
// 引入 Curator
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
client.start();
// 创建分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");
try {
// 获取锁,等待 10 秒
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.release(); // 释放锁
}
}
} catch (Exception e) {
e.printStackTrace();
}
4.4 优缺点
| 优点 | 缺点 |
|---|---|
| 强一致性(ZAB 协议) | 性能较差(磁盘操作) |
| 自动释放(临时节点) | 实现复杂 |
| 支持可重入 | 依赖 Zookeeper |
| 高可用(集群) | 集群性能下降 |
五、三种实现对比
| 维度 | 数据库锁 | Redis 锁 | Zookeeper 锁 |
|---|---|---|---|
| 性能 | 差(磁盘) | 好(内存) | 中(磁盘) |
| 一致性 | 强 | 最终(主从异步) | 强(ZAB) |
| 可靠性 | 中(单点) | 中(主从切换丢锁) | 高(集群) |
| 实现复杂度 | 低 | 中 | 高 |
| 超时释放 | 需要实现 | 需要实现 | 自动(临时节点) |
| 可重入 | 需要实现 | 需要实现 | 支持 |
| 适用场景 | 低并发 | 高并发 | 强一致要求 |
5.1 选型建议
| 场景 | 推荐方案 |
|---|---|
| 低并发、简单场景 | 数据库锁 |
| 高并发、性能优先 | Redis 锁(Redisson) |
| 强一致、可靠性优先 | Zookeeper 锁(Curator) |
六、Redis 锁的问题与解决
6.1 主从切换丢锁
问题:
1. 客户端 A 在 Master 上获取锁
2. Master 宕机,锁数据未同步到 Slave
3. Slave 晋升为 Master
4. 客户端 B 在新 Master 上获取锁成功
→ 两个客户端同时持有锁
解决:RedLock 算法
java
// 部署 N 个独立的 Redis 节点
List<RedissonClient> nodes = Arrays.asList(
Redisson.create(config1),
Redisson.create(config2),
Redisson.create(config3)
);
// 获取锁:在 N/2+1 个节点上成功才算获取锁
RLock lock = new RedissonRedLock(nodes.stream()
.map(r -> r.getLock("myLock"))
.toArray(RLock[]::new));
lock.lock();
6.2 RedLock 争议
- 支持:Martin Fowler 认为解决了主从切换丢锁问题
- 反对:Antirez(Redis 作者)提出 RedLock,但存在时钟跳跃等问题
- 结论:对一致性要求极高,用 Zookeeper;一般场景用 Redis 单节点 + 看门狗即可
🗺️ 思维导图速览
分布式锁
├── 数据库锁
│ ├── 唯一索引锁(SETNX 思想)
│ └── 悲观锁(FOR UPDATE)
├── Redis 锁
│ ├── 基础版(SETNX + 过期时间)
│ ├── 改进版(Lua 脚本,防误删)
│ ├── 完善版(看门狗,自动续期)
│ └── Redisson(推荐)
├── Zookeeper 锁
│ ├── 临时顺序节点
│ ├── Watcher 机制
│ └── Curator(推荐)
└── 对比
├── 性能:Redis > ZK > 数据库
├── 一致性:ZK > 数据库 > Redis
└── 可靠性:ZK > Redis > 数据库
📝 写在最后
学习建议
- 数据库锁要知道两种实现:唯一索引、FOR UPDATE
- Redis 锁要知道演进过程:SETNX → Lua 脚本 → 看门狗 → Redisson
- Zookeeper 锁要知道原理:临时顺序节点 + Watcher
- 选型要知道差异:性能优先用 Redis,一致性优先用 ZK
面试回答模板
Q:说说分布式锁的实现?
三种主流实现:数据库锁,利用唯一索引或 FOR UPDATE,实现简单但性能差;Redis 锁,用 SETNX + 过期时间,性能高但主从切换可能丢锁,推荐用 Redisson 的看门狗机制;Zookeeper 锁,用临时顺序节点 + Watcher,强一致但性能较差,推荐用 Curator。
Q:Redis 锁有什么问题?怎么解决?
问题:SETNX 和设置过期时间非原子操作,可能死锁;释放锁时可能误删别人的锁;主从切换可能丢锁。解决:用 Lua 脚本保证原子性;value 存储 requestId,释放时判断;用 RedLock 算法在多个节点上加锁,但存在争议。一般场景用 Redisson 的看门狗机制即可。
Q:Redis 锁和 Zookeeper 锁怎么选?
性能优先选 Redis(内存操作,QPS 高),一致性优先选 Zookeeper(ZAB 协议,强一致)。Redis 锁在主从切换时可能丢锁,ZK 锁不会丢但性能较差。电商秒杀等高并发场景用 Redis,金融转账等强一致场景用 ZK。