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>2019-11-14 05:16:45 +0300
committerotis <xiaohei.zyx@gmail.com>2019-11-14 05:16:45 +0300
commitc967b40331acb373e8f47f75546d5fc34ec0b8e2 (patch)
tree6d95296ee74bdcb3bae816f277da275200c270c3
parent0390e8afe6fc9bbcf7d91a65c40e63ccb57a1957 (diff)
update gh-pages
-rw-r--r--2019/08/001-two-sum/index.html2
-rw-r--r--2019/08/026-remove-duplicates-from-sorted-array/index.html2
-rw-r--r--2019/08/189-rotate-array/index.html2
-rw-r--r--2019/08/arraylist/index.html2
-rw-r--r--2019/08/hashmap/index.html2
-rw-r--r--2019/08/hashset/index.html2
-rw-r--r--2019/08/linkedhashmap/index.html2
-rw-r--r--2019/08/linkedlist/index.html2
-rw-r--r--2019/08/rust/index.html2
-rw-r--r--2019/08/rxjava-guide/index.html2
-rw-r--r--2019/08/rxjava-in-hystrix/index.html2
-rw-r--r--2019/08/treemap/index.html2
-rw-r--r--2019/08/treeset/index.html2
-rw-r--r--2019/09/amqp-0-9-1-model-explained/index.html2
-rw-r--r--2019/09/hugo-theme-dev-note/index.html2
-rw-r--r--2019/09/rabbitmq-guide-and-ha-cluster/index.html2
-rw-r--r--2019/09/rabbitmq-msg-distribution/index.html2
-rw-r--r--2019/10/data-structure/index.html2
-rw-r--r--2019/10/rabbitmq-ack-confirm/index.html2
-rw-r--r--2019/11/db/index.html2
-rw-r--r--2019/11/distributed-lock/index.html2
-rw-r--r--2019/11/obj/index.html2
-rw-r--r--2019/11/rdb/index.html12
-rw-r--r--about/index.html2
-rw-r--r--categories/corejava/index.html2
-rw-r--r--categories/hystrix/index.html2
-rw-r--r--categories/index.html2
-rw-r--r--categories/leetcode/index.html42
-rw-r--r--categories/redis/index.html2
-rw-r--r--categories/消息队列/index.html2
-rw-r--r--index.html4
-rw-r--r--posts/index.html2
-rw-r--r--searchindex.json2
-rw-r--r--sitemap.xml4
-rw-r--r--tags/collections/index.html2
-rw-r--r--tags/hugo/index.html2
-rw-r--r--tags/hystrix/index.html2
-rw-r--r--tags/index.html2
-rw-r--r--tags/leetcode/index.html2
-rw-r--r--tags/rabbitmq/index.html2
-rw-r--r--tags/redis/index.html2
-rw-r--r--tags/rust/index.html2
-rw-r--r--tags/rxjava/index.html2
-rw-r--r--tags/分布式锁/index.html2
-rw-r--r--tags/响应式编程/index.html2
-rw-r--r--tags/数据结构/index.html2
46 files changed, 63 insertions, 83 deletions
diff --git a/2019/08/001-two-sum/index.html b/2019/08/001-two-sum/index.html
index 0f884e5..a5575b1 100644
--- a/2019/08/001-two-sum/index.html
+++ b/2019/08/001-two-sum/index.html
@@ -437,7 +437,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/026-remove-duplicates-from-sorted-array/index.html b/2019/08/026-remove-duplicates-from-sorted-array/index.html
index a99709b..caa2576 100644
--- a/2019/08/026-remove-duplicates-from-sorted-array/index.html
+++ b/2019/08/026-remove-duplicates-from-sorted-array/index.html
@@ -419,7 +419,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/189-rotate-array/index.html b/2019/08/189-rotate-array/index.html
index 6816064..0627eee 100644
--- a/2019/08/189-rotate-array/index.html
+++ b/2019/08/189-rotate-array/index.html
@@ -499,7 +499,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/arraylist/index.html b/2019/08/arraylist/index.html
index 9a0d0d4..a054a86 100644
--- a/2019/08/arraylist/index.html
+++ b/2019/08/arraylist/index.html
@@ -551,7 +551,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/hashmap/index.html b/2019/08/hashmap/index.html
index a674555..f10d091 100644
--- a/2019/08/hashmap/index.html
+++ b/2019/08/hashmap/index.html
@@ -1130,7 +1130,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/hashset/index.html b/2019/08/hashset/index.html
index 6ca273b..97651ce 100644
--- a/2019/08/hashset/index.html
+++ b/2019/08/hashset/index.html
@@ -488,7 +488,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/linkedhashmap/index.html b/2019/08/linkedhashmap/index.html
index d67f42b..212bcbc 100644
--- a/2019/08/linkedhashmap/index.html
+++ b/2019/08/linkedhashmap/index.html
@@ -568,7 +568,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/linkedlist/index.html b/2019/08/linkedlist/index.html
index 97f3a49..80738c6 100644
--- a/2019/08/linkedlist/index.html
+++ b/2019/08/linkedlist/index.html
@@ -800,7 +800,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/rust/index.html b/2019/08/rust/index.html
index 78670a0..d72d011 100644
--- a/2019/08/rust/index.html
+++ b/2019/08/rust/index.html
@@ -529,7 +529,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/rxjava-guide/index.html b/2019/08/rxjava-guide/index.html
index 9c6d56b..3dcf10c 100644
--- a/2019/08/rxjava-guide/index.html
+++ b/2019/08/rxjava-guide/index.html
@@ -913,7 +913,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/rxjava-in-hystrix/index.html b/2019/08/rxjava-in-hystrix/index.html
index 1ae2418..b990de3 100644
--- a/2019/08/rxjava-in-hystrix/index.html
+++ b/2019/08/rxjava-in-hystrix/index.html
@@ -1029,7 +1029,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/treemap/index.html b/2019/08/treemap/index.html
index d7a9ada..8d5fc9f 100644
--- a/2019/08/treemap/index.html
+++ b/2019/08/treemap/index.html
@@ -625,7 +625,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/08/treeset/index.html b/2019/08/treeset/index.html
index fd32980..a395e19 100644
--- a/2019/08/treeset/index.html
+++ b/2019/08/treeset/index.html
@@ -534,7 +534,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/09/amqp-0-9-1-model-explained/index.html b/2019/09/amqp-0-9-1-model-explained/index.html
index 9ae9bf1..0b6ead9 100644
--- a/2019/09/amqp-0-9-1-model-explained/index.html
+++ b/2019/09/amqp-0-9-1-model-explained/index.html
@@ -731,7 +731,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/09/hugo-theme-dev-note/index.html b/2019/09/hugo-theme-dev-note/index.html
index 1f572c0..8a595f7 100644
--- a/2019/09/hugo-theme-dev-note/index.html
+++ b/2019/09/hugo-theme-dev-note/index.html
@@ -613,7 +613,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/09/rabbitmq-guide-and-ha-cluster/index.html b/2019/09/rabbitmq-guide-and-ha-cluster/index.html
index cdf4867..f2f38e8 100644
--- a/2019/09/rabbitmq-guide-and-ha-cluster/index.html
+++ b/2019/09/rabbitmq-guide-and-ha-cluster/index.html
@@ -540,7 +540,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/09/rabbitmq-msg-distribution/index.html b/2019/09/rabbitmq-msg-distribution/index.html
index c05ee24..f110799 100644
--- a/2019/09/rabbitmq-msg-distribution/index.html
+++ b/2019/09/rabbitmq-msg-distribution/index.html
@@ -504,7 +504,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/10/data-structure/index.html b/2019/10/data-structure/index.html
index ba07b68..9c65b19 100644
--- a/2019/10/data-structure/index.html
+++ b/2019/10/data-structure/index.html
@@ -1049,7 +1049,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/10/rabbitmq-ack-confirm/index.html b/2019/10/rabbitmq-ack-confirm/index.html
index 78937e2..580fd76 100644
--- a/2019/10/rabbitmq-ack-confirm/index.html
+++ b/2019/10/rabbitmq-ack-confirm/index.html
@@ -514,7 +514,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/11/db/index.html b/2019/11/db/index.html
index 5f7f7aa..27e554b 100644
--- a/2019/11/db/index.html
+++ b/2019/11/db/index.html
@@ -639,7 +639,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/11/distributed-lock/index.html b/2019/11/distributed-lock/index.html
index ebddafe..0f91ee4 100644
--- a/2019/11/distributed-lock/index.html
+++ b/2019/11/distributed-lock/index.html
@@ -593,7 +593,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/11/obj/index.html b/2019/11/obj/index.html
index 0066823..11dc7cf 100644
--- a/2019/11/obj/index.html
+++ b/2019/11/obj/index.html
@@ -593,7 +593,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/2019/11/rdb/index.html b/2019/11/rdb/index.html
index 6313754..d40b640 100644
--- a/2019/11/rdb/index.html
+++ b/2019/11/rdb/index.html
@@ -44,7 +44,7 @@
<meta itemprop="datePublished" content="2019-11-06T19:08:56&#43;08:00" />
<meta itemprop="dateModified" content="2019-11-06T19:08:56&#43;08:00" />
-<meta itemprop="wordCount" content="3445">
+<meta itemprop="wordCount" content="3442">
@@ -234,7 +234,7 @@
<ul>
<li>
<ul>
-<li><a href="#介绍">介绍</a>
+<li><a href="#什么是-rdb">什么是 RDB?</a>
<ul>
<li><a href="#如何执行">如何执行?</a></li>
<li><a href="#如何载入">如何载入?</a></li>
@@ -318,7 +318,7 @@
</span>
<span class="post-comment"><i class="icon icon-comment"></i> <a href="/hugo-theme-pure/2019/11/rdb/#comments"
class="article-comment-link">评论</a></span>
- <span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计:3445字</span>
+ <span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计:3442字</span>
<span class="post-readcount hidden-xs" itemprop="timeRequired">阅读时长:7分 </span>
</div>
</div>
@@ -329,7 +329,7 @@
<p>官方关于持久化的文章: <a href="https://redis.io/topics/persistence">https://redis.io/topics/persistence</a></p>
</blockquote>
-<h2 id="介绍">介绍</h2>
+<h2 id="什么是-rdb">什么是 RDB?</h2>
<p><code>RDB</code> 是 <code>redis</code> 提供的一种持久化方式,可以手动执行,也可以通过定时任务定期执行,可以将某个时间节点的数据库状态保存到一个 <code>RDB</code> 文件中,叫做 <code>dump.rdb</code>.如果开启了压缩算法( <code>LZF</code> )的支持,则可以利用算法减少文件大小.服务器意外宕机或者断电后重启都可以通过该文件来恢复数据库状态.</p>
@@ -715,7 +715,7 @@ int rdbSaveInfoAuxFields(rio *rdb, int flags, rdbSaveInfo *rsi) {
l l o 377 374 016 k y 376 G 032 6
</code></pre>
-<p>最后一行输出中 <code>0xff</code> , 文件结束符, 剩下的八个字节就是 <code>CRC64</code> 校验码了.</p>
+<p>最后一行输出中 <code>0xff</code> , 文件结束符, 剩下的八个字节就是 <code>CRC64</code></p>
<h2 id="参考">参考</h2>
@@ -849,7 +849,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/about/index.html b/about/index.html
index 7142950..c128f58 100644
--- a/about/index.html
+++ b/about/index.html
@@ -424,7 +424,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/categories/corejava/index.html b/categories/corejava/index.html
index 016caed..2a77179 100644
--- a/categories/corejava/index.html
+++ b/categories/corejava/index.html
@@ -390,7 +390,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/categories/hystrix/index.html b/categories/hystrix/index.html
index 0abacd7..9e362d1 100644
--- a/categories/hystrix/index.html
+++ b/categories/hystrix/index.html
@@ -335,7 +335,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/categories/index.html b/categories/index.html
index 3f266f3..82dad87 100644
--- a/categories/index.html
+++ b/categories/index.html
@@ -478,7 +478,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/categories/leetcode/index.html b/categories/leetcode/index.html
index 44dace4..ef9e61d 100644
--- a/categories/leetcode/index.html
+++ b/categories/leetcode/index.html
@@ -290,36 +290,16 @@
itemprop="datePublished">2019-08-21</time>
<span>&nbsp;&nbsp;&nbsp;</span>[LeetCode In Rust]189-Rotate Array
</a>
-<article class="panel panel-default hover-shadow hover-grow" itemscope itemtype="http://schema.org/BlogPosting">
- <div class="panel-body">
- <div class="article-meta">
- <time datetime="2019-08-20 15:54:47 &#43;0800 CST"
- itemprop="datePublished">2019-08-20</time>
- </div>
- <h3 class="article-title" itemprop="name">
- <a class="article-link" href="https://xiaohei.im/hugo-theme-pure/2019/08/026-remove-duplicates-from-sorted-array/">[LeetCode In Rust]026-Remove Duplicates From Sorted Array</a>
- </h3>
- </div>
- <div class="panel-footer">
- <a href="https://xiaohei.im/hugo-theme-pure/tags/leetcode" class="label label-default mb">leetcode</a>
- <a href="https://xiaohei.im/hugo-theme-pure/tags/rust" class="label label-default mb">rust</a>
- </div>
-</article>
-<article class="panel panel-default hover-shadow hover-grow" itemscope itemtype="http://schema.org/BlogPosting">
- <div class="panel-body">
- <div class="article-meta">
- <time datetime="2019-08-16 18:22:05 &#43;0800 CST"
- itemprop="datePublished">2019-08-16</time>
- </div>
- <h3 class="article-title" itemprop="name">
- <a class="article-link" href="https://xiaohei.im/hugo-theme-pure/2019/08/001-two-sum/">[LeetCode In Rust]001-Two Sum</a>
- </h3>
- </div>
- <div class="panel-footer">
- <a href="https://xiaohei.im/hugo-theme-pure/tags/leetcode" class="label label-default mb">leetcode</a>
- <a href="https://xiaohei.im/hugo-theme-pure/tags/rust" class="label label-default mb">rust</a>
- </div>
-</article>
+<a href="https://xiaohei.im/hugo-theme-pure/2019/08/026-remove-duplicates-from-sorted-array/" class="collection-item" itemprop="url" target="_blank" >
+ <time datetime="2019-08-20 15:54:47 &#43;0800 CST"
+ itemprop="datePublished">2019-08-20</time>
+ <span>&nbsp;&nbsp;&nbsp;</span>[LeetCode In Rust]026-Remove Duplicates From Sorted Array
+</a>
+<a href="https://xiaohei.im/hugo-theme-pure/2019/08/001-two-sum/" class="collection-item" itemprop="url" target="_blank" >
+ <time datetime="2019-08-16 18:22:05 &#43;0800 CST"
+ itemprop="datePublished">2019-08-16</time>
+ <span>&nbsp;&nbsp;&nbsp;</span>[LeetCode In Rust]001-Two Sum
+</a>
</div>
</div>
</div>
@@ -370,7 +350,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/categories/redis/index.html b/categories/redis/index.html
index 154e22e..f50eeb5 100644
--- a/categories/redis/index.html
+++ b/categories/redis/index.html
@@ -380,7 +380,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/categories/消息队列/index.html b/categories/消息队列/index.html
index b4beb9b..c33142a 100644
--- a/categories/消息队列/index.html
+++ b/categories/消息队列/index.html
@@ -365,7 +365,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/index.html b/index.html
index 2354121..120ae11 100644
--- a/index.html
+++ b/index.html
@@ -268,7 +268,7 @@
</span>
<span class="post-comment"><i class="icon icon-comment"></i> <a href="/hugo-theme-pure/2019/11/rdb/#comments" class="article-comment-link">评论</a></span>
- <span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计:3445字</span>
+ <span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计:3442字</span>
<span class="post-readcount hidden-xs" itemprop="timeRequired">阅读时长:7分 </span>
</p>
</article><article
@@ -1055,7 +1055,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/posts/index.html b/posts/index.html
index ba38b50..dbad80c 100644
--- a/posts/index.html
+++ b/posts/index.html
@@ -480,7 +480,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/searchindex.json b/searchindex.json
index d9d6bf1..3757c52 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/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"posts":[{"content":"","id":0,"section":"posts","summary":"","tags":null,"title":"Posts","uri":"https://xiaohei.im/hugo-theme-pure/posts/","year":"2019"},{"content":"redis 为内存数据库,一旦服务器进程退出,服务器中的数据就不见了.所以内存中的数据需要持久化的硬盘中来保证可以在必要的时候进行故障恢复. RDB 就是 redis 提供的一种持久化方式.\n 官方关于持久化的文章: https://redis.io/topics/persistence\n 介绍 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":1,"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 会记录当前函数检查的进度,并在下一次函数执行时,接着上次的执行.循环往复地执行.\nAOF,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":2,"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":3,"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":4,"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":5,"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":6,"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":7,"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":8,"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":9,"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":10,"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":11,"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":12,"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":13,"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":14,"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":"基本类型-Primitives 标准类型 Scalar Types 有符号整型 signed integers: i8, i16, i32, i64, i128 and isize (pointer size)\n 无符号整型 unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)\n 浮点型 floating point: f32, f64\n 字符 char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)\n 布尔 bool either true or false\n the unit type (), whose only possible value is an empty tuple: ()\n 我的理解就是个empty吧\n 数值字面,没有后缀时.默认整数为 i32,浮点数 f64\n三级目录 有符号整型 signed integers: i8, i16, i32, i64, i128 and isize (pointer size)\n 无符号整型 unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)\n 浮点型 floating point: f32, f64\n 字符 char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)\n 布尔 bool either true or false\n the unit type (), whose only possible value is an empty tuple: ()\n 我的理解就是个empty吧\n 数值字面,没有后缀时.默认整数为 i32,浮点数 f64\n四级目录 有符号整型 signed integers: i8, i16, i32, i64, i128 and isize (pointer size)\n 无符号整型 unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)\n 浮点型 floating point: f32, f64\n 字符 char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)\n 布尔 bool either true or false\n the unit type (), whose only possible value is an empty tuple: ()\n 我的理解就是个empty吧\n 数值字面,没有后缀时.默认整数为 i32,浮点数 f64\n复合类型 Compound Types 数组 array [1,2,3] 元组 tuple (1,true,\u0026ldquo;str\u0026rdquo;,\u0026lsquo;a\u0026rsquo;\u0026hellip;) Note: 默认变量赋值后是不可变的,需要重复赋值需要用mut 修饰\nlet immutable = 1; immutable = 2; // ERROR let mut immutable = 1; immutable = 2; // SUCCESS 所有权踩坑 可变引用在作用域下有且只能有一个可变引用\n 不可在拥有不可变引用的同时拥有可变引用,除非作用域没有重叠.即在引用可变引用时,不可变引用已经失效了.\nlet mut s = String::from(\u0026quot;hello\u0026quot;); let r1 = \u0026amp;s; // 没问题 let r2 = \u0026amp;s; // 没问题 let r3 = \u0026amp;mut s; // 大问题 println!(\u0026quot;{}, {}, and {}\u0026quot;, r1, r2, r3); ////////////////////////////////////// let mut s = String::from(\u0026quot;hello\u0026quot;); let r1 = \u0026amp;s; // 没问题 let r2 = \u0026amp;s; // 没问题 println!(\u0026quot;{} and {}\u0026quot;, r1, r2); // 此位置之后 r1 和 r2 不再使用 let r3 = \u0026amp;mut s; // 没问题 println!(\u0026quot;{}\u0026quot;, r3); ","id":15,"section":"posts","summary":"","tags":["rust"],"title":"rust踩坑笔记","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/rust/","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":16,"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"},{"content":"ArrayList和LinkedList和Vector的区别 简单讲: 1. ArrayList和LinkedList是线程不安全的,而Vector在增删改的操作上都有synchronized关键字修饰,是线性的,但是效率不高 2. ArrayList和Vector都是基于数组的实现,扩容时,ArrayList扩容为1.5倍,Vector扩容为2倍,查找快,增删慢.LinkedList是一个双向链表,增删快,查找慢. \u0026gt; 参考blog\nSynchronizedList和Vector的区别 SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。 使用SynchronizedList的时候,进行遍历时要手动进行同步处理。 SynchronizedList可以指定锁定的对象。 \u0026gt; Hollis的blog 参数 static final int DEFAULT_CAPACITY=10 默认大小 transient object[] elementData; arraylist存储对象的数组.当空ArrayList第一次添加对象时,容量会扩展成DEFAULT_CAPACITY size elementData中实际的对象数 构造函数 看几个主要的 1. 带初始化参数,参数违法时会抛RuntimeException\npublic ArrayList(int initialCapacity) { if (initialCapacity \u0026gt; 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(\u0026quot;Illegal Capacity: \u0026quot;+ initialCapacity); } } 从其他集合中导入,collection需要notNull,否则会抛空指针\npublic ArrayList(Collection\u0026lt;? extends E\u0026gt; c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 主要方法 get\npublic E get(int index) { //判断index是否在范围内 不在会抛 IndexOutOfBoundsException rangeCheck(index); //获取对象值 return elementData(index); } set 替换指定位置的值\npublic E set(int index, E element) { rangeCheck(index);//范围检查 E oldValue = elementData(index);//获取对象旧值 elementData[index] = element; //赋新值 return oldValue; //返回旧值 } add 在elementData尾部添加一个对象\npublic boolean add(E e) { //确保在容量范围内,不在则扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++;//操作list的次数 //若最小容量大于 elementData的长度 则扩容 // overflow-conscious code if (minCapacity - elementData.length \u0026gt; 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1); //扩容大概是1.5倍 if (newCapacity - minCapacity \u0026lt; 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE \u0026gt; 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } add 指定位置添加对象\npublic void add(int index, E element) { //专门的add操作范围检查 主要是保证 0 \u0026lt; index \u0026lt; size rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } ","id":17,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Arraylist","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/arraylist/","year":"2019"},{"content":" 美团的blog:https://tech.meituan.com/java_hashmap.html 参考blog: 田小波的博客 红黑树介绍\n HashMap,HashTable,ConcurrentHashMap的区别? HashMap是非线程安全的,HashTable和ConcurrentHashMap是线程安全的.HashTable不允许null key和null Value,HashMap允许. ConcurrentHashMap推出之后官方推荐不要在使用HashTable作为线程安全的使用类,而是使用这个.关于ConcurrentHashMap后面再学习. \u0026gt; 参考blog\nHashMap在不同版本之间实现的区别? 区别\n 官方文档介绍: 基于Map接口实现的哈希表.提供了所有map可选的操作,允许key为null,value为null.HashMap与HashTable基本一致,除了HashMap 线程不安全并且允许为空. 不保证有序,尤其不保证顺序一直不变(因为扩容时会rehash,基本上就顺序就重排了) 假设hash分布均匀的情况下,基本的操作(get/put)性能很不错.迭代所需要的时间与buckets数量与每个bukets下的键值对的数量之和成正比.所以官方建议如果要求hashmap的迭代性能的话,初始的capacity不能太高,loadFactor不要太高. HashMap有两个重要的参数:initial capacity,load factor.capacity定义bucket的数量,initial capacity定义的是初始化bucket数量.load factor(中文名: 加载因子 )是判断哈希表是否需要扩容的阈值,当entries数量超过(load factor * current capacity),哈希表会触发rehash操作,内部数据结构会重整,buckets数量会变为之前大约两倍左右\n 通常情况下,load factor 默认0.75f,在时间空间上是很平衡的.值偏高时,空间减少,查找时间上升了(影响大部分的操作,get/put之类的),在设置初始容量时,需要考虑到预期的entries数量和加载因子,以便最小化rehash的数量.如果初始化的容量大于最大数量的entries除以加载因子,不会发生rehash操作.\n 如果有大量的键值对存到hashmap中,那么创建一个足够大的hashmap来存储要比让他自动rehash扩容来存储的性能要好很多.注意:具有相同hashcode的多个key肯定会影响哈希表的性能.为了改善这种影响,当key是Comparable类型时,可以通过key之间的比较顺序来打破这种关系.\n 注意hashmap是Non synchronized,即 非线程安全.如果多线程并发访问hashmap,并且至少有一个线程操作map的结构,在外部必须synchronized.(结构修改是指任何关于add或delte的操作,仅仅只是修改key关联的value时则不属于结构修改).通常在将object封装进map做synchronized操作\n 如果不存在上面的objects,那这个map需要被Collections.synchronizedMap包装下.最好在创建的时候就做好,防止偶然的并发访问.\nMap m = Collections.synchronizedMap(new HashMap(...)); 迭代器的所有方法都是fail-fast,如果迭代器创建后,在迭代器里的结构操作必须通过迭代器的方法来操作,否则会抛ConcurrentModificationException.因此,面对并发修改,迭代器会快速而干净的失败,而不是在未来的不确定时间冒任意非确定行为的风险.\n 请注意,迭代器的快速失败行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证. 快速失败迭代器会尽最大努力抛出ConcurrentModificationException. 因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误.\n 源码 /** * The default initial capacity - MUST be a power of two. * 默认的容量16 必须是2的n次方 */ static final int DEFAULT_INITIAL_CAPACITY = 1 \u0026lt;\u0026lt; 4; // aka 16 /** * 最大的容量限制 * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two \u0026lt;= 1\u0026lt;\u0026lt;30. */ static final int MAXIMUM_CAPACITY = 1 \u0026lt;\u0026lt; 30; /** * 默认的加载因子 * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 大于这个值转红黑树 * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8; /** * 大于这个值小于 TREEIFY_THRESHOLD 不转树 * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6; /** * hashmap整体容量大于这个值时才能树化 * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. */ static final int MIN_TREEIFY_CAPACITY = 64; /** * node节点 * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { final int hash; final K key; V value; Node\u0026lt;K,V\u0026gt; next; Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + \u0026quot;=\u0026quot; + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry\u0026lt;?,?\u0026gt; e = (Map.Entry\u0026lt;?,?\u0026gt;)o; if (Objects.equals(key, e.getKey()) \u0026amp;\u0026amp; Objects.equals(value, e.getValue())) return true; } return false; } } //hashMap中的静态方法 /** * hash方法详解 blog:http://www.hollischuang.com/archives/2091 * 扰动算法--使hash分布更均匀 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } //取模运算,获得对象存储到bukets的下标 //实际上就是取模,一般取模使用% 但是考虑到效率问题,采用位运算 //X % 2^n = X \u0026amp; (2^n-1) 这也是为什么hashmap容量为2的n次方的原因 static int indexFor(int h, int length) { return h \u0026amp; (length-1); } //返回hashmap的容量 2的n次方 很巧妙的位运算 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n \u0026gt;\u0026gt;\u0026gt; 1; n |= n \u0026gt;\u0026gt;\u0026gt; 2; n |= n \u0026gt;\u0026gt;\u0026gt; 4; n |= n \u0026gt;\u0026gt;\u0026gt; 8; n |= n \u0026gt;\u0026gt;\u0026gt; 16; return (n \u0026lt; 0) ? 1 : (n \u0026gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } /** * 参数都用transient不让序列化的原因:https://segmentfault.com/q/1010000000630486 */ //bukets hashmap是链表加数组的结构.此为数组 transient Node\u0026lt;K,V\u0026gt;[] table; //保存键值对的Entry transient Set\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt; entrySet; //hashmap的size transient int size; //结构操作次数 可用于快速失败的比较条件 例如并发操作时 transient int modCount; //resize的临界点: capacity * load factor int threshold; //加载因子 final float loadFactor; //公有操作方法 //构造方法 /** * 根据 initial capactity 和 loadFactor创建空的hashmap * Constructs an empty \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public HashMap(int initialCapacity, float loadFactor) { //校验initialCapacity if (initialCapacity \u0026lt; 0) throw new IllegalArgumentException(\u0026quot;Illegal initial capacity: \u0026quot; + initialCapacity); //容量校验 if (initialCapacity \u0026gt; MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //校验loadFactor isNaN--\u0026gt; 是否是一个number Not-a-Number if (loadFactor \u0026lt;= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException(\u0026quot;Illegal load factor: \u0026quot; + loadFactor); //加载因子赋值 this.loadFactor = loadFactor; //扩容阈值赋值 2的n次方 this.threshold = tableSizeFor(initialCapacity); } /** * 通过initialCapacity赋值 * Constructs an empty \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 根据默认容量和默认加载因子创建空的hashmap * Constructs an empty \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * 根据传进来的map创建一个新的hashMap * initialCapacity 足以装下参数map的数量 * loadFactor使用默认值 * Constructs a new \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the same mappings as the * specified \u0026lt;tt\u0026gt;Map\u0026lt;/tt\u0026gt;. The \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified \u0026lt;tt\u0026gt;Map\u0026lt;/tt\u0026gt;. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public HashMap(Map\u0026lt;? extends K, ? extends V\u0026gt; m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } /** * Implements Map.putAll and Map constructor * * @param m the map * @param evict false when initially constructing this map, else * true (relayed to method afterNodeInsertion). * evict 初始化构建map时 为false 其他情况下为true */ final void putMapEntries(Map\u0026lt;? extends K, ? extends V\u0026gt; m, boolean evict) { int s = m.size(); if (s \u0026gt; 0) { //初次创建hashmap if (table == null) { // pre-size //计算m所需要的容量 float ft = ((float)s / loadFactor) + 1.0F; //获得真实的容量 int t = ((ft \u0026lt; (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //如果比默认的阈值大则计算该 t 对应的capacity if (t \u0026gt; threshold) threshold = tableSizeFor(t); } else if (s \u0026gt; threshold) // 如果是table不为null 即是后续往map中添加 如果s \u0026gt; 阈值就要重置map了 resize();//resize操作 后面介绍 //确定容量后put操作 for (Map.Entry\u0026lt;? extends K, ? extends V\u0026gt; e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict);// } } } /*主要调用 putVal */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * put 操作 * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value 不存在才put\u0026lt;D-[\u0026gt; * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; //若是新建map的情况下 resize创建指定长度的table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //取模计算该key对应的数组下标 并判断该坐标下的对象是否为null //为null时创建一个新node存入tab[i] if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) tab[i] = newNode(hash, key, value, null); else {//tab[i] != null Node\u0026lt;K,V\u0026gt; e; K k; //如果p与存入的key完全相同 if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) e = p; else if (p instanceof TreeNode) //如果是红黑树节点 调用putTreeVal e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); else { //普通的put //binCount记录了链表的长度 for (int binCount = 0; ; ++binCount) { //如果当前node的next==null说明就可以往该链上添加一个节点 if ((e = p.next) == null) { //新建node接到p.next下面 p.next = newNode(hash, key, value, null); //如果binCount大于设定的红黑树化阈值 if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//红黑树化 break; } //如果key与链表中的任意node完全相同break if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) break; p = e; } } //如果存在该key if (e != null) { // existing mapping for key V oldValue = e.value;//获得旧值 if (!onlyIfAbsent || oldValue == null)//若没有设置不存在才put或者oldValue=null e.value = value;//赋新值 afterNodeAccess(e);//LinkedHashMap操作 return oldValue;//返回旧值 } } ++modCount; if (++size \u0026gt; threshold)//是否需要扩容 resize(); afterNodeInsertion(evict);//LinkedHashMap操作 return null; } /** * 扩容操作 * 若是初始化则根据initialCapacity创建一个table * 否则,扩容为2的n次方倍 * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node\u0026lt;K,V\u0026gt;[] resize() { Node\u0026lt;K,V\u0026gt;[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap \u0026gt; 0) { //超过最大值不会再扩容了 if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap \u0026lt;\u0026lt; 1) \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; oldCap \u0026gt;= DEFAULT_INITIAL_CAPACITY) newThr = oldThr \u0026lt;\u0026lt; 1; // double threshold 扩成两倍 } else if (oldThr \u0026gt; 0) // initial capacity was placed in threshold newCap = oldThr; else { // 默认配置 zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //计算新的阈值 float ft = (float)newCap * loadFactor; newThr = (newCap \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; ft \u0026lt; (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({\u0026quot;rawtypes\u0026quot;,\u0026quot;unchecked\u0026quot;}) Node\u0026lt;K,V\u0026gt;[] newTab = (Node\u0026lt;K,V\u0026gt;[])new Node[newCap]; table = newTab; if (oldTab != null) { //把old buket 移到新的bukets里 for (int j = 0; j \u0026lt; oldCap; ++j) { Node\u0026lt;K,V\u0026gt; e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null)//直接添加 newTab[e.hash \u0026amp; (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode\u0026lt;K,V\u0026gt;)e).split(this, newTab, j, oldCap); else { // preserve order Node\u0026lt;K,V\u0026gt; loHead = null, loTail = null; Node\u0026lt;K,V\u0026gt; hiHead = null, hiTail = null; Node\u0026lt;K,V\u0026gt; next; do { next = e.next; //这个取模很精辟 请结合美团的blog resize 1.8优化学习 //因为扩容是2倍扩容,二进制中相当于左移一位 /** * 假设一次扩容\t* 扩容前\toldCap = 00010000 oldCap - 1 = 00001111 * 扩容后\tnewCap = 00100000 newCap - 1 = 00011111 * 可以看出扩容后 newCap-1 在高位多了1 * 计算index时 hash \u0026amp; n-1 = 原位置 + oldCap * 所以只需要判断hash \u0026amp; oldCap是否为1 * 为1则把该node的位置移到 oldCap+原位置 * 为 0 还在原位置 */ if ((e.hash \u0026amp; oldCap) == 0) {//为0说明位置没有变 if (loTail == null)//第一次添加时loHead=e loHead = e; else loTail.next = e;//直接往后插入 loTail = e; } else {//为1 说明位置会+oldCap长度 if (hiTail == null) hiHead = e;//头节点初始化 else hiTail.next = e;//直接插入 hiTail = e; } } while ((e = next) != null); if (loTail != null) {//放在原位置上 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) {//放在原位置+oldCap上 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } /** * get操作 * 为null时返回null 这个要注意下 */ public V get(Object key) { Node\u0026lt;K,V\u0026gt; e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * get方法 * 主要是 key相等 或者 key equals的比较 * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node\u0026lt;K,V\u0026gt; getNode(int hash, Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; first, e; int n; K k; if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (first = tab[(n - 1) \u0026amp; hash]) != null) {//获得节点 if (first.hash == hash \u0026amp;\u0026amp; // always check first node ((k = first.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode)//树节点 return ((TreeNode\u0026lt;K,V\u0026gt;)first).getTreeNode(hash, key); do { if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 1.8红黑树化源码解析 /** * TreeNode extends LinkedHashMap.Entry * LinkedHashMap.Entry extends HashMap.Node */ static final class TreeNode\u0026lt;K,V\u0026gt; extends LinkedHashMap.Entry\u0026lt;K,V\u0026gt; { TreeNode\u0026lt;K,V\u0026gt; parent; // red-black tree links 红黑树父节点 TreeNode\u0026lt;K,V\u0026gt; left; TreeNode\u0026lt;K,V\u0026gt; right; TreeNode\u0026lt;K,V\u0026gt; prev; // needed to unlink next upon deletion 删除的时候用来连接前后 boolean red;//红还是黑 TreeNode(int hash, K key, V val, Node\u0026lt;K,V\u0026gt; next) { super(hash, key, val, next); } } /**树化 * putVal里有用到 * 将链表重置为红黑树并放到该hash映射的tab下,如果tab过下则resize * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node\u0026lt;K,V\u0026gt;[] tab, int hash) { int n, index; Node\u0026lt;K,V\u0026gt; e; if (tab == null || (n = tab.length) \u0026lt; MIN_TREEIFY_CAPACITY)//小于最小树化的容量时不树化而resize capacity为64, resize(); else if ((e = tab[index = (n - 1) \u0026amp; hash]) != null) { TreeNode\u0026lt;K,V\u0026gt; hd = null, tl = null;//头尾节点 do { TreeNode\u0026lt;K,V\u0026gt; p = replacementTreeNode(e, null);//这个就是返回一个新建的TreeNode对象,内容为e if (tl == null)//确定是头结点 hd = p;//标记头结点 else {//非头结点就首尾连接 p.prev = tl; tl.next = p; } tl = p;//尾节点一直为p } while ((e = e.next) != null);//遍历链表 其实此时形成也还算是个链表 if ((tab[index] = hd) != null)//将该treeNode挂到table下 hd.treeify(tab);//完成红黑树化 } } /** * Forms tree of the nodes linked from this node. * @return root of tree */ final void treeify(Node\u0026lt;K,V\u0026gt;[] tab) { TreeNode\u0026lt;K,V\u0026gt; root = null; for (TreeNode\u0026lt;K,V\u0026gt; x = this, next; x != null; x = next) {//x 从当前节点开始(从treeifyBin里调用看是头结点) next = (TreeNode\u0026lt;K,V\u0026gt;)x.next;//获取下个节点 x.left = x.right = null; if (root == null) {//设置root节点并给他黑色 x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class\u0026lt;?\u0026gt; kc = null; //遍历所有节点与当前节点x比较 调整位置 有点像冒泡排序 for (TreeNode\u0026lt;K,V\u0026gt; p = root;;) { int dir, ph; K pk = p.key; //比较hash值 if ((ph = p.hash) \u0026gt; h) dir = -1; else if (ph \u0026lt; h) dir = 1; else if ((kc == null \u0026amp;\u0026amp; (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); //根据dir判断x是p的左孩子 还是 右孩子 TreeNode\u0026lt;K,V\u0026gt; xp = p; if ((p = (dir \u0026lt;= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir \u0026lt;= 0) xp.left = x; else xp.right = x; //平衡节点 root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); } /** * Returns a list of non-TreeNodes replacing those linked from * this node. */ final Node\u0026lt;K,V\u0026gt; untreeify(HashMap\u0026lt;K,V\u0026gt; map) { Node\u0026lt;K,V\u0026gt; hd = null, tl = null; for (Node\u0026lt;K,V\u0026gt; q = this; q != null; q = q.next) { Node\u0026lt;K,V\u0026gt; p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } /** * 红黑树版put操作 * Tree version of putVal. */ final TreeNode\u0026lt;K,V\u0026gt; putTreeVal(HashMap\u0026lt;K,V\u0026gt; map, Node\u0026lt;K,V\u0026gt;[] tab, int h, K k, V v) { Class\u0026lt;?\u0026gt; kc = null; boolean searched = false; TreeNode\u0026lt;K,V\u0026gt; root = (parent != null) ? root() : this;//每次从根节点遍历 for (TreeNode\u0026lt;K,V\u0026gt; p = root;;) { int dir, ph; K pk; if ((ph = p.hash) \u0026gt; h) dir = -1; else if (ph \u0026lt; h) dir = 1; else if ((pk = p.key) == k || (k != null \u0026amp;\u0026amp; k.equals(pk))) //如果当前节点key相同或equals 返回 return p; else if ((kc == null \u0026amp;\u0026amp; (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { //hash值如果相等 但类不相同,只能挨个对比左右孩子 if (!searched) { TreeNode\u0026lt;K,V\u0026gt; q, ch; searched = true; if (((ch = p.left) != null \u0026amp;\u0026amp; (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null \u0026amp;\u0026amp; (q = ch.find(h, k, kc)) != null)) return q; } //哈希值相等 但键无法比较 只能通过其他方法比较 dir = tieBreakOrder(k, pk); } //得到两个节点的大小关系 即dir的值时 //并判断只有在左孩子或右孩子不能 TreeNode\u0026lt;K,V\u0026gt; xp = p; if ((p = (dir \u0026lt;= 0) ? p.left : p.right) == null) { Node\u0026lt;K,V\u0026gt; xpn = xp.next; TreeNode\u0026lt;K,V\u0026gt; x = map.newTreeNode(h, k, v, xpn); if (dir \u0026lt;= 0) xp.left = x; else xp.right = x; xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode\u0026lt;K,V\u0026gt;)xpn).prev = x; //平衡二叉树 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } /** 查找操作 传入 hash值 和 key值 * Calls find for root node. */ final TreeNode\u0026lt;K,V\u0026gt; getTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null);//判断从当前节点还是root节点开始查找 } /** * Finds the node starting at root p with the given hash and key. * The kc argument caches comparableClassFor(key) upon first use * comparing keys. */ final TreeNode\u0026lt;K,V\u0026gt; find(int h, Object k, Class\u0026lt;?\u0026gt; kc) { TreeNode\u0026lt;K,V\u0026gt; p = this; do { int ph, dir; K pk; TreeNode\u0026lt;K,V\u0026gt; pl = p.left, pr = p.right, q; //根据hash值查找 当前节点hash值大于h则 查左孩子 否则右孩子 当key相等或者equal时返回 if ((ph = p.hash) \u0026gt; h) p = pl; else if (ph \u0026lt; h) p = pr; else if ((pk = p.key) == k || (k != null \u0026amp;\u0026amp; k.equals(pk))) return p; else if (pl == null) p = pr; else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) \u0026amp;\u0026amp; (dir = compareComparables(kc, k, pk)) != 0) p = (dir \u0026lt; 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null)//不相等则从子树继续查找 return q; else p = pl; } while (p != null); return null; } ","id":18,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Hashmap","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/hashmap/","year":"2019"},{"content":" HashSet详解\n 介绍 基于HashMap保存元素不保证有序,不包含重复元素,允许null值.主要继承关系如图:\ngraph BT HashSet--\u0026gt;AbstractSet HashSet-.-\u0026gt;Set AbstractSet-.-\u0026gt;Set AbstractSet--\u0026gt;AbstractCollection AbstractCollection-.-\u0026gt;Collection\t 参数 static final long serialVersionUID = -5024744406713321676L; //底层存储用的hashMap private transient HashMap\u0026lt;E,Object\u0026gt; map; //定义一个Object对象作为HashMap的value private static final Object PRESENT = new Object(); 常用方法 由于底层是hashmap存储的,所以基本是一样的.\u0008没什么区别.实现\u0008不重复插入是通过比较put操作的返回值是不是null\npublic Iterator\u0026lt;E\u0026gt; iterator() { return map.keySet().iterator(); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); } @SuppressWarnings(\u0026quot;unchecked\u0026quot;) public Object clone() { try { HashSet\u0026lt;E\u0026gt; newSet = (HashSet\u0026lt;E\u0026gt;) super.clone(); newSet.map = (HashMap\u0026lt;E, Object\u0026gt;) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(e); } } ","id":19,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Hashset","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/hashset/","year":"2019"},{"content":" 参考blog:田小波的blog\n 介绍 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。还提供了一些增删改查操作时的回调方法.\n源码介绍 Entry LinkedHashMap的节点 LinkedHashMap.Entry继承了hashMap的Node 增加了before和after两个节点,用于维护链表有序\nstatic class Entry\u0026lt;K,V\u0026gt; extends HashMap.Node\u0026lt;K,V\u0026gt; { Entry\u0026lt;K,V\u0026gt; before, after; Entry(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { super(hash, key, value, next); } } 变量 /** * 头结点 * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entry\u0026lt;K,V\u0026gt; head; /** * 尾节点 * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry\u0026lt;K,V\u0026gt; tail; /* 设置链表排序规则: true 按照访问顺序排序 false按照插入顺序排序*/ final boolean accessOrder; 构造参数 LinkedHashMap基本都是复用的的hashmap的构造方法,只是对accessOrder初始化\npublic LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } public LinkedHashMap() { super(); accessOrder = false; } public LinkedHashMap(Map\u0026lt;? extends K, ? extends V\u0026gt; m) { super(); accessOrder = false; putMapEntries(m, false); } public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } 基本操作 /* get操作:和hashmap逻辑一致,只是会判断是否根据访问顺序排序 */ public V get(Object key) { Node\u0026lt;K,V\u0026gt; e; if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e);//访问后的操作,将节点放到最后 return e.value; } /** * 将节点移到最后的逻辑很简单 * 将该节点的before,after节点相连,并将该节点挂到last上 * 如果该节点的before为头结点,则head=after 否则before.after = after * 如果该节点的after为尾节点,则last=before 否则 after.before = before * 最后再把该节点挂在尾节点上 */ void afterNodeAccess(Node\u0026lt;K,V\u0026gt; e) { // move node to last LinkedHashMap.Entry\u0026lt;K,V\u0026gt; last; if (accessOrder \u0026amp;\u0026amp; (last = tail) != e) {//e不为尾节点时 LinkedHashMap.Entry\u0026lt;K,V\u0026gt; p = (LinkedHashMap.Entry\u0026lt;K,V\u0026gt;)e, b = p.before, a = p.after;//获取前后节点 p.after = null; if (b == null)//如果b为头结点时 head = a; else b.after = a;//否则与a连接 if (a != null)//a不为尾节点时 a.before = b;//与b连接 else last = b; if (last == null)//说明链表是空的 head = p; else { p.before = last; last.after = p; } tail = p;//挂靠到tail上 ++modCount; } } /** * return true 删除链表中最久的entry * 一般重写来实现简单版的LRU缓存 */ protected boolean removeEldestEntry(Map.Entry\u0026lt;K,V\u0026gt; eldest) { return false; } /* 新建一个node 并插入到尾部 */ Node\u0026lt;K,V\u0026gt; newNode(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; e) { LinkedHashMap.Entry\u0026lt;K,V\u0026gt; p = new LinkedHashMap.Entry\u0026lt;K,V\u0026gt;(hash, key, value, e); linkNodeLast(p); return p; } // link at the end of list private void linkNodeLast(LinkedHashMap.Entry\u0026lt;K,V\u0026gt; p) { LinkedHashMap.Entry\u0026lt;K,V\u0026gt; last = tail; tail = p; if (last == null)//链表为null时 head = p; else { p.before = last; last.after = p; } } ","id":20,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Linkedhashmap","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/linkedhashmap/","year":"2019"},{"content":"实现了List和Deque接口的双向队列,允许插入null值\n主要属性 transient int size = 0;//大小 /** * Pointer to first node. * Invariant: (first == null \u0026amp;\u0026amp; last == null) || * (first.prev == null \u0026amp;\u0026amp; first.item != null) */ transient Node\u0026lt;E\u0026gt; first; //头结点 /** * Pointer to last node. * Invariant: (first == null \u0026amp;\u0026amp; last == null) || * (last.next == null \u0026amp;\u0026amp; last.item != null) */ transient Node\u0026lt;E\u0026gt; last; //尾节点 //Node节点长这样:item实体对象,prev/next指向前一个后一个节点 private static class Node\u0026lt;E\u0026gt; { E item; Node\u0026lt;E\u0026gt; next; Node\u0026lt;E\u0026gt; prev; Node(Node\u0026lt;E\u0026gt; prev, E element, Node\u0026lt;E\u0026gt; next) { this.item = element; this.next = next; this.prev = prev; } } 构造函数 主要介绍带参数的构造函数\n//c == null 时会抛空指针异常 public LinkedList(Collection\u0026lt;? extends E\u0026gt; c) { this(); addAll(c); } //最终会走到 addAll方法 public boolean addAll(int index, Collection\u0026lt;? extends E\u0026gt; c) { checkPositionIndex(index);//判断index是否非法 index\u0026lt;0 || index\u0026gt;size //集合转数组 Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node\u0026lt;E\u0026gt; pred, succ; if (index == size) { //默认会从最后一个节点追加 succ = null; pred = last; } else { //自定义index后 会先取到该index对应的对象 succ = node(index);//调用node方法取得index位置下的node pred = succ.prev; } //遍历需要赋值的数组 for (Object o : a) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) E e = (E) o; Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(pred, e, null);//不要next节点是因为迭代时可以自行设置 if (pred == null)//说明是从头开始 first = newNode; else pred.next = newNode; pred = newNode; } //尾部node双向绑定 if (succ == null) {//不是指定index地方插入时,即从尾部插入,没有succ节点 last = pred; } else {//从指定index插入时,需要与尾部节点连接 pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true; } Node\u0026lt;E\u0026gt; node(int index) { // assert isElementIndex(index); //掰成两半儿查找 if (index \u0026lt; (size \u0026gt;\u0026gt; 1)) {//小于size的一半时从头开始查 Node\u0026lt;E\u0026gt; x = first; for (int i = 0; i \u0026lt; index; i++) x = x.next; return x; } else {//index大于size的一半时,从后往前找 Node\u0026lt;E\u0026gt; x = last; for (int i = size - 1; i \u0026gt; index; i--) x = x.prev; return x; } } 主要方法 1.get\npublic E get(int index) { checkElementIndex(index);//检查index界限,会抛下标越界异常 return node(index).item;//遍历取得指定index下的node } 2.set\npublic E set(int index, E element) { //检查下标 checkElementIndex(index); Node\u0026lt;E\u0026gt; x = node(index);//获取对应的node E oldVal = x.item;//取出旧item x.item = element;//赋值新item return oldVal; } add\npublic void add(int index, E element) { checkPositionIndex(index); if (index == size)//index等于size大小时在最后追加 linkLast(element); else linkBefore(element, node(index));//在该index直接添加 } //在尾部连接一个对象 void linkLast(E e) { final Node\u0026lt;E\u0026gt; l = last;//获得当前的最后一个节点 final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(l, e, null);//新建一个节点,当前最后一个节点作为prev节点 last = newNode; if (l == null)//若前一个节点为null list还没有值 则头尾都用该节点 first = newNode; else l.next = newNode;//将上一个节点与新节点连接起来 size++;//链表长度+1 modCount++;//操作链表次数+1 } //官方文档上这么说:在一个非空节点前插入新节点 //但是其实没有做非空校验了 void linkBefore(E e, Node\u0026lt;E\u0026gt; succ) { // assert succ != null; 非空校验注释掉了 final Node\u0026lt;E\u0026gt; pred = succ.prev;//中间插入 那就是succ的prev节点要重新与新节点的prev连接,新节点的next节点为succ节点 final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(pred, e, succ); succ.prev = newNode;//succ节点与新节点连接 if (pred == null)//上一个节点为空时则首尾节点都是该节点 first = newNode; else pred.next = newNode; size++;//链表长度+1 modCount++;//操作次数+1 } 4.remove 除了各种逻辑最后都会用到unlink方法:大致是将需要删除的对象的prev和next节点重新连接起来,在将该对象置空让gc回收\nE unlink(Node\u0026lt;E\u0026gt; x) { // assert x != null; final E element = x.item; final Node\u0026lt;E\u0026gt; next = x.next;//获取该节点下一个节点 final Node\u0026lt;E\u0026gt; prev = x.prev;//获取该节点上一个节点 if (prev == null) {//上一个节点为null时则删除的是头结点 将下一个节点变为头结点 first = next; } else { prev.next = next;//prev不为null时 将prev的next改为x的next x.prev = null;//将x的prev置空 让gc回收 } if (next == null) {//x的next为null时说明该节点是尾节点 last = prev; } else { next.prev = prev;//next的prev挂靠到x的prev x.next = null;//x的next置空回收 } x.item = null;//item置空回收 size--;//长度减1 modCount++;//操作次数+1 return element; } clear操作\n//清空所有的节点 public void clear() { // Clearing all of the links between nodes is \u0026quot;unnecessary\u0026quot;, but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator for (Node\u0026lt;E\u0026gt; x = first; x != null; ) { Node\u0026lt;E\u0026gt; next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++; } 查询操作 1.indexOf 容易看懂就不解释了,查不到时返回-1,可以查null\npublic int indexOf(Object o) { int index = 0; if (o == null) { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; } 队列类的操作 peek\n//获取头节点 但不删除 可以为null public E peek() { final Node\u0026lt;E\u0026gt; f = first; return (f == null) ? null : f.item; } element 和peek一样也是获取头结点 但是若为null会抛空指针异常\n poll 就是队列里的pop操作,即出队\npublic E poll() { final Node\u0026lt;E\u0026gt; f = first; return (f == null) ? null : unlinkFirst(f); } 4.offer入队 队尾插入元素\npublic boolean offer(E e) { return add(e); } LinkedList中的迭代器 //该迭代器是快速失败的,如果在创建迭代器后操作了链表(add/remove),不是迭代器中的操作(add/remove),就会抛ConcurrentModificationException异常. //原因是迭代器中维护了expectedModCount每次操作前都会比较该值与modCount是否一致,不一致就抛,所以在迭代中增删节点时还是要通过迭代器的操作比较好 private class ListItr implements ListIterator\u0026lt;E\u0026gt; { private Node\u0026lt;E\u0026gt; lastReturned;//用作返回节点 private Node\u0026lt;E\u0026gt; next;//记录下一个节点 private int nextIndex;//下一个index private int expectedModCount = modCount;//初始化期望操作值 ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index);//若index==size则是尾节点 nextIndex = index; } public boolean hasNext() { return nextIndex \u0026lt; size;//通过坐标值判断是否有下一个 } public E next() { checkForComodification();//校验操作避免使用list的add/remove操作 if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex \u0026gt; 0; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } //迭代器的删除节点操作 public void remove() { checkForComodification();//校验操作次数 if (lastReturned == null)//状态校验 throw new IllegalStateException(); Node\u0026lt;E\u0026gt; lastNext = lastReturned.next;//获得当前节点的下一个节点 unlink(lastReturned);//断开当前节点,将当前节点的前后节点相连接 size会减1 modCount会+1 if (next == lastReturned) next = lastNext; else nextIndex--;//坐标值减1 lastReturned = null; expectedModCount++;//保持与modCount一致 } //设置当前的节点的item public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } //和普通add操作一样 主要是需要将nextIndex和expectedModCount都+1 public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; } //挺方便的迭代 重写下accept方法 可以用lambda表达式 public void forEachRemaining(Consumer\u0026lt;? super E\u0026gt; action) { Objects.requireNonNull(action); while (modCount == expectedModCount \u0026amp;\u0026amp; nextIndex \u0026lt; size) { action.accept(next.item); lastReturned = next; next = next.next; nextIndex++; } checkForComodification(); } //操作校验 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } ","id":21,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Linkedlist","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/linkedlist/","year":"2019"},{"content":" 红黑树介绍 TreeMap解析\n简介 TreeMap底层基于红黑树实现,可以保证在log(n)时间复杂度下完成增删改查的操作,效率很高,由于基于红黑树,所以保持有序. TreeMap继承自AbstractMap,并实现了Nav\u0008igableMap(主要是提供一些\u0008导航类的操作,比如获得\u0008比当前节点小\u0008的最大值,比当前节点大的最小值等) key不允许为空 graph BT A(C:TreeMap) -.-\u0026gt; B(I:NavigableMap) A --\u0026gt; C(C:AbstractMap) B --\u0026gt; E(I:SortedMap) E --\u0026gt; D(I:Map) C -.-\u0026gt;D(I:Map) 基本操作 /* get 操作 可以返回null值 如果key为null 则会抛 NPE */ public V get(Object key) { Entry\u0026lt;K,V\u0026gt; p = getEntry(key); return (p==null ? null : p.value); } /* get操作实际调用的方法 key为null时 抛NPE */ final Entry\u0026lt;K,V\u0026gt; getEntry(Object key) { // Offload comparator-based version for sake of performance // 除非用comparator构建TreeMap,否则不使用它,为了性能考虑 if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings(\u0026quot;unchecked\u0026quot;) Comparable\u0026lt;? super K\u0026gt; k = (Comparable\u0026lt;? super K\u0026gt;) key; Entry\u0026lt;K,V\u0026gt; p = root; while (p != null) { int cmp = k.compareTo(p.key);//compare key遍历二叉树 if (cmp \u0026lt; 0) p = p.left; else if (cmp \u0026gt; 0) p = p.right; else return p; } return null; } public V put(K key, V value) { //用t表示二叉树的当前节点 Entry\u0026lt;K,V\u0026gt; t = root; //t为null表示一个空树,即TreeMap中没有任何元素,直接插入 if (t == null) { //比较key值,个人觉得这句代码没有任何意义,空树还需要比较、排序? compare(key, key); // type (and possibly null) check //将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root root = new Entry\u0026lt;\u0026gt;(key, value, null); //容器的size = 1,表示TreeMap集合中存在一个元素 size = 1; //修改次数 + 1 modCount++; return null; } int cmp; //cmp表示key排序的返回结果 Entry\u0026lt;K,V\u0026gt; parent; //父节点 // split comparator and comparable paths Comparator\u0026lt;? super K\u0026gt; cpr = comparator; //指定的排序算法 //如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合 if (cpr != null) { do { parent = t; //parent指向上次循环后的t //比较新增节点的key和当前节点key的大小 cmp = cpr.compare(key, t.key); //cmp返回值小于0,表示新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点 if (cmp \u0026lt; 0) t = t.left; //cmp返回值大于0,表示新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点 else if (cmp \u0026gt; 0) t = t.right; //cmp返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值 else return t.setValue(value); } while (t != null); } //如果cpr为空,则采用默认的排序算法进行创建TreeMap集合 else { if (key == null) //key值为空抛出异常 throw new NullPointerException(); /* 下面处理过程和上面一样 */ Comparable\u0026lt;? super K\u0026gt; k = (Comparable\u0026lt;? super K\u0026gt;) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp \u0026lt; 0) t = t.left; else if (cmp \u0026gt; 0) t = t.right; else return t.setValue(value); } while (t != null); } //将新增节点当做parent的子节点 Entry\u0026lt;K,V\u0026gt; e = new Entry\u0026lt;\u0026gt;(key, value, parent); //如果新增节点的key小于parent的key,则当做左子节点 if (cmp \u0026lt; 0) parent.left = e; //如果新增节点的key大于parent的key,则当做右子节点 else parent.right = e; /* * 上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置 * 下面fixAfterInsertion()方法就是对这棵树进行调整、平衡,具体过程参考上面的五种情况 */ fixAfterInsertion(e); //TreeMap元素数量 + 1 size++; //TreeMap容器修改次数 + 1 modCount++; return null; } /** * 上面代码中do{}代码块是实现排序二叉树的核心算法,通过该算法我们可以确认新增节点在该树的正确位置。 * 找到正确位置后将插入即可,这样做了其实还没有完成,因为我知道TreeMap的底层实现是红黑树,红黑树是一棵平衡排序二叉树, * 普通的排序二叉树可能会出现失衡的情况,所以下一步就是要进行调整。fixAfterInsertion(e); 调整的过程务必会涉及到红黑树的左 * 旋、右旋、着色三个基本操作 * 新增节点后的修复操作 * x 表示新增节点 */ private void fixAfterInsertion(Entry\u0026lt;K,V\u0026gt; x) { x.color = RED; //新增节点的颜色为红色 //循环 直到 x不是根节点,且x的父节点不为红色 while (x != null \u0026amp;\u0026amp; x != root \u0026amp;\u0026amp; x.parent.color == RED) { //如果X的父节点(P)是其父节点的父节点(G)的左节点 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //获取X的叔节点(U) Entry\u0026lt;K,V\u0026gt; y = rightOf(parentOf(parentOf(x))); //如果X的叔节点(U) 为红色(情况三) if (colorOf(y) == RED) { //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的叔节点(U)设置为黑色 setColor(y, BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } //如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五) else { //如果X节点为其父节点(P)的右子树,则进行左旋转(情况四) if (x == rightOf(parentOf(x))) { //将X的父节点作为X x = parentOf(x); //右旋转 rotateLeft(x); } //(情况五) //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); //以X的父节点的父节点(G)为中心右旋转 rotateRight(parentOf(parentOf(x))); } } //如果X的父节点(P)是其父节点的父节点(G)的右节点 else { //获取X的叔节点(U) Entry\u0026lt;K,V\u0026gt; y = leftOf(parentOf(parentOf(x))); //如果X的叔节点(U) 为红色(情况三) if (colorOf(y) == RED) { //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的叔节点(U)设置为黑色 setColor(y, BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } //如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五) else { //如果X节点为其父节点(P)的右子树,则进行左旋转(情况四) if (x == leftOf(parentOf(x))) { //将X的父节点作为X x = parentOf(x); //右旋转 rotateRight(x); } //(情况五) //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); //以X的父节点的父节点(G)为中心右旋转 rotateLeft(parentOf(parentOf(x))); } } } //将根节点G强制设置为黑色 root.color = BLACK; } ","id":22,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Treemap","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/treemap/","year":"2019"},{"content":"如同HashSet基于HashMap一样,\u001bTreeSet基于TreeMap,TreeMap是一棵有序的红黑树,那么\u0008TreeSet也如此.提供有序的set集合,不允许重复插入 继承关系如下:\ngraph BT A(TreeSet) -.-\u0026gt; B(NavigableSet) A --\u0026gt; C(AbstractSet) C--\u0026gt;D(AbstractCollection) D-.-\u0026gt;E(Collection) 参数 private transient NavigableMap\u0026lt;E,Object\u0026gt; m; //PRESENT会被当做Map的value与key构建成键值对 private static final Object PRESENT = new Object(); 构造方法 //创建TreeSet的基础构成 map TreeSet(NavigableMap\u0026lt;E,Object\u0026gt; m) { this.m = m; } //按照自然排序构建 public TreeSet() { this(new TreeMap\u0026lt;E,Object\u0026gt;()); } //按照自定义排序构建 public TreeSet(Comparator\u0026lt;? super E\u0026gt; comparator) { this(new TreeMap\u0026lt;\u0026gt;(comparator)); } //按照自然排序构建 并\u0008添加入参集合的元素 public TreeSet(Collection\u0026lt;? extends E\u0026gt; c) { this(); addAll(c); } //根据已有的TreeSet构建\u0008一个新的TreeSet public TreeSet(SortedSet\u0026lt;E\u0026gt; s) { this(s.comparator()); addAll(s); } 主要方法 /** * 将集合中所有的\u0008元素添加到TreeMap中 * 如果集合为空,\u0008或者任一元素为null并且使用的是自然排序,或者 * comparator不允许为空元素则会抛NPE */ public boolean addAll(Collection\u0026lt;? extends E\u0026gt; c) { // Use linear-time version if applicable if (m.size()==0 \u0026amp;\u0026amp; c.size() \u0026gt; 0 \u0026amp;\u0026amp; c instanceof SortedSet \u0026amp;\u0026amp; m instanceof TreeMap) { SortedSet\u0026lt;? extends E\u0026gt; set = (SortedSet\u0026lt;? extends E\u0026gt;) c; TreeMap\u0026lt;E,Object\u0026gt; map = (TreeMap\u0026lt;E, Object\u0026gt;) m; Comparator\u0026lt;?\u0026gt; cc = set.comparator(); Comparator\u0026lt;? super E\u0026gt; mc = map.comparator(); if (cc==mc || (cc != null \u0026amp;\u0026amp; cc.equals(mc))) { map.addAllForTreeSet(set, PRESENT); return true; } } return super.addAll(c); } /* add操作 会去重 \u0008put\u0008返回值为null时说明成功 */ public boolean add(E e) { return m.put(e, PRESENT)==null; } /* 获取并移除第一个元素 如果set为空 则返回null */ public E pollFirst() { Map.Entry\u0026lt;E,?\u0026gt; e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); } /* 获取并移除最后一个元素 如果set为空 则返回null */ public E pollLast() { Map.Entry\u0026lt;E,?\u0026gt; e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); } /** * 返回此 set 的部分视图,其元素大于(或等于,如果 inclusive 为 true)fromElement。 */ public NavigableSet\u0026lt;E\u0026gt; tailSet(E fromElement, boolean inclusive) { return new TreeSet\u0026lt;\u0026gt;(m.tailMap(fromElement, inclusive)); } /** * 返回此 set 的部分视图,其元素大于等于 fromElement。 */ public SortedSet\u0026lt;E\u0026gt; tailSet(E fromElement) { return tailSet(fromElement, true); } /** * 返回此 set 的部分视图,其元素范围从 fromElement 到 toElement。 */ public NavigableSet\u0026lt;E\u0026gt; subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) { return new TreeSet\u0026lt;\u0026gt;(m.subMap(fromElement, fromInclusive, toElement, toInclusive)); } /** * 返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。 */ public SortedSet\u0026lt;E\u0026gt; subSet(E fromElement, E toElement) { return subSet(fromElement, true, toElement, false); } ","id":23,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Treeset","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/treeset/","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":"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/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"posts":[{"content":"","id":0,"section":"posts","summary":"","tags":null,"title":"Posts","uri":"https://xiaohei.im/hugo-theme-pure/posts/","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":1,"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 会记录当前函数检查的进度,并在下一次函数执行时,接着上次的执行.循环往复地执行.\nAOF,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":2,"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":3,"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":4,"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":5,"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":6,"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":7,"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":8,"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":9,"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":10,"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":11,"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":12,"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":13,"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":14,"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":"基本类型-Primitives 标准类型 Scalar Types 有符号整型 signed integers: i8, i16, i32, i64, i128 and isize (pointer size)\n 无符号整型 unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)\n 浮点型 floating point: f32, f64\n 字符 char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)\n 布尔 bool either true or false\n the unit type (), whose only possible value is an empty tuple: ()\n 我的理解就是个empty吧\n 数值字面,没有后缀时.默认整数为 i32,浮点数 f64\n三级目录 有符号整型 signed integers: i8, i16, i32, i64, i128 and isize (pointer size)\n 无符号整型 unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)\n 浮点型 floating point: f32, f64\n 字符 char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)\n 布尔 bool either true or false\n the unit type (), whose only possible value is an empty tuple: ()\n 我的理解就是个empty吧\n 数值字面,没有后缀时.默认整数为 i32,浮点数 f64\n四级目录 有符号整型 signed integers: i8, i16, i32, i64, i128 and isize (pointer size)\n 无符号整型 unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)\n 浮点型 floating point: f32, f64\n 字符 char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)\n 布尔 bool either true or false\n the unit type (), whose only possible value is an empty tuple: ()\n 我的理解就是个empty吧\n 数值字面,没有后缀时.默认整数为 i32,浮点数 f64\n复合类型 Compound Types 数组 array [1,2,3] 元组 tuple (1,true,\u0026ldquo;str\u0026rdquo;,\u0026lsquo;a\u0026rsquo;\u0026hellip;) Note: 默认变量赋值后是不可变的,需要重复赋值需要用mut 修饰\nlet immutable = 1; immutable = 2; // ERROR let mut immutable = 1; immutable = 2; // SUCCESS 所有权踩坑 可变引用在作用域下有且只能有一个可变引用\n 不可在拥有不可变引用的同时拥有可变引用,除非作用域没有重叠.即在引用可变引用时,不可变引用已经失效了.\nlet mut s = String::from(\u0026quot;hello\u0026quot;); let r1 = \u0026amp;s; // 没问题 let r2 = \u0026amp;s; // 没问题 let r3 = \u0026amp;mut s; // 大问题 println!(\u0026quot;{}, {}, and {}\u0026quot;, r1, r2, r3); ////////////////////////////////////// let mut s = String::from(\u0026quot;hello\u0026quot;); let r1 = \u0026amp;s; // 没问题 let r2 = \u0026amp;s; // 没问题 println!(\u0026quot;{} and {}\u0026quot;, r1, r2); // 此位置之后 r1 和 r2 不再使用 let r3 = \u0026amp;mut s; // 没问题 println!(\u0026quot;{}\u0026quot;, r3); ","id":15,"section":"posts","summary":"","tags":["rust"],"title":"rust踩坑笔记","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/rust/","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":16,"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"},{"content":"ArrayList和LinkedList和Vector的区别 简单讲: 1. ArrayList和LinkedList是线程不安全的,而Vector在增删改的操作上都有synchronized关键字修饰,是线性的,但是效率不高 2. ArrayList和Vector都是基于数组的实现,扩容时,ArrayList扩容为1.5倍,Vector扩容为2倍,查找快,增删慢.LinkedList是一个双向链表,增删快,查找慢. \u0026gt; 参考blog\nSynchronizedList和Vector的区别 SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。 使用SynchronizedList的时候,进行遍历时要手动进行同步处理。 SynchronizedList可以指定锁定的对象。 \u0026gt; Hollis的blog 参数 static final int DEFAULT_CAPACITY=10 默认大小 transient object[] elementData; arraylist存储对象的数组.当空ArrayList第一次添加对象时,容量会扩展成DEFAULT_CAPACITY size elementData中实际的对象数 构造函数 看几个主要的 1. 带初始化参数,参数违法时会抛RuntimeException\npublic ArrayList(int initialCapacity) { if (initialCapacity \u0026gt; 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(\u0026quot;Illegal Capacity: \u0026quot;+ initialCapacity); } } 从其他集合中导入,collection需要notNull,否则会抛空指针\npublic ArrayList(Collection\u0026lt;? extends E\u0026gt; c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 主要方法 get\npublic E get(int index) { //判断index是否在范围内 不在会抛 IndexOutOfBoundsException rangeCheck(index); //获取对象值 return elementData(index); } set 替换指定位置的值\npublic E set(int index, E element) { rangeCheck(index);//范围检查 E oldValue = elementData(index);//获取对象旧值 elementData[index] = element; //赋新值 return oldValue; //返回旧值 } add 在elementData尾部添加一个对象\npublic boolean add(E e) { //确保在容量范围内,不在则扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++;//操作list的次数 //若最小容量大于 elementData的长度 则扩容 // overflow-conscious code if (minCapacity - elementData.length \u0026gt; 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1); //扩容大概是1.5倍 if (newCapacity - minCapacity \u0026lt; 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE \u0026gt; 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } add 指定位置添加对象\npublic void add(int index, E element) { //专门的add操作范围检查 主要是保证 0 \u0026lt; index \u0026lt; size rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } ","id":17,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Arraylist","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/arraylist/","year":"2019"},{"content":" 美团的blog:https://tech.meituan.com/java_hashmap.html 参考blog: 田小波的博客 红黑树介绍\n HashMap,HashTable,ConcurrentHashMap的区别? HashMap是非线程安全的,HashTable和ConcurrentHashMap是线程安全的.HashTable不允许null key和null Value,HashMap允许. ConcurrentHashMap推出之后官方推荐不要在使用HashTable作为线程安全的使用类,而是使用这个.关于ConcurrentHashMap后面再学习. \u0026gt; 参考blog\nHashMap在不同版本之间实现的区别? 区别\n 官方文档介绍: 基于Map接口实现的哈希表.提供了所有map可选的操作,允许key为null,value为null.HashMap与HashTable基本一致,除了HashMap 线程不安全并且允许为空. 不保证有序,尤其不保证顺序一直不变(因为扩容时会rehash,基本上就顺序就重排了) 假设hash分布均匀的情况下,基本的操作(get/put)性能很不错.迭代所需要的时间与buckets数量与每个bukets下的键值对的数量之和成正比.所以官方建议如果要求hashmap的迭代性能的话,初始的capacity不能太高,loadFactor不要太高. HashMap有两个重要的参数:initial capacity,load factor.capacity定义bucket的数量,initial capacity定义的是初始化bucket数量.load factor(中文名: 加载因子 )是判断哈希表是否需要扩容的阈值,当entries数量超过(load factor * current capacity),哈希表会触发rehash操作,内部数据结构会重整,buckets数量会变为之前大约两倍左右\n 通常情况下,load factor 默认0.75f,在时间空间上是很平衡的.值偏高时,空间减少,查找时间上升了(影响大部分的操作,get/put之类的),在设置初始容量时,需要考虑到预期的entries数量和加载因子,以便最小化rehash的数量.如果初始化的容量大于最大数量的entries除以加载因子,不会发生rehash操作.\n 如果有大量的键值对存到hashmap中,那么创建一个足够大的hashmap来存储要比让他自动rehash扩容来存储的性能要好很多.注意:具有相同hashcode的多个key肯定会影响哈希表的性能.为了改善这种影响,当key是Comparable类型时,可以通过key之间的比较顺序来打破这种关系.\n 注意hashmap是Non synchronized,即 非线程安全.如果多线程并发访问hashmap,并且至少有一个线程操作map的结构,在外部必须synchronized.(结构修改是指任何关于add或delte的操作,仅仅只是修改key关联的value时则不属于结构修改).通常在将object封装进map做synchronized操作\n 如果不存在上面的objects,那这个map需要被Collections.synchronizedMap包装下.最好在创建的时候就做好,防止偶然的并发访问.\nMap m = Collections.synchronizedMap(new HashMap(...)); 迭代器的所有方法都是fail-fast,如果迭代器创建后,在迭代器里的结构操作必须通过迭代器的方法来操作,否则会抛ConcurrentModificationException.因此,面对并发修改,迭代器会快速而干净的失败,而不是在未来的不确定时间冒任意非确定行为的风险.\n 请注意,迭代器的快速失败行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证. 快速失败迭代器会尽最大努力抛出ConcurrentModificationException. 因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误.\n 源码 /** * The default initial capacity - MUST be a power of two. * 默认的容量16 必须是2的n次方 */ static final int DEFAULT_INITIAL_CAPACITY = 1 \u0026lt;\u0026lt; 4; // aka 16 /** * 最大的容量限制 * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two \u0026lt;= 1\u0026lt;\u0026lt;30. */ static final int MAXIMUM_CAPACITY = 1 \u0026lt;\u0026lt; 30; /** * 默认的加载因子 * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 大于这个值转红黑树 * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8; /** * 大于这个值小于 TREEIFY_THRESHOLD 不转树 * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6; /** * hashmap整体容量大于这个值时才能树化 * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. */ static final int MIN_TREEIFY_CAPACITY = 64; /** * node节点 * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { final int hash; final K key; V value; Node\u0026lt;K,V\u0026gt; next; Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + \u0026quot;=\u0026quot; + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry\u0026lt;?,?\u0026gt; e = (Map.Entry\u0026lt;?,?\u0026gt;)o; if (Objects.equals(key, e.getKey()) \u0026amp;\u0026amp; Objects.equals(value, e.getValue())) return true; } return false; } } //hashMap中的静态方法 /** * hash方法详解 blog:http://www.hollischuang.com/archives/2091 * 扰动算法--使hash分布更均匀 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } //取模运算,获得对象存储到bukets的下标 //实际上就是取模,一般取模使用% 但是考虑到效率问题,采用位运算 //X % 2^n = X \u0026amp; (2^n-1) 这也是为什么hashmap容量为2的n次方的原因 static int indexFor(int h, int length) { return h \u0026amp; (length-1); } //返回hashmap的容量 2的n次方 很巧妙的位运算 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n \u0026gt;\u0026gt;\u0026gt; 1; n |= n \u0026gt;\u0026gt;\u0026gt; 2; n |= n \u0026gt;\u0026gt;\u0026gt; 4; n |= n \u0026gt;\u0026gt;\u0026gt; 8; n |= n \u0026gt;\u0026gt;\u0026gt; 16; return (n \u0026lt; 0) ? 1 : (n \u0026gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } /** * 参数都用transient不让序列化的原因:https://segmentfault.com/q/1010000000630486 */ //bukets hashmap是链表加数组的结构.此为数组 transient Node\u0026lt;K,V\u0026gt;[] table; //保存键值对的Entry transient Set\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt; entrySet; //hashmap的size transient int size; //结构操作次数 可用于快速失败的比较条件 例如并发操作时 transient int modCount; //resize的临界点: capacity * load factor int threshold; //加载因子 final float loadFactor; //公有操作方法 //构造方法 /** * 根据 initial capactity 和 loadFactor创建空的hashmap * Constructs an empty \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public HashMap(int initialCapacity, float loadFactor) { //校验initialCapacity if (initialCapacity \u0026lt; 0) throw new IllegalArgumentException(\u0026quot;Illegal initial capacity: \u0026quot; + initialCapacity); //容量校验 if (initialCapacity \u0026gt; MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //校验loadFactor isNaN--\u0026gt; 是否是一个number Not-a-Number if (loadFactor \u0026lt;= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException(\u0026quot;Illegal load factor: \u0026quot; + loadFactor); //加载因子赋值 this.loadFactor = loadFactor; //扩容阈值赋值 2的n次方 this.threshold = tableSizeFor(initialCapacity); } /** * 通过initialCapacity赋值 * Constructs an empty \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 根据默认容量和默认加载因子创建空的hashmap * Constructs an empty \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * 根据传进来的map创建一个新的hashMap * initialCapacity 足以装下参数map的数量 * loadFactor使用默认值 * Constructs a new \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; with the same mappings as the * specified \u0026lt;tt\u0026gt;Map\u0026lt;/tt\u0026gt;. The \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified \u0026lt;tt\u0026gt;Map\u0026lt;/tt\u0026gt;. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public HashMap(Map\u0026lt;? extends K, ? extends V\u0026gt; m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } /** * Implements Map.putAll and Map constructor * * @param m the map * @param evict false when initially constructing this map, else * true (relayed to method afterNodeInsertion). * evict 初始化构建map时 为false 其他情况下为true */ final void putMapEntries(Map\u0026lt;? extends K, ? extends V\u0026gt; m, boolean evict) { int s = m.size(); if (s \u0026gt; 0) { //初次创建hashmap if (table == null) { // pre-size //计算m所需要的容量 float ft = ((float)s / loadFactor) + 1.0F; //获得真实的容量 int t = ((ft \u0026lt; (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //如果比默认的阈值大则计算该 t 对应的capacity if (t \u0026gt; threshold) threshold = tableSizeFor(t); } else if (s \u0026gt; threshold) // 如果是table不为null 即是后续往map中添加 如果s \u0026gt; 阈值就要重置map了 resize();//resize操作 后面介绍 //确定容量后put操作 for (Map.Entry\u0026lt;? extends K, ? extends V\u0026gt; e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict);// } } } /*主要调用 putVal */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * put 操作 * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value 不存在才put\u0026lt;D-[\u0026gt; * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; //若是新建map的情况下 resize创建指定长度的table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //取模计算该key对应的数组下标 并判断该坐标下的对象是否为null //为null时创建一个新node存入tab[i] if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) tab[i] = newNode(hash, key, value, null); else {//tab[i] != null Node\u0026lt;K,V\u0026gt; e; K k; //如果p与存入的key完全相同 if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) e = p; else if (p instanceof TreeNode) //如果是红黑树节点 调用putTreeVal e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); else { //普通的put //binCount记录了链表的长度 for (int binCount = 0; ; ++binCount) { //如果当前node的next==null说明就可以往该链上添加一个节点 if ((e = p.next) == null) { //新建node接到p.next下面 p.next = newNode(hash, key, value, null); //如果binCount大于设定的红黑树化阈值 if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//红黑树化 break; } //如果key与链表中的任意node完全相同break if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) break; p = e; } } //如果存在该key if (e != null) { // existing mapping for key V oldValue = e.value;//获得旧值 if (!onlyIfAbsent || oldValue == null)//若没有设置不存在才put或者oldValue=null e.value = value;//赋新值 afterNodeAccess(e);//LinkedHashMap操作 return oldValue;//返回旧值 } } ++modCount; if (++size \u0026gt; threshold)//是否需要扩容 resize(); afterNodeInsertion(evict);//LinkedHashMap操作 return null; } /** * 扩容操作 * 若是初始化则根据initialCapacity创建一个table * 否则,扩容为2的n次方倍 * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node\u0026lt;K,V\u0026gt;[] resize() { Node\u0026lt;K,V\u0026gt;[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap \u0026gt; 0) { //超过最大值不会再扩容了 if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap \u0026lt;\u0026lt; 1) \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; oldCap \u0026gt;= DEFAULT_INITIAL_CAPACITY) newThr = oldThr \u0026lt;\u0026lt; 1; // double threshold 扩成两倍 } else if (oldThr \u0026gt; 0) // initial capacity was placed in threshold newCap = oldThr; else { // 默认配置 zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //计算新的阈值 float ft = (float)newCap * loadFactor; newThr = (newCap \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; ft \u0026lt; (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({\u0026quot;rawtypes\u0026quot;,\u0026quot;unchecked\u0026quot;}) Node\u0026lt;K,V\u0026gt;[] newTab = (Node\u0026lt;K,V\u0026gt;[])new Node[newCap]; table = newTab; if (oldTab != null) { //把old buket 移到新的bukets里 for (int j = 0; j \u0026lt; oldCap; ++j) { Node\u0026lt;K,V\u0026gt; e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null)//直接添加 newTab[e.hash \u0026amp; (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode\u0026lt;K,V\u0026gt;)e).split(this, newTab, j, oldCap); else { // preserve order Node\u0026lt;K,V\u0026gt; loHead = null, loTail = null; Node\u0026lt;K,V\u0026gt; hiHead = null, hiTail = null; Node\u0026lt;K,V\u0026gt; next; do { next = e.next; //这个取模很精辟 请结合美团的blog resize 1.8优化学习 //因为扩容是2倍扩容,二进制中相当于左移一位 /** * 假设一次扩容\t* 扩容前\toldCap = 00010000 oldCap - 1 = 00001111 * 扩容后\tnewCap = 00100000 newCap - 1 = 00011111 * 可以看出扩容后 newCap-1 在高位多了1 * 计算index时 hash \u0026amp; n-1 = 原位置 + oldCap * 所以只需要判断hash \u0026amp; oldCap是否为1 * 为1则把该node的位置移到 oldCap+原位置 * 为 0 还在原位置 */ if ((e.hash \u0026amp; oldCap) == 0) {//为0说明位置没有变 if (loTail == null)//第一次添加时loHead=e loHead = e; else loTail.next = e;//直接往后插入 loTail = e; } else {//为1 说明位置会+oldCap长度 if (hiTail == null) hiHead = e;//头节点初始化 else hiTail.next = e;//直接插入 hiTail = e; } } while ((e = next) != null); if (loTail != null) {//放在原位置上 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) {//放在原位置+oldCap上 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } /** * get操作 * 为null时返回null 这个要注意下 */ public V get(Object key) { Node\u0026lt;K,V\u0026gt; e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * get方法 * 主要是 key相等 或者 key equals的比较 * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node\u0026lt;K,V\u0026gt; getNode(int hash, Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; first, e; int n; K k; if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (first = tab[(n - 1) \u0026amp; hash]) != null) {//获得节点 if (first.hash == hash \u0026amp;\u0026amp; // always check first node ((k = first.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode)//树节点 return ((TreeNode\u0026lt;K,V\u0026gt;)first).getTreeNode(hash, key); do { if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 1.8红黑树化源码解析 /** * TreeNode extends LinkedHashMap.Entry * LinkedHashMap.Entry extends HashMap.Node */ static final class TreeNode\u0026lt;K,V\u0026gt; extends LinkedHashMap.Entry\u0026lt;K,V\u0026gt; { TreeNode\u0026lt;K,V\u0026gt; parent; // red-black tree links 红黑树父节点 TreeNode\u0026lt;K,V\u0026gt; left; TreeNode\u0026lt;K,V\u0026gt; right; TreeNode\u0026lt;K,V\u0026gt; prev; // needed to unlink next upon deletion 删除的时候用来连接前后 boolean red;//红还是黑 TreeNode(int hash, K key, V val, Node\u0026lt;K,V\u0026gt; next) { super(hash, key, val, next); } } /**树化 * putVal里有用到 * 将链表重置为红黑树并放到该hash映射的tab下,如果tab过下则resize * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node\u0026lt;K,V\u0026gt;[] tab, int hash) { int n, index; Node\u0026lt;K,V\u0026gt; e; if (tab == null || (n = tab.length) \u0026lt; MIN_TREEIFY_CAPACITY)//小于最小树化的容量时不树化而resize capacity为64, resize(); else if ((e = tab[index = (n - 1) \u0026amp; hash]) != null) { TreeNode\u0026lt;K,V\u0026gt; hd = null, tl = null;//头尾节点 do { TreeNode\u0026lt;K,V\u0026gt; p = replacementTreeNode(e, null);//这个就是返回一个新建的TreeNode对象,内容为e if (tl == null)//确定是头结点 hd = p;//标记头结点 else {//非头结点就首尾连接 p.prev = tl; tl.next = p; } tl = p;//尾节点一直为p } while ((e = e.next) != null);//遍历链表 其实此时形成也还算是个链表 if ((tab[index] = hd) != null)//将该treeNode挂到table下 hd.treeify(tab);//完成红黑树化 } } /** * Forms tree of the nodes linked from this node. * @return root of tree */ final void treeify(Node\u0026lt;K,V\u0026gt;[] tab) { TreeNode\u0026lt;K,V\u0026gt; root = null; for (TreeNode\u0026lt;K,V\u0026gt; x = this, next; x != null; x = next) {//x 从当前节点开始(从treeifyBin里调用看是头结点) next = (TreeNode\u0026lt;K,V\u0026gt;)x.next;//获取下个节点 x.left = x.right = null; if (root == null) {//设置root节点并给他黑色 x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class\u0026lt;?\u0026gt; kc = null; //遍历所有节点与当前节点x比较 调整位置 有点像冒泡排序 for (TreeNode\u0026lt;K,V\u0026gt; p = root;;) { int dir, ph; K pk = p.key; //比较hash值 if ((ph = p.hash) \u0026gt; h) dir = -1; else if (ph \u0026lt; h) dir = 1; else if ((kc == null \u0026amp;\u0026amp; (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); //根据dir判断x是p的左孩子 还是 右孩子 TreeNode\u0026lt;K,V\u0026gt; xp = p; if ((p = (dir \u0026lt;= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir \u0026lt;= 0) xp.left = x; else xp.right = x; //平衡节点 root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); } /** * Returns a list of non-TreeNodes replacing those linked from * this node. */ final Node\u0026lt;K,V\u0026gt; untreeify(HashMap\u0026lt;K,V\u0026gt; map) { Node\u0026lt;K,V\u0026gt; hd = null, tl = null; for (Node\u0026lt;K,V\u0026gt; q = this; q != null; q = q.next) { Node\u0026lt;K,V\u0026gt; p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } /** * 红黑树版put操作 * Tree version of putVal. */ final TreeNode\u0026lt;K,V\u0026gt; putTreeVal(HashMap\u0026lt;K,V\u0026gt; map, Node\u0026lt;K,V\u0026gt;[] tab, int h, K k, V v) { Class\u0026lt;?\u0026gt; kc = null; boolean searched = false; TreeNode\u0026lt;K,V\u0026gt; root = (parent != null) ? root() : this;//每次从根节点遍历 for (TreeNode\u0026lt;K,V\u0026gt; p = root;;) { int dir, ph; K pk; if ((ph = p.hash) \u0026gt; h) dir = -1; else if (ph \u0026lt; h) dir = 1; else if ((pk = p.key) == k || (k != null \u0026amp;\u0026amp; k.equals(pk))) //如果当前节点key相同或equals 返回 return p; else if ((kc == null \u0026amp;\u0026amp; (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { //hash值如果相等 但类不相同,只能挨个对比左右孩子 if (!searched) { TreeNode\u0026lt;K,V\u0026gt; q, ch; searched = true; if (((ch = p.left) != null \u0026amp;\u0026amp; (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null \u0026amp;\u0026amp; (q = ch.find(h, k, kc)) != null)) return q; } //哈希值相等 但键无法比较 只能通过其他方法比较 dir = tieBreakOrder(k, pk); } //得到两个节点的大小关系 即dir的值时 //并判断只有在左孩子或右孩子不能 TreeNode\u0026lt;K,V\u0026gt; xp = p; if ((p = (dir \u0026lt;= 0) ? p.left : p.right) == null) { Node\u0026lt;K,V\u0026gt; xpn = xp.next; TreeNode\u0026lt;K,V\u0026gt; x = map.newTreeNode(h, k, v, xpn); if (dir \u0026lt;= 0) xp.left = x; else xp.right = x; xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode\u0026lt;K,V\u0026gt;)xpn).prev = x; //平衡二叉树 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } /** 查找操作 传入 hash值 和 key值 * Calls find for root node. */ final TreeNode\u0026lt;K,V\u0026gt; getTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null);//判断从当前节点还是root节点开始查找 } /** * Finds the node starting at root p with the given hash and key. * The kc argument caches comparableClassFor(key) upon first use * comparing keys. */ final TreeNode\u0026lt;K,V\u0026gt; find(int h, Object k, Class\u0026lt;?\u0026gt; kc) { TreeNode\u0026lt;K,V\u0026gt; p = this; do { int ph, dir; K pk; TreeNode\u0026lt;K,V\u0026gt; pl = p.left, pr = p.right, q; //根据hash值查找 当前节点hash值大于h则 查左孩子 否则右孩子 当key相等或者equal时返回 if ((ph = p.hash) \u0026gt; h) p = pl; else if (ph \u0026lt; h) p = pr; else if ((pk = p.key) == k || (k != null \u0026amp;\u0026amp; k.equals(pk))) return p; else if (pl == null) p = pr; else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) \u0026amp;\u0026amp; (dir = compareComparables(kc, k, pk)) != 0) p = (dir \u0026lt; 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null)//不相等则从子树继续查找 return q; else p = pl; } while (p != null); return null; } ","id":18,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Hashmap","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/hashmap/","year":"2019"},{"content":" HashSet详解\n 介绍 基于HashMap保存元素不保证有序,不包含重复元素,允许null值.主要继承关系如图:\ngraph BT HashSet--\u0026gt;AbstractSet HashSet-.-\u0026gt;Set AbstractSet-.-\u0026gt;Set AbstractSet--\u0026gt;AbstractCollection AbstractCollection-.-\u0026gt;Collection\t 参数 static final long serialVersionUID = -5024744406713321676L; //底层存储用的hashMap private transient HashMap\u0026lt;E,Object\u0026gt; map; //定义一个Object对象作为HashMap的value private static final Object PRESENT = new Object(); 常用方法 由于底层是hashmap存储的,所以基本是一样的.\u0008没什么区别.实现\u0008不重复插入是通过比较put操作的返回值是不是null\npublic Iterator\u0026lt;E\u0026gt; iterator() { return map.keySet().iterator(); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); } @SuppressWarnings(\u0026quot;unchecked\u0026quot;) public Object clone() { try { HashSet\u0026lt;E\u0026gt; newSet = (HashSet\u0026lt;E\u0026gt;) super.clone(); newSet.map = (HashMap\u0026lt;E, Object\u0026gt;) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(e); } } ","id":19,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Hashset","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/hashset/","year":"2019"},{"content":" 参考blog:田小波的blog\n 介绍 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。还提供了一些增删改查操作时的回调方法.\n源码介绍 Entry LinkedHashMap的节点 LinkedHashMap.Entry继承了hashMap的Node 增加了before和after两个节点,用于维护链表有序\nstatic class Entry\u0026lt;K,V\u0026gt; extends HashMap.Node\u0026lt;K,V\u0026gt; { Entry\u0026lt;K,V\u0026gt; before, after; Entry(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { super(hash, key, value, next); } } 变量 /** * 头结点 * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entry\u0026lt;K,V\u0026gt; head; /** * 尾节点 * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry\u0026lt;K,V\u0026gt; tail; /* 设置链表排序规则: true 按照访问顺序排序 false按照插入顺序排序*/ final boolean accessOrder; 构造参数 LinkedHashMap基本都是复用的的hashmap的构造方法,只是对accessOrder初始化\npublic LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } public LinkedHashMap() { super(); accessOrder = false; } public LinkedHashMap(Map\u0026lt;? extends K, ? extends V\u0026gt; m) { super(); accessOrder = false; putMapEntries(m, false); } public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } 基本操作 /* get操作:和hashmap逻辑一致,只是会判断是否根据访问顺序排序 */ public V get(Object key) { Node\u0026lt;K,V\u0026gt; e; if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e);//访问后的操作,将节点放到最后 return e.value; } /** * 将节点移到最后的逻辑很简单 * 将该节点的before,after节点相连,并将该节点挂到last上 * 如果该节点的before为头结点,则head=after 否则before.after = after * 如果该节点的after为尾节点,则last=before 否则 after.before = before * 最后再把该节点挂在尾节点上 */ void afterNodeAccess(Node\u0026lt;K,V\u0026gt; e) { // move node to last LinkedHashMap.Entry\u0026lt;K,V\u0026gt; last; if (accessOrder \u0026amp;\u0026amp; (last = tail) != e) {//e不为尾节点时 LinkedHashMap.Entry\u0026lt;K,V\u0026gt; p = (LinkedHashMap.Entry\u0026lt;K,V\u0026gt;)e, b = p.before, a = p.after;//获取前后节点 p.after = null; if (b == null)//如果b为头结点时 head = a; else b.after = a;//否则与a连接 if (a != null)//a不为尾节点时 a.before = b;//与b连接 else last = b; if (last == null)//说明链表是空的 head = p; else { p.before = last; last.after = p; } tail = p;//挂靠到tail上 ++modCount; } } /** * return true 删除链表中最久的entry * 一般重写来实现简单版的LRU缓存 */ protected boolean removeEldestEntry(Map.Entry\u0026lt;K,V\u0026gt; eldest) { return false; } /* 新建一个node 并插入到尾部 */ Node\u0026lt;K,V\u0026gt; newNode(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; e) { LinkedHashMap.Entry\u0026lt;K,V\u0026gt; p = new LinkedHashMap.Entry\u0026lt;K,V\u0026gt;(hash, key, value, e); linkNodeLast(p); return p; } // link at the end of list private void linkNodeLast(LinkedHashMap.Entry\u0026lt;K,V\u0026gt; p) { LinkedHashMap.Entry\u0026lt;K,V\u0026gt; last = tail; tail = p; if (last == null)//链表为null时 head = p; else { p.before = last; last.after = p; } } ","id":20,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Linkedhashmap","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/linkedhashmap/","year":"2019"},{"content":"实现了List和Deque接口的双向队列,允许插入null值\n主要属性 transient int size = 0;//大小 /** * Pointer to first node. * Invariant: (first == null \u0026amp;\u0026amp; last == null) || * (first.prev == null \u0026amp;\u0026amp; first.item != null) */ transient Node\u0026lt;E\u0026gt; first; //头结点 /** * Pointer to last node. * Invariant: (first == null \u0026amp;\u0026amp; last == null) || * (last.next == null \u0026amp;\u0026amp; last.item != null) */ transient Node\u0026lt;E\u0026gt; last; //尾节点 //Node节点长这样:item实体对象,prev/next指向前一个后一个节点 private static class Node\u0026lt;E\u0026gt; { E item; Node\u0026lt;E\u0026gt; next; Node\u0026lt;E\u0026gt; prev; Node(Node\u0026lt;E\u0026gt; prev, E element, Node\u0026lt;E\u0026gt; next) { this.item = element; this.next = next; this.prev = prev; } } 构造函数 主要介绍带参数的构造函数\n//c == null 时会抛空指针异常 public LinkedList(Collection\u0026lt;? extends E\u0026gt; c) { this(); addAll(c); } //最终会走到 addAll方法 public boolean addAll(int index, Collection\u0026lt;? extends E\u0026gt; c) { checkPositionIndex(index);//判断index是否非法 index\u0026lt;0 || index\u0026gt;size //集合转数组 Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node\u0026lt;E\u0026gt; pred, succ; if (index == size) { //默认会从最后一个节点追加 succ = null; pred = last; } else { //自定义index后 会先取到该index对应的对象 succ = node(index);//调用node方法取得index位置下的node pred = succ.prev; } //遍历需要赋值的数组 for (Object o : a) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) E e = (E) o; Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(pred, e, null);//不要next节点是因为迭代时可以自行设置 if (pred == null)//说明是从头开始 first = newNode; else pred.next = newNode; pred = newNode; } //尾部node双向绑定 if (succ == null) {//不是指定index地方插入时,即从尾部插入,没有succ节点 last = pred; } else {//从指定index插入时,需要与尾部节点连接 pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true; } Node\u0026lt;E\u0026gt; node(int index) { // assert isElementIndex(index); //掰成两半儿查找 if (index \u0026lt; (size \u0026gt;\u0026gt; 1)) {//小于size的一半时从头开始查 Node\u0026lt;E\u0026gt; x = first; for (int i = 0; i \u0026lt; index; i++) x = x.next; return x; } else {//index大于size的一半时,从后往前找 Node\u0026lt;E\u0026gt; x = last; for (int i = size - 1; i \u0026gt; index; i--) x = x.prev; return x; } } 主要方法 1.get\npublic E get(int index) { checkElementIndex(index);//检查index界限,会抛下标越界异常 return node(index).item;//遍历取得指定index下的node } 2.set\npublic E set(int index, E element) { //检查下标 checkElementIndex(index); Node\u0026lt;E\u0026gt; x = node(index);//获取对应的node E oldVal = x.item;//取出旧item x.item = element;//赋值新item return oldVal; } add\npublic void add(int index, E element) { checkPositionIndex(index); if (index == size)//index等于size大小时在最后追加 linkLast(element); else linkBefore(element, node(index));//在该index直接添加 } //在尾部连接一个对象 void linkLast(E e) { final Node\u0026lt;E\u0026gt; l = last;//获得当前的最后一个节点 final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(l, e, null);//新建一个节点,当前最后一个节点作为prev节点 last = newNode; if (l == null)//若前一个节点为null list还没有值 则头尾都用该节点 first = newNode; else l.next = newNode;//将上一个节点与新节点连接起来 size++;//链表长度+1 modCount++;//操作链表次数+1 } //官方文档上这么说:在一个非空节点前插入新节点 //但是其实没有做非空校验了 void linkBefore(E e, Node\u0026lt;E\u0026gt; succ) { // assert succ != null; 非空校验注释掉了 final Node\u0026lt;E\u0026gt; pred = succ.prev;//中间插入 那就是succ的prev节点要重新与新节点的prev连接,新节点的next节点为succ节点 final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(pred, e, succ); succ.prev = newNode;//succ节点与新节点连接 if (pred == null)//上一个节点为空时则首尾节点都是该节点 first = newNode; else pred.next = newNode; size++;//链表长度+1 modCount++;//操作次数+1 } 4.remove 除了各种逻辑最后都会用到unlink方法:大致是将需要删除的对象的prev和next节点重新连接起来,在将该对象置空让gc回收\nE unlink(Node\u0026lt;E\u0026gt; x) { // assert x != null; final E element = x.item; final Node\u0026lt;E\u0026gt; next = x.next;//获取该节点下一个节点 final Node\u0026lt;E\u0026gt; prev = x.prev;//获取该节点上一个节点 if (prev == null) {//上一个节点为null时则删除的是头结点 将下一个节点变为头结点 first = next; } else { prev.next = next;//prev不为null时 将prev的next改为x的next x.prev = null;//将x的prev置空 让gc回收 } if (next == null) {//x的next为null时说明该节点是尾节点 last = prev; } else { next.prev = prev;//next的prev挂靠到x的prev x.next = null;//x的next置空回收 } x.item = null;//item置空回收 size--;//长度减1 modCount++;//操作次数+1 return element; } clear操作\n//清空所有的节点 public void clear() { // Clearing all of the links between nodes is \u0026quot;unnecessary\u0026quot;, but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator for (Node\u0026lt;E\u0026gt; x = first; x != null; ) { Node\u0026lt;E\u0026gt; next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++; } 查询操作 1.indexOf 容易看懂就不解释了,查不到时返回-1,可以查null\npublic int indexOf(Object o) { int index = 0; if (o == null) { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; } 队列类的操作 peek\n//获取头节点 但不删除 可以为null public E peek() { final Node\u0026lt;E\u0026gt; f = first; return (f == null) ? null : f.item; } element 和peek一样也是获取头结点 但是若为null会抛空指针异常\n poll 就是队列里的pop操作,即出队\npublic E poll() { final Node\u0026lt;E\u0026gt; f = first; return (f == null) ? null : unlinkFirst(f); } 4.offer入队 队尾插入元素\npublic boolean offer(E e) { return add(e); } LinkedList中的迭代器 //该迭代器是快速失败的,如果在创建迭代器后操作了链表(add/remove),不是迭代器中的操作(add/remove),就会抛ConcurrentModificationException异常. //原因是迭代器中维护了expectedModCount每次操作前都会比较该值与modCount是否一致,不一致就抛,所以在迭代中增删节点时还是要通过迭代器的操作比较好 private class ListItr implements ListIterator\u0026lt;E\u0026gt; { private Node\u0026lt;E\u0026gt; lastReturned;//用作返回节点 private Node\u0026lt;E\u0026gt; next;//记录下一个节点 private int nextIndex;//下一个index private int expectedModCount = modCount;//初始化期望操作值 ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index);//若index==size则是尾节点 nextIndex = index; } public boolean hasNext() { return nextIndex \u0026lt; size;//通过坐标值判断是否有下一个 } public E next() { checkForComodification();//校验操作避免使用list的add/remove操作 if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex \u0026gt; 0; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } //迭代器的删除节点操作 public void remove() { checkForComodification();//校验操作次数 if (lastReturned == null)//状态校验 throw new IllegalStateException(); Node\u0026lt;E\u0026gt; lastNext = lastReturned.next;//获得当前节点的下一个节点 unlink(lastReturned);//断开当前节点,将当前节点的前后节点相连接 size会减1 modCount会+1 if (next == lastReturned) next = lastNext; else nextIndex--;//坐标值减1 lastReturned = null; expectedModCount++;//保持与modCount一致 } //设置当前的节点的item public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } //和普通add操作一样 主要是需要将nextIndex和expectedModCount都+1 public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; } //挺方便的迭代 重写下accept方法 可以用lambda表达式 public void forEachRemaining(Consumer\u0026lt;? super E\u0026gt; action) { Objects.requireNonNull(action); while (modCount == expectedModCount \u0026amp;\u0026amp; nextIndex \u0026lt; size) { action.accept(next.item); lastReturned = next; next = next.next; nextIndex++; } checkForComodification(); } //操作校验 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } ","id":21,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Linkedlist","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/linkedlist/","year":"2019"},{"content":" 红黑树介绍 TreeMap解析\n简介 TreeMap底层基于红黑树实现,可以保证在log(n)时间复杂度下完成增删改查的操作,效率很高,由于基于红黑树,所以保持有序. TreeMap继承自AbstractMap,并实现了Nav\u0008igableMap(主要是提供一些\u0008导航类的操作,比如获得\u0008比当前节点小\u0008的最大值,比当前节点大的最小值等) key不允许为空 graph BT A(C:TreeMap) -.-\u0026gt; B(I:NavigableMap) A --\u0026gt; C(C:AbstractMap) B --\u0026gt; E(I:SortedMap) E --\u0026gt; D(I:Map) C -.-\u0026gt;D(I:Map) 基本操作 /* get 操作 可以返回null值 如果key为null 则会抛 NPE */ public V get(Object key) { Entry\u0026lt;K,V\u0026gt; p = getEntry(key); return (p==null ? null : p.value); } /* get操作实际调用的方法 key为null时 抛NPE */ final Entry\u0026lt;K,V\u0026gt; getEntry(Object key) { // Offload comparator-based version for sake of performance // 除非用comparator构建TreeMap,否则不使用它,为了性能考虑 if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings(\u0026quot;unchecked\u0026quot;) Comparable\u0026lt;? super K\u0026gt; k = (Comparable\u0026lt;? super K\u0026gt;) key; Entry\u0026lt;K,V\u0026gt; p = root; while (p != null) { int cmp = k.compareTo(p.key);//compare key遍历二叉树 if (cmp \u0026lt; 0) p = p.left; else if (cmp \u0026gt; 0) p = p.right; else return p; } return null; } public V put(K key, V value) { //用t表示二叉树的当前节点 Entry\u0026lt;K,V\u0026gt; t = root; //t为null表示一个空树,即TreeMap中没有任何元素,直接插入 if (t == null) { //比较key值,个人觉得这句代码没有任何意义,空树还需要比较、排序? compare(key, key); // type (and possibly null) check //将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root root = new Entry\u0026lt;\u0026gt;(key, value, null); //容器的size = 1,表示TreeMap集合中存在一个元素 size = 1; //修改次数 + 1 modCount++; return null; } int cmp; //cmp表示key排序的返回结果 Entry\u0026lt;K,V\u0026gt; parent; //父节点 // split comparator and comparable paths Comparator\u0026lt;? super K\u0026gt; cpr = comparator; //指定的排序算法 //如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合 if (cpr != null) { do { parent = t; //parent指向上次循环后的t //比较新增节点的key和当前节点key的大小 cmp = cpr.compare(key, t.key); //cmp返回值小于0,表示新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点 if (cmp \u0026lt; 0) t = t.left; //cmp返回值大于0,表示新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点 else if (cmp \u0026gt; 0) t = t.right; //cmp返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值 else return t.setValue(value); } while (t != null); } //如果cpr为空,则采用默认的排序算法进行创建TreeMap集合 else { if (key == null) //key值为空抛出异常 throw new NullPointerException(); /* 下面处理过程和上面一样 */ Comparable\u0026lt;? super K\u0026gt; k = (Comparable\u0026lt;? super K\u0026gt;) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp \u0026lt; 0) t = t.left; else if (cmp \u0026gt; 0) t = t.right; else return t.setValue(value); } while (t != null); } //将新增节点当做parent的子节点 Entry\u0026lt;K,V\u0026gt; e = new Entry\u0026lt;\u0026gt;(key, value, parent); //如果新增节点的key小于parent的key,则当做左子节点 if (cmp \u0026lt; 0) parent.left = e; //如果新增节点的key大于parent的key,则当做右子节点 else parent.right = e; /* * 上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置 * 下面fixAfterInsertion()方法就是对这棵树进行调整、平衡,具体过程参考上面的五种情况 */ fixAfterInsertion(e); //TreeMap元素数量 + 1 size++; //TreeMap容器修改次数 + 1 modCount++; return null; } /** * 上面代码中do{}代码块是实现排序二叉树的核心算法,通过该算法我们可以确认新增节点在该树的正确位置。 * 找到正确位置后将插入即可,这样做了其实还没有完成,因为我知道TreeMap的底层实现是红黑树,红黑树是一棵平衡排序二叉树, * 普通的排序二叉树可能会出现失衡的情况,所以下一步就是要进行调整。fixAfterInsertion(e); 调整的过程务必会涉及到红黑树的左 * 旋、右旋、着色三个基本操作 * 新增节点后的修复操作 * x 表示新增节点 */ private void fixAfterInsertion(Entry\u0026lt;K,V\u0026gt; x) { x.color = RED; //新增节点的颜色为红色 //循环 直到 x不是根节点,且x的父节点不为红色 while (x != null \u0026amp;\u0026amp; x != root \u0026amp;\u0026amp; x.parent.color == RED) { //如果X的父节点(P)是其父节点的父节点(G)的左节点 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //获取X的叔节点(U) Entry\u0026lt;K,V\u0026gt; y = rightOf(parentOf(parentOf(x))); //如果X的叔节点(U) 为红色(情况三) if (colorOf(y) == RED) { //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的叔节点(U)设置为黑色 setColor(y, BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } //如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五) else { //如果X节点为其父节点(P)的右子树,则进行左旋转(情况四) if (x == rightOf(parentOf(x))) { //将X的父节点作为X x = parentOf(x); //右旋转 rotateLeft(x); } //(情况五) //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); //以X的父节点的父节点(G)为中心右旋转 rotateRight(parentOf(parentOf(x))); } } //如果X的父节点(P)是其父节点的父节点(G)的右节点 else { //获取X的叔节点(U) Entry\u0026lt;K,V\u0026gt; y = leftOf(parentOf(parentOf(x))); //如果X的叔节点(U) 为红色(情况三) if (colorOf(y) == RED) { //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的叔节点(U)设置为黑色 setColor(y, BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } //如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五) else { //如果X节点为其父节点(P)的右子树,则进行左旋转(情况四) if (x == leftOf(parentOf(x))) { //将X的父节点作为X x = parentOf(x); //右旋转 rotateRight(x); } //(情况五) //将X的父节点(P)设置为黑色 setColor(parentOf(x), BLACK); //将X的父节点的父节点(G)设置红色 setColor(parentOf(parentOf(x)), RED); //以X的父节点的父节点(G)为中心右旋转 rotateLeft(parentOf(parentOf(x))); } } } //将根节点G强制设置为黑色 root.color = BLACK; } ","id":22,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Treemap","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/treemap/","year":"2019"},{"content":"如同HashSet基于HashMap一样,\u001bTreeSet基于TreeMap,TreeMap是一棵有序的红黑树,那么\u0008TreeSet也如此.提供有序的set集合,不允许重复插入 继承关系如下:\ngraph BT A(TreeSet) -.-\u0026gt; B(NavigableSet) A --\u0026gt; C(AbstractSet) C--\u0026gt;D(AbstractCollection) D-.-\u0026gt;E(Collection) 参数 private transient NavigableMap\u0026lt;E,Object\u0026gt; m; //PRESENT会被当做Map的value与key构建成键值对 private static final Object PRESENT = new Object(); 构造方法 //创建TreeSet的基础构成 map TreeSet(NavigableMap\u0026lt;E,Object\u0026gt; m) { this.m = m; } //按照自然排序构建 public TreeSet() { this(new TreeMap\u0026lt;E,Object\u0026gt;()); } //按照自定义排序构建 public TreeSet(Comparator\u0026lt;? super E\u0026gt; comparator) { this(new TreeMap\u0026lt;\u0026gt;(comparator)); } //按照自然排序构建 并\u0008添加入参集合的元素 public TreeSet(Collection\u0026lt;? extends E\u0026gt; c) { this(); addAll(c); } //根据已有的TreeSet构建\u0008一个新的TreeSet public TreeSet(SortedSet\u0026lt;E\u0026gt; s) { this(s.comparator()); addAll(s); } 主要方法 /** * 将集合中所有的\u0008元素添加到TreeMap中 * 如果集合为空,\u0008或者任一元素为null并且使用的是自然排序,或者 * comparator不允许为空元素则会抛NPE */ public boolean addAll(Collection\u0026lt;? extends E\u0026gt; c) { // Use linear-time version if applicable if (m.size()==0 \u0026amp;\u0026amp; c.size() \u0026gt; 0 \u0026amp;\u0026amp; c instanceof SortedSet \u0026amp;\u0026amp; m instanceof TreeMap) { SortedSet\u0026lt;? extends E\u0026gt; set = (SortedSet\u0026lt;? extends E\u0026gt;) c; TreeMap\u0026lt;E,Object\u0026gt; map = (TreeMap\u0026lt;E, Object\u0026gt;) m; Comparator\u0026lt;?\u0026gt; cc = set.comparator(); Comparator\u0026lt;? super E\u0026gt; mc = map.comparator(); if (cc==mc || (cc != null \u0026amp;\u0026amp; cc.equals(mc))) { map.addAllForTreeSet(set, PRESENT); return true; } } return super.addAll(c); } /* add操作 会去重 \u0008put\u0008返回值为null时说明成功 */ public boolean add(E e) { return m.put(e, PRESENT)==null; } /* 获取并移除第一个元素 如果set为空 则返回null */ public E pollFirst() { Map.Entry\u0026lt;E,?\u0026gt; e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); } /* 获取并移除最后一个元素 如果set为空 则返回null */ public E pollLast() { Map.Entry\u0026lt;E,?\u0026gt; e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); } /** * 返回此 set 的部分视图,其元素大于(或等于,如果 inclusive 为 true)fromElement。 */ public NavigableSet\u0026lt;E\u0026gt; tailSet(E fromElement, boolean inclusive) { return new TreeSet\u0026lt;\u0026gt;(m.tailMap(fromElement, inclusive)); } /** * 返回此 set 的部分视图,其元素大于等于 fromElement。 */ public SortedSet\u0026lt;E\u0026gt; tailSet(E fromElement) { return tailSet(fromElement, true); } /** * 返回此 set 的部分视图,其元素范围从 fromElement 到 toElement。 */ public NavigableSet\u0026lt;E\u0026gt; subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) { return new TreeSet\u0026lt;\u0026gt;(m.subMap(fromElement, fromInclusive, toElement, toInclusive)); } /** * 返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。 */ public SortedSet\u0026lt;E\u0026gt; subSet(E fromElement, E toElement) { return subSet(fromElement, true, toElement, false); } ","id":23,"section":"posts","summary":"","tags":["collections"],"title":"Collections-Treeset","uri":"https://xiaohei.im/hugo-theme-pure/2019/08/treeset/","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":"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/sitemap.xml b/sitemap.xml
index 175abb9..ec3ad99 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -17,14 +17,14 @@
</url>
<url>
- <loc>https://xiaohei.im/hugo-theme-pure/tags/redis/</loc>
+ <loc>https://xiaohei.im/hugo-theme-pure/categories/redis/</loc>
<lastmod>2019-11-06T19:08:56+08:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
- <loc>https://xiaohei.im/hugo-theme-pure/categories/redis/</loc>
+ <loc>https://xiaohei.im/hugo-theme-pure/tags/redis/</loc>
<lastmod>2019-11-06T19:08:56+08:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
diff --git a/tags/collections/index.html b/tags/collections/index.html
index 52b96de..e610b3c 100644
--- a/tags/collections/index.html
+++ b/tags/collections/index.html
@@ -456,7 +456,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/hugo/index.html b/tags/hugo/index.html
index f5667aa..0b8b601 100644
--- a/tags/hugo/index.html
+++ b/tags/hugo/index.html
@@ -320,7 +320,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/hystrix/index.html b/tags/hystrix/index.html
index cb88ecd..c00e54e 100644
--- a/tags/hystrix/index.html
+++ b/tags/hystrix/index.html
@@ -321,7 +321,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/index.html b/tags/index.html
index de66ceb..4726e48 100644
--- a/tags/index.html
+++ b/tags/index.html
@@ -884,7 +884,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/leetcode/index.html b/tags/leetcode/index.html
index 509da9e..604d8ca 100644
--- a/tags/leetcode/index.html
+++ b/tags/leetcode/index.html
@@ -375,7 +375,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/rabbitmq/index.html b/tags/rabbitmq/index.html
index 19b81b9..ccf2027 100644
--- a/tags/rabbitmq/index.html
+++ b/tags/rabbitmq/index.html
@@ -398,7 +398,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/redis/index.html b/tags/redis/index.html
index 9a88290..cf18481 100644
--- a/tags/redis/index.html
+++ b/tags/redis/index.html
@@ -426,7 +426,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/rust/index.html b/tags/rust/index.html
index b07faf6..4913bab 100644
--- a/tags/rust/index.html
+++ b/tags/rust/index.html
@@ -401,7 +401,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/rxjava/index.html b/tags/rxjava/index.html
index e1292cf..346475b 100644
--- a/tags/rxjava/index.html
+++ b/tags/rxjava/index.html
@@ -348,7 +348,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/分布式锁/index.html b/tags/分布式锁/index.html
index 4848036..e52da04 100644
--- a/tags/分布式锁/index.html
+++ b/tags/分布式锁/index.html
@@ -321,7 +321,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/响应式编程/index.html b/tags/响应式编程/index.html
index 5e535f3..f91a149 100644
--- a/tags/响应式编程/index.html
+++ b/tags/响应式编程/index.html
@@ -321,7 +321,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);
diff --git a/tags/数据结构/index.html b/tags/数据结构/index.html
index e2e4427..179222b 100644
--- a/tags/数据结构/index.html
+++ b/tags/数据结构/index.html
@@ -321,7 +321,7 @@ hljs.initHighlightingOnLoad();
UNTITLED: '(未命名)',
},
ROOT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure',
- CONTENT_URL: 'https:\/\/xiaohei.im\/searchindex.json ',
+ CONTENT_URL: 'https:\/\/xiaohei.im\/hugo-theme-pure\/searchindex.json ',
};
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
})(window);