基于 Redis 的分布式锁:原理剖析与 Spring Boot 实战(含看门狗续期)

一、什么是分布式锁?

在单机应用中,我们常用synchronizedReentrantLock解决多线程并发问题,但这些锁的作用域局限于单个 JVM 进程。

当系统从单体架构演进为分布式集群时,多个服务实例、多个进程会并发操作共享资源(如库存、订单),此时本地锁已无法保证数据一致性,分布式锁应运而生。

分布式锁的核心目标:

  1. 互斥性:同一时刻只有一个客户端能持有锁
  2. 防死锁:锁必须能被释放,避免客户端崩溃导致锁永久占用
  3. 解铃还须系铃人:只能由加锁者自己释放锁,防止误删他人锁
  4. 高可用:锁服务本身不能成为单点故障

二、基于 Redis 的分布式锁核心原理

Redis 因其高性能、原子操作特性,成为实现分布式锁的主流选择,核心依赖以下几个关键能力:

1. 加锁:SET key value NX EX seconds 原子指令

  • NX:仅当 key 不存在时才设置,保证互斥性
  • EX seconds:设置 key 的过期时间,避免客户端崩溃导致死锁
  • value:设置为客户端唯一 ID(如 UUID),用于后续判断锁的归属

2. 解锁:Lua 脚本原子删除

直接用DEL key解锁存在风险:若锁已过期,当前客户端可能误删其他客户端的锁。因此需要通过 Lua 脚本保证 "判断锁归属 + 删除锁" 的原子性:

复制代码
-- 只有锁属于当前客户端,才执行删除
if redis.call('get',KEYS[1]) == ARGV[1] then 
    return redis.call('del',KEYS[1]) 
else 
    return 0 
end

3. 看门狗机制:解决业务超时锁释放问题

上面的时间轴,帮助我们理解不加看门狗的时候,如果业务执行时间超过锁的过期时间,锁会被自动释放,导致并发安全问题。

看门狗机制则会在业务执行期间,周期性(通常为锁过期时间的 1/3)续期锁的过期时间,直到业务执行完成:

复制代码
时间轴(秒) →  0        5        10       15
事务A(业务):  ├──────────────────────┤ (执行完成)
锁A(分布式锁):├──续期→├──续期→├──续期→┤ (始终有效)
事务B(其他业务):        (全程拿不到锁,阻塞等待)

三、Spring Boot + Jedis 4.x 实战实现

理解了上述原理,接下来请跟我一起实现吧!

1. 环境准备:依赖引入redis.clients:jedis:4.4.3

2.Jedis 连接池配置(适配 Jedis 4.x)

复制代码
@Configuration
public class JedisPoolConfig {

    @Value("${spring.redis.host:localhost}")
    private String host;

    @Value("${spring.redis.port:6379}")
    private int port;

    @Value("${spring.redis.password:}")
    private String password;

    @Value("${spring.redis.timeout:2000}")
    private int timeout;

    @Bean
    public JedisPooled jedisPooled() {
        // 先定义 host+port
        HostAndPort hostAndPort = new HostAndPort(host, port);

        // 构建客户端配置(超时、密码)
        DefaultJedisClientConfig.Builder configBuilder = DefaultJedisClientConfig.builder()
                .connectionTimeoutMillis(timeout)
                .socketTimeoutMillis(timeout);

        if (password != null && !password.isBlank()) {
            configBuilder.password(password);
        }
        DefaultJedisClientConfig clientConfig = configBuilder.build();

        // 构建连接池配置(最大连接数、空闲连接)
        GenericObjectPoolConfig<Connection> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(20);
        poolConfig.setMaxIdle(10);
        poolConfig.setMinIdle(5);

        // Jedis 4.x 唯一正确的构造方法
        return new JedisPooled(poolConfig, hostAndPort, clientConfig);
    }
}

3. 分布式锁核心

3.1 加锁

复制代码
 // 加锁
    public RedisLock lock(String lockKey, int expireSeconds) {
        this.lockKey = lockKey;
        this.expireSeconds = expireSeconds;
        this.requestId = UUID.randomUUID().toString();
        this.isLocked = false;

        SetParams params = new SetParams();
        params.nx().ex(expireSeconds);
        String result = jedis.set(lockKey, requestId, params);
        if (!LOCK_SUCCESS.equals(result)) {
            return null;
        }

        this.isLocked = true;
        startWatchDog();
        return this;
    }

3.2 解锁

直接DEL会导致误删锁,必须先判断锁归属再删除。(用lua脚本)

复制代码
 // 解锁
    public void unlock() {
        if (!isLocked) return;
        isLocked = false;
        stopWatchDog();

        String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        jedis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    }



   private void stopWatchDog() {
        if (scheduler != null) scheduler.shutdown();
    }


 @Override
    public void close() {
        unlock();
    }

3.3 看门狗机制

注意在业务结束或异常时要停止续期线程,避免资源泄漏

复制代码
 // 看门狗自动续期
    private void startWatchDog() {
        scheduler = Executors.newSingleThreadScheduledExecutor();
        long delay = expireSeconds * 1000L / 3;

        scheduler.scheduleAtFixedRate(() -> {
            if (!isLocked) {
                stopWatchDog();
                return;
            }
            try {
                String currentVal = jedis.get(lockKey);
                if (requestId.equals(currentVal)) {
                    jedis.expire(lockKey, expireSeconds);
                    System.out.println("[看门狗] 续期成功: " + lockKey);
                }
            } catch (Exception e) {
                stopWatchDog();
            }
        }, delay, delay, TimeUnit.MILLISECONDS);
    }

测试结果:

并发锁测试:

同一时间只有一个线程能抢到锁,其他线程全部抢锁失败,验证了互斥性。

看门狗续期测试

执行业务时间超过锁过期时间(如锁 5 秒过期,业务执行 15 秒),可以看到看门狗周期性打印续期日志,锁不会被提前释放。

总结

redis分布式锁要保证原子性 + 过期时间 + 唯一标识 + 锁续命

1)通过SET NX EX指令实现原子加锁并设置过期时间;

2)使用Lua脚本保证解锁的原子性,防止误删;

3)引入看门狗机制自动续期,解决业务执行超时问题。

相关推荐
绝知此事1 分钟前
RabbitMQ 从入门到精通:Spring Boot 实战三部曲(一)—— 基础核心与快速上手
spring boot·rabbitmq·java-rabbitmq
来杯@Java9 小时前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
qq_25183645711 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
yurenpai(27届找实习中)14 小时前
redis_点评(21.好友关注——关注、取关功能实现;共同关注功能实现)
数据库·redis·缓存
Trouvaille ~15 小时前
【Redis篇】Set 与 Zset:集合运算与排行榜的终极武器
数据库·redis·缓存·set·跳表·后端开发·zset
小小工匠18 小时前
Redis - 基本架构:一个键值数据库到底由什么组成
数据库·redis·架构
Java程序员-小白19 小时前
Spring Boot整合Sa-Token框架(入门篇)
java·spring boot·后端·sa-token
小楊不秃头19 小时前
SpringBoot: IoC&DI
spring boot·ioc·di
绝知此事19 小时前
ELK 从入门到精通:Spring Boot 实战三部曲(三)—— 高级应用与架构设计
spring boot·后端·elk