秒杀项目学习第六章

主要内容

  1. Redis预见库存减少数据库访问
  2. 内存标记减少Redis访问
  3. RabbitMQ队列缓冲,异步下单,增强用户体验
  4. RabbitMQ安装与Spring Boot集成
  5. 访问Nginx水平扩展
  6. 压测

思路:减少数据库访问

  1. 系统初始化,把商品库存数量加载到Redis中
  2. 收到请求,Redis预减库存,库存不足,直接返回,否则进入3
  3. 请求入队,立即返回排队中
  4. 请求出队,生成订单,减少库存
  5. 客户端轮询,是否秒杀成功

一、集成RabbitMQ

安装并启动RabbitMQ

  • 安装erlang
  • 安装RabbitMQ
  • 启动RabbitMQ
./rabbitmq-server 
netstat -nap|grep 5672
./rabbitmqctl stop

导包

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
   </dependency>

配置

#rabbitmq
spring.rabbitmq.host=192.168.174.10
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

二、RabbitMQ的使用介绍

发送者先把数据发送到交换机上然后交换机在发送到对应的queue,由此引出四种模式

新建rabbitmq包

Direct模式的使用

1.MQConfig

@Configuration
public class MQConfig {
	public static final String QUEUE = "queue";
	/**
	 * Direct模式 交换机Exchange
	 * */
	@Bean
	public Queue queue() {
		return new Queue(QUEUE, true);
	}
}

2.MQSender

@Service
public class MQSender {
	private static Logger log = LoggerFactory.getLogger(MQSender.class);
	
	@Autowired
	AmqpTemplate amqpTemplate ;
	
	public void send(Object message) {
		String msg = RedisService.beanToString(message);
		log.info("send message:"+msg);
		amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
	}
}

3.MQReceiver

@Service
public class MQReceiver {

	private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

	@RabbitListener(queues=MQConfig.QUEUE)
	public void receive(String message) {
		log.info("receive message:"+message);
	}
}

4.写个controller测试一波
在这里插入图片描述在这里插入图片描述

Topic模式的使用

交换机根据key把消息发送到对应绑定的queue
1.MQConfig

@Configuration
public class MQConfig {
	
	public static final String TOPIC_QUEUE1 = "topic.queue1";
	public static final String TOPIC_QUEUE2 = "topic.queue2";
	public static final String TOPIC_EXCHANGE = "topicExchage";
	
	/**
	 * Topic模式 交换机Exchange
	 * */
	@Bean
	public Queue topicQueue1() {
		return new Queue(TOPIC_QUEUE1, true);
	}
	@Bean
	public Queue topicQueue2() {
		return new Queue(TOPIC_QUEUE2, true);
	}
	@Bean
	public TopicExchange topicExchage(){
		return new TopicExchange(TOPIC_EXCHANGE);
	}
	@Bean
	public Binding topicBinding1() {
		return BindingBuilder.bind(
				topicQueue1())
				.to(topicExchage()).
						with("topic.key1");
	}
	@Bean
	public Binding topicBinding2() {
		//*代表一个单词,#代表多个单词
		return BindingBuilder.bind(
				topicQueue2())
				.to(topicExchage()).
						with("topic.#");
	}
}

2.MQSender

@Service
public class MQSender {
	private static Logger log = LoggerFactory.getLogger(MQSender.class);
	
	@Autowired
	AmqpTemplate amqpTemplate ;
	
	public void sendTopic(Object message) {
		String msg = RedisService.beanToString(message);
		log.info("send topic message:"+msg);
		//也就是交换机会根据key的值传递给绑定的queue,可以传递给多个queue只要匹配上
		amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg+"1");
		amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg+"2");
	}
}

3.MQReceiver

@Service
public class MQReceiver {

	private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

	@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
	public void receiveTopic1(String message) {
		log.info(" topic  queue1 message:"+message);
	}

	@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
	public void receiveTopic2(String message) {
		log.info(" topic  queue2 message:"+message);
	}
}
		

4.写个controller测试一波

在这里插入图片描述在这里插入图片描述

Fanout模式的使用

交换机把消息发送给所有队列
1.MQConfig

@Configuration
public class MQConfig {
	
	public static final String TOPIC_QUEUE1 = "topic.queue1";
	public static final String TOPIC_QUEUE2 = "topic.queue2";
	public static final String FANOUT_EXCHANGE = "fanoutxchage";
	
