侧边栏壁纸
博主头像
程序员进阶之路

技术之道,不可浅尝辄止;架构之路,须当支持以恒!

  • 累计撰写 18 篇文章
  • 累计创建 3 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

分布式锁深度解析:Redis、Redlock、ZooKeeper原理与生产实践

云端行笔
2026-02-05 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

摘要:本文全面解析Java分布式锁机制,涵盖分布式锁核心原理、Redis实现与Redlock算法、ZooKeeper实现、数据库乐观锁对比,以及生产环境实战经验与最佳实践。适合需要解决多服务实例并发问题的Java开发者。

前言:为什么分布式锁比想象中复杂

早些年我做过一个秒杀系统,上线后发现偶尔会出现超卖问题。排查后才知道是分布式锁用错了。用的是Redis的SETNX,看起来没问题,但实际在高并发下出现了锁竞争异常,导致同一个商品被多次售出。

那时候我对分布式锁的理解就是"加个锁就行",完全没意识到在分布式环境下,锁的实现要考虑:网络延迟、进程崩溃、时钟同步、锁过期、并发竞争等一系列问题。每一个细节没处理好,都可能导致锁失效。

这篇文章会系统梳理分布式锁的原理、各种实现方案的优劣、实际项目中的踩坑经验。希望能帮你避开我曾经踩过的坑。

一、分布式锁的核心概念

1.1 为什么需要分布式锁

单机环境下,Java的synchronized或ReentrantLock就能保证线程安全(可参考Java锁机制深度解析)。但分布式系统中,多个服务实例部署在不同机器上,内存不共享,本地锁就失效了。

问题场景

  // 秒杀扣库存
  public boolean reduceStock(Long productId) {
      synchronized (this) {  // 本地锁,只对当前JVM有效
          Product product = productDao.getById(productId);
          if (product.getStock() > 0) {
              product.setStock(product.getStock() - 1);
              productDao.update(product);
              return true;
          }
          return false;
      }
  }

部署三个实例:

  实例A: 执行synchronized → 获得本地锁 → 查库存=10 → 扣库存
  实例B: 执行synchronized → 获得本地锁 → 查库存=10 → 扣库存
  实例C: 执行synchronized → 获得本地锁 → 查库存=10 → 扣库存

三个实例同时执行,库存被扣了三次,出现超卖。

根本原因:每个实例的锁只保护自己的进程,不同进程之间没有协调机制。

1.2 分布式锁的核心要求

一个可靠的分布式锁必须满足:

互斥性:同一时刻只有一个客户端持有锁

防死锁:即使客户端崩溃,锁也能自动释放

容错性:锁服务故障时,不影响业务(可能退化)

可重入:同一线程可以多次获取锁

公平性(可选):按请求顺序分配锁

锁续期:长时间任务执行时,锁不会自动过期

1.3 分布式锁的实现方式对比

二、Redis分布式锁详解

2.1 基础实现:SETNX

最早期的实现使用SETNX命令:

  public class RedisLock {
  
      private Jedis jedis;
      private String lockKey;
      private String lockValue;
  
      public boolean tryLock() {
          lockValue = UUID.randomUUID().toString();
          Long result = jedis.setnx(lockKey, lockValue);
          if (result == 1) {
              return true;
          }
          return false;
      }
  
      public void unlock() {
          jedis.del(lockKey);
      }
  }

问题1:死锁

如果获取锁后进程崩溃,锁永远不会释放。

解决:设置过期时间

  public boolean tryLock() {
      lockValue = UUID.randomUUID().toString();
      Long result = jedis.setnx(lockKey, lockValue);
      if (result == 1) {
          jedis.expire(lockKey, 30);  // 30秒过期
          return true;
      }
      return false;
  }

问题2:SETNX和EXPIRE不是原子操作

如果SETNX成功后,还没执行EXPIRE就崩溃,仍然会死锁。

解决:使用SET命令的NX和EX选项(Redis 2.6.12+)

public boolean tryLock(long expireTime) {
    lockValue = UUID.randomUUID().toString();
    String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
    return "OK".equals(result);
}

这是原子操作,要么同时成功,要么同时失败。

2.2 锁续期问题

设置了30秒过期,但如果任务执行超过30秒怎么办?

解决:看门狗机制(Watchdog)

