算法上还是这一周更多的还是中等的题目,主要还是动态规划与贪心算法比较难。
这段时间在看B站上netty的课程( 深入理解netty )圣思院的课程,老师讲的真的很好,特别是在教育方面,会交很多学习的方法。
工作上主要在研究IM相关业务,现在处于初级阶段,还在选型过程中,作为一次选型记录。
1. although
1.1. 每日一题
1.1.1. 合并K个排序链表
/**
* Created with IntelliJ IDEA.
* Description:
* User: CDz
* Create: 2020-04-26 21:41
* <p>
* 23. 合并K个排序链表
* 合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
* <p>
* 示例:
* <p>
* 输入:
* [
* 1->4->5,
* 1->3->4,
* 2->6
* ]
* 输出: 1->1->2->3->4->4->5->6
**/
借助优先队列数据结构求解。
- 思路一优先队列求解
- 优先队列也有两种方式
- 一种是全部放入
- 另一种是先放入表头,pop出后加入下一个
- 思路二分治思想
- 链表两两合并,最终合并成一个
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<Integer> queue = new PriorityQueue<>(Integer::compareTo);
//这里因为我是一次性将所有的元素加入到queue中
// 第一 queue的插入复杂度是 (logn) n代表queue中的元素个数
// 假设总元素有n个 时间复杂度就是 log1+log2...+logn约等于 nlogn
//所以 总体 时间复杂度是 O(nlogn)
//此地方可以优化
//优化方式:
//一直保持优先队列里面有k个元素 (k是list长度)
//如何保持?
//优先队列中先添加 所有list的头结点,然后弹出最小一个 插入其再拿出下一个
for (int i = 0; i < lists.length; i++) {
while (lists[i]!=null){
queue.add(lists[i].val);
lists[i] = lists[i].next;
}
}
if (queue.isEmpty())return null;
ListNode preHead = new ListNode(-1);
ListNode head = preHead;
while (!queue.isEmpty()){
head.next = new ListNode(queue.poll());
head = head.next;
}
return preHead.next;
}
public ListNode mergeKLists1(ListNode[] lists) {
if (lists.length==0)return null;
PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>(Comparator.comparing(listNode -> listNode.val));
//这里不能使用这样的操作,因为可能 ListNode[] length不为0
// 但是 ListNode本身为空
// priorityQueue.addAll(Arrays.asList(lists));
for (ListNode list : lists) {
priorityQueue.add(list);
}
ListNode preHead = new ListNode(0);
ListNode head = preHead;
while (!priorityQueue.isEmpty()){
ListNode listNode = priorityQueue.poll();
head.next = listNode;
head = head.next;
if (listNode.next!=null){
priorityQueue.add(listNode.next);
}
}
return preHead.next;
}
1.1.2. 数组中的逆序对
/**
*面试题51. 数组中的逆序对
* 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
*
*
*
* 示例 1:
*
* 输入: [7,5,6,4]
* 输出: 5
*
*
* 限制:
*
* 0 <= 数组长度 <= 50000
*/
查找逆序对其实就是排序的过程:
- 暴力破解
O(n^2)
- 归并排序
O(nlgn)
public int reversePairs(int[] nums) {
int length = nums.length;
int res = 0;
for (int i = 0; i < length; i++) {
for (int j = i; j < length; j++) {
if (nums[i]>nums[j]){
res++;
}
}
}
return res;
}
public int reversePairs1(int[] nums) {
int[] arr = new int[nums.length];
System.arraycopy(nums,0,arr,0,nums.length);
int[] temp = new int[nums.length];
return margeSort(arr,0,nums.length-1,temp);
}
/**
* [left...right] 两闭区间 求其逆序数
* @param arr
* @param left
* @param right
* @param temp
* @return
*/
private int margeSort(int[] arr, int left, int right, int[] temp) {
if (left==right)return 0;
int mid = left + (right-left)/2;
int leftCount = margeSort(arr, left, mid, temp);
int rightCount = margeSort(arr, mid + 1, right, temp);
//归并优化
if (arr[mid+1]>=arr[mid]){
return leftCount+rightCount;
}
int count = reverseCount(arr,left,mid,right,temp);
return leftCount+rightCount+count;
}
/**
* 归并 [left...mid] [mid+1...right] 两个有序区间的逆序对
* @param arr
* @param left
* @param mid
* @param right
* @param temp
* @return
*/
private int reverseCount(int[] arr, int left, int mid, int right, int[] temp) {
//拷贝 arr 的[left...right] 区间元素到 temp中,方便对比排序
System.arraycopy(arr,left,temp,left,right-left+1);
int count = 0;
int i = left;//左边数组 初始位置
int j = mid+1;//右边数组 初始位置
for (int k = left; k <= right; k++) {
//判断两个边界
//由于每次计算完后 都会++操作
//就所以边界的计算是要在 两数组中 最右区间下标+1的位置判断
if (i==mid+1){
arr[k] = temp[j++];
}else if (j==right+1){
arr[k] = temp[i++];
}else if (temp[i]<=temp[j]){
arr[k]=temp[i];
i++;
}else {
arr[k] = temp[j];
j++;
//说明右边数组的j位置 小于 左边所有的元素
count += (mid-i+1);
}
}
return count;
}
1.1.3. 统计「优美子数组」
/**
* Created with IntelliJ IDEA.
* Description:
* User: CDz
* Create: 2020-04-21 22:06
*
* 1248. 统计「优美子数组」
* 给你一个整数数组 nums 和一个整数 k。
*
* 如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。
*
* 请返回这个数组中「优美子数组」的数目。
*
*
*
* 示例 1:
*
* 输入:nums = [1,1,2,1,1], k = 3
* 输出:2
* 解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
* 示例 2:
*
* 输入:nums = [2,4,6], k = 1
* 输出:0
* 解释:数列中不包含任何奇数,所以不存在优美子数组。
* 示例 3:
*
* 输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
* 输出:16
*
*
* 提示:
*
* 1 <= nums.length <= 50000
* 1 <= nums[i] <= 10^5
* 1 <= k <= nums.length
**/
- 两种方法:
- 滑动窗口(看到子序列第一反应就是滑动窗口)
- 前缀和
/**
* 滑动窗口
*
* 1. 先找到 k个奇数
* 2. 找到 左右 两边到第一个奇数的距离
* 3. 距离相乘
* 4. 左边前进一位,ood(记录奇数个数减一)
* 5. 重复
*
*
* @param nums
* @param k
* @return
*/
public int numberOfSubarrays(int[] nums, int k) {
int left=0,right=0,ood=0,n=nums.length,res = 0;
while (right < n ) {
if ((nums[right++]&1)==1){
ood++;
}
if (ood==k){
//查找右边第一个奇数距离right的距离
int tempRight = right;
while (right<n&&(nums[right]&1)==0){
right++;
}
//右边第一个奇数距离right的距离
int rightEventCont = right-tempRight;
//查找左边第一个奇数距离left的距离
int leftEventCont = 0;
while (left<n&&(nums[left]&1)==0){
left++;
leftEventCont++;
}
res +=((leftEventCont+1)*(rightEventCont+1));
ood--;
left++;
}
}
return res;
}
前缀和:
public int numberOfSubarrays1(int[] nums, int k) {
int res = 0;
int count = 0;
int[] oddCount = new int[nums.length+1];
oddCount[0] =1;
for (int num : nums) {
count+= (num&1);
oddCount[count]++;
if (count>=k){
res+=oddCount[count-k];
}
}
return res;
}
1.1.4. 全排列
/**
* Created with IntelliJ IDEA.
* Description:
* User: CDz
* Create: 2020-04-25 20:28
* 46. 全排列
* 给定一个 没有重复 数字的序列,返回其所有可能的全排列。
* <p>
* 示例:
* <p>
* 输入: [1,2,3]
* 输出:
* [
* [1,2,3],
* [1,3,2],
* [2,1,3],
* [2,3,1],
* [3,1,2],
* [3,2,1]
* ]
**/
排列组合其实可以看做为一个树型结构。排序组合就是遍历整棵树,其中还有回溯算法,回溯算法的关键是状态重置。
总结:
- 排列组合是一棵树
DFS
- 回溯算法
- 状态重置
public List<List<Integer>> permute(int[] nums) {
// 排列组合 本质上是一个树形的结构
// 排列组合就是去遍历整棵树,
// 但是用到了 回溯算法,回溯算法的关键是 要状态重置
//利用了 两个个结构
// dfs本质上利用了 方法栈
// 每一个小的结果中使用到了 stack栈 来做回溯状态变更
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len==0)return res;
boolean[] use = new boolean[len];
int path = 0;//层数
Deque<Integer> stack = new ArrayDeque<Integer>();
dfs(nums,len,path,use,stack,res);
return res;
}
1.1.5. 二叉树的右视图
/**
*199. 二叉树的右视图
* 给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
*
* 示例:
*
* 输入: [1,2,3,null,5,null,4]
* 输出: [1, 3, 4]
* 解释:
*
* 1 <---
* / \
* 2 3 <---
* \ \
* 5 4 <---
*/
按层级遍历树BFS
public List<Integer> rightSideView(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
ArrayList<Integer> res = new ArrayList<>();
while (!queue.isEmpty()){
int size = queue.size();
TreeNode lastNode = null;
for (int i = 0; i < size; i++) {
TreeNode poll = queue.poll();
if (poll!=null){
queue.add(poll.left);
queue.add(poll.right);
lastNode = poll;
}
}
if (lastNode!=null){
res.add(lastNode.val);
}else {
return res;
}
}
return res;
}
1.1.6. 硬币
/**
* Created with IntelliJ IDEA.
* Description:
* User: CDz
* Create: 2020-04-23 21:44
*
* 面试题 08.11. 硬币
* 硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)
*
* 示例1:
*
* 输入: n = 5
* 输出:2
* 解释: 有两种方式可以凑成总金额:
* 5=5
* 5=1+1+1+1+1
* 示例2:
*
* 输入: n = 10
* 输出:4
* 解释: 有四种方式可以凑成总金额:
* 10=10
* 10=5+5
* 10=5+1+1+1+1+1
* 10=1+1+1+1+1+1+1+1+1+1
* 说明:
*
* 注意:
*
* 你可以假设:
*
* 0 <= n (总金额) <= 1000000
**/
动态规划:
- 转移公式为:
f(i,j) = f(i-1,j)+f(i,j-coin[i])
public int waysToChange(int n) {
int [] coin = {25,10,5,1};
int[][] dp = new int[5][n+1];
for (int i = 1; i <= 4; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= n; j++) {
if (j-coin[i-1]>=0){
//都可以选
// f(i,j) = f(i-1,j)+f(i,j-coin[i]);
dp[i][j] = (dp[i-1][j]+dp[i][j-coin[i-1]])%1000000007;
}else {
//只能不选
dp[i][j] = dp[i-1][j]%1000000007;
}
}
}
return dp[4][n];
}
状态压缩
public int waysToChange1(int n) {
int [] coins = {25,10,5,1};
int[] dp = new int[n+1];
dp[0] = 1;
for (int coin : coins) {
for (int i = 1; i <= n; i++) {
if (i-coin>=0){
dp[i] = (dp[i]+dp[i-coin])%1000000007;
}
}
}
return dp[n];
}
1.1.7. 岛屿数量
/**
* Created with IntelliJ IDEA.
* Description:
* User: CDz
* Create: 2020-04-20 08:11
*
* 200. 岛屿数量
* 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
*
* 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
*
* 此外,你可以假设该网格的四条边均被水包围。
*
* 示例 1:
*
* 输入:
* 11110
* 11010
* 11000
* 00000
* 输出: 1
* 示例 2:
*
* 输入:
* 11000
* 11000
* 00100
* 00011
* 输出: 3
* 解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
**/
关键点,找到岛的一边,通过DFS将岛的所有部分都沉下去
public int numIslands(char[][] grid) {
if (grid.length==0)return 0;
int n = grid.length;
int m = grid[0].length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
ans += grid[i][j]-'0';
dfs(grid,i,j,n,m);
}
}
return ans;
}
private int[] movex= {0,1,0,-1};
private int[] movey = {-1,0,1,0};
private void dfs(char[][] grid, int i, int j, int n, int m) {
if (i<0||i>=n||j<0||j>=m||grid[i][j]=='0'){
return;
}
grid[i][j]='0';
dfs(grid,i+movex[0],j+movey[0],n,m);
dfs(grid,i+movex[1],j+movey[1],n,m);
dfs(grid,i+movex[2],j+movey[2],n,m);
dfs(grid,i+movex[3],j+movey[3],n,m);
}
2. review
3. tip
3.1. 如何学习一门框架、技术?
- 进入官网
- 首页预览
- 然后进入文档部分
- 先看介绍
- 再看
quick start
demo- 一定要跟着一起演示,自己做一遍
- 然后仔细的读官方文档,也是锻炼自己英文的能力
quick startdemo是关键,不要看例子简单就不去做,这是非常不对的事情。一个简单的例子是官网精心准备的,可以从感官上去认识整个框架是如何使用的。
3.2. Java 类中 函数/行为 存在哪里?
涉及到类加载机制的过程。
类加载机制:
- 读取class
- 通过classloader加载类
- 加载类的定义信息
- 疑问点:放在哪了呢?
- 放在方法区中
- 当new出一个对象时,找到此类的定义,按照定义去生成一个对象
- 其中需要注意点是:
- 并发分配内存问题
- 连续性的(标记整理、复制算法):通过指针碰撞
- 不连续的(标记清除):内存列表
- 并发分配内存问题
- 其中需要注意点是:
4. share
4.1. IM系统架构选型过程
4.1.1. IM核心:如何集群长连接
其实IM的核心就是长连接,只有长连接能保证与客户端即时通信,做到主动推送信息。
而长连接的问题在于,一台机器上能容纳的长连接数量是有限的,业务拓展性上非常差。而长连接的session是不能够进行缓存的(因为长连接上绑定的有端口号)缓存后其他机器是没办法使用的。
如何做集群?
自然想到的是使用外部缓存记录每台机器上有哪些长连接,当服务器向客户端推送时,先找到此客户端的长连接机器,然后转发过去。一般使用Redis来做缓存。
在IM系统中,就可以使用机器号与userID做缓存。
中间还有一些问题,就是长连接断线后需要删除。
具体思路方法:
- Redis缓存机器号与userID
- Redis发布订阅模式使用topic的方式
- 具体可以使用一些规则,比如使用用户ID作为topic,来传输信息
4.1.2. 什么叫做通信协议?
这个问题源自于最近的项目,第一次接触IM项目,并且对网络方面的知识也不是特别的熟悉,在头脑风暴的时候,留下的问题。
在我来想,IM的关键是去解决如何缓存用户长连接这个事情,但是其实后来才知道这只是其中的一小部分。
中间与客户端讨论最多的是定协议,而讨论了很久我也没有明白他们口中的协议是什么意思,总觉得我们说话不在一个频道上。
最终脑子里留下了一个很大的问号,什么是XMPP协议、什么是MQTT协议、什么叫自定义协议?他们和TCP、websocket、HTTP有什么关系?
确切的说,XMPP协议、MQTT协议、websocket、HTTP、TCP他们之间的关系是什么?
4.1.2.1. TCP
从TCP说起,这些协议都是基于TCP来的。而自定义协议也是说的意思是基于TCP来自定义协议。
再往上HTTP与Websocket,HTTP其实就是我们最常使用的,它本质是是一种命令式的。只有客户端发送指令,服务器根据客户端发送的指令进行处理应答。
这个最大的问题就是在实时性上,在websocket没有出现之前,只能通过客户端轮询的方式去达到伪实时。
4.1.2.2. websocket
为了解决这个问题,诞生出websocket,它解决的问题是:
- 维持长连接,这样节省了HTTP冗长的header
- 服务器可以向客户端推送消息
注意:Websocket连接建立还是使用的HTTP请求。
4.1.2.3. XMPP协议
也是基于TCP而来,他是定义了一套标准的即时通讯的规范。传输的文本协议的定义都是使用的XML的方式来定义的,所以这也是诟病最多的地方,网络带宽消耗太大,是一个PC时代的IM协议。
即时通讯规范是什么意思?
- 弱网状态下,信息传输
- 长连接断开的回调
- 消息的重试机制
- 等等…
这是一系列的问题,而这套协议中包含了所有的处理逻辑,开发者不需要关心网络问题只需要通过封装好的API去调用即可,其实是对客户端最大限度的简化,标准化,也就是说有现成的实现。
而服务器,这只是一个协议,实现上并不管如何实现,所以XMPP服务器其实现在市面上开源的并不算特别的好。真正比较好的都是闭源赚钱了。
4.1.2.4. MQTT协议
MQTT全称是Message Queuing Telemetry Transport。
MQTT1消息队列遥测传输(英语:Message Queuing Telemetry Transport)是ISO 标准(ISO/IEC PRF 20922)2下基于发布 (Publish)/订阅 (Subscribe)范式的消息协议,可视为“数据传递的桥梁”3它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件,以解决当前繁重的数据传输协议,如:HTTP。 4 IBM公司的安迪·斯坦福-克拉克及Arcom公司的阿兰·尼普于1999年撰写了该协议的第一个版本 —— wiki
从介绍可以看出,MQTT本来解决的场景是远程控制设备的,现在更多的应用在物联网上,其实还挺亲切的,因为我的第一份工作做物联网当时就接触到了MQTT,当然知道还很少,以至于误会以为MQTT是基于websocket。
相比与XMPP主要特定是协议简洁,传输效率高,信息冗余小。基于订阅发布者模式。
当然因为不是专门为IM而设计的协议,所以还需要根据IM的特性去灵活的应用MQTT server。
其实可以直接将其当做一个长连接服务器,剩下的其他业务由我们自己来开发。这也算是一个重要的技术问题解决了。
4.1.3. 总结协议是什么意思?
协议是解决一个特定场景问题,所指定的标准。考虑到了此场景下各种可能出现的问题,有了标准就可以两端(服务端/客户端)分别开发。
4.1.4. 协议选型
搞懂了各种协议后开始选型:
- 确定通信协议
- 什么是通信协议?
- 即时通讯本质上是要维护很多个长连接,在维护长连接的过程中,需要很多工作要做,客户端和服务器,比如
- 长连接保活
- 消息发送的可达性
- 实时性
- 等
- 通信协议就是解决这个问题的
- 所以协议定义好了之后,就有一套标准的实现,对应的客户端就有一套标准的实现。这也是我一开始疑惑的地方,不知道开始讨论的协议是什么意思
- 关键点:要搞秦楚TPC、websocket、XMPP、mqtt这些之间的关系是什么
- 所以协议定义好了之后,就有一套标准的实现,对应的客户端就有一套标准的实现。这也是我一开始疑惑的地方,不知道开始讨论的协议是什么意思
- 选型备案
- XMPP
- 优点
- 有现成的客户端与服务端IM实现
- 缺点
- 协议太老,基于XML实现,流量与耗电量上都很大
- 开源的server并发性支持不够
- 优点
- mqtt
- 优点
- 传输数据定义非常简洁,冗余数据量小
- 消息可靠性上设计之初就有保障
- 不同档位的消息可达性设置
- 缺点
- 没有非常可靠的server实现
- 没有现成的基于mqtt的IM开发,只能作为一个长连接服务器,其他相关IM业务都需要自己实现
- 集群困难
- 自己实现上非常困难
- 优点
- websocket自定义
- 优点
- 自定义,业务结构熟悉,拓展新好
- 消息流量消耗少
- 缺点
- 坑比较多
- 很多时候需要去借鉴MQTT协议
- 存活方面
- 消息投递方面
- 工作量大
- 据说websocket比较容易攻击
- 优点
- tcp自定义
- 优点
- 更底层的实现,类似websocket优点
- 缺点
- 技能要求高
- 周期长
- 优点
- XMPP
- 即时通讯本质上是要维护很多个长连接,在维护长连接的过程中,需要很多工作要做,客户端和服务器,比如
- 什么是通信协议?
- 确定MQTT协议
- MQTT选型
- 总结:
- 将MQTT作为插件使用,存储业务以及IM业务都在此之外做
- 在MQTT基础上改造成符合IM业务的server
- 结论:
- 使用mqtt作为长连接服务器
- 选型锁定
- EMQ√
- hiveMQ
- 选型锁定
- 火峰开源基于Mqtt项目的IM
- 研究其IM业务
- 剥离MQTT模块,使用我们自己的MQTT服务器
- 使用mqtt作为长连接服务器
- 总结:
- MQTT选型
4.1.5. EMQ的使用
4.1.5.1. 如何搭建集群?
下载软件包,在不同的机器上。
修改文件\etc\emqx.conf 的node.name = emqx@192.168.1.47
,后面跟的一定要是本机域名,一定要相同如果不同会导致join出错。
client端clientid参数是关键,本地不同的clientid会导致新的链接
实现离线消息:
但是有最大队列数量,不多1000。
// MQTT的连接设置
options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(false);
4.1.5.2. EMQ集群是如何工作的?
相互互联 EMQ集群是相互互联的。因为长连接是有状态的(也就是说长连接是有状态的),所以需要找到对应推送的长连接才行。
共享订阅与发布信息 mqtt协议是基于发布订阅模式实现长连接的 推拉消息的。
如何共享订阅与发布信息? 假设有mqtt服务器4台(m1,m2,m3,m4)
- 所有节点都有相同的(所有订阅节点的总和)一份订阅树
- 所有订阅树上绑定着对应的节点(哪些订阅是属于哪些节点的),
- 由接受到的消息的服务器进行转发
具体发布订阅流程:
- 订阅消息
- client1订阅
topic/client1
,client1所在节点订阅成功后,广播给其他server(m2,m3,m4) - 所以集群中不仅有订阅树,还有一个订阅URL与节点的路由表
- client1订阅
- 发布消息
- 在找到与之匹配的topic,并且根据路由表找到对应的server,并广播
4.1.5.3. EMQ集群测试
- 集群两节点自动断开会自动断开
- 当两边任意一个断网时,集群会断开链接
- client端会自动断开
- 客户端可以通过编码的方式,维护一个长连接不断线
4.1.6. EMQ桥接模式与集群模式的区别
桥接模式:顾名思义 连接两个MQTT broker。
并且是定向的。
只能从一方->另一方
需要配置好。
疑问:
- 那么如果发送的一方挂掉了,那么不就桥接模式就断掉了吗?
- 如果多个桥接会不会有问题,比如说两个集群A,B。A中多个broker桥接到B集群中不同(或者相同的)的broker上会不会出现消息重发的现象
4.1.7. EMQ高级使用
在官方文档中有一个规则的使用,当时我一直不知道这个有什么用处。
直到后来思考IM业务的时候,才发现这个规则有巨大的用处。简单说这个规则就是定义一个特定的规则,当某个topic有消息传输时,接下来可以自定义动作要做的事情,仔细想想所有业务也逃不过这些操作,要么是操作数据库,要么是操作一段业务,而业务的话可以使用HTTP,如果是发送其他信息,可以直接配置。
这样想象空间就大很多。
4.1.8. 小问题
- 集群情况下如何监控每个节点与集群的健康关系
- 集群配置,每台设备都是一样,但是好像需要单独配置
- 数据库存储
- 消息业务操作钩子