1
xzh20121116g 2022-09-07 10:09:15 +08:00
redis 更快一点,最多给用户加个锁;
生成预备订单的话,是要给接口加锁吧,要大并发的话,还是选 redis 吧 |
2
stonewu 2022-09-07 10:15:49 +08:00
这个 UUID 的方案是可行的,但是直觉上会放到后端生成返回给前端
库存问题的话,扣减库存数的操作在能保证原子性的情况下,速度应该是比查找预备订单快的,扣库存的方案足够满足需求了 |
3
opengps 2022-09-07 10:15:51 +08:00
order_table 不现实,抢购对于处理效率要求极高,能在内存层面快速处理就不适合放入硬盘层面降低效率。并发系统选用 redis 原因:redis 在内存中快速处理,redis 提供可靠的锁来防超售
|
4
lmshl 2022-09-07 10:27:34 +08:00
正好在写一篇关于秒杀抢购系统的文章,我先说结论:抢购秒杀系统考验的是你对计算机体系结构的认知,所有涉及 Redis / 分布式锁的方案,有一说一,路走窄了
前两天我自己写的秒杀例子,在我笔记本上使用 2 核心,性能大约是 !!!: 17704ms consume 524287 折合每秒 2.96 万单,模拟的是 50 万请求同时抢购同一件商品,尚未做拆分。 代码: https://github.com/mingyang91/akka-ticketing |
5
LeegoYih 2022-09-07 10:57:29 +08:00 1
秒杀的主要瓶颈在数据库,尤其是 MySQL 这种,所以目标是减少无效请求,保证请求到数据库层面的都是有效的,所以用缓存是必要的。
1. 客户端按钮设置隐式冷却 OP 描述的“提交信息后跳转到指定页面”,用户点提交按钮,可能会返回上一页,然后继续提交,这样会生成很多无效的订单,我抢消费券就是这么干的,所以需要加一个冷却时间,用户一直操作,实际上每秒可能只有一次请求 2. 网关层限流 按钮冷却只能防普通用户,网关限流用来防止懂技术的脚本哥,根据 IP 或者 UserId 进行限制请求次数。 或者直接对商品进行限流,例如:如果商品只有 10 个,每秒请求有 10000 个,那么实际上大部分请求都是无效的,允许每秒 100 - 1000 请求进来即可,其他的直接返回「已抢光」,这里可以使用「令牌桶」和「滑动窗口」算法。 3. 服务层 OP 说用 UUID ,我感觉还是后台生成比较好,收到请求后,生成一个 OrderId 放到缓存中,然后通过 MQ 异步创建订单,直接把 OrderId 返回给客户端,再提供一个接口让前端轮询缓存中的订单状态。 MQ 消费完成后更新缓存中的订单状态,客户端发现订单已创建,再去查询真正的订单详情。 服务层使用 Redis 维护库存数量的优点也很明显,如果库存没了直接返回即可,不用调数据库,保证数据库扣减库存、生成订单都是有效操作。 4. 数据库层 通过数据库自身的锁保证原子性,防止超卖,此时数据库基本没什么压力。 |
7
lmshl 2022-09-07 11:27:01 +08:00
文章昨天才开始写,示例代码已经放 GitHub( https://github.com/mingyang91/akka-ticketing) 了。
在我笔记本连远程数据库的环境中,单商品每秒接近三万的订单确认速率,完全不需要引入任何的 MQ / Redis / 主从分表等复杂中间件 |
9
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 |
10
Pythoner666666 2022-09-07 12:53:47 +08:00
文章发出来记得 @我 我很想拜读一下
@lmshl |
12
sujin190 2022-09-07 16:07:00 +08:00
@lmshl #4 粗略看下,秒杀的难点本来也不是多快,否则这的人大概率都能写出一个处理 10k 以上 qps 的程序,现实中秒杀麻烦的是除了要处理商品库存订单问题外,还有营销折扣系统、优惠系统、风控系统、配送与地址系统等等,这一系列下来之后会是一个非常长的流程,在各系统负载一致和事务一致处理起来会非常麻烦,从这一点上来说,直接使用带库存数分布式锁直接拦截在所有系统前面才是最容易实现且靠谱的方案,反而是使用队列平滑再反馈结果其实更麻烦
而且还有现实大概率不会出现却又不得不考虑的崩溃恢复问题,队列造成了较长处理链路是清理的复杂性想满足秒杀场景下较短的崩溃恢复时间还是十分难的,使用分布式锁拦截则可以设置较短的等待时间即可,也没有进入下单的业务流程,随着时间超时后就会自然恢复 |
13
buster 2022-09-07 16:31:44 +08:00
网上搜索了一下,大概思路可能是这个 https://scala.cool/2017/08/learning-akka-8/
|
14
lmshl 2022-09-07 16:40:24 +08:00
@sujin190 并不是
1. 我的例子代码中已经保证了库存严格不超售,且订单记录有审计。 2. 其他系统和核心下单系统并不属于一个 Domain ,属于可以垂直拆分的组件,同时,折扣优惠,封控属于可以被前置业务逻辑检查的,也绝对不应该被合并到下单事务中一起处理 3. 崩溃恢复已经被 akka-projection 实现了,当然自己实现也不难,从快照+快照以后发生的日志快速回放一遍很容易。 分布式锁是最差最差的方案了,使用了分布式锁以后,你的库存扣减逻辑很难高过 100tps ,这是理论上限 |
15
lmshl 2022-09-07 16:43:03 +08:00
@buster 是的没错,扫了一眼他的文章,和我思路基本一致,不过他没提供代码示例
我用的 akka-projection 是依赖 akka-cluster-sharding 和 akka- persistence 实现的 |
16
sujin190 2022-09-07 16:50:04 +08:00
@lmshl #14 那你这分布式锁实现有问题吧,否则按你这么说 mysql 也无法完成超过这个限值了,那怎么搞岂不是都没用了
超不超卖的本来也不难处理吧,想要尽可能可靠从秒杀来说,既然库存非常小否则也不叫秒杀了,那么就应该除了让正常库存进入下单流程外,其他请求的处理过程都尽可能短,最好到达网关就直接返回,你再搞快照搞回滚,那么不是增加了可能出错的点了吧,多一步就多一步出错的点,啥都不用做自然也不可能有任何异常了 |
17
lmshl 2022-09-07 17:00:00 +08:00
@sujin190 磁盘和网络 IO 都需要时间,即使是内网,最简单的 KV 访问也需要 >1ms 时间
你用分布式锁,怎么和软件内存中实现的事务比,直接输在起跑线上了好吧 访问 L1/2/3 到内存,时间单位可都是 ns ,内存运算完事务扔给磁盘持久化,磁盘写入多快,秒杀就能有多快。 高下立判 |
18
sujin190 2022-09-07 17:14:07 +08:00
@lmshl #17 网络请求又不是串行的,整个分布式锁本来也就只在加减一才串行,哪里有问题了,就算要多机强一致,网络请求同步过程依然可以并行,多机强一致下百万以上 qps 可能做不到,十几万 qps 也还是可以的,一旦库存抢完就进入完全并行阶段,百万以上 qps 不很轻松么,这个的实现有简单,直接集成在网关里也没毫无问题,这显然已经是改造最小集成最简单的方案了
|
19
sujin190 2022-09-07 17:22:14 +08:00 1
@lmshl #17 https://github.com/snower/slock
顺便说我已经做出来了,真的不是在嘴炮,3 节点多机强一致阶段大概能有超过 10 万 qps ,并行阶段能接近 200 万 qps ,极其简单的协议也可以直接集成在 openresty 里,我还是觉得这种在非淘宝拼多多这种超大站点,这个应该是最容易实现维护的架构了,毕竟只要你不是动辄秒大量商品库存,只要在现有订单系统前添加拦截流程就可以,几乎不需要针对秒杀逻辑对订单系统做单独调整 |
20
lmshl 2022-09-07 17:24:34 +08:00
@sujin190 “整个分布式锁本来也就只在加减一才串行” 这不就是串行了么?如果失败了你还得再分布式里处理回滚,复杂度不就上升了么?
而且我是秒杀在 1 个根上就可以做到一秒接近 3 万笔交易,横向扩展也没有上限,整个系统除了业务 Java 进程,只需要 数据库 + 负载均衡就可以运作了,维护成本更低。显然更小吧 |
21
wangchonglie 2022-09-07 17:42:35 +08:00
@lmshl #4 mark 一下
|
23
lmshl 2022-09-07 18:37:23 +08:00
@micean 这可太简单了
如果仅允许一次下单,直接前过滤,TPS 丝毫不受影响 如果允许多次下单,但总购买数量不能超过 n 个,那无非就是在软件事务内存里多加一个 if/else branch ,TPS 下降可能都低于可测量误差了 |
24
micean 2022-09-08 10:17:32 +08:00
|
25
lmshl 2022-09-08 10:58:35 +08:00
@micean STM ( Software transactional memory )软件事务内存,https://hackage.haskell.org/package/stm
|
26
lmshl 2022-09-08 11:01:09 +08:00
@micean 如果你了解 STM 的话就不会这么说了,软件事务远比分布式事务更容易实现,也更容易做到高并发高吞吐量,几行代码能描述清楚的规则,犯不着上分布式事务,上了分布式事务你不得设计回滚么?
而且我的事务以持久化视为事务提交并返回客户端,已经把系统最重要的部分模拟出来了,且保证正确性。 |
27
micean 2022-09-08 11:54:49 +08:00
@lmshl
也没有说分布式事务(基本也是避免使用分布式事务)。你的性能我并没有异议,stm 没毛病。但是场景更像网络游戏而不是电商。 现实是不可能只用一台服务器去承载所有业务(只用一台服务器的电商业务量相信也用不上你的架构),既然是集群的话,必然就要考虑一致性的问题,秒杀的压力并不在于最终事务,而是在于查询过滤的效率,前面小哥也证明了加锁减库存也不差。 |
28
lmshl 2022-09-08 13:57:04 +08:00
@micean 我列举单机数据的意思是,“我只用单机(甚至单核)就可以做的比别人集群更快”
而不是我只能用单机,实际上我是用的框架是 akka-cluster-sharding ,是一套水平伸缩集群,意味着并发抢购多件商品的时候,性能是随着节点增加而近乎线性增长的。 而且,你说场景更像网络游戏的话,akka-cluster-sharding 的一大优势应用场景就是 MMORPG ,腾讯和暴雪都在用 akka 做百万同服同时在线。 加锁减库存的问题在于,锁掉整个商品的长时间事务,这个商品的交易会被串行化导致吞吐量急剧下降。 如果采用预扣除算法,又会要求所有其他组件都要设计回滚操作,增加了整个架构的成本。 |