0.
0.1. 回顾(菜菜的店铺目前存在的问题)
上次分享通过集成 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=localhostspring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.password=guest# RabbitMQ 服务创建的虚拟主机(非必须)spring.rabbitmq.virtual-host=/
2. 创建商品购买记录代码改造
可以考虑对同步保存商品购买记录的操作进行异步化保存,这样可以提高请求的响应速度,提高用户的使用体验,减少了流量高峰对数据库的压力。
- 创建常量类
- 商品购买记录生产者(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;
@Servicepublic 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;
@Componentpublic 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 消息:
代码语言: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;
@Servicepublic 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 相关技术熟练使用一下。
雕塑自己的过程必定伴随着疼痛与辛苦,可那一锤一凿的自我敲打,会让我们收获更好的自己。