分布式锁:跨 JVM 的“工商局备案章”

在单机时代,synchronizedReentrantLock 就像部门内部的印章 ,只要在这个办公室(JVM 进程)里,大家都认。

但在微服务集群时代,你的应用可能部署了 10 个实例(10 个办公室)。此时,线程 A 在实例 1 加了锁,线程 B 在实例 2 根本不知道这个锁的存在,直接冲进去操作共享资源(如数据库库存),导致超卖数据不一致! 这可怎么办?这时候,我们需要一个全公司(集群)都认的"工商局备案章" ------ 分布式锁!

它的核心要求只有三条:

  1. 互斥性:任何时刻,只有一个客户端能持有锁。
  2. 防死锁:如果持有锁的客户端挂了,锁必须能自动释放(不能让别人永远等下去)。
  3. 高可用:锁服务本身不能挂(Redis 集群、ZK 集群)。

第一部分:基于 Redis 的分布式锁

Redis 因其高性能(单线程模型 + 内存操作)成为分布式锁的首选。但自己手写 Redis 锁坑非常多,强烈建议使用 Redisson。

1、naive 实现 vs. 生产级实现

复制代码
// 错误示范:不要在生产环境这样写!
if (redis.setnx(lockKey, "myId") == 1) {
    redis.expire(lockKey, 30); // 设置过期时间
    try {
        // 业务逻辑
    } finally {
        redis.del(lockKey); // 释放锁
    }
}

这个为什么错呢?思考三秒钟

  1. 原子性问题setnxexpire 不是原子的。如果 setnx 成功但服务器宕机没执行 expire,锁就永远死锁了。(Redis 2.6.12+ 支持 SET key value NX EX seconds 解决了原子性,但还有下一个问题)。
  2. 误删锁 :线程 A 持有锁,但业务执行时间超过 30 秒,锁自动过期释放。线程 B 抢到了锁。此时线程 A 执行完毕,去 del 锁,结果把线程 B 的锁给删了!
  3. 主从切换丢失:线程 A 在 Master 节点加锁,Master 还没同步给 Slave 就挂了。Slave 晋升为新 Master,锁丢了,线程 B 也能加锁。

2、生产级方案:Redisson 与"看门狗" (Watchdog)

原理深度解析:看门狗是如何工作的?

加锁逻辑

  • 调用 lock() 时,如果不指定 leaseTime(租约时间),Redisson 默认开启看门狗。

  • 初始锁过期时间设置为 30 秒 (lockWatchdogTimeout)。

  • Redis 中存储的结构不是简单的 String,而是一个 Hash

    Key: "lock_key"
    Value: { "uuid:threadId": 1 }
    // 重入次数为 1,如果是重入锁,value 会递增

  • 看门狗启动

    • Redisson在后台启动定时任务Timer,每隔 10 秒 (lockWatchdogTimeout / 3) 检查一次
    • 检查条件:如果当前线程还持有这把锁。
    • 动作 :将锁的过期时间重置回 30 秒
    • 效果:只要业务线程还在运行(没挂),锁就永远不会过期。
  • 释放锁

    • 业务执行完,调用 unlock()
    • Redisson执行Lua脚本:判断锁UUID 是否匹配,匹配则删除否则则不删(防止误删)。
    • 关键点:一旦手动解锁,看门狗定时任务会自动停止。
  • 故障场景

    • 如果业务线程宕机(断电、Kill -9),看门狗线程也一起死了。

    • 没人再去"续期"了。

    • 30 秒后,锁自动过期释放。其他线程可以重新竞争。完美解决死锁

      import org.redisson.api.RLock;
      import org.redisson.api.RedissonClient;
      import java.util.concurrent.TimeUnit;

      public class RedissonLockDemo {

      复制代码
      private final RedissonClient redissonClient;
      
      public void processOrder(String orderId) {
          String lockKey = "order_lock:" + orderId;
          RLock lock = redissonClient.getLock(lockKey);
      
          // 1. 尝试加锁,最多等待 5 秒,上锁以后 30 秒自动解锁(如果不指定第二个参数,则开启看门狗)
          // 注意:如果指定了 leaseTime (如 30 秒),看门狗不会启动!锁会在 30 秒后强制释放,即使业务没跑完。
          boolean isLocked = false;
          try {
              // 推荐用法:不指定 leaseTime,让看门狗自动续期
              isLocked = lock.tryLock(5, -1, TimeUnit.SECONDS); 
              
              if (isLocked) {
                  System.out.println("获取锁成功,开始处理订单...");
                  // 模拟业务耗时 40 秒 (超过默认的 30 秒)
                  Thread.sleep(40000); 
                  System.out.println("订单处理完成");
              } else {
                  System.out.println("获取锁失败,业务繁忙");
              }
          } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
          } finally {
              if (isLocked && lock.isHeldByCurrentThread()) {
                  lock.unlock(); // 安全释放
              }
          }
      }

      }

🌟 关于 Redlock 算法

为了解决 Redis 主从切换导致锁丢失的问题,Redis 作者提出了 Redlock 算法(向 N 个独立的 Redis 实例依次加锁,超过半数成功才算成功)。

  • 争议:Martin Kleppmann 等大牛指出 Redlock 在极端时钟跳变下仍不安全(少数情况)
  • 建议 :对于绝大多数业务(如秒杀扣库存),Redis 主从 + 哨兵/集群 + 看门狗 已经足够。如果对一致性要求极高(涉及资金),请考虑 Zookeeper数据库乐观锁

如何优化

第一层:代码与逻辑兜底

不要试图让锁本身 100% 完美,而要让业务逻辑能容忍锁的失效

1. 数据库乐观锁 (Optimistic Locking)

原理 :在数据库表中增加 version 字段或利用唯一索引。
作用:即使 Redis 锁失效,导致两个线程同时进入临界区,数据库层面的检查会拦截第二个提交者。

  • 场景:库存扣减、余额修改。

    -- 假设 Redis 锁失效,线程 A 和 B 都进来了
    -- 线程 A 执行:
    UPDATE stock SET count = count - 1, version = version + 1
    WHERE id = 100 AND version = 5; -- 成功,影响行数 1

    -- 线程 B 执行(此时数据库中 version 已经是 6 了):
    UPDATE stock SET count = count - 1, version = version + 1
    WHERE id = 100 AND version = 5; -- 失败,影响行数 0

  • 优化点 :在代码中判断 update 返回值,如果为 0,说明并发冲突,抛出异常或重试。 结论 :Redis 锁作为**"高性能过滤器"** 挡住 99% 的请求,数据库乐观锁作为**"最后一道防线"**保证数据绝对一致。

2.唯一索引防重 (Unique Constraint)

原理 :利用数据库的唯一约束(Unique Key)。
作用:防止重复下单、重复领取优惠券。

  • 操作 :建立 user_id + order_no 的唯一索引。
  • 效果 :即使 Redis 锁失效导致重复插入,数据库会报 DuplicateKeyException,捕获该异常即可。
3. 幂等性设计 (Idempotency)

原理 :业务逻辑本身支持重复执行而不产生副作用。
作用:即使锁失效导致接口被调用两次,结果也是一样的。

  • 方法
    • 状态机控制UPDATE order SET status = 'PAID' WHERE id = 1 AND status = 'UNPAID'。第二次执行时状态已变,SQL 不生效。
    • 去重表:先插入一张去重表(唯一索引),成功再执行业务。
第二层:Redis 架构与配置优化
1. 禁用主从复制的"异步"特性 (强一致性模式)

问题 :Master 写锁后,还没同步给 Slave 就挂了,Slave 晋升后锁丢失。
优化 :配置 Redis 的 min-slaves-to-writemin-slaves-max-lag

复制代码
# 至少有 1 个从节点且延迟不超过 10 秒,Master 才接受写请求
min-slaves-to-write 1
min-slaves-max-lag 10
  • 效果 :如果 Master 发现没有健康的 Slave,它会拒绝写入(返回错误),而不是冒险写入导致数据丢失。
  • 代价:可用性降低(网络抖动时可能无法加锁),但保证了数据一致性(CP 倾向)。
2. 使用 Redlock 算法 (谨慎使用)

原理:向 N 个(通常 5 个)独立的 Redis 实例依次申请锁,只有超过半数(N/2 + 1)成功,才算加锁成功。

  • 优点:即使挂掉几个节点,只要过半数活着,锁就是安全的。
  • 缺点
    • 性能差(需要往返 N 次网络)。
    • 实现复杂。
    • 争议 :分布式系统专家 Martin Kleppmann 指出,在发生时钟跳变GC 停顿时,Redlock 依然不安全。
  • 建议 :除非你对一致性要求极高且无法使用 ZK/Etcd,否则不推荐在常规业务中使用 Redlock。优先选择"Redis 单实例 + 数据库兜底"。
