秒杀系统的设计思路(应对高并发,超卖等问题的解决思路)

首先我们先看一下设计秒杀系统时,我们应该考虑的问题。

秒杀系统的设计思路(应对高并发,超卖等问题的解决思路)_第1张图片

解决方案:

 一.页面静态化结合CDN内容分发

前端把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

        秒杀活动的页面内容,大多数都是固定不变的。我们可以对秒杀页面做静态化处理,减少访问服务端的压力。在秒杀活动开始前,提前生成包含秒杀商品信息、倒计时等固定内容的静态 HTML 页面。这些页面可以根据秒杀活动的特点,例如开始时间、结束时间等来预先生成。

示例如下:

// 生成秒杀页面的HTML
public String generateSeckillPage(long goodsId) {
    GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
    // 其他固定信息的获取

    // 生成HTML页面,包含秒杀商品信息、倒计时等
    String htmlContent = "......";

    // 将HTML页面保存到静态文件,或者存储到数据库中

    return htmlContent;
}

        参与秒杀的用户可能分布在全国各地,地域不同,网速不同。为了让用户可以快速的进入到秒杀活动的页面,可以使用 CDN(Content Delivery Network,内容分发网络)让用户可以就近获取所需内容。将生成的静态 HTML 页面通过 CDN 分发,使用户可以就近获取所需内容。CDN 的作用是将页面的静态资源分发到全球不同的节点,用户访问时可以从离他们最近的节点获取内容,减少网络延迟,提高访问速度。

        用户访问秒杀活动页面时,直接请求 CDN 上的静态 HTML 页面,不再需要服务端动态生成页面内容。这样可以减轻服务端的压力,提高页面加载速度,提升用户体验。

二.按钮置灰控制

        秒杀活动开始前,按钮一般需要置灰。只有时间到了,才能变得可以点击。 这防止,秒杀用户在时间快到的前几秒,疯狂请求服务器,然后秒杀时间点 还没到,服务器就自己挂了。

三. 服务单一职责

        我们都知道微服务设计思想,也就是各个功能模块拆分,功能那个类似放 一起,再用分布式部署方式进行部署。

        如用户登录相关,就设计个用户服务,订单相关就搞个订单服务等等。那么,秒杀相关业务逻辑也可以放到一起, 搞个秒杀服务,单独给它搞个秒杀数据库。” 服务单一职责有个好处:如果秒杀 没抗住高并发压力,秒杀库崩了,服务挂了,也不会影响到系统其他服。

四.秒杀地址加盐(url动态化)

        链接如果明文暴露的话,会有人获取到请求 Url,提前秒杀了。因此,需要给 秒杀链接加盐。可以让URL 动态化,如通过 MD5 加密算法加密随机字符串来生成,秒杀链接的URL。

时间校验: 通过获取商品的起始时间和结束时间,判断当前时间与秒杀活动的状态(未开始、进行中、已结束),从而判断用户是否可以参与秒杀。

 GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        long startAt = goods.getStartDate().getTime();
        long endAt = goods.getEndDate().getTime();
        long now = System.currentTimeMillis();
        int miaoshaStatus = 0;
        int remainSeconds = 0;
        if(now < startAt ) {//秒杀还没开始,倒计时
            miaoshaStatus = 0;
            remainSeconds = (int)((startAt - now )/1000);
        }else  if(now > endAt){//秒杀已经结束
            miaoshaStatus = 2;
            remainSeconds = -1;
        }else {//秒杀进行中
            miaoshaStatus = 1;
            remainSeconds = 0;
        }

        但仅仅是这样的话还是不够的,如果我们的秒杀的真实地址被人知道了,就可以写一个脚本不断的获取北京时间,在秒杀开始的毫秒级别时就可以请求。而且机器能在短时间内发送大量的请求,绝对会对我们的秒杀活动造成巨大的影响。
那怎么办呢?
        我们可以把我们的秒杀地址动态化,也就是通过MD5之类的加密算法去处理我们的秒杀地址,存入 redis缓存中,根据前端请求的url获取path。 判断与缓存中的字符串是否一致,一致就认为请求是正常的。这就是秒杀链接加盐,用这样的方法来阻止恶意用户直接请求我们的秒杀地址。

@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result getMiaoshaPath( 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);
    }



public String createMiaoshaPath(MiaoshaUser user, long goodsId) {
        if(user == null || goodsId <=0) {
            return null;
        }
        String str = MD5Util.md5(UUIDUtil.uuid()+"123456");
        redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, str);
        return str;
    }

