You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redis集群中Key提前消失致分布式锁失效,求排查方案

Redis集群下Spring Webflux自定义锁异常:锁提前失效问题排查

问题场景

基于Kotlin+Spring Boot Webflux实现自定义分布式锁,用于规避同一用户的并发事务处理。预期逻辑为:第二个事务循环等待,直到第一个事务完成并释放锁Key(未到TTL前锁应持续有效)。但实际测试发现,未达TTL时仅几秒后第二个进程就能获取锁;日志显示获取锁前查询锁Key为空,但通过TTL验证Key实际仍存在,且初始能查到Key值,随机一段时间后查询结果变为空。已确认代码采用Redis集群模式连接。

锁获取核心代码

redisTemplate
    .opsForValue()
    .get(lockKey)
    .flatMap { containValue ->
        redisTemplate.getExpire(lockKey).flatMap { ttl ->
            Mono.fromSupplier {
                // 打印锁状态日志
                containValue.isNullOrEmpty()
            }
        }
    }
    .switchIfEmpty(Mono.just(true))
    .flatMap { isUnlocked ->
        // 打印锁判定日志
        if (isUnlocked) {
            redisTemplate
                .opsForValue()
                .setIfAbsent(lockKey, traceId, LOCK_EXPIRY)
                .defaultIfEmpty(false)
        } else {
            Mono.just(false)
        }
    }

排查发现与原因分析

  1. Redis集群Key迁移的客户端处理缺陷:Redis集群在扩容、缩容或槽位平衡时会触发Key迁移,此时针对该Key的读请求会收到MOVE指令,要求客户端转向目标节点查询。若使用的Spring Data Redis Reactive客户端版本较旧,可能存在MOVE指令处理漏洞,直接返回空值而非转发请求到目标节点,导致误判锁已释放。
  2. 非原子性的锁状态检查:代码中get(lockKey)和getExpire(lockKey)是两个独立异步操作,在Redis集群环境下,这两个请求可能被路由到不同节点(若Key正处于迁移过程中),出现get返回空但getExpire仍能查到TTL的状态不一致情况。
  3. 空值处理逻辑的误判:switchIfEmpty(Mono.just(true))会在get(lockKey)返回空时直接判定锁未被持有,但未区分「Key真的不存在」和「查询失败(如MOVE指令导致的空)」两种场景。

解决方案

  • 升级Spring Data Redis版本:建议升级到Spring Data Redis 2.7+(对应Spring Boot 2.7+),新版本修复了Reactive客户端在集群环境下的指令转发问题,能正确处理MOVE指令。
  • 原子化锁状态检查:用Lua脚本替代分开的get和getExpire操作,保证锁状态查询的原子性,避免集群节点间的状态不一致。示例如下:
    local lockValue = redis.call('GET', KEYS[1])
    local ttl = redis.call('TTL', KEYS[1])
    return {lockValue, ttl}
    
    Webflux中调用Lua脚本的实现:
    val lockCheckScript = DefaultRedisScript<List<Any?>>("""
        local lockValue = redis.call('GET', KEYS[1])
        local ttl = redis.call('TTL', KEYS[1])
        return {lockValue, ttl}
    """, List::class.java)
    
    redisTemplate.execute(lockCheckScript, listOf(lockKey))
        .flatMap { result ->
            val lockValue = result[0]
            val ttl = result[1] as Long?
            // 结合TTL判断锁是否真的不存在:若TTL>0则锁仍有效
            Mono.just(lockValue.isNullOrEmpty() && (ttl == null || ttl <= 0))
        }
        .flatMap { isUnlocked ->
            if (isUnlocked) {
                redisTemplate.opsForValue()
                    .setIfAbsent(lockKey, traceId, LOCK_EXPIRY)
                    .defaultIfEmpty(false)
            } else {
                Mono.just(false)
            }
        }
    
  • 检查Redis集群状态:通过redis-cli cluster nodes查看集群节点状态和槽位分布,确认是否存在频繁的槽位迁移。若为临时迁移导致的问题,迁移完成后自动恢复;若频繁迁移需调整集群平衡策略。

内容的提问来源于stack exchange,提问作者DDDDDD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:36:07