请教秒杀抢购架构设计问题

2022-09-07 10:04:00 +08:00
 franklinre
需求:用户可以抢购某个商品,提交信息后跳转到指定页面( http:\\host\productId\userId )等待刷新,后台任务队列处理完成后,刷新出抢购的订单信息。
这样可以等待 order_table 出现相应的 productId 和 userId 的记录出现时能正确刷新出订单信息。
但是,如果 order_table 已经有该 productId 和 userId 的记录或者允许多次抢购,order_table 会出现多条 productId 和 userId 的记录,该怎么确定指定的订单信息呢?
我暂定是前端生成一个 uuid ,跳转到: http:\\host\productId\userId\uuid 等待刷新,uuid 存进该条记录到 order_table 。
各位老哥,你们也是类似这样设计的吗?

另外,我看到很多文章设计抢购系统是在 redis 放进库存数量,抢购成功扣库存,类似的设计。
我想,能不能生成预备订单数据,库存 100 个,就先生成 100 个预备订单,抢购时就查找有无预备订单,有的话就把用户信息放进该预备订单,就表示抢购成功。
问:在每次订单只允许抢购一个商品的前提下,是否方法二的可靠性更强?
3401 次点击
所在节点    问与答
30 条回复
xzh20121116g
2022-09-07 10:09:15 +08:00
redis 更快一点,最多给用户加个锁;
生成预备订单的话,是要给接口加锁吧,要大并发的话,还是选 redis 吧
stonewu
2022-09-07 10:15:49 +08:00
这个 UUID 的方案是可行的,但是直觉上会放到后端生成返回给前端

库存问题的话,扣减库存数的操作在能保证原子性的情况下,速度应该是比查找预备订单快的,扣库存的方案足够满足需求了
opengps
2022-09-07 10:15:51 +08:00
order_table 不现实,抢购对于处理效率要求极高,能在内存层面快速处理就不适合放入硬盘层面降低效率。并发系统选用 redis 原因:redis 在内存中快速处理,redis 提供可靠的锁来防超售
lmshl
2022-09-07 10:27:34 +08:00
正好在写一篇关于秒杀抢购系统的文章,我先说结论:抢购秒杀系统考验的是你对计算机体系结构的认知,所有涉及 Redis / 分布式锁的方案,有一说一,路走窄了

前两天我自己写的秒杀例子,在我笔记本上使用 2 核心,性能大约是
!!!: 17704ms consume 524287
折合每秒 2.96 万单,模拟的是 50 万请求同时抢购同一件商品,尚未做拆分。

代码: https://github.com/mingyang91/akka-ticketing
LeegoYih
2022-09-07 10:57:29 +08:00
秒杀的主要瓶颈在数据库,尤其是 MySQL 这种,所以目标是减少无效请求,保证请求到数据库层面的都是有效的,所以用缓存是必要的。


1. 客户端按钮设置隐式冷却

OP 描述的“提交信息后跳转到指定页面”,用户点提交按钮,可能会返回上一页,然后继续提交,这样会生成很多无效的订单,我抢消费券就是这么干的,所以需要加一个冷却时间,用户一直操作,实际上每秒可能只有一次请求

2. 网关层限流

按钮冷却只能防普通用户,网关限流用来防止懂技术的脚本哥,根据 IP 或者 UserId 进行限制请求次数。
或者直接对商品进行限流,例如:如果商品只有 10 个,每秒请求有 10000 个,那么实际上大部分请求都是无效的,允许每秒 100 - 1000 请求进来即可,其他的直接返回「已抢光」,这里可以使用「令牌桶」和「滑动窗口」算法。

3. 服务层

OP 说用 UUID ,我感觉还是后台生成比较好,收到请求后,生成一个 OrderId 放到缓存中,然后通过 MQ 异步创建订单,直接把 OrderId 返回给客户端,再提供一个接口让前端轮询缓存中的订单状态。
MQ 消费完成后更新缓存中的订单状态,客户端发现订单已创建,再去查询真正的订单详情。

服务层使用 Redis 维护库存数量的优点也很明显,如果库存没了直接返回即可,不用调数据库,保证数据库扣减库存、生成订单都是有效操作。

4. 数据库层