3. 调整看门狗 (Watchdog) 参数

问题 :默认 30 秒过期,10 秒续期。如果 Full GC 停顿超过 10 秒,看门狗没来得及续期,锁可能过期。
优化

  • 增大 lockWatchdogTimeout(如改为 60 秒)。
  • 确保 JVM 堆内存充足,减少 Full GC 频率和停顿时间。
  • 关键:业务逻辑中避免在持锁期间进行长时间的外部 RPC 调用或大对象序列化。

第三层:应对时钟跳变 (Clock Skew)

问题 :服务器时间突然回拨(如 NTP 同步),导致 Redis 认为锁已经过期并自动删除,而此时客户端认为自己还持有锁。
优化方案

  1. 禁用 NTP 大幅调整

    • 配置 Linux 的 ntpdchrony 使用 Slew Mode (平滑模式) 而不是 Step Mode。让时间慢慢追回,而不是瞬间跳变。
    • 命令:chronyc -a makestep 0 -1 (禁止步进调整)。
  2. 逻辑校验 (Token 机制)

    • Redisson 内部已经做了部分工作:释放锁时,会检查 Lua 脚本中的 UUID 是否匹配。
    • 但如果时间回拨导致锁在 Redis 端自动过期,UUID 校验也无效(因为锁已经没了)。
    • 根本解法 :还是回到第一层,依靠数据库版本号或状态机来防止并发修改。不要依赖时间来判断锁的有效性。

第四层:架构升级

如果业务场景是金融核心账务强一致性元数据管理,且完全不能容忍上述任何概率的异常

1.迁移到 Zookeeper / Etcd / Consul
  • 理由 :这些是基于 Raft 或 ZAB 协议的 CP 系统。它们通过多数派投票和日志复制,从根本上解决了主从切换丢锁的问题。
  • 代价:性能比 Redis 低一个数量级(毫秒级 vs 微秒级)。
  • 策略
    • 读写分离:读走 Redis,写(加锁)走 ZK。
    • 分级锁:高频低一致性要求用 Redis,低频高一致性要求用 ZK。
2. 数据库行锁 (Database Row Lock)
  • 理由 :直接利用 MySQL/PostgreSQL 的 SELECT ... FOR UPDATE
  • 优点:绝对可靠,天然事务支持。
  • 缺点:性能最差,数据库连接池容易爆满。
  • 适用:并发量不高(QPS < 500),但对数据一致性要求极高的场景。

第二部分:基于 Zookeeper 的分布式锁

Zookeeper (ZK) 是强一致性协议(ZAB),天生适合做分布式协调。它的锁机制基于 临时顺序节点

1. 核心原理:临时顺序节点

ZK 的数据结构像文件系统,每个节点叫 ZNode

  1. 创建节点

    • 客户端尝试在 /locks/order_123 目录下创建一个 临时顺序节点 (Ephemeral Sequential Node)。
    • 例如:/locks/order_123-000000001/locks/order_123-000000002
    • 临时:客户端会话断开(宕机),节点自动删除(防死锁)。
    • 顺序:ZK 保证全局唯一递增序号。
  2. 判断锁

    • 客户端获取 /locks/order_123 下的所有子节点。
    • 判断自己创建的节点是否是序号最小的那个。
    • :获得锁,执行业务。
    • :没获得锁。
  3. 监听等待 (Watch)

    • 如果没有拿到锁,客户端不需要轮询(浪费资源)。
    • 它只需要监听 比自己序号小 1 的那个节点 的删除事件。
    • 例如:你创建了 003,你只监听 002
    • 002 执行完业务删除节点后,ZK 通知你。你再次检查,发现自己是现在最小的了,获得锁。

2. 优缺点对比

特性 Redis (Redisson) Zookeeper
性能 极高 (内存操作,毫秒级) 较低 (磁盘同步,ZAB 协议开销)
一致性 最终一致性 (主从切换可能丢锁) 强一致性 (CP 模型,Leader 选举期间不可用)
实现复杂度 简单 (客户端库成熟) 较复杂 (需处理 Watch 注册/重连)
适用场景 高并发、允许极小概率不一致 (如秒杀、缓存重建) 对一致性要求极高、并发量适中 (如元数据管理、调度任务)
防死锁机制 看门狗 (心跳续期) 临时节点 (会话断开即删)

