【中间件: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难点~

相关推荐
q***47432 小时前
【服务治理中间件】consul介绍和基本原理
中间件·consul
无心水2 小时前
【中间件:Redis】3、Redis数据安全机制:持久化(RDB+AOF)+事务+原子性(面试3大考点)
redis·中间件·面试·后端面试·redis事务·redis持久化·redis原子性
q***07142 小时前
【分布式】Hadoop完全分布式的搭建(零基础)
大数据·hadoop·分布式
KYumii2 小时前
RabbitMQ应用(1)
分布式·rabbitmq
麦嘟学编程3 小时前
快速配置 HBase 完全分布式(依赖已部署的 Hadoop+ZooKeeper)
hadoop·分布式·hbase
陈果然DeepVersion3 小时前
Java大厂面试真题:从Spring Boot到AI微服务的三轮技术拷问
spring boot·redis·微服务·ai·智能客服·java面试·rag
有梦想的攻城狮11 小时前
通过Lettuce实现PB3格式对象在Redis中的存储与查询
数据库·redis·缓存·pb3