一、redis
通过redis
预减库存,来减少数据库访问。
可以在初始化阶段,将商品库存加入到redis中。后续直接在redis中进行预减库存操作。后续的下单可以先返回给客户端提示信息,同时将请求发送到消息队列,来实现订单的创建等操作,实现异步操作。
客户端的页面使用轮询来判断订单是否创建成功。
1.1 预减库存
两步操作
- 项目启动时,通过实现
InitializingBean
里的afterPropertiesSet()
方法,把系统库存加载到redis中。 - 通过redis扣减库存,并进行判断。
- 下单通过RabbitMQ来实现。
/**
* 系统初始化,把商品库存数量加载到redis
* @author 47roro
* @date 2022/5/12
* @param
**/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> list = goodsService.findGoodsVo();
if(CollectionUtils.isEmpty(list)){
return;
}
list.forEach(goodsVo ->
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()));
}
二、内存标记
通过内存标记减少redis访问。
如果当库存减为0时,大量请求访问,还是要和redis进行通信,这样的话也会加重redis的消耗,可以通过内存来进行标记。
2.1 控制层逻辑
在系统初始化中添加内存标记。
/**
* 系统初始化,把商品库存数量加载到redis
* @author 47roro
* @date 2022/5/12
* @param
**/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> list = goodsService.findGoodsVo();
if(CollectionUtils.isEmpty(list)){
return;
}
list.forEach(goodsVo -> {
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
EmptyStockMap.put(goodsVo.getId(), false);
}
);
}
预减库存操作之前先判断内存标记。
/**
* 秒杀
* @author 47roro
* @date 2022/4/16
* @param model
* @param user
* @param goodsId
* @return java.lang.String
**/
@RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(Model model, User user, Long goodsId){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
ValueOperations valueOperations = redisTemplate.opsForValue();
//判断是否重复抢购(mybatis plus)
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if(seckillOrder != null){
return RespBean.error(RespBeanEnum.REPEAT_ERROR);
}
//内存标记减少redis访问
if(EmptyStockMap.get(goodsId)){
return RespBean.error(RespBeanEnum.EMPT_STOCK);
}
//预减库存
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
if(stock < 0){
EmptyStockMap.put(goodsId, true);
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPT_STOCK);
}
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
return RespBean.success(0);
}
三、消息队列
请求进入消息队列,实现异步下单。
3.1 RabbitMQ安装
首先将erlang以及RabbitMQ安装包传到虚拟机中,注意两个版本需要对应。
执行安装。 yum -y install esl-erlang_23.0.2-1_centos_7_amd64.rpm
。
安装完成后,查看erl,输出以下内容即安装成功。
然后安装rabbitMQ。yum -y install rabbitmq-server-3.8.5-1.el7.noarch.rpm
安装完成后,还要安装一个可视化的管理控制台,以插件形式存在。
rabbitmq-plugins enable rabbitmq_management
.
安装完成后,启动rabbitmq systemctl start rabbitmq-server.service
rabbitmq默认端口号:15672.
3.2 rabbitmq远程登录
进入目录cd /etc/rabbitmq
添加配置文件vim rabbitmq.config
,在配置文件中添加配置:[{rabbit,[{loopback_users,[]}]}].
最后的这个.
不可缺少。
重新启动rabbitmq服务。
重启成功后,在网页上输入网址,用默认的用户名密码登录。可成功登录。
用户名:guest
密码:guest
3.3 SpringBoot继承RabbitMQ
在pom中添加依赖
<!--AMQP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在application中进行配置。这里的端口应该是5672。因为15672是可视化界面的登录端口。实际的服务端口在5672
spring:
# RabbitMQ
rabbitmq:
host: 192.168.222.129
username: guest
password: guest
virtual-host: /
port: 5672
listener:
simple:
# 消费者最小数量
concurrency: 10
# 消费者最大数量
max-concurrency: 10
# 限制消费者每次处理消息的数量
prefetch: 1
template:
retry:
# 发布重试
enabled: true
3.3.1添加配置类
/**
* @author 47roro
* @create 2022/5/12
* @description: RabbitMQ配置类
*/
@Configuration
public class RabbitMQConfig {
//seckill
private static final String QUEUE = "seckillQueue";
private static final String EXCHANGE = "seckillExchange";
@Bean
public Queue queue(){
return new Queue(QUEUE);
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
}
}
3.3.2 准备消息发送者和消费者
/**
* @author 47roro
* @create 2022/5/12
* @description: 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//发送秒杀信息
public void sendSeckillMessage(Message msg){
log.info("发送消息:" + msg);
rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", msg);
}
}
/**
* @author 47roro
* @create 2022/5/12
* @description: 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IOrderService orderService;
/**
* 下单操作
* @author 47roro
* @date 2022/5/12
* @param msg
**/
@RabbitListener(queues = "seckillQueue")
public void receive(String msg){
log.info("接收消息:" + msg);
SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(msg, SeckillMessage.class);
Long goodsId = seckillMessage.getGoodId();
User user = seckillMessage.getUser();
//判断库存
GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
if(goodsVo.getStockCount() < 1){
return;
}
//判断是否重复抢购(mybatis plus)
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if(seckillOrder != null){
return;
}
//下单操作
Order order = orderService.seckill(user, goodsVo);
}
}
3.3.3 控制层逻辑
即内存标记中的代码。
四、 下单结果
之前是返回order对象,成功后直接跳转订单详情。现在前面下单返回的是return RespBean.success(0);
,只是显示排队中,客户无法知道下单是否成功。所以现在要修改前端页面来实现查询下单结果。
4.1 静态页面修改
function doSeckill(){
$.ajax({
url:'/seckill/doSeckill',
type:'POST',
data:{
goodsId:$("#goodsId").val()
},
success:function(data){
if(data.code == 200){
//window.location.href="/orderDetail.htm?orderId="+data.obj.id;
getResult($("#goodsId").val());
}else{
layer.msg(data.message);
}
},
error:function(){
layer.msg("客户端请求出错");
}
})
}
function getResult(goodsId){
g_showLoading();
$.ajax({
url:"/seckill/result",
type:"GET",
data:{
goodsId:goodsId,
},
success:function (data){
if(data.code==200){
var result = data.obj;
if(result < 0){
layer.msg("秒杀失败");
} else if(result == 0){
setTimeout(function(){
getResult(goodsId);
}, 50);
} else {
layer.confirm("秒杀成功,是否查看订单?", {btn:["确定","取消"]},
function(){
window.location.herf="/orderDetail.htm?orderId=" + result;
},
function(){
layer.close();
})
}
}
},
error:function(){
layer.msg("客户端请求错误");
}
})
}
4.2 控制器修改
/**
* 获取秒杀结果
* @author 47roro
* @date 2022/5/12
* @param user
* @param goodsId
* @return com.example.seckill.vo.RespBean
* orderId: 成功; -1:秒杀失败; 0:排队中
**/
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public RespBean getResult(User user, Long goodsId){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
Long orderId = seckillOrderService.getResult(user, goodsId);
return RespBean.success(orderId);
}
4.3 服务层修改
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 获取秒杀结果
* @author 47roro
* @date 2022/5/12
* @param user
* @param goodsId
* @return java.lang.Long
* orderId: 成功; -1:秒杀失败; 0:排队中
**/
@Override
public Long getResult(User user, Long goodsId) {
SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder>().eq(
"user_id", user.getId()).eq("goods_id", goodsId));
if (seckillOrder != null) {
return seckillOrder.getOrderId();
} else if(redisTemplate.hasKey("isStockEmpty:" + goodsId)){
return -1L;
} else {
return 0L;
}
}
四、结果测试
redis缓存
数据库记录
五、压力测试
订单详情,正好10条:
之前缓存的QPS大约为460
可以看到优化后的QPS约为580.