给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

  public class RedisLockWithWatchdog {
  
      private Jedis jedis;
      private String lockKey;
      private String lockValue;
      private ScheduledExecutorService watchdog;
      private volatile boolean locked = false;
  
      public boolean tryLock(long expireTime) {
          lockValue = UUID.randomUUID().toString();
          String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
          if ("OK".equals(result)) {
              locked = true;
              startWatchdog(expireTime);
              return true;
          }
          return false;
      }
  
      // 看门狗:定期续期
      private void startWatchdog(long expireTime) {
          watchdog = Executors.newSingleThreadScheduledExecutor();
          watchdog.scheduleAtFixedRate(() -> {
              if (locked) {
                  jedis.expire(lockKey, expireTime);
              }
          }, expireTime / 3, expireTime / 3, TimeUnit.SECONDS);
      }
  
      public void unlock() {
          locked = false;
          if (watchdog != null) {
              watchdog.shutdown();
          }
          // 只释放自己的锁
          String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                          "return redis.call('del', KEYS[1]) else return 0 end";
          jedis.eval(script, 1, lockKey, lockValue);
      }
  }

看门狗每expireTime/3秒续期一次,保持锁不过期。

2.3 锁误释放问题

客户端A的锁过期后,被客户端B获取。A执行完释放锁,实际释放了B的锁。

解决:释放锁时检查是否是自己的锁

public void unlock() {
    // Lua脚本保证原子性
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "return redis.call('del', KEYS[1]) else return 0 end";
    jedis.eval(script, 1, lockKey, lockValue);
}

只有lockValue匹配时才删除,避免误删别人的锁。

2.4 Redisson实现

Redisson是Redis分布式锁的成熟实现,推荐直接使用:

  public class RedissonLockDemo {
  
      private RedissonClient redisson;
  
      public void process() {
          RLock lock = redisson.getLock("my-lock");
  
          try {
              // 尝试获取锁,最多等待100秒,锁自动过期30秒
              boolean acquired = lock.tryLock(100, 30, TimeUnit.SECONDS);
              if (acquired) {
                  // 执行业务逻辑
                  doBusiness();
              } else {
                  // 获取锁失败
                  handleLockFailed();
              }
          } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
          } finally {
              // 只释放自己持有的锁
              if (lock.isHeldByCurrentThread()) {
                  lock.unlock();
              }
          }
      }
  }

Redisson的优势:

  • 看门狗自动续期:默认30秒过期,看门狗每10秒续期

  • 可重入锁:同一线程可以多次获取

  • 公平锁:支持公平锁模式

  • 联锁:可以同时锁定多个资源

  • 读写锁:支持分布式读写锁

  • 红锁:多节点Redlock算法

2.5 Redlock算法

Redis主从架构下,主节点获取锁后还没同步到从节点就崩溃,从节点升级为主节点,新客户端也能获取锁,出现两个锁。

Redlock解决方案

在多个独立的Redis节点上同时获取锁,只有多数节点成功才算真正获取锁。

  public class RedlockDemo {
  
      private RedissonClient client1;
      private RedissonClient client2;
      private RedissonClient client3;
  
      public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
          RLock lock1 = client1.getLock(lockKey);
          RLock lock2 = client2.getLock(lockKey);
          RLock lock3 = client3.getLock(lockKey);
  
          RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
  
          try {
              return redLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
          } catch (InterruptedException e) {
              return false;
          }
      }
  
      public void unlock(String lockKey) {
          RLock lock1 = client1.getLock(lockKey);
          RLock lock2 = client2.getLock(lockKey);
          RLock lock3 = client3.getLock(lockKey);
  
          RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
          redLock.unlock();
      }
  }

Redlock流程

争议

Redlock算法在学术界有争议,主要问题:

  • 多节点时钟同步假设

  • 网络分区情况下的行为

实际项目中,如果可靠性要求极高,建议用ZooKeeper。

三、ZooKeeper分布式锁详解

3.1 ZooKeeper锁原理

ZooKeeper的顺序临时节点实现锁:

创建顺序临时节点

每个客户端在锁目录下创建临时顺序节点:

判断是否获得锁

由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。其他客户端监听前一个节点的删除事件。

临时节点特性

客户端会话断开时,临时节点自动删除,避免死锁。

3.2 ZooKeeper锁实现