3. 代码实战 (Curator Framework)

复制代码
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import java.util.concurrent.TimeUnit;

public class ZkLockDemo {

    private final CuratorFramework client;

    public void processOrder(String orderId) {
        String lockPath = "/locks/order_" + orderId;
        // 创建互斥锁对象
        InterProcessMutex lock = new InterProcessMutex(client, lockPath);

        try {
            // 尝试获取锁,最多等待 10 秒
            if (lock.acquire(10, TimeUnit.SECONDS)) {
                try {
                    System.out.println("ZK 获取锁成功,处理业务...");
                    // 业务逻辑
                    Thread.sleep(5000);
                } finally {
                    lock.release(); // 释放锁,节点删除
                }
            } else {
                System.out.println("ZK 获取锁超时");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 底层细节acquire 方法内部会自动处理创建顺序节点、获取子列表、注册 Watcher、回调唤醒等复杂逻辑。

    // 伪代码:Zookeeper 分布式锁 acquire() 核心逻辑
    public boolean acquire(String lockPath, long waitTime) {
    String myNode = null;
    String predecessor = null; // 前一个节点的名称(比自己序号小1的节点)

    复制代码
      // 1. 创建临时顺序节点
      // 例如:/locks/order_123-000000005
      myNode = zkClient.createEphemeralSequential(lockPath + "/lock-", myData);
      print("我创建了节点: " + myNode);
    
      long startTime = System.currentTimeMillis();
    
      while (true) {
          // 2. 获取当前目录下所有子节点列表,并排序
          // 结果示例:["lock-000000001", "lock-000000003", "lock-000000005"(我), "lock-000000008"]
          List<String> children = zkClient.getChildren(lockPath);
          sort(children); 
    
          // 3. 判断自己是否是最小的节点
          if (myNode.equals(children.get(0))) {
              print(" 我是最小的,获得锁!");
              return true; // 成功获取锁
          }
    
          // 4. 如果不是最小,找到"前驱节点" (Predecessor)
          // 比如我是 005,前驱就是 003。我只需要监听 003 的删除事件。
          predecessor = findPredecessor(children, myNode);
          
          if (predecessor == null) {
              // 极端情况:列表变了,重试
              continue; 
          }
    
          // 5. 检查是否超时
          if (System.currentTimeMillis() - startTime > waitTime) {
              print("等待超时,放弃获取锁");
              // 删除自己创建的节点,释放资源
              zkClient.delete(myNode); 
              return false;
          }
    
          // 6. 注册 Watcher (监听前驱节点的删除事件)
          // 关键点:这里会阻塞当前线程,直到前驱节点被删除 或 超时
          print("我没拿到锁,开始监听前驱节点: " + predecessor);
          
          boolean eventReceived = zkClient.watchAndWaitForDelete(predecessor, waitTime);
    
          if (!eventReceived) {
              // 如果是超时返回,再次循环检查是否超时或重新竞争
              continue; 
          }
          
          // 7. 被唤醒 (前驱节点已删除)
          print("收到通知:前驱节点 " + predecessor + " 已删除,准备重新竞争...");
          // 循环回到第 2 步,重新获取列表,看自己是不是变成了最小
      }

    }

    // --- 辅助逻辑说明 ---

    // 释放锁 (release) 的逻辑非常简单:
    public void release(String myNode) {
    // 直接删除自己创建的临时节点
    zkClient.delete(myNode);
    // ZK 会自动通知监听该节点的下一个等待者
    }

如何优化Zookeeper的锁获取性能

ZK 的锁机制本质是:写操作(创建/删除节点)强一致,读操作(获取列表)相对轻量。性能瓶颈通常出现在高并发下的频繁写操作和大量的 Watcher 通知

1. 客户端代码层优化 (最直接有效)
A. 严格遵循"只监听前驱节点" (避免惊群)

这是 Curator InterProcessMutex 默认做的,但如果你手写 ZK 锁,绝对不能监听父节点!

  • 错误 :所有线程监听 /locks 目录的 NodeChildrenChanged 事件。锁释放时,1000 个线程同时被唤醒,瞬间发起 1000 次 getChildren 请求,ZK 直接被打挂。
  • 正确 :每个线程只监听序号比自己小 1 的那个节点NodeDeleted 事件。
    • 效果 :锁释放时,只有一个线程被唤醒。网络流量从 O(N)O(N) 降为 O(1)O(1) 。
B. 本地缓存子节点列表 (减少 getChildren 调用)

在竞争激烈的场景下,不要每次被唤醒都去 ZK 拉取全量子节点列表。

  • 优化策略
    1. 第一次调用 getChildren 获取列表并排序。
    2. 在内存中维护这个列表。
    3. 当收到前驱节点删除的 Watcher 通知时,仅在本地内存中移除该节点,重新判断自己是否最小。
    4. 只有在本地列表为空异常时,才再次请求 ZK 获取最新列表。
  • 收益:将大量的读请求从网络 IO 变为内存操作。
C. 批量操作与管道 (Pipeline)

如果业务允许(例如需要连续获取多个不同资源的锁),尽量合并请求。

  • 虽然 ZK 的原生 API 对单条命令支持有限,但 Curator 框架内部使用了批处理优化。确保使用最新版本的 Curator,它会自动合并某些元数据请求。
D. 减少锁的粒度与持有时间
  • 细粒度锁 :锁 order_123 而不是锁 all_orders。让不相关的业务并行执行。
  • 快速失败 :设置合理的 waitTime。如果短时间内拿不到锁,直接返回失败或降级,不要让线程在 ZK 上无限等待,占用会话资源。
  • 逻辑前置:能在本地校验的参数(如库存预检查),先在本地做完,再申请 ZK 锁。不要在持锁期间做耗时操作(如 RPC 调用、复杂计算)。
2. 架构设计层优化 (根本性解决)
A. "ZK 协调 + Redis 执行" 混合模式 (推荐 🔥)

ZK 擅长做协调 (谁该执行),不擅长做高频互斥

  • 方案
    1. 利用 ZK 的顺序节点特性,选出一个"Leader"或"执行者"。
    2. 只有拿到 ZK 锁的节点,才去 Redis 里执行真正的业务锁逻辑(或直接执行业务)。
    3. 或者:用 ZK 做低频的全局调度 (如每秒只允许一个任务运行),用 Redis 做高频的并发控制
  • 场景:定时任务调度、配置变更通知。
B. 分段锁 (Sharding / Striping)

如果所有请求都争抢同一个 ZK 节点(如 /lock/global),ZK 必死无疑。

方案:将锁分散到多个 ZK 路径上。

复制代码
// 根据订单 ID 哈希,路由到 100 个不同的锁路径之一
int index = orderId.hashCode() % 100;
String lockPath = "/locks/shard_" + index;
C. 本地锁 + 分布式锁 (两级锁)

在单机多核环境下,先抢本地锁 (ReentrantLock),抢到了再去抢 ZK 锁。

  • 流程
    1. 线程 A 尝试获取本地 synchronized
    2. 如果成功,再尝试获取 ZK 锁。
    3. 如果 ZK 锁失败,释放本地锁,休眠随机时间后重试。
  • 收益:过滤掉大量同一进程内的无效 ZK 请求。只有真正代表该进程出战的线程才会去访问 ZK。
3. ZK 服务端调优 (运维层面)
A. 调整会话超时时间 (tickTime, maxSessionTimeout)
  • 问题:超时时间太短,网络抖动会导致会话过期,触发大量临时节点删除和重建,造成"锁风暴"。
  • 建议 :适当调大 maxSessionTimeout(默认 40s,可调至 60s+),给网络波动留出缓冲期。但也不能太大,否则故障恢复慢。
B. 增加 ZK 集群节点数
  • ZK 的写性能受限于 Quorum (过半数) 确认机制。
  • 公式:写延迟 ≈≈ 网络往返 ×× (N/2 + 1)。
  • 建议
    • 3 节点集群:允许挂 1 台,写性能一般。
    • 5 节点集群:允许挂 2 台,写性能略降,但可用性高。
    • 不要超过 7 节点:节点越多,达成一致的开销越大,写性能反而下降。通常 5 或 7 是上限。
C. 独立部署 (专机专用)
  • 严禁将 ZK 与 Kafka、HBase 或其他高 IO 应用混部。
  • ZK 对磁盘延迟极其敏感(每次写都要刷盘 fsync)。必须使用 SSD,并单独部署在低负载机器上。
D. 快照与日志优化
  • 调整 snapCount:减少快照频率,避免频繁磁盘 IO 阻塞写请求。
  • dataDir (数据) 和 dataLogDir (日志) 分盘存放,避免 IO 争抢。

终极建议

如果你的业务场景是 每秒数万次的锁请求(如秒杀扣库存):

  1. 放弃纯 ZK 锁方案。ZK 的设计初衷是协调,不是高并发互斥。
  2. 采用 Redis (Redisson) 方案 抗住大部分流量。
  3. ZK 仅用于
    • 极低频的全局配置切换。
    • Master 选举。
    • 作为 Redis 挂掉后的降级兜底(平时不用,紧急时启用)。

第三部分:终极对决与选型指南

特性 Redis (Redisson) Zookeeper (上述伪代码)
等待方式 自旋 + 休眠 (不断尝试 setnx) 阻塞挂起 (OS 级别 wait,由 ZK 事件唤醒)
流量压力 高 (频繁请求 Redis) 低 (平时无请求,只有事件触发)
实现难度 依赖 Lua 脚本 + 定时任务 (看门狗) 依赖 ZK 的 Watcher 机制 + 临时节点
代码复杂度 客户端逻辑较重 客户端逻辑较轻,依赖服务端特性

1. 为什么 Redis 可能会"丢锁"?

在 Redis 主从架构下:

  1. 线程 A 在 Master 加锁成功。
  2. Master 还没来得及把数据同步给 Slave,Master 宕机。
  3. Slave 晋升为新 Master。
  4. 线程 B 在新 Master 上加锁,成功(因为刚才 A 的锁没同步过来)。
  5. 结果:A 和 B 同时持有锁。

解决方案权衡

  • 容忍:大多数业务(如防止重复提交、普通库存扣减)可以容忍极低概率的这种冲突,通过数据库唯一索引或乐观锁兜底。
  • Redlock:实现复杂,性能下降,且仍有理论缺陷。
  • 切换 ZK:如果业务绝对不能容忍并发冲突(如金融账务核心),直接用 ZK。

2. 为什么 ZK 不适合高并发秒杀?

  • 羊群效应 (Herd Effect):虽然 Curator 优化了监听(只监听前一个节点),但在超高并发下(如每秒 10 万请求),创建/删除大量临时节点会给 ZK 集群带来巨大的写压力和网络风暴。
  • 延迟:ZK 的写操作需要过半数节点确认,延迟通常在 10ms-50ms 级别,而 Redis 是 <1ms。

总结:分布式锁的"心法"

  1. 没有银弹:CAP 定理告诉我们,无法同时满足强一致性、高可用和分区容错性。Redis 偏向 AP(高可用),ZK 偏向 CP(强一致)。
  2. Redisson 是标配 :只要用 Redis 做锁,必须 用 Redisson 的看门狗机制。千万别自己写 setnx + expire
  3. 兜底思维 :分布式锁只是第一道防线。在涉及资金、库存等核心数据时,数据库层面的乐观锁 (version 字段) 或唯一约束才是最后的救命稻草。
  4. 粒度控制 :锁的粒度越细越好。锁 order_123 而不是锁 all_orders
相关推荐
iPadiPhone1 小时前
Java 反射机制底层原理、面试陷阱与实战指南
java·开发语言·后端·面试
iPadiPhone1 小时前
Java SPI 机制全链路深度解析与面试通关指南
java·后端·面试
wanhengidc2 小时前
服务器分布式存储的功能
运维·服务器·分布式
掘金安东尼2 小时前
⏰前端周刊第 456 期(v2026.3.15)
前端·javascript·面试
程序员爱钓鱼2 小时前
Go运行时系统解析: runtime包深度指南
后端·面试·go
码云数智-大飞2 小时前
分布式锁的三种实现方案:Redis、ZooKeeper与数据库的深度对比与选型指南
数据库·redis·分布式
星辰_mya2 小时前
线上故障排查实战经验总结一
java·开发语言·jvm·面试
填满你的记忆3 小时前
JVM 内存模型详解:Java 程序到底是如何运行的?
java·开发语言·jvm
xUxIAOrUIII3 小时前
【Kafka】快速入门
分布式·kafka