0.
0.0. 历史文章整理
玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)
玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)
玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)
玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)
玩转 Spring Boot 集成篇(定时任务框架Quartz)
玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)
玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)
玩转 Spring Boot 应用篇(解决菜菜店铺商品超卖问题)
玩转 Spring Boot 应用篇(引入Redis解决店铺高并发读的问题)
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服务的地址
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# RabbitMQ 服务创建的虚拟主机(非必须)
spring.rabbitmq.virtual-host=/
2. 创建商品购买记录代码改造
可以考虑对同步保存商品购买记录的操作进行异步化保存,这样可以提高请求的响应速度,提高用户的使用体验,减少了流量高峰对数据库的压力。
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 完成");
}
}
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 ? "成功" : "失败"));
}
}
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揭秘:快速构建微服务体系》