使用Apache Curator框架:

  public class ZooKeeperLockDemo {
  
      private CuratorFramework client;
  
      public void process() {
          InterProcessMutex lock = new InterProcessMutex(client, "/locks/my-lock");
  
          try {
              // 尝试获取锁,最多等待10秒
              boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
              if (acquired) {
                  // 执行业务逻辑
                  doBusiness();
              }
          } catch (Exception e) {
              e.printStackTrace();
          } finally {
              try {
                  // 释放锁
                  lock.release();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
  }

Curator提供的锁类型

3.3 ZooKeeper锁的优势

可靠性高

  • ZooKeeper的CP架构,保证一致性

  • 临时节点自动清理,不会死锁

  • 会话断开自动释放锁

顺序性保证

节点顺序保证公平锁,先请求的先获得锁。

可监听

Watch机制实时感知锁状态变化。

3.4 ZooKeeper锁的劣势

性能较低

每次锁操作需要多次ZooKeeper交互:

  • 创建节点

  • 查询节点列表

  • 判断顺序

  • 设置监听

高并发下性能不如Redis。

依赖ZooKeeper集群

需要维护ZooKeeper集群,增加运维成本。

四、数据库分布式锁

4.1 基于唯一索引

利用数据库唯一索引的互斥性:

  CREATE TABLE distributed_lock (
      lock_key VARCHAR(64) PRIMARY KEY,
      holder_id VARCHAR(64),
      create_time TIMESTAMP
  );
  public class DatabaseLock {
  
      private DataSource dataSource;
  
      public boolean tryLock(String lockKey, String holderId) {
          try (Connection conn = dataSource.getConnection()) {
              String sql = "INSERT INTO distributed_lock (lock_key, holder_id, create_time) " +
                           "VALUES (?, ?, NOW())";
              PreparedStatement ps = conn.prepareStatement(sql);
              ps.setString(1, lockKey);
              ps.setString(2, holderId);
              ps.executeUpdate();
              return true;
          } catch (SQLException e) {
              // 唯一索引冲突,说明锁已被占用
              return false;
          }
      }
  
      public void unlock(String lockKey, String holderId) {
          try (Connection conn = dataSource.getConnection()) {
              String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND holder_id = ?";
              PreparedStatement ps = conn.prepareStatement(sql);
              ps.setString(1, lockKey);
              ps.setString(2, holderId);
              ps.executeUpdate();
          } catch (SQLException e) {
              e.printStackTrace();
          }
      }
  }

问题:锁没有过期机制,如果客户端崩溃,锁永远不释放。

解决:定时清理超时锁

  -- 定时任务清理超过5分钟的锁
  DELETE FROM distributed_lock WHERE create_time < NOW() - INTERVAL 5 MINUTE

4.2 乐观锁

乐观锁不真正加锁,而是用版本号控制:

  CREATE TABLE product (
      id BIGINT PRIMARY KEY,
      name VARCHAR(100),
      stock INT,
      version INT DEFAULT 0
  );
  public boolean reduceStock(Long productId) {
      // 查询当前版本
      Product product = productDao.getById(productId);
  
      if (product.getStock() <= 0) {
          return false;
      }
  
      // 更新时检查版本
      int rows = productDao.updateWithVersion(
          productId,
          product.getStock() - 1,
          product.getVersion()
      );
  
      return rows > 0;  // 更新成功说明获取到锁
  }
  UPDATE product
  SET stock = ?, version = version + 1
  WHERE id = ? AND version = ?

如果版本不匹配,更新失败,说明有其他线程已修改。

乐观锁适用场景

  • 读多写少

  • 冲突概率低

  • 可以容忍失败重试

4.3 数据库锁的优缺点

优点

  • 实现简单,不需要额外组件

  • 利用已有数据库基础设施

  • 可靠性依赖数据库的事务特性

缺点

  • 性能较低,每次锁操作都要访问数据库

  • 获取锁失败需要轮询重试

  • 数据库压力增大

五、实战经验与踩坑总结

5.1 秒杀系统的锁优化

问题场景

秒杀商品库存扣减,使用Redis分布式锁。高峰期每秒上万请求,锁竞争严重。

原始实现

  public boolean reduceStock(Long productId) {
      RLock lock = redisson.getLock("stock:" + productId);
      try {
          lock.lock();
          Product product = productDao.getById(productId);
          if (product.getStock() > 0) {
              product.setStock(product.getStock() - 1);
              productDao.update(product);
              return true;
          }
          return false;
      } finally {
          lock.unlock();
      }
  }

问题分析

  • 每次请求都竞争锁,大量请求等待

  • 锁内包含数据库查询,耗时较长

  • 锁粒度太大,阻塞所有库存操作

优化方案

  public boolean reduceStock(Long productId) {
      // 1. 先检查库存(不加锁)
      Product product = productDao.getById(productId);
      if (product.getStock() <= 0) {
          return false;  // 库存不足直接返回
      }
  
      // 2. Redis预扣库存(原子操作)
      Long remain = redisTemplate.opsForValue().decrement("stock:cache:" + productId);
      if (remain < 0) {
          // 库存不足,回滚Redis
          redisTemplate.opsForValue().increment("stock:cache:" + productId);
          return false;
      }
  
      // 3. 分布式锁+数据库扣库存
      RLock lock = redisson.getLock("stock:" + productId);
      try {
          lock.lock();
          // 异步写数据库(不阻塞)
          asyncUpdateStock(productId);
          return true;
      } finally {
          lock.unlock();
      }
  }

优化效果

  • Redis预扣库存,减少锁竞争

  • 异步写数据库,不阻塞请求

  • 锁内操作减少,吞吐量提升10倍

5.2 分布式锁超时问题

问题场景

任务执行时间超过锁过期时间,锁自动释放,另一个客户端获取锁,两个客户端同时执行。

踩坑经历

  // 锁过期30秒
  RLock lock = redisson.getLock("process-lock");
  lock.lock(30, TimeUnit.SECONDS);
  
  try {
      // 执行任务(实际耗时50秒)
      longRunningTask();
  } finally {
      lock.unlock();  // 此时锁已过期,释放了别人的锁
  }

解决方案

  RLock lock = redisson.getLock("process-lock");
  // 不指定过期时间,使用看门狗自动续期
  lock.lock();
  
  try {
      longRunningTask();
  } finally {
      // 只释放自己持有的锁
      if (lock.isHeldByCurrentThread()) {
          lock.unlock();
      }
  }

Redisson默认30秒过期,看门狗每10秒续期,保持锁不过期。

5.3 锁等待超时处理

问题场景

高并发下,大量请求等待锁,等待超时后请求失败,用户体验差。

优化方案

  public boolean processWithLock(Long orderId) {
      RLock lock = redisson.getLock("order:" + orderId);
  
      try {
          // 尝试获取锁,最多等待100ms
          boolean acquired = lock.tryLock(100, TimeUnit.MILLISECONDS);
          if (acquired) {
              return processOrder(orderId);
          } else {
              // 获取锁失败,降级处理
              return fallbackProcess(orderId);
          }
      } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          return false;
      } finally {
          if (lock.isHeldByCurrentThread()) {
              lock.unlock();
          }
      }
  }
  
  // 降级处理:记录请求,异步处理
  private boolean fallbackProcess(Long orderId) {
      asyncQueue.add(orderId);
      return true;  // 返回"已提交",后台异步处理
  }

关键点

  • 设置合理的等待时间(如100ms)

  • 等待失败时降级处理,不阻塞用户

  • 异步队列兜底,保证请求不丢失

5.4 锁粒度优化

问题场景

锁粒度太大,不相关的操作也被阻塞。

原始实现

  // 锁整个订单处理
  RLock lock = redisson.getLock("order-process");
  lock.lock();
  try {
      // 处理订单(包括:库存扣减、优惠券核销、积分计算、日志记录)
      processOrder();
  } finally {
      lock.unlock();
  }

优化方案

  // 分段锁:不同操作用不同的锁
  public void processOrder(Order order) {
      // 库存锁
      reduceStockWithLock(order.getProductId());
  
      // 优惠券锁
      useCouponWithLock(order.getCouponId());
  
      // 积分锁
      calculatePointsWithLock(order.getUserId());
  
      // 日志记录(不需要锁)
      recordLog(order);
  }
  
  private void reduceStockWithLock(Long productId) {
      RLock lock = redisson.getLock("stock:" + productId);
      try {
          lock.lock();
          reduceStock(productId);
      } finally {
          lock.unlock();
      }
  }

效果

  • 不同商品的库存扣减可以并行

  • 不需要同步的操作不加锁

  • 整体吞吐量提升

5.5 锁与事务的协调

问题场景

锁在事务内获取,事务还没提交锁就释放了。

  @Transactional
  public void process(Long orderId) {
      RLock lock = redisson.getLock("order:" + orderId);
      lock.lock();
      try {
          updateOrder(orderId);
          // 锁释放
      } finally {
          lock.unlock();  // 释放锁
      }
      // 事务提交(此时锁已释放,其他线程可以修改)
  }

另一个线程获取锁,但前一个线程的事务还没提交,读到的数据是旧值。

解决方案

  public void process(Long orderId) {
      RLock lock = redisson.getLock("order:" + orderId);
      lock.lock();
      try {
          // 在锁内调用事务方法
          transactionalUpdate(orderId);
      } finally {
          lock.unlock();  // 事务提交后释放锁
      }
  }
  
  @Transactional
  private void transactionalUpdate(Long orderId) {
      updateOrder(orderId);
  }

锁包裹事务,确保事务提交后才释放锁。

六、分布式锁最佳实践

6.1 选择合适的实现方案

6.2 锁的使用原则

原则1:锁范围最小化

  // 不推荐
  lock.lock();
  try {
      validateData();  // 不需要锁
      processData();   // 需要锁
      sendNotification();  // 不需要锁
  } finally {
      lock.unlock();
  }
  
  // 推荐
  validateData();
  lock.lock();
  try {
      processData();
  } finally {
      lock.unlock();
  }
  sendNotification();

原则2:设置合理的过期时间

过期时间应该大于任务执行时间,但也不能太长(客户端崩溃后锁释放太慢)。

建议:

  • 简单操作:10-30秒

  • 复杂操作:使用看门狗自动续期

原则3:正确释放锁

  finally {
      // 只释放自己持有的锁
      if (lock.isHeldByCurrentThread()) {
          lock.unlock();
      }
  }

原则4:锁失败降级处理

不要让用户长时间等待,获取锁失败时降级处理:

  boolean acquired = lock.tryLock(100, TimeUnit.MILLISECONDS);
  if (!acquired) {
      // 降级:记录请求,异步处理
      asyncQueue.add(request);
      return "已提交,请稍后查看结果";
  }

6.3 监控与告警

监控指标

  • 锁获取成功率

  • 锁等待时间

  • 锁持有时间

  • 锁竞争次数

告警规则

  • 锁获取成功率 < 80%:告警

  • 锁等待时间 > 1秒:告警

  • 锁持有时间 > 预期时间2倍:告警

七、常见问题解答

Q1:Redis锁和ZooKeeper锁哪个好?

Redis性能好,适合高并发场景。ZooKeeper可靠性高,适合对一致性要求高的场景。根据需求选择,不是哪个绝对好。

Q2:Redlock算法必须用吗?

不是。Redlock解决主从切换丢锁问题,但增加了复杂度。如果业务允许偶尔锁失效(如非关键操作),不需要Redlock。

Q3:看门狗机制会一直续期吗?

不会。客户端主动释放锁,或者会话断开,看门狗停止。Redisson的实现是安全的。

Q4:分布式锁能保证绝对安全吗?

不能。网络分区、时钟问题等极端情况可能导致锁失效。关键业务要有兜底方案(如事后校验)。

Q5:如何测试分布式锁的正确性?

测试并发竞争、锁过期、客户端崩溃、网络延迟等场景。可以用JMeter模拟高并发,或写测试脚本随机杀死进程。

Q6:分布式锁可以用在数据库事务中吗?

可以,但要正确处理顺序:先获取锁,再开启事务,事务提交后释放锁。不要在事务内获取锁。

八、总结

分布式锁不是"加个锁就行",而是要考虑:

实现选择

  • Redis:高性能,适合高并发

  • ZooKeeper:高可靠,适合一致性要求高

  • 数据库:简单,适合已有基础设施

Redis关键点

  • SET命令原子加锁

  • 看门狗自动续期

  • Lua脚本安全释放

  • Redlock解决主从问题

ZooKeeper关键点

  • 顺序临时节点

  • 自动清理,不死锁

  • Curator简化实现

实战经验

  • 锁粒度要小

  • 过期时间要合理

  • 释放锁要检查所有权

  • 失败要有降级方案

  • 监控告警要完善

用好分布式锁,能让分布式系统安全可靠。但关键是理解每种实现的优缺点,根据场景选择合适的方案,并做好兜底准备。

0

评论区