综上所述,可以总结出秒杀系统场景的几个特点:
秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
01
秒杀架构设计
限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。
02
架构原则
争取做到4要1不要
2.请求数要尽量少:用户请求的页面返回后,浏览器渲染页面还包含其他的额外请求,比如说这个页面依赖的CSS/Javascript,图片以及Ajax请求等,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,另外不同请求访问的域名不一样,还需要做DNS域名解析,可能会耗时更久。
3.路径要尽量短:所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。
4.依赖要尽量少:所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。
5.不要有单点:系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。
03
流量削峰
排队:要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。在这里,消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。
答题:防止用户作弊,延缓用户请求。
分层过滤:分层校验的目的是:在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。
04
异步处理
我们大家想一想,即使我们做了上面的限流机制,卡住了绝大部分请求到业务系统中,但还是有很多请求进入了业务系统。如果我们商品库存数为5000个库存,在大流量的时候,会有很多用户都有资格去下单,那也就是同时会有5000个并发去操作mysql数据库。那数据库也是抗不住的,一般mysql的并发量500左右。
这个时候我们需要做一些异步处理,让5000个下单请求进入消息队列,让订单消费服务去慢慢处理5000个请求,这样就有效的把并发同步请求,改为了串行异步。
当然改成了异步处理,前端在下单的时候,就没法立刻得到下单的结果,所以前端要做一个【抢购中。。。】的状态页面,在此页面中定时轮询下单结果。这样就大大提升了系统的吞吐量,降低了系统压力。
消息队列中间件有很多选择,一般选择RabbitMq,RocketMq。
05
多级缓存
使用场景:所有商品库存全部依赖数据,在高并发情况下,数据库死翘翘了...
秒杀商品一个用户只能抢购一次,一次只能抢购一个!
一级缓存:在程序启动完成后,将商品信息存入redis中
二级缓存:ConcurrentHashMap内记录商品是否售完标识;
大概思路:程序启动加载init代码块,将商品库存信息放入redis中,下单时直接使用redisdecr;如果stock小于0,记录jvm缓存中商品售完标识;启动zk监听,同步多个jvm内存标识;
坑坑坑点:redis原子减会减成负数,如果有下单失败退单等操作,负数+1还是负数,然鹅是有库存的!所以redis库存一定要还原或者改成0
商品信息加入redis中:
@PostConstruct
publicvoidinit()throwsException{
ProductproductParam=newProduct();
productParam.setSpecial(ProductType.PRODUCT_MIAOSHA);
ListmiaoshaProducts=productService.selectList(productParam);
for(Productproduct:miaoshaProducts){
RedisUtil.set(RedisKeyPrefix.PRODUCT_STOCK+"_"+product.getId(),String.valueOf(product.getStock()));
}
减少库存zk监听:
Longstock=RedisUtil.decr(RedisKeyPrefix.PRODUCT_STOCK+"_"+productId);
if(stock==null){
returnReturnMessage.error("商品数据还未准备好");
if(stock<0){
RedisUtil.incr(RedisKeyPrefix.PRODUCT_STOCK+"_"+productId);
productSoldOutMap.put(productId,true);
//写zk的商品售完标记true
//判断父节点是否存在
if(zooKeeper.exists(ZookeeperPathPrefix.PRODUCT_SOLD_OUT,false)==null){
//创建父节点
zooKeeper.create(ZookeeperPathPrefix.PRODUCT_SOLD_OUT,"".getBytes(),Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
//判断节点内商品信息是否存在
if(zooKeeper.exists(ZookeeperPathPrefix.getZKSoldOutProductPath(productId),true)==null){
//创建商品子节点
zooKeeper.create(ZookeeperPathPrefix.getZKSoldOutProductPath(productId),"true".getBytes(),Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
if("false".equals(newString(zooKeeper.getData(ZookeeperPathPrefix.getZKSoldOutProductPath(productId),true,newStat())))){
zooKeeper.setData(ZookeeperPathPrefix.getZKSoldOutProductPath(productId),"true".getBytes(),-1);
//监听zk售完标记节点
zooKeeper.exists(ZookeeperPathPrefix.getZKSoldOutProductPath(productId),true);
returnReturnMessage.error("商品已抢完");
"zooKeeperWatcher">
"zookeeperAddr"value="${zk.ip}"/>
"zookeeper"lazy-init="true">
"connectString"value="${zk.ip}"/>
"sessionTimeout"value="5000"/>
"watcher"ref="zooKeeperWatcher"/>
/**
*zk更新缓存watcher
*/
publicclassZooKeeperWatcherimplementsWatcher{
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(ZooKeeperWatcher.class);
privateZooKeeperzooKeeper;
publicZooKeeperWatcher(StringzookeeperAddr)throwsIOException{
super();
this.zooKeeper=newZooKeeper(zookeeperAddr,500,null);
@Override
publicvoidprocess(WatchedEventevent){
if(event.getType()==EventType.NodeDataChanged){//zk目录节点数据变化通知事件
try{
Stringpath=event.getPath();///product_sold_out/10270
StringsoldOutFlag=newString(zooKeeper.getData(path,true,newStat()));
logger.info("zookeeper数据节点修改变动,path={},value={}",path,soldOutFlag);
if("false".equals(soldOutFlag)){
StringproductId=path.substring(path.lastIndexOf("/")+1,path.length());
MiaoshaAction.getProductSoldOutMap().remove(productId);
}catch(Exceptione){
logger.error("zookeeper数据节点修改回调事件异常",e);
06
其他问题
服务单一职责:设计个能抗住高并发的系统,我觉得还是得单一职责。
什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式
单独给他建立一个数据库,现在的互联网架构部署都是分库的,一样的就是订单服务对应订单库,秒杀我们也给他建立自己的秒杀库。
至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。(不了解的小伙伴也没事,MySQL章节我会说的)
单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(强行高可用)
Redis集群:秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群,主从同步、读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!
资源静态化:秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所以页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。
秒杀系统的特点就是千万不要卖超,限量卖!如果买超了老板那里可能就不好交代了,这是前提。
例如,我们平常购物都是这样,看到喜欢的商品然后下单,但并不是每个下单请求你都最后付款了。你说系统是用户下单了就算这个商品卖出去了,还是等到用户真正付款了才算卖出了呢?这的确是个问题!