【中间件:Redis】5、Redis分布式锁实战:从基础实现到Redisson高级版(避坑指南)

在分布式系统中,当多个服务实例需要竞争同一资源(如秒杀库存、分布式任务调度)时,"分布式锁"是保证操作原子性的核心工具。

Redis凭借高性能、易部署的特点,成为实现分布式锁的主流选择。但很多开发者只知道"用SET NX加锁",却踩过"锁过期业务没执行完""主从切换锁丢失"等坑。

本文将从"基础实现→优化演进→Redisson高级版"逐步拆解,附完整Java代码和生产环境避坑指南,帮你彻底掌握Redis分布式锁的实战要点。

一、分布式锁的核心要求(面试必背)

一个可靠的分布式锁必须满足4个核心条件,少一个都可能出问题:

  1. 互斥性:同一时间只有一个服务能获取锁;
  2. 安全性:不能出现"释放别人的锁"的情况;
  3. 可用性:锁获取/释放过程不能阻塞,避免单点故障;
  4. 防死锁:即使服务宕机,锁也能自动释放(如设置过期时间)。

Redis分布式锁的实现,本质上是围绕这4个条件逐步优化的过程。

二、基础版:SET NX EX实现(最简化方案)

1. 核心原理

利用Redis的SET命令扩展参数:

bash 复制代码
SET lock_key unique_value NX EX 10
  • NX:只有当lock_key不存在时才设置成功(保证互斥性);
  • EX 10:设置10秒过期时间(防死锁,避免服务宕机后锁永远不释放);
  • unique_value:每个请求的唯一标识(如UUID,用于后续释放锁时验证"自己的锁",保证安全性)。

2. Java代码实现(Spring Boot)

java 复制代码
@Component
public class BasicRedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String LOCK_PREFIX = "lock:";
    private static final long LOCK_EXPIRE = 10; // 锁过期时间(秒)

    // 获取锁
    public String tryLock(String resource) {
        String lockKey = LOCK_PREFIX + resource;
        String uniqueValue = UUID.randomUUID().toString(); // 生成唯一标识
        // 执行SET NX EX命令
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS);
        return success != null && success ? uniqueValue : null; // 成功返回唯一值,失败返回null
    }

    // 释放锁(注意:此实现有坑,下文会优化)
    public void unlock(String resource, String uniqueValue) {
        String lockKey = LOCK_PREFIX + resource;
        // 先判断是否是自己的锁,再删除(注意:这两步非原子操作,有风险)
        String value = redisTemplate.opsForValue().get(lockKey);
        if (uniqueValue.equals(value)) {
            redisTemplate.delete(lockKey);
        }
    }
}

3. 存在的问题(面试高频坑)

  • 问题1:释放锁的"判断+删除"非原子操作
    假设锁的过期时间是10秒,当线程A执行到"判断是自己的锁"后,还没来得及删除,锁过期自动释放,线程B已获取新锁。此时线程A删除的是线程B的锁,导致互斥性失效。
  • 问题2:锁过期了,业务还没执行完
    若业务逻辑执行时间超过10秒,锁会提前释放,其他线程会获取到锁,导致"并发操作同一资源"。

三、优化版1:Lua脚本保证释放锁原子性

1. 核心优化点

用Lua脚本将"判断锁标识"和"删除锁"合并为一个原子操作,避免中间被打断。Redis会将整个Lua脚本作为单个命令执行,保证原子性。

2. Lua脚本释放锁(关键代码)

lua 复制代码
-- 脚本逻辑:只有当lock_key的值等于unique_value时,才删除锁
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

3. 优化后的Java代码

java 复制代码
@Component
public class LuaRedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String LOCK_PREFIX = "lock:";
    private static final long LOCK_EXPIRE = 10; // 秒
    // 释放锁的Lua脚本
    private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    // 获取锁(同基础版)
    public String tryLock(String resource) {
        String lockKey = LOCK_PREFIX + resource;
        String uniqueValue = UUID.randomUUID().toString();
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS);
        return success != null && success ? uniqueValue : null;
    }

    // 用Lua脚本释放锁(原子操作)
    public void unlock(String resource, String uniqueValue) {
        String lockKey = LOCK_PREFIX + resource;
        // 执行Lua脚本
        redisTemplate.execute(
                new DefaultRedisScript<>(UNLOCK_LUA, Integer.class),
                Collections.singletonList(lockKey), // KEYS[1]
                uniqueValue // ARGV[1]
        );
    }
}

4. 仍存在的问题

解决了"误删锁"的问题,但**"锁过期业务未完成"的问题仍存在**。例如:锁过期时间10秒,而业务逻辑需要15秒,第10秒锁释放后,其他线程会获取到锁,导致并发冲突。

