突击Redis重大事故现场,又是“分布式锁”惹的祸

我用Redis分布式锁,抢了瓶茅台,然后GG了~~ 在并发编程中,我们通过锁,来避免由于竞争而造成的数据不一致问题。通常,我们以synchronized 、Lock来使用它(单机情况) 我们来看一个案例: 高并发下单超卖问题 @Autowired RedisTemplate<String,String> redisTemplate; ​ String maotai = "maotai20210321001";//茅台商品编号 ​ @PostConstruct public void init(... 阅读详情

前言

基于Redis使用分布式锁在当今已经不是什么新鲜事了。本篇文章主要是基于我们实际项目中因为redis分布式锁造成的事故分析及解决方案。 背景:我们项目中的抢购订单采用的是分布式锁来解决的。有一次,运营做了一个飞天茅台的抢购活动,库存100瓶,但是却超卖了!要知道,这个地球上飞天茅台的稀缺性啊!!!事故定为P0级重大事故...只能坦然接受。整个项目组被扣绩效了~~事故发生后,CTO指名点姓让我带头冲锋来处理,好吧,冲~

事故现场

经过一番了解后,得知这个抢购活动接口以前从来没有出现过这种情况,但是这次为什么会超卖呢?原因在于:之前的抢购商品都不是什么稀缺性商品,而这次活动居然是飞天茅台,通过埋点数据分析,各项数据基本都是成倍增长,活动热烈程度可想而知!话不多说,直接上核心代码,机密部分做了伪代码处理。。。

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;    String key = "key:" + request.getSeckillId;
    try {
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
        if (lockFlag) {
            // HTTP请求用户服务进行用户相关的校验
            // 用户活动校验
                        // 库存校验
            Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
            assert stock != null;
            if (Integer.parseInt(stock.toString()) <= 0) {
                // 业务异常
            } else {
                redisTemplate.opsForHash().increment(key+":info", "stock", -1);
                // 生成订单
                // 发布订单创建成功事件
                // 构建响应VO
            }        }    } finally {
        // 释放锁
        stringRedisTemplate.delete("key");
        // 构建响应VO
    }    return response;
}

以上代码,通过分布式锁过期时间有效期10s来保障业务逻辑有足够的执行时间;采用try-finally语句块保证锁一定会及时释放。业务代码内部也对库存进行了校验。看起来很安全啊~ 别急,继续分析。

突击Redis重大事故现场,又是“分布式锁”惹的祸

 

事故原因

飞天茅台抢购活动吸引了大量新用户下载注册我们的APP,其中,不乏很多羊毛党,采用专业的手段来注册新用户来薅羊毛和刷单。当然我们的用户系统提前做好了防备,接入阿里云人机验证、三要素认证以及自研的风控系统等各种十八般武艺,挡住了大量的非法用户。此处不禁点个赞~ 但也正因如此,让用户服务一直处于较高的运行负载中。 抢购活动开始的一瞬间,大量的用户校验请求打到了用户服务。导致用户服务网关出现了短暂的响应延迟,有些请求的响应时长超过了10s,但由于HTTP请求的响应超时我们设置的是30s,这就导致接口一直阻塞在用户校验那里,10s后,分布式锁已经失效了,此时有新的请求进来是可以拿到锁的,也就是说锁被覆盖了。这些阻塞的接口执行完之后,又会执行释放锁的逻辑,这就把其他线程的锁释放了,导致新的请求也可以竞争到锁~这真是一个极其恶劣的循环。 这个时候只能依赖库存校验,但是偏偏库存校验不是非原子性的,采用的是get and compare 的方式,超卖的悲剧就这样发生了~~~

事故分析

