在高并发场景下,分布式锁是保证数据一致性的关键组件。但很多开发者对Redis分布式锁的理解停留在"SETNX"层面,导致线上事故频发。

本文将从原理、实现、常见误用到生产级解决方案,全面梳理Redis分布式锁的正确使用姿势。

为什么需要分布式锁?

单机应用中,我们使用synchronizedReentrantLock就能解决并发问题。但在分布式系统中:

  • 多个服务实例同时访问共享资源
  • 数据库事务无法覆盖跨服务的业务逻辑
  • 缓存与数据库一致性需要协调

这时就需要分布式锁来保证同一时间只有一个线程/进程能执行关键代码段。

基础实现:SETNX + EXPIRE

最简单的实现:

# 获取锁
SETNX lock_key 1
EXPIRE lock_key 30

# 释放锁
DEL lock_key

问题

  1. 原子性问题:SETNX和EXPIRE不是原子操作
  2. 死锁风险:如果获取锁后服务崩溃,锁永远不会释放
  3. 误删风险:释放锁时可能删除了其他客户端的锁

改进方案:SET NX PX + Lua脚本

# 获取锁(原子操作)
SET lock_key client_id NX PX 30000

# 释放锁(Lua脚本保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

优势

  • SET命令的NX和PX参数确保原子性
  • Lua脚本保证释放锁的原子性
  • client_id避免误删其他客户端的锁

生产级解决方案

方案1:Redisson(推荐)

Redisson提供了成熟的分布式锁实现:

RLock lock = redisson.getLock("myLock");
lock.lock(); // 自动续期
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

特性

  • 自动续期(Watchdog机制)
  • 可重入锁
  • 公平锁/非公平锁
  • 超时自动释放

方案2:Redission + Spring Boot

# application.yml
redisson:
  config: |
    singleServerConfig:
      address: "redis://127.0.0.1:6379"
      connectionMinimumIdleSize: 10
      connectionPoolSize: 64
      database: 0
      idleConnectionTimeout: 10000
      pingTimeout: 1000
      connectTimeout: 10000
      timeout: 3000
      retryAttempts: 3
      retryInterval: 1500

常见误用案例

案例1:锁过期时间设置不合理

  • 问题:设置过短导致业务未完成锁就过期
  • 后果:多个客户端同时获取到锁,数据不一致
  • 解决方案:根据业务最大执行时间 + 安全余量设置

案例2:没有处理锁续期

  • 问题:长耗时任务中锁过期
  • 后果:其他客户端获取锁并修改数据
  • 解决方案:使用自动续期机制或手动续期

案例3:忽略网络分区问题

  • 问题:网络抖动导致客户端认为获取锁成功
  • 后果:脑裂问题,多个客户端都认为自己持有锁
  • 解决方案:使用Redlock算法或强一致性存储

最佳实践清单

  1. 必须设置超时时间:防止死锁
  2. 使用唯一标识:避免误删其他客户端的锁
  3. 原子操作:获取和释放锁都需原子性
  4. 考虑自动续期:对于长耗时任务
  5. 监控告警:锁获取失败率、等待时间等指标
  6. 降级方案:当锁服务不可用时的备用策略

性能对比测试

方案 获取锁耗时 释放锁耗时 可靠性 适用场景
原生SETNX 0.5ms 0.3ms 测试环境
SET NX PX 0.6ms 0.4ms 简单业务
Redisson 1.2ms 0.8ms 生产环境
Redlock 2.5ms 1.5ms 极高 金融级系统

总结

Redis分布式锁不是简单的"加锁-解锁"操作,而是一套完整的并发控制方案。选择合适的实现方式,结合业务场景特点,才能真正发挥分布式锁的价值。

记住:锁只是手段,一致性才是目标

本文基于实际生产经验整理,欢迎在评论区分享你的分布式锁实战经验。