P0级重大事故:超卖了100瓶飞天茅台,整个项目组慌得一逼~

Python预处理+Tableau可视化:解决非结构化数据落地难题 在BI工程实践中,非结构化数据(如XML/JSON/HTML)和复杂清洗需求(正则提取、嵌套解析、文本情感分析)常超出Tableau原生能力边界。其核心原理在于工具分工:Python作为数据工厂,承担API调用、爬虫获取、多层嵌套解析(如xmltodict+json_normalize)、缺失值智能填充与特征工程;Tableau专注交互式可视化与业务叙事。这种‘分段流水线’模式显著提升数据准备效率与分析可靠性,广泛适用于电商评论分析、社交舆情挖掘、IoT时序监控等真实场景,尤其适合Goodreads类无标准 阅读详情

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 8:55 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:juejin.cn/post/6854573212831842311


前言

基于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语句块保证锁一定会及时释放。业务代码内部也对库存进行了校验。看起来很安全啊~  别急,继续分析。。。

事故原因

飞天茅台抢购活动吸引了大量新用户下载注册我们的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的情况下,库存校验是没有问题的。但是,当分布式锁不安全可靠的时候,库存校验就没有用了。

解决方案

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

实现相对安全的分布式锁

相对安全的定义: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了。性能和安全性两方面都能进一步得到提升!当然,此方案没有考虑到机器的动态扩容、缩容等复杂场景,如果还要考虑这些话,则不如直接考虑分布式锁的解决方案。

总结

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



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

兄弟,艿一口,点个赞!????

一次Redis生产事故,翻车了... 一、前因 公司有个核心项目redis的客户端一直是使用的jedis,后面技术负责人要求把jedis客户端替换成效能更高的lettuce客户端,同时使用spring框架自带的RedisTemplate类来操作redis。 然而世事难料,就是这么一个简单的需求却让老司机也翻了车。。。 二、事故预演 按照预设的结果,本次开发任务应该是非常轻松的: 将配置文件中jedis连接池的配置项平移替换成lettuce的; 把项目中jedis配置相关的代码删掉; 把使用到jedis的地方替换成redisTemp 阅读详情

相关推荐

P0级故障:头部电商双11大促 全链路故障损失超1.2亿,起因是 服务发现组件雪崩。骇人听闻P0级案例 (1)

P0级故障:头部电商双11大促 全链路故障损失超1.2亿,起因是 服务发现组件雪崩。骇人听闻P0级案例 (1)

架构师尼恩 1404

软件开发事故级别分类

开发事故级别

u013367040的博客 1万+

设计数据库 ER 图太麻烦?不妨试试这两款工具,自动生成数据库 ER 图!!!

告别手动画图,提高效率~

楼下小黑哥 2万+

信息化系统生产环境P0-P4事故级别划分

● P0:核心业务重要功能不可用,且影响范围广泛,如大面积影响用户。信息化系统 P0至P4事故级别描述了不同严重程度的服务中断或功能故障,其中P0代表最高级别的事故。● P2:核心业务周边功能不可用,如果持续故障会大面积影响用户体验。● P1:核心业务重要功能不可用,但影响用户有限,例如仅影响内部用户。● P4:周边业务功能不可用,但基本不影响用户正常使用。这些级别有助于组织和服务提供商根据事故的严重程度和影响范围,优先处理和响应问题。● P3:周边业务功能不可用,轻微影响用户体验。

1万+

为什么类型论是函数式编程的理想基础?

函数式编程(如Haskell、Scala、OCaml)以“函数为核心”“无副作用”“不可变数据”等特性著称,但它的“安全高效”并非凭空而来——背后的关键支撑是类型论。本文将聚焦“类型论如何支撑函数式编程”,从基础概念到具体案例,解释两者的本质联系。本文将按“故事引入→核心概念解释→关系拆解→原理+代码案例→实际应用→总结”的逻辑展开,用“乐高积木”“菜谱”等比喻贯穿全文,确保抽象理论可视化。类型论:程序的“接口规则手册”,定义类型和函数的约束;函数式编程。

