如何设计一个秒杀系统

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 如何设计一个秒杀系统


秒杀整体架构图

一、秒杀接口优化思路:
1、系统初始化把商品数量加载到redis
2、收到请求redis预减库存,库存不足直接返回,否则进入3
3、后端请求进入mq队列,前端显示请求中
4、请求出队,生成订单,减少库存
5、客户端轮询,是否秒杀成功
二、秒杀接口的隐藏:秒杀开始之前先去请求获取秒杀的地址
1、接口改造带上PathVariable参数
2、添加生成地址的接口
3、秒杀收到请求先验证PathVariable
三、图形验证码分散用户请求:
1、添加生成验证码的接口
2、在获取秒杀路径的时候,验证验证码
3、ScriptEngine的使用
四、接口防刷:用redis对接口限流

后端主要代码:

@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {
  @Autowired
  MiaoshaUserService userService;
  @Autowired
  RedisService redisService;
  @Autowired
  GoodsService goodsService;
  @Autowired
  OrderService orderService;
  @Autowired
  MiaoshaService miaoshaService;
  @Autowired
  MQSender sender;
  private HashMap<Long, Boolean> localOverMap =  new HashMap<Long, Boolean>();
  /**
   * 系统初始化
   * */
  public void afterPropertiesSet() throws Exception {
    List<GoodsVo> goodsList = goodsService.listGoodsVo();
    if(goodsList == null) {
      return;
    }
    for(GoodsVo goods : goodsList) {
      redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
      localOverMap.put(goods.getId(), false);
    }
  }
  @RequestMapping(value="/reset", method=RequestMethod.GET)
    @ResponseBody
    public Result<Boolean> reset(Model model) {
    List<GoodsVo> goodsList = goodsService.listGoodsVo();
    for(GoodsVo goods : goodsList) {
      goods.setStockCount(10);
      redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), 10);
      localOverMap.put(goods.getId(), false);
    }
    redisService.delete(OrderKey.getMiaoshaOrderByUidGid);
    redisService.delete(MiaoshaKey.isGoodsOver);
    miaoshaService.reset(goodsList);
    return Result.success(true);
  }
    @RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)
    @ResponseBody
    public Result<Integer> miaosha(Model model,MiaoshaUser user,
        @RequestParam("goodsId")long goodsId,
        @PathVariable("path") String path) {
      model.addAttribute("user", user);
      if(user == null) {
        return Result.error(CodeMsg.SESSION_ERROR);
      }
      //验证path
      boolean check = miaoshaService.checkPath(user, goodsId, path);
      if(!check){
        return Result.error(CodeMsg.REQUEST_ILLEGAL);
      }
      //内存标记,减少redis访问
      boolean over = localOverMap.get(goodsId);
      if(over) {
        return Result.error(CodeMsg.MIAO_SHA_OVER);
      }
      //预减库存
      long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
      if(stock < 0) {
         localOverMap.put(goodsId, true);
        return Result.error(CodeMsg.MIAO_SHA_OVER);
      }
      //判断是否已经秒杀到了
      MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
      if(order != null) {
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
      }
      //入队
      MiaoshaMessage mm = new MiaoshaMessage();
      mm.setUser(user);
      mm.setGoodsId(goodsId);
      sender.sendMiaoshaMessage(mm);
      return Result.success(0);//排队中
    }
    /**
     * orderId:成功
     * -1:秒杀失败
     * 0: 排队中
     * */
    @RequestMapping(value="/result", method=RequestMethod.GET)
    @ResponseBody
    public Result<Long> miaoshaResult(Model model,MiaoshaUser user,
        @RequestParam("goodsId")long goodsId) {
      model.addAttribute("user", user);
      if(user == null) {
        return Result.error(CodeMsg.SESSION_ERROR);
      }
      long result  =miaoshaService.getMiaoshaResult(user.getId(), goodsId);
      return Result.success(result);
    }
    @AccessLimit(seconds=5, maxCount=5, needLogin=true)
    @RequestMapping(value="/path", method=RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
        @RequestParam("goodsId")long goodsId,
        @RequestParam(value="verifyCode", defaultValue="0")int verifyCode
        ) {
      if(user == null) {
        return Result.error(CodeMsg.SESSION_ERROR);
      }
      boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
      if(!check) {
        return Result.error(CodeMsg.REQUEST_ILLEGAL);
      }
      String path  =miaoshaService.createMiaoshaPath(user, goodsId);
      return Result.success(path);
    }
    @RequestMapping(value="/verifyCode", method=RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
        @RequestParam("goodsId")long goodsId) {
      if(user == null) {
        return Result.error(CodeMsg.SESSION_ERROR);
      }
      try {
        BufferedImage image  = miaoshaService.createVerifyCode(user, goodsId);
        OutputStream out = response.getOutputStream();
        ImageIO.write(image, "JPEG", out);
        out.flush();
        out.close();
        return null;
      }catch(Exception e) {
        e.printStackTrace();
        return Result.error(CodeMsg.MIAOSHA_FAIL);
      }
    }
}

