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 小时前
(二)TIDB搭建正式集群
linux·数据库·tidb
姚不倒1 小时前
三节点 TiDB 集群部署与负载均衡搭建实战
运维·数据库·分布式·负载均衡·tidb
隔壁小邓1 小时前
批量更新方式与对比
数据库
数据知道1 小时前
MongoDB复制集架构原理:Primary、Secondary 与 Arbiter 的角色分工
数据库·mongodb·架构
人道领域1 小时前
苍穹外卖:菜品新增功能全流程解析
数据库·后端·状态模式
修行者Java1 小时前
(七)从 “非结构化数据难存储” 到 “MongoDB 灵活赋能”——MongoDB 实战进阶指南
数据库·mongodb
野犬寒鸦1 小时前
TCP协议核心:TCP详细图解及TCP与UDP核心区别对比(附实战解析)
服务器·网络·数据库·后端·面试
江一破1 小时前
InfluxDB 详细介绍
数据库·influxdb
小小unicorn1 小时前
[微服务即时通讯系统]文件存储子服务的实现与测试
c++·redis·微服务·云原生·架构