分布式锁简介

分布式锁是一种 在分布式系统中 确保 多个节点 互斥访问共享资源的机制,主要用于 防止并发修改 数据。

在 Java 开发中,常用的分布式锁方案

  1. Redis 分布式锁
  2. Zookeeper 分布式锁(基于 临时有序节点
  3. MySQL 分布式锁

🚀 Redis 分布式

适用场景

  • 高并发环境,比如 秒杀、库存扣减、订单支付 等。
  • 需要 轻量级、高性能 的锁。

实现方式

  1. 尝试获取锁 → SETNX(如果锁不存在,则创建)
  2. 设置过期时间 → EX 防止死锁
  3. 释放锁 → 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();
        }
    }
}

📌 代码解析

  1. 获取锁:tryLock(5, 10, TimeUnit.SECONDS)
    • 最多 等待 5 秒 获取锁,避免死锁问题。
    • 持有 10 秒,防止程序崩溃导致锁无法释放。
  2. 执行业务逻辑:task.run()
  3. 释放锁:lock.unlock()

推荐使用 Redis + Redisson,适合 高并发场景 🚀
如果有高一致性要求,可以用 Zookeeper
小并发场景 可以用 MySQL 行锁


分布式锁原理

🔐 分布式锁:保证多个节点互斥访问共享资源

分布式系统 中,多个节点可能同时 访问、修改 共享资源,例如 秒杀商品库存、订单支付、短链统计。如果没有 锁机制,可能会导致:

  1. 超卖(多个请求同时减少库存,导致库存负数)
  2. 数据不一致(多个节点同时更新数据,导致数据冲突)
  3. 事务冲突(多个事务并发修改,导致事务失败)

👉 分布式锁 可以保证 多个节点互斥访问,确保资源不会被 同时修改

1️⃣ 分布式锁的基本原理

分布式锁的核心目标: ✅ 互斥性:保证 同一时刻 只有一个节点持有锁
避免死锁:如果某个节点 宕机,锁能 自动释放
高可用:锁系统 不能成为性能瓶颈
可重入:支持 同一线程多次获取

2️⃣ 分布式锁的实现方式

🔹 方式 1:基于 Redis 实现分布式锁

Redis 具有

  • 高性能(内存存储,锁操作快)
  • 单线程模型(天然避免竞争)
  • 支持 TTL 过期时间(防止死锁)

📌 实现步骤

  1. 加锁 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 { // 获取锁失败 }
  2. 释放锁
    • 先判断是不是自己加的锁再删除,防止误删其他节点的锁
    String lockValue = redisTemplate.opsForValue().get(lockKey); if (“locked”.equals(lockValue)) { redisTemplate.delete(lockKey); }

🚀 适用场景

秒杀库存扣减
短链接访问统计
分布式任务调度

⚠️ 存在问题

非原子性(先判断再删除可能会误删别的节点的锁)
Redis 宕机,锁丢失
可以用 Redisson 解决(支持自动续期,防止锁失效)

🔹 方式 2:基于 Zookeeper 实现分布式锁

Zookeeper 通过 临时有序节点 实现锁机制:

  1. 创建临时顺序节点(/lock/0001、/lock/0002)
  2. 最小节点获得锁(0001 获得锁)
  3. 删除节点后,后续节点获得锁

📌 代码示例

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 分布式锁

其他节点如何判断锁被占用?

  1. 尝试加锁,如果 SET NX 失败,说明锁已被占用: Boolean success = redisTemplate.opsForValue().setIfAbsent(“lock:order”, “locked”, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(success)) { // 获取锁成功 } else { // 获取锁失败,说明其他节点已占用 }
  2. 其他节点需要等待?
    • 轮询尝试获取锁(自旋重试
    • 设置超时时间,避免一直等待

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 是 单线程 处理命令的,但它能 高效支持并发,主要是因为:

  1. ⚡ 内存操作快:Redis 所有操作都在内存,没有磁盘 IO 负担,速度极快
  2. 📌 原子性:Redis 提供 SET NX(原子操作),即使单线程也能保证 多个节点竞争锁时不会发生并发问题
  3. 🔀 事件驱动模型:Redis 采用 事件循环 处理多个请求,不会阻塞
  4. 🔄 多线程 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() + " 获取锁失败,稍后重试...");
}

📌 竞争锁流程:

时间事件
T1Node A 发送 SET NX lock:order:1001 locked
T2Node A 拿到锁,执行业务
T3Node B、C 也想加锁,但 SET NX 失败
T4Node A 释放锁 (DEL lock:order:1001)
T5Node B 重新尝试 SET NX,成功获得锁

结论:单线程 不会影响多个节点竞争锁的公平性,因为 Redis 的 原子操作 确保了只有一个节点能成功加锁。

🔹 Redis 分布式锁的常见问题

  1. ❓ 断电/崩溃会怎样?
    • 解决方案:使用 Redisson,支持 看门狗自动续期,避免锁因崩溃丢失
  2. ❓ 锁超时释放,任务还没执行完怎么办?
    • 解决方案:任务执行完毕前主动续期
  3. ❓ 多个节点重复消费任务?
    • 解决方案:设置唯一锁标识,防止锁被误释放

🔹 总结

问题答案
多个节点使用同一个 Redis 吗?是的!多个节点共享同一个 Redis
Redis 单线程,为什么还能处理多个节点加锁?因为 SET NX 是原子操作,并且 Redis 是高性能的
如何保证锁的正确性?用 Redisson 或手动续期,防止锁误释放
怎么优化 Redis 分布式锁?看门狗机制、UUID 防误删、超时续期

🚀 Redis 单线程 ≠ 低并发!分布式锁仍然能高效运作! 💪

Categories:

Tags:

暂时没有回复

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注