前端主要代码:

<div class="row">
        <div class="form-inline">
      <img id="verifyCodeImg" width="80" height="32"  style="display:none" onclick="refreshVerifyCode()"/>
      <input id="verifyCode"  class="form-control" style="display:none"/>
      <button class="btn btn-primary" type="button" id="buyButton"onclick="getMiaoshaPath()">立即秒杀</button>
        </div>
</div>
function getMiaoshaPath(){
  var goodsId = $("#goodsId").val();
  g_showLoading();
  $.ajax({
    url:"/miaosha/path",
    type:"GET",
    data:{
      goodsId:goodsId,
      verifyCode:$("#verifyCode").val()
    },
    success:function(data){
      if(data.code == 0){
        var path = data.data;
        doMiaosha(path);
      }else{
        layer.msg(data.msg);
      }
    },
    error:function(){
      layer.msg("客户端请求有误");
    }
  });
function getMiaoshaResult(goodsId){
  g_showLoading();
  $.ajax({
    url:"/miaosha/result",
    type:"GET",
    data:{
      goodsId:$("#goodsId").val(),
    },
    success:function(data){
      if(data.code == 0){
        var result = data.data;
        if(result < 0){
          layer.msg("对不起,秒杀失败");
        }else if(result == 0){//继续轮询
          setTimeout(function(){
            getMiaoshaResult(goodsId);
          }, 200);
        }else{
          layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]},
              function(){
                window.location.href="/order_detail.htm?orderId="+result;
              },
              function(){
                layer.closeAll();
              });
        }
      }else{
        layer.msg(data.msg);
      }
    },
    error:function(){
      layer.msg("客户端请求有误");
    }
  });
}
function doMiaosha(path){
  $.ajax({
    url:"/miaosha/"+path+"/do_miaosha",
    type:"POST",
    data:{
      goodsId:$("#goodsId").val()
    },
    success:function(data){
      if(data.code == 0){
        //window.location.href="/order_detail.htm?orderId="+data.data.id;
        getMiaoshaResult($("#goodsId").val());
      }else{
        layer.msg(data.msg);
      }
    },
    error:function(){
      layer.msg("客户端请求有误");
    }
  });
}
function refreshVerifyCode(){
  $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()+"&timestamp="+new Date().getTime());
}

附录一:数据库的设计

商品表:
create table Goods (
 id bigint  PRIMARY KEY ,
 goodsName VARCHAR(255) ,
 goodsTitle VARCHAR(255) ,
 goodsImg VARCHAR(255) ,
 goodsDetail VARCHAR(255) ,
 goodsPrice double precision not null ,
 goodsStock INTEGER
 )ENGINE =INNODB DEFAULT  CHARSET= utf8;
秒杀商品表:
create table MiaoshaGoods (
id bigint  PRIMARY KEY ,
 goodsId bigint ,
 stockCount INTEGER ,
 startDate null ,
 endDate null
 )ENGINE =INNODB DEFAULT  CHARSET= utf8;
秒杀订单表:
create table MiaoshaOrder (
id bigint  PRIMARY KEY ,
 userId bigint ,
 orderId bigint ,
 goodsId bigint
 )ENGINE =INNODB DEFAULT  CHARSET= utf8;
用户表:
create table MiaoshaUser (
 id bigint  PRIMARY KEY ,
 nickname VARCHAR(255) ,
 password VARCHAR(255) ,
 salt VARCHAR(255) ,
 head VARCHAR(255) ,
 registerDate null ,
 lastLoginDate null ,
 loginCount INTEGER
 )ENGINE =INNODB DEFAULT  CHARSET= utf8;
