正文
首先设计一个系统之前,我们需要先确认我们的业务场景是怎么样子的,我们就设计一个便于理解的秒杀场景吧。
场景
我们现场要卖100件下面这个看着些不错的鱼竿,然后我们根据以往这样秒杀活动的数据经验来看,目测来抢这100件鱼竿的人足足有10万人。
你一听,完了呀,这我们的服务器哪里顶得住啊!说真的如果直接对数据库操作肯定挂。但是别急嘛,我们在开始之前应该先思考下会出现哪些问题?
秒杀可能会产生的问题
问题一:高并发
是的高并发这个是我们想都不用想的一个点,一瞬间这么多人进来这不是高并发什么时候是呢?
秒杀的特点就是这样时间极短、 瞬间用户量大。
正常的店铺营销都是用极低的价格配合上短信、APP的精准推送,吸引特别多的用户来参与这场秒杀,爽了商家苦了开发呀。
秒杀大家都知道如果真的营销到位,价格诱人,几十万的流量我觉得完全不是问题,那单机的Redis我感觉3-4W的QPS还是能顶得住的,但是再高了就没办法了。大量的请求进来,我们需要考虑的点就很多了,缓存雪崩,缓存击穿,缓存穿透 这些点都是有可能发生的,出现这种问题那就很难受了,活动失败用户体验差,活动人气没了,最后背锅的还是开发。
拓展小课堂
缓存穿透:
当 Redis 和数据库中都没有我们想要的数据时,就需要考虑缓存穿透的问题了。下面这段逻辑大家用的会比较多:先去 Redis 中查找某资源,Redis 中查不到就去 DB 中查,DB 中查到后回写一份数据到 Redis 中。
这段逻辑正常情况下问题并不大,但是如果用户恶意重复请求资源 X,该资源在 Redis 和 DB 中都不存在。那么每次请求都会直接打到 DB 上,甚至导致物理 DB 宕机。
解决方案:适用布隆过滤器,接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;从缓存取不到的数据,在数据库中也没有取到,这时也可以将redis中的key-value对写为key-null
缓存击穿:
关键词:定点打击
怎么理解呢?举个极端的例子:比如某某明星爆出一个惊天狠料,海量吃瓜群众同时访问微博去查看该八卦新闻,而微博 Redis 集群中数据在此刻正好过期了,那么无数的请求则直接打到了微博系统的物理 DB 上,DB 瞬间挂了。
解决方案:
1、热点数据永远不过期
比如我们可以将某个 key 的缓存时间设置为 25 小时,然后后台有个 JOB 每隔 24 小时就去批量刷新一下热点数据。就可以解决这个问题了。
2、使用互斥锁
如果你的解决方案仅仅是一把 Java 锁,那么绝对达不到生产环境的要求。
所以实际上,缓存穿透,加锁解决,必须还要涉及到分布式锁的概念。这里不谈 zookeeper 之类的东西,既然谈 redis,那么就用 redis 来解决这个问题。首先,当一个 key 失效,不管是时间过期,还是被 LRU、LFU 剔除,假设会有 1w 个并发来访问这个 key,那么它们就会先查询 redis,然后都发现,这个 key 不存在;然后,它们就会对应的,往 redis 用 setnx 设置一个 key,来表示这是一把锁;然后,只有一个线程,会设置成功,然后去读取数据库,写回 redis;其他的 9999 个线程,则 sleep 一小会,然后再去访问我们的 redis。有人看到这,首先会问,这个 sleep 要多久?
这个是要根据压测,以及线上环境进行调整的,一般会给出一个合适的值,也就是大约从数据库取出数据的时间。所以,正常情况是不会出现大面积长时间等待的情况的。关于redis实现分布式锁传送门
缓存雪崩:
描述:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。
问题二:超卖
但凡是个秒杀,都怕超卖,我这里举例的只是鱼竿,要是换成100个华为MatePro30,商家的预算经费卖100个可以赚点还可以造势,结果你写错程序多卖出去200个,你不发货用户投诉你,平台封你店,你发货就血亏,你怎么办?
那跑路就应该是我们唯一的选择了(开个玩笑~),秒杀的价格本来就低了,基本上都是不怎么赚钱的,超卖了就恐怖了呀,所以超卖也是很关键的一个点。
问题三:恶意请求
你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛…)肯定也知道的。
那简单啊,我知道你什么时候抢,我搞个几台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。
真实情况可能远远不止,因为机器请求的速度比人的手速往往快太多了
Tip:科普下,小道消息了解到的,黄牛的抢票系统,比国内很多小公司的系统还吊很多,架构设计都是顶级的,我用顶配的服务加上顶配的架构设计,
问题四:数据库QPS
每秒上万甚至十几万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,而且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的一起挂,小公司的话可能全站崩溃404。
反正不管你秒杀怎么挂,你别把别的搞挂了对吧。
二、问题解决方案
服务单一职责
设计个能抗住高并发的系统,我觉得还是得单一职责。
什么意思呢,就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。单独给他建立一个数据库,现在的互联网架构部署都是分库的,一样的就是订单服务对应订单库,秒杀我们也给他建立自己的秒杀库。
单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(强行高可用)
部署Redis集群
之前不是说单机的Redis顶不住嘛,那简单多找几个兄弟啊,秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群,主从同步、读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!
使用Nginx负载均衡
Nginx大家想必都不陌生了吧,这玩意是高性能的web服务器,并发也随便顶几万不是梦,但是我们的Tomcat只能顶几百的并发呀,那简单呀负载均衡嘛,一台服务几百,那就多搞点,在秒杀的时候多租点流量机。
Tip:据我所知国内某大厂就是在去年春节活动期间租光了亚洲所有的服务器,小公司也很喜欢在双十一期间买流量机来顶住压力。
按钮控制
大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。这是因为怕大家在时间快到的最后几秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可用状态。
按钮可以点击之后也得给他置灰几秒,不然他一样在开始之后一直点的。你敢说你们秒杀的时候不是这样的?
库存预热
秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。
那怎么办?
我们都知道数据库顶不住但是他的兄弟非关系型的数据库Redis能顶啊!
那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。
但是用了Redis就有一个问题了,我们上面说了我们采用主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了。
这里我就不画图了,我本来想画图的,想了半天我觉得语言可能更好表达一点。
多品几遍!!!就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。咋办?
Lua:
Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。这点是关键。
知道原理了,我们就写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求,然后再做后面的事情嘛。
限流&降级&熔断&隔离:
这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了,限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。
总结:
最后最最最重要的
注意:看完这个可以忘记前面所讲的秒杀的设计,上面可作为面试时和面试管唠嗑使用,惊不惊喜 意不意外
对于这个技术可以这样开篇:
当领导把这个业务交给我的时候,给我的第一印象,卧槽,秒杀这样的业务我能行么?内心OS了一丢丢儿的时间,然后直接雄赳赳气昂昂的接下了这个任务,剩下的两个周,就用来研究上面的那种设计模式和相关技术了;然而,尽管我这么努力,领导感觉时间差不多了,就问我开发过程中有没有遇到什么问题,然后我就把上面的哪些内容叭叭的说了一遍,领导先是表示震惊,然后不由得笑了一下,开始给我说了下面这种秒杀的黑幕,接下来,Are you Okey?
- 对于高并发秒杀、抽奖之类的业务,任何提到数据库、xx锁、redis等关键词的回答都是在扯淡!
- 对于高并发秒杀、抽奖之类的业务,任何提到数据库、xx锁、redis等关键词的回答都是在扯淡!
- 对于高并发秒杀、抽奖之类的业务,任何提到数据库、xx锁、redis等关键词的回答都是在扯淡!
设计理念
不信?我们来试试:
1亿用户,在1秒钟内秒杀10个商品。
够“大数据”、“高并发”了吧?
把这1亿个用户id在塞进redis中,数据类型使用list此时就变得非常相得益彰,取前100个id(排队模式,先到先得)、或者随机取100个id
(抽签模式,绝对公平),剩下的直接丢弃。最终在1亿人中抢到商品的那个锦鲤用户,就在这100个id中产生。1秒后,把商品在内存中标记为“售罄”,如果后面还有进来秒杀的散户,直接无视。
1、Redis列表是简单的字符串列表
按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。所以完全够用
2、使用list的这三个方法就可以很好的实现
- LINDEX key index:通过索引获取列表中的元素
- LRANGE key start stop:获取列表指定范围内的元素
- LTRIM key start stop:对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
现在,1亿并发的问题变成了100并发的问题——没人会问这个100人的秒杀功能怎么实现了吧?
● 用这个方案,用户点下秒杀按钮后,会感觉到1秒的延迟可以接受——Good;
● 用户在1秒后就会知道自己到底有没有秒杀成功——Good;
● 服务器端没有什么大数据计算,只需在内存中存下1亿个用户id即可。每个用户id算10字节,1亿个id只占用1G内存——Good;
● 数据库根本不存在1亿次操作,连100次都不—定有——Good;
1亿用户id集中到一台服务器抽签,每人抽中的概率完全均等。如果实际来的用户数量少于预期,比如只来了100万人、甚至100人,也不会出问题,最终总能产生十个幸运用户喜提商品——很好,比那些在前端写死秒杀成功率=1亿分之一的耍猴方案厚道多了。
超卖/少卖的问题
“防止少卖是用延迟队列或定时任务,15分钟没付款时,取消并重新回库”,这种方法适用于一般情况下的购物,不适合秒杀。
想一想,如果只放1个请求进去,一旦这位用户占着茅坑不拉X,在15分钟内,剩下的1亿-1人会看到什么?
- A.显示“秒杀已结束”?万一他最终没有付款,这1个商品岂不是卖不掉了?
- B.显示“茅坑正被人占用,如果他在15分钟内都没拉出X,系统将从剩余的1亿-1人中抽取一位幸运用户接着上”?你确定不会被围观的1亿-1个用户打死?
放100个请求进去付款,先付先得,不仅符合技术,而且符合人性。想想看,如果付款时系统友情提示一下“除你之外,还有99人正在付款,先付先得哦”,是不是剁起手来顿时就不纠结了?
点付款按钮后,再点取消怎么办?或者余额不足,或者刚好用户断网!整个付款过程是你无法控制的!建议你实战演练—下
好问题,值得细说一番。
你所说的“付款过程无法控制”的前提是使用第三方支付,比如普通商户接支付宝接口。一旦从电商平台跳转到
支付宝的网站/APP,电商就无法干预了。如果用户A先进入结算页,然后在支付宝里纠结半天,结果后来者B果断把钱付了,等到A付款后就会造成超卖,电商明知没货了也无法实时中止支付宝内的交易。
但是,这里的预设前提可是1亿人同时秒杀的巨头电商啊,如此体量的平台一般会主推自有支付体系(余额/花呗/白条),再次也是自己能控制的合作渠道,所以能在扣款前检查库存并中止交易。他们绝不可能像普通商户一样去接自己无法控制的第三方支付,否则就等于把客户信息拱手相送——这就是为什么京东不让你用支付宝的原因。
如果非要用无法控制的第三方支付,那么在理论上,少卖、超卖两者必居其一,哪怕只有2个人抢1个商品也是如此:
● 如果在A拍下商品(暂未付款)后锁库存,令B无法购买,结果最后A没付款=>少卖;
● 如果拍下后不锁库存,结果两人都付了款=>超卖。
当然,在真实的商业中,少卖和超卖再正常不过,有的是办法用非技术手段应对。要知道,秒杀100个手机,并不是只卖掉99个就不行,也不是仓库里真就拿不出第101个。不过,从纯技术上讲,只要用了“无法控制”的第三方支付,少卖和超卖的可能性就无法消除了。
● 多卖:实在没货了,可以进行沟通退款;实际上可以通过结合实际的库存来控制发送的请求数,例如库存有100件,用来秒杀的货件是10件,那么我们就从redis中获取前50个用户,这样即使出现超卖的情况,也可以在可控制的范围内
● 少卖:如果出现少卖的情况,那就等下次进行秒杀的活动,简单干脆不,反正搞活动打广告的目的已经达到了,用户也无法知道是不是一定就是秒杀了这10件物品,数量对不对的上,也无关重要了。
设置权重
经常参与秒杀的用户,如果每次都铩羽而归,这样可能给用户的体验感不好,对公司的推广力度也有所打折,这里就可以像统筹一样设计个小黑幕,把经常参见的用户,如果此次秒杀没成功或者统筹没成功,那么权重值就加1,以后再次参与秒杀或者统筹的时候就会以统筹较高的 秒杀或者统筹的几率就越大。
设计:
在秒杀或者统筹开始前,用户需要先点击下相关按钮或者连接(‘表示我要参与秒杀(统筹)’)点击过的会将用户的id和权重值放入到reids中,秒杀状态默认设置为0,秒杀开始的时候,用户点击秒杀按钮,就去将参与秒杀的状态值改为1,最后在秒杀状态为1的,权重值比较高的用户中选取幸运用户名单,没有秒杀成功的,就会将权重值加1,秒杀成功的将权重值重新设置为0;
这样设计的好处:
增加一步用户先提前进行预约比较符合实际的情况,也有利于秒杀时不需要再去从数据库中查询权重的值,减轻了秒杀业务的负担。