玩转 Spring Boot 应用篇(引入RabbitMQ解决店铺下单峰值问题)

0. 

18f708a7119047431ba72e0440920274.png

0.0. 历史文章整理

玩转 Spring Boot 入门篇

玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)

玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)

玩转 Spring Boot 集成篇(Redis)

玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)

玩转 Spring Boot 集成篇(RabbitMQ)

玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)

玩转 Spring Boot 集成篇(任务动态管理代码篇)

玩转 Spring Boot 集成篇(定时任务框架Quartz)

玩转 Spring Boot 原理篇(源码环境搭建)

玩转 Spring Boot 原理篇(核心注解知多少)

玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)

玩转 Spring Boot 原理篇(自动装配源码剖析)

玩转 Spring Boot 原理篇(启动机制源码剖析)

玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

玩转 Spring Boot 应用篇(搭建菜菜的店铺)

玩转 Spring Boot 应用篇(解决菜菜店铺商品超卖问题)

玩转 Spring Boot 应用篇(引入Redis解决店铺高并发读的问题)

0.1. 回顾(菜菜的店铺目前存在的问题)

f2f4dc40bfe707c7cbf2b0b58cb6ff3a.png

上次分享通过集成 Redis 技术组件,让请求不再直接查询数据库,而是优先从 Redis 查询商品信息,进而来解决数据库高并发读的问题。

但是此时的技术实现,当面对瞬间特高的访问流量峰值时,会导致服务或者数据库宕机,那么面对流量峰值,该如何解决呢?

坊间,多数是引入 MQ 来削峰,本次采取集成 RabbitMQ 来支持。

1. 菜菜的店铺技术升级:集成 RabbitMQ

有关 Spring Boot 集成 RabbitMQ 的详细操作步骤,可以参考历史文章《玩转 Spring Boot 集成篇(RabbitMQ)》,本次采取集成 RabbitMQ 来缓解流量峰值的问题。

  • 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 添加 RabbitMQ 配置

## RabbitMQ 配置
# RabbitMQ服务的地址
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# RabbitMQ 服务创建的虚拟主机(非必须)
spring.rabbitmq.virtual-host=/

2. 创建商品购买记录代码改造

44c3b0a44e596aa9c9dcbe6286505587.png

可以考虑对同步保存商品购买记录的操作进行异步化保存,这样可以提高请求的响应速度,提高用户的使用体验,减少了流量高峰对数据库的压力。

  • 创建常量类

f31a64431dc1a3c60724d3d8958626e6.png

  • 商品购买记录生产者(UserGoodsProducer)

package org.growup.caicaishop.mq;


import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.utils.Constant;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;


import javax.annotation.Resource;
import java.util.logging.Logger;


@Service
public class UserGoodsProducer {


    private final Logger logger = Logger.getLogger("UserGoodsProducer");


    @Resource
    private RabbitTemplate rabbitTemplate;


    public void sendMessage(UserGoods userGoods) {
        logger.info("【生产者】- 待保存的商品购买记录 - " + userGoods);
        rabbitTemplate.convertAndSend(Constant.USER_GOODS_QUEUE, userGoods);
        logger.info("【生产者】- 商品购买记录" + userGoods.getGoodsId() + "发送 MQ 完成");
    }
}
  • 商品购买记录消费者(UserGoodsConsumer)

package org.growup.caicaishop.mq;


import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.UserGoodsService;
import org.growup.caicaishop.utils.Constant;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;


import javax.annotation.Resource;
import java.util.logging.Logger;


@Component
public class UserGoodsConsumer {


    private final Logger logger = Logger.getLogger("UserGoodsConsumer");


    @Resource
    private UserGoodsService userGoodsService;


    @RabbitHandler
    @RabbitListener(queuesToDeclare = @Queue(Constant.USER_GOODS_QUEUE))
    public void process(UserGoods userGoods) {
        // 保存商品购买记录信息
        int saveRes = userGoodsService.save(userGoods);
        logger.info("【消费者】商品购买记录创建:" + (saveRes != 0 ? "成功" : "失败"));
    }
}
  • 商品购买 Service 修改

int saveRes = userGoodsDao.insert(userGoods); 
logger.info("插入购买记录:" + saveRes);

修改为发送 MQ 消息:

userGoodsProducer.sendMessage(userGoods);

详细代码如下:

package org.growup.caicaishop.service.impl;


