Redis 分布式锁:从 0 到 1 完整演变

从最简单的 setnx 到 Redisson 完整方案,一步步理解分布式锁的演进过程

为什么需要分布式锁?

单机环境 vs 分布式环境

复制代码
单机环境:
┌─────────────────┐
│    JVM 进程     │
│  ┌───────────┐  │
│  │ synchronized│ │  ← 单机锁可以解决
│  │ Lock      │  │
│  └───────────┘  │
└─────────────────┘

分布式环境:
┌───────────┐   ┌───────────┐   ┌───────────┐
│  服务 A   │   │  服务 B   │   │  服务 C   │
│  JVM 1    │   │  JVM 2    │   │  JVM 3    │
└─────┬─────┘   └─────┬─────┘   └─────┬─────┘
      │               │               │
      └───────────────┼───────────────┘
                      ↓
              ┌───────────────┐
              │   共享资源     │
              │  (MySQL/Redis)│
              └───────────────┘

问题:synchronized 只能锁住当前 JVM,无法锁住其他服务
解决:需要一个所有服务都能访问的"共享锁" → Redis

演进过程

第一阶段:最简单的实现

java 复制代码
public boolean tryLock(String key) {
    return redis.setnx(key, "1");
}

public void unlock(String key) {
    redis.del(key);
}

问题:死锁

复制代码
线程A 加锁 → 服务宕机 → 锁未释放 → 其他线程永远无法获取锁

第二阶段:加过期时间

java 复制代码
public boolean tryLock(String key) {
    redis.setnx(key, "1");
    redis.expire(key, 30, TimeUnit.SECONDS);
    return true;
}

问题:原子性问题

复制代码
setnx 成功 → 宕机 → expire 未执行 → 死锁

第三阶段:原子性加锁

java 复制代码
public boolean tryLock(String key) {
    return redis.set(key, "1", "NX", "EX", 30);
}
复制代码
SET key value NX EX 30

NX:不存在才设置
EX:设置过期时间(秒)

一条命令完成,原子性有保障

问题:误删别人的锁

复制代码
时间线:

T1: 线程A 加锁成功,过期时间 30s

T2: 线程A 业务执行时间过长(GC/网络慢),超过 30s

T3: 锁过期自动释放

T4: 线程B 加锁成功

T5: 线程A 执行完毕,执行 unlock → 删了线程B 的锁!

T6: 线程C 加锁成功 → 线程B 和 C 同时持有锁 → 并发问题

第四阶段:锁标识 + Lua 释放

java 复制代码
public boolean tryLock(String key) {
    String threadId = UUID.randomUUID().toString();
    return redis.set(key, threadId, "NX", "EX", 30);
}

public void unlock(String key, String threadId) {
    String script = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "    return redis.call('del', KEYS[1]) " +
        "else " +
        "    return 0 " +
        "end";
    redis.eval(script, key, threadId);
}

释放锁逻辑:

  1. 判断锁是不是自己的(比较 threadId)
  2. 是自己的才删除
  3. 用 Lua 脚本保证原子性

问题:锁过期但业务未执行完

复制代码
业务执行 40s,锁过期 30s → 锁提前释放 → 并发问题

第五阶段:看门狗自动续期

java 复制代码
public boolean tryLock(String key, String threadId) {
    boolean locked = redis.set(key, threadId, "NX", "EX", 30);
    if (locked) {
        new Thread(() -> {
            while (true) {
                Thread.sleep(10000);  // 每10s检查一次
                if (redis.exists(key) && 
                    redis.get(key).equals(threadId)) {
                    redis.expire(key, 30);  // 续期
                } else {
                    break;  // 锁不存在或不是自己的,停止续期
                }
            }
        }).start();
    }
    return locked;
}

问题:不可重入

复制代码
同一线程调用 methodA() 获取锁
methodA() 内部调用 methodB() 也需要同一把锁
→ 死锁(自己等自己)

第六阶段:可重入锁(Hash 结构)

java 复制代码
public boolean tryLock(String key, String threadId) {
    String script = 
        "if redis.call('exists', KEYS[1]) == 0 then " +
        "    redis.call('hset', KEYS[1], ARGV[1], 1) " +
        "    redis.call('expire', KEYS[1], ARGV[2]) " +
        "    return 1 " +
        "end " +
        "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
        "    redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
        "    redis.call('expire', KEYS[1], ARGV[2]) " +
        "    return 1 " +
        "end " +
        "return 0";
    return redis.eval(script, key, threadId, 30);
}

锁结构变化:

复制代码
String → Hash

原来:lock:key = "thread-1"
现在:lock:key = {
         "thread-1": 2    ← 重入次数
     }

逻辑:

  1. 锁不存在 → 创建,计数=1
  2. 锁存在且是当前线程 → 计数+1
  3. 锁存在但不是当前线程 → 返回失败

完整方案总结

复制代码
┌─────────────────────────────────────────────────────────────┐
│                  分布式锁完整方案                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  加锁:                                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  SET key value NX EX 30                             │    │
│  │  ↓                                                  │    │
│  │  启动看门狗线程(每10s续期)                          │    │
│  │  ↓                                                  │    │
│  │  Hash结构存储重入次数                                │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
│  解锁:                                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Lua脚本:                                           │    │
│  │  if 锁是当前线程 then                                │    │
│  │      计数-1                                         │    │
│  │      if 计数==0 then 删除锁                         │    │
│  │      else 重置过期时间                               │    │
│  │  end                                                │    │
│  │  停止看门狗                                          │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Redisson 实现(生产推荐)

