【Redis】Redis分布式锁Day13(2026年)

写在前面

在分布式系统中,多个服务实例可能同时访问共享资源,如何保证同一时刻只有一个实例能操作资源?分布式锁是解决这个问题的经典方案。今天我们来深入学习Redis分布式锁的实现原理和最佳实践。

文章目录

    • 写在前面
    • 一、为什么需要分布式锁
      • [1.1 单机锁的局限性](#1.1 单机锁的局限性)
      • [1.2 分布式锁的需求](#1.2 分布式锁的需求)
      • [1.3 分布式锁的特性](#1.3 分布式锁的特性)
    • 二、SETNX实现分布式锁
      • [2.1 基本原理](#2.1 基本原理)
      • [2.2 基础实现](#2.2 基础实现)
      • [2.3 加锁失败重试](#2.3 加锁失败重试)
      • [2.4 SETNX方案的缺陷](#2.4 SETNX方案的缺陷)
    • 三、Redisson实现分布式锁
      • [3.1 Redisson简介](#3.1 Redisson简介)
      • [3.2 Redisson基本使用](#3.2 Redisson基本使用)
      • [3.3 看门狗机制](#3.3 看门狗机制)
      • [3.4 可重入锁](#3.4 可重入锁)
      • [3.5 读写锁](#3.5 读写锁)
      • [3.6 公平锁](#3.6 公平锁)
    • 四、Redlock算法
      • [4.1 Redlock原理](#4.1 Redlock原理)
      • [4.2 Redisson实现Redlock](#4.2 Redisson实现Redlock)
      • [4.3 Redlock的争议](#4.3 Redlock的争议)
    • 五、锁的续期
      • [5.1 为什么需要续期](#5.1 为什么需要续期)
      • [5.2 手动续期](#5.2 手动续期)
      • [5.3 Redisson自动续期](#5.3 Redisson自动续期)
    • 六、踩坑提醒
      • [6.1 锁超时释放](#6.1 锁超时释放)
      • [6.2 误删其他线程的锁](#6.2 误删其他线程的锁)
      • [6.3 主从切换丢锁](#6.3 主从切换丢锁)
      • [6.4 锁的可重入问题](#6.4 锁的可重入问题)
      • [6.5 常见问题汇总](#6.5 常见问题汇总)
    • 七、分布式锁对比
      • [7.1 Redis vs Zookeeper vs Database](#7.1 Redis vs Zookeeper vs Database)
      • [7.2 选择建议](#7.2 选择建议)
    • 八、面试高频考点
      • [8.1 Redis分布式锁的实现方式?](#8.1 Redis分布式锁的实现方式?)
      • [8.2 如何保证Redis分布式锁的可靠性?](#8.2 如何保证Redis分布式锁的可靠性?)
      • [8.3 Redis分布式锁和Zookeeper分布式锁的区别?](#8.3 Redis分布式锁和Zookeeper分布式锁的区别?)
      • [8.4 如何解决锁续期问题?](#8.4 如何解决锁续期问题?)
    • 九、参考资料
    • 十、互动话题

一、为什么需要分布式锁

1.1 单机锁的局限性

实际场景:电商系统部署了3个实例,用户购买商品时需要扣减库存,如何保证库存不会被超卖?

单机锁的问题

java 复制代码
// 单机锁只能保证单个JVM内的线程安全
public void deductStock(Long productId) {
    synchronized(this) {
        // 只能保证单个实例内的线程安全
        // 多实例部署时,synchronized失效
        int stock = getStock(productId);
        if (stock > 0) {
            updateStock(productId, stock - 1);
        }
    }
}

1.2 分布式锁的需求

场景 说明
库存扣减 防止超卖
订单创建 防止重复下单
定时任务 防止重复执行
秒杀活动 控制并发数量
分布式事务 保证原子性

1.3 分布式锁的特性

经验之谈:一个可靠的分布式锁需要满足以下条件。

特性 说明
互斥性 任意时刻只有一个客户端持有锁
防死锁 锁必须有超时机制,避免死锁
唯一性 只有持有锁的客户端才能释放锁
容错性 Redis部分节点宕机,锁仍然可用

二、SETNX实现分布式锁

2.1 基本原理

核心命令:SET key value NX PX milliseconds

  • NX:只有key不存在时才设置成功
  • PX:设置过期时间(毫秒)
redis 复制代码
# 加锁
SET lock:product:123 "uuid-xxx" NX PX 30000

# 解锁(需要使用Lua脚本保证原子性)
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:product:123 "uuid-xxx"

2.2 基础实现

java 复制代码
public class RedisLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 加锁
     * @param key 锁的key
     * @param value 锁的值(唯一标识,用于释放锁时校验)
     * @param expireTime 过期时间(毫秒)
     * @return 是否加锁成功
     */
    public boolean lock(String key, String value, long expireTime) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS)
        );
    }
    
    /**
     * 解锁
     * @param key 锁的key
     * @param value 锁的值
     * @return 是否解锁成功
     */
    public boolean unlock(String key, String value) {
        // 使用Lua脚本保证原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
        return result != null && result == 1L;
    }
}

2.3 加锁失败重试

实际场景:加锁失败时,需要重试等待,而不是直接返回失败。

java 复制代码
public boolean lockWithRetry(String key, String value, long expireTime, long waitTime) {
    long startTime = System.currentTimeMillis();
    
    while (true) {
        // 尝试加锁
        if (lock(key, value, expireTime)) {
            return true;
        }
        
        // 检查是否超时
        if (System.currentTimeMillis() - startTime > waitTime) {
            return false;
        }
        
        // 等待一段时间后重试
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

2.4 SETNX方案的缺陷

踩坑提醒:SETNX方案在Redis主从切换时可能出现问题。

问题 说明
锁超时释放 业务执行时间超过锁过期时间
主从切换丢锁 主节点加锁后宕机,从节点未同步
无法续期 长时间任务无法自动续期
单点故障 单Redis实例故障导致锁不可用

三、Redisson实现分布式锁

3.1 Redisson简介

经验之谈:Redisson是Redis官方推荐的Java分布式锁实现,提供了丰富的分布式对象和服务。

Redisson优势

  • 自动续期(看门狗机制)
  • 可重入锁
  • 公平锁
  • 读写锁
  • 联锁
  • 红锁(RedLock)

3.2 Redisson基本使用

java 复制代码
// 1. 配置Redisson
Config config = new Config();
config.useSingleServer()
    .setAddress("redis://127.0.0.1:6379")
    .setPassword("password");

RedissonClient redisson = Redisson.create(config);

// 2. 获取锁
RLock lock = redisson.getLock("lock:product:123");

try {
    // 3. 加锁(带自动续期)
    lock.lock();
    
    // 或者指定过期时间
    // lock.lock(30, TimeUnit.SECONDS);
    
    // 尝试加锁,最多等待100秒,锁过期时间10秒
    // boolean acquired = lock.tryLock(100, 10, TimeUnit.SECONDS);
    
    // 4. 执行业务逻辑
    doBusiness();
    
} finally {
    // 5. 释放锁
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

3.3 看门狗机制

核心原理:Redisson的看门狗机制会自动为锁续期,防止业务执行时间超过锁过期时间。

看门狗工作流程

复制代码
加锁成功 → 启动看门狗线程
    ↓
每隔10秒(过期时间/3)检查
    ↓
如果锁还被当前线程持有 → 续期30秒
    ↓
业务执行完成 → 停止看门狗

看门狗源码分析

java 复制代码
// Redisson内部实现
private void scheduleExpirationRenewal(long threadId) {
    // 每10秒执行一次续期
    Timeout task = commandExecutor.getConnectionManager()
        .newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                // 续期Lua脚本
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (res) {
                        // 续期成功,继续调度
                        scheduleExpirationRenewal(threadId);
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}

3.4 可重入锁

java 复制代码
public void methodA() {
    RLock lock = redisson.getLock("myLock");
    lock.lock();
    try {
        // 调用methodB,同一把锁可以重入
        methodB();
    } finally {
        lock.unlock();
    }
}

public void methodB() {
    RLock lock = redisson.getLock("myLock");
    lock.lock();
    try {
        // 执行业务逻辑
    } finally {
        lock.unlock();
    }
}

3.5 读写锁

实际场景:读多写少的场景,使用读写锁提高并发性能。

java 复制代码
// 获取读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("rwLock:product");

// 读锁(共享锁)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
    // 读取数据,多个线程可以同时读
    Product product = getProduct(id);
} finally {
    readLock.unlock();
}

// 写锁(排他锁)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
    // 写入数据,独占资源
    updateProduct(product);
} finally {
    writeLock.unlock();
}

3.6 公平锁

java 复制代码
// 公平锁:按照请求顺序获取锁
RLock fairLock = redisson.getFairLock("fairLock:order");

fairLock.lock();
try {
    // 按照请求顺序执行
    processOrder();
} finally {
    fairLock.unlock();
}

四、Redlock算法

4.1 Redlock原理

面试高频考点:Redlock是Redis作者提出的分布式锁算法,用于解决单点故障问题。

Redlock核心思想

  1. 获取当前时间戳
  2. 按顺序向N个Redis节点请求加锁
  3. 计算获取锁消耗的时间
  4. 如果在大多数节点(N/2+1)加锁成功,且消耗时间小于锁过期时间,则加锁成功
  5. 否则,向所有节点请求解锁

Redlock示意图

复制代码
┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       ├──→ Redis1 (加锁成功)
       │
       ├──→ Redis2 (加锁成功)
       │
       ├──→ Redis3 (加锁失败)
       │
       ├──→ Redis4 (加锁成功)
       │
       └──→ Redis5 (加锁成功)
       
结果:4/5成功,超过半数,加锁成功

4.2 Redisson实现Redlock

java 复制代码
// 配置多个Redis节点
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://redis1:6379");

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://redis2:6379");

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://redis3:6379");

RedissonClient client1 = Redisson.create(config1);
RedissonClient client2 = Redisson.create(config2);
RedissonClient client3 = Redisson.create(config3);

// 创建Redlock
RLock lock1 = client1.getLock("lock:product");
RLock lock2 = client2.getLock("lock:product");
RLock lock3 = client3.getLock("lock:product");

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    // 加锁
    redLock.lock();
    // 或者尝试加锁
    // boolean acquired = redLock.tryLock(100, 10, TimeUnit.SECONDS);
    
    // 执行业务
    doBusiness();
    
} finally {
    redLock.unlock();
}

4.3 Redlock的争议

踩坑提醒:Redlock算法存在争议,需要根据实际场景选择。

支持观点 反对观点
解决单点故障 时钟跳变可能导致问题
高可用性 实现复杂
官方推荐 性能开销大

Martin Kleppmann的质疑

  • 网络延迟可能导致锁失效
  • 时钟同步问题影响锁的正确性
  • GC暂停可能导致锁过期

Redis作者的回应

  • 建议使用物理时钟而非NTP同步
  • 锁过期时间要考虑网络延迟
  • 实际场景中问题很少出现

五、锁的续期

5.1 为什么需要续期

实际场景:业务执行时间不确定,如果锁过期时间太短,业务还没执行完锁就释放了。

问题场景

复制代码
时间轴:
├──────┼──────┼──────┼──────┤
0s     10s    20s    30s    40s
│      │      │      │
加锁   锁过期  │      │
       ↓      │      │
       其他线程获取锁  │
                    业务执行完成
                    ↓
                    尝试解锁(但锁已经不是自己的了)

5.2 手动续期

java 复制代码
public class RedisLockWithRenewal {
    
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public boolean lockWithRenewal(String key, String value, long expireTime) {
        // 加锁
        if (!lock(key, value, expireTime)) {
            return false;
        }
        
        // 启动续期任务
        scheduler.scheduleAtFixedRate(() -> {
            // 续期Lua脚本
            String script = 
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "    return redis.call('pexpire', KEYS[1], ARGV[2]) " +
                "else " +
                "    return 0 " +
                "end";
            redisTemplate.execute(RedisScript.of(script, Long.class), 
                Collections.singletonList(key), value, String.valueOf(expireTime));
        }, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
        
        return true;
    }
}

5.3 Redisson自动续期

经验之谈:推荐使用Redisson的看门狗机制,自动续期更可靠。

java 复制代码
RLock lock = redisson.getLock("myLock");

// 不指定过期时间,启用看门狗自动续期
lock.lock();

try {
    // 业务执行时间可能很长
    Thread.sleep(60000);  // 60秒
} finally {
    lock.unlock();
}

六、踩坑提醒

6.1 锁超时释放

踩坑提醒:业务执行时间超过锁过期时间,锁自动释放,其他线程获取锁,导致并发问题。

解决方案

  1. 设置合理的过期时间
  2. 使用Redisson看门狗自动续期
  3. 监控业务执行时间

6.2 误删其他线程的锁

踩坑提醒:线程A的锁过期释放,线程B获取锁,线程A执行完释放了线程B的锁。

错误示例

java 复制代码
// 错误:直接删除锁,可能删除其他线程的锁
redisTemplate.delete("lock:product");

正确做法

java 复制代码
// 正确:使用Lua脚本校验锁的归属
String script = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "    return redis.call('del', KEYS[1]) " +
    "else " +
    "    return 0 " +
    "end";
redisTemplate.execute(RedisScript.of(script, Long.class), 
    Collections.singletonList(key), value);

6.3 主从切换丢锁

踩坑提醒:主节点加锁后宕机,从节点未同步锁信息,导致锁丢失。

解决方案

  1. 使用Redlock算法
  2. 使用Redis Cluster
  3. 使用Zookeeper等一致性协议

6.4 锁的可重入问题

踩坑提醒:同一个线程多次获取同一把锁,需要支持可重入。

Redisson可重入锁原理

redis 复制代码
# Redis存储结构
HSET lock:myLock <threadId>:1 <count>
# value存储线程ID和重入次数

6.5 常见问题汇总

问题 原因 解决方案
锁无法释放 客户端宕机 设置过期时间
误删锁 锁过期被其他线程获取 使用唯一标识校验
主从切换丢锁 异步复制 使用Redlock
死锁 锁未释放 设置过期时间+监控
锁续期失败 网络问题 重试机制

七、分布式锁对比

7.1 Redis vs Zookeeper vs Database

对比项 Redis Zookeeper Database
性能
可靠性
实现复杂度 简单 中等 简单
一致性 最终一致 强一致 强一致
适用场景 高并发 高可靠 低并发

7.2 选择建议

场景 推荐方案
高并发、允许偶尔失败 Redis分布式锁
高可靠、强一致性 Zookeeper分布式锁
低并发、简单场景 数据库乐观锁
混合场景 Redis + 数据库双保险

八、面试高频考点

8.1 Redis分布式锁的实现方式?

答案

  1. SET NX EX:使用SET命令的NX和EX参数实现原子加锁
redis 复制代码
SET lock:key value NX EX 30
  1. SETNX + EXPIRE:先SETNX再EXPIRE(非原子,不推荐)

  2. Lua脚本:使用Lua脚本保证原子性

  3. Redisson:使用Redisson框架,提供完善的分布式锁实现

8.2 如何保证Redis分布式锁的可靠性?

答案

  1. 设置过期时间:防止死锁

  2. 唯一标识:使用UUID等唯一标识,防止误删

  3. Lua脚本:解锁时使用Lua脚本保证原子性

  4. 看门狗机制:自动续期,防止业务未完成锁过期

  5. Redlock算法:多节点部署,防止单点故障

  6. 监控告警:监控锁的持有时间和等待时间

8.3 Redis分布式锁和Zookeeper分布式锁的区别?

答案

对比项 Redis Zookeeper
实现方式 SET NX + 过期时间 临时顺序节点
一致性 最终一致 强一致
性能
锁释放 主动删除或过期 Session断开自动释放
适用场景 高并发 高可靠

8.4 如何解决锁续期问题?

答案

  1. 预估业务时间:设置足够长的过期时间

  2. 看门狗机制:Redisson自动续期

  3. 手动续期:定时任务续期

  4. 监控告警:监控业务执行时间


九、参考资料

  1. Redis官方文档 - SET命令
  2. Redisson官方文档
  3. Redlock算法详解

十、互动话题

  1. 你的项目中使用哪种分布式锁方案?遇到过什么问题?
  2. Redis分布式锁和Zookeeper分布式锁你会如何选择?
  3. 对于秒杀场景,分布式锁是否是最佳方案?还有什么替代方案?

欢迎在评论区分享你的经验和看法!


下期预告:Day14我们将学习Redis性能优化,深入理解慢查询分析、内存优化、网络优化等技巧。

相关推荐
心之伊始11 小时前
Java 后端接入大模型:从 Token、并发到推理成本的完整估算方法
java·spring boot·性能优化·大模型·llm
BlackTurn11 小时前
技术经理投标
java
YG亲测源码屋12 小时前
java配置环境变量、jdk环境变量配置、java环境变量设置方法
java·开发语言
MIUMIUKK12 小时前
从语法层面,看懂 Python 的特殊处
java·开发语言·python
hujinyuan2016012 小时前
2026年3月 中国电子学会青少年软件编程(Python)三级考试试卷 真题及答案
java·python·算法
basketball61612 小时前
C++ 高级编程:2. 基本线程池实现
java·开发语言·c++
MageGojo13 小时前
天气 API 接入实战:基于 ApiZero 实现实时天气、分钟级降水和 15 天预报查询
java·后端·spring·api 接口接入·接口实战
自动跟随13 小时前
UWB自动跟随技术全栈解析:从定位算法到“位控一体化“
java·网络·人工智能
喜欢打篮球的普通人13 小时前
LLVM 后端流程与关键数据结构:从 IR 到机器码的入门笔记
java·数据结构·笔记