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 来缓解流量峰值的问题。
引入依赖
org.springframework.boot
spring-boot-starter-amqp
添加 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. 创建商品购买记录代码改造
可以考虑对同步保存商品购买记录的操作进行异步化保存,这样可以提高请求的响应速度,提高用户的使用体验,减少了流量高峰对数据库的压力。
创建常量类
商品购买记录生产者(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. 验证
运行菜菜的店铺服务,然后选择钟意的商品点击“买它”。
服务端控制台日志输出
生产者、消费者正常运行,此时数据库记录插入也成功啦。
数据库记录
至此,引入 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揭秘:快速构建微服务体系》