一切福田,不離方寸,從心而覓,感無不通。

服务级高并发秒杀优化(RabbitMQ+接口优化)

1. 安装RabbitMQ

linux下的安装没什么可说的,因为本机懒得重装虚拟机了,所以就下载了windows版本进行安装。

erlang下载地址:http://www.erlang.org/download.html

rabbitMQ下载:http://www.rabbitmq.com/download.html

直接下载安装即可。

因为想用可视化界面监控消息,所以先激活这个功能。

 

然后重启rabbitMQ服务。输入网址:http://localhost:15672/。 使用默认用户guest/guest进入网页端控制台。

2. rabbitMQ基本原理和使用

rabbitMQ原理

  • Broker:简单来说就是消息队列服务器实体。
  • Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
  • Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
  • Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
  • Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
  • vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
  • producer:消息生产者,就是投递消息的程序。
  • consumer:消息消费者,就是接受消息的程序。
  • channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。

消息队列的使用过程大概如下

  • 客户端连接到消息队列服务器,打开一个channel。
  • 客户端声明一个exchange,并设置相关属性。
  • 客户端声明一个queue,并设置相关属性。
  • 客户端使用routing key,在exchange和queue之间建立好绑定关系。
  • 客户端投递消息到exchange。

总结:exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。

Direct交换机

完全根据key进行投递的叫做Direct交换机,例如,绑定时设置了routing key为”abc”,那么客户端提交的消息,只有设置了key为”abc”的才会投递到队列。

所有发送到Direct Exchange的消息被转发到RouteKey中指定的Queue。

Direct模式,可以使用rabbitMQ自带的Exchange:default Exchange 。所以不需要将Exchange进行任何绑定(binding)操作 。消息传递时,RouteKey必须完全匹配,才会被队列接收,否则该消息会被抛弃。

Topic交换机

对key进行模式匹配后进行投递的叫做Topic交换机。*(星号)可以代替一个任意标识符 ;#(井号)可以代替零个或多个标识符。

在上图例子中,我们发送描述动物的消息。消息会转发给包含3个单词(2个小数点)的路由键绑定的队列中。绑定键中的第一个单词描述的是速度,第二个是颜色,第三个是物种:“速度.颜色.物种”。
我们创建3个绑定:Q1绑定键是“.orange.”,Q2绑定键是“..rabbit”,Q3绑定键是“lazy.#”。这些绑定可以概括为:Q1只对橙色的动物感兴趣。Q2则是关注兔子和所有懒的动物。

所有发送到Topic Exchange的消息被转发到所有关心RouteKey中指定Topic的Queue上,

所有发送到Topic Exchange的消息被转发到所有关心RouteKey中指定Topic的Queue上,

Exchange 将RouteKey 和某Topic 进行模糊匹配。此时队列需要绑定一个Topic。可以使用通配符进行模糊匹配,符号“#”匹配一个或多个词,符号“”匹配不多不少一个词。因此“log.#”能够匹配到“log.info.oa”,但是“log.只会匹配到“log.error”。

Fanout交换机

还有一种不需要key的,叫做Fanout交换机,它采取广播模式,一个消息进来时,投递到与该交换机绑定的所有队列。

所有发送到Fanout Exchange的消息都会被转发到与该Exchange 绑定(Binding)的所有Queue上。

Fanout Exchange 不需要处理RouteKey 。只需要简单的将队列绑定到exchange 上。这样发送到exchange的消息都会被转发到与该交换机绑定的所有队列上。类似子网广播,每台子网内的主机都获得了一份复制的消息。

所以,Fanout Exchange 转发消息是最快的。

Headers交换机

首部交换机是忽略routing_key的一种路由方式。路由器和交换机路由的规则是通过Headers信息来交换的,这个有点像HTTP的Headers。将一个交换机声明成首部交换机,绑定一个队列的时候,定义一个Hash的数据结构,消息发送的时候,会携带一组hash数据结构的信息,当Hash的内容匹配上的时候,消息就会被写入队列

绑定交换机和队列的时候,Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串(string)。

持久化

RabbitMQ支持消息的持久化,也就是数据写在磁盘上,为了数据安全考虑,我想大多数用户都会选择持久化。消息队列持久化包括3个部分:
– exchange持久化,在声明时指定durable => 1
– queue持久化,在声明时指定durable => 1
– 消息持久化,在投递时指定delivery_mode => 2(1是非持久化)

