思路:减少数据库访问
- 系统初始化,把商品库存数量加载到redis
- 收到请求,redis预减库存,库存不够,直接返回,否则进入3
- 请求入队,立即返回排队中
- 请求出队,生成订单,减少库存
- 客户端轮询,是否秒杀成功
对于之前的秒杀接口do_miaosha:
@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
if(user == null)
return Result.error(CodeMsg.SESSION_ERROR);
//判断库存
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
if(goodsVo.getStockCount() <= 0){
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder != null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//减库存、下订单、写入秒杀订单,需要在一个事务中执行
OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);
return Result.success(orderInfo);
}
这里判断库存是直接从数据库查,因为并发量比较大,存在性能问题。后面秒杀到之后,也不是直接减库存, 而是将其放到消息队列中慢慢交给数据库去调整。
@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
if(user == null)
return Result.error(CodeMsg.SESSION_ERROR);
//1.预减库存进行优化
/*********************************优化1开始*************************************/
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
if(stock < 0){
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
/*********************************优化1结束*************************************/
//2.判断是否已经秒杀到了
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder != null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
/*********************************优化2开始*************************************/
//3.进入消息队列
MiaoshaMessage message = new MiaoshaMessage();
message.setUser(user);
message.setGoodsId(goodsId);
sender.sendMiaoshaMessage(message);
/*********************************优化2结束*************************************/
return Result.success(0);//排队中
}
这是sender
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
在消息队列中对消息进行消化:
@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receive(String message){
log.info("receive message:{}",message);
MiaoshaMessage msg = RedisService.stringToBean(message,MiaoshaMessage.class);
MiaoshaUser user = msg.getUser();
long goodsId = msg.getGoodsId();
//判断数据库库存是否真的足够
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
if(goodsVo.getStockCount() <= 0){
return;
}
//判断是否已经秒杀到了
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder != null){
return;
}
//减库存、下订单、写入秒杀订单,需要在一个事务中执行
OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);
}
对于controller中的优化1:redis预减库存。那么需要在系统启动的时候将秒杀商品的库存先添加到redis中:
public class MiaoshaController implements InitializingBean
重写afterPropertiesSet()方法:
/**
* 系统初始化
* */
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
localOverMap.put(goods.getId(), false);
}
}
对于前端,这时也要进行修改了,因为点击秒杀商品按键后,这里考虑三种情况:排队等待、失败、成功。那么这里规定-1为失败,0为排队,1为秒杀成功已经写入数据库
原来的detail.htm中秒杀事件函数:
function doMiaosha(){
$.ajax({
url:"/miaosha/do_miaosha",
type:"POST",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
window.location.href="/order_detail.htm?orderId="+data.data.id;
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
秒杀到商品就直接返回,现在后端改为消息队列,所以需要增加函数进行判断,必要时需要轮询
所以将其改为:
if(data.code == 0){
//window.location.href="/order_detail.htm?orderId="+data.data.id;
//秒杀到商品的时候,这个时候不是直接返回成功,后端是进入消息队列,所以前端是轮询结果,显示排队中
getMiaoshaResult($("#goodsId").val());
}else{
layer.msg(data.msg);
}
而
function getMiaoshaResult(goodsId) {
g_showLoading();
$.ajax({
url:"/miaosha/result",
type:"GET",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
var result = data.data;
//失败--- -1
if(result <= 0){
layer.msg("对不起,秒杀失败!");
}
//排队等待,轮询--- 0
else if(result == 0){//继续轮询
setTimeout(function () {
getMiaoshaResult(goodsId);
},50);
}
//成功---- 1
else {
layer.msg("恭喜你,秒杀成功,查看订单?",{btn:["确定","取消"]},
function () {
window.location.href="/order_detail.htm?orderId="+result;
},
function () {
layer.closeAll();
}
);
}
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
那么相应地,后台也要增加一个方法:result
/**
* orderId:成功
* -1:秒杀失败
* 0: 排队中
* */
@RequestMapping(value="/result", method=RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId);
return Result.success(result);
}
那么如何标记状态呢?这就是getMiaoshaResult方法所做的事情。
对于成功的状态判断,很简单,从数据库查,能查到就说明已经秒杀成功,否则就是两种情况:失败或者正在等待生成订单。
对于这两种状态,我们需要用redis来实现,思路是:在系统初始化的时候,redis中设置秒杀商品是否卖完的状态为false—即未卖完;
public long getMiaoshaResult(Long userId, long goodsId) {
MiaoshaOrder orderInfo = orderService.getMiaoshaOrderByUserIdGoodsId(userId,goodsId);
if(orderInfo != null){
return orderInfo.getId();
}else{
boolean isOver = getGoodsOver(goodsId);
if(isOver){
//库存已经没了
return -1;
}else{
//表示还没入库,继续等待结果
return 0;
}
}
}
在MiaoshaService中的Miaosha方法:数据库减库存失败的话,说明数据库的库存已经小于0了,那么这个时候,立即将redis初始设置的秒杀商品是否卖完的状态为true,表示商品已经全部卖完,返回秒杀失败。否则就是要前端等待等待。
原来的代码:
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存 下订单 写入秒杀订单
goodsService.reduceStock(goods);
//order_info maiosha_order
return orderService.createOrder(user, goods);
}
改为:
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存、下订单、写入秒杀订单
boolean success =goodsService.reduceStock(goods);
if(success){
return orderService.createOrder(user,goods);
}else{
setGoodsOver(goods.getId());
return null;
}
}
对于两个小方法getGoodsOver和setGoodsOver:
private void setGoodsOver(long goodId){
redisService.set(MiaoshaKey.isGoodsOver,""+goodId,true);
}
private boolean getGoodsOver(long goodsId) {
return redisService.exists(MiaoshaKey.isGoodsOver,""+goodsId);
}
那么redis预减库存,然后消息队列来进行创建订单就实现了。
当然,对于redis预减库存这一点,还有要优化的地方,就是现在的do_miaosha接口是这样的:
@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
if(user == null)
return Result.error(CodeMsg.SESSION_ERROR);
//1.预减库存进行优化
/*********************************优化1开始*************************************/
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
if(stock < 0){
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
/*********************************优化1结束*************************************/
//2.判断是否已经秒杀到了
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder != null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
/*********************************优化2开始*************************************/
//3.进入消息队列
MiaoshaMessage message = new MiaoshaMessage();
message.setUser(user);
message.setGoodsId(goodsId);
sender.sendMiaoshaMessage(message);
/*********************************优化2结束*************************************/
return Result.success(0);//排队中
}
但是,当秒杀商品已经没了的时候,就没有必要再去redis中进行判断了,毕竟查询redis也是需要网络开销的,解决思路是:在内存中进行判断,如果redisService.decr得到的stock少于零的时候,直接将内存中的一个标志改变一下,那么下次再进入do_miaosha接口,先判断内存这个标记,如果库存已经小于0了,就不再访问redis,而是直接返回秒杀商品已经卖完。
改为:
@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
if(user == null)
return Result.error(CodeMsg.SESSION_ERROR);
//内存标记,减少不必要的redis的访问
boolean over = localOverMap.get(goodsId);
if(over){
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//预减库存进行优化
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
if(stock < 0){
localOverMap.put(goodsId,true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder != null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//进入消息队列
MiaoshaMessage message = new MiaoshaMessage();
message.setUser(user);
message.setGoodsId(goodsId);
sender.sendMiaoshaMessage(message);
return Result.success(0);//排队中
}
声明一个map:
private Map<Long,Boolean> localOverMap = new HashMap<>();
那么在afterPropertiesSet这个系统加载的初始化方法中对这个map进行初始化,goodsId–stock:
localOverMap.put(goods.getId(),false);
在原来的redis预减库存初,发现库存小于0 ,就改为true:
if(stock < 0){
localOverMap.put(goodsId,true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
这样,整个关于redis预减库存和rabbitMQ创建订单这个优化已经基本完成了。
部分摘自:http://fossi.oursnail.cn/2019/04/23/miaosha/6.服务级高并发秒杀优化(RabbitMQ+接口优化)/