秒杀项目系列之七: 交易性能优化技术之缓存库存(库存放在redis+使用rocketmq异步写入数据库)

  1. 下单请求压测
  • 创建测试计划
    在这里插入图片描述
  • 线程属性: 线程数200, ramp-up时间:10, 循环次数:20
  • 压测结果: 平均值3617ms,95值4640ms,TPS吞吐量47.4/sec
    在这里插入图片描述
  1. 交易性能瓶颈分析
  • 交易验证完全依赖于数据库

  • 库存行锁(修改库存时对item_id值所在行加行锁)

    <!--  库存扣减操作-->
      <update id="decreaseStock">
        update item_stock
        set stock = stock - #{amount,jdbcType=INTEGER}
        where item_id = #{itemId,jdbcType=INTEGER} and stock >= #{amount}
      </update>
    </mapper>
    
  • 后置处理逻辑(要进行6次左右的数据库操作)

    // 获取商品信息(包括下面的获取库存信息和获取活动商品信息)
    ItemModel itemModel = itemService.getItemById(itemId);
    // 查询数据库1: 获取库存数量
    ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId(itemDO.getId());
    // 查询数据库2: 获取活动商品信息
    PromoModel promoModel = promoService.getPromoByItemId(itemModel.getId());
    // 查询数据库3: 获取用户信息
    UserModel userModel = userService.getUserById(userId);
    // 数据库操作4: 减库存
    boolean result = itemService.descreaseStock(itemId, amount);
    // 数据库操作5: 订单入库
    orderDOMapper.insertSelective(orderDO);
    // 数据库操作6: 增加销量
    itemService.increaseSales(itemId, amount);
    

