- 首先先确定是否相应的消费者

- 3.14上有多个容器,如何确定是哪个容器运行的呢?
- 由于在linux的docker中,容器里运行的进程会直接通过宿主机执行,所以可以通过查看是否有对应进程
bash
ps aux | grep cront/trades/Cashier_trade/payed

- 获取对应进程的父级pid(也就是运行该脚本的pm2进程),比如这里选择第一个进程的pid为2163,获取到上级pid为29217
bash
# ps -ef|awk '$2 ~ /pid/{print $3}'
ps -ef|awk '$2 ~ /2163/{print $3}'

- 再获取pm2的上级(即运行容器的pid),获取到为24450
bash
ps -ef|awk '$2 ~ /29217/{print $3}' - 查看容器pid相对应的进程
bash
ps aux | grep 24450

- 有一串容器编号,复制前一小段,查询对应容器
bash
docker ps | grep 0d7eccf5ab

- 由于在linux的docker中,容器里运行的进程会直接通过宿主机执行,所以可以通过查看是否有对应进程
【初阶】基于redis的抢购秒杀设计
秒杀系统架构思路
应用场景
限时抢购,10w人抢购100个商品。这种情况下如果直接走到数据存储层,瞬间会把数据库打挂掉,如果业务处理不好(流程过长),会出现超卖现象
优化方向
尽量将请求拦截在上游
充分利用缓存
1. 前端方案
- 页面静态
- 防重复提交
- 用户限流
2. 后端方案
- 网关限制
- 缓存
- 消息队列削峰
优化细节
这里我们由简单往细节深入(因为要考虑实际的用户量,先做最简单最有效的处理)
简单高效处理方式
- 前端防重复提交
这是最基本的 - redis缓存校验
如果没有缓存,由于业务校验时间过长,比如在200ms内10w个人同时读了数据库库存仅剩1,都是可购买的,然后同时走到下单流程就会超卖,另外就是如果并发量过高,数据库会挂掉。
如果是缓存校验,则只有一个可下单。其余全部直接拦截。
redis数据类型选型
如果不考虑抢购的下单数量和购买限制大于1,那直接kv或者list都是可以的。否则可以考虑hash或者zset。
这里对比一下kv和list:
kv的一般处理方式是预加或者预减:累计购买数量超过总库存或者剩余库存小于0,则校验不通过,同时回退。
list:预存库存长度的list,不断pop,无法pop则校验不通过。
tip:注意原子性
判断最好走lua。
比如kv,如果库存1,10人同时下单,如果是程序判断,则全部不能下单。而如果用lua,用户判断和回退是原子性的,则有一个人可以下单成功。
如果使用list,当抢购数量大于1时,回退也需要用事务,不然会出现,比如库存3,A下单5,B下单2。B在回退push的过程中,又被Apop了1从而判断库存不足。导致两人都不能下单。
复杂完善处理方式
- redis集群,主从同步,读写分离(读多写少)。
- nginx负载均衡
- 前端资源静态化:只有少部分内容是动态的的
- 按钮控制
- 缓存预热
- mq削峰:队列下单
详细内容可以参考大佬们的文章,我只是一个搬运工
架构设计图:

参考文档:
https://www.zhihu.com/question/54895548
https://yq.aliyun.com/articles/69704?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&utm_content=m_10737
使用HAProxy实现RabbitMq集群负载均衡
一、目的
在使用 RabbitMq集群时可能会遇到集群中某个节点出现异常或者连接数过多的情况,这个时候与该节点连接的Consumer将会断开,Publisher也会无法将消息发送至集群。为了解决这些问题,本文中将使用 HAProxy来代理集群,实现多个节点的负载均衡以及在某个节点异常时自动将连接切换至其他正常节点等功能。
二、HAProxy安装配置(Centos 7)
- 安装
HAProxy- 下载最新稳定版
2.0.8并解压// PWD:/opt/ci123/www/html/rabbitMq/ wget https://www.haproxy.org/download/2.0/src/haproxy-2.0.8.tar.gz tar xf haproxy-2.0.8.tar.gz - 查看系统内核版本来指定编译版本
uname -r 3.10.0-862.6.3.el7.x86_64版本参考:
```
</ol>
<ul>
<li>linux22 for Linux 2.2</li>
<li>linux24 for Linux 2.4 and above (default)</li>
<li>linux24e for Linux 2.4 with support for a working epoll (> 0.21)</li>
<li>linux26 for Linux 2.6 and above</li>
<li>linux2628 for Linux 2.6.28, 3.x, and above (enables splice and tproxy)</li>
<li>solaris for Solaris 8 or 10 (others untested)</li>
<li>freebsd for FreeBSD 5 to 10 (others untested)</li>
<li>netbsd for NetBSD</li>
<li>osx for Mac OS/X</li>
<li>openbsd for OpenBSD 5.7 and above</li>
<li>aix51 for AIX 5.1</li>
<li>aix52 for AIX 5.2</li>
<li>cygwin for Cygwin</li>
<li>haiku for Haiku</li>
<li>generic for any other OS or version.</li>
<li><p>custom to manually adjust every setting
```根据版本参考,这里我们选择
linux2628版本进行编译。 - 下载最新稳定版
-
编译到指定目录
cd haproxy-2.0.8 make TARGET=linux2628 PREFIX=/opt/ci123/haproxy // 这里出现报错,从2.0版本开始linux2628已被废弃 Target 'linux2628' was removed from HAProxy 2.0 due to being irrelevant and often wrong. Please use 'linux-glibc' instead or define your custom target by checking available options using 'make help TARGET=<your-target>'. // 根据提示修改参数后编译 make TARGET=linux-glibc PREFIX=/opt/ci123/haproxy make install PREFIX=/opt/ci123/haproxy - 配置
- 复制
haproxy命令至全局变量cp /opt/ci123/haproxy/sbin/haproxy /usr/bin/ - 创建系统用户
useradd -r haproxy - 添加
haproxy配置文件haproxy配置文件由五部分组成:
global: 参数是进程级的,通常和操作系统相关。这些参数一般只设置一次,如果配置无误,就不需要再次配置进行修改。
default:默认参数。frontend:用于接收客户端请求的前端节点,可以设置相应的转发规则来指定使用哪个backend。backend:后端服务器代理配置,可实现代理多台服务器实现负载均衡、为请求添加额外报文数据等功能。listen:是frontend和backend的结合,通常只对tcp流量有用。
- 添加配置文件
/opt/ci123/haproxy/conf/haproxy.cfg```
# 全局配置
global
log 127.0.0.1 local3 # 设置日志
pidfile /opt/ci123/haproxy/logs/haproxy.pid
maxconn 4000 # 最大连接数
user haproxy
group haproxy
daemon # 守护进程运行# 默认配置
defaults
log global
mode tcp # 默认的模式mode { tcp|http|health },tcp是4层,http是7层,health只会返回OK
option httplog # http 日志格式,仅在http模式下可用
option dontlognull # 不记录健康检查日志信息;
option redispatch # serverId对应的服务器挂掉后,强制定向到其他健康的服务器
option http-server-close
#option abortonclose # 当服务器负载很高的时候,自动结束掉当前队列处理比较久的链接;
#option forwardfor # 如果后端服务器需要获得客户端真实ip需要配置的参数,可以从Http Header中获得客户端ip;
#option httpclose # 主动关闭http通道,每次请求完毕后主动关闭http通道,ha-proxy不支持keep-alive,只能模拟这种模式的实现;<br />
balance roundrobin # 负载均衡算法,轮询;
retries 3 # 重试次数;<pre class="prism-highlight line-numbers" data-start="1"><code class="language-null"> timeout http-request 10s # 客户端建立连接但不请求数据时,关闭客户端连接;
timeout queue 1m # 高负载响应haproxy时,会把haproxy发送来的请求放进一个队列中,timeout queue定义放入这个队列的超时时间;
timeout connect 10s # 定义haproxy将客户端请求转发至后端服务器所等待的超时时间;
timeout client 1m # 客户端非活动状态的超长时间(默认毫秒)
timeout server 1m # 服务端与客户端非活动状态连接的超时时间。(默认毫秒)
timeout http-keep-alive 10s # 定义保持连接的超时时长;
timeout check 10s # 心跳检测超时;
maxconn 3000 # 每个server最大的连接数;
</code></pre>#前端配置
frontend rabbitmq_cluster_front
bind 0.0.0.0:10000 # http请求的端口,会被转发到设置的ip及端口
default_backend rabbitmq_cluster_back# 后端配置
backend rabbitmq_cluster_back
#roundrobin 轮询方式
balance roundrobin # 负载均衡的方式,轮询方式<pre class="prism-highlight line-numbers" data-start="1"><code class="language-null"> # 配置Rabbitmq连接负载均衡
# 需要转发的ip及端口
# inter 2000 健康检查时间间隔2秒
# rise 3 检测多少次才认为是正常的
# fall 3 失败多少次才认为是不可用的
# weight 30 权重
server clusterRabbit1 192.168.3.14:5672 check inter 2000 rise 3 fall 3 weight 30
server clusterRabbit2 192.168.3.14:5673 check inter 2000 rise 3 fall 3 weight 30
</code></pre># 统计页面配置
listen admin_stats<br />
bind 0.0.0.0:10080 # 监听IP和端口,为了安全可以设置本机的局域网IP及端口;
mode http
option httplog # 采用http日志格式<br />
stats refresh 30s # 统计页面自动刷新时间<br />
stats uri /haproxy # 状态管理页面,通过/haproxy来访问
stats realm Haproxy Manager # 统计页面密码框上提示文本<br />
stats auth duomai:shijiemori@2012 # 统计页面用户名和密码设置<br />
#stats hide-version # 隐藏统计页面上HAProxy的版本信息
```- 配置日志
rsyslog
vim /etc/rsyslog.conf # 取消如下2行注释 $ModLoad imudp $UDPServerRun 51 # 新增配置(自定义的日志设备) local3.* /opt/ci123/haproxy/logs/haproxy.log # 重启rsyslog服务 systemctl restart rsyslog- 启动
haproxy
haproxy -f /opt/ci123/haproxy/conf/haproxy.cfg访问统计页面出现如下界面:

- 配置日志
- 复制
三、集群测试
沿用上一篇【RabbitMq 镜像队列集群搭建】中的集群测试环境,在测试中将Publiser和Consumer的连接替换为HAProxy的地址192.168.3.14:10000。
- 测试环境
- 节点:
- 节点一:
clusterRabbit1- 端口:
192.168.3.14:5672- 节点二:
clusterRabbit2- 端口:
192.168.3.14:5673
- 队列:
- 节点一
rabbit@clusterRabbit1:- 队列一:
name:clusterRabbit1Queue1routing_key:clusterRabbit1key
- 队列二:
name:clusterRabbit1Queue2routing_key:clusterRabbitCommonKey
- 节点二
rabbit@clusterRabbit2:- 队列三:
name:clusterRabbit2Queue1routing_key:clusterRabbit2key
- 队列四:
name:clusterRabbit2Queue2routing_key:clusterRabbitCommonKey(与队列二 相同)
- 节点一
- 节点:
- 启动消费者
- 消费者
- 消费者一:
- 连接节点:节点一
rabbit@clusterRabbit1 - 消费队列:队列一
clusterRabbit1Queue1- 消费者二:
- 连接节点:节点一
rabbit@clusterRabbit1 - 消费队列:队列二
clusterRabbit1Queue2- 消费者三:
- 连接节点:节点二
rabbit@clusterRabbit2 - 消费队列:队列三
clusterRabbit2Queue1- 消费者四:
- 连接节点:节点二
rabbit@clusterRabbit2 - 消费队列:队列四
clusterRabbit2Queue2
- 启动结果:
启动成功,但是在一分钟后客户端异常退出,原因是
HAProxy设置了timeout client 1m 和 timeout server 1m,消费者在一分钟内都没有接收到消息导致被判定为不活跃连接从而被删除。由于
HAProxy默认不支持长连接,上述问题可以使用pm2管理消费者的方法来解决,消费者进程在不活跃退出后pm2将自动重启此进程。
- 消费者
-
发布消息
- 发布
clusterRabbit1key消息
- 消费者一 成功收到消息。
- 发布
clusterRabbitCommonKey消息
- 发布
- 消费者二/四 成功收到消息。
- 发布
-
关闭节点二
rabbit@clusterRabbit2后再次发布消息
- 连接节点二的消费者先退出后重新使用
HAProxy成功连接。 - 发布的消息均能被成功消费。
四、使用说明
HAProxy默认不支持tcp长连接,需要使用PM2之类的守护进程管理工具或者长连接技术来实现rabbitMq客户端持续连接。HAProxy通过活跃检测机制来判定负载均衡中的节点是否可用, 当rabbitMq集群中某个节点不可用时,在经过一段时间的活跃检测之后,HAProxy将弃用该节点直至节点恢复。在这种情况下,rabbitMq消费者将断开连接后选择剩余可用的节点再次启动,客户端发布时也会自动选择剩余可用的节点。- 在
rabbitMq集群中某个节点宕机之后,HAProxy会自动使用可用的集群节点,所以不会出现在HAProxy活跃检测期间发布消息出现一半成功一半失败的情况,所有的消息都将通过可用的集群节点发布至集群。
延迟队列使用说明
一、应用场景
- 对于商城系统而言,定时任务会非常多,比如优惠券过期、订单定时关闭、2小时订单未支付自动取消等等。常用做法是写数据库指定过期时间,定时循环读表,当数据量一大,这种会严重影响性能,产生巨大的io。
- 而另外一些基于redis实现的延迟队列,也是基于pull拉模式去实现的,也会产生io,只是变成了内存读取。
- 一种好的做法是 push 推模式,服务端主动推送,这里就是rabbitmq的死信队列去实现
二、延迟队列原理
延迟队列一般分为两种:
- 基于消息的延迟和基于队列的延迟。基于消息的延迟是指为每条消息设置不同的延迟时间,那么每当队列中有新消息进入的时候就会重新根据延迟时间排序,当然这也会对性能造成极大的影响。
- 实际应用中大多采用基于队列的延迟,设置不同延迟级别的队列,比如 5s、10s、30s、1min、5mins、10mins 等,每个队列中消息的延迟时间都是相同的,这样免去了延迟排序所要承受的性能之苦,通过一定的扫描策略(比如定时)即可投递超时的消息。
- 这里主要通过rabbitmq的死信队列实现,需要同时声明一个缓冲队列和一个延迟队列,给缓冲队列设置统一消息过期时间,当消息过期后,会自动被重新投递到死信队列(死信队列也是一个普通队列),只需要监听死信队列即可
三、api项目使用说明
- 首先需要定义延迟消费队列:即不管是订单关闭,还是优惠券过期的后续处理,都需要去监听这个消息
在 application/libraries/mq/MqDlxQueue 去定义

- 运行声明脚本
php mq/StartDlx.php DELAY_QUEUE_TEST - 定义缓冲区队列:即所有投递到这个队列的消息,设置统一的过期时间,比如固定2小时关闭订单,可以设置统一的队列过期时间为2小时。
这里需要指定 dlx_key:即过期后投递到的死信队列的路由
需要指定 message_ttl:即统一的消息过期时间,单位 毫秒注意:该 缓冲队列的 key,不要有任何消费者,不然被其他消费者消费后,就不会过期
在 application/libraries/mq/MqQueue 去定义

- 声明 缓冲队列
php mq/Start.php BUFFER_QUEUE_TEST - 设置 延迟队列消费者

- 发送 消息到 缓冲区

-
测试效果:


RabbitMq 镜像队列集群搭建
建议食用本文前请先阅读【RabbitMq 普通集群搭建】篇
一、镜像队列集群概念
镜像队列是基于普通集群模式的扩展,普通集群模式下如果某一个节点宕机,该节点下的队列操作将完全失效。而在镜像队列模式下,队列的数据将被复制到所有节点(或者配置过的节点)中,从而保证了一个节点宕机,其余节点也可以正常消费此消息。但此模式下也必然会带来性能下降、内存/磁盘消耗增加、网络IO负担增加等问题,所以镜像队列适用于对高可用要求比较高的系统。
- 每个镜像队列由一个主队列和一个或多个镜像组成。每个镜像队列都有自己的主节点,主队列存放于主节点上。对队列产生的操作将首先应用于队列的主节点,然后传播到镜像节点。包括发布队列、向消费者传递消息、跟踪来自消费者的确认等行为。
- 发布到集群中的消息将被复制到所有的镜像队列中,消费者连接任意节点消费队列实际上都将被连接至主队列的节点上。如果主队列已经确认消费了消息,则其余镜像队列中的消息将被丢弃。
- 如果主队列所在的节点发生异常,默认情况下最“老”的镜像队列将被选举为主队列,当然也可以制定不同的选举策略。
二、配置镜像队列
- 将队列配置成镜像队列需要通过创建
policy来实现。policy包含策略键ha-mode和其对应的键值ha-params(可选)组成。
exactly模式ha-params为count,表示队列的总数量。count表示主队列+镜像队列的总数量,如果count为1,则表示只存在于主队列。如果count为2则表示存在主队列和一个镜像队列,以此类推。如果count值大于集群中节点的总数则表示所有节点都将同步一份镜像队列。如果某个镜像队列的节点宕机,则会寻找一个剩余未同步的节点来同步镜像队列。
all模式- 不需要
ha-params。 - 此模式下所有节点都将同步镜像队列,如果有新节点加入,则新节点也会进行同步。官方建议同步镜像队列的节点数为
N/2 + 1,其中N表示节点总数。同步到所有节点会增加所有集群节点的负载,包括网络I/O、磁盘I/O和磁盘空间的使用等。
- 不需要
nodes模式ha-params为node names,节点的名称。- 此模式下将在指定的节点上同步镜像队列,如果声明队列时其余节点均不在新,则只会在声明连接的那个节点上创建队列。
- 配置
policy
rabbitmqctl命令配置set_policy [-p vhost] [--priority priority] [--apply-to apply-to] name pattern definitionname:策略名称pattern:策略匹配符,正则表达式。当与给定资源匹配时,将应用该策略。definition:策略内容定义,JSON字符串。priority:策略的优先级,整数。数字越大,优先级越高。默认值为0。apply-to:策略应用的对象,支持queues,exchanges,all,默认值为all// exactly模式,匹配前缀为two的资源 rabbitmqctl set_policy -p cluster ha-two ^two. '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}' // all模式,匹配所有前缀 rabbitmqctl set_policy -p cluster ha-all ^ '{"ha-mode":"all"}' // nodes模式,匹配所有前缀 rabbitmqctl set_policy -p cluster ha-nodes ^ '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
management管理后台配置Admin -> Policies -> Add / update a policy
- 新镜像队列同步设置
-
ha-sync-mode:manual默认模式,新队列镜像将不同步现有消息,只接收新消息。当消费者消费完所有仅存在于主队列上的消息后,新的队列镜像将随着时间的推移成为和主节点队列相同的精确镜像队列。
-
ha-sync-mode: automatic当新队列镜像加入时,队列将自动同步所有消息。
三、额外说明
- 独占队列不会被复制为镜像队列。
- 主节点队列失效后,其中一个镜像队列将被选举为主队列并带来如下影响:
- 与主机点连接的客户端将全部断开。
- 运行时间最长的镜像队列将被选举为主队列,如果镜像队列尚未开始同步,则队列上的消息将丢失。
- 新的主队列会认为之前所有的消费者的连接都已经断开,它将重新发送旧队列中没有收到
ack的消息。这可能出现客户端已经发送过ack, 但是服务端在接收到之前就已宕机的情况,从而导致发送两遍相同的消息,因此所有未确认的消息都将使用redelivered标志重新发送。 - 如果消费者连着的是镜像队列节点,并且消费者在启动时设置了
x-cancel-on-ha-failover参数,则消费者将收到一个服务端消费取消的通知,如果未设置此参数,则消费者将无法感知主节点已宕机。 - 如果使用自动
ack机制则消息将丢失。
- 如果停止了某个包含主节点队列的节点,则其他节点的镜像队列将被选举为主节点队列。在重新启动此节点后,该节点只会被当做是一个新加入集群的节点,不会重新成为主节点队列。
- 在主节点队列宕机并且其他镜像队列尚未同步的极端情况下,
rabbitMq集群将拒绝任何镜像队列选举为主队列,整个队将不可用且被关闭。如果在镜像队列尚未同步的情况下也需要将某个镜像队列选举为主队列,需要配置policy中ha-promote-on-shutdown为always(默认为when-synced),并且ha-promote-on-failure不可配置为hen-synced(默认值为always)。
四、集群测试
-
启动节点、创建用户、
vhost、exchange、queue,启动消费者。(快速启动,参考【RabbitMq 使用docker搭建集群】篇)- 节点:
- 节点一:
rabbit@clusterRabbit1 - 节点二:
rabbit@clusterRabbit2
- 节点一:
- 用户:
api_management(administrator标签,开放虚拟主机cluster所有权限) -
策略:
rabbitmqctl set_policy -p cluster ha-all ^ '{"ha-mode":"all"}' vhost:cluster-
exchange:cluster(直连交换机) -
queue:- 节点一
rabbit@clusterRabbit1:- 队列一:
name:clusterRabbit1Queue1routing_key:clusterRabbit1key
- 队列二:
name:clusterRabbit1Queue2routing_key:clusterRabbitCommonKey
- 节点二
rabbit@clusterRabbit2:- 队列三:
name:clusterRabbit2Queue1routing_key:clusterRabbit2key
- 队列四:
name:clusterRabbit2Queue2routing_key:clusterRabbitCommonKey(与队列二 相同)
- 节点一
consumer:- 消费者一:
- 连接节点:节点一
rabbit@clusterRabbit1 - 消费队列:队列一
clusterRabbit1Queue1- 消费者二:
- 连接节点:节点一
rabbit@clusterRabbit1 - 消费队列:队列二
clusterRabbit1Queue2- 消费者三:
- 连接节点:节点二
rabbit@clusterRabbit2 - 消费队列:队列三
clusterRabbit2Queue1- 消费者四(非此节点的队列):
- 连接节点:节点一
rabbit@clusterRabbit1 - 消费队列:队列三
clusterRabbit2Queue1- 消费者五:
- 连接节点:节点二
rabbit@clusterRabbit2 - 消费队列:队列四
clusterRabbit2Queue2
- 节点:
-
连接节点二
rabbit@clusterRabbit2,再次声明队列名和队列一同名的队列clusterRabbit1Queue1。【同名队列再次声明】
- 结果:声明不成功,节点二中没有生成新的
clusterRabbit1Queue1队列,而节点一中clusterRabbit1Queue1队列多出了新绑定的routing_key:clusterRabbit2key,这意味着在同一集群中不同节点之间的队列名是唯一的,在一个节点中可以操作另一个节点的队列数据。
- 生产者连接节点一
rabbit@clusterRabbit1,发布clusterRabbit1key消息。【发布此节点队列消息】
- 消费者一 成功接收到消息。
- 生产者连接节点二
rabbit@clusterRabbi2,发布clusterRabbit1key消息。【发布非此节点队列消息】
- 消费者一 成功接收到消息。
- 生产者连接节点一
rabbit@clusterRabbit1,发布clusterRabbitCommonKey消息。【多个节点队列绑定相同消息】
- 消费者二 成功接收到消息。
- 消费者五 成功接收到消息。
- 生产者连接节点一
rabbit@clusterRabbit1,发布clusterRabbit2key消息。【消费非此节点队列消息】
- 消费三、四 轮询接收到消息。
- 关闭节点二
rabbit@clusterRabbit2,连接节点一rabbit@clusterRabbit1,发布clusterRabbit2key消息【投递消息给集群中意外退出的节点】
- 连接节点二
rabbit@clusterRabbit2的消费者全部断开,消费者四并未断开。 - 节点一
rabbit@clusterRabbit1晋升成为队列三、队列四的主节点。 - 消费者四 可以继续成功接收到消息。
- 再次启动节点二
rabbit@clusterRabbit2,没有再次成为队列三、队列四的主节点,只是复制了队列三、队列四的镜像队列。
- 修改消费者四,在
ack之前sleep(20),发布clusterRabbit2key消息,并立即关闭节点二rabbit@clusterRabbit2
- 和上面一样,消费者四并未断开。
- 消费者收到两条相同的消息,说明节点二在收到
ack之前宕机后,此消息会被当做未消费的消息重新放入队列后消费。
RabbitMq 使用docker搭建集群
建议食用本文前请先阅读【RabbitMq 普通集群搭建】篇
一、RabbitMq镜像
- 镜像:
rabbitmq:3.8-management - 启动参数:
--name: 容器名称-h / --hostname:rabbitMq默认节点的host-v:文件挂载映射-p:端口映射- 5672:容器内默认的
rabbitMq启动端口 - 15672:容器内默认的
rabbitMq管理插件启动端口
- 5672:容器内默认的
- 环境变量:(支持所有
rabbitMq环境变量)RABBITMQ_NODENAME:节点名称,缺省为rabbit@[hostname]RABBITMQ_DEFAULT_USER:默认用户名,缺省为gusetRABBITMQ_DEFAULT_PASS:默认密码,缺省为guestRABBITMQ_DEFAULT_VHOST:默认虚拟主机,缺省为/RABBITMQ_ERLANG_COOKIE:erlang.cookie值
二、搭建集群
- 启动多份
rabbitMq容器# 启动节点一 docker run -d --hostname clusterRabbit1 --name clusterRabbit1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit1:/var/lib/rabbitmq rabbitmq:3.8-management # 启动节点二 docker run -d --hostname clusterRabbit2 --name clusterRabbit2 -p 5673:5672 --link clusterRabbit1:clusterRabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit2:/var/lib/rabbitmq rabbitmq:3.8-management
/var/lib/rabbitmq是容器内部文件数据存放目录,使用-v进行文件目录挂载。- 多个容器之间使用
--link <name or id>:alias连接,否则需要自行在各个容器添加供rabbitMq相互访问的host。 Erlang Cookie值必须相同,rabbitMQ是通过Erlang实现的,Erlang Cookie相当于不同节点之间相互通讯的秘钥,Erlang节点通过交换Erlang Cookie获得认证。
- 启动集群
- 设置节点二
rabbit@clusterRabbit2,加入节点一rabbit@clusterRabbit1的集群rabbitmqctl stop_app rabbitmqctl reset rabbitmqctl join_cluster rabbit@clusterRabbit1 rabbitmqctl start_app - 查看
rabbitMq管理后台集群数据
- 设置节点二
-
一键化启动脚本
#!bin/sh # 启动节点一 docker run -d --hostname clusterRabbit1 --name clusterRabbit1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit1:/var/lib/rabbitmq rabbitmq:3.8-management # 启动节点二 docker run -d --hostname clusterRabbit2 --name clusterRabbit2 -p 5673:5672 --link clusterRabbit1:clusterRabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit2:/var/lib/rabbitmq rabbitmq:3.8-management echo "即将开始初始化" sleep 5 # 加入集群 docker exec -it clusterRabbit2 sh -c 'rabbitmqctl stop_app && rabbitmqctl reset && rabbitmqctl join_cluster rabbit@clusterRabbit1 && rabbitmqctl start_app' # 添加虚拟主机 docker exec -it clusterRabbit1 sh -c 'rabbitmqctl add_vhost cluster' # 添加用户 docker exec -it clusterRabbit1 sh -c 'rabbitmqctl add_user api_management shijiemori2012' # 添加用户权限 docker exec -it clusterRabbit1 sh -c 'rabbitmqctl set_permissions -p cluster api_management ".*" ".*" ".*"' # 设置用户标签 docker exec -it clusterRabbit1 sh -c 'rabbitmqctl set_user_tags api_management administrator'
RabbitMq 普通集群搭建
一、集群概念
RabbitMQ集群是一个或多个节点的逻辑分组,每个节点共享用户、虚拟主机、队列、交换、绑定路由、运行时参数和其他分布式状态。集群中的节点可以动态地添加/删除,RabbitMQ代理一开始都运行在单个节点上,可以将这些节点连接到集群中,然后再将其转换回各个代理。
- 默认情况下,
RabbitMq将复制除消息队列外的所有数据至集群中的每一个节点。而消息队列的完整数据只会存放于创建该队列的节点上,其余节点仅保存该队列的元数据和指针(类似于索引)。如果需要复制队列,则需要启用镜像队列集群。 -
集群中每个节点是平等的,不存在主从和特殊的节点。
-
集群中的节点通过
Erlang Cookie相互通信,每个节点必须具有相同的cookie。 -
节点分为磁盘节点和
RAM节点,RAM节点只在RAM中存储内部数据库表,并不存储包括消息、消息存储索引、队列索引和其他节点状态等数据,RAM节点的性能更加高效,但是由于数据是非持久化的,一旦宕机将无法恢复。默认创建的都是磁盘节点。 -
单节点拓扑图如下,集群的拓扑是基于多个Node节点的扩展。

二、配置需求
- 配置方式
config文件配置rabbitmqctl命令配置(下文中使用此方法配置)
- 集群中的节点名必须是唯一的
- 可以在启动时使用环境变量
RABBITMQ_NODENAME设置 - 节点名由
[节点名称]@[host]组成
- 可以在启动时使用环境变量
- 各个节点的启动端口可以被成功连接
- 节点之间通过节点名相互访问,要求各个节点之间的
host可以相互进行DNS解析 - 每个节点之间必须配置相同的
Erlang Cookie(多机环境需要额外配置)
三、集群配置
-
启动多个独立的节点
RABBITMQ_NODE_PORT=5674 RABBITMQ_NODE_IPDDRESS=192.168.0.235 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME="clusterRabbit1@VM235" /usr/local/rabbitmq/3.1.3/sbin/rabbitmq-server -detached RABBITMQ_NODE_PORT=5675 RABBITMQ_NODE_IPDDRESS=192.168.0.235 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME="clusterRabbit2@VM235" /usr/local/rabbitmq/3.1.3/sbin/rabbitmq-server -detached执行完成后,分别创建了名称为
clusterRabbit1@VM235,clusterRabbit2@VM235的两个节点。VM235需要提前配置host文件,确保各个节点之间的host可以相互进行DNS解析。 -
创建集群
- 关闭节点
clusterRabbit1@VM235./rabbitmqctl -n clusterRabbit1@VM235 stop_app - 重置节点
clusterRabbit1@VM235./rabbitmqctl -n clusterRabbit1@VM235 reset必须重置节点才能加入现有集群,重置节点将删除该节点上以前存在的所有资源和数据。这意味着节点不能在成为集群的成员时保留其现有数据,节点中的数据需要进行额外的备份和恢复。
-
将节点
clusterRabbit1@VM235加入clusterRabbit2@VM235的集群./rabbitmqctl -n clusterRabbit1@VM235 join_cluster clusterRabbit2@VM235 - 启动节点
clusterRabbit1@VM235./rabbitmqctl -n clusterRabbit1@VM235 start_app - 查看集群信息
./rabbitmqctl -n clusterRabbit1@VM235 cluster_status 结果: Cluster status of node clusterRabbit1@VM235 ... [{nodes,[{disc,[clusterRabbit1@VM235,clusterRabbit2@VM235]}]}, {running_nodes,[clusterRabbit2@VM235,clusterRabbit1@VM235]}, {partitions,[]}] ...done.
- 关闭节点
- 集群中节点的关闭与重启
- 节点关闭并重启之后会选择一个在线的集群成员(只考虑磁盘节点)进行同步。在重新启动节点时,默认情况下将尝试与该成员联系10次,并有30秒的响应超时。如果该成员在时间间隔内可用则节点将成功启动,并与该成员同步所需内容后继续运行。如果该成员无法响应,则重新启动的节点将放弃同步数据并启动。
- 以下情况将导致节点无法重新加入集群:
- 修改节点名/主机名,节点的数据目录路径会因此更改。
- 重置节点数据/更换节点数据目录
- 移除集群中的节点
- 关闭该节点
- 重置该节点
- 再次启动该节点
四、集群测试
- 创建用户、
vhost、exchange、queue,并启动消费者。【测试基础】- 用户:
api_management(management标签,开放虚拟主机cluster所有权限) vhost:clusterexchange:cluster(直连交换机)queue:- 节点一
clusterRabbit1:- 队列一:
name:clusterRabbit1Queue1routing_key:clusterRabbit1key
- 队列二:
name:clusterRabbit1Queue2routing_key:clusterRabbitCommonKey
- 节点二
clusterRabbit2:- 队列三:
name:clusterRabbit2Queue1routing_key:clusterRabbit2key
- 队列四:
name:clusterRabbit2Queue2routing_key:clusterRabbitCommonKey(与队列二 相同)
- 节点一
consumer:- 消费者一:
- 连接节点:节点一
clusterRabbit1 - 消费队列:队列一
clusterRabbit1Queue1- 消费者二:
- 连接节点:节点一
clusterRabbit1 - 消费队列:队列二
clusterRabbit1Queue2- 消费者三(非此节点的队列):
- 连接节点:节点一
clusterRabbit1 - 消费队列:队列三
clusterRabbit2Queue1- 消费者四:
- 连接节点:节点二
clusterRabbit2 - 消费队列:队列四
clusterRabbit2Queue2
- 用户:
- 连接节点二
clusterRabbit2,再次声明队列名和队列一同名的队列clusterRabbit1Queue1。【同名队列再次声明】- 结果:声明不成功,节点二中没有生成新的
clusterRabbit1Queue1队列,而节点一中clusterRabbit1Queue1队列多出了新绑定的routing_key,这意味着在同一集群中不同节点之间的队列名是唯一的,在一个节点中可以操作另一个节点的队列数据。
- 结果:声明不成功,节点二中没有生成新的
- 生产者连接节点一
clusterRabbit1,发布clusterRabbit1key消息。【发布此节点队列消息】- 消费者一 成功接收到消息
- 生产者连接节点二
clusterRabbit2,发布clusterRabbit1key消息。【发布非此节点队列消息】- 消费者一 成功接收到消息
- 生产者连接节点一
clusterRabbit1,发布clusterRabbitCommonKey消息。【多个节点队列绑定相同消息】- 消费者二 成功接收到消息
- 消费者四 成功接收到消息
- 生产者连接节点一
clusterRabbit1,发布clusterRabbit2key消息。【消费非此节点队列消息】- 消费三 成功接收到消息
- 关闭节点二
clusterRabbit2,连接节点一,发布clusterRabbit2key消息【投递消息给集群中意外退出的节点】- 关闭节点二
clusterRabbit2之后,消费者三、四异常退出 - 投递消息至集群成功
- 再次启动节点二
clusterRabbit2以及消费者三、四,之前投递的消息没有成功接收
- 关闭节点二
五、使用总结
- 同一集群中不同节点之间的队列名是唯一的,在一个节点中可以操作另一个节点的队列数据。
- 生产者可以发布集群中任一节点队列绑定的消息,集群将自动匹配出符合条件的节点队列,并投递给消费者进行消费。
- 集群中不同节点的队列如果绑定了相同的
routing_key,消息将投递到集群中所有符合路由匹配条件的节点队列中。 - 消费者可以订阅集群中任意节点的队列。
- 集群中某个节点异常退出后,生产者投递到集群中的消息将无法送达至该节点,但是不影响其他节点的接收。(解决这个问题需要使用镜像队列集群)
六、错误记录
- 启动新的节点报错:
could_not_start,rabbitmq_management解决:
rabbitmq_management插件默认使用的是15672端口,这个端口已被之前启动的节点占用,修改启动命名为rabbitmq_management插件指定一个新的端口即可。RABBITMQ_NODE_PORT=5674 RABBITMQ_NODE_IPDDRESS=192.168.0.235 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME="clusterRabbit1@VM235" /usr/local/rabbitmq/3.1.3/sbin/rabbitmq-server -detached - 【认知错误】
rabbitmq_management管理后台Overview中节点的Memory表示使用的内存,Disk space表示剩余可用的磁盘空间,不是已使用的磁盘空间。 -
修改节点的
host后启动失败:``ERROR: epmd error for host "VM235": nxdomain (non-existing domain)`解决:
VM235需要预先添加到HOST文件,确保可以被正确解析。 -
节点一
clusterRabbit1退出集群后重新加入集群,再次声明之前的队列提示routing_key绑定不成功NOT_FOUND - no binding clusterRabbitCommonKey between exchange 'cluster' in vhost 'cluster' and queue 'clusterRabbit1Queue2' in vhost 'cluster'此问题是
rabbitMq集群本身的问题,且未得到官方明确的解决方案。一些临时解放方案:
- 修改节点一名称
- 删除并重建交换机
-
修改旧的队列名或者队列参数
根本解决:
升级
rabbitMq至3.8.0,参考【RabbitMq 使用docker搭建集群篇】。
redis实战经验
如何避免key冲突?
- select db
- 默认db16个,编号0~15,可通过配置修改db个数
- 默认使用db-0
- key命名格式
- 以"xxx:yyy:zzz"格式命名
安全考虑
- 访问限制
- 只对内网访问,以防外部通过6379端口访问
- 危险函数
- 禁用危险函数:flush、flushall
- 使用秘钥
- $redis->auth(password)
性能提升
- 连接
- 使用官方扩展,弃用predis
- 使用单例方式连接,使用pconnect
- 用中间件,实现连接池
- 持久化策略
- 根据实际情况,关闭持久化
- 服务部署
- 与web服务共存,本地调用
- 与存储服务隔离(mysql),避免高io
- 一主多从,提升性能,或使用集群
- 版本升级
- 低版本:setnx + expire
- 高版本:set(k, v, array(ex, nx))
- 批量操作
- 使用管道命令,批量导入数据
- 使用mset, mget
- 合理操作
- 使用hset存放json,而不是set
- 使用scan代替keys
- Nginx + Lua + Redis
问题:
如何检查redis服务是否健康?
缓存设计中的要点
高性能
- 简洁的通讯协议,快速连接
- 操作基于内存,快速读写
- 成熟的数据结构,快速定位
- 基于牛X的语言实现,快速运行
- 主从架构,读写分离
高可用
- 异常监控,自动化应急
- 数据灾备,快速恢复
- 主从架构,从服务上位
- 分布式、集群化
- 服务自动降级
- 多级缓存
存储
- 成熟的数据结构,减少冗余
- 数据压缩方案,减小数据大小
- 置换算法:FIFO、LRU、LFU
- 索引,以空间换时间
- 数据分片,快捷扩容、无限扩容
- 一定要设置过期时间!
常见缓存过期策略
| 策略 | 优点 | 缺点 |
|---|---|---|
| 定时过期: 每个设置了过期时间的key,都携带一个定时器做倒计时,到期自动删除 |
立即清除,无空间浪费 | 定时器占用大量cpu,影响性能 |
| 惰性过期: 只有当访问的时候,才会判断是否过期,过期则删除 |
最大化节省cpu | 过期数据未被访问时,会占用存储,造成空间浪费 |
| 定期过期: 每隔一定时间,随机扫描一定数量带有有效期的key,并清除其中已过期的key |
前两种的这种方案 | 难点: 需要合理设置 “时间”、 “数量” |
常见存储置换策略:FIFO、LRU、LFU
- FIFO(first in first out)
- 淘汰最早的数据
- LRU(least recently used)
- 淘汰最长时间未被使用的数据
- LFU(least frequently used)
- 淘汰使用次数最少的数据
Redis置换策略配置(maxmemory-policy)
- noeviction:不置换
- volatile-[ lru | random | ttl ]
- 对具备有效期的key,按lru/随机/最短置换
- allkeys-[ lru | random ]
- 对所有key,按lru/随机置换
一致性
缓存与数据库保持一致
- 低效做法:使用事务,数据库与缓存的更新
- 符合ACID
- 业务普遍做法:先更新数据库,再删除缓存
- 严谨但复杂的做法:消息队列、订阅binlog
旁路缓存原则:
读操作:
- 先读缓存
- 如果命中,直接返回
- 如果未命中,访问DB,并写入缓存写操作:
- 删缓存,而不是更新缓存
- 先写DB,再删缓存问题:
以下方式,存在什么隐患?
- 先更新缓存,再更新数据库
- 先删除缓存,再更新数据库
- 先更新数据库,再更新缓存
防雪崩
高并发时失效
雪崩:在用户高并发瞬间,如果缓存不可用(失效),用户的请求压力,都转到数据库,导致数据库挂掉,并最终导致整个系统挂掉。
如何防止缓存雪崩的发生?
- 缓存服务高可用
- 服务挂了,做什么都没用
- 多个缓存不能同时过期
- 随机失效,而不是同时或定点
- 多级缓存
- 多级缓存,尽可能把数据库挡在后面
- 使用互斥锁
- 只有拿到锁的请求,才能查库
- 排队限流
- 控制查库的并发峰值
- 提前预热
- 预先更新缓存
- 公用性质缓存更应重视
- 如:首页、推荐位
- 个性化数据相对风险小
// 业务中改成这样是否有问题
function getDataBuyKey($strKey) {
// 从缓存中取数据
$arrData = getCacheData($strKey);
// 如果未命中缓存,或缓存即将失效,抢占锁
if (!$arrData || $arrData['ttl'] - time() < 10) {
// 如果抢锁成功(带有效期的锁),可以查库
if (redis::set('lock_' . $strKey, 1, array('nx', 'ex' => 10))) {
$arrData = getDbData($strKey); // 查库
setCacheData($arrData); // 更新缓存
}
}
return $arrData;
}
防穿透
被故意不命中
用户伪造大量请求,故意不命中缓存,当这些请求集中转到数据库时,导致数据库挂掉,并最终导致整个系统挂掉。
如何防止缓存穿透的发生?
- 缓存空值
- 额外缓存一份空值,以防反复查库
- 缓存数据范围
- 如,应用ID范围、页码范围、时间段
- 预先使用布隆过滤(bloom filter)
- 一定不存在的数据,请求将被拦截
function getDataBuyKey($strKey) {
$arrData = [];
// 先执行key的有效验证
if (checkKey($strKey)) {
// 从缓存中取数据,未取到,返回null
$arrData = getCacheData($strKey);
}
// 如果未命中缓存,或者缓存即将失效,抢占锁
if (is_null($arrData) || $arrData['ttl'] - time() < 10) {
// 如果抢锁成功(带有效期的锁),可以查库
if (redis::set('lock_' . $strKey, 1, array('nx', 'ex' => 10))) {
$arrData = (array)getDbData($strKey); // 查库
setCacheData($arrData); // 更新缓存,若为空值,写入"[]"
}
is_null($arrData) && $arrData = []
}
return $arrData; // 返回空数据,或即将失效的数据,或最新的数据
}
cookie中的domain设置
项目中遇到这样一个问题:用户登录后,跳转到该域名下的其他页面时,cookie的值改变了。由于是在app中,只能抓包,看到请求header头中有两个同名cookie。而在服务端,超全局变量cookie数组,只取了第一个cookie。由于项目变动,重新设置cookie时只设置了当前域名下的cookie,所以旧的cookie永远无法清除。
这种问题,其实如果基础好一点,对cookie足够了解,是很快能够解决的。由于很久不写这些基础,所以还是花了很长时间。因为当时无法确定cookie的domain和path,所以只能靠猜去清除cookie。
虽然通过git log找到原先设置cookie的域名成功清除了旧cookie,但是如果早点看资料,应该并不难解决。(看文档胜过一切)
对于cookie只需要牢记两点:设置范围和作用域
- 设置范围:只能设置当前域名以及父域名(无法设置子域或者同级域名)
- 作用域:当前域名以及所有子域名
举个简单的例子:
对于域名 test.com,在访问a.test.com时,只能设置domain为a.test.com或者test.com;如果设置domain为a.test.com,则child.a.test.com、tool.child.a.test.com等都是能够访问到该cookie的。而b.test.com是无法访问到的。
如果设置domain为test.com,则a.test.com以及test.com下的所有子域名都能访问下,这意味着它的同级域名b.test.com也能够访问到。有什么用呢?
这样可以用来解决sso单点登录或者跨域cookie等问题。当然,现在的单点登录一般是通过返回的ticket凭证到服务端做校验去获取信息。
比如,对于a.test.com和b.test.com两个项目,公用user.test.com用户系统做单点登录,可以通过设置domain为test.com,在user退出的时候清除根域名下的cookie以达到协同退出的功能。
回到上面的问题:所以在当前域名无法清除cookie的时候,只可能是它的上级域名设置了cookie。而对于path,一般默认情况下是 / 根目录。