仔细分析下来,可以发现,这个抢购接口在高并发场景下,是有严重的安全隐患的,主要集中在三个地方:

  • 没有其他系统风险容错处理
    由于用户服务吃紧,网关响应延迟,但没有任何应对方式,这是超卖的导火索。
  • 看似安全的分布式锁其实一点都不安全
    虽然采用了set key value [EX seconds] [PX milliseconds] [NX|XX]的方式,但是如果线程A执行的时间较长没有来得及释放,锁就过期了,此时线程B是可以获取到锁的。当线程A执行完成之后,释放锁,实际上就把线程B的锁释放掉了。这个时候,线程C又是可以获取到锁的,而此时如果线程B执行完释放锁实际上就是释放的线程C设置的锁。这是超卖的直接原因。
  • 非原子性的库存校验
    非原子性的库存校验导致在并发场景下,库存校验的结果不准确。这是超卖的根本原因。

通过以上分析,问题的根本原因在于库存校验严重依赖了分布式锁。因为在分布式锁正常set、del的情况下,库存校验是没有问题的。但是,当分布式锁不安全可靠的时候,库存校验就没有用了。

突击Redis重大事故现场,又是“分布式锁”惹的祸

 

解决方案

知道了原因之后,我们就可以对症下药了。

实现相对安全的分布式锁

相对安全的定义:set、del是一一映射的,不会出现把其他现成的锁del的情况。从实际情况的角度来看,即使能做到set、del一一映射,也无法保障业务的绝对安全。因为锁的过期时间始终是有界的,除非不设置过期时间或者把过期时间设置的很长,但这样做也会带来其他问题。故没有意义。 要想实现相对安全的分布式锁,必须依赖key的value值。在释放锁的时候,通过value值的唯一性来保证不会勿删。我们基于LUA脚本实现原子性的get and compare,如下:

public void safedUnLock(String key, String val) {
    String luaScript = "local in = ARGV[1] local curr=redis.call('get', KEYS[1]) if in==curr then redis.call('del', KEYS[1]) end return 'OK'"";
    RedisScript<String> redisScript = RedisScript.of(luaScript);
    redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));
}

我们通过LUA脚本来实现安全地解锁。

实现安全的库存校验

如果我们对于并发有比较深入的了解的话,会发现想 get and compare/ read and save 等操作,都是非原子性的。如果要实现原子性,我们也可以借助LUA脚本来实现。但就我们这个例子中,由于抢购活动一单只能下1瓶,因此可以不用基于LUA脚本实现而是基于redis本身的原子性。原因在于:

// redis会返回操作之后的结果,这个过程是原子性的
Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);

发现没有,代码中的库存校验完全是“画蛇添足”。

改进之后的代码

经过以上的分析之后,我们决定新建一个DistributedLocker类专门用于处理分布式锁。

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;    String key = "key:" + request.getSeckillId();
    String val = UUID.randomUUID().toString();    try {        Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);
        if (!lockFlag) {
            // 业务异常
        }        // 用户活动校验
        // 库存校验,基于redis本身的原子性来保证
        Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
        if (currStock < 0) { // 说明库存已经扣减完了。
            // 业务异常。
            log.error("[抢购下单] 无库存");
        } else {
            // 生成订单
            // 发布订单创建成功事件
            // 构建响应
        }
    } finally {
        distributedLocker.safedUnLock(key, val);
        // 构建响应
    }
    return response;
}

深度思考

分布式锁有必要么

改进之后,其实可以发现,我们借助于redis本身的原子性扣减库存,也是可以保证不会超卖的。对的。但是如果没有这一层锁的话,那么所有请求进来都会走一遍业务逻辑,由于依赖了其他系统,此时就会造成对其他系统的压力增大。这会增加的性能损耗和服务不稳定性,得不偿失。基于分布式锁可以在一定程度上拦截一些流量。

分布式锁的选型

有人提出用RedLock来实现分布式锁。RedLock的可靠性更高,但其代价是牺牲一定的性能。在本场景,这点可靠性的提升远不如性能的提升带来的性价比高。如果对于可靠性极高要求的场景,则可以采用RedLock来实现。

再次思考分布式锁有必要么

