秒杀专题-系统的设计(一)

秒杀专题-系统的设计(一)

观察从客户端请求访问到服务器,整个过程经历了 从服务器网关->代码(Service层)->数据库

根据木桶理论,整个访问的速度取决于系统中响应速度最慢的地方。而访问数据库是内存对磁盘进行IO,是系统中效率速度最低的地方,同时数据库所支持的QPS也是最小的,所有系统在大并发时数据库是最容易崩溃的。

因此所有对并发秒杀的优化核心在于如何减少全部请求直接对数据库的访问,全部思路和技术也是围绕此展开

简单优化思路如下:

  1. 将数据放到Redis中,也就是放到内存中,提高查询的效率,而不去直接访问DB,对于已经秒杀完的产品可以直接全部返回。绝大部分秒杀失败的请求都被Redis挡住,快速做了处理。

  2. 使用MQ,将Redis放进处理程序的请求进行异步处理,直接对用户进行返回而不等待同步处理完成。提高了对用户的响应速度,但并没有减少整体的处理时间,因为到实际处理的代码还是同步在操作数据库(创建订单,减库存)。

  3. 前端的缓存,页面缓存,减少对刷新页面服务器的请求

总结上面的思路就得到了如下处理方式:

把所有可以缓存的东西缓存起来

  • 用户登录:用户第一次登录是携带账号,密码进行登录的,必须要查询一次数据库。第一次之后就用token存到用户cookie中进行登录,但是这个token如果写到DB中那么之后的登录即使是检查token登陆也要访问DB,这就是需要避免的。所以可以将用户对象存储起来,(使用JSON提供的功能,对象可以被序列化也可以通过字节码反序列化变成对象),那么之后用户再登录直接通过token就能在redis中找到用户对象进行使用

  • 秒杀商品的库存信息:显然每次秒杀后库存是减少了的,但不应该立即就去MySQL中进行修改,那样又会直接访问DB,这些信息同样也是缓存在redis中。

  • 秒杀商品的订单信息:用户秒杀之后生成了对应的订单用来组织用户再次秒杀,那么显然成功秒杀的订单应该存在于redis中。

  • 秒杀是否已经结束:正常来说用户每次访问都会先去redis中查询库存尝试减库存,但是考虑到redis也是通过网络提供服务,所以对于秒杀是否还在进行这种信息(不需要入库的信息)可以直接放在本地内存中(使用内存标记)其实就是使用一个数据结构存储特定商品的秒杀状态

单机优化思路

  1. 增加redis缓存,在Redis中减库存。所有请求都会过redis,只有成功减库存的才会进行MySQL。减少了 除秒杀成功之外的请求,增加了全部请求对Redis的访问

  2. 使用内存标记,在库存已经减完的情况下不再去访问Redis,请求redis也算网络开销了,内存中就是JVM可以直接访问到,速度最快。只有在内存标记被置为售完之前的请求,(瞬间并发冲进来的那一部分请求)会访问redis,修改完内存标记后,剩下的请求不会访问redis了。

  3. 使用MQ提升用户体验。首先MQ将创建订单和MySQL中真实减库存的操作去异步处理,但是这一步是没有提升效率的,因为原本即使并发去执行操作MySQL,也是线程安全的(而且因为Redis保证了进来的线程均是秒杀成功的线程)而且是串行执行的,放到MQ中仍然是串行执行(执行的线程数也一样)。但是区别在于整个串行执行过程中,所有秒杀到商品的线程是在阻塞等待去操作MySQL(操作同一行的会阻塞,也就是减库存),客户端的请求也就阻塞了,而异步可以马上给用户一个反馈,并让客户端再进行定时来请求结果(结果是存在Redis中的)。那么这样,原本阻塞到减库存和创建订单全部成功的长请求,被分割成了两段,第一次请求可以快速响应,第二段是连续多段的缓存访问阻塞DB->快速响应查询结果(redis),(DB服务被MQ去执行了).

我们可以得到目前的系统链路图大致如下:


秒杀系统.png

可以看到,目前为止整个系统对于DB的冲击已经十分小了。单机的QPS就差不多这样了。但是这个系统仍然还有其他非常多值得讨论的细节。

但是后端还有一些需要进行处理的问题,比如超卖&重复秒杀

超卖&重复秒杀

这个算是最好解决的问题了。超卖问题出现在程序直接去检查MySQL中的库存来作为库存是否充足的判断标准,但实际上一个服务线程的运行流程是:检查库存->减少库存。这中间至少包含了两步,一定不是原子性的操作,而导致了多个服务线程可以减少同一份库存。

只需要直接操作Redis中的缓存就可以了,无论多少个线程都是被Redis单线程执行的,每个线程的操作结果一定正确。而之后只需要判断操作结果是否大于等于0即可,线程就安全了,代码如下:

 @RequestMapping("/seckill/{id}")
    public Result SecKill(@PathVariable("id") String secId, HttpServletRequest request){
        //获取登录用户
        User user = secKillService.getLoginUser(request);
        if(user==null){
            return ResultUtil.error(CodeMsgUtil.USER_NOT_LOGIN);
        }
        //加内存标记
        if(proMap.containsKey(secId)&&proMap.get(secId)){
            return ResultUtil.error(CodeMsgUtil.SEC_SOLD_OUT);
        }
        //预减库存
        Long remain = redisService.decr(secId);     //redis是安全的
        if(remain < 0){
            proMap.put(secId, true);
            //不能秒杀的归还库存
            redisService.incr(secId);
            return ResultUtil.error(CodeMsgUtil.SEC_SOLD_OUT);
        }
        //到这里多少并发的线程都是线程安全的了

如果一定要检查MySQL去看库存数量,那么在sql语句中加上对库存数量≥0的限制就可以了,这样会有很多的线程尝试去减库存,但只有等于秒杀商品数量的线程可以成功减库存。因为MySQL中对同一行数据的操作(同一件商品的库存信息)是加了行锁的,所以在这里也变成了线程安全的操作。

<update id="SecKillGoods">
        update seckill_goods_list
        set stock_count = stock_count - 1
        where good_id = #{secId} and stock_count > 0
    </update>

而对于重复秒杀而已,订单在MQ中处理完成之后会写入到Redis中,用于之后用户再次秒杀时阻止。但MQ处理订单,到写入Redis中间有相当长的一段时间,可能此时用户已经再次进来秒杀。所以这里存在的情况是:任务刚进MQ队列,还没有写MySQL也没有写Redis,所以此时去RedisMySQL中检查是都无法得到订单信息的。

这里考虑一种在订单表中通过用户id商品id建立唯一索引(用户和秒杀的商品联系起来)的方法,MySQL自身的特性会阻止第一个订单之后的订单写入,那么这样MQ最终在创建重复订单时就会失败,重复秒杀就不可能了。

当然还可以利用Redis在缓存中多记录一些信息来实现,充分利用Redis的单线程特性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354