【SpringBoot商城秒杀系统项目实战20】高并发秒杀接口优化

高并发秒杀接口优化

秒杀业务场景,并发量很大,瓶颈在数据库,怎么解决,加缓存。用户发起请求时,从浏览器开始,在浏览器上做页面静态化直接将页面缓存到用户的浏览器端,然后请求到达网站之前可以部署CDN节点,让请求先访问CDN,到达网站时候使用页面缓存。页面缓存再进一步的话,粒度再细一点的话就是对象缓存。缓存层依次请求完之后,才是数据库。通过一层一层的访问缓存逐步的削减到达数据库的请求数量,这样才能保证网站在大并发之下抗住压力。

但是仅仅依靠缓存还不够,所以还需要进行接口优化。
接口优化核心思路:减少数据库的访问。(数据库抗并发的能力有限)

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

具体实现步骤:

  1. 系统初始化,把商品库存数量加载到Redis上面来

  2. 收到请求,Redis预减库存(先减少Redis里面的库存数量,库存不足,直接返回),如果库存已经到达临界值的时候,即=0,就不需要继续往下走,直接返回失败

  3. 请求入队,立即返回排队中

  4. 请求出队,生成订单,减少库存

  5. 客户端轮询,是否秒杀成功

1.商品库存数量预加载库存到Redis上

MiaoshaController实现InitializingBean接口,重写afterPropertiesSet方法。
  在容器启动的时候,检测到了实现了接口InitializingBean之后,就回去回调afterPropertiesSet方法。将每种商品的库存数量加载到redis里面去。

	@RequestMapping("/miaosha")
	@Controller
	public class MiaoshaController implements InitializingBean{	
	public void afterPropertiesSet() throws Exception {
		List<GoodsVo> goodslist=goodsService.getGoodsVoList();
		if(goodslist==null) {
			return;
		}
		for(GoodsVo goods:goodslist) {
			//如果不是null的时候,将库存加载到redis里面去 prefix---GoodsKey:gs ,	 key---商品id,	 value
			redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
		}
	}
	}
	@Service
	public class GoodsService {
	public static final String COOKIE1_NAME_TOKEN="token";	
	@Autowired
	GoodsDao goodsDao;
	@Autowired
	RedisService redisService;
	
	public List<GoodsVo>  getGoodsVoList() {
		return goodsDao.getGoodsVoList();
	}
	}
	@Mapper
	public interface GoodsDao {
	//两个查询
	@Select("select g.*,mg.stock_count,mg.start_date,mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id=g.id")  
	public List<GoodsVo> getGoodsVoList();
	}

2.收到请求后预减库存

后端接收秒杀请求的接口doMiaosha,收到请求,Redis预减库存(先减少Redis里面的库存数量,库存不足,直接返回),如果库存已经到达临界值的时候,即=0,就不需要继续往下走,直接返回失败

	@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
	@ResponseBody
	public Result<Integer> doMiaosha(Model model,MiaoshaUser user,
			@RequestParam(value="goodsId",defaultValue="0") long goodsId,
			@PathVariable("path")String path) {
		model.addAttribute("user", user);
		//1.如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}	
		//2.预减少库存,减少redis里面的库存
		long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
		//3.判断减少数量1之后的stock,区别于查数据库时候的stock<=0
		if(stock<0) {//线程不安全---库存至临界值1的时候,此时刚好来了加入10个线程,那么库存就会-10
			return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
		}
		//4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
		if (order != null) {// 查询到了已经有秒杀订单,代表重复下单
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//5.可以秒杀,原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务
		OrderInfo orderinfo=miaoshaService.miaosha(user,goodsvo);
		return Result.success(orderinfo);
	}

RedisService里面的decr方法:减少key对应的值

	/**
	 * 减少值
	 * @param prefix
	 * @param key
	 * @return
	 */
	public <T> Long decr(KeyPrefix prefix,String key){
		Jedis jedis=null;
		try {
			jedis=jedisPool.getResource();
			String realKey=prefix.getPrefix()+key;
			return jedis.decr(realKey);
		}finally {
			returnToPool(jedis);
		}
	}

3.消息入队(并将用户信息和商品信息封装起来传入队列)

再次改造doMiaosha接口方法,在收到请求之后,请求入队:

	@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
	@ResponseBody
	public Result<Integer> doMiaosha(Model model,MiaoshaUser user,
			@RequestParam(value="goodsId",defaultValue="0") long goodsId,
			@PathVariable("path")String path) {
		model.addAttribute("user", user);
		//1.如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}	
		//2.预减少库存,减少redis里面的库存
		long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
		//3.判断减少数量1之后的stock,减少到0一下,则代表之后的请求都失败,直接返回
		if(stock<0) {
			return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
		}
		//4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
		if (order != null) {// 查询到了已经有秒杀订单,代表重复下单
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//5.正常请求,入队,发送一个秒杀message到队列里面去,入队之后客户端应该进行轮询。
		MiaoshaMessage mms=new MiaoshaMessage();
		mms.setUser(user);
		mms.setGoodsId(goodsId);
		mQSender.sendMiaoshaMessage(mms);
		//返回0代表排队中
		return Result.success(0);
	}

MiaoshaMessage 消息的封装类:

	//MiaoshaMessage 消息的封装  MiaoshaMessage Bean
	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;
	}
	}