基本使用

java 复制代码
@Service
public class VoteService {

    @Autowired
    private RedissonClient redissonClient;

    public void submitVote(Long voteId) {
        RLock lock = redissonClient.getLock("vote:lock:" + voteId);
        
        try {
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                // 执行业务逻辑
                doVote(voteId);
            } else {
                throw new RuntimeException("获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

看门狗机制

java 复制代码
// 不指定 leaseTime,启用看门狗
RLock lock = redissonClient.getLock("myLock");
lock.lock();  // 默认30s,看门狗自动续期

// 指定 leaseTime,禁用看门狗
lock.lock(10, TimeUnit.SECONDS);  // 10s后强制过期,不续期

看门狗工作流程:

复制代码
默认过期时间:30s
看门狗间隔:10s(过期时间/3)
自动续期:每隔10s重置为30s

┌──────────┐     ┌──────────┐     ┌──────────┐
│  加锁    │────→│ 启动定时 │────→│ 检查锁   │
│  30s     │     │ 任务10s  │     │ 是否持有 │
└──────────┘     └──────────┘     └──────────┘
                                        │
                      ┌─────────────────┴─────────────────┐
                      ↓                                   ↓
               ┌──────────┐                      ┌──────────┐
               │ 持有:续期│                      │ 未持有: │
               │ 重置30s  │                      │ 停止看门狗│
               └──────────┘                      └──────────┘

Redisson 加锁 Lua 脚本

lua 复制代码
if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hset', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
end; 
return redis.call('pttl', KEYS[1]);

参数说明:

复制代码
KEYS[1] = 锁名称
ARGV[1] = 过期时间
ARGV[2] = 线程标识

实际应用场景

场景1:防止重复提交

java 复制代码
public void submitOrder(Long orderId) {
    RLock lock = redissonClient.getLock("order:submit:" + orderId);
    if (!lock.tryLock()) {
        throw new RuntimeException("请勿重复提交");
    }
    try {
        // 创建订单
    } finally {
        lock.unlock();
    }
}

场景2:库存扣减

java 复制代码
public void deductStock(Long productId, Integer count) {
    RLock lock = redissonClient.getLock("stock:" + productId);
    try {
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            Integer stock = getStock(productId);
            if (stock < count) {
                throw new RuntimeException("库存不足");
            }
            updateStock(productId, stock - count);
        }
    } finally {
        lock.unlock();
    }
}

场景3:定时任务防并发

java 复制代码
@Scheduled(cron = "0 0 2 * * ?")
public void dailyTask() {
    RLock lock = redissonClient.getLock("task:daily");
    if (lock.tryLock()) {
        try {
            // 执行定时任务
        } finally {
            lock.unlock();
        }
    }
}

注意事项

问题 说明 解决方案
锁超时设置 过短业务未完,过长宕机释放慢 看门狗自动续期
异常导致死锁 业务异常未释放锁 finally 中释放锁
误删别人的锁 锁过期被其他线程获取 Lua 脚本判断锁标识
主从切换丢锁 Redis 主从同步延迟 Redlock 算法

演进路线总结

复制代码
v1: setnx                    → 死锁
v2: setnx + expire           → 原子性问题
v3: set nx ex                → 误删别人的锁
v4: set nx ex + Lua释放      → 锁过期业务未完
v5: + 看门狗续期              → 不可重入
v6: + Hash可重入              → 完整方案
v7: Redisson                 → 开箱即用

小结

Redis 分布式锁的演进过程,本质上是在解决以下问题:

  1. 原子性:加锁和设置过期时间必须原子
  2. 安全性:只能释放自己持有的锁
  3. 可靠性:业务未执行完锁不能过期
  4. 可重入:同一线程可多次获取同一把锁

生产环境推荐直接使用 Redisson,它已经帮我们解决了所有这些问题。

相关推荐
_千思_1 小时前
【小白说】数据库系统概念 5
数据库
落羽的落羽2 小时前
【Linux系统】磁盘ext文件系统与软硬链接
linux·运维·服务器·数据库·c++·人工智能·机器学习
树码小子2 小时前
Mybatis(17)Mybatis-Plus条件构造器(2)& 自定义 SQL
数据库·sql·mybatis-plus
橘子132 小时前
redis主从复制
数据库·redis·缓存
白太岁2 小时前
Redis:(5) 分布式锁实现:原子性设置锁与 Lua 释放锁
数据库·redis·分布式
zhu62019762 小时前
Postgres数据库docker快速安装
数据库·docker·容器
闲人编程2 小时前
定时任务与周期性调度
分布式·python·wpf·调度·cron·定时人物·周期性
Coder_Boy_3 小时前
Java高级_资深_架构岗 核心知识点全解析(模块四:分布式)
java·spring boot·分布式·微服务·设计模式·架构
专注前端30年3 小时前
【Java高并发系统与安全监控】高并发与性能调优实战:JVM+线程池+Redis+分库分表
java·jvm·redis