import org.growup.caicaishop.dao.GoodsDao;
import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.mq.UserGoodsProducer;
import org.growup.caicaishop.service.PurchaseService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


import javax.annotation.Resource;
import java.sql.Timestamp;
import java.util.logging.Logger;


import static org.growup.caicaishop.utils.Constant.GOODS_LIST_CACHE_KEY;


@Service
public class PurchaseServiceImpl implements PurchaseService {


    private final Logger logger = Logger.getLogger("PurchaseServiceImpl");


    @Resource
    private GoodsDao goodsDao;


    @Resource
    private RedisTemplate redisTemplate;


    @Resource
    private UserGoodsProducer userGoodsProducer;


    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public boolean purchase(Integer userId, Integer goodsId, int quantity) {
        // 加入尝试固定次数限制
        for (int i = 0; i < 3; i++) {
            Goods goodsInfo = goodsDao.getGoodsById(goodsId);
            if (goodsInfo.getStock() < quantity) {
                // 库存不足
                logger.info("库存不足: " + goodsInfo.getStock());
                return false;
            }


            //扣减库存
            int res = goodsDao.reduceStock(goodsId, quantity, goodsInfo.getVersion());
            logger.info("扣减库存结果:" + res);
            if (res == 0) {
                logger.info("数据被修改,本次购买失败,继续尝试");
                continue;
            }
            //扣减库存成功,则更新 redis 中缓存的商品信息
            redisTemplate.opsForHash().put(GOODS_LIST_CACHE_KEY, goodsId, goodsDao.getGoodsById(goodsId));
            logger.info("更新缓存中的商品信息:" + goodsId + "成功");
            //插入购买记录
            UserGoods userGoods = new UserGoods();
            userGoods.setUserId(userId);
            userGoods.setGoodsId(goodsId);
            userGoods.setQuantity(quantity);
            userGoods.setState(1);
            userGoods.setCreateTime(new Timestamp(System.currentTimeMillis()));
            //int saveRes = userGoodsDao.insert(userGoods);
            //logger.info("插入购买记录:" + saveRes);
            userGoodsProducer.sendMessage(userGoods);
            return true;
        }
        logger.info("重试 3 次后依然失败");
        return false;
    }
}

3. 验证

运行菜菜的店铺服务,然后选择钟意的商品点击“买它”。

00823e34e3b4cddb308f4febf1670b0a.png

  • 服务端控制台日志输出

18649061d3a4e343d4691d63b71b1655.png

生产者、消费者正常运行,此时数据库记录插入也成功啦。

  • 数据库记录

4dda5ad04012cb0c3896e949c5ffef6a.png

至此,引入 MQ 来降低高并发保存商品购买记录对数据库的压力,而且保存购买记录有同步变异步,也缩短了处理时间,一定程度上提升了用户的体验。

4. 例行回顾

本文主要是对菜菜的店铺中的瞬间高峰带来的数据库压力进行缓冲,主要引入基于 RabbitMQ 来缓解流量高峰的问题。

8952e2a25178a5110559c0426d031a6a.png

此时架构演变如下:

30788ac6b8c5082c33aafd55c572a85b.png

第一版:基于 Spring Boot 整合 MyBatis 完成商品的 CRUD,整合 Thymeleaf 完成视图展示解析;

第二版:基于 Spring Boot 整合 Redis 完成商品信息缓存,缓解数据库查询压力;

第三版:基于 Spring Boot 整合 RabbitMQ 环节下单流量高峰。

至此,菜菜的店铺就搭建完成了,店铺基本能用,主要是一起把前期的 Spring Boot 相关技术熟练使用一下。

雕塑自己的过程必定伴随着疼痛与辛苦,可那一锤一凿的自我敲打,会让我们收获更好的自己。

一起聊技术、谈业务、喷架构,少走弯路,不踩大坑,会持续输出更多精彩分享,欢迎关注,敬请期待!

f5dbfc682942280ab27a9ff906d5cf0e.png

参考资料:

https://spring.io/

https://start.spring.io/

https://spring.io/projects/spring-boot

https://github.com/spring-projects/spring-boot

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

https://stackoverflow.com/questions/tagged/spring-boot

《Spring Boot实战》《深入浅出Spring Boot 2.x》

《一步一步学Spring Boot:微服务项目实战(第二版)》

《Spring Boot揭秘:快速构建微服务体系》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值