Sentinel + Redis + Mysql + RabbitMQ spike function design and back-end code implementation

foreword

When developing the seckill system functions, the following points need to be considered but not limited to:
  1. Ensure data consistency
  2. Ensure system high performance
  3. Handle high concurrency scenarios
In fact, for different seckill business scenarios, there are different solutions to the problems that need to be considered.

data consistency

  The data consistency of the seckill system, on the one hand, is reflected in the calculation of the inventory quantity. We must not only ensure that the products are sold out as much as possible, but also ensure that the final order quantity generated cannot exceed the preset inventory value, otherwise it will be oversold. This is also the most basic requirement of our entire seckill service.
  In order to prevent oversold, we can query the remaining inventory of the current flash sale product before each order is generated. Insufficient inventory does not allow orders to be generated.
  On the other hand, when we are deducting inventory, different deduction schemes will also affect the actual performance of the entire seckill function. Under normal circumstances, purchasing goods is divided into two steps: placing an order and paying. There are roughly three types of deduction schemes.

  • Deducting inventory after placing an order: Deducting inventory immediately after placing an order will minimize the pressure on the server, because there are at most only a limited number of users who have successfully placed an order. But this model has a drawback, because we cannot guarantee that users who place orders successfully will pay in the end, so some users may not pay all the time, but they still occupy resources, resulting in the failure to sell all the products.
  • Deduct inventory after payment: As long as the user does not pay, the inventory will always be there, and it will ensure that all the goods can be sold. However, this model also has a disadvantage. If the number of payment orders does not reach the upper limit of inventory, all order requests will be successful, which will lead to a surge in the information that the order was successfully placed at a certain point. Then, when most users pay, due to insufficient inventory, the payment cannot be successful, which will bring a bad experience to users.
  • Pre-deduction of inventory after placing an order: That is, the order information is kept for a period of time after the order is placed. If there is no payment, the inventory will be released and the order information will be invalidated. Although it does not solve any substantive problems, it is a compromise between the above two situations. The advantages and disadvantages of the two are neutralized. At present, most online shopping orders are used in this way.

The scheme of deducting inventory after payment used in the following DEMO is a spike system, and it is normal not to be able to grab it.

high performance

static and dynamic separation

  Generally speaking, the page content of the seckill system will not change. We don't have to request many back-end interfaces every time we refresh the page.
  So we need to figure out which information can be fixed and which information must be provided by the background. For example, for a flash sale of a product, the price, product introduction, and discount information of this product will generally not change, so we can directly set this information as hard-coded data on the page to reduce the number of requests for back-end services.

static resource caching

  It is recommended to use a CDN (Content Delivery Network), which copies data from the source server to other servers. When the user visits, the CDN will select an optimal server according to its own load balancing, and then the user will access the content on this optimal server. If there is no target resource on the server, it will go back to the source (obtain information from the source server).
  Because I don't know much about the principle of CDN, I only know that it can improve the access speed of static pages, so I won't make too many explanations here.

Flow Control

  In order to alleviate the pressure on the server due to the huge amount of traffic at the moment of flash sales, we can properly screen out some requests before processing the requests, that is, perform flow control downgrade. For example, if 10,000 requests hit the server at the same time within one second, we only allow 1,000 of them to enter the real business logic, and the remaining requests will return a downgrade response. Generally speaking, this downgrade response directly returns a business response failure, or returns a reminder such as "Service is busy, please try again".
  Sentinel provides such a function. Sentinel allows us to set flow control rules for the specified interface. We can set the threshold through QPS or the number of concurrent threads. Using QPS can control how many effective responses are made per second, while using the number of concurrent threads will control the number of system threads not to exceed the preset value.
   insert image description here

cache database

Improving system performance will inevitably require the help of a cache database. Redis   , one of the most commonly used cache databases, is very suitable for this kind of spike scenario. The multiplexing technology in Redis and the design of operating data on memory have created Redis 's high throughput, enabling it to respond to more queries in a short time.

message queue

  In business development, there are some logics that we often do not need to complete in time. We only need to focus on the core business, and some logics can be postponed.
  We may consider using multi-threading, but using multi-threading may have the following problems :
  1. The thread is still executed in the current process. Although the response speed of the interface is accelerated, the negative pressure on the server is still the same, and it does not actually reduce the pressure on the server
.
  2. If there are operations on the persistent database in the logic, a large number of write operations may flood into the database at a certain moment when using multi-threading. The concurrency capability of the database is relatively weak, and too many write requests may cause the database to go down.