注意:消息队列这里,消息只能传字符串,MiaoshaMessage 这里是个Bean对象,是先用beanToString方法,将转换为String,放入队列,使用AmqpTemplate传输。

@Autowired
	RedisService redisService;
	@Autowired
	AmqpTemplate amqpTemplate;
	public void sendMiaoshaMessage(MiaoshaMessage mmessage) {
		// 将对象转换为字符串
		String msg = RedisService.beanToString(mmessage);
		log.info("send message:" + msg);
		// 第一个参数队列的名字,第二个参数发出的信息
		amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
	}
	/**
	 * 将Bean对象转换为字符串类型
	 * @param <T>
	 */
	public static <T> String beanToString(T value) {
		//如果是null
		if(value==null) return null;
		//如果不是null
		Class<?> clazz=value.getClass();
		if(clazz==int.class||clazz==Integer.class) {
			return ""+value;
		}else if(clazz==String.class) {
			return ""+value;
		}else if(clazz==long.class||clazz==Long.class) {
			return ""+value;
		}else {
			return JSON.toJSONString(value);
		}		
	}

redis多线程情况下是否安全?

1.新建RedisConcurrentTestUtil类来测试redis多线程是否安全,如果安全,那么应该只有10个线程能通过if(stock>=0)的判断,进行后面的秒杀操作!

@Service
public class RedisConcurrentTestUtil {
	@Autowired
	RedisService redisService;	//会出现循环依赖---Circular reference
	class ThreadTest implements Runnable{
		@Override
		public void run() {
			long stock=redisService.get("GoodsKey:gs1",Long.class);
			String name=Thread.currentThread().getName();
			System.out.println("当前线程 :"+name+"  stock:"+stock);
			//2.预减少库存,减少redis里面的库存
			//stock最初为10,100个线程同时去减少1次,最终stock应该为-90
			stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+1);
			//是否线程安全?
			if(stock<0) {
				System.out.println("结束!!!");
				return;
			}
			//应该只有10个线程能从这里通过
			System.out.println("验证当前有几个线程通过if(stock<0)  当前线程 :"+name+"  减1之后的stock:"+stock);
		}
	}
	public  void test(){
		ThreadTest t1=new ThreadTest();
		//开启50个线程
		for(int i=1;i<=100;i++){
			new Thread(t1,"Thread-"+i).start();
		}
	}

}

2.在某一个类(DemoController)里面定义一个接收请求的方法。
在这里插入图片描述
在这里插入图片描述

3.进入redis,设置GoodsKey:gs1这个键的值为10。

几个常用的redis命令:
auth [password] 输入密码
keys *查询所有键
flushdb 清空数据
在这里插入图片描述
4.发送请求,验证结果!
在这里插入图片描述
5.结果如下:
可以得出多线程下redis的操作是线程安全的:
在这里插入图片描述
redis里面库存结果:

在这里插入图片描述

RabbitMQ在Windows上面的安装与集成

安装rabbitmq首先要首先安装erlang。

1.在Win环境下安装Erlang

步骤一:下载erlang

下载地址:

64位:http://erlang.org/download/otp_win64_20.3.exe

32位:http://erlang.org/download/otp_win32_20.3.exe

自己选择适合自己系统的,进行下载安装。

步骤二:安装erlang

直接点击exe安装,安装路径自己配的要记住在哪里,最好自己指定一个安装目录,等等会用到。

步骤三:最重要的一步了,配置环境变量

配置ERLANG_HOME环境变量,其值指向erlang的安装目录(就是步骤二的路径)。另外将 ;%ERLANG_HOME%\bin 加入到Path中。
在这里插入图片描述
新建添加即可:
在这里插入图片描述
步骤四:检查安装是否成功

打开cmd,输入erl

在这里插入图片描述
2.在Win环境下安装RabbitMQ
下载资源:
RabbitMQ,下载地址 http://www.rabbitmq.com/install-windows.html

对应版本(必须是与mq版本适应)的erlang,下载地址 http://www.erlang.org/downloads/20.2
首先安装erlang,然后安装rabbitmq。

安装完RabbitMQ以后,服务会自动运行,这时环境变量里的ERLANG_HOME会自动生成,在”环境变量”中检查是否存在,如果不存在,请在”环境变量”中手动添加,配置Erlang环境变量ERLANG_HOME=E:\Apply\Erlang213\erl10.2。如果没有,点击"新建"。

详细RabbitMQ安装请参考:https://blog.csdn.net/h363659487/article/details/80913354

2.集成RabbitMQ

  • 添加依赖
	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  • 在application.properties配置文件里面添加RabbitMQ配置信息
#RabbitMQ配置
spring.rabbitmq.host=127.0.0.1
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.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
#最大间隔 10s
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
  • 创建消息发送者与接收者
//发送者
@Service
public class MQSender {
	
	private static Logger log=LoggerFactory.getLogger(MQSender.class);
	@Autowired
	RedisService redisService;
	@Autowired
	AmqpTemplate amqpTemplate;
	/**
	 * 发送秒杀信息,使用derict模式的交换机。(包含秒杀用户信息,秒杀商品id)
	 */
	public void sendMiaoshaMessage(MiaoshaMessage mmessage) {
		// 将对象转换为字符串
		String msg = RedisService.beanToString(mmessage);
		log.info("send message:" + msg);
		// 第一个参数队列的名字,第二个参数发出的信息
		amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
	}	
}
  • 创建MQ的config,然后创建队列
@Configuration
public class MQConfig {
	public static final String QUEUE="queue";
	public static final String MIAOSHA_QUEUE="miaosha.queue";
	public static final String TOPIC_QUEUE1="topic.queue1";
	public static final String TOPIC_QUEUE2="topic.queue2";
	public static final String HEADER_QUEUE="header.queue";
	public static final String TOPIC_EXCHANGE="topic.exchange";
	public static final String FANOUT_EXCHANGE="fanout.exchange";
	public static final String HEADER_EXCHANGE="header.exchange";
	public static final String ROUTINIG_KEY1="topic.key1";
	public static final String ROUTINIG_KEY2="topic.#";
	/**
	 * Direct模式,交换机Exchange:
	 * 发送者,将消息往外面发送的时候,并不是直接投递到队列里面去,而是先发送到交换机上面,然后由交换机发送数据到queue上面去,
	 * 做了依次路由。
	 */
	@Bean
	public Queue queue() {
		//名称,是否持久化
		return new Queue(QUEUE,true);
	}
	
	@Bean
	public Queue miaoshaqueue() {
		//名称,是否持久化
		return new Queue(MIAOSHA_QUEUE,true);
	}
}

  • MiaoshaMessage类
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;
	}
}
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值