由于bug需要紧急修复上线,因此我们将其优化并在测试环境进行了压测之后,就立马热部署上线了。实际证明,这个优化是成功的,性能方面略微提升了一些,并在分布式锁失效的情况下,没有出现超卖的情况。 然而,还有没有优化空间呢?有的! 由于服务是集群部署,我们可以将库存均摊到集群中的每个服务器上,通过广播通知到集群的各个服务器。网关层基于用户ID做hash算法来决定请求到哪一台服务器。这样就可以基于应用缓存来实现库存的扣减和判断。性能又进一步提升了!

// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();
...
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
    Long seckillId = request.getSeckillId();
    if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
        // 业务异常
    }
     // 用户活动校验
     // 库存校验
    if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
        SECKILL_FLAG_MAP.put(seckillId, false);
        // 业务异常
    }
    // 生成订单
    // 发布订单创建成功事件
    // 构建响应
    return response;
}

通过以上的改造,我们就完全不需要依赖redis了。性能和安全性两方面都能进一步得到提升! 当然,此方案没有考虑到机器的动态扩容、缩容等复杂场景,如果还要考虑这些话,则不如直接考虑分布式锁的解决方案。

总结

稀缺商品超卖绝对是重大事故。如果超卖数量多的话,甚至会给平台带来非常严重的经营影响和社会影响。经过本次事故,让我意识到对于项目中的任何一行代码都不能掉以轻心,否则在某些场景下,这些正常工作的代码就会变成致命杀手!对于一个开发者而言,则设计开发方案时,一定要将方案考虑周全。怎样才能将方案考虑周全?唯有持续不断地学习!

金融AI模型上线后的系统性风险与生产治理实战 机器学习模型部署并非开发终点,而是进入真实业务环境的起点。在银行、保险等强监管场景中,模型稳定性高度依赖数据管道健康度、服务契约一致性与全链路可观测性。本文聚焦模型上线后最关键的系统性挑战——数据漂移如何量化预警、特征时效性如何保障SLA、服务降级时fallback逻辑如何设计、决策过程如何满足审计溯源要求。结合37起生产事故复盘与多个银行级项目实践,详解从延迟预算穿透管理、多维模型健康度仪表盘,到变更控制矩阵与决策快照(DecisionSnapshot)的工程落地路径,为构建高可信、可审计、可持续演进的A 阅读详情

相关推荐

避开这些坑!海天MA1600注塑机数据采集项目经验分享

本文分享了海天MA1600注塑机数据采集项目的实战经验。针对KEBA控制器接口不开放的难题,项目采用专用网关进行协议转换,并详细剖析了网络部署、ModbusTCP配置、数据准确性保障及现场运维中遇到的典型陷阱与解决方案,为同类工业物联网项目提供宝贵参考。

o4p5q6r7s的博客 491

记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

前言 基于Redis使用分布式锁在当今已经不是什么新鲜事了。本篇文章主要是基于我们实际项目中因为redis分布式锁造成的事故分析及解决方案。 背景:我们项目中的抢购订单采用的是分布式锁来解决的。 有一次,运营做了一个飞天茅台的抢购活动,库存100瓶,但是却超卖了!要知道,这个地球上飞天茅台的稀缺性啊!!!事故定为P0级重大事故…只能坦然接受。整个项目组被扣绩效了~~ 事故发生后,CTO指名点姓让我带头冲锋来处理,好吧,冲~ 事故现场 经过一番了解后,得知这个抢购活动接口以前从来没有出现过这种情况,但是这次为

a3961401的博客 4775

使用cartographer纯定位替换amcl定位

经过实际使用,发现cartographer定位对于重定位的表现比amcl好。如果机器人的起始位置离原点较远,amcl很难自行重定位到正确的位置,只能手动修改初始位置。然而cartographer定位的表现则很好,在行驶一段距离或原地旋转一段时间后能够正确重定位。

XJ_75的博客 8313

Redis配置文件redis.conf详解

# Redis 配置文件样例 # Note on units: when memory size is needed, it is possible to specify # it in the usual form of 1k 5GB 4M and so forth: # # 1k => 1000 bytes # 1kb => 1024 bytes # 1m => 10000...

