Clay 的技术空间

用进废退 | 艺不压身

大纲

前言

本文适用于在 Centos/Debian/Ubuntu 等 Linux 发行版系统上,使用单机搭建 Kafka 集群。

Zookeeper 集群搭建

本文的 Kafka 集群搭建依赖于 Zookeeper,因此需要将 Zookeeper 单机集群提前搭建起来。值得一提的是,从 Kafka 2.8.0 版本开始,Kafka 自身实现了 Raft 分布式一致性机制,这意味着 Kafka 集群是可以脱离 ZooKeeper 独立运行的。

集群规划

节点 IP 地址端口版本号
Zookeeper 节点 1127.0.0.121813.4.10
Zookeeper 节点 2127.0.0.121823.4.10
Zookeeper 节点 3127.0.0.121833.4.10
阅读全文 »

前言

在企业级开发中,我们经常会有编写数据库文档的时间付出,关于数据库文档的状态:要么没有、要么有但都是手写、后期运维开发都需要手动对文档进行维护,很是繁琐。如果忘记一次维护就会给以后的工作造成很多困扰,这无形中留了很多坑给自己和后人。screw 是一款简洁好用的数据库文档生成工具,专为解决这一开发痛点而生。

screw 介绍

特色功能

  • 灵活扩展
  • 支持自定义模板
  • 支持多种数据库
  • 支持多种格式的文档
  • 简洁、轻量、设计良好
阅读全文 »

大纲

set 与 multiset 容器

set 与 multiset 容器的概念

set 容器的概念

set 是一个集合容器,其中所包含的元素是唯一的,集合中的元素会自动按一定的顺序排列。元素插入过程是按排序规则插入,所以不能指定插入位置。set 的元素不像 map 那样可以同时拥有实值和键值,set 的元素即是键值又是实值。Set 不允许两个元素有相同的键,也不可以通过 set 的迭代器改变 set 元素的值,因为 set 元素值就是其键值,关系到 set 元素的排序规则。如果任意改变 set 元素值,会严重破坏 set 的组织。换句话说 set 的 iterator 是一种 const iterator。set 拥有和 list 某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。

阅读全文 »

大纲

list 容器

list 容器的概念

list 是一个双向链表容器,而且还是一个双向循环链表,可以高效地进行插入和删除元素。链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的(如下图所示)。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。相较于 vector 的连续线性空间,list 就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。值得一提的是,对于任何位置的元素插入或元素的移除,list 永远是常数时间的耗时(效率较高);但对于查询操作来说,list 的执行效率较低。

总结

  • 链表采用动态内存分配,不会造成内存浪费和溢出。
  • 链表虽然灵活,但是空间和时间的额外耗费较大。
  • 链表执行插入和删除操作都十分方便,仅修改指针即可实现,不需要移动大量元素。
  • 链表不可以随机存取元素,所以不支持 at.(pos) 函数与 [] 操作符的使用。

list 容器的迭代器

list 容器不能像 vector 一样以普通指针作为迭代器,因为其节点不能保证都在同一块连续的内存空间上。list 迭代器必须有能力指向 list 的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓 “list 正确的递增、递减、取值、成员取用” 是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。由于 list 是一个双向链表,迭代器必须能够具备前移、后移的能力,所以 list 容器提供的迭代器是 BidirectionalIterators。list 有一个重要的性质,插入操作和删除操作都不会造成原有 list 送代器的失效。这在 vector 是不成立的,因为 vector 的插入操作可能造成记忆体重新配置,导致原有的送代器全部失效;甚至 list 元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。

list 容器的使用

list 的构造与赋值

默认构造函数

list 采用模板类实现,对象的默认构造形式:list<T> lstT;,其中 <> 尖括号内还可以设置指针类型或自定义类型

1
2
3
list<int> lstInt;         // 定义一个存放 int 数据的 list 容器
list<float> lstFloat; // 定义一个存放 float 数据的 list 容器
list<string> lstString; // 定义一个存放 string 数据的 list 容器
有参构造函数
1
2
3
list(beg, end);           // 构造函数将 [beg, end) 区间中的元素拷贝给本身,注意该区间是左闭右开的区间
list(n, elem); // 构造函数将 n 个 elem 拷贝给本身
list(const list &lst); // 拷贝构造函数
赋值操作说明
1
2
3
4
list.assign(beg, end);               // 构造函数将 [beg, end) 区间中的元素拷贝给本身,注意该区间是左闭右开的区间
list.assign(n, elem); // 将 n 个 elem 拷贝赋值给本身
list& operator=(const list &lst); // 重载等号操作符
list.swap(lst); // 将 lst 与本身的元素互换
构造与赋值的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <list>

