秒杀项目实战:遇到的问题及解决方案分享

简介: 构建了一个基于Springboot2的秒杀系统。项目利用K8S上的主从结构部署Redis和MySQL,通过Traefik作为网关。RabbitMQ在本地虚拟机的docker环境中,用Prometheus+Grafana监控。设计思路包括隐藏秒杀地址以防止脚本攻击,使用Lua脚本保证库存预扣原子性,但初期版本未处理重复订单校验。为防止MQ故障,将订单信息先保存到Redis,再通过脚本发送到MQ。采用分布式锁防止用户重复下单和缓存击穿问题,使用编程式事务确保库存扣减与订单保存一致性。项目通过JMeter测试,观察性能并分析Redis和RabbitMQ的使用情况。完整代码可在GitHub找到。

搭建秒杀项目,我使用的技术栈如下👇(项目地址在文末)

Springboot2 + Redis7 + Lua + Redisson + MySQL8 + RabbitMQ3.9 + MybatisPlus + Hutool

其中 RedisMySQL 都是之前搭建在云端 K8S 上的 主从 结构,用 Traefik 做总网关。

RabbitMQ 则是之前在本地虚拟机上用 docker 搭建的 ,还有 Prometheus + Grafana 监控。

思路

隐藏秒杀地址

这个就是实现一个用户一个地址,给脚本工具加点难度。

根据需要生成这个 path,比如用 md5 混淆下 。

然后放到 Redis 中 key :秒杀活动ID+’path‘ + 秒杀商品ID+用户ID , value :path

真实的秒杀地址如下

lua 脚本预扣库存

用 lua 脚本来保证这个操作的原子性,判断 库存key 存不存在,数量够不够,够的话执行扣减操作

bug

我这样写的脚本是有问题的,没有进行 重复订单校验 , 以及 set 这个 订单信息 到 redis 中。

这 3 步操作应该是原子性的,校验,扣减,设置

所以即便 lua 脚本能保证操作的原子性,但是并发情况下会出现 少卖 的情况。

模拟同个用户 50 个并发 100个库存

改正版

改正后也就正常了,之前我是老想着 订单ID 的生成要从 分布式ID 中获取,想尽量较少这个 网络请求 的,一不小心就疏忽了。(以后得先把 核心思路 写下,再思考优化,不能边写边想优化了🐷)

分布式ID ,我之前研究这个 美团Leaf 也是想简单搭建一个,奈何总喜欢偷懒🐷,这里我是用 Hutool雪花算法 简单生成的。

保存订单信息到 Redis

大bug 之前,我以为这里只是做 重复订单校验 的,没想到,还有这种情况 👇

MQ 挂了,消息还没发送出去,甚至一开始就没连接上的情况。

比如 我这个本机和虚拟机 休眠后得重启下 虚拟网络vm8,不然连不上去。

意料之外~

所以,这里得写个小脚本,将 订单信息 发送到 MQ 中,在紧急情况下能快速补救。

分布式锁

目前用 jvm 级别的锁其实就足够了,但后面上集群还得改代码,干脆一鼓作气。

锁的粒度,不能太大,主要防止用户重复下单。

比如

  1. 第一版 错误的 lua 脚本中,就会出现 重复下单 的情况
  2. 集群模式下,多个消费者的情况,此时谁先拿到分布式锁,谁就可以消费这个订单,避免重复下单

通过分布式锁,保证这个订单只有一个消费者消费,即便在多个消费者模式下,也不会出现 重复下单 的情况。

同时,也可以防止使用 Redis 出现意外,就像上面 错误使用 lua 脚本的案例,以及 可能存在的 key 过期等问题导致的重复下单问题。

当然,这还不是 兜底方案 ,万一这个 分布式锁 也出现意外了呢,所以保险起见,还需要给 订单表 建立 唯一索引(用户id+商品id),靠数据库本身保证了。

这里如果不用分布式锁,那就得从数据库层面去保证了,得用 select …… for update 开启 悲观锁,那效率会进一步降低的。

注意,这里也是 缓存击穿 的常见解决思路,分布式锁,双重检查锁模式。

事务

我这里是简易版的,没有涉及到 分库分表,所以也谈不上这个 分布式事务。

这里我用的 编程式事务 ,毕竟 扣减库存和保存订单 要在一个事务里,用注解的话还得考虑这个失效的场景,获取这个代理对象去执行,没有这个 编程式事务 来得方便。

假设 订单在订单库中,商品在商品库中,那这种情况下,是不是还得考虑这个 分布式事务 呢?

我可能还是不会选择这个 分布式事务 ,我会直接往 商品库 中 建立一个 秒杀订单表 或者在 订单库 中建立这个 秒杀商品库存表,甚至专门弄一个 秒杀库冗余 一下,事后如果需要同步到相应的 库表 中,再进行相关的操作。

那假如还有个积分系统呢 ?

比如 支付回调后,更新订单状态的同时,还要更新这个用户积分。

这我还是会选择 MQ ,通过 MQ 的可靠性 来达到这个 最终一致性

