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

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 构建了一个基于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

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

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

相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
1月前
|
消息中间件 NoSQL Redis
秒杀的设计思路与实践
秒杀的设计思路与实践
33 1
|
1月前
|
缓存 前端开发 安全
秒杀系统架构分析与实战
秒杀系统架构分析与实战
118 0
|
1月前
|
移动开发 小程序 JavaScript
【经验分享】记一次支付宝小程序的性能优化
【经验分享】记一次支付宝小程序的性能优化
52 6
|
1月前
|
小程序 开发工具 git
小程序学习电商小项目实战(1)--框架搭建和准备
小程序学习电商小项目实战(1)--框架搭建和准备
|
SQL Dubbo NoSQL
推荐一个秒杀系统设计与实现项目(已开源)!
推荐一个秒杀系统设计与实现项目(已开源)!
393 0
推荐一个秒杀系统设计与实现项目(已开源)!
|
存储 缓存 负载均衡
秒杀架构分析与实践
秒杀系统相信很多人见过,比如京东或淘宝的秒杀,那么秒杀系统是如何实现的呢?如何设计一个秒杀系统呢?对于秒杀系统应该考虑哪些问题呢?
502 4
|
存储 canal 缓存
【高并发项目实战】千万级并发的购物车系统设计与代码详解
本文主要介绍redis在千万级系统中设计架构方案,如何在设计上抛弃mysql,主架构设计、缓存一致性方案、大value处理方案和redis限流和故障恢复降级方案设计与代码详解
|
消息中间件 Dubbo 前端开发
秒杀架构实践(上)
之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。 本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果
|
消息中间件 缓存 NoSQL
秒杀架构实践(下)
之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。 本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果
|
XML 监控 druid
秒杀架构实践(中)
之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。 本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果