通过数据库自身的锁保证原子性,防止超卖,此时数据库基本没什么压力。
wdwwtzy
2022-09-07 11:20:24 +08:00
@lmshl 请问文章在哪里能看到呢?
lmshl
2022-09-07 11:27:01 +08:00
文章昨天才开始写,示例代码已经放 GitHub( https://github.com/mingyang91/akka-ticketing) 了。
在我笔记本连远程数据库的环境中,单商品每秒接近三万的订单确认速率,完全不需要引入任何的 MQ / Redis / 主从分表等复杂中间件
wdwwtzy
2022-09-07 11:35:01 +08:00
@lmshl 这么神奇吗,拿本地锁来做的吗?
lmshl
2022-09-07 12:19:27 +08:00
@wdwwtzy

因为在计算机体系结构中
L1/2/3 >> Memory >> SSD > Network
所以只要开始扯 Redis ,就已经输在起跑线上了
限制秒杀系统极限的有两个因素
1. CPU 的 IPC 和 主频 :提供锁
2. 磁盘顺序写入速度:提供持久化

结论显而易见,数据库只存储交易日志,事务由程序保证,以数据库写入响应视为事务提交
同时将交易日志的持久化做批量化聚合,一次写入一批以最大程度减少 磁盘 / 网络 的 IO 响应次数

做到以上几点,软件正确性和性能就都得到了保证,在此之上还可以利用负载均衡 hash ,或引入 akka cluster sharding 等分布式集群组件来做横向扩展和故障转移,一个接近理论上限的秒杀系统就完成了。

这也就是为什么我选择了 CQRS 模型的 Akka-Projection
Pythoner666666
2022-09-07 12:53:47 +08:00
文章发出来记得 @我 我很想拜读一下
@lmshl
2bad4u
2022-09-07 12:59:29 +08:00
@lmshl 有干货,求细讲。
sujin190
2022-09-07 16:07:00 +08:00
@lmshl #4 粗略看下,秒杀的难点本来也不是多快,否则这的人大概率都能写出一个处理 10k 以上 qps 的程序,现实中秒杀麻烦的是除了要处理商品库存订单问题外,还有营销折扣系统、优惠系统、风控系统、配送与地址系统等等,这一系列下来之后会是一个非常长的流程,在各系统负载一致和事务一致处理起来会非常麻烦,从这一点上来说,直接使用带库存数分布式锁直接拦截在所有系统前面才是最容易实现且靠谱的方案,反而是使用队列平滑再反馈结果其实更麻烦

而且还有现实大概率不会出现却又不得不考虑的崩溃恢复问题,队列造成了较长处理链路是清理的复杂性想满足秒杀场景下较短的崩溃恢复时间还是十分难的,使用分布式锁拦截则可以设置较短的等待时间即可,也没有进入下单的业务流程,随着时间超时后就会自然恢复
buster
2022-09-07 16:31:44 +08:00
网上搜索了一下,大概思路可能是这个 https://scala.cool/2017/08/learning-akka-8/
lmshl
2022-09-07 16:40:24 +08:00
@sujin190 并不是
1. 我的例子代码中已经保证了库存严格不超售,且订单记录有审计。
2. 其他系统和核心下单系统并不属于一个 Domain ,属于可以垂直拆分的组件,同时,折扣优惠,封控属于可以被前置业务逻辑检查的,也绝对不应该被合并到下单事务中一起处理
3. 崩溃恢复已经被 akka-projection 实现了,当然自己实现也不难,从快照+快照以后发生的日志快速回放一遍很容易。

分布式锁是最差最差的方案了,使用了分布式锁以后,你的库存扣减逻辑很难高过 100tps ,这是理论上限
lmshl
2022-09-07 16:43:03 +08:00
@buster 是的没错,扫了一眼他的文章,和我思路基本一致,不过他没提供代码示例

我用的 akka-projection 是依赖 akka-cluster-sharding 和 akka- persistence 实现的
sujin190
2022-09-07 16:50:04 +08:00
@lmshl #14 那你这分布式锁实现有问题吧,否则按你这么说 mysql 也无法完成超过这个限值了,那怎么搞岂不是都没用了

超不超卖的本来也不难处理吧,想要尽可能可靠从秒杀来说,既然库存非常小否则也不叫秒杀了,那么就应该除了让正常库存进入下单流程外,其他请求的处理过程都尽可能短,最好到达网关就直接返回,你再搞快照搞回滚,那么不是增加了可能出错的点了吧,多一步就多一步出错的点,啥都不用做自然也不可能有任何异常了
lmshl
2022-09-07 17:00:00 +08:00
@sujin190 磁盘和网络 IO 都需要时间,即使是内网,最简单的 KV 访问也需要 >1ms 时间
你用分布式锁,怎么和软件内存中实现的事务比,直接输在起跑线上了好吧

访问 L1/2/3 到内存,时间单位可都是 ns ,内存运算完事务扔给磁盘持久化,磁盘写入多快,秒杀就能有多快。
高下立判
sujin190
2022-09-07 17:14:07 +08:00
@lmshl #17 网络请求又不是串行的,整个分布式锁本来也就只在加减一才串行,哪里有问题了,就算要多机强一致,网络请求同步过程依然可以并行,多机强一致下百万以上 qps 可能做不到,十几万 qps 也还是可以的,一旦库存抢完就进入完全并行阶段,百万以上 qps 不很轻松么,这个的实现有简单,直接集成在网关里也没毫无问题,这显然已经是改造最小集成最简单的方案了
sujin190
2022-09-07 17:22:14 +08:00
@lmshl #17 https://github.com/snower/slock
顺便说我已经做出来了,真的不是在嘴炮,3 节点多机强一致阶段大概能有超过 10 万 qps ,并行阶段能接近 200 万 qps ,极其简单的协议也可以直接集成在 openresty 里,我还是觉得这种在非淘宝拼多多这种超大站点,这个应该是最容易实现维护的架构了,毕竟只要你不是动辄秒大量商品库存,只要在现有订单系统前添加拦截流程就可以,几乎不需要针对秒杀逻辑对订单系统做单独调整
lmshl
2022-09-07 17:24:34 +08:00
@sujin190 “整个分布式锁本来也就只在加减一才串行” 这不就是串行了么?如果失败了你还得再分布式里处理回滚,复杂度不就上升了么?

而且我是秒杀在 1 个根上就可以做到一秒接近 3 万笔交易,横向扩展也没有上限,整个系统除了业务 Java 进程,只需要 数据库 + 负载均衡就可以运作了,维护成本更低。显然更小吧

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/878258

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX