前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转 Spring Boot 应用篇(引入RabbitMQ解决店铺下单峰值问题)

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

作者头像
一猿小讲
发布2022-05-31 09:45:41
3270
发布2022-05-31 09:45:41
举报
文章被收录于专栏:一猿小讲一猿小讲

0.

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. 回顾(菜菜的店铺目前存在的问题)

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

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

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

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

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

  • 引入依赖
代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 添加 RabbitMQ 配置
代码语言:javascript
复制
## RabbitMQ 配置
# RabbitMQ服务的地址
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# RabbitMQ 服务创建的虚拟主机(非必须)
spring.rabbitmq.virtual-host=/

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

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

  • 创建常量类
  • 商品购买记录生产者(UserGoodsProducer)
代码语言:javascript
复制
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)
代码语言:javascript
复制
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 修改
代码语言:javascript
复制
int saveRes = userGoodsDao.insert(userGoods); 
logger.info("插入购买记录:" + saveRes);

修改为发送 MQ 消息:

代码语言:javascript
复制
userGoodsProducer.sendMessage(userGoods);

详细代码如下:

代码语言:javascript
复制
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. 验证

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

  • 服务端控制台日志输出

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

  • 数据库记录

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

4. 例行回顾

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

此时架构演变如下:

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

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

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

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

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

参考资料:

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揭秘:快速构建微服务体系》

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-04-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一猿小讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档