搭建一个秒杀项目以及优化

一、基础项目搭建

gitee 地址https://gitee.com/wlby/seckill

克隆项目之后导入SQL文件

主要有五张表

  • t_goods 保存了所有商品列表
  • t_order 保存订单信息
  • t_seckill_goods 将秒杀的商品列为新的一张表,因为商品会有各种优惠活动,如果在商品表新建字段不好维护,或者秒杀渠道和不秒杀渠道可能同时开启,所以新建一张有利于维护秒杀商品
  • t_seckill_order 存储秒杀订单
  • t_user 用户表

并且插入一些数据用于测试

/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50536
Source Host           : localhost:3306
Source Database       : seckill

Target Server Type    : MYSQL
Target Server Version : 50536
File Encoding         : 65001

Date: 2021-05-31 17:02:53
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `t_goods`
-- ----------------------------
DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `goods_name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  `goods_title` varchar(255) DEFAULT NULL COMMENT '商品标题',
  `goods_img` varchar(255) DEFAULT NULL COMMENT '商品图片',
  `goods_detail` varchar(255) DEFAULT NULL COMMENT '商品详情',
  `goods_price` decimal(10,2) DEFAULT NULL COMMENT '商品价格',
  `goods_stock` int(11) DEFAULT NULL COMMENT '商品库存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Records of t_goods
-- ----------------------------
INSERT INTO `t_goods` VALUES ('1', 'IPHONE 12 64GB', 'IPHONE 12 64GB', '/img/iphone12.png', 'IPHONE12 销量秒杀', '6299.00', '100');
INSERT INTO `t_goods` VALUES ('2', 'IPHONE12 PRO 128GB', 'IPHONE12 PRO 128GB', '/img/iphone12pro.png', 'IPHONE12PRO限量,限时秒杀,先到先得', '9299.00', '100');

-- ----------------------------
-- Table structure for `t_order`
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户ID',
  `goods_id` bigint(11) DEFAULT NULL COMMENT '商品ID',
  `deliver_addr_id` bigint(11) DEFAULT NULL COMMENT '收获地址ID',
  `goods_name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  `goods_count` int(11) DEFAULT NULL COMMENT '商品数量',
  `goods_price` decimal(10,2) DEFAULT NULL COMMENT '商品单价',
  `order_channel` int(11) DEFAULT NULL COMMENT '设备信息',
  `status` int(11) DEFAULT NULL COMMENT '订单状态',
  `create_date` datetime DEFAULT NULL COMMENT '订单创建时间',
  `pay_date` datetime DEFAULT NULL COMMENT '支付时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1548 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Records of t_order
-- ----------------------------

-- ----------------------------
-- Table structure for `t_seckill_goods`
-- ----------------------------
DROP TABLE IF EXISTS `t_seckill_goods`;
CREATE TABLE `t_seckill_goods` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
  `goods_id` bigint(11) DEFAULT NULL COMMENT '商品ID',
  `seckill_price` decimal(10,2) DEFAULT NULL COMMENT '秒杀价',
  `stock_count` int(11) DEFAULT NULL COMMENT '库存数量',
  `start_date` datetime DEFAULT NULL COMMENT '秒杀开始时间',
  `end_date` datetime DEFAULT NULL COMMENT '秒杀结束时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Records of t_seckill_goods
-- ----------------------------
INSERT INTO `t_seckill_goods` VALUES ('1', '1', '629.00', '10', '2021-05-25 22:47:47', '2021-06-06 21:29:57');
INSERT INTO `t_seckill_goods` VALUES ('2', '2', '929.00', '10', '2021-05-25 21:30:14', '2021-06-05 21:30:17');

-- ----------------------------
-- Table structure for `t_seckill_order`
-- ----------------------------
DROP TABLE IF EXISTS `t_seckill_order`;
CREATE TABLE `t_seckill_order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户ID',
  `order_id` bigint(11) DEFAULT NULL COMMENT '订单ID',
  `goods_id` bigint(11) DEFAULT NULL COMMENT '商品ID',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `seckill_uid_gid` (`user_id`,`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1547 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