战丶后风!!的博客 7491

用Python+PyTorch实现DQN无人机避障:从环境搭建到实战训练(附完整代码)

本文详细介绍了如何使用Python和PyTorch框架,基于深度强化学习中的DQN算法,实现一个无人机自主避障系统。内容涵盖从自定义环境搭建、DQN智能体核心算法实现,到完整的训练流程与超参数调优,并提供了可视化演示与进阶优化方向,为开发者提供了从理论到实践的完整指南。

kubernetes8ctl的博客 640

RK3588实战:如何将8路1080P视频流实时拼接成一张超清全景图?聊聊AVS模块的那些事

本文深入解析了RK3588芯片的AVS模块如何实现8路1080P视频流的实时全景拼接技术。通过硬件加速、时间同步、几何校正和色彩均衡等关键技术,RK3588能够高效处理高密度视频流,输出超清全景图。文章还提供了优化策略和实战经验,帮助开发者充分发挥RK3588的潜力。

weixin_27014595的博客 741

Redis配置文件redis.conf内容

由于近期我在网上找了大多数redis配置文件下载,都需要收取一定的费用,为了给大家整理配置文件的所有内容,特意去官网找到redis配置文件内容,免费给小白准备一份redis配置文件。 直接拿去改一下配置就能用,不用谢!如有所需资料可以直接在博客中问。 最新配置文件内容如下: # Redis configuration file example. # # Note that in order to read the configuration file, Redis must be # started wit

weixin_43322048的博客 525

SAP RAP 里的 EML 全解:从语言定位到事务语义,再到真实案例的落地方式

摘要:EML(Entity Manipulation Language)是ABAP RAP模型的核心组件,作为ABAP的子集用于操作业务对象(BO)。它通过声明式语法(READ/MODIFY/EXECUTE/COMMIT)统一访问BO的行为与数据,并由RAP运行时自动处理校验、锁定、草稿管理等生命周期。EML将业务语义融入代码,确保与Fiori应用共享同一套校验逻辑,并通过FAILED/REPORTED/MAPPED管道传递错误与消息。其事务控制(IN LOCAL MODE与COMMIT ENTITIES)

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 528

因Redis分布式锁造成的P0级重大事故,整个项目组被扣了绩效......,请慎用

作者:浪漫先生出处:juejin.im/post/5f159cd8f265da22e425f71d背景我们项目中的抢购订单采用的是分布式锁来解决的。有一次,运营做了一个飞天茅台的抢购活动...

emprere的博客 358

FAST-LIO论文公式解析

FAST-LIO论文公式解析

努力努力努力 670

5分钟快速上手:用AI生成3D虚拟场景的保姆级教程

你是否想过:不用学复杂的3D建模软件,不用懂“多边形”“UV贴图”这些专业术语,也能快速生成一个梦幻的森林、未来感的城市,甚至外太空基地?本文的目的就是教完全没接触过3D技术的新手,用AI工具5分钟生成3D虚拟场景。我们会覆盖主流AI工具的选择、提示词编写技巧、生成后的简单调整,以及常见问题解决。本文会像“游戏任务指南”一样,从“认识工具”→“写提示词”→“生成场景”→“调整优化”逐步讲解,最后还会教你把生成的3D场景用起来!提示词:你给AI的“订单备注”,越详细效果越好。扩散模型。

AI 原生应用开发的博客 1962

SpringBoot 分布式锁实战:从单机锁到Redis分布式锁全覆盖,解决超卖、重复下单、幂等并发问题

做电商、支付、库存扣减、优惠券核销、订单创建、接口幂等场景,并发安全是底线。很多新手本地测试加个或者 ReentrantLock,本地压测没问题,一上生产集群部署,立马出现:✅ 库存莫名超卖、✅ 同一用户重复下单、✅ 重复扣款对账不平、✅ 定时任务集群重复执行核心原因就一句话:单机锁只锁当前JVM实例,集群多台服务互不感知,锁直接失效。

