2026/4/15 21:59:46
网站建设
项目流程
网站建立后怎么做推广,网站信息建设,松江网站建设培训费用,企业网站前端模板#x1f4bb; Hello World, 我是 予枫。代码不止#xff0c;折腾不息。作为一个正在升级打怪的 Java 后端练习生#xff0c;我喜欢把踩过的坑和学到的招式记录下来。 保持空杯心态#xff0c;让我们开始今天的技术分享。在分布式系统中#xff0c;单机锁#xff08;如 sy… Hello World, 我是 予枫。代码不止折腾不息。作为一个正在升级打怪的Java 后端练习生我喜欢把踩过的坑和学到的招式记录下来。 保持空杯心态让我们开始今天的技术分享。在分布式系统中单机锁如synchronized、ReentrantLock只能保证单个 JVM 内的线程安全而跨服务、跨节点的并发场景如秒杀库存扣减、分布式任务调度、订单幂等处理则需要分布式锁来保证数据一致性。Redis 凭借高性能、高可用的特性成为实现分布式锁的首选方案。本文将从最基础的setnx手写实现出发剖析死锁、集群失效等核心问题最终落地 Redisson 分布式锁的最佳实践。一、为什么需要分布式锁先看一个典型的业务场景电商平台的库存扣减。单机部署时用synchronized修饰扣减方法即可保证同一时刻只有一个线程修改库存集群部署时多节点 / 多服务实例每个实例有独立的 JVM本地锁无法跨实例生效会出现多个线程同时扣减库存导致超卖库存为负或重复扣减库存数据不一致。分布式锁的核心目标在分布式环境下保证同一时刻只有一个线程执行临界区代码。Redis 实现分布式锁的核心思路是利用 Redis 的原子性命令将 “锁” 存储为 Redis 中的一个 Key线程获取锁即创建该 Key释放锁即删除该 Key。二、基础版实现基于 SETNX 命令2.1 核心命令SETNXSETNXSET if Not Exists当 Key 不存在时才设置值返回 1若 Key 已存在则不操作返回 0。该命令是原子性的这是实现分布式锁的基础。# 语法SETNX key value 127.0.0.1:6379 SETNX lock:stock 1 # 锁Keylock:stock值1可自定义 (integer) 1 # 返回1获取锁成功 127.0.0.1:6379 SETNX lock:stock 1 # 再次执行Key已存在获取锁失败 (integer) 02.2 手写基础版分布式锁Java Jedis首先引入 Jedis 依赖Mavendependency groupIdredis.clients/groupId artifactIdjedis/artifactId version4.4.3/version /dependency基础版实现代码import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * 基于 SETNX 的基础版分布式锁 */ public class SimpleRedisLock { // Redis 连接池 private final JedisPool jedisPool; // 锁Key前缀 private static final String LOCK_PREFIX lock:; // 锁过期时间默认10秒防止死锁 private static final int DEFAULT_EXPIRE_SECONDS 10; public SimpleRedisLock() { // 初始化Jedis连接池实际项目中建议配置化 JedisPoolConfig config new JedisPoolConfig(); config.setMaxTotal(20); config.setMaxIdle(10); this.jedisPool new JedisPool(config, 127.0.0.1, 6379); } /** * 获取锁 * param lockKey 业务锁Key如stock_1001 * return 是否获取成功 */ public boolean lock(String lockKey) { try (Jedis jedis jedisPool.getResource()) { // 核心SETNX 命令 Long result jedis.setnx(LOCK_PREFIX lockKey, 1); // 设置过期时间避免死锁 if (result 1) { jedis.expire(LOCK_PREFIX lockKey, DEFAULT_EXPIRE_SECONDS); return true; } return false; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 释放锁 * param lockKey 业务锁Key */ public void unlock(String lockKey) { try (Jedis jedis jedisPool.getResource()) { jedis.del(LOCK_PREFIX lockKey); } catch (Exception e) { e.printStackTrace(); } } // 测试方法 public static void main(String[] args) { SimpleRedisLock redisLock new SimpleRedisLock(); String lockKey stock_1001; // 模拟线程1获取锁 new Thread(() - { if (redisLock.lock(lockKey)) { try { System.out.println(线程1获取锁成功执行库存扣减...); Thread.sleep(5000); // 模拟业务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } finally { redisLock.unlock(lockKey); System.out.println(线程1释放锁); } } else { System.out.println(线程1获取锁失败); } }).start(); // 模拟线程2竞争锁 new Thread(() - { if (redisLock.lock(lockKey)) { try { System.out.println(线程2获取锁成功执行库存扣减...); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } finally { redisLock.unlock(lockKey); System.out.println(线程2释放锁); } } else { System.out.println(线程2获取锁失败); } }).start(); } }2.3 基础版的核心问题看似能工作但存在 3 个致命问题死锁风险setnx和expire是两个独立命令若执行setnx后程序崩溃如 JVM 宕机expire未执行锁 Key 会永久存在导致死锁误删锁若线程 A 的业务执行时间超过锁过期时间锁自动释放此时线程 B 获取锁线程 A 执行完业务后调用unlock会误删线程 B 的锁过期时间难设置设置太短业务没执行完锁就释放设置太长若线程异常锁释放慢影响并发效率。三、进阶优化解决死锁与误删问题3.1 核心优化点原子化设置锁 过期时间使用 Redis 的SET key value NX EX seconds命令将setnx和expire合并为一个原子命令防误删给锁 Value 设置唯一标识如 UUID 线程 ID释放锁时先校验标识再删除看门狗机制若业务未执行完自动续期锁的过期时间避免锁提前释放。3.2 优化版实现解决死锁 防误删import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.UUID; /** * 优化版原子设置锁过期时间 防误删 简易看门狗 */ public class OptimizedRedisLock { private final JedisPool jedisPool; private static final String LOCK_PREFIX lock:; private static final int DEFAULT_EXPIRE_SECONDS 10; // 唯一标识每个线程的锁Value唯一 private String lockValue; public OptimizedRedisLock() { JedisPoolConfig config new JedisPoolConfig(); config.setMaxTotal(20); config.setMaxIdle(10); this.jedisPool new JedisPool(config, 127.0.0.1, 6379); // 生成唯一标识UUID 线程ID this.lockValue UUID.randomUUID().toString() : Thread.currentThread().getId(); } /** * 获取锁原子化 SET NX EX * param lockKey 业务锁Key * return 是否获取成功 */ public boolean lock(String lockKey) { try (Jedis jedis jedisPool.getResource()) { // SET key value NX仅Key不存在时设置 EX过期时间 String result jedis.set(LOCK_PREFIX lockKey, lockValue, NX, EX, DEFAULT_EXPIRE_SECONDS); // OK 表示设置成功获取锁成功 return OK.equals(result); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 释放锁先校验标识再删除Lua脚本保证原子性 * param lockKey 业务锁Key * return 是否释放成功 */ public boolean unlock(String lockKey) { // Lua脚本先判断Value是否匹配匹配则删除 String luaScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; try (Jedis jedis jedisPool.getResource()) { Object result jedis.eval(luaScript, 1, LOCK_PREFIX lockKey, lockValue); // 返回1表示删除成功 return 1.equals(result.toString()); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 简易看门狗定时续期锁的过期时间 * param lockKey 业务锁Key * param delay 续期间隔如3秒 */ public void watchDog(String lockKey, long delay) { new Thread(() - { while (true) { try { Thread.sleep(delay * 1000); // 校验锁是否还属于当前线程是则续期 try (Jedis jedis jedisPool.getResource()) { String currentValue jedis.get(LOCK_PREFIX lockKey); if (lockValue.equals(currentValue)) { // 续期重置过期时间为10秒 jedis.expire(LOCK_PREFIX lockKey, DEFAULT_EXPIRE_SECONDS); System.out.println(看门狗续期成功锁Key lockKey); } else { // 锁已释放退出看门狗 break; } } } catch (InterruptedException e) { e.printStackTrace(); break; } } }).start(); } // 测试方法 public static void main(String[] args) { OptimizedRedisLock redisLock new OptimizedRedisLock(); String lockKey stock_1001; // 模拟线程1获取锁业务执行时间超过默认过期时间 new Thread(() - { if (redisLock.lock(lockKey)) { try { System.out.println(线程1获取锁成功执行库存扣减...); // 启动看门狗每3秒续期一次 redisLock.watchDog(lockKey, 3); Thread.sleep(15000); // 业务执行15秒超过默认10秒过期时间 } catch (InterruptedException e) { e.printStackTrace(); } finally { boolean unlockResult redisLock.unlock(lockKey); System.out.println(线程1释放锁结果 unlockResult); } } else { System.out.println(线程1获取锁失败); } }).start(); // 模拟线程2竞争锁 new Thread(() - { // 等待5秒确保线程1先获取锁 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } if (redisLock.lock(lockKey)) { try { System.out.println(线程2获取锁成功执行库存扣减...); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } finally { boolean unlockResult redisLock.unlock(lockKey); System.out.println(线程2释放锁结果 unlockResult); } } else { System.out.println(线程2获取锁失败线程1的看门狗续期了锁); } }).start(); } }3.3 关键优化点解释原子化设置锁jedis.set(key, value, NX, EX, seconds)是原子操作避免了setnx和expire分离导致的死锁防误删锁释放锁时使用 Lua 脚本先校验get(key)的值是否等于当前线程的唯一标识再删除Lua 脚本在 Redis 中是原子执行的避免 “校验 - 删除” 过程中锁被其他线程修改简易看门狗启动一个后台线程每隔一段时间如锁过期时间的 1/3检查锁是否还属于当前线程若是则重置过期时间保证业务执行完前锁不释放。3.4 仍存在的问题尽管做了优化但手写实现仍有短板看门狗实现简陋如未处理线程中断、异常生产环境需考虑更多边界集群环境下Redis 主从复制存在延迟若主节点宕机从节点未同步锁 Key会导致锁失效需手动处理锁超时、重试、释放等逻辑开发效率低。四、最佳实践Redisson 分布式锁Redisson 是 Redis 官方推荐的 Java 客户端内置了分布式锁的完整实现解决了手写实现的所有痛点是生产环境的首选。4.1 Redisson 核心特性基于 Lua 脚本保证锁操作的原子性内置自动看门狗机制默认 30 秒过期每 10 秒续期一次支持可重入锁、公平锁、读写锁等多种锁类型提供 RedLock 算法解决集群环境下的锁失效问题自动处理锁释放、超时、重试等边界情况。4.2 Redisson 集成与使用Spring Boot步骤 1引入依赖Mavendependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.23.3/version /dependency步骤 2配置 Redissonapplication.ymlspring: redis: host: 127.0.0.1 port: 6379 password: database: 0 # Redisson 配置简化版 redisson: config: | singleServerConfig: address: redis://127.0.0.1:6379 password: database: 0 threads: 10 nettyThreads: 10步骤 3Redisson 分布式锁实现代码import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * Redisson 分布式锁最佳实践 */ Component public class RedissonDistributedLock { Autowired private RedissonClient redissonClient; /** * 获取可重入分布式锁 * param lockKey 业务锁Key * param waitTime 最大等待时间秒获取锁的超时时间 * param leaseTime 锁持有时间秒0表示使用看门狗自动续期 * return 是否获取成功 */ public boolean lock(String lockKey, long waitTime, long leaseTime) { RLock lock redissonClient.getLock(lockKey); try { // tryLock尝试获取锁超时返回false // 参数waitTime(等待时间), leaseTime(持有时间), 时间单位 return lock.tryLock(waitTime, leaseTime, java.util.concurrent.TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); return false; } } /** * 释放锁 * param lockKey 业务锁Key */ public void unlock(String lockKey) { RLock lock redissonClient.getLock(lockKey); // 校验锁是否属于当前线程避免误删 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } // 业务层使用示例 Component static class StockService { Autowired private RedissonDistributedLock redissonLock; public void deductStock(Long stockId) { String lockKey stock: stockId; // 获取锁最大等待3秒持有时间0开启看门狗 if (redissonLock.lock(lockKey, 3, 0)) { try { System.out.println(线程 Thread.currentThread().getId() 获取锁成功扣减库存...); // 模拟业务执行超过30秒看门狗会自动续期 Thread.sleep(40000); } catch (InterruptedException e) { e.printStackTrace(); } finally { redissonLock.unlock(lockKey); System.out.println(线程 Thread.currentThread().getId() 释放锁); } } else { System.out.println(线程 Thread.currentThread().getId() 获取锁失败超时); } } } // 测试 public static void main(String[] args) { // Spring Boot 环境下可通过ApplicationContext获取Bean // 此处简化模拟业务调用 StockService stockService new StockService(); // 模拟多线程扣减库存 new Thread(() - stockService.deductStock(1001L)).start(); new Thread(() - stockService.deductStock(1001L)).start(); } }4.3 Redisson 分布式锁原理Redisson 实现分布式锁的核心是 Lua 脚本以tryLock为例核心逻辑如下-- 1. 检查锁是否存在若不存在则设置锁支持可重入 if (redis.call(exists, KEYS[1]) 0) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; -- 2. 若锁已存在检查是否是当前线程持有可重入 if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; -- 3. 锁被其他线程持有返回剩余过期时间 return redis.call(pttl, KEYS[1]);可重入使用 Hash 结构存储锁Key 是锁标识Field 是线程 IDValue 是重入次数看门狗当leaseTime0时Redisson 会启动一个定时任务TimeoutTask每隔lockWatchdogTimeout/3默认 10 秒执行一次续期将锁过期时间重置为 30 秒若线程正常释放锁看门狗自动停止若线程异常看门狗也会停止锁到期自动释放。五、集群环境下的锁失效RedLock 算法5.1 集群环境的锁失效问题Redis 主从集群中主节点负责写操作从节点同步数据。若主节点宕机从节点升级为主节点但此时主节点的锁 Key 尚未同步到从节点导致新主节点中无锁 Key其他线程可重新获取锁引发并发问题。5.2 RedLock 原理RedLock 是 Redis 作者提出的分布式锁算法核心思路部署多个独立的 Redis 节点至少 3 个无主从关系线程依次向所有节点请求获取锁只有当超过半数节点获取锁成功且总耗时小于锁过期时间才认为锁获取成功释放锁时向所有节点发送释放请求。5.3 Redisson 实现 RedLockimport org.redisson.api.RedissonClient; import org.redisson.api.RedissonRedLock; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * RedLock 解决集群锁失效问题 */ Component public class RedissonRedLockDemo { // 假设配置了3个独立的Redis节点客户端 Autowired private RedissonClient redissonClient1; Autowired private RedissonClient redissonClient2; Autowired private RedissonClient redissonClient3; public void redLockDemo(String lockKey) { // 获取3个节点的锁 RLock lock1 redissonClient1.getLock(lockKey); RLock lock2 redissonClient2.getLock(lockKey); RLock lock3 redissonClient3.getLock(lockKey); // 组合为RedLock RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3); try { // 尝试获取锁等待3秒持有10秒 boolean isLock redLock.tryLock(3, 10, java.util.concurrent.TimeUnit.SECONDS); if (isLock) { System.out.println(RedLock获取成功执行临界区业务...); Thread.sleep(5000); } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁会向所有节点释放 redLock.unlock(); } } }注意RedLock 性能略低于普通分布式锁需访问多个节点仅在对数据一致性要求极高的场景如金融交易使用普通业务场景使用单节点 Redisson 锁即可。六、基础版 vs Redisson 版对比特性手写基础版Redisson 版原子性需手动保证SET NX EX内置 Lua 脚本天然原子性死锁问题需手动处理过期时间 看门狗内置看门狗自动续期 / 释放防误删锁需手动写 Lua 脚本校验标识内置校验逻辑支持isHeldByCurrentThread可重入性需手动实现 Hash 存储重入次数原生支持可重入锁集群适配无主从切换易失效支持 RedLock解决集群锁失效问题边界处理超时 / 重试需手动编写内置完善的超时、重试、异常处理逻辑开发效率低需处理大量边界高一行代码调用生产可用性低易踩坑高经过生产验证七、总结关键点回顾基础实现基于SETNX的分布式锁需解决原子性SET NX EX、死锁过期时间、误删唯一标识 Lua 脚本三大问题最佳实践生产环境优先使用 Redisson 分布式锁其内置看门狗、可重入、集群适配等特性能规避手写实现的所有痛点集群场景普通业务用单节点 Redisson 锁高一致性场景如金融使用 RedLock 算法。落地建议非核心业务如缓存更新可使用手写基础版但需严格校验原子性和过期时间核心业务如库存、订单、支付必须使用 Redisson 分布式锁优先选择tryLock(waitTime, 0, TimeUnit.SECONDS)开启看门狗集群部署若 Redis 为主从架构且对数据一致性要求高升级为 RedLock 或使用 Redis Cluster Redisson。Redis 分布式锁的核心是原子性和高可用手写实现适合学习原理而 Redisson 是生产环境的最优解既能保证正确性又能提升开发效率。关注【予枫】获取更多技术干货身份一名热爱技术的研二学生️标签Java / 算法 / 个人成长Slogan只写对自己和他人有用的文字。