-- ----------------------------
-- Records of t_seckill_order
-- ----------------------------

-- ----------------------------
-- Table structure for `t_user`
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL,
  `nickname` varchar(255) NOT NULL,
  `password` varchar(32) DEFAULT NULL,
  `slat` varchar(10) DEFAULT NULL,
  `head` varchar(128) DEFAULT NULL,
  `register_date` datetime DEFAULT NULL,
  `last_login_date` datetime DEFAULT NULL,
  `login_count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `t_user` VALUES ('18012345678', 'admin', 'b7797cce01b4b131b433b6acf4add449', '1a2b3c4d', null, null, null, '0');


将application.yml Redis 和 RabbitMQ的配置改为自己的配置应该就可以启动了!

可以访问 localhost:8080/user/toLogin

账号:18012345678

密码:123456

二、项目优化

1. 前置:JMeter 压测方法

  1. 进入 util 包下的UserUtil中将getConn() 方法中改为自己的数据库连接
  2. 先启动SpringBoot项目,再运行UserUtil的main方法,会在创建一个config.txt文件,并且创建很多个user用户在数据库。
  3. 再进行如下配置,即可进行压测

配置线程组

在这里插入图片描述
配置Http请求地址

在这里插入图片描述

选中生成的config.txt如图配置用户信息

在这里插入图片描述

配置cookie的管理器

在这里插入图片描述

配置秒杀地址

在这里插入图片描述

2. 解决超卖

2.1 唯一索引

在这里插入图片描述

将user_id 和 goods_id 设置为唯一索引,防止同一个用户重复抢购

2.2 Redis 预减

        // redis 预减库存
        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
        if (stock < 0) {
            //将该商品置为true
            emptyStockMap.put(goodsId, true);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
  • 利用redis预减,如果库存小于0了就会将内存标记设置为true,并且直接返回,不会有后续下单操作

2.3 减少数据库库存的时候加上判断条件

  • 如果大于0才减少库存
        boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
                .setSql("stock_count = stock_count - 1")
                .eq("goods_id", goods.getId())
                .gt("stock_count", 0));

3. 虚拟机优化

使用JDK8默认参数,5000个线程10组 QPS在1500左右,会引发四到五次Full GC

因为秒杀大多数对象朝生夕死,对象生命周期短,并且通过visual VM观察老年代空间都是突然爆满引发fullGC,所以调整年轻代大小,分别使用CMS 收集器和 G1 收集器QPS在2200左右,没有产生full GC,可能由于我的内存空间比较小并且并发量不大,所以G1对于CMS并没有压倒性的优势

-server
-Xmx3g	# 堆最大内存
-Xms3g	# 堆初始内存
-Xmn2g	# 年轻代大小
-Xss128k	# 每个线程堆栈大小
-XX:MetaspaceSize=2048m
-XX:MaxMetaspaceSize=2048m
-XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:LargePageSizeInBytes=64m
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-Dfile.encoding=UTF8
-Duser.timezone=GMT+08
-server
-Xmx3g 
-Xms3g
-Xmn2g
-Xss128k
-XX:+UseG1GC
-XX:HandlePromotionFailure=false
-XX:LargePageSizeInBytes=64m
-XX:MetaspaceSize=2048m
-XX:MaxMetaspaceSize=2048m
-XX:+UseFastAccessorMethods
-Dfile.encoding=UTF8
-Duser.timezone=GMT+08

4. Tomcat优化

1. application.yml 中配置一些参数,例如

server:
  tomcat:
    accept-count: 1000 # 等待队列长度
    threads:
      max: 800 #最大工作线程数
      min-spare: 100 #最小工作线程数

利用这些参数可以提高tomcat可以使用的最大线程数,提高支持的并发数

  • accept-count : 任务队列的长度,可以接受更多的任务(不能无限长,出入队列也会耗费cpu并且,任务堆积有可能造成out of memory)
  • threads.max : 最大工作线程数,当任务队列满后,创建救急线程工作(4核cpu 8G 内存 800 - 1000合适,否则将花费巨大的时间在cpu调度上)
  • min-spare : 最小工作线程,初始的工作线程,当无法满足需求再慢慢增加

2. 通过编程式定制内嵌tomcat

使用发起keepAlive请求,使用长连接,减少握手挥手的消耗

import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;

/**
 * @Description:
 * @Author: Aiguodala
 * @CreateDate: 2021/5/28 13:38
 */
@Configuration
public class WebServerConfig implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ((TomcatServletWebServerFactory)factory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
                // 设置三十秒没有请求则自动断开keepAlive
                protocol.setKeepAliveTimeout(30000);
                // 设置超过10000个请求就断开keepAlive
                protocol.setMaxKeepAliveRequests(10000);
            }
        });
    }
}

5. 缓存优化

5.1 商品页面缓存

将商品列表以及商品信息缓存至redis,如果获取不到再到数据库查询。QPS提升较大

也可以使用三级缓存,利用guava包的将热点数据存入到本地缓存,如果没有再去redis中取,如果还没有则去查询数据库。

  /**
     * 跳转商品列表
     *
     * windows 优化前 5000个线程 10 组 QPS : 1360.2
     * windows 缓存优化后 5000个线程 10 组 QPS : 6037
     *
     * @param model
     * @param user
     * @return
     */
    @RequestMapping(value = "/toList", produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toList(Model model,User user, HttpServletRequest request, HttpServletResponse response) {
        ValueOperations operations = redisTemplate.opsForValue();
        String html = (String) operations.get("goodsList");
        if (!StringUtils.isEmpty(html)) {
            return html;
        }
        model.addAttribute("user", user);
        List<GoodsVo> goodsList = goodsService.listGoodsVo();
        model.addAttribute("goodsList", goodsList);

        WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
        html = thymeleafViewResolver.getTemplateEngine().process("goodsList", context);
        if (!StringUtils.isEmpty(html)) {
            operations.set("goodsList", html, 60, TimeUnit.SECONDS);
        }
        return html;
    }

    /**
     * 商品详情
     * @param model
     * @param user
     * @param goodsId
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/toDetail/{goodsId}", produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toDetail(Model model, User user, @PathVariable(value = "goodsId") Long goodsId
            , HttpServletRequest request, HttpServletResponse response) {

        ValueOperations operations = redisTemplate.opsForValue();
        String html = (String) operations.get("goodsDetail:" + goodsId);
        if (!StringUtils.isEmpty(html)) {
            return html;
        }
        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        Date startDate = goodsVo.getStartDate();
        Date endDate = goodsVo.getEndDate();
        Date nowDate = new Date();
        //秒杀状态
        int secKillStatus = 0;
        //秒杀倒计时
        int remainSeconds = 0;
        //秒杀还未开始
        if (nowDate.before(startDate)) {
            remainSeconds = ((int) ((startDate.getTime() - nowDate.getTime()) / 1000));
        } else if (nowDate.after(endDate)) {
            //	秒杀已结束
            secKillStatus = 2;
            remainSeconds = -1;
        } else {
            //秒杀中
            secKillStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("remainSeconds", remainSeconds);
        model.addAttribute("secKillStatus", secKillStatus);
        model.addAttribute("goods", goodsVo);

        WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
        html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", context);
        if (!StringUtils.isEmpty(html)) {
            operations.set("goodsDetail:" + goodsId, html, 60, TimeUnit.SECONDS);
        }
        return html;
    }

5.2 秒杀缓存以及秒杀逻辑

  • 让该类实现InitializingBean 接口,重写afterPropertiesSet方法,在该bean初始化属性赋值之后进行操作,也就是系统初始化的时候将商品秒杀库存加载到redis中
public class SeckillGoodsController implements InitializingBean {
	. . . 

    /**
     * 系统初始化的时候将商品库存数量加载到redis
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        List<GoodsVo> goodsVos = goodsService.listGoodsVo();
        if (CollectionUtils.isEmpty(goodsVos)) {
            return;
        }
        goodsVos.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            emptyStockMap.put(goodsVo.getId(), false);
        });

    }
  • 添加一个内存标记emptyStockMap ,使用支持并发的ConcurrentHashMap,如果已经被抢完了,就给该商品ID的value置为true,则无需再访问redis。
  • 之后判断是否是同一个用户重复抢购,每次抢购生成订单以后会在redis中生成一条订单数据用来判断
  • 如果以上均没有问题,则对redis该商品库存进行预减,使用decrement是原子操作,减少之后如果不小于0,则预减成功,则像消息队列发送消息,如果库存小于0则失败,并且将内存标记置为true


    /**
     * 判断库存是否已经是空
     */
    private Map<Long, Boolean> emptyStockMap = new ConcurrentHashMap<>();
  

    @PostMapping(value = "/doSeckill")
    @ResponseBody
    public RespBean doSeckill(User user, Long goodsId) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        ValueOperations valueOperations = redisTemplate.opsForValue();
        // 通过内存标记,如果已经被抢购空了则无需访问redis
        if (emptyStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        // 判断是否重复抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (seckillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }
        // redis 预减库存
        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
        if (stock < 0) {
            //将该商品置为true
            emptyStockMap.put(goodsId, true);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
        mqProvider.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
        return RespBean.success(0);
    }

6. 异步处理订单

  • 如上如果redis预减成功,则将消息发送到消息队列
  • 监听消息队列消费者则接受到消息,进行一些缓慢的生成订单等数据库操作
  • 发送消息之后服务器马上返回结果,减轻服务器压力,之后客户端再通过轮询调用getResult方法获取结果
    @RabbitListener(queues = "seckillQueue")
    public void receive(String message) {
        log.info("接受消息" + message);

        SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
        User user = seckillMessage.getUser();
        Long goodsId = seckillMessage.getGoodsId();
        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        if (goodsVo.getStockCount() < 1) {
            return;
        }
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsVo.getId());

        if (seckillOrder != null) {
            return;
        }
        orderService.seckill(user, goodsVo);
    }

6.1 确保消息不丢失

  • 发送消息给MQ的时候注册一个回调事件,如果ack为false 既任务失败或者没有发送成功则将库存加上一
    /**
     * 发送秒杀信息
     * @param message
     */
    public void sendSeckillMessage(String message) {
        // 注册回调,如果发送失败,将库存加1
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (!ack) {
                    SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
                    redisTemplate.opsForValue().increment("seckillGoods:" + seckillMessage.getGoodsId());
                }
            }
        });
        log.info("发送信息" + message);
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);
    }
  • 消费者确保消息不丢失
  • 通过取消自动的ack,采用手动的ack,如果有异常则返回错误ack,触发回调中的逻辑

    @RabbitListener(queues = "seckillQueue")
    public void receive(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        try {
            SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
            User user = seckillMessage.getUser();
            Long goodsId = seckillMessage.getGoodsId();
            GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
            if (goodsVo.getStockCount() < 1) {
                return;
            }
            SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsVo.getId());

            if (seckillOrder != null) {
                return;
            }
            orderService.seckill(user, goodsVo);
            /**
             * 无异常就确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:取出来当前消息在队列中的的索引;
             * multiple:为true的话就是批量确认
             */
            channel.basicAck(tag, false);
        }catch (Exception e) {
            /**
             * 有异常就绝收消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * requeue:true为将消息重返当前消息队列,还可以重新发送给消费者;
             *         false:将消息丢弃
             */
            try {
                channel.basicNack(tag,false,true);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }

        }

7. 接口防刷

  • 先校验验证码
  • 再采用先在redis中获取秒杀路径,再通过拼接秒杀路径进行秒杀
    @PostMapping(value = "/{path}/doSeckill")
    @ResponseBody
    public RespBean doSeckill(@PathVariable String path, User user, Long goodsId) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        ValueOperations valueOperations = redisTemplate.opsForValue();
        boolean check = orderService.checkPath(user, goodsId, path);
        if (!check) {
            return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
        }
        // 通过内存标记,如果已经被抢购空了则无需访问redis
        if (emptyStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        // 判断是否重复抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (seckillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }
        // redis 预减库存
        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);

/*       Long stock = (Long) redisTemplate.execute(script, Collections.singletonList("seckillGoods:" + goodsId),
                Collections.EMPTY_LIST);*/
        if (stock < 0) {
            //将该商品置为true
            emptyStockMap.put(goodsId, true);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
        mqProvider.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
        return RespBean.success(0);
    }

    @AccessLimit(second = 5, maxCount = 5, needLogin = true)
    @RequestMapping(value = "/path", method = RequestMethod.GET)
    @ResponseBody
    public RespBean getPath(User user, Long goodsId, String captcha, HttpServletRequest request) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        boolean check = orderService.checkCaptcha(user, goodsId, captcha);
        if (!check) {
            return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
        }
        String str = orderService.createPath(user, goodsId);
        return RespBean.success(str);
    }

    @GetMapping(value = "/captcha")
    public void verifyCode(User user, Long goodsId, HttpServletResponse response) {
        if (user == null || goodsId < 0) {
            throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
        }
        //设置请求头为输出图片的类型
        response.setContentType("image/jpg");
        response.setHeader("Pargam", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        //生成验证码,将结果放入Redis
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
        redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha.text(), 300,
                TimeUnit.SECONDS);
        try {
            captcha.out(response.getOutputStream());
        } catch (IOException e) {
            log.error("验证码生成失败", e.getMessage());
        }
    }
  • 另外我还通过自定义注解,来减少疯狂点击的大量请求, second 表示秒数,maxCount表示在该秒数下最多能进行几次请求
  • 具体可以看我的源码实现,再通过拦截器AccessLimitInterceptor进行处理逻辑
@AccessLimit(second = 5, maxCount = 5, needLogin = true)
  • 同样也可以采用令牌桶的方式解决
public class TokenBucket {

    /**
     * 默认令牌个数,也就是桶大小
     * 流量为 64 MB
     */
    private static final int DEFAULT_BUCKET_SIZE = 1064 * 1064 * 64;

    private static final byte TOKEN_BYTE = 'a';

    /**
     * 存放令牌的桶
     */
    private ArrayBlockingQueue<Byte> tokenBucket;

    /**
     * 添加令牌的定时线程池
     */
    private ScheduledExecutorService scheduledExecutorService;

    /**
     * 全局锁
     */
    private ReentrantLock lock = new ReentrantLock();


    public TokenBucket() {
        tokenBucket = new ArrayBlockingQueue<>(DEFAULT_BUCKET_SIZE);
        scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            addTokens(1000);
        }, 0, 1, TimeUnit.SECONDS);
    }

    /**
     * 添加令牌
     * @param tokenNum
     */
    public void addTokens(Integer tokenNum) {
        for (int i = 0; i < tokenNum; i++) {
            tokenBucket.offer(Byte.valueOf(TOKEN_BYTE));
        }
    }

    /**
     * 获取令牌
     * @param data
     * @return
     */
    public boolean getTokens(byte[] data) {
        boolean res = data.length <= tokenBucket.size();
        if (!res) {
            return false;
        }else {

            /**
             * 这种写法主要有两种好处
             *  (1)将全局变量读入到局部变量表里,读取速度更快,因为取数据只需要一条指令,而对于全局变量需要两条
             *  (2)加 final 保证线程安全,这点我也没理解。
             */
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                for (int i = 0; i < data.length; i++) {
                    tokenBucket.poll();
                }
                return true;
            }finally {
                lock.unlock();
            }
        }
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值