KmSH8umpK的博客 632

Xilinx FPGA ICAP原语实现多重配置

众所周知,常见的FPGA通常为SRAM结构,固件程序一般存放在外置的串行Flash中,比如SPI Flash,M25P16或N25Q128等。 FPGA启动时,一般先从SPI起始地址开始加载数据到内部的SRAM,加载完成、校验通过则会直接运行。 那么有没有可能在SPI Flash中存放两个或多个FPGA固件呢?

whik1194的博客 4612

Redis高并发防止秒杀超卖实战源码解决方案

1:解决思路 将活动写入 redis 中,通过 redis 自减指令扣除库存。 2:添加 redis 常量 commons/constant/RedisKeyConstant.java seckill_vouchers("seckill_vouchers:","秒杀券的 key"), 3:添加 redis 配置类 4:修改业务层 废话不多说,直接上源码 1:秒杀业务逻辑层 @Service public class SeckillService { @Resource priv..

chongfa2008的博客 7915

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

Redis分布式锁-秒杀类锁不住及各种因为锁导致的“血案”现场全侦破代码详解

本文通过使用一个真实业务场景“秒杀(包括订单盖帽、抢券)”以及各种实际使用中因为不熟悉、没理解透技术造成的各种“redis锁翻车”实例。通过并结合jmeter大并发测试结果来说明不同的代码会带来怎么样的不同结果以及告诉大家redis分布式锁的真正正确用法是什么。

打造全国最全的AI Agent开发知识领域的博客 1285

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

redis 默认的配置redis.conf

# Redis配置文件样例 # Note on units: when memory size is needed, it is possible to specifiy # it in the usual form of 1k 5GB 4M and so forth: # # 1k => 1000 bytes # 1kb => 1024 bytes # 1m => 1000000 bytes # 1mb => 1024*1024 bytes # 1g => 10000000

yjph83的专栏 2080

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

redis.conf

1 # redis 配置文件示例 2 3 # 当你需要为某个配置项指定内存大小的时候,必须要带上单位, 4 # 通常的格式就是 1k 5gb 4m 等酱紫: 5 # 6 # 1k => 1000 bytes 7 # 1kb => 1024 bytes 8 # 1m => 1000000 bytes 9 # 1mb =&...

weixin_34153893的博客 664

i茅台自动预约项目宝塔部署详解

4、在网站管理里添加java项目,项目jar目录选择第2点上传的jar文件;2、在文件管理里,wwwroot目录下创建一个目录,上传jar文件以及application-prod.yml到该目录下;5、保存项目配置,如无意外,项目将会自动运行并运行成功;3、在网站管理里,java项目中安装java环境,JAVA环境管理->添加自定义JDK,选择1.8.0版本即可;1、宝塔安装mysql、redis,创建一个数据库,直接导入文件里的数据库文件;自己不喝酒,拿来送领导,送朋友,或者转卖都是很不错的。

weixin_45683900的博客 2714

Redis的配置文件

Redis 的配置文件

乾坤鸟的博客 683

RedisTemplet opsForHash中方法使用说明

以下各方法皆来自于Redis的命令,命令详细介绍参考https://blog.youkuaiyun.com/ppjsyw/article/details/125087623hset key field value 参数说明 key:键名 hashKey:字段名 value:值 示例 2.设置多个field 相关命令 hset key field value [field value …] 说明 key:键名m:多个键值对的map 3.当field不存在时设置 相关命令 hsetnx key field val

ppjsyw的博客 1万+

RedisTemplate map集合使用说明-opsForHash(三)

 1、put(H key, HK hashKey, HV v...

振钧 3023

Redis应用之并发处理

Redis应用之并发处理

miracleldf的博客 1254
上一篇: 备战金九银十!如何成功入职阿里,你差的也许就是这份学习笔记
下一篇: 真香!腾讯技术官发布Java零基础就业宝典,原来学习还可以这么简单
Java领域指导者
博客等级 码龄7年 404粉丝 265原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值