redis的锁(setnx和redisson)

发布时间:2026/10/13 6:20:00
redis的锁(setnx和redisson) 代码/** * redis服务 * */ Service Slf4j public class RedisService { Value(${redis.pipeline.size:50}) private Integer pipelineSize; /** * 定义获取锁的lua脚本 */ private static final DefaultRedisScriptlt;Longgt; LOCK_LUA_SCRIPT new DefaultRedisScriptlt;gt;(if redis.call(setnx, KEYS[1], ARGV[1]) 1 then return redis.call(pexpire, KEYS[1], ARGV[2]) else return 0 end, Long.class); /** * 定义释放锁的lua脚本 */ private static final DefaultRedisScriptlt;Longgt; UNLOCK_LUA_SCRIPT new DefaultRedisScriptlt;gt;( if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return -1 end, Long.class); private static final Long LOCK_SUCCESS 1L; private static final Long LOCK_EXPIRED -1L; Autowired private RedisTemplate redisTemplate; Autowired private RedissonClient redissonClient; /** * 加锁时间为秒 * * param key 加锁键值 * param value 值 * param timeout 超时时间 单位为秒 * param retryTimes 重试次数 * return 是否加锁成功 */ public boolean lock(String key, String value, long timeout, int retryTimes) { try { log.debug( lock :::: redisKey:[{}], requestid:[{}], key, value); // 组装lua脚本参数 Listlt;Stringgt; keys Arrays.asList(key); // timeout需要转化为毫秒放入脚本 long timeoutNew timeout * CommonConstant.SYNC_DATA_PAGE_SIZE; // 执行脚本 Object result redisTemplate.execute(LOCK_LUA_SCRIPT, keys, value, String.valueOf(timeoutNew)); // 存储本地变量 if (LOCK_SUCCESS.equals(result)) { log.debug(Job:[{}]; success to acquire lock:[{}], Status code reply:[{}], key, Thread.currentThread().getName(), result); return true; } else if (retryTimes 0) { // 重试次数为0直接返回失败 return false; } else { // 重试获取锁 log.debug(Job:[{}]; retry to acquire lock:[{}], Status code reply:[{}], key, Thread.currentThread().getName(), result); return retry(key, keys, value, retryTimes, timeoutNew); } } catch (RedisException e1) { log.error(acquire redis occured an exception, threadName:[{}], Thread.currentThread().getName(), e1); } return false; } private boolean retry(String key, Listlt;Stringgt; keys, String value, int retryTimes, long timeoutNew) { int count 0; while (true) { try { // 休眠一定时间后再获取锁这里时间可以通过外部设置 Thread.sleep(CommonConstant.SLEEPTIME); Object result redisTemplate.execute(LOCK_LUA_SCRIPT, keys, value, String.valueOf(timeoutNew)); if (LOCK_SUCCESS.equals(result)) { log.debug(Job:[{}]; success to acquire lock:[{}], Status code reply:[{}], key, Thread.currentThread().getName(), result); return true; } else { count; if (retryTimes count) { log.debug(Job:[{}]; fail to acquire lock for:[{}], Status code reply:[{}], key, Thread.currentThread().getName(), result); return false; } else { log.debug(Job:[{}]; [{}] times try to acquire lock for:[{}], Status code reply:[{}], key, count, Thread.currentThread().getName(), result); continue; } } } catch (InterruptedException e) { log.error(acquire redis occured an InterruptedException:[{}], Thread.currentThread().getName(), e); break; } catch (RedisException e) { log.error(acquire redis occured an RedisException:[{}], Thread.currentThread().getName(), e); break; } } return false; } /** * 功能描述 * * param key * param value * return boolean **/ public boolean unlock(String key, String value) { try { // 组装lua脚本参数 Listlt;Stringgt; keys Arrays.asList(key); log.debug(unlock :::: redisKey:[{}], requestid:[{}], key, value); // 使用lua脚本删除redis中匹配value的key可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 Object result redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys, value); // 如果这里抛异常后续锁无法释放 if (LOCK_SUCCESS.equals(result)) { log.debug(Job:[{}]; release lock success:[{}], Status code reply:[{}], key, Thread.currentThread().getName(), result); return true; } else if (LOCK_EXPIRED.equals(result)) { // 返回-1说明获取到的KEY值与requestId不一致或者KEY不存在可能已经过期或被其他线程加锁 // 一般发生在key的过期时间短于业务处理时间属于正常可接受情况 log.debug(Job:[{}]; release lock exception:[{}], key has expired or released. Status code reply:[{}], key, Thread.currentThread().getName(), result); } else { // 其他情况一般是删除KEY失败返回0 log.error(Job:[{}]; release lock failed:[{}], del key failed. Status code reply:[{}], key, Thread.currentThread().getName(), result); } } catch (RedisException e) { log.debug(release lock occured an exception, e); } return false; } public Maplt;String, Booleangt; batchExistKeys(Listlt;Stringgt; keys) { if (CollectionUtils.isEmpty(keys)) { return Collections.emptyMap(); } Listlt;Listlt;Stringgt;gt; lists Lists.partition(keys, pipelineSize); Maplt;String, Booleangt; batchResult new HashMaplt;gt;(); for (Listlt;Stringgt; subKeys : lists) { Listlt;Booleangt; pipeResults redisTemplate.executePipelined((RedisCallbacklt;Booleangt;) (connection) -gt; { for (String key : subKeys) { connection.exists(key.getBytes()); } return null; }); for (int i 0; i lt; pipeResults.size(); i) { boolean exists pipeResults.get(i); batchResult.put(subKeys.get(i), exists); } } return batchResult; } /** * 非阻塞锁拿不到锁就释放锁 * 如果需要使用看门狗机制(锁续期)leaseTime必须是-1然后加锁时间默认为30s * * param key 锁键值 * return 是否加锁成功 */ public boolean lock(String key) { RLock lock redissonClient.getLock(key); try { boolean locked lock.tryLock(0, -1, TimeUnit.SECONDS); return locked; } catch (InterruptedException e) { log.error(Try lock failed., e); return false; } } /** * 阻塞锁尝试加锁一定时间 * 如果需要使用看门狗机制(锁续期)leaseTime必须是-1然后加锁时间默认为30s * * param key 锁键值 * param waitLockTimes 获取锁最大等待时间 * param maxRetryCount 最大尝试次数 * param timeUnit 时间单位 * return 是否加锁成功 */ public boolean tryLock(String key, int waitLockTimes, int maxRetryCount, TimeUnit timeUnit) { RLock lock redissonClient.getLock(key); try { boolean locked lock.tryLock(waitLockTimes, -1L, timeUnit); if (!locked amp;amp; maxRetryCount gt; 0) { int count 1; while (count lt; maxRetryCount amp;amp; !locked) { locked lock.tryLock(waitLockTimes, -1L, timeUnit); log.info(LockKey:[{}], Try:[{}] time to lock, the lock result is:[{}], key, count, locked); count; } } return locked; } catch (InterruptedException e) { log.error(LockKey:[{}], Try lock failed., key, e); return false; } } public boolean tryLockV2(String key, int waitLockTimes, int maxRetryCount, TimeUnit timeUnit, Long expireTime) { RLock lock redissonClient.getLock(key); try { return lock.tryLock(waitLockTimes, expireTime, timeUnit); } catch (InterruptedException e) { log.error(Try lock failed., e); return false; } } public void unlock(String key) { RLock lock redissonClient.getLock(key); if (lock.isLocked() amp;amp; lock.isHeldByCurrentThread()) { lock.unlock(); } } public void unlockV2(String key) { RLock lock redissonClient.getLock(key); if (lock.isLocked()) { lock.unlock(); } } public RRateLimiter getRateLimiter(String key) { return redissonClient.getRateLimiter(key); } }正确使用String lockKey String.format(前缀X, 唯一区分字段); String requestId UUID.randomUUID().toString(); // lua脚本setnx boolean lockAcquired redisService.lock(lockKey, requestId, LOCK_TIMEOUT, LOCK_RETRY_TIMES); if (!lockAcquired) { log.error(XXX); } String[] args new String[]{前缀X, 唯一区分字段}; String key CommonUtils.buildCacheKey(args); // Redison看门狗机智 redisService.tryLock(key, 2, 5, TimeUnit.SECONDS)requestId使用固定值会有严重问题(setnx)具体风险1.锁误删风险// 如果多个线程/实例使用相同的 requestId boolean lockAcquired redisService.lock(lockKey, fixed_request_id, LOCK_TIMEOUT, LOCK_RETRY_TIMES); // 线程A获取锁后线程B可能误删线程A的锁 // 因为解锁时无法区分是哪个请求持有的锁2.分布式环境下的锁冲突在分布式系统中多个服务实例可能同时尝试获取同一个资源的锁如果使用固定requestId一个实例可能意外释放其他实例持有的锁导致数据不一致或业务逻辑错误3.重入锁问题如果锁实现支持重入固定requestId会导致无法正确识别同一个线程的重复加锁请求锁计数混乱推荐解决方案方案1使用UUID推荐String requestId UUID.randomUUID().toString(); boolean lockAcquired redisService.lock(lockKey, requestId, LOCK_TIMEOUT, LOCK_RETRY_TIMES);方案2结合线程和实例标识String requestId instanceId : Thread.currentThread().getId() : System.currentTimeMillis(); boolean lockAcquired redisService.lock(lockKey, requestId, LOCK_TIMEOUT, LOCK_RETRY_TIMES);方案3使用Snowflake算法// 如果项目中有分布式ID生成器 String requestId idGenerator.nextId().toString(); boolean lockAcquired redisService.lock(lockKey, requestId, LOCK_TIMEOUT, LOCK_RETRY_TIMES);最佳实践Component public class RedisLockHelper { Autowired private RedisService redisService; public boolean tryLock(String lockKey, long timeout, int retryTimes) { String requestId generateRequestId(); return redisService.lock(lockKey, requestId, timeout, retryTimes); } public void unlock(String lockKey, String requestId) { // 解锁时验证requestId确保只能释放自己加的锁 redisService.unlock(lockKey, requestId); } private String generateRequestId() { return UUID.randomUUID().toString(); // 或者: return Thread.currentThread().getName() : System.currentTimeMillis() : UUID.randomUUID(); } }总结强烈建议不要使用固定值作为requestId应该使用全局唯一的标识符来确保锁的安全性分布式环境下的正确性避免意外的锁释放这样可以保证Redis分布式锁的可靠性和数据一致性。redis锁的存储结构Redis锁通常使用SET key value NX PX timeout命令实现key: 锁的唯一标识lockKeyvalue: 请求标识requestIdNX: 只在键不存在时设置PX: 设置过期时间与Redisson锁结构的对比这个存储结构和Redisson的锁结构不完全一样两者在存储模型上有本质区别1. 存储类型不同SET NX PX 方式使用String类型存储key对应一个简单的字符串值requestIdRedisson方式使用Hash类型存储一个key下包含多个字段客户端ID、线程ID、重入计数等2. 存储结构对比// SET NX PX 方式的存储结构String类型 Key: lock:order:123 Value: 550e8400-e29b-41d4-a716-446655440000 // 仅一个requestId字符串 Expire: 30000ms // Redisson的存储结构Hash类型 Key: lock:order:123 Hash字段: UUID:threadId: 550e8400-e29b-41d4-a716-446655440000:thread-1 // 客户端ID:线程ID count: 1 // 重入计数 Expire: 30000ms3. 核心差异特性SET NX PX手写Redisson存储类型StringHash锁标识单个requestId客户端ID 线程ID重入支持不支持需自行实现内置支持count字段自动续期不支持需自行实现看门狗机制自动续期解锁验证Lua脚本比对requestIdLua脚本比对客户端ID线程ID4. 为什么Redisson用Hash结构Redisson选择Hash结构是为了支持可重入锁和更丰富的元数据重入计数通过count字段记录同一线程重复加锁的次数解锁时递减锁持有者标识通过客户端ID:线程ID精确标识持有者避免误删扩展性Hash结构便于后续增加更多锁属性如公平锁、读写锁等5. 总结SET NX PX 和 Redisson的锁结构并不相同。前者是简单的String类型只存储一个requestId后者是Hash类型存储客户端ID、线程ID和重入计数等结构化信息。Redisson的Hash结构设计使其天然支持可重入、自动续期等高级特性而手写SET NX PX方式则需要自己实现这些能力。区分机制的核心通过requestId来区分不同请求持有的锁// 每个请求都有唯一的requestId String requestId UUID.randomUUID().toString(); boolean lockAcquired redisService.lock(lockKey, requestId, LOCK_TIMEOUT, LOCK_RETRY_TIMES);具体区分流程加锁过程// Redis中存储的锁数据 // Key: canvas_lock:123 // Value: 550e8400-e29b-41d4-a716-446655440000 (requestId) // Expire: 30000ms解锁过程// 解锁时验证requestId确保只能释放自己加的锁 public void unlock(String lockKey, String requestId) { // 1. 获取当前锁的requestId String currentRequestId redis.get(lockKey); // 2. 验证是否是自己加的锁 if (requestId.equals(currentRequestId)) { // 3. 只有匹配时才删除锁 redis.del(lockKey); } }原子性操作Lua脚本确保获取值和删除操作的原子性唯一标识每个请求使用UUID确保requestId全局唯一防误删只有requestId匹配时才删除锁过期保护锁有过期时间避免死锁总结Redis锁通过存储唯一的requestId作为锁的值在解锁时验证当前锁的requestId与请求的requestId是否匹配从而精确区分不同请求持有的锁。这种机制确保了分布式环境下锁的安全性和正确性。boolean locked lock.tryLock(waitLockTimes, -1L, timeUnit);这里的-1L代表什么意思在Redisson分布式锁中-1L代表无限等待时间。让我详细解释一下tryLock方法参数含义boolean locked lock.tryLock(waitLockTimes, -1L, timeUnit);三个参数分别表示waitLockTimes- 等待获取锁的最长时间-1L- 锁的持有时间leaseTimetimeUnit- 时间单位-1L的具体含义-1L表示获取锁成功后锁的持有时间为无限长不会自动过期。不同取值的含义-1L锁不会自动释放需要手动调用unlock()释放正数值锁会在指定时间后自动过期释放单位由timeUnit决定0L使用默认的锁过期时间通常是30秒在您代码中的实际应用1.基本含义-1L表示锁的持有时间为无限长需要手动调用unlock()来释放锁。2.Redisson的看门狗机制从注释可以看出-1L会启用Redisson的看门狗Watchdog机制// 注释说明 // 如果需要使用看门狗机制(锁续期)leaseTime必须是-1然后加锁时间默认为30s3.看门狗机制的工作原理当使用-1L时默认锁过期时间30秒自动续期Redisson会每隔10秒检查锁是否还在使用如果业务仍在执行自动将锁的过期时间重置为30秒业务完成后需要手动调用unlock()释放锁源码 org.redisson.config.Config#lockWatchdogTimeout this.lockWatchdogTimeout 30000L;4.对比不同参数的效果使用-1L看门狗机制lock.tryLock(10, -1L, TimeUnit.SECONDS); // 效果 // - 等待10秒获取锁 // - 获取成功后锁默认30秒过期 // - 自动续期直到手动unlock()使用固定时间lock.tryLock(10, 60L, TimeUnit.SECONDS); // 效果 // - 等待10秒获取锁 // - 获取成功后锁60秒后自动过期 // - 无自动续期机制5.在您代码中的实际应用场景public boolean tryLock(String key, int waitLockTimes, int maxRetryCount, TimeUnit timeUnit) { RLock lock redissonClient.getLock(key); try { // 使用-1L启用看门狗机制适合长时间业务操作 boolean locked lock.tryLock(waitLockTimes, -1L, timeUnit); if (!locked amp;amp; maxRetryCount gt; 0) { // 重试机制每次重试都使用看门狗 int count 1; while (count lt; maxRetryCount amp;amp; !locked) { locked lock.tryLock(waitLockTimes, -1L, timeUnit); count; } } return locked; } catch (InterruptedException e) { return false; } }6.为什么选择-1L防止死锁即使业务执行时间很长锁也不会过期自动续期Redisson自动维护锁的有效性安全性需要显式调用unlock()避免意外释放适合场景适合执行时间不确定的长任务总结-1L在Redisson锁中表示启用看门狗机制锁不会自动过期需要手动释放同时Redisson会自动续期确保锁在业务执行期间不会失效。这为长时间运行的业务操作提供了更好的安全保障。redisService.tryLock(key, 2, 5, TimeUnit.SECONDS))是怎么区分是哪个请求持有的锁的Redis分布式锁的区分机制1.锁的存储结构在Redisson中每个锁在Redis中的存储包含Key: 锁的唯一标识您代码中的keyValue: 客户端唯一标识 线程IDHash字段: 包含重入计数等元数据源码 T RFutureT tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommandT command) { return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, if ((redis.call(exists, KEYS[1]) 0) or (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; return redis.call(pttl, KEYS[1]);, Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}); } org.redisson.RedissonBaseLock#getLockName protected String getLockName(long threadId) { return this.id : threadId; }2.Redisson的锁标识机制Redisson通过客户端ID 线程ID来唯一标识锁的持有者// Redisson内部实现大致如下 String lockValue clientId : Thread.currentThread().getId(); // 示例 abc123:thread-13.在您代码中的具体流程// 您的调用 if (!redisService.tryLock(key, 2, 5, TimeUnit.SECONDS)) { // 锁获取失败的处理 }参数含义key: 锁的唯一标识2: 等待获取锁的最长时间2秒5: 最大重试次数TimeUnit.SECONDS: 时间单位秒4.区分不同请求的核心机制场景示例// 请求A线程1尝试获取锁 redisService.tryLock(activity_lock:123, 2, 5, TimeUnit.SECONDS) // 请求B线程2尝试获取同一个锁 redisService.tryLock(activity_lock:123, 2, 5, TimeUnit.SECONDS)Redis中的存储Key: activity_lock:123 Value: { mode: read, UUID: 550e8400-e29b-41d4-a716-446655440000, // 客户端唯一标识 threadId: thread-1, // 线程ID count: 1 // 重入计数 }5.解锁时的验证逻辑当解锁时Redisson会验证// 伪代码 public void unlock() { // 1. 获取当前锁的持有者信息 LockInfo currentLock redis.get(lockKey); // 2. 验证是否是当前客户端和线程持有的锁 if (currentLock.clientId.equals(myClientId) amp;amp; currentLock.threadId.equals(Thread.currentThread().getId())) { // 3. 只有匹配时才释放锁 redis.del(lockKey); } }6.为什么这样设计是安全的客户端唯一性每个Redisson客户端有唯一的UUID线程隔离同一客户端的不同线程也能区分重入支持支持同一线程多次加锁可重入锁原子性操作使用Lua脚本确保操作的原子性7.与之前分析的requestId机制对比特性您之前的代码requestId当前代码Redisson区分方式手动生成UUID作为requestId客户端ID 线程ID实现层级业务层实现框架层自动管理重入支持需要手动实现内置支持使用复杂度需要显式传递requestId自动管理无需额外参数总结redisService.tryLock(key, 2, 5, TimeUnit.SECONDS)通过Redisson框架自动生成的客户端唯一标识 线程ID来区分不同请求持有的锁。这种机制比手动管理requestId更加简洁和安全由框架自动处理锁的标识和验证。redis锁的存储结构Redis锁通常使用SET key value NX PX timeout命令实现key: 锁的唯一标识lockKeyvalue: 请求标识requestIdNX: 只在键不存在时设置PX: 设置过期时间这个和Redisson里面的锁结构一样吗对比Redis锁的存储结构与Redisson的实现对比在Redis中使用SET key value NX PX timeout命令实现分布式锁时其基本存储结构如下key锁的唯一标识lockKey通常为资源名称或业务标识。value请求标识requestId用于区分不同客户端持有的锁防止误删。NX仅当键不存在时才设置保证原子性。PX设置键的过期时间毫秒避免死锁。该方式实现了基础的互斥锁功能但存在一些局限性例如无法重入、无法公平竞争、无法自动续期等。Redisson中的锁结构特点Redisson 是一个基于 Redis 的 Java 客户端提供了丰富的分布式锁实现包括可重入锁ReentrantLock、公平锁FairLock、读写锁ReadWriteLock等。其内部锁的存储结构与简单SET命令有所不同主要体现在以下几个方面1. 锁的键名结构Redisson 使用复合键名格式通常为redisson_lock:{lockName}其中lockName是用户定义的锁名称如myLock。2. 锁值的存储内容锁的 value 不再只是一个简单的 requestId而是包含以下信息的序列化对象当前持有锁的线程/客户端标识类似 requestId持有锁的次数重入计数器锁的创建时间戳可选的超时时间信息这些信息通过 JSON 或自定义序列化方式存储支持锁的可重入性和状态追踪。3. 使用 Hash 结构存储锁状态在 Redisson 内部锁的状态可能以 Hash 表形式存储例如HGETALL redisson_lock:myLock返回结果示例{ owner: client-123, count: 3, timestamp: 1719800000000 }这种结构允许对锁进行更复杂的操作如判断是否已重入、获取持有者等。4. 自动续期机制WatchdogRedisson 引入了 Watchdog 机制在锁被获取后会定期自动延长锁的过期时间防止因业务执行时间过长导致锁提前释放。该机制通过后台线程定时发送EXPIRE命令更新锁的过期时间确保锁不会因超时而失效。5. 支持多种锁类型可重入锁RLock支持同一个线程多次加锁而不阻塞。公平锁RReadWriteLock按请求顺序排队获取锁。联锁MultiLock多个锁同时加锁提升一致性。对比总结特性简单 SET 命令实现Redisson 实现键名lockKeyredisson_lock:{lockName}值内容单纯 requestId包含 owner、count、timestamp 等字段的结构化数据数据结构StringHash部分场景或 Lua 脚本控制重入支持❌ 不支持✅ 支持自动续期❌ 无✅ 有Watchdog锁的可查询性有限高可通过 HGETALL 查看状态多种锁类型仅基础锁支持多种高级锁结论虽然两者都基于 Redis 的SET ... NX PX原语实现锁的互斥性但Redisson 的锁结构远比简单的SET key value NX PX更复杂和强大。它不仅保留了基本的锁机制还引入了结构化存储、重入支持、自动续期、多锁类型等特性更适合生产环境下的高并发、高可用场景。因此Redisson 的锁结构与简单命令实现不完全相同它是对基础命令的封装和增强而非直接等价。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询