So we can consider message queues, here we use RabbitMQ.

Advantages of RabbitMQ

  • Asynchronous execution : It can improve the response speed of the interface, and different process services can consume messages to process the remaining work, reducing the pressure on the current server.
  • Peak shaving and valley filling : For example, if the maximum message processing volume per second is set to 1000, when there are too many requests, it will be controlled to process 1000 messages per second, and the rest of the messages will be backlogged in the RabbitMQ queue , which is peak shaving. After the peak period, the request volume drops, but at this time, due to the news accumulated during the peak period, it can still provide a certain amount of news in a short period of time, which is valley filling. This can relieve the pressure on the database during peak request periods and avoid database crashes.

High concurrency

distributed lock

  In a microservice project, multiple service instances are generally provided. When one of the services modifies the remaining value of the inventory, no other service process is allowed to read or write the value. At this time, we often need to use distributed locks to deal with it.
  Because we use Redis as a cache database, we can use Redis to implement distributed locks. Redission provides convenient methods to operate Redis and acquire distributed locks.

Backend code implementation

What is implemented here is a simple seckill system, which uses a plan to deduct inventory after payment to ensure that the goods are sold as much as possible, and each person is limited to only one purchase.
The DEMO function is not perfect enough, the following code is not complete code, the complete code link is attached at the end.

middleware

  • Mysql : In any case, our data needs to be stored in the database eventually, and a persistent database is a must.
  • Reids : As a cache server, the basic information required for seckill is stored in Redis.
  • RabbitMQ : The operation of the persistent database and other unimportant logic processing in the logic will affect the response speed of the interface, so RabbitMQ is used to notify other service process operations.
  • Timing task : It is used to store the seckill information we set into Redis. Timing task frameworks such as elastic-job, xxl-job, etc. can be used. Here, the timing task framework that comes with springframework is used.

Table Structure