	/**
	 * Fanout模式 交换机Exchange
	 * */
	  
	@Bean
	public Queue topicQueue1() {
		return new Queue(TOPIC_QUEUE1, true);
	}
	@Bean
	public Queue topicQueue2() {
		return new Queue(TOPIC_QUEUE2, true);
	}
	@Bean
	public FanoutExchange fanoutExchage(){
		return new FanoutExchange(FANOUT_EXCHANGE);
	}
	@Bean
	public Binding FanoutBinding1() {
		return BindingBuilder.bind(topicQueue1()).to(fanoutExchage());
	}
	@Bean
	public Binding FanoutBinding2() {
		return BindingBuilder.bind(topicQueue2()).to(fanoutExchage());
	}
}

2.MQSender

@Service
public class MQSender {
	private static Logger log = LoggerFactory.getLogger(MQSender.class);
	
	@Autowired
	AmqpTemplate amqpTemplate ;
	
	public void sendFanout(Object message) {
		String msg = RedisService.beanToString(message);
		log.info("send fanout message:"+msg);
		//不用添加key,广播发送
		amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE, "", msg);
	}
}

3.MQReceiver

@Service
public class MQReceiver {

	private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

	@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
	public void receiveTopic1(String message) {
		log.info(" topic  queue1 message:"+message);
	}

	@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
	public void receiveTopic2(String message) {
		log.info(" topic  queue2 message:"+message);
	}
}

4.写个controller测试一波
在这里插入图片描述
在这里插入图片描述

Header模式的使用

发送的是原始数据加头部信息,如果头部信息匹配了则发送到对应的queue
1.MQConfig

public class MQConfig {
	
	public static final String HEADER_QUEUE = "header.queue";
	public static final String HEADERS_EXCHANGE = "headersExchage";
	/**
	 * Header模式 交换机Exchange
	 * */
	@Bean
	public HeadersExchange headersExchage(){
		return new HeadersExchange(HEADERS_EXCHANGE);
	}
	@Bean
	public Queue headerQueue1() {
		return new Queue(HEADER_QUEUE, true);
	}
	@Bean
	public Binding headerBinding() {
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("header1", "value1");
		map.put("header2", "value2");
		return BindingBuilder.bind(
				 headerQueue1()).
				 to(headersExchage()).
				 whereAll(map).match();
	}
}

2.MQSender

@Service
public class MQSender {
	private static Logger log = LoggerFactory.getLogger(MQSender.class);
	
	@Autowired
	AmqpTemplate amqpTemplate ;
	
	public void sendHeader(Object message) {
		String msg = RedisService.beanToString(message);
		log.info("send header message:"+msg);
		MessageProperties properties = new MessageProperties();
		properties.setHeader("header1", "value1");
		properties.setHeader("header2", "value2");
		Message obj = new Message(msg.getBytes(), properties);
		//发送的是原始数据加头部信息,如果头部信息匹配了则发送到对应的queue
		amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", obj);
	}
}

3.MQReceiver

@Service
public class MQReceiver {

	private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

	@RabbitListener(queues=MQConfig.HEADER_QUEUE)
	public void receiveHeaderQueue(byte[] message) {
		log.info(" header  queue message:"+new String(message));
	}
}

4.写个controller测试一波
在这里插入图片描述
在这里插入图片描述

三、秒杀接口优化

分析以前的流程

在这里插入图片描述

优化思路

  1. 系统初始化,把商品库存数量加载到Redis
  2. 收到请求,Redis预见库存,库存不足,直接返回,否则进入3
  3. 请求入队,立即返回排队中(异步下单)
  4. 请求出队,生成订单,减少库存,把订单写入Redis中
  5. 客户端轮询,判断是否秒杀成功

代码实现

1.系统初始化时把库存加载到数据库
MiaoshaController 继承InitializingBean实现afterPropertiesSet方法即可

@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {
	。。。忽略其他代码
	@Autowired
	RedisService redisService;
	
	@Autowired
	GoodsService goodsService;
	//系统初始化时把商品库存读取到Redis中
	@Override
	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());
		}
	}

