基于 Redis 的分布式锁:原理剖析与 Spring Boot 实战(含看门狗续期)
一、什么是分布式锁在单机应用中我们常用synchronized、ReentrantLock解决多线程并发问题但这些锁的作用域局限于单个 JVM 进程。当系统从单体架构演进为分布式集群时多个服务实例、多个进程会并发操作共享资源如库存、订单此时本地锁已无法保证数据一致性分布式锁应运而生。分布式锁的核心目标互斥性同一时刻只有一个客户端能持有锁防死锁锁必须能被释放避免客户端崩溃导致锁永久占用解铃还须系铃人只能由加锁者自己释放锁防止误删他人锁高可用锁服务本身不能成为单点故障二、基于 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 end3. 看门狗机制解决业务超时锁释放问题上面的时间轴帮助我们理解不加看门狗的时候如果业务执行时间超过锁的过期时间锁会被自动释放导致并发安全问题。看门狗机制则会在业务执行期间周期性通常为锁过期时间的 1/3续期锁的过期时间直到业务执行完成时间轴秒 → 0 5 10 15 事务A业务 ├──────────────────────┤ 执行完成 锁A分布式锁├──续期→├──续期→├──续期→┤ 始终有效 事务B其他业务 全程拿不到锁阻塞等待三、Spring Boot Jedis 4.x 实战实现理解了上述原理接下来请跟我一起实现吧1. 环境准备依赖引入redis.clients:jedis:4.4.32.Jedis 连接池配置适配 Jedis 4.xConfiguration 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() { // 先定义 hostport 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(); // 构建连接池配置最大连接数、空闲连接 GenericObjectPoolConfigConnection 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引入看门狗机制自动续期解决业务执行超时问题。