Java实现在线秒杀系统(主要问题以及源码)
前言最近在整理电脑文件的时候,发现了毕业之前看视频学习的在线秒杀系统,现在毕业半年了,正好公司使用了dubbo+zookeeper的分布式框架,合计着整理下之前看过的在线秒杀项目,然后希望能够尽可能地整理成分布式框架(不过希望以后有时间完成吧。。。)。本文呢,主要是看一下这个秒杀系统主要会涉及哪些问题。其中用到了SpringBoot、Redis、RabbitMQ、MySQL。文章末尾会给出源代码。
前言
最近在整理电脑文件的时候,发现了毕业之前看视频学习的在线秒杀系统,现在毕业半年了,正好公司使用了dubbo+zookeeper的分布式框架,合计着整理下之前看过的在线秒杀项目,然后希望能够尽可能地整理成分布式框架(不过希望以后有时间完成吧。。。)。
本文呢,主要是看一下这个秒杀系统主要会涉及哪些问题。其中用到了SpringBoot、Redis、RabbitMQ、MySQL。文章末尾会给出源代码。
正文
其实吧,对于秒杀,在我们的生活中也是很常见的,一有个节日,商家就会搞个秒杀活动促销,我就想起之前的红米k30s至尊纪念版也是抢了好久没抢到。。。
总体来说,我们在这个秒杀过程中买东西的过程与平常购物流程差不多:买东西->抢到了下订单->付款完成订单。但是在这个过程中,因为秒杀活动流量大,请求多,如果活动优惠力度大,几万,几十万都是很有可能的,但是数据库又不可能扛下来这么高的并发。可能活动刚开始数据库就挂了,刚开始就结束了,可能会被消费者直接骂死。
所以为了不get一份鱿鱼套餐,我们的目的就是需要减少服务端对数据库的请求,使得系统稳定运行。我们就来看下设计秒杀系统时会遇到的常见问题。
超卖
如果说服务器崩了,那影响的是购物者的体验。但是如果商品卖超了,那影响的就是money了,尤其是像手机啦,电脑啦之类的电子产品,多卖个几十件,几百件,问题就很大了。所以首先我们就要关注的就是超卖问题。
其实可以先考虑下为啥会超卖呢?
在这个秒杀过程中,因为数据量大,请求多。当请求一窝蜂冲进来之后,数据库中数量已经为0了,但是由于已经有请求放进来了,所以还会去执行sql语句去数据库中进行操作。所以有三种方法可以应付这个问题:
1、sql语句中对库存进行判断,防止为负数时仍执行sqk语句。
2、数据库加唯一索引防止用户重复购买。
3、redis中进行库存预减,请求入队进行排队,实现异步下单。
资源静态化
资源静态化有页面静态化与对象静态化。静态化就是将信息保存到Redis缓存中,当访问的时候可以直接取缓存,而不用每次都去请求服务器。对于服务器来说,也是一件好事。
URL动态化
比如我们打开谷歌浏览器,进入network,我们每点击一个地方,都会出现对应的url,如果拿到这个url,那么就可以在秒杀的时候不断地去请求,这样就很有可能造成什么呢?你的秒杀商品直接被有心人给刷完了。不过我们在秒杀开始之前可以将按钮置灰,这样秒杀前在页面上也拿不到url。
要是开发人员知道这个url,直接自己暗中操作,自己将商品给刷完。比如0.01元抢爱疯12pro,开发人员自己刷个100台,然后辞职跑路。
咳,这样肯定是不道德的,但是为了避免拿到url暗中操作,可以将url进行拼接一个随机字符串,比如md5,uuid之类的都是可以的,只要保证url不是固定的就OK。在秒杀的时候,先对这个url进行判断,通过之后才去进行接下来的操作,这样也提高了一定的安全性。
恶意请求
其实也很正常啊,比如可以以很低的价格在你这里买到还不错的东西啊,那我多搞点服务器,不停的去请求秒杀接口,我拿到之后转手卖掉,不久少工作几个月了么?这么好的事,要我我也干。
当然了,鉴于本人的人品,也是不可能那么做滴,但是我们设计的时候也要防患于未然,安全性同样也很重要。
比如上面提到的动态接口,也是安全性的一种方式。同时我们还可以限制请求的次数,比如5秒内只能请求5次,如果五秒内请求多于五次,请求就会无效并提醒用户。
也可以使用验证码,每次请求都要完成验证码验证后才会发起请求。
限流
限流的话,我们可以从前端、后端两方面来看。
前端:在秒杀开始之前,可以将秒杀按钮置灰,当到达秒杀时间的时候,再将按钮置为正常状态,不然开始之前疯狂点击按钮的话,不就成了活动还没开始就结束了。
后端:如果开始阶段顺利的话,那么就可以进入到下订单,完成订单的操作了,不过我是没在秒杀活动中进行到这一步。。。当数据库中预减库存为0的时候,那么就返回一个秒杀活动结束的标志。
降级:如果流量实在太大的话,也可以在尽量完成秒杀功能的情况下,放弃掉一部分用户来保证系统的可用性。
异步削峰
通过上面的限流操作之后,并不会对所有的请求都完成下单操作。因为肯定存在某个瞬间是有很大流量的请求的,可以将这些请求放入到一个队列中,实现流量削峰,达到流量可控和异步处理。
比如在抢票或者秒杀活动中,因为流量大,不会直接执行所有的请求的,先将请求放到队列中一步一步去消费,不管成功还是失败,都要给前端一个反馈。
分布式Session
为了保证服务的高可用,整个系统会有多个服务,这样就造成了,从别的模块过来了,我怎么知道你的状态呢?我怎么知道你是谁呢?
针对这个问题,我们可以使用uuid作为cookie,并存放到Redis中,在拦截器中队方法进行拦截,通过对方法的拦截,根据cookie来获取对象。在每个页面中,我们可以通过这个key来重新获取对象。
redis的库存如何与数据库的库存保持一致
首先要知道的是,redis中的数量不是库存,我们使用redis就是为了阻挡多余的请求透穿到DB,起到一个保护的作用。请求优先命中redis,缓存中没有再命中数据库。
在并发的情况下,如果想保证一致性,就需要与事务相关,但是redis与数据库是两个数据源,之间是没有事务的。因此使用缓存就会存在一定的不一致。
比如现在有10个商品,如果一万个请求过来,这些请求访问数据库或者访问redis,其实是都一样的,因为最终的目的是将商品数量从10减到0,所以redis将10减到0之后就会返回秒杀结束的信息。所以这两也没有必要非得保持一致。
redis 预减成功,DB扣减库存失败怎么办
其实这个的影响并不是很大,对用户而言,秒杀不成功是很正常的。因为大家都知道这个参与秒杀的分母是很庞大的,秒杀成功也就是个小概率事件。
而且对于商家来说,没有秒杀卖出去还省了一部分费用,对于程序员也不用半夜接电话起床救火。不过在实际中还是要尽量避免这样的情况发生,毕竟秒杀中一次好不容易啊。
单独维护一个秒杀结束标志
1、所有的秒杀相关的接口都要加上活动是否结束的标志,如果结束就直接返回,包括轮寻的接口防止一直轮寻,干脆省事。
2、管理后台也可以手动的更改这个标志,防止出现活动开始以后就没办法结束这种意外的事件。
浏览人数统计
因为我们在项目中已经使用了redis,所以实现人数统计就很简单了。redis有个基本数据类型:String。我们可以维护一个String类型的key,在用户每进入一次页面。让这个key自动+1即可。关于redis五种基本数据类型与三种特殊类型,可以参考下我的这篇:Redis的五种基本数据类型与三种特殊数据类型
总结
其实秒杀的关键呢,还是在于减少数据库的访问。你看前面我们让这个页面静态化、对象静态化、使用缓存、使用队列等等,目的不就是为了减少对数据库的访问么?所以无论再怎么复杂,都是减少DB访问、保证系统高可用为核心滴。
源代码
要运行项目,首先得保证你的redis、rabbitmq都可以正常运行,然后在配置文件中修改对应的配置即可:
然后修改对应的maven仓库、jdk即可。我的jdk用的是当前使用最多的jdk8。将sql文件放到数据库就OK了。
Github地址:
https://github.com/xhXiaoQinDong/miaosha
更多推荐
所有评论(0)