2.修改miaosha方法

	@RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
	@ResponseBody
	public Result<Integer> miaosha(Model model, MiaoshaUser user,
									 @RequestParam("goodsId")long goodsId) {
		model.addAttribute("user", user);
		if(user == null) {
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		//redis预减库存
		//预减库存
		long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
		if(stock < 0) {
			return Result.error(CodeMsg.MIAO_SHA_OVER);
		}
		//判断是否已经秒杀到了
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
		if(order != null) {
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//入队(以前是减库存,下订单,写入秒杀订单)
		MiaoshaMessage mm = new MiaoshaMessage();//见3
		mm.setUser(user);
		mm.setGoodsId(goodsId);
		sender.sendMiaoshaMessage(mm);//见4
		return Result.success(0);//排队中
	}
}

3.MiaoshaMessage

public class MiaoshaMessage {
	private MiaoshaUser user;
	private long goodsId;

4.RabbitMQ相关
MQSender.sendMiaoshaMessage

@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);
	}
}

MQReceiver
主要完成写入秒杀订单

@Service
public class MQReceiver {

		private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
		
		@Autowired
		RedisService redisService;
		
		@Autowired
		GoodsService goodsService;
		
		@Autowired
		OrderService orderService;
		
		@Autowired
		MiaoshaService miaoshaService;
		
		@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)
		public void receive(String message) {
			log.info("receive message:"+message);
			MiaoshaMessage mm  = RedisService.stringToBean(message, MiaoshaMessage.class);
			MiaoshaUser user = mm.getUser();
			long goodsId = mm.getGoodsId();
			//判断数据库中的库存
			GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
	    	int stock = goods.getStockCount();
	    	if(stock <= 0) {
	    		return;
	    	}
	    	//判断是否已经秒杀到了,其实因为表中加了唯一索引也可以不用这一步
	    	//但是无论如何都会访问一次数据库--要么查询,要么更新
	    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
	    	if(order != null) {
	    		return;
	    	}
	    	//减库存 下订单 写入秒杀订单
	    	miaoshaService.miaosha(user, goods);
		}
	}

MQConfig

@Configuration
public class MQConfig {
	
	public static final String MIAOSHA_QUEUE = "miaosha.queue";
	
	/**
	 * Direct模式 交换机Exchange
	 * */
	@Bean
	public Queue queue() {
		return new Queue(MIAOSHA_QUEUE, true);
	}
}

几个问题

1.下图中如果减库存失败了可以直接返回所以修改一下
在这里插入图片描述
解决
在这里插入图片描述2.下图中insert返回的并不是订单id而是插入的数量
在这里插入图片描述

解决
在这里插入图片描述

5.处理页面
goods_details.htm页面
在这里插入图片描述getMiaoshaResult

function getMiaoshaResult(goodsId){
        g_showLoading();
        $.ajax({
        //见6
            url:"/miaosha/result",
            type:"GET",
            data:{
                goodsId:$("#goodsId").val(),
            },
            success:function(data){
                if(data.code == 0){
                    var result = data.data;
                    if(result < 0){
                        layer.msg("对不起,秒杀失败");
                    }else if(result == 0){//继续轮询
                        setTimeout(function(){
                            getMiaoshaResult(goodsId);
                        }, 50);
                    }else{
                        layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]},
                            function(){
                                window.location.href="/order_detail.htm?orderId="+result;
                            },
                            function(){
                                layer.closeAll();
                            });
                    }
                }else{
                    layer.msg(data.msg);
                }
            },
            error:function(){
                layer.msg("客户端请求有误");
            }
        });
    }

6.处理逻辑
MiaoshaController
在这里插入图片描述
MiaoshaService
在这里插入图片描述为了能够判断出是否卖完了还是RabbitMQ还没来得及处理,在Redis中做一个标记,而做标记的时间就是判断出已经卖完了的时候,所以如下图:
在这里插入图片描述

内存标记减少Redis访问

看红框里的,秒杀前预减库存,如果已经减到0了后续的判断时还是会访问Redis判断,我们可以想办法让Redis中库存已经为0时,不在访问Redis判断
在这里插入图片描述
解决方法
1.设置本地标记,并全部初始化为没有秒杀完,即false
在这里插入图片描述
2.在Redis预减库存前先从本地map中判断
在这里插入图片描述

四、Nginx横向扩展配置

找到nginx.conf文件,这里只是简单介绍。

反向代理

在这里插入图片描述

负载均衡

在这里插入图片描述

开启缓存

在这里插入图片描述

五、一般网站架构

在这里插入图片描述
或者再加一层LVS应付更大的并发
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值