四、优化版2:"看门狗"自动续期(解决锁过期问题)

1. 核心思路

当业务未执行完时,启动一个"看门狗"线程,定期(如每隔5秒)检查锁是否仍持有,若持有则延长锁的过期时间(如续期至10秒),避免锁提前释放。

2. Java代码实现(自定义看门狗)

java 复制代码
@Component
public class WatchDogRedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String LOCK_PREFIX = "lock:";
    private static final long LOCK_EXPIRE = 10; // 初始过期时间(秒)
    private static final long WATCH_DOG_INTERVAL = 5; // 看门狗续期间隔(秒)
    private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    // 存储看门狗线程,用于释放锁时停止
    private final Map<String, ScheduledFuture<?>> watchDogMap = new ConcurrentHashMap<>();

    // 获取锁并启动看门狗
    public String tryLock(String resource) {
        String lockKey = LOCK_PREFIX + resource;
        String uniqueValue = UUID.randomUUID().toString();
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS);
        if (success != null && success) {
            // 启动看门狗线程,定期续期
            ScheduledFuture<?> watchDog = Executors.newScheduledThreadPool(1)
                    .scheduleAtFixedRate(() -> {
                        // 续期:只有锁存在且是自己的,才延长过期时间
                        redisTemplate.execute(
                                new DefaultRedisScript<>(
                                        "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end",
                                        Integer.class
                                ),
                                Collections.singletonList(lockKey),
                                uniqueValue,
                                String.valueOf(LOCK_EXPIRE)
                        );
                    }, WATCH_DOG_INTERVAL, WATCH_DOG_INTERVAL, TimeUnit.SECONDS);
            watchDogMap.put(lockKey + ":" + uniqueValue, watchDog);
            return uniqueValue;
        }
        return null;
    }

    // 释放锁并停止看门狗
    public void unlock(String resource, String uniqueValue) {
        String lockKey = LOCK_PREFIX + resource;
        String key = lockKey + ":" + uniqueValue;
        // 执行Lua脚本释放锁
        redisTemplate.execute(
                new DefaultRedisScript<>(UNLOCK_LUA, Integer.class),
                Collections.singletonList(lockKey),
                uniqueValue
        );
        // 停止看门狗线程
        ScheduledFuture<?> watchDog = watchDogMap.get(key);
        if (watchDog != null) {
            watchDog.cancel(true);
            watchDogMap.remove(key);
        }
    }
}

3. 优化效果

  • 解决了"锁过期业务未完成"的问题:只要业务还在执行,看门狗会自动续期;
  • 业务执行完调用unlock时,会停止看门狗,避免资源浪费。

五、高级版:Redisson分布式锁(生产环境首选)

手动实现的分布式锁存在线程池管理、异常处理等细节问题,生产环境更推荐使用成熟框架------Redisson。它封装了"自动续期""可重入锁""公平锁"等高级特性,且经过大量实践验证。

1. Redisson的核心优势

  • 自动续期:内置看门狗,默认每30秒续期一次,无需手动实现;
  • 可重入性:支持同一线程多次获取锁(类似ReentrantLock);
  • 公平锁:避免线程饥饿,按请求顺序获取锁;
  • 异常安全:自动处理锁释放、线程中断等异常场景。

2. Redisson集成与使用(Spring Boot)

(1)引入依赖
xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.20.0</version>
</dependency>
(2)配置Redisson
yaml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
(3)代码实现(可重入锁示例)
java 复制代码
@Service
public class RedissonLockService {
    @Autowired
    private RedissonClient redissonClient;

    public void seckill(Long goodsId) {
        String lockKey = "lock:seckill:" + goodsId;
        // 获取可重入锁
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁:最多等待3秒,10秒后自动释放(未手动释放时)
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                throw new RuntimeException("获取锁失败,请重试");
            }

            // 执行业务逻辑(如扣减库存)
            deductStock(goodsId);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 释放锁(只有持有锁的线程才能释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void deductStock(Long goodsId) {
        // 实际库存扣减逻辑
        System.out.println("扣减商品" + goodsId + "库存成功");
    }
}

3. Redisson高级特性(按需使用)

  • 公平锁RLock fairLock = redissonClient.getFairLock(lockKey);------避免线程饥饿,适合对顺序性要求高的场景;
  • 读写锁RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);------读锁共享,写锁互斥,适合"多读少写"场景;
  • 红锁(Redlock)RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);------解决集群环境下的锁丢失问题(下文详解)。

六、集群场景下的坑:主从切换导致锁丢失

1. 问题描述

Redis主从复制是异步的:当主节点获取锁后,未同步到从节点就宕机,哨兵会将从节点升级为主节点。此时新主节点中没有锁信息,其他线程可重新获取锁,导致"一把锁被两个线程持有"。