在这里插入图片描述

  1. 交易验证优化策略
  • 用户风控策略优化: 策略缓存模型化
    获取用户信息是为了进行风控,还有判断是否异地登陆、是否最近修改密码等风控.
  • 活动校验策略优化: 引入活动发布流程,模型缓存化,紧急下线能力
  1. 用户信息与商品信息缓存代码
  • ItemService.interface

    UserModel getUserByIdInCache(Integer id);
    
  • ItemServiceImpl.java

    /**
     * item及promo model缓存模型
     */
    public ItemModel getItemByIdInCache(Integer id){
          
          
        ItemModel itemModel = (ItemModel)redisTemplate.opsForValue().get("item_validate_" + id);
        if(itemModel == null){
          
          
            itemModel = this.getItemById(id);
            redisTemplate.opsForValue().set("item_validate_" + id, itemModel);
            redisTemplate.expire("item_validate_" + id, 10, TimeUnit.MINUTES);
        }
        return itemModel;
    }
    
  • OrderServiceImpl.java

    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount) throws BusinessException {
          
          
    // 1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
    // ItemModel itemModel = itemService.getItemById(itemId);
    // 替换为使用缓存的方式
    ItemModel itemModel = itemService.getItemByIdInCache(itemId);
    
  • 本地测试

    • 执行代码后可以调试OrderServiceImpl和ItemServiceImpl文件,看是否能够从缓存中获取用户与商品信息.
    • redis缓存信息如下
      在这里插入图片描述
  • 代码部署到应用服务器

  • 分布式环境下jmeter压测: 平均值2152ms, 95值2930ms, TPS: 72.7/sec
    在这里插入图片描述
    可见,与没有使用用户信息、商品信息缓存相比有了较大提升.

  • redis查看是否用户信息和商品信息放入缓存中
    在这里插入图片描述

  1. 库存行锁优化
  • 扣减库存缓存化

    • 方案: 活动发布同步库存进缓存、下单交易减缓存库存
    • 活动发布同步库存进缓存核心代码
      • PromoServiceImpl.java(省略PromoService接口中的方法声明)
        @Resource
        private ItemService itemService;
        @Resource
        private RedisTemplate redisTemplate;
        
        @Override
        public void publishpromo(Integer promoId) {
                  
                  
            // 通过活动id获取活动
            PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
            if(promoDO.getItemId() == null || promoDO.getItemId() == 0){
                  
                  
                return;
            }
            ItemModel itemModel = itemService.getItemById(promoDO.getItemId());
            // 将库存同步到redis中
            redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId(), itemModel.getStock());
        }
        
      • ItemController.java
        @Resource
        private PromoService promoService;
        /**
         * 发布促销活动:将库存同步到redis中
         * @param id
         * @return
         */
        @GetMapping(value = "/publishpromo")
        public CommonReturnType publishPromo(@RequestParam("id") Integer id){
                  
                  
            promoService.publishpromo(id);
            return CommonReturnType.create(null);
        }
        
      • 本地缓存测试 在这里插入图片描述
      • 查看本地redis 在这里插入图片描述
    • 下单交易减缓存库存核心代码
      • ItemServiceImpl.java
        public boolean descreaseStock(Integer itemId, Integer amount) throws BusinessException {
                  
                  
        // int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount);
            // 减缓存中的库存操作,返回剩余库存数量
            long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1);
            if(result >= 0){
                  
                  
                // 更新库存成功
                return true;
            }else{
                  
                  
                // 更新库存失败
                return false;
            }
        }
        
      • 扣减库存测试
        item_id原库存为9982在这里插入图片描述
        下单后redis中的库存如下,为9981:
        在这里插入图片描述
    • 目前存在的问题
      redis中的库存和数据库中的不一致.
    • 解决方法: 使用异步消息队列rocketmq.
  • 异步写入数据库

  • 库存数据库最终一致性保证(下一个博客中讲)

  1. rocketmq的概念、安装、使用
  2. rocketmq异步写入数据库代码编写
  • pom.xml文件引入rocketmq

    <dependency>
      <groupId>org.apache.rocketmq</groupId>
      <artifactId>rocketmq-client</artifactId>
      <version>4.3.0</version>
    </dependency>
    
  • application.yml文件变量引入

    mq:
      nameserver:
        addr: rocketmq服务器ip:9876
      topicname: stock
    
  • 开放rocketmq服务器9876、10911端口

    # 9876端口为nameserver默认端口,10911端口为broker默认端口
    firewall-cmd --add-port=9876/tcp --permanent
    firewall-cmd --add-port=10911/tcp --permanent
    firewall-cmd --reload
    firewall-cmd --list-all
    
  • MqProducer.java(rocketmq消息生产者)

    package com.kenai.mq;
    import com.alibaba.fastjson.JSON;
    import com.sun.org.apache.xpath.internal.operations.Bool;
    import org.apache.rocketmq.client.exception.MQBrokerException;
    import org.apache.rocketmq.client.exception.MQClientException;
    import org.apache.rocketmq.client.producer.DefaultMQProducer;
    import org.apache.rocketmq.client.producer.SendResult;
    import org.apache.rocketmq.common.message.Message;
    import org.apache.rocketmq.remoting.exception.RemotingException;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import javax.annotation.PostConstruct;
    import java.nio.charset.StandardCharsets;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class MqProducer {
          
          
        private DefaultMQProducer producer;
    
        @Value("${mq.nameserver.addr}")
        private String nameAddr;
    
        @Value("${mq.topicname}")
        private String topicName;
    
        @PostConstruct
        public void init() throws MQClientException {
          
          
            // 做mq producer的初始化
            producer = new DefaultMQProducer("product_group");
            // producer连接nameserver
            producer.setNamesrvAddr(nameAddr);
            producer.start();
        }
    
        // 同步库存扣减消息
        public Boolean asyncReduceStock(Integer itemId, Integer amount){
          
          
            Map<String, Object> bodyMap = new HashMap<>();
            bodyMap.put("itemId", itemId);
            bodyMap.put("amount", amount);
            Message message = new Message(topicName, "increase",
                    JSON.toJSON(bodyMap).toString().getBytes(StandardCharsets.UTF_8));
            try {
          
          
                producer.send(message);
            } catch (MQClientException e) {
          
          
                e.printStackTrace();
                return false;
            } catch (RemotingException e) {
          
          
                e.printStackTrace();
                return false;
            } catch (MQBrokerException e) {
          
          
                e.printStackTrace();
                return false;
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
                return false;
            }
            return true;
        }
    }
    
  • ItemServiceImpl.java(减库存、发消息)

    @Override
    @Transactional
    public boolean descreaseStock(Integer itemId, Integer amount) throws BusinessException {
          
          
    	// int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount);
        // 减缓存中的库存操作,返回剩余库存数量
        long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1);
        if(result >= 0){
          
          
            // 发送消息
            boolean mqResult = mqProducer.asyncReduceStock(itemId, amount);
            // 发送消息失败,则将库存补回去,返回false
            if(!mqResult){
          
          
                redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount);
                return false;
            }
            return true;
        }else{
          
          
            // 更新库存失败,将库存补回去
            redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount);
            return false;
        }
    }
    
  • MqConsumer.java(rocketmq消息消费者)

    package com.kenai.mq;
    import com.alibaba.fastjson.JSON;
    import com.kenai.dao.ItemStockDOMapper;
    import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
    import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
    import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
    import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
    import org.apache.rocketmq.client.exception.MQClientException;
    import org.apache.rocketmq.common.message.Message;
    import org.apache.rocketmq.common.message.MessageExt;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import javax.annotation.PostConstruct;
    import javax.annotation.Resource;
    import java.util.List;
    import java.util.Map;
    
    @Component
    public class MqConsumer {
          
          
        private DefaultMQPushConsumer consumer;
    
        @Resource
        private ItemStockDOMapper itemStockDOMapper;
    
        @Value("${mq.nameserver.addr}")
        private String nameAddr;
    
        @Value("${mq.topicname}")
        private String topicName;
    
        @PostConstruct
        public void init() throws MQClientException {
          
          
            consumer = new DefaultMQPushConsumer("stock_consumer_group");
            // consumer连接nameserver
            consumer.setNamesrvAddr(nameAddr);
            // consumer订阅所有stock topic消息
            consumer.subscribe(topicName, "*");
            // 当消息推送过来之后的处理方式
            consumer.registerMessageListener(new MessageListenerConcurrently() {
          
          
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
          
          
                    // 实现库存真正到数据库内扣减的逻辑
                    Message msg = msgs.get(0);
                    String jsonString = new String(msg.getBody());
                    Map<String, Object> map = JSON.parseObject(jsonString, Map.class);
                    Integer itemId = (Integer) map.get("itemId");
                    Integer amount = (Integer) map.get("amount");
                    itemStockDOMapper.decreaseStock(itemId, amount);
                    // 说明该消息已经被消费
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            consumer.start();
        }
    }
    
  • 结果验证

    • 下单
      在这里插入图片描述
    • 查看redis中缓存的库存数量
      在这里插入图片描述
    • 查看数据库中库存数量 在这里插入图片描述
    • 总结
      redis缓存库存+使用rocketmq异步写入数据库操作成功
  1. 分布式环境redis库存缓存+使用rocketmq异步写入数据库测试
  • jar包部署
  • 启动应用服务器、nginx服务器、redis/rocketmq/mysql服务器
  • 发布秒杀商品
    在这里插入图片描述
  • 查看redis中商品库存缓存信息
    在这里插入图片描述
  • 秒杀商品下单测试
    在这里插入图片描述
  • 查看redis中商品库存缓存信息
    在这里插入图片描述
  • 查看数据库中商品库存信息
    在这里插入图片描述
  • 可见redis中缓存商品库存和mysql数据库中的库存一致
  1. 使用jmeter分布式环境下单测试
  • 线程属性值设置在这里插入图片描述

  • redis中库存缓存数据查看
    在这里插入图片描述

  • mysql数据库中数据查看
    在这里插入图片描述
    可见redis中库存缓存数据和mysql数据库数据一致

  • jmeter测试结果
    在这里插入图片描述

  • 压测原因分析

    • 服务器为1核2G
    • mysql数据库服务器、redis服务器、rocketmq服务器共用一台服务器,压力比较大.
  1. 目前存在的问题
  • 异步写入数据库消息发送失败
    当前的处理方式是回滚,应该使用更好的处理方式
  • 库存扣减/回补操作执行失败
  • 下单失败无法正确回补库存
    下单操作在减库存操作后,如果减库存操作成功但是下单操作失败,由于redis不会回滚,rocketmq的消息也成功消费掉了,则会出现多减库存的情况发生.

猜你喜欢

转载自blog.csdn.net/qq_26496077/article/details/113444828