订单表:
create table OrderInfo (
id bigint  PRIMARY KEY ,
 userId bigint ,
 goodsId bigint ,
 deliveryAddrId bigint ,
 goodsName VARCHAR(255) ,
 goodsCount INTEGER ,
 goodsPrice double precision not null ,
 orderChannel INTEGER ,
 status INTEGER ,
 createDate null ,
 payDate null
 )ENGINE =INNODB DEFAULT  CHARSET= utf8;

附录二:拦截器内实现的接口限流

@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
  int seconds();
  int maxCount();
  boolean needLogin() default true;
}
@Service
public class AccessInterceptor  extends HandlerInterceptorAdapter{
  @Autowired
  MiaoshaUserService userService;
  @Autowired
  RedisService redisService;
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    if(handler instanceof HandlerMethod) {
      MiaoshaUser user = getUser(request, response);
      UserContext.setUser(user);
      HandlerMethod hm = (HandlerMethod)handler;
      AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
      if(accessLimit == null) {
        return true;
      }
      int seconds = accessLimit.seconds();
      int maxCount = accessLimit.maxCount();
      boolean needLogin = accessLimit.needLogin();
      String key = request.getRequestURI();
      if(needLogin) {
        if(user == null) {
          render(response, CodeMsg.SESSION_ERROR);
          return false;
        }
        key += "_" + user.getId();
      }else {
        //do nothing
      }
      AccessKey ak = AccessKey.withExpire(seconds);
      Integer count = redisService.get(ak, key, Integer.class);
        if(count  == null) {
           redisService.set(ak, key, 1);
        }else if(count < maxCount) {
           redisService.incr(ak, key);
        }else {
          render(response, CodeMsg.ACCESS_LIMIT_REACHED);
          return false;
        }
    }
    return true;
  }
  private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
    response.setContentType("application/json;charset=UTF-8");
    OutputStream out = response.getOutputStream();
    String str  = JSON.toJSONString(Result.error(cm));
    out.write(str.getBytes("UTF-8"));
    out.flush();
    out.close();
  }
  private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
    String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
    String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
    if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
      return null;
    }
    String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
    return userService.getByToken(response, token);
  }
  private String getCookieValue(HttpServletRequest request, String cookiName) {
    Cookie[]  cookies = request.getCookies();
    if(cookies == null || cookies.length <= 0){
      return null;
    }
    for(Cookie cookie : cookies) {
      if(cookie.getName().equals(cookiName)) {
        return cookie.getValue();
      }
    }
    return null;
  }
}

参考代码

https://gitee.com/lzhcode/maven-parent/tree/master/lzh-seckill2


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
9月前
|
缓存 NoSQL 数据库
【高并发】秒杀系统设计思路
【高并发】秒杀系统设计思路
140 0
|
7月前
|
消息中间件 缓存 安全
秒杀系统(1)——秒杀功能设计理念
秒杀系统(1)——秒杀功能设计理念
118 0
|
8月前
|
消息中间件 缓存 JavaScript
如何设计一个秒杀系统
如何设计一个秒杀系统
|
消息中间件 缓存 NoSQL
如何设计一个秒杀系统???
如何设计一个秒杀系统???
157 0
|
数据采集 域名解析 缓存
设计一个秒杀系统架构
对于秒杀架构的设计,需要遵循以下个原则: 东西不能超卖、 下单成功的订单数据不能丢失、 服务器和数据库不能挂 尽量不让机器人抢走 整体的思路 秒杀架构的设计方案就是一个不断过滤请求的过程,从系统架构层面来说,秒杀系统的分层思路如下。
132 0
|
缓存 NoSQL 安全
秒杀系统的设计思路
你好看官,里面请!今天笔者讲的是秒杀系统的设计思路。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途,如需转载请跟我联系。
388 2
|
消息中间件 缓存 运维
如何设计一个秒杀系统(下)
这里我们讲解最后一部分
279 0
如何设计一个秒杀系统(下)
|
SQL 存储 缓存
如何设计一个秒杀系统(中)
我们接着上篇继续讲,这篇主要讲一致性
252 0
|
数据采集 缓存 前端开发
如何设计一个秒杀系统(上)
秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大流量的秒杀系统需要进行哪些关注,就是本文讨论的话题。
392 0
如何设计一个秒杀系统(上)
|
缓存 监控 NoSQL
秒杀系统
秒杀能够以极小的经费撬动巨大的流量,虽然会带来一定的口碑损失,但因为极具性价比,所以经常被运营同学使用。本文介绍如何设计一款能够支撑60W QPS的秒杀系统,希望能够帮助到大家。