Build a spike project and optimize

Article Directory

1. Basic project construction

gitee address : https://gitee.com/wlby/seckill

Import the SQL file after cloning the project

There are mainly five tables

  • t_goods saved a list of all goods
  • t_order save order information
  • t_seckill_goods lists the seckill goods as a new list, because the products will have various discounts. If it is not easy to create a new field in the product table, or the seckill channel and the non-seckill channel may be opened at the same time, creating a new one is good for maintenance Spike goods
  • t_seckill_order stores the seckill order
  • t_user user table

And insert some data for testing

/*
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');


Change the configuration of application.yml Redis and RabbitMQ to your own configuration and you should be able to start!

Can visit localhost:8080/user/toLogin

Account number: 18012345678

Password: 123456

2. Project optimization

1. Front: JMeter pressure measurement method

  1. Enter UserUtil under the util package and change the getConn() method to your own database connection
  2. Start the SpringBoot project first, and then run the main method of UserUtil, a config.txt file will be created, and many user users will be created in the database.
  3. Then configure the following to perform pressure test

Configure thread group

Insert picture description here


Configure Http request address

Insert picture description here

Select the generated config.txt to configure user information as shown in the figure

Insert picture description here

Configure cookie manager

Insert picture description here

Configure spike address

Insert picture description here

2. Solve oversold

2.1 Unique index

Insert picture description here

Set user_id and goods_id as a unique index to prevent repeated purchases by the same user

2.2 Redis pre-reduction

        // redis 预减库存
        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
        if (stock < 0) {
            //将该商品置为true
            emptyStockMap.put(goodsId, true);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
  • Using redis pre-decrement, if the inventory is less than 0, the memory flag will be set to true and return directly, there will be no subsequent order operations

2.3 Add judgment conditions when reducing database inventory

  • Reduce inventory if it is greater than 0
        boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
                .setSql("stock_count = stock_count - 1")
                .eq("goods_id", goods.getId())
                .gt("stock_count", 0));

3. Virtual machine optimization

Using JDK8 default parameters, the QPS of 5000 threads and 10 groups is about 1500, which will cause four to five Full GCs

Because most objects are killed in seconds, the object life cycle is short, and the old space is suddenly full and fullGC is observed through visual VM, so adjust the size of the young generation and use the CMS collector and the G1 collector respectively. The QPS is around 2200, no Generate full GC, maybe because my memory space is relatively small and the amount of concurrency is not large, so G1 does not have an overwhelming advantage over CMS

-server
-Xmx3g
-Xms3g
-Xmn2g
-Xss500k
-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
-Xss500k
-XX:+UseG1GC
-XX:LargePageSizeInBytes=64m
-XX:MetaspaceSize=2048m
-XX:MaxMetaspaceSize=2048m
-XX:+UseFastAccessorMethods
-Dfile.encoding=UTF8
-Duser.timezone=GMT+08

4. Tomcat optimization

1. Configure some parameters in application.yml, for example

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

Using these parameters can increase the maximum number of threads that tomcat can use, and increase the number of concurrent support

  • accept-count: the length of the task queue, you can accept more tasks (not infinitely long, the queue will also consume cpu and the accumulation of tasks may cause out of memory)
  • threads.max: The maximum number of working threads, when the task queue is full, create emergency thread work (4 core cpu 8G memory 800-1000 is appropriate, otherwise it will spend a huge time on cpu scheduling)
  • min-spare: minimum worker thread, initial worker thread, increase slowly when demand cannot be met

2. Programmatically customize the embedded tomcat

Use to initiate a keepAlive request and use a long connection to reduce the consumption of handshake and wave

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. Cache optimization

5.1 Merchandise page caching

Cache the product list and product information to redis, and query the database if it is not available. QPS improved

You can also use the three-level cache, use the guava package to store the hotspot data in the local cache, if you don’t have it, go to redis, and if you haven’t, you can query the database.

  /**
     * 跳转商品列表
     *
     * 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 spike cache and spike logic

  • Let this class implement the InitializingBean interface, rewrite the afterPropertiesSet method, and perform operations after the bean initialization property assignment, that is, load the product spike inventory into redis when the system is initialized
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);
        });

    }
  • Add a memory tag emptyStockMap, use ConcurrentHashMap that supports concurrency, if it has been robbed, set the value of the product ID to true, and there is no need to access redis.
  • After that, it is judged whether it is the same user repeating the snap-up purchase. After each snap-up order is generated, a piece of order data will be generated in redis for judgment
  • If there is no problem with the above, the redis product inventory is pre-decremented. Using decrement is an atomic operation. After the decrease, if it is not less than 0, the pre-decrement is successful and sends a message like a message queue. If the inventory is less than 0, it fails and will Memory flag is set to 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. Asynchronous processing of orders

  • As above, if the redis pre-decrement is successful, the message will be sent to the message queue
  • The listener message queue consumer receives the message and performs some database operations such as slow order generation
  • After sending the message, the server immediately returns the result to reduce the pressure on the server, and then the client calls the getResult method to obtain the result through polling
    @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 Ensure that the message is not lost

  • Register a callback event when sending a message to MQ, if ack is false, the task fails or the delivery is not successful, the inventory is added by one
    /**
     * 发送秒杀信息
     * @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);
    }
  • Consumers ensure that messages are not lost
  • By canceling automatic ack, manual ack is used, if there is an exception, an error ack is returned, triggering the logic in the callback

    @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. Interface anti-brush

  • Verify the verification code first
  • Then use first to obtain the spike path in redis, and then to splice the spike path to perform the spike
    @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());
        }
    }
  • In addition, I also used custom annotations to reduce a large number of requests for crazy clicks. second represents the number of seconds, and maxCount represents the maximum number of requests that can be made under that number of seconds.
  • Specifically, you can see my source code implementation, and then use the interceptor AccessLimitInterceptor for processing logic
@AccessLimit(second = 5, maxCount = 5, needLogin = true)
  • The same can also be solved by using a token bucket

Four, some other unrealized problems

1. Distributed transaction

2. Redis distributed lock

3. Bloom filter