如果exchange和queue都是持久化的,那么它们之间的binding也是持久化的。如果exchange和queue两者之间有一个持久化,一个非持久化,就不允许建立绑定。

3. rabbitMQ-Direct交换机

这种模式是最简单的模式,就发送一串字符串,这个字符串为key,接收的时候也完全以这个字符串本来来确定,不需要绑定任何exchange,使用默认的就行。我们以这个模式开始在原来的项目上继续集成。

首先是引入依赖:

 

appilication.yml:

 

rabbitMQ配置类MQConfig:

 

发送者MQSender:

 

接收者MQReceiver:

 

这样,就完成了最简单的一个字符串的发送-接受。可以在controller中随便测试一下:

 

4. rabbitMQ-Topic交换机

这个模式正如上面所言,是可以匹配通配符的,显然更加灵活,这里用程序测试一下这个模式效果。

MQConfig:

先来几个常量:

 

下面配置几个bean:

注:带有 @Configuration 的注解类表示这个类可以使用 Spring IoC 容器作为 bean 定义的来源。@Bean 注解告诉 Spring,一个带有 @Bean 的注解方法将返回一个对象,该对象应该被注册为在 Spring 应用程序上下文中的 bean。

 

MQSender:

 

MQReceiver:

 

最后测试一把:

 

运行结果:

 

运行结果与初期的分析结果一致。

5. rabbitMQ-Fanout交换机

这种就是广播模式,即所有的绑定到指定的exchange上的queue都可以接收消息。

MQConfig:

 

MQSender:

 

MQReceiver:

 

运行结果:

 

queue1和queue2都接受到了消息。

6. rabbitMQ-Headers交换机

MQConfig:

 

MQSender:

 

MQReceiver:

 

7. 秒杀优化

思路:减少数据库访问

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

对于之前的秒杀接口do_miaosha:

 

这里判断库存是直接从数据库查,因为并发量比较大,存在性能问题。后面秒杀到之后,也不是直接减库存, 而是将其放到消息队列中慢慢交给数据库去调整。

 

在消息队列中对消息进行消化:

 

对于controller中的优化1:redis预减库存。那么需要在系统启动的时候将秒杀商品的库存先添加到redis中:

 

重写afterPropertiesSet()方法:

 

对于前端,这时也要进行修改了,因为点击秒杀商品按键后,这里考虑三种情况:排队等待、失败、成功。那么这里规定-1为失败,0为排队,1为秒杀成功已经写入数据库。

原来的detail.htm中秒杀事件函数:

 

秒杀到商品就直接返回,现在后端改为消息队列,所以需要增加函数进行判断,必要时需要轮询:

 

所以将其改为:

 

那么相应地,后台也要增加一个方法:result

 

那么如何标记状态呢?这就是getMiaoshaResult方法所做的事情。

对于成功的状态判断,很简单,从数据库查,能查到就说明已经秒杀成功,否则就是两种情况:失败或者正在等待生成订单。

对于这两种状态,我们需要用redis来实现,思路是:在系统初始化的时候,redis中设置秒杀商品是否卖完的状态为false—即未卖完;

 

在MiaoshaService中的Miaosha方法:数据库减库存失败的话,说明数据库的库存已经小于0了,那么这个时候,立即将redis初始设置的秒杀商品是否卖完的状态为true,表示商品已经全部卖完,返回秒杀失败。否则就是要前端等待等待。

 

对于两个小方法getGoodsOver和setGoodsOver:

 

那么redis预减库存,然后消息队列来进行创建订单就实现了。

当然,对于redis预减库存这一点,还有要优化的地方,就是现在的do_miaosha接口是这样的:

 

但是,当秒杀商品已经没得时候,就没有必要再去redis中进行判断了,毕竟查询redis也是需要网络开销的,解决思路是:在内存中进行判断,如果redisService.decr得到的stock少于零的时候,直接将内存中的一个标志改变一下,那么下次再进入do_miaosha接口,先判断内存这个标记,如果库存已经小于0了,就不再访问redis,而是直接返回秒杀商品已经卖完。

 

声明一个map:

 

那么在afterPropertiesSet这个系统加载的初始化方法中对这个map进行初始化,goodsId–stock:

 

在原来的redis预减库存初,发现库存小于0 ,就改为true:

 

最后do_miaosha接口变为:

 

ok,整个关于redis预减库存和rabbitMQ创建订单这个优化已经基本完成了。