先发送消息到积分系统,更新订单信息单独在事务中。

这是分布式事务中常见的一种解决方案 基于MQ可靠性消息的最终一致性方案

有时间可以学习下 Seata

重试机制

上图将 MySQL 和 MQ 的操作放一起,还得小心这个 MQ 的异常,导致这个 事务回滚,但是 ACK 还是正常发出去的情况。

这里我最后还将异常抛出去,是为了触发这个 重试机制 ,配置文件中 开启 RabbitMQ 消费者重试机制即可。

ACK 前发生异常,事务回滚,触发重试机制。

ACK 中发生异常,捕获,丢弃异常,提交事务。再次消费时,发现是重复订单。

ACK 后还有异常,未捕获,事务回滚,但消息已经被 ACK,触发了重试机制,在重试期间没有异常,则正常处理。如果重试后还有异常,则会出现 消息丢失 的情况,这又得 紧急处理 了。

防止超卖

有两个扣减动作

Redis 预扣库存,这里得在 lua 脚本中操作。

MySQL 扣减库存,这里核心就是 乐观锁的方式 a=a-1 where a > 0;

缓存

这里再简短啰嗦下

缓存穿透

针对不存在的 key ,可以用 布隆过滤器

缓存击穿

key 刚好过期,或者 商品成了爆款

分布式锁双重检查锁模式 能解决上面这两种情况,锁的粒度也是这个 商品。

针对 key 刚好过期 的情况,我了解到一种新的处理思路:逻辑过期

不在 Redis 中判断是否过期,在 代码 中进行判断,过期的话获取锁,开线程去更新,但实现起来比较复杂。

缓存雪崩

大量 key 同时过期,可以 给不同的Key的TTL添加随机值给业务添加多级缓存降级限流策略安排上

总结

到这里,这个简易秒杀系统就介绍完了,至于 限流,用户鉴权,标记 ,订单支付,超时处理,消息的顺序性 …… 再到大一点的 集群,缓存一致性 等等东西,得抽空再完善下了。

搭建过程中,最有意思的是,一直防着 超卖,结果还出现了 少卖 的场景😂

所以这 Redis 预扣库存 也得谨慎呀,lua脚本 三合一疗程:查,扣,存 😂

MySQL 也一样,分布式锁事务 ,查,扣,存

下面是我用 JMeter 测试的一些数据情况👇

JMeter

这里两个 http 请求分别模拟,获取秒杀地址开始秒杀

jmeter 500 个并发,100 件库存

报告一

这个 平均响应 是 326 ms , 50 % 的请求是 245 ms,99% 是 1342 ms ,最小是 21 ms,最大是 1359 ms ,吞吐量是 605/s 。

这个成绩。。一言难尽,这还是用了 MQ 异步下单 ,还有 内存标记Redis 预扣库存 的结果,而且是 预热了 JVM 的情况😱

这最大的开销应该是网络问题,要访问 云服务器 K8S 中的 Redis 以及 本地虚拟机上的 MQ。

或者是我的老伙计性能问题,又得跑项目,还得测试,这 CPU ,内存,网卡 估计也忙坏了。

简单分析下 👇

获取秒杀地址 , 这里就访问一次 Redis ,执行 Set 命令。

开始秒杀 中,涉及的网路操作有

  1. 校验地址
  2. 是否重复下单
  3. 预扣库存 lua 脚本
  4. 发送订单信息到 MQ(虚拟机上)

后面把项目搭建到云服务器上再来测下。

报告二

这里看到 第一个请求 的 RT 都比第二个请求的 小。

Redis

Redis 内存使用情况(测试前)

Redis 内存使用情况(使用后)

可以看到,内存多了 0.1M 左右,这是多了 601 个 key

至于怎么多了 32 条 client connection , 只能做个简单的推测先了

项目中使用了这个 redisson 做分布式锁,占用了 25 条

简单看下源码

拿到服务器上的所有连接,排掉之前的 5 条,刚好剩下 32 条。

这里看到使用 resp3 的有 7 条,刚好符合,应该是 RedisTemplate 相关创建的。

这里简单看下源码, Redis 6 开始默认使用 RESP3 的协议的


RabbitMQ

下面是从 Prometheus + Grafana 监控截取的

RabbitMQ 使用情况(测试前)

RabbitMQ 使用情况(测试中)

这里 发送端和消费端 在一个应用上,共用一条 connection, 发送端创建了 24 个 channel , 消费端 2 个。

发送端第一条 MQ 数据

发送端第一条 MQ 数据被 ACK

从这个监控图可以看到,消费端开始消费的时间点大概是 16:47:00

而生产者发送第一条消息和被confirm 的时间大概是 16:46:30 ; 这个有误差是因为这个监控自动刷新的频率是 15s ,目前是最小的了(可能是我挑的模板问题,或者是这并发太小😂)

消费者消费能力,大概每秒 2 个 ack

channel


K8S

minikube 节点,上面运行了 Redis 主从 , MySQL 主从。

K8S的情况(测试前)

K8S的情况(测试后)

基本没变化。

