秒杀项目系列之十: 防刷限流技术(图形验证码错峰+令牌桶/漏桶算法限流)

  1. 验证码技术
  • 通过包装秒杀令牌前置可以实现削峰的的目的,需要验证码来错峰.

  • 数学公式验证码生成器

  • 验证码技术代码实现

    • CodeUtil.java
    package com.kenai.util;
    import javax.imageio.ImageIO;
    import java.awt.*;
    import java.awt.image.BufferedImage;
    import java.awt.image.RenderedImage;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Random;
    
    public class CodeUtil {
          
          
        private static final int width = 90;      //定义图片的宽度
        private static final int height = 20;     //定义图片的高度
        private static final int codeCount = 4;   //定义图片上显示的验证码的个数
        private static final int xx = 15;         //验证码左侧x坐标起始位置
        private static final int fontHeight = 18; // 字体大小
        private static final int codeY = 16;      //验证码左侧y坐标起始位置
        private static final char[] codeSequence = {
          
          'A','B','C','D','E','F','G','H','I','J','K','L','M','N',
                'O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'};
    
        /**
         * 生成一个map集合
         * code为生成的验证码
         * codePic为生成的验证码BufferedImage对象
         * @return
         */
        public static Map<String, Object> generateCodeAndPic(){
          
          
    		//定义图像buffer
            BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics gd = buffImg.getGraphics();
    		//创造一个随机数生成器类
            Random random = new Random();
    		//将图像填充为白色
            gd.setColor(Color.WHITE);
            gd.fillRect(0, 0, width, height);
    		//创建字体,字体的大小应该根据图片的高度决定, Font.BOLD:字体为粗体
            Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
    		//设置字体
            gd.setFont(font);
    		//画边框
            gd.setColor(Color.BLACK);
            gd.drawRect(0, 0, width - 1, height - 1);
    		//随机产生40条干扰线,使图像中的认证码不易被其他程序探测到
            gd.setColor(Color.BLACK);
            for(int i = 0; i < 30; i++ ){
          
          
                int x = random.nextInt(width);
                int y = random.nextInt(height);
                int xl = random.nextInt(12);
                int yl = random.nextInt(12);
    		//前两个参数为起点坐标,后两个参数为终点坐标
                gd.drawLine(x, y, x + xl, y + yl);
            }
    		//randomeCode用于保证随机产生的验证码,以便用户登陆后进行验证
            StringBuffer randomCode = new StringBuffer();
            int red = 0, green = 0, blue = 0;
    		//随机产生codeCount数字的验证码
            for(int i = 0; i < codeCount; i ++){
          
          
    		//得到随机产生的验证码数字
                String code = String.valueOf(codeSequence[random.nextInt(36)]);
    		//产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同
                red = random.nextInt(255);
                green = random.nextInt(255);
                blue = random.nextInt(255);
    		//用随机产生的颜色将验证码绘制到图像中
                gd.setColor(new Color(red, green, blue));
                gd.drawString(code, (i + 1) * xx, codeY);
    		//将产生的四个随机数组合在一起
                randomCode.append(code);
            }
            Map<String ,Object> map = new HashMap<String, Object>();
    		//存放验证码
            map.put("code", randomCode);
    		//存放验证码BuffedImage对象
            map.put("codePic", buffImg);
            return map;
        }
    
        public static void main(String[] args) throws IOException {
          
          
    		//创建文件输出流对象
            OutputStream out = new FileOutputStream("/Users/zhaolijian/Desktop/miaoshaStable/" + System.currentTimeMillis() + ".jpg");
            Map<String, Object> map = CodeUtil.generateCodeAndPic();
            ImageIO.write((RenderedImage)map.get("codePic"), "jpeg", out);
            System.out.println("验证码的值为:" + map.get("code"));
        }
    }
    
    • OrderController.java
    // 生成验证码
    @GetMapping(value = "/generateverifycode")
    public void generateverifycode(HttpServletResponse response) throws BusinessException, IOException {
          
          
        // 根据token获取用户信息
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if (StringUtils.isEmpty(token)) {
          
          
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户未登陆,不能生成验证码");
        }
        // 获取用户登陆信息
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
        if (userModel == null) {
          
          
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户未登陆,不能生成验证码");
        }
        Map<String, Object> map = CodeUtil.generateCodeAndPic();
        redisTemplate.opsForValue().set("verify_code_" + userModel.getId(), map.get("code"));
        redisTemplate.expire("verify_code_" + userModel.getId(), 5, TimeUnit.MINUTES);
        ImageIO.write((RenderedImage)map.get("codePic"), "jpeg", response.getOutputStream());
    }
    
    /**
     * 生成秒杀令牌
     * @param itemId
     * @param promoId
     * @return
     * @throws BusinessException
     */
    @PostMapping(value = "/generatetoken", consumes = {
          
          CONTENT_TYPE_FORMED})
    public CommonReturnType generatetoken(@RequestParam("itemId") Integer itemId,
                                          @RequestParam("promoId") Integer promoId,
                                          @RequestParam("verifyCode") String verifyCode) throws BusinessException {
          
          
        // 根据token获取用户信息
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if(StringUtils.isEmpty(token)){
          
          
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户未登陆,不能下单");
        }
        // 获取用户登陆信息
        UserModel userModel = (UserModel)redisTemplate.opsForValue().get(token);
        if(userModel == null){
          
          
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户未登陆,不能下单");
        }
        // 验证验证码的有效性
        String redisVerifyCode = (String) redisTemplate.opsForValue().get("verify_code_" + userModel.getId());
        if(StringUtils.isEmpty(redisVerifyCode)){
          
          
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "请求非法");
        }
        if(!redisVerifyCode.equalsIgnoreCase(verifyCode)){
          
          
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "验证码错误");
        }
        // 获取秒杀访问令牌
        String promoToken = promoService.generateSecondKillToken(promoId, itemId, userModel.getId());
        if(promoToken == null){
          
          
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "生成令牌失败");
        }
        return CommonReturnType.create(promoToken);
    }
    
    • getitem.html
    <body>
    	<div class="content">
            <div id = "verifyDiv" style="display: none" class="form-actions">
                <img src=""/>
                <input type="text" id="verifyContent" value="">
                <button class="btn blue" id="verifyButton" type="submit">
                    验证
                </button>
            </div>
        </div>
    </body>
     // jQuery(document).ready()这个方法在dom载入就绪时对其进行操纵并调用执行它所绑定的函数。
    jQuery(document).ready(function(){
        var token = window.localStorage["token"];
        // 点击下单显示验证码,输入验证码点击验证提交生成令牌和下单请求
        $("#createorder").on("click", function () {
            if(token == null){
                alert("没有登陆,不能下单");
                window.location.href="login.html";
                return false;
            }
            $("#verifyDiv img").attr("src", "http://" + g_host + "/order/generateverifycode?token=" + token);
            $("#verifyDiv").show();
        });
        // 点击下单显示验证码,输入验证码点击验证提交生成令牌和下单请求
        $("#verifyButton").on("click", function () {
            $.ajax({
                type:"POST",
                contentType: "application/x-www-form-urlencoded",
                url:"http://" + g_host + "/order/generatetoken?token=" + token,
                data:{
                    "itemId":g_itemVO.id,
                    "promoId":g_itemVO.promoId,
                    "verifyCode":$("#verifyContent").val()
                },
                xhrFields:{withCredentials:true},
                success:function (data) {
                    if(data.status == "success"){
                        var promoToken = data.data;
                        $.ajax({
                            type:"POST",
                            contentType: "application/x-www-form-urlencoded",
                            url:"http://" + g_host + "/order/createorder?token=" + token,
                            data:{
                                "itemId":g_itemVO.id,
                                "promoId":g_itemVO.promoId,
                                "amount":1,
                                "promoToken":promoToken
                            },
                            xhrFields:{withCredentials:true},
                            success:function (data) {
                                if(data.status == "success"){
                                    alert("下单成功");
                                    window.location.reload();
                                }else{
                                    alert("下单失败,原因为"+data.data.errMsg);
                                    if(data.data.errCode == 20003){
                                        window.location.href="login.html";
                                    }
                                }
                            },
                            error:function (data) {
                                alert("下单失败,原因为"+data.responseText);
                            }
                        });
                    }else{
                        alert("获取令牌失败,原因为"+data.data.errMsg);
                        if(data.data.errCode == 20003){
                            window.location.href="login.html";
                        }
                    }
                },
                error:function (data) {
                    alert("获取令牌失败,原因为"+data.responseText);
                }
            });
        })
        initView();
    });
    
  1. 限流
  • 限流目的

    • 因为流量远比你想的要多
    • 系统活着比挂了要好
    • 宁愿只让少数人用,也不要让所有人都不能用
  • 限流方案

    • 限制用户并发数
      缺点: 简单粗暴,不精确. 使用TPS/QPS更加精确.

    • 令牌桶算法
      系统会以一个恒定的速度往桶里放入令牌(令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃). 而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务.令牌桶的好处是可以应对突发流量.

    • 漏桶算法
      把请求比作是水,水来了都先放进桶里,并以限定的速度出水,当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务.它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量.

    • 令牌桶算法和漏桶算法的区别
      漏桶算法能够强行限制数据的传输速率,令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输.而互联网行业中突发流量比较多,所以使用令牌桶算法较多.

  • 限流力度

    • 接口纬度
      限制createOrder()接口的调用,限制TPS

    • 总纬度
      所有接口总TPS限流

  • 限流范围

    • 集群限流
      依赖redis或其他的中间件技术做统一计数器,往往会产生性能瓶颈.
    • 单机限流
      负载均衡前提下单机平均限流效果更好
  • 限流代码实现

    • OrderController.java
    private RateLimiter orderCreateRateLimiter;
    
    @PostConstruct
    public void init(){
          
          
        // 参数为permitspersecond,即每秒钟允许通过的请求数,即TPS
        orderCreateRateLimiter = RateLimiter.create(100);
    }
    
    @PostMapping(value = "/createorder", consumes = {
          
          CONTENT_TYPE_FORMED})
    public CommonReturnType createOrder(@RequestParam("itemId") Integer itemId,
                                        @RequestParam(value = "promoId", required = false) Integer promoId,
                                        @RequestParam("amount") Integer amount,
                                        @RequestParam(value = "promoToken", required = false) String promoToken) throws BusinessException {
          
          
            // 没有令牌则不能下单
            if(!orderCreateRateLimiter.tryAcquire()){
          
          
                throw new BusinessException(EmBusinessError.RATELIMIT);
            }
            // 验证用户是否登陆
            ...
            // 校验秒杀令牌是否正确
            ...
            // 初始化库存流水
            ...
            // 完成对应的下单事务型消息机制
            ...
        }
    
  1. 防刷
  • 出发点
    排队、限流、令牌都只能控制总流量,无法控制黄牛流量.黄牛可能会通过接口、模拟器进行下单.

  • 传统防刷

    • 限制一个会话(session_id, token)同一秒钟/分钟调用接口多少次
      缺点: 多会话(多个session_id或多个token)接入绕开无效.一个淘宝账号在电脑上可以同时登陆.
    • 限制一个IP同一秒钟/同一分钟接口调用多少次
      缺点: 数量不好控制、容易误伤.因为一个企业出口的IP在服务端看来可能是同一个IP,但其实代理了很多正常用户.另外黑客可以仿造IP请求头.
  • 黄牛为什么难防

    • 模拟器作弊: 模拟硬件设备,可修改设备信息
    • 设备牧场作弊: 工作室一批移动设备
    • 人工作弊: 靠佣金吸引兼职人员刷单
  • 防刷之设备指纹

    • 采集终端设备各项参数,启动应用时生成唯一设备指纹
    • 根据对应设备指纹的参数猜测出模拟器等可疑设备概率
  • 防刷之凭证系统

    • 根据设备下发凭证
    • 关键业务链路上带上凭证并由业务系统到凭证服务器上验证
    • 凭证服务器根据对应凭证所等价的设备指纹参数并根据实时行为风控系统判定对应凭证的可疑度分数
    • 若分数低于某个数值则由业务系统返回固定错误码,拉起前端验证码验身,验身成功后加入凭证服务器对应分数

猜你喜欢

转载自blog.csdn.net/qq_26496077/article/details/113699912