【商城秒杀项目】-- 秒杀的业务逻辑、接口的优化

秒杀业务场景并发量很大,瓶颈在数据库,怎么解决?可以加缓存。用户在发起请求时,从浏览器开始,在浏览器上做页面静态化直接将页面缓存到用户的浏览器端,然后请求到达网站之前可以部署CDN节点,让请求先访问CDN,到达网站的时候使用页面缓存。页面缓存再进一步,粒度再细一点的话就是对象缓存,缓存层依次请求完之后,才是数据库。通过一层一层的访问缓存逐步的削减到达数据库的请求数量,这样才能保证网站在高并发之下扛住压力,但是仅仅依靠缓存还不够,所以还需要进行接口优化,接口优化的核心思路:减少数据库的访问(因为数据库抗并发的能力有限)

  • 使用Redis预减库存减少对数据库的访问
  • 使用内存标记减少Redis的访问
  • 使用RabbitMQ消息队列缓冲,异步下单,增强用户体验

具体实现步骤:

  1. 系统初始化时,把商品库存数量加载到Redis里面去
  2. 收到秒杀请求时,Redis预减库存(先减少Redis里面的库存数量,库存不足,则直接返回),如果库存已经到达临界值的时候,即=0,就不需要继续往下走,直接返回秒杀失败
  3. 将请求放入消息队列,立即返回排队中
  4. 将请求从消息队列取出来,生成订单,减少库存
  5. 客户端轮询秒杀的结果,看是否秒杀成功

流程图如下:

将商品库存数量预加载库存到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
发布了133 篇原创文章 · 获赞 94 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_42687829/article/details/104493180