实现原理总结如下:

  1. 用户通过前端请求获取秒杀地址的path。
  2. 后端根据用户信息、商品ID等生成动态的秒杀地址。
  3. 将生成的地址通过加盐的加密算法处理,存入Redis缓存中,建立用户ID和商品ID的关联。
  4. 用户提交秒杀请求时,携带path参数,后端根据前端请求的URL获取path,再与Redis缓存中的地址进行比对,判断请求的合法性。

通过这种方法,成功防止了用户通过直接请求秒杀地址进行恶意操作,保障了秒杀活动的公正性和安全性。

五.接口限流进行防刷

        一般有两种方式限流:nginx 限流和redis 限流。

为了防止某个用户请求过于频繁,我们可以对同一用户限流;

为了防止黄牛模拟几个用户请求,我们可以对某个 IP 进行限流;

为了防止有人使用代理,每次请求都更换 IP 请求,我们可以对接口进行限流。

1. Nginx 提供了基于令牌桶算法的限流机制,可以有效控制请求的流量。通过配置 ngx_http_limit_req_module 模块,可以设定每个 IP 或每个 URI 的请求频率上限。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        location /api {
            limit_req zone=one burst=5;
            # 其他配置
        }
    }
}

上述配置中,limit_req_zone 定义了一个名为 one 的限流区域,每个 IP 的请求速率为 1 次/秒,limit_req 用于限制每秒的请求数,burst 指定了可以超出速率的请求数 

2.0为了防止瞬时过大流量压垮系统,还可以使用阿里Sentinel、Hystrix 组件进行限流。

六. 超卖问题解决思路

        只要是一个秒杀系统,就必然会存在超卖问题。不同用户在读请求的时候,发现商品库存足够,然后同时发起请求,进行秒杀操作,减库存,导致库存减为负数。

1.0使用乐观锁解决

        如果要使用乐观锁,那么我们就要给个商品库存一个版本号version字段,在每次我们读取库存的时候把版本号也读取出来,当这一个线程去执行扣减库存的操作的时候,去判断数据库当前的版本号是否是刚刚读取出来的版本号,如果不是则秒杀失败。

/**
     * 查询商品库存
     * @param id 商品id
     * @return
     */
    @Select("SELECT * FROM goods WHERE id = #{id}")
    Goods getStock(@Param("id") int id);

/**
     * 乐观锁方案扣减库存
     * @param id 商品id
     * @param version 版本号
     * @return
     */
    @Update("UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND stock > 0 AND version = #{version}")
    int decreaseStockForVersion(@Param("id") int id, @Param("version") int version);

service层:

/**
     * 扣减库存
     * @param gid 商品id
     * @param uid 用户id
     * @return SUCCESS 1 FAILURE 0
     */
    @Transactional
    public int sellGoods(int gid, int uid) {

        // 获取库存
        Goods goods = goodsDao.getStock(gid);
        if (goods.getStock() > 0) {
            // 乐观锁更新库存
            int update = goodsDao.decreaseStockForVersion(gid, goods.getVersion());
            // 更新失败,说明其他线程已经修改过数据,本次扣减库存失败,可以重试一定次数或者返回
            if (update == 0) {
                return 0;
            }
            // 库存扣减成功,生成订单
            Order order = new Order();
            order.setUid(uid);
            order.setGid(gid);
            int result = orderDao.insertOrder(order);
            return result;
        }
        // 失败返回
        return 0;
    }

2.0使用redis分布式锁

用 Redis  SET EX PX NX + 校验唯一随机值,再删除释放锁。

if (jedis.set(key_resource id, uni request id,"NX","Ex",100s) == 1)[ //加锁
    try {
        do something //业务处理
    catch(){
    }
    finally {
        //判断是不是当前线程加的锁,是才释放if (uni_request id.equals(jedis.get(key_resource_ id))) {
        jedis.del(lockKey);
        }
    }
}
七.MQ异步处理

如果瞬间流量特别大,可以使用消息队列削峰,异步处理。用户请求过来的时 候,先放到消息队列,再拿出来消费。

八.限流&降级&熔断

 限流,就是限制请求,防止过大的请求压垮服务器;

降级,就是秒杀服务有问题了,就降级处理,不要影响别的服务;

熔断,服务有问题就熔断,一般熔断降级是一起出现。

你可能感兴趣的:(服务器,java,spring,spring,boot,开发语言,redis)