using namespace std;

void printList(list<int> &L) {
// 遍历容器
for (list<int>::iterator it = L.begin(); it != L.end(); it++) {
cout << *it << " ";
}
cout << endl;
}

int main() {

cout << "------ list 构造函数 ------" << endl;

// 默认构造函数
list<int> myList1;

// 有参构造函数,将 n 个 elem 元素拷贝给本身
list<int> myList2(5, 10);
printList(myList2);

// 有参构造函数,将 [begin, end) 区间中的元素拷贝给本身
list<int> myList3(myList2.begin(), myList2.end());
printList(myList3);

// 拷贝构造函数
list<int> myList4 = myList2;
printList(myList4);

cout << "------ list 赋值操作 ------" << endl;

// 赋值操作,将 [begin, end) 区间中的元素拷贝给本身
list<int> myList5;
myList5.assign(myList2.begin(), myList2.end());
printList(myList5);

// 赋值操作,将 n 个 elem 元素拷贝给本身
list<int> myList6;
myList6.assign(5, 8);
printList(myList6);

// 赋值操作,重载等号操作符
list<int> myList7;
myList7 = myList2;
printList(myList7);

// 赋值操作,将其他容器与本身的元素互换
myList6.swap(myList7);
printList(myList6);
printList(myList7);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
------ list 构造函数 ------
10 10 10 10 10
10 10 10 10 10
10 10 10 10 10
------ list 赋值操作 ------
10 10 10 10 10
8 8 8 8 8
10 10 10 10 10
10 10 10 10 10
8 8 8 8 8

list 的常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <iostream>
#include <list>

using namespace std;

void printList(list<int> &L) {
// 遍历容器
for (list<int>::iterator it = L.begin(); it != L.end(); it++) {
cout << *it << " ";
}
cout << endl;
}

void reversePrintList(list<int> &L) {
// 逆向遍历
for (list<int>::reverse_iterator it = L.rbegin(); it != L.rend(); it++) {
cout << *it << " ";
}
cout << endl;
}

int main() {
cout << "------ list 插入操作 ------" << endl;

list<int> myList1;

// 往容器的尾部插入元素
myList1.push_back(10);
myList1.push_back(20);
myList1.push_back(30);
printList(myList1);

// 往容器的头部插入元素
myList1.push_front(300);
myList1.push_front(200);
myList1.push_front(100);
printList(myList1);

// 往 pos 位置插入 elem 数据的拷贝,返回新数据的位置
myList1.insert(myList1.begin(), 400);
printList(myList1);

// 往 pos 位置插入 n 个 elem 数据,无返回值
myList1.insert(myList1.begin(), 2, 500);
printList(myList1);

// 往 pos 位置插入 [begin, end) 区间的数据,无返回值
list<int> myList2;
myList2.push_back(1);
myList2.push_back(2);
myList2.push_back(3);
myList1.insert(myList1.begin(), myList2.begin(), myList2.end());
printList(myList1);

cout << "------ list 删除操作 ------" << endl;

// 删除容器头部的数据
myList1.pop_front();
printList(myList1);

// 删除容器尾部的数据
myList1.pop_back();
printList(myList1);

// 删除 pos 位置的数据,返回下一个数据的位置
myList1.erase(myList1.begin());
printList(myList1);

// 删除容器中所有与 elem 值匹配的元素
myList1.remove(100);
printList(myList1);

cout << "------ list 读取操作 ------" << endl;

// 获取第一个元素
cout << myList1.front() << endl;

// 获取最后一个元素
cout << myList1.back() << endl;

cout << "------ list 清空操作 ------" << endl;

list<int> myList3;
myList3.push_back(10);
myList3.push_back(10);
myList3.clear();
printList(myList3);

cout << "------ list 大小操作 ------" << endl;

// 返回容器中元素的个数
cout << "size = " << myList1.size() << endl;

// 判断容器是否为空
bool isEmpty = myList1.empty();
cout << "isEmpty = " << (isEmpty ? "true" : "false") << endl;

// 重新指定容器的长度 num,若容器变长,则以默认值填充新位置,若容器变短,则末尾超出容器长度的元素会被删除
myList1.resize(6);
cout << "size = " << myList1.size() << endl;

// 重新指定容器的长度 num,若容器变长,则以 elem 值填充新位置,若容器变短,则末尾超出容器长度的元素会被删除
myList1.resize(9, 11);
printList(myList1);
cout << "size = " << myList1.size() << endl;

cout << "------ list 逆向遍历操作 ------" << endl;

list<int> myList11;
myList11.push_back(1);
myList11.push_back(2);
myList11.push_back(3);
reversePrintList(myList11);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
------ list 插入操作 ------
10 20 30
100 200 300 10 20 30
400 100 200 300 10 20 30
500 500 400 100 200 300 10 20 30
1 2 3 500 500 400 100 200 300 10 20 30
------ list 删除操作 ------
2 3 500 500 400 100 200 300 10 20 30
2 3 500 500 400 100 200 300 10 20
3 500 500 400 100 200 300 10 20
3 500 500 400 200 300 10 20
------ list 读取操作 ------
3
20
------ list 清空操作 ------

------ list 大小操作 ------
size = 8
isEmpty = false
size = 6
3 500 500 400 200 300 11 11 11
size = 9
------ list 逆向遍历操作 ------
3 2 1

list 的反转与排序操作

提示

  • 所有不支持随机访问的容器,都不可以使用系统提供的排序算法。
  • 如果容器不支持使用系统提供的排序算法,那么这个容器的内部往往会提供对应的排序算法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <list>

using namespace std;

void printList(list<int> &L) {
// 遍历容器
for (list<int>::iterator it = L.begin(); it != L.end(); it++) {
cout << *it << " ";
}
cout << endl;
}

bool myCompare(int v1, int v2) {
// 从大到小排序
return v1 > v2;
}

int main() {
list<int> myList;
myList.push_back(1);
myList.push_back(3);
myList.push_back(2);

cout << "------ list 反转操作 ------" << endl;

myList.reverse();
printList(myList);

cout << "------ list 排序操作 ------" << endl;

// 排序(从小到大)
myList.sort();
printList(myList);

// 排序(从大到小)
myList.sort(myCompare);
printList(myList);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
------ list 反转操作 ------
2 3 1
------ list 排序操作 ------
1 2 3
3 2 1

list 的自定义数据类型操作

提示

  • 对 list 的自定义类型数据类型进行排序时,必须指定排序规则。
  • 调用 remove() 函数移除 list 容器中的元素时,自定义数据类型必须重载 == 双等号操作符,否则移除操作会执行失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <iostream>
#include <string>
#include <list>

using namespace std;

class Person {

public:
Person(string name, int age) {
this->name = name;
this->age = age;
}

// 获取名称
string getName() const {
return this->name;
}

// 获取年龄
int getAge() const {
return this->age;
}

// 重载 == 双等号操作符
bool operator==(const Person &p) {
return this->name == p.name && this->age == p.age;
}

private:
string name;
int age;

};

void printList(list<Person> &L) {
// 遍历容器
for (list<Person>::iterator it = L.begin(); it != L.end(); it++) {
cout << "name: " << it->getName() << ", age: " << it->getAge() << endl;
}
}

bool myCompare(Person &p1, Person &p2) {
// 按年龄从大到小排序
return p1.getAge() > p2.getAge();
}

int main() {
list<Person> myList;

Person p1("Tom", 17);
Person p2("Jim", 18);
Person p3("Peter", 19);
Person p4("David", 20);

cout << "------ list 插入操作(自定义数据类型) ------" << endl;

myList.push_back(p1);
myList.push_back(p2);
myList.push_back(p3);
myList.push_back(p4);
printList(myList);

cout << "------ list 删除操作(自定义数据类型) ------" << endl;

// 调用 remove() 函数移除 list 容器中的元素时,自定义数据类型必须重载 `==` 双等号操作符,否则移除操作会执行失败
myList.remove(p3);
printList(myList);

cout << "------ list 排序操作(自定义数据类型) ------" << endl;

// 对 list 的自定义类型数据类型进行排序时,必须指定排序规则
myList.sort(myCompare);
printList(myList);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
------ list 插入操作(自定义数据类型) ------
name: Tom, age: 17
name: Jim, age: 18
name: Peter, age: 19
name: David, age: 20
------ list 删除操作(自定义数据类型) ------
name: Tom, age: 17
name: Jim, age: 18
name: David, age: 20
------ list 排序操作(自定义数据类型) ------
name: David, age: 20
name: Jim, age: 18
name: Tom, age: 17

stack 容器

stack 容器的概念

stack 是堆栈容器,属于一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口。stack 容器允许新增元素、移除元素、取得栈顶元素,但是除了最顶端的元素外,没有任何其他方法可以存取 stack 中的其他元素。stack 没有迭代器,容器中所有元素的进出都必须符合 “先进后出” 的规则,只有 stack 最顶端的元素,才有机会被外界取用。换言之,stack 不提供遍历功能,也不提供迭代器。deque 是双向开口的数据结构,若以 deque 为底部结构并封闭其头端开口,便轻而易举地形成一个 stack。因此,SGI STL 便以 deque 作为缺省情况下的 stack 底部结构。由于 stack 以 deque 做为底部容器完成其所有工作,而具有这种 “修改某物接口,形成另一种风貌” 的性质者,称为 adapter(配接器),因此,stack 往往不被归类为 container(容器),而被归类为 container adapter(容器配接器)。简而言之,stack 是简单地装饰 deque 容器而成为另外的一种容器。

阅读全文 »

大纲

vector 容器

vector 容器的概念

vector 的数据存储以及操作方式,与 Array 非常相似,两者的唯一差别在于空间运用的灵活性。Array 是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,可以,一切琐碎的细节得由自己来实现;首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。Vector 是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此 vector 的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就初始化一个大的 Array 了。Vector 的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦 vector 旧空间满了,如果客户每新增一个元素 vector 内部只是扩充一个元素的空间,实为不智,因为所谓的扩充空间(不论多大),一如刚才所说,是 “配置新空间 - 数据移动 - 释放旧空间” 的大工程,时间成本很高,应该加入某种未雨绸缪的考虑。

阅读全文 »

前言

本文将介绍 MyBatis-Plus 如何使用雪花算法(Snowflake)生成分布式全局唯一 ID,并解决 ID 冲突问题。

版本说明

组件版本
MyBatis-Plus3.3.1
阅读全文 »

一、日期处理

1. 时间格式化

该方法可以用于将时间转化为 hour:minutes:seconds 的格式:

1
2
3
4
5
const timeFromDate = date => date.toTimeString().slice(0, 8);

timeFromDate(new Date(2021, 11, 2, 12, 30, 0)); // 12:30:00
timeFromDate(new Date()); // 返回当前时间 09:00:00
复制代码

2. 检察日期是否有效

该方法用于检测给出的日期是否有效:

1
2
3
4
const isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf());

isDateValid("December 17, 1995 03:24:00"); // true
复制代码
阅读全文 »

大纲

前言

随着微服务的流行以及多语言互操作诉求日益增多,在 Dubbo 中暴露 REST 服务变成了一个不容忽视的诉求。为了在 Dubbo 中暴露 REST 服务,通常有两种做法,一种是直接依赖 Spring REST 或者其他 REST 框架来直接暴露,另一种是通过 Dubbo 框架内置的 REST 能力暴露。两种做法各有优缺点,主要体现在前者与微服务体系中的服务发现组件能够更好地工作,而后者可以无缝的享受到 Dubbo 体系中的服务发现以及服务治理的能力。本文将介绍如何通过 Dubbo 框架内置的 REST 能力来暴露 REST 服务。

阅读全文 »

大纲

前言

集群模式

RabbitMQ 是基于 Erang 开发的,集群模式分为两种,包括普通模式和镜像模式;可以说镜像模式是普通模式的升级版,其中 RabbitMQ 默认使用的是普通模式。

  • 普通模式:

    • 以两个节点(rabbit01、rabbit02)为例来进行说明,rabbit01 和 rabbit02 两个节点仅有相同的元数据,即队列的结构,但消息实体只存在于其中一个节点 rabbit01(或者 rabbit02)中。当消息进入 rabbit01 节点的 Queue 后,Consumer 从 rabbit02 节点消费时,RabbitMQ 会临时在 rabbit01、rabbit02 间进行消息传输,把 A 中的消息实体取出并经过 B 发送给 Consumer。所以,对于同一个逻辑队列,要在多个节点上建立物理队列,比如使用镜像队列或者 Quorum。换言之,如果使用的是 RabbitMQ 默认队列,Consumer 一定要尽量直连到队列所在的节点,才能避免跨节点传输带来的性能问题。否则,无论 Consumer 连接 rabbit01 还是 rabbit02,出口总在 rabbit01,会产生性能瓶颈。另外,当 rabbit01 节点故障后,rabbit02 节点无法取到 rabbit01 节点中还未消费的消息实体。如果做了消息持久化,那么得等 rabbit01 节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。
  • 镜像模式:

    • 在普通模式的基础上,通过镜像队列策略(HA Policy)将需要的队列配置成镜像队列,让队列存储在多个节点上,消息实体会主动在镜像节点之间同步,而不是在客户端取数据时临时拉取,也就是说有多少个镜像节点消息就会备份多少份。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉,所以在对业务可靠性要求较高的场合中适用。由于镜像队列之间消息自动同步,且内部有选举 Master 机制,即使 Master 节点宕机也不会影响整个集群的使用,达到去中心化的目的,从而有效的防止消息丢失及服务不可用等问题

集群节点

RabbitMQ 的集群节点分为磁盘节点、内存节点。RabbitMQ 支持消息的持久化,也就是数据写在磁盘上。在 RabbitMQ 集群中,必须至少有一个磁盘节点,否则队列元数据无法写入到集群中。当磁盘节点宕掉时,集群将无法写入新的队列元数据信息。如果 RabbitMQ 集群全部宕机,必须先启动磁盘节点,然后再启动内存节点。最合适的方案就是既有磁盘节点,又有内存节点;建议至少 2 个磁盘节点,其他节点可以作为内存节点来提高性能,比如采用 2 个 磁盘节点 + 1 个内存节点的集群搭建方式。

节点类型节点特点
disc(磁盘节点)元数据持久化到磁盘,集群元数据(如队列、交换机、绑定、用户、权限的定义等)会复制到这些节点上。至少需要一个 disc 节点才能组成完整的 RabbitMQ 集群。
ram(内存节点)大部分集群元数据仅保存在内存中,性能更高。启动时会从磁盘节点同步元数据,但自身不会持久化元数据。一旦内存节点宕机或重启,它的元数据就会丢失(除非从磁盘节点重新同步)。掉电就丢失元数据,不适合长期存储,只适合当辅助节点使用

磁盘节点补充说明

  • RabbitMQ 默认的集群节点类型是 disc(磁盘节点),至少需要一个 disc 节点才能组成完整的 RabbitMQ 集群。

内存节点补充说明

  • ram(内存节点)决定的是 RabbitMQ 的集群元数据保存在内存中,元数据包括队列、交换机、绑定、用户、权限的定义等,而不是决定队列和消息都存储在内存里面,也就是消息的存储还是由队列持久化、消息持久化决定的。
  • 假设在 RabbitMQ 的一个内存节点上创建了队列,如果这个节点宕机,它的元数据就没了,重启后队列的定义也不存在。但是,如果将队列设置为持久化(durable=true),消息也设置为持久化(delivery_mode=2),即使内存节点宕机了,只要还有磁盘节点存在,就可以恢复内存节点的队列和消息数据。

集群架构

RabbitMQ 集群的典型架构如下图所示,展示了如何通过多个节点实现高可用和负载均衡。这种架构可以有效地提高 RabbitMQ 的吞吐量和容错能力,适用于对消息可靠性和系统可用性要求较高的生产环境。架构说明如下:

  • 客户端连接:​客户端通过虚拟 IP(VIP)连接到集群,VIP 由 Keepalived 管理,实现高可用性。
  • 负载均衡:​HAProxy 作为负载均衡器,将客户端请求分发到后端的 RabbitMQ 节点。在 HAProxy 的配置中启用健康检查后,HAProxy 可以感知到 RabbitMQ 节点的宕机,并自动将其从负载均衡列表中剔除。
  • RabbitMQ 节点:​多个 RabbitMQ 节点组成集群,节点之间通过镜像队列(Mirrored Queue)机制同步消息,确保消息的高可用性。

提示

由于篇幅有限,本文只介绍如何使用多机搭建 RabbitMQ 集群,不再介绍如何使用 Keepalived + HAProxy 来实现 RabbitMQ 集群的负载均衡,可以参考这里的 Keepalived + HAProxy 使用教程。

重要提示

无论 RabbitMQ 集群是运行在普通模式,还是启用了镜像模式(使用镜像队列),客户端(包括生产者和消费者)都可以直接连接到集群中的任意节点进行消息的发送与接收(类似于无状态设计)。RabbitMQ 集群节点之间通过内部通信机制来保持数据同步和状态一致,从而对客户端屏蔽了具体的队列位置,使得客户端无需关心消息实际存储在哪个节点上,连接任一节点即可正常使用。这就是为什么可以使用 Keepalived + HAProxy 来实现 RabbitMQ 高可用负载均衡集群的原因。若希望进一步提高可用性,推荐使用 RabbitMQ 的 Quorum Queue + Keepalived + HAProxy + Prometheus + AlertManager 的组合,可以构建一个企业级高可用消息中间件系统。

阅读全文 »