高并发下jmeter性能压测及性能提升解决方案(十)防刷限流

验证码生成与验证技术

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

代码:

生成验证码

package com.miaoshaProject.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.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * @author aric
 * @create 2021-08-11-17:05
 * @fun
 */
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<>();
        //存放验证码
        map.put("code",randomCode);
        //存放生成的验证码BufferedImage对象
        map.put("codePic",buffImg);
        return map;
    }

    public static void main(String[] args) throws IOException {
        //创建文件输出流对象
        FileOutputStream out = new FileOutputStream("/Desktop/javaworkspace/miaoshaStable/" + System.currentTimeMillis()+".jpg");
        Map<String, Object> map = CodeUtil.generateCodeAndPic();
        ImageIO.write((RenderedImage) map.get("codePic"),"jepg",out);  //画在页面上
        System.out.println(""+map.get("code"));
    }
}

controller层加入验证码验证:

package com.miaoshaProject.controller;

import com.miaoshaProject.error.BusinessException;
import com.miaoshaProject.error.EmBusinessError;
import com.miaoshaProject.mq.MqProducer;
import com.miaoshaProject.response.CommonReturnType;
import com.miaoshaProject.service.ItemService;
import com.miaoshaProject.service.OrderService;
import com.miaoshaProject.service.PromoService;
import com.miaoshaProject.service.model.UserModel;
import com.miaoshaProject.util.CodeUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.*;

/**
 * @author aric
 * @create 2021-07-02-18:41
 * @fun
 */
@Controller("order")
@RequestMapping("/user")
@CrossOrigin(origins = {"*"}, allowedHeaders = "true")
public class orderController extends BaseController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private HttpServletRequest httpServletRequest;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private MqProducer mqProducer;

    @Autowired
    private ItemService itemService;

    @Autowired
    private PromoService promoService;

    private ExecutorService executorService;

    @PostConstruct
    public void init(){
        executorService = Executors.newFixedThreadPool(20);  //new出来一个线程池只有20个线程可工作赋给executorService
    }

    //生成验证码
    @RequestMapping(value = "/generateverifycode", method = {RequestMethod.GET,RequestMethod.POST})
    @ResponseBody
    public void generateverifycode(HttpServletResponse response) throws BusinessException, IOException {
        //获取用户的登录信息,之后替换从Redis读取
        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"));  //将验证码和用户Id做绑定
        redisTemplate.expire("verify_code_"+userModel.getId(),5,TimeUnit.MINUTES);
        ImageIO.write((RenderedImage) map.get("codePic"),"jepg",response.getOutputStream());  //写到HttpServletResponse中返回给前端
    }

   //生成秒杀令牌
    @RequestMapping(value = "/generatetoken", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType generatetoken(@RequestParam(name = "itemId") Integer itemId,
                                          @RequestParam(name = "promoId") Integer promoId,
                                          @RequestParam(name = "verifyCode")String verifyCode) throws BusinessException {
        //获取用户的登录信息,之后替换从Redis读取
        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, "用户还未登录,不能下单");
        }

        //通过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(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);
    }

    //封装下单请求
    @RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId,
                                        @RequestParam(name = "promoId", required = false) Integer promoId,
                                        @RequestParam(name = "amount") Integer amount,
                                        @RequestParam(name = "promoToken") String promoToken) throws BusinessException {
        //获取用户的登录信息,之后替换从Redis读取
//        Boolean isLogin = (Boolean)httpServletRequest.getSession().getAttribute("IS_LOGIN");
        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, "用户还未登录,不能下单");
        }
        //校验秒杀令是否正确
        if (promoId != null) {
            String promoTokenInRedis = (String) redisTemplate.opsForValue().get("promo_token_" + promoId + "_userid_" + userModel.getId() + "_itemid_" + itemId);
            if (promoTokenInRedis == null) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
            }
            if (!StringUtils.equals(promoToken, promoTokenInRedis)) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
            }
        }

        //同步调用线程池的submit方法,new Callable的异步线程
        //拥塞窗口为20的等待队列,用来队列泄洪
        Future<Object> future = executorService.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                //加入库存流水init状态
                String stockLogId = itemService.initStockLog(itemId, amount);

                //再去完成对应的下单事务型消息机制
                if (!mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId, promoId, amount, stockLogId)) {
                    throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败");
                }
                return null;
            }
        });
        try {
            future.get();
        } catch (InterruptedException e) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        } catch (ExecutionException e) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        }
        return CommonReturnType.create(null);
    }
}

限流原理与实现

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

方案:1.限并发 2.令牌桶算法(限制TPS) 3.漏桶算法(平滑网络流量,以固定的速率提供网络服务,无法应对突发流量)

令牌桶算法:

 

每秒钟只发放10(有几个客户端)个令牌,定时器每秒往桶内防10个令牌,对应的客户端1s可以10个对应的流量进去,下一秒就是下一个10个。这样就做到了限制流量在每一个接口都在10TPS内左右的性能。

漏桶算法:

一次只允许一滴水过去,满的时候会拒绝,不能应对突发情况。

限流力度:1.接口维度 2.总维度

接口维度:每个接口设置令牌桶算法

总维度:每个接口*总接口数的80%作为总维度一般

限流范围:1.集群限流 2.单机限流

集群限流:依赖redis或其他的中间件技术做统一计数器,往往会产生性能瓶颈

单机限流:负载均衡的前提下单机平均限流效果更好

代码:

/**
 * @author aric
 * @create 2021-07-02-18:41
 * @fun
 */
@Controller("order")
@RequestMapping("/user")
@CrossOrigin(origins = {"*"}, allowedHeaders = "true")
public class orderController extends BaseController {    

    private RateLimiter orderCreateRateLimiter;

    @PostConstruct
    public void init(){
        orderCreateRateLimiter = RateLimiter.create(300);  //初始化限流,单台机器1s钟过300个(取决每秒压测TPS数)
    }

    //封装下单请求
    @RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId,
                                        @RequestParam(name = "promoId", required = false) Integer promoId,
                                        @RequestParam(name = "amount") Integer amount,
                                        @RequestParam(name = "promoToken") String promoToken) throws BusinessException {
        if(orderCreateRateLimiter.tryAcquire()){  //底层实现类似于令牌桶算法,但是可以预支下一个时间的请求,比较超前
            throw new BusinessException(EmBusinessError.RATELIMIT);
        }
    }
}

防黄牛技术

防刷技术:排队,限流令牌均只能控制总流量,无法控制黄牛流量。

传统防刷:

1.限制一个会话(session_id,token)同一秒钟/分钟接口调用多少次。缺点:多会话介入绕开无效

2.限制一个ip同一秒钟/分钟接口调用多少次。缺点:数量不好控制,容易误伤。

黄牛为什么难防:

1.模拟器作弊:模拟硬件设备,可修改设备信息

2.设备牧场作弊:工作室里一批移动设备

3.人工作弊:靠佣金吸引兼职人员刷单

设别指纹:

1.采集终端设备各项参数,启动应用时生成唯一设备指纹

2.根据对应设备指纹的参数猜测出模拟器等可疑设备概率。

3.根据设备指纹下发凭证

4.关键业务链路上带上凭证并由业务系统到凭证服务器上验证

5.凭证服务器根据对应凭证所等价的设备指纹参数并根据实时行为风控系统判定对应凭证的可疑度分数

6.若分数低于某个数值则由业务系统返回固定错误码,拉起前端验证码验身,验身成功后加入凭证服务器对应分数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值