当Redis锁失效:代购系统库存扣减的最后一公里

举报
云上老码农 发表于 2026/07/11 16:03:51 2026/07/11
【摘要】 本文分析了Redis分布式锁在代购系统库存扣减场景中的失效原因,从自研锁到RedLock再到阿里云Tair扩展,给出了完整的方案演进和运维监控实践

适合谁看

本文适合正在搭建代购系统或电商后端、处理过高并发库存扣减的开发者。如果只是了解分布式锁概念,可以跳过代码部分直接看方案对比。

锁失效的真实场景

先说一个最简单的场景。代购系统里最怕的不是流量低,而是流量突然冲高时Redis锁失效。Taocarts的1688代采系统在秒杀活动中同时收到500个抢购请求,分布式锁本应只放行一个,结果锁提前超时释放,多个请求同时扣减库存,超卖3单。每单亏损200元,一次事故损失600元——对中小团队来说够心疼一周。

问题说起来很简单。在在系统中,这个问题的处理方式也经历了从自研锁到RedLock再到云原生Tair扩展的演进:Redis锁的TTL设了2秒,但业务执行花了3秒。锁已经释放了,业务还以为自己独占着。这不复杂,但很隐蔽——因为它只在流量高峰时才暴露。

问题拆解:锁为什么会失效

Redis分布式锁最常见的失效模式有两种:

  1. 锁超时释放:业务执行时间超过锁的TTL,锁自动释放,后续请求拿到锁后并发执行
  2. 主从切换丢锁:Redis主节点宕机,锁数据未同步到从节点,新主节点上锁不存在

第一种更隐蔽——业务代码正常执行,只是某个1688商品详情页解析耗时从200ms涨到3s,而锁TTL设了2s。锁释放后另一个请求也拿到锁,两个请求同时下单,库存扣两次。

从这个问题往前看,云原生时代的一个核心趋势是把边界问题交给基础设施。就像当年从自建数据库换到RDS,你不用再操心主从切换和数据备份一样,锁的续期问题也应该交给Redis服务端来处理。阿里云Tair的EXSET命令就是这个思路——把看门狗逻辑下沉到基础设施层,应用层只管业务逻辑。

方案设计:从自研锁到可靠锁

第一阶段:自研锁(不推荐)

// 自研Redis锁——有超卖风险
$lockKey = "product:{$productId}:lock";
$locked = Redis::setnx($lockKey, 1);
Redis::expire($lockKey, 2); // 2秒超时

if ($locked) {
    // 扣库存逻辑——可能超过2秒
    $this->deductStock($productId, $quantity);
    Redis::del($lockKey);
}

这段代码的问题:setnxexpire 不是原子操作。如果 setnx 成功但 expire 前进程崩溃,锁变成永不过期的死锁。更致命的是业务超时后锁自动释放,导致并发问题。

早期版本也踩过这个坑,后来重构为RedLock方案,配合SLS日志采集做锁异常告警。

第二阶段:RedLock + 看门狗

// 使用RedLock算法,原子操作加锁
$lockKey = "product:{$productId}:lock";
$lockToken = uniqid('', true); // 唯一标识,用于安全释放
$ttl = 3000; // 3秒

$result = Redis::eval(
    "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then return 1 else return 0 end",
    1, $lockKey, $lockToken, $ttl
);

if ($result) {
    // 启动看门狗协程,每1秒续期一次
    $this->startWatchdog($lockKey, $lockToken, $ttl);

    try {
        $this->deductStock($productId, $quantity);
    } finally {
        // Lua脚本安全释放
        Redis::eval(
            "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
            1, $lockKey, $lockToken
        );
    }
}

关键改进:

  • 原子加锁:SET NX PX 一步到位
  • 看门狗机制:业务未完成时自动续期,避免锁超时释放
  • 安全释放:用Lua脚本校验锁持有者,防止误删别人的锁

但看门狗也有代价:每个持有锁的请求需要额外一个进程做续期,高并发下会增加Redis连接数。对于200 QPS的扣库存场景,Redis连接数会从50涨到200左右。

第三阶段:结合阿里云Redis的优化方案

阿里云Redis提供了 Redlock 的优化实现——Tair 扩展支持 EXSET 命令,自带续期能力,省掉看门狗的额外开销。

// 阿里云Tair扩展——内置续期
$lockKey = "product:{$productId}:lock";
$lockToken = uniqid('', true);

// 加锁并自动续期,续期间隔1秒
$result = Redis::rawCommand('EXSET', $lockKey, $lockToken, 'NX', 'PX', 3000, 'KEEPTTL', 1000);

if ($result) {
    try {
        $this->deductStock($productId, $quantity);
    } finally {
        // 安全释放
        Redis::rawCommand('EXSET', $lockKey, $lockToken, 'XX', 'KEEPTTL', 0, 'DEL');
    }
}

这个方案把看门狗的逻辑下沉到Redis服务端,客户端代码更简洁,网络开销也更低。配合阿里云Redis的主从架构,主从切换时锁数据通过RDB/AOF持久化,不会丢锁。

运维监控:让锁失效可观测

光有代码不够,线上跑起来后怎么知道锁有没有失效?Taocarts的做法是:

  1. SLS日志采集:每次加锁/解锁都记录日志,包括锁Key、持有时间、是否续期
  2. CloudMonitor告警:设置锁持有时间超过TTL 80% 的告警阈值,提前发现慢查询
  3. OOS自动化运维:锁异常时自动执行诊断脚本,检查Redis连接数和慢日志
# CloudMonitor告警规则示例
报警规则: Redis锁持有时间异常
指标: 自定义指标 > lock_hold_time
阈值: >= 2500ms (TTL 3000ms的80%)
持续周期: 3次
通知方式: 钉钉群机器人 + 短信

这样配置后,即使业务代码出现偶发慢查询,也能在锁失效前收到告警,提前介入排查。

实际效果

采用RedLock + 看门狗方案后,在压测1000并发扣库存的场景下,零超卖。相比自研锁方案,Redis连接数增加了约3倍,但CPU使用率只上升了8%——因为看门狗续期是轻量操作。

成本方面:从自研锁升级到RedLock,代码改动量约50行,开发测试耗时2天。如果使用阿里云Tair扩展,代码更少,但需要Redis实例支持(标准版即可)。

总结

说穿了就一件事:**Redis锁失效不是锁本身的问题,而是锁的生命周期管理没到位。**从自研锁到RedLock再到服务端续期,每一步都在解决上一层的边界情况。如果你正在做类似的系统,建议直接从RedLock起步,配合SLS日志和CloudMonitor告警。

你们在实际项目中遇到过Redis锁失效的情况吗?用的什么方案解决的?欢迎聊聊。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。