秒杀业务场景并发量很大,瓶颈在数据库,怎么解决?可以加缓存。用户在发起请求时,从浏览器开始,在浏览器上做页面静态化直接将页面缓存到用户的浏览器端,然后请求到达网站之前可以部署CDN节点,让请求先访问CDN,到达网站的时候使用页面缓存。页面缓存再进一步,粒度再细一点的话就是对象缓存,缓存层依次请求完之后,才是数据库。通过一层一层的访问缓存逐步的削减到达数据库的请求数量,这样才能保证网站在高并发之下扛住压力,但是仅仅依靠缓存还不够,所以还需要进行接口优化,接口优化的核心思路:减少数据库的访问(因为数据库抗并发的能力有限)
- 使用Redis预减库存减少对数据库的访问
- 使用内存标记减少Redis的访问
- 使用RabbitMQ消息队列缓冲,异步下单,增强用户体验
具体实现步骤:
- 系统初始化时,把商品库存数量加载到Redis里面去
- 收到秒杀请求时,Redis预减库存(先减少Redis里面的库存数量,库存不足,则直接返回),如果库存已经到达临界值的时候,即=0,就不需要继续往下走,直接返回秒杀失败
- 将请求放入消息队列,立即返回排队中
- 将请求从消息队列取出来,生成订单,减少库存
- 客户端轮询秒杀的结果,看是否秒杀成功
流程图如下:
将商品库存数量预加载库存到Redis里面并标记到内存里
将MiaoshaController实现InitializingBean接口,重写afterPropertiesSet方法:
收到秒杀请求后的具体业务逻辑
后端收到秒杀请求,实行Redis预减库存(先减少Redis里面的库存数量,库存不足,直接返回),如果库存到达临界值的时候,即库存=0,就不需要继续往下走,直接返回秒杀失败;如果所有的判断都通过,则将请求放入消息队列,具体业务逻辑分析如下图:
秒杀接口代码如下:
@RequestMapping(value = "/{path}/do_miaosha", method = RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId") long goodsId,
@PathVariable("path") String path) {
model.addAttribute("user", user);
try {
//内存标记,减少redis访问,从map中取出
boolean over = localOverMap.get(goodsId);
if (over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//预减库存:从缓存中减去库存
//利用redis中的方法,减去库存,返回值为减去1之后的值
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
//这里判断不能小于等于,因为减去之后等于0说明还有是正常范围
if (stock < 0) {
localOverMap.put(goodsId, true);
//返回秒杀完毕
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了,避免一个账户秒杀多个商品
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
//还原库存
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
} catch (Exception e) {
/**
* 当最后一个商品下单出现错误时,数据库减少库存失败,redis减少库存成功
* 这时就会出现库存售不完的情况,所以要将redis缓存还原,即redis库存数加1
*/
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
//将请求入队
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//返回排队中
}
MiaoshaMessage代码(消息的封装类):
package com.javaxl.miaosha_05.rabbitmq;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
public class MiaoshaMessage {
private MiaoshaUser user;
private long goodsId;
public MiaoshaUser getUser() {
return user;
}
public void setUser(MiaoshaUser user) {
this.user = user;
}
public long getGoodsId() {
return goodsId;
}
public void setGoodsId(long goodsId) {
this.goodsId = goodsId;
}
}
MQSender代码(发送消息到rabbitmq去):
package com.javaxl.miaosha_05.rabbitmq;
import com.javaxl.miaosha_05.redis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
注:消息队列这里的消息只能传字符串,MiaoshaMessage是个Bean对象,先用beanToString方法,将其转换为String,放入队列,再使用AmqpTemplate发送
MQConfig代码(创建MQ的config类):
package com.javaxl.miaosha_05.rabbitmq;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";
public static final String QUEUE = "queue";
public static final String MIAOSHA_EXCHANGE = "miaosha.exchange";
/**
* Direct模式 交换机Exchange
* */
@Bean
public Queue queue() {
return new Queue(QUEUE, true);
}
}
application.properties中关于rabbitmq的配置:
#rabbitmq
spring.rabbitmq.host=xxx
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#消费者数量
spring.rabbitmq.listener.simple.concurrency= 10
spring.rabbitmq.listener.simple.max-concurrency= 10
#消费者每次从队列获取的消息数量
spring.rabbitmq.listener.simple.prefetch= 1
#消费者自动启动
spring.rabbitmq.listener.simple.auto-startup=true
#消费失败,自动重新入队
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#启用发送重试
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000
spring.rabbitmq.template.retry.max-attempts=3
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0