AI天才研究院 698

23数字图像置乱技术(matlab程序)

所谓“置乱”,就是将图像的信息次序打乱,a像素移动到b像素位置上,b像素移动到c像素位置上,……,使其变换成杂乱无章难以辨认的图片。数字图像置乱技术属于加密技术,是指发送发借助数学或者其他领域的技术,对一幅有意义的数字图像做变换,使之变成一幅杂乱无章的图像用于传输。在图像传输过程中,它通过对图像像素矩阵的重排,破坏了图像矩阵的相关性,使非法截获者无法从杂乱无章的图像中获得原图像信息,以此实现信息的加密。接收方经去乱解密可恢复原图像。

m0_57943157的博客 1209

p0级重大事故:超卖了100瓶飞天茅台,整个项目组慌得一逼~

Python实战社群Java实战社群长按识别下方二维码,按需求添加扫码关注添加客服进Python社群▲扫码关注添加客服进Java社群▲作者:浪漫先生来源:https://juejin.c...

<sdffdsfsdfdfs>sfsfsfsdfsdffds</sdfsDS>Fsd 2177

STM32CUBEMX | STM32L431RCT6的内部Flash操作

一、STM32CUBEMX配置 1、选择芯片型号: 2、配置时钟源 1、 如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC; 2、 如果使用默认内部时钟(HSI),这一步可以略过; 3、配置时钟树 STM32L4的最高主频到80M,所以配置PLL,最后使HCLK = 80Mhz即可: 4、代码生成设置 点击GENERATE CODE即可生成MDK-V5工程: 二、在MDK中编写、编译、下载用户代码 1、STM32内部Flash及HAL库API 查看所使用芯片的信息,

yongridada的博客 5570

P0级重大事故:超卖了100瓶飞天茅台,整个项目组慌了

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

weixin_39787242的博客 7496

ilqr算法原理推导及代码实践

ilqr算法原理详解

Forwarding3的博客 1741

TypeError: ‘<=‘not supported between instances of ‘float’and ‘str‘解决方案

本文主要介绍了TypeError: '

weixin_43178406的博客 28万+

使用 Redis 分布式锁造成重大事故,导致超卖 100 瓶飞天茅台!

>>号外:关注“Java精选”公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料、开源项目。基于Redis使用分布式锁在当今已经不是什么新鲜事了。本篇文章...

Java精选 341

pcie硬件时序分析实战(PHY层眼图测量)​

PCIe高速接口的信号完整性(SI)直接影响系统带宽和稳定性。其中,​。

xiaoheshang_123的博客 861

Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

点击上方蓝色字体,选择“设为星标”回复 ”资源“ 获取面试宝典基于Redis使用分布式锁在当今已经不是什么新鲜事了。本篇文章主要是基于我们实际项目中因为redis分布式锁造成的事故分析及...

程序IT圈 141

实战篇| app爬虫不用真机

之前写过一篇基于真机app爬虫《App爬虫实战篇-以华为真机手机爬取集换社的app为例》,如果大家有用真机爬虫的经历,会深刻认识到真的不方便,一是自己的手机,经常会被电话、微信等打断,二是闲置手机,面对小批量数据还能勉强,大批量爬虫,会导致成本指数级上升,并且控制起来相对麻烦。以下就是常见的模拟器。emulator -avd my_avd -no-snapshot-save # 启动时不保存 snapshot。emulator -avd my_avd -wipe-data # 清除数据(恢复到出厂)

youbingchen的博客 597

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Redis 分布式锁使用不当,超卖了100瓶飞天茅台!!!

往期热门文章:1、《往期精选优秀博文都在这里了!》 2、接口的幂等性的N种考虑,你知道吗? 3、如果程序员和产品经理都用凡尔赛文学对话...... 4、Spring Boot 2.4.0...

Java后端技术 532

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

上一篇: 再见,Navicat!这个 IDEA 的兄弟,真香!
下一篇: 科普 | 越来越火的图数据库究竟是什么?
公众号-芋道源码
博客等级 码龄10年 1636粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值