后面再把 MQ 和 镜像仓库搭建一下,然后再把项目丢上去跑跑看看 ,到时再看看这个测试报告。

结尾

Github地址:https://github.com/Java4ye/springboot-demo-4ye/tree/main/springboot-redis

本文就到这里啦,感谢您的阅读,有不对的地方也请您帮忙指正!谢谢~😋

喜欢的小伙伴们,别忘了点赞关注呀~😋 祝你有个美好的一天!😝如下

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
NoSQL IDE 开发工具
**《惊爆!揭开函数调用关系图的神秘面纱,让你的代码世界天翻地覆!》**
【8月更文挑战第16天】函数调用关系图是软件开发中的重要工具,帮助直观理解程序结构与逻辑流程,有效进行代码优化、调试及复杂系统理解。可通过静态分析工具(如SourceMonitor)在不运行代码情况下构建调用图,或利用动态跟踪(如GDB、Python的`sys.settrace`)在运行时记录调用顺序。集成开发环境(IDE)如Visual Studio亦提供相关功能。不同方法各有优势,可根据需求灵活选择。
580 4
|
网络协议 网络架构
UDP包的大小与MTU
在进行UDP编程的时候,我们最容易想到的问题就是,一次发送多少bytes好?当然,这个没有唯一答案,相对于不同的系统,不同的要求,其得到的答案是不一样的,我这里仅对像ICQ一类的发送聊天消息的情况作分析,对于其他情况,你或许也能得到一点帮助:首先,我们知道,TCP/IP通常被认为是一个四层协议系统,包括链路层,网络层,运输层,应用层.
4205 0
|
11月前
|
消息中间件 NoSQL 架构师
招行面试:亿级秒杀,超卖问题+少卖问题,如何解决?(图解+秒懂+史上最全)
45岁资深架构师尼恩在读者交流群中分享了如何系统化解决高并发下的库存抢购超卖少买问题,特别是针对一线互联网企业的面试题。文章详细解析了秒杀系统的四个阶段(扣库预扣、库存扣减、支付回调、库存补偿),并通过Redis分布式锁和Java代码示例展示了如何防止超卖。此外,还介绍了使用RocketMQ延迟消息和xxl-job定时任务解决少卖问题的方法。尼恩强调,掌握这些技术不仅能提升面试表现,还能增强实际项目中的高并发处理能力。相关答案已收入《尼恩Java面试宝典PDF》V175版本,供后续参考。
|
数据采集 前端开发 算法
Python Requests 的高级使用技巧:应对复杂 HTTP 请求场景
本文介绍了如何使用 Python 的 `requests` 库应对复杂的 HTTP 请求场景,包括 Spider Trap(蜘蛛陷阱)、SESSION 访问限制和请求频率限制。通过代理、CSS 类链接数控制、多账号切换和限流算法等技术手段,提高爬虫的稳定性和效率,增强在反爬虫环境中的生存能力。文中提供了详细的代码示例,帮助读者掌握这些高级用法。
770 1
Python Requests 的高级使用技巧:应对复杂 HTTP 请求场景
|
消息中间件 Java 数据库
秒杀系统库存超卖问题:从传统解决方案到引入RabbitMQ
秒杀系统库存超卖问题:从传统解决方案到引入RabbitMQ
828 0
|
12月前
|
SQL 关系型数据库 MySQL
小索引大力量,记一次explain的性能优化经历
本文介绍了在MySQL生产环境中使用EXPLAIN工具进行性能优化的过程。通过分析慢查询日志,识别出性能瓶颈,并利用EXPLAIN命令解析SQL执行计划,找出全表扫描、未使用索引等问题。文章还详细描述了如何配置慢查询日志、解读EXPLAIN输出的关键字段(如type、key、rows等),并提供了优化建议,如避免左右模糊查询、减少多表联查等。最终验证优化效果,确保系统性能提升。此外,强调了项目初期建立索引的重要性,以应对未来数据量增长带来的挑战。
486 0
|
负载均衡 Java API
小红书商品详情API接口获取步骤
小红书商品详情API接口使用指南:先注册并实名认证获取权限,阅读API文档了解使用方法;通过编程调用API,构建请求参数,处理返回数据;注意高并发下的性能优化,确保安全合规;申请API权限,查阅文档,完成开发与调试。
|
弹性计算 负载均衡 网络协议
云计算中的弹性伸缩与负载均衡技术解析
【7月更文挑战第4天】弹性伸缩与负载均衡作为云计算平台中的两大关键技术,对于构建高可用、可扩展的应用系统具有重要意义。通过合理利用这两种技术,企业可以灵活应对不断变化的业务需求,降低运营成本,提高资源利用效率。未来,随着技术的不断进步和应用的深入,弹性伸缩与负载均衡技术将在更多领域发挥重要作用,推动云计算技术的持续发展。
|
SQL 安全 Java
java的SQL注入与XSS攻击
java的SQL注入与XSS攻击
359 2
|
消息中间件 NoSQL Java
Java必备面试题(100题)-八股篇
主要包括一些高频的Java面试的八股文面试题和答案