写点什么

秒杀活动!!!! 如何撑住 10W QPS

作者:CTO技术共享
  • 2022-10-21
    广东
  • 本文字数:3546 字

    阅读完需:约 1 分钟

秒杀活动!!!! 如何撑住10W QPS

CTO 技术共享经常有遇到朋友提问:如果存在一个大客户,这一个客户并发量就非常高,版本号比对会导致大量的更新失败。于是推出,这个方案不适用于高并发场景。

究竟是不是这样呢?大家对高并发是不是有什么误解呢?

我经常说,任何脱离业务场景的架构设计都是耍流氓,今天来聊一聊三个高并发业务场景的架构设计差异。

一、QQ

QQ 的一些核心业务有:

  • 个人:user(uid, user_info, …)

  • 好友:user_friends(uid, friend_id, …)

  • 加入的群:user_groups(uid, group_id, …)

  • 群:group(gid, group_info, …)

  • 群成员:group_members(gid, uid, …)

  • 个人消息:msgs_user(msg_id, uid, …)

  • 群消息:msgs_group(msg_id, gid, …)

这些信息的读写有一个特点,都会带上 uid/gid/msgid 属性。


例如,拉取好友列表

select friend_id from user_friends 

where uid=$uid;

在用户量很大,并发量很大时,不同用户/群/消息数据的读写并没有锁冲突。

CTO 技术共享提醒:10W 个用户同时读写,彼此没有锁冲突。

只有当,同一个用户,很短的时间内,有大量并发时,才可能存在锁冲突。

CTO 技术共享提醒:例如,1 个用户,1 秒钟读写 1W 次。

这类场景下,使用《并发扣款,如何保证一致性?》中的 CAS 乐观来解决同一个用户的并发冲突一致性,是绝对没有问题的。

二、微博

微博的核心业务是 feed 流:

  • 发消息,写操作

  • 刷消息,读操作

微博业务显然是读多写少的,在用户刷消息时,自己 feed 流里的消息,是由别人发出的。


查看自己主页 feed 流,最朴素的实现方法是:

(1) 拉取自己关注的用户 id_list;

(2) 拉取这些用户最近 N 条消息;

(3) 将这 N*id_list 条消息排序;

(4) 返回第一页消息,得到自己主页 feed 流;

在用户量很大,并发量很大时,会有一定数据的读写锁冲突。

CTO 技术共享提醒:不像 QQ,基本是读写自己的数据,微博要写自己的数据,读别人的数据。

这类场景下,《读扩散,写扩散,终于讲清楚了!》中提到的读扩散,写扩散,也是常见的解决方案。


三、12306

12306 的核心业务是:

  • 查票,读操作

  • 买票,写操作

stock(id, num) // 某一列车有多少张余票


在用户量很大,并发量很大时,有极大的锁冲突。

CTO 技术共享提醒:这个业务,数据量并不大。

这类“秒杀”业务,如果不做特殊的优化,数据库很容易死锁卡死,没有任何人能买票成功。

这类“秒杀”业务,有什么常见的优化手段呢?

一般来说,系统上业务上分别需要配合优化。


系统层面,秒杀业务的优化方向如何?

主要有两项:

(1)将请求尽量拦截在系统上游,而不要让锁冲突落到数据库。

传统秒杀系统之所以挂,是因为请求都压到了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,访问流量大,下单成功的有效流量小。

一趟火车 2000 张票,200w 个人同时来买,没有人能买成功,请求有效率为 0。

CTO 技术共享提醒:此时系统的效率,还不如线下售票窗口。

(2)充分利用缓存。

秒杀买票,这是一个典型的读多写少的业务场景:

  • 车次查询,读,量大

  • 余票查询,读,量大

  • 下单和支付,写,量小

一趟火车 2000 张票,200w 个人同时来买,最多 2000 个人下单成功,其他人都是查询库存,写比例只有 0.1%,读比例占 99.9%,非常适合使用缓存来优化。


秒杀业务,常见的系统分层架构如何?

秒杀业务,可以使用典型的服务化分层架构:

  • (浏览器/APP),最上层,面向用户

  • 站点层,访问后端数据,拼装 html/json 返回

  • 服务层,屏蔽底层数据细节,提供数据访问

  • 数据层,DB 存储库存,当然也有缓存

这四层分别应该如何优化呢?


一、端上的请求拦截(浏览器/APP)

想必春节大家都玩过微信的摇一摇抢红包,用户每摇一次,真的就会往后端发送一次请求么?

回顾抢票的场景,用户点击“查询”按钮之后,系统卡顿,用户着急,会不自觉的再去频繁点击“查询”,不但没用,反而平白无故增加系统负载,平均一个用户点 5 次,80%的请求是这么多出来的。

JS 层面,可以限制用户在 x 秒之内只能提交一次请求,从而降低系统负载。

画外音:频繁提交,可以友好提示“频率过快”。

APP 层面,可以做类似的事情,虽然用户疯狂的在摇微信抢红包,但其实 x 秒才向后端发起一次请求。

画外音:这就是所谓的“将请求尽量拦截在系统上游”,浏览器/APP 层就能拦截 80%+的请求。

不过,端上的拦截只能挡住普通用户(99%的用户是普通用户),程序员 firebug 一抓包,写个 for 循环直接调用后端 http 接口,js 拦截根本不起作用,这下怎么办?

二、站点层的请求拦截

如何抗住程序员写 for 循环调用 http 接口,首先要确定用户的唯一标识,对于频繁访问的用户予以拦截。

用什么来做用户的唯一标识?