-- 秒杀信息表
CREATE TABLE `seckill` (
  `id` int NOT NULL AUTO_INCREMENT,
  `product_id` int DEFAULT NULL COMMENT '商品ID',
  `count` int DEFAULT '0' COMMENT '秒杀库存',
  `seckill_price` decimal(10,2) DEFAULT NULL COMMENT '秒杀价格',
  `start_time` datetime DEFAULT NULL COMMENT '秒杀开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '秒杀结束时间',
  `is_cached` tinyint DEFAULT '0' COMMENT '是否放入Redis缓存 0 否 1 是',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `UK_PRODUCT_CODE` (`product_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 订单表
CREATE TABLE `seckill_orders` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
  `order_no` varchar(32) DEFAULT NULL COMMENT '订单号',
  `account_id` int DEFAULT NULL COMMENT '账户ID',
  `seckill_id` int DEFAULT NULL COMMENT '秒杀ID',
  `count` int DEFAULT NULL COMMENT '秒杀数量',
  `payment_amount` decimal(10,0) DEFAULT NULL COMMENT '应付金额',
  `checkout_time` datetime DEFAULT NULL COMMENT '下单时间',
  `status` tinyint DEFAULT '0' COMMENT '状态 0待支付 1已支付 2已取消',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=176 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

add dependencies

Only part of the dependencies are placed here, and there are more information about all dependencies. You can check the final source code link. The
configuration files are relatively basic stand-alone configurations, and some even have default configurations such as RabbitMQ, so I won’t post them here.

<!-- Sentinel 配置中心 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.0.4.0</version>
</dependency>
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.6</version>
</dependency>
<!-- Redission -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.9.1</version>
</dependency>
<!-- Mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<!-- MybatisPlus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.1</version>
</dependency>
<dependency>
	 <groupId>com.baomidou</groupId>
	 <artifactId>mybatis-plus-core</artifactId>
	 <version>3.1.1</version>
	 <scope>compile</scope>
</dependency>
<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>1.5.2.RELEASE</version>
</dependency>

public constant

It is mainly used to query Redis information or obtain the prefix of distributed lock time, and can be placed in the public constant class.

public class CommonConst {
    
    
    public static String SECKILL_LOCK_USER = "SECKILL_USER_LOCK:";//用户个人锁
    public static String SECKILL_LOCK_GLOBAL = "SECKILL_GLOBAL_LOCK:";//全局锁
    public static String SECKILL_START_TIMESTEMP = "SECKILL_START_TIMESTEMP:";//秒杀开始时间
    public static String SECKILL_STOP_TIMESTEMP = "SECKILL_STOP_TIMESTEMP:";//秒杀结束时间
    public static String SECKILL_REMAIN_COUNT = "SECKILL_REMAIN_COUNT:";//秒杀商品剩余数量
    public static String SECKILL_ORDER_USERS = "SECKILL_ORDER_USERS:";//下单成功用户列表
    public static String SECKILL_SUCCEED_USERS = "SECKILL_SUCCEED_USERS:";//付款成功用户列表
}

Entity class

@Data
public class Seckill {
    
    
	//对应数据库表 seckill 字段
}
@Data
public class SeckillOrders implements Serializable {
    
    
	//对应数据库表 seckill_orders 字段
}

Redission configuration

Redisssion is used here to operate Redis because it provides simple lock operations.

@Configuration
public class RedissonConfig {
    
    
    @Bean
    public RedissonClient redissonClient() {
    
    
        // 配置信息
        Config config = new Config();
        //地址、密码
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");//                .setPassword("pwd");
        return Redisson.create(config);
    }
}

timed task

Here is a simple timed task, executed every five seconds, and put the uncached seckill information into Redis

@Component
@EnableScheduling
public class SeckillTask {
    
    
    @Autowired
    SeckillDao seckillDao;
    @Autowired
    RedissonClient redission;

    /**
     * 5分钟执行一次
     * 查询没放进缓存中的秒杀任务并放入缓存
     */
    @Scheduled(cron="0/5 * * * * ?")
    public void seckillRedisCache() {
    
    
        //将秒杀信息缓存进REDIS
        QueryWrapper<Seckill> ew = new QueryWrapper<>();
        ew.eq("is_cached",0);
        List<Seckill> seckills = seckillDao.selectList(ew);

        if(CollectionUtils.isNotEmpty(seckills)){
    
    
            List<Long> cachedIds = new ArrayList<>();
            for (Seckill seckill : seckills) {
    
    
                RBucket<Integer> count = redission.getBucket(CommonConst.SECKILL_REMAIN_COUNT + seckill.getId());
                //先判断下确实没有缓存过
                if(!count.isExists()){
    
    
                    count.set(seckill.getCount());
                    redission.getBucket(CommonConst.SECKILL_START_TIMESTEMP + seckill.getId()).set(seckill.getStartTime());
                    redission.getBucket(CommonConst.SECKILL_STOP_TIMESTEMP + seckill.getId()).set(seckill.getEndTime());
                }
                cachedIds.add(seckill.getId());
            }

            //修改缓存状态
            if(CollectionUtils.isNotEmpty(cachedIds)) {
    
    
                UpdateWrapper<Seckill> uw = new UpdateWrapper<>();
                uw.setSql("is_cached = 1");
                uw.in("id", cachedIds);
                seckillDao.update(null,uw);
            }
        }
    }
}

Controller

Increase the Sentinel current limit setting, and the limited interface will return a degraded response of "Activity is hot, please try again!".
A few points to note are:
1. Except for the BlockException parameter, the other parameter types of the downgrading method need to be consistent with the parameter types of the flow-controlled interface. 2.
If you set the flow control rules through the Sentinel console, you will find that there are no resources after the program starts. At this time, we only need to call the flow-controlled interface once, and then add flow control rules to it.
Before calling
insert image description here
After calling
insert image description here

@Autowired
private ISeckillService seckillService;

/**
 * 流控降级
 * */
public R<Orders> seckillFallback(Long accountId,Long pid,BlockException ex) {
    
    
    return R.failure("活动火爆,请重新尝试!");
}

//秒杀接口(通过Sentinel限流)
@PostMapping("/seckill/{accountId}/{pid}")
@SentinelResource(value="seckill",blockHandler = "seckillFallback")
public R seckill(@PathVariable("accountId") Long accountId,@PathVariable("pid") Long pid) throws Exception {
    
    
    return seckillService.seckill(accountId,pid);
}

//支付接口
@PostMapping("/killpay/{seckillOrder}")
public R killpay(@PathVariable("seckillOrder") String seckillOrder) throws Exception {
    
    
    return seckillService.killpay(seckillOrder);
}

Order interface

Before placing an order, we need to judge whether the event has started, whether it has ended, whether the snap-up has been successful, whether there is an order record, whether there is still stock, etc.
At the same time, we have to deal with the concurrency of the same user, because too fast requests may be abnormal, and we can try to refuse the continued access of some abnormal requests.

public R seckill(Long accountId,Long kid) throws InterruptedException {
    
    
        //获取活动开始时间
        RBucket<Date> startTime = redission.getBucket(CommonConst.SECKILL_START_TIMESTEMP + kid);
        if(!startTime.isExists() || new Date().compareTo(startTime.get())<0) {
    
    //获取不到表示活动还未开始
            return R.failure("活动未开始!");
        }

        //获取活动结束时间
        RBucket<Date> stopTime = redission.getBucket(CommonConst.SECKILL_STOP_TIMESTEMP + kid);
        if(new Date().compareTo(stopTime.get())>0){
    
    //判断活动是否结束
            return R.failure("活动已结束!");
        }

        //获取用户个人锁,处理同一用户同时多次请求秒杀接口
        //采用自动释放锁的方式,500ms后自动释放,500ms内统一用户的请求视为非正常请求
        RLock lock = redission.getLock(CommonConst.SECKILL_LOCK_USER + kid + ":" + accountId);
        boolean locked = lock.tryLock(0,500,TimeUnit.MILLISECONDS);
        if(locked){
    
    

            //判断是否已经购买成功过
            RBucket<Set> succedUsers = redission.getBucket(CommonConst.SECKILL_SUCCEED_USERS + kid);
            if(succedUsers.isExists() && succedUsers.get().contains(accountId)){
    
    
                return R.failure("抢购次数已用尽!");
            }

            //判断是否有下单记录
            RBucket<Set> checkoutUsers = redission.getBucket(CommonConst.SECKILL_ORDER_USERS + kid);
            if(checkoutUsers.isExists() && checkoutUsers.get().contains(accountId)){
    
    
                return R.failure("已有下单记录,请前往支付!");
            }

			//判断是否还有库存(下单时做初步判断,防止没有库存了仍旧能下单。)
            RAtomicLong count = redission.getAtomicLong(CommonConst.SECKILL_REMAIN_COUNT + kid);
            if(!count.isExists() || count.get()<=0) {
    
    
                return R.failure("已售罄!");
            }

            //写入下单成功的人员列表 操作时要获取锁,避免其他进程读取或者操作
            RLock gwlock = redission.getLock(CommonConst.SECKILL_LOCK_GLOBAL + kid );
            if(gwlock.tryLock(1000, TimeUnit.MILLISECONDS)) {
    
    
                Set<Long> newUsers = new HashSet<>();
                if(checkoutUsers.isExists()){
    
    
                    newUsers = checkoutUsers.get();
                    newUsers.add(accountId);
                }else{
    
    
                    newUsers.add(accountId);
                }
                checkoutUsers.set(newUsers);
                //释放写锁
                gwlock.unlock();

                String secOrder = UUID.randomUUID().toString().replace("-","");//返回订单标志
                //生成下单所需基本信息,例如:账户、秒杀ID,
                SeckillOrders checkout = new SeckillOrders();
                checkout.setOrderNo(secOrder);
                checkout.setAccountId(accountId);
                checkout.setSeckillId(kid);
                checkout.setCount(1);
                checkout.setRabbitMqType(0);
                //放进消息队列中处理 我这里交换器 my-mq-exchange_A 绑定的是队列 QUEUE_A 绑定的路由键是 spring-boot-routingKey_A
                RabbitPublishUtils.sendDirectMessage(RabbitMQExchanges.EXCHANGE_A, RabbitMQRoutingKeys.ROUTINGKEY_A, checkout);

                // 最后返回下单单号供前端刷新界面使用
                return R.success(secOrder,"下单成功");
            }else{
    
    
               return R.failure("活动火爆,请重新尝试!");
            }
        }else{
    
    
            return R.failure("操作频繁!");
        }
    }

payment interface

Primarily involves inventory verification, deduction of inventory and recovery operations.

    public R killpay(String seckillOrder) throws InterruptedException {
    
    
        //订单信息从数据库中查询 防篡改
        QueryWrapper<SeckillOrders> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no",seckillOrder);
        SeckillOrders checkout = ordersDao.selectOne(queryWrapper);
        Long kid = checkout.getSeckillId();

        RBucket<Set> succedUsers = redission.getBucket(SECKILL_SUCCEED_USERS + kid);

        //库存先扣减1 写锁控制下不让其他进程读取和修改库存值
        RLock glock = redission.getLock(CommonConst.SECKILL_LOCK_GLOBAL + kid );
        if (glock.tryLock(1000, TimeUnit.MILLISECONDS)) {
    
    
            Set<Long> newUsers = new HashSet<>();
            if(succedUsers.isExists()){
    
    
                newUsers = succedUsers.get();
                if(newUsers.contains(checkout.getAccountId())){
    
    
                    glock.unlock();
                    return R.failure("请勿重复付款!");
                }
            }

            RAtomicLong count = redission.getAtomicLong(CommonConst.SECKILL_REMAIN_COUNT + kid);
            if (count.isExists() && count.get() > 0) {
    
    
                count.getAndDecrement();
            }else{
    
    
                glock.unlock();
                return R.failure("已售罄!");
            }

            //添加到购买成功的人员列表中
            newUsers.add(checkout.getAccountId());
            succedUsers.set(newUsers);
			//释放锁
            glock.unlock();
        }

        //扣减数据库中的库存交由消息队列中去处理
        checkout.setRabbitMqType(1);
        RabbitPublishUtils.sendDirectMessage(RabbitMQExchanges.EXCHANGE_A, RabbitMQRoutingKeys.ROUTINGKEY_A, checkout);

        try {
    
    
            //TODO 调用支付接口
            //模拟支付失败的操作 用来查看库存恢复是否正常
//            int sd = 0;
//            Object sds = 10/sd;
        }catch (Exception ex){
    
    

            //支付失败 恢复商品数量 可以交由消息队列中去处理
            checkout.setRabbitMqType(2);
            RabbitPublishUtils.sendDirectMessage(RabbitMQExchanges.EXCHANGE_A, RabbitMQRoutingKeys.ROUTINGKEY_A, checkout);
            if (glock.tryLock(1000, TimeUnit.MILLISECONDS)) {
    
    
            	//缓存库存恢复
                RAtomicLong count = redission.getAtomicLong(SECKILL_REMAIN_COUNT + kid);
                count.getAndIncrement();
				//购买成功列表移除当前用户
                Set<Long> newUsers = new HashSet<>();
                if(succedUsers.isExists()){
    
    
                    newUsers = succedUsers.get();
                    newUsers.remove(checkout.getAccountId());
                }
                succedUsers.set(newUsers);
				//释放写锁
                glock.unlock();
            }
            return R.failure("支付失败");
        }

        //下单状态修改 改为已付款
        checkout.setStatus(1);
        ordersDao.updateById(checkout);

        return R.success();
    }

receive channel messages

@Component
public class RabbitMqReceiver {
    
    

    @Autowired
    SeckillOrdersDao seckillCheckoutDao;

    @Autowired
    SeckillDao seckillDao;

    @RabbitListener(bindings = @QueueBinding(value = @Queue(value = "QUEUE_A", durable = "true", ignoreDeclarationExceptions = "true"),
            exchange = @Exchange(value = "my-mq-exchange_A"), key = "spring-boot-routingKey_A", ignoreDeclarationExceptions = "true"))
    public void handleMessage(SeckillOrders checkout){
    
    
        if(null != checkout){
    
    
            switch (checkout.getRabbitMqType()){
    
    
                case 0://生成订单以及其他业务逻辑
                    Seckill seckill = seckillDao.selectById(checkout.getSeckillId());
                    BigDecimal price = seckill.getSeckillPrice();
                    BigDecimal payment = price.multiply(new BigDecimal(checkout.getCount().toString()));
                    checkout.setPaymentAmount(payment);
                    checkout.setCheckoutTime(new Date());
                    //下单信息落库
                    seckillCheckoutDao.insert(checkout);
                    break;
                case 1://扣减库存以及其他业务逻辑
                    UpdateWrapper<Seckill> updateWapper = new UpdateWrapper<>();
                    updateWapper.eq("id",checkout.getSeckillId());
                    updateWapper.setSql("count = count - 1 ");
                    seckillDao.update(null,updateWapper);
                    break;
                case 2://恢复库存以及其它业务逻辑
                    updateWapper = new UpdateWrapper<>();
                    updateWapper.eq("id",checkout.getSeckillId());
                    updateWapper.setSql("count = count + 1 ");
                    seckillDao.update(null,updateWapper);
                    break;
                default:
                    break;
            }
        }
    }
}

full code

For the complete code, please refer to
the seckill function Demo

Guess you like

Origin blog.csdn.net/qq_40096897/article/details/128487251