分布式锁简介
分布式锁是一种 在分布式系统中 确保 多个节点 互斥访问共享资源的机制,主要用于 防止并发修改 数据。
在 Java 开发中,常用的分布式锁方案:
- Redis 分布式锁
- Zookeeper 分布式锁(基于 临时有序节点)
- MySQL 分布式锁
🚀 Redis 分布式锁
适用场景:
- 高并发环境,比如 秒杀、库存扣减、订单支付 等。
- 需要 轻量级、高性能 的锁。
实现方式:
- 尝试获取锁 → SETNX(如果锁不存在,则创建)
- 设置过期时间 → EX 防止死锁
- 释放锁 → DEL 删除
📝 代码实现
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
public class RedisDistributedLock {
private final RedissonClient redissonClient;
public RedisDistributedLock(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void executeWithLock(String lockKey, Runnable task) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待 5 秒,锁 10 秒后自动释放
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
task.run(); // 执行业务逻辑
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
📌 代码解析
- 获取锁:tryLock(5, 10, TimeUnit.SECONDS)
- 最多 等待 5 秒 获取锁,避免死锁问题。
- 持有 10 秒,防止程序崩溃导致锁无法释放。
- 执行业务逻辑:task.run()
- 释放锁:lock.unlock()
✅ 推荐使用 Redis + Redisson,适合 高并发场景 🚀
✅ 如果有高一致性要求,可以用 Zookeeper
✅ 小并发场景 可以用 MySQL 行锁
分布式锁原理
🔐 分布式锁:保证多个节点互斥访问共享资源
在 分布式系统 中,多个节点可能同时 访问、修改 共享资源,例如 秒杀商品库存、订单支付、短链统计。如果没有 锁机制,可能会导致:
- 超卖(多个请求同时减少库存,导致库存负数)
- 数据不一致(多个节点同时更新数据,导致数据冲突)
- 事务冲突(多个事务并发修改,导致事务失败)
👉 分布式锁 可以保证 多个节点互斥访问,确保资源不会被 同时修改。
1️⃣ 分布式锁的基本原理
分布式锁的核心目标: ✅ 互斥性:保证 同一时刻 只有一个节点持有锁
✅ 避免死锁:如果某个节点 宕机,锁能 自动释放
✅ 高可用:锁系统 不能成为性能瓶颈
✅ 可重入:支持 同一线程多次获取
2️⃣ 分布式锁的实现方式
🔹 方式 1:基于 Redis 实现分布式锁
Redis 具有:
- 高性能(内存存储,锁操作快)
- 单线程模型(天然避免竞争)
- 支持 TTL 过期时间(防止死锁)
📌 实现步骤
- 加锁 String lockKey = “lock:product:1001”; Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, “locked”, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(success)) { // 成功获得锁 try { // 执行业务逻辑 } finally { redisTemplate.delete(lockKey); // 释放锁 } } else { // 获取锁失败 }
- 释放锁
- 先判断是不是自己加的锁再删除,防止误删其他节点的锁
🚀 适用场景
✅ 秒杀库存扣减
✅ 短链接访问统计
✅ 分布式任务调度
⚠️ 存在问题
❌ 非原子性(先判断再删除可能会误删别的节点的锁)
❌ Redis 宕机,锁丢失
✅ 可以用 Redisson 解决(支持自动续期,防止锁失效)
🔹 方式 2:基于 Zookeeper 实现分布式锁
Zookeeper 通过 临时有序节点 实现锁机制:
- 创建临时顺序节点(/lock/0001、/lock/0002)
- 最小节点获得锁(0001 获得锁)
- 删除节点后,后续节点获得锁
📌 代码示例
public class ZkDistributedLock {
private ZooKeeper zk;
private String lockPath = "/lock";
public void acquireLock() throws Exception {
String myNode = zk.create(lockPath + "/node_", new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
List<String> nodes = zk.getChildren(lockPath, false);
Collections.sort(nodes);
if (myNode.endsWith(nodes.get(0))) {
// 获得锁
} else {
// 监听前一个节点
}
}
}
🚀 适用场景
✅ 分布式任务调度
✅ 跨集群锁管理
⚠️ 存在问题
❌ 性能比 Redis 低(ZooKeeper 适合持久锁,而非高频加解锁)
❌ 运维成本高(需要部署 ZooKeeper 集群)
🔹 方式 3:基于数据库实现分布式锁
数据库锁的方式:
- 悲观锁(SELECT … FOR UPDATE)
- 乐观锁(版本号机制)
- 表字段加锁(UPDATE … WHERE id = ? AND locked = 0)
📌 实现步骤
-- 加锁(INSERT 失败说明锁已存在)
INSERT INTO distributed_lock (lock_key, locked) VALUES ('order_1001', 1);
-- 释放锁
DELETE FROM distributed_lock WHERE lock_key = 'order_1001';
🚀 适用场景
✅ 数据一致性要求高的业务
✅ 短时间并发较低的场景
⚠️ 存在问题
❌ 性能低(数据库事务开销大)
❌ 不能自动续期(锁表后宕机,锁可能无法释放)
3️⃣ 分布式锁实战案例
📍 场景 1:电商秒杀(防止超卖)
问题:多个用户同时购买 1001 号商品,如何防止超卖?
解决方案:使用 Redis 分布式锁
String lockKey = "lock:product:1001";
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
try {
// 读取库存
int stock = productService.getStock(1001);
if (stock > 0) {
productService.decreaseStock(1001);
}
} finally {
redisTemplate.delete(lockKey);
}
}
📍 场景 2:短链接访问统计
问题:多个用户访问短链接,如何防止并发冲突?
解决方案:使用 Redis 作为分布式锁
String lockKey = "lock:short-link:1234";
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
try {
shortLinkService.incrementVisitCount("1234");
} finally {
redisTemplate.delete(lockKey);
}
}
4️⃣ 总结
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis 分布式锁 | 高性能、支持过期时间 | 锁丢失、非原子性删除 | 秒杀、库存扣减、访问统计 |
| ZooKeeper 分布式锁 | 强一致性、自动释放 | 性能比 Redis 低 | 任务调度、分布式事务 |
| 数据库分布式锁 | 数据可靠性高 | 性能低,容易死锁 | 低并发事务 |
🔥 结论
✅ 高并发业务(秒杀、短链统计)→ Redis 分布式锁
✅ 强一致性业务(分布式事务、任务调度)→ ZooKeeper 分布式锁
✅ 小规模业务(支付事务锁)→ 数据库锁
💡 一个节点拿到分布式锁后会怎样?其他节点会如何处理?
1️⃣ 一个节点拿到锁后发生什么?
当某个节点(比如 Node A)成功获取 分布式锁 后: ✅ 只有 Node A 能执行 临界区(关键业务逻辑)
✅ 其他节点(比如 Node B、C、D)无法获取锁,必须等待
✅ 锁不会自动释放,Node A 执行完业务后,手动释放锁(或超时释放)
2️⃣ 其他节点会不会拿到锁?
在 Node A 持有锁期间,Node B、C、D 不能获得锁,它们会:
- 🔄 轮询重试(一直尝试获取锁)
- 🕒 阻塞等待(直到锁被释放)
- 💥 直接失败(锁被占用就返回错误)
🔹 具体情况取决于锁的实现方式:
| 实现方式 | 其他节点的行为 |
|---|---|
| Redis 分布式锁 | 其他节点 轮询重试 或 快速失败 |
| Zookeeper 分布式锁 | 其他节点 阻塞等待(监听前一个节点) |
| 数据库分布式锁 | 其他节点 直接失败 或 不断重试 |
3️⃣ 其他节点怎么知道锁已经被占用?
不同的 分布式锁实现方式,其他节点知道锁 被占用 的方式不同:
🔹 Redis 分布式锁
其他节点如何判断锁被占用?
- 尝试加锁,如果 SET NX 失败,说明锁已被占用: Boolean success = redisTemplate.opsForValue().setIfAbsent(“lock:order”, “locked”, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(success)) { // 获取锁成功 } else { // 获取锁失败,说明其他节点已占用 }
- 其他节点需要等待?
- 轮询尝试获取锁(自旋重试)
- 设置超时时间,避免一直等待
4️⃣ 实战示例:多个节点竞争 Redis 分布式锁
🌟 假设 3 个节点(A、B、C)同时竞争锁
📌 代码示例
String lockKey = "lock:product:1001";
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
try {
System.out.println("✅ " + Thread.currentThread().getName() + " 获得锁,执行业务逻辑...");
Thread.sleep(5000); // 模拟业务逻辑执行
} finally {
redisTemplate.delete(lockKey);
System.out.println("🔓 " + Thread.currentThread().getName() + " 释放锁");
}
} else {
System.out.println("❌ " + Thread.currentThread().getName() + " 获取锁失败,稍后重试...");
}
📌 运行结果
✅ Node A 获得锁,执行业务逻辑...
❌ Node B 获取锁失败,稍后重试...
❌ Node C 获取锁失败,稍后重试...
🔓 Node A 释放锁
✅ Node B 获得锁,执行业务逻辑...
🔓 Node B 释放锁
✅ Node C 获得锁,执行业务逻辑...
🔓 Node C 释放锁
📌 结论:
- Node A 先获得锁,执行完释放
- Node B 轮询等待,A 释放后获取
- Node C 依次执行
🔹 多个节点使用同一个 Redis 吗?
✅ 在分布式系统中,多个节点(如 Node A、B、C)通常 共享同一个 Redis 实例,用 Redis 作为 分布式锁的存储中心。
Redis 负责管理锁的状态,不同节点都会去同一个 Redis 实例申请锁。
🔹 Redis 是单线程模型,为什么能支持多个节点加锁?
Redis 是 单线程 处理命令的,但它能 高效支持并发,主要是因为:
- ⚡ 内存操作快:Redis 所有操作都在内存,没有磁盘 IO 负担,速度极快
- 📌 原子性:Redis 提供 SET NX(原子操作),即使单线程也能保证 多个节点竞争锁时不会发生并发问题
- 🔀 事件驱动模型:Redis 采用 事件循环 处理多个请求,不会阻塞
- 🔄 多线程 IO 处理(Redis 6.0+):虽然 命令执行是单线程,但 网络 IO 是多线程,提高吞吐量
🔹 多个节点如何在 Redis 上加锁?
多个节点会同时请求 Redis,使用 原子操作 来竞争锁,典型的方式是:
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:order:1001", "locked", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
System.out.println(Thread.currentThread().getName() + " 获得锁,执行业务逻辑");
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败,稍后重试...");
}
📌 竞争锁流程:
| 时间 | 事件 |
|---|---|
| T1 | Node A 发送 SET NX lock:order:1001 locked |
| T2 | Node A 拿到锁,执行业务 |
| T3 | Node B、C 也想加锁,但 SET NX 失败 |
| T4 | Node A 释放锁 (DEL lock:order:1001) |
| T5 | Node B 重新尝试 SET NX,成功获得锁 |
✅ 结论:单线程 不会影响多个节点竞争锁的公平性,因为 Redis 的 原子操作 确保了只有一个节点能成功加锁。
🔹 Redis 分布式锁的常见问题
- ❓ 断电/崩溃会怎样?
- 解决方案:使用 Redisson,支持 看门狗自动续期,避免锁因崩溃丢失
- ❓ 锁超时释放,任务还没执行完怎么办?
- 解决方案:任务执行完毕前主动续期
- ❓ 多个节点重复消费任务?
- 解决方案:设置唯一锁标识,防止锁被误释放
🔹 总结
| 问题 | 答案 |
|---|---|
| 多个节点使用同一个 Redis 吗? | ✅ 是的!多个节点共享同一个 Redis |
| Redis 单线程,为什么还能处理多个节点加锁? | ✅ 因为 SET NX 是原子操作,并且 Redis 是高性能的 |
| 如何保证锁的正确性? | ✅ 用 Redisson 或手动续期,防止锁误释放 |
| 怎么优化 Redis 分布式锁? | ✅ 看门狗机制、UUID 防误删、超时续期 |
🚀 Redis 单线程 ≠ 低并发!分布式锁仍然能高效运作! 💪
暂时没有回复