【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 小时前
JDK26中的List.ofLazy()
java·后端·java ee
小蒜学长11 小时前
基于SpringBoot的公寓报修管理系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·公寓报修管理系统·多角色协同
for_ever_love__11 小时前
MySQL 主从复制讲透:binlog 原理、搭建步骤与读写分离
java·数据库·mysql·binlog·读写分离·原理·主从复制
ly768912 小时前
Redis 内存优化与性能调优:编码转换、内存碎片与慢查询的定位链路
redis·性能调优·内存优化·慢查询·编码转换
小π军13 小时前
操作系统面试题
java·linux·服务器
倔强的石头10614 小时前
应用账号最小权限实践:读写账号、报表账号、运维账号分层
java·运维·开发语言
Shaoshing14 小时前
ThreadLocal
java·jvm·threadlocal
写后端的胖头鱼14 小时前
一文详解CAS
java·jvm·spring·cas·乐观锁·自旋
鱼宵14 小时前
Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用
java·人工智能·spring·few-shot·提示词工程·springai