聚焦Java性能优化 打造亿级流量秒杀系统学习笔记09_防刷限流技术

 2020-07-05 

文章目录

    • 本章目标
    • 10-2 验证码技术
      • 验证码代码实现
    • 10-4 限流的目的
      • 限流方案
        • 限并发
        • 令牌桶算法
        • 漏桶算法
      • 限流范围
    • 10-6 限流代码实现(Guava RateLimit)
    • 10-7 防刷技术
      • 传统防刷
      • 黄牛为什么难防
      • 设备指纹
      • 凭证系统

本章目标

  • 掌握验证码生成与验证技术
  • 掌握限流原理与实现
  • 掌握防黄牛技术

10-2 验证码技术

  • 包装秒杀令牌前置,需要验证码来错峰

  • 数学公式验证码生成器

    验证码代码实现

    新建CodeUtil实现生成验证码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    package com.miaoshaproject.util;

    import java.awt.Color;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.image.BufferedImage;
    import java.awt.image.RenderedImage;
    import java.io.FileOutputStream;
    import java.io.OutputStream;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Random;

    import javax.imageio.ImageIO;

    public class CodeUtil {
        private static int width = 90;// 定义图片的width
        private static int height = 20;// 定义图片的height
        private static int codeCount = 4;// 定义图片上显示验证码的个数
        private static int xx = 15;
        private static int fontHeight = 18;
        private static  int codeY = 16;
        private static 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);
            // Graphics2D gd = buffImg.createGraphics();
            // Graphics2D gd = (Graphics2D) buffImg.getGraphics();
            Graphics gd = buffImg.getGraphics();
            // 创建一个随机数生成器类
            Random random = new Random();
            // 将图像填充为白色
            gd.setColor(Color.WHITE);
            gd.fillRect(0, 0, width, height);

            // 创建字体,字体的大小应该根据图片的高度来定。
            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);
            }

            // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
            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);
            //存放生成的验证码BufferedImage对象
            map.put("codePic", buffImg);
            return map;
        }

        public static void main(String[] args) throws Exception {
            //创建文件输出流对象
            OutputStream out = new FileOutputStream("/Users/hzllb/Desktop/javaworkspace/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中加入生成验证码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        @RequestMapping(value = "/generateverifycode",method = {RequestMethod.POST,RequestMethod.GET})
        @ResponseBody
        public void generateeverifycode(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();
            ImageIO.write((RenderedImage) map.get("codePic"), "jpeg", response.getOutputStream());
            redisTemplate.opsForValue().set("verify_code_"+userModel.getId(),map.get("code"));
            redisTemplate.expire("verify_code_"+userModel.getId(),10,TimeUnit.MINUTES);
        }

    生成秒杀令牌前验证验证码的有效性

1
2
3
4
5
6
7
8
        //通过verifycode验证验证码的有效性
        String redisVerifyCode = (String) redisTemplate.opsForValue().get("verify_code_"+userModel.getId());
        if(StringUtils.isEmpty(redisVerifyCode)) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"请求非法");
        }
        if(!redisVerifyCode.equalsIgnoreCase(redisVerifyCode)) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"请求非法,验证码错误");
        }

10-4 限流的目的

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

限流方案

限并发

对同一时间固定访问接口的线程数做限制,利用全局计数器,在下单接口OrderController处加一个全局计数器,并支持并发操作,当controller在入口的时候,计数器减1,判断计数器是否大于0,在出口时计数器加一,就可以控制同一时间访问的固定。

令牌桶算法

令牌桶算法可以做到客户端一秒访问10个流量,下一秒就是下一个10个流量,限定某个时刻的最大值

漏桶算法

漏桶算法的目的用来平滑网络流量,没有办法应对突发流量

限流范围

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

10-6 限流代码实现(Guava RateLimit)

RateLimiter没有实现令牌桶内定时器的功能,
reserve方法是当前秒的令牌数,如果当前秒内还有令牌就直接返回;
若没有令牌,需要计算下一秒是否有对应的令牌,有一个下一秒计算的提前量
使得下一秒请求过来的时候,仍然不需要重复计算
RateLimiter的设计思想比较超前,没有依赖于人为定时器的方式,而是将整个时间轴
归一化到一个数组内,看对应的这一秒如果不够了,预支下一秒的令牌数,并且让当前的线程睡眠;
如果当前线程睡眠成功,下一秒唤醒的时候令牌也会扣掉,程序也实现了限流

1
2
3
4
5
6
7
8
private RateLimiter orderCreateRateLimiter;

    @PostConstruct
    public void init(){
        executorService = Executors.newFixedThreadPool(30);

        orderCreateRateLimiter = RateLimiter.create(300);
    }

10-7 防刷技术

  • 排队,限流,令牌均只能控制总流量,无法控制黄牛流量

传统防刷

  • 限制一个会话(session_id,token)同一秒/分钟接口调用多少次:多会话接入绕开无效(黄牛开多个会话)
  • 限制一个ip同一秒钟/分钟 接口调用多少次:数量不好控制,容易误伤,黑客仿制ip

黄牛为什么难防

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

设备指纹

  • 菜鸡终端设备各项参数,启动应用时生成唯一设备指纹
  • 根据对应设备指纹的参数猜测出模拟器等可疑设备概率

凭证系统

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

—————————————————————————————————
【本课程已整理完毕】

01_电商秒杀商品回顾
02_云端部署
03_分布式扩展
04_查询性能优化技术之多级缓存
05_查询性能优化技术之页面静态化
06_交易性能优化技术之缓存库存
07_交易性能优化技术之事务型消息
08_流量削峰技术
09_防刷限流技术
10_课程总结

—————————————————————————————————