ip?cookie-id?别想得太复杂,购票类业务都需要登录,用 uid 就能标识用户

在站点层,对同一个 uid 的请求进行计数和限速,例如:一个 uid,5 秒只准透过 1 个请求,这样又能拦住 99%的 for 循环请求。


一个 uid,5s 只透过一个请求,其余的请求怎么办?

缓存,页面缓存,5 秒内到达站点层的其他请求,均返回上次返回的页面。

画外音:车次查询和余票查询都能够这么做,既能保证用户体验(至少没有返回 404 页面),又能保证系统的健壮性(利用页面缓存,把请求拦截在站点层了)。

OK,通过计数、限速、页面缓存拦住了 99%的普通程序员,但仍有些高端程序员,例如黑客,控制了 10w 个肉鸡,手里有 10w 个 uid,同时发请求,这下怎么办?

三、服务层的请求拦截

并发的请求已经到了服务层,如何进拦截?

服务层非常清楚业务的库存,非常清楚数据库的抗压能力,可以根据这两者进行削峰限速。

例如,业务服务很清楚的知道,一列火车只有 2000 张车票,此时透传 10w 个请求去数据库,是没有意义的。

画外音:假如数据库每秒只能抗 500 个写请求,就只透传 500 个。

用什么削峰?

请求队列。

对于写请求,做请求队列,每次只透传有限的写请求去数据层(下订单,支付这样的写业务)。

只有 2000 张火车票,即使 10w 个请求过来,也只透传 2000 个去访问数据库:

  • 如果前一批请求均成功,再放下一批

  • 如果前一批请求库存已经不足,则后续请求全部返回“已售罄”

对于读请求,怎么优化?

cache 抗,不管是 memcached 还是 redis,单机抗个每秒 10w 应该都是没什么问题的。

CTO 技术共享提醒:缓存做水平扩展,很容易线性扩容。

如此削峰限流,只有非常少的写请求,和非常少的读缓存 mis 的请求会透到数据层去,又有 99%的请求被拦住了。

四、数据库层

经过前三层的优化:

  • 浏览器拦截了 80%请求

  • 站点层拦截了 99%请求,并做了页面缓存

  • 服务层根据业务库存,以及数据库抗压能力,做了写请求队列与数据缓存

你会发现,每次透到数据库层的请求都是可控的。


db 基本就没什么压力了,闲庭信步。

CTO 技术共享提醒:这类业务数据量不大,无需分库,数据库做一个高可用就行。

此时,透 2000 个到数据库,全部成功,请求有效率 100%。

CTO 技术共享提醒:优化前,10w 个请求 0 个成功,有效性 0%。

按照上面的优化方案,其实压力最大的反而是站点层,假设真实有效的请求数是每秒 100w,这部分的压力怎么处理?

解决方向有两个:

(1)站点层水平扩展,通过加机器扩容,一台抗 5000,200 台搞定;

(2)服务降级,抛弃请求,例如抛弃 50%;

原则是要保护系统,不能让所有用户都失败。

站点层限速,是每个 uid 的请求计数放到 redis 里么?吞吐量很大情况下,高并发访问 redis,网络带宽会不会成为瓶颈?

同一个 uid 计数与限速,如果担心访问 redis 带宽成为瓶颈,可以这么优化:

(1)计数直接放在内存,这样就省去了网络请求;

(2)在 nginx 层做 7 层均衡,让一个 uid 的请求落到同一个机器上;

CTO 技术共享提醒:这个计数对数据一致性、准确性要求不高,即使服务重启计数丢了,大不了重新开始计。

除了系统上的优化,产品与业务还能够做一些折衷,降低架构难度。


业务折衷一

一般来说,下单和支付放在同一个流程里,能够提高转化率。对于秒杀场景,产品上,下单流程和支付流程异步,放在两个环节里,能够降低数据库写压力。以 12306 为例,下单成功后,系统占住库存,45 分钟之内支付即可。


业务折衷二

一般来说,所有用户规则相同,体验会更好。对于秒杀场景,产品上,不同地域分时售票,虽然不是所有用户规则相同,但能够极大降低系统压力。北京 9:00 开始售票,上海 9:30 开始售票,广州 XX 开始售票,能够分担系统压力。


业务折衷三

秒杀场景,由于短时间内并发较大,系统返回较慢,用户心情十分焦急,可能会频繁点击按钮,对系统造成压力。产品上可以优化为,一旦点击,不管系统是否返回,按钮立刻置灰,不给用户机会频繁点击。


业务折衷四

一般来说,显示具体的库存数量,能够加强用户体验。对于秒杀场景,产品上,只显示有/无车票,而不是显示具体票数目,能够降低缓存淘汰率。

CTO 技术共享提醒:显示库存会淘汰 N 次,显示有无只会淘汰 1 次。更多的,用户关注是否有票,而不是票有几张。


无论如何,产品技术运营一起,目标是一致的,把事情做好,不存在谁是甲方,谁是乙方的关系。

总结


对于秒杀类业务,除了业务折衷,架构设计上主要有两大优化方向:

(1)尽量将请求拦截在系统上游;

(2)读多写少用缓存;

发布于: 刚刚阅读数: 3
用户头像

学如逆水行舟,不进则退 2022-08-05 加入

大型企业CTO,专注大数据、架构框架、集群、中间件、分布式、数据库、监控、开源、基础架构等技术分享,助力数字化转型。

评论

发布
暂无评论
秒杀活动!!!! 如何撑住10W QPS_10 月月更_CTO技术共享_InfoQ写作社区