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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 Tair(兼容Redis),内存型 2GB
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

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

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

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
6月前
|
消息中间件 NoSQL Redis
秒杀的设计思路与实践
秒杀的设计思路与实践
60 1
|
1月前
|
缓存 NoSQL Java
秒杀圣经:10Wqps高并发秒杀,16大架构杀招,帮你秒变架构师 (1)
高并发下,如何设计秒杀系统?这是一个高频面试题。40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试Shopee时遇到了这个问题,未能很好地回答,导致面试失败。为此,尼恩进行了系统化、体系化的梳理,帮助大家提升“技术肌肉”,让面试官刮目相看。秒杀系统设计涉及16个架构要点,涵盖业务架构、流量架构、异步架构、分层架构、缓存架构、库存扣减、MQ异步处理、限流、熔断、降级、存储架构等多个方面。掌握这些要点,可以有效应对高并发场景下的秒杀系统设计挑战。
秒杀圣经:10Wqps高并发秒杀,16大架构杀招,帮你秒变架构师 (1)
|
27天前
|
消息中间件 架构师 Java
阿里面试:秒杀的分布式事务, 是如何设计的?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试阿里、滴滴、极兔等一线互联网企业时,遇到了许多关于分布式事务的重要面试题。为了帮助大家更好地应对这些面试题,尼恩进行了系统化的梳理,详细介绍了Seata和RocketMQ事务消息的结合,以及如何实现强弱结合型事务。文章还提供了分布式事务的标准面试答案,并推荐了《尼恩Java面试宝典PDF》等资源,帮助大家在面试中脱颖而出。
|
6月前
|
缓存 前端开发 安全
秒杀系统架构分析与实战
秒杀系统架构分析与实战
195 1
|
6月前
|
小程序 开发工具 git
小程序学习电商小项目实战(1)--框架搭建和准备
小程序学习电商小项目实战(1)--框架搭建和准备
|
缓存 监控 Linux
C++项目实战-高并发服务器详析(二)
C++项目实战-高并发服务器详析(二)
71 0
|
Linux 调度 C++
C++项目实战-高并发服务器详析(一)
C++项目实战-高并发服务器详析(一)
208 0
|
SQL Dubbo NoSQL
推荐一个秒杀系统设计与实现项目(已开源)!
推荐一个秒杀系统设计与实现项目(已开源)!
454 0
推荐一个秒杀系统设计与实现项目(已开源)!
|
canal 缓存 NoSQL
关于项目点赞问题的高并发解决
采用的技术方案是redis解决的
566 0
|
存储 缓存 负载均衡
秒杀架构分析与实践
秒杀系统相信很多人见过,比如京东或淘宝的秒杀,那么秒杀系统是如何实现的呢?如何设计一个秒杀系统呢?对于秒杀系统应该考虑哪些问题呢?
544 4