2. 解决方案:Redlock算法(极端场景使用)

Redlock算法的核心是"多节点加锁":

  1. 向多个独立的Redis节点(如5个)请求加锁;
  2. 当超过半数节点(如3个)加锁成功,且总耗时不超过锁过期时间的1/3时,认为锁获取成功;
  3. 释放锁时,向所有节点发送释放请求。

Redisson已实现Redlock,代码示例:

java 复制代码
// 创建5个独立的Redis节点锁
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RLock lock4 = redissonClient4.getLock(lockKey);
RLock lock5 = redissonClient5.getLock(lockKey);

// 组合为红锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);

// 尝试获取锁:最多等待3秒,10秒自动释放
boolean locked = redLock.tryLock(3, 10, TimeUnit.SECONDS);
if (locked) {
    try {
        // 业务逻辑
    } finally {
        redLock.unlock();
    }
}

3. 注意事项

Redlock算法虽能解决集群锁丢失问题,但复杂度高、性能损耗大(需访问多个节点)。大部分场景下,可接受"主从切换瞬间的锁丢失风险"(概率极低),仅在金融级强一致性场景下使用Redlock。

七、生产环境避坑指南(实战经验)

  1. 锁的粒度要小 :避免用"大锁"(如lock:order),应细化到资源ID(如lock:order:123),减少锁竞争;
  2. 过期时间要合理:初始过期时间应大于业务最大耗时(如业务需5秒,设10秒),预留冗余;
  3. 避免长时间持有锁:锁内逻辑尽量精简(如只做库存扣减,不包含复杂计算),减少锁占用时间;
  4. 失败重试要有限制:获取锁失败时,重试次数不宜过多(如3次),避免线程阻塞;
  5. 监控锁状态 :用Redis的KEYS lock:*或监控工具(如Prometheus)跟踪锁的持有时间,及时发现异常。

八、面试高频题&标准答案

  1. 问:Redis分布式锁和ZooKeeper分布式锁有什么区别?
    答:① 实现方式:Redis用SET NX,ZooKeeper用临时节点;② 性能:Redis更高(内存操作);③ 可靠性:ZooKeeper更强(主从同步是同步的,无锁丢失风险);④ 适用场景:Redis适合高性能场景,ZooKeeper适合高可靠场景。
  2. 问:Redisson的看门狗原理是什么?
    答:当获取锁后,Redisson会启动一个定时任务(默认每30秒),检查锁是否仍被当前线程持有,若是则延长锁的过期时间(默认续期至30秒),直到业务执行完释放锁。
  3. 问:如何解决Redis主从切换导致的锁丢失问题?
    答:① 接受低概率风险(大部分场景);② 用Redlock算法(多节点加锁,超过半数成功才算获取锁);③ 结合本地锁(如先获取本地锁,再获取分布式锁,减少分布式锁竞争)。
  4. 问:分布式锁为什么需要uniqueValue?
    答:防止"误删别人的锁"。当锁过期后,若没有uniqueValue,线程A可能删除线程B的锁,导致互斥性失效。uniqueValue确保只有锁的持有者才能释放锁。

九、总结

Redis分布式锁的演进路径是"解决问题→发现新问题→再优化"的过程:

  • 基础版:SET NX EX实现互斥和防死锁,但存在误删锁风险;
  • 优化版1:Lua脚本保证释放锁原子性,解决误删问题;
  • 优化版2:看门狗自动续期,解决锁过期业务未完成问题;
  • 高级版:Redisson封装所有特性,生产环境首选;
  • 集群场景:极端情况用Redlock,多数场景可简化。

掌握这些演进逻辑和避坑点,就能在面试和实战中应对自如。

至此,Redis核心面试系列(线程模型→高并发→数据安全→缓存问题→分布式锁)已全部更新完毕。建议结合实际场景多动手实践,真正理解每个机制的"为什么",而非死记硬背。

如果觉得系列文章有用,欢迎收藏+转发,帮助更多开发者攻克Redis难点~

相关推荐
糖果店的幽灵2 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
爬也要爬着前进5 小时前
redis主从搭建
数据库·redis·缓存
Leo.yuan6 小时前
数据仓库建设怎么做?从源数据到分析报表全流程讲清
大数据·分布式·spark
用户095367515838 小时前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌10 小时前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆12 小时前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰12 小时前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_7275356212 小时前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf
阿标在干嘛13 小时前
从单机到分布式:政策快报爬虫系统的三次重构
分布式·爬虫·重构
笨鸟先飞的橘猫13 小时前
游戏后端分布式学习——开篇
分布式·学习·游戏