Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/xiaoheiAh/hugo-theme-pure.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorotis <xiaohei.zyx@gmail.com>2020-01-02 14:17:06 +0300
committerotis <xiaohei.zyx@gmail.com>2020-01-02 14:17:06 +0300
commit0ffa5699bf1e55f028d4ccacfbf516bec52388fc (patch)
treeb2049169d0557704d15b0178dbc4c01068705b3a
parent49b29cdf3d9feef087cb30fd22ddd58e7b0ead00 (diff)
update gh-pagesgh-pages
-rw-r--r--2019/12/array/index.html5
-rw-r--r--2019/12/linkedlist/index.html5
-rw-r--r--categories/corejava/index.xml4
-rw-r--r--categories/hystrix/index.xml4
-rw-r--r--categories/index.xml4
-rw-r--r--categories/leetcode/index.xml4
-rw-r--r--categories/redis/index.xml4
-rw-r--r--categories/学习笔记/index.xml4
-rw-r--r--categories/消息队列/index.xml4
-rw-r--r--collections/index.xml4
-rw-r--r--index.html4
-rw-r--r--index.xml4
-rw-r--r--posts/index.xml4
-rw-r--r--searchindex.json2
-rw-r--r--tags/collections/index.xml4
-rw-r--r--tags/hugo/index.xml4
-rw-r--r--tags/hystrix/index.xml4
-rw-r--r--tags/index.xml4
-rw-r--r--tags/leetcode/index.xml4
-rw-r--r--tags/netty/index.xml4
-rw-r--r--tags/rabbitmq/index.xml4
-rw-r--r--tags/redis/index.xml4
-rw-r--r--tags/rust/index.xml4
-rw-r--r--tags/rxjava/index.xml4
-rw-r--r--tags/分布式锁/index.xml4
-rw-r--r--tags/响应式编程/index.xml4
-rw-r--r--tags/数据结构/index.xml4
27 files changed, 50 insertions, 58 deletions
diff --git a/2019/12/array/index.html b/2019/12/array/index.html
index 8efeece..0e782dd 100644
--- a/2019/12/array/index.html
+++ b/2019/12/array/index.html
@@ -346,9 +346,7 @@
</div>
</div>
<div class="article-entry marked-body js-toc-content" itemprop="articleBody">
-
-
-<h3 id="easy-1252-cells-with-odd-values-in-a-matrix-https-leetcode-com-problems-cells-with-odd-values-in-a-matrix-submissions"><a href="https://leetcode.com/problems/cells-with-odd-values-in-a-matrix/submissions/">Easy =&gt; 1252. Cells with Odd Values in a Matrix</a></h3>
+ <h3 id="easy-1252-cells-with-odd-values-in-a-matrix-https-leetcode-com-problems-cells-with-odd-values-in-a-matrix-submissions"><a href="https://leetcode.com/problems/cells-with-odd-values-in-a-matrix/submissions/">Easy =&gt; 1252. Cells with Odd Values in a Matrix</a></h3>
<pre><code class="language-java"> public int oddCells(int n, int m, int[][] indices) {
boolean[] oddRows = new boolean[n];
@@ -408,7 +406,6 @@
return curr;
}
</code></pre>
-
</div>
<div class="article-footer">
<blockquote class="mt-2x">
diff --git a/2019/12/linkedlist/index.html b/2019/12/linkedlist/index.html
index d21a2f7..c120554 100644
--- a/2019/12/linkedlist/index.html
+++ b/2019/12/linkedlist/index.html
@@ -346,9 +346,7 @@
</div>
</div>
<div class="article-entry marked-body js-toc-content" itemprop="articleBody">
-
-
-<p>链表需要注意的问题:</p>
+ <p>链表需要注意的问题:</p>
<ul>
<li>边界: 头结点尾结点的处理,链表长度为1时的处理</li>
@@ -497,7 +495,6 @@
return head;
}
</code></pre>
-
</div>
<div class="article-footer">
<blockquote class="mt-2x">
diff --git a/categories/corejava/index.xml b/categories/corejava/index.xml
index adb513d..17f9edd 100644
--- a/categories/corejava/index.xml
+++ b/categories/corejava/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/categories/hystrix/index.xml b/categories/hystrix/index.xml
index 07024a7..01c30cc 100644
--- a/categories/hystrix/index.xml
+++ b/categories/hystrix/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/categories/index.xml b/categories/index.xml
index 2c86a31..9fce5a0 100644
--- a/categories/index.xml
+++ b/categories/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/categories/leetcode/index.xml b/categories/leetcode/index.xml
index c4759c1..d54f14b 100644
--- a/categories/leetcode/index.xml
+++ b/categories/leetcode/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/categories/redis/index.xml b/categories/redis/index.xml
index 5012eab..b336867 100644
--- a/categories/redis/index.xml
+++ b/categories/redis/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/categories/学习笔记/index.xml b/categories/学习笔记/index.xml
index 4af5387..b5be7f4 100644
--- a/categories/学习笔记/index.xml
+++ b/categories/学习笔记/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/categories/消息队列/index.xml b/categories/消息队列/index.xml
index a303059..b845c94 100644
--- a/categories/消息队列/index.xml
+++ b/categories/消息队列/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/collections/index.xml b/collections/index.xml
index 4841af7..78f3ebc 100644
--- a/collections/index.xml
+++ b/collections/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/index.html b/index.html
index baccb17..4d81a2c 100644
--- a/index.html
+++ b/index.html
@@ -308,7 +308,6 @@
</h1>
</div>
- <div class="article-entry text-muted" itemprop="description">链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&gt; Remove Nth Node From End of</div>
<p class="article-meta">
<span class="article-date">
<i class="icon icon-calendar-check"></i>&nbsp;
@@ -338,14 +337,13 @@
<div class="article-header">
<h1 itemprop="name">
<a
- class="article-date"
+ class="article-title"
href="/hugo-theme-pure/2019/12/array/"
>「LeetCode」数组题解</a
>
</h1>
</div>
- <div class="article-entry text-muted" itemprop="description">Easy =&gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</div>
<p class="article-meta">
<span class="article-date">
<i class="icon icon-calendar-check"></i>&nbsp;
diff --git a/index.xml b/index.xml
index a735053..b985c0b 100644
--- a/index.xml
+++ b/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/posts/index.xml b/posts/index.xml
index d34471e..45cb555 100644
--- a/posts/index.xml
+++ b/posts/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/searchindex.json b/searchindex.json
index 20c6696..39e7f76 100644
--- a/searchindex.json
+++ b/searchindex.json
@@ -1 +1 @@
-{"categories":[{"title":"CoreJava","uri":"https://xiaohei.im/hugo-theme-pure/categories/corejava/"},{"title":"Hystrix","uri":"https://xiaohei.im/hugo-theme-pure/categories/hystrix/"},{"title":"leetcode","uri":"https://xiaohei.im/hugo-theme-pure/categories/leetcode/"},{"title":"redis","uri":"https://xiaohei.im/hugo-theme-pure/categories/redis/"},{"title":"学习笔记","uri":"https://xiaohei.im/hugo-theme-pure/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"},{"title":"消息队列","uri":"https://xiaohei.im/hugo-theme-pure/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"posts":[{"content":" 链表需要注意的问题:\n 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =\u0026gt; Remove Nth Node From End of List Medium public ListNode removeNthFromEnd(ListNode head, int n) { // 快慢指针 ListNode fast = head,slow = head,prev = null; while(fast.next != null) { if (--n \u0026lt;= 0) { // 先走 n 步后,slow 再走 prev = slow; slow = slow.next; } fast = fast.next; } // fast 走完,slow 刚好到倒数 n 的位置 // 删除 slow 节点 if( prev == null) { // 说明删除的是头结点 if(slow.next == null) { // 说明链表只有一个节点.且需要删除的就是这个. head = null; } else { // 大于一个节点就需要把 head 指向 slow.next head = prev = slow.next; } } else { prev.next = slow.next; } return head; } No.2 =\u0026gt; Add Two Numbers Medium 思路: 就按着这个链表顺序加,然后生成一个链表就自然是倒序的了.\npublic ListNode addTwoNumbers(ListNode l1, ListNode l2) { // 一次累加,reverse ListNode result = new ListNode(0); ListNode head = result; int carry = 0; while(l1 != null || l2 != null || carry \u0026gt; 0) { int v1 = 0; int v2 = 0; if(l1 != null) { v1 = l1.val; l1 = l1.next; } if(l2 != null) { v2 = l2.val; l2 = l2.next; } int sum = v1 + v2 + carry; result.next = new ListNode( sum % 10); result = result.next; carry = sum / 10 ; } return head.next; } No.21 =\u0026gt; Merge Two Sorted Lists EASY 非递归 public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 边界条件需要判断 if (l1 == null) return l2; if (l2 == null) return l1; ListNode dummy = new ListNode(0), head = dummy; while(l1 !=null || l2 != null) { if(l1 == null) { head.next = l2; l2 = l2.next; } else if (l2 == null) { head.next = l1; l1 = l1.next; } else if (l1.val \u0026gt;= l2.val) { head.next = l2; l2 = l2.next; } else { head.next = l1; l1 = l1.next; } head = head.next; } return dummy.next; } 递归 public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 边界条件需要判断 if (l1 == null) return l2; if (l2 == null) return l1; if (l1.val \u0026lt;= l2.val) { l1.next = mergeTwoLists(l1.next,l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } } No.24 =\u0026gt; Swap Nodes in Pairs Medium 思路: 走两步然后替换这两个node,考虑head为空和头两个节点交换的情况\npublic ListNode swapPairs(ListNode head) { if(head == null) { return null; } int step = 1; ListNode prev = null; ListNode curr = head; while(curr.next != null) { if(++step % 2 == 0) { // 每两个反转链表 if (prev == null) { // 是头结点与第二个节点反转 ListNode second = curr.next; curr.next = second.next; second.next = curr; head = second; } else { // 中间节点 swap ListNode second = curr.next; curr.next = curr.next.next; prev.next = second; second.next = curr; } } else { // 走两步~ prev = curr; curr = curr.next; } } return head; } ","id":0,"section":"posts","summary":"链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =\u0026gt; Remove Nth Node From End of","tags":["leetcode"],"title":"「LeetCode」链表题解","uri":"https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/","year":"2019"},{"content":" Easy =\u0026gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^= true; oddCols[item[1]] ^= true; } int oddCnt = 0; for(int i = 0; i \u0026lt; n; i++) { for(int j = 0; j \u0026lt; m; j++) { // 行列出现 奇数次 + 偶数次 才能是产生奇数 oddCnt += oddRows[i] != oddCols[j] ? 1 : 0; } } // Time Complexity: O(m*n + indices.length) return oddCnt; } EASY =\u0026gt;26. Remove Duplicates from Sorted Array 快慢指针的运用\npublic int removeDuplicates(int[] nums) { int slow = 0; int fast = 1; while(fast \u0026lt; nums.length) { if(nums[slow] != nums[fast]) { nums[++slow] = nums[fast++]; } else { fast++; } } return slow + 1; } EASY =\u0026gt; 27. Remove Element 快慢指针的运用\npublic int removeElement(int[] nums, int val) { int curr = 0; int p = 0; while(p \u0026lt; nums.length) { if(nums[p] == val ) { p++; } else { nums[curr++] = nums[p++]; } } return curr; }\t ","id":1,"section":"posts","summary":"Easy =\u0026gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=","tags":["leetcode"],"title":"「LeetCode」数组题解","uri":"https://xiaohei.im/hugo-theme-pure/2019/12/array/","year":"2019"},{"content":" Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。\n NIO: selector 模型,用一个线程监听多个连接的读写请求,减少线程资源的浪费.\nnetty 优点 使用 JDK 自带的NIO需要了解太多的概念,编程复杂,一不小心 bug 横飞 Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从 NIO 模型变身为 IO 模型 Netty 自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑 Netty 解决了 JDK 的很多包括空轮询在内的 Bug Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手 Netty 社区活跃,遇到问题随时邮件列表或者 issue Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大 Server端 // 负责服务端的启动 ServerBootstrap serverBootstrap = new ServerBootstrap(); // 负责接收新连接 NioEventLoopGroup boss = new NioEventLoopGroup(); // 负责读取数据及业务逻辑处理 NioEventLoopGroup worker = new NioEventLoopGroup(); serverBootstrap.group(boss, worker) // 指定服务端 IO 模型为 NIO .channel(NioServerSocketChannel.class) // 业务逻辑处理 .childHandler(new ChannelInitializer\u0026lt;NioSocketChannel\u0026gt;() { protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler\u0026lt;String\u0026gt;() { protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception { System.out.println(s); } }); } }) .bind(8000); NioSocketChannel/NioServerSocketChannel Netty 对 NIO 类型连接的抽象 handler() \u0026amp; childHandler() handler() 用于指定服务器端在启动过程中的一些逻辑 childHandler() 用于指定处理新连接数据的读写逻辑 attr() \u0026amp; childAttr() 分别可以给服务端连接,客户端连接指定相应的属性,后续通过 channel.attr() 可以拿到.\noption() \u0026amp; childOption() option() 用于给服务端连接设定一系列的属性,最常见的是 so_backlog\n// 表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024) childOption() 给每条连接设置一些属性\nserverBootstrap // 是否开启TCP底层心跳机制,true为开启 .childOption(ChannelOption.SO_KEEPALIVE, true) // 是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。 .childOption(ChannelOption.TCP_NODELAY, true) Client端 带连接失败重试的客户端,失败重试延迟为 2 的幂次.\n// 客户端启动 Bootstrap bootstrap = new Bootstrap(); // 线程模型 NioEventLoopGroup group = new NioEventLoopGroup(); bootstrap.group(group) // 指定 IO 模型为 NIO .channel(NioSocketChannel.class) // 业务逻辑处理 .handler(new ChannelInitializer\u0026lt;Channel\u0026gt;() { protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new StringEncoder()); } }); connect(bootstrap,\u0026quot;127.0.0.1\u0026quot;, 8000, MAX_RETRY); private static void connect(Bootstrap bootstrap, String host, int port, int retry) { bootstrap.connect(host, port).addListener(future -\u0026gt; { if (future.isSuccess()) { System.out.println(\u0026quot;连接成功!\u0026quot;); } else if (retry == 0) { System.err.println(\u0026quot;重试次数已用完,放弃连接!\u0026quot;); } else { // 第几次重连 int order = (MAX_RETRY - retry) + 1; // 本次重连的间隔 int delay = 1 \u0026lt;\u0026lt; order; System.err.println(new Date() + \u0026quot;: 连接失败,第\u0026quot; + order + \u0026quot;次重连……\u0026quot;); bootstrap.config().group().schedule(() -\u0026gt; connect(bootstrap, host, port, retry - 1), delay, TimeUnit .SECONDS); } }); } 其他方法 attr() 客户端绑定属性 option() 设置客户端 TCP 连接 ByteBuf netty 中的数据都是以 ByteBuf 为单位的,所有需要写出的数据都必须塞到 ByteBuf 中.\n ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃的字节,这部分数据是无效的;第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;最后一部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段。最后一部分虚线表示的是该 ByteBuf 最多还能扩容多少容量 以上三段内容是被两个指针给划分出来的,从左到右,依次是读指针(readerIndex)、写指针(writerIndex),然后还有一个变量 capacity,表示 ByteBuf 底层内存的总容量 从 ByteBuf 中每读取一个字节,readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读, 由此可以推论出当 readerIndex 与 writerIndex 相等的时候,ByteBuf 不可读 写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了 ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,如果容量不足,那么这个时候可以进行扩容,直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错 ByteBuf 容量相关API capacity() 表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实现机制有不同的计算方式,后面我们讲 ByteBuf 的分类的时候会讲到\nmaxCapacity() 表示 ByteBuf 底层最大能够占用多少字节的内存,当向 ByteBuf 中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常\nreadableBytes() 与 isReadable() readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于 writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false\nwritableBytes()、 isWritable() 与 maxWritableBytes() writableBytes() 表示 ByteBuf 当前可写的字节数,它的值等于 capacity-writerIndex,如果两者相等,则表示不可写,isWritable() 返回 false,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往 ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于 maxCapacity-writerIndex\nByteBuf 读写指针相关 API readerIndex() 与 readerIndex(int) 前者表示返回当前的读指针 readerIndex, 后者表示设置读指针\nwriteIndex() 与 writeIndex(int) 前者表示返回当前的写指针 writerIndex, 后者表示设置写指针\nmarkReaderIndex() 与 resetReaderIndex() 前者表示把当前的读指针保存起来,后者表示把当前的读指针恢复到之前保存的值\nmarkWriterIndex() 与 resetWriterIndex() 同上,但是针对写指针\nByteBuf 读写 API writeBytes(byte[] src) 与 buffer.readBytes(byte[] dst) writeBytes() 表示把字节数组 src 里面的数据全部写到 ByteBuf,而 readBytes() 指的是把 ByteBuf 里面的数据全部读取到 dst,这里 dst 字节数组的大小通常等于 readableBytes(),而 src 字节数组大小的长度通常小于等于 writableBytes()\nwriteByte(byte b) 与 buffer.readByte() writeByte() 表示往 ByteBuf 中写一个字节,而 buffer.readByte() 表示从 ByteBuf 中读取一个字节,类似的 API 还有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble()\n与读写 API 类似的 API 还有 getBytes、getByte() 与 setBytes()、setByte() 系列,唯一的区别就是 get/set 不会改变读写指针,而 read/write 会改变读写指针,这点在解析数据的时候千万要注意\nrelease() 与 retain() 由于 Netty 使用了 堆外内存,而堆外内存是不被 jvm 直接管理的,也就是说申请到的内存无法被垃圾回收器直接回收,所以需要我们手动回收。有点类似于c语言里面,申请到的内存必须手工释放,否则会造成内存泄漏。\nNetty 的 ByteBuf 是通过引用计数的方式管理的,如果一个 ByteBuf 没有地方被引用到,需要回收底层内存。默认情况下,当创建完一个 ByteBuf,它的引用为1,然后每次调用 retain() 方法, 它的引用就加一, release() 方法原理是将引用计数减一,减完之后如果发现引用计数为0,则直接回收 ByteBuf 底层的内存。\nslice()、duplicate()、copy() 这三个方法通常情况会放到一起比较,这三者的返回值都是一个新的 ByteBuf 对象\n slice() 方法从原始 ByteBuf 中截取一段,这段数据是从 readerIndex 到 writeIndex,同时,返回的新的 ByteBuf 的最大容量 maxCapacity 为原始 ByteBuf 的 readableBytes() duplicate() 方法把整个 ByteBuf 都截取出来,包括所有的数据,指针信息 slice() 方法与 duplicate() 方法的相同点是:底层内存以及引用计数与原始的 ByteBuf 共享,也就是说经过 slice() 或者 duplicate() 返回的 ByteBuf 调用 write 系列方法都会影响到 原始的 ByteBuf,但是它们都维持着与原始 ByteBuf 相同的内存引用计数和不同的读写指针 slice() 方法与 duplicate() 不同点就是:slice() 只截取从 readerIndex 到 writerIndex 之间的数据,它返回的 ByteBuf 的最大容量被限制到 原始 ByteBuf 的 readableBytes(), 而 duplicate() 是把整个 ByteBuf 都与原始的 ByteBuf 共享 slice() 方法与 duplicate() 方法不会拷贝数据,它们只是通过改变读写指针来改变读写的行为,而最后一个方法 copy() 会直接从原始的 ByteBuf 中拷贝所有的信息,包括读写指针以及底层对应的数据,因此,往 copy() 返回的 ByteBuf 中写数据不会影响到原始的 ByteBuf slice() 和 duplicate() 不会改变 ByteBuf 的引用计数,所以原始的 ByteBuf 调用 release() 之后发现引用计数为零,就开始释放内存,调用这两个方法返回的 ByteBuf 也会被释放,这个时候如果再对它们进行读写,就会报错。因此,我们可以通过调用一次 retain() 方法 来增加引用,表示它们对应的底层的内存多了一次引用,引用计数为2,在释放内存的时候,需要调用两次 release() 方法,将引用计数降到零,才会释放内存 这三个方法均维护着自己的读写指针,与原始的 ByteBuf 的读写指针无关,相互之间不受影响 Pipeline \u0026amp; ChannelHandler pipeline 的数据结构为 双向链表, 节点的类型是一个 ChannelHandlerContext 包含着 每一个 channel 的上下文信息, contenxt 中包裹着一个 handler 用于处理用户的逻辑,pipeline 利用 责任链 的模式执行完所有的 handler.\n内置的 ChannelHandler ByteToMessageDecoder 二进制 -\u0026gt; Java 对象转换,重写 decode 方法即可.默认情况下 ByteBuf 使用的是对外内存,通过引用计数判断是否需要清除.而该 Decoder 可以自动释放内存无需关心.\n SimpleChannelInboundHandler 自动选择对应的消息进行处理,自动传递对象\n MessageToByteEncoder 对象 -\u0026gt; 二进制\n粘包 \u0026amp; 拆包 https://www.cnblogs.com/wade-luffy/p/6165671.html\n TCP 的传输是基于字节流的,没有明显的分界,有可能会把应用层的多个包合在一块发出去(粘包),有可能把一个过大的包分多次发出(拆包),粘包/拆包是相对的,一方拆包,一方就要粘包.\nTCP粘包/拆包发生的原因 问题产生的原因有三个,分别如下。\n(1)应用程序write写入的字节大小大于套接口发送缓冲区大小;\n(2)进行MSS大小的TCP分段;\n(3)以太网帧的payload大于MTU进行IP分片。\n解决策略 通过应用层设计通用的结构保证.\n 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格; 在包尾增加回车换行符进行分割,例如FTP协议; 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度; 更复杂的应用层协议 netty 解决方案 netty 提供了多种拆包器,满足用户的需求,不需要自己来对 TCP 流进行处理.\n 固定长度拆包器 FixedLengthFrameDecoder\n 行拆包器 LineBasedFrameDecoder\n 数据包以换行符作为分隔.\n 分隔符拆包器 DelimiterBasedFrameDecoder 行拆包器的通用版,自定义分隔符\n 基于长度域拆包器 LengthFieldBasedFrameDecoder 自定义的协议中包含长度域字段,即可使用来拆包\n 每次的包不是定长的,怎么就能通过位移确认长度域,进而确定长度?\n答: 通过设置一个完整包的开始标志,确定是一个新包就可以了.比如通常会设置一个魔数,拆包前先判断是不是我们定义的包.然后再去通过位移定位到长度域.\n ChannelHandler 生命周期 handlerAdded() :指的是当检测到新连接之后,调用 ch.pipeline().addLast(new xxxHandler()); 之后的回调,表示在当前的 channel 中,已经成功添加了一个 handler 处理器。 channelRegistered():这个回调方法,表示当前的 channel 的所有的逻辑处理已经和某个 NIO 线程建立了绑定关系,accept 到新的连接,然后创建一个线程来处理这条连接的读写,Netty 里面是使用了线程池的方式,只需要从线程池里面去抓一个线程绑定在这个 channel 上即可,这里的 NIO 线程通常指的是 NioEventLoop,不理解没关系,后面我们还会讲到。 channelActive():当 channel 的所有的业务逻辑链准备完毕(也就是说 channel 的 pipeline 中已经添加完所有的 handler)以及绑定好一个 NIO 线程之后,这条连接算是真正激活了,接下来就会回调到此方法。 channelRead():客户端向服务端发来数据,每次都会回调此方法,表示有数据可读。 channelReadComplete():服务端每次读完一次完整的数据之后,回调该方法,表示数据读取完毕。 channelInactive(): 表面这条连接已经被关闭了,这条连接在 TCP 层面已经不再是 ESTABLISH 状态了 channelUnregistered(): 既然连接已经被关闭,那么与这条连接绑定的线程就不需要对这条连接负责了,这个回调就表明与这条连接对应的 NIO 线程移除掉对这条连接的处理 handlerRemoved():最后,我们给这条连接上添加的所有的业务逻辑处理器都给移除掉。 心跳 \u0026amp; 空闲检测 IdleStateHandler 空闲检测(一段时间内是否有读写).\n实现一个心跳 public class HeartBeatTimerHandler extends ChannelInboundHandlerAdapter { private static final int HEARTBEAT_INTERVAL = 5; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { scheduleSendHeartBeat(ctx); super.channelActive(ctx); } private void scheduleSendHeartBeat(ChannelHandlerContext ctx) { ctx.executor().schedule(() -\u0026gt; { if (ctx.channel().isActive()) { ctx.writeAndFlush(new HeartBeatRequestPacket()); scheduleSendHeartBeat(ctx); } }, HEARTBEAT_INTERVAL, TimeUnit.SECONDS); } } 性能优化方案 共享 handler @ChannelHandler.Sharable 压缩 handler - 合并编解码器 —— MessageToMessageCodec 虽然有状态的 handler 不能搞单例,但是你可以绑定到 channel 属性上,强行单例 缩短事件传播路径—— 放 Map 里,在第一个 handler 里根据指令来找具体 handler。 更改事件传播源—— 用 ctx.writeAndFlush() 不要用 ctx.channel().writeAndFlush() 减少阻塞主线程的操作—— 使用业务线程池,RPC 优化重点 计算耗时,使用回调 Future ","id":2,"section":"posts","summary":"","tags":["netty"],"title":"[学习笔记] Netty","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/netty/","year":"2019"},{"content":"Redis 官方高可用(HA)方案之一: Cluster.可以解决 sentinel 模式单点写入的问题.\n参考 https://juejin.im/post/5b8fc5536fb9a05d2d01fb11 http://www.redis.cn/topics/cluster-spec.html https://redis.io/topics/cluster-spec 玩玩集群 https://redis.io/topics/cluster-tutorial\n如果使用源码构建的,utils 目录下有一个脚本可以创建集群试玩.\n\nRedis Cluster 实现原理 一致性Hash算法 一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 \\(K/n\\) 个关键字重新映射,其中\\(K\\)是关键字的数量,\\(n\\)是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。\n-- 来自自由的百科全书\n 一致性 Hash 算法在很多领域都有实践,分布式缓存 Redis, 负载均衡 Nginx,一些 RPC 框架.一句话解释这个算法就是将请求均匀的分配给各个节点的算法.在 Redis 中就是对 key 的离散化,将其存到不同的节点上,在 Nginx 中就是讲请求离散化,均匀地达到不同的机器上,相同的请求始终可以打到同一台上.毕竟取模以后值又不会变,总是会到相同的一台嘛.\n一致性哈希 可以很好的解决 稳定性问题,可以将所有的 存储节点 排列在 收尾相接 的 Hash 环上,每个 key 在计算 Hash 后会 顺时针 找到 临接 的 存储节点 存放。而当有节点 加入 或 退出 时,仅影响该节点在 Hash 环上 顺时针相邻 的 后续节点。\n普通模式 \n 优点 加入 和 删除 节点只影响 哈希环 中 顺时针方向 的 相邻的节点,对其他节点无影响。\n 缺点 加减节点 会造成 哈希环 中部分数据 无法命中。当使用 少量节点 时,节点变化 将大范围影响 哈希环 中 数据映射,不适合 少量数据节点 的分布式方案。普通 的 一致性哈希分区 在增减节点时需要 增加一倍 或 减去一半 节点才能保证 数据 和 负载的均衡。\n虚拟槽 虚拟槽分区 巧妙地使用了 哈希空间,使用 分散度良好 的 哈希函数 把所有数据 映射 到一个 固定范围 的 整数集合 中,整数定义为 槽(slot)。这个范围一般 远远大于 节点数,比如 Redis Cluster 槽范围是 0 ~ 16383。槽 是集群内 数据管理 和 迁移 的 基本单位。采用 大范围槽 的主要目的是为了方便 数据拆分 和 集群扩展。每个节点会负责 一定数量的槽,如图所示:\n\n为什么是 16384 个槽? redis github 上有个对应的 issue, antirez 给了对应的回答.回答如下:\n\n总结下来就是避免节点之间交换消息时消息包过大.每个消息包都会通过 bitmap 存储当前节点的 slots 分配信息,slots = 16384 时占用 16384/8/1024 = 2KB. 65K slots就太大了,而且官方建议最好不要超过 1000 个节点,16k slots也就足够分配了.够用就行.\n为什么要提到 65K? \nRedis 实现的 CRC16 算法是 16 位的,最大值就是 65535,以这个值来计算 bitmap 就是 8K 左右.\n中文参考:https://www.cnblogs.com/rjzheng/p/11430592.html\n官方集群文档 Redis Cluster Bus: 节点的 TCP 通信及二进制协议的总称.所有节点通过 cluster bus 进行连接.还可以在集群中 传递 Pub/Sub 消息,处理手动的故障转移请求(用户执行).\nGossip: 通过 Gossip 协议传递集群消息保证集群每个节点最终都能获得所有节点的完整信息.\n客户端不需要在意请求到哪个节点,随机请求一个后,如果没有查到对应的key,会通过返回重定向的结果 -MOVED,-ASK 来重定向到真正含有请求key数据的节点上.\n可用性 Availability 出现网络分区时, cluster 在少数节点侧分区是不可用的.在多数节点的分区侧(假设至少有半数的节点且存在每个不可用的主节点的 slave ),集群会在 NODE_TIMEOUT + 选举及故障转移所需一定时间 后恢复.\nRedis Cluster 核心组件 键的分布式模型 Keys distribution model 键空间分割为 16384 个 slot, 也就是集群可以最多有 16384 个节点.(官方建议上线 1000 节点为佳)\n每个 master 节点维护一段 slot.每一个主节点可以有多个 slave 来应对网络分区或者故障转移时的问题,以及分担读的压力.\n核心算法: 将key进行hash取模映射到一个 slot 上. \\( HASH\\_SLOT = CRC16(key)\\ mod\\ 16384 \\)\nCRC16 在测试中针对不同的key能很好的离散化.效果显著.\n集群拓补 Redis Cluster 的节点连接是网状的,假设有 N 个节点,那么每个节点都会与 N-1 个节点建立 TCP 连接, 且每个节点需要接受 N-1 个外来的 TCP 连接.所有连接都是 keep alive.如果等待足够长时间没有得到对方的 PING 回复,就会尝试重连.由于连接呈网状的原因,节点使用的是 Gossip 协议来传递消息,更新节点信息,可以避免节点之间同时交换巨量的消息,防止消息的指数型增长.\n重定向\u0026amp;重新分片 MOVED Redirection 客户端可以向任意一个节点发送查询请求,包括 slave 节点.节点会对查询请求进行分析,如果该 key 就在当前节点,就直接查出来返回,如果不在,节点会去寻找该 key 对应的 slot 所属的节点是哪一个,然后返回给客户端一个 MOVED error.\nGET x -MOVED 3999 127.0.0.1:6381 该 error 包括了 key 所在的 slot,以及对应节点的 ip:port.客户端就可以重新对真正持有该 key 的节点发起查询请求.如果在发起请求前经过了很长的时间导致集群产生了重新配置(reconfiguration),客户端再发起请求后可能仍然没有拿到值,还是会收到一个 MOVED 响应,如此循环下去.\nCluster live reconfiguration Redis 集群是允许运行时增删节点的.增删节点的影响就是对 hash slot 的调整.增加一个节点就需要把现有的节点匀出来一部分给新节点,删除一个节点就要把该节点的 slot 合并给其他节点.\n核心的逻辑其实就是对 hash slots 的移动.从一个特殊的角度来看,移动 slot 就是移动一组 key,所以集群在 resharding 时真正做的其实是对 key 的移动.移动一个 hash slot 就是对该 slot 下的所有 keys 移动到另一个 slot 下.\nCluster Slot 相关命令 CLUSTER ADDSLOTS slot1 [slot2] ... [slotN] CLUSTER DELSLOTS slot1 [slot2] ... [slotN] CLUSTER SETSLOT slot NODE node CLUSTER SETSLOT slot MIGRATING node CLUSTER SETSLOT slot IMPORTING node ADDSLOTS, DELSLOTS 用于给节点分配/删除指定的 slots.分配后会通过 Gossip 广播该信息.ADDSLOTS 通常用在集群新建时为每一个 master 节点分配一部分 slot. DELSLOTS 主要用于手动设置集群配置或者用于 debug 时的操作.通常很少用.\nSETSLOT \u0026lt;slot\u0026gt; NODE 使用该命令就是给一个节点分配指定的 slot.\n否则就是需要设置 MIGRATING 和 IMPORTING 的命令了.这两个特殊的状态是为了将一个 slot 从一个节点迁移到另一个时使用的.\n 设置为 MIGRATING 时,节点会接受所有关于该 slot 的查询,但仅当 key 存在时,否则会返回一个 -ASK 的重定向转发到需要迁移到的目标节点. 设置为 IMPORTING 时,节点只接受带有 ASKING 的请求,如果客户端没有携带该命令,就会重定向到原来的节点去. 假设我们需要将节点A的 slot 8 迁移到节点B.那我们需要发送两条命令:\n 给B发送 : CLUSTER SETSLOT 8 IMPORTING A 给A发送: CLUSTER SETSLOT 8 MIGRATING B 如此操作后,客户端还是会对key存在于 SLOT 8 的请求给到 A 节点,当该 key 在节点A存在时返回,不存在时会让客户端 ASKING Node B 处理.\n此时不会再在节点 A 中创建新 key 了.同时, redis-trib 会执行对应的迁移操作.\nCLUSTER GETKEYSINSLOT slot count 上述命令会查询出指定 slot 下 count 个需要迁移的key.并对每一个key 执行 migrate 命令,将 key 从 A 迁移到 B.该操作为原子操作.\nMIGRATE target_host target_port key target_database id timeout migrate 对复杂键也进行了优化,迁移延迟较低,但是在集群中 big key 并不是一个明智的选择.\nASK redirection ASK 与 MOVED 的区别在于, MOVED 可以确定 slot 的确在其他的节点上,下一次查询就直接查重定向后的节点.而 ASK 只是将本次查询重定向到指定的节点,接下来的其他查询仍然要请求当前的节点.\n语义:\n 如果收到一个 ASK 重定向,仅将当前查询重定向到指定节点,后续查询仍指向当前节点. 使用 ASKING 进行重定向查询 还不能更新本地客户端的 slot -\u0026gt; node 缓存映射关系 当 slot 迁移完成后,节点 A 会发送 MOVED 消息,客户端就可以永久的将 slot 8 的请求指定到新的 ip:port.\n容错 Fault Tolerance 心跳 \u0026amp; Gossip Redis Cluster 节点会持续的交换 ping/pong 信息,两种信息没有本质区别,就是 message type 不同.下文我们统称 ping/pong 为 心跳包 (heartbeat packets)\n通常来说,PING一下就要触发一次 PONG 回复.但也并不全对,节点也可能就把 PONG 信息(包含了自己的配置)发给其他节点就不管了,这样的好处是可以尽快广播新的配置.\n通常,一个节点每秒会随机的 PING 几个节点,所以每个节点发出PING 包的数量是恒定的(收到 PONG 包的数量也是恒定的) ,而不去理会节点的数量了. 去中心化\n每个节点会确保给没有 PING 过的节点和超过一半 NODE_TIMEOT 没有收到 PONG 的节点发送 PING 消息.NODE_TIMEOUT 过后,节点还会尝试重连没响应的节点,确保是因为网络问题才不可达的.\n如果将 NODE_TIMEOUT 设置为较小的数字,并且节点数非常大,则全局交换的消息数会相当大,因为每个节点都将尝试对超过一半 NODE_TIMEOUT 还未刷新信息的其他节点发送 PING.\n心跳包结构 结构如下,源码注释就很详细了:\ntypedef struct { char sig[4]; /* Signature \u0026quot;RCmb\u0026quot; (Redis Cluster message bus). */ uint32_t totlen; /* Total length of this message */ uint16_t ver; /* Protocol version, currently set to 1. */ uint16_t port; /* TCP base port number. */ uint16_t type; /* Message type */ uint16_t count; /* Only used for some kind of messages. */ uint64_t currentEpoch; /* The epoch accordingly to the sending node. */ uint64_t configEpoch; /* The config epoch if it's a master, or the last epoch advertised by its master if it is a slave. */ uint64_t offset; /* Master replication offset if node is a master or processed replication offset if node is a slave. */ char sender[CLUSTER_NAMELEN]; /* Name of the sender node */ unsigned char myslots[CLUSTER_SLOTS/8]; char slaveof[CLUSTER_NAMELEN]; char myip[NET_IP_STR_LEN]; /* Sender IP, if not all zeroed. */ char notused1[34]; /* 34 bytes reserved for future usage. */ uint16_t cport; /* Sender TCP cluster bus port */ uint16_t flags; /* Sender node flags */ unsigned char state; /* Cluster state from the POV of the sender */ unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */ union clusterMsgData data; } clusterMsg; typedef struct { char nodename[CLUSTER_NAMELEN]; uint32_t ping_sent; uint32_t pong_received; char ip[NET_IP_STR_LEN]; /* IP address last time it was seen */ uint16_t port; /* base port last time it was seen */ uint16_t cport; /* cluster port last time it was seen */ uint16_t flags; /* node-\u0026gt;flags copy */ uint32_t notused1; } clusterMsgDataGossip; ","id":3,"section":"posts","summary":"\u003cp\u003eRedis 官方高可用(HA)方案之一: \u003cstrong\u003eCluster\u003c/strong\u003e.可以解决 \u003ccode\u003esentinel\u003c/code\u003e 模式单点写入的问题.\u003c/p\u003e","tags":["redis"],"title":"Redis HA - Cluster","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/cluster/","year":"2019"},{"content":"Redis 官方高可用(HA)方案之一: 哨兵模式\n这篇文章已经介绍的很全面了:https://juejin.im/post/5b7d226a6fb9a01a1e01ff64 自己就总结一些问题:\nsentinel 如何保证集群高可用? 时刻与监控的节点保持心跳(PING),订阅 __sentinel__:hello 频道实时更新配置并持久化到磁盘 自动发现监听节点的其他 sentinel 保持通信 节点不可达时询问其他节点确认是否不可达,是否需要执行故障转移(半数投票) 故障转移后广播配置,帮助其他从节点切换到新的主节点,以 epoch 最大的配置为准 sentinel 如何判定节点下线? 主观下线/客观下线\nsentinel 的局限性? Redis Sentinel 仅仅解决了 高可用 的问题,对于 主节点 单点写入和单节点无法扩容等问题,还需要引入 Redis Cluster 集群模式 予以解决。\n官方文档介绍 https://redis.io/topics/sentinel\n使用 sentinel 的原因: 做到无人工介入的自动容错 redis 集群.\n sentinel 宏观概览:\n 监控 Monitoring: 持续监控主从节点的运行状态 通知 Notification: 节点异常时,通过暴露的 API 可以及时报警 自动故障转移 Automatic Failover: 主节点宕机后,可以自动晋升从节点为新主节点,其他节点会重新连接到新主节点,应用也会被通知相应的节点变化 提供配置 Configuration Provider: sentinel 维护着主从的节点信息,客户端会连接sentinel 获取主节点信息. sentinel 天生分布式,多节点协同的好处在于:\n 多数节点都同意主节点不可用时才执行故障检测.有效避免错判. 多节点可以提升系统鲁棒性(system robust),避免单点故障 使用须知 至少 3 个 sentinel 保证系统鲁棒性 节点最好放在不同的主机或虚拟机,降低级联故障(一下全GG) 由于 redis 采用的是异步复制,sentinel + redis 不能保证故障期间确认的写入(主从可能无法通信,确认复制进度).sentinel 可以在发布时控制一定时间内数据不丢失,但也不是万全之策. 客户端需要支持 sentinel (常用 Java 客户端基本都支持) 高可用并不是百分之百有效,即时你时时刻刻都在测试,产线环境也在跑,保不准凌晨就 GG,也没办法不是. Sentinel,Docker,或者其他形式的网络地址交换或端口映射需要加倍小心:Docker执行端口重新映射,破坏Sentinel自动发现其他的哨兵进程和主节点的 replicas 列表。 Sentinel 设置 redis 安装目录下有一个 sentinel.conf 模板配置可以参考.最小化配置如下:\nsentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1 sentinel monitor resque 192.168.1.3 6380 4 sentinel down-after-milliseconds resque 10000 sentinel failover-timeout resque 180000 sentinel parallel-syncs resque 5 不需要配置 replicas , sentinel 可以自动从主节点中获取 INFO 信息.同时该配置也会实时重写的: 新的 sentinel 节点加入或者故障转移 replica 晋升时.\nsentinel monitor \u0026lt;master-group-name\u0026gt; \u0026lt;ip\u0026gt; \u0026lt;port\u0026gt; \u0026lt;quorum\u0026gt; 从命令就可以看出来一些名堂: 监控地址为 ip:port,name 为 master-group-name 的主节点.\nquorum: 判断节点确实已经下线的支持票数(由 Sentinel 节点进行投票),票数超过一定范围后就可以让节点下线并作故障转移.但 quorum 只是针对于下线判断,执行故障转移需要在 sentinel 集群选举(投票)出一个 leader 来执行故障转移.\ne.g. quorum = 2, sentinel 节点数 = 5\n 如果有两个 sentinel 节点认为主节点下线了,那么这两个节点中的一个会尝试开始执行故障转移. 如果有超过半数 sentinel 节点存在(当前情况下即活着 3 个 sentinel 节点),故障转移就会被授权真正开始执行. 核心概念: sentinel 节点半数不可达就不允许执行 故障转移.\nsentinel \u0026lt;option_name\u0026gt; \u0026lt;master_name\u0026gt; \u0026lt;option_value\u0026gt; sentinel 其它的配置基本都是这个格式.\n down-after-milliseconds 节点宕机超过该毫秒时间后 sentinel 节点才能认为其不可达. parallel-syncs 在发生failover主从切换时,这个选项指定了最多可以有多少个 replica 同时对新的master 进行同步,这个数字越小,完成主从故障转移所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为主从同步而不可用。可以通过将这个值设为1来保证每次只有一个 replica 处于不能处理命令请求的状态。 所有配置都可以通过 SENTINEL SET 热更新.\n添加/删除 sentinel 节点 添加: 启动一个新的 sentinel 即可.10s就可以获得其他 sentinel 节点以及主节点的 replicas 信息了.\n多节点添加:建议 one by one,等到当前节点添加进集群后,再添加下一个.添加节点过程中可能会出故障.\n删除节点: sentinel 节点不会丢失见过 sentinel 节点信息,即使这些节点已经挂了.所以需要在没有网络分区的情况下做以下几步:\n 终止你想要关掉的 sentinel 节点进程 发送一条命令 SENTINEL RESET * 给所有 sentinel 节点.如果只想对单一 master 处理,把 * 换成主节点名称.等一会儿~ 通过 SENTINEL MASTER 命令查看节点是否已删除 主观下线/客观下线 sentinel 中有两种下线状态.\n 主观下线(Subjectively Down) aka. SDOWN 当前 sentinel 认为自己监控的节点下线了,即主观下线.SDOWN 判定的条件为: sentinel 节点向监控节点发送 PING 命令在设置的 is-master-down-after-milliseconds 毫秒后没有收到有效回复则判定为 SDOWN\n 客观下线(Objectively Down) aka. ODOWN 有 quorum 数量的 sentinel 节点认为监控的节点 SDOWN.当一个 sentinel 节点认为监控的节点 SDOWN 后,会向其它节点发送 SENTINEL is-master-down-by-addr 命令来判断其它节点对该节点的监控状态.如果回执为 已下线 的节点数+自身大于 quorum 数量,则判定为 客观下线\nPING 命令的有效回复有什么?\n +PONG -LOADING error -MASTERDOWN error 其它回复都是无效的.需要注意的是: 只要收到有效回复就不会认为其 SDOWN 了.\nSDOWN 并不能触发故障转移,只能判定节点不可用.要触发故障转移,必须达到 ODOWN 状态.\nSDOWN -\u0026gt; ODWN? sentinel 没有使用强一致性的算法来保证 SDOWN -\u0026gt; ODOWN 的转换,而是使用的Gossip协议来保证最终一致性.在给定的时间范围内,给定的 sentinel 节点收到了足够多(quorum)的其它 sentinel 节点的 SDOWN 确认,就会从 SDOWN 切换到 ODOWN 了.\n真正执行故障转移时会有比较严格的授权,但是前提也得是 ODOWN 状态才行.ODOWN 只针对 master 节点,replicas 和 sentinels 只会有 SDOWN 状态.如果 replica 变为 SDOWN 了,在故障转移的时候就不会被晋升.\n自动发现 auto discovery sentinel 节点之间会保持连接来互相检查是否可用,交换信息,但是并不需要在启动的时候配置一长串其他 sentinel 节点的地址. sentinel 会利用 redis 的 Pub/Sub 能力来发现监控了相同 master/replicas 的 sentinel 节点.replicas 自动发现是一样的原理.\n如何实现的? 向一个叫 __sentinel__:hello 的 channel 发送 hello 消息.\n 每个 sentinel 节点都会向每一个它监控的 master 和 replica 的叫做 __sentinel__:hello 的Pub/Sub channel 广播自己的 ip,port,runid.2s 一次. 每个订阅了 master 和 replica 的 sentinel 都会收到消息,并会去判断有新的 sentinel 节点就会被添加进来. 这个 hello 消息同样包含着最新的 master 全量配置,每个收到消息的 sentinel 会进行比对更新. 添加新的 sentinel 节点时会提前判断该节点信息是否已经存在. sentinel 强制更新配置 sentinel 是一个总会尝试将当前最新的配置强制更新到所有监控节点的系统.\n 这可能也是一种 tradeoff 吧.比如 replica 如果连错 master 了,那 sentinel 就必须把它矫正过来,重连正确的master.\n 副本选举 sentinel 可以执行故障转移时,需要选择一个合适的 replica 晋升.\n评估条件 与 master 的断连时间 replica 优先级-\u0026gt;可以设置 复制进度 offset Run ID 判定需要跳过的节点 (down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state 如果一个 replica 的断连时间超过上面这个表达式,那就认为该节点不可靠,不考虑. down-after-milliseconds 是通过设置的,milliseconds_since_master_is_in_SDOWN_state 指在执行故障转移时 master 仍不可用的时间.\n选举过程 符合上述条件后才会对其按照条件进行排序.顺序如下:\n 首先根据 replica-priority 排序(redis.conf 进行设置),值越小越优先 priority 相同时,比较 offset,值越大越优先(同步最完整) 如果 priority,offset 都相同,就会判断 run ID 的字典序.越小的 run ID 并不是说有什么优势,但是比起重排序随机选一个 replica,字典序选举方式更有确定性更有用(大白话). 建议所有节点都设置 replica-priority.如果 replica-priority 设置为 0, 表示永远不会被选为 master .但是在故障转移后 sentinel 会重置通过这种方式设置的配置,以便可以与新的 master 连接,唯一的区别就是该节点不会是主节点.\n深入算法内部 Quorum quorum 参数会被 sentinel 集群用来判断是否有这个数量的 sentinel 节点认为 master 已经 SDOWN 了,需不需要转为 ODOWN 触发故障转移 failover.\n但是,触发故障转移后,至少需要有半数的 sentinel 节点(如果 quorum 值比半数还多,那其实需要有quorum个节点)授权给一个 sentinel 节点才能真正执行.小于半数节点不允许执行.\n e.g. 5 instances quorum = 2\n当有2个节点认为 master 不可达时,就会触发 failover.但是需要有至少3个节点授权给这2个节点之一才能真正执行failover.\n如果 quorum = 5,那就需要所有节点都认为 master 不可达,才能触发failover,并且所有节点都要授权.\n 纪元 Configuration Epochs 为什么需要获取半数以上的授权执行 failover?\n当一个 sentinel 节点被授权后,会获得一个可以用于故障转移节点的唯一的纪元(configuration epoch)标志.这是一个在故障转移完成后针对新配置的版本号 number.因为是多数同意将指定的版本分配给指定授权的 sentinel ,所以不会有其他节点使用这个版本号.也就意味着每一次故障转移时生成的新配置都有唯一的版本号标识.\nsentinel 集群有一条规则: 如果 sentinel A 投票给 sentinel B 去执行故障转移,A 会等待一段时间后对同一个主节点再次进行故障转移.这个时间可以通过 sentinel.conf 的 failover-timeout 进行配置.这就意味着不会有节点在同一时间对同一个主节点进行故障转移,被授权的节点回先执行,失败了后面会有其他的节点进行重试.\nsentinel 保证 liveness 特性(我的理解就是不会宕机一直存活):如果有多个节点可用,只会选择一个节点去执行故障转移.\nsentinel 同样保证 safety 特性:每一个节点都会尝试使用不同的 configuration epoch 对相同的节点进行故障转移.\n配置传递 Configuration propagation 故障转移完成后,sentinel 会广播新的配置给其他 sentinel 节点更新这个新的主节点信息.执行故障转移的主节点还需要对新的主节点执行 SLAVE NO ONE,稍后在 INFO 命令中就可以看到这个主节点了.\n所有 sentinel 节点都会广播配置信息,通过 __sentinel__:hello channel 广播出去.配置信息都带有 epoch ,值越大越会被当做最新的配置.\n网络分区后的一致性问题 Redis + Sentinel 架构是保证最终一致性的系统,在发生网络分区恢复时,不可避免的会丢失数据.\n如果把 redis 当做缓存来用,数据丢了也没事,可以再去库里查嘛.\n如果把 redis 当做存储来用,那最好配上下面两个配置降低损失.\nmin-replicas-to-write 1 min-replicas-max-lag 10 Sentinel 状态持久化 Sentinel 状态持久化在 sentinel.conf 中,每次手挡新配置,或者创建配置,都会带着configuration epoch 一起持久化到硬盘,重启时就没有问题了.\n","id":4,"section":"posts","summary":"\u003cp\u003eRedis 官方高可用(HA)方案之一: \u003cstrong\u003e哨兵模式\u003c/strong\u003e\u003c/p\u003e","tags":["redis"],"title":"Redis HA - 哨兵模式","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/sentinel/","year":"2019"},{"content":"之前对redis 的复制只有一点点了解,这次想要搞明白的是:如何实现的复制? 复制会遇到哪些问题(时延/一致性保证/网络故障时的处理)? 如何解决?高可用实现方案?\n文章有部分是直接翻译的 https://redis.io/topics/replication\n复制是什么? 分布式系统有一个重要的点时保证数据不丢失,数据不丢失就意味着不能单点,不能单点就意味着最好能把数据多存几份形成数据的冗余.这就是复制的来由.复制类型主要是两种: 同步, 异步. 前者需要等待所有的节点返回写入确认,后者只需要返回个确认收到就行.\nRedis 主从复制 主从复制作用 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。 Redis 复制设计要点 默认使用异步复制 \u0026ndash; replica -\u0026gt; master 异步返回处理了多少数据的结果(偏移量)\n master 可以多 replicas\n replicas 可以从其他 replica 同步(从从复制).类似于级联更新的架构.\n master 主节点在同步时不会阻塞. 在 replica 侧复制也是非阻塞的.在进行初始化同步(全量)时,可以使用 replica上的旧数据供客户端查询.也可以在 redis.conf 进行配置,在初始化同步完成前客户端的请求都报错.初始化同步完成后,需要删除老数据,加载新数据.在这段时间中会阻塞外部连接请求,数据量大的话可能要很久.从 4.0 版本后,删除老数据可以通过多线程来优化效率,但是加载新数据还是会 阻塞. 复制可以用来弹性扩容,提供多可读副本,提升数据安全性,保证高可用 副本可以避免 master 保存全量数据到磁盘的资源消耗:可以由 replica 完成持久化,或者开启 aof 写入.不过需要慎重: 这样会导致 master 节点再重启时会是空的,其他 replica复制时也就成空的了. 主从复制过程 每一个 master 节点都会有一个特别大的随机数(40字节十六进制随机字符)作为 replication ID 来标识自己.每个 master 节点也有一个 持续递增的 offset 来记录发送给 replicas 的每一个 byte,利用该 offset 来保证副本更新的状态.\n当一个 replica 连接到 master 时,会使用 PSYNC 命令发送之前复制的 master 的 replicationID,以及自己的更新进度(offset).master 可以根据这个值给副本按需返回为更新的数据.如果在master 的 backlog buffer 中没有对应的数据可以给到,副本发送的 replicationID 与 master 的 ID 不一致,就会触发全量复制(Full Synchronization).\nbacklog buffer 是啥? 复制积压缓冲区,在 master 有 replica 进行复制时,存储 master 最近一段时间的写命令,以便在 replica 断开重连后,可以利用缓冲区更新断开这段时间中,从节点丢掉的更新.\nbacklog buffer 是有固定的长度,先进先出的队列,默认大小 1MB. 其实就是一个环.buffer 会存储每一个 offset 已经对应的写命令,这样 replica 在断连恢复后,发送 PSYNC 命令提供其最后一次更新的 offset, master 就可以根据 replica 提供的 offset 去 buffer 中找对应的数据发送给 replica 保持最新.\n如果断开时间过长,buffer 存储的数据已经换了一批又一批, replica 在重连后发送给 master 的 offset 在 buffer 已经找不到了.此时会触发 全量复制.\n全量复制 master调用 bgsave 在后台生成 rdb 文件.同时记录客户端新的写命令到 backlog buffer 中. rdb 文件生成后,发送给 replica 保存到其硬盘中,然后再加载到内存中并通知master 加载完成.然后 master 会发送 buffer pool 中的命令给 replica 完成最后的同步.\nSYNC/PSYNC 两者都是同步的命令.SYNC 只支持全量同步, PSYNC 支持上述的部分同步.2.8 版本之前只有 SYNC,为了避免每次都只能全量同步造成资源的浪费,就新增了 PSYNC 命令实现部分同步的语义.\nReplication ID Replication ID 标记了数据的历史信息,从0开始成为master 的节点,或者晋升成为 master 的 replica 节点,都会生成一个 Replication ID.replicas 的 replId 是和其复制的 master 一致的,master 通过该 ID 和 offset 来判断主从之间数据是否一致.\n为什么有两个replId? /* src/server.h */ struct redisServer { ... /* Replication (master) */ char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */ char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master*/ ... long long master_repl_offset; /* My current replication offset */ long long second_replid_offset; /* Accept offsets up to this for replid2. */ ... } 一般情况下,故障转移(failover)后,晋升的 replica 需要记录自己之前复制的 master 对应的 replId.其他 replicas 会向新 master 进行部分同步,但发送过来的 replId 还是之前 master 的.所以 replica 在晋升时,会生成新的replId,并将原来的 replId 记录到 replId2,同时记录下当时所更新到的 offset 到 second_replid_offset.当其他的 replica 向新 master 进行连接时,新 master 会比较当前的和之前 master 的 replId,offset,这样就可以防止在故障转移后导致不必要的 全量复制.\n为什么晋升后需要生成新 replId? old master 可能还存活,但由于网络分区原因无法和其他 replicas 通信,如果保留原来的 id 不再生成,就会导致有相同数据相同id的master 存在.\n无盘复制 全量复制时,master 会创建 rdb 文件存到磁盘,然后再读取 rdb 文件发送给 replicas.磁盘性能差的情况下,效率会很低,所以支持了 无盘复制 \u0026ndash; 子进程直接发送 rdb 给 replicas,不经过硬盘存储.\n如何处理可以过期的键? 副本不会主动去过期键,而是由 master 过期键后向副本发送 DEL 命令. 由于是通过 master 驱动,副本收到 DEL 命令可能有延迟,这就会导致从副本中还可能查到已过期的键.针对这种情况,副本会利用自身的物理时钟作为依据报告该键不存在(仅在不违反数据一致性的 只读操作),因为 DEL 命令总是会发过来的. LUA 脚本执行期间,是不会去执行 key 过期的.脚本执行期间相当于 master 时间冻结了,不作过期时间的记录,所以在这期间过期键只有存在或不存在的概念.这样可以防止键在执行期间过期.同时,master 也需要发送同样的脚本给副本,保持一致. 如果replica 晋升 master 了,它就会自己去处理键的过期了.\n心跳机制 在正常的进行 部分同步 期间,主从之间会维持心跳,来协助超时判断,数据安全等问题.\nmaster -\u0026gt; slave 主节点发送 PING ,从节点回复 PONG.目的是让从节点进行超时判断.发送频率有 repl-ping-slave-period 参数控制.单位秒,默认 10s.\nreplica -\u0026gt; master 从节点向主节点发送 REPLCONF ACK {offset} ,频率每秒1次.作用:\n 试试检测主从网络状态,该命令被主节点用于复制超时的判断. 检测命令丢失,主节点会比较从节点发送的 offset 与自身的是否一致,不一致则从 buffer 中查找对应数据进行补发,如果 buffer 中没有对应数据,则会进行全量复制. 辅助保证从节点的数量和延迟,master 通过 min-salves-to-write 和 min-slaves-max-lag 参数,来保证主节点在不安全情况下不会执行写命令.是指从节点数量太少,或延迟过高。例如 min-slaves-to-write 和min-slaves-max-lag 分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。 复制惨痛案例 数据过期问题 数据删除没有及时同步到从节点,其实在 3.2 版本后避免了这个问题.从节点会对键进行判断,已过期不展示.\n如何处理可以过期的键?\n数据延迟不一致 这种情况不可避免.可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟通过offset判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。\n复制超时导致复制中断 为什么要判断超时? master 在判断超时后,会释放从节点的连接,释放资源. 断开后即时重连 判断机制? 核心参数: repl-timeout ,默认 60s.\n(1)主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点 REPLCONF ACK 的时间,是否超过了 repl-timeout 值,如果超过了则释放相应从节点的连接。\n(2)从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:\n 如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过 repl-timeout,则释放与主节点的连接; 如果当前处于数据同步阶段,且收到主节点的 RDB 文件的时间超时,则停止数据同步,释放连接; 如果当前处于命令传播阶段,且距离上次收到主节点的 PING 命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。 问题 全量复制时,如果 RDB 文件过大,耗时很长就会触发超时,此时从节点会重连,再生成RDB,再超时,在生成RDB\u0026hellip;解决方案就是单机数据量尽量不要太大,增大 repl-timeout. 慢查询导致服务器阻塞: keys *,hgetall backlog 过小导致无限全量复制 backlog buffer 是固定大小的,写入命令超出长度就会覆盖.如果再全量复制的时候用时超长,存入buffer 的命令超过了其大小限制,那么就会导致连接中断,再重连,全量复制,连接中断,全量复制\u0026hellip;.死循环.解决方案就是需要正确设置 backlog buffer 的大小. 通过 client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds} 配置,默认值为 client-output-buffer-limit slave 256MB 64MB 60,其含义是:如果 buffer 大于256MB,或者连续 60s 大于 64MB ,则主节点会断开与该从节点的连接。该参数是可以通过 config set 命令动态配置的(即不重启Redis也可以生效).\n参考 深入学习Redis(3):主从复制\n 「Redis 设计与实现」\n https://redis.io/topics/replication\n ","id":5,"section":"posts","summary":"\u003cp\u003e之前对\u003ccode\u003eredis\u003c/code\u003e 的复制只有一点点了解,这次想要搞明白的是:如何实现的复制? 复制会遇到哪些问题(时延/一致性保证/网络故障时的处理)? 如何解决?高可用实现方案?\u003c/p\u003e\n\n\u003cp\u003e文章有部分是直接翻译的 \u003ca href=\"https://redis.io/topics/replication\"\u003ehttps://redis.io/topics/replication\u003c/a\u003e\u003c/p\u003e","tags":["redis"],"title":"Redis-复制功能探索","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/replication/","year":"2019"},{"content":" 事件驱动程序设计(英语:Event-driven programming)是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动作(如鼠标的按键,键盘的按键动作)或者是由其他程序的消息来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由程序员来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在交互程序(Interactive program)的情况下孕育而生的。 \u0026ndash;wikipedia\n 文件事件 服务端通过套接字与客户端进行连接,文件事件就是服务端对套接字操作的抽象.服务端与客户端的通信会产生多种文件事件(连接 accept ,读取 read, 写入 write ,关闭 close),服务器监听并处理相应的事件.\n文件事件处理器 redis 基于 Reactor 模式实现了网络事件处理 \u0026ndash;\u0026gt; 文件时间处理器.通过 I/O 多路复用 保证了单进程下的高性能网络模型.\n什么是 I/O Multiplexing? 参考: https://draveness.me/redis-io-multiplexing\n首先需要知道什么是文件描述符(File Descriptor ,简称 FD)? 文件描述符就是操作系统中操作文件时内核返回的一个 非负整数,可以通过文件描述符来指定待读写的文件.而套接字 socket 本质上也是一种文件描述符.\n简单来说就是通常我们使用的 I/O 模型是阻塞型的,服务器在处理一个客户端请求(即处理一个FD)时无法再处理其它的了. I/O多路复用 是通过利用操作系统的多路复用函数(select())来监听多个 FD 的可读可写情况,一旦有可读或可写的 FD,select() 就返回对应的个数.\n由于不同操作系统的有不同的多路复用函数,select是性能最差的.而 redis 也会根据操作系统的不同选择性能最好的函数来使用.并且由于不同平台的差异, redis 提供了一套相同的结构并针对不同平台进行了实现,以此屏蔽了对上层应用的影响.\n#ifdef HAVE_EVPORT #include \u0026quot;ae_evport.c\u0026quot; #else #ifdef HAVE_EPOLL #include \u0026quot;ae_epoll.c\u0026quot; #else #ifdef HAVE_KQUEUE #include \u0026quot;ae_kqueue.c\u0026quot; #else #include \u0026quot;ae_select.c\u0026quot; #endif #endif #endif 文件事件处理器结构 每一个套接字 socket 可以执行连接,读写,关闭操作时,会产生一个 文件事件.,I/O 多路复用 监听这些 FD 的操作请求,并向 文件事件派发器 传递产生文件事件的 FD. 虽然会并发的产生 N 个文件事件,但 I/O多路复用 会将其都放入一个队列中,顺序且同步地向 文件事件分派器 传送.处理完一个再传下一个.\n文件事件派发器 接收到 FD 后,就会根据FD 所绑定的文件事件类型选择相应的事件处理器进行处理.\n文件事件类型 AE_READABLE 可读事件 客户端对套接字 write 操作, close 操作或者客户端与服务端进行连接(出现 acceptable 套接字)时产生可读事件\n AE_WRITABLE 可写事件 客户端对套接字执行 read 操作,套接字产生可写事件\n AE_NONE 无任何事件 事件处理的先后顺序 AE_READABLE \u0026gt; AE_WRITABLE\n事件处理器 事件处理器是针对不同的文件事件实现的逻辑.客户端连接时,服务器需要进行应答,此时服务器就会将套接字关联到应答处理器.接收客户端的命令请求,服务器会将套接字关联到命令请求处理器.\n常用时间处理器 连接应答处理器 networking.c/acceptTcpHandler 客户端连接时会对其进应答.redis 在初始化时会将服务器的监听套接字的可读事件与该处理器关联起来,客户端只要连接监听套接字就会产生可读事件,执行对应的逻辑.\n 命令请求处理器 networking.c/readQueryFromClient 客户端连接服务器后,服务器会将客户端套接字的可读事件与命令请求处理器关联起来,当客户端向服务器发送命令请求时,产生可读事件,执行对应逻辑.\n 命令回复处理器 networking.c/sendReplyToClient 服务器有命令回复需要传送给客户端时,服务器会将客户端套接字的可写事件与命令回复处理器关联起来,客户端准备好接收服务器回复时,会产生可写事件,触发命令回复器执行.服务器发送完毕时,会解除关联.\n文件事件处理流程 aeCreateFileEvent 可以将一个给定FD 的给定事件加入到多路复用的监听范围中,并将事件与时间处理器关联\naeDeleteFileEvent 取消给定FD 的给定事件的监听\naeApiPoll 该方法会在每个平台的多路复用中进行实现,阻塞等待所有监听的FD 所产生的事件并返回可用时间的数量.会有超时处理.\n时间事件 Redis 中有两种时间事件 \u0026mdash;- 定时事件(隔一段时间执行一次),非定时事件(某个时间点执行一次)\n属性 id 全局唯一ID,顺序递增 when 毫秒精度 UNIX 时间戳,记录时间事件到达时间 timeProc 时间事件处理器,需要执行时间事件时,根据该处理器执行 时间事件是定时还是非定时,取决去 timeProc 返回值是否等于 AE_NOMORE. 等于则给事件ID标记为待删除,不等于则更新执行时间到下一次.\nretval = te-\u0026gt;timeProc(eventLoop, id, te-\u0026gt;clientData); if (retval != AE_NOMORE) { aeAddMillisecondsToNow(retval,\u0026amp;te-\u0026gt;when_sec,\u0026amp;te-\u0026gt;when_ms); } else { te-\u0026gt;id = AE_DELETED_EVENT_ID; } Redis 处理时间事件时,不会在当前循环中直接移除不再需要执行的事件,而是会在当前循环中将时间事件的 id 设置为 AE_DELETED_EVENT_ID,然后再下一个循环中删除,并执行绑定的 finalizerProc。\n/* Remove events scheduled for deletion. */ if (te-\u0026gt;id == AE_DELETED_EVENT_ID) { aeTimeEvent *next = te-\u0026gt;next; if (te-\u0026gt;prev) te-\u0026gt;prev-\u0026gt;next = te-\u0026gt;next; else eventLoop-\u0026gt;timeEventHead = te-\u0026gt;next; if (te-\u0026gt;next) te-\u0026gt;next-\u0026gt;prev = te-\u0026gt;prev; if (te-\u0026gt;finalizerProc) te-\u0026gt;finalizerProc(eventLoop, te-\u0026gt;clientData); zfree(te); te = next; continue; }\t 时钟问题 时间事件的执行影响最大的因素就是 系统时间. 系统时间的调整会影响时间事件的执行,所以在eventLoop 中有个 lastTime 属性来检测系统时间.如果发现系统时间改变了,比上次执行时间事件的时间小,就会强制尽早执行.\n时间事件执行流程 事件循环 Event Loop 上述的 文件事件, 时间事件 是从何时开始? 在 事件循环 中开始. 事件循环 是 redis 在启动后初始化完服务配置,就会陷入一个巨大的循环 aeEventLoop 中. 这个巨大的循环从 aeMain() 开始.\nvoid aeMain(aeEventLoop *eventLoop) { eventLoop-\u0026gt;stop = 0; while (!eventLoop-\u0026gt;stop) { if (eventLoop-\u0026gt;beforesleep != NULL) eventLoop-\u0026gt;beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP); } } 源码中可以看出来,除非给 eventLoop-\u0026gt;stop 设置为 true ,程序会一直跑,一直执行 aeProcessEvents.\naeEventLoop aeEventLoop 保存着事件循环的上下文信息,并有三个重要的数组:保存监听的文件事件 aeFileEvent , 时间事件 aeTimeEvent, 待处理文件事件 aeFiredEvent.\naeProcessEvent 在一般情况下,aeProcessEvents 都会先计算最近的时间事件发生所需要等待的时间,然后调用 aeApiPoll 方法在这段时间中等待事件的发生,在这段时间中如果发生了文件事件,就会优先处理文件事件,否则就会一直等待,直到最近的时间事件需要触发.\nint aeProcessEvents(aeEventLoop *eventLoop, int flags) { int processed = 0, numevents; if (!(flags \u0026amp; AE_TIME_EVENTS) \u0026amp;\u0026amp; !(flags \u0026amp; AE_FILE_EVENTS)) return 0; if (eventLoop-\u0026gt;maxfd != -1 || ((flags \u0026amp; AE_TIME_EVENTS) \u0026amp;\u0026amp; !(flags \u0026amp; AE_DONT_WAIT))) { struct timeval *tvp; #1:计算 I/O 多路复用的等待时间 tvp numevents = aeApiPoll(eventLoop, tvp); for (int j = 0; j \u0026lt; numevents; j++) { aeFileEvent *fe = \u0026amp;eventLoop-\u0026gt;events[eventLoop-\u0026gt;fired[j].fd]; int mask = eventLoop-\u0026gt;fired[j].mask; int fd = eventLoop-\u0026gt;fired[j].fd; int rfired = 0; if (fe-\u0026gt;mask \u0026amp; mask \u0026amp; AE_READABLE) { rfired = 1; fe-\u0026gt;rfileProc(eventLoop,fd,fe-\u0026gt;clientData,mask); } if (fe-\u0026gt;mask \u0026amp; mask \u0026amp; AE_WRITABLE) { if (!rfired || fe-\u0026gt;wfileProc != fe-\u0026gt;rfileProc) fe-\u0026gt;wfileProc(eventLoop,fd,fe-\u0026gt;clientData,mask); } processed++; } } if (flags \u0026amp; AE_TIME_EVENTS) processed += processTimeEvents(eventLoop); return processed; } 参考 https://draveness.me/redis-eventloop https://draveness.me/redis-io-multiplexing Redis设计与实现 ","id":6,"section":"posts","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e事件驱动程序设计\u003c/strong\u003e(英语:\u003cstrong\u003eEvent-driven programming\u003c/strong\u003e)是一种电脑\u003ca href=\"https://zh.wikipedia.org/wiki/程式設計\"\u003e程序设计\u003c/a\u003e\u003ca href=\"https://zh.wikipedia.org/wiki/模型\"\u003e模型\u003c/a\u003e。这种模型的程序运行流程是由用户的动作(如\u003ca href=\"https://zh.wikipedia.org/wiki/滑鼠\"\u003e鼠标\u003c/a\u003e的按键,键盘的按键动作)或者是由其他程序的\u003ca href=\"https://zh.wikipedia.org/wiki/訊息\"\u003e消息\u003c/a\u003e来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由\u003ca href=\"https://zh.wikipedia.org/wiki/程式設計師\"\u003e程序员\u003c/a\u003e来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在\u003ca href=\"https://zh.wikipedia.org/w/index.php?title=互動程序\u0026amp;action=edit\u0026amp;redlink=1\"\u003e交互程序\u003c/a\u003e(Interactive program)的情况下孕育而生的。 \u003ca href=\"https://zh.wikipedia.org/wiki/事件驅動程式設計\"\u003e\u0026ndash;wikipedia\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","tags":["redis"],"title":"Redis-事件","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/event/","year":"2019"},{"content":"RDB 和 AOF 区别在于: 前者保存数据库快照,持久化所有键值对,后者通过保存 写命令 保证数据库的状态.\n什么是 AOF ? AOF 持久化通过保存服务器执行的写命令实现,进行恢复时通过重放 AOF 文件中的写命令,来保证数据安全.就像 mysql 的 binlog 一样.\n开启 AOF 通过在 redis.conf 中将 appendonly 设为 yes 即可\n# redis.conf appendonly yes # 设置 aof 文件名字 appendfilename \u0026quot;appendonly.aof\u0026quot; # Redis支持三种不同的刷写模式: # appendfsync always #每次收到写命令就立即强制写入磁盘,是最有保证的完全的持久化,但速度也是最慢的,一般不推荐使用。 appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,是受推荐的方式。 # appendfsync no #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不被推荐。 AOF 文件格式 AOF 文件格式以 redis 命令请求协议为标准的,*.aof 文件可以直接打开.\nAOF 持久化过程 命令追加 append redis 执行完客户端的写命令后,会将该命令以协议的格式写入到 aof_buf 中.该属性为 redisServer 中的一个.\n#src/server.h struct redisServer { .... sds aof_buf; /* AOF buffer, written before entering the event loop */ } AOF 写入同步 redis 的服务进程是一个 事件循环 - event loop , 每次循环大概会做三件事.\n 文件事件: 接收客户端的命令,返回结果 时间事件: 执行系统的定时任务(serverCron), 完成渐进 rehash 扩容之类的操作 aof flush: 是否将 aof_buf 中的内容写入文件中\n# 伪代码 def eventloop(): while true: processFileEvents() # 处理命令 processTimeEvents() # 处理定时任务 flushAppendOnlyFile() # 处理 aof 写入 flushAppendOnlyFile 中的动作是否执行是根据一个配置决定的.\nappendfsync 该配置有几个值可选,默认是 everysec.\n always: 总是写入.只要程序执行到这一步了,就将 aof_buf 中命令协议写入到文件 everysec: 每秒写入. 每次执行前会先判断是否与上次写入间隔一秒,再次同步时通过 一个线程 专门执行 no: 不写入. 命令写入 aof_buf 后由操作系统决定何时同步到文件 fsync: 现代操作系统为了提高文件读写的效率,通常会将 write 函数写入的数据缓存在内存中,等到缓存空间填满或者超过一定时限,再将其写入磁盘.这样的问题在于宕机时缓存中的数据就无法恢复.所以操作系统提供了 fsync/fdatasync 两个函数,强制操作系统将数据立即写入磁盘,保证数据安全.两函数区别在于: 前者会更新文件的属性,后者只更新数据.\n 三种模式在性能和数据上都有相对的优缺点. always 模式数据安全性更强,毕竟每次都是直接写入,但是就会影响性能.磁盘读写是比较慢的. everysec 模式性能较好,但会丢失一秒内的缓存数据. no 模式就完全取决于操作系统了.\nAOF 还原数据 AOF 重写 AOF 重写的意思其实就是对单个命令的多个操作进行整理,留下最终态的执行命令来减少 aof 文件的大小.你可以想象一下执行 1w 次 incr 操作,写入 aof 1w 次的场景.\n触发条件 AOF 重写可以自动触发.通过配置 auto-aof-rewrite-min-size 和auto-aof-rewrite-percentage,满足条件就会自动重写.具体可以查看官方的 redis.conf\n重写过程 创建子进程,根据内存里的数据重写aof,保存到temp文件 此时主进程还会接收命令,会将写操作追加到旧的aof文件中,并保存在server.aof_rewrite_buf_blocks中,通过管道发送给子进程存在server.aof_child_diff中,最后追加到temp文件结尾 子进程重写完成后退出,主进程根据子进程退出状态,判断成功与否。成功就将剩余的server.aof_rewrite_buf_blocks追加到temp file中,然后rename()覆盖原aof文件 重写的过程中主进程还是会一直接受客户端的命令,所以重写子进程与主进程肯定会存在数据不一致的情况.redis针对这种情况作出了解决方案: 新增一个 aof_rewrite_buf_blocks, aof 写入命令时,不仅写入到 aof_buf, 如果正在重写,那么也写入到 aof_rewrite_buf_blocks 中,这样在子进程重写完毕后,可以将 aof_rewrite_buf_blocks 的命令追加到新文件中,保证数据不丢失.\nrename 操作是原子的,也是唯一会造成主进程阻塞的操作.\n参考 https://redis.io/topics/persistence https://youjiali1995.github.io/redis/persistence/ ","id":7,"section":"posts","summary":"\u003cp\u003e\u003ccode\u003eRDB\u003c/code\u003e 和 \u003ccode\u003eAOF\u003c/code\u003e 区别在于: 前者保存数据库快照,持久化所有键值对,后者通过保存 \u003cstrong\u003e写命令\u003c/strong\u003e 保证数据库的状态.\u003c/p\u003e","tags":["redis"],"title":"Redis-AOF持久化","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/aof/","year":"2019"},{"content":"redis 为内存数据库,一旦服务器进程退出,服务器中的数据就不见了.所以内存中的数据需要持久化的硬盘中来保证可以在必要的时候进行故障恢复. RDB 就是 redis 提供的一种持久化方式.\n 官方关于持久化的文章: https://redis.io/topics/persistence\n 什么是 RDB? RDB 是 redis 提供的一种持久化方式,可以手动执行,也可以通过定时任务定期执行,可以将某个时间节点的数据库状态保存到一个 RDB 文件中,叫做 dump.rdb.如果开启了压缩算法( LZF )的支持,则可以利用算法减少文件大小.服务器意外宕机或者断电后重启都可以通过该文件来恢复数据库状态.\n如何执行? 有两个命令可以生成 RDB 文件.\n SAVE: 执行时进程阻塞,无法处理其他命令 BGSAVE: 新建一个子进程来后台生成 RDB 文件 具体实现逻辑在: src/rdb.c/rdbSave(),从官方文档可知,该实现是基于 cow 的.\n https://redis.io/topics/persistence\nThis method allows Redis to benefit from copy-on-write semantics.\n 如何载入? RDB 文件会在 redis 启动时自动载入.\n由于 AOF 持久化的实时性更好,所以如果同时开启了 AOF , RDB 两种持久化,会优先使用 AOF 来恢复.\nBGSAVE 执行时的状态 BGSAVE 执行期间会拒绝 SAVE/BGSAVE 的命令,避免产生 竞争条件.\nBGSAVE 执行期间 BGREWRITEAOF 命令会延迟到 BGSAVE 执行完之后执行.\nBGREWRITEAOF 在执行时, BGSAVE 命令会被拒绝.\nBGSAVE 和 BGREWRITEAOF 命令的权衡完全是性能方面的考虑.毕竟都会有大量的磁盘写入,影响性能.\n定时执行BGSAVE BGSAVE 不会阻塞服务器进程,所以 redis 允许用户通过配置, 定时执行 BGSAVE 命令.\n快照策略 Snapshotting 可以通过设置 N 秒内至少 M 次修改来触发一次 BGSAVE.\nsave 60 1000 # 60s内有至少1000次修改时 bgsave 一次 默认的保存条件 save 900 1 save 300 10 save 60 10000 dirty 计数器 \u0026amp; lastsave 属性 redis 中维护了一个计数器,来记录距离上一次 SAVE/BGSAVE 后服务器对所有数据库进行了多少次增删改,叫做 dirty计数器.属于 redisServer 结构体的属性之一.\nlastsave 是记录了上一次成功执行 SAVE/BGSAVE 的 UNIX时间戳 , 同样是 redisServer 结构体的属性之一.\n# src/server.h struct redisServer { ... long long dirty; /* Changes to DB from the last save */ time_t lastsave; /* Unix time of last successful save */ ... } 定时执行过程 redis 有一个定时任务 serverCron , 每隔 100ms 就会执行一次,用于维护服务器.该任务就会检查 save 设置的保存条件是否满足,满足则执行 BGSAVE\n满足条件逻辑 遍历设置的 save 参数, 计算当前时间到 lastsave 的间隔 interval , 如果 dirty \u0026gt; save.change \u0026amp; interval \u0026gt; save.seconds 那么就执行保存\nRDB 文件结构 https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format\n写下这篇文章时参考版本为 2019.09.05 更新的版本\n RDB 文件格式对读写进行了很多优化,这类优化导致其格式与内存中存在的形式极其相似,同时利用 LZF 压缩算法来优化文件的大小.一般来讲, redis 对象都会提前标记自身的大小,所以备份RDB 在读取这些 object 时,可以提前知道要分配多少内存.\n解析RDB结构 下面的代码展示的是 16 进制下 RDB 文件的结构,便于理解\n----------------------------# RDB is a binary format. There are no new lines or spaces in the file. 52 45 44 49 53 # 魔数 REDIS的16进制表示,代表这是个RDB文件 30 30 30 37 # 4位ascii码表示当前RDB版本号,这里表示\u0026quot;0007\u0026quot; = 7 ---------------------------- FE 00 # FE 表明这是数据库选择标记. 00 表示选中0号数据库 ----------------------------# Key-Value pair starts FD $unsigned int # FD 是秒级过期时间的标记. 紧接着是 4 byte unsigned int 过期时间 $value-type # 1 byte 标记 value 类型 - set, map, sorted set etc. $string-encoded-key # 经过编码后的键 $encoded-value # 值,编码格式取决去 $value-type ---------------------------- FC $unsigned long # FC 表明是毫秒级过期时间. 过期时间值是 8 bytes的 unsigned long,是一个unit时间戳 $value-type # 同上秒级时间 $string-encoded-key # 同上秒级时间 $encoded-value # 同上秒级时间 ---------------------------- $value-type # 这一栏是没有过期时间的key-value $string-encoded-key $encoded-value ---------------------------- FE $length-encoding # 前一个数据库的编码完成,选择新的数据库进行处理.数据库编号会根据 length-encoding 格式获得 ---------------------------- ... # Key value pairs for this database, additonal database FF ## 表明 RDB 文件结束了 8 byte checksum ## 8byte CRC 64 校验码 value type 1 byte 表示了 value 的类型.\n type(以下为十进制表示) 编码类型 0 String 1 List 2 Set 3 Sorted Set 4 Hash 9 Zipmap 10 Ziplist 11 Intset 12 Sorted Set in Ziplist 13 HashMap in Ziplist 键值编码格式 键(key)都是字符串,所以使用string 编码格式.\n值(value)就会有不同的区分:\n 如果 value type 为 0 ,会是简单的字符串. 如果 value type 为 9,10,11,12, 值会被包装为 string, 在读到该字符串后,会进一步解析. 如果 value type 为 1,2,3,4, 值会是一个字符串数组. Length Encoding 长度编码是用来存储对象的长度的.是一种可变字节码,旨在使用尽可能少的字节.\n如何工作? 从流中读取 1byte,得到高两位. 如果是 00 开头, 那么剩下 6 位表示长度 如果是 01 开头, 会再从流中读取 1byte,合起来总共 14 位作为长度. 如果是 10 开头, 会直接丢弃剩下的 6 位.再从流中读取 4bytes作为长度. 如果是 11 开头, 说明这个对象是一种特殊编码格式. 剩下的 6 位表示了它的格式类型.这个编码通常用来将数字存储为字符串,或者存储被编码过得字符串(String Encoding). 编码结果是? 从上述可得,可能的编码格式是这样的:\n 1 byte 最多存储到 63 2 bytes 最多存储到 16383 5 bytes 最多存储到 2^32 - 1 String Encoding redis 的字符串是二进制安全的,所以可以存储 anything. 没有任何字符串结尾的标记.最好将 redis 字符串视为一个字节数组.\n有三种类型的字符串:\n 长度编码字符串 这是最简单的一种,字符串的长度会利用 Length Encoding 编码作为前缀,后面跟着字符串的编码\n 数字作为字符串 这里就将上面 Length Encoding 的特殊编码格式联系起来了,数字作为字符串时以 11 开头,剩下的 6 位表示不同的数字类型\n 0 表示接下来是一个 8 位数字 1 表示接下来是一个 16 位数字 2 表示接下来是一个 32 位数字 压缩字符串 压缩字符串的 Length Encoding 还是以 11 开头的, 但是剩下的6 位二进制的值为 4, 表明后面读取到的是一个压缩字符串.压缩字符串会存储压缩前和压缩后的长度.解析规则如下:\n 根据 Length Encoding 读取压缩的长度 clen 根据 Length Encoding 读取未压缩的长度 从流中读取 clen bytes 的数据 利用 LZF 算法进行解析 分析RDB文件 利用 od 命令来分析来看看 rdb 文件长什么样子.我将 redis 数据库清空后,执行了 set msg hello,所以现在只有一个键 msg, 值为 hello.下面的命令第一行输出的是 16 进制,下面一行输出的是对应的 ascii. 下面进行解析~\n➜ od -A x -t x1c -v dump.rdb 0000000 52 45 44 49 53 30 30 30 39 fa 09 72 65 64 69 73 R E D I S 0 0 0 9 372 \\t r e d i s 0000010 2d 76 65 72 05 35 2e 30 2e 34 fa 0a 72 65 64 69 - v e r 005 5 . 0 . 4 372 \\n r e d i 0000020 73 2d 62 69 74 73 c0 40 fa 05 63 74 69 6d 65 c2 s - b i t s 300 @ 372 005 c t i m e 051 0000030 29 e8 c3 5d fa 08 75 73 65 64 2d 6d 65 6d c2 d0 ) 350 303 ] 372 \\b u s e d - m e m 302 007 0000040 07 10 00 fa 0c 61 6f 66 2d 70 72 65 61 6d 62 6c \\a 020 \\0 372 \\f a o f - p r e a m b l 0000050 65 c0 00 fe 00 fb 01 00 00 03 6d 73 67 05 68 65 e 300 \\0 376 \\0 373 001 \\0 \\0 003 m s g 005 h e 0000060 6c 6c 6f ff fc 0e 6b 79 fe 47 1a 36 l l o 377 374 016 k y 376 G 032 6 000006c 魔数和版本号 前 5 个字节就是我们看到的 REDIS,以及后四个字节对应的版本号9\n辅助字段 Aux Fields 这是 Version 7 之后加入的字段, Redis设计与实现 所使用的版本是没有这个,所以一开始有点懵~ 只能看代码了.\n# src/rdb.c /* 该函数负责执行 RDB 文件的写入 */ int rdbSave(char *filename, rdbSaveInfo *rsi) { //伪代码 1. 创建一个临时文件 temp-$pid.rdb,并处理创建失败的逻辑 2. 新建一个redis封装的I/O流 3. 写入rdb文件 rdbSaveRio() 4. 将文件重命名, 默认重命名为 dump.rdb 5. 更新服务器的一些状态: dirty计数器置0,更新lastsave等 } 然后我们来看下写入的 Aux Fields, 在函数 rdbSaveRio 中\nint rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) { // 忽略所有只看重点 if (server.rdb_checksum) rdb-\u0026gt;update_cksum = rioGenericUpdateChecksum; // 生成校验码 snprintf(magic,sizeof(magic),\u0026quot;REDIS%04d\u0026quot;,RDB_VERSION); // 生成魔数及版本号 if (rdbWriteRaw(rdb,magic,9) == -1) goto werr; // 写入魔数及版本号 if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr; // 写入 AuxFileds } /* Save a few default AUX fields with information about the RDB generated. */ int rdbSaveInfoAuxFields(rio *rdb, int flags, rdbSaveInfo *rsi) { int redis_bits = (sizeof(void*) == 8) ? 64 : 32; int aof_preamble = (flags \u0026amp; RDB_SAVE_AOF_PREAMBLE) != 0; /* Add a few fields about the state when the RDB was created. */ if (rdbSaveAuxFieldStrStr(rdb,\u0026quot;redis-ver\u0026quot;,REDIS_VERSION) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;redis-bits\u0026quot;,redis_bits) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;ctime\u0026quot;,time(NULL)) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;used-mem\u0026quot;,zmalloc_used_memory()) == -1) return -1; /* Handle saving options that generate aux fields. */ if (rsi) { if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;repl-stream-db\u0026quot;,rsi-\u0026gt;repl_stream_db) == -1) return -1; if (rdbSaveAuxFieldStrStr(rdb,\u0026quot;repl-id\u0026quot;,server.replid) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;repl-offset\u0026quot;,server.master_repl_offset) == -1) return -1; } if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;aof-preamble\u0026quot;,aof_preamble) == -1) return -1; return 1; } 以上可以看出会写入这些字段.\n redis-ver:版本号\n redis-bits:OS 操作系统位数 32\u0026frasl;64\n ctime:RDB文件创建时间\n used-mem:使用内存大小\n repl-stream-db:在server.master客户端中选择的数据库\n repl-id:当前实例 replication ID\n repl-offset:当前实例复制的偏移量\n 每一个属性写入前都会写入 0XFA, 标记这是一个辅助字段.在上面命令行输出中,ascii 展示为 372\n数据库相关标记 0000050 65 c0 00 fe 00 fb 01 00 00 03 6d 73 67 05 68 65 e 300 \\0 376 \\0 373 001 \\0 \\0 003 m s g 005 h e 这一行中的 0XFE 表示选择数据库,后面紧接着 00 即为,选择 0 号数据库. 0XFB 是标记了当前数据库中键存储的数量,这里用到了 Length Encoding, 01 是我们存储的字典中key-value的数量,00 是过期字典(expires)中的数量.\n redisDB中有两个属性, dict 记录了我们写入的所有键, expires 存储了我们设置有过期时间的键以及其过期时间.\n Key Value 结构 我们设置了 msg -\u0026gt; hello,在输出中是这样的.\n0000050 65 c0 00 fe 00 fb 01 00 00 03 6d 73 67 05 68 65 e 300 \\0 376 \\0 373 001 \\0 \\0 003 m s g 005 h e 在 msg 前面的字段 \\0 003, 表示他是 string 类型, 且长度为 3, 005 hello, 表示是长度为 5 的 hello.\n还有其他数据结构这里就不做展示了.\n结束符 \u0026amp; 校验码 0000060 6c 6c 6f ff fc 0e 6b 79 fe 47 1a 36 l l o 377 374 016 k y 376 G 032 6 最后一行输出中 0xff , 文件结束符, 剩下的八个字节就是 CRC64\n参考 https://cloud.tencent.com/developer/article/1179710\n Redis5.0 RDB文件解析\n ","id":8,"section":"posts","summary":"\u003cp\u003e\u003ccode\u003eredis\u003c/code\u003e 为内存数据库,一旦服务器进程退出,服务器中的数据就不见了.所以内存中的数据需要持久化的硬盘中来保证可以在必要的时候进行故障恢复. \u003ccode\u003eRDB\u003c/code\u003e 就是 \u003ccode\u003eredis\u003c/code\u003e 提供的一种持久化方式.\u003c/p\u003e","tags":["redis"],"title":"Redis-RDB持久化","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/rdb/","year":"2019"},{"content":"服务器中的数据库 redis的数据库是保存在一个db数组中的,默认会新建16个数组.\n# src/server.h struct redisServer { ... redisDb *db; // db 存放的数组 int dbnum; /* 根据该属性决定创建数据库数量 默认: 16 */ ... } 切换数据库 redis 数据库从 0 开始计算,通过 select 命令切换数据库. client 会有一个属性指向当前选中的 DB.\n# src/server.h typedef struct client { ... redisDb *db; /* 指向当前选中的redisDb */ ... } 键空间 redisDb 的结构是怎样的呢?\n# src/server.h /* Redis database representation. There are multiple databases identified * by integers from 0 (the default database) up to the max configured * database. The database number is the 'id' field in the structure. */ typedef struct redisDb { dict *dict; /* 键空间 */ dict *expires; /* Timeout of keys with a timeout set */ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ dict *ready_keys; /* Blocked keys that received a PUSH */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ int id; /* Database ID */ long long avg_ttl; /* Average TTL, just for stats */ list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */ } redisDb; 键空间 指的是每一个数据库中存放用户设置键和值的地方. 可以看到上述结构中, dict 属性就是每一个数据库的键空间, 字典结构, 也就是我们命令的执行结构.例如 set msg \u0026quot;hello world~\u0026quot; .\n所以针对数据库的操作就是操作字典.\n读写键空间后的操作 维护 hit, miss 次数, 可以利用 info stats 查看 keyspace_hits 以及 keyspace_misses 读取一个键后会更新键的 LRU ,用于计算键的闲置时间 object idletime {key} 查看 服务器读取一个键后发现已经过期,则会删除这个键在执行其他操作 如果客户端 watch 了某个键, 该键修改之后,会被标记为 dirty, 从而事务程序可以注意到该键已经被修改了 服务器每修改一个键后, 都会对 dirty 计数器 +1 ,这个计数器会触发服务器的持久化和复制操作 服务器开启数据库通知之后,键修改后会发送相应的数据库通知 过期时间保存 上述的 redisDb 结构中有 expires 的字典, redis 就是将我们设置的过期时间存到了这个字典中.键就是数据库键,值是一个 long long 类型的整数, 保存了键的过期时间: 一个毫秒精度的 UNI\u0010X 时间戳.\nRedis的过期键删除策略 有这么三种删除方式.\n定时删除 设置键过期时间的同时,创建一个定时器,到期自动删除\n优点 内存友好,键过期就删除\n缺点 对 CPU 不友好,过期键较多时,会占用较长时间,CPU 资源紧张的情况下会影响服务器的响应时间和吞吐量 创建定时器需要用到 redis 的时间事件,实现方式为无序链表,查找效率低 惰性删除 无视键是否过期,每次从键空间取键时,先判断是否过期,过期就删除,没过期就返回.\n优点 对 CPU 友好,遇到过期键才删除\n缺点 如果过期键很多,且一直不会被访问,就会导致大量内存被浪费\n定期删除 定期的在数据库中检查,删除过期的键.定期删除策略是上面两种策略的折中方案.\n优点 每隔一段时间删除过期键,可以减少删除操作对 CPU 的影响 定期删除也可以减少过期键带来的内存浪费 难点 确定删除操作执行的时长和频率\nredis采用方案 惰性删除 + 定期删除\n惰性删除是在所有读写数据库命令执行之前检查键是否过期来实现的.\n定期删除是通过 redis 的定时任务执行.在规定的时间内,多次遍历服务器的各个数据库,从 expires 字典中 随机抽查 一部分键的过期时间.current_db 会记录当前函数检查的进度,并在下一次函数执行时,接着上次的执行.循环往复地执行.\n内存淘汰策略 默认策略是 volatile-lru,即超过最大内存后,在过期键中使用 lru 算法进行 key 的剔除,保证不过期数据不被删除,但是可能会出现 OOM 问题。\n其他策略如下: allkeys-lru:根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。 allkeys-random:随机删除所有键,直到腾出足够空间为止。 volatile-random: 随机删除过期键,直到腾出足够空间为止。 volatile-ttl:根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。 noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息 \u0026ldquo;(error) OOM command not allowed when used memory\u0026rdquo;,此时 Redis 只响应读操作。 AOF,RDB \u0026amp; 复制功能对过期键的处理 生成 RDB 文件时,过期键不会被保存到新文件中 载入 RDB 文件 以主服务器运行:未过期的键被载入,过期键忽略 以从服务器运行:保存所有键,无论是否过期.由于主从服务器在进行数据同步时,从服务器数据库就会被清空,所以一般来讲,也不会造成什么影响. AOF 写入时,键过期还没有被删除,AOF 文件不会受到影响,当键被惰性删除或被定期删除后,AOF 文件会追加一条 DEL 命令来显示记录该键已被删除 AOF 重写时,会对键过期进行确认,过期补充些. 复制模式下,从服务器的过期键删除由主服务器控制. 主服务器删除一个键后,会显示发送 DEL 命令给从服务器. 从服务器接收读命令时,如果键已过期,也不会将其删除,正常处理 从服务器只在主服务器发送 DEL 命令才删除键 主从复制不及时怎么办?会有脏读现象~\n数据库通知 通过订阅的模式,可以实时获取键的变化,命令的执行情况.通过 redis 的 pub/sub 模式来实现.命令对数据库进行了操作后,就会触发该通知,置于能不能发送出去完全看你的配置了.\nnotify_keyspace_events 系统配置决定了服务器发送的配置类型.如果给定的 type 不是服务器允许发送的类型,程序就直接返回了.然后就判断能发送键通知就发送,能发送命令通知就发送.\n/* The API provided to the rest of the Redis core is a simple function: * * notifyKeyspaceEvent(char *event, robj *key, int dbid); * * 'event' is a C string representing the event name. * 'key' is a Redis object representing the key name. * 'dbid' is the database ID where the key lives. */ void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) { sds chan; robj *chanobj, *eventobj; int len = -1; char buf[24]; /* If any modules are interested in events, notify the module system now. * This bypasses the notifications configuration, but the module engine * will only call event subscribers if the event type matches the types * they are interested in. */ moduleNotifyKeyspaceEvent(type, event, key, dbid); /* If notifications for this class of events are off, return ASAP. */ if (!(server.notify_keyspace_events \u0026amp; type)) return; eventobj = createStringObject(event,strlen(event)); /* __keyspace@\u0026lt;db\u0026gt;__:\u0026lt;key\u0026gt; \u0026lt;event\u0026gt; notifications. */ if (server.notify_keyspace_events \u0026amp; NOTIFY_KEYSPACE) { chan = sdsnewlen(\u0026quot;__keyspace@\u0026quot;,11); len = ll2string(buf,sizeof(buf),dbid); chan = sdscatlen(chan, buf, len); chan = sdscatlen(chan, \u0026quot;__:\u0026quot;, 3); chan = sdscatsds(chan, key-\u0026gt;ptr); chanobj = createObject(OBJ_STRING, chan); pubsubPublishMessage(chanobj, eventobj); decrRefCount(chanobj); } /* __keyevent@\u0026lt;db\u0026gt;__:\u0026lt;event\u0026gt; \u0026lt;key\u0026gt; notifications. */ if (server.notify_keyspace_events \u0026amp; NOTIFY_KEYEVENT) { chan = sdsnewlen(\u0026quot;__keyevent@\u0026quot;,11); if (len == -1) len = ll2string(buf,sizeof(buf),dbid); chan = sdscatlen(chan, buf, len); chan = sdscatlen(chan, \u0026quot;__:\u0026quot;, 3); chan = sdscatsds(chan, eventobj-\u0026gt;ptr); chanobj = createObject(OBJ_STRING, chan); pubsubPublishMessage(chanobj, key); decrRefCount(chanobj); } decrRefCount(eventobj); } ","id":9,"section":"posts","summary":"","tags":["redis"],"title":"Redis-数据库长什么样?","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/db/","year":"2019"},{"content":"Redis有很多种数据结构,但其并没有直接使用这些数据结构来构建这个 NOSQL, 而是通过 对象系统 完成了对所有数据结构的统一管理, 实现内存回收, 对象共享等特性~\n类型及编码 在 Redis 中使用任何命令操作,都是操作的一个对象.有键对象,值对象.\nset msg \u0026quot;hello~\u0026quot; # msg 为键对象, \u0026quot;hello~\u0026quot; 为值对象 每个对象都会有如下的结构:\ntypedef struct redisObject { unsigned type:4; // 类型 unsigned encoding:4; // 编码 unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; // 引用计数 void *ptr; // 指向底层实现数据结构的指针 } robj; type 类型 type 指明了该对象的类型. redis 中类型有如下几种\n/* The actual Redis Object */ #define OBJ_STRING 0 /* String object. */ #define OBJ_LIST 1 /* List object. */ #define OBJ_SET 2 /* Set object. */ #define OBJ_ZSET 3 /* Sorted set object. */ #define OBJ_HASH 4 /* Hash object. */ #define OBJ_MODULE 5 /* Module object. */ #define OBJ_STREAM 6 /* Stream object. */ redis 中键都为字符串对象,利用 type 命令可以查看值对象的类型\nreids\u0026gt; type language list encoding 编码 encoding 属性记录了该对象使用的什么数据结构存储底层的实现,即 *ptr 所指向的那个数据结构.以下是目前的编码类型.\n/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ #define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */ 基本上每种类型的对象都会对应两种编码类型,可以动态的根据用户输入的值提供最有的数据结构,减少资源消耗.\n字符串对象 字符串对象有三种编码格式. int,embstr,raw,不同长度不同格式有不一样的编码类型.\n47.100.254.74:6379\u0026gt; set msg \u0026quot;abcdefg\u0026quot; OK (0.53s) 47.100.254.74:6379\u0026gt; object encoding msg \u0026quot;embstr\u0026quot; 47.100.254.74:6379\u0026gt; set msg \u0026quot;abcdefghijklmnopqrstuvwxyz01234567890123456789\u0026quot; OK 47.100.254.74:6379\u0026gt; object encoding msg \u0026quot;raw\u0026quot; 47.100.254.74:6379\u0026gt; set msg 123 OK 47.100.254.74:6379\u0026gt; object encoding msg \u0026quot;int\u0026quot; embstr vs raw 一个字符串对象包括 redisObject 和 sds 两部分组成.正常情况下是需要分配两次内存来创建这两个结构.这也是raw 的格式,但是如果当 value 长度较短时, (由于 redis 使用的是 jemalloc 分配内存)我们可以将内存分配控制在一次,将 RedisObject 和 sds 分配在连续的内存空间,这也就是 embstr 编码格式了.那多短算短呢?\n在此之前先了解下创建一个 redisObject 时所占用的空间.\nembstr编码是由 代表着 字符串的数据结构是 SDS.假设为 sdshdr8\nstruct sdshdr8 { uint8_t len; /* 1byte used */ uint8_t alloc; /* 1byte excluding the header and null terminator */ unsigned char flags; /* 1byte 3 lsb of type, 5 unused bits */ char buf[]; }; jemalloc 可以分配 8/16/32/64 字节大小的内存,从上可以发现最少的内存需要占用 19 字节, Redis 在总体大于 64 字节时,会改为 raw 存储. 所以 embstr 形式时最大长度是 64 - 19 - 结束符\\0长度 = 44\n编码转换 由于 redis 没有为 embstr 编写修改相关的程序,所以是只读的, 如果对其执行任何修改命令,就会变为 raw 格式.\n类型检查 redis 中的操作命令一般有两种: 所有类型都能用的(DEL, EXPIRE\u0026hellip;), 特定类型适用的(各种数据类型对应的命令).若操作键的命令不对, redis 会提示报错.\n47.100.254.74:6379\u0026gt; set numbers 1 OK 47.100.254.74:6379\u0026gt; object encoding numbers \u0026quot;int\u0026quot; 47.100.254.74:6379\u0026gt; rpush numbers a (error) WRONGTYPE Operation against a key holding the wrong kind of value 如何实现? 利用 RedisObject 的type 来控制.在输入一个命令时, 服务器会先检查输入键所对应的的值对象是否为命令对应的类型,是的话就执行,不是就报错.\n多态命令 同一种数据结构可能有多种编码格式.比如字符串对象的编码格式可能有 int, embstr, raw.所以当命令执行前,还需要根据值对象的编码来选择正确的命令来实现.\n比如想要执行 llen 获取 list 长度, 如果编码为 ziplist, 那么程序就会使用 ziplist 对应的函数来计算, 编码为 quicklist 时则是使用 quicklist 对应的函数来计算. 此为命令的 多态 .\n内存回收 redis 利用引用计数来实现内存回收机制.由 RedisObject 中的 refcount 属性记录.\n引用计数是有导致循环引用的弊端的,那么redis为啥还是会用的?找了很久也没有找到答案.\n有一个说法是: 引用的复杂度很低,不太容易导致循环引用.就一切从简呗.\n对象共享 对象共享指的是创建一次对象后,后面如果还有客户端需要创建同样的值对象则直接把现在这个的引用只给他,引用计数加1,可以节省内存的开销.类似 Java 常量池. 所以refcount 也被用来做对象共享的.\nredis 在初始化服务器时, 会创建 0 - 9999 一万个整数字符串, 为了节省资源.\n为什么不共享其他的复杂对象? 整数复用几率很大 整数比较算法时间复杂度是 O(1), 字符串是 O(N), hash/list 复杂度是 O(n2) 键的空转时长 redisObject 的 lru 属性记录着该对象最后一次被命令程序访问的时间.该属性在内存回收中有很大的作用.\n空转时长指的是now() - lru\n47.100.254.74:6379\u0026gt; object idletime numbers (integer) 4023 ","id":10,"section":"posts","summary":"\u003cp\u003eRedis有很多种数据结构,但其并没有直接使用这些数据结构来构建这个 \u003ccode\u003eNOSQL\u003c/code\u003e, 而是通过 \u003ccode\u003e对象系统\u003c/code\u003e 完成了对所有数据结构的统一管理, 实现内存回收, 对象共享等特性~\u003c/p\u003e","tags":["redis"],"title":"Redis-万物皆「对象」","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/obj/","year":"2019"},{"content":"分布式锁有很多中实现(纯数据库,zookeeper,redis),纯数据库的受限于数据库性能,zk 可以保证加锁的顺序,是公平锁.Redis中的实现就是接下来要学习的.\n为什么使用分布式锁? 在分布式环境下想要保证只能有一个请求更新一条数据,普通的加锁(比如 Java 中的 synchronized,JUC 中的各种 Lock)都不能胜任. 分布式锁的意义在于可以将操作锁的权利中心化,从而串行控制业务的执行.但是使用分布式锁也有很多弊端,后面再说.\n分布式锁的特点? 互斥:具有强排他性,需要保证不同节点不同线程的互斥 可重入:同一个节点的同一个线程如果获得了锁,那也可以再次获得 高效,高可用:加锁解锁要高效,高可用保证分布式锁服务不会宕机失效 阻塞/非阻塞:像 ReentrantLock 支持 lock, tryLock, tryLock(long timeout) 支持公平锁/非公平锁(Option) 如何使用分布式锁? Redis中有多种实现分布式锁的方式,一个一个看看.\n简单粗暴版 设置一个坑,让所有节点去抢就好.即语义为: set if not exist, 抢到后执行逻辑,逻辑完成后在del即可.\nredis 2.8 版本之前我们会通过以下方式:\nsetnx {resource-name} {anystring} 我们还需要加一个过期时间,以免各种异常宕机情况导致锁无法释放的问题.\nexpire key {max-lock-time} 这两条命令并不是原子操作的,所以我们需要通过 Lua 脚本来保证其原子性\nredis 2.8 版本之后官方提供了 nx ex 的原子操作,使用起来更加简单了.\nset {resource-name} {anystring} nx ex {max-lock-time} Redission版 https://github.com/redisson/redisson\n Redission 和 Jedis 都是 Java 中的 redis 客户端, Jedis 使用的是阻塞式 I/O, 而 Redission 使用的 Netty 来进行通信,而且 API 封装更友好, 继承了 java.util.concurrent.locks.Lock 的接口,可以像操作本地 Lock 一样操作分布式锁. 而且 Redission 还提供了不同编程模式的 API: sync/async, Reactive, RxJava, 非常人性化. Redission 有丰富的接口实现以及对不同异常情况的处理设计很值得学习.\n// 1. 设置 config Config config = new Config(); // 2. 创建 redission 实例 RedissonClient redisson = Redisson.create(config); // 4. 获取锁 RLock lock = redisson.getLock(\u0026quot;myLock\u0026quot;); // 5. 加锁 // 方式一 // 加锁以后10秒钟自动解锁 // 无需调用unlock方法手动解锁 lock.lock(10, TimeUnit.SECONDS); // 方式二 // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } } // 方式三 // 异步加锁 RLock lock = redisson.getLock(\u0026quot;anyLock\u0026quot;); lock.lockAsync(); lock.lockAsync(10, TimeUnit.SECONDS); Future\u0026lt;Boolean\u0026gt; res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS); RedLock https://redis.io/topics/distlock\n 上述的分布式锁实现都是基于单实例实现,所以会出现单点问题.胆大RedLock 基本原理是利用多个 Redis 集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率。\n加锁过程 客户端获取当前的时间戳。 对 N 个 Redis 实例进行获取锁的操作,具体的操作同单机分布式锁。对 Redis 实例的操作时间需要远小于分布式锁的超时时间,这样可以保证在少数 Redis 节点 Down 掉的时候仍可快速对下一个节点进行操作。 客户端会记录所有实例返回加锁成功的时间,只有从多半的实例(在这里例子中 \u0026gt;= 3)获取到了锁,且操作的时间远小于分布式锁的超时时间,锁才被人为是正确获取。 如果锁被成功获取了,当前分布式锁的合法时间为初始设定的合法时间减去上锁所花的时间。 若分布式锁获取失败,会强制对所有实例进行锁释放的操作,即使这个实例上不存在相应的键值。 分布式锁的一些问题 锁被其他客户端释放 如果线程 A 在获取锁后处理业务时间过长,导致锁被自动释放了,此时 线程 B 重新获取到了锁. 线程 A 在执行完业务逻辑后释放锁(DEL操作),这是就会把线程 B 获取到的锁给释放掉.\n如何解决? 在设置 value 时,生成一个随机 token, 删除 key 时先做判断,只有在 token 与自己持有的相等时,才能删除. 由于需要保证原子性, 我们需要通过 Lua 脚本来实现.像下面这样,不过 Redission 已经有对应的实现了.\nif redis.call(\u0026quot;get\u0026quot;,KEYS[1]) == ARGV[1] then return redis.call(\u0026quot;del\u0026quot;,KEYS[1]) else return 0 end 超时问题 如果在加锁和释放锁之间的业务逻辑过长,超出了锁的过期时间,那么就可能会导致另一个线程获取到锁,导致逻辑不能严格的串行执行.所以分布式锁的初衷是: 逻辑越短越好,持有锁的时间越短越好.\n如何解决? 这个目前没有太好解决的方案,后面如果看到了,就更新到这里.自己觉得: 尽量保证持锁时间短,优化代码逻辑.虽然可以延长锁的时间,但是会影响吞吐量的吧.如果真的有多个客户端持有了锁,还需要尽量保证业务逻辑中数据的幂等性,日志监控,及时报警,这样也可以做到尽快的人工介入.\n 技术莫得银弹~适合的才是最好的.\n 时钟不一致 RedLock 强依赖时间,所以机器时间不一致会有很大的问题\n如何解决? 人为调整 NTP自动调整: 可以将时间精度控制在一定范围内. 性能、故障恢复和 fsync 假设 Redis 没有持久性,当一个客户端获得了 5 个实例中的 3 个锁,若 3 个锁所在的实例 Down 掉了,实例再次启动时,其他的客户端也可以再次获得锁。\n这个问题会因为开启了 Redis 的持久化而改观,对于 AOF 持久化(区别与 RDB 的二进制持久化,是文本持久化)。默认采用的是每秒钟通过 fsync 落盘,这意味着会丢失一秒内的数据,如果需要更有安全保证的持久化,可以设置 fsync=always,但对应的会损失一部分性能。\n更好的解决办法是在实例 Down 掉后延迟一个略长于锁合法时间的时间,这样就可以保证在实例启动起来时锁一定是过期的,从而无须以损失性能为代价而使用 fsync=always 的持久化。\n参考 再有人问你分布式锁,这篇文章扔给他 RedLock中译 ","id":11,"section":"posts","summary":"\u003cp\u003e分布式锁有很多中实现(纯数据库,zookeeper,redis),纯数据库的受限于数据库性能,zk 可以保证加锁的顺序,是公平锁.Redis中的实现就是接下来要学习的.\u003c/p\u003e","tags":["分布式锁","redis"],"title":"Redis-分布式锁","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/distributed-lock/","year":"2019"},{"content":"系统学习 redis 相关的知识,从数据结构开始~\nString 字符串 Redis 的字符串是 动态字符串, 长度可变,自动扩容。利用预分配空间方式减少内存的分配。默认分配 1M 大小的内存。扩容时加倍现有空间,最大占用为 512M.\n常用命令 SET,SETNX\u0026hellip;\n结构 struct SDS\u0026lt;T\u0026gt; { T capacity; // 数组容量 T len; // 数组长度 byte flags; // 特殊标识位,不理睬它 byte [] content; // 数组内容 } Redis 中的字符串叫做 Simple Dynamic String, 上述 struct 是一个简化版,实际的代码中,redis 会根据 str 的不同长度,使用不同的 SDS, 有 sdshdr8, sdshdr16, sdshdr32 等等\u0026hellip; 但结构体都是如上的类型.\ncapacity 存储数组的长度,len 表示数组的实际长度。需要注意的是: string 的字符串是以 \\0 结尾的,这样可以便于调试打印,还可以直接使用 glibc 的字符串函数进行操作.\n字符串存储 字符串有两种存储方式,长度很短时,使用 emb 形式存储,长度超过 44 时,使用 raw 形式存储.\n可以使用 debug object {your_string} 来查看存储形式\n\u0026gt; set codehole abcdefghijklmnopqrstuvwxyz012345678912345678 OK \u0026gt; debug object codehole Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1 \u0026gt; set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789 OK \u0026gt; debug object codehole Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1 WHY? 首先需要解释 RedisObject, 所有 Redis 对象都有的结构体\nstruct RedisObject { int4 type; // 4bits int4 encoding; // 4bits int24 lru; // 24bits int32 refcount; // 4bytes void *ptr; // 8bytes,64-bit system } robj; 不同的对象具有不同的类型 type (4bit),同一个类型的 type 会有不同的存储形式 encoding (4bit),为了记录对象的 LRU 信息,使用了 24 个 bit 来记录 LRU 信息。每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对象内容 (body) 的具体存储位置。这样一个 RedisObject 对象头需要占据 16 字节的存储空间。\n接着我们再看 SDS 结构体的大小,在字符串比较小时,SDS 对象头的大小是 capacity+3,至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)。\n一张图解释:\nList 列表 Redis 的列表是用链表来实现的,插入删除 O (1), 查找 O (n), 列表弹出最后一个元素时,数据结构删除,内存回收.\n常用命令 LPUSH,LPOP,RPUSH,RPOP,LRANGE\u0026hellip;\n列表的数据结构 列表底层的存储结构并不是简简单单的一个链表~通过 ziplist 连接起来组成 quicklist.\nziplist 压缩列表 在列表元素较少时,redis 会使用一块连续内存来进行存储,这个结构就是 ziplist. 所有的元素紧挨着存储.\n\u0026gt; zadd z_lang 1 java 2 rust 3 go (integer) 3 \u0026gt; debug object z_lang Value at:0x7fde1c466660 refcount:1 encoding:ziplist serializedlength:34 lru:11974320 lru_seconds_idle:11 可以看到上述输出 encoding 为 ziplist.\nstruct ziplist\u0026lt;T\u0026gt; { int32 zlbytes; // 整个压缩列表占用字节数 int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点 int16 zllength; // 元素个数 T [] entries; // 元素内容列表,挨个挨个紧凑存储 int8 zlend; // 标志压缩列表的结束,值恒为 0xFF } zltail_offset 是为了支持双向遍历才设计的,可以快速定位到最后一个元素,然后倒着遍历.\nentry 会随着容纳的元素不同而结构不同.\nstruct entry { int\u0026lt;var\u0026gt; prevlen; // 前一个 entry 的字节长度 int\u0026lt;var\u0026gt; encoding; // 元素类型编码 optional byte [] content; // 元素内容 } prevlen 表示前一个 entry 的字节长度,倒序遍历时,可以根据这个字段来推算前一个 entry 的位置。它是变长的整数,字符串长度小于 254 ( 0XFE ) 时,使用一个字节表示,大于等于 254, 使用 5 个字节来表示。第一个字节是 254, 剩余四个字节表示字符串长度.\nencoding 编码类型 encoding 存储编码类型信息,ziplist 通过其来决定 content 内容的形式。所以其设计是很复杂的.\n 00xxxxxx 最大长度位 63 的短字符串,后面的 6 个位存储字符串的位数,剩余的字节就是字符串的内容。 01xxxxxx xxxxxxxx 中等长度的字符串,后面 14 个位来表示字符串的长度,剩余的字节就是字符串的内容。 10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd 特大字符串,需要使用额外 4 个字节来表示长度。第一个字节前缀是 10,剩余 6 位没有使用,统一置为零。后面跟着字符串内容。不过这样的大字符串是没有机会使用的,压缩列表通常只是用来存储小数据的。 11000000 表示 int16,后跟两个字节表示整数。 11010000 表示 int32,后跟四个字节表示整数。 11100000 表示 int64,后跟八个字节表示整数。 11110000 表示 int24,后跟三个字节表示整数。 11111110 表示 int8,后跟一个字节表示整数。 11111111 表示 ziplist 的结束,也就是 zlend 的值 0xFF。 1111xxxx 表示极小整数,xxxx 的范围只能是 (0001~1101), 也就是 1~13,因为 0000、1110、1111 都被占用了。读取到的 value 需要将 xxxx 减 1,也就是整数 0~12 就是最终的 value。 增加元素 ziplist 是连续存储的,没有多余空间,这意味着每次插入一个元素,就需要扩展内存。如果占用内存过大,重新分配内存和拷贝内存就会有很大的消耗。所以其缺点是不适合存储 大型字符串, 存储元素不宜 过多.\n级联更新 每一个 entry 都是有 prevlen, 而且时而为 1 字节存储,时而为 5 字节存储,取决于字符串的字节长度是否大于 254, 如果某次操作导致字节长度从 254 变为 256, 那么其下一个节点所存储的 prevlen 就要从 1 个字节变为 5 个字节来存储,如果下一个节点刚好因此超过了 254 的长度,那么下下个节点也要更新\u0026hellip; 这就是级联更新了~\nquicklist Redis 中 list 的存储结构就是 quicklist. 下面的 language 是一个记录编程语言的集合。可以看到 encoding 即为 quicklist.\n\u0026gt; debug object language Value at:0x7fde1c4665f0 refcount:1 encoding:quicklist serializedlength:29 lru:11974264 lru_seconds_idle:62740 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:27 Redis 的 quicklist 是一种基于 ziplist 实现的可压缩(quicklistLZF)的双向链表,结合了链表和 ziplist 的 优点 组成的。下面可以看下他的结构体.\n/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ /** * quicklist 是一个 40byte (64 位系统) 的结构 */ typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* 元素总数 */ unsigned long len; /* quicklistNode 的长度 */ int fill : 16; /* ziplist 的最大长度 */ unsigned int compress : 16; /* 节点压缩深度 */ } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; /* 没有压缩,指向 ziplist, 否则指向 quicklistLZF unsigned int sz; /* ziplist 字节总数 */ unsigned int count : 16; /* ziplist 元素数量 */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ ... } quicklistNode; //LZF 无损压缩算法,压缩过的 ziplist typedef struct quicklistLZF { // 未压缩之前的大小 unsigned int sz; /* LZF size in bytes*/ // 存放压缩过的 ziplist 数组 char compressed []; } quicklistLZF; 一张图展示结构 压缩深度 quicklist 默认的压缩深度是 0,也就是不压缩。压缩的实际深度由配置参数 list-compress-depth 决定。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。\nSet 集合 Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。\n常用命令 SADD,SMEMBERS,SPOP,SISMEMBER,SCARD\u0026hellip;\nHash 哈希 Redis 的 Hash相当于Java 中的 HashMap, 数组 + 链表的二维结构.与 HashMap 不同的地方在于 rehash 方式不同, HashMap 中的 rehash 是阻塞式的, 需要一次性全部 rehash, 而 redis 为了性能考虑, 采用的是 渐进式 rehash.\n常用命令 HSET,HGET,HMSET,HLEN\u0026hellip;\n\u0026gt; hset books java \u0026quot;think in java\u0026quot; # 命令行的字符串如果包含空格,要用引号括起来 (integer) 1 \u0026gt; hset books golang \u0026quot;concurrency in go\u0026quot; (integer) 1 \u0026gt; hset books python \u0026quot;python cookbook\u0026quot; (integer) 1 \u0026gt; hgetall books # entries(),key 和 value 间隔出现 1) \u0026quot;java\u0026quot; 2) \u0026quot;think in java\u0026quot; 3) \u0026quot;golang\u0026quot; 4) \u0026quot;concurrency in go\u0026quot; 5) \u0026quot;python\u0026quot; 6) \u0026quot;python cookbook\u0026quot; \u0026gt; hlen books (integer) 3 \u0026gt; hget books java \u0026quot;think in java\u0026quot; \u0026gt; hset books golang \u0026quot;learning go programming\u0026quot; # 因为是更新操作,所以返回 0 (integer) 0 \u0026gt; hget books golang \u0026quot;learning go programming\u0026quot; \u0026gt; hmset books java \u0026quot;effective java\u0026quot; python \u0026quot;learning python\u0026quot; golang \u0026quot;modern golang programming\u0026quot; # 批量 set OK 字典 Redis 的 Hash 是通过 dict 结构来实现的, 该结构的底层是由哈希表来实现.类似于 HashMap, 数组+链表, 超过负载因子所对应的阈值时,进行 rehash, 扩容. 在具体实现中,使用了渐进式hash的方式来避免 HashMap 这种阻塞式的 rehash, 将 rehash 的工作分摊到对字典的增删改查中.\nstruct typedef struct dictEntry { void *key; //键 union { void *val; //值 uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; //指向下一节点,形成链表 } dictEntry; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ typedef struct dictht { dictEntry **table; // 哈希表数组,数组的每一项都是 distEntry 的头结点 unsigned long size; // 哈希表的大小,也是触发扩容的阈值 unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size-1 unsigned long used; // 哈希表中实际保存的节点数量 } dictht; typedef struct dict { dictType *type; //属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数 void *privdata; // 保存了需要传给那些类型特定函数的可选参数 dictht ht[2]; // 在字典内部,维护了两张哈希表. 一般情况下,字典只使用 ht[0] 哈希表,ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用 long rehashidx; // 记录 rehash 的状态, 没有进行 rehash 则为 -1 unsigned long iterators; /* number of iterators currently running */ } dict; 一张图来表示 何时扩容? 找到dictAddRow 函数观察源码可以发现,会在 _dictExpandIfNeeded 函数中进行扩容的判断.\n/* Expand the hash table if needed */ static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ // 正在渐进式扩容, 就返回 OK if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ // 如果哈希表 ht[0] size 为 0 ,初始化, 说明 redis 是懒加载的,延长初始化策略 if (d-\u0026gt;ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the \u0026quot;safe\u0026quot; threshold, we resize doubling * the number of buckets. */ /* * 如果哈希表ht[0]中保存的key个数与哈希表大小的比例已经达到1:1,即保存的节点数已经大于哈希表大小 * 且redis服务当前允许执行rehash,或者保存的节点数与哈希表大小的比例超过了安全阈值(默认值为5) * 则将哈希表大小扩容为原来的两倍 */ if (d-\u0026gt;ht[0].used \u0026gt;= d-\u0026gt;ht[0].size \u0026amp;\u0026amp; (dict_can_resize || d-\u0026gt;ht[0].used/d-\u0026gt;ht[0].size \u0026gt; dict_force_resize_ratio)) { return dictExpand(d, d-\u0026gt;ht[0].used*2); } return DICT_OK; } 正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。\n何时缩容? 当哈希表的负载因子小于 0.1 时,自动缩容.这个操作会在 redis 的定时任务中来完成.函数为 databasesCron,该函数的作用是在后台慢慢的处理过期,rehashing, 缩容.\n执行条件: 没有子进程执行aof重写或者生成RDB文件\n/* 遍历所有的redis数据库,尝试缩容 */ for (j = 0; j \u0026lt; dbs_per_call; j++) { tryResizeHashTables(resize_db % server.dbnum); resize_db++; } /* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL * we resize the hash table to save memory */ void tryResizeHashTables(int dbid) { if (htNeedsResize(server.db[dbid].dict)) dictResize(server.db[dbid].dict); if (htNeedsResize(server.db[dbid].expires)) dictResize(server.db[dbid].expires); } /* Hash table parameters */ #define HASHTABLE_MIN_FILL 10 /* Minimal hash table fill 10% */ int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); return (size \u0026gt; DICT_HT_INITIAL_SIZE \u0026amp;\u0026amp; (used*100/size \u0026lt; HASHTABLE_MIN_FILL)); } /* Resize the table to the minimal size that contains all the elements, * but with the invariant of a USED/BUCKETS ratio near to \u0026lt;= 1 */ int dictResize(dict *d) { int minimal; if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; minimal = d-\u0026gt;ht[0].used; if (minimal \u0026lt; DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE; return dictExpand(d, minimal); } 从 htNeedsResize函数中可以看到,当哈希表保存的key数量与哈希表的大小的比例小于10%时需要缩容.最小容量为DICT_HT_INITIAL_SIZE = 4. dictResize 函数中,当正在执行 aof 重写或生成 rdb 时, dict_can_resize 会变为 0, 也就说明上面的 执行条件.\n渐进式 rehash 从上述源码中可以看出,所有的扩容或者创建都经过 dictExpand 函数.\n/* Expand or create the hash table */ int dictExpand(dict *d, unsigned long size) { /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d-\u0026gt;ht[0].used \u0026gt; size) return DICT_ERR; // 计算新的哈希表大小,获得大于等于size的第一个2次方 dictht n; /* the new hash table */ unsigned long realsize = _dictNextPower(size); /* Rehashing to the same table size is not useful. */ if (realsize == d-\u0026gt;ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ // 第一次初始化也会通过这里来完成创建 if (d-\u0026gt;ht[0].table == NULL) { d-\u0026gt;ht[0] = n; return DICT_OK; } /* Prepare a second hash table for incremental rehashing */ // ht[1] 开始派上用场,扩容时是在 ht[1] 上操作, rehash 完毕后,在交换到 ht[0] d-\u0026gt;ht[1] = n; d-\u0026gt;rehashidx = 0; return DICT_OK; } 从 dictExpand 这个函数可以发现做了这么几件事:\n 校验是否可以执行 rehash 创建一个新的哈希表 n, 分配更大的内存 将哈希表 n 复制给 ht[1], 将 rehashidx 标志置为 0 ,意味着开启了渐进式rehash. 该值也标志渐进式rehash当前已经进行到了哪个hash槽. 该函数没有将key重新 rehash 到新的 slot 上,而是交由增删改查的操作, 以及后台定时任务来处理.\n增删改查辅助rehash 看源码其实可以发现在所有增删改查的源码中,开头都会有一个判断,是否处于渐进式rehash中.\ndictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); ... } // 进入 rehash 后是 \u0026gt;=0的值 #define dictIsRehashing(d) ((d)-\u0026gt;rehashidx != -1) /* * 此函数仅执行一步hash表的重散列,并且仅当没有安全迭代器绑定到哈希表时。 * 当我们在重新散列中有迭代器时,我们不能混淆打乱两个散列表的数据,否则某些元素可能被遗漏或重复遍历。 * * 该函数被在字典中查找或更新等普通操作调用,以致字典中的数据能自动的从哈系表1迁移到哈系表2 */ static void _dictRehashStep(dict *d) { if (d-\u0026gt;iterators == 0) dictRehash(d,1); } 后台任务rehash 虽然redis实现了在读写操作时,辅助服务器进行渐进式rehash操作,但是如果服务器比较空闲,redis数据库将很长时间内都一直使用两个哈希表.所以在redis周期函数中,如果发现有字典正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作.\n还是上面缩容时使用的任务函数databasesCron.源码如下:\n/* Rehash */ if (server.activerehashing) { for (j = 0; j \u0026lt; dbs_per_call; j++) { int work_done = incrementallyRehash(rehash_db); if (work_done) { /* If the function did some work, stop here, we'll do * more at the next cron loop. */ break; } else { /* If this db didn't need rehash, we'll try the next one. */ rehash_db++; rehash_db %= server.dbnum; } } } 渐进式rehash弊端 渐进式rehash避免了redis阻塞,可以说非常完美,但是由于在rehash时,需要分配一个新的hash表,在rehash期间,同时有两个hash表在使用,会使得redis内存使用量瞬间突增,在Redis 满容状态下由于Rehash会导致大量Key驱逐.\nZset 有序集合 首先 zset 是一个 set 结构,拥有 set 的所有特性,其次他可以给每一个 value 赋予一个 score 作为权重.内部实现用的跳表(skiplist)\n常用命令 ZADD,ZRANGE,ZREVRANGE,ZSCORE,ZCARD,ZRANK\u0026hellip;\n\u0026gt; zadd books 9.0 \u0026quot;think in java\u0026quot; (integer) 1 \u0026gt; zadd books 8.9 \u0026quot;java concurrency\u0026quot; (integer) 1 \u0026gt; zadd books 8.6 \u0026quot;java cookbook\u0026quot; (integer) 1 \u0026gt; zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;java concurrency\u0026quot; 3) \u0026quot;think in java\u0026quot; \u0026gt; zrevrange books 0 -1 # 按 score 逆序列出,参数区间为排名范围 1) \u0026quot;think in java\u0026quot; 2) \u0026quot;java concurrency\u0026quot; 3) \u0026quot;java cookbook\u0026quot; \u0026gt; zcard books # 相当于 count() (integer) 3 \u0026gt; zscore books \u0026quot;java concurrency\u0026quot; # 获取指定 value 的 score \u0026quot;8.9000000000000004\u0026quot; # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题 \u0026gt; zrank books \u0026quot;java concurrency\u0026quot; # 排名 (integer) 1 \u0026gt; zrangebyscore books 0 8.91 # 根据分值区间遍历 zset 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;java concurrency\u0026quot; \u0026gt; zrangebyscore books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;8.5999999999999996\u0026quot; 3) \u0026quot;java concurrency\u0026quot; 4) \u0026quot;8.9000000000000004\u0026quot; \u0026gt; zrem books \u0026quot;java concurrency\u0026quot; # 删除 value (integer) 1 \u0026gt; zrange books 0 -1 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;think in java\u0026quot; 数据结构 众所周知, Zset 是一个有序的set集合, redis 通过 hash table 来存储 value 和 score 的映射关系,可以达到 O(1), 通过 score 排序或者说按照 score 范围来获取这个区间的 value, 则是通过 跳表 来实现的. Zset 可以达到 O(log(N)) 的插入和读写.\n什么是跳跃列表? 如图,跳跃列表是指具有纵向高度的有序链表.跳表会随机的某提升些链表的高度,并将每一层的节点进行连接,相当于构建多级索引,这样在查找的时候,从最高层开始查,可以过滤掉一大部分的范围,有点类似于二分查找.跳表也是典型的空间换时间的方式.\n每一个 kv 块对应的结构如下面的代码中的zslnode结构,kv header 也是这个结构,只不过 value 字段是 null 值——无效的,score 是 Double.MIN_VALUE,用来垫底的。\nstruct struct zslnode { string value; double score; zslnode*[] forwards; // 多层连接指针 zslnode* backward; // 回溯指针 } struct zsl { zslnode* header; // 跳跃列表头指针 int maxLevel; // 跳跃列表当前的最高层 map\u0026lt;string, zslnode*\u0026gt; ht; // hash 结构的所有键值对 } redis中跳表的优化 允许 score 是重复的 比较不仅是通过 key(即 score), 也还会比较 data 最底层(Level 1)是有反向指针的,所以是一个双向链表,这样适用于从大到小的排序需求(ZREVRANGE) 一次查找的过程 redis中level是如何生成的? /* Returns a random level for the new skiplist node we are going to create. * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL * (both inclusive), with a powerlaw-alike distribution where higher * levels are less likely to be returned. */ int zslRandomLevel(void) { int level = 1; while ((random()\u0026amp;0xFFFF) \u0026lt; (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level\u0026lt;ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; } ZSKIPLIST_MAXLEVEL 最大值是 64, 也就是最多 64 层.ZSKIPLIST_P 为 1/4, 也就是说有 25% 的概率有机会获得level,要获得更高的level,概率更小. 这也就导致了, redis中的跳表层级不会特别高,较扁平,较低层节点较多.有个小优化的地方: 跳表会记录下当前的最高层数 MaxLevel 这样就不需要从最顶层开始遍历了.\n为什么使用跳表而不是红黑树或者哈希表? skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。 从算法实现难度上来比较,skiplist比平衡树要简单得多。 参考 渐进式 rehash 机制 美团针对Redis Rehash机制的探索和实践 zset内部实现 ","id":12,"section":"posts","summary":"\u003cp\u003e系统学习 redis 相关的知识,从数据结构开始~\u003c/p\u003e","tags":["redis","数据结构"],"title":"Redis-数据结构","uri":"https://xiaohei.im/hugo-theme-pure/2019/10/data-structure/","year":"2019"},{"content":"RabbitMQ在保证生产端与消费端的数据安全上,提供了消息确认的机制来保证. 消费端到 broker 端的确认常叫做ack机制, broker 到生产端常叫做confirm.\n消费端确认机制 Delivery Tag Delivery Tag 是 RabbitMQ 来确认消息如何发送的标志. Consumer 在注册到 RabbitMQ 上后, RabbitMQ 通过 basic.deliver 方法向消费者推送消息, 这个方法中就带着可以在 Channel中唯一识别消息的 delivery tag . Delivery Tag 是channel 隔离的.\ntag是一个大于零的增长的整型, 客户端在确认消息时将其当做参数传回来就可以保证是同一条消息的确认了.\ntag是channel隔离的, 所以必须在接受消息的channel上确认消息收到,否则会抛 unknown delivery tag的异常.\n最大值: delivery tag 是 64 位的long,最大值是 9223372036854775807. tag是channel隔离的,理论上来说是不会超过这个值的.\n确认机制 消息确认有两种模式: 自动/手动.\n自动模式会在消息一经发出就自动确认.这是在吞吐量和 可靠投递之间的权衡.如果在发送的过程中, TCP断掉了或是其他的问题,那消息就会丢掉了,这个问题需要考虑.还需要考虑的一个问题是: Consumer 消费速率如果不能跟上broker的发送速率, 会导致Consumer过载(消息堆积,内存耗尽),而在手动模式中可以通过prefetch来控制消费端的速率.有些客户端会提供TCP的背压,不能处理时,就丢弃了.\n手动模式需要Consumer端在收到消息后调用:\n basic.ack : 消息处理好了,可以丢掉了 basic.nack : 可以批量reject, 如果Consumer设置了requeue,消息还会重新回到broker的队列中 basic.reject : 消息没有处理但是也需要删除 Channel Prefetch 由于消息的发送和接收是独立的且完全异步,消息的手动确认也是完全异步的.所以这里有一个未确认消息的滑动窗口.在消费端我们经常需要控制接收消息的数量,防止出现消息缓存buffer越界的问题.此时我们就可以通过basic.qos来设置prefetch count, 该值定义了一个Channel中能存放的消息条数上限,超过这个值,RabbitMQ在收到至少一条ack之前都不能再往Channel上发送消息了.\n这里需要注意前面说的滑动窗口: 意味着当Channel满的时候,不会再往Channel上发消息,但是当你ack了一条,就会往Channel上发一条,ack了N条,就会发N条到Channel上.\nbasic.get设置prefetch是无效的,请使用basic.consume\n吞吐量影响因素: Ack机制 \u0026amp; Prefetch 确认机制的选择和Prefetch的值决定了消费端的吞吐量.一般来讲,增大Prefetch值以及 自动确认 会提升推送消息的速率,但也会增加待处理消息的堆积,消费端内存压力也会上升.\n如果Prefetch无界,Consumer在消费大量消息时没有ack会导致消费端连接的那个节点内存压力上升.所以找到一个完美的Prefetch值还是很重要的. 一般 100-300 左右吞吐量还不错,且消费端压力不大. 设置为 1 时,就很保守了,这种情况下吞吐量就很低,延迟较高.\n发布端确认机制 网络有很多种失败的方式,并且需要花时间检测.所以客户端并不能保证消息可以正常的发送到broker,正常的被处理.有可能丢了也有可能有延迟.\n根据AMQP-0-9-1, 只有通过 事务 的方式来保证.将Channel设置为事务型的,每条消息都以事务形式推送提交.但是,事务是很重,会降低吞吐量,所以RabbitMQ就换了种方式来实现: 通过模仿已有的Consumer端的确认机制.\n启用Confirm,客户端调用confirm.select即可.Broker会返回confirm.select-ok,取决于是否有no-wait设置. Channel如果设置了confirm.select,说明处于confirm模式,此时是不能设置为事务型Channel,两者不可互通.\nBroker的应答机制同Consumer一致,通过basic.ack即可,也可批量ack.\n发布端的NACK 在某些情况下,broker无法再接收消息,就会向发布端回执basic.nack,意味着消息会被丢弃,发布端需要重新发布这些消息.当Channel置为Confirm模式后,后面收到的消息都将会confirm或者nack 一次. 需要注意的几点:\n 不能保证消息何时confirm. 消息也不会同时confirm和nack 只有在Erlang进程内部报错时才会有nack Broker何时确认发布的消息? 无法路由的消息: 当确认消息不会被路由时, broker会立即发出confirm. 如果消息设置了强制(mandatory)发送,basic.return会在basic.ack之前回执. nack逻辑一致.\n可路由的消息: 所有queue接受了消息时返回basic.ack ,如果队列是持久化的,意味着持久化完成后才发出.对镜像队列(Mirrored Queues),意味着所有镜像都收到后发出.\n持久化消息的ack延迟 RabbitMQ的持久化通常是批量的,需要间隔几百毫秒来减少 fsync(2)的调用次数或者等待 queue 是空闲状态的时候,这意味着,每一次basic.ack的延迟可能达到几百毫秒.为了提高吞吐量最好是将持久化做成异步的,或者使用批量publish,这个需要参考客户端的api实现.\n确认消息的顺序 大多数情况下, RabbitMQ 会根据消息发送的顺序依次回执(要求消息发送在同一个channel上).但确认回执都是异步的,并且可以确认一条,或一组消息.确切的confirm发送时间取决于: 消息是否需要持久化,消息的路由方式.意味着不同的消息的确认时间是不同的.也就意味着返回确认的顺序并不一定相同.应用方不能将其作为一个依据.\n参考 https://www.rabbitmq.com/confirms.html#acknowledgement-modes ","id":13,"section":"posts","summary":"\u003cp\u003eRabbitMQ在保证生产端与消费端的数据安全上,提供了消息确认的机制来保证. 消费端到 \u003ccode\u003ebroker\u003c/code\u003e 端的确认常叫做\u003ccode\u003eack机制\u003c/code\u003e, \u003ccode\u003ebroker\u003c/code\u003e 到生产端常叫做\u003ccode\u003econfirm\u003c/code\u003e.\u003c/p\u003e","tags":["rabbitmq"],"title":"RabbitMQ-消息确认机制","uri":"https://xiaohei.im/hugo-theme-pure/2019/10/rabbitmq-ack-confirm/","year":"2019"},{"content":" 最近使用Hugo作为博客引擎后,闲不下来总想去找一些简单好看的主题.在官方的主题列表搜罗了一圈后,选择了yinyang,非常简单,但是用了一段时间还是想找个功能全点的,无意中瞄到了一个博主的博客,主题特别吸引我,但是是 hexo 平台的,搜了半天也没有人移植,就自己来吧~ 移植的过程中,遇到了挺多问题,也是这些问题慢慢的熟悉了hugo的模板结构.下面就来写一写自己遇到的问题~\n 页面变量参数 https://gohugo.io/variables/\n hugo的页面有基本的变量(我更愿意称为属性,根据这些属性来实现我们的主题模板.最主要的有三类:Site, Page, Taxonomy.\n.Site 站点相关的属性,即config.toml(yml)文件中的配置.\n 在页面模板中,我们可以使用{{- .Site.Autor }}这样的方式来获取你想要的站点属性.具体的站点属性可以查看https://gohugo.io/variables/site/. .Site 属于全局配置,在 作用域 得当的情况下是可以正常调用的.非正常情况我们下面再讲.\n常用属性 .Site.Pages : 获取所有文章(包含生成的一些分类页,比如说 标签页),按时间倒序排序.返回是一个数组.我们经常用来渲染一个列表.比如 归档 页面.\n .Site.Taxonomies : 获取所有的分类(这里的分类是广义上的),可以获取到按tag分类的集合,也可以获取到按category分类的集合,可以用这个属性来完成分类的页面.下面这段代码就代表着我可以拿到所有的 分类页 ,循环得到分类页的链接和标题.\n {{- range .Site.Taxonomies.categories }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;{{ .Page.Permalink }}\u0026quot;\u0026gt;{{ .Page.Title }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} .Site.Params 可以获取到我们在config.toml的Params标签下设置的内容.也是很重要的属性.比如说下面的例子.我可以设置日期的格式化样式,展示成你想要的类型. \u0026lt;p\u0026gt;{{ .Date.Format (.Site.Params.dateFormatToUse | default \u0026quot;2006-01-02\u0026quot;)}}\u0026lt;/p\u0026gt; .Page 页面中定义的属性.\n 页面属性可以大致分为两部分,一个是Hugo原生的属性,一个是每一篇文章的文件头,即front matter中的属性.具体的属性可以在https://gohugo.io/variables/page/查看. 在一个页面的作用域中使用时可以直接调用.比如我们想要知道页面的创建日期就可以直接 {{ .Date }} 即可.\n常用属性 .Date/.Title/.ReadingTime/.WordCount 见名知意 .Permalink/.RelPermalink 永久链接及相对连接 .Summary 摘要,默认70字 .Pages 为什么页面中还有一个这样的属性呢? Page是包含生成的分类页, 标签页的,所有当处于这些页面时会返回一个集合,若是我们自己真正写的文件,即markdown文件,会返回nil的. .Taxonomies 用作内容分类的管理. 我们经常在写文章时会写上 categories 或者 tags, 这些标签类目就是 .Taxonomies 的集中展示, Hugo 默认会有 categories 和 tags 两种分类. 你也可以自己再自定义设置. 具体参考: https://gohugo.io/content-management/taxonomies\n 使用案例 官方提供了多种 Template 实现常用的遍历.\n 我通常会用来写标签页(tags)和分类页(categories). 直接调用 .Taxonomies 会获得所有的分类项(即: tags, categories, 自定义分类项), .Taxonomies.tags 就可以获得所有的标签,以及标签下的所有文章.以下就是我的主题中 标签 页的实现逻辑.\n{{- $tags := .Site.Taxonomies.tags }} \u0026lt;main class=\u0026quot;main\u0026quot; role=\u0026quot;main\u0026quot;\u0026gt; \u0026lt;article class=\u0026quot;article article-tags post-type-list\u0026quot; itemscope=\u0026quot;\u0026quot;\u0026gt; \u0026lt;header class=\u0026quot;article-header\u0026quot;\u0026gt; \u0026lt;h1 itemprop=\u0026quot;name\u0026quot; class=\u0026quot;hidden-xs\u0026quot;\u0026gt;{{- .Title }}\u0026lt;/h1\u0026gt; \u0026lt;p class=\u0026quot;text-muted hidden-xs\u0026quot;\u0026gt;{{- T \u0026quot;total_tag\u0026quot; (len $tags) }}\u0026lt;/p\u0026gt; \u0026lt;nav role=\u0026quot;navigation\u0026quot; id=\u0026quot;nav-main\u0026quot; class=\u0026quot;okayNav\u0026quot;\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;{{- \u0026quot;tags\u0026quot; | relURL }}\u0026quot;\u0026gt;{{- T \u0026quot;nav_all\u0026quot; }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- range $tags }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;{{ .Page.Permalink }}\u0026quot;\u0026gt;{{ .Page.Title }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;!-- /header --\u0026gt; \u0026lt;div class=\u0026quot;article-body\u0026quot;\u0026gt; {{- range $name, $taxonomy := $tags }} \u0026lt;h3 class=\u0026quot;panel-title mb-1x\u0026quot;\u0026gt; \u0026lt;a href=\u0026quot;{{ \u0026quot;/tags/\u0026quot; | relURL}}{{ $name | urlize }}\u0026quot; title=\u0026quot;#{{- $name }}\u0026quot;\u0026gt;# {{ $name }}\u0026lt;/a\u0026gt; \u0026lt;small class=\u0026quot;text-muted\u0026quot;\u0026gt;(Total {{- .Count }} articles)\u0026lt;/small\u0026gt; \u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026quot;row\u0026quot;\u0026gt; {{- range $taxonomy }} \u0026lt;div class=\u0026quot;col-md-6\u0026quot;\u0026gt; {{ .Page.Scratch.Set \u0026quot;type\u0026quot; \u0026quot;card\u0026quot;}} {{- partial \u0026quot;item-post.html\u0026quot; . }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; \u0026lt;/article\u0026gt; \u0026lt;/main\u0026gt; 上下文传递 刚开始写 Hugo 的页面时,最让我头疼的地方就在在于此.现在想想他的逻辑是很标准的.不同的代码块上下文隔离.\n 在Hugo中,上下文的传递一般是靠.符号来完成的. 用的最多的就是再组装页面时,需要将当前页面的作用域传递到 partial 的页面中去以便组装进来的页面可以获得当前页面的属性.\n以下是我的 baseof.html 页面, 可以看到 partial 相关的代码中都有 . 符号, 这里就是将当前页面的属性传递下去了, 其他页面也就可以正常使用该页面的属性了.\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026quot;{{ .Site.Language }}\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot; /\u0026gt; \u0026lt;meta http-equiv=\u0026quot;X-UA-Compatible\u0026quot; content=\u0026quot;IE=edge,chrome=1\u0026quot; /\u0026gt; \u0026lt;title\u0026gt; {{- block \u0026quot;title\u0026quot; . -}} {{ if .IsPage }} {{ .Title }} - {{ .Site.Title }} {{ else}} {{ .Site.Title}}{{ end }} {{- end -}} \u0026lt;/title\u0026gt; {{ partial \u0026quot;head.html\u0026quot; . }} \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026quot;main-center\u0026quot; itemscope itemtype=\u0026quot;http://schema.org/WebPage\u0026quot;\u0026gt; {{- partial \u0026quot;header.html\u0026quot; .}} {{- if and (.Site.Params.sidebar) (or (ne .Params.sidebar \u0026quot;none\u0026quot;) (ne .Params.sidebar \u0026quot;custom\u0026quot;))}} {{- partial \u0026quot;sidebar.html\u0026quot; . }} {{end}} {{ block \u0026quot;content\u0026quot; . }}{{ end }} {{- partial \u0026quot;footer.html\u0026quot; . }} {{- partial \u0026quot;script.html\u0026quot; . }} \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 页面组织 baseof.html baseof 可以理解为一种模板,符合规范定义的页面都会按照 baseof.html 的框架完成最后的渲染,具体可以查看官网页, 以本次移植主题的 baseof.html 来说一下.\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026quot;{{ .Site.Language }}\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot; /\u0026gt; \u0026lt;meta http-equiv=\u0026quot;X-UA-Compatible\u0026quot; content=\u0026quot;IE=edge,chrome=1\u0026quot; /\u0026gt; \u0026lt;title\u0026gt; {{- block \u0026quot;title\u0026quot; . -}} {{ if .IsPage }} {{ .Title }} - {{ .Site.Title }} {{ else}} {{ .Site.Title}}{{ end }} {{- end -}} \u0026lt;/title\u0026gt; {{ partial \u0026quot;head.html\u0026quot; . }} \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026quot;main-center\u0026quot; itemscope itemtype=\u0026quot;http://schema.org/WebPage\u0026quot;\u0026gt; {{- partial \u0026quot;header.html\u0026quot; .}} {{- if and (.Site.Params.sidebar) (or (ne .Params.sidebar \u0026quot;none\u0026quot;) (ne .Params.sidebar \u0026quot;custom\u0026quot;))}} {{- partial \u0026quot;sidebar.html\u0026quot; . }} {{end}} {{ block \u0026quot;content\u0026quot; . }}{{ end }} {{- partial \u0026quot;footer.html\u0026quot; . }} {{- partial \u0026quot;script.html\u0026quot; . }} \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 可以看到上面的页面中就是一个完整的 HTML 结构,我在其中组装了很多页面,比如head,header,footer等等,这些在最后渲染的时候都会加入进来组成一个完整的页面.\n在上面还有一个关键字 block, 比如 {{ block \u0026quot;title\u0026quot; }}, {{ block \u0026quot;content\u0026quot;}}.该关键字允许你自定义一个模板嵌进来, 只要按照规定的方式来.比如说我的文章页 single.html.\n{{- define \u0026quot;content\u0026quot;}} \u0026lt;main class=\u0026quot;main\u0026quot; role=\u0026quot;main\u0026quot;\u0026gt; {{- partial \u0026quot;article.html\u0026quot; . }} \u0026lt;/main\u0026gt; {{- end}} 这里我们定义了 content 的模板, 和 baseof.html 的模板呼应,在渲染一篇文章时,就会将single.html 嵌入 baseof.html 生成最后的页面了.\n模板页面查询规则 Hugo要怎么知道文章页还是标签页对应的模板是什么呢?答案: 有一套以多个属性作为依据的查询各类模板的标准.具体可以查看官网页.\n以文章页来举例, Hugo 官网上的内容页寻址规则如下:\n\n由上可见,会按照该顺序依次往下找,我一般会写在layouts/_default/single.html 下,这样可以在所有页面下通用.\n这里有个小坑也是之前文档没看好遇到的: 标签页和分类页这种对应的查找规则要按照该指引.\n参考 https://harmstyler.me/posts/2019/how-to-pass-variables-to-a-partial-template-in-hugo/ https://www.qikqiak.com/post/hugo-integrated-algolia-search/ ","id":14,"section":"posts","summary":"\u003cblockquote\u003e\n\u003cp\u003e最近使用\u003ca href=\"https://gohugo.io/\"\u003eHugo\u003c/a\u003e作为博客引擎后,闲不下来总想去找一些简单好看的主题.在\u003ca href=\"https://themes.gohugo.io/\"\u003e官方的主题列表\u003c/a\u003e搜罗了一圈后,选择了\u003ca href=\"https://github.com/joway/hugo-theme-yinyang\"\u003eyinyang\u003c/a\u003e,非常简单,但是用了一段时间还是想找个功能全点的,无意中瞄到了一个博主的博客,主题特别吸引我,但是是 \u003ccode\u003ehexo\u003c/code\u003e 平台的,搜了半天也没有人移植,就自己来吧~ 移植的过程中,遇到了挺多问题,也是这些问题慢慢的熟悉了hugo的模板结构.下面就来写一写自己遇到的问题~\u003c/p\u003e\n\u003c/blockquote\u003e","tags":["hugo"],"title":"Hexo =\u003e Hugo主题移植记录","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/hugo-theme-dev-note/","year":"2019"},{"content":"rabbitmq有多种使用模式,在这里记录下不同模式的消息路由规则\n预备知识 总结的不错的文章: https://blog.csdn.net/qq_27529917/article/details/79289564\n Binding Exchange 与 队列 之间的绑定为 Binding.绑定时可以设置 binding key, 发消息时会有一个 routing key, 当 routing key 与 binding key 相同时, 这条消息才能发送到队列中去.\nExchange Type Exchange 有不同的类型, 每种类型的功能也是不一致的\n Fanout 把所有发送到该 Exchange 的消息转发到所有绑定到他的队列中\n Direct/默认(empty string) 根据 routing_key 来决定发送到具体的队列去\n Topic binding key 可以带有匹配规则.\n Headers 不依赖 binding key 和 routing key, 只根据消息中的 headers 属性来匹配\n模式列表 参考: https://www.rabbitmq.com/getstarted.html\n 直连 上图展示了 Producer 与 Consumer 通过 Queue 直连, 实际上在 rabbitmq 中是不能直连的,必须通过 Exchange 指定 routingKey 才可以. 这里我们可以使用一个默认的 Exchange (空字符串) 来绕过限制.\n工作队列 直连 属于一对一的模式,工作队列 则属于一对多, 通常用于分发耗时任务给多个Consumer.可以提升响应效率.消息的分发策略是 轮询分发 .\n发布/订阅 发布订阅模型是 RabbitMQ 的核心模式. 我们大多数也是使用它来写业务.之前的 直连/工作队列 模式, 我们并没有用到 Exchange ,都是使用默认的空exchange.但是在 发布订阅 模式中, Producer 只会把消息发到 Exchange 中,不会关注是否会发送到队列, 由 Exchange 来决定.\n发布/订阅 中的 Exchange 类型为 Fanout, 所有发到 Exchange 上的消息都会再发到绑定在这个Exchange 上的所有队列中.\n路由模式 路由模式 采用 direct 类型的 Exchange 利用 binding key 来约束发送的队列.\nTopic Topic模式 利用模式匹配,以及 .的格式来按规则过滤. * 代表只有一个词, #代表 0 或 多个.\n","id":15,"section":"posts","summary":"\u003cp\u003erabbitmq有多种使用模式,在这里记录下不同模式的消息路由规则\u003c/p\u003e","tags":["rabbitmq"],"title":"RabbitMQ-消息分发机制","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/rabbitmq-msg-distribution/","year":"2019"},{"content":" rabbitmq version: 3.7.15\n 常用操作 sbin/rabbitmq-server 启动 sbin/rabbitmq-server -detached 后台启动 sbin/rabbitmqctl shutdown/stop 关闭/停止server sbin/rabbitmqctl status 检查server状态 sbin/rabbitmq-plugins enable rabbitmq_management 开启控制台 端口 server启动后默认监听5672 控制台默认监听15672 构建集群 构建集群的方式 在config文件中声明节点信息 使用DNS发现 使用AWS实例发现(通过插件) 使用kubernetes发现(通过插件) 使用consul发现(通过插件) 使用etcd发现(通过插件) 手动执行rabbitmqctl 节点名称 节点名称是节点的身份识别证明.两部分组成: prefix \u0026amp; hostname.例如 rabbit@node1.messaging.svc.local的prefix是 rabbit ,hostname是 node1.messaging.svc.local.\n 集群中名称必须 唯一. 如果使用同一个hostname 那么prefix要保持不一致\n 集群中,节点通过节点名称互相进行识别和通信.所以hostname必须能解析.CLI Tools也要使用节点名称.\n 单机集群构建 单机运行多节点需要保证:\n 不同节点名称 \u0010RABBITMQ_NODENAME 不同存储路径 RABBITMQ_DIST_PORT 不同日志路径 RABBITMQ_LOG_BASE 不同端口,包括插件使用的 RABBITMQ_NODE_PORT rabbitmqctl 构建集群 RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit rabbitmq-server -detached RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=hare rabbitmq-server -detached # 重置正在运行的节点 rabbitmqctl -n hare stop_app # 加入集群 rabbitmqctl -n hare join_cluster rabbit@`hostname -s` rabbitmqctl -n hare start_app 每个节点若配置有其他的插件.那么每个节点插件监听的端口不能冲突,例如添加控制台\n# 首先开启控制台插件 ./rabbitmq-plugins enable rabbitmq_management RABBITMQ_NODE_PORT=5672 RABBITMQ_SERVER_START_ARGS=\u0026quot;-rabbitmq_management listener [{port,15672}]\u0026quot; RABBITMQ_NODENAME=rabbit ./rabbitmq-server -detached RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS=\u0026quot;-rabbitmq_management listener [{port,15673}]\u0026quot; RABBITMQ_NODENAME=hare ./rabbitmq-server -detached # 加入rabbit节点生成集群 rabbitmqctl -n hare stop_app # 加入集群 rabbitmqctl -n hare join_cluster rabbit@`hostname -s` rabbitmqctl -n hare start_app 以上就建了带控制台的两个节点.\n遇到的问题 添加节点进集群时,报错 ./rabbitmqctl -n rabbit2 join_cluster rabbit@`hostname -s` Clustering node rabbit2@localhost with rabbit@localhost Error: {:inconsistent_cluster, 'Node rabbit@localhost thinks it\\'s clustered with node rabbit2@localhost, but rabbit2@localhost disagrees'} 集群残留的cluster信息导致认证失败.删除${RABBIT_MQ_HOME}/var/lib/rabbitmq/mnesia文件夹.再reset节点\n建集群报错 Clustering node rabbit2@localhost with rabbit@localhost Error: {:corrupt_or_missing_cluster_files, {:error, :enoent}, {:error, :enoent}} 同上\n启动第三个节点时爆端口占用,该端口是第一个节点的控制台端口15672.没有解决 2019-09-05 15:35:42.749 [error] \u0026lt;0.555.0\u0026gt; Failed to start Ranch listener rabbit_web_dispatch_sup_15672 in ranch_tcp:listen([{cacerts,'...'},{key,'...'},{cert,'...'},{port,15672}]) for reason eaddrinuse (address already in use) 使用案例 Topic Exchange topic类型的exchange ,routing key 是按一定规则来的,通过.连接,类似于正则.有两种符号:\n * 代表一个单词 # 代表0或多个单词 如果 单单只有#号, 那么topic exchange就像fanout exchange,如果没有使用*和#,那就是direct exchange了.\n参考 docker hub rabbit mq 镜像 ","id":16,"section":"posts","summary":"","tags":["rabbitmq"],"title":"RabbitMQ-入门及高可用集群部署","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/rabbitmq-guide-and-ha-cluster/","year":"2019"},{"content":" 转载自 https://rabbitmq.mr-ping.com/AMQP/AMQP_0-9-1_Model_Explained.html\n AMQP 0-9-1 和 AMQP 模型高阶概述 AMQP是什么 AMQP(高级消息队列协议)是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信。\n消息代理和他们所扮演的角色 消息代理(message brokers)从发布者(publishers)亦称生产者(producers)那儿接收消息,并根据既定的路由规则把接收到的消息发送给处理消息的消费者(consumers)。\n由于AMQP是一个网络协议,所以这个过程中的发布者,消费者,消息代理 可以存在于不同的设备上。\nAMQP 0-9-1 模型简介 AMQP 0-9-1的工作过程如下图:消息(message)被发布者(publisher)发送给交换机(exchange),交换机常常被比喻成邮局或者邮箱。然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。\n\n发布者(publisher)发布消息时可以给消息指定各种消息属性(message meta-data)。有些属性有可能会被消息代理(brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。\n从安全角度考虑,网络是不可靠的,接收消息的应用也有可能在处理消息的时候失败。基于此原因,AMQP模块包含了一个消息确认(message acknowledgements)的概念:当一个消息从队列中投递给消费者后(consumer),消费者会通知一下消息代理(broker),这个可以是自动的也可以由处理消息的应用的开发者执行。当“消息确认”被启用的时候,消息代理不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。\n在某些情况下,例如当一个消息无法被成功路由时,消息或许会被返回给发布者并被丢弃。或者,如果消息代理执行了延期操作,消息会被放入一个所谓的死信队列中。此时,消息发布者可以选择某些参数来处理这些特殊情况。\n队列,交换机和绑定统称为AMQP实体(AMQP entities)。\nAMQP是一个可编程的协议 AMQP 0-9-1是一个可编程协议,某种意义上说AMQP的实体和路由规则是由应用本身定义的,而不是由消息代理定义。包括像声明队列和交换机,定义他们之间的绑定,订阅队列等等关于协议本身的操作。\n这虽然能让开发人员自由发挥,但也需要他们注意潜在的定义冲突。当然这在实践中很少会发生,如果发生,会以配置错误(misconfiguration)的形式表现出来。\n应用程序(Applications)声明AMQP实体,定义需要的路由方案,或者删除不再需要的AMQP实体。\n交换机和交换机类型 交换机是用来发送消息的AMQP实体。交换机拿到一个消息之后将它路由给一个或零个队列。它使用哪种路由算法是由交换机类型和被称作绑定(bindings)的规则所决定的。AMQP 0-9-1的代理提供了四种交换机\n Name(交换机类型) Default pre-declared names(预声明的默认名称) Direct exchange(直连交换机) (Empty string) and amq.direct Fanout exchange(扇型交换机) amq.fanout Topic exchange(主题交换机) amq.topic Headers exchange(头交换机) amq.match (and amq.headers in RabbitMQ) 除交换机类型外,在声明交换机时还可以附带许多其他的属性,其中最重要的几个分别是:\n Name Durability (消息代理重启后,交换机是否还存在) Auto-delete (当所有与之绑定的消息队列都完成了对此交换机的使用后,删掉它) Arguments(依赖代理本身) 交换机可以有两个状态:持久(durable)、暂存(transient)。持久化的交换机会在消息代理(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在代理再次上线后重新被声明)。然而并不是所有的应用场景都需要持久化的交换机。\n默认交换机 默认交换机(default exchange)实际上是一个由消息代理预先声明好的没有名字(名字为空字符串)的直连交换机(direct exchange)。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。\n举个栗子:当你声明了一个名为\u0026quot;search-indexing-online\u0026quot;的队列,AMQP代理会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为\u0026quot;search-indexing-online\u0026quot;。因此,当携带着名为\u0026quot;search-indexing-online\u0026quot;的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为\u0026quot;search-indexing-online\u0026quot;的队列中。换句话说,默认交换机看起来貌似能够直接将消息投递给队列,尽管技术上并没有做相关的操作。\n直连交换机 直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的。直连交换机用来处理消息的单播路由(unicast routing)(尽管它也可以处理多播路由)。下边介绍它是如何工作的:\n 将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key) 当一个携带着路由键为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。 直连交换机经常用来循环分发任务给多个工作者(workers)。当这样做的时候,我们需要明白一点,在AMQP 0-9-1中,消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)之间。\n直连型交换机图例: \n扇型交换机 扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的拷贝分别发送给这所有的N个队列。扇型用来交换机处理消息的广播路由(broadcast routing)。\n因为扇型交换机投递消息的拷贝到所有绑定到它的队列,所以他的应用案例都极其相似:\n 大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件 体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端 分发系统使用它来广播各种状态和配置更新 在群聊的时候,它被用来分发消息给参与群聊的用户。(AMQP没有内置presence的概念,因此XMPP可能会是个更好的选择) 扇型交换机图例: \n主题交换机 主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发/订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。\n主题交换机拥有非常广泛的用户案例。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的 多消费者/多应用(multiple consumers/applications) 的时候,主题交换机都可以被列入考虑范围。\n使用案例:\n 分发有关于特定地理位置的数据,例如销售点 由多个工作者(workers)完成的后台任务,每个工作者负责处理某些特定的任务 股票价格更新(以及其他类型的金融数据更新) 涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍) 云端的不同种类服务的协调 分布式架构/基于系统的软件封装,其中每个构建者仅能处理一个特定的架构或者系统。 头交换机 有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头交换机(headers exchange)就是为此而生的。头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。\n我们可以绑定一个队列到头交换机上,并给他们之间的绑定使用多个用于匹配的头(header)。这个案例中,消息代理得从应用开发者那儿取到更多一段信息,换句话说,它需要考虑某条消息(message)是需要部分匹配还是全部匹配。上边说的“更多一段消息”就是\u0026quot;x-match\u0026quot;参数。当\u0026quot;x-match\u0026quot;设置为“any”时,消息头的任意一个值被匹配就可以满足条件,而当\u0026quot;x-match\u0026quot;设置为“all”的时候,就需要消息头的所有值都匹配成功。\n头交换机可以视为直连交换机的另一种表现形式。头交换机能够像直连交换机一样工作,不同之处在于头交换机的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。\n队列 AMQP中的队列(queue)跟其他消息队列或任务队列中的队列是很相似的:它们存储着即将被应用消费掉的消息。队列跟交换机共享某些属性,但是队列也有一些另外的属性。\n Name Durable(消息代理重启后,队列依旧存在) Exclusive(只被一个连接(connection)使用,而且当连接关闭后队列即被删除) Auto-delete(当最后一个消费者退订后即被删除) Arguments(一些消息代理用他来完成类似与TTL的某些额外功能) 队列在声明(declare)后才能被使用。如果一个队列尚不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为406的通道级异常就会被抛出。\n队列名称 队列的名字可以由应用(application)来取,也可以让消息代理(broker)直接生成一个。队列的名字可以是最多255字节的一个utf-8字符串。若希望AMQP消息代理生成队列名,需要给队列的name参数赋值一个空字符串:在同一个通道(channel)的后续的方法(method)中,我们可以使用空字符串来表示之前生成的队列名称。之所以之后的方法可以获取正确的队列名是因为通道可以默默地记住消息代理最后一次生成的队列名称。\n以\u0026quot;amq.\u0026quot;开始的队列名称被预留做消息代理内部使用。如果试图在队列声明时打破这一规则的话,一个通道级的403 (ACCESS_REFUSED)错误会被抛出。\n队列持久化 持久化队列(Durable queues)会被存储在磁盘上,当消息代理(broker)重启的时候,它依旧存在。没有被持久化的队列称作暂存队列(Transient queues)。并不是所有的场景和案例都需要将队列持久化。\n持久化的队列并不会使得路由到它的消息也具有持久性。倘若消息代理挂掉了,重新启动,那么在重启的过程中持久化队列会被重新声明,无论怎样,只有经过持久化的消息才能被重新恢复。\n绑定 绑定(Binding)是交换机(exchange)将消息(message)路由给队列(queue)所需遵循的规则。如果要指示交换机“E”将消息路由给队列“Q”,那么“Q”就需要与“E”进行绑定。绑定操作需要定义一个可选的路由键(routing key)属性给某些类型的交换机。路由键的意义在于从发送给交换机的众多消息中选择出某些消息,将其路由给绑定的队列。\n打个比方:\n 队列(queue)是我们想要去的位于纽约的目的地 交换机(exchange)是JFK机场 绑定(binding)就是JFK机场到目的地的路线。能够到达目的地的路线可以是一条或者多条 拥有了交换机这个中间层,很多由发布者直接到队列难以实现的路由方案能够得以实现,并且避免了应用开发者的许多重复劳动。\n如果AMQP的消息无法路由到队列(例如,发送到的交换机没有绑定队列),消息会被就地销毁或者返还给发布者。如何处理取决于发布者设置的消息属性。\n消费者 消息如果只是存储在队列里是没有任何用处的。被应用消费掉,消息的价值才能够体现。在AMQP 0-9-1 模型中,有两种途径可以达到此目的:\n 将消息投递给应用 (\u0026quot;push API\u0026quot;) 应用根据需要主动获取消息 (\u0026quot;pull API\u0026quot;) 使用push API,应用(application)需要明确表示出它在某个特定队列里所感兴趣的,想要消费的消息。如是,我们可以说应用注册了一个消费者,或者说订阅了一个队列。一个队列可以注册多个消费者,也可以注册一个独享的消费者(当独享消费者存在时,其他消费者即被排除在外)。\n每个消费者(订阅者)都有一个叫做消费者标签的标识符。它可以被用来退订消息。消费者标签实际上是一个字符串。\n消息确认 消费者应用(Consumer applications) - 用来接受和处理消息的应用 - 在处理消息的时候偶尔会失败或者有时会直接崩溃掉。而且网络原因也有可能引起各种问题。这就给我们出了个难题,AMQP代理在什么时候删除消息才是正确的?AMQP 0-9-1 规范给我们两种建议:\n 当消息代理(broker)将消息发送给应用后立即删除。(使用AMQP方法:basic.deliver或basic.get-ok) 待应用(application)发送一个确认回执(acknowledgement)后再删除消息。(使用AMQP方法:basic.ack) 前者被称作自动确认模式(automatic acknowledgement model),后者被称作显式确认模式(explicit acknowledgement model)。在显式模式下,由消费者应用来选择什么时候发送确认回执(acknowledgement)。应用可以在收到消息后立即发送,或将未处理的消息存储后发送,或等到消息被处理完毕后再发送确认回执(例如,成功获取一个网页内容并将其存储之后)。\n如果一个消费者在尚未发送确认回执的情况下挂掉了,那AMQP代理会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。\n拒绝消息 当一个消费者接收到某条消息后,处理过程有可能成功,有可能失败。应用可以向消息代理表明,本条消息由于“拒绝消息(Rejecting Messages)”的原因处理失败了(或者未能在此时完成)。当拒绝某条消息时,应用可以告诉消息代理如何处理这条消息——销毁它或者重新放入队列。当此队列只有一个消费者时,请确认不要由于拒绝消息并且选择了重新放入队列的行为而引起消息在同一个消费者身上无限循环的情况发生。\nNegative Acknowledgements 在AMQP中,basic.reject方法用来执行拒绝消息的操作。但basic.reject有个限制:你不能使用它决绝多个带有确认回执(acknowledgements)的消息。但是如果你使用的是RabbitMQ,那么你可以使用被称作negative acknowledgements(也叫nacks)的AMQP 0-9-1扩展来解决这个问题。更多的信息请参考帮助页面\n预取消息 在多个消费者共享一个队列的案例中,明确指定在收到下一个确认回执前每个消费者一次可以接受多少条消息是非常有用的。这可以在试图批量发布消息的时候起到简单的负载均衡和提高消息吞吐量的作用。For example, if a producing application sends messages every minute because of the nature of the work it is doing.(???例如,如果生产应用每分钟才发送一条消息,这说明处理工作尚在运行。)\n注意,RabbitMQ只支持通道级的预取计数,而不是连接级的或者基于大小的预取。\n消息属性和有效载荷(消息主体) AMQP模型中的消息(Message)对象是带有属性(Attributes)的。有些属性及其常见,以至于AMQP 0-9-1 明确的定义了它们,并且应用开发者们无需费心思思考这些属性名字所代表的具体含义。例如:\n Content type(内容类型) Content encoding(内容编码) Routing key(路由键) Delivery mode (persistent or not) 投递模式(持久化 或 非持久化) Message priority(消息优先权) Message publishing timestamp(消息发布的时间戳) Expiration period(消息有效期) Publisher application id(发布应用的ID) 有些属性是被AMQP代理所使用的,但是大多数是开放给接收它们的应用解释器用的。有些属性是可选的也被称作消息头(headers)。他们跟HTTP协议的X-Headers很相似。消息属性需要在消息被发布的时候定义。\nAMQP的消息除属性外,也含有一个有效载荷 - Payload(消息实际携带的数据),它被AMQP代理当作不透明的字节数组来对待。消息代理不会检查或者修改有效载荷。消息可以只包含属性而不携带有效载荷。它通常会使用类似JSON这种序列化的格式数据,为了节省,协议缓冲器和MessagePack将结构化数据序列化,以便以消息的有效载荷的形式发布。AMQP及其同行者们通常使用\u0026quot;content-type\u0026quot; 和 \u0026quot;content-encoding\u0026quot; 这两个字段来与消息沟通进行有效载荷的辨识工作,但这仅仅是基于约定而已。\n消息能够以持久化的方式发布,AMQP代理会将此消息存储在磁盘上。如果服务器重启,系统会确认收到的持久化消息未丢失。简单地将消息发送给一个持久化的交换机或者路由给一个持久化的队列,并不会使得此消息具有持久化性质:它完全取决与消息本身的持久模式(persistence mode)。将消息以持久化方式发布时,会对性能造成一定的影响(就像数据库操作一样,健壮性的存在必定造成一些性能牺牲)。\n消息确认 由于网络的不确定性和应用失败的可能性,处理确认回执(acknowledgement)就变的十分重要。有时我们确认消费者收到消息就可以了,有时确认回执意味着消息已被验证并且处理完毕,例如对某些数据已经验证完毕并且进行了数据存储或者索引操作。\n这种情形很常见,所以 AMQP 0-9-1 内置了一个功能叫做 消息确认(message acknowledgements),消费者用它来确认消息已经被接收或者处理。如果一个应用崩溃掉(此时连接会断掉,所以AMQP代理亦会得知),而且消息的确认回执功能已经被开启,但是消息代理尚未获得确认回执,那么消息会被从新放入队列(并且在还有还有其他消费者存在于此队列的前提下,立即投递给另外一个消费者)。\n协议内置的消息确认功能将帮助开发者建立强大的软件。\nAMQP 0-9-1 方法 AMQP 0-9-1由许多方法(methods)构成。方法即是操作,这跟面向对象编程中的方法没半毛钱关系。AMQP的方法被分组在类(class)中。这里的类仅仅是对AMQP方法的逻辑分组而已。在 AMQP 0-9-1参考中有对AMQP方法的详细介绍。\n让我们来看看交换机类,有一组方法被关联到了交换机的操作上。这些方法如下所示:\n exchange.declare exchange.declare-ok exchange.delete exchange.delete-ok (请注意,RabbitMQ网站参考中包含了特用于RabbitMQ的交换机类的扩展,这里我们不对其进行讨论)\n以上的操作来自逻辑上的配对:exchange.declare 和 exchange.declare-ok,exchange.delete 和 exchange.delete-ok. 这些操作分为“请求 - requests”(由客户端发送)和“响应 - responses”(由代理发送,用来回应之前提到的“请求”操作)。\n如下的例子:客户端要求消息代理使用exchange.declare方法声明一个新的交换机: \n如上图所示,exchange.declare方法携带了好几个参数。这些参数可以允许客户端指定交换机名称、类型、是否持久化等等。\n操作成功后,消息代理使用exchange.declare-ok方法进行回应: \nexchange.declare-ok方法除了通道号之外没有携带任何其他参数(通道-channel 会在本指南稍后章节进行介绍)。\nAMQP队列类的配对方法 - queue.declare方法 和 queue.declare-ok有着与其他配对方法非常相似的一系列事件: \n\n不是所有的AMQP方法都有与其配对的“另一半”。许多(basic.publish是最被广泛使用的)都没有相对应的“响应”方法,另外一些(如basic.get)有着一种以上与之对应的“响应”方法。\n连接 AMQP连接通常是长连接。AMQP是一个使用TCP提供可靠投递的应用层协议。AMQP使用认证机制并且提供TLS(SSL)保护。当一个应用不再需要连接到AMQP代理的时候,需要优雅的释放掉AMQP连接,而不是直接将TCP连接关闭。\n通道 有些应用需要与AMQP代理建立多个连接。无论怎样,同时开启多个TCP连接都是不合适的,因为这样做会消耗掉过多的系统资源并且使得防火墙的配置更加困难。AMQP 0-9-1提供了通道(channels)来处理多连接,可以把通道理解成共享一个TCP连接的多个轻量化连接。\n在涉及多线程/进程的应用中,为每个线程/进程开启一个通道(channel)是很常见的,并且这些通道不能被线程/进程共享。\n一个特定通道上的通讯与其他通道上的通讯是完全隔离的,因此每个AMQP方法都需要携带一个通道号,这样客户端就可以指定此方法是为哪个通道准备的。\n虚拟主机 为了在一个单独的代理上实现多个隔离的环境(用户、用户组、交换机、队列 等),AMQP提供了一个虚拟主机(virtual hosts - vhosts)的概念。这跟Web servers虚拟主机概念非常相似,这为AMQP实体提供了完全隔离的环境。当连接被建立的时候,AMQP客户端来指定使用哪个虚拟主机。\nAMQP是可扩展的 AMQP 0-9-1 拥有多个扩展点:\n 定制化交换机类型 可以让开发者们实现一些开箱即用的交换机类型尚未很好覆盖的路由方案。例如 geodata-based routing。 交换机和队列的声明中可以包含一些消息代理能够用到的额外属性。例如RabbitMQ中的per-queue message TTL即是使用该方式实现。 特定消息代理的协议扩展。例如RabbitMQ所实现的扩展。 新的 AMQP 0-9-1 方法类可被引入。 消息代理可以被其他的插件扩展,例如RabbitMQ的管理前端 和 已经被插件化的HTTP API。 这些特性使得AMQP 0-9-1模型更加灵活,并且能够适用于解决更加宽泛的问题。\nAMQP 0-9-1 客户端生态系统 AMQP 0-9-1 拥有众多的适用于各种流行语言和框架的客户端。其中一部分严格遵循AMQP规范,提供AMQP方法的实现。另一部分提供了额外的技术,方便使用的方法和抽象。有些客户端是异步的(非阻塞的),有些是同步的(阻塞的),有些将这两者同时实现。有些客户端支持“供应商的特定扩展”(例如RabbitMQ的特定扩展)。\n因为AMQP的主要目标之一就是实现交互性,所以对于开发者来讲,了解协议的操作方法而不是只停留在弄懂特定客户端的库就显得十分重要。这样一来,开发者使用不同类型的库与协议进行沟通时就会容易的多。\n","id":17,"section":"posts","summary":"","tags":["rabbitmq"],"title":"AMQP消息模型","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/amqp-0-9-1-model-explained/","year":"2019"},{"content":"前言 Hystrix已经不在维护了,但是成功的开源项目总是值得学习的.刚开始看 Hystrix 源码时,会发现一堆 Action,Function 的逻辑,这其实就是 RxJava 的特点了\u0026ndash;响应式编程.上篇文章已经对RxJava作过入门介绍,不熟悉的同学可以先去看看.本文会简单介绍 Hystrix,再根据demo结合源码来了解Hystrix的执行流程.\nHystrix简单介绍 什么是 Hystrix?\nHystrix 是一个延迟和容错库,旨在隔离对远程系统、服务和第三方库的访问点,停止级联故障,并在错误不可避免的复杂分布式系统中能够弹性恢复。\n 核心概念\n Command 命令\nCommand 是Hystrix的入口,对用户来说,我们只需要创建对应的 command,将需要保护的接口包装起来就可以.可以无需关注再之后的逻辑.与 Spring 深度集成后还可以通过注解的方式,就更加对开发友好了.\n Circuit Breaker 断路器\n断路器,是从电气领域引申过来的概念,具有过载、短路和欠电压保护功能,有保护线路和电源的能力.在Hystrix中即为当请求超过一定比例响应失败时,hystrix 会对请求进行拦截处理,保证服务的稳定性,以及防止出现服务之间级联雪崩的可能性.\n Isolation 隔离策略\n隔离策略是 Hystrix 的设计亮点所在,利用舱壁模式的思想来对访问的资源进行隔离,每个资源是独立的依赖,单个资源的异常不应该影响到其他. Hystrix 的隔离策略目前有两种:线程池隔离,信号量隔离.\n Hystrix的运行流程\n 官方的 How it Works 对流程有很详细的介绍,图示清晰,相信看完流程图就能对运行流程有一定的了解.\n 一次Command执行 HystrixCommand是标准的命令模式实现,每一次请求即为一次命令的创建执行经历的过程.从上述Hystrix流程图可以看出创建流程最终会指向toObservable,在之前RxJava入门时有介绍到Observable即为被观察者,作用是发送数据给观察者进行相应的,因此可以知道这个方法应该是较为关键的.\nUML HystrixInvokable 标记这个一个可执行的接口,没有任何抽象方法或常量 HystrixExecutable 是为HystrixCommand设计的接口,主要提供执行命令的抽象方法,例如:execute(),queue(),observe() HystrixObservable 是为Observable设计的接口,主要提供自动订阅(observe())和生成Observable(toObservable())的抽象方法 HystrixInvokableInfo 提供大量的状态查询(获取属性配置,是否开启断路器等) AbstractCommand 核心逻辑的实现 HystrixCommand 定制逻辑实现以及留给用户实现的接口(比如:run()) 样例代码 通过新建一个 command 来看 Hystrix 是如何创建并执行的.HystrixCommand 是一个抽象类,其中有一个run方法需要我们实现自己的业务逻辑,以下是偷懒采用匿名内部类的形式呈现.构造方法的内部实现我们就不关注了,直接看下执行的逻辑吧.\nHystrixCommand demo = new HystrixCommand\u0026lt;String\u0026gt;(HystrixCommandGroupKey.Factory.asKey(\u0026quot;demo-group\u0026quot;)) { @Override protected String run() { return \u0026quot;Hello World~\u0026quot;; } }; demo.execute(); 执行过程 流程图 这是官方给出的一次完整调用的链路.上述的 demo 中我们直接调用了execute方法,所以调用的路径为execute() -\u0026gt; queue() -\u0026gt; toObservable() -\u0026gt; toBlocking() -\u0026gt; toFuture() -\u0026gt; get().核心的逻辑其实就在toObservable()中.\nHystrixCommand.java execute execute方法为同步调用返回结果,并对异常作处理.内部会调用queue\n// 同步调用执行 public R execute() { try { // queue()返回的是Future类型的对象,所以这里是阻塞get return queue().get(); } catch (Exception e) { throw decomposeException(e); } } queue queue的第一行代码完成了核心的订阅逻辑.\n toObservable() 生成了 Hystrix 的 Observable 对象 将 Observable 转换为 BlockingObservable 可以阻塞控制数据发送 toFuture 实现对 BlockingObservable 的订阅\npublic Future\u0026lt;R\u0026gt; queue() { // 着重关注的是这行代码 // 完成了Observable的创建及订阅 // toBlocking()是将Observable转为BlockingObservable,转换后的Observable可以阻塞数据的发送 final Future\u0026lt;R\u0026gt; delegate = toObservable().toBlocking().toFuture(); final Future\u0026lt;R\u0026gt; f = new Future\u0026lt;R\u0026gt;() { // 由于toObservable().toBlocking().toFuture()返回的Future如果中断了, // 不会对当前线程进行中断,所以这里将返回的Future进行了再次包装,处理异常逻辑 ... } // 判断是否已经结束了,有异常则直接抛出 if (f.isDone()) { try { f.get(); return f; } catch (Exception e) { // 省略这段判断 } } return f; } BlockingObservable.java // 被包装的Observable private final Observable\u0026lt;? extends T\u0026gt; o; // toBlocking()会调用该静态方法将 源Observable简单包装成BlockingObservable public static \u0026lt;T\u0026gt; BlockingObservable\u0026lt;T\u0026gt; from(final Observable\u0026lt;? extends T\u0026gt; o) { return new BlockingObservable\u0026lt;T\u0026gt;(o); } public Future\u0026lt;T\u0026gt; toFuture() { return BlockingOperatorToFuture.toFuture((Observable\u0026lt;T\u0026gt;)o); } BlockingOperatorToFuture.java ReactiveX 关于toFuture的解读\nThe toFuture operator applies to the BlockingObservable subclass, so in order to use it, you must first convert your source Observable into a BlockingObservable by means of either the BlockingObservable.from method or the Observable.toBlocking operator.\n toFuture只能作用于BlockingObservable所以也才会有上文想要转换为BlockingObservable的操作\n// 该操作将 源Observable转换为返回单个数据项的Future public static \u0026lt;T\u0026gt; Future\u0026lt;T\u0026gt; toFuture(Observable\u0026lt;? extends T\u0026gt; that) { // CountDownLatch 判断是否完成 final CountDownLatch finished = new CountDownLatch(1); // 存储执行结果 final AtomicReference\u0026lt;T\u0026gt; value = new AtomicReference\u0026lt;T\u0026gt;(); // 存储错误结果 final AtomicReference\u0026lt;Throwable\u0026gt; error = new AtomicReference\u0026lt;Throwable\u0026gt;(); // single()方法可以限制Observable只发送单条数据 // 如果有多条数据 会抛 IllegalArgumentException // 如果没有数据可以发送 会抛 NoSuchElementException @SuppressWarnings(\u0026quot;unchecked\u0026quot;) final Subscription s = ((Observable\u0026lt;T\u0026gt;)that).single().subscribe(new Subscriber\u0026lt;T\u0026gt;() { // single()返回的Observable就可以对其进行标准的处理了 @Override public void onCompleted() { finished.countDown(); } @Override public void onError(Throwable e) { error.compareAndSet(null, e); finished.countDown(); } @Override public void onNext(T v) { // \u0026quot;single\u0026quot; guarantees there is only one \u0026quot;onNext\u0026quot; value.set(v); } }); // 最后将Subscription返回的数据封装成Future,实现对应的逻辑 return new Future\u0026lt;T\u0026gt;() { // 可以查看源码 }; } AbstractCommand.java AbstractCommand是toObservable实现的地方,属于Hystrix的核心逻辑,代码较长,可以和方法调用的流程图一起食用.toObservable主要是完成缓存和创建Observable,requestLog的逻辑,当第一次创建Observable时,applyHystrixSemantics方法是Hystrix的语义实现,可以跳着看.\n tips: 下文中有很多 Action和 Function,他们很相似,都有call方法,但是区别在于Function有返回值,而Action没有,方法后跟着的数字代表有几个入参.Func0/Func3即没有入参和有三个入参\n toObservable toObservable代码较长且分层还是清晰的,所以下面一块一块写.其逻辑和文章开始提到的Hystrix流程图是完全一致的.\npublic Observable\u0026lt;R\u0026gt; toObservable() { final AbstractCommand\u0026lt;R\u0026gt; _cmd = this; // 此处省略掉了很多个Action和Function,大部分是来做扫尾清理的函数,所以用到的时候再说 // defer在上篇rxjava入门中提到过,是一种创建型的操作符,每次订阅时会产生新的Observable,回调方法中所实现的才是真正我们需要的Observable return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { // 校验命令的状态,保证其只执行一次 if (!commandState.compareAndSet(CommandState.NOT_STARTED, CommandState.OBSERVABLE_CHAIN_CREATED)) { IllegalStateException ex = new IllegalStateException(\u0026quot;This instance can only be executed once. Please instantiate a new instance.\u0026quot;); //TODO make a new error type for this throw new HystrixRuntimeException(FailureType.BAD_REQUEST_EXCEPTION, _cmd.getClass(), getLogMessagePrefix() + \u0026quot; command executed multiple times - this is not permitted.\u0026quot;, ex, null); } commandStartTimestamp = System.currentTimeMillis(); // properties为当前command的所有属性 // 允许记录请求log时会保存当前执行的command if (properties.requestLogEnabled().get()) { // log this command execution regardless of what happened if (currentRequestLog != null) { currentRequestLog.addExecutedCommand(_cmd); } } // 是否开启了请求缓存 final boolean requestCacheEnabled = isRequestCachingEnabled(); // 获取缓存key final String cacheKey = getCacheKey(); // 开启缓存后,尝试从缓存中取 if (requestCacheEnabled) { HystrixCommandResponseFromCache\u0026lt;R\u0026gt; fromCache = (HystrixCommandResponseFromCache\u0026lt;R\u0026gt;) requestCache.get(cacheKey); if (fromCache != null) { isResponseFromCache = true; return handleRequestCacheHitAndEmitValues(fromCache, _cmd); } } // 没有开启请求缓存时,就执行正常的逻辑 Observable\u0026lt;R\u0026gt; hystrixObservable = // 这里又通过defer创建了我们需要的Observable Observable.defer(applyHystrixSemantics) // 发送前会先走一遍hook,默认executionHook是空实现的,所以这里就跳过了 .map(wrapWithAllOnNextHooks); // 得到最后的封装好的Observable后,将其放入缓存 if (requestCacheEnabled \u0026amp;\u0026amp; cacheKey != null) { // wrap it for caching HystrixCachedObservable\u0026lt;R\u0026gt; toCache = HystrixCachedObservable.from(hystrixObservable, _cmd); HystrixCommandResponseFromCache\u0026lt;R\u0026gt; fromCache = (HystrixCommandResponseFromCache\u0026lt;R\u0026gt;) requestCache.putIfAbsent(cacheKey, toCache); if (fromCache != null) { // another thread beat us so we'll use the cached value instead toCache.unsubscribe(); isResponseFromCache = true; return handleRequestCacheHitAndEmitValues(fromCache, _cmd); } else { // we just created an ObservableCommand so we cast and return it afterCache = toCache.toObservable(); } } else { afterCache = hystrixObservable; } return afterCache // 终止时的操作 .doOnTerminate(terminateCommandCleanup) // perform cleanup once (either on normal terminal state (this line), or unsubscribe (next line)) // 取消订阅时的操作 .doOnUnsubscribe(unsubscribeCommandCleanup) // perform cleanup once // 完成时的操作 .doOnCompleted(fireOnCompletedHook); } } handleRequestCacheHitAndEmitValues 缓存击中时的处理\nprivate Observable\u0026lt;R\u0026gt; handleRequestCacheHitAndEmitValues(final HystrixCommandResponseFromCache\u0026lt;R\u0026gt; fromCache, final AbstractCommand\u0026lt;R\u0026gt; _cmd) { try { // Hystrix中有大量的hook 如果有心做二次开发的,可以利用这些hook做到很完善的监控 executionHook.onCacheHit(this); } catch (Throwable hookEx) { logger.warn(\u0026quot;Error calling HystrixCommandExecutionHook.onCacheHit\u0026quot;, hookEx); } // 将缓存的结果赋给当前command return fromCache.toObservableWithStateCopiedInto(this) // doOnTerminate 或者是后面看到的doOnUnsubscribe,doOnError,都指的是在响应onTerminate/onUnsubscribe/onError后的操作,即在Observable的生命周期上注册一个动作优雅的处理逻辑 .doOnTerminate(new Action0() { @Override public void call() { // 命令最终状态的不同进行不同处理 if (commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.TERMINAL)) { cleanUpAfterResponseFromCache(false); //user code never ran } else if (commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.TERMINAL)) { cleanUpAfterResponseFromCache(true); //user code did run } } }) .doOnUnsubscribe(new Action0() { @Override public void call() { // 命令最终状态的不同进行不同处理 if (commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.UNSUBSCRIBED)) { cleanUpAfterResponseFromCache(false); //user code never ran } else if (commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.UNSUBSCRIBED)) { cleanUpAfterResponseFromCache(true); //user code did run } } }); } applyHystrixSemantics 因为本片文章的主要目的是在讲执行流程,所以失败回退和断路器相关的就留到以后的文章中再写.\nfinal Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt; applyHystrixSemantics = new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { // 不再订阅了就返回不发送数据的Observable if (commandState.get().equals(CommandState.UNSUBSCRIBED)) { // 不发送任何数据或通知 return Observable.never(); } return applyHystrixSemantics(_cmd); } }; private Observable\u0026lt;R\u0026gt; applyHystrixSemantics(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { // 标记开始执行的hook // 如果hook内抛异常了,会快速失败且没有fallback处理 executionHook.onStart(_cmd); /* determine if we're allowed to execute */ // 断路器核心逻辑: 判断是否允许执行(TODO) if (circuitBreaker.allowRequest()) { // Hystrix自己造的信号量轮子,之所以不用juc下,官方解释为juc的Semphore实现太复杂,而且没有动态调节的信号量大小的能力,简而言之,不满足需求! // 根据不同隔离策略(线程池隔离/信号量隔离)获取不同的TryableSemphore final TryableSemaphore executionSemaphore = getExecutionSemaphore(); // Semaphore释放标志 final AtomicBoolean semaphoreHasBeenReleased = new AtomicBoolean(false); // 释放信号量的Action final Action0 singleSemaphoreRelease = new Action0() { @Override public void call() { if (semaphoreHasBeenReleased.compareAndSet(false, true)) { executionSemaphore.release(); } } }; // 异常处理 final Action1\u0026lt;Throwable\u0026gt; markExceptionThrown = new Action1\u0026lt;Throwable\u0026gt;() { @Override public void call(Throwable t) { // HystrixEventNotifier是hystrix的插件,不同的事件发送不同的通知,默认是空实现. eventNotifier.markEvent(HystrixEventType.EXCEPTION_THROWN, commandKey); } }; // 线程池隔离的TryableSemphore始终为true if (executionSemaphore.tryAcquire()) { try { /* used to track userThreadExecutionTime */ // executionResult是一次命令执行的结果信息封装 // 这里设置起始时间是为了记录命令的生命周期,执行过程中会set其他属性进去 executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis()); return executeCommandAndObserve(_cmd) // 报错时的处理 .doOnError(markExceptionThrown) // 终止时释放 .doOnTerminate(singleSemaphoreRelease) // 取消订阅时释放 .doOnUnsubscribe(singleSemaphoreRelease); } catch (RuntimeException e) { return Observable.error(e); } } else { // tryAcquire失败后会做fallback处理,TODO return handleSemaphoreRejectionViaFallback(); } } else { // 断路器短路(拒绝请求)fallback处理 TODO return handleShortCircuitViaFallback(); } } executeCommandAndObserve /** * 执行run方法的地方 */ private Observable\u0026lt;R\u0026gt; executeCommandAndObserve(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { // 获取当前上下文 final HystrixRequestContext currentRequestContext = HystrixRequestContext.getContextForCurrentThread(); // 发送数据时的Action响应 final Action1\u0026lt;R\u0026gt; markEmits = new Action1\u0026lt;R\u0026gt;() { @Override public void call(R r) { // 如果onNext时需要上报时,做以下处理 if (shouldOutputOnNextEvents()) { // result标记 executionResult = executionResult.addEvent(HystrixEventType.EMIT); // 通知 eventNotifier.markEvent(HystrixEventType.EMIT, commandKey); } // commandIsScalar是一个我不解的地方,在网上也没有查到好的解释 // 该方法为抽象方法,有HystrixCommand实现返回true.HystrixObservableCommand返回false if (commandIsScalar()) { // 耗时 long latency = System.currentTimeMillis() - executionResult.getStartTimestamp(); // 通知 eventNotifier.markCommandExecution(getCommandKey(), properties.executionIsolationStrategy().get(), (int) latency, executionResult.getOrderedList()); eventNotifier.markEvent(HystrixEventType.SUCCESS, commandKey); executionResult = executionResult.addEvent((int) latency, HystrixEventType.SUCCESS); // 断路器标记成功(断路器半开时的反馈,决定是否关闭断路器) circuitBreaker.markSuccess(); } } }; final Action0 markOnCompleted = new Action0() { @Override public void call() { if (!commandIsScalar()) { // 同markEmits 类似处理 } } }; // 失败回退的逻辑 final Func1\u0026lt;Throwable, Observable\u0026lt;R\u0026gt;\u0026gt; handleFallback = new Func1\u0026lt;Throwable, Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call(Throwable t) { // 不是重点略过了 } }; // 请求上下文的处理 final Action1\u0026lt;Notification\u0026lt;? super R\u0026gt;\u0026gt; setRequestContext = new Action1\u0026lt;Notification\u0026lt;? super R\u0026gt;\u0026gt;() { @Override public void call(Notification\u0026lt;? super R\u0026gt; rNotification) { setRequestContextIfNeeded(currentRequestContext); } }; Observable\u0026lt;R\u0026gt; execution; // 如果有执行超时限制,会将包装后的Observable再转变为支持TimeOut的 if (properties.executionTimeoutEnabled().get()) { // 根据不同的隔离策略包装为不同的Observable execution = executeCommandWithSpecifiedIsolation(_cmd) // lift 是rxjava中一种基本操作符 可以将Observable转换成另一种Observable // 包装为带有超时限制的Observable .lift(new HystrixObservableTimeoutOperator\u0026lt;R\u0026gt;(_cmd)); } else { execution = executeCommandWithSpecifiedIsolation(_cmd); } return execution.doOnNext(markEmits) .doOnCompleted(markOnCompleted) .onErrorResumeNext(handleFallback) .doOnEach(setRequestContext); } executeCommandWithSpecifiedIsolation 根据不同的隔离策略创建不同的执行Observable\nprivate Observable\u0026lt;R\u0026gt; executeCommandWithSpecifiedIsolation(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { if (properties.executionIsolationStrategy().get() == ExecutionIsolationStrategy.THREAD) { // mark that we are executing in a thread (even if we end up being rejected we still were a THREAD execution and not SEMAPHORE) return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { // 由于源码太长,这里只关注正常的流程,需要详细了解可以去看看源码 if (threadState.compareAndSet(ThreadState.NOT_USING_THREAD, ThreadState.STARTED)) { try { return getUserExecutionObservable(_cmd); } catch (Throwable ex) { return Observable.error(ex); } } else { //command has already been unsubscribed, so return immediately return Observable.error(new RuntimeException(\u0026quot;unsubscribed before executing run()\u0026quot;)); } }}) .doOnTerminate(new Action0() {}) .doOnUnsubscribe(new Action0() {}) // 指定在某一个线程上执行,是rxjava中很重要的线程调度的概念 .subscribeOn(threadPool.getScheduler(new Func0\u0026lt;Boolean\u0026gt;() { })); } else { // 信号量隔离策略 return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { // 逻辑与线程池大致相同 }); } } getUserExecutionObservable 获取用户执行的逻辑\nprivate Observable\u0026lt;R\u0026gt; getUserExecutionObservable(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { Observable\u0026lt;R\u0026gt; userObservable; try { // getExecutionObservable是抽象方法,有HystrixCommand自行实现 userObservable = getExecutionObservable(); } catch (Throwable ex) { // the run() method is a user provided implementation so can throw instead of using Observable.onError // so we catch it here and turn it into Observable.error userObservable = Observable.error(ex); } // 将Observable作其他中转 return userObservable .lift(new ExecutionHookApplication(_cmd)) .lift(new DeprecatedOnRunHookApplication(_cmd)); } lift操作符\nlift可以转换成一个新的Observable,它很像一个代理,将原来的Observable代理到自己这里,订阅时通知原来的Observable发送数据,经自己这里流转加工处理再返回给订阅者.Map/FlatMap操作符底层其实就是用的lift进行实现的.\ngetExecutionObservable @Override final protected Observable\u0026lt;R\u0026gt; getExecutionObservable() { return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { try { // just操作符就是直接执行的Observable // run方法就是我们实现的业务逻辑: Hello World~ return Observable.just(run()); } catch (Throwable ex) { return Observable.error(ex); } } }).doOnSubscribe(new Action0() { @Override public void call() { // 执行订阅时将执行线程记为当前线程,必要时我们可以interrupt executionThread.set(Thread.currentThread()); } }); } 总结 希望自己能把埋下的坑一一填完: 容错机制,metrics,断路器等等\u0026hellip;\n参考 Hystrix How it Works ReactiveX官网 阮一峰: 中文技术文档写作规范 RxJava lift 原理解析 ","id":18,"section":"posts","summary":"","tags":["rxjava","hystrix"],"title":"Hystrix命令执行流程","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/rxjava-in-hystrix/","year":"2019"},{"content":" 本文基于 rxjava 1.x 版本\n 前言 写这篇文章是因为之前在看Hystrix时,觉得响应式编程很有意思,之前也了解到Spring5主打特性就是响应式,就想来试试水,入个门.本文主要介绍RxJava的特点,入门操作\nRxJava是什么 Reactive X ReactiveX是使用Observable序列来组合异步操作且基于事件驱动的一个库.其继承自观察者模式来支持数据流或者事件流通过添加操作符(operators)的方式来声明式的操作,并抽象出对低级别线程(low-level thread),同步,线程安全,并发数据结构,非阻塞IO问题的关注.\nReactiveX 在不同语言中都有实现,RxJava 只是在JVM上实现的一套罢了.\n概念 观察者模式是该框架的灵魂~\n 上图可以表述为: 观察者(Observer) 订阅(subscribe)被观察者(Observable),当Observable产生事件或数据时,会调用Observer的方法进行回调.\n听起来有点别扭,这里举一个形象点的例子.\n显示器开关\n显示器开关即为 Observable, 显示器为 Observer,这两个组件就会形成联系.当开关按下时,显示器就会通电点亮,这里即可抽象成Observable发出一个事件,Observer对事件做了处理.做什么样的处理其实在Subscribe时就已经决定了.\n回调方法\n在subscribe时会要求实现对应的回调方法,标准方法有以下三个:\n onNext Observable调用这个方法发射数据,方法的参数就是Observable发射的数据,这个方法可能会被调用多次,取决于你的实现。\n onError 当Observable遇到错误或者无法返回期望的数据时会调用这个方法,这个调用会终止Observable,后续不会再调用onNext和onCompleted,onError方法的参数是抛出的异常。\n onCompleted 正常终止,如果没有遇到错误,Observable在最后一次调用onNext之后调用此方法。\n\u0026ldquo;Hot\u0026rdquo; or \u0026ldquo;Cold\u0026rdquo; Observables Observable何时开始发送数据呢?基于此问题,可以将Observable分为两类: Hot \u0026amp; Cold . 可以理解为主动型和被动型.\nHot Observable: Observable一经创建,就会开始发送数据. 所以后面订阅的Observer可能消费不到Observable完整的数据.\nCold Observable: Observable会等到有Observer订阅时才开始发送数据,此时Observer会消费到完整的数据\nRxJava入门 Hello World Observable.create(new Observable.OnSubscribe\u0026lt;String\u0026gt;() { @Override public void call(Subscriber\u0026lt;? super String\u0026gt; subscriber) { subscriber.onNext(\u0026quot;Hello World\u0026quot;); subscriber.onCompleted(); //subscriber.onError(new RuntimeException(\u0026quot;error\u0026quot;)); } }).subscribe(new Subscriber\u0026lt;String\u0026gt;() { @Override public void onCompleted() { System.out.println(\u0026quot;观察结束啦~~~\u0026quot;); } @Override public void onError(Throwable e) { System.out.println(\u0026quot;观察出错啦~~~\u0026quot;); } @Override public void onNext(String s) { System.out.println(\u0026quot;onNext:\u0026quot; + s); } }); } // onNext:Hello World // 观察结束啦~~~ // 注释掉上一行 打开下一行注释 就会输出 // onNext:Hello World // 观察出错啦~~~ 上述即为一个标准的创建观察者被观察者并订阅,实现订阅逻辑.\n疑问\n 为什么subscribe方法的参数是Subscriber呢? 在rxjava中Observer是接口,Subscriber实现了Observer并提供了拓展.所以普遍用这个.\n 为什么是Observable.subscribe(Observer)?用上面的显示器开关的例子来说就相当于显示器开关订阅显示器. 为了保证流式风格~rxjava提供了一系列的操作符来对Observable发出的数据做处理,流式风格可以使操作符使用起来更友好.所以就当做Observable订阅了Observer吧🤦‍♂\n操作符 Operators 单纯的使用上面的Hello World撸码只能说是观察者模式的运用罢了,操作符才是ReactiveX最强大的地方.我们可以通过功能不同的操作符对Observable发出的数据做过滤(filter),转换(map)来满足业务的需求.其实就可以当作是Java8的lambda特性.\n Observable在经过操作符处理后还是一个Observable,对应上述的流式风格\n 案例: 假设我们需要监听鼠标在一个直角坐标系中的点击,取得所有在第一象限点击的坐标.\n从该流程图可以看出,鼠标点击后会发出很多数据,一次点击一个点,我们对数据进行filter,得到了下方时间轴上的数据源.这就是我们想要的.下面来看下常用的操作符有哪些?\n创建型操作符 用于创建Observable对象的操作符\n Create 创建一个Observable,需要传递一个Function来完成调用Observer的逻辑.\n一个标准的Observable必须只能调用一次(Exactly Once)onCompleted或者onError,并且在调用后不能再调用Observer的其他方法(eg: onNext).\nsample code\nObservable.create(new Observable.OnSubscribe\u0026lt;Integer\u0026gt;() { @Override public void call(Subscriber\u0026lt;? super Integer\u0026gt; observer) { try { if (!observer.isUnsubscribed()) { for (int i = 1; i \u0026lt; 5; i++) { observer.onNext(i); } observer.onCompleted(); } } catch (Exception e) { observer.onError(e); } } } ).subscribe(new Subscriber\u0026lt;Integer\u0026gt;() { @Override public void onNext(Integer item) { System.out.println(\u0026quot;Next: \u0026quot; + item); } @Override public void onError(Throwable error) { System.err.println(\u0026quot;Error: \u0026quot; + error.getMessage()); } @Override public void onCompleted() { System.out.println(\u0026quot;Sequence complete.\u0026quot;); } }); Next: 1 Next: 2 Next: 3 Next: 4 Sequence complete. Defer 直到有Observer订阅时才会创建,并且会为每一个Observer创建新的Observable,这样可以保证所有Observer可以看到相同的数据,并且从头开始消费.\nsample code\nObservable\u0026lt;String\u0026gt; defer = Observable.defer(new Func0\u0026lt;Observable\u0026lt;String\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;String\u0026gt; call() { return Observable.just(\u0026quot;Hello\u0026quot;, \u0026quot;World\u0026quot;); } }); defer.subscribe(new Subscriber\u0026lt;String\u0026gt;() { @Override public void onCompleted() { System.out.println(\u0026quot;第一个订阅完成啦~\u0026quot;); } @Override public void onError(Throwable e) { System.out.println(\u0026quot;第一个订阅报错啦~\u0026quot;); } @Override public void onNext(String s) { System.out.println(\u0026quot;第一个订阅收到:\u0026quot; + s); } }); defer.subscribe(new Subscriber\u0026lt;String\u0026gt;() { //与上一个订阅逻辑相同 }); 第一个订阅收到:Hello 第一个订阅收到:World 第一个订阅完成啦~ 第二个订阅收到:Hello 第二个订阅收到:World 第二个订阅完成啦~ Note:\nDefer在RxJava中的实现其实有点像指派,可以看到构建时,传参为Func0\u0026lt;Observable\u0026lt;T\u0026gt;\u0026gt;,Observer真正订阅的是传参中的Observable.\nJust 在上文Defer中代码中就用了Just,指的是可以发送特定的数据.代码一致就不作展示了.\nInterval 可以按照指定时间间隔从0开始发送无限递增序列.\n参数 initalDelay 延迟多长时间开始第一次发送 period 指定时间间隔 unit 时间单位 如下例子:延迟0秒后开始发送,每1秒发送一次. 因为sleep 100秒,会发送0-99终止\nsample code\nObservable.interval(0,1,TimeUnit.SECONDS).subscribe(new Action1\u0026lt;Long\u0026gt;() { // 这里只实现了OnNext方法,onError和onCompleted可以有默认实现.一种偷懒写法 @Override public void call(Long aLong) { System.out.println(aLong); } }); try { //阻塞当前线程使程序一直跑 TimeUnit.SECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } 转换操作符 将Observable发出的数据进行各类转换的操作符\n Buffer 如上图所示,buffer定期将数据收集到集合中,并将集合打包发送.\nsample code\nObservable.just(2,3,5,6) .buffer(3) .subscribe(new Action1\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt;() { @Override public void call(List\u0026lt;Integer\u0026gt; integers) { System.out.println(integers); } }); [2, 3, 5] [6] Window\nwindow和buffer是非常像的两个操作符,区别在于buffer会将存起来的item打包再发出去,而window则只是单纯的将item堆起来,达到阈值再发出去,不对原数据结构做修改.\nsample code\nObservable.just(2,3,5,6) .window(3) .subscribe(new Action1\u0026lt;Observable\u0026lt;Integer\u0026gt;\u0026gt;() { @Override public void call(Observable\u0026lt;Integer\u0026gt; integerObservable) { integerObservable.subscribe(new Action1\u0026lt;Integer\u0026gt;() { @Override public void call(Integer integer) { // do anything } }); } }); 合并操作符 将多个Observable合并为一个的操作符\n Zip 使用一个函数组合多个Observable发射的数据集合,然后再发射这个结果。如果多个Observable发射的数据量不一样,则以最少的Observable为标准进行组合.\nsample code\nObservable\u0026lt;Integer\u0026gt; observable1=Observable.just(1,2,3,4); Observable\u0026lt;Integer\u0026gt; observable2=Observable.just(4,5,6); Observable.zip(observable1, observable2, new Func2\u0026lt;Integer, Integer, String\u0026gt;() { @Override public String call(Integer item1, Integer item2) { return item1+\u0026quot;and\u0026quot;+item2; } }).subscribe(new Action1\u0026lt;String\u0026gt;() { @Override public void call(String s) { System.out.println(s); } }); 1and4 2and5 3and6 背压操作符 用于平衡Observer消费速度,Observable生产速度的操作符\n 背压是指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略.下图可以很好阐释背压机制是如何运行的.\n宗旨就是下游告诉上游我能处理多少你就给我发多少.\n//被观察者将产生100000个事件 Observable observable=Observable.range(1,100000); observable.observeOn(Schedulers.newThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(Object o) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\u0026quot;on Next Request...\u0026quot;); request(1); } }); 背压支持 上述样例代码中创建Observable使用的是range操作符,这是因为他是支持背压的,如果用interval,request的方法将不起作用.因为interval不支持背压.那什么样的Observable支持背压呢?\n在前面介绍概念时,有提到过Hot\u0026amp;Cold的区别,Hot类型的Observable,即一经创建就开始发送,不支持背压,Cold类型的Observable也只是部分支持.\nonBackpressurebuffer/onBackpressureDrop 不支持背压的操作符我们可以如何实现背压呢?就通过onBackpressurebuffer/onBackpressureDrop来实现.顾名思义一个是缓存,一个是丢弃.\n这里以drop方式来展示.\nObservable.interval(1, TimeUnit.MILLISECONDS) .onBackpressureDrop() //指定observer调度io线程上,并将缓存size置为1,这个缓存会提前将数据存好在消费, //默认在PC上是128,设置小一点可以快速的看到drop的效果 .observeOn(Schedulers.io(), 1) .subscribe(new Subscriber\u0026lt;Long\u0026gt;() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { System.out.println(\u0026quot;Error:\u0026quot; + e.getMessage()); } @Override public void onNext(Long aLong) { System.out.println(\u0026quot;订阅 \u0026quot; + aLong); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }) 订阅 0 订阅 103 订阅 207 订阅 300 订阅 417 订阅 519 订阅 624 订阅 726 订阅 827 订阅 931 订阅 1035 订阅 1138 订阅 1244 订阅 1349 可以很明显的看出很多数据被丢掉了,这就是背压的效果.\n总结 写了这么多后,想来说说自己的感受.\n 转变思想: 响应式编程的思想跟我们现在后端开发思路是有区别的.可能刚开始会不适应. 不易调试: 流式风格写着爽,调着难 参考 ReactiveX官网\n关于RxJava最友好的文章——背压(Backpressure)\n如何形象地描述RxJava中的背压和流控机制?\n","id":19,"section":"posts","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文基于 rxjava 1.x 版本\u003c/p\u003e\n\u003c/blockquote\u003e","tags":["rxjava","响应式编程"],"title":"RxJava入门","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/rxjava-guide/","year":"2019"},{"content":"Given an array, rotate the array to the right by k steps, where k is non-negative.\nExample 1:\nInput: [1,2,3,4,5,6,7] and k = 3 Output: [5,6,7,1,2,3,4] Explanation: rotate 1 steps to the right: [7,1,2,3,4,5,6] rotate 2 steps to the right: [6,7,1,2,3,4,5] rotate 3 steps to the right: [5,6,7,1,2,3,4] Example 2:\nInput: [-1,-100,3,99] and k = 2 Output: [3,99,-1,-100] Explanation: rotate 1 steps to the right: [99,-1,-100,3] rotate 2 steps to the right: [3,99,-1,-100] Note:\n Try to come up as many solutions as you can, there are at least 3 different ways to solve this problem. Could you do it in-place with O(1) extra space? 思路\n依次反转前半部分及后半部分,最后反转整个数组\neg: 1,2,3,4,5,6,7 k=3\n 反转前半部分 4,3,2,1,5,6,7\n 反转后半部分 4,3,2,1,7,6,5\n 反转整个数组 5,6,7,1,2,3,4\nSolution 1\npub fn rotate(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, k: i32) { if nums.is_empty() || k \u0026lt;= 0 { return; } let o_len = nums.len(); let mod_k = k as usize % o_len; reverse(nums, 0, o_len - mod_k - 1); reverse(nums, o_len - mod_k, o_len - 1); reverse(nums, 0, o_len - 1); } pub fn reverse(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, start: usize, end: usize) { let mut o_start = start; let mut o_end = end; while o_start \u0026lt; o_end { nums.swap(o_start, o_end); o_start += 1; o_end -= 1; } } Solution 2\n api 解法,效率不高,但好看\n pub fn rotate(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, k: i32) { if nums.is_empty() { return; } let mod_k = k % nums.len() as i32; for _ in 0..mod_k as usize { let item = nums.pop().unwrap(); nums.insert(0, item); } } ","id":20,"section":"posts","summary":"","tags":["leetcode","rust"],"title":"[LeetCode In Rust]189-Rotate Array","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/189-rotate-array/","year":"2019"},{"content":"pub fn remove_duplicates(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;) -\u0026gt; i32 { if nums.is_empty() { return 0 } let mut idx = 0; for i in idx .. nums.len() { if nums[i].gt(\u0026amp;nums[idx]) { idx += 1; nums.swap(i,idx); } } (idx + 1) as i32 } ","id":21,"section":"posts","summary":"","tags":["leetcode","rust"],"title":"[LeetCode In Rust]026-Remove Duplicates From Sorted Array","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/026-remove-duplicates-from-sorted-array/","year":"2019"},{"content":"pub fn two_sum(nums: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let map: HashMap\u0026lt;i32, usize\u0026gt; = nums.iter().enumerate().map(|(idx, \u0026amp;data)| (data, idx)).collect(); nums.iter().enumerate().find(|(idx, \u0026amp;num)| { match map.get(\u0026amp;(target - num)) { Some(\u0026amp;idx_in_map) =\u0026gt; idx_in_map != *idx, None =\u0026gt; false, } }).map(|(idx, \u0026amp;num)| vec![*map.get(\u0026amp;(target - num)).unwrap() as i32, idx as i32]).unwrap() } pub fn two_sum_v2(nums: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let map: HashMap\u0026lt;i32, usize\u0026gt; = nums.iter().enumerate().map(|(idx, \u0026amp;data)| (data, idx)).collect(); for (i,\u0026amp;num) in nums.iter().enumerate() { match map.get(\u0026amp;(target - num) ) { Some(\u0026amp;x) =\u0026gt; { if i != x { return vec![i as i32, x as i32] } }, None =\u0026gt; continue, } } vec![] } ","id":22,"section":"posts","summary":"","tags":["leetcode","rust"],"title":"[LeetCode In Rust]001-Two Sum","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/001-two-sum/","year":"2019"}],"tags":[{"title":"collections","uri":"https://xiaohei.im/hugo-theme-pure/tags/collections/"},{"title":"hugo","uri":"https://xiaohei.im/hugo-theme-pure/tags/hugo/"},{"title":"hystrix","uri":"https://xiaohei.im/hugo-theme-pure/tags/hystrix/"},{"title":"leetcode","uri":"https://xiaohei.im/hugo-theme-pure/tags/leetcode/"},{"title":"netty","uri":"https://xiaohei.im/hugo-theme-pure/tags/netty/"},{"title":"rabbitmq","uri":"https://xiaohei.im/hugo-theme-pure/tags/rabbitmq/"},{"title":"redis","uri":"https://xiaohei.im/hugo-theme-pure/tags/redis/"},{"title":"rust","uri":"https://xiaohei.im/hugo-theme-pure/tags/rust/"},{"title":"rxjava","uri":"https://xiaohei.im/hugo-theme-pure/tags/rxjava/"},{"title":"分布式锁","uri":"https://xiaohei.im/hugo-theme-pure/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"},{"title":"响应式编程","uri":"https://xiaohei.im/hugo-theme-pure/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/"},{"title":"数据结构","uri":"https://xiaohei.im/hugo-theme-pure/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"}]} \ No newline at end of file
+{"categories":[{"title":"CoreJava","uri":"https://xiaohei.im/hugo-theme-pure/categories/corejava/"},{"title":"Hystrix","uri":"https://xiaohei.im/hugo-theme-pure/categories/hystrix/"},{"title":"leetcode","uri":"https://xiaohei.im/hugo-theme-pure/categories/leetcode/"},{"title":"redis","uri":"https://xiaohei.im/hugo-theme-pure/categories/redis/"},{"title":"学习笔记","uri":"https://xiaohei.im/hugo-theme-pure/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"},{"title":"消息队列","uri":"https://xiaohei.im/hugo-theme-pure/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"posts":[{"content":"链表需要注意的问题:\n 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =\u0026gt; Remove Nth Node From End of List Medium public ListNode removeNthFromEnd(ListNode head, int n) { // 快慢指针 ListNode fast = head,slow = head,prev = null; while(fast.next != null) { if (--n \u0026lt;= 0) { // 先走 n 步后,slow 再走 prev = slow; slow = slow.next; } fast = fast.next; } // fast 走完,slow 刚好到倒数 n 的位置 // 删除 slow 节点 if( prev == null) { // 说明删除的是头结点 if(slow.next == null) { // 说明链表只有一个节点.且需要删除的就是这个. head = null; } else { // 大于一个节点就需要把 head 指向 slow.next head = prev = slow.next; } } else { prev.next = slow.next; } return head; } No.2 =\u0026gt; Add Two Numbers Medium 思路: 就按着这个链表顺序加,然后生成一个链表就自然是倒序的了.\npublic ListNode addTwoNumbers(ListNode l1, ListNode l2) { // 一次累加,reverse ListNode result = new ListNode(0); ListNode head = result; int carry = 0; while(l1 != null || l2 != null || carry \u0026gt; 0) { int v1 = 0; int v2 = 0; if(l1 != null) { v1 = l1.val; l1 = l1.next; } if(l2 != null) { v2 = l2.val; l2 = l2.next; } int sum = v1 + v2 + carry; result.next = new ListNode( sum % 10); result = result.next; carry = sum / 10 ; } return head.next; } No.21 =\u0026gt; Merge Two Sorted Lists EASY 非递归 public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 边界条件需要判断 if (l1 == null) return l2; if (l2 == null) return l1; ListNode dummy = new ListNode(0), head = dummy; while(l1 !=null || l2 != null) { if(l1 == null) { head.next = l2; l2 = l2.next; } else if (l2 == null) { head.next = l1; l1 = l1.next; } else if (l1.val \u0026gt;= l2.val) { head.next = l2; l2 = l2.next; } else { head.next = l1; l1 = l1.next; } head = head.next; } return dummy.next; } 递归 public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 边界条件需要判断 if (l1 == null) return l2; if (l2 == null) return l1; if (l1.val \u0026lt;= l2.val) { l1.next = mergeTwoLists(l1.next,l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } } No.24 =\u0026gt; Swap Nodes in Pairs Medium 思路: 走两步然后替换这两个node,考虑head为空和头两个节点交换的情况\npublic ListNode swapPairs(ListNode head) { if(head == null) { return null; } int step = 1; ListNode prev = null; ListNode curr = head; while(curr.next != null) { if(++step % 2 == 0) { // 每两个反转链表 if (prev == null) { // 是头结点与第二个节点反转 ListNode second = curr.next; curr.next = second.next; second.next = curr; head = second; } else { // 中间节点 swap ListNode second = curr.next; curr.next = curr.next.next; prev.next = second; second.next = curr; } } else { // 走两步~ prev = curr; curr = curr.next; } } return head; } ","id":0,"section":"posts","summary":"","tags":["leetcode"],"title":"「LeetCode」链表题解","uri":"https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/","year":"2019"},{"content":"Easy =\u0026gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^= true; oddCols[item[1]] ^= true; } int oddCnt = 0; for(int i = 0; i \u0026lt; n; i++) { for(int j = 0; j \u0026lt; m; j++) { // 行列出现 奇数次 + 偶数次 才能是产生奇数 oddCnt += oddRows[i] != oddCols[j] ? 1 : 0; } } // Time Complexity: O(m*n + indices.length) return oddCnt; } EASY =\u0026gt;26. Remove Duplicates from Sorted Array 快慢指针的运用\npublic int removeDuplicates(int[] nums) { int slow = 0; int fast = 1; while(fast \u0026lt; nums.length) { if(nums[slow] != nums[fast]) { nums[++slow] = nums[fast++]; } else { fast++; } } return slow + 1; } EASY =\u0026gt; 27. Remove Element 快慢指针的运用\npublic int removeElement(int[] nums, int val) { int curr = 0; int p = 0; while(p \u0026lt; nums.length) { if(nums[p] == val ) { p++; } else { nums[curr++] = nums[p++]; } } return curr; }\t","id":1,"section":"posts","summary":"","tags":["leetcode"],"title":"「LeetCode」数组题解","uri":"https://xiaohei.im/hugo-theme-pure/2019/12/array/","year":"2019"},{"content":" Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。\n NIO: selector 模型,用一个线程监听多个连接的读写请求,减少线程资源的浪费.\nnetty 优点 使用 JDK 自带的NIO需要了解太多的概念,编程复杂,一不小心 bug 横飞 Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从 NIO 模型变身为 IO 模型 Netty 自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑 Netty 解决了 JDK 的很多包括空轮询在内的 Bug Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手 Netty 社区活跃,遇到问题随时邮件列表或者 issue Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大 Server端 // 负责服务端的启动 ServerBootstrap serverBootstrap = new ServerBootstrap(); // 负责接收新连接 NioEventLoopGroup boss = new NioEventLoopGroup(); // 负责读取数据及业务逻辑处理 NioEventLoopGroup worker = new NioEventLoopGroup(); serverBootstrap.group(boss, worker) // 指定服务端 IO 模型为 NIO .channel(NioServerSocketChannel.class) // 业务逻辑处理 .childHandler(new ChannelInitializer\u0026lt;NioSocketChannel\u0026gt;() { protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler\u0026lt;String\u0026gt;() { protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception { System.out.println(s); } }); } }) .bind(8000); NioSocketChannel/NioServerSocketChannel Netty 对 NIO 类型连接的抽象 handler() \u0026amp; childHandler() handler() 用于指定服务器端在启动过程中的一些逻辑 childHandler() 用于指定处理新连接数据的读写逻辑 attr() \u0026amp; childAttr() 分别可以给服务端连接,客户端连接指定相应的属性,后续通过 channel.attr() 可以拿到.\noption() \u0026amp; childOption() option() 用于给服务端连接设定一系列的属性,最常见的是 so_backlog\n// 表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024) childOption() 给每条连接设置一些属性\nserverBootstrap // 是否开启TCP底层心跳机制,true为开启 .childOption(ChannelOption.SO_KEEPALIVE, true) // 是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。 .childOption(ChannelOption.TCP_NODELAY, true) Client端 带连接失败重试的客户端,失败重试延迟为 2 的幂次.\n// 客户端启动 Bootstrap bootstrap = new Bootstrap(); // 线程模型 NioEventLoopGroup group = new NioEventLoopGroup(); bootstrap.group(group) // 指定 IO 模型为 NIO .channel(NioSocketChannel.class) // 业务逻辑处理 .handler(new ChannelInitializer\u0026lt;Channel\u0026gt;() { protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new StringEncoder()); } }); connect(bootstrap,\u0026quot;127.0.0.1\u0026quot;, 8000, MAX_RETRY); private static void connect(Bootstrap bootstrap, String host, int port, int retry) { bootstrap.connect(host, port).addListener(future -\u0026gt; { if (future.isSuccess()) { System.out.println(\u0026quot;连接成功!\u0026quot;); } else if (retry == 0) { System.err.println(\u0026quot;重试次数已用完,放弃连接!\u0026quot;); } else { // 第几次重连 int order = (MAX_RETRY - retry) + 1; // 本次重连的间隔 int delay = 1 \u0026lt;\u0026lt; order; System.err.println(new Date() + \u0026quot;: 连接失败,第\u0026quot; + order + \u0026quot;次重连……\u0026quot;); bootstrap.config().group().schedule(() -\u0026gt; connect(bootstrap, host, port, retry - 1), delay, TimeUnit .SECONDS); } }); } 其他方法 attr() 客户端绑定属性 option() 设置客户端 TCP 连接 ByteBuf netty 中的数据都是以 ByteBuf 为单位的,所有需要写出的数据都必须塞到 ByteBuf 中.\n ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃的字节,这部分数据是无效的;第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;最后一部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段。最后一部分虚线表示的是该 ByteBuf 最多还能扩容多少容量 以上三段内容是被两个指针给划分出来的,从左到右,依次是读指针(readerIndex)、写指针(writerIndex),然后还有一个变量 capacity,表示 ByteBuf 底层内存的总容量 从 ByteBuf 中每读取一个字节,readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读, 由此可以推论出当 readerIndex 与 writerIndex 相等的时候,ByteBuf 不可读 写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了 ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,如果容量不足,那么这个时候可以进行扩容,直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错 ByteBuf 容量相关API capacity() 表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实现机制有不同的计算方式,后面我们讲 ByteBuf 的分类的时候会讲到\nmaxCapacity() 表示 ByteBuf 底层最大能够占用多少字节的内存,当向 ByteBuf 中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常\nreadableBytes() 与 isReadable() readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于 writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false\nwritableBytes()、 isWritable() 与 maxWritableBytes() writableBytes() 表示 ByteBuf 当前可写的字节数,它的值等于 capacity-writerIndex,如果两者相等,则表示不可写,isWritable() 返回 false,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往 ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于 maxCapacity-writerIndex\nByteBuf 读写指针相关 API readerIndex() 与 readerIndex(int) 前者表示返回当前的读指针 readerIndex, 后者表示设置读指针\nwriteIndex() 与 writeIndex(int) 前者表示返回当前的写指针 writerIndex, 后者表示设置写指针\nmarkReaderIndex() 与 resetReaderIndex() 前者表示把当前的读指针保存起来,后者表示把当前的读指针恢复到之前保存的值\nmarkWriterIndex() 与 resetWriterIndex() 同上,但是针对写指针\nByteBuf 读写 API writeBytes(byte[] src) 与 buffer.readBytes(byte[] dst) writeBytes() 表示把字节数组 src 里面的数据全部写到 ByteBuf,而 readBytes() 指的是把 ByteBuf 里面的数据全部读取到 dst,这里 dst 字节数组的大小通常等于 readableBytes(),而 src 字节数组大小的长度通常小于等于 writableBytes()\nwriteByte(byte b) 与 buffer.readByte() writeByte() 表示往 ByteBuf 中写一个字节,而 buffer.readByte() 表示从 ByteBuf 中读取一个字节,类似的 API 还有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble()\n与读写 API 类似的 API 还有 getBytes、getByte() 与 setBytes()、setByte() 系列,唯一的区别就是 get/set 不会改变读写指针,而 read/write 会改变读写指针,这点在解析数据的时候千万要注意\nrelease() 与 retain() 由于 Netty 使用了 堆外内存,而堆外内存是不被 jvm 直接管理的,也就是说申请到的内存无法被垃圾回收器直接回收,所以需要我们手动回收。有点类似于c语言里面,申请到的内存必须手工释放,否则会造成内存泄漏。\nNetty 的 ByteBuf 是通过引用计数的方式管理的,如果一个 ByteBuf 没有地方被引用到,需要回收底层内存。默认情况下,当创建完一个 ByteBuf,它的引用为1,然后每次调用 retain() 方法, 它的引用就加一, release() 方法原理是将引用计数减一,减完之后如果发现引用计数为0,则直接回收 ByteBuf 底层的内存。\nslice()、duplicate()、copy() 这三个方法通常情况会放到一起比较,这三者的返回值都是一个新的 ByteBuf 对象\n slice() 方法从原始 ByteBuf 中截取一段,这段数据是从 readerIndex 到 writeIndex,同时,返回的新的 ByteBuf 的最大容量 maxCapacity 为原始 ByteBuf 的 readableBytes() duplicate() 方法把整个 ByteBuf 都截取出来,包括所有的数据,指针信息 slice() 方法与 duplicate() 方法的相同点是:底层内存以及引用计数与原始的 ByteBuf 共享,也就是说经过 slice() 或者 duplicate() 返回的 ByteBuf 调用 write 系列方法都会影响到 原始的 ByteBuf,但是它们都维持着与原始 ByteBuf 相同的内存引用计数和不同的读写指针 slice() 方法与 duplicate() 不同点就是:slice() 只截取从 readerIndex 到 writerIndex 之间的数据,它返回的 ByteBuf 的最大容量被限制到 原始 ByteBuf 的 readableBytes(), 而 duplicate() 是把整个 ByteBuf 都与原始的 ByteBuf 共享 slice() 方法与 duplicate() 方法不会拷贝数据,它们只是通过改变读写指针来改变读写的行为,而最后一个方法 copy() 会直接从原始的 ByteBuf 中拷贝所有的信息,包括读写指针以及底层对应的数据,因此,往 copy() 返回的 ByteBuf 中写数据不会影响到原始的 ByteBuf slice() 和 duplicate() 不会改变 ByteBuf 的引用计数,所以原始的 ByteBuf 调用 release() 之后发现引用计数为零,就开始释放内存,调用这两个方法返回的 ByteBuf 也会被释放,这个时候如果再对它们进行读写,就会报错。因此,我们可以通过调用一次 retain() 方法 来增加引用,表示它们对应的底层的内存多了一次引用,引用计数为2,在释放内存的时候,需要调用两次 release() 方法,将引用计数降到零,才会释放内存 这三个方法均维护着自己的读写指针,与原始的 ByteBuf 的读写指针无关,相互之间不受影响 Pipeline \u0026amp; ChannelHandler pipeline 的数据结构为 双向链表, 节点的类型是一个 ChannelHandlerContext 包含着 每一个 channel 的上下文信息, contenxt 中包裹着一个 handler 用于处理用户的逻辑,pipeline 利用 责任链 的模式执行完所有的 handler.\n内置的 ChannelHandler ByteToMessageDecoder 二进制 -\u0026gt; Java 对象转换,重写 decode 方法即可.默认情况下 ByteBuf 使用的是对外内存,通过引用计数判断是否需要清除.而该 Decoder 可以自动释放内存无需关心.\n SimpleChannelInboundHandler 自动选择对应的消息进行处理,自动传递对象\n MessageToByteEncoder 对象 -\u0026gt; 二进制\n粘包 \u0026amp; 拆包 https://www.cnblogs.com/wade-luffy/p/6165671.html\n TCP 的传输是基于字节流的,没有明显的分界,有可能会把应用层的多个包合在一块发出去(粘包),有可能把一个过大的包分多次发出(拆包),粘包/拆包是相对的,一方拆包,一方就要粘包.\nTCP粘包/拆包发生的原因 问题产生的原因有三个,分别如下。\n(1)应用程序write写入的字节大小大于套接口发送缓冲区大小;\n(2)进行MSS大小的TCP分段;\n(3)以太网帧的payload大于MTU进行IP分片。\n解决策略 通过应用层设计通用的结构保证.\n 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格; 在包尾增加回车换行符进行分割,例如FTP协议; 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度; 更复杂的应用层协议 netty 解决方案 netty 提供了多种拆包器,满足用户的需求,不需要自己来对 TCP 流进行处理.\n 固定长度拆包器 FixedLengthFrameDecoder\n 行拆包器 LineBasedFrameDecoder\n 数据包以换行符作为分隔.\n 分隔符拆包器 DelimiterBasedFrameDecoder 行拆包器的通用版,自定义分隔符\n 基于长度域拆包器 LengthFieldBasedFrameDecoder 自定义的协议中包含长度域字段,即可使用来拆包\n 每次的包不是定长的,怎么就能通过位移确认长度域,进而确定长度?\n答: 通过设置一个完整包的开始标志,确定是一个新包就可以了.比如通常会设置一个魔数,拆包前先判断是不是我们定义的包.然后再去通过位移定位到长度域.\n ChannelHandler 生命周期 handlerAdded() :指的是当检测到新连接之后,调用 ch.pipeline().addLast(new xxxHandler()); 之后的回调,表示在当前的 channel 中,已经成功添加了一个 handler 处理器。 channelRegistered():这个回调方法,表示当前的 channel 的所有的逻辑处理已经和某个 NIO 线程建立了绑定关系,accept 到新的连接,然后创建一个线程来处理这条连接的读写,Netty 里面是使用了线程池的方式,只需要从线程池里面去抓一个线程绑定在这个 channel 上即可,这里的 NIO 线程通常指的是 NioEventLoop,不理解没关系,后面我们还会讲到。 channelActive():当 channel 的所有的业务逻辑链准备完毕(也就是说 channel 的 pipeline 中已经添加完所有的 handler)以及绑定好一个 NIO 线程之后,这条连接算是真正激活了,接下来就会回调到此方法。 channelRead():客户端向服务端发来数据,每次都会回调此方法,表示有数据可读。 channelReadComplete():服务端每次读完一次完整的数据之后,回调该方法,表示数据读取完毕。 channelInactive(): 表面这条连接已经被关闭了,这条连接在 TCP 层面已经不再是 ESTABLISH 状态了 channelUnregistered(): 既然连接已经被关闭,那么与这条连接绑定的线程就不需要对这条连接负责了,这个回调就表明与这条连接对应的 NIO 线程移除掉对这条连接的处理 handlerRemoved():最后,我们给这条连接上添加的所有的业务逻辑处理器都给移除掉。 心跳 \u0026amp; 空闲检测 IdleStateHandler 空闲检测(一段时间内是否有读写).\n实现一个心跳 public class HeartBeatTimerHandler extends ChannelInboundHandlerAdapter { private static final int HEARTBEAT_INTERVAL = 5; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { scheduleSendHeartBeat(ctx); super.channelActive(ctx); } private void scheduleSendHeartBeat(ChannelHandlerContext ctx) { ctx.executor().schedule(() -\u0026gt; { if (ctx.channel().isActive()) { ctx.writeAndFlush(new HeartBeatRequestPacket()); scheduleSendHeartBeat(ctx); } }, HEARTBEAT_INTERVAL, TimeUnit.SECONDS); } } 性能优化方案 共享 handler @ChannelHandler.Sharable 压缩 handler - 合并编解码器 —— MessageToMessageCodec 虽然有状态的 handler 不能搞单例,但是你可以绑定到 channel 属性上,强行单例 缩短事件传播路径—— 放 Map 里,在第一个 handler 里根据指令来找具体 handler。 更改事件传播源—— 用 ctx.writeAndFlush() 不要用 ctx.channel().writeAndFlush() 减少阻塞主线程的操作—— 使用业务线程池,RPC 优化重点 计算耗时,使用回调 Future ","id":2,"section":"posts","summary":"","tags":["netty"],"title":"[学习笔记] Netty","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/netty/","year":"2019"},{"content":"Redis 官方高可用(HA)方案之一: Cluster.可以解决 sentinel 模式单点写入的问题.\n参考 https://juejin.im/post/5b8fc5536fb9a05d2d01fb11 http://www.redis.cn/topics/cluster-spec.html https://redis.io/topics/cluster-spec 玩玩集群 https://redis.io/topics/cluster-tutorial\n如果使用源码构建的,utils 目录下有一个脚本可以创建集群试玩.\n\nRedis Cluster 实现原理 一致性Hash算法 一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 \\(K/n\\) 个关键字重新映射,其中\\(K\\)是关键字的数量,\\(n\\)是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。\n-- 来自自由的百科全书\n 一致性 Hash 算法在很多领域都有实践,分布式缓存 Redis, 负载均衡 Nginx,一些 RPC 框架.一句话解释这个算法就是将请求均匀的分配给各个节点的算法.在 Redis 中就是对 key 的离散化,将其存到不同的节点上,在 Nginx 中就是讲请求离散化,均匀地达到不同的机器上,相同的请求始终可以打到同一台上.毕竟取模以后值又不会变,总是会到相同的一台嘛.\n一致性哈希 可以很好的解决 稳定性问题,可以将所有的 存储节点 排列在 收尾相接 的 Hash 环上,每个 key 在计算 Hash 后会 顺时针 找到 临接 的 存储节点 存放。而当有节点 加入 或 退出 时,仅影响该节点在 Hash 环上 顺时针相邻 的 后续节点。\n普通模式 \n 优点 加入 和 删除 节点只影响 哈希环 中 顺时针方向 的 相邻的节点,对其他节点无影响。\n 缺点 加减节点 会造成 哈希环 中部分数据 无法命中。当使用 少量节点 时,节点变化 将大范围影响 哈希环 中 数据映射,不适合 少量数据节点 的分布式方案。普通 的 一致性哈希分区 在增减节点时需要 增加一倍 或 减去一半 节点才能保证 数据 和 负载的均衡。\n虚拟槽 虚拟槽分区 巧妙地使用了 哈希空间,使用 分散度良好 的 哈希函数 把所有数据 映射 到一个 固定范围 的 整数集合 中,整数定义为 槽(slot)。这个范围一般 远远大于 节点数,比如 Redis Cluster 槽范围是 0 ~ 16383。槽 是集群内 数据管理 和 迁移 的 基本单位。采用 大范围槽 的主要目的是为了方便 数据拆分 和 集群扩展。每个节点会负责 一定数量的槽,如图所示:\n\n为什么是 16384 个槽? redis github 上有个对应的 issue, antirez 给了对应的回答.回答如下:\n\n总结下来就是避免节点之间交换消息时消息包过大.每个消息包都会通过 bitmap 存储当前节点的 slots 分配信息,slots = 16384 时占用 16384/8/1024 = 2KB. 65K slots就太大了,而且官方建议最好不要超过 1000 个节点,16k slots也就足够分配了.够用就行.\n为什么要提到 65K? \nRedis 实现的 CRC16 算法是 16 位的,最大值就是 65535,以这个值来计算 bitmap 就是 8K 左右.\n中文参考:https://www.cnblogs.com/rjzheng/p/11430592.html\n官方集群文档 Redis Cluster Bus: 节点的 TCP 通信及二进制协议的总称.所有节点通过 cluster bus 进行连接.还可以在集群中 传递 Pub/Sub 消息,处理手动的故障转移请求(用户执行).\nGossip: 通过 Gossip 协议传递集群消息保证集群每个节点最终都能获得所有节点的完整信息.\n客户端不需要在意请求到哪个节点,随机请求一个后,如果没有查到对应的key,会通过返回重定向的结果 -MOVED,-ASK 来重定向到真正含有请求key数据的节点上.\n可用性 Availability 出现网络分区时, cluster 在少数节点侧分区是不可用的.在多数节点的分区侧(假设至少有半数的节点且存在每个不可用的主节点的 slave ),集群会在 NODE_TIMEOUT + 选举及故障转移所需一定时间 后恢复.\nRedis Cluster 核心组件 键的分布式模型 Keys distribution model 键空间分割为 16384 个 slot, 也就是集群可以最多有 16384 个节点.(官方建议上线 1000 节点为佳)\n每个 master 节点维护一段 slot.每一个主节点可以有多个 slave 来应对网络分区或者故障转移时的问题,以及分担读的压力.\n核心算法: 将key进行hash取模映射到一个 slot 上. \\( HASH\\_SLOT = CRC16(key)\\ mod\\ 16384 \\)\nCRC16 在测试中针对不同的key能很好的离散化.效果显著.\n集群拓补 Redis Cluster 的节点连接是网状的,假设有 N 个节点,那么每个节点都会与 N-1 个节点建立 TCP 连接, 且每个节点需要接受 N-1 个外来的 TCP 连接.所有连接都是 keep alive.如果等待足够长时间没有得到对方的 PING 回复,就会尝试重连.由于连接呈网状的原因,节点使用的是 Gossip 协议来传递消息,更新节点信息,可以避免节点之间同时交换巨量的消息,防止消息的指数型增长.\n重定向\u0026amp;重新分片 MOVED Redirection 客户端可以向任意一个节点发送查询请求,包括 slave 节点.节点会对查询请求进行分析,如果该 key 就在当前节点,就直接查出来返回,如果不在,节点会去寻找该 key 对应的 slot 所属的节点是哪一个,然后返回给客户端一个 MOVED error.\nGET x -MOVED 3999 127.0.0.1:6381 该 error 包括了 key 所在的 slot,以及对应节点的 ip:port.客户端就可以重新对真正持有该 key 的节点发起查询请求.如果在发起请求前经过了很长的时间导致集群产生了重新配置(reconfiguration),客户端再发起请求后可能仍然没有拿到值,还是会收到一个 MOVED 响应,如此循环下去.\nCluster live reconfiguration Redis 集群是允许运行时增删节点的.增删节点的影响就是对 hash slot 的调整.增加一个节点就需要把现有的节点匀出来一部分给新节点,删除一个节点就要把该节点的 slot 合并给其他节点.\n核心的逻辑其实就是对 hash slots 的移动.从一个特殊的角度来看,移动 slot 就是移动一组 key,所以集群在 resharding 时真正做的其实是对 key 的移动.移动一个 hash slot 就是对该 slot 下的所有 keys 移动到另一个 slot 下.\nCluster Slot 相关命令 CLUSTER ADDSLOTS slot1 [slot2] ... [slotN] CLUSTER DELSLOTS slot1 [slot2] ... [slotN] CLUSTER SETSLOT slot NODE node CLUSTER SETSLOT slot MIGRATING node CLUSTER SETSLOT slot IMPORTING node ADDSLOTS, DELSLOTS 用于给节点分配/删除指定的 slots.分配后会通过 Gossip 广播该信息.ADDSLOTS 通常用在集群新建时为每一个 master 节点分配一部分 slot. DELSLOTS 主要用于手动设置集群配置或者用于 debug 时的操作.通常很少用.\nSETSLOT \u0026lt;slot\u0026gt; NODE 使用该命令就是给一个节点分配指定的 slot.\n否则就是需要设置 MIGRATING 和 IMPORTING 的命令了.这两个特殊的状态是为了将一个 slot 从一个节点迁移到另一个时使用的.\n 设置为 MIGRATING 时,节点会接受所有关于该 slot 的查询,但仅当 key 存在时,否则会返回一个 -ASK 的重定向转发到需要迁移到的目标节点. 设置为 IMPORTING 时,节点只接受带有 ASKING 的请求,如果客户端没有携带该命令,就会重定向到原来的节点去. 假设我们需要将节点A的 slot 8 迁移到节点B.那我们需要发送两条命令:\n 给B发送 : CLUSTER SETSLOT 8 IMPORTING A 给A发送: CLUSTER SETSLOT 8 MIGRATING B 如此操作后,客户端还是会对key存在于 SLOT 8 的请求给到 A 节点,当该 key 在节点A存在时返回,不存在时会让客户端 ASKING Node B 处理.\n此时不会再在节点 A 中创建新 key 了.同时, redis-trib 会执行对应的迁移操作.\nCLUSTER GETKEYSINSLOT slot count 上述命令会查询出指定 slot 下 count 个需要迁移的key.并对每一个key 执行 migrate 命令,将 key 从 A 迁移到 B.该操作为原子操作.\nMIGRATE target_host target_port key target_database id timeout migrate 对复杂键也进行了优化,迁移延迟较低,但是在集群中 big key 并不是一个明智的选择.\nASK redirection ASK 与 MOVED 的区别在于, MOVED 可以确定 slot 的确在其他的节点上,下一次查询就直接查重定向后的节点.而 ASK 只是将本次查询重定向到指定的节点,接下来的其他查询仍然要请求当前的节点.\n语义:\n 如果收到一个 ASK 重定向,仅将当前查询重定向到指定节点,后续查询仍指向当前节点. 使用 ASKING 进行重定向查询 还不能更新本地客户端的 slot -\u0026gt; node 缓存映射关系 当 slot 迁移完成后,节点 A 会发送 MOVED 消息,客户端就可以永久的将 slot 8 的请求指定到新的 ip:port.\n容错 Fault Tolerance 心跳 \u0026amp; Gossip Redis Cluster 节点会持续的交换 ping/pong 信息,两种信息没有本质区别,就是 message type 不同.下文我们统称 ping/pong 为 心跳包 (heartbeat packets)\n通常来说,PING一下就要触发一次 PONG 回复.但也并不全对,节点也可能就把 PONG 信息(包含了自己的配置)发给其他节点就不管了,这样的好处是可以尽快广播新的配置.\n通常,一个节点每秒会随机的 PING 几个节点,所以每个节点发出PING 包的数量是恒定的(收到 PONG 包的数量也是恒定的) ,而不去理会节点的数量了. 去中心化\n每个节点会确保给没有 PING 过的节点和超过一半 NODE_TIMEOT 没有收到 PONG 的节点发送 PING 消息.NODE_TIMEOUT 过后,节点还会尝试重连没响应的节点,确保是因为网络问题才不可达的.\n如果将 NODE_TIMEOUT 设置为较小的数字,并且节点数非常大,则全局交换的消息数会相当大,因为每个节点都将尝试对超过一半 NODE_TIMEOUT 还未刷新信息的其他节点发送 PING.\n心跳包结构 结构如下,源码注释就很详细了:\ntypedef struct { char sig[4]; /* Signature \u0026quot;RCmb\u0026quot; (Redis Cluster message bus). */ uint32_t totlen; /* Total length of this message */ uint16_t ver; /* Protocol version, currently set to 1. */ uint16_t port; /* TCP base port number. */ uint16_t type; /* Message type */ uint16_t count; /* Only used for some kind of messages. */ uint64_t currentEpoch; /* The epoch accordingly to the sending node. */ uint64_t configEpoch; /* The config epoch if it's a master, or the last epoch advertised by its master if it is a slave. */ uint64_t offset; /* Master replication offset if node is a master or processed replication offset if node is a slave. */ char sender[CLUSTER_NAMELEN]; /* Name of the sender node */ unsigned char myslots[CLUSTER_SLOTS/8]; char slaveof[CLUSTER_NAMELEN]; char myip[NET_IP_STR_LEN]; /* Sender IP, if not all zeroed. */ char notused1[34]; /* 34 bytes reserved for future usage. */ uint16_t cport; /* Sender TCP cluster bus port */ uint16_t flags; /* Sender node flags */ unsigned char state; /* Cluster state from the POV of the sender */ unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */ union clusterMsgData data; } clusterMsg; typedef struct { char nodename[CLUSTER_NAMELEN]; uint32_t ping_sent; uint32_t pong_received; char ip[NET_IP_STR_LEN]; /* IP address last time it was seen */ uint16_t port; /* base port last time it was seen */ uint16_t cport; /* cluster port last time it was seen */ uint16_t flags; /* node-\u0026gt;flags copy */ uint32_t notused1; } clusterMsgDataGossip; ","id":3,"section":"posts","summary":"\u003cp\u003eRedis 官方高可用(HA)方案之一: \u003cstrong\u003eCluster\u003c/strong\u003e.可以解决 \u003ccode\u003esentinel\u003c/code\u003e 模式单点写入的问题.\u003c/p\u003e","tags":["redis"],"title":"Redis HA - Cluster","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/cluster/","year":"2019"},{"content":"Redis 官方高可用(HA)方案之一: 哨兵模式\n这篇文章已经介绍的很全面了:https://juejin.im/post/5b7d226a6fb9a01a1e01ff64 自己就总结一些问题:\nsentinel 如何保证集群高可用? 时刻与监控的节点保持心跳(PING),订阅 __sentinel__:hello 频道实时更新配置并持久化到磁盘 自动发现监听节点的其他 sentinel 保持通信 节点不可达时询问其他节点确认是否不可达,是否需要执行故障转移(半数投票) 故障转移后广播配置,帮助其他从节点切换到新的主节点,以 epoch 最大的配置为准 sentinel 如何判定节点下线? 主观下线/客观下线\nsentinel 的局限性? Redis Sentinel 仅仅解决了 高可用 的问题,对于 主节点 单点写入和单节点无法扩容等问题,还需要引入 Redis Cluster 集群模式 予以解决。\n官方文档介绍 https://redis.io/topics/sentinel\n使用 sentinel 的原因: 做到无人工介入的自动容错 redis 集群.\n sentinel 宏观概览:\n 监控 Monitoring: 持续监控主从节点的运行状态 通知 Notification: 节点异常时,通过暴露的 API 可以及时报警 自动故障转移 Automatic Failover: 主节点宕机后,可以自动晋升从节点为新主节点,其他节点会重新连接到新主节点,应用也会被通知相应的节点变化 提供配置 Configuration Provider: sentinel 维护着主从的节点信息,客户端会连接sentinel 获取主节点信息. sentinel 天生分布式,多节点协同的好处在于:\n 多数节点都同意主节点不可用时才执行故障检测.有效避免错判. 多节点可以提升系统鲁棒性(system robust),避免单点故障 使用须知 至少 3 个 sentinel 保证系统鲁棒性 节点最好放在不同的主机或虚拟机,降低级联故障(一下全GG) 由于 redis 采用的是异步复制,sentinel + redis 不能保证故障期间确认的写入(主从可能无法通信,确认复制进度).sentinel 可以在发布时控制一定时间内数据不丢失,但也不是万全之策. 客户端需要支持 sentinel (常用 Java 客户端基本都支持) 高可用并不是百分之百有效,即时你时时刻刻都在测试,产线环境也在跑,保不准凌晨就 GG,也没办法不是. Sentinel,Docker,或者其他形式的网络地址交换或端口映射需要加倍小心:Docker执行端口重新映射,破坏Sentinel自动发现其他的哨兵进程和主节点的 replicas 列表。 Sentinel 设置 redis 安装目录下有一个 sentinel.conf 模板配置可以参考.最小化配置如下:\nsentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1 sentinel monitor resque 192.168.1.3 6380 4 sentinel down-after-milliseconds resque 10000 sentinel failover-timeout resque 180000 sentinel parallel-syncs resque 5 不需要配置 replicas , sentinel 可以自动从主节点中获取 INFO 信息.同时该配置也会实时重写的: 新的 sentinel 节点加入或者故障转移 replica 晋升时.\nsentinel monitor \u0026lt;master-group-name\u0026gt; \u0026lt;ip\u0026gt; \u0026lt;port\u0026gt; \u0026lt;quorum\u0026gt; 从命令就可以看出来一些名堂: 监控地址为 ip:port,name 为 master-group-name 的主节点.\nquorum: 判断节点确实已经下线的支持票数(由 Sentinel 节点进行投票),票数超过一定范围后就可以让节点下线并作故障转移.但 quorum 只是针对于下线判断,执行故障转移需要在 sentinel 集群选举(投票)出一个 leader 来执行故障转移.\ne.g. quorum = 2, sentinel 节点数 = 5\n 如果有两个 sentinel 节点认为主节点下线了,那么这两个节点中的一个会尝试开始执行故障转移. 如果有超过半数 sentinel 节点存在(当前情况下即活着 3 个 sentinel 节点),故障转移就会被授权真正开始执行. 核心概念: sentinel 节点半数不可达就不允许执行 故障转移.\nsentinel \u0026lt;option_name\u0026gt; \u0026lt;master_name\u0026gt; \u0026lt;option_value\u0026gt; sentinel 其它的配置基本都是这个格式.\n down-after-milliseconds 节点宕机超过该毫秒时间后 sentinel 节点才能认为其不可达. parallel-syncs 在发生failover主从切换时,这个选项指定了最多可以有多少个 replica 同时对新的master 进行同步,这个数字越小,完成主从故障转移所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为主从同步而不可用。可以通过将这个值设为1来保证每次只有一个 replica 处于不能处理命令请求的状态。 所有配置都可以通过 SENTINEL SET 热更新.\n添加/删除 sentinel 节点 添加: 启动一个新的 sentinel 即可.10s就可以获得其他 sentinel 节点以及主节点的 replicas 信息了.\n多节点添加:建议 one by one,等到当前节点添加进集群后,再添加下一个.添加节点过程中可能会出故障.\n删除节点: sentinel 节点不会丢失见过 sentinel 节点信息,即使这些节点已经挂了.所以需要在没有网络分区的情况下做以下几步:\n 终止你想要关掉的 sentinel 节点进程 发送一条命令 SENTINEL RESET * 给所有 sentinel 节点.如果只想对单一 master 处理,把 * 换成主节点名称.等一会儿~ 通过 SENTINEL MASTER 命令查看节点是否已删除 主观下线/客观下线 sentinel 中有两种下线状态.\n 主观下线(Subjectively Down) aka. SDOWN 当前 sentinel 认为自己监控的节点下线了,即主观下线.SDOWN 判定的条件为: sentinel 节点向监控节点发送 PING 命令在设置的 is-master-down-after-milliseconds 毫秒后没有收到有效回复则判定为 SDOWN\n 客观下线(Objectively Down) aka. ODOWN 有 quorum 数量的 sentinel 节点认为监控的节点 SDOWN.当一个 sentinel 节点认为监控的节点 SDOWN 后,会向其它节点发送 SENTINEL is-master-down-by-addr 命令来判断其它节点对该节点的监控状态.如果回执为 已下线 的节点数+自身大于 quorum 数量,则判定为 客观下线\nPING 命令的有效回复有什么?\n +PONG -LOADING error -MASTERDOWN error 其它回复都是无效的.需要注意的是: 只要收到有效回复就不会认为其 SDOWN 了.\nSDOWN 并不能触发故障转移,只能判定节点不可用.要触发故障转移,必须达到 ODOWN 状态.\nSDOWN -\u0026gt; ODWN? sentinel 没有使用强一致性的算法来保证 SDOWN -\u0026gt; ODOWN 的转换,而是使用的Gossip协议来保证最终一致性.在给定的时间范围内,给定的 sentinel 节点收到了足够多(quorum)的其它 sentinel 节点的 SDOWN 确认,就会从 SDOWN 切换到 ODOWN 了.\n真正执行故障转移时会有比较严格的授权,但是前提也得是 ODOWN 状态才行.ODOWN 只针对 master 节点,replicas 和 sentinels 只会有 SDOWN 状态.如果 replica 变为 SDOWN 了,在故障转移的时候就不会被晋升.\n自动发现 auto discovery sentinel 节点之间会保持连接来互相检查是否可用,交换信息,但是并不需要在启动的时候配置一长串其他 sentinel 节点的地址. sentinel 会利用 redis 的 Pub/Sub 能力来发现监控了相同 master/replicas 的 sentinel 节点.replicas 自动发现是一样的原理.\n如何实现的? 向一个叫 __sentinel__:hello 的 channel 发送 hello 消息.\n 每个 sentinel 节点都会向每一个它监控的 master 和 replica 的叫做 __sentinel__:hello 的Pub/Sub channel 广播自己的 ip,port,runid.2s 一次. 每个订阅了 master 和 replica 的 sentinel 都会收到消息,并会去判断有新的 sentinel 节点就会被添加进来. 这个 hello 消息同样包含着最新的 master 全量配置,每个收到消息的 sentinel 会进行比对更新. 添加新的 sentinel 节点时会提前判断该节点信息是否已经存在. sentinel 强制更新配置 sentinel 是一个总会尝试将当前最新的配置强制更新到所有监控节点的系统.\n 这可能也是一种 tradeoff 吧.比如 replica 如果连错 master 了,那 sentinel 就必须把它矫正过来,重连正确的master.\n 副本选举 sentinel 可以执行故障转移时,需要选择一个合适的 replica 晋升.\n评估条件 与 master 的断连时间 replica 优先级-\u0026gt;可以设置 复制进度 offset Run ID 判定需要跳过的节点 (down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state 如果一个 replica 的断连时间超过上面这个表达式,那就认为该节点不可靠,不考虑. down-after-milliseconds 是通过设置的,milliseconds_since_master_is_in_SDOWN_state 指在执行故障转移时 master 仍不可用的时间.\n选举过程 符合上述条件后才会对其按照条件进行排序.顺序如下:\n 首先根据 replica-priority 排序(redis.conf 进行设置),值越小越优先 priority 相同时,比较 offset,值越大越优先(同步最完整) 如果 priority,offset 都相同,就会判断 run ID 的字典序.越小的 run ID 并不是说有什么优势,但是比起重排序随机选一个 replica,字典序选举方式更有确定性更有用(大白话). 建议所有节点都设置 replica-priority.如果 replica-priority 设置为 0, 表示永远不会被选为 master .但是在故障转移后 sentinel 会重置通过这种方式设置的配置,以便可以与新的 master 连接,唯一的区别就是该节点不会是主节点.\n深入算法内部 Quorum quorum 参数会被 sentinel 集群用来判断是否有这个数量的 sentinel 节点认为 master 已经 SDOWN 了,需不需要转为 ODOWN 触发故障转移 failover.\n但是,触发故障转移后,至少需要有半数的 sentinel 节点(如果 quorum 值比半数还多,那其实需要有quorum个节点)授权给一个 sentinel 节点才能真正执行.小于半数节点不允许执行.\n e.g. 5 instances quorum = 2\n当有2个节点认为 master 不可达时,就会触发 failover.但是需要有至少3个节点授权给这2个节点之一才能真正执行failover.\n如果 quorum = 5,那就需要所有节点都认为 master 不可达,才能触发failover,并且所有节点都要授权.\n 纪元 Configuration Epochs 为什么需要获取半数以上的授权执行 failover?\n当一个 sentinel 节点被授权后,会获得一个可以用于故障转移节点的唯一的纪元(configuration epoch)标志.这是一个在故障转移完成后针对新配置的版本号 number.因为是多数同意将指定的版本分配给指定授权的 sentinel ,所以不会有其他节点使用这个版本号.也就意味着每一次故障转移时生成的新配置都有唯一的版本号标识.\nsentinel 集群有一条规则: 如果 sentinel A 投票给 sentinel B 去执行故障转移,A 会等待一段时间后对同一个主节点再次进行故障转移.这个时间可以通过 sentinel.conf 的 failover-timeout 进行配置.这就意味着不会有节点在同一时间对同一个主节点进行故障转移,被授权的节点回先执行,失败了后面会有其他的节点进行重试.\nsentinel 保证 liveness 特性(我的理解就是不会宕机一直存活):如果有多个节点可用,只会选择一个节点去执行故障转移.\nsentinel 同样保证 safety 特性:每一个节点都会尝试使用不同的 configuration epoch 对相同的节点进行故障转移.\n配置传递 Configuration propagation 故障转移完成后,sentinel 会广播新的配置给其他 sentinel 节点更新这个新的主节点信息.执行故障转移的主节点还需要对新的主节点执行 SLAVE NO ONE,稍后在 INFO 命令中就可以看到这个主节点了.\n所有 sentinel 节点都会广播配置信息,通过 __sentinel__:hello channel 广播出去.配置信息都带有 epoch ,值越大越会被当做最新的配置.\n网络分区后的一致性问题 Redis + Sentinel 架构是保证最终一致性的系统,在发生网络分区恢复时,不可避免的会丢失数据.\n如果把 redis 当做缓存来用,数据丢了也没事,可以再去库里查嘛.\n如果把 redis 当做存储来用,那最好配上下面两个配置降低损失.\nmin-replicas-to-write 1 min-replicas-max-lag 10 Sentinel 状态持久化 Sentinel 状态持久化在 sentinel.conf 中,每次手挡新配置,或者创建配置,都会带着configuration epoch 一起持久化到硬盘,重启时就没有问题了.\n","id":4,"section":"posts","summary":"\u003cp\u003eRedis 官方高可用(HA)方案之一: \u003cstrong\u003e哨兵模式\u003c/strong\u003e\u003c/p\u003e","tags":["redis"],"title":"Redis HA - 哨兵模式","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/sentinel/","year":"2019"},{"content":"之前对redis 的复制只有一点点了解,这次想要搞明白的是:如何实现的复制? 复制会遇到哪些问题(时延/一致性保证/网络故障时的处理)? 如何解决?高可用实现方案?\n文章有部分是直接翻译的 https://redis.io/topics/replication\n复制是什么? 分布式系统有一个重要的点时保证数据不丢失,数据不丢失就意味着不能单点,不能单点就意味着最好能把数据多存几份形成数据的冗余.这就是复制的来由.复制类型主要是两种: 同步, 异步. 前者需要等待所有的节点返回写入确认,后者只需要返回个确认收到就行.\nRedis 主从复制 主从复制作用 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。 Redis 复制设计要点 默认使用异步复制 \u0026ndash; replica -\u0026gt; master 异步返回处理了多少数据的结果(偏移量)\n master 可以多 replicas\n replicas 可以从其他 replica 同步(从从复制).类似于级联更新的架构.\n master 主节点在同步时不会阻塞. 在 replica 侧复制也是非阻塞的.在进行初始化同步(全量)时,可以使用 replica上的旧数据供客户端查询.也可以在 redis.conf 进行配置,在初始化同步完成前客户端的请求都报错.初始化同步完成后,需要删除老数据,加载新数据.在这段时间中会阻塞外部连接请求,数据量大的话可能要很久.从 4.0 版本后,删除老数据可以通过多线程来优化效率,但是加载新数据还是会 阻塞. 复制可以用来弹性扩容,提供多可读副本,提升数据安全性,保证高可用 副本可以避免 master 保存全量数据到磁盘的资源消耗:可以由 replica 完成持久化,或者开启 aof 写入.不过需要慎重: 这样会导致 master 节点再重启时会是空的,其他 replica复制时也就成空的了. 主从复制过程 每一个 master 节点都会有一个特别大的随机数(40字节十六进制随机字符)作为 replication ID 来标识自己.每个 master 节点也有一个 持续递增的 offset 来记录发送给 replicas 的每一个 byte,利用该 offset 来保证副本更新的状态.\n当一个 replica 连接到 master 时,会使用 PSYNC 命令发送之前复制的 master 的 replicationID,以及自己的更新进度(offset).master 可以根据这个值给副本按需返回为更新的数据.如果在master 的 backlog buffer 中没有对应的数据可以给到,副本发送的 replicationID 与 master 的 ID 不一致,就会触发全量复制(Full Synchronization).\nbacklog buffer 是啥? 复制积压缓冲区,在 master 有 replica 进行复制时,存储 master 最近一段时间的写命令,以便在 replica 断开重连后,可以利用缓冲区更新断开这段时间中,从节点丢掉的更新.\nbacklog buffer 是有固定的长度,先进先出的队列,默认大小 1MB. 其实就是一个环.buffer 会存储每一个 offset 已经对应的写命令,这样 replica 在断连恢复后,发送 PSYNC 命令提供其最后一次更新的 offset, master 就可以根据 replica 提供的 offset 去 buffer 中找对应的数据发送给 replica 保持最新.\n如果断开时间过长,buffer 存储的数据已经换了一批又一批, replica 在重连后发送给 master 的 offset 在 buffer 已经找不到了.此时会触发 全量复制.\n全量复制 master调用 bgsave 在后台生成 rdb 文件.同时记录客户端新的写命令到 backlog buffer 中. rdb 文件生成后,发送给 replica 保存到其硬盘中,然后再加载到内存中并通知master 加载完成.然后 master 会发送 buffer pool 中的命令给 replica 完成最后的同步.\nSYNC/PSYNC 两者都是同步的命令.SYNC 只支持全量同步, PSYNC 支持上述的部分同步.2.8 版本之前只有 SYNC,为了避免每次都只能全量同步造成资源的浪费,就新增了 PSYNC 命令实现部分同步的语义.\nReplication ID Replication ID 标记了数据的历史信息,从0开始成为master 的节点,或者晋升成为 master 的 replica 节点,都会生成一个 Replication ID.replicas 的 replId 是和其复制的 master 一致的,master 通过该 ID 和 offset 来判断主从之间数据是否一致.\n为什么有两个replId? /* src/server.h */ struct redisServer { ... /* Replication (master) */ char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */ char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master*/ ... long long master_repl_offset; /* My current replication offset */ long long second_replid_offset; /* Accept offsets up to this for replid2. */ ... } 一般情况下,故障转移(failover)后,晋升的 replica 需要记录自己之前复制的 master 对应的 replId.其他 replicas 会向新 master 进行部分同步,但发送过来的 replId 还是之前 master 的.所以 replica 在晋升时,会生成新的replId,并将原来的 replId 记录到 replId2,同时记录下当时所更新到的 offset 到 second_replid_offset.当其他的 replica 向新 master 进行连接时,新 master 会比较当前的和之前 master 的 replId,offset,这样就可以防止在故障转移后导致不必要的 全量复制.\n为什么晋升后需要生成新 replId? old master 可能还存活,但由于网络分区原因无法和其他 replicas 通信,如果保留原来的 id 不再生成,就会导致有相同数据相同id的master 存在.\n无盘复制 全量复制时,master 会创建 rdb 文件存到磁盘,然后再读取 rdb 文件发送给 replicas.磁盘性能差的情况下,效率会很低,所以支持了 无盘复制 \u0026ndash; 子进程直接发送 rdb 给 replicas,不经过硬盘存储.\n如何处理可以过期的键? 副本不会主动去过期键,而是由 master 过期键后向副本发送 DEL 命令. 由于是通过 master 驱动,副本收到 DEL 命令可能有延迟,这就会导致从副本中还可能查到已过期的键.针对这种情况,副本会利用自身的物理时钟作为依据报告该键不存在(仅在不违反数据一致性的 只读操作),因为 DEL 命令总是会发过来的. LUA 脚本执行期间,是不会去执行 key 过期的.脚本执行期间相当于 master 时间冻结了,不作过期时间的记录,所以在这期间过期键只有存在或不存在的概念.这样可以防止键在执行期间过期.同时,master 也需要发送同样的脚本给副本,保持一致. 如果replica 晋升 master 了,它就会自己去处理键的过期了.\n心跳机制 在正常的进行 部分同步 期间,主从之间会维持心跳,来协助超时判断,数据安全等问题.\nmaster -\u0026gt; slave 主节点发送 PING ,从节点回复 PONG.目的是让从节点进行超时判断.发送频率有 repl-ping-slave-period 参数控制.单位秒,默认 10s.\nreplica -\u0026gt; master 从节点向主节点发送 REPLCONF ACK {offset} ,频率每秒1次.作用:\n 试试检测主从网络状态,该命令被主节点用于复制超时的判断. 检测命令丢失,主节点会比较从节点发送的 offset 与自身的是否一致,不一致则从 buffer 中查找对应数据进行补发,如果 buffer 中没有对应数据,则会进行全量复制. 辅助保证从节点的数量和延迟,master 通过 min-salves-to-write 和 min-slaves-max-lag 参数,来保证主节点在不安全情况下不会执行写命令.是指从节点数量太少,或延迟过高。例如 min-slaves-to-write 和min-slaves-max-lag 分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。 复制惨痛案例 数据过期问题 数据删除没有及时同步到从节点,其实在 3.2 版本后避免了这个问题.从节点会对键进行判断,已过期不展示.\n如何处理可以过期的键?\n数据延迟不一致 这种情况不可避免.可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟通过offset判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。\n复制超时导致复制中断 为什么要判断超时? master 在判断超时后,会释放从节点的连接,释放资源. 断开后即时重连 判断机制? 核心参数: repl-timeout ,默认 60s.\n(1)主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点 REPLCONF ACK 的时间,是否超过了 repl-timeout 值,如果超过了则释放相应从节点的连接。\n(2)从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:\n 如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过 repl-timeout,则释放与主节点的连接; 如果当前处于数据同步阶段,且收到主节点的 RDB 文件的时间超时,则停止数据同步,释放连接; 如果当前处于命令传播阶段,且距离上次收到主节点的 PING 命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。 问题 全量复制时,如果 RDB 文件过大,耗时很长就会触发超时,此时从节点会重连,再生成RDB,再超时,在生成RDB\u0026hellip;解决方案就是单机数据量尽量不要太大,增大 repl-timeout. 慢查询导致服务器阻塞: keys *,hgetall backlog 过小导致无限全量复制 backlog buffer 是固定大小的,写入命令超出长度就会覆盖.如果再全量复制的时候用时超长,存入buffer 的命令超过了其大小限制,那么就会导致连接中断,再重连,全量复制,连接中断,全量复制\u0026hellip;.死循环.解决方案就是需要正确设置 backlog buffer 的大小. 通过 client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds} 配置,默认值为 client-output-buffer-limit slave 256MB 64MB 60,其含义是:如果 buffer 大于256MB,或者连续 60s 大于 64MB ,则主节点会断开与该从节点的连接。该参数是可以通过 config set 命令动态配置的(即不重启Redis也可以生效).\n参考 深入学习Redis(3):主从复制\n 「Redis 设计与实现」\n https://redis.io/topics/replication\n ","id":5,"section":"posts","summary":"\u003cp\u003e之前对\u003ccode\u003eredis\u003c/code\u003e 的复制只有一点点了解,这次想要搞明白的是:如何实现的复制? 复制会遇到哪些问题(时延/一致性保证/网络故障时的处理)? 如何解决?高可用实现方案?\u003c/p\u003e\n\n\u003cp\u003e文章有部分是直接翻译的 \u003ca href=\"https://redis.io/topics/replication\"\u003ehttps://redis.io/topics/replication\u003c/a\u003e\u003c/p\u003e","tags":["redis"],"title":"Redis-复制功能探索","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/replication/","year":"2019"},{"content":" 事件驱动程序设计(英语:Event-driven programming)是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动作(如鼠标的按键,键盘的按键动作)或者是由其他程序的消息来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由程序员来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在交互程序(Interactive program)的情况下孕育而生的。 \u0026ndash;wikipedia\n 文件事件 服务端通过套接字与客户端进行连接,文件事件就是服务端对套接字操作的抽象.服务端与客户端的通信会产生多种文件事件(连接 accept ,读取 read, 写入 write ,关闭 close),服务器监听并处理相应的事件.\n文件事件处理器 redis 基于 Reactor 模式实现了网络事件处理 \u0026ndash;\u0026gt; 文件时间处理器.通过 I/O 多路复用 保证了单进程下的高性能网络模型.\n什么是 I/O Multiplexing? 参考: https://draveness.me/redis-io-multiplexing\n首先需要知道什么是文件描述符(File Descriptor ,简称 FD)? 文件描述符就是操作系统中操作文件时内核返回的一个 非负整数,可以通过文件描述符来指定待读写的文件.而套接字 socket 本质上也是一种文件描述符.\n简单来说就是通常我们使用的 I/O 模型是阻塞型的,服务器在处理一个客户端请求(即处理一个FD)时无法再处理其它的了. I/O多路复用 是通过利用操作系统的多路复用函数(select())来监听多个 FD 的可读可写情况,一旦有可读或可写的 FD,select() 就返回对应的个数.\n由于不同操作系统的有不同的多路复用函数,select是性能最差的.而 redis 也会根据操作系统的不同选择性能最好的函数来使用.并且由于不同平台的差异, redis 提供了一套相同的结构并针对不同平台进行了实现,以此屏蔽了对上层应用的影响.\n#ifdef HAVE_EVPORT #include \u0026quot;ae_evport.c\u0026quot; #else #ifdef HAVE_EPOLL #include \u0026quot;ae_epoll.c\u0026quot; #else #ifdef HAVE_KQUEUE #include \u0026quot;ae_kqueue.c\u0026quot; #else #include \u0026quot;ae_select.c\u0026quot; #endif #endif #endif 文件事件处理器结构 每一个套接字 socket 可以执行连接,读写,关闭操作时,会产生一个 文件事件.,I/O 多路复用 监听这些 FD 的操作请求,并向 文件事件派发器 传递产生文件事件的 FD. 虽然会并发的产生 N 个文件事件,但 I/O多路复用 会将其都放入一个队列中,顺序且同步地向 文件事件分派器 传送.处理完一个再传下一个.\n文件事件派发器 接收到 FD 后,就会根据FD 所绑定的文件事件类型选择相应的事件处理器进行处理.\n文件事件类型 AE_READABLE 可读事件 客户端对套接字 write 操作, close 操作或者客户端与服务端进行连接(出现 acceptable 套接字)时产生可读事件\n AE_WRITABLE 可写事件 客户端对套接字执行 read 操作,套接字产生可写事件\n AE_NONE 无任何事件 事件处理的先后顺序 AE_READABLE \u0026gt; AE_WRITABLE\n事件处理器 事件处理器是针对不同的文件事件实现的逻辑.客户端连接时,服务器需要进行应答,此时服务器就会将套接字关联到应答处理器.接收客户端的命令请求,服务器会将套接字关联到命令请求处理器.\n常用时间处理器 连接应答处理器 networking.c/acceptTcpHandler 客户端连接时会对其进应答.redis 在初始化时会将服务器的监听套接字的可读事件与该处理器关联起来,客户端只要连接监听套接字就会产生可读事件,执行对应的逻辑.\n 命令请求处理器 networking.c/readQueryFromClient 客户端连接服务器后,服务器会将客户端套接字的可读事件与命令请求处理器关联起来,当客户端向服务器发送命令请求时,产生可读事件,执行对应逻辑.\n 命令回复处理器 networking.c/sendReplyToClient 服务器有命令回复需要传送给客户端时,服务器会将客户端套接字的可写事件与命令回复处理器关联起来,客户端准备好接收服务器回复时,会产生可写事件,触发命令回复器执行.服务器发送完毕时,会解除关联.\n文件事件处理流程 aeCreateFileEvent 可以将一个给定FD 的给定事件加入到多路复用的监听范围中,并将事件与时间处理器关联\naeDeleteFileEvent 取消给定FD 的给定事件的监听\naeApiPoll 该方法会在每个平台的多路复用中进行实现,阻塞等待所有监听的FD 所产生的事件并返回可用时间的数量.会有超时处理.\n时间事件 Redis 中有两种时间事件 \u0026mdash;- 定时事件(隔一段时间执行一次),非定时事件(某个时间点执行一次)\n属性 id 全局唯一ID,顺序递增 when 毫秒精度 UNIX 时间戳,记录时间事件到达时间 timeProc 时间事件处理器,需要执行时间事件时,根据该处理器执行 时间事件是定时还是非定时,取决去 timeProc 返回值是否等于 AE_NOMORE. 等于则给事件ID标记为待删除,不等于则更新执行时间到下一次.\nretval = te-\u0026gt;timeProc(eventLoop, id, te-\u0026gt;clientData); if (retval != AE_NOMORE) { aeAddMillisecondsToNow(retval,\u0026amp;te-\u0026gt;when_sec,\u0026amp;te-\u0026gt;when_ms); } else { te-\u0026gt;id = AE_DELETED_EVENT_ID; } Redis 处理时间事件时,不会在当前循环中直接移除不再需要执行的事件,而是会在当前循环中将时间事件的 id 设置为 AE_DELETED_EVENT_ID,然后再下一个循环中删除,并执行绑定的 finalizerProc。\n/* Remove events scheduled for deletion. */ if (te-\u0026gt;id == AE_DELETED_EVENT_ID) { aeTimeEvent *next = te-\u0026gt;next; if (te-\u0026gt;prev) te-\u0026gt;prev-\u0026gt;next = te-\u0026gt;next; else eventLoop-\u0026gt;timeEventHead = te-\u0026gt;next; if (te-\u0026gt;next) te-\u0026gt;next-\u0026gt;prev = te-\u0026gt;prev; if (te-\u0026gt;finalizerProc) te-\u0026gt;finalizerProc(eventLoop, te-\u0026gt;clientData); zfree(te); te = next; continue; }\t 时钟问题 时间事件的执行影响最大的因素就是 系统时间. 系统时间的调整会影响时间事件的执行,所以在eventLoop 中有个 lastTime 属性来检测系统时间.如果发现系统时间改变了,比上次执行时间事件的时间小,就会强制尽早执行.\n时间事件执行流程 事件循环 Event Loop 上述的 文件事件, 时间事件 是从何时开始? 在 事件循环 中开始. 事件循环 是 redis 在启动后初始化完服务配置,就会陷入一个巨大的循环 aeEventLoop 中. 这个巨大的循环从 aeMain() 开始.\nvoid aeMain(aeEventLoop *eventLoop) { eventLoop-\u0026gt;stop = 0; while (!eventLoop-\u0026gt;stop) { if (eventLoop-\u0026gt;beforesleep != NULL) eventLoop-\u0026gt;beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP); } } 源码中可以看出来,除非给 eventLoop-\u0026gt;stop 设置为 true ,程序会一直跑,一直执行 aeProcessEvents.\naeEventLoop aeEventLoop 保存着事件循环的上下文信息,并有三个重要的数组:保存监听的文件事件 aeFileEvent , 时间事件 aeTimeEvent, 待处理文件事件 aeFiredEvent.\naeProcessEvent 在一般情况下,aeProcessEvents 都会先计算最近的时间事件发生所需要等待的时间,然后调用 aeApiPoll 方法在这段时间中等待事件的发生,在这段时间中如果发生了文件事件,就会优先处理文件事件,否则就会一直等待,直到最近的时间事件需要触发.\nint aeProcessEvents(aeEventLoop *eventLoop, int flags) { int processed = 0, numevents; if (!(flags \u0026amp; AE_TIME_EVENTS) \u0026amp;\u0026amp; !(flags \u0026amp; AE_FILE_EVENTS)) return 0; if (eventLoop-\u0026gt;maxfd != -1 || ((flags \u0026amp; AE_TIME_EVENTS) \u0026amp;\u0026amp; !(flags \u0026amp; AE_DONT_WAIT))) { struct timeval *tvp; #1:计算 I/O 多路复用的等待时间 tvp numevents = aeApiPoll(eventLoop, tvp); for (int j = 0; j \u0026lt; numevents; j++) { aeFileEvent *fe = \u0026amp;eventLoop-\u0026gt;events[eventLoop-\u0026gt;fired[j].fd]; int mask = eventLoop-\u0026gt;fired[j].mask; int fd = eventLoop-\u0026gt;fired[j].fd; int rfired = 0; if (fe-\u0026gt;mask \u0026amp; mask \u0026amp; AE_READABLE) { rfired = 1; fe-\u0026gt;rfileProc(eventLoop,fd,fe-\u0026gt;clientData,mask); } if (fe-\u0026gt;mask \u0026amp; mask \u0026amp; AE_WRITABLE) { if (!rfired || fe-\u0026gt;wfileProc != fe-\u0026gt;rfileProc) fe-\u0026gt;wfileProc(eventLoop,fd,fe-\u0026gt;clientData,mask); } processed++; } } if (flags \u0026amp; AE_TIME_EVENTS) processed += processTimeEvents(eventLoop); return processed; } 参考 https://draveness.me/redis-eventloop https://draveness.me/redis-io-multiplexing Redis设计与实现 ","id":6,"section":"posts","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e事件驱动程序设计\u003c/strong\u003e(英语:\u003cstrong\u003eEvent-driven programming\u003c/strong\u003e)是一种电脑\u003ca href=\"https://zh.wikipedia.org/wiki/程式設計\"\u003e程序设计\u003c/a\u003e\u003ca href=\"https://zh.wikipedia.org/wiki/模型\"\u003e模型\u003c/a\u003e。这种模型的程序运行流程是由用户的动作(如\u003ca href=\"https://zh.wikipedia.org/wiki/滑鼠\"\u003e鼠标\u003c/a\u003e的按键,键盘的按键动作)或者是由其他程序的\u003ca href=\"https://zh.wikipedia.org/wiki/訊息\"\u003e消息\u003c/a\u003e来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由\u003ca href=\"https://zh.wikipedia.org/wiki/程式設計師\"\u003e程序员\u003c/a\u003e来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在\u003ca href=\"https://zh.wikipedia.org/w/index.php?title=互動程序\u0026amp;action=edit\u0026amp;redlink=1\"\u003e交互程序\u003c/a\u003e(Interactive program)的情况下孕育而生的。 \u003ca href=\"https://zh.wikipedia.org/wiki/事件驅動程式設計\"\u003e\u0026ndash;wikipedia\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","tags":["redis"],"title":"Redis-事件","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/event/","year":"2019"},{"content":"RDB 和 AOF 区别在于: 前者保存数据库快照,持久化所有键值对,后者通过保存 写命令 保证数据库的状态.\n什么是 AOF ? AOF 持久化通过保存服务器执行的写命令实现,进行恢复时通过重放 AOF 文件中的写命令,来保证数据安全.就像 mysql 的 binlog 一样.\n开启 AOF 通过在 redis.conf 中将 appendonly 设为 yes 即可\n# redis.conf appendonly yes # 设置 aof 文件名字 appendfilename \u0026quot;appendonly.aof\u0026quot; # Redis支持三种不同的刷写模式: # appendfsync always #每次收到写命令就立即强制写入磁盘,是最有保证的完全的持久化,但速度也是最慢的,一般不推荐使用。 appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,是受推荐的方式。 # appendfsync no #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不被推荐。 AOF 文件格式 AOF 文件格式以 redis 命令请求协议为标准的,*.aof 文件可以直接打开.\nAOF 持久化过程 命令追加 append redis 执行完客户端的写命令后,会将该命令以协议的格式写入到 aof_buf 中.该属性为 redisServer 中的一个.\n#src/server.h struct redisServer { .... sds aof_buf; /* AOF buffer, written before entering the event loop */ } AOF 写入同步 redis 的服务进程是一个 事件循环 - event loop , 每次循环大概会做三件事.\n 文件事件: 接收客户端的命令,返回结果 时间事件: 执行系统的定时任务(serverCron), 完成渐进 rehash 扩容之类的操作 aof flush: 是否将 aof_buf 中的内容写入文件中\n# 伪代码 def eventloop(): while true: processFileEvents() # 处理命令 processTimeEvents() # 处理定时任务 flushAppendOnlyFile() # 处理 aof 写入 flushAppendOnlyFile 中的动作是否执行是根据一个配置决定的.\nappendfsync 该配置有几个值可选,默认是 everysec.\n always: 总是写入.只要程序执行到这一步了,就将 aof_buf 中命令协议写入到文件 everysec: 每秒写入. 每次执行前会先判断是否与上次写入间隔一秒,再次同步时通过 一个线程 专门执行 no: 不写入. 命令写入 aof_buf 后由操作系统决定何时同步到文件 fsync: 现代操作系统为了提高文件读写的效率,通常会将 write 函数写入的数据缓存在内存中,等到缓存空间填满或者超过一定时限,再将其写入磁盘.这样的问题在于宕机时缓存中的数据就无法恢复.所以操作系统提供了 fsync/fdatasync 两个函数,强制操作系统将数据立即写入磁盘,保证数据安全.两函数区别在于: 前者会更新文件的属性,后者只更新数据.\n 三种模式在性能和数据上都有相对的优缺点. always 模式数据安全性更强,毕竟每次都是直接写入,但是就会影响性能.磁盘读写是比较慢的. everysec 模式性能较好,但会丢失一秒内的缓存数据. no 模式就完全取决于操作系统了.\nAOF 还原数据 AOF 重写 AOF 重写的意思其实就是对单个命令的多个操作进行整理,留下最终态的执行命令来减少 aof 文件的大小.你可以想象一下执行 1w 次 incr 操作,写入 aof 1w 次的场景.\n触发条件 AOF 重写可以自动触发.通过配置 auto-aof-rewrite-min-size 和auto-aof-rewrite-percentage,满足条件就会自动重写.具体可以查看官方的 redis.conf\n重写过程 创建子进程,根据内存里的数据重写aof,保存到temp文件 此时主进程还会接收命令,会将写操作追加到旧的aof文件中,并保存在server.aof_rewrite_buf_blocks中,通过管道发送给子进程存在server.aof_child_diff中,最后追加到temp文件结尾 子进程重写完成后退出,主进程根据子进程退出状态,判断成功与否。成功就将剩余的server.aof_rewrite_buf_blocks追加到temp file中,然后rename()覆盖原aof文件 重写的过程中主进程还是会一直接受客户端的命令,所以重写子进程与主进程肯定会存在数据不一致的情况.redis针对这种情况作出了解决方案: 新增一个 aof_rewrite_buf_blocks, aof 写入命令时,不仅写入到 aof_buf, 如果正在重写,那么也写入到 aof_rewrite_buf_blocks 中,这样在子进程重写完毕后,可以将 aof_rewrite_buf_blocks 的命令追加到新文件中,保证数据不丢失.\nrename 操作是原子的,也是唯一会造成主进程阻塞的操作.\n参考 https://redis.io/topics/persistence https://youjiali1995.github.io/redis/persistence/ ","id":7,"section":"posts","summary":"\u003cp\u003e\u003ccode\u003eRDB\u003c/code\u003e 和 \u003ccode\u003eAOF\u003c/code\u003e 区别在于: 前者保存数据库快照,持久化所有键值对,后者通过保存 \u003cstrong\u003e写命令\u003c/strong\u003e 保证数据库的状态.\u003c/p\u003e","tags":["redis"],"title":"Redis-AOF持久化","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/aof/","year":"2019"},{"content":"redis 为内存数据库,一旦服务器进程退出,服务器中的数据就不见了.所以内存中的数据需要持久化的硬盘中来保证可以在必要的时候进行故障恢复. RDB 就是 redis 提供的一种持久化方式.\n 官方关于持久化的文章: https://redis.io/topics/persistence\n 什么是 RDB? RDB 是 redis 提供的一种持久化方式,可以手动执行,也可以通过定时任务定期执行,可以将某个时间节点的数据库状态保存到一个 RDB 文件中,叫做 dump.rdb.如果开启了压缩算法( LZF )的支持,则可以利用算法减少文件大小.服务器意外宕机或者断电后重启都可以通过该文件来恢复数据库状态.\n如何执行? 有两个命令可以生成 RDB 文件.\n SAVE: 执行时进程阻塞,无法处理其他命令 BGSAVE: 新建一个子进程来后台生成 RDB 文件 具体实现逻辑在: src/rdb.c/rdbSave(),从官方文档可知,该实现是基于 cow 的.\n https://redis.io/topics/persistence\nThis method allows Redis to benefit from copy-on-write semantics.\n 如何载入? RDB 文件会在 redis 启动时自动载入.\n由于 AOF 持久化的实时性更好,所以如果同时开启了 AOF , RDB 两种持久化,会优先使用 AOF 来恢复.\nBGSAVE 执行时的状态 BGSAVE 执行期间会拒绝 SAVE/BGSAVE 的命令,避免产生 竞争条件.\nBGSAVE 执行期间 BGREWRITEAOF 命令会延迟到 BGSAVE 执行完之后执行.\nBGREWRITEAOF 在执行时, BGSAVE 命令会被拒绝.\nBGSAVE 和 BGREWRITEAOF 命令的权衡完全是性能方面的考虑.毕竟都会有大量的磁盘写入,影响性能.\n定时执行BGSAVE BGSAVE 不会阻塞服务器进程,所以 redis 允许用户通过配置, 定时执行 BGSAVE 命令.\n快照策略 Snapshotting 可以通过设置 N 秒内至少 M 次修改来触发一次 BGSAVE.\nsave 60 1000 # 60s内有至少1000次修改时 bgsave 一次 默认的保存条件 save 900 1 save 300 10 save 60 10000 dirty 计数器 \u0026amp; lastsave 属性 redis 中维护了一个计数器,来记录距离上一次 SAVE/BGSAVE 后服务器对所有数据库进行了多少次增删改,叫做 dirty计数器.属于 redisServer 结构体的属性之一.\nlastsave 是记录了上一次成功执行 SAVE/BGSAVE 的 UNIX时间戳 , 同样是 redisServer 结构体的属性之一.\n# src/server.h struct redisServer { ... long long dirty; /* Changes to DB from the last save */ time_t lastsave; /* Unix time of last successful save */ ... } 定时执行过程 redis 有一个定时任务 serverCron , 每隔 100ms 就会执行一次,用于维护服务器.该任务就会检查 save 设置的保存条件是否满足,满足则执行 BGSAVE\n满足条件逻辑 遍历设置的 save 参数, 计算当前时间到 lastsave 的间隔 interval , 如果 dirty \u0026gt; save.change \u0026amp; interval \u0026gt; save.seconds 那么就执行保存\nRDB 文件结构 https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format\n写下这篇文章时参考版本为 2019.09.05 更新的版本\n RDB 文件格式对读写进行了很多优化,这类优化导致其格式与内存中存在的形式极其相似,同时利用 LZF 压缩算法来优化文件的大小.一般来讲, redis 对象都会提前标记自身的大小,所以备份RDB 在读取这些 object 时,可以提前知道要分配多少内存.\n解析RDB结构 下面的代码展示的是 16 进制下 RDB 文件的结构,便于理解\n----------------------------# RDB is a binary format. There are no new lines or spaces in the file. 52 45 44 49 53 # 魔数 REDIS的16进制表示,代表这是个RDB文件 30 30 30 37 # 4位ascii码表示当前RDB版本号,这里表示\u0026quot;0007\u0026quot; = 7 ---------------------------- FE 00 # FE 表明这是数据库选择标记. 00 表示选中0号数据库 ----------------------------# Key-Value pair starts FD $unsigned int # FD 是秒级过期时间的标记. 紧接着是 4 byte unsigned int 过期时间 $value-type # 1 byte 标记 value 类型 - set, map, sorted set etc. $string-encoded-key # 经过编码后的键 $encoded-value # 值,编码格式取决去 $value-type ---------------------------- FC $unsigned long # FC 表明是毫秒级过期时间. 过期时间值是 8 bytes的 unsigned long,是一个unit时间戳 $value-type # 同上秒级时间 $string-encoded-key # 同上秒级时间 $encoded-value # 同上秒级时间 ---------------------------- $value-type # 这一栏是没有过期时间的key-value $string-encoded-key $encoded-value ---------------------------- FE $length-encoding # 前一个数据库的编码完成,选择新的数据库进行处理.数据库编号会根据 length-encoding 格式获得 ---------------------------- ... # Key value pairs for this database, additonal database FF ## 表明 RDB 文件结束了 8 byte checksum ## 8byte CRC 64 校验码 value type 1 byte 表示了 value 的类型.\n type(以下为十进制表示) 编码类型 0 String 1 List 2 Set 3 Sorted Set 4 Hash 9 Zipmap 10 Ziplist 11 Intset 12 Sorted Set in Ziplist 13 HashMap in Ziplist 键值编码格式 键(key)都是字符串,所以使用string 编码格式.\n值(value)就会有不同的区分:\n 如果 value type 为 0 ,会是简单的字符串. 如果 value type 为 9,10,11,12, 值会被包装为 string, 在读到该字符串后,会进一步解析. 如果 value type 为 1,2,3,4, 值会是一个字符串数组. Length Encoding 长度编码是用来存储对象的长度的.是一种可变字节码,旨在使用尽可能少的字节.\n如何工作? 从流中读取 1byte,得到高两位. 如果是 00 开头, 那么剩下 6 位表示长度 如果是 01 开头, 会再从流中读取 1byte,合起来总共 14 位作为长度. 如果是 10 开头, 会直接丢弃剩下的 6 位.再从流中读取 4bytes作为长度. 如果是 11 开头, 说明这个对象是一种特殊编码格式. 剩下的 6 位表示了它的格式类型.这个编码通常用来将数字存储为字符串,或者存储被编码过得字符串(String Encoding). 编码结果是? 从上述可得,可能的编码格式是这样的:\n 1 byte 最多存储到 63 2 bytes 最多存储到 16383 5 bytes 最多存储到 2^32 - 1 String Encoding redis 的字符串是二进制安全的,所以可以存储 anything. 没有任何字符串结尾的标记.最好将 redis 字符串视为一个字节数组.\n有三种类型的字符串:\n 长度编码字符串 这是最简单的一种,字符串的长度会利用 Length Encoding 编码作为前缀,后面跟着字符串的编码\n 数字作为字符串 这里就将上面 Length Encoding 的特殊编码格式联系起来了,数字作为字符串时以 11 开头,剩下的 6 位表示不同的数字类型\n 0 表示接下来是一个 8 位数字 1 表示接下来是一个 16 位数字 2 表示接下来是一个 32 位数字 压缩字符串 压缩字符串的 Length Encoding 还是以 11 开头的, 但是剩下的6 位二进制的值为 4, 表明后面读取到的是一个压缩字符串.压缩字符串会存储压缩前和压缩后的长度.解析规则如下:\n 根据 Length Encoding 读取压缩的长度 clen 根据 Length Encoding 读取未压缩的长度 从流中读取 clen bytes 的数据 利用 LZF 算法进行解析 分析RDB文件 利用 od 命令来分析来看看 rdb 文件长什么样子.我将 redis 数据库清空后,执行了 set msg hello,所以现在只有一个键 msg, 值为 hello.下面的命令第一行输出的是 16 进制,下面一行输出的是对应的 ascii. 下面进行解析~\n➜ od -A x -t x1c -v dump.rdb 0000000 52 45 44 49 53 30 30 30 39 fa 09 72 65 64 69 73 R E D I S 0 0 0 9 372 \\t r e d i s 0000010 2d 76 65 72 05 35 2e 30 2e 34 fa 0a 72 65 64 69 - v e r 005 5 . 0 . 4 372 \\n r e d i 0000020 73 2d 62 69 74 73 c0 40 fa 05 63 74 69 6d 65 c2 s - b i t s 300 @ 372 005 c t i m e 051 0000030 29 e8 c3 5d fa 08 75 73 65 64 2d 6d 65 6d c2 d0 ) 350 303 ] 372 \\b u s e d - m e m 302 007 0000040 07 10 00 fa 0c 61 6f 66 2d 70 72 65 61 6d 62 6c \\a 020 \\0 372 \\f a o f - p r e a m b l 0000050 65 c0 00 fe 00 fb 01 00 00 03 6d 73 67 05 68 65 e 300 \\0 376 \\0 373 001 \\0 \\0 003 m s g 005 h e 0000060 6c 6c 6f ff fc 0e 6b 79 fe 47 1a 36 l l o 377 374 016 k y 376 G 032 6 000006c 魔数和版本号 前 5 个字节就是我们看到的 REDIS,以及后四个字节对应的版本号9\n辅助字段 Aux Fields 这是 Version 7 之后加入的字段, Redis设计与实现 所使用的版本是没有这个,所以一开始有点懵~ 只能看代码了.\n# src/rdb.c /* 该函数负责执行 RDB 文件的写入 */ int rdbSave(char *filename, rdbSaveInfo *rsi) { //伪代码 1. 创建一个临时文件 temp-$pid.rdb,并处理创建失败的逻辑 2. 新建一个redis封装的I/O流 3. 写入rdb文件 rdbSaveRio() 4. 将文件重命名, 默认重命名为 dump.rdb 5. 更新服务器的一些状态: dirty计数器置0,更新lastsave等 } 然后我们来看下写入的 Aux Fields, 在函数 rdbSaveRio 中\nint rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) { // 忽略所有只看重点 if (server.rdb_checksum) rdb-\u0026gt;update_cksum = rioGenericUpdateChecksum; // 生成校验码 snprintf(magic,sizeof(magic),\u0026quot;REDIS%04d\u0026quot;,RDB_VERSION); // 生成魔数及版本号 if (rdbWriteRaw(rdb,magic,9) == -1) goto werr; // 写入魔数及版本号 if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr; // 写入 AuxFileds } /* Save a few default AUX fields with information about the RDB generated. */ int rdbSaveInfoAuxFields(rio *rdb, int flags, rdbSaveInfo *rsi) { int redis_bits = (sizeof(void*) == 8) ? 64 : 32; int aof_preamble = (flags \u0026amp; RDB_SAVE_AOF_PREAMBLE) != 0; /* Add a few fields about the state when the RDB was created. */ if (rdbSaveAuxFieldStrStr(rdb,\u0026quot;redis-ver\u0026quot;,REDIS_VERSION) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;redis-bits\u0026quot;,redis_bits) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;ctime\u0026quot;,time(NULL)) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;used-mem\u0026quot;,zmalloc_used_memory()) == -1) return -1; /* Handle saving options that generate aux fields. */ if (rsi) { if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;repl-stream-db\u0026quot;,rsi-\u0026gt;repl_stream_db) == -1) return -1; if (rdbSaveAuxFieldStrStr(rdb,\u0026quot;repl-id\u0026quot;,server.replid) == -1) return -1; if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;repl-offset\u0026quot;,server.master_repl_offset) == -1) return -1; } if (rdbSaveAuxFieldStrInt(rdb,\u0026quot;aof-preamble\u0026quot;,aof_preamble) == -1) return -1; return 1; } 以上可以看出会写入这些字段.\n redis-ver:版本号\n redis-bits:OS 操作系统位数 32\u0026frasl;64\n ctime:RDB文件创建时间\n used-mem:使用内存大小\n repl-stream-db:在server.master客户端中选择的数据库\n repl-id:当前实例 replication ID\n repl-offset:当前实例复制的偏移量\n 每一个属性写入前都会写入 0XFA, 标记这是一个辅助字段.在上面命令行输出中,ascii 展示为 372\n数据库相关标记 0000050 65 c0 00 fe 00 fb 01 00 00 03 6d 73 67 05 68 65 e 300 \\0 376 \\0 373 001 \\0 \\0 003 m s g 005 h e 这一行中的 0XFE 表示选择数据库,后面紧接着 00 即为,选择 0 号数据库. 0XFB 是标记了当前数据库中键存储的数量,这里用到了 Length Encoding, 01 是我们存储的字典中key-value的数量,00 是过期字典(expires)中的数量.\n redisDB中有两个属性, dict 记录了我们写入的所有键, expires 存储了我们设置有过期时间的键以及其过期时间.\n Key Value 结构 我们设置了 msg -\u0026gt; hello,在输出中是这样的.\n0000050 65 c0 00 fe 00 fb 01 00 00 03 6d 73 67 05 68 65 e 300 \\0 376 \\0 373 001 \\0 \\0 003 m s g 005 h e 在 msg 前面的字段 \\0 003, 表示他是 string 类型, 且长度为 3, 005 hello, 表示是长度为 5 的 hello.\n还有其他数据结构这里就不做展示了.\n结束符 \u0026amp; 校验码 0000060 6c 6c 6f ff fc 0e 6b 79 fe 47 1a 36 l l o 377 374 016 k y 376 G 032 6 最后一行输出中 0xff , 文件结束符, 剩下的八个字节就是 CRC64\n参考 https://cloud.tencent.com/developer/article/1179710\n Redis5.0 RDB文件解析\n ","id":8,"section":"posts","summary":"\u003cp\u003e\u003ccode\u003eredis\u003c/code\u003e 为内存数据库,一旦服务器进程退出,服务器中的数据就不见了.所以内存中的数据需要持久化的硬盘中来保证可以在必要的时候进行故障恢复. \u003ccode\u003eRDB\u003c/code\u003e 就是 \u003ccode\u003eredis\u003c/code\u003e 提供的一种持久化方式.\u003c/p\u003e","tags":["redis"],"title":"Redis-RDB持久化","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/rdb/","year":"2019"},{"content":"服务器中的数据库 redis的数据库是保存在一个db数组中的,默认会新建16个数组.\n# src/server.h struct redisServer { ... redisDb *db; // db 存放的数组 int dbnum; /* 根据该属性决定创建数据库数量 默认: 16 */ ... } 切换数据库 redis 数据库从 0 开始计算,通过 select 命令切换数据库. client 会有一个属性指向当前选中的 DB.\n# src/server.h typedef struct client { ... redisDb *db; /* 指向当前选中的redisDb */ ... } 键空间 redisDb 的结构是怎样的呢?\n# src/server.h /* Redis database representation. There are multiple databases identified * by integers from 0 (the default database) up to the max configured * database. The database number is the 'id' field in the structure. */ typedef struct redisDb { dict *dict; /* 键空间 */ dict *expires; /* Timeout of keys with a timeout set */ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ dict *ready_keys; /* Blocked keys that received a PUSH */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ int id; /* Database ID */ long long avg_ttl; /* Average TTL, just for stats */ list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */ } redisDb; 键空间 指的是每一个数据库中存放用户设置键和值的地方. 可以看到上述结构中, dict 属性就是每一个数据库的键空间, 字典结构, 也就是我们命令的执行结构.例如 set msg \u0026quot;hello world~\u0026quot; .\n所以针对数据库的操作就是操作字典.\n读写键空间后的操作 维护 hit, miss 次数, 可以利用 info stats 查看 keyspace_hits 以及 keyspace_misses 读取一个键后会更新键的 LRU ,用于计算键的闲置时间 object idletime {key} 查看 服务器读取一个键后发现已经过期,则会删除这个键在执行其他操作 如果客户端 watch 了某个键, 该键修改之后,会被标记为 dirty, 从而事务程序可以注意到该键已经被修改了 服务器每修改一个键后, 都会对 dirty 计数器 +1 ,这个计数器会触发服务器的持久化和复制操作 服务器开启数据库通知之后,键修改后会发送相应的数据库通知 过期时间保存 上述的 redisDb 结构中有 expires 的字典, redis 就是将我们设置的过期时间存到了这个字典中.键就是数据库键,值是一个 long long 类型的整数, 保存了键的过期时间: 一个毫秒精度的 UNI\u0010X 时间戳.\nRedis的过期键删除策略 有这么三种删除方式.\n定时删除 设置键过期时间的同时,创建一个定时器,到期自动删除\n优点 内存友好,键过期就删除\n缺点 对 CPU 不友好,过期键较多时,会占用较长时间,CPU 资源紧张的情况下会影响服务器的响应时间和吞吐量 创建定时器需要用到 redis 的时间事件,实现方式为无序链表,查找效率低 惰性删除 无视键是否过期,每次从键空间取键时,先判断是否过期,过期就删除,没过期就返回.\n优点 对 CPU 友好,遇到过期键才删除\n缺点 如果过期键很多,且一直不会被访问,就会导致大量内存被浪费\n定期删除 定期的在数据库中检查,删除过期的键.定期删除策略是上面两种策略的折中方案.\n优点 每隔一段时间删除过期键,可以减少删除操作对 CPU 的影响 定期删除也可以减少过期键带来的内存浪费 难点 确定删除操作执行的时长和频率\nredis采用方案 惰性删除 + 定期删除\n惰性删除是在所有读写数据库命令执行之前检查键是否过期来实现的.\n定期删除是通过 redis 的定时任务执行.在规定的时间内,多次遍历服务器的各个数据库,从 expires 字典中 随机抽查 一部分键的过期时间.current_db 会记录当前函数检查的进度,并在下一次函数执行时,接着上次的执行.循环往复地执行.\n内存淘汰策略 默认策略是 volatile-lru,即超过最大内存后,在过期键中使用 lru 算法进行 key 的剔除,保证不过期数据不被删除,但是可能会出现 OOM 问题。\n其他策略如下: allkeys-lru:根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。 allkeys-random:随机删除所有键,直到腾出足够空间为止。 volatile-random: 随机删除过期键,直到腾出足够空间为止。 volatile-ttl:根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。 noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息 \u0026ldquo;(error) OOM command not allowed when used memory\u0026rdquo;,此时 Redis 只响应读操作。 AOF,RDB \u0026amp; 复制功能对过期键的处理 生成 RDB 文件时,过期键不会被保存到新文件中 载入 RDB 文件 以主服务器运行:未过期的键被载入,过期键忽略 以从服务器运行:保存所有键,无论是否过期.由于主从服务器在进行数据同步时,从服务器数据库就会被清空,所以一般来讲,也不会造成什么影响. AOF 写入时,键过期还没有被删除,AOF 文件不会受到影响,当键被惰性删除或被定期删除后,AOF 文件会追加一条 DEL 命令来显示记录该键已被删除 AOF 重写时,会对键过期进行确认,过期补充些. 复制模式下,从服务器的过期键删除由主服务器控制. 主服务器删除一个键后,会显示发送 DEL 命令给从服务器. 从服务器接收读命令时,如果键已过期,也不会将其删除,正常处理 从服务器只在主服务器发送 DEL 命令才删除键 主从复制不及时怎么办?会有脏读现象~\n数据库通知 通过订阅的模式,可以实时获取键的变化,命令的执行情况.通过 redis 的 pub/sub 模式来实现.命令对数据库进行了操作后,就会触发该通知,置于能不能发送出去完全看你的配置了.\nnotify_keyspace_events 系统配置决定了服务器发送的配置类型.如果给定的 type 不是服务器允许发送的类型,程序就直接返回了.然后就判断能发送键通知就发送,能发送命令通知就发送.\n/* The API provided to the rest of the Redis core is a simple function: * * notifyKeyspaceEvent(char *event, robj *key, int dbid); * * 'event' is a C string representing the event name. * 'key' is a Redis object representing the key name. * 'dbid' is the database ID where the key lives. */ void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) { sds chan; robj *chanobj, *eventobj; int len = -1; char buf[24]; /* If any modules are interested in events, notify the module system now. * This bypasses the notifications configuration, but the module engine * will only call event subscribers if the event type matches the types * they are interested in. */ moduleNotifyKeyspaceEvent(type, event, key, dbid); /* If notifications for this class of events are off, return ASAP. */ if (!(server.notify_keyspace_events \u0026amp; type)) return; eventobj = createStringObject(event,strlen(event)); /* __keyspace@\u0026lt;db\u0026gt;__:\u0026lt;key\u0026gt; \u0026lt;event\u0026gt; notifications. */ if (server.notify_keyspace_events \u0026amp; NOTIFY_KEYSPACE) { chan = sdsnewlen(\u0026quot;__keyspace@\u0026quot;,11); len = ll2string(buf,sizeof(buf),dbid); chan = sdscatlen(chan, buf, len); chan = sdscatlen(chan, \u0026quot;__:\u0026quot;, 3); chan = sdscatsds(chan, key-\u0026gt;ptr); chanobj = createObject(OBJ_STRING, chan); pubsubPublishMessage(chanobj, eventobj); decrRefCount(chanobj); } /* __keyevent@\u0026lt;db\u0026gt;__:\u0026lt;event\u0026gt; \u0026lt;key\u0026gt; notifications. */ if (server.notify_keyspace_events \u0026amp; NOTIFY_KEYEVENT) { chan = sdsnewlen(\u0026quot;__keyevent@\u0026quot;,11); if (len == -1) len = ll2string(buf,sizeof(buf),dbid); chan = sdscatlen(chan, buf, len); chan = sdscatlen(chan, \u0026quot;__:\u0026quot;, 3); chan = sdscatsds(chan, eventobj-\u0026gt;ptr); chanobj = createObject(OBJ_STRING, chan); pubsubPublishMessage(chanobj, key); decrRefCount(chanobj); } decrRefCount(eventobj); } ","id":9,"section":"posts","summary":"","tags":["redis"],"title":"Redis-数据库长什么样?","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/db/","year":"2019"},{"content":"Redis有很多种数据结构,但其并没有直接使用这些数据结构来构建这个 NOSQL, 而是通过 对象系统 完成了对所有数据结构的统一管理, 实现内存回收, 对象共享等特性~\n类型及编码 在 Redis 中使用任何命令操作,都是操作的一个对象.有键对象,值对象.\nset msg \u0026quot;hello~\u0026quot; # msg 为键对象, \u0026quot;hello~\u0026quot; 为值对象 每个对象都会有如下的结构:\ntypedef struct redisObject { unsigned type:4; // 类型 unsigned encoding:4; // 编码 unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; // 引用计数 void *ptr; // 指向底层实现数据结构的指针 } robj; type 类型 type 指明了该对象的类型. redis 中类型有如下几种\n/* The actual Redis Object */ #define OBJ_STRING 0 /* String object. */ #define OBJ_LIST 1 /* List object. */ #define OBJ_SET 2 /* Set object. */ #define OBJ_ZSET 3 /* Sorted set object. */ #define OBJ_HASH 4 /* Hash object. */ #define OBJ_MODULE 5 /* Module object. */ #define OBJ_STREAM 6 /* Stream object. */ redis 中键都为字符串对象,利用 type 命令可以查看值对象的类型\nreids\u0026gt; type language list encoding 编码 encoding 属性记录了该对象使用的什么数据结构存储底层的实现,即 *ptr 所指向的那个数据结构.以下是目前的编码类型.\n/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ #define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */ 基本上每种类型的对象都会对应两种编码类型,可以动态的根据用户输入的值提供最有的数据结构,减少资源消耗.\n字符串对象 字符串对象有三种编码格式. int,embstr,raw,不同长度不同格式有不一样的编码类型.\n47.100.254.74:6379\u0026gt; set msg \u0026quot;abcdefg\u0026quot; OK (0.53s) 47.100.254.74:6379\u0026gt; object encoding msg \u0026quot;embstr\u0026quot; 47.100.254.74:6379\u0026gt; set msg \u0026quot;abcdefghijklmnopqrstuvwxyz01234567890123456789\u0026quot; OK 47.100.254.74:6379\u0026gt; object encoding msg \u0026quot;raw\u0026quot; 47.100.254.74:6379\u0026gt; set msg 123 OK 47.100.254.74:6379\u0026gt; object encoding msg \u0026quot;int\u0026quot; embstr vs raw 一个字符串对象包括 redisObject 和 sds 两部分组成.正常情况下是需要分配两次内存来创建这两个结构.这也是raw 的格式,但是如果当 value 长度较短时, (由于 redis 使用的是 jemalloc 分配内存)我们可以将内存分配控制在一次,将 RedisObject 和 sds 分配在连续的内存空间,这也就是 embstr 编码格式了.那多短算短呢?\n在此之前先了解下创建一个 redisObject 时所占用的空间.\nembstr编码是由 代表着 字符串的数据结构是 SDS.假设为 sdshdr8\nstruct sdshdr8 { uint8_t len; /* 1byte used */ uint8_t alloc; /* 1byte excluding the header and null terminator */ unsigned char flags; /* 1byte 3 lsb of type, 5 unused bits */ char buf[]; }; jemalloc 可以分配 8/16/32/64 字节大小的内存,从上可以发现最少的内存需要占用 19 字节, Redis 在总体大于 64 字节时,会改为 raw 存储. 所以 embstr 形式时最大长度是 64 - 19 - 结束符\\0长度 = 44\n编码转换 由于 redis 没有为 embstr 编写修改相关的程序,所以是只读的, 如果对其执行任何修改命令,就会变为 raw 格式.\n类型检查 redis 中的操作命令一般有两种: 所有类型都能用的(DEL, EXPIRE\u0026hellip;), 特定类型适用的(各种数据类型对应的命令).若操作键的命令不对, redis 会提示报错.\n47.100.254.74:6379\u0026gt; set numbers 1 OK 47.100.254.74:6379\u0026gt; object encoding numbers \u0026quot;int\u0026quot; 47.100.254.74:6379\u0026gt; rpush numbers a (error) WRONGTYPE Operation against a key holding the wrong kind of value 如何实现? 利用 RedisObject 的type 来控制.在输入一个命令时, 服务器会先检查输入键所对应的的值对象是否为命令对应的类型,是的话就执行,不是就报错.\n多态命令 同一种数据结构可能有多种编码格式.比如字符串对象的编码格式可能有 int, embstr, raw.所以当命令执行前,还需要根据值对象的编码来选择正确的命令来实现.\n比如想要执行 llen 获取 list 长度, 如果编码为 ziplist, 那么程序就会使用 ziplist 对应的函数来计算, 编码为 quicklist 时则是使用 quicklist 对应的函数来计算. 此为命令的 多态 .\n内存回收 redis 利用引用计数来实现内存回收机制.由 RedisObject 中的 refcount 属性记录.\n引用计数是有导致循环引用的弊端的,那么redis为啥还是会用的?找了很久也没有找到答案.\n有一个说法是: 引用的复杂度很低,不太容易导致循环引用.就一切从简呗.\n对象共享 对象共享指的是创建一次对象后,后面如果还有客户端需要创建同样的值对象则直接把现在这个的引用只给他,引用计数加1,可以节省内存的开销.类似 Java 常量池. 所以refcount 也被用来做对象共享的.\nredis 在初始化服务器时, 会创建 0 - 9999 一万个整数字符串, 为了节省资源.\n为什么不共享其他的复杂对象? 整数复用几率很大 整数比较算法时间复杂度是 O(1), 字符串是 O(N), hash/list 复杂度是 O(n2) 键的空转时长 redisObject 的 lru 属性记录着该对象最后一次被命令程序访问的时间.该属性在内存回收中有很大的作用.\n空转时长指的是now() - lru\n47.100.254.74:6379\u0026gt; object idletime numbers (integer) 4023 ","id":10,"section":"posts","summary":"\u003cp\u003eRedis有很多种数据结构,但其并没有直接使用这些数据结构来构建这个 \u003ccode\u003eNOSQL\u003c/code\u003e, 而是通过 \u003ccode\u003e对象系统\u003c/code\u003e 完成了对所有数据结构的统一管理, 实现内存回收, 对象共享等特性~\u003c/p\u003e","tags":["redis"],"title":"Redis-万物皆「对象」","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/obj/","year":"2019"},{"content":"分布式锁有很多中实现(纯数据库,zookeeper,redis),纯数据库的受限于数据库性能,zk 可以保证加锁的顺序,是公平锁.Redis中的实现就是接下来要学习的.\n为什么使用分布式锁? 在分布式环境下想要保证只能有一个请求更新一条数据,普通的加锁(比如 Java 中的 synchronized,JUC 中的各种 Lock)都不能胜任. 分布式锁的意义在于可以将操作锁的权利中心化,从而串行控制业务的执行.但是使用分布式锁也有很多弊端,后面再说.\n分布式锁的特点? 互斥:具有强排他性,需要保证不同节点不同线程的互斥 可重入:同一个节点的同一个线程如果获得了锁,那也可以再次获得 高效,高可用:加锁解锁要高效,高可用保证分布式锁服务不会宕机失效 阻塞/非阻塞:像 ReentrantLock 支持 lock, tryLock, tryLock(long timeout) 支持公平锁/非公平锁(Option) 如何使用分布式锁? Redis中有多种实现分布式锁的方式,一个一个看看.\n简单粗暴版 设置一个坑,让所有节点去抢就好.即语义为: set if not exist, 抢到后执行逻辑,逻辑完成后在del即可.\nredis 2.8 版本之前我们会通过以下方式:\nsetnx {resource-name} {anystring} 我们还需要加一个过期时间,以免各种异常宕机情况导致锁无法释放的问题.\nexpire key {max-lock-time} 这两条命令并不是原子操作的,所以我们需要通过 Lua 脚本来保证其原子性\nredis 2.8 版本之后官方提供了 nx ex 的原子操作,使用起来更加简单了.\nset {resource-name} {anystring} nx ex {max-lock-time} Redission版 https://github.com/redisson/redisson\n Redission 和 Jedis 都是 Java 中的 redis 客户端, Jedis 使用的是阻塞式 I/O, 而 Redission 使用的 Netty 来进行通信,而且 API 封装更友好, 继承了 java.util.concurrent.locks.Lock 的接口,可以像操作本地 Lock 一样操作分布式锁. 而且 Redission 还提供了不同编程模式的 API: sync/async, Reactive, RxJava, 非常人性化. Redission 有丰富的接口实现以及对不同异常情况的处理设计很值得学习.\n// 1. 设置 config Config config = new Config(); // 2. 创建 redission 实例 RedissonClient redisson = Redisson.create(config); // 4. 获取锁 RLock lock = redisson.getLock(\u0026quot;myLock\u0026quot;); // 5. 加锁 // 方式一 // 加锁以后10秒钟自动解锁 // 无需调用unlock方法手动解锁 lock.lock(10, TimeUnit.SECONDS); // 方式二 // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } } // 方式三 // 异步加锁 RLock lock = redisson.getLock(\u0026quot;anyLock\u0026quot;); lock.lockAsync(); lock.lockAsync(10, TimeUnit.SECONDS); Future\u0026lt;Boolean\u0026gt; res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS); RedLock https://redis.io/topics/distlock\n 上述的分布式锁实现都是基于单实例实现,所以会出现单点问题.胆大RedLock 基本原理是利用多个 Redis 集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率。\n加锁过程 客户端获取当前的时间戳。 对 N 个 Redis 实例进行获取锁的操作,具体的操作同单机分布式锁。对 Redis 实例的操作时间需要远小于分布式锁的超时时间,这样可以保证在少数 Redis 节点 Down 掉的时候仍可快速对下一个节点进行操作。 客户端会记录所有实例返回加锁成功的时间,只有从多半的实例(在这里例子中 \u0026gt;= 3)获取到了锁,且操作的时间远小于分布式锁的超时时间,锁才被人为是正确获取。 如果锁被成功获取了,当前分布式锁的合法时间为初始设定的合法时间减去上锁所花的时间。 若分布式锁获取失败,会强制对所有实例进行锁释放的操作,即使这个实例上不存在相应的键值。 分布式锁的一些问题 锁被其他客户端释放 如果线程 A 在获取锁后处理业务时间过长,导致锁被自动释放了,此时 线程 B 重新获取到了锁. 线程 A 在执行完业务逻辑后释放锁(DEL操作),这是就会把线程 B 获取到的锁给释放掉.\n如何解决? 在设置 value 时,生成一个随机 token, 删除 key 时先做判断,只有在 token 与自己持有的相等时,才能删除. 由于需要保证原子性, 我们需要通过 Lua 脚本来实现.像下面这样,不过 Redission 已经有对应的实现了.\nif redis.call(\u0026quot;get\u0026quot;,KEYS[1]) == ARGV[1] then return redis.call(\u0026quot;del\u0026quot;,KEYS[1]) else return 0 end 超时问题 如果在加锁和释放锁之间的业务逻辑过长,超出了锁的过期时间,那么就可能会导致另一个线程获取到锁,导致逻辑不能严格的串行执行.所以分布式锁的初衷是: 逻辑越短越好,持有锁的时间越短越好.\n如何解决? 这个目前没有太好解决的方案,后面如果看到了,就更新到这里.自己觉得: 尽量保证持锁时间短,优化代码逻辑.虽然可以延长锁的时间,但是会影响吞吐量的吧.如果真的有多个客户端持有了锁,还需要尽量保证业务逻辑中数据的幂等性,日志监控,及时报警,这样也可以做到尽快的人工介入.\n 技术莫得银弹~适合的才是最好的.\n 时钟不一致 RedLock 强依赖时间,所以机器时间不一致会有很大的问题\n如何解决? 人为调整 NTP自动调整: 可以将时间精度控制在一定范围内. 性能、故障恢复和 fsync 假设 Redis 没有持久性,当一个客户端获得了 5 个实例中的 3 个锁,若 3 个锁所在的实例 Down 掉了,实例再次启动时,其他的客户端也可以再次获得锁。\n这个问题会因为开启了 Redis 的持久化而改观,对于 AOF 持久化(区别与 RDB 的二进制持久化,是文本持久化)。默认采用的是每秒钟通过 fsync 落盘,这意味着会丢失一秒内的数据,如果需要更有安全保证的持久化,可以设置 fsync=always,但对应的会损失一部分性能。\n更好的解决办法是在实例 Down 掉后延迟一个略长于锁合法时间的时间,这样就可以保证在实例启动起来时锁一定是过期的,从而无须以损失性能为代价而使用 fsync=always 的持久化。\n参考 再有人问你分布式锁,这篇文章扔给他 RedLock中译 ","id":11,"section":"posts","summary":"\u003cp\u003e分布式锁有很多中实现(纯数据库,zookeeper,redis),纯数据库的受限于数据库性能,zk 可以保证加锁的顺序,是公平锁.Redis中的实现就是接下来要学习的.\u003c/p\u003e","tags":["分布式锁","redis"],"title":"Redis-分布式锁","uri":"https://xiaohei.im/hugo-theme-pure/2019/11/distributed-lock/","year":"2019"},{"content":"系统学习 redis 相关的知识,从数据结构开始~\nString 字符串 Redis 的字符串是 动态字符串, 长度可变,自动扩容。利用预分配空间方式减少内存的分配。默认分配 1M 大小的内存。扩容时加倍现有空间,最大占用为 512M.\n常用命令 SET,SETNX\u0026hellip;\n结构 struct SDS\u0026lt;T\u0026gt; { T capacity; // 数组容量 T len; // 数组长度 byte flags; // 特殊标识位,不理睬它 byte [] content; // 数组内容 } Redis 中的字符串叫做 Simple Dynamic String, 上述 struct 是一个简化版,实际的代码中,redis 会根据 str 的不同长度,使用不同的 SDS, 有 sdshdr8, sdshdr16, sdshdr32 等等\u0026hellip; 但结构体都是如上的类型.\ncapacity 存储数组的长度,len 表示数组的实际长度。需要注意的是: string 的字符串是以 \\0 结尾的,这样可以便于调试打印,还可以直接使用 glibc 的字符串函数进行操作.\n字符串存储 字符串有两种存储方式,长度很短时,使用 emb 形式存储,长度超过 44 时,使用 raw 形式存储.\n可以使用 debug object {your_string} 来查看存储形式\n\u0026gt; set codehole abcdefghijklmnopqrstuvwxyz012345678912345678 OK \u0026gt; debug object codehole Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1 \u0026gt; set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789 OK \u0026gt; debug object codehole Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1 WHY? 首先需要解释 RedisObject, 所有 Redis 对象都有的结构体\nstruct RedisObject { int4 type; // 4bits int4 encoding; // 4bits int24 lru; // 24bits int32 refcount; // 4bytes void *ptr; // 8bytes,64-bit system } robj; 不同的对象具有不同的类型 type (4bit),同一个类型的 type 会有不同的存储形式 encoding (4bit),为了记录对象的 LRU 信息,使用了 24 个 bit 来记录 LRU 信息。每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对象内容 (body) 的具体存储位置。这样一个 RedisObject 对象头需要占据 16 字节的存储空间。\n接着我们再看 SDS 结构体的大小,在字符串比较小时,SDS 对象头的大小是 capacity+3,至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)。\n一张图解释:\nList 列表 Redis 的列表是用链表来实现的,插入删除 O (1), 查找 O (n), 列表弹出最后一个元素时,数据结构删除,内存回收.\n常用命令 LPUSH,LPOP,RPUSH,RPOP,LRANGE\u0026hellip;\n列表的数据结构 列表底层的存储结构并不是简简单单的一个链表~通过 ziplist 连接起来组成 quicklist.\nziplist 压缩列表 在列表元素较少时,redis 会使用一块连续内存来进行存储,这个结构就是 ziplist. 所有的元素紧挨着存储.\n\u0026gt; zadd z_lang 1 java 2 rust 3 go (integer) 3 \u0026gt; debug object z_lang Value at:0x7fde1c466660 refcount:1 encoding:ziplist serializedlength:34 lru:11974320 lru_seconds_idle:11 可以看到上述输出 encoding 为 ziplist.\nstruct ziplist\u0026lt;T\u0026gt; { int32 zlbytes; // 整个压缩列表占用字节数 int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点 int16 zllength; // 元素个数 T [] entries; // 元素内容列表,挨个挨个紧凑存储 int8 zlend; // 标志压缩列表的结束,值恒为 0xFF } zltail_offset 是为了支持双向遍历才设计的,可以快速定位到最后一个元素,然后倒着遍历.\nentry 会随着容纳的元素不同而结构不同.\nstruct entry { int\u0026lt;var\u0026gt; prevlen; // 前一个 entry 的字节长度 int\u0026lt;var\u0026gt; encoding; // 元素类型编码 optional byte [] content; // 元素内容 } prevlen 表示前一个 entry 的字节长度,倒序遍历时,可以根据这个字段来推算前一个 entry 的位置。它是变长的整数,字符串长度小于 254 ( 0XFE ) 时,使用一个字节表示,大于等于 254, 使用 5 个字节来表示。第一个字节是 254, 剩余四个字节表示字符串长度.\nencoding 编码类型 encoding 存储编码类型信息,ziplist 通过其来决定 content 内容的形式。所以其设计是很复杂的.\n 00xxxxxx 最大长度位 63 的短字符串,后面的 6 个位存储字符串的位数,剩余的字节就是字符串的内容。 01xxxxxx xxxxxxxx 中等长度的字符串,后面 14 个位来表示字符串的长度,剩余的字节就是字符串的内容。 10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd 特大字符串,需要使用额外 4 个字节来表示长度。第一个字节前缀是 10,剩余 6 位没有使用,统一置为零。后面跟着字符串内容。不过这样的大字符串是没有机会使用的,压缩列表通常只是用来存储小数据的。 11000000 表示 int16,后跟两个字节表示整数。 11010000 表示 int32,后跟四个字节表示整数。 11100000 表示 int64,后跟八个字节表示整数。 11110000 表示 int24,后跟三个字节表示整数。 11111110 表示 int8,后跟一个字节表示整数。 11111111 表示 ziplist 的结束,也就是 zlend 的值 0xFF。 1111xxxx 表示极小整数,xxxx 的范围只能是 (0001~1101), 也就是 1~13,因为 0000、1110、1111 都被占用了。读取到的 value 需要将 xxxx 减 1,也就是整数 0~12 就是最终的 value。 增加元素 ziplist 是连续存储的,没有多余空间,这意味着每次插入一个元素,就需要扩展内存。如果占用内存过大,重新分配内存和拷贝内存就会有很大的消耗。所以其缺点是不适合存储 大型字符串, 存储元素不宜 过多.\n级联更新 每一个 entry 都是有 prevlen, 而且时而为 1 字节存储,时而为 5 字节存储,取决于字符串的字节长度是否大于 254, 如果某次操作导致字节长度从 254 变为 256, 那么其下一个节点所存储的 prevlen 就要从 1 个字节变为 5 个字节来存储,如果下一个节点刚好因此超过了 254 的长度,那么下下个节点也要更新\u0026hellip; 这就是级联更新了~\nquicklist Redis 中 list 的存储结构就是 quicklist. 下面的 language 是一个记录编程语言的集合。可以看到 encoding 即为 quicklist.\n\u0026gt; debug object language Value at:0x7fde1c4665f0 refcount:1 encoding:quicklist serializedlength:29 lru:11974264 lru_seconds_idle:62740 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:27 Redis 的 quicklist 是一种基于 ziplist 实现的可压缩(quicklistLZF)的双向链表,结合了链表和 ziplist 的 优点 组成的。下面可以看下他的结构体.\n/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ /** * quicklist 是一个 40byte (64 位系统) 的结构 */ typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* 元素总数 */ unsigned long len; /* quicklistNode 的长度 */ int fill : 16; /* ziplist 的最大长度 */ unsigned int compress : 16; /* 节点压缩深度 */ } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; /* 没有压缩,指向 ziplist, 否则指向 quicklistLZF unsigned int sz; /* ziplist 字节总数 */ unsigned int count : 16; /* ziplist 元素数量 */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ ... } quicklistNode; //LZF 无损压缩算法,压缩过的 ziplist typedef struct quicklistLZF { // 未压缩之前的大小 unsigned int sz; /* LZF size in bytes*/ // 存放压缩过的 ziplist 数组 char compressed []; } quicklistLZF; 一张图展示结构 压缩深度 quicklist 默认的压缩深度是 0,也就是不压缩。压缩的实际深度由配置参数 list-compress-depth 决定。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。\nSet 集合 Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。\n常用命令 SADD,SMEMBERS,SPOP,SISMEMBER,SCARD\u0026hellip;\nHash 哈希 Redis 的 Hash相当于Java 中的 HashMap, 数组 + 链表的二维结构.与 HashMap 不同的地方在于 rehash 方式不同, HashMap 中的 rehash 是阻塞式的, 需要一次性全部 rehash, 而 redis 为了性能考虑, 采用的是 渐进式 rehash.\n常用命令 HSET,HGET,HMSET,HLEN\u0026hellip;\n\u0026gt; hset books java \u0026quot;think in java\u0026quot; # 命令行的字符串如果包含空格,要用引号括起来 (integer) 1 \u0026gt; hset books golang \u0026quot;concurrency in go\u0026quot; (integer) 1 \u0026gt; hset books python \u0026quot;python cookbook\u0026quot; (integer) 1 \u0026gt; hgetall books # entries(),key 和 value 间隔出现 1) \u0026quot;java\u0026quot; 2) \u0026quot;think in java\u0026quot; 3) \u0026quot;golang\u0026quot; 4) \u0026quot;concurrency in go\u0026quot; 5) \u0026quot;python\u0026quot; 6) \u0026quot;python cookbook\u0026quot; \u0026gt; hlen books (integer) 3 \u0026gt; hget books java \u0026quot;think in java\u0026quot; \u0026gt; hset books golang \u0026quot;learning go programming\u0026quot; # 因为是更新操作,所以返回 0 (integer) 0 \u0026gt; hget books golang \u0026quot;learning go programming\u0026quot; \u0026gt; hmset books java \u0026quot;effective java\u0026quot; python \u0026quot;learning python\u0026quot; golang \u0026quot;modern golang programming\u0026quot; # 批量 set OK 字典 Redis 的 Hash 是通过 dict 结构来实现的, 该结构的底层是由哈希表来实现.类似于 HashMap, 数组+链表, 超过负载因子所对应的阈值时,进行 rehash, 扩容. 在具体实现中,使用了渐进式hash的方式来避免 HashMap 这种阻塞式的 rehash, 将 rehash 的工作分摊到对字典的增删改查中.\nstruct typedef struct dictEntry { void *key; //键 union { void *val; //值 uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; //指向下一节点,形成链表 } dictEntry; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ typedef struct dictht { dictEntry **table; // 哈希表数组,数组的每一项都是 distEntry 的头结点 unsigned long size; // 哈希表的大小,也是触发扩容的阈值 unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size-1 unsigned long used; // 哈希表中实际保存的节点数量 } dictht; typedef struct dict { dictType *type; //属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数 void *privdata; // 保存了需要传给那些类型特定函数的可选参数 dictht ht[2]; // 在字典内部,维护了两张哈希表. 一般情况下,字典只使用 ht[0] 哈希表,ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用 long rehashidx; // 记录 rehash 的状态, 没有进行 rehash 则为 -1 unsigned long iterators; /* number of iterators currently running */ } dict; 一张图来表示 何时扩容? 找到dictAddRow 函数观察源码可以发现,会在 _dictExpandIfNeeded 函数中进行扩容的判断.\n/* Expand the hash table if needed */ static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ // 正在渐进式扩容, 就返回 OK if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ // 如果哈希表 ht[0] size 为 0 ,初始化, 说明 redis 是懒加载的,延长初始化策略 if (d-\u0026gt;ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the \u0026quot;safe\u0026quot; threshold, we resize doubling * the number of buckets. */ /* * 如果哈希表ht[0]中保存的key个数与哈希表大小的比例已经达到1:1,即保存的节点数已经大于哈希表大小 * 且redis服务当前允许执行rehash,或者保存的节点数与哈希表大小的比例超过了安全阈值(默认值为5) * 则将哈希表大小扩容为原来的两倍 */ if (d-\u0026gt;ht[0].used \u0026gt;= d-\u0026gt;ht[0].size \u0026amp;\u0026amp; (dict_can_resize || d-\u0026gt;ht[0].used/d-\u0026gt;ht[0].size \u0026gt; dict_force_resize_ratio)) { return dictExpand(d, d-\u0026gt;ht[0].used*2); } return DICT_OK; } 正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。\n何时缩容? 当哈希表的负载因子小于 0.1 时,自动缩容.这个操作会在 redis 的定时任务中来完成.函数为 databasesCron,该函数的作用是在后台慢慢的处理过期,rehashing, 缩容.\n执行条件: 没有子进程执行aof重写或者生成RDB文件\n/* 遍历所有的redis数据库,尝试缩容 */ for (j = 0; j \u0026lt; dbs_per_call; j++) { tryResizeHashTables(resize_db % server.dbnum); resize_db++; } /* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL * we resize the hash table to save memory */ void tryResizeHashTables(int dbid) { if (htNeedsResize(server.db[dbid].dict)) dictResize(server.db[dbid].dict); if (htNeedsResize(server.db[dbid].expires)) dictResize(server.db[dbid].expires); } /* Hash table parameters */ #define HASHTABLE_MIN_FILL 10 /* Minimal hash table fill 10% */ int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); return (size \u0026gt; DICT_HT_INITIAL_SIZE \u0026amp;\u0026amp; (used*100/size \u0026lt; HASHTABLE_MIN_FILL)); } /* Resize the table to the minimal size that contains all the elements, * but with the invariant of a USED/BUCKETS ratio near to \u0026lt;= 1 */ int dictResize(dict *d) { int minimal; if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; minimal = d-\u0026gt;ht[0].used; if (minimal \u0026lt; DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE; return dictExpand(d, minimal); } 从 htNeedsResize函数中可以看到,当哈希表保存的key数量与哈希表的大小的比例小于10%时需要缩容.最小容量为DICT_HT_INITIAL_SIZE = 4. dictResize 函数中,当正在执行 aof 重写或生成 rdb 时, dict_can_resize 会变为 0, 也就说明上面的 执行条件.\n渐进式 rehash 从上述源码中可以看出,所有的扩容或者创建都经过 dictExpand 函数.\n/* Expand or create the hash table */ int dictExpand(dict *d, unsigned long size) { /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d-\u0026gt;ht[0].used \u0026gt; size) return DICT_ERR; // 计算新的哈希表大小,获得大于等于size的第一个2次方 dictht n; /* the new hash table */ unsigned long realsize = _dictNextPower(size); /* Rehashing to the same table size is not useful. */ if (realsize == d-\u0026gt;ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ // 第一次初始化也会通过这里来完成创建 if (d-\u0026gt;ht[0].table == NULL) { d-\u0026gt;ht[0] = n; return DICT_OK; } /* Prepare a second hash table for incremental rehashing */ // ht[1] 开始派上用场,扩容时是在 ht[1] 上操作, rehash 完毕后,在交换到 ht[0] d-\u0026gt;ht[1] = n; d-\u0026gt;rehashidx = 0; return DICT_OK; } 从 dictExpand 这个函数可以发现做了这么几件事:\n 校验是否可以执行 rehash 创建一个新的哈希表 n, 分配更大的内存 将哈希表 n 复制给 ht[1], 将 rehashidx 标志置为 0 ,意味着开启了渐进式rehash. 该值也标志渐进式rehash当前已经进行到了哪个hash槽. 该函数没有将key重新 rehash 到新的 slot 上,而是交由增删改查的操作, 以及后台定时任务来处理.\n增删改查辅助rehash 看源码其实可以发现在所有增删改查的源码中,开头都会有一个判断,是否处于渐进式rehash中.\ndictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); ... } // 进入 rehash 后是 \u0026gt;=0的值 #define dictIsRehashing(d) ((d)-\u0026gt;rehashidx != -1) /* * 此函数仅执行一步hash表的重散列,并且仅当没有安全迭代器绑定到哈希表时。 * 当我们在重新散列中有迭代器时,我们不能混淆打乱两个散列表的数据,否则某些元素可能被遗漏或重复遍历。 * * 该函数被在字典中查找或更新等普通操作调用,以致字典中的数据能自动的从哈系表1迁移到哈系表2 */ static void _dictRehashStep(dict *d) { if (d-\u0026gt;iterators == 0) dictRehash(d,1); } 后台任务rehash 虽然redis实现了在读写操作时,辅助服务器进行渐进式rehash操作,但是如果服务器比较空闲,redis数据库将很长时间内都一直使用两个哈希表.所以在redis周期函数中,如果发现有字典正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作.\n还是上面缩容时使用的任务函数databasesCron.源码如下:\n/* Rehash */ if (server.activerehashing) { for (j = 0; j \u0026lt; dbs_per_call; j++) { int work_done = incrementallyRehash(rehash_db); if (work_done) { /* If the function did some work, stop here, we'll do * more at the next cron loop. */ break; } else { /* If this db didn't need rehash, we'll try the next one. */ rehash_db++; rehash_db %= server.dbnum; } } } 渐进式rehash弊端 渐进式rehash避免了redis阻塞,可以说非常完美,但是由于在rehash时,需要分配一个新的hash表,在rehash期间,同时有两个hash表在使用,会使得redis内存使用量瞬间突增,在Redis 满容状态下由于Rehash会导致大量Key驱逐.\nZset 有序集合 首先 zset 是一个 set 结构,拥有 set 的所有特性,其次他可以给每一个 value 赋予一个 score 作为权重.内部实现用的跳表(skiplist)\n常用命令 ZADD,ZRANGE,ZREVRANGE,ZSCORE,ZCARD,ZRANK\u0026hellip;\n\u0026gt; zadd books 9.0 \u0026quot;think in java\u0026quot; (integer) 1 \u0026gt; zadd books 8.9 \u0026quot;java concurrency\u0026quot; (integer) 1 \u0026gt; zadd books 8.6 \u0026quot;java cookbook\u0026quot; (integer) 1 \u0026gt; zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;java concurrency\u0026quot; 3) \u0026quot;think in java\u0026quot; \u0026gt; zrevrange books 0 -1 # 按 score 逆序列出,参数区间为排名范围 1) \u0026quot;think in java\u0026quot; 2) \u0026quot;java concurrency\u0026quot; 3) \u0026quot;java cookbook\u0026quot; \u0026gt; zcard books # 相当于 count() (integer) 3 \u0026gt; zscore books \u0026quot;java concurrency\u0026quot; # 获取指定 value 的 score \u0026quot;8.9000000000000004\u0026quot; # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题 \u0026gt; zrank books \u0026quot;java concurrency\u0026quot; # 排名 (integer) 1 \u0026gt; zrangebyscore books 0 8.91 # 根据分值区间遍历 zset 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;java concurrency\u0026quot; \u0026gt; zrangebyscore books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;8.5999999999999996\u0026quot; 3) \u0026quot;java concurrency\u0026quot; 4) \u0026quot;8.9000000000000004\u0026quot; \u0026gt; zrem books \u0026quot;java concurrency\u0026quot; # 删除 value (integer) 1 \u0026gt; zrange books 0 -1 1) \u0026quot;java cookbook\u0026quot; 2) \u0026quot;think in java\u0026quot; 数据结构 众所周知, Zset 是一个有序的set集合, redis 通过 hash table 来存储 value 和 score 的映射关系,可以达到 O(1), 通过 score 排序或者说按照 score 范围来获取这个区间的 value, 则是通过 跳表 来实现的. Zset 可以达到 O(log(N)) 的插入和读写.\n什么是跳跃列表? 如图,跳跃列表是指具有纵向高度的有序链表.跳表会随机的某提升些链表的高度,并将每一层的节点进行连接,相当于构建多级索引,这样在查找的时候,从最高层开始查,可以过滤掉一大部分的范围,有点类似于二分查找.跳表也是典型的空间换时间的方式.\n每一个 kv 块对应的结构如下面的代码中的zslnode结构,kv header 也是这个结构,只不过 value 字段是 null 值——无效的,score 是 Double.MIN_VALUE,用来垫底的。\nstruct struct zslnode { string value; double score; zslnode*[] forwards; // 多层连接指针 zslnode* backward; // 回溯指针 } struct zsl { zslnode* header; // 跳跃列表头指针 int maxLevel; // 跳跃列表当前的最高层 map\u0026lt;string, zslnode*\u0026gt; ht; // hash 结构的所有键值对 } redis中跳表的优化 允许 score 是重复的 比较不仅是通过 key(即 score), 也还会比较 data 最底层(Level 1)是有反向指针的,所以是一个双向链表,这样适用于从大到小的排序需求(ZREVRANGE) 一次查找的过程 redis中level是如何生成的? /* Returns a random level for the new skiplist node we are going to create. * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL * (both inclusive), with a powerlaw-alike distribution where higher * levels are less likely to be returned. */ int zslRandomLevel(void) { int level = 1; while ((random()\u0026amp;0xFFFF) \u0026lt; (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level\u0026lt;ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; } ZSKIPLIST_MAXLEVEL 最大值是 64, 也就是最多 64 层.ZSKIPLIST_P 为 1/4, 也就是说有 25% 的概率有机会获得level,要获得更高的level,概率更小. 这也就导致了, redis中的跳表层级不会特别高,较扁平,较低层节点较多.有个小优化的地方: 跳表会记录下当前的最高层数 MaxLevel 这样就不需要从最顶层开始遍历了.\n为什么使用跳表而不是红黑树或者哈希表? skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。 从算法实现难度上来比较,skiplist比平衡树要简单得多。 参考 渐进式 rehash 机制 美团针对Redis Rehash机制的探索和实践 zset内部实现 ","id":12,"section":"posts","summary":"\u003cp\u003e系统学习 redis 相关的知识,从数据结构开始~\u003c/p\u003e","tags":["redis","数据结构"],"title":"Redis-数据结构","uri":"https://xiaohei.im/hugo-theme-pure/2019/10/data-structure/","year":"2019"},{"content":"RabbitMQ在保证生产端与消费端的数据安全上,提供了消息确认的机制来保证. 消费端到 broker 端的确认常叫做ack机制, broker 到生产端常叫做confirm.\n消费端确认机制 Delivery Tag Delivery Tag 是 RabbitMQ 来确认消息如何发送的标志. Consumer 在注册到 RabbitMQ 上后, RabbitMQ 通过 basic.deliver 方法向消费者推送消息, 这个方法中就带着可以在 Channel中唯一识别消息的 delivery tag . Delivery Tag 是channel 隔离的.\ntag是一个大于零的增长的整型, 客户端在确认消息时将其当做参数传回来就可以保证是同一条消息的确认了.\ntag是channel隔离的, 所以必须在接受消息的channel上确认消息收到,否则会抛 unknown delivery tag的异常.\n最大值: delivery tag 是 64 位的long,最大值是 9223372036854775807. tag是channel隔离的,理论上来说是不会超过这个值的.\n确认机制 消息确认有两种模式: 自动/手动.\n自动模式会在消息一经发出就自动确认.这是在吞吐量和 可靠投递之间的权衡.如果在发送的过程中, TCP断掉了或是其他的问题,那消息就会丢掉了,这个问题需要考虑.还需要考虑的一个问题是: Consumer 消费速率如果不能跟上broker的发送速率, 会导致Consumer过载(消息堆积,内存耗尽),而在手动模式中可以通过prefetch来控制消费端的速率.有些客户端会提供TCP的背压,不能处理时,就丢弃了.\n手动模式需要Consumer端在收到消息后调用:\n basic.ack : 消息处理好了,可以丢掉了 basic.nack : 可以批量reject, 如果Consumer设置了requeue,消息还会重新回到broker的队列中 basic.reject : 消息没有处理但是也需要删除 Channel Prefetch 由于消息的发送和接收是独立的且完全异步,消息的手动确认也是完全异步的.所以这里有一个未确认消息的滑动窗口.在消费端我们经常需要控制接收消息的数量,防止出现消息缓存buffer越界的问题.此时我们就可以通过basic.qos来设置prefetch count, 该值定义了一个Channel中能存放的消息条数上限,超过这个值,RabbitMQ在收到至少一条ack之前都不能再往Channel上发送消息了.\n这里需要注意前面说的滑动窗口: 意味着当Channel满的时候,不会再往Channel上发消息,但是当你ack了一条,就会往Channel上发一条,ack了N条,就会发N条到Channel上.\nbasic.get设置prefetch是无效的,请使用basic.consume\n吞吐量影响因素: Ack机制 \u0026amp; Prefetch 确认机制的选择和Prefetch的值决定了消费端的吞吐量.一般来讲,增大Prefetch值以及 自动确认 会提升推送消息的速率,但也会增加待处理消息的堆积,消费端内存压力也会上升.\n如果Prefetch无界,Consumer在消费大量消息时没有ack会导致消费端连接的那个节点内存压力上升.所以找到一个完美的Prefetch值还是很重要的. 一般 100-300 左右吞吐量还不错,且消费端压力不大. 设置为 1 时,就很保守了,这种情况下吞吐量就很低,延迟较高.\n发布端确认机制 网络有很多种失败的方式,并且需要花时间检测.所以客户端并不能保证消息可以正常的发送到broker,正常的被处理.有可能丢了也有可能有延迟.\n根据AMQP-0-9-1, 只有通过 事务 的方式来保证.将Channel设置为事务型的,每条消息都以事务形式推送提交.但是,事务是很重,会降低吞吐量,所以RabbitMQ就换了种方式来实现: 通过模仿已有的Consumer端的确认机制.\n启用Confirm,客户端调用confirm.select即可.Broker会返回confirm.select-ok,取决于是否有no-wait设置. Channel如果设置了confirm.select,说明处于confirm模式,此时是不能设置为事务型Channel,两者不可互通.\nBroker的应答机制同Consumer一致,通过basic.ack即可,也可批量ack.\n发布端的NACK 在某些情况下,broker无法再接收消息,就会向发布端回执basic.nack,意味着消息会被丢弃,发布端需要重新发布这些消息.当Channel置为Confirm模式后,后面收到的消息都将会confirm或者nack 一次. 需要注意的几点:\n 不能保证消息何时confirm. 消息也不会同时confirm和nack 只有在Erlang进程内部报错时才会有nack Broker何时确认发布的消息? 无法路由的消息: 当确认消息不会被路由时, broker会立即发出confirm. 如果消息设置了强制(mandatory)发送,basic.return会在basic.ack之前回执. nack逻辑一致.\n可路由的消息: 所有queue接受了消息时返回basic.ack ,如果队列是持久化的,意味着持久化完成后才发出.对镜像队列(Mirrored Queues),意味着所有镜像都收到后发出.\n持久化消息的ack延迟 RabbitMQ的持久化通常是批量的,需要间隔几百毫秒来减少 fsync(2)的调用次数或者等待 queue 是空闲状态的时候,这意味着,每一次basic.ack的延迟可能达到几百毫秒.为了提高吞吐量最好是将持久化做成异步的,或者使用批量publish,这个需要参考客户端的api实现.\n确认消息的顺序 大多数情况下, RabbitMQ 会根据消息发送的顺序依次回执(要求消息发送在同一个channel上).但确认回执都是异步的,并且可以确认一条,或一组消息.确切的confirm发送时间取决于: 消息是否需要持久化,消息的路由方式.意味着不同的消息的确认时间是不同的.也就意味着返回确认的顺序并不一定相同.应用方不能将其作为一个依据.\n参考 https://www.rabbitmq.com/confirms.html#acknowledgement-modes ","id":13,"section":"posts","summary":"\u003cp\u003eRabbitMQ在保证生产端与消费端的数据安全上,提供了消息确认的机制来保证. 消费端到 \u003ccode\u003ebroker\u003c/code\u003e 端的确认常叫做\u003ccode\u003eack机制\u003c/code\u003e, \u003ccode\u003ebroker\u003c/code\u003e 到生产端常叫做\u003ccode\u003econfirm\u003c/code\u003e.\u003c/p\u003e","tags":["rabbitmq"],"title":"RabbitMQ-消息确认机制","uri":"https://xiaohei.im/hugo-theme-pure/2019/10/rabbitmq-ack-confirm/","year":"2019"},{"content":" 最近使用Hugo作为博客引擎后,闲不下来总想去找一些简单好看的主题.在官方的主题列表搜罗了一圈后,选择了yinyang,非常简单,但是用了一段时间还是想找个功能全点的,无意中瞄到了一个博主的博客,主题特别吸引我,但是是 hexo 平台的,搜了半天也没有人移植,就自己来吧~ 移植的过程中,遇到了挺多问题,也是这些问题慢慢的熟悉了hugo的模板结构.下面就来写一写自己遇到的问题~\n 页面变量参数 https://gohugo.io/variables/\n hugo的页面有基本的变量(我更愿意称为属性,根据这些属性来实现我们的主题模板.最主要的有三类:Site, Page, Taxonomy.\n.Site 站点相关的属性,即config.toml(yml)文件中的配置.\n 在页面模板中,我们可以使用{{- .Site.Autor }}这样的方式来获取你想要的站点属性.具体的站点属性可以查看https://gohugo.io/variables/site/. .Site 属于全局配置,在 作用域 得当的情况下是可以正常调用的.非正常情况我们下面再讲.\n常用属性 .Site.Pages : 获取所有文章(包含生成的一些分类页,比如说 标签页),按时间倒序排序.返回是一个数组.我们经常用来渲染一个列表.比如 归档 页面.\n .Site.Taxonomies : 获取所有的分类(这里的分类是广义上的),可以获取到按tag分类的集合,也可以获取到按category分类的集合,可以用这个属性来完成分类的页面.下面这段代码就代表着我可以拿到所有的 分类页 ,循环得到分类页的链接和标题.\n {{- range .Site.Taxonomies.categories }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;{{ .Page.Permalink }}\u0026quot;\u0026gt;{{ .Page.Title }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} .Site.Params 可以获取到我们在config.toml的Params标签下设置的内容.也是很重要的属性.比如说下面的例子.我可以设置日期的格式化样式,展示成你想要的类型. \u0026lt;p\u0026gt;{{ .Date.Format (.Site.Params.dateFormatToUse | default \u0026quot;2006-01-02\u0026quot;)}}\u0026lt;/p\u0026gt; .Page 页面中定义的属性.\n 页面属性可以大致分为两部分,一个是Hugo原生的属性,一个是每一篇文章的文件头,即front matter中的属性.具体的属性可以在https://gohugo.io/variables/page/查看. 在一个页面的作用域中使用时可以直接调用.比如我们想要知道页面的创建日期就可以直接 {{ .Date }} 即可.\n常用属性 .Date/.Title/.ReadingTime/.WordCount 见名知意 .Permalink/.RelPermalink 永久链接及相对连接 .Summary 摘要,默认70字 .Pages 为什么页面中还有一个这样的属性呢? Page是包含生成的分类页, 标签页的,所有当处于这些页面时会返回一个集合,若是我们自己真正写的文件,即markdown文件,会返回nil的. .Taxonomies 用作内容分类的管理. 我们经常在写文章时会写上 categories 或者 tags, 这些标签类目就是 .Taxonomies 的集中展示, Hugo 默认会有 categories 和 tags 两种分类. 你也可以自己再自定义设置. 具体参考: https://gohugo.io/content-management/taxonomies\n 使用案例 官方提供了多种 Template 实现常用的遍历.\n 我通常会用来写标签页(tags)和分类页(categories). 直接调用 .Taxonomies 会获得所有的分类项(即: tags, categories, 自定义分类项), .Taxonomies.tags 就可以获得所有的标签,以及标签下的所有文章.以下就是我的主题中 标签 页的实现逻辑.\n{{- $tags := .Site.Taxonomies.tags }} \u0026lt;main class=\u0026quot;main\u0026quot; role=\u0026quot;main\u0026quot;\u0026gt; \u0026lt;article class=\u0026quot;article article-tags post-type-list\u0026quot; itemscope=\u0026quot;\u0026quot;\u0026gt; \u0026lt;header class=\u0026quot;article-header\u0026quot;\u0026gt; \u0026lt;h1 itemprop=\u0026quot;name\u0026quot; class=\u0026quot;hidden-xs\u0026quot;\u0026gt;{{- .Title }}\u0026lt;/h1\u0026gt; \u0026lt;p class=\u0026quot;text-muted hidden-xs\u0026quot;\u0026gt;{{- T \u0026quot;total_tag\u0026quot; (len $tags) }}\u0026lt;/p\u0026gt; \u0026lt;nav role=\u0026quot;navigation\u0026quot; id=\u0026quot;nav-main\u0026quot; class=\u0026quot;okayNav\u0026quot;\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;{{- \u0026quot;tags\u0026quot; | relURL }}\u0026quot;\u0026gt;{{- T \u0026quot;nav_all\u0026quot; }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- range $tags }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;{{ .Page.Permalink }}\u0026quot;\u0026gt;{{ .Page.Title }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;!-- /header --\u0026gt; \u0026lt;div class=\u0026quot;article-body\u0026quot;\u0026gt; {{- range $name, $taxonomy := $tags }} \u0026lt;h3 class=\u0026quot;panel-title mb-1x\u0026quot;\u0026gt; \u0026lt;a href=\u0026quot;{{ \u0026quot;/tags/\u0026quot; | relURL}}{{ $name | urlize }}\u0026quot; title=\u0026quot;#{{- $name }}\u0026quot;\u0026gt;# {{ $name }}\u0026lt;/a\u0026gt; \u0026lt;small class=\u0026quot;text-muted\u0026quot;\u0026gt;(Total {{- .Count }} articles)\u0026lt;/small\u0026gt; \u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026quot;row\u0026quot;\u0026gt; {{- range $taxonomy }} \u0026lt;div class=\u0026quot;col-md-6\u0026quot;\u0026gt; {{ .Page.Scratch.Set \u0026quot;type\u0026quot; \u0026quot;card\u0026quot;}} {{- partial \u0026quot;item-post.html\u0026quot; . }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; \u0026lt;/article\u0026gt; \u0026lt;/main\u0026gt; 上下文传递 刚开始写 Hugo 的页面时,最让我头疼的地方就在在于此.现在想想他的逻辑是很标准的.不同的代码块上下文隔离.\n 在Hugo中,上下文的传递一般是靠.符号来完成的. 用的最多的就是再组装页面时,需要将当前页面的作用域传递到 partial 的页面中去以便组装进来的页面可以获得当前页面的属性.\n以下是我的 baseof.html 页面, 可以看到 partial 相关的代码中都有 . 符号, 这里就是将当前页面的属性传递下去了, 其他页面也就可以正常使用该页面的属性了.\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026quot;{{ .Site.Language }}\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot; /\u0026gt; \u0026lt;meta http-equiv=\u0026quot;X-UA-Compatible\u0026quot; content=\u0026quot;IE=edge,chrome=1\u0026quot; /\u0026gt; \u0026lt;title\u0026gt; {{- block \u0026quot;title\u0026quot; . -}} {{ if .IsPage }} {{ .Title }} - {{ .Site.Title }} {{ else}} {{ .Site.Title}}{{ end }} {{- end -}} \u0026lt;/title\u0026gt; {{ partial \u0026quot;head.html\u0026quot; . }} \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026quot;main-center\u0026quot; itemscope itemtype=\u0026quot;http://schema.org/WebPage\u0026quot;\u0026gt; {{- partial \u0026quot;header.html\u0026quot; .}} {{- if and (.Site.Params.sidebar) (or (ne .Params.sidebar \u0026quot;none\u0026quot;) (ne .Params.sidebar \u0026quot;custom\u0026quot;))}} {{- partial \u0026quot;sidebar.html\u0026quot; . }} {{end}} {{ block \u0026quot;content\u0026quot; . }}{{ end }} {{- partial \u0026quot;footer.html\u0026quot; . }} {{- partial \u0026quot;script.html\u0026quot; . }} \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 页面组织 baseof.html baseof 可以理解为一种模板,符合规范定义的页面都会按照 baseof.html 的框架完成最后的渲染,具体可以查看官网页, 以本次移植主题的 baseof.html 来说一下.\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026quot;{{ .Site.Language }}\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot; /\u0026gt; \u0026lt;meta http-equiv=\u0026quot;X-UA-Compatible\u0026quot; content=\u0026quot;IE=edge,chrome=1\u0026quot; /\u0026gt; \u0026lt;title\u0026gt; {{- block \u0026quot;title\u0026quot; . -}} {{ if .IsPage }} {{ .Title }} - {{ .Site.Title }} {{ else}} {{ .Site.Title}}{{ end }} {{- end -}} \u0026lt;/title\u0026gt; {{ partial \u0026quot;head.html\u0026quot; . }} \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026quot;main-center\u0026quot; itemscope itemtype=\u0026quot;http://schema.org/WebPage\u0026quot;\u0026gt; {{- partial \u0026quot;header.html\u0026quot; .}} {{- if and (.Site.Params.sidebar) (or (ne .Params.sidebar \u0026quot;none\u0026quot;) (ne .Params.sidebar \u0026quot;custom\u0026quot;))}} {{- partial \u0026quot;sidebar.html\u0026quot; . }} {{end}} {{ block \u0026quot;content\u0026quot; . }}{{ end }} {{- partial \u0026quot;footer.html\u0026quot; . }} {{- partial \u0026quot;script.html\u0026quot; . }} \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 可以看到上面的页面中就是一个完整的 HTML 结构,我在其中组装了很多页面,比如head,header,footer等等,这些在最后渲染的时候都会加入进来组成一个完整的页面.\n在上面还有一个关键字 block, 比如 {{ block \u0026quot;title\u0026quot; }}, {{ block \u0026quot;content\u0026quot;}}.该关键字允许你自定义一个模板嵌进来, 只要按照规定的方式来.比如说我的文章页 single.html.\n{{- define \u0026quot;content\u0026quot;}} \u0026lt;main class=\u0026quot;main\u0026quot; role=\u0026quot;main\u0026quot;\u0026gt; {{- partial \u0026quot;article.html\u0026quot; . }} \u0026lt;/main\u0026gt; {{- end}} 这里我们定义了 content 的模板, 和 baseof.html 的模板呼应,在渲染一篇文章时,就会将single.html 嵌入 baseof.html 生成最后的页面了.\n模板页面查询规则 Hugo要怎么知道文章页还是标签页对应的模板是什么呢?答案: 有一套以多个属性作为依据的查询各类模板的标准.具体可以查看官网页.\n以文章页来举例, Hugo 官网上的内容页寻址规则如下:\n\n由上可见,会按照该顺序依次往下找,我一般会写在layouts/_default/single.html 下,这样可以在所有页面下通用.\n这里有个小坑也是之前文档没看好遇到的: 标签页和分类页这种对应的查找规则要按照该指引.\n参考 https://harmstyler.me/posts/2019/how-to-pass-variables-to-a-partial-template-in-hugo/ https://www.qikqiak.com/post/hugo-integrated-algolia-search/ ","id":14,"section":"posts","summary":"\u003cblockquote\u003e\n\u003cp\u003e最近使用\u003ca href=\"https://gohugo.io/\"\u003eHugo\u003c/a\u003e作为博客引擎后,闲不下来总想去找一些简单好看的主题.在\u003ca href=\"https://themes.gohugo.io/\"\u003e官方的主题列表\u003c/a\u003e搜罗了一圈后,选择了\u003ca href=\"https://github.com/joway/hugo-theme-yinyang\"\u003eyinyang\u003c/a\u003e,非常简单,但是用了一段时间还是想找个功能全点的,无意中瞄到了一个博主的博客,主题特别吸引我,但是是 \u003ccode\u003ehexo\u003c/code\u003e 平台的,搜了半天也没有人移植,就自己来吧~ 移植的过程中,遇到了挺多问题,也是这些问题慢慢的熟悉了hugo的模板结构.下面就来写一写自己遇到的问题~\u003c/p\u003e\n\u003c/blockquote\u003e","tags":["hugo"],"title":"Hexo =\u003e Hugo主题移植记录","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/hugo-theme-dev-note/","year":"2019"},{"content":"rabbitmq有多种使用模式,在这里记录下不同模式的消息路由规则\n预备知识 总结的不错的文章: https://blog.csdn.net/qq_27529917/article/details/79289564\n Binding Exchange 与 队列 之间的绑定为 Binding.绑定时可以设置 binding key, 发消息时会有一个 routing key, 当 routing key 与 binding key 相同时, 这条消息才能发送到队列中去.\nExchange Type Exchange 有不同的类型, 每种类型的功能也是不一致的\n Fanout 把所有发送到该 Exchange 的消息转发到所有绑定到他的队列中\n Direct/默认(empty string) 根据 routing_key 来决定发送到具体的队列去\n Topic binding key 可以带有匹配规则.\n Headers 不依赖 binding key 和 routing key, 只根据消息中的 headers 属性来匹配\n模式列表 参考: https://www.rabbitmq.com/getstarted.html\n 直连 上图展示了 Producer 与 Consumer 通过 Queue 直连, 实际上在 rabbitmq 中是不能直连的,必须通过 Exchange 指定 routingKey 才可以. 这里我们可以使用一个默认的 Exchange (空字符串) 来绕过限制.\n工作队列 直连 属于一对一的模式,工作队列 则属于一对多, 通常用于分发耗时任务给多个Consumer.可以提升响应效率.消息的分发策略是 轮询分发 .\n发布/订阅 发布订阅模型是 RabbitMQ 的核心模式. 我们大多数也是使用它来写业务.之前的 直连/工作队列 模式, 我们并没有用到 Exchange ,都是使用默认的空exchange.但是在 发布订阅 模式中, Producer 只会把消息发到 Exchange 中,不会关注是否会发送到队列, 由 Exchange 来决定.\n发布/订阅 中的 Exchange 类型为 Fanout, 所有发到 Exchange 上的消息都会再发到绑定在这个Exchange 上的所有队列中.\n路由模式 路由模式 采用 direct 类型的 Exchange 利用 binding key 来约束发送的队列.\nTopic Topic模式 利用模式匹配,以及 .的格式来按规则过滤. * 代表只有一个词, #代表 0 或 多个.\n","id":15,"section":"posts","summary":"\u003cp\u003erabbitmq有多种使用模式,在这里记录下不同模式的消息路由规则\u003c/p\u003e","tags":["rabbitmq"],"title":"RabbitMQ-消息分发机制","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/rabbitmq-msg-distribution/","year":"2019"},{"content":" rabbitmq version: 3.7.15\n 常用操作 sbin/rabbitmq-server 启动 sbin/rabbitmq-server -detached 后台启动 sbin/rabbitmqctl shutdown/stop 关闭/停止server sbin/rabbitmqctl status 检查server状态 sbin/rabbitmq-plugins enable rabbitmq_management 开启控制台 端口 server启动后默认监听5672 控制台默认监听15672 构建集群 构建集群的方式 在config文件中声明节点信息 使用DNS发现 使用AWS实例发现(通过插件) 使用kubernetes发现(通过插件) 使用consul发现(通过插件) 使用etcd发现(通过插件) 手动执行rabbitmqctl 节点名称 节点名称是节点的身份识别证明.两部分组成: prefix \u0026amp; hostname.例如 rabbit@node1.messaging.svc.local的prefix是 rabbit ,hostname是 node1.messaging.svc.local.\n 集群中名称必须 唯一. 如果使用同一个hostname 那么prefix要保持不一致\n 集群中,节点通过节点名称互相进行识别和通信.所以hostname必须能解析.CLI Tools也要使用节点名称.\n 单机集群构建 单机运行多节点需要保证:\n 不同节点名称 \u0010RABBITMQ_NODENAME 不同存储路径 RABBITMQ_DIST_PORT 不同日志路径 RABBITMQ_LOG_BASE 不同端口,包括插件使用的 RABBITMQ_NODE_PORT rabbitmqctl 构建集群 RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit rabbitmq-server -detached RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=hare rabbitmq-server -detached # 重置正在运行的节点 rabbitmqctl -n hare stop_app # 加入集群 rabbitmqctl -n hare join_cluster rabbit@`hostname -s` rabbitmqctl -n hare start_app 每个节点若配置有其他的插件.那么每个节点插件监听的端口不能冲突,例如添加控制台\n# 首先开启控制台插件 ./rabbitmq-plugins enable rabbitmq_management RABBITMQ_NODE_PORT=5672 RABBITMQ_SERVER_START_ARGS=\u0026quot;-rabbitmq_management listener [{port,15672}]\u0026quot; RABBITMQ_NODENAME=rabbit ./rabbitmq-server -detached RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS=\u0026quot;-rabbitmq_management listener [{port,15673}]\u0026quot; RABBITMQ_NODENAME=hare ./rabbitmq-server -detached # 加入rabbit节点生成集群 rabbitmqctl -n hare stop_app # 加入集群 rabbitmqctl -n hare join_cluster rabbit@`hostname -s` rabbitmqctl -n hare start_app 以上就建了带控制台的两个节点.\n遇到的问题 添加节点进集群时,报错 ./rabbitmqctl -n rabbit2 join_cluster rabbit@`hostname -s` Clustering node rabbit2@localhost with rabbit@localhost Error: {:inconsistent_cluster, 'Node rabbit@localhost thinks it\\'s clustered with node rabbit2@localhost, but rabbit2@localhost disagrees'} 集群残留的cluster信息导致认证失败.删除${RABBIT_MQ_HOME}/var/lib/rabbitmq/mnesia文件夹.再reset节点\n建集群报错 Clustering node rabbit2@localhost with rabbit@localhost Error: {:corrupt_or_missing_cluster_files, {:error, :enoent}, {:error, :enoent}} 同上\n启动第三个节点时爆端口占用,该端口是第一个节点的控制台端口15672.没有解决 2019-09-05 15:35:42.749 [error] \u0026lt;0.555.0\u0026gt; Failed to start Ranch listener rabbit_web_dispatch_sup_15672 in ranch_tcp:listen([{cacerts,'...'},{key,'...'},{cert,'...'},{port,15672}]) for reason eaddrinuse (address already in use) 使用案例 Topic Exchange topic类型的exchange ,routing key 是按一定规则来的,通过.连接,类似于正则.有两种符号:\n * 代表一个单词 # 代表0或多个单词 如果 单单只有#号, 那么topic exchange就像fanout exchange,如果没有使用*和#,那就是direct exchange了.\n参考 docker hub rabbit mq 镜像 ","id":16,"section":"posts","summary":"","tags":["rabbitmq"],"title":"RabbitMQ-入门及高可用集群部署","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/rabbitmq-guide-and-ha-cluster/","year":"2019"},{"content":" 转载自 https://rabbitmq.mr-ping.com/AMQP/AMQP_0-9-1_Model_Explained.html\n AMQP 0-9-1 和 AMQP 模型高阶概述 AMQP是什么 AMQP(高级消息队列协议)是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信。\n消息代理和他们所扮演的角色 消息代理(message brokers)从发布者(publishers)亦称生产者(producers)那儿接收消息,并根据既定的路由规则把接收到的消息发送给处理消息的消费者(consumers)。\n由于AMQP是一个网络协议,所以这个过程中的发布者,消费者,消息代理 可以存在于不同的设备上。\nAMQP 0-9-1 模型简介 AMQP 0-9-1的工作过程如下图:消息(message)被发布者(publisher)发送给交换机(exchange),交换机常常被比喻成邮局或者邮箱。然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。\n\n发布者(publisher)发布消息时可以给消息指定各种消息属性(message meta-data)。有些属性有可能会被消息代理(brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。\n从安全角度考虑,网络是不可靠的,接收消息的应用也有可能在处理消息的时候失败。基于此原因,AMQP模块包含了一个消息确认(message acknowledgements)的概念:当一个消息从队列中投递给消费者后(consumer),消费者会通知一下消息代理(broker),这个可以是自动的也可以由处理消息的应用的开发者执行。当“消息确认”被启用的时候,消息代理不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。\n在某些情况下,例如当一个消息无法被成功路由时,消息或许会被返回给发布者并被丢弃。或者,如果消息代理执行了延期操作,消息会被放入一个所谓的死信队列中。此时,消息发布者可以选择某些参数来处理这些特殊情况。\n队列,交换机和绑定统称为AMQP实体(AMQP entities)。\nAMQP是一个可编程的协议 AMQP 0-9-1是一个可编程协议,某种意义上说AMQP的实体和路由规则是由应用本身定义的,而不是由消息代理定义。包括像声明队列和交换机,定义他们之间的绑定,订阅队列等等关于协议本身的操作。\n这虽然能让开发人员自由发挥,但也需要他们注意潜在的定义冲突。当然这在实践中很少会发生,如果发生,会以配置错误(misconfiguration)的形式表现出来。\n应用程序(Applications)声明AMQP实体,定义需要的路由方案,或者删除不再需要的AMQP实体。\n交换机和交换机类型 交换机是用来发送消息的AMQP实体。交换机拿到一个消息之后将它路由给一个或零个队列。它使用哪种路由算法是由交换机类型和被称作绑定(bindings)的规则所决定的。AMQP 0-9-1的代理提供了四种交换机\n Name(交换机类型) Default pre-declared names(预声明的默认名称) Direct exchange(直连交换机) (Empty string) and amq.direct Fanout exchange(扇型交换机) amq.fanout Topic exchange(主题交换机) amq.topic Headers exchange(头交换机) amq.match (and amq.headers in RabbitMQ) 除交换机类型外,在声明交换机时还可以附带许多其他的属性,其中最重要的几个分别是:\n Name Durability (消息代理重启后,交换机是否还存在) Auto-delete (当所有与之绑定的消息队列都完成了对此交换机的使用后,删掉它) Arguments(依赖代理本身) 交换机可以有两个状态:持久(durable)、暂存(transient)。持久化的交换机会在消息代理(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在代理再次上线后重新被声明)。然而并不是所有的应用场景都需要持久化的交换机。\n默认交换机 默认交换机(default exchange)实际上是一个由消息代理预先声明好的没有名字(名字为空字符串)的直连交换机(direct exchange)。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。\n举个栗子:当你声明了一个名为\u0026quot;search-indexing-online\u0026quot;的队列,AMQP代理会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为\u0026quot;search-indexing-online\u0026quot;。因此,当携带着名为\u0026quot;search-indexing-online\u0026quot;的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为\u0026quot;search-indexing-online\u0026quot;的队列中。换句话说,默认交换机看起来貌似能够直接将消息投递给队列,尽管技术上并没有做相关的操作。\n直连交换机 直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的。直连交换机用来处理消息的单播路由(unicast routing)(尽管它也可以处理多播路由)。下边介绍它是如何工作的:\n 将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key) 当一个携带着路由键为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。 直连交换机经常用来循环分发任务给多个工作者(workers)。当这样做的时候,我们需要明白一点,在AMQP 0-9-1中,消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)之间。\n直连型交换机图例: \n扇型交换机 扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的拷贝分别发送给这所有的N个队列。扇型用来交换机处理消息的广播路由(broadcast routing)。\n因为扇型交换机投递消息的拷贝到所有绑定到它的队列,所以他的应用案例都极其相似:\n 大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件 体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端 分发系统使用它来广播各种状态和配置更新 在群聊的时候,它被用来分发消息给参与群聊的用户。(AMQP没有内置presence的概念,因此XMPP可能会是个更好的选择) 扇型交换机图例: \n主题交换机 主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发/订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。\n主题交换机拥有非常广泛的用户案例。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的 多消费者/多应用(multiple consumers/applications) 的时候,主题交换机都可以被列入考虑范围。\n使用案例:\n 分发有关于特定地理位置的数据,例如销售点 由多个工作者(workers)完成的后台任务,每个工作者负责处理某些特定的任务 股票价格更新(以及其他类型的金融数据更新) 涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍) 云端的不同种类服务的协调 分布式架构/基于系统的软件封装,其中每个构建者仅能处理一个特定的架构或者系统。 头交换机 有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头交换机(headers exchange)就是为此而生的。头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。\n我们可以绑定一个队列到头交换机上,并给他们之间的绑定使用多个用于匹配的头(header)。这个案例中,消息代理得从应用开发者那儿取到更多一段信息,换句话说,它需要考虑某条消息(message)是需要部分匹配还是全部匹配。上边说的“更多一段消息”就是\u0026quot;x-match\u0026quot;参数。当\u0026quot;x-match\u0026quot;设置为“any”时,消息头的任意一个值被匹配就可以满足条件,而当\u0026quot;x-match\u0026quot;设置为“all”的时候,就需要消息头的所有值都匹配成功。\n头交换机可以视为直连交换机的另一种表现形式。头交换机能够像直连交换机一样工作,不同之处在于头交换机的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。\n队列 AMQP中的队列(queue)跟其他消息队列或任务队列中的队列是很相似的:它们存储着即将被应用消费掉的消息。队列跟交换机共享某些属性,但是队列也有一些另外的属性。\n Name Durable(消息代理重启后,队列依旧存在) Exclusive(只被一个连接(connection)使用,而且当连接关闭后队列即被删除) Auto-delete(当最后一个消费者退订后即被删除) Arguments(一些消息代理用他来完成类似与TTL的某些额外功能) 队列在声明(declare)后才能被使用。如果一个队列尚不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为406的通道级异常就会被抛出。\n队列名称 队列的名字可以由应用(application)来取,也可以让消息代理(broker)直接生成一个。队列的名字可以是最多255字节的一个utf-8字符串。若希望AMQP消息代理生成队列名,需要给队列的name参数赋值一个空字符串:在同一个通道(channel)的后续的方法(method)中,我们可以使用空字符串来表示之前生成的队列名称。之所以之后的方法可以获取正确的队列名是因为通道可以默默地记住消息代理最后一次生成的队列名称。\n以\u0026quot;amq.\u0026quot;开始的队列名称被预留做消息代理内部使用。如果试图在队列声明时打破这一规则的话,一个通道级的403 (ACCESS_REFUSED)错误会被抛出。\n队列持久化 持久化队列(Durable queues)会被存储在磁盘上,当消息代理(broker)重启的时候,它依旧存在。没有被持久化的队列称作暂存队列(Transient queues)。并不是所有的场景和案例都需要将队列持久化。\n持久化的队列并不会使得路由到它的消息也具有持久性。倘若消息代理挂掉了,重新启动,那么在重启的过程中持久化队列会被重新声明,无论怎样,只有经过持久化的消息才能被重新恢复。\n绑定 绑定(Binding)是交换机(exchange)将消息(message)路由给队列(queue)所需遵循的规则。如果要指示交换机“E”将消息路由给队列“Q”,那么“Q”就需要与“E”进行绑定。绑定操作需要定义一个可选的路由键(routing key)属性给某些类型的交换机。路由键的意义在于从发送给交换机的众多消息中选择出某些消息,将其路由给绑定的队列。\n打个比方:\n 队列(queue)是我们想要去的位于纽约的目的地 交换机(exchange)是JFK机场 绑定(binding)就是JFK机场到目的地的路线。能够到达目的地的路线可以是一条或者多条 拥有了交换机这个中间层,很多由发布者直接到队列难以实现的路由方案能够得以实现,并且避免了应用开发者的许多重复劳动。\n如果AMQP的消息无法路由到队列(例如,发送到的交换机没有绑定队列),消息会被就地销毁或者返还给发布者。如何处理取决于发布者设置的消息属性。\n消费者 消息如果只是存储在队列里是没有任何用处的。被应用消费掉,消息的价值才能够体现。在AMQP 0-9-1 模型中,有两种途径可以达到此目的:\n 将消息投递给应用 (\u0026quot;push API\u0026quot;) 应用根据需要主动获取消息 (\u0026quot;pull API\u0026quot;) 使用push API,应用(application)需要明确表示出它在某个特定队列里所感兴趣的,想要消费的消息。如是,我们可以说应用注册了一个消费者,或者说订阅了一个队列。一个队列可以注册多个消费者,也可以注册一个独享的消费者(当独享消费者存在时,其他消费者即被排除在外)。\n每个消费者(订阅者)都有一个叫做消费者标签的标识符。它可以被用来退订消息。消费者标签实际上是一个字符串。\n消息确认 消费者应用(Consumer applications) - 用来接受和处理消息的应用 - 在处理消息的时候偶尔会失败或者有时会直接崩溃掉。而且网络原因也有可能引起各种问题。这就给我们出了个难题,AMQP代理在什么时候删除消息才是正确的?AMQP 0-9-1 规范给我们两种建议:\n 当消息代理(broker)将消息发送给应用后立即删除。(使用AMQP方法:basic.deliver或basic.get-ok) 待应用(application)发送一个确认回执(acknowledgement)后再删除消息。(使用AMQP方法:basic.ack) 前者被称作自动确认模式(automatic acknowledgement model),后者被称作显式确认模式(explicit acknowledgement model)。在显式模式下,由消费者应用来选择什么时候发送确认回执(acknowledgement)。应用可以在收到消息后立即发送,或将未处理的消息存储后发送,或等到消息被处理完毕后再发送确认回执(例如,成功获取一个网页内容并将其存储之后)。\n如果一个消费者在尚未发送确认回执的情况下挂掉了,那AMQP代理会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。\n拒绝消息 当一个消费者接收到某条消息后,处理过程有可能成功,有可能失败。应用可以向消息代理表明,本条消息由于“拒绝消息(Rejecting Messages)”的原因处理失败了(或者未能在此时完成)。当拒绝某条消息时,应用可以告诉消息代理如何处理这条消息——销毁它或者重新放入队列。当此队列只有一个消费者时,请确认不要由于拒绝消息并且选择了重新放入队列的行为而引起消息在同一个消费者身上无限循环的情况发生。\nNegative Acknowledgements 在AMQP中,basic.reject方法用来执行拒绝消息的操作。但basic.reject有个限制:你不能使用它决绝多个带有确认回执(acknowledgements)的消息。但是如果你使用的是RabbitMQ,那么你可以使用被称作negative acknowledgements(也叫nacks)的AMQP 0-9-1扩展来解决这个问题。更多的信息请参考帮助页面\n预取消息 在多个消费者共享一个队列的案例中,明确指定在收到下一个确认回执前每个消费者一次可以接受多少条消息是非常有用的。这可以在试图批量发布消息的时候起到简单的负载均衡和提高消息吞吐量的作用。For example, if a producing application sends messages every minute because of the nature of the work it is doing.(???例如,如果生产应用每分钟才发送一条消息,这说明处理工作尚在运行。)\n注意,RabbitMQ只支持通道级的预取计数,而不是连接级的或者基于大小的预取。\n消息属性和有效载荷(消息主体) AMQP模型中的消息(Message)对象是带有属性(Attributes)的。有些属性及其常见,以至于AMQP 0-9-1 明确的定义了它们,并且应用开发者们无需费心思思考这些属性名字所代表的具体含义。例如:\n Content type(内容类型) Content encoding(内容编码) Routing key(路由键) Delivery mode (persistent or not) 投递模式(持久化 或 非持久化) Message priority(消息优先权) Message publishing timestamp(消息发布的时间戳) Expiration period(消息有效期) Publisher application id(发布应用的ID) 有些属性是被AMQP代理所使用的,但是大多数是开放给接收它们的应用解释器用的。有些属性是可选的也被称作消息头(headers)。他们跟HTTP协议的X-Headers很相似。消息属性需要在消息被发布的时候定义。\nAMQP的消息除属性外,也含有一个有效载荷 - Payload(消息实际携带的数据),它被AMQP代理当作不透明的字节数组来对待。消息代理不会检查或者修改有效载荷。消息可以只包含属性而不携带有效载荷。它通常会使用类似JSON这种序列化的格式数据,为了节省,协议缓冲器和MessagePack将结构化数据序列化,以便以消息的有效载荷的形式发布。AMQP及其同行者们通常使用\u0026quot;content-type\u0026quot; 和 \u0026quot;content-encoding\u0026quot; 这两个字段来与消息沟通进行有效载荷的辨识工作,但这仅仅是基于约定而已。\n消息能够以持久化的方式发布,AMQP代理会将此消息存储在磁盘上。如果服务器重启,系统会确认收到的持久化消息未丢失。简单地将消息发送给一个持久化的交换机或者路由给一个持久化的队列,并不会使得此消息具有持久化性质:它完全取决与消息本身的持久模式(persistence mode)。将消息以持久化方式发布时,会对性能造成一定的影响(就像数据库操作一样,健壮性的存在必定造成一些性能牺牲)。\n消息确认 由于网络的不确定性和应用失败的可能性,处理确认回执(acknowledgement)就变的十分重要。有时我们确认消费者收到消息就可以了,有时确认回执意味着消息已被验证并且处理完毕,例如对某些数据已经验证完毕并且进行了数据存储或者索引操作。\n这种情形很常见,所以 AMQP 0-9-1 内置了一个功能叫做 消息确认(message acknowledgements),消费者用它来确认消息已经被接收或者处理。如果一个应用崩溃掉(此时连接会断掉,所以AMQP代理亦会得知),而且消息的确认回执功能已经被开启,但是消息代理尚未获得确认回执,那么消息会被从新放入队列(并且在还有还有其他消费者存在于此队列的前提下,立即投递给另外一个消费者)。\n协议内置的消息确认功能将帮助开发者建立强大的软件。\nAMQP 0-9-1 方法 AMQP 0-9-1由许多方法(methods)构成。方法即是操作,这跟面向对象编程中的方法没半毛钱关系。AMQP的方法被分组在类(class)中。这里的类仅仅是对AMQP方法的逻辑分组而已。在 AMQP 0-9-1参考中有对AMQP方法的详细介绍。\n让我们来看看交换机类,有一组方法被关联到了交换机的操作上。这些方法如下所示:\n exchange.declare exchange.declare-ok exchange.delete exchange.delete-ok (请注意,RabbitMQ网站参考中包含了特用于RabbitMQ的交换机类的扩展,这里我们不对其进行讨论)\n以上的操作来自逻辑上的配对:exchange.declare 和 exchange.declare-ok,exchange.delete 和 exchange.delete-ok. 这些操作分为“请求 - requests”(由客户端发送)和“响应 - responses”(由代理发送,用来回应之前提到的“请求”操作)。\n如下的例子:客户端要求消息代理使用exchange.declare方法声明一个新的交换机: \n如上图所示,exchange.declare方法携带了好几个参数。这些参数可以允许客户端指定交换机名称、类型、是否持久化等等。\n操作成功后,消息代理使用exchange.declare-ok方法进行回应: \nexchange.declare-ok方法除了通道号之外没有携带任何其他参数(通道-channel 会在本指南稍后章节进行介绍)。\nAMQP队列类的配对方法 - queue.declare方法 和 queue.declare-ok有着与其他配对方法非常相似的一系列事件: \n\n不是所有的AMQP方法都有与其配对的“另一半”。许多(basic.publish是最被广泛使用的)都没有相对应的“响应”方法,另外一些(如basic.get)有着一种以上与之对应的“响应”方法。\n连接 AMQP连接通常是长连接。AMQP是一个使用TCP提供可靠投递的应用层协议。AMQP使用认证机制并且提供TLS(SSL)保护。当一个应用不再需要连接到AMQP代理的时候,需要优雅的释放掉AMQP连接,而不是直接将TCP连接关闭。\n通道 有些应用需要与AMQP代理建立多个连接。无论怎样,同时开启多个TCP连接都是不合适的,因为这样做会消耗掉过多的系统资源并且使得防火墙的配置更加困难。AMQP 0-9-1提供了通道(channels)来处理多连接,可以把通道理解成共享一个TCP连接的多个轻量化连接。\n在涉及多线程/进程的应用中,为每个线程/进程开启一个通道(channel)是很常见的,并且这些通道不能被线程/进程共享。\n一个特定通道上的通讯与其他通道上的通讯是完全隔离的,因此每个AMQP方法都需要携带一个通道号,这样客户端就可以指定此方法是为哪个通道准备的。\n虚拟主机 为了在一个单独的代理上实现多个隔离的环境(用户、用户组、交换机、队列 等),AMQP提供了一个虚拟主机(virtual hosts - vhosts)的概念。这跟Web servers虚拟主机概念非常相似,这为AMQP实体提供了完全隔离的环境。当连接被建立的时候,AMQP客户端来指定使用哪个虚拟主机。\nAMQP是可扩展的 AMQP 0-9-1 拥有多个扩展点:\n 定制化交换机类型 可以让开发者们实现一些开箱即用的交换机类型尚未很好覆盖的路由方案。例如 geodata-based routing。 交换机和队列的声明中可以包含一些消息代理能够用到的额外属性。例如RabbitMQ中的per-queue message TTL即是使用该方式实现。 特定消息代理的协议扩展。例如RabbitMQ所实现的扩展。 新的 AMQP 0-9-1 方法类可被引入。 消息代理可以被其他的插件扩展,例如RabbitMQ的管理前端 和 已经被插件化的HTTP API。 这些特性使得AMQP 0-9-1模型更加灵活,并且能够适用于解决更加宽泛的问题。\nAMQP 0-9-1 客户端生态系统 AMQP 0-9-1 拥有众多的适用于各种流行语言和框架的客户端。其中一部分严格遵循AMQP规范,提供AMQP方法的实现。另一部分提供了额外的技术,方便使用的方法和抽象。有些客户端是异步的(非阻塞的),有些是同步的(阻塞的),有些将这两者同时实现。有些客户端支持“供应商的特定扩展”(例如RabbitMQ的特定扩展)。\n因为AMQP的主要目标之一就是实现交互性,所以对于开发者来讲,了解协议的操作方法而不是只停留在弄懂特定客户端的库就显得十分重要。这样一来,开发者使用不同类型的库与协议进行沟通时就会容易的多。\n","id":17,"section":"posts","summary":"","tags":["rabbitmq"],"title":"AMQP消息模型","uri":"https://xiaohei.im/hugo-theme-pure/2019/09/amqp-0-9-1-model-explained/","year":"2019"},{"content":"前言 Hystrix已经不在维护了,但是成功的开源项目总是值得学习的.刚开始看 Hystrix 源码时,会发现一堆 Action,Function 的逻辑,这其实就是 RxJava 的特点了\u0026ndash;响应式编程.上篇文章已经对RxJava作过入门介绍,不熟悉的同学可以先去看看.本文会简单介绍 Hystrix,再根据demo结合源码来了解Hystrix的执行流程.\nHystrix简单介绍 什么是 Hystrix?\nHystrix 是一个延迟和容错库,旨在隔离对远程系统、服务和第三方库的访问点,停止级联故障,并在错误不可避免的复杂分布式系统中能够弹性恢复。\n 核心概念\n Command 命令\nCommand 是Hystrix的入口,对用户来说,我们只需要创建对应的 command,将需要保护的接口包装起来就可以.可以无需关注再之后的逻辑.与 Spring 深度集成后还可以通过注解的方式,就更加对开发友好了.\n Circuit Breaker 断路器\n断路器,是从电气领域引申过来的概念,具有过载、短路和欠电压保护功能,有保护线路和电源的能力.在Hystrix中即为当请求超过一定比例响应失败时,hystrix 会对请求进行拦截处理,保证服务的稳定性,以及防止出现服务之间级联雪崩的可能性.\n Isolation 隔离策略\n隔离策略是 Hystrix 的设计亮点所在,利用舱壁模式的思想来对访问的资源进行隔离,每个资源是独立的依赖,单个资源的异常不应该影响到其他. Hystrix 的隔离策略目前有两种:线程池隔离,信号量隔离.\n Hystrix的运行流程\n 官方的 How it Works 对流程有很详细的介绍,图示清晰,相信看完流程图就能对运行流程有一定的了解.\n 一次Command执行 HystrixCommand是标准的命令模式实现,每一次请求即为一次命令的创建执行经历的过程.从上述Hystrix流程图可以看出创建流程最终会指向toObservable,在之前RxJava入门时有介绍到Observable即为被观察者,作用是发送数据给观察者进行相应的,因此可以知道这个方法应该是较为关键的.\nUML HystrixInvokable 标记这个一个可执行的接口,没有任何抽象方法或常量 HystrixExecutable 是为HystrixCommand设计的接口,主要提供执行命令的抽象方法,例如:execute(),queue(),observe() HystrixObservable 是为Observable设计的接口,主要提供自动订阅(observe())和生成Observable(toObservable())的抽象方法 HystrixInvokableInfo 提供大量的状态查询(获取属性配置,是否开启断路器等) AbstractCommand 核心逻辑的实现 HystrixCommand 定制逻辑实现以及留给用户实现的接口(比如:run()) 样例代码 通过新建一个 command 来看 Hystrix 是如何创建并执行的.HystrixCommand 是一个抽象类,其中有一个run方法需要我们实现自己的业务逻辑,以下是偷懒采用匿名内部类的形式呈现.构造方法的内部实现我们就不关注了,直接看下执行的逻辑吧.\nHystrixCommand demo = new HystrixCommand\u0026lt;String\u0026gt;(HystrixCommandGroupKey.Factory.asKey(\u0026quot;demo-group\u0026quot;)) { @Override protected String run() { return \u0026quot;Hello World~\u0026quot;; } }; demo.execute(); 执行过程 流程图 这是官方给出的一次完整调用的链路.上述的 demo 中我们直接调用了execute方法,所以调用的路径为execute() -\u0026gt; queue() -\u0026gt; toObservable() -\u0026gt; toBlocking() -\u0026gt; toFuture() -\u0026gt; get().核心的逻辑其实就在toObservable()中.\nHystrixCommand.java execute execute方法为同步调用返回结果,并对异常作处理.内部会调用queue\n// 同步调用执行 public R execute() { try { // queue()返回的是Future类型的对象,所以这里是阻塞get return queue().get(); } catch (Exception e) { throw decomposeException(e); } } queue queue的第一行代码完成了核心的订阅逻辑.\n toObservable() 生成了 Hystrix 的 Observable 对象 将 Observable 转换为 BlockingObservable 可以阻塞控制数据发送 toFuture 实现对 BlockingObservable 的订阅\npublic Future\u0026lt;R\u0026gt; queue() { // 着重关注的是这行代码 // 完成了Observable的创建及订阅 // toBlocking()是将Observable转为BlockingObservable,转换后的Observable可以阻塞数据的发送 final Future\u0026lt;R\u0026gt; delegate = toObservable().toBlocking().toFuture(); final Future\u0026lt;R\u0026gt; f = new Future\u0026lt;R\u0026gt;() { // 由于toObservable().toBlocking().toFuture()返回的Future如果中断了, // 不会对当前线程进行中断,所以这里将返回的Future进行了再次包装,处理异常逻辑 ... } // 判断是否已经结束了,有异常则直接抛出 if (f.isDone()) { try { f.get(); return f; } catch (Exception e) { // 省略这段判断 } } return f; } BlockingObservable.java // 被包装的Observable private final Observable\u0026lt;? extends T\u0026gt; o; // toBlocking()会调用该静态方法将 源Observable简单包装成BlockingObservable public static \u0026lt;T\u0026gt; BlockingObservable\u0026lt;T\u0026gt; from(final Observable\u0026lt;? extends T\u0026gt; o) { return new BlockingObservable\u0026lt;T\u0026gt;(o); } public Future\u0026lt;T\u0026gt; toFuture() { return BlockingOperatorToFuture.toFuture((Observable\u0026lt;T\u0026gt;)o); } BlockingOperatorToFuture.java ReactiveX 关于toFuture的解读\nThe toFuture operator applies to the BlockingObservable subclass, so in order to use it, you must first convert your source Observable into a BlockingObservable by means of either the BlockingObservable.from method or the Observable.toBlocking operator.\n toFuture只能作用于BlockingObservable所以也才会有上文想要转换为BlockingObservable的操作\n// 该操作将 源Observable转换为返回单个数据项的Future public static \u0026lt;T\u0026gt; Future\u0026lt;T\u0026gt; toFuture(Observable\u0026lt;? extends T\u0026gt; that) { // CountDownLatch 判断是否完成 final CountDownLatch finished = new CountDownLatch(1); // 存储执行结果 final AtomicReference\u0026lt;T\u0026gt; value = new AtomicReference\u0026lt;T\u0026gt;(); // 存储错误结果 final AtomicReference\u0026lt;Throwable\u0026gt; error = new AtomicReference\u0026lt;Throwable\u0026gt;(); // single()方法可以限制Observable只发送单条数据 // 如果有多条数据 会抛 IllegalArgumentException // 如果没有数据可以发送 会抛 NoSuchElementException @SuppressWarnings(\u0026quot;unchecked\u0026quot;) final Subscription s = ((Observable\u0026lt;T\u0026gt;)that).single().subscribe(new Subscriber\u0026lt;T\u0026gt;() { // single()返回的Observable就可以对其进行标准的处理了 @Override public void onCompleted() { finished.countDown(); } @Override public void onError(Throwable e) { error.compareAndSet(null, e); finished.countDown(); } @Override public void onNext(T v) { // \u0026quot;single\u0026quot; guarantees there is only one \u0026quot;onNext\u0026quot; value.set(v); } }); // 最后将Subscription返回的数据封装成Future,实现对应的逻辑 return new Future\u0026lt;T\u0026gt;() { // 可以查看源码 }; } AbstractCommand.java AbstractCommand是toObservable实现的地方,属于Hystrix的核心逻辑,代码较长,可以和方法调用的流程图一起食用.toObservable主要是完成缓存和创建Observable,requestLog的逻辑,当第一次创建Observable时,applyHystrixSemantics方法是Hystrix的语义实现,可以跳着看.\n tips: 下文中有很多 Action和 Function,他们很相似,都有call方法,但是区别在于Function有返回值,而Action没有,方法后跟着的数字代表有几个入参.Func0/Func3即没有入参和有三个入参\n toObservable toObservable代码较长且分层还是清晰的,所以下面一块一块写.其逻辑和文章开始提到的Hystrix流程图是完全一致的.\npublic Observable\u0026lt;R\u0026gt; toObservable() { final AbstractCommand\u0026lt;R\u0026gt; _cmd = this; // 此处省略掉了很多个Action和Function,大部分是来做扫尾清理的函数,所以用到的时候再说 // defer在上篇rxjava入门中提到过,是一种创建型的操作符,每次订阅时会产生新的Observable,回调方法中所实现的才是真正我们需要的Observable return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { // 校验命令的状态,保证其只执行一次 if (!commandState.compareAndSet(CommandState.NOT_STARTED, CommandState.OBSERVABLE_CHAIN_CREATED)) { IllegalStateException ex = new IllegalStateException(\u0026quot;This instance can only be executed once. Please instantiate a new instance.\u0026quot;); //TODO make a new error type for this throw new HystrixRuntimeException(FailureType.BAD_REQUEST_EXCEPTION, _cmd.getClass(), getLogMessagePrefix() + \u0026quot; command executed multiple times - this is not permitted.\u0026quot;, ex, null); } commandStartTimestamp = System.currentTimeMillis(); // properties为当前command的所有属性 // 允许记录请求log时会保存当前执行的command if (properties.requestLogEnabled().get()) { // log this command execution regardless of what happened if (currentRequestLog != null) { currentRequestLog.addExecutedCommand(_cmd); } } // 是否开启了请求缓存 final boolean requestCacheEnabled = isRequestCachingEnabled(); // 获取缓存key final String cacheKey = getCacheKey(); // 开启缓存后,尝试从缓存中取 if (requestCacheEnabled) { HystrixCommandResponseFromCache\u0026lt;R\u0026gt; fromCache = (HystrixCommandResponseFromCache\u0026lt;R\u0026gt;) requestCache.get(cacheKey); if (fromCache != null) { isResponseFromCache = true; return handleRequestCacheHitAndEmitValues(fromCache, _cmd); } } // 没有开启请求缓存时,就执行正常的逻辑 Observable\u0026lt;R\u0026gt; hystrixObservable = // 这里又通过defer创建了我们需要的Observable Observable.defer(applyHystrixSemantics) // 发送前会先走一遍hook,默认executionHook是空实现的,所以这里就跳过了 .map(wrapWithAllOnNextHooks); // 得到最后的封装好的Observable后,将其放入缓存 if (requestCacheEnabled \u0026amp;\u0026amp; cacheKey != null) { // wrap it for caching HystrixCachedObservable\u0026lt;R\u0026gt; toCache = HystrixCachedObservable.from(hystrixObservable, _cmd); HystrixCommandResponseFromCache\u0026lt;R\u0026gt; fromCache = (HystrixCommandResponseFromCache\u0026lt;R\u0026gt;) requestCache.putIfAbsent(cacheKey, toCache); if (fromCache != null) { // another thread beat us so we'll use the cached value instead toCache.unsubscribe(); isResponseFromCache = true; return handleRequestCacheHitAndEmitValues(fromCache, _cmd); } else { // we just created an ObservableCommand so we cast and return it afterCache = toCache.toObservable(); } } else { afterCache = hystrixObservable; } return afterCache // 终止时的操作 .doOnTerminate(terminateCommandCleanup) // perform cleanup once (either on normal terminal state (this line), or unsubscribe (next line)) // 取消订阅时的操作 .doOnUnsubscribe(unsubscribeCommandCleanup) // perform cleanup once // 完成时的操作 .doOnCompleted(fireOnCompletedHook); } } handleRequestCacheHitAndEmitValues 缓存击中时的处理\nprivate Observable\u0026lt;R\u0026gt; handleRequestCacheHitAndEmitValues(final HystrixCommandResponseFromCache\u0026lt;R\u0026gt; fromCache, final AbstractCommand\u0026lt;R\u0026gt; _cmd) { try { // Hystrix中有大量的hook 如果有心做二次开发的,可以利用这些hook做到很完善的监控 executionHook.onCacheHit(this); } catch (Throwable hookEx) { logger.warn(\u0026quot;Error calling HystrixCommandExecutionHook.onCacheHit\u0026quot;, hookEx); } // 将缓存的结果赋给当前command return fromCache.toObservableWithStateCopiedInto(this) // doOnTerminate 或者是后面看到的doOnUnsubscribe,doOnError,都指的是在响应onTerminate/onUnsubscribe/onError后的操作,即在Observable的生命周期上注册一个动作优雅的处理逻辑 .doOnTerminate(new Action0() { @Override public void call() { // 命令最终状态的不同进行不同处理 if (commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.TERMINAL)) { cleanUpAfterResponseFromCache(false); //user code never ran } else if (commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.TERMINAL)) { cleanUpAfterResponseFromCache(true); //user code did run } } }) .doOnUnsubscribe(new Action0() { @Override public void call() { // 命令最终状态的不同进行不同处理 if (commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.UNSUBSCRIBED)) { cleanUpAfterResponseFromCache(false); //user code never ran } else if (commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.UNSUBSCRIBED)) { cleanUpAfterResponseFromCache(true); //user code did run } } }); } applyHystrixSemantics 因为本片文章的主要目的是在讲执行流程,所以失败回退和断路器相关的就留到以后的文章中再写.\nfinal Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt; applyHystrixSemantics = new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { // 不再订阅了就返回不发送数据的Observable if (commandState.get().equals(CommandState.UNSUBSCRIBED)) { // 不发送任何数据或通知 return Observable.never(); } return applyHystrixSemantics(_cmd); } }; private Observable\u0026lt;R\u0026gt; applyHystrixSemantics(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { // 标记开始执行的hook // 如果hook内抛异常了,会快速失败且没有fallback处理 executionHook.onStart(_cmd); /* determine if we're allowed to execute */ // 断路器核心逻辑: 判断是否允许执行(TODO) if (circuitBreaker.allowRequest()) { // Hystrix自己造的信号量轮子,之所以不用juc下,官方解释为juc的Semphore实现太复杂,而且没有动态调节的信号量大小的能力,简而言之,不满足需求! // 根据不同隔离策略(线程池隔离/信号量隔离)获取不同的TryableSemphore final TryableSemaphore executionSemaphore = getExecutionSemaphore(); // Semaphore释放标志 final AtomicBoolean semaphoreHasBeenReleased = new AtomicBoolean(false); // 释放信号量的Action final Action0 singleSemaphoreRelease = new Action0() { @Override public void call() { if (semaphoreHasBeenReleased.compareAndSet(false, true)) { executionSemaphore.release(); } } }; // 异常处理 final Action1\u0026lt;Throwable\u0026gt; markExceptionThrown = new Action1\u0026lt;Throwable\u0026gt;() { @Override public void call(Throwable t) { // HystrixEventNotifier是hystrix的插件,不同的事件发送不同的通知,默认是空实现. eventNotifier.markEvent(HystrixEventType.EXCEPTION_THROWN, commandKey); } }; // 线程池隔离的TryableSemphore始终为true if (executionSemaphore.tryAcquire()) { try { /* used to track userThreadExecutionTime */ // executionResult是一次命令执行的结果信息封装 // 这里设置起始时间是为了记录命令的生命周期,执行过程中会set其他属性进去 executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis()); return executeCommandAndObserve(_cmd) // 报错时的处理 .doOnError(markExceptionThrown) // 终止时释放 .doOnTerminate(singleSemaphoreRelease) // 取消订阅时释放 .doOnUnsubscribe(singleSemaphoreRelease); } catch (RuntimeException e) { return Observable.error(e); } } else { // tryAcquire失败后会做fallback处理,TODO return handleSemaphoreRejectionViaFallback(); } } else { // 断路器短路(拒绝请求)fallback处理 TODO return handleShortCircuitViaFallback(); } } executeCommandAndObserve /** * 执行run方法的地方 */ private Observable\u0026lt;R\u0026gt; executeCommandAndObserve(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { // 获取当前上下文 final HystrixRequestContext currentRequestContext = HystrixRequestContext.getContextForCurrentThread(); // 发送数据时的Action响应 final Action1\u0026lt;R\u0026gt; markEmits = new Action1\u0026lt;R\u0026gt;() { @Override public void call(R r) { // 如果onNext时需要上报时,做以下处理 if (shouldOutputOnNextEvents()) { // result标记 executionResult = executionResult.addEvent(HystrixEventType.EMIT); // 通知 eventNotifier.markEvent(HystrixEventType.EMIT, commandKey); } // commandIsScalar是一个我不解的地方,在网上也没有查到好的解释 // 该方法为抽象方法,有HystrixCommand实现返回true.HystrixObservableCommand返回false if (commandIsScalar()) { // 耗时 long latency = System.currentTimeMillis() - executionResult.getStartTimestamp(); // 通知 eventNotifier.markCommandExecution(getCommandKey(), properties.executionIsolationStrategy().get(), (int) latency, executionResult.getOrderedList()); eventNotifier.markEvent(HystrixEventType.SUCCESS, commandKey); executionResult = executionResult.addEvent((int) latency, HystrixEventType.SUCCESS); // 断路器标记成功(断路器半开时的反馈,决定是否关闭断路器) circuitBreaker.markSuccess(); } } }; final Action0 markOnCompleted = new Action0() { @Override public void call() { if (!commandIsScalar()) { // 同markEmits 类似处理 } } }; // 失败回退的逻辑 final Func1\u0026lt;Throwable, Observable\u0026lt;R\u0026gt;\u0026gt; handleFallback = new Func1\u0026lt;Throwable, Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call(Throwable t) { // 不是重点略过了 } }; // 请求上下文的处理 final Action1\u0026lt;Notification\u0026lt;? super R\u0026gt;\u0026gt; setRequestContext = new Action1\u0026lt;Notification\u0026lt;? super R\u0026gt;\u0026gt;() { @Override public void call(Notification\u0026lt;? super R\u0026gt; rNotification) { setRequestContextIfNeeded(currentRequestContext); } }; Observable\u0026lt;R\u0026gt; execution; // 如果有执行超时限制,会将包装后的Observable再转变为支持TimeOut的 if (properties.executionTimeoutEnabled().get()) { // 根据不同的隔离策略包装为不同的Observable execution = executeCommandWithSpecifiedIsolation(_cmd) // lift 是rxjava中一种基本操作符 可以将Observable转换成另一种Observable // 包装为带有超时限制的Observable .lift(new HystrixObservableTimeoutOperator\u0026lt;R\u0026gt;(_cmd)); } else { execution = executeCommandWithSpecifiedIsolation(_cmd); } return execution.doOnNext(markEmits) .doOnCompleted(markOnCompleted) .onErrorResumeNext(handleFallback) .doOnEach(setRequestContext); } executeCommandWithSpecifiedIsolation 根据不同的隔离策略创建不同的执行Observable\nprivate Observable\u0026lt;R\u0026gt; executeCommandWithSpecifiedIsolation(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { if (properties.executionIsolationStrategy().get() == ExecutionIsolationStrategy.THREAD) { // mark that we are executing in a thread (even if we end up being rejected we still were a THREAD execution and not SEMAPHORE) return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { // 由于源码太长,这里只关注正常的流程,需要详细了解可以去看看源码 if (threadState.compareAndSet(ThreadState.NOT_USING_THREAD, ThreadState.STARTED)) { try { return getUserExecutionObservable(_cmd); } catch (Throwable ex) { return Observable.error(ex); } } else { //command has already been unsubscribed, so return immediately return Observable.error(new RuntimeException(\u0026quot;unsubscribed before executing run()\u0026quot;)); } }}) .doOnTerminate(new Action0() {}) .doOnUnsubscribe(new Action0() {}) // 指定在某一个线程上执行,是rxjava中很重要的线程调度的概念 .subscribeOn(threadPool.getScheduler(new Func0\u0026lt;Boolean\u0026gt;() { })); } else { // 信号量隔离策略 return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { // 逻辑与线程池大致相同 }); } } getUserExecutionObservable 获取用户执行的逻辑\nprivate Observable\u0026lt;R\u0026gt; getUserExecutionObservable(final AbstractCommand\u0026lt;R\u0026gt; _cmd) { Observable\u0026lt;R\u0026gt; userObservable; try { // getExecutionObservable是抽象方法,有HystrixCommand自行实现 userObservable = getExecutionObservable(); } catch (Throwable ex) { // the run() method is a user provided implementation so can throw instead of using Observable.onError // so we catch it here and turn it into Observable.error userObservable = Observable.error(ex); } // 将Observable作其他中转 return userObservable .lift(new ExecutionHookApplication(_cmd)) .lift(new DeprecatedOnRunHookApplication(_cmd)); } lift操作符\nlift可以转换成一个新的Observable,它很像一个代理,将原来的Observable代理到自己这里,订阅时通知原来的Observable发送数据,经自己这里流转加工处理再返回给订阅者.Map/FlatMap操作符底层其实就是用的lift进行实现的.\ngetExecutionObservable @Override final protected Observable\u0026lt;R\u0026gt; getExecutionObservable() { return Observable.defer(new Func0\u0026lt;Observable\u0026lt;R\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;R\u0026gt; call() { try { // just操作符就是直接执行的Observable // run方法就是我们实现的业务逻辑: Hello World~ return Observable.just(run()); } catch (Throwable ex) { return Observable.error(ex); } } }).doOnSubscribe(new Action0() { @Override public void call() { // 执行订阅时将执行线程记为当前线程,必要时我们可以interrupt executionThread.set(Thread.currentThread()); } }); } 总结 希望自己能把埋下的坑一一填完: 容错机制,metrics,断路器等等\u0026hellip;\n参考 Hystrix How it Works ReactiveX官网 阮一峰: 中文技术文档写作规范 RxJava lift 原理解析 ","id":18,"section":"posts","summary":"","tags":["rxjava","hystrix"],"title":"Hystrix命令执行流程","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/rxjava-in-hystrix/","year":"2019"},{"content":" 本文基于 rxjava 1.x 版本\n 前言 写这篇文章是因为之前在看Hystrix时,觉得响应式编程很有意思,之前也了解到Spring5主打特性就是响应式,就想来试试水,入个门.本文主要介绍RxJava的特点,入门操作\nRxJava是什么 Reactive X ReactiveX是使用Observable序列来组合异步操作且基于事件驱动的一个库.其继承自观察者模式来支持数据流或者事件流通过添加操作符(operators)的方式来声明式的操作,并抽象出对低级别线程(low-level thread),同步,线程安全,并发数据结构,非阻塞IO问题的关注.\nReactiveX 在不同语言中都有实现,RxJava 只是在JVM上实现的一套罢了.\n概念 观察者模式是该框架的灵魂~\n 上图可以表述为: 观察者(Observer) 订阅(subscribe)被观察者(Observable),当Observable产生事件或数据时,会调用Observer的方法进行回调.\n听起来有点别扭,这里举一个形象点的例子.\n显示器开关\n显示器开关即为 Observable, 显示器为 Observer,这两个组件就会形成联系.当开关按下时,显示器就会通电点亮,这里即可抽象成Observable发出一个事件,Observer对事件做了处理.做什么样的处理其实在Subscribe时就已经决定了.\n回调方法\n在subscribe时会要求实现对应的回调方法,标准方法有以下三个:\n onNext Observable调用这个方法发射数据,方法的参数就是Observable发射的数据,这个方法可能会被调用多次,取决于你的实现。\n onError 当Observable遇到错误或者无法返回期望的数据时会调用这个方法,这个调用会终止Observable,后续不会再调用onNext和onCompleted,onError方法的参数是抛出的异常。\n onCompleted 正常终止,如果没有遇到错误,Observable在最后一次调用onNext之后调用此方法。\n\u0026ldquo;Hot\u0026rdquo; or \u0026ldquo;Cold\u0026rdquo; Observables Observable何时开始发送数据呢?基于此问题,可以将Observable分为两类: Hot \u0026amp; Cold . 可以理解为主动型和被动型.\nHot Observable: Observable一经创建,就会开始发送数据. 所以后面订阅的Observer可能消费不到Observable完整的数据.\nCold Observable: Observable会等到有Observer订阅时才开始发送数据,此时Observer会消费到完整的数据\nRxJava入门 Hello World Observable.create(new Observable.OnSubscribe\u0026lt;String\u0026gt;() { @Override public void call(Subscriber\u0026lt;? super String\u0026gt; subscriber) { subscriber.onNext(\u0026quot;Hello World\u0026quot;); subscriber.onCompleted(); //subscriber.onError(new RuntimeException(\u0026quot;error\u0026quot;)); } }).subscribe(new Subscriber\u0026lt;String\u0026gt;() { @Override public void onCompleted() { System.out.println(\u0026quot;观察结束啦~~~\u0026quot;); } @Override public void onError(Throwable e) { System.out.println(\u0026quot;观察出错啦~~~\u0026quot;); } @Override public void onNext(String s) { System.out.println(\u0026quot;onNext:\u0026quot; + s); } }); } // onNext:Hello World // 观察结束啦~~~ // 注释掉上一行 打开下一行注释 就会输出 // onNext:Hello World // 观察出错啦~~~ 上述即为一个标准的创建观察者被观察者并订阅,实现订阅逻辑.\n疑问\n 为什么subscribe方法的参数是Subscriber呢? 在rxjava中Observer是接口,Subscriber实现了Observer并提供了拓展.所以普遍用这个.\n 为什么是Observable.subscribe(Observer)?用上面的显示器开关的例子来说就相当于显示器开关订阅显示器. 为了保证流式风格~rxjava提供了一系列的操作符来对Observable发出的数据做处理,流式风格可以使操作符使用起来更友好.所以就当做Observable订阅了Observer吧🤦‍♂\n操作符 Operators 单纯的使用上面的Hello World撸码只能说是观察者模式的运用罢了,操作符才是ReactiveX最强大的地方.我们可以通过功能不同的操作符对Observable发出的数据做过滤(filter),转换(map)来满足业务的需求.其实就可以当作是Java8的lambda特性.\n Observable在经过操作符处理后还是一个Observable,对应上述的流式风格\n 案例: 假设我们需要监听鼠标在一个直角坐标系中的点击,取得所有在第一象限点击的坐标.\n从该流程图可以看出,鼠标点击后会发出很多数据,一次点击一个点,我们对数据进行filter,得到了下方时间轴上的数据源.这就是我们想要的.下面来看下常用的操作符有哪些?\n创建型操作符 用于创建Observable对象的操作符\n Create 创建一个Observable,需要传递一个Function来完成调用Observer的逻辑.\n一个标准的Observable必须只能调用一次(Exactly Once)onCompleted或者onError,并且在调用后不能再调用Observer的其他方法(eg: onNext).\nsample code\nObservable.create(new Observable.OnSubscribe\u0026lt;Integer\u0026gt;() { @Override public void call(Subscriber\u0026lt;? super Integer\u0026gt; observer) { try { if (!observer.isUnsubscribed()) { for (int i = 1; i \u0026lt; 5; i++) { observer.onNext(i); } observer.onCompleted(); } } catch (Exception e) { observer.onError(e); } } } ).subscribe(new Subscriber\u0026lt;Integer\u0026gt;() { @Override public void onNext(Integer item) { System.out.println(\u0026quot;Next: \u0026quot; + item); } @Override public void onError(Throwable error) { System.err.println(\u0026quot;Error: \u0026quot; + error.getMessage()); } @Override public void onCompleted() { System.out.println(\u0026quot;Sequence complete.\u0026quot;); } }); Next: 1 Next: 2 Next: 3 Next: 4 Sequence complete. Defer 直到有Observer订阅时才会创建,并且会为每一个Observer创建新的Observable,这样可以保证所有Observer可以看到相同的数据,并且从头开始消费.\nsample code\nObservable\u0026lt;String\u0026gt; defer = Observable.defer(new Func0\u0026lt;Observable\u0026lt;String\u0026gt;\u0026gt;() { @Override public Observable\u0026lt;String\u0026gt; call() { return Observable.just(\u0026quot;Hello\u0026quot;, \u0026quot;World\u0026quot;); } }); defer.subscribe(new Subscriber\u0026lt;String\u0026gt;() { @Override public void onCompleted() { System.out.println(\u0026quot;第一个订阅完成啦~\u0026quot;); } @Override public void onError(Throwable e) { System.out.println(\u0026quot;第一个订阅报错啦~\u0026quot;); } @Override public void onNext(String s) { System.out.println(\u0026quot;第一个订阅收到:\u0026quot; + s); } }); defer.subscribe(new Subscriber\u0026lt;String\u0026gt;() { //与上一个订阅逻辑相同 }); 第一个订阅收到:Hello 第一个订阅收到:World 第一个订阅完成啦~ 第二个订阅收到:Hello 第二个订阅收到:World 第二个订阅完成啦~ Note:\nDefer在RxJava中的实现其实有点像指派,可以看到构建时,传参为Func0\u0026lt;Observable\u0026lt;T\u0026gt;\u0026gt;,Observer真正订阅的是传参中的Observable.\nJust 在上文Defer中代码中就用了Just,指的是可以发送特定的数据.代码一致就不作展示了.\nInterval 可以按照指定时间间隔从0开始发送无限递增序列.\n参数 initalDelay 延迟多长时间开始第一次发送 period 指定时间间隔 unit 时间单位 如下例子:延迟0秒后开始发送,每1秒发送一次. 因为sleep 100秒,会发送0-99终止\nsample code\nObservable.interval(0,1,TimeUnit.SECONDS).subscribe(new Action1\u0026lt;Long\u0026gt;() { // 这里只实现了OnNext方法,onError和onCompleted可以有默认实现.一种偷懒写法 @Override public void call(Long aLong) { System.out.println(aLong); } }); try { //阻塞当前线程使程序一直跑 TimeUnit.SECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } 转换操作符 将Observable发出的数据进行各类转换的操作符\n Buffer 如上图所示,buffer定期将数据收集到集合中,并将集合打包发送.\nsample code\nObservable.just(2,3,5,6) .buffer(3) .subscribe(new Action1\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt;() { @Override public void call(List\u0026lt;Integer\u0026gt; integers) { System.out.println(integers); } }); [2, 3, 5] [6] Window\nwindow和buffer是非常像的两个操作符,区别在于buffer会将存起来的item打包再发出去,而window则只是单纯的将item堆起来,达到阈值再发出去,不对原数据结构做修改.\nsample code\nObservable.just(2,3,5,6) .window(3) .subscribe(new Action1\u0026lt;Observable\u0026lt;Integer\u0026gt;\u0026gt;() { @Override public void call(Observable\u0026lt;Integer\u0026gt; integerObservable) { integerObservable.subscribe(new Action1\u0026lt;Integer\u0026gt;() { @Override public void call(Integer integer) { // do anything } }); } }); 合并操作符 将多个Observable合并为一个的操作符\n Zip 使用一个函数组合多个Observable发射的数据集合,然后再发射这个结果。如果多个Observable发射的数据量不一样,则以最少的Observable为标准进行组合.\nsample code\nObservable\u0026lt;Integer\u0026gt; observable1=Observable.just(1,2,3,4); Observable\u0026lt;Integer\u0026gt; observable2=Observable.just(4,5,6); Observable.zip(observable1, observable2, new Func2\u0026lt;Integer, Integer, String\u0026gt;() { @Override public String call(Integer item1, Integer item2) { return item1+\u0026quot;and\u0026quot;+item2; } }).subscribe(new Action1\u0026lt;String\u0026gt;() { @Override public void call(String s) { System.out.println(s); } }); 1and4 2and5 3and6 背压操作符 用于平衡Observer消费速度,Observable生产速度的操作符\n 背压是指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略.下图可以很好阐释背压机制是如何运行的.\n宗旨就是下游告诉上游我能处理多少你就给我发多少.\n//被观察者将产生100000个事件 Observable observable=Observable.range(1,100000); observable.observeOn(Schedulers.newThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(Object o) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\u0026quot;on Next Request...\u0026quot;); request(1); } }); 背压支持 上述样例代码中创建Observable使用的是range操作符,这是因为他是支持背压的,如果用interval,request的方法将不起作用.因为interval不支持背压.那什么样的Observable支持背压呢?\n在前面介绍概念时,有提到过Hot\u0026amp;Cold的区别,Hot类型的Observable,即一经创建就开始发送,不支持背压,Cold类型的Observable也只是部分支持.\nonBackpressurebuffer/onBackpressureDrop 不支持背压的操作符我们可以如何实现背压呢?就通过onBackpressurebuffer/onBackpressureDrop来实现.顾名思义一个是缓存,一个是丢弃.\n这里以drop方式来展示.\nObservable.interval(1, TimeUnit.MILLISECONDS) .onBackpressureDrop() //指定observer调度io线程上,并将缓存size置为1,这个缓存会提前将数据存好在消费, //默认在PC上是128,设置小一点可以快速的看到drop的效果 .observeOn(Schedulers.io(), 1) .subscribe(new Subscriber\u0026lt;Long\u0026gt;() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { System.out.println(\u0026quot;Error:\u0026quot; + e.getMessage()); } @Override public void onNext(Long aLong) { System.out.println(\u0026quot;订阅 \u0026quot; + aLong); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }) 订阅 0 订阅 103 订阅 207 订阅 300 订阅 417 订阅 519 订阅 624 订阅 726 订阅 827 订阅 931 订阅 1035 订阅 1138 订阅 1244 订阅 1349 可以很明显的看出很多数据被丢掉了,这就是背压的效果.\n总结 写了这么多后,想来说说自己的感受.\n 转变思想: 响应式编程的思想跟我们现在后端开发思路是有区别的.可能刚开始会不适应. 不易调试: 流式风格写着爽,调着难 参考 ReactiveX官网\n关于RxJava最友好的文章——背压(Backpressure)\n如何形象地描述RxJava中的背压和流控机制?\n","id":19,"section":"posts","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文基于 rxjava 1.x 版本\u003c/p\u003e\n\u003c/blockquote\u003e","tags":["rxjava","响应式编程"],"title":"RxJava入门","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/rxjava-guide/","year":"2019"},{"content":"Given an array, rotate the array to the right by k steps, where k is non-negative.\nExample 1:\nInput: [1,2,3,4,5,6,7] and k = 3 Output: [5,6,7,1,2,3,4] Explanation: rotate 1 steps to the right: [7,1,2,3,4,5,6] rotate 2 steps to the right: [6,7,1,2,3,4,5] rotate 3 steps to the right: [5,6,7,1,2,3,4] Example 2:\nInput: [-1,-100,3,99] and k = 2 Output: [3,99,-1,-100] Explanation: rotate 1 steps to the right: [99,-1,-100,3] rotate 2 steps to the right: [3,99,-1,-100] Note:\n Try to come up as many solutions as you can, there are at least 3 different ways to solve this problem. Could you do it in-place with O(1) extra space? 思路\n依次反转前半部分及后半部分,最后反转整个数组\neg: 1,2,3,4,5,6,7 k=3\n 反转前半部分 4,3,2,1,5,6,7\n 反转后半部分 4,3,2,1,7,6,5\n 反转整个数组 5,6,7,1,2,3,4\nSolution 1\npub fn rotate(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, k: i32) { if nums.is_empty() || k \u0026lt;= 0 { return; } let o_len = nums.len(); let mod_k = k as usize % o_len; reverse(nums, 0, o_len - mod_k - 1); reverse(nums, o_len - mod_k, o_len - 1); reverse(nums, 0, o_len - 1); } pub fn reverse(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, start: usize, end: usize) { let mut o_start = start; let mut o_end = end; while o_start \u0026lt; o_end { nums.swap(o_start, o_end); o_start += 1; o_end -= 1; } } Solution 2\n api 解法,效率不高,但好看\n pub fn rotate(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, k: i32) { if nums.is_empty() { return; } let mod_k = k % nums.len() as i32; for _ in 0..mod_k as usize { let item = nums.pop().unwrap(); nums.insert(0, item); } } ","id":20,"section":"posts","summary":"","tags":["leetcode","rust"],"title":"[LeetCode In Rust]189-Rotate Array","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/189-rotate-array/","year":"2019"},{"content":"pub fn remove_duplicates(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;) -\u0026gt; i32 { if nums.is_empty() { return 0 } let mut idx = 0; for i in idx .. nums.len() { if nums[i].gt(\u0026amp;nums[idx]) { idx += 1; nums.swap(i,idx); } } (idx + 1) as i32 } ","id":21,"section":"posts","summary":"","tags":["leetcode","rust"],"title":"[LeetCode In Rust]026-Remove Duplicates From Sorted Array","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/026-remove-duplicates-from-sorted-array/","year":"2019"},{"content":"pub fn two_sum(nums: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let map: HashMap\u0026lt;i32, usize\u0026gt; = nums.iter().enumerate().map(|(idx, \u0026amp;data)| (data, idx)).collect(); nums.iter().enumerate().find(|(idx, \u0026amp;num)| { match map.get(\u0026amp;(target - num)) { Some(\u0026amp;idx_in_map) =\u0026gt; idx_in_map != *idx, None =\u0026gt; false, } }).map(|(idx, \u0026amp;num)| vec![*map.get(\u0026amp;(target - num)).unwrap() as i32, idx as i32]).unwrap() } pub fn two_sum_v2(nums: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let map: HashMap\u0026lt;i32, usize\u0026gt; = nums.iter().enumerate().map(|(idx, \u0026amp;data)| (data, idx)).collect(); for (i,\u0026amp;num) in nums.iter().enumerate() { match map.get(\u0026amp;(target - num) ) { Some(\u0026amp;x) =\u0026gt; { if i != x { return vec![i as i32, x as i32] } }, None =\u0026gt; continue, } } vec![] } ","id":22,"section":"posts","summary":"","tags":["leetcode","rust"],"title":"[LeetCode In Rust]001-Two Sum","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/001-two-sum/","year":"2019"}],"tags":[{"title":"collections","uri":"https://xiaohei.im/hugo-theme-pure/tags/collections/"},{"title":"hugo","uri":"https://xiaohei.im/hugo-theme-pure/tags/hugo/"},{"title":"hystrix","uri":"https://xiaohei.im/hugo-theme-pure/tags/hystrix/"},{"title":"leetcode","uri":"https://xiaohei.im/hugo-theme-pure/tags/leetcode/"},{"title":"netty","uri":"https://xiaohei.im/hugo-theme-pure/tags/netty/"},{"title":"rabbitmq","uri":"https://xiaohei.im/hugo-theme-pure/tags/rabbitmq/"},{"title":"redis","uri":"https://xiaohei.im/hugo-theme-pure/tags/redis/"},{"title":"rust","uri":"https://xiaohei.im/hugo-theme-pure/tags/rust/"},{"title":"rxjava","uri":"https://xiaohei.im/hugo-theme-pure/tags/rxjava/"},{"title":"分布式锁","uri":"https://xiaohei.im/hugo-theme-pure/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"},{"title":"响应式编程","uri":"https://xiaohei.im/hugo-theme-pure/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/"},{"title":"数据结构","uri":"https://xiaohei.im/hugo-theme-pure/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"}]} \ No newline at end of file
diff --git a/tags/collections/index.xml b/tags/collections/index.xml
index 33ef921..29e7dff 100644
--- a/tags/collections/index.xml
+++ b/tags/collections/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/hugo/index.xml b/tags/hugo/index.xml
index 1670276..9f24fbd 100644
--- a/tags/hugo/index.xml
+++ b/tags/hugo/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/hystrix/index.xml b/tags/hystrix/index.xml
index 0be04e9..8b1f154 100644
--- a/tags/hystrix/index.xml
+++ b/tags/hystrix/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/index.xml b/tags/index.xml
index e02a0b8..f918beb 100644
--- a/tags/index.xml
+++ b/tags/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/leetcode/index.xml b/tags/leetcode/index.xml
index bfba000..6ed2032 100644
--- a/tags/leetcode/index.xml
+++ b/tags/leetcode/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/netty/index.xml b/tags/netty/index.xml
index 76d6526..bc7e787 100644
--- a/tags/netty/index.xml
+++ b/tags/netty/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/rabbitmq/index.xml b/tags/rabbitmq/index.xml
index 37f81ed..1949b42 100644
--- a/tags/rabbitmq/index.xml
+++ b/tags/rabbitmq/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/redis/index.xml b/tags/redis/index.xml
index c32fa65..1d6dadc 100644
--- a/tags/redis/index.xml
+++ b/tags/redis/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/rust/index.xml b/tags/rust/index.xml
index aa846c1..4a0fef2 100644
--- a/tags/rust/index.xml
+++ b/tags/rust/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/rxjava/index.xml b/tags/rxjava/index.xml
index 914bc4a..a48ffb0 100644
--- a/tags/rxjava/index.xml
+++ b/tags/rxjava/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/分布式锁/index.xml b/tags/分布式锁/index.xml
index a7b017d..8bb6a53 100644
--- a/tags/分布式锁/index.xml
+++ b/tags/分布式锁/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/响应式编程/index.xml b/tags/响应式编程/index.xml
index c181ff3..ebf9f4e 100644
--- a/tags/响应式编程/index.xml
+++ b/tags/响应式编程/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>
diff --git a/tags/数据结构/index.xml b/tags/数据结构/index.xml
index 75d25d1..a9ce6c2 100644
--- a/tags/数据结构/index.xml
+++ b/tags/数据结构/index.xml
@@ -16,7 +16,7 @@
<pubDate>Thu, 26 Dec 2019 18:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/linkedlist/</guid>
- <description>链表需要注意的问题: 边界: 头结点尾结点的处理,链表长度为1时的处理 多画图,跟一次循环,边界情况也画图试试 思路大多都是 快慢指针 No.19 =&amp;gt; Remove Nth Node From End of</description>
+ <description></description>
</item>
<item>
@@ -25,7 +25,7 @@
<pubDate>Thu, 26 Dec 2019 17:22:05 +0800</pubDate>
<guid>https://xiaohei.im/hugo-theme-pure/2019/12/array/</guid>
- <description>Easy =&amp;gt; 1252. Cells with Odd Values in a Matrix public int oddCells(int n, int m, int[][] indices) { boolean[] oddRows = new boolean[n]; boolean[] oddCols = new boolean[m]; for(int[] item : indices) { // 遍历 indices 获取每一列每一行的出现次数是否为奇数 // 异或: 相同为0 不同为1 oddRows[item[0]] ^=</description>
+ <description></description>
</item>
<item>