查看原文
其他

秒杀系统设计分析

点击关注 👉 顶级架构师 2022-07-01
推荐关注
顶级架构师后台回复 1024 有特别礼包


作者:Muser
来源:xiangxianzui.github.io/2020/02/秒杀系统设计分析

上一篇:美团外卖分布式系统架构设计


大家好,我是顶级架构师。


业务场景分析

业务场景:商品秒杀、商品抢购、群红包、抢优惠券、抽奖、买火车票

业务特点:瞬时售空、瞬时并发高、持续时间短、一般是商品定时上架

技术特点:读多写少、高并发、资源冲突

技术特点分析

读多写少:秒杀系统请求数量巨大,但是真正抢到资源的请求很少,所以是读多写少。使用缓存减缓对后端的压力,比如利用CDN将图片等页面静态资源缓存掉。

高并发:秒杀系统最关键的就是要利用限流,把大多数请求挡在外面;负载均衡将请求分摊给各个后端;利用消息队列中间件做异步削峰;缓存缓解后端压力。

资源冲突:保证关键资源(如商品库存)的原子操作。数据库锁(乐观锁、悲观锁),分布式锁(redis、zk),利用redis decr的原子操作。

请求链路分析

一次秒杀请求,首先是由客户端发起的,经过网络链路到达负载层,再由负载层分发到后端服务层,服务层处理业务逻辑之后,最终结果反映在数据库上。那么我们分析一下在整个请求链路上,针对秒杀系统,我们都有哪些解决方案。

客户端层:客户端呈现的秒杀页面中包含的图片、音乐、商品详情等静态资源,可以在客户端缓存起来,避免每次都要请求服务端而增加后端压力。客户端需要控制好确认秒杀的按钮,如果秒杀过了要即时置灰,另外可以使用图形验证码防止恶意的接口秒杀。

网络层:对超大并发的秒杀,有必要购买CDN产品做静态资源的加速和缓存。

负载层:Nginx做负载均衡,也可以做动静分离,把对静态资源的请求重定向到指定服务;也可以在Nginx做反向代理缓存和限流(ngx_http_limit_req模块)。

服务层:服务端层可以使动态页面静态化,利用本地或分布式缓存,利用消息队列异步削峰,并做好对关键资源的原子操作。

数据库:数据库层可利用乐观锁或悲观锁实现原子操作。

最后需要注意的是,在做秒杀系统之前,要对并发量做预估,估计的标准主要是宣传和广告的力度、往年经验等。然而,有时候我们并不能准确预估并发量,为保险起见,秒杀系统要与主营业务分开部署,防止出现秒杀并发过大而影响主业务的情况。

乐观锁 vs. 悲观锁

假设有商品表goods,schema如下

1
2
3
4
5
6
7
CREATE TABLE `goods` (
`id` bigint(20) NOT NULL DEFAULT 0 AUTO_INCREMENT COMMENT '商品id',
`name` varchar(32) NOT NULL DEFAULT '' COMMENT '商品名称',
`count` int(11) NOT NULL DEFAULT 0 COMMENT '剩余库存数量',
`version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

goods表中id是1的数据为:

1
2
3
4
5
+-------+-----------+-----------+
| id | name | count | version |
+-------+-----------+-----------+
| 1 | toy | 100 | 0 |
+-------+-----------+-----------+

下面我们分析一下两个并发请求同时要买id是1的商品,使用以下3种锁时发生的情况。另外,搜索公众号顶级算法后台回复“算法心得”,获取一份惊喜礼包。

使用乐观锁

2个请求都需要做以下2步db操作。

(1) select version from goods where id = 1

(2) update goods set count = count-1, version = version+1 where id = 1 and version = {version}

首先获取版本号,然后把库存减一、版本号加一,当然,更新成功的条件是当前版本号必须要等于之前获取到的版本号,否则更新失败。

请求1和请求2同时到来,此时二者获取到的版本号都是0,只有一个请求能成功执行步骤(2),假设是请求1,所以结果就是请求1生成订单、秒杀成功了,请求2秒杀失败。

使用带重试的乐观锁

带重试的乐观锁是指更新失败就立即重试,直到更新成功为止。

请求2失败后,如果立即进行重试,那么下次可能就会秒杀成功

使用悲观锁

如果使用悲观锁,首先会锁住商品,select * from goods where id = 1 for update,然后更新库存、生成订单。

乐观锁和悲观锁的选择

通常考虑以下几点

  • 响应速度

  • 冲突频率

  • 重试代价

如果要求响应速度快,尽量使用乐观锁,因为不用锁住这一行

如果高并发下秒杀数量很少的商品,比如2000并发抢10个商品,如果使用乐观锁冲突频率会很高,会有大量请求更新失败

如果重试代价很高,尽量不要使用带重试的乐观锁

一个小例子

假设有一场秒杀活动,并发预估为200,商品是儿童玩具,库存数量为100个。最终生成订单数量、卖出商品数量与并发控制方式的对应关系如下表所示。
并发控制方式生成订单数量卖出商品数量
不使用锁
200
<100
使用乐观锁
n (n<=100)
n (n<=100)
使用带重试的乐观锁
100
100
使用悲观锁
100
100

正常情况下,生成订单的数量与卖出的商品数量要相同,否则就说明出现了数据不一致的情况,可以看到,使用乐观锁、重试乐观锁和悲观锁都能保证这一点。重试乐观锁和悲观锁都能保证库存中的所有商品都能被卖掉;而不带重试的乐观锁,由于失败后没有重试,因此很有可能出现商品没卖完的情况,通常来说机器的性能越高,并发处理能力就越强,冲突失败的请求就越多,卖出的商品就会越少。

最后给读者整理了一份BAT大厂面试真题,需要的可扫码回复“面试题”即可获取。


公众号后台回复 架构 或者 架构整洁 有惊喜礼包!顶级架构师交流群

 「顶级架构师」建立了读者架构师交流群,大家可以添加小编微信进行加群。欢迎有想法、乐于分享的朋友们一起交流学习。

扫描添加好友邀你进架构师群,加我时注明姓名+公司+职位】


版权申明:内容来源网络,版权归原作者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

猜你还想看

推荐一套开源通用后台管理系统(附源码)

看看人家那 IM 即时通讯系统,那叫一个优雅(附源码)

面试官:生成订单30分钟未支付,则自动取消,该怎么实现?

阿里技术专家:一文教你高效画出技术架构图

16个 Redis 常见使用场景,面试有内容聊啦

面试官问:MySQL的自增 ID 用完了,怎么办?

知名国产论坛,凉了!!!!

牛逼啊!接私活必备的 N 个开源项目!赶快收藏吧(附源码合集第一期)!

面试官:为什么要尽量避免使用 IN 和 NOT IN 呢?

多账号统一登陆,账号模块的系统设计

MySQL!性能被 MariaDB 吊打 ! ! !

一个很酷的 API 快速生成系统


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存