diff --git a/2020/06/26/聊一下-RocketMQ-的-Consumer/index.html b/2020/06/26/聊一下-RocketMQ-的-Consumer/index.html index 34837fecac..1a4dc82618 100644 --- a/2020/06/26/聊一下-RocketMQ-的-Consumer/index.html +++ b/2020/06/26/聊一下-RocketMQ-的-Consumer/index.html @@ -1,4 +1,4 @@ -聊一下 RocketMQ 的 DefaultMQPushConsumer 源码 | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

聊一下 RocketMQ 的 DefaultMQPushConsumer 源码

首先看下官方的小 demo

public static void main(String[] args) throws InterruptedException, MQClientException {
+聊一下 RocketMQ 的 DefaultMQPushConsumer 源码 | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

聊一下 RocketMQ 的 DefaultMQPushConsumer 源码

首先看下官方的小 demo

public static void main(String[] args) throws InterruptedException, MQClientException {
 
         /*
          * Instantiate with specified consumer group name.
@@ -737,4 +737,4 @@
                 throw new RemotingTimeoutException(info);
             }
         }
-    }
\ No newline at end of file + }
\ No newline at end of file diff --git a/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/index.html b/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/index.html index ff7390496f..f29b663824 100644 --- a/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/index.html +++ b/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/index.html @@ -1,4 +1,4 @@ -聊一下 RocketMQ 的 NameServer 源码 | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

聊一下 RocketMQ 的 NameServer 源码

前面介绍了,nameserver相当于dubbo的注册中心,用与管理broker,broker会在启动的时候注册到nameserver,并且会发送心跳给namaserver,nameserver负责保存活跃的broker,包括master和slave,同时保存topic和topic下的队列,以及filter列表,然后为producer和consumer的请求提供服务。

启动过程

public static void main(String[] args) {
+聊一下 RocketMQ 的 NameServer 源码 | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

聊一下 RocketMQ 的 NameServer 源码

前面介绍了,nameserver相当于dubbo的注册中心,用与管理broker,broker会在启动的时候注册到nameserver,并且会发送心跳给namaserver,nameserver负责保存活跃的broker,包括master和slave,同时保存topic和topic下的队列,以及filter列表,然后为producer和consumer的请求提供服务。

启动过程

public static void main(String[] args) {
         main0(args);
     }
 
@@ -563,4 +563,4 @@
         response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
             + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
         return response;
-    }

首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteDataorg.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。

简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。

\ No newline at end of file + }

首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteDataorg.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。

简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。

\ No newline at end of file diff --git a/baidusitemap.xml b/baidusitemap.xml index d19de71ccd..f5e1549d90 100644 --- a/baidusitemap.xml +++ b/baidusitemap.xml @@ -88,10 +88,6 @@ https://nicksxs.me/2021/02/21/AQS-%E4%B9%8B-Condition-%E6%B5%85%E6%9E%90%E7%AC%94%E8%AE%B0/ 2022-06-11 - - https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ - 2022-06-11 - https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ 2022-06-11 @@ -124,6 +120,10 @@ https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-06-11 + + https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ + 2022-06-11 + https://nicksxs.me/2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/ 2022-06-11 @@ -161,11 +161,11 @@ 2022-06-11 - https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ 2022-06-11 - https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ 2022-06-11 @@ -173,15 +173,15 @@ 2022-06-11 - https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ + https://nicksxs.me/2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/ 2022-06-11 - https://nicksxs.me/2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/ + https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ 2022-06-11 - https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ + https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ 2022-06-11 @@ -197,11 +197,11 @@ 2022-06-11 - https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/ 2022-06-11 - https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/ + https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ 2022-06-11 @@ -209,15 +209,15 @@ 2022-06-11 - https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ + https://nicksxs.me/2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ 2022-06-11 - https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ 2022-06-11 - https://nicksxs.me/2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ 2022-06-11 @@ -605,15 +605,15 @@ 2020-01-12 - https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 - https://nicksxs.me/2015/03/13/Reverse-Integer/ + https://nicksxs.me/2015/03/11/Reverse-Bits/ 2020-01-12 - https://nicksxs.me/2015/03/11/Reverse-Bits/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 @@ -624,10 +624,6 @@ https://nicksxs.me/2016/09/29/binary-watch/ 2020-01-12 - - https://nicksxs.me/2019/12/10/Redis-Part-1/ - 2020-01-12 - https://nicksxs.me/2017/05/09/ambari-summary/ 2020-01-12 @@ -641,11 +637,11 @@ 2020-01-12 - https://nicksxs.me/2019/06/18/openresty/ + https://nicksxs.me/2016/11/10/php-abstract-class-and-interface/ 2020-01-12 - https://nicksxs.me/2016/11/10/php-abstract-class-and-interface/ + https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ 2020-01-12 @@ -653,23 +649,23 @@ 2020-01-12 - https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ 2020-01-12 - https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ + https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ 2020-01-12 - https://nicksxs.me/2019/09/23/AbstractQueuedSynchronizer/ + https://nicksxs.me/2019/06/18/openresty/ 2020-01-12 - https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ + https://nicksxs.me/2019/09/23/AbstractQueuedSynchronizer/ 2020-01-12 - https://nicksxs.me/2015/04/15/Leetcode-No-3/ + https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ 2020-01-12 @@ -688,6 +684,10 @@ https://nicksxs.me/2014/12/23/my-new-post/ 2020-01-12 + + https://nicksxs.me/2015/04/15/Leetcode-No-3/ + 2020-01-12 + https://nicksxs.me/2015/01/16/pcre-intro-and-a-simple-package/ 2020-01-12 @@ -697,7 +697,7 @@ 2020-01-12 - https://nicksxs.me/2016/07/13/swoole-websocket-test/ + https://nicksxs.me/2016/10/12/summary-ranges-228/ 2020-01-12 @@ -705,7 +705,7 @@ 2020-01-12 - https://nicksxs.me/2016/10/12/summary-ranges-228/ + https://nicksxs.me/2016/07/13/swoole-websocket-test/ 2020-01-12 diff --git a/categories/Java/GC/index.html b/categories/Java/GC/index.html index ece85a6a44..5e2584830a 100644 --- a/categories/Java/GC/index.html +++ b/categories/Java/GC/index.html @@ -1 +1 @@ -分类: gc | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
,"url":"https://nicksxs.me/categories/Java/GC/"} \ No newline at end of file +分类: gc | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/categories/Java/index.html b/categories/Java/index.html index 3caa38ef95..3737fc0bdb 100644 --- a/categories/Java/index.html +++ b/categories/Java/index.html @@ -1 +1 @@ -分类: java | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
/json">{"enable":true,"home":false,"archive":false,"delay":true,"timeout":3000,"priority":true,"url":"https://nicksxs.me/categories/Java/"} \ No newline at end of file +分类: java | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/categories/Redis/index.html b/categories/Redis/index.html index 44e13d510e..b45b3a405f 100644 --- a/categories/Redis/index.html +++ b/categories/Redis/index.html @@ -1 +1 @@ -分类: redis | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
xo-generator-searchdb/1.4.0/search.js" integrity="sha256-vXZMYLEqsROAXkEw93GGIvaB2ab+QW6w3+1ahD9nXXA=" crossorigin="anonymous"> \ No newline at end of file +分类: redis | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/categories/leetcode/java/linked-list/index.html b/categories/leetcode/java/linked-list/index.html index 2f717c7bee..826142452a 100644 --- a/categories/leetcode/java/linked-list/index.html +++ b/categories/leetcode/java/linked-list/index.html @@ -1 +1 @@ -分类: linked list | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

linked list 分类

2020
ript> \ No newline at end of file +分类: linked list | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

linked list 分类

2020
\ No newline at end of file diff --git a/categories/linked-list/index.html b/categories/linked-list/index.html index c9c9bb2260..2dd0145c29 100644 --- a/categories/linked-list/index.html +++ b/categories/linked-list/index.html @@ -1 +1 @@ -分类: linked list | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

linked list 分类

2020
ytics.js"> \ No newline at end of file +分类: linked list | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

linked list 分类

2020
\ No newline at end of file diff --git a/categories/php/index.html b/categories/php/index.html index 8b7b3656fd..039d4a3b5e 100644 --- a/categories/php/index.html +++ b/categories/php/index.html @@ -1 +1 @@ -分类: PHP | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
w","server_url":"https://leancloud.cn","security":true} \ No newline at end of file +分类: PHP | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/css/main.css b/css/main.css index 9c016bf4e9..c8154157aa 100644 --- a/css/main.css +++ b/css/main.css @@ -1 +1 @@ -:root{--body-bg-color:#f5f7f9;--content-bg-color:#fff;--card-bg-color:#f5f5f5;--text-color:#555;--blockquote-color:#666;--link-color:#555;--link-hover-color:#222;--brand-color:#fff;--brand-hover-color:#fff;--table-row-odd-bg-color:#f9f9f9;--table-row-hover-bg-color:#f5f5f5;--menu-item-bg-color:#f5f5f5;--theme-color:#222;--btn-default-bg:#fff;--btn-default-color:#555;--btn-default-border-color:#555;--btn-default-hover-bg:#222;--btn-default-hover-color:#fff;--btn-default-hover-border-color:#222;--highlight-background:#f3f3f3;--highlight-foreground:#444;--highlight-gutter-background:#e1e1e1;--highlight-gutter-foreground:#555;color-scheme:light}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background:0 0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}::selection{background:#262a30;color:#eee}body,html{height:100%}body{background:var(--body-bg-color);box-sizing:border-box;color:var(--text-color);font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.2em;line-height:2;min-height:100%;position:relative;transition:padding .2s ease-in-out}h1,h2,h3,h4,h5,h6{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-weight:700;line-height:1.5;margin:30px 0 15px}h1{font-size:1.5em}h2{font-size:1.375em}h3{font-size:1.25em}h4{font-size:1.125em}h5{font-size:1em}h6{font-size:.875em}p{margin:0 0 20px}a{border-bottom:1px solid #999;color:var(--link-color);cursor:pointer;outline:0;text-decoration:none;overflow-wrap:break-word}a:hover{border-bottom-color:var(--link-hover-color);color:var(--link-hover-color)}embed,iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{background-image:repeating-linear-gradient(-45deg,#ddd,#ddd 4px,transparent 4px,transparent 8px);border:0;height:3px;margin:40px 0}blockquote{border-left:4px solid #ddd;color:var(--blockquote-color);margin:0;padding:0 15px}blockquote cite::before{content:'-';padding:0 5px}dt{font-weight:700}dd{margin:0;padding:0}.table-container{overflow:auto}table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 20px;width:100%}tbody tr:nth-of-type(odd){background:var(--table-row-odd-bg-color)}tbody tr:hover{background:var(--table-row-hover-bg-color)}caption,td,th{padding:8px}td,th{border:1px solid #ddd;border-bottom:3px solid #ddd}th{font-weight:700;padding-bottom:10px}td{border-bottom-width:1px}.btn{background:var(--btn-default-bg);border:2px solid var(--btn-default-border-color);border-radius:2px;color:var(--btn-default-color);display:inline-block;font-size:.875em;line-height:2;padding:0 20px;transition:background-color .2s ease-in-out}.btn:hover{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.btn+.btn{margin:0 0 8px 8px}.btn .fa-fw{text-align:left;width:1.285714285714286em}.toggle{line-height:0}.toggle .toggle-line{background:#fff;display:block;height:2px;left:0;position:relative;top:0;transition:all .4s;width:100%}.toggle .toggle-line:not(:first-child){margin-top:3px}.toggle.toggle-arrow .toggle-line:first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}.toggle.toggle-arrow .toggle-line:last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}.toggle.toggle-close .toggle-line:nth-child(2){opacity:0}.toggle.toggle-close .toggle-line:first-child{top:5px;transform:rotate(45deg)}.toggle.toggle-close .toggle-line:last-child{top:-5px;transform:rotate(-45deg)}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.highlight:hover .copy-btn,pre:hover .copy-btn{opacity:1}figure.highlight .table-container{position:relative}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;color:var(--highlight-foreground);font-size:14px;right:0;top:2px}figure.highlight{border-radius:5px;box-shadow:0 10px 30px 0 rgba(0,0,0,.4);padding-top:30px}figure.highlight .table-container{border-radius:0 0 5px 5px}figure.highlight::before{background:#fc625d;box-shadow:20px 0 #fdbc40,40px 0 #35cd4b;left:12px;margin-top:-20px;position:absolute;border-radius:50%;content:' ';height:12px;width:12px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption,pre figcaption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a,pre figcaption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover,pre figcaption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{position:relative}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption,pre figcaption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre{overflow:auto;padding:10px;position:relative}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 5 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 5 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:1fr 1fr;margin-bottom:20px;padding:1rem}@media (max-width:767px){.post-body .link-grid{grid-template-columns:1fr}}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:next-shake .5s;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@-moz-keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}@-webkit-keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}@-o-keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note blockquote:first-child,.post-body .note img:first-child,.post-body .note ol:first-child,.post-body .note p:first-child,.post-body .note pre:first-child,.post-body .note table:first-child,.post-body .note ul:first-child{margin-top:0}.post-body .note blockquote:last-child,.post-body .note img:last-child,.post-body .note ol:last-child,.post-body .note p:last-child,.post-body .note pre:last-child,.post-body .note table:last-child,.post-body .note ul:last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:-webkit-sticky;position:sticky;top:0;z-index:5}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0 0 0 0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent}}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:all .2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i,.tabs-comment ul.nav-tabs li.tab a i{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom-color:transparent;border-left-color:#ddd;border-right-color:#ddd;border-top-color:#fc6423}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom-color:#ddd;border-left-color:#fc6423;border-right-color:#ddd;border-top-color:#ddd}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0 0 0 0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}@media (max-width:767px){.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}}.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}header.header{background:0 0}.header-inner{margin:0 auto;width:calc(100% - 20px)}@media (min-width:1200px){.header-inner{width:1360px}}@media (min-width:1600px){.header-inner{width:79%}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .site-brand-container .toggle,.use-motion header.header{opacity:0}.site-meta{flex-grow:1;text-align:center}@media (max-width:767px){.site-meta{text-align:center}}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0 40px}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height)}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item .fa,.menu-item .fab,.menu-item .far,.menu-item .fas{margin-right:8px}.menu-item .badge{display:inline-block;font-weight:700;line-height:1;margin-left:.35em;margin-top:.35em;text-align:center;white-space:nowrap}@media (max-width:767px){.menu-item .badge{float:right;margin-left:0}}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:octocat-wave 560ms ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}@-moz-keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@-webkit-keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@-o-keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}.sidebar-inner{color:#999;max-height:calc(100vh - 24px);padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.site-overview-item:not(:first-child){margin-top:10px}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.links-of-author a{font-size:.8125em}.links-of-author .fa,.links-of-author .fab,.links-of-author .far,.links-of-author .fas{margin-right:2px}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:1px solid #fc6423;border-radius:4px}.sidebar .sidebar-button button:hover{background:#fc6423;color:#fff}.sidebar .sidebar-button button .fa,.sidebar .sidebar-button button .fab,.sidebar .sidebar-button button .far,.sidebar .sidebar-button button .fas{margin-right:5px}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600;margin-top:0}.links-of-blogroll-list{list-style:none;margin:0;padding:0}.sidebar-dimmer{display:none}@media (max-width:991px){.sidebar-dimmer{background:#000;display:block;height:100%;left:0;opacity:0;position:fixed;top:0;transition:visibility .4s,opacity .4s;visibility:hidden;width:100%;z-index:10}.sidebar-active .sidebar-dimmer{opacity:.7;visibility:visible}}.sidebar-nav{display:none;margin:0;padding-bottom:20px;padding-left:0}.sidebar-nav-active .sidebar-nav{display:block}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;font-size:.875em}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{flex:1;overflow-x:hidden;overflow-y:auto}.sidebar-panel{display:none}.sidebar-overview-active .site-overview-wrap{display:flex;flex-direction:column;justify-content:center}.sidebar-toc-active .post-toc-wrap{display:block}.sidebar-toggle{bottom:45px;height:12px;padding:6px 5px;width:14px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}@media (max-width:991px){.sidebar-toggle{right:20px}}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover .toggle-line:first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover .toggle-line:last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle .toggle-line:nth-child(2){opacity:0}.sidebar-active .sidebar-toggle .toggle-line:first-child{top:5px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle .toggle-line:last-child{top:-5px;transform:rotate(-45deg)}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 5px 10px;text-align:left}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:all .2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{display:none}.post-toc .nav .active>.nav-child{display:block}.post-toc .nav .active-current>.nav-child{display:block}.post-toc .nav .active-current>.nav-child>.nav-item{display:block}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a{color:#fc6423}.post-toc .nav .active-current>a:hover{color:#fc6423}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item{padding:0 15px}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.footer{color:#999;font-size:.875em;padding:20px 0}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (min-width:1200px){.footer-inner{width:1360px}}@media (min-width:1600px){.footer-inner{width:79%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}.beian img{display:inline-block;margin:0 3px;vertical-align:middle}.busuanzi-count #busuanzi_container_site_uv{display:none}.busuanzi-count #busuanzi_container_site_pv{display:none}@-moz-keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@-webkit-keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@-o-keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@media (max-width:567px){.main-inner{padding:initial!important}.posts-expand .post-header{margin-bottom:10px!important}.post-block{margin-top:initial!important;padding:8px 18px 8px!important}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{margin:20px 0 8px}.post-body .note h1,.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6,.post-body .tabs .tab-content .tab-pane h1,.post-body .tabs .tab-content .tab-pane h2,.post-body .tabs .tab-content .tab-pane h3,.post-body .tabs .tab-content .tab-pane h4,.post-body .tabs .tab-content .tab-pane h5,.post-body .tabs .tab-content .tab-pane h6{margin:0 5px}.post-body>p{margin:0 0 10px}.post-body .note>p,.post-body .tabs .tab-content .tab-pane>p{padding:0 5px}.post-body img,.post-body video{margin-bottom:10px!important}.post-body .note{margin-bottom:10px!important;padding:10px!important}.post-body .tabs .tab-content .tab-pane{padding:10px 10px 0!important}.post-eof{margin:40px auto 20px!important}.pagination{margin-top:40px}}.noscript-warning{background-color:#f55;color:#fff;font-family:sans-serif;font-size:1rem;font-weight:700;left:0;position:fixed;text-align:center;top:0;width:100%;z-index:50}.back-to-top{font-size:12px;bottom:-100px;box-sizing:border-box;color:#fff;padding:0 6px;transition:bottom .2s ease-in-out;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px;width:24px}.back-to-top span{display:none}@media (max-width:991px){.back-to-top{right:20px}}.back-to-top:hover{opacity:.8}@media (max-width:991px){.back-to-top{opacity:.8}}.back-to-top:hover{color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .comments,.use-motion .pagination,.use-motion .post-block{visibility:hidden}.use-motion .post-header{visibility:hidden}.use-motion .post-body{visibility:hidden}.use-motion .collection-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content .post-title .fa-external-link-alt{font-size:.875em;margin-left:5px}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}@media (max-width:991px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 5 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body .fancybox+figcaption,.post-body .image-caption,.post-body img+figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa-external-link-alt{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}#busuanzi_container_page_pv{display:none}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;padding:.5em 1em;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.post-widgets{border-top:1px solid #eee;margin-top:15px;text-align:center}.wpac-rating-container{height:20px;line-height:20px;margin-top:10px;padding-top:6px;text-align:center}.social-like{display:flex;font-size:.875em;justify-content:center;text-align:center}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;transition:all .2s ease-in-out;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@-moz-keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}@-webkit-keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}@-o-keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{color:#bbb}.category-all-page .category-list-count::before{content:' ('}.category-all-page .category-list-count::after{content:') '}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:dot-flash 1s alternate infinite ease-in-out;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 5 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@-moz-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@-webkit-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@-o-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;padding:5px 0}.search-popup .search-input-container{flex-grow:1;padding:2px}.search-popup .no-result{display:flex}.search-popup .search-result-list{width:100%}.search-popup .search-result-icon{color:#ccc;margin:auto}mark.search-keyword{background:0 0;border-bottom:1px dashed #ff2a2a;color:#ff2a2a;font-weight:700}.popular-posts-header{border-bottom:1px solid #eee;font-size:1.125em;margin-bottom:10px;margin-top:60px}.popular-posts{padding:0}.popular-posts .popular-posts-item{margin-left:2em}.popular-posts .popular-posts-item .popular-posts-title{font-size:.875em;font-weight:400;line-height:2.4;margin:0}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}.header-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;width:240px}@media (max-width:991px){.header-inner{border-radius:initial;width:auto}}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (min-width:1200px){.main{width:1360px}}@media (min-width:1600px){.main{width:79%}}@media (max-width:991px){.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}}.footer-inner{padding-left:252px}@media (max-width:991px){.footer-inner{padding-left:0;padding-right:0;width:auto}}.site-brand-container{background:var(--theme-color)}@media (max-width:991px){.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}}.site-meta{padding:20px 0}.brand{padding:0}.site-subtitle{margin:10px 10px 0}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height)}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;text-align:left;transition-property:background-color}@media (max-width:991px){.menu .menu-item.menu-item-search{display:none}}.menu .menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);float:right;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}.sidebar{margin-top:12px;position:-webkit-sticky;position:sticky;top:12px;width:240px}@media (max-width:991px){.sidebar{display:none}}.sidebar-toggle{display:none}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color)}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;padding:0 5px;text-overflow:ellipsis;white-space:nowrap}.links-of-author-item a{border-bottom:0;border-radius:4px;display:block}.links-of-author-item a:hover{background:var(--body-bg-color)}.main-inner{background:var(--content-bg-color);box-shadow:initial;padding:40px}@media (max-width:991px){.main-inner{padding:20px}}.sub-menu{border-bottom:1px solid #ddd}.post-block:first-of-type{padding-top:40px}@media (max-width:767px){.pagination{margin-bottom:10px}} \ No newline at end of file +:root{--body-bg-color:#f5f7f9;--content-bg-color:#fff;--card-bg-color:#f5f5f5;--text-color:#555;--blockquote-color:#666;--link-color:#555;--link-hover-color:#222;--brand-color:#fff;--brand-hover-color:#fff;--table-row-odd-bg-color:#f9f9f9;--table-row-hover-bg-color:#f5f5f5;--menu-item-bg-color:#f5f5f5;--theme-color:#222;--btn-default-bg:#fff;--btn-default-color:#555;--btn-default-border-color:#555;--btn-default-hover-bg:#222;--btn-default-hover-color:#fff;--btn-default-hover-border-color:#222;--highlight-background:#f3f3f3;--highlight-foreground:#444;--highlight-gutter-background:#e1e1e1;--highlight-gutter-foreground:#555;color-scheme:light}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background:0 0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}::selection{background:#262a30;color:#eee}body,html{height:100%}body{background:var(--body-bg-color);box-sizing:border-box;color:var(--text-color);font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.2em;line-height:2;min-height:100%;position:relative;transition:padding .2s ease-in-out}h1,h2,h3,h4,h5,h6{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-weight:700;line-height:1.5;margin:30px 0 15px}h1{font-size:1.5em}h2{font-size:1.375em}h3{font-size:1.25em}h4{font-size:1.125em}h5{font-size:1em}h6{font-size:.875em}p{margin:0 0 20px}a{border-bottom:1px solid #999;color:var(--link-color);cursor:pointer;outline:0;text-decoration:none;overflow-wrap:break-word}a:hover{border-bottom-color:var(--link-hover-color);color:var(--link-hover-color)}embed,iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{background-image:repeating-linear-gradient(-45deg,#ddd,#ddd 4px,transparent 4px,transparent 8px);border:0;height:3px;margin:40px 0}blockquote{border-left:4px solid #ddd;color:var(--blockquote-color);margin:0;padding:0 15px}blockquote cite::before{content:'-';padding:0 5px}dt{font-weight:700}dd{margin:0;padding:0}.table-container{overflow:auto}table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 20px;width:100%}tbody tr:nth-of-type(odd){background:var(--table-row-odd-bg-color)}tbody tr:hover{background:var(--table-row-hover-bg-color)}caption,td,th{padding:8px}td,th{border:1px solid #ddd;border-bottom:3px solid #ddd}th{font-weight:700;padding-bottom:10px}td{border-bottom-width:1px}.btn{background:var(--btn-default-bg);border:2px solid var(--btn-default-border-color);border-radius:2px;color:var(--btn-default-color);display:inline-block;font-size:.875em;line-height:2;padding:0 20px;transition:background-color .2s ease-in-out}.btn:hover{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.btn+.btn{margin:0 0 8px 8px}.btn .fa-fw{text-align:left;width:1.285714285714286em}.toggle{line-height:0}.toggle .toggle-line{background:#fff;display:block;height:2px;left:0;position:relative;top:0;transition:all .4s;width:100%}.toggle .toggle-line:not(:first-child){margin-top:3px}.toggle.toggle-arrow .toggle-line:first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}.toggle.toggle-arrow .toggle-line:last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}.toggle.toggle-close .toggle-line:nth-child(2){opacity:0}.toggle.toggle-close .toggle-line:first-child{top:5px;transform:rotate(45deg)}.toggle.toggle-close .toggle-line:last-child{top:-5px;transform:rotate(-45deg)}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.highlight:hover .copy-btn,pre:hover .copy-btn{opacity:1}figure.highlight .table-container{position:relative}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;color:var(--highlight-foreground);font-size:14px;right:0;top:2px}figure.highlight{border-radius:5px;box-shadow:0 10px 30px 0 rgba(0,0,0,.4);padding-top:30px}figure.highlight .table-container{border-radius:0 0 5px 5px}figure.highlight::before{background:#fc625d;box-shadow:20px 0 #fdbc40,40px 0 #35cd4b;left:12px;margin-top:-20px;position:absolute;border-radius:50%;content:' ';height:12px;width:12px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption,pre figcaption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a,pre figcaption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover,pre figcaption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{position:relative}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption,pre figcaption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre{overflow:auto;padding:10px;position:relative}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 5 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 5 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:1fr 1fr;margin-bottom:20px;padding:1rem}@media (max-width:767px){.post-body .link-grid{grid-template-columns:1fr}}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:next-shake .5s;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@-moz-keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}@-webkit-keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}@-o-keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note blockquote:first-child,.post-body .note img:first-child,.post-body .note ol:first-child,.post-body .note p:first-child,.post-body .note pre:first-child,.post-body .note table:first-child,.post-body .note ul:first-child{margin-top:0}.post-body .note blockquote:last-child,.post-body .note img:last-child,.post-body .note ol:last-child,.post-body .note p:last-child,.post-body .note pre:last-child,.post-body .note table:last-child,.post-body .note ul:last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:-webkit-sticky;position:sticky;top:0;z-index:5}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0 0 0 0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent}}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:all .2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i,.tabs-comment ul.nav-tabs li.tab a i{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom-color:transparent;border-left-color:#ddd;border-right-color:#ddd;border-top-color:#fc6423}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom-color:#ddd;border-left-color:#fc6423;border-right-color:#ddd;border-top-color:#ddd}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0 0 0 0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}@media (max-width:767px){.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}}.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}header.header{background:0 0}.header-inner{margin:0 auto;width:calc(100% - 20px)}@media (min-width:1200px){.header-inner{width:1160px}}@media (min-width:1600px){.header-inner{width:73%}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .site-brand-container .toggle,.use-motion header.header{opacity:0}.site-meta{flex-grow:1;text-align:center}@media (max-width:767px){.site-meta{text-align:center}}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0 40px}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height)}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item .fa,.menu-item .fab,.menu-item .far,.menu-item .fas{margin-right:8px}.menu-item .badge{display:inline-block;font-weight:700;line-height:1;margin-left:.35em;margin-top:.35em;text-align:center;white-space:nowrap}@media (max-width:767px){.menu-item .badge{float:right;margin-left:0}}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:octocat-wave 560ms ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}@-moz-keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@-webkit-keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@-o-keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}.sidebar-inner{color:#999;max-height:calc(100vh - 24px);padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.site-overview-item:not(:first-child){margin-top:10px}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.links-of-author a{font-size:.8125em}.links-of-author .fa,.links-of-author .fab,.links-of-author .far,.links-of-author .fas{margin-right:2px}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:1px solid #fc6423;border-radius:4px}.sidebar .sidebar-button button:hover{background:#fc6423;color:#fff}.sidebar .sidebar-button button .fa,.sidebar .sidebar-button button .fab,.sidebar .sidebar-button button .far,.sidebar .sidebar-button button .fas{margin-right:5px}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600;margin-top:0}.links-of-blogroll-list{list-style:none;margin:0;padding:0}.sidebar-dimmer{display:none}@media (max-width:991px){.sidebar-dimmer{background:#000;display:block;height:100%;left:0;opacity:0;position:fixed;top:0;transition:visibility .4s,opacity .4s;visibility:hidden;width:100%;z-index:10}.sidebar-active .sidebar-dimmer{opacity:.7;visibility:visible}}.sidebar-nav{display:none;margin:0;padding-bottom:20px;padding-left:0}.sidebar-nav-active .sidebar-nav{display:block}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;font-size:.875em}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{flex:1;overflow-x:hidden;overflow-y:auto}.sidebar-panel{display:none}.sidebar-overview-active .site-overview-wrap{display:flex;flex-direction:column;justify-content:center}.sidebar-toc-active .post-toc-wrap{display:block}.sidebar-toggle{bottom:45px;height:12px;padding:6px 5px;width:14px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}@media (max-width:991px){.sidebar-toggle{right:20px}}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover .toggle-line:first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover .toggle-line:last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle .toggle-line:nth-child(2){opacity:0}.sidebar-active .sidebar-toggle .toggle-line:first-child{top:5px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle .toggle-line:last-child{top:-5px;transform:rotate(-45deg)}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 5px 10px;text-align:left}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:all .2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{display:none}.post-toc .nav .active>.nav-child{display:block}.post-toc .nav .active-current>.nav-child{display:block}.post-toc .nav .active-current>.nav-child>.nav-item{display:block}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a{color:#fc6423}.post-toc .nav .active-current>a:hover{color:#fc6423}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item{padding:0 15px}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.footer{color:#999;font-size:.875em;padding:20px 0}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}.beian img{display:inline-block;margin:0 3px;vertical-align:middle}.busuanzi-count #busuanzi_container_site_uv{display:none}.busuanzi-count #busuanzi_container_site_pv{display:none}@-moz-keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@-webkit-keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@-o-keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@media (max-width:567px){.main-inner{padding:initial!important}.posts-expand .post-header{margin-bottom:10px!important}.post-block{margin-top:initial!important;padding:8px 18px 8px!important}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{margin:20px 0 8px}.post-body .note h1,.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6,.post-body .tabs .tab-content .tab-pane h1,.post-body .tabs .tab-content .tab-pane h2,.post-body .tabs .tab-content .tab-pane h3,.post-body .tabs .tab-content .tab-pane h4,.post-body .tabs .tab-content .tab-pane h5,.post-body .tabs .tab-content .tab-pane h6{margin:0 5px}.post-body>p{margin:0 0 10px}.post-body .note>p,.post-body .tabs .tab-content .tab-pane>p{padding:0 5px}.post-body img,.post-body video{margin-bottom:10px!important}.post-body .note{margin-bottom:10px!important;padding:10px!important}.post-body .tabs .tab-content .tab-pane{padding:10px 10px 0!important}.post-eof{margin:40px auto 20px!important}.pagination{margin-top:40px}}.noscript-warning{background-color:#f55;color:#fff;font-family:sans-serif;font-size:1rem;font-weight:700;left:0;position:fixed;text-align:center;top:0;width:100%;z-index:50}.back-to-top{font-size:12px;bottom:-100px;box-sizing:border-box;color:#fff;padding:0 6px;transition:bottom .2s ease-in-out;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px;width:24px}.back-to-top span{display:none}@media (max-width:991px){.back-to-top{right:20px}}.back-to-top:hover{opacity:.8}@media (max-width:991px){.back-to-top{opacity:.8}}.back-to-top:hover{color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .comments,.use-motion .pagination,.use-motion .post-block{visibility:hidden}.use-motion .post-header{visibility:hidden}.use-motion .post-body{visibility:hidden}.use-motion .collection-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content .post-title .fa-external-link-alt{font-size:.875em;margin-left:5px}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}@media (max-width:991px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 5 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body .fancybox+figcaption,.post-body .image-caption,.post-body img+figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa-external-link-alt{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}#busuanzi_container_page_pv{display:none}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;padding:.5em 1em;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.post-widgets{border-top:1px solid #eee;margin-top:15px;text-align:center}.wpac-rating-container{height:20px;line-height:20px;margin-top:10px;padding-top:6px;text-align:center}.social-like{display:flex;font-size:.875em;justify-content:center;text-align:center}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;transition:all .2s ease-in-out;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@-moz-keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}@-webkit-keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}@-o-keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{color:#bbb}.category-all-page .category-list-count::before{content:' ('}.category-all-page .category-list-count::after{content:') '}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:dot-flash 1s alternate infinite ease-in-out;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 5 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@-moz-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@-webkit-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@-o-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;padding:5px 0}.search-popup .search-input-container{flex-grow:1;padding:2px}.search-popup .no-result{display:flex}.search-popup .search-result-list{width:100%}.search-popup .search-result-icon{color:#ccc;margin:auto}mark.search-keyword{background:0 0;border-bottom:1px dashed #ff2a2a;color:#ff2a2a;font-weight:700}.popular-posts-header{border-bottom:1px solid #eee;font-size:1.125em;margin-bottom:10px;margin-top:60px}.popular-posts{padding:0}.popular-posts .popular-posts-item{margin-left:2em}.popular-posts .popular-posts-item .popular-posts-title{font-size:.875em;font-weight:400;line-height:2.4;margin:0}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}.header-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;width:240px}@media (max-width:991px){.header-inner{border-radius:initial;width:auto}}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (min-width:1200px){.main{width:1160px}}@media (min-width:1600px){.main{width:73%}}@media (max-width:991px){.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}}.footer-inner{padding-left:252px}@media (max-width:991px){.footer-inner{padding-left:0;padding-right:0;width:auto}}.site-brand-container{background:var(--theme-color)}@media (max-width:991px){.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}}.site-meta{padding:20px 0}.brand{padding:0}.site-subtitle{margin:10px 10px 0}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height)}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;text-align:left;transition-property:background-color}@media (max-width:991px){.menu .menu-item.menu-item-search{display:none}}.menu .menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);float:right;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}.sidebar{margin-top:12px;position:-webkit-sticky;position:sticky;top:12px;width:240px}@media (max-width:991px){.sidebar{display:none}}.sidebar-toggle{display:none}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color)}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;padding:0 5px;text-overflow:ellipsis;white-space:nowrap}.links-of-author-item a{border-bottom:0;border-radius:4px;display:block}.links-of-author-item a:hover{background:var(--body-bg-color)}.main-inner{background:var(--content-bg-color);box-shadow:initial;padding:40px}@media (max-width:991px){.main-inner{padding:20px}}.sub-menu{border-bottom:1px solid #ddd}.post-block:first-of-type{padding-top:40px}@media (max-width:767px){.pagination{margin-bottom:10px}} \ No newline at end of file diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 0887235a8c..99fcb16131 100644 --- a/leancloud_counter_security_urls.json +++ b/leancloud_counter_security_urls.json @@ -1 +1 @@ -[{"title":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"title":"2020 年终总结","url":"/2021/03/31/2020-年终总结/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"Apollo 如何获取当前环境","url":"/2022/09/04/Apollo-如何获取当前环境/"},{"title":"Apollo 客户端启动过程分析","url":"/2022/09/18/Apollo-客户端启动过程分析/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"Disruptor 系列二","url":"/2022/02/27/Disruptor-系列二/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"JVM源码分析之G1垃圾收集器分析一","url":"/2019/12/07/JVM-G1-Part-1/"},{"title":"Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析","url":"/2021/10/07/Leetcode-021-合并两个有序链表-Merge-Two-Sorted-Lists-题解分析/"},{"title":"Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"title":"Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"title":"Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"title":"Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析","url":"/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/"},{"title":"Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"title":"Disruptor 系列三","url":"/2022/09/25/Disruptor-系列三/"},{"title":"Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"title":"Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析","url":"/2022/07/22/Leetcode-1260-二维网格迁移-Shift-2D-Grid-Easy-题解分析/"},{"title":"Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 155 最小栈(Min Stack) 题解分析","url":"/2020/12/06/Leetcode-155-最小栈-Min-Stack-题解分析/"},{"title":"Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析","url":"/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/"},{"title":"Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"title":"Leetcode 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析","url":"/2022/09/11/Leetcode-1862-向下取整数对和-Sum-of-Floored-Pairs-Hard-题解分析/"},{"title":"Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"title":"Leetcode 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"title":"Leetcode 3 Longest Substring Without Repeating Characters 题解分析","url":"/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-题解分析/"},{"title":"Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析","url":"/2022/03/07/Leetcode-349-两个数组的交集-Intersection-of-Two-Arrays-Easy-题解分析/"},{"title":"Leetcode 42 接雨水 (Trapping Rain Water) 题解分析","url":"/2021/07/04/Leetcode-42-接雨水-Trapping-Rain-Water-题解分析/"},{"title":"Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"title":"Leetcode 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析","url":"/2022/06/19/Leetcode-698-划分为k个相等的子集-Partition-to-K-Equal-Sum-Subsets-Medium-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析","url":"/2022/08/23/Leetcode-885-螺旋矩阵-III-Spiral-Matrix-III-Medium-题解分析/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"binary-watch","url":"/2016/09/29/binary-watch/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"title":"docker-mysql-cluster","url":"/2016/08/14/docker-mysql-cluster/"},{"title":"docker比一般多一点的初学者介绍三","url":"/2020/03/21/docker比一般多一点的初学者介绍三/"},{"title":"docker比一般多一点的初学者介绍","url":"/2020/03/08/docker比一般多一点的初学者介绍/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"title":"minimum-size-subarray-sum-209","url":"/2016/10/11/minimum-size-subarray-sum-209/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"title":"C++ 指针使用中的一个小问题","url":"/2014/12/23/my-new-post/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"openresty","url":"/2019/06/18/openresty/"},{"title":"pcre-intro-and-a-simple-package","url":"/2015/01/16/pcre-intro-and-a-simple-package/"},{"title":"php-abstract-class-and-interface","url":"/2016/11/10/php-abstract-class-and-interface/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"redis数据结构介绍三-第三部分 整数集合","url":"/2020/01/10/redis数据结构介绍三/"},{"title":"redis数据结构介绍-第一部分 SDS,链表,字典","url":"/2019/12/26/redis数据结构介绍/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"redis数据结构介绍五-第五部分 对象","url":"/2020/01/20/redis数据结构介绍五/"},{"title":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析","url":"/2022/10/02/Leetcode-747-至少是其他数字两倍的最大数-Largest-Number-At-Least-Twice-of-Others-Easy-题解分析/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点","url":"/2021/09/19/聊一下-SpringBoot-中使用的-cglib-作为动态代理中的一个注意点/"},{"title":"聊一下 RocketMQ 的消息存储四","url":"/2021/10/17/聊一下-RocketMQ-的消息存储四/"},{"title":"聊一下 RocketMQ 的顺序消息","url":"/2021/08/29/聊一下-RocketMQ-的顺序消息/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊一下 SpringBoot 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"记一个容器中 dubbo 注册的小知识点","url":"/2022/10/09/记一个容器中-dubbo-注册的小知识点/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"}] \ No newline at end of file +[{"title":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"2020 年终总结","url":"/2021/03/31/2020-年终总结/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"Apollo 客户端启动过程分析","url":"/2022/09/18/Apollo-客户端启动过程分析/"},{"title":"Apollo 如何获取当前环境","url":"/2022/09/04/Apollo-如何获取当前环境/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"Disruptor 系列二","url":"/2022/02/27/Disruptor-系列二/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析","url":"/2021/10/07/Leetcode-021-合并两个有序链表-Merge-Two-Sorted-Lists-题解分析/"},{"title":"Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"title":"Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"title":"Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"title":"Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析","url":"/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/"},{"title":"Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"title":"Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Disruptor 系列三","url":"/2022/09/25/Disruptor-系列三/"},{"title":"Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 155 最小栈(Min Stack) 题解分析","url":"/2020/12/06/Leetcode-155-最小栈-Min-Stack-题解分析/"},{"title":"Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析","url":"/2022/07/22/Leetcode-1260-二维网格迁移-Shift-2D-Grid-Easy-题解分析/"},{"title":"Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"title":"Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析","url":"/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/"},{"title":"Leetcode 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析","url":"/2022/09/11/Leetcode-1862-向下取整数对和-Sum-of-Floored-Pairs-Hard-题解分析/"},{"title":"Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"title":"Leetcode 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"title":"Leetcode 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"title":"Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"title":"Leetcode 3 Longest Substring Without Repeating Characters 题解分析","url":"/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-题解分析/"},{"title":"Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析","url":"/2022/03/07/Leetcode-349-两个数组的交集-Intersection-of-Two-Arrays-Easy-题解分析/"},{"title":"Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"title":"Leetcode 42 接雨水 (Trapping Rain Water) 题解分析","url":"/2021/07/04/Leetcode-42-接雨水-Trapping-Rain-Water-题解分析/"},{"title":"Leetcode 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析","url":"/2022/06/19/Leetcode-698-划分为k个相等的子集-Partition-to-K-Equal-Sum-Subsets-Medium-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析","url":"/2022/08/23/Leetcode-885-螺旋矩阵-III-Spiral-Matrix-III-Medium-题解分析/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析","url":"/2022/10/02/Leetcode-747-至少是其他数字两倍的最大数-Largest-Number-At-Least-Twice-of-Others-Easy-题解分析/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"binary-watch","url":"/2016/09/29/binary-watch/"},{"title":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"title":"docker-mysql-cluster","url":"/2016/08/14/docker-mysql-cluster/"},{"title":"docker比一般多一点的初学者介绍","url":"/2020/03/08/docker比一般多一点的初学者介绍/"},{"title":"docker比一般多一点的初学者介绍三","url":"/2020/03/21/docker比一般多一点的初学者介绍三/"},{"title":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"title":"minimum-size-subarray-sum-209","url":"/2016/10/11/minimum-size-subarray-sum-209/"},{"title":"C++ 指针使用中的一个小问题","url":"/2014/12/23/my-new-post/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"title":"php-abstract-class-and-interface","url":"/2016/11/10/php-abstract-class-and-interface/"},{"title":"pcre-intro-and-a-simple-package","url":"/2015/01/16/pcre-intro-and-a-simple-package/"},{"title":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"redis数据结构介绍-第一部分 SDS,链表,字典","url":"/2019/12/26/redis数据结构介绍/"},{"title":"redis数据结构介绍三-第三部分 整数集合","url":"/2020/01/10/redis数据结构介绍三/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"redis数据结构介绍五-第五部分 对象","url":"/2020/01/20/redis数据结构介绍五/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"JVM源码分析之G1垃圾收集器分析一","url":"/2019/12/07/JVM-G1-Part-1/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"openresty","url":"/2019/06/18/openresty/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"聊一下 RocketMQ 的消息存储四","url":"/2021/10/17/聊一下-RocketMQ-的消息存储四/"},{"title":"聊一下 RocketMQ 的顺序消息","url":"/2021/08/29/聊一下-RocketMQ-的顺序消息/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊一下 SpringBoot 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"title":"聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点","url":"/2021/09/19/聊一下-SpringBoot-中使用的-cglib-作为动态代理中的一个注意点/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"记一个容器中 dubbo 注册的小知识点","url":"/2022/10/09/记一个容器中-dubbo-注册的小知识点/"}] \ No newline at end of file diff --git a/search.xml b/search.xml index a2a4d58cb6..68080df209 100644 --- a/search.xml +++ b/search.xml @@ -49,31 +49,6 @@ 2019 - - 2020 年终总结 - /2021/03/31/2020-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 拖更原因

这篇年终总结本来应该在农历过完年就出来的,结果是对没有受疫情影响的春节放假时间空闲情况预估太良好,虽然公司调了几天假,但是因为春节期间疫情状况比较好,本来酒店都不让接待聚餐什么的,后来统统放开,结果就是从初一到初六每天要不就是去亲戚家,要不就是去酒店饭店吃饭,计划很丰满,现实很骨感,时间感觉一下就没了,然后年后感觉有点犯懒了,所以才拖到现在。

-

生活-健身跑步

去年(19 年)的时候跑步突破了 300 公里,然后20 年给自己定了个 400 公里的目标,结果意料之中的没成功,原因可能疫情算一点吧,后面买了跑步机之后,基本周末回家都能跑一下,但是最后还是只跑了300 多公里,总的keep 记录跑量也没超过 1000 公里,所以跑步这个目标还是没成功的,不过还算是比去年多跑一点,这样也算后面好突破点,后面的目标就不定的太高了,每年能比前一年多一点就好,其实跑步已经从一种减肥方式变成一种习惯了,一周一次的跑步已经比较难有效减重了,但是对于保持精力和身体状态还是很有效和重要的,只是对于目前的体重还是要多减下去一些跑步才好,太重了对膝盖负担太大了,可惜还是时间呐,游泳骑车什么的都需要更苛刻的条件和时间,饮食呢控制起来比较难(贪吃
终于在 3 月底之前跑到了 1000 公里,迟了三个月,不过也总算达到了,只是体重控制还是不行,有试着走走楼梯,但是感觉对膝盖负担比较大,得再想想用什么方式

-

-

技术成长

一直提不起笔来写这篇年终总结还有个比较大的原因是觉得20 年的成长不如预期,大小目标都没怎么完成,比如深入了解 jvm,是想能有些深入的见解,而不再是某些点的比较片面的理解,系统性的归纳总结也比较少,每个方向或多或少有些看法和理解,但是不全面,一些东西看过了也会忘记,需要温故而知新,比如 AQS 的内容,第一次读其实理解比较浅,后面就强迫自己去读,去写,才有了一些比之前更深入的理解,因为很多文章都是带有作者思路的引导,适不适合自己都要看是否能从他的思路把它看懂,有些就差别很大,这个跟看书也一样,有些书大众一致推荐,一般情况下大多是经典的好的,但是也有可能是不太适合自己的,可能有时候机缘巧合看到的反而让人茅塞顿开,在 todo 里已经积攒了好多的点和面需要去学习实践,一方面是自己懒,一方面是时间也相对偏少,看看 21 年能不能有所提升,加强“时间管理”,哈哈

-

技术上主要是看了 mysql 的 mvcc 相关内容,rocketmq 的,redis 的代码,还有 mybatis 等,其实每一个都能写很多,也有很多值得学习的,需要全面系统学习,之前想好好画一个思维导图,将整个技术体系都梳理下,还只做了一点点,方式也有点问题,应该从大到小,而不是深度优先,细节有很多,每一个方面都有自己比较熟悉擅长的,也有不太了解的,可以做一个评分,这个也是亟待改善的,希望今年能完成。

-

博客

博客方面 20 年一年整是写了 53 篇,差不多是一周一篇的节奏,这个还是不错的,虽然博客质量参差不齐,但是这个更新频率还是比较好的,并且也定了个潜规则,可以一周技术一周生活,这样能缓解水文的频率,提高些技术文章的质量,虽然结果并没有好多少,不过感觉还是可以这么坚持的,能提高一些技术文章的质量那就更好了

-]]>
- - 生活 - 年终总结 - 2020 - 年终总结 - 2020 - - - 生活 - 年终总结 - 2020 - 2021 - 拖更 - -
2020年中总结 /2020/07/11/2020%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ @@ -94,18 +69,26 @@ - 2021 年终总结 - /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 又是一年年终总结,本着极度讨厌实时需求的理念,我还是 T+N 发布这个年终总结

-

工作篇

工作没什么大变化,有了些微的提升,可能因为是来了之后做了些项目对比公司与来还算是比较重要的,但是技术难度上没有特别突出的点,可能最开始用 openresty+lua 做了个 ab 测的工具,还是让我比较满意的,后面一般都是业务型的需求,今年可能在业务相关的技术逻辑上有了一些深度的了解,而原来一直想做的业务架构升级和通用型技术中间件这样的优化还是停留在想象中,前面说的 ab 测应该算是个半成品,还是没能多走出这一步,得需要多做一些实在的事情,比如轻量级的业务框架,能够对原先不熟悉的业务逻辑,代码逻辑有比较深入的理解,而不是一直都是让特定的同学负责特定的逻辑,很多时候还是在偷懒,习惯以一些简单安全的方案去做事情,在技术上还是要有所追求,还有就是能够在新语言,主要是 rust,swift 这类的能有些小玩具可以做,rust 的话是因为今年看了一本相关的书,后面三分之一其实消化得不好,这本书整体来说是很不错的,只是 rust 本身在所有权这块,还有引用包装等方面是设计得比较难懂,也可能是我基础差,所以还是想在复习下,可以做一个简单的命令行工具这种,然后 swift 是想说可以做点 mac 的小软件,原生的毕竟性能好点,又小。基于 web 做的客户端大部分都是又丑又大,极少数能好看点,但也是很重,起码 7~80M 的大小,原生的估计能除以 10。
整体的职业规划貌似陷入了比较大的困惑期,在目前公司发展前景不是很大,但是出去貌似也没有比较适合我的机会,总的来说还是杭州比较卷,个人觉得有自己的时间是非常重要的,而且这个不光是用来自我提升的,还是让自己有足够的时间做缓冲,有足够的时间锻炼减肥,时间少的情况下,不光会在仅有的时间里暴饮暴食,还没空锻炼,身体是革命的本钱,现在其实能特别明显地感觉到身体状态下滑,容易疲劳,焦虑。所以是否也许有可能以后要往外企这类的方向去发展。
工作上其实还是有个不大不小的缺点,就是容易激动,容易焦虑,前一点可能有稍稍地改观,因为工作中的很多现状其实是我个人难以改变的,即使觉得不合理,但是结构在那里,还不如自己放宽心,尽量做好事情就行。第二点的话还是做得比较差,一直以来抗压能力都比较差,跟成长环境,家庭环境都有比较大的关系,而且说实在的特别是父母,基本也没有在这方面给我正向的帮助,比较擅长给我施压,从小就是通过压力让我好好读书,当个乖学生,考个好学校,并没有能真正地理解我的压力,教我或者帮助我解压,只会在那说着不着边际的空话,甚至经常反过来对我施压。还是希望能慢慢解开,这点可能对我身体也有影响,也许需要看一些心理疏导相关的书籍。工作篇暂时到这,后续还有其他篇,未完待续哈哈😀

+ 2020 年终总结 + /2021/03/31/2020-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 拖更原因

这篇年终总结本来应该在农历过完年就出来的,结果是对没有受疫情影响的春节放假时间空闲情况预估太良好,虽然公司调了几天假,但是因为春节期间疫情状况比较好,本来酒店都不让接待聚餐什么的,后来统统放开,结果就是从初一到初六每天要不就是去亲戚家,要不就是去酒店饭店吃饭,计划很丰满,现实很骨感,时间感觉一下就没了,然后年后感觉有点犯懒了,所以才拖到现在。

+

生活-健身跑步

去年(19 年)的时候跑步突破了 300 公里,然后20 年给自己定了个 400 公里的目标,结果意料之中的没成功,原因可能疫情算一点吧,后面买了跑步机之后,基本周末回家都能跑一下,但是最后还是只跑了300 多公里,总的keep 记录跑量也没超过 1000 公里,所以跑步这个目标还是没成功的,不过还算是比去年多跑一点,这样也算后面好突破点,后面的目标就不定的太高了,每年能比前一年多一点就好,其实跑步已经从一种减肥方式变成一种习惯了,一周一次的跑步已经比较难有效减重了,但是对于保持精力和身体状态还是很有效和重要的,只是对于目前的体重还是要多减下去一些跑步才好,太重了对膝盖负担太大了,可惜还是时间呐,游泳骑车什么的都需要更苛刻的条件和时间,饮食呢控制起来比较难(贪吃
终于在 3 月底之前跑到了 1000 公里,迟了三个月,不过也总算达到了,只是体重控制还是不行,有试着走走楼梯,但是感觉对膝盖负担比较大,得再想想用什么方式

+

+

技术成长

一直提不起笔来写这篇年终总结还有个比较大的原因是觉得20 年的成长不如预期,大小目标都没怎么完成,比如深入了解 jvm,是想能有些深入的见解,而不再是某些点的比较片面的理解,系统性的归纳总结也比较少,每个方向或多或少有些看法和理解,但是不全面,一些东西看过了也会忘记,需要温故而知新,比如 AQS 的内容,第一次读其实理解比较浅,后面就强迫自己去读,去写,才有了一些比之前更深入的理解,因为很多文章都是带有作者思路的引导,适不适合自己都要看是否能从他的思路把它看懂,有些就差别很大,这个跟看书也一样,有些书大众一致推荐,一般情况下大多是经典的好的,但是也有可能是不太适合自己的,可能有时候机缘巧合看到的反而让人茅塞顿开,在 todo 里已经积攒了好多的点和面需要去学习实践,一方面是自己懒,一方面是时间也相对偏少,看看 21 年能不能有所提升,加强“时间管理”,哈哈

+

技术上主要是看了 mysql 的 mvcc 相关内容,rocketmq 的,redis 的代码,还有 mybatis 等,其实每一个都能写很多,也有很多值得学习的,需要全面系统学习,之前想好好画一个思维导图,将整个技术体系都梳理下,还只做了一点点,方式也有点问题,应该从大到小,而不是深度优先,细节有很多,每一个方面都有自己比较熟悉擅长的,也有不太了解的,可以做一个评分,这个也是亟待改善的,希望今年能完成。

+

博客

博客方面 20 年一年整是写了 53 篇,差不多是一周一篇的节奏,这个还是不错的,虽然博客质量参差不齐,但是这个更新频率还是比较好的,并且也定了个潜规则,可以一周技术一周生活,这样能缓解水文的频率,提高些技术文章的质量,虽然结果并没有好多少,不过感觉还是可以这么坚持的,能提高一些技术文章的质量那就更好了

]]>
生活 年终总结 + 2020 + 年终总结 + 2020 生活 年终总结 + 2020 2021 拖更 @@ -676,6 +659,23 @@ public: unlock
+ + 2021 年终总结 + /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 又是一年年终总结,本着极度讨厌实时需求的理念,我还是 T+N 发布这个年终总结

+

工作篇

工作没什么大变化,有了些微的提升,可能因为是来了之后做了些项目对比公司与来还算是比较重要的,但是技术难度上没有特别突出的点,可能最开始用 openresty+lua 做了个 ab 测的工具,还是让我比较满意的,后面一般都是业务型的需求,今年可能在业务相关的技术逻辑上有了一些深度的了解,而原来一直想做的业务架构升级和通用型技术中间件这样的优化还是停留在想象中,前面说的 ab 测应该算是个半成品,还是没能多走出这一步,得需要多做一些实在的事情,比如轻量级的业务框架,能够对原先不熟悉的业务逻辑,代码逻辑有比较深入的理解,而不是一直都是让特定的同学负责特定的逻辑,很多时候还是在偷懒,习惯以一些简单安全的方案去做事情,在技术上还是要有所追求,还有就是能够在新语言,主要是 rust,swift 这类的能有些小玩具可以做,rust 的话是因为今年看了一本相关的书,后面三分之一其实消化得不好,这本书整体来说是很不错的,只是 rust 本身在所有权这块,还有引用包装等方面是设计得比较难懂,也可能是我基础差,所以还是想在复习下,可以做一个简单的命令行工具这种,然后 swift 是想说可以做点 mac 的小软件,原生的毕竟性能好点,又小。基于 web 做的客户端大部分都是又丑又大,极少数能好看点,但也是很重,起码 7~80M 的大小,原生的估计能除以 10。
整体的职业规划貌似陷入了比较大的困惑期,在目前公司发展前景不是很大,但是出去貌似也没有比较适合我的机会,总的来说还是杭州比较卷,个人觉得有自己的时间是非常重要的,而且这个不光是用来自我提升的,还是让自己有足够的时间做缓冲,有足够的时间锻炼减肥,时间少的情况下,不光会在仅有的时间里暴饮暴食,还没空锻炼,身体是革命的本钱,现在其实能特别明显地感觉到身体状态下滑,容易疲劳,焦虑。所以是否也许有可能以后要往外企这类的方向去发展。
工作上其实还是有个不大不小的缺点,就是容易激动,容易焦虑,前一点可能有稍稍地改观,因为工作中的很多现状其实是我个人难以改变的,即使觉得不合理,但是结构在那里,还不如自己放宽心,尽量做好事情就行。第二点的话还是做得比较差,一直以来抗压能力都比较差,跟成长环境,家庭环境都有比较大的关系,而且说实在的特别是父母,基本也没有在这方面给我正向的帮助,比较擅长给我施压,从小就是通过压力让我好好读书,当个乖学生,考个好学校,并没有能真正地理解我的压力,教我或者帮助我解压,只会在那说着不着边际的空话,甚至经常反过来对我施压。还是希望能慢慢解开,这点可能对我身体也有影响,也许需要看一些心理疏导相关的书籍。工作篇暂时到这,后续还有其他篇,未完待续哈哈😀

+]]>
+ + 生活 + 年终总结 + + + 生活 + 年终总结 + 2021 + 拖更 + +
AbstractQueuedSynchronizer /2019/09/23/AbstractQueuedSynchronizer/ @@ -742,6 +742,109 @@ public: aqs + + add-two-number + /2015/04/14/Add-Two-Number/ + problem

You are given two linked lists representing two non-negative numbers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

+

Input:(2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8

+

分析(不用英文装逼了)
这个代码是抄来的,链接原作是这位大大。

+ +

一开始没看懂题,后来发现是要进位的,自己写的时候想把长短不同时长的串接到结果
串的后面,试了下因为进位会有些问题比较难搞定,这样的话就是在其中一个为空的
时候还是会循环操作,在链表太大的时候可能会有问题,就这样(逃
原来是有个小错误没发现,改进后的代码也AC了,棒棒哒!

+

正确代码

/**
+ * Definition for singly-linked list.
+ * struct ListNode {
+ *     int val;
+ *     ListNode *next;
+ *     ListNode(int x) : val(x), next(NULL) {}
+ * };
+ */
+class Solution {
+public:
+    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
+        ListNode dummy(0);
+        ListNode* p = &dummy;
+
+        int cn = 0;
+        while(l1 || l2){
+            int val = cn + (l1 ? l1->val : 0) + (l2 ? l2->val : 0);
+            cn = val / 10;
+            val = val % 10;
+            p->next = new ListNode(val);
+            p = p->next;
+            if(l1){
+                l1 = l1->next;
+            }
+            if(l2){
+                l2 = l2->next;
+            }
+        }
+        if(cn != 0){
+            p->next = new ListNode(cn);
+            p = p->next;
+        }
+        return dummy.next;
+    }
+};
+ +

失败的代码

/**
+ * Definition for singly-linked list.
+ * struct ListNode {
+ *     int val;
+ *     ListNode *next;
+ *     ListNode(int x) : val(x), next(NULL) {}
+ * };
+ */
+class Solution {
+public:
+    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
+        ListNode dummy(0);
+        ListNode* p = &dummy;
+
+        int cn = 0;
+        int flag = 0;
+        while(l1 || l2){
+            int val = cn + (l1 ? l1->val : 0) + (l2 ? l2->val : 0);
+            cn = val / 10;
+            val = val % 10;
+            p->next = new ListNode(val);
+            p = p->next;
+            if(!l1 && cn == 0){
+                flag = 1;
+                break;
+            }
+            if(!l2 && cn == 0){
+                flag = 1;
+                break;
+            }
+            if(l1){
+                l1 = l1->next;
+            }
+            if(l2){
+                l2 = l2->next;
+            }
+        }
+        if(!l1 && cn == 0 && flag == 1){
+            p->next = l2->next;
+        }
+        if(!l2 && cn == 0 && flag == 1){
+            p->next = l1->next;
+        }
+        if(cn != 0){
+            p->next = new ListNode(cn);
+            p = p->next;
+        }
+        return dummy.next;
+    }
+};
+]]>
+ + leetcode + + + leetcode + c++ + +
AQS篇一 /2021/02/14/AQS%E7%AF%87%E4%B8%80/ @@ -971,165 +1074,21 @@ public: - add-two-number - /2015/04/14/Add-Two-Number/ - problem

You are given two linked lists representing two non-negative numbers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

-

Input:(2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8

-

分析(不用英文装逼了)
这个代码是抄来的,链接原作是这位大大。

- -

一开始没看懂题,后来发现是要进位的,自己写的时候想把长短不同时长的串接到结果
串的后面,试了下因为进位会有些问题比较难搞定,这样的话就是在其中一个为空的
时候还是会循环操作,在链表太大的时候可能会有问题,就这样(逃
原来是有个小错误没发现,改进后的代码也AC了,棒棒哒!

-

正确代码

/**
- * Definition for singly-linked list.
- * struct ListNode {
- *     int val;
- *     ListNode *next;
- *     ListNode(int x) : val(x), next(NULL) {}
- * };
- */
-class Solution {
-public:
-    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
-        ListNode dummy(0);
-        ListNode* p = &dummy;
+    Apollo 客户端启动过程分析
+    /2022/09/18/Apollo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90/
+    入口是可以在 springboot 的启动类上打上EnableApolloConfig 注解

+
@Import(ApolloConfigRegistrar.class)
+public @interface EnableApolloConfig {
+

这个 import 实现了

+
public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
 
-        int cn = 0;
-        while(l1 || l2){
-            int val = cn + (l1 ? l1->val : 0) + (l2 ? l2->val : 0);
-            cn = val / 10;
-            val = val % 10;
-            p->next = new ListNode(val);
-            p = p->next;
-            if(l1){
-                l1 = l1->next;
-            }
-            if(l2){
-                l2 = l2->next;
-            }
-        }
-        if(cn != 0){
-            p->next = new ListNode(cn);
-            p = p->next;
-        }
-        return dummy.next;
-    }
-};
+ private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class); -

失败的代码

/**
- * Definition for singly-linked list.
- * struct ListNode {
- *     int val;
- *     ListNode *next;
- *     ListNode(int x) : val(x), next(NULL) {}
- * };
- */
-class Solution {
-public:
-    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
-        ListNode dummy(0);
-        ListNode* p = &dummy;
-
-        int cn = 0;
-        int flag = 0;
-        while(l1 || l2){
-            int val = cn + (l1 ? l1->val : 0) + (l2 ? l2->val : 0);
-            cn = val / 10;
-            val = val % 10;
-            p->next = new ListNode(val);
-            p = p->next;
-            if(!l1 && cn == 0){
-                flag = 1;
-                break;
-            }
-            if(!l2 && cn == 0){
-                flag = 1;
-                break;
-            }
-            if(l1){
-                l1 = l1->next;
-            }
-            if(l2){
-                l2 = l2->next;
-            }
-        }
-        if(!l1 && cn == 0 && flag == 1){
-            p->next = l2->next;
-        }
-        if(!l2 && cn == 0 && flag == 1){
-            p->next = l1->next;
-        }
-        if(cn != 0){
-            p->next = new ListNode(cn);
-            p = p->next;
-        }
-        return dummy.next;
-    }
-};
-]]>
- - leetcode - - - leetcode - c++ - - - - Apollo 如何获取当前环境 - /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/ - 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境

-
public enum Env{
-  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
-
-  public static Env fromString(String env) {
-    Env environment = EnvUtils.transformEnv(env);
-    Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
-    return environment;
-  }
-}
-

而这些解释

-
/**
- * Here is the brief description for all the predefined environments:
- * <ul>
- *   <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
- *   <li>DEV: Development environment</li>
- *   <li>FWS: Feature Web Service Test environment</li>
- *   <li>FAT: Feature Acceptance Test environment</li>
- *   <li>UAT: User Acceptance Test environment</li>
- *   <li>LPT: Load and Performance Test environment</li>
- *   <li>PRO: Production environment</li>
- *   <li>TOOLS: Tooling environment, a special area in production environment which allows
- * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
- * </ul>
- */
-

那如果要在运行时知道 apollo 当前使用的环境可以用这个

-
Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
-

简单记录下。

-]]>
- - Java - - - Java - Apollo - environment - -
- - Apollo 客户端启动过程分析 - /2022/09/18/Apollo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90/ - 入口是可以在 springboot 的启动类上打上EnableApolloConfig 注解

-
@Import(ApolloConfigRegistrar.class)
-public @interface EnableApolloConfig {
-

这个 import 实现了

-
public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
-
-  private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);
-
-  @Override
-  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
-    helper.registerBeanDefinitions(importingClassMetadata, registry);
-  }
-}
+ @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + helper.registerBeanDefinitions(importingClassMetadata, registry); + } +}

然后就调用了

com.ctrip.framework.apollo.spring.spi.DefaultApolloConfigRegistrarHelper#registerBeanDefinitions
@@ -1459,6 +1418,47 @@ public: Apollo
+ + Apollo 如何获取当前环境 + /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/ + 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境

+
public enum Env{
+  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
+
+  public static Env fromString(String env) {
+    Env environment = EnvUtils.transformEnv(env);
+    Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
+    return environment;
+  }
+}
+

而这些解释

+
/**
+ * Here is the brief description for all the predefined environments:
+ * <ul>
+ *   <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
+ *   <li>DEV: Development environment</li>
+ *   <li>FWS: Feature Web Service Test environment</li>
+ *   <li>FAT: Feature Acceptance Test environment</li>
+ *   <li>UAT: User Acceptance Test environment</li>
+ *   <li>LPT: Load and Performance Test environment</li>
+ *   <li>PRO: Production environment</li>
+ *   <li>TOOLS: Tooling environment, a special area in production environment which allows
+ * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
+ * </ul>
+ */
+

那如果要在运行时知道 apollo 当前使用的环境可以用这个

+
Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
+

简单记录下。

+]]>
+ + Java + + + Java + Apollo + environment + +
Clone Graph Part I /2014/12/30/Clone-Graph-Part-I/ @@ -1599,95 +1599,6 @@ Node *clone(Node *graph) { 注解 - - Disruptor 系列一 - /2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ - 很久之前就听说过这个框架,不过之前有点跟消息队列混起来,这个也是种队列,但不是跟 rocketmq,nsq 那种一样的,而是在进程内部提供队列服务的,偏向于取代ArrayBlockingQueue,因为这个阻塞队列是使用了锁来控制阻塞,关于并发其实有一些通用的最佳实践,就是用锁,即使是 JDK 提供的锁,也是比较耗资源的,当然这是跟不加锁的对比,同样是锁,JDK 的实现还是性能比较优秀的。常见的阻塞队列中例如 ArrayBlockingQueueLinkedBlockingQueue 都有锁的身影的存在,区别在于 ArrayBlockingQueue 是一把锁,后者是两把锁,不过重点不在几把锁,这里其实是两个问题,一个是所谓的 lock free, 对于一个单生产者的 disruptor 来说,因为写入是只有一个线程的,是可以不用加锁,多生产者的时候使用的是 cas 来获取对应的写入坑位,另一个是解决“伪共享”问题,后面可以详细点分析,先介绍下使用
首先是数据源

-
public class LongEvent {
-    private long value;
-
-    public void set(long value) {
-        this.value = value;
-    }
-
-    public long getValue() {
-        return value;
-    }
-
-    public void setValue(long value) {
-        this.value = value;
-    }
-}
-

事件生产

-
public class LongEventFactory implements EventFactory<LongEvent>
-{
-    public LongEvent newInstance()
-    {
-        return new LongEvent();
-    }
-}
-

事件处理器

-
public class LongEventHandler implements EventHandler<LongEvent> {
-
-    // event 事件,
-    // sequence 当前的序列 
-    // 是否当前批次最后一个数据
-    public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
-    {
-        String str = String.format("long event : %s l:%s b:%s", event.getValue(), sequence, endOfBatch);
-        System.out.println(str);
-    }
-}
-
-

主方法代码

-
package disruptor;
-
-import com.lmax.disruptor.RingBuffer;
-import com.lmax.disruptor.dsl.Disruptor;
-import com.lmax.disruptor.util.DaemonThreadFactory;
-
-import java.nio.ByteBuffer;
-
-public class LongEventMain
-{
-    public static void main(String[] args) throws Exception
-    {
-        // 这个需要是 2 的幂次,这样在定位的时候只需要位移操作,也能减少各种计算操作
-        int bufferSize = 1024; 
-
-        Disruptor<LongEvent> disruptor = 
-                new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
-
-        // 类似于注册处理器
-        disruptor.handleEventsWith(new LongEventHandler());
-        // 或者直接用 lambda
-        disruptor.handleEventsWith((event, sequence, endOfBatch) ->
-                System.out.println("Event: " + event));
-        // 启动我们的 disruptor
-        disruptor.start(); 
-
-
-        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); 
-        ByteBuffer bb = ByteBuffer.allocate(8);
-        for (long l = 0; true; l++)
-        {
-            bb.putLong(0, l);
-            // 生产事件
-            ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
-            Thread.sleep(1000);
-        }
-    }
-}
-

运行下可以看到运行结果

这里其实就只是最简单的使用,生产者只有一个,然后也不是批量的。

-]]>
- - Java - - - Java - Disruptor - -
Comparator使用小记 /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/ @@ -1851,96 +1762,13 @@ Node *clone(Node *graph) { - G1收集器概述 - /2020/02/09/G1%E6%94%B6%E9%9B%86%E5%99%A8%E6%A6%82%E8%BF%B0/ - G1: The Garbage-First Collector, 垃圾回收优先的垃圾回收器,目标是用户多核 cpu 和大内存的机器,最大的特点就是可预测的停顿时间,官方给出的介绍是提供一个用户在大的堆内存情况下一个低延迟表现的解决方案,通常是 6GB 及以上的堆大小,有低于 0.5 秒稳定的可预测的停顿时间。

-

这里主要介绍这个比较新的垃圾回收器,在 G1 之前的垃圾回收器都是基于如下图的内存结构分布,有新生代,老年代和永久代(jdk8 之前),然后G1 往前的那些垃圾回收器都有个分代,比如 serial,parallel 等,一般有个应用的组合,最初的 serial 和 serial old,因为新生代和老年代的收集方式不太一样,新生代主要是标记复制,所以有 eden 跟两个 survival区,老年代一般用标记整理方式,而 G1 对这个不太一样。

看一下 G1 的内存分布

可以看到这有很大的不同,G1 通过将内存分成大小相等的 region,每个region是存在于一个连续的虚拟内存范围,对于某个 region 来说其角色是类似于原来的收集器的Eden、Survivor、Old Generation,这个具体在代码层面

-
// We encode the value of the heap region type so the generation can be
- // determined quickly. The tag is split into two parts:
- //
- //   major type (young, old, humongous, archive)           : top N-1 bits
- //   minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
- //
- // If there's need to increase the number of minor types in the
- // future, we'll have to increase the size of the latter and hence
- // decrease the size of the former.
- //
- // 00000 0 [ 0] Free
- //
- // 00001 0 [ 2] Young Mask
- // 00001 0 [ 2] Eden
- // 00001 1 [ 3] Survivor
- //
- // 00010 0 [ 4] Humongous Mask
- // 00100 0 [ 8] Pinned Mask
- // 00110 0 [12] Starts Humongous
- // 00110 1 [13] Continues Humongous
- //
- // 01000 0 [16] Old Mask
- //
- // 10000 0 [32] Archive Mask
- // 11100 0 [56] Open Archive
- // 11100 1 [57] Closed Archive
- //
- typedef enum {
-   FreeTag               = 0,
-
-   YoungMask             = 2,
-   EdenTag               = YoungMask,
-   SurvTag               = YoungMask + 1,
-
-   HumongousMask         = 4,
-   PinnedMask            = 8,
-   StartsHumongousTag    = HumongousMask | PinnedMask,
-   ContinuesHumongousTag = HumongousMask | PinnedMask + 1,
+    Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?
+    /2020/08/22/Filter-Intercepter-Aop-%E5%95%A5-%E5%95%A5-%E5%95%A5-%E8%BF%99%E4%BA%9B%E9%83%BD%E6%98%AF%E5%95%A5/
+    本来是想取个像现在那些公众号转了又转的文章标题,”面试官再问你xxxxx,就把这篇文章甩给他看”这种标题,但是觉得实在太 low 了,还是用一部我比较喜欢的电影里的一句台词,《人在囧途》里王宝强对着那张老板给他的欠条,看不懂字时候说的那句,这些都是些啥(第四声)
当我刚开始面 Java 的时候,其实我真的没注意这方面的东西,实话说就是不知道这些是啥,开发中用过 Interceptor和 Aop,了解 aop 的实现原理,但是不知道 Java web 中的 Filter 是怎么回事,知道 dubbo 的 filter,就这样,所以被问到了的确是回答不出来,可能就觉得这个渣渣,这么简单的都不会,所以还是花点时间来看看这个是个啥,为了避免我口吐芬芳,还是耐下性子来简单说下这几个东西
首先是 servlet,怎么去解释这个呢,因为之前是 PHPer,所以比较喜欢用它来举例子,在普通的 PHP 的 web 应用中一般有几部分组成,接受 HTTP 请求的是前置的 nginx 或者 apache,但是这俩玩意都是只能处理静态的请求,远古时代 PHP 和 HTML 混编是通过 apache 的 php module,跟后来 nginx 使用 php-fpm 其实道理类似,就是把请求中需要 PHP 处理的转发给 PHP,在 Java 中呢,是有个比较牛叉的叫 Tomcat 的,它可以把请求转成 servlet,而 servlet 其实就是一种实现了特定接口的 Java 代码,

+

+package javax.servlet;
 
-   OldMask               = 16,
-   OldTag                = OldMask,
-
-   // Archive regions are regions with immutable content (i.e. not reclaimed, and
-   // not allocated into during regular operation). They differ in the kind of references
-   // allowed for the contained objects:
-   // - Closed archive regions form a separate self-contained (closed) object graph
-   // within the set of all of these regions. No references outside of closed
-   // archive regions are allowed.
-   // - Open archive regions have no restrictions on the references of their objects.
-   // Objects within these regions are allowed to have references to objects
-   // contained in any other kind of regions.
-   ArchiveMask           = 32,
-   OpenArchiveTag        = ArchiveMask | PinnedMask | OldMask,
-   ClosedArchiveTag      = ArchiveMask | PinnedMask | OldMask + 1
- } Tag;
- -

hotspot/share/gc/g1/heapRegionType.hpp

-

当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

-

由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

-

需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。

-

注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。

-

在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。

-]]>
- - Java - JVM - GC - C++ - - - Java - JVM - C++ - G1 - GC - Garbage-First Collector - - - - Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥? - /2020/08/22/Filter-Intercepter-Aop-%E5%95%A5-%E5%95%A5-%E5%95%A5-%E8%BF%99%E4%BA%9B%E9%83%BD%E6%98%AF%E5%95%A5/ - 本来是想取个像现在那些公众号转了又转的文章标题,”面试官再问你xxxxx,就把这篇文章甩给他看”这种标题,但是觉得实在太 low 了,还是用一部我比较喜欢的电影里的一句台词,《人在囧途》里王宝强对着那张老板给他的欠条,看不懂字时候说的那句,这些都是些啥(第四声)
当我刚开始面 Java 的时候,其实我真的没注意这方面的东西,实话说就是不知道这些是啥,开发中用过 Interceptor和 Aop,了解 aop 的实现原理,但是不知道 Java web 中的 Filter 是怎么回事,知道 dubbo 的 filter,就这样,所以被问到了的确是回答不出来,可能就觉得这个渣渣,这么简单的都不会,所以还是花点时间来看看这个是个啥,为了避免我口吐芬芳,还是耐下性子来简单说下这几个东西
首先是 servlet,怎么去解释这个呢,因为之前是 PHPer,所以比较喜欢用它来举例子,在普通的 PHP 的 web 应用中一般有几部分组成,接受 HTTP 请求的是前置的 nginx 或者 apache,但是这俩玩意都是只能处理静态的请求,远古时代 PHP 和 HTML 混编是通过 apache 的 php module,跟后来 nginx 使用 php-fpm 其实道理类似,就是把请求中需要 PHP 处理的转发给 PHP,在 Java 中呢,是有个比较牛叉的叫 Tomcat 的,它可以把请求转成 servlet,而 servlet 其实就是一种实现了特定接口的 Java 代码,

-

-package javax.servlet;
-
-import java.io.IOException;
+import java.io.IOException;
 
 /**
  * Defines methods that all servlets must implement.
@@ -2401,907 +2229,934 @@ Node *clone(Node *graph) {
       
   
   
-    JVM源码分析之G1垃圾收集器分析一
-    /2019/12/07/JVM-G1-Part-1/
-    对 Java 的 gc 实现比较感兴趣,原先一般都是看周志明的书,但其实并没有讲具体的 gc 源码,而是把整个思路和流程讲解了一下
特别是 G1 的具体实现
一般对 G1 的理解其实就是把原先整块的新生代老年代分成了以 region 为单位的小块内存,简而言之,就是原先对新生代老年代的收集会涉及到整个代的堆内存空间,而G1 把它变成了更细致的小块内存
这带来了一个很明显的好处和一个很明显的坏处,好处是内存收集可以更灵活,耗时会变短,但整个收集的处理复杂度就变高了
目前看了一点点关于 G1 收集的预期时间相关的代码

-
HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
-                                               uint gc_count_before,
-                                               bool* succeeded,
-                                               GCCause::Cause gc_cause) {
-  assert_heap_not_locked_and_not_at_safepoint();
-  VM_G1CollectForAllocation op(word_size,
-                               gc_count_before,
-                               gc_cause,
-                               false, /* should_initiate_conc_mark */
-                               g1_policy()->max_pause_time_ms());
-  VMThread::execute(&op);
-
-  HeapWord* result = op.result();
-  bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
-  assert(result == NULL || ret_succeeded,
-         "the result should be NULL if the VM did not succeed");
-  *succeeded = ret_succeeded;
+    G1收集器概述
+    /2020/02/09/G1%E6%94%B6%E9%9B%86%E5%99%A8%E6%A6%82%E8%BF%B0/
+    G1: The Garbage-First Collector, 垃圾回收优先的垃圾回收器,目标是用户多核 cpu 和大内存的机器,最大的特点就是可预测的停顿时间,官方给出的介绍是提供一个用户在大的堆内存情况下一个低延迟表现的解决方案,通常是 6GB 及以上的堆大小,有低于 0.5 秒稳定的可预测的停顿时间。

+

这里主要介绍这个比较新的垃圾回收器,在 G1 之前的垃圾回收器都是基于如下图的内存结构分布,有新生代,老年代和永久代(jdk8 之前),然后G1 往前的那些垃圾回收器都有个分代,比如 serial,parallel 等,一般有个应用的组合,最初的 serial 和 serial old,因为新生代和老年代的收集方式不太一样,新生代主要是标记复制,所以有 eden 跟两个 survival区,老年代一般用标记整理方式,而 G1 对这个不太一样。

看一下 G1 的内存分布

可以看到这有很大的不同,G1 通过将内存分成大小相等的 region,每个region是存在于一个连续的虚拟内存范围,对于某个 region 来说其角色是类似于原来的收集器的Eden、Survivor、Old Generation,这个具体在代码层面

+
// We encode the value of the heap region type so the generation can be
+ // determined quickly. The tag is split into two parts:
+ //
+ //   major type (young, old, humongous, archive)           : top N-1 bits
+ //   minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
+ //
+ // If there's need to increase the number of minor types in the
+ // future, we'll have to increase the size of the latter and hence
+ // decrease the size of the former.
+ //
+ // 00000 0 [ 0] Free
+ //
+ // 00001 0 [ 2] Young Mask
+ // 00001 0 [ 2] Eden
+ // 00001 1 [ 3] Survivor
+ //
+ // 00010 0 [ 4] Humongous Mask
+ // 00100 0 [ 8] Pinned Mask
+ // 00110 0 [12] Starts Humongous
+ // 00110 1 [13] Continues Humongous
+ //
+ // 01000 0 [16] Old Mask
+ //
+ // 10000 0 [32] Archive Mask
+ // 11100 0 [56] Open Archive
+ // 11100 1 [57] Closed Archive
+ //
+ typedef enum {
+   FreeTag               = 0,
 
-  assert_heap_not_locked();
-  return result;
-}
-

这里就是收集时需要停顿的,其中VMThread::execute(&op);是具体执行的,真正执行的是VM_G1CollectForAllocation::doit方法

-
void VM_G1CollectForAllocation::doit() {
-  G1CollectedHeap* g1h = G1CollectedHeap::heap();
-  assert(!_should_initiate_conc_mark || g1h->should_do_concurrent_full_gc(_gc_cause),
-      "only a GC locker, a System.gc(), stats update, whitebox, or a hum allocation induced GC should start a cycle");
+   YoungMask             = 2,
+   EdenTag               = YoungMask,
+   SurvTag               = YoungMask + 1,
 
-  if (_word_size > 0) {
-    // An allocation has been requested. So, try to do that first.
-    _result = g1h->attempt_allocation_at_safepoint(_word_size,
-                                                   false /* expect_null_cur_alloc_region */);
-    if (_result != NULL) {
-      // If we can successfully allocate before we actually do the
-      // pause then we will consider this pause successful.
-      _pause_succeeded = true;
-      return;
-    }
-  }
+   HumongousMask         = 4,
+   PinnedMask            = 8,
+   StartsHumongousTag    = HumongousMask | PinnedMask,
+   ContinuesHumongousTag = HumongousMask | PinnedMask + 1,
 
-  GCCauseSetter x(g1h, _gc_cause);
-  if (_should_initiate_conc_mark) {
-    // It's safer to read old_marking_cycles_completed() here, given
-    // that noone else will be updating it concurrently. Since we'll
-    // only need it if we're initiating a marking cycle, no point in
-    // setting it earlier.
-    _old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();
+   OldMask               = 16,
+   OldTag                = OldMask,
 
-    // At this point we are supposed to start a concurrent cycle. We
-    // will do so if one is not already in progress.
-    bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);
+   // Archive regions are regions with immutable content (i.e. not reclaimed, and
+   // not allocated into during regular operation). They differ in the kind of references
+   // allowed for the contained objects:
+   // - Closed archive regions form a separate self-contained (closed) object graph
+   // within the set of all of these regions. No references outside of closed
+   // archive regions are allowed.
+   // - Open archive regions have no restrictions on the references of their objects.
+   // Objects within these regions are allowed to have references to objects
+   // contained in any other kind of regions.
+   ArchiveMask           = 32,
+   OpenArchiveTag        = ArchiveMask | PinnedMask | OldMask,
+   ClosedArchiveTag      = ArchiveMask | PinnedMask | OldMask + 1
+ } Tag;
- // The above routine returns true if we were able to force the - // next GC pause to be an initial mark; it returns false if a - // marking cycle is already in progress. - // - // If a marking cycle is already in progress just return and skip the - // pause below - if the reason for requesting this initial mark pause - // was due to a System.gc() then the requesting thread should block in - // doit_epilogue() until the marking cycle is complete. - // - // If this initial mark pause was requested as part of a humongous - // allocation then we know that the marking cycle must just have - // been started by another thread (possibly also allocating a humongous - // object) as there was no active marking cycle when the requesting - // thread checked before calling collect() in - // attempt_allocation_humongous(). Retrying the GC, in this case, - // will cause the requesting thread to spin inside collect() until the - // just started marking cycle is complete - which may be a while. So - // we do NOT retry the GC. - if (!res) { - assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating"); - if (_gc_cause != GCCause::_g1_humongous_allocation) { - _should_retry_gc = true; - } - return; - } - } +

hotspot/share/gc/g1/heapRegionType.hpp

+

当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

+

由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

+

需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。

+

注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。

+

在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。

+]]>
+ + Java + JVM + GC + C++ + + + Java + JVM + C++ + G1 + GC + Garbage-First Collector + + + + Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析 + /2021/10/07/Leetcode-021-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8-Merge-Two-Sorted-Lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.

+

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

+

示例 1

+
+

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

+
+

示例 2

+

输入: l1 = [], l2 = []
输出: []

+
+

示例 3

+

输入: l1 = [], l2 = [0]
输出: [0]

+
+

简要分析

这题是 Easy 的,看着也挺简单,两个链表进行合并,就是比较下大小,可能将就点的话最好就在两个链表中原地合并

+

题解代码

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
+        // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
+        if (l1 == null) {
+            return l2;
+        }
+        if (l2 == null) {
+            return l1;
+        }
+        // new 一个合并后的头结点
+        ListNode merged = new ListNode();
+        // 这个是当前节点
+        ListNode current = merged;
+        // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
+        // 因为内部前两个if就是跳出条件
+        while (true) {
+            if (l1 == null) {
+                // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
+                // 所以不能是直接current = l2,这样就是把后面的直接丢了
+                current.val = l2.val;
+                current.next = l2.next;
+                break;
+            }
+            if (l2 == null) {
+                current.val = l1.val;
+                current.next = l1.next;
+                break;
+            }
+            // 这里是两个链表都不为空的时候,就比较下大小
+            if (l1.val < l2.val) {
+                current.val = l1.val;
+                l1 = l1.next;
+            } else {
+                current.val = l2.val;
+                l2 = l2.next;
+            }
+            // 这里是new个新的,其实也可以放在循环头上
+            current.next = new ListNode();
+            current = current.next;
+        }
+        current = null;
+        // 返回这个头结点
+        return merged;
+    }
- // Try a partial collection of some kind. - _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms); - - if (_pause_succeeded) { - if (_word_size > 0) { - // An allocation had been requested. Do it, eventually trying a stronger - // kind of GC. - _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded); - } else { - bool should_upgrade_to_full = !g1h->should_do_concurrent_full_gc(_gc_cause) && - !g1h->has_regions_left_for_allocation(); - if (should_upgrade_to_full) { - // There has been a request to perform a GC to free some space. We have no - // information on how much memory has been asked for. In case there are - // absolutely no regions left to allocate into, do a maximally compacting full GC. - log_info(gc, ergo)("Attempting maximally compacting collection"); - _pause_succeeded = g1h->do_full_collection(false, /* explicit gc */ - true /* clear_all_soft_refs */); - } - } - guarantee(_pause_succeeded, "Elevated collections during the safepoint must always succeed."); - } else { - assert(_result == NULL, "invariant"); - // The only reason for the pause to not be successful is that, the GC locker is - // active (or has become active since the prologue was executed). In this case - // we should retry the pause after waiting for the GC locker to become inactive. - _should_retry_gc = true; - } -}
-

这里可以看到核心的是G1CollectedHeap::do_collection_pause_at_safepoint这个方法,它带上了目标暂停时间的值

-
G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {
-  assert_at_safepoint_on_vm_thread();
-  guarantee(!is_gc_active(), "collection is not reentrant");
-
-  if (GCLocker::check_active_before_gc()) {
-    return false;
-  }
-
-  _gc_timer_stw->register_gc_start();
-
-  GCIdMark gc_id_mark;
-  _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
-
-  SvcGCMarker sgcm(SvcGCMarker::MINOR);
-  ResourceMark rm;
-
-  g1_policy()->note_gc_start();
-
-  wait_for_root_region_scanning();
-
-  print_heap_before_gc();
-  print_heap_regions();
-  trace_heap_before_gc(_gc_tracer_stw);
-
-  _verifier->verify_region_sets_optional();
-  _verifier->verify_dirty_young_regions();
-
-  // We should not be doing initial mark unless the conc mark thread is running
-  if (!_cm_thread->should_terminate()) {
-    // This call will decide whether this pause is an initial-mark
-    // pause. If it is, in_initial_mark_gc() will return true
-    // for the duration of this pause.
-    g1_policy()->decide_on_conc_mark_initiation();
-  }
-
-  // We do not allow initial-mark to be piggy-backed on a mixed GC.
-  assert(!collector_state()->in_initial_mark_gc() ||
-          collector_state()->in_young_only_phase(), "sanity");
-
-  // We also do not allow mixed GCs during marking.
-  assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
-
-  // Record whether this pause is an initial mark. When the current
-  // thread has completed its logging output and it's safe to signal
-  // the CM thread, the flag's value in the policy has been reset.
-  bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
-
-  // Inner scope for scope based logging, timers, and stats collection
-  {
-    EvacuationInfo evacuation_info;
-
-    if (collector_state()->in_initial_mark_gc()) {
-      // We are about to start a marking cycle, so we increment the
-      // full collection counter.
-      increment_old_marking_cycles_started();
-      _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
-    }
-
-    _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
-
-    GCTraceCPUTime tcpu;
-
-    G1HeapVerifier::G1VerifyType verify_type;
-    FormatBuffer<> gc_string("Pause Young ");
-    if (collector_state()->in_initial_mark_gc()) {
-      gc_string.append("(Concurrent Start)");
-      verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
-    } else if (collector_state()->in_young_only_phase()) {
-      if (collector_state()->in_young_gc_before_mixed()) {
-        gc_string.append("(Prepare Mixed)");
-      } else {
-        gc_string.append("(Normal)");
-      }
-      verify_type = G1HeapVerifier::G1VerifyYoungNormal;
-    } else {
-      gc_string.append("(Mixed)");
-      verify_type = G1HeapVerifier::G1VerifyMixed;
-    }
-    GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
-
-    uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
-                                                                  workers()->active_workers(),
-                                                                  Threads::number_of_non_daemon_threads());
-    active_workers = workers()->update_active_workers(active_workers);
-    log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
-
-    TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
-    TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
-                                collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
-
-    G1HeapTransition heap_transition(this);
-    size_t heap_used_bytes_before_gc = used();
-
-    // Don't dynamically change the number of GC threads this early.  A value of
-    // 0 is used to indicate serial work.  When parallel work is done,
-    // it will be set.
-
-    { // Call to jvmpi::post_class_unload_events must occur outside of active GC
-      IsGCActiveMark x;
-
-      gc_prologue(false);
-
-      if (VerifyRememberedSets) {
-        log_info(gc, verify)("[Verifying RemSets before GC]");
-        VerifyRegionRemSetClosure v_cl;
-        heap_region_iterate(&v_cl);
-      }
-
-      _verifier->verify_before_gc(verify_type);
-
-      _verifier->check_bitmaps("GC Start");
-
-#if COMPILER2_OR_JVMCI
-      DerivedPointerTable::clear();
-#endif
-
-      // Please see comment in g1CollectedHeap.hpp and
-      // G1CollectedHeap::ref_processing_init() to see how
-      // reference processing currently works in G1.
-
-      // Enable discovery in the STW reference processor
-      _ref_processor_stw->enable_discovery();
-
-      {
-        // We want to temporarily turn off discovery by the
-        // CM ref processor, if necessary, and turn it back on
-        // on again later if we do. Using a scoped
-        // NoRefDiscovery object will do this.
-        NoRefDiscovery no_cm_discovery(_ref_processor_cm);
-
-        // Forget the current alloc region (we might even choose it to be part
-        // of the collection set!).
-        _allocator->release_mutator_alloc_region();
-
-        // This timing is only used by the ergonomics to handle our pause target.
-        // It is unclear why this should not include the full pause. We will
-        // investigate this in CR 7178365.
-        //
-        // Preserving the old comment here if that helps the investigation:
-        //
-        // The elapsed time induced by the start time below deliberately elides
-        // the possible verification above.
-        double sample_start_time_sec = os::elapsedTime();
-
-        g1_policy()->record_collection_pause_start(sample_start_time_sec);
-
-        if (collector_state()->in_initial_mark_gc()) {
-          concurrent_mark()->pre_initial_mark();
-        }
-
-        g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
-
-        evacuation_info.set_collectionset_regions(collection_set()->region_length());
-
-        // Make sure the remembered sets are up to date. This needs to be
-        // done before register_humongous_regions_with_cset(), because the
-        // remembered sets are used there to choose eager reclaim candidates.
-        // If the remembered sets are not up to date we might miss some
-        // entries that need to be handled.
-        g1_rem_set()->cleanupHRRS();
-
-        register_humongous_regions_with_cset();
-
-        assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
-
-        // We call this after finalize_cset() to
-        // ensure that the CSet has been finalized.
-        _cm->verify_no_cset_oops();
-
-        if (_hr_printer.is_active()) {
-          G1PrintCollectionSetClosure cl(&_hr_printer);
-          _collection_set.iterate(&cl);
-        }
-
-        // Initialize the GC alloc regions.
-        _allocator->init_gc_alloc_regions(evacuation_info);
-
-        G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
-        pre_evacuate_collection_set();
-
-        // Actually do the work...
-        evacuate_collection_set(&per_thread_states);
-
-        post_evacuate_collection_set(evacuation_info, &per_thread_states);
-
-        const size_t* surviving_young_words = per_thread_states.surviving_young_words();
-        free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
-
-        eagerly_reclaim_humongous_regions();
-
-        record_obj_copy_mem_stats();
-        _survivor_evac_stats.adjust_desired_plab_sz();
-        _old_evac_stats.adjust_desired_plab_sz();
-
-        double start = os::elapsedTime();
-        start_new_collection_set();
-        g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
-
-        if (evacuation_failed()) {
-          set_used(recalculate_used());
-          if (_archive_allocator != NULL) {
-            _archive_allocator->clear_used();
-          }
-          for (uint i = 0; i < ParallelGCThreads; i++) {
-            if (_evacuation_failed_info_array[i].has_failed()) {
-              _gc_tracer_stw->report_evacuation_failed(_evacuation_failed_info_array[i]);
-            }
-          }
-        } else {
-          // The "used" of the the collection set have already been subtracted
-          // when they were freed.  Add in the bytes evacuated.
-          increase_used(g1_policy()->bytes_copied_during_gc());
-        }
-
-        if (collector_state()->in_initial_mark_gc()) {
-          // We have to do this before we notify the CM threads that
-          // they can start working to make sure that all the
-          // appropriate initialization is done on the CM object.
-          concurrent_mark()->post_initial_mark();
-          // Note that we don't actually trigger the CM thread at
-          // this point. We do that later when we're sure that
-          // the current thread has completed its logging output.
-        }
-
-        allocate_dummy_regions();
-
-        _allocator->init_mutator_alloc_region();
-
-        {
-          size_t expand_bytes = _heap_sizing_policy->expansion_amount();
-          if (expand_bytes > 0) {
-            size_t bytes_before = capacity();
-            // No need for an ergo logging here,
-            // expansion_amount() does this when it returns a value > 0.
-            double expand_ms;
-            if (!expand(expand_bytes, _workers, &expand_ms)) {
-              // We failed to expand the heap. Cannot do anything about it.
-            }
-            g1_policy()->phase_times()->record_expand_heap_time(expand_ms);
-          }
-        }
-
-        // We redo the verification but now wrt to the new CSet which
-        // has just got initialized after the previous CSet was freed.
-        _cm->verify_no_cset_oops();
-
-        // This timing is only used by the ergonomics to handle our pause target.
-        // It is unclear why this should not include the full pause. We will
-        // investigate this in CR 7178365.
-        double sample_end_time_sec = os::elapsedTime();
-        double pause_time_ms = (sample_end_time_sec - sample_start_time_sec) * MILLIUNITS;
-        size_t total_cards_scanned = g1_policy()->phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanRS, G1GCPhaseTimes::ScanRSScannedCards);
-        g1_policy()->record_collection_pause_end(pause_time_ms, total_cards_scanned, heap_used_bytes_before_gc);
-
-        evacuation_info.set_collectionset_used_before(collection_set()->bytes_used_before());
-        evacuation_info.set_bytes_copied(g1_policy()->bytes_copied_during_gc());
-
-        if (VerifyRememberedSets) {
-          log_info(gc, verify)("[Verifying RemSets after GC]");
-          VerifyRegionRemSetClosure v_cl;
-          heap_region_iterate(&v_cl);
-        }
-
-        _verifier->verify_after_gc(verify_type);
-        _verifier->check_bitmaps("GC End");
-
-        assert(!_ref_processor_stw->discovery_enabled(), "Postcondition");
-        _ref_processor_stw->verify_no_references_recorded();
-
-        // CM reference discovery will be re-enabled if necessary.
-      }
-
-#ifdef TRACESPINNING
-      ParallelTaskTerminator::print_termination_counts();
-#endif
-
-      gc_epilogue(false);
-    }
-
-    // Print the remainder of the GC log output.
-    if (evacuation_failed()) {
-      log_info(gc)("To-space exhausted");
-    }
-
-    g1_policy()->print_phases();
-    heap_transition.print();
-
-    // It is not yet to safe to tell the concurrent mark to
-    // start as we have some optional output below. We don't want the
-    // output from the concurrent mark thread interfering with this
-    // logging output either.
-
-    _hrm.verify_optional();
-    _verifier->verify_region_sets_optional();
-
-    TASKQUEUE_STATS_ONLY(print_taskqueue_stats());
-    TASKQUEUE_STATS_ONLY(reset_taskqueue_stats());
-
-    print_heap_after_gc();
-    print_heap_regions();
-    trace_heap_after_gc(_gc_tracer_stw);
-
-    // We must call G1MonitoringSupport::update_sizes() in the same scoping level
-    // as an active TraceMemoryManagerStats object (i.e. before the destructor for the
-    // TraceMemoryManagerStats is called) so that the G1 memory pools are updated
-    // before any GC notifications are raised.
-    g1mm()->update_sizes();
-
-    _gc_tracer_stw->report_evacuation_info(&evacuation_info);
-    _gc_tracer_stw->report_tenuring_threshold(_g1_policy->tenuring_threshold());
-    _gc_timer_stw->register_gc_end();
-    _gc_tracer_stw->report_gc_end(_gc_timer_stw->gc_end(), _gc_timer_stw->time_partitions());
-  }
-  // It should now be safe to tell the concurrent mark thread to start
-  // without its logging output interfering with the logging output
-  // that came from the pause.
-
-  if (should_start_conc_mark) {
-    // CAUTION: after the doConcurrentMark() call below,
-    // the concurrent marking thread(s) could be running
-    // concurrently with us. Make sure that anything after
-    // this point does not assume that we are the only GC thread
-    // running. Note: of course, the actual marking work will
-    // not start until the safepoint itself is released in
-    // SuspendibleThreadSet::desynchronize().
-    do_concurrent_mark();
-  }
-
-  return true;
-}
-

往下走就是这一步G1Policy::finalize_collection_set,去处理新生代和老年代

-
void G1Policy::finalize_collection_set(double target_pause_time_ms, G1SurvivorRegions* survivor) {
-  double time_remaining_ms = _collection_set->finalize_young_part(target_pause_time_ms, survivor);
-  _collection_set->finalize_old_part(time_remaining_ms);
-}
-

这里分别调用了两个方法,可以看到剩余时间是往下传的,来看一下具体的方法

-
double G1CollectionSet::finalize_young_part(double target_pause_time_ms, G1SurvivorRegions* survivors) {
-  double young_start_time_sec = os::elapsedTime();
-
-  finalize_incremental_building();
-
-  guarantee(target_pause_time_ms > 0.0,
-            "target_pause_time_ms = %1.6lf should be positive", target_pause_time_ms);
-
-  size_t pending_cards = _policy->pending_cards();
-  double base_time_ms = _policy->predict_base_elapsed_time_ms(pending_cards);
-  double time_remaining_ms = MAX2(target_pause_time_ms - base_time_ms, 0.0);
-
-  log_trace(gc, ergo, cset)("Start choosing CSet. pending cards: " SIZE_FORMAT " predicted base time: %1.2fms remaining time: %1.2fms target pause time: %1.2fms",
-                            pending_cards, base_time_ms, time_remaining_ms, target_pause_time_ms);
-
-  // The young list is laid with the survivor regions from the previous
-  // pause are appended to the RHS of the young list, i.e.
-  //   [Newly Young Regions ++ Survivors from last pause].
-
-  uint survivor_region_length = survivors->length();
-  uint eden_region_length = _g1h->eden_regions_count();
-  init_region_lengths(eden_region_length, survivor_region_length);
-
-  verify_young_cset_indices();
-
-  // Clear the fields that point to the survivor list - they are all young now.
-  survivors->convert_to_eden();
-
-  _bytes_used_before = _inc_bytes_used_before;
-  time_remaining_ms = MAX2(time_remaining_ms - _inc_predicted_elapsed_time_ms, 0.0);
-
-  log_trace(gc, ergo, cset)("Add young regions to CSet. eden: %u regions, survivors: %u regions, predicted young region time: %1.2fms, target pause time: %1.2fms",
-                            eden_region_length, survivor_region_length, _inc_predicted_elapsed_time_ms, target_pause_time_ms);
-
-  // The number of recorded young regions is the incremental
-  // collection set's current size
-  set_recorded_rs_lengths(_inc_recorded_rs_lengths);
-
-  double young_end_time_sec = os::elapsedTime();
-  phase_times()->record_young_cset_choice_time_ms((young_end_time_sec - young_start_time_sec) * 1000.0);
-
-  return time_remaining_ms;
-}
-

下面是老年代的部分

-
void G1CollectionSet::finalize_old_part(double time_remaining_ms) {
-  double non_young_start_time_sec = os::elapsedTime();
-  double predicted_old_time_ms = 0.0;
-
-  if (collector_state()->in_mixed_phase()) {
-    cset_chooser()->verify();
-    const uint min_old_cset_length = _policy->calc_min_old_cset_length();
-    const uint max_old_cset_length = _policy->calc_max_old_cset_length();
-
-    uint expensive_region_num = 0;
-    bool check_time_remaining = _policy->adaptive_young_list_length();
-
-    HeapRegion* hr = cset_chooser()->peek();
-    while (hr != NULL) {
-      if (old_region_length() >= max_old_cset_length) {
-        // Added maximum number of old regions to the CSet.
-        log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached max). old %u regions, max %u regions",
-                                  old_region_length(), max_old_cset_length);
-        break;
-      }
-
-      // Stop adding regions if the remaining reclaimable space is
-      // not above G1HeapWastePercent.
-      size_t reclaimable_bytes = cset_chooser()->remaining_reclaimable_bytes();
-      double reclaimable_percent = _policy->reclaimable_bytes_percent(reclaimable_bytes);
-      double threshold = (double) G1HeapWastePercent;
-      if (reclaimable_percent <= threshold) {
-        // We've added enough old regions that the amount of uncollected
-        // reclaimable space is at or below the waste threshold. Stop
-        // adding old regions to the CSet.
-        log_debug(gc, ergo, cset)("Finish adding old regions to CSet (reclaimable percentage not over threshold). "
-                                  "old %u regions, max %u regions, reclaimable: " SIZE_FORMAT "B (%1.2f%%) threshold: " UINTX_FORMAT "%%",
-                                  old_region_length(), max_old_cset_length, reclaimable_bytes, reclaimable_percent, G1HeapWastePercent);
-        break;
-      }
-
-      double predicted_time_ms = predict_region_elapsed_time_ms(hr);
-      if (check_time_remaining) {
-        if (predicted_time_ms > time_remaining_ms) {
-          // Too expensive for the current CSet.
-
-          if (old_region_length() >= min_old_cset_length) {
-            // We have added the minimum number of old regions to the CSet,
-            // we are done with this CSet.
-            log_debug(gc, ergo, cset)("Finish adding old regions to CSet (predicted time is too high). "
-                                      "predicted time: %1.2fms, remaining time: %1.2fms old %u regions, min %u regions",
-                                      predicted_time_ms, time_remaining_ms, old_region_length(), min_old_cset_length);
-            break;
-          }
-
-          // We'll add it anyway given that we haven't reached the
-          // minimum number of old regions.
-          expensive_region_num += 1;
-        }
-      } else {
-        if (old_region_length() >= min_old_cset_length) {
-          // In the non-auto-tuning case, we'll finish adding regions
-          // to the CSet if we reach the minimum.
-
-          log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached min). old %u regions, min %u regions",
-                                    old_region_length(), min_old_cset_length);
-          break;
-        }
-      }
+

结果

+]]> + + Java + leetcode + + + leetcode + java + 题解 + + + + Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析 + /2021/10/31/Leetcode-028-%E5%AE%9E%E7%8E%B0-strStr-Implement-strStr-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Implement strStr().

+

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

+

Clarification:

What should we return when needle is an empty string? This is a great question to ask during an interview.

+

For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C’s strstr() and Java’s indexOf().

+

示例

Example 1:

+
Input: haystack = "hello", needle = "ll"
+Output: 2
+

Example 2:

+
Input: haystack = "aaaaa", needle = "bba"
+Output: -1
+

Example 3:

+
Input: haystack = "", needle = ""
+Output: 0
- // We will add this region to the CSet. - time_remaining_ms = MAX2(time_remaining_ms - predicted_time_ms, 0.0); - predicted_old_time_ms += predicted_time_ms; - cset_chooser()->pop(); // already have region via peek() - _g1h->old_set_remove(hr); - add_old_region(hr); +

题解

字符串比较其实是写代码里永恒的主题,底层的编译器等处理肯定需要字符串对比,像 kmp 算法也是很厉害

+

code

public int strStr(String haystack, String needle) {
+        // 如果两个字符串都为空,返回 -1
+        if (haystack == null || needle == null) {
+            return -1;
+        }
+        // 如果 haystack 长度小于 needle 长度,返回 -1
+        if (haystack.length() < needle.length()) {
+            return -1;
+        }
+        // 如果 needle 为空字符串,返回 0
+        if (needle.equals("")) {
+            return 0;
+        }
+        // 如果两者相等,返回 0
+        if (haystack.equals(needle)) {
+            return 0;
+        }
+        int needleLength = needle.length();
+        int haystackLength = haystack.length();
+        for (int i = needleLength - 1; i <= haystackLength - 1; i++) {
+            // 比较 needle 最后一个字符,倒着比较稍微节省点时间
+            if (needle.charAt(needleLength - 1) == haystack.charAt(i)) {
+                // 如果needle 是 1 的话直接可以返回 i 作为位置了
+                if (needle.length() == 1) {
+                    return i;
+                }
+                boolean flag = true;
+                // 原来比的是 needle 的最后一个位置,然后这边从倒数第二个位置开始
+                int j = needle.length() - 2;
+                for (; j >= 0; j--) {
+                    // 这里的 i- (needleLength - j) + 1 ) 比较绕,其实是外循环的 i 表示当前 i 位置的字符跟 needle 最后一个字符
+                    // 相同,j 在上面的循环中--,对应的 haystack 也要在 i 这个位置 -- ,对应的位置就是 i - (needleLength - j) + 1
+                    if (needle.charAt(j) != haystack.charAt(i - (needleLength - j) + 1)) {
+                        flag = false;
+                        break;
+                    }
+                }
+                // 循环完了之后,如果 flag 为 true 说明 从 i 开始倒着对比都相同,但是这里需要起始位置,就需要
+                // i - needleLength + 1
+                if (flag) {
+                    return i - needleLength + 1;
+                }
+            }
+        }
+        // 这里表示未找到
+        return  -1;
+    }
]]>
+ + Java + leetcode + + + leetcode + java + 题解 + +
+ + Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析 + /2021/11/28/Leetcode-053-%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C-Maximum-Subarray-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

+

A subarray is a contiguous part of an array.

+

示例

Example 1:

+
+

Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

+
+

Example 2:

+
+

Input: nums = [1]
Output: 1

+
+

Example 3:

+
+

Input: nums = [5,4,-1,7,8]
Output: 23

+
+

说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。

+

代码

public int maxSubArray(int[] nums) {
+        int max = nums[0];
+        int sum = nums[0];
+        for (int i = 1; i < nums.length; i++) {
+            // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
+            sum = Math.max(nums[i], sum + nums[i]);
+            // max 是用来承载最大值的
+            max = Math.max(max, sum);
+        }
+        return max;
+    }
]]>
+ + Java + leetcode + + + leetcode + java + 题解 + +
+ + Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 + /2020/10/25/Leetcode-104-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6-Maximum-Depth-of-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

给定一个二叉树,找出其最大深度。

+

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

+

说明: 叶子节点是指没有子节点的节点。

+

示例:
给定二叉树 [3,9,20,null,null,15,7],

+
  3
+ / \
+9  20
+  /  \
+ 15   7
+

返回它的最大深度 3 。

+

代码

// 主体是个递归的应用
+public int maxDepth(TreeNode root) {
+    // 节点的退出条件之一
+    if (root == null) {
+        return 0;
+    }
+    int left = 0;
+    int right = 0;
+    // 存在左子树,就递归左子树
+    if (root.left != null) {
+        left = maxDepth(root.left);
+    }
+    // 存在右子树,就递归右子树
+    if (root.right != null) {
+        right = maxDepth(root.right);
+    }
+    // 前面返回后,左右取大者
+    return Math.max(left + 1, right + 1);
+}
+

分析

其实对于树这类题,一般是以递归形式比较方便,只是要注意退出条件

+]]>
+ + Java + leetcode + Binary Tree + java + Binary Tree + DFS + + + leetcode + java + Binary Tree + DFS + 二叉树 + 题解 + +
+ + Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析 + /2020/12/13/Leetcode-105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Given preorder and inorder traversal of a tree, construct the binary tree.
给定一棵树的前序和中序遍历,构造出一棵二叉树

+

注意

You may assume that duplicates do not exist in the tree.
你可以假设树中没有重复的元素。(PS: 不然就没法做了呀)

+

例子:

preorder = [3,9,20,15,7]
+inorder = [9,3,15,20,7]
+

返回的二叉树

+
  3
+ / \
+9  20
+  /  \
+ 15   7
- hr = cset_chooser()->peek(); - } - if (hr == NULL) { - log_debug(gc, ergo, cset)("Finish adding old regions to CSet (candidate old regions not available)"); - } - if (expensive_region_num > 0) { - // We print the information once here at the end, predicated on - // whether we added any apparently expensive regions or not, to - // avoid generating output per region. - log_debug(gc, ergo, cset)("Added expensive regions to CSet (old CSet region num not reached min)." - "old: %u regions, expensive: %u regions, min: %u regions, remaining time: %1.2fms", - old_region_length(), expensive_region_num, min_old_cset_length, time_remaining_ms); - } +

简要分析

看到这个题可以想到一个比较常规的解法就是递归拆树,前序就是根左右,中序就是左根右,然后就是通过前序已经确定的根在中序中找到,然后去划分左右子树,这个例子里是 3,找到中序中的位置,那么就可以确定,9 是左子树,15,20,7是右子树,然后对应的可以根据左右子树的元素数量在前序中划分左右子树,再继续递归就行

+
class Solution {
+    public TreeNode buildTree(int[] preorder, int[] inorder) {
+      // 获取下数组长度
+        int n = preorder.length;
+        // 排除一下异常和边界
+        if (n != inorder.length) {
+            return null;
+        }
+        if (n == 0) {
+            return null;
+        }
+        if (n == 1) {
+            return new TreeNode(preorder[0]);
+        }
+        // 获得根节点
+        TreeNode node = new TreeNode(preorder[0]);
+        int pos = 0;
+        // 找到中序中的位置
+        for (int i = 0; i < inorder.length; i++) {
+            if (node.val == inorder[i]) {
+                pos = i;
+                break;
+            }
+        }
+        // 划分左右再进行递归,注意下`Arrays.copyOfRange`的用法
+        node.left = buildTree(Arrays.copyOfRange(preorder, 1, pos + 1), Arrays.copyOfRange(inorder, 0, pos));
+        node.right = buildTree(Arrays.copyOfRange(preorder, pos + 1, n), Arrays.copyOfRange(inorder, pos + 1, n));
+        return node;
+    }
+}
]]>
+ + Java + leetcode + Binary Tree + java + Binary Tree + DFS + + + leetcode + java + Binary Tree + 二叉树 + 题解 + 递归 + Preorder Traversal + Inorder Traversal + 前序 + 中序 + +
+ + Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析 + /2021/03/14/Leetcode-121-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-Best-Time-to-Buy-and-Sell-Stock-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

You are given an array prices where prices[i] is the price of a given stock on the ith day.

+

You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.

+

Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

+

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

+

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

+

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

+

简单分析

其实这个跟二叉树的最长路径和有点类似,需要找到整体的最大收益,但是在迭代过程中需要一个当前的值

+
int maxSofar = 0;
+public int maxProfit(int[] prices) {
+    if (prices.length <= 1) {
+        return 0;
+    }
+    int maxIn = prices[0];
+    int maxOut = prices[0];
+    for (int i = 1; i < prices.length; i++) {
+        if (maxIn > prices[i]) {
+            // 当循环当前值小于之前的买入值时就当成买入值,同时卖出也要更新
+            maxIn = prices[i];
+            maxOut = prices[i];
+        }
+        if (prices[i] > maxOut) {
+            // 表示一个可卖出点,即比买入值高时
+            maxOut = prices[i];
+            // 需要设置一个历史值
+            maxSofar = Math.max(maxSofar, maxOut - maxIn);
+        }
+    }
+    return maxSofar;
+}
- cset_chooser()->verify(); - } +

总结下

一开始看到 easy 就觉得是很简单,就没有 maxSofar ,但是一提交就出现问题了
对于[2, 4, 1]这种就会变成 0,所以还是需要一个历史值来存放历史最大值,这题有点动态规划的意思

+]]>
+ + Java + leetcode + java + DP + DP + + + leetcode + java + 题解 + DP + +
+ + Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析 + /2022/05/01/Leetcode-1115-%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0-FooBar-Print-FooBar-Alternately-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 无聊想去 roll 一题就看到了有并发题,就找到了这题,其实一眼看我的想法也是用信号量,但是用 condition 应该也是可以处理的,不过这类问题好像本地有点难调,因为它好像是抽取代码执行的,跟直观的逻辑比较不一样
Suppose you are given the following code:

+
class FooBar {
+  public void foo() {
+    for (int i = 0; i < n; i++) {
+      print("foo");
+    }
+  }
 
-  stop_incremental_building();
+  public void bar() {
+    for (int i = 0; i < n; i++) {
+      print("bar");
+    }
+  }
+}
+

The same instance of FooBar will be passed to two different threads:

+
    +
  • thread A will call foo(), while
  • +
  • thread B will call bar().
    Modify the given program to output "foobar" n times.
  • +
+

示例

Example 1:

+

Input: n = 1
Output: “foobar”
Explanation: There are two threads being fired asynchronously. One of them calls foo(), while the other calls bar().
“foobar” is being output 1 time.

+
+

Example 2:

+

Input: n = 2
Output: “foobarfoobar”
Explanation: “foobar” is being output 2 times.

+
+

题解

简析

其实用信号量是很直观的,就是让打印 foo 的线程先拥有信号量,打印后就等待,给 bar 信号量 + 1,然后 bar 线程运行打印消耗 bar 信号量,再给 foo 信号量 + 1

+

code

class FooBar {
+    
+    private final Semaphore foo = new Semaphore(1);
+    private final Semaphore bar = new Semaphore(0);
+    private int n;
 
-  log_debug(gc, ergo, cset)("Finish choosing CSet. old: %u regions, predicted old region time: %1.2fms, time remaining: %1.2f",
-                            old_region_length(), predicted_old_time_ms, time_remaining_ms);
+    public FooBar(int n) {
+        this.n = n;
+    }
 
-  double non_young_end_time_sec = os::elapsedTime();
-  phase_times()->record_non_young_cset_choice_time_ms((non_young_end_time_sec - non_young_start_time_sec) * 1000.0);
+    public void foo(Runnable printFoo) throws InterruptedException {
+        
+        for (int i = 0; i < n; i++) {
+            foo.acquire();
+        	// printFoo.run() outputs "foo". Do not change or remove this line.
+        	printFoo.run();
+            bar.release();
+        }
+    }
 
-  QuickSort::sort(_collection_set_regions, _collection_set_cur_length, compare_region_idx, true);
-}
-

上面第三行是个判断,当前是否是 mixed 回收阶段,如果不是的话其实是没有老年代什么事的,所以可以看到代码基本是从这个 if 判断
if (collector_state()->in_mixed_phase()) {开始往下走的
先写到这,偏向于做笔记用,有错轻拍

+ public void bar(Runnable printBar) throws InterruptedException { + + for (int i = 0; i < n; i++) { + bar.acquire(); + // printBar.run() outputs "bar". Do not change or remove this line. + printBar.run(); + foo.release(); + } + } +}
]]>
Java - JVM - GC - C++ + leetcode - Java - JVM - C++ + leetcode + java + 题解 + Print FooBar Alternately
- Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析 - /2021/10/07/Leetcode-021-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8-Merge-Two-Sorted-Lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.

-

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

-

示例 1

-
-

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

-
-

示例 2

-

输入: l1 = [], l2 = []
输出: []

-
-

示例 3

-

输入: l1 = [], l2 = [0]
输出: [0]

-
-

简要分析

这题是 Easy 的,看着也挺简单,两个链表进行合并,就是比较下大小,可能将就点的话最好就在两个链表中原地合并

-

题解代码

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
-        // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
-        if (l1 == null) {
-            return l2;
+    Disruptor 系列三
+    /2022/09/25/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%89/
+    原来一直有点被误导,
gatingSequences用来标识每个 processer 的操作位点,但是怎么记录更新有点搞不清楚
其实问题在于 gatingSequences 是个 Sequence 数组,首先要看下怎么加进去的,
可以看到是在 com.lmax.disruptor.RingBuffer#addGatingSequences 这个方法里添加
首先是 com.lmax.disruptor.dsl.Disruptor#handleEventsWith(com.lmax.disruptor.EventHandler<? super T>...)
然后执行 com.lmax.disruptor.dsl.Disruptor#createEventProcessors(com.lmax.disruptor.Sequence[], com.lmax.disruptor.EventHandler<? super T>[])

+
EventHandlerGroup<T> createEventProcessors(
+        final Sequence[] barrierSequences,
+        final EventHandler<? super T>[] eventHandlers)
+    {
+        checkNotStarted();
+
+        final Sequence[] processorSequences = new Sequence[eventHandlers.length];
+        final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);
+
+        for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
+        {
+            final EventHandler<? super T> eventHandler = eventHandlers[i];
+
+            // 这里将 handler 包装成一个 BatchEventProcessor
+            final BatchEventProcessor<T> batchEventProcessor =
+                new BatchEventProcessor<>(ringBuffer, barrier, eventHandler);
+
+            if (exceptionHandler != null)
+            {
+                batchEventProcessor.setExceptionHandler(exceptionHandler);
+            }
+
+            consumerRepository.add(batchEventProcessor, eventHandler, barrier);
+            processorSequences[i] = batchEventProcessor.getSequence();
         }
-        if (l2 == null) {
-            return l1;
+
+        updateGatingSequencesForNextInChain(barrierSequences, processorSequences);
+
+        return new EventHandlerGroup<>(this, consumerRepository, processorSequences);
+    }
+ +

BatchEventProcessor 在类内有个定义 sequence

+
private final Sequence sequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE);
+

然后在上面循环中的这一句取出来

+
processorSequences[i] = batchEventProcessor.getSequence();
+

调用com.lmax.disruptor.dsl.Disruptor#updateGatingSequencesForNextInChain 方法

+
private void updateGatingSequencesForNextInChain(final Sequence[] barrierSequences, final Sequence[] processorSequences)
+    {
+        if (processorSequences.length > 0)
+        {
+            // 然后在这里添加
+            ringBuffer.addGatingSequences(processorSequences);
+            for (final Sequence barrierSequence : barrierSequences)
+            {
+                ringBuffer.removeGatingSequence(barrierSequence);
+            }
+            consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
         }
-        // new 一个合并后的头结点
-        ListNode merged = new ListNode();
-        // 这个是当前节点
-        ListNode current = merged;
-        // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
-        // 因为内部前两个if就是跳出条件
-        while (true) {
-            if (l1 == null) {
-                // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
-                // 所以不能是直接current = l2,这样就是把后面的直接丢了
-                current.val = l2.val;
-                current.next = l2.next;
-                break;
+    }
+ +

而如何更新则是在处理器 com.lmax.disruptor.BatchEventProcessor#run

+
public void run()
+    {
+        if (running.compareAndSet(IDLE, RUNNING))
+        {
+            sequenceBarrier.clearAlert();
+
+            notifyStart();
+            try
+            {
+                if (running.get() == RUNNING)
+                {
+                    processEvents();
+                }
             }
-            if (l2 == null) {
-                current.val = l1.val;
-                current.next = l1.next;
-                break;
+            finally
+            {
+                notifyShutdown();
+                running.set(IDLE);
+            }
+        }
+        else
+        {
+            // This is a little bit of guess work.  The running state could of changed to HALTED by
+            // this point.  However, Java does not have compareAndExchange which is the only way
+            // to get it exactly correct.
+            if (running.get() == RUNNING)
+            {
+                throw new IllegalStateException("Thread is already running");
+            }
+            else
+            {
+                earlyExit();
+            }
+        }
+    }
+

然后是

+
private void processEvents()
+    {
+        T event = null;
+        long nextSequence = sequence.get() + 1L;
+
+        while (true)
+        {
+            try
+            {
+                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
+                if (batchStartAware != null)
+                {
+                    batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
+                }
+
+                while (nextSequence <= availableSequence)
+                {
+                    event = dataProvider.get(nextSequence);
+                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
+                    nextSequence++;
+                }
+                // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
+                sequence.set(availableSequence);
+            }
+            catch (final TimeoutException e)
+            {
+                notifyTimeout(sequence.get());
+            }
+            catch (final AlertException ex)
+            {
+                if (running.get() != RUNNING)
+                {
+                    break;
+                }
             }
-            // 这里是两个链表都不为空的时候,就比较下大小
-            if (l1.val < l2.val) {
-                current.val = l1.val;
-                l1 = l1.next;
-            } else {
-                current.val = l2.val;
-                l2 = l2.next;
+            catch (final Throwable ex)
+            {
+                handleEventException(ex, nextSequence, event);
+                // 如果是异常就只是 nextSequence
+                sequence.set(nextSequence);
+                nextSequence++;
             }
-            // 这里是new个新的,其实也可以放在循环头上
-            current.next = new ListNode();
-            current = current.next;
         }
-        current = null;
-        // 返回这个头结点
-        return merged;
-    }
+ }
+]]>
+ + Java + + + Java + Disruptor + +
+ + Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析 + /2021/01/24/Leetcode-124-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C-Binary-Tree-Maximum-Path-Sum-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root.

+

The path sum of a path is the sum of the node’s values in the path.

+

Given the root of a binary tree, return the maximum path sum of any path.

+

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径 至少包含一个 节点,且不一定经过根节点。

+

路径和 是路径中各节点值的总和。

+

给你一个二叉树的根节点 root ,返回其 最大路径和

+

简要分析

其实这个题目会被误解成比较简单,左子树最大的,或者右子树最大的,或者两边加一下,仔细想想都不对,其实有可能是产生于左子树中,或者右子树中,这两个都是指跟左子树根还有右子树根没关系的,这么说感觉不太容易理解,画个图

可以看到图里,其实最长路径和是左边这个子树组成的,跟根节点还有右子树完全没关系,然后再想一种情况,如果是整棵树就是图中的左子树,那么这个最长路径和就是左子树加右子树加根节点了,所以不是我一开始想得那么简单,在代码实现中也需要一些技巧

+

代码

int ansNew = Integer.MIN_VALUE;
+public int maxPathSum(TreeNode root) {
+        maxSumNew(root);
+        return ansNew;
+    }
+    
+public int maxSumNew(TreeNode root) {
+    if (root == null) {
+        return 0;
+    }
+    // 这里是个简单的递归,就是去递归左右子树,但是这里其实有个概念,当这样处理时,其实相当于把子树的内部的最大路径和已经算出来了
+    int left = maxSumNew(root.left);
+    int right = maxSumNew(root.right);
+    // 这里前面我有点没想明白,但是看到 ansNew 的比较,其实相当于,返回的是三种情况里的最大值,一个是左子树+根,一个是右子树+根,一个是单独根节点,
+    // 这样这个递归的返回才会有意义,不然像原来的方法,它可能是跳着的,但是这种情况其实是借助于 ansNew 这个全局的最大值,因为原来我觉得要比较的是
+    // left, right, left + root , right + root, root, left + right + root 这些的最大值,这里是分成了两个阶段,left 跟 right 的最大值已经在上面的
+    // 调用过程中赋值给 ansNew 了    
+    int currentSum = Math.max(Math.max(root.val + left , root.val + right), root.val);
+    // 这边返回的是 currentSum,然后再用它跟 left + right + root 进行对比,然后再去更新 ans
+    // PS: 有个小点也是这边的破局点,就是这个 ansNew
+    int res = Math.max(left + right + root.val, currentSum);
+    ans = Math.max(res, ans);
+    return currentSum;
+}
-

结果

+

这里非常重要的就是 ansNew 是最后的一个结果,而对于 maxSumNew 这个函数的返回值其实是需要包含了一个连续结果,因为要返回继续去算路径和,所以返回的是 currentSum,最终结果是 ansNew

+

结果图

难得有个 100%,贴个图哈哈

]]>
Java leetcode + Binary Tree + java + Binary Tree leetcode java + Binary Tree + 二叉树 题解
- Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析 - /2021/10/31/Leetcode-028-%E5%AE%9E%E7%8E%B0-strStr-Implement-strStr-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

Implement strStr().

-

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

-

Clarification:

What should we return when needle is an empty string? This is a great question to ask during an interview.

-

For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C’s strstr() and Java’s indexOf().

-

示例

Example 1:

-
Input: haystack = "hello", needle = "ll"
-Output: 2
-

Example 2:

-
Input: haystack = "aaaaa", needle = "bba"
-Output: -1
-

Example 3:

-
Input: haystack = "", needle = ""
-Output: 0
+ Leetcode 155 最小栈(Min Stack) 题解分析 + /2020/12/06/Leetcode-155-%E6%9C%80%E5%B0%8F%E6%A0%88-Min-Stack-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
设计一个栈,支持压栈,出站,获取栈顶元素,通过常数级复杂度获取栈中的最小元素

+
    +
  • push(x) – Push element x onto stack.
  • +
  • pop() – Removes the element on top of the stack.
  • +
  • top() – Get the top element.
  • +
  • getMin() – Retrieve the minimum element in the stack.
  • +
+

示例

Example 1:

+
Input
+["MinStack","push","push","push","getMin","pop","top","getMin"]
+[[],[-2],[0],[-3],[],[],[],[]]
+
+Output
+[null,null,null,null,-3,null,0,-2]
+
+Explanation
+MinStack minStack = new MinStack();
+minStack.push(-2);
+minStack.push(0);
+minStack.push(-3);
+minStack.getMin(); // return -3
+minStack.pop();
+minStack.top();    // return 0
+minStack.getMin(); // return -2
+ +

简要分析

其实现在大部分语言都自带类栈的数据结构,Java 也自带 stack 这个数据结构,所以这个题的主要难点的就是常数级的获取最小元素,最开始的想法是就一个栈外加一个记录最小值的变量就行了,但是仔细一想是不行的,因为随着元素被 pop 出去,这个最小值也可能需要梗着变化,就不太好判断了,所以后面是用了一个辅助栈。

+

代码

class MinStack {
+        // 这个作为主栈
+        Stack<Integer> s1 = new Stack<>();
+        // 这个作为辅助栈,放最小值的栈
+        Stack<Integer> s2 = new Stack<>();
+        /** initialize your data structure here. */
+        public MinStack() {
 
-

题解

字符串比较其实是写代码里永恒的主题,底层的编译器等处理肯定需要字符串对比,像 kmp 算法也是很厉害

-

code

public int strStr(String haystack, String needle) {
-        // 如果两个字符串都为空,返回 -1
-        if (haystack == null || needle == null) {
-            return -1;
         }
-        // 如果 haystack 长度小于 needle 长度,返回 -1
-        if (haystack.length() < needle.length()) {
-            return -1;
+
+        public void push(int x) {
+            // 放入主栈
+            s1.push(x);
+            // 当 s2 是空或者当前值是小于"等于" s2 栈顶时,压入辅助最小值的栈
+            // 注意这里的"等于"非常必要,因为当最小值有多个的情况下,也需要压入栈,否则在 pop 的时候就会不对等
+            if (s2.isEmpty() || x <= s2.peek()) {
+                s2.push(x);
+            }
         }
-        // 如果 needle 为空字符串,返回 0
-        if (needle.equals("")) {
-            return 0;
+
+        public void pop() {
+            // 首先就是主栈要 pop,然后就是第二个了,跟上面的"等于"很有关系,
+            // 因为如果有两个最小值,如果前面等于的情况没有压栈,那这边相等的时候 pop 就会少一个了,可能就导致最小值不对了
+            int x = s1.pop();
+            if (x == s2.peek())  {
+                s2.pop();
+            }
         }
-        // 如果两者相等,返回 0
-        if (haystack.equals(needle)) {
-            return 0;
+
+        public int top() {
+            // 栈顶的元素
+            return s1.peek();
         }
-        int needleLength = needle.length();
-        int haystackLength = haystack.length();
-        for (int i = needleLength - 1; i <= haystackLength - 1; i++) {
-            // 比较 needle 最后一个字符,倒着比较稍微节省点时间
-            if (needle.charAt(needleLength - 1) == haystack.charAt(i)) {
-                // 如果needle 是 1 的话直接可以返回 i 作为位置了
-                if (needle.length() == 1) {
-                    return i;
-                }
-                boolean flag = true;
-                // 原来比的是 needle 的最后一个位置,然后这边从倒数第二个位置开始
-                int j = needle.length() - 2;
-                for (; j >= 0; j--) {
-                    // 这里的 i- (needleLength - j) + 1 ) 比较绕,其实是外循环的 i 表示当前 i 位置的字符跟 needle 最后一个字符
-                    // 相同,j 在上面的循环中--,对应的 haystack 也要在 i 这个位置 -- ,对应的位置就是 i - (needleLength - j) + 1
-                    if (needle.charAt(j) != haystack.charAt(i - (needleLength - j) + 1)) {
-                        flag = false;
-                        break;
-                    }
-                }
-                // 循环完了之后,如果 flag 为 true 说明 从 i 开始倒着对比都相同,但是这里需要起始位置,就需要
-                // i - needleLength + 1
-                if (flag) {
-                    return i - needleLength + 1;
-                }
-            }
+
+        public int getMin() {
+            // 辅助最小栈的栈顶
+            return s2.peek();
         }
-        // 这里表示未找到
-        return  -1;
-    }
]]> + }
+ +]]>
Java leetcode + java + stack + stack leetcode java 题解 + stack + min stack + 最小栈 + leetcode 155
- Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析 - /2021/11/28/Leetcode-053-%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C-Maximum-Subarray-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

-

A subarray is a contiguous part of an array.

-

示例

Example 1:

+ Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析 + /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Given a 2D grid of size m x n and an integer k. You need to shift the grid k times.

+

In one shift operation:

+

Element at grid[i][j] moves to grid[i][j + 1].
Element at grid[i][n - 1] moves to grid[i + 1][0].
Element at grid[m - 1][n - 1] moves to grid[0][0].
Return the 2D grid after applying shift operation k times.

+

示例

Example 1:

-

Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

+

Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 1
Output: [[9,1,2],[3,4,5],[6,7,8]]

-

Example 2:

+

Example 2:

-

Input: nums = [1]
Output: 1

+

Input: grid = [[3,8,1,9],[19,7,2,5],[4,6,11,10],[12,0,21,13]], k = 4
Output: [[12,0,21,13],[3,8,1,9],[19,7,2,5],[4,6,11,10]]

-

Example 3:

-
-

Input: nums = [5,4,-1,7,8]
Output: 23

+

Example 3:

+

Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 9
Output: [[1,2,3],[4,5,6],[7,8,9]]

-

说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。

-

代码

public int maxSubArray(int[] nums) {
-        int max = nums[0];
-        int sum = nums[0];
-        for (int i = 1; i < nums.length; i++) {
-            // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
-            sum = Math.max(nums[i], sum + nums[i]);
-            // max 是用来承载最大值的
-            max = Math.max(max, sum);
+

提示

    +
  • m == grid.length
  • +
  • n == grid[i].length
  • +
  • 1 <= m <= 50
  • +
  • 1 <= n <= 50
  • +
  • -1000 <= grid[i][j] <= 1000
  • +
  • 0 <= k <= 100
  • +
+

解析

这个题主要是矩阵或者说数组的操作,并且题目要返回的是个 List,所以也不用原地操作,只需要找对位置就可以了,k 是多少就相当于让这个二维数组头尾衔接移动 k 个元素

+

代码

public List<List<Integer>> shiftGrid(int[][] grid, int k) {
+        // 行数
+        int m = grid.length;
+        // 列数
+        int n = grid[0].length;
+        // 偏移值,取下模
+        k = k % (m * n);
+        // 反向取下数量,因为我打算直接从头填充新的矩阵
+        /*
+         *    比如
+         *    1 2 3
+         *    4 5 6
+         *    7 8 9
+         *    需要变成
+         *    9 1 2
+         *    3 4 5
+         *    6 7 8
+         *    就要从 9 开始填充
+         */
+        int reverseK = m * n - k;
+        List<List<Integer>> matrix = new ArrayList<>();
+        // 这类就是两层循环
+        for (int i = 0; i < m; i++) {
+            List<Integer> line = new ArrayList<>();
+            for (int j = 0; j < n; j++) {
+                // 数量会随着循环迭代增长, 确认是第几个
+                int currentNum = reverseK + i * n +  (j + 1);
+                // 这里处理下到达矩阵末尾后减掉 m * n
+                if (currentNum > m * n) {
+                    currentNum -= m * n;
+                }
+                // 根据矩阵列数 n 算出在原来矩阵的位置
+                int last = (currentNum - 1) % n;
+                int passLine = (currentNum - 1) / n;
+
+                line.add(grid[passLine][last]);
+            }
+            matrix.add(line);
         }
-        return max;
-    }
]]> - - Java - leetcode - - - leetcode - java - 题解 - - - - Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 - /2020/10/25/Leetcode-104-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6-Maximum-Depth-of-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

给定一个二叉树,找出其最大深度。

-

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

-

说明: 叶子节点是指没有子节点的节点。

-

示例:
给定二叉树 [3,9,20,null,null,15,7],

-
  3
- / \
-9  20
-  /  \
- 15   7
-

返回它的最大深度 3 。

-

代码

// 主体是个递归的应用
-public int maxDepth(TreeNode root) {
-    // 节点的退出条件之一
-    if (root == null) {
-        return 0;
-    }
-    int left = 0;
-    int right = 0;
-    // 存在左子树,就递归左子树
-    if (root.left != null) {
-        left = maxDepth(root.left);
-    }
-    // 存在右子树,就递归右子树
-    if (root.right != null) {
-        right = maxDepth(root.right);
-    }
-    // 前面返回后,左右取大者
-    return Math.max(left + 1, right + 1);
-}
-

分析

其实对于树这类题,一般是以递归形式比较方便,只是要注意退出条件

+ return matrix; + }
+ +

结果数据


比较慢

]]> Java leetcode - Binary Tree - java - Binary Tree - DFS leetcode java - Binary Tree - DFS - 二叉树 题解 + Shift 2D Grid - Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析 - /2022/05/01/Leetcode-1115-%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0-FooBar-Print-FooBar-Alternately-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 无聊想去 roll 一题就看到了有并发题,就找到了这题,其实一眼看我的想法也是用信号量,但是用 condition 应该也是可以处理的,不过这类问题好像本地有点难调,因为它好像是抽取代码执行的,跟直观的逻辑比较不一样
Suppose you are given the following code:

-
class FooBar {
-  public void foo() {
-    for (int i = 0; i < n; i++) {
-      print("foo");
-    }
-  }
-
-  public void bar() {
-    for (int i = 0; i < n; i++) {
-      print("bar");
-    }
-  }
-}
-

The same instance of FooBar will be passed to two different threads:

-
    -
  • thread A will call foo(), while
  • -
  • thread B will call bar().
    Modify the given program to output "foobar" n times.
  • -
+ Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析 + /2022/08/06/Leetcode-16-%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C-3Sum-Closest-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Given an integer array nums of length n and an integer target, find three integers in nums such that the sum is closest to target.

+

Return the sum of the three integers.

+

You may assume that each input would have exactly one solution.

+

简单解释下就是之前是要三数之和等于目标值,现在是找到最接近的三数之和。

示例

Example 1:

-

Input: n = 1
Output: “foobar”
Explanation: There are two threads being fired asynchronously. One of them calls foo(), while the other calls bar().
“foobar” is being output 1 time.

+

Input: nums = [-1,2,1,-4], target = 1
Output: 2
Explanation: The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).

Example 2:

-

Input: n = 2
Output: “foobarfoobar”
Explanation: “foobar” is being output 2 times.

+

Input: nums = [0,0,0], target = 1
Output: 0

-

题解

简析

其实用信号量是很直观的,就是让打印 foo 的线程先拥有信号量,打印后就等待,给 bar 信号量 + 1,然后 bar 线程运行打印消耗 bar 信号量,再给 foo 信号量 + 1

-

code

class FooBar {
-    
-    private final Semaphore foo = new Semaphore(1);
-    private final Semaphore bar = new Semaphore(0);
-    private int n;
-
-    public FooBar(int n) {
-        this.n = n;
-    }
-
-    public void foo(Runnable printFoo) throws InterruptedException {
-        
-        for (int i = 0; i < n; i++) {
-            foo.acquire();
-        	// printFoo.run() outputs "foo". Do not change or remove this line.
-        	printFoo.run();
-            bar.release();
+

Constraints:

    +
  • 3 <= nums.length <= 1000
  • +
  • -1000 <= nums[i] <= 1000
  • +
  • -10^4 <= target <= 10^4
  • +
+

简单解析

这个题思路上来讲不难,也是用原来三数之和的方式去做,利用”双指针法”或者其它描述法,但是需要简化逻辑

+

code

public int threeSumClosest(int[] nums, int target) {
+        Arrays.sort(nums);
+        // 当前最近的和
+        int closestSum = nums[0] + nums[1] + nums[nums.length - 1];
+        for (int i = 0; i < nums.length - 2; i++) {
+            if (i == 0 || nums[i] != nums[i - 1]) {
+                // 左指针
+                int left = i + 1;
+                // 右指针
+                int right = nums.length - 1;
+                // 判断是否遍历完了
+                while (left < right) {
+                    // 当前的和
+                    int sum = nums[i] + nums[left] + nums[right];
+                    // 小优化,相等就略过了
+                    while (left < right && nums[left] == nums[left + 1]) {
+                        left++;
+                    }
+                    while (left < right && nums[right] == nums[right - 1]) {
+                        right--;
+                    }
+                    // 这里判断,其实也还是希望趋近目标值
+                    if (sum < target) {
+                        left++;
+                    } else {
+                        right--;
+                    }
+                    // 判断是否需要替换
+                    if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
+                        closestSum = sum;
+                    }
+                }
+            }
         }
-    }
+        return closestSum;
+    }
- public void bar(Runnable printBar) throws InterruptedException { - - for (int i = 0; i < n; i++) { - bar.acquire(); - // printBar.run() outputs "bar". Do not change or remove this line. - printBar.run(); - foo.release(); - } - } -}
+

结果

]]>
Java @@ -3311,407 +3166,427 @@ Output: 0
- Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析 - /2020/12/13/Leetcode-105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

Given preorder and inorder traversal of a tree, construct the binary tree.
给定一棵树的前序和中序遍历,构造出一棵二叉树

-

注意

You may assume that duplicates do not exist in the tree.
你可以假设树中没有重复的元素。(PS: 不然就没法做了呀)

-

例子:

preorder = [3,9,20,15,7]
-inorder = [9,3,15,20,7]
-

返回的二叉树

-
  3
- / \
-9  20
-  /  \
- 15   7
- - -

简要分析

看到这个题可以想到一个比较常规的解法就是递归拆树,前序就是根左右,中序就是左根右,然后就是通过前序已经确定的根在中序中找到,然后去划分左右子树,这个例子里是 3,找到中序中的位置,那么就可以确定,9 是左子树,15,20,7是右子树,然后对应的可以根据左右子树的元素数量在前序中划分左右子树,再继续递归就行

-
class Solution {
-    public TreeNode buildTree(int[] preorder, int[] inorder) {
-      // 获取下数组长度
-        int n = preorder.length;
-        // 排除一下异常和边界
-        if (n != inorder.length) {
+    Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析
+    /2021/01/10/Leetcode-160-%E7%9B%B8%E4%BA%A4%E9%93%BE%E8%A1%A8-intersection-of-two-linked-lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

写一个程序找出两个单向链表的交叉起始点,可能是我英语不好,图里画的其实还有一点是交叉以后所有节点都是相同的
Write a program to find the node at which the intersection of two singly linked lists begins.

+

For example, the following two linked lists:

begin to intersect at node c1.

+

Example 1:

+
Input: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
+Output: Reference of the node with value = 8
+Input Explanation: The intersected node's value is 8 (note that this must not be 0 if the two lists intersect). From the head of A, it reads as [4,1,8,4,5]. From the head of B, it reads as [5,6,1,8,4,5]. There are 2 nodes before the intersected node in A; There are 3 nodes before the intersected node in B.
+

分析题解

一开始没什么头绪,感觉只能最原始的遍历,后来看了一些文章,发现比较简单的方式就是先找两个链表的长度差,因为从相交点开始肯定是长度一致的,这是个很好的解题突破口,找到长度差以后就是先跳过长链表的较长部分,然后开始同步遍历比较 A,B 链表;

+

代码

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
+        if (headA == null || headB == null) {
             return null;
         }
-        if (n == 0) {
-            return null;
+        // 算 A 的长度
+        int countA = 0;
+        ListNode tailA = headA;
+        while (tailA != null) {
+            tailA = tailA.next;
+            countA++;
         }
-        if (n == 1) {
-            return new TreeNode(preorder[0]);
+        // 算 B 的长度
+        int countB = 0;
+        ListNode tailB = headB;
+        while (tailB != null) {
+            tailB = tailB.next;
+            countB++;
         }
-        // 获得根节点
-        TreeNode node = new TreeNode(preorder[0]);
-        int pos = 0;
-        // 找到中序中的位置
-        for (int i = 0; i < inorder.length; i++) {
-            if (node.val == inorder[i]) {
-                pos = i;
-                break;
+        tailA = headA;
+        tailB = headB;
+        // 依据长度差,先让长的链表 tail 指针往后移
+        if (countA > countB) {
+            while (countA > countB) {
+                tailA = tailA.next;
+                countA--;
+            }
+        } else if (countA < countB) {
+            while (countA < countB) {
+                tailB = tailB.next;
+                countB--;
+            }
+        }
+        // 然后以相同速度遍历两个链表比较
+        while (tailA != null) {
+            if (tailA == tailB) {
+                return tailA;
+            } else {
+                tailA = tailA.next;
+                tailB = tailB.next;
             }
         }
-        // 划分左右再进行递归,注意下`Arrays.copyOfRange`的用法
-        node.left = buildTree(Arrays.copyOfRange(preorder, 1, pos + 1), Arrays.copyOfRange(inorder, 0, pos));
-        node.right = buildTree(Arrays.copyOfRange(preorder, pos + 1, n), Arrays.copyOfRange(inorder, pos + 1, n));
-        return node;
-    }
-}
]]>
+ return null; + }
+

总结

可能缺少这种思维,做的还是比较少,所以没法一下子反应过来,需要锻炼,我的第一反应是两重遍历,不过那样复杂度就高了,这里应该是只有 O(N) 的复杂度。

+]]>
Java leetcode - Binary Tree + Linked List java - Binary Tree - DFS + Linked List leetcode java - Binary Tree - 二叉树 题解 - 递归 - Preorder Traversal - Inorder Traversal - 前序 - 中序 + Linked List
- Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析 - /2021/03/14/Leetcode-121-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-Best-Time-to-Buy-and-Sell-Stock-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

You are given an array prices where prices[i] is the price of a given stock on the ith day.

-

You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.

-

Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

-

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

-

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

-

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

-

简单分析

其实这个跟二叉树的最长路径和有点类似,需要找到整体的最大收益,但是在迭代过程中需要一个当前的值

-
int maxSofar = 0;
-public int maxProfit(int[] prices) {
-    if (prices.length <= 1) {
-        return 0;
+    Leetcode 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析
+    /2022/09/11/Leetcode-1862-%E5%90%91%E4%B8%8B%E5%8F%96%E6%95%B4%E6%95%B0%E5%AF%B9%E5%92%8C-Sum-of-Floored-Pairs-Hard-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

Given an integer array nums, return the sum of floor(nums[i] / nums[j]) for all pairs of indices 0 <= i, j < nums.length in the array. Since the answer may be too large, return it modulo 10^9 + 7.

+

The floor() function returns the integer part of the division.

+

对应中文
给你一个整数数组 nums ,请你返回所有下标对 0 <= i, j < nums.length 的 floor(nums[i] / nums[j]) 结果之和。由于答案可能会很大,请你返回答案对10^9 + 7 取余 的结果。

+

函数 floor() 返回输入数字的整数部分。

+

示例

Example 1:

+

Input: nums = [2,5,9]
Output: 10
Explanation:
floor(2 / 5) = floor(2 / 9) = floor(5 / 9) = 0
floor(2 / 2) = floor(5 / 5) = floor(9 / 9) = 1
floor(5 / 2) = 2
floor(9 / 2) = 4
floor(9 / 5) = 1
We calculate the floor of the division for every pair of indices in the array then sum them up.

+
+

Example 2:

+

Input: nums = [7,7,7,7,7,7,7]
Output: 49

+
+

Constraints:

    +
  • 1 <= nums.length <= 10^5
  • +
  • 1 <= nums[i] <= 10^5
  • +
+

简析

这题不愧是 hard,要不是看了讨论区的一个大神的解答感觉从头做得想好久,
主要是两点,对于任何一个在里面的数,随便举个例子是 k,最简单的就是循环所有数对 k 除一下,
这样效率会很低,那么对于 k 有什么规律呢,就是对于所有小于 k 的数,往下取整都是 0,所以不用考虑,
对于所有大于 k 的数我们可以分成一个个的区间,[k,2k-1),[2k,3k-1),[3k,4k-1)……对于这些区间的
除了 k 往下取整,每个区间内的都是一样的,所以可以简化为对于任意一个 k,我只要知道与k 相同的有多少个,然后比 k 大的各个区间各有多少个数就可以了

+

代码

static final int MAXE5 = 100_000;
+
+static final int MODULUSE9 = 1_000_000_000 + 7;
+
+public int sumOfFlooredPairs(int[] nums) {
+    int[] counts = new int[MAXE5+1];
+    for (int num : nums) {
+        counts[num]++;
     }
-    int maxIn = prices[0];
-    int maxOut = prices[0];
-    for (int i = 1; i < prices.length; i++) {
-        if (maxIn > prices[i]) {
-            // 当循环当前值小于之前的买入值时就当成买入值,同时卖出也要更新
-            maxIn = prices[i];
-            maxOut = prices[i];
+    // 这里就是很巧妙的给后一个加上前一个的值,这样其实前后任意两者之差就是这中间的元素数量
+    for (int i = 1; i <= MAXE5; i++) {
+        counts[i] += counts[i - 1];
+    }
+    long total = 0;
+    for (int i = 1; i <= MAXE5; i++) {
+        long sum = 0;
+        if (counts[i] == counts[i-1]) {
+            continue;
         }
-        if (prices[i] > maxOut) {
-            // 表示一个可卖出点,即比买入值高时
-            maxOut = prices[i];
-            // 需要设置一个历史值
-            maxSofar = Math.max(maxSofar, maxOut - maxIn);
+        for (int j = 1; i*j <= MAXE5; j++) {
+            int min = i * j - 1;
+            int upper = i * (j + 1) - 1;
+            // 在每一个区间内的数量,
+            sum += (counts[Math.min(upper, MAXE5)] - counts[min]) * (long)j;
         }
+        // 左边乘数的数量,即 i 位置的元素数量
+        total = (total + (sum % MODULUSE9  ) * (counts[i] - counts[i-1])) % MODULUSE9;
     }
-    return maxSofar;
-}
+ return (int)total; +}
-

总结下

一开始看到 easy 就觉得是很简单,就没有 maxSofar ,但是一提交就出现问题了
对于[2, 4, 1]这种就会变成 0,所以还是需要一个历史值来存放历史最大值,这题有点动态规划的意思

+

贴出来大神的解析,解析

+

结果

]]>
Java leetcode - java - DP - DP leetcode java 题解 - DP
- Disruptor 系列三 - /2022/09/25/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%89/ - 原来一直有点被误导,
gatingSequences用来标识每个 processer 的操作位点,但是怎么记录更新有点搞不清楚
其实问题在于 gatingSequences 是个 Sequence 数组,首先要看下怎么加进去的,
可以看到是在 com.lmax.disruptor.RingBuffer#addGatingSequences 这个方法里添加
首先是 com.lmax.disruptor.dsl.Disruptor#handleEventsWith(com.lmax.disruptor.EventHandler<? super T>...)
然后执行 com.lmax.disruptor.dsl.Disruptor#createEventProcessors(com.lmax.disruptor.Sequence[], com.lmax.disruptor.EventHandler<? super T>[])

-
EventHandlerGroup<T> createEventProcessors(
-        final Sequence[] barrierSequences,
-        final EventHandler<? super T>[] eventHandlers)
-    {
-        checkNotStarted();
-
-        final Sequence[] processorSequences = new Sequence[eventHandlers.length];
-        final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);
-
-        for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
-        {
-            final EventHandler<? super T> eventHandler = eventHandlers[i];
-
-            // 这里将 handler 包装成一个 BatchEventProcessor
-            final BatchEventProcessor<T> batchEventProcessor =
-                new BatchEventProcessor<>(ringBuffer, barrier, eventHandler);
-
-            if (exceptionHandler != null)
-            {
-                batchEventProcessor.setExceptionHandler(exceptionHandler);
-            }
+    Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析
+    /2022/07/02/Leetcode-20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7-Valid-Parentheses-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.

+

An input string is valid if:

+
    +
  1. Open brackets must be closed by the same type of brackets.
  2. +
  3. Open brackets must be closed in the correct order.
  4. +
+

示例

Example 1:

+

Input: s = “()”
Output: true

+
+

Example 2:

+

Input: s = “()[]{}”
Output: true

+
+

Example 3:

+

Input: s = “(]”
Output: false

+
+

Constraints:

    +
  • 1 <= s.length <= 10^4
  • +
  • s consists of parentheses only '()[]{}'.
  • +
+

解析

easy题,并且看起来也是比较简单的,三种括号按对匹配,直接用栈来做,栈里面存的是括号的类型,如果是左括号,就放入栈中,如果是右括号,就把栈顶的元素弹出,如果弹出的元素不是左括号,就返回false,如果弹出的元素是左括号,就继续往下走,如果遍历完了,如果栈里面还有元素,就返回false,如果遍历完了,如果栈里面没有元素,就返回true

+

代码

class Solution {
+    public boolean isValid(String s) {
 
-            consumerRepository.add(batchEventProcessor, eventHandler, barrier);
-            processorSequences[i] = batchEventProcessor.getSequence();
+        if (s.length() % 2 != 0) {
+            return false;
         }
-
-        updateGatingSequencesForNextInChain(barrierSequences, processorSequences);
-
-        return new EventHandlerGroup<>(this, consumerRepository, processorSequences);
-    }
- -

BatchEventProcessor 在类内有个定义 sequence

-
private final Sequence sequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE);
-

然后在上面循环中的这一句取出来

-
processorSequences[i] = batchEventProcessor.getSequence();
-

调用com.lmax.disruptor.dsl.Disruptor#updateGatingSequencesForNextInChain 方法

-
private void updateGatingSequencesForNextInChain(final Sequence[] barrierSequences, final Sequence[] processorSequences)
-    {
-        if (processorSequences.length > 0)
-        {
-            // 然后在这里添加
-            ringBuffer.addGatingSequences(processorSequences);
-            for (final Sequence barrierSequence : barrierSequences)
-            {
-                ringBuffer.removeGatingSequence(barrierSequence);
+        Stack<String> stk = new Stack<>();
+        for (int i = 0; i < s.length(); i++) {
+            if (s.charAt(i) == '{' || s.charAt(i) == '(' || s.charAt(i) == '[') {
+                stk.push(String.valueOf(s.charAt(i)));
+                continue;
             }
-            consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
-        }
-    }
- -

而如何更新则是在处理器 com.lmax.disruptor.BatchEventProcessor#run

-
public void run()
-    {
-        if (running.compareAndSet(IDLE, RUNNING))
-        {
-            sequenceBarrier.clearAlert();
-
-            notifyStart();
-            try
-            {
-                if (running.get() == RUNNING)
-                {
-                    processEvents();
+            if (s.charAt(i) == '}') {
+                if (stk.isEmpty()) {
+                    return false;
                 }
+                String cur = stk.peek();
+                if (cur.charAt(0) != '{') {
+                    return false;
+                } else {
+                    stk.pop();
+                }
+                continue;
             }
-            finally
-            {
-                notifyShutdown();
-                running.set(IDLE);
-            }
-        }
-        else
-        {
-            // This is a little bit of guess work.  The running state could of changed to HALTED by
-            // this point.  However, Java does not have compareAndExchange which is the only way
-            // to get it exactly correct.
-            if (running.get() == RUNNING)
-            {
-                throw new IllegalStateException("Thread is already running");
-            }
-            else
-            {
-                earlyExit();
-            }
-        }
-    }
-

然后是

-
private void processEvents()
-    {
-        T event = null;
-        long nextSequence = sequence.get() + 1L;
-
-        while (true)
-        {
-            try
-            {
-                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
-                if (batchStartAware != null)
-                {
-                    batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
+            if (s.charAt(i) == ']') {
+                if (stk.isEmpty()) {
+                    return false;
+                }
+                String cur = stk.peek();
+                if (cur.charAt(0) != '[') {
+                    return false;
+                } else {
+                    stk.pop();
                 }
-
-                while (nextSequence <= availableSequence)
-                {
-                    event = dataProvider.get(nextSequence);
-                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
-                    nextSequence++;
+                continue;
+            }
+            if (s.charAt(i) == ')') {
+                if (stk.isEmpty()) {
+                    return false;
                 }
-                // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
-                sequence.set(availableSequence);
+                String cur = stk.peek();
+                if (cur.charAt(0) != '(') {
+                    return false;
+                } else {
+                    stk.pop();
+                }
+                continue;
             }
-            catch (final TimeoutException e)
-            {
-                notifyTimeout(sequence.get());
+
+        }
+        return stk.size() == 0;
+    }
+}
+ +]]>
+ + Java + leetcode + + + leetcode + java + + + + Leetcode 2 Add Two Numbers 题解分析 + /2020/10/11/Leetcode-2-Add-Two-Numbers-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 又 roll 到了一个以前做过的题,不过现在用 Java 也来写一下,是 easy 级别的,所以就简单说下

+

简要介绍

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.

+

You may assume the two numbers do not contain any leading zero, except the number 0 itself.
就是给了两个链表,用来表示两个非负的整数,在链表中倒序放着,每个节点包含一位的数字,把他们加起来以后也按照原来的链表结构输出

+

样例

example 1

Input: l1 = [2,4,3], l2 = [5,6,4]
+Output: [7,0,8]
+Explanation: 342 + 465 = 807.
+ +

example 2

Input: l1 = [0], l2 = [0]
+Output: [0]
+ +

example 3

Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
+Output: [8,9,9,9,0,0,0,1]
+ +

题解

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
+        ListNode root = new ListNode();
+        if (l1 == null && l2 == null) {
+            return root;
+        }
+        ListNode tail = root;
+        int entered = 0;
+        // 这个条件加了 entered,就是还有进位的数
+        while (l1 != null || l2 != null || entered != 0) {
+            int temp = entered;
+            if (l1 != null) {
+                temp += l1.val;
+                l1 = l1.next;
             }
-            catch (final AlertException ex)
-            {
-                if (running.get() != RUNNING)
-                {
-                    break;
-                }
+            if (l2 != null) {
+                temp += l2.val;
+                l2 = l2.next;
             }
-            catch (final Throwable ex)
-            {
-                handleEventException(ex, nextSequence, event);
-                // 如果是异常就只是 nextSequence
-                sequence.set(nextSequence);
-                nextSequence++;
+            entered = (temp - temp % 10) / 10;
+            tail.val = temp % 10;
+            // 循环内部的控制是为了排除最后的空节点
+            if (l1 != null || l2 != null || entered != 0) {
+                tail.next = new ListNode();
+                tail = tail.next;
             }
         }
-    }
+// tail = null; + return root; + }
+

这里唯二需要注意的就是两个点,一个是循环条件需要包含进位值还存在的情况,还有一个是最后一个节点,如果是空的了,就不要在 new 一个出来了,写的比较挫

]]>
Java + leetcode + java + linked list + linked list - Java - Disruptor + leetcode + java + 题解 + linked list
- Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析 - /2022/08/06/Leetcode-16-%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C-3Sum-Closest-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

Given an integer array nums of length n and an integer target, find three integers in nums such that the sum is closest to target.

-

Return the sum of the three integers.

-

You may assume that each input would have exactly one solution.

-

简单解释下就是之前是要三数之和等于目标值,现在是找到最接近的三数之和。

-

示例

Example 1:

-

Input: nums = [-1,2,1,-4], target = 1
Output: 2
Explanation: The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).

-
-

Example 2:

-

Input: nums = [0,0,0], target = 1
Output: 0

-
-

Constraints:

    -
  • 3 <= nums.length <= 1000
  • -
  • -1000 <= nums[i] <= 1000
  • -
  • -10^4 <= target <= 10^4
  • -
-

简单解析

这个题思路上来讲不难,也是用原来三数之和的方式去做,利用”双指针法”或者其它描述法,但是需要简化逻辑

-

code

public int threeSumClosest(int[] nums, int target) {
-        Arrays.sort(nums);
-        // 当前最近的和
-        int closestSum = nums[0] + nums[1] + nums[nums.length - 1];
-        for (int i = 0; i < nums.length - 2; i++) {
-            if (i == 0 || nums[i] != nums[i - 1]) {
-                // 左指针
-                int left = i + 1;
-                // 右指针
-                int right = nums.length - 1;
-                // 判断是否遍历完了
-                while (left < right) {
-                    // 当前的和
-                    int sum = nums[i] + nums[left] + nums[right];
-                    // 小优化,相等就略过了
-                    while (left < right && nums[left] == nums[left + 1]) {
-                        left++;
-                    }
-                    while (left < right && nums[right] == nums[right - 1]) {
-                        right--;
-                    }
-                    // 这里判断,其实也还是希望趋近目标值
-                    if (sum < target) {
-                        left++;
-                    } else {
-                        right--;
-                    }
-                    // 判断是否需要替换
-                    if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
-                        closestSum = sum;
-                    }
-                }
+    Leetcode 234 回文链表(Palindrome Linked List) 题解分析
+    /2020/11/15/Leetcode-234-%E5%9B%9E%E6%96%87%E8%81%94%E8%A1%A8-Palindrome-Linked-List-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

Given a singly linked list, determine if it is a palindrome.
给定一个单向链表,判断是否是回文链表

+

例一 Example 1:

Input: 1->2
Output: false

+

例二 Example 2:

Input: 1->2->2->1
Output: true

+

挑战下自己

Follow up:
Could you do it in O(n) time and O(1) space?

+

简要分析

首先这是个单向链表,如果是双向的就可以一个从头到尾,一个从尾到头,显然那样就没啥意思了,然后想过要不找到中点,然后用一个栈,把前一半塞进栈里,但是这种其实也比较麻烦,比如长度是奇偶数,然后如何找到中点,这倒是可以借助于双指针,还是比较麻烦,再想一想,回文链表,就跟最开始的一样,链表只有单向的,我用个栈不就可以逆向了么,先把链表整个塞进栈里,然后在一个个 pop 出来跟链表从头开始比较,全对上了就是回文了

+
/**
+ * Definition for singly-linked list.
+ * public class ListNode {
+ *     int val;
+ *     ListNode next;
+ *     ListNode() {}
+ *     ListNode(int val) { this.val = val; }
+ *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
+ * }
+ */
+class Solution {
+    public boolean isPalindrome(ListNode head) {
+        if (head == null) {
+            return true;
+        }
+        ListNode tail = head;
+        LinkedList<Integer> stack = new LinkedList<>();
+        // 这里就是一个循环,将所有元素依次压入栈
+        while (tail != null) {
+            stack.push(tail.val);
+            tail = tail.next;
+        }
+        // 在逐个 pop 出来,其实这个出来的顺序就等于链表从尾到头遍历,同时跟链表从头到尾遍历进行逐对对比
+        while (!stack.isEmpty()) {
+            if (stack.peekFirst() == head.val) {
+                stack.pollFirst();
+                head = head.next;
+            } else {
+                return false;
             }
         }
-        return closestSum;
-    }
- -

结果

-]]>
+ return true; + } +}
]]>
Java leetcode + Linked List + java + Linked List leetcode java 题解 - 3Sum Closest + Linked List
- Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析 - /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

Given a 2D grid of size m x n and an integer k. You need to shift the grid k times.

-

In one shift operation:

-

Element at grid[i][j] moves to grid[i][j + 1].
Element at grid[i][n - 1] moves to grid[i + 1][0].
Element at grid[m - 1][n - 1] moves to grid[0][0].
Return the 2D grid after applying shift operation k times.

-

示例

Example 1:

-
-

Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 1
Output: [[9,1,2],[3,4,5],[6,7,8]]

-
-

Example 2:

-
-

Input: grid = [[3,8,1,9],[19,7,2,5],[4,6,11,10],[12,0,21,13]], k = 4
Output: [[12,0,21,13],[3,8,1,9],[19,7,2,5],[4,6,11,10]]

+ Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析 + /2021/05/23/Leetcode-236-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88-Lowest-Common-Ancestor-of-a-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.

+

According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

+

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

+

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 pq,最近公共祖先表示为一个节点 x,满足 xpq 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

+

代码

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
+        // 如果当前节点就是 p 或者是 q 的时候,就直接返回了
+        // 当没找到,即 root == null 的时候也会返回 null,这是个重要的点
+        if (root == null || root == p || root == q) return root;
+        // 在左子树中找 p 和 q
+        TreeNode left = lowestCommonAncestor(root.left, p, q);
+        // 在右子树中找 p 和 q
+        TreeNode right = lowestCommonAncestor(root.right, p, q);
+        // 当左边是 null 就直接返回右子树,但是这里不表示右边不是 null,所以这个顺序是不影响的
+        // 考虑一种情况,如果一个节点的左右子树都是 null,那么其实对于这个节点来说首先两个子树分别调用
+        // lowestCommonAncestor会在开头就返回 null,那么就是上面 left 跟 right 都是 null,然后走下面的判断的时候
+        // 其实第一个 if 就返回了 null,如此递归返回就能达到当子树中没有找到 p 或者 q 的时候只返回 null
+        if (left == null) {
+            return right;
+        } else if (right == null) {
+            return left;
+        } else {
+            return root;
+        }
+//        if (right == null) {
+//            return left;
+//        } else if (left == null) {
+//            return right;
+//        } else {
+//            return root;
+//        }
+//        return left == null ? right : right == null ? left : root;
+    }
+]]>
+ + Java + leetcode + Lowest Common Ancestor of a Binary Tree + + + leetcode + java + 题解 + Lowest Common Ancestor of a Binary Tree + + + + Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析 + /2022/08/14/Leetcode-278-%E7%AC%AC%E4%B8%80%E4%B8%AA%E9%94%99%E8%AF%AF%E7%9A%84%E7%89%88%E6%9C%AC-First-Bad-Version-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad.

+

Suppose you have n versions [1, 2, ..., n] and you want to find out the first bad one, which causes all the following ones to be bad.

+

You are given an API bool isBadVersion(version) which returns whether version is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API.

+

示例

Example 1:

+

Input: n = 5, bad = 4
Output: 4
Explanation:
call isBadVersion(3) -> false
call isBadVersion(5) -> true
call isBadVersion(4) -> true
Then 4 is the first bad version.

-

Example 3:

-

Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 9
Output: [[1,2,3],[4,5,6],[7,8,9]]

+

Example 2:

+

Input: n = 1, bad = 1
Output: 1

-

提示

    -
  • m == grid.length
  • -
  • n == grid[i].length
  • -
  • 1 <= m <= 50
  • -
  • 1 <= n <= 50
  • -
  • -1000 <= grid[i][j] <= 1000
  • -
  • 0 <= k <= 100
  • -
-

解析

这个题主要是矩阵或者说数组的操作,并且题目要返回的是个 List,所以也不用原地操作,只需要找对位置就可以了,k 是多少就相当于让这个二维数组头尾衔接移动 k 个元素

-

代码

public List<List<Integer>> shiftGrid(int[][] grid, int k) {
-        // 行数
-        int m = grid.length;
-        // 列数
-        int n = grid[0].length;
-        // 偏移值,取下模
-        k = k % (m * n);
-        // 反向取下数量,因为我打算直接从头填充新的矩阵
-        /*
-         *    比如
-         *    1 2 3
-         *    4 5 6
-         *    7 8 9
-         *    需要变成
-         *    9 1 2
-         *    3 4 5
-         *    6 7 8
-         *    就要从 9 开始填充
-         */
-        int reverseK = m * n - k;
-        List<List<Integer>> matrix = new ArrayList<>();
-        // 这类就是两层循环
-        for (int i = 0; i < m; i++) {
-            List<Integer> line = new ArrayList<>();
-            for (int j = 0; j < n; j++) {
-                // 数量会随着循环迭代增长, 确认是第几个
-                int currentNum = reverseK + i * n +  (j + 1);
-                // 这里处理下到达矩阵末尾后减掉 m * n
-                if (currentNum > m * n) {
-                    currentNum -= m * n;
-                }
-                // 根据矩阵列数 n 算出在原来矩阵的位置
-                int last = (currentNum - 1) % n;
-                int passLine = (currentNum - 1) / n;
-
-                line.add(grid[passLine][last]);
-            }
-            matrix.add(line);
+

简析

简单来说就是一个二分查找,但是这个问题其实处理起来还是需要搞清楚一些边界问题

+

代码

public int firstBadVersion(int n) {
+    // 类似于双指针法
+    int left = 1, right = n, mid;
+    while (left < right) {
+        // 取中点
+        mid = left + (right - left) / 2;
+        // 如果不是错误版本,就往右找
+        if (!isBadVersion(mid)) {
+            left = mid + 1;
+        } else {
+        // 如果是的话就往左查找
+            right = mid;
         }
-        return matrix;
-    }
+ } + // 这里考虑交界情况是,在上面循环中如果 left 是好的,right 是坏的,那进入循环的时候 mid == left + // 然后 left = mid + 1 就会等于 right,循环条件就跳出了,此时 left 就是那个起始的错误点了 + // 其实这两个是同一个值 + return left; +}
-

结果数据


比较慢

+

往右移动示例

往左移动示例

+

结果

]]> Java @@ -3721,356 +3596,435 @@ inorder = [9,3,15,20,7]
+ dubbo 客户端配置的一个重要知识点 + /2022/06/11/dubbo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E4%B8%80%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9F%A5%E8%AF%86%E7%82%B9/ + 在配置项目中其实会留着比较多的问题,由于不同的项目没有比较统一的规划和框架模板,一般都是只有创建者会比较了解(可能也不了解),譬如前阵子在配置一个 springboot + dubbo 的项目,发现了dubbo 连接注册中间客户端的问题,这里可以结合下代码来看
比如有的应用是用的这个

+
<dependency>
+    <groupId>org.apache.curator</groupId>
+    <artifactId>curator-client</artifactId>
+    <version>${curator.version}</version>            
+</dependency>
+<dependency>
+    <groupId>org.apache.curator</groupId>
+    <artifactId>curator-recipes</artifactId>
+    <version>${curator.version}</version>
+</dependency>
+

有个别应用用的是这个

+
<dependency>
+    <groupId>com.101tec</groupId>
+    <artifactId>zkclient</artifactId>
+    <version>0.11</version>
+</dependency>
+

还有的应用是找不到相关的依赖,并且这些的使用没有个比较好的说明,为啥用前者,为啥用后者,有啥注意点,
首先在使用 2.6.5 的 alibaba 的 dubbo 的时候,只使用后者是会报错的,至于为啥会报错,其实就是这篇文章想说明的点
报错的内容其实很简单, 就是缺少这个 org.apache.curator.framework.CuratorFrameworkFactory
这个类看着像是依赖上面的配置,但是应该不需要两个配置一块用的,所以还是需要去看代码
通过找上面类被依赖的和 dubbo 连接注册中心相关的代码,看到了这段指点迷津的代码

+
@SPI("curator")
+public interface ZookeeperTransporter {
 
-

模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。

+ @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY}) + ZookeeperClient connect(URL url); + +}
+

众所周知,dubbo 创造了叫自适应扩展点加载的神奇技术,这里的 adaptive 注解中的Constants.CLIENT_KEYConstants.TRANSPORTER_KEY 可以在配置 dubbo 的注册信息的时候进行配置,如果是通过 xml 配置的话,可以在 <dubbo:registry/> 这个 tag 中的以上两个 key 进行配置,
具体在 dubbo.xsd 中有描述

+
<xsd:element name="registry" type="registryType">
+        <xsd:annotation>
+            <xsd:documentation><![CDATA[ The registry config ]]></xsd:documentation>
+        </xsd:annotation>
+    </xsd:element>
+ +


并且在 spi 的配置com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter 中可以看到

+
zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
+curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
+
+zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
+curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
+
+zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
+curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
+

而在上面的代码里默认的SPI 值是 curator,所以如果不配置,那就会报上面找不到类的问题,所以如果需要使用 zkclient 的,就需要在<dubbo:registry/> 配置中添加 client="zkclient"这个配置,所以有些地方还是需要懂一些更深层次的原理,但也不至于每个东西都要抠到每一行代码原理,除非就是专门做这一块的。
还有一点是发现有些应用是碰运气,刚好有个三方包把这个类带进来了,但是这个应用就没有单独配置这块,如果不了解或者后续忘了再来查问题就会很奇怪

]]>
- C++ + Java + Dubbo - c++ - mfc + Java + Dubbo
- Maven实用小技巧 - /2020/02/16/Maven%E5%AE%9E%E7%94%A8%E5%B0%8F%E6%8A%80%E5%B7%A7/ - Maven 翻译为”专家”、”内行”,是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

-

Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。

-

Maven 也可被用于构建和管理各种项目,例如 C#,Ruby,Scala 和其他语言编写的项目。Maven 曾是 Jakarta 项目的子项目,现为由 Apache 软件基金会主持的独立 Apache 项目。

-

maven也是我们日常项目中实用的包管理工具,相比以前需要用把包下载下来,放进 lib 中,在平时工作中使用的话,其实像 idea 这样的 ide 工具都会自带 maven 工具和插件

-

maven的基本操作

    -
  • mvn -v
    查看 maven 信息
  • -
  • mvn compile
    将 Java 编译成 class 文件
  • -
  • mvn test
    执行 test 包下的测试用例
  • -
  • mvn package
    将项目打成 jar 包
  • -
  • mvn clean
    删除package 在 target 目录下面打出来的 jar 包和 target 目录
  • -
  • mvn install
    将打出来的 jar 包复制到 maven 的本地仓库里
  • -
  • mvn deploy
    将打出来的 jar 包上传到远程仓库里
  • -
-

与 composer 对比

因为我也是个 PHP 程序员,所以对比一下两种语言,很容易想到在 PHP 的 composer 跟 Java 的 maven 是比较类似的作用,有一点两者是非常相似的,就是原仓库都是因为某些原因连接拉取都会很慢,所以像 composer 会有一些国内源,前阵子阿里也出了一个,类似的 maven 一般也会使用阿里的镜像仓库,通过在 setting.xml 文件中的设置

-
<mirrors>
-    <mirror>
-        <id>aliyun</id>
-        <name>aliyun maven</name>
-        <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
-        <mirrorOf>central</mirrorOf>
-    </mirror> 
-</mirrors>
-

这算是个尴尬的共同点,然后因为 PHP 是解释型脚本语言,所以 php 打出来的 composer 包其实就是个 php 代码包,使用SPL Autoload等方式加载代码包,maven 包则是经过编译的 class 包,还有一点是 composer 也可以直接使用 github 地址作为包代码的拉取源,这点也是比较大的区别,maven使用 pom 文件管理依赖

-

maven 的个人小技巧

    -
  • maven 拉取依赖时,同时将 snapshot 也更新了,就是 mvn compile加个-U参数,如果还不行就需要将本地仓库的 snapshot 删掉,
    这个命令的 help 命令解释是 -U,–update-snapshots Forces a check for missing releases and updated snapshots on
    remote repositories,这个在日常使用中还是很经常使用的
  • -
  • maven 出现依赖冲突的时候的解决方法
    首先是依赖分析,使用mvn dependency:tree分析下依赖关系,如果要找具体某个包的依赖引用关系可以使用mvn dependency:tree -Dverbose -Dincludes=org.springframework:spring-webmvc命令进行分析,如果发现有冲突的依赖关系,本身 maven 中依赖引用有相对的顺序,大致来说是引用路径短的优先,pom 文件中定义的顺序优先,如果要把冲突的包排除掉可以在 pom 中用
    <exclusions>
    -  <exclusion>
    -      <groupId>ch.qos.logback</groupId>
    -      <artifactId>logback-classic</artifactId>
    -  </exclusion>
    -</exclusions>
    -将冲突的包排除掉
  • -
  • maven 依赖的 jdk 版本管理
    前面介绍的mvn -v可以查看 maven 的安装信息
    比如
    Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
    -Maven home: /usr/local/Cellar/maven/3.6.3_1/libexec
    -Java version: 1.8.0_201, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre
    -Default locale: zh_CN, platform encoding: UTF-8
    -OS name: "mac os x", version: "10.14.6", arch: "x86_64", family: "mac"
    -这里可以看到用了 mac 自带的 jdk1.8,结合我之前碰到的一个问题,因为使用 homebrew 升级了 gradle,而 gradle 又依赖了 jdk13,因为这个 mvn 的 Java version 也变成 jdk13 了,然后 mvn 编译的时候出现了 java.lang.ExceptionInInitializerError: com.sun.tools.javac.code.TypeTags这个问题,所以需要把这个版本给改回来,但是咋改呢,网上搜来的一大堆都是在 pom 文件里的
    source和 target 版本
    <build>
    -  <plugins>
    -<plugin>
    -	<groupId>org.apache.maven.plugins</groupId>
    -	<artifactId>maven-compiler-plugin</artifactId>
    -	<configuration>
    -		<source>1.8</source>
    -		<target>1.8</target>
    -		<encoding>UTF-8</encoding>
    -	</configuration>
    -</plugin>
    -  </plugins>
    -<build>
    -或者修改 maven 的 setting.xml中的
    <profiles>
    -      <profile>
    -          <id>ngmm-nexus</id>
    -        <activation>
    -          <jdk>1.8</jdk>
    -        </activation>
    -        <properties>
    -          <maven.compiler.source>1.8</maven.compiler.source>
    -          <maven.compiler.target>1.8</maven.compiler.target>
    -          <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
    -      </properties>
    -      </profile>
    -</profiles>
    -但是这些都没啥用啊,真正有办法的是建个 .mavenrc,这个顾名思义就是 maven 的资源文件,类似于 .bashrc.zshrc,在里面添加 MAVEN_HOME 和 JAVA_HOME,然后执行 source .mavenrc就 OK 啦
  • + gogs使用webhook部署react单页应用 + /2020/02/22/gogs%E4%BD%BF%E7%94%A8webhook%E9%83%A8%E7%BD%B2react%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8/ + 众所周知,我是个前端彩笔,但是也想做点自己可以用的工具页面,所以就让朋友推荐了蚂蚁出品的 ant design,说基本可以直接 ctrl-c ctrl-v,实测对我这种来说还是有点难的,不过也能写点,但是现在碰到的问题是怎么部署到自己的服务器上去
    用 ant design 写的是个单页应用,实际来说就是一个 html 加 css 跟 js,最初的时候是直接 build 完就 scp 上去,也考虑过 rsync 之类的,但是都感觉不够自动化,正好自己还没这方面的经验就想折腾下,因为我自己搭的仓库应用是 gogs,搜了一下主要是跟 drones 配合做 ci/cd,研究了一下发现其实这个事情没必要这么搞(PS:drone 也不好用),整个 hook 就可以了, 但是实际上呢,这东西也不是那么简单
    首先是需要在服务器上装 webhook,这个我一开始用 snap 安装,但是出现问题,run 的时候会出现后面参数带的 hooks.json 文件找不到,然后索性就直接 github 上下最新版,放 /usr/local/bin 了,webhook 的原理呢其实也比较简单,就是起一个 http 服务,通过 post 请求调用,解析下参数,如果跟配置的参数一致,就调用对应的命令或者脚本。

    +

    配置 hooks.json

    webhook 的配置,需要的两个文件,一个是 hooks.json,这个是 webhook 服务的配置文件,像这样

    +
    [
    +  {
    +    "id": "redeploy-app",
    +    "execute-command": "/opt/scripts/redeploy.sh",
    +    "command-working-directory": "/opt/scripts",
    +    "pass-arguments-to-command":
    +    [
    +      {
    +        "source": "payload",
    +        "name": "head_commit.message"
    +      },
    +      {
    +        "source": "payload",
    +        "name": "pusher.name"
    +      },
    +      {
    +        "source": "payload",
    +        "name": "head_commit.id"
    +      }
    +    ],
    +    "trigger-rule":
    +    {
    +      "and":
    +      [
    +        {
    +          "match":
    +          {
    +            "type": "payload-hash-sha1",
    +            "secret": "your-github-secret",
    +            "parameter":
    +            {
    +              "source": "header",
    +              "name": "X-Hub-Signature"
    +            }
    +          }
    +        },
    +        {
    +          "match":
    +          {
    +            "type": "value",
    +            "value": "refs/heads/master",
    +            "parameter":
    +            {
    +              "source": "payload",
    +              "name": "ref"
    +            }
    +          }
    +        }
    +      ]
    +    }
    +  }
    +]
    + +

    这是个跟 github搭配的示例,首先 id 表示的是这个对应 hook 的识别 id,也可以看到这个 hooks.json 的结构是这样的一个数组,然后就是要执行的命令和命令执行的参数,值得注意的是这个trigger-rule,就是请求进来了回去匹配里面的,比如前一个是一个加密的,放在请求头里,第二个 match 表示请求里的 ref 是个 master 分支,就可以区分分支进行不同操作,但是前面的加密配合 gogs 使用的时候有个问题(PS: webhook 的文档是真的烂),gogs 设置 webhook 的加密是用的

    +
    +

    密钥文本将被用于计算推送内容的 SHA256 HMAC 哈希值,并设置为 X-Gogs-Signature 请求头的值。

    +
    +

    这种加密方式,所以 webhook 的这个示例的加密方式不行,但这货的文档里居然没有说明支持哪些加密,神TM,后来还是翻 issue 翻到了, 需要使用这个payload-hash-sha256

    +

    执行脚本 redeploy.sh

    脚本类似于这样

    +
    #!/bin/bash -e
    +
    +function cleanup {
    +      echo "Error occoured"
    +}
    +trap cleanup ERR
    +
    +commit_message=$1 # head_commit.message
    +pusher_name=$2 # pusher.name
    +commit_id=$3 # head_commit.id
    +
    +
    +cd ~/do-react-example-app/
    +git pull origin master
    +yarn && yarn build
    + +

    就是简单的拉代码,然后构建下,真实使用时可能不是这样,因为页面会部署在 nginx 的作用目录,还需要 rsync 过去,这部分可能还涉及到两个问题第一个是使用 rsync 还是其他的 cp,不过这个无所谓;第二个是目录权限的问题,以我的系统ubuntu 为例,默认用户是 ubuntu,nginx 部署的目录是 www,所以需要切换用户等操作,一开始是想用在shell 文件中直接写了密码,但是不知道咋传,查了下是类似于这样 echo "passwd" | sudo -S cmd,通过管道命令往后传,然后就是这个-S, 参数的解释是-S, --stdin read password from standard input,但是这样么也不是太安全的赶脚,又看了下还有两种方法,

    +
      +
    • 就是给root 设置一个不需要密码的命令类似于这样,

      +
      myusername ALL = (ALL) ALL
      +myusername ALL = (root) NOPASSWD: /path/to/my/program
      +
    • +
    • 另一种就是把默认用户跟 root 设置成同一个 group 的

      +
    +

    使用

    真正实操的时候其实还有不少问题,首先运行 webhook 就碰到了我前面说的,使用 snap 运行的时候会找不到前面的 hooks.json配置文件,执行snap run webhook -hooks /opt/hooks/hooks.json -verbose就碰到下面的couldn't load hooks from file! open /opt/hooks/hooks.json: no such file or directory,后来直接下了个官方最新的 release,就直接执行 webhook -hooks /opt/hooks/hooks.json -verbose 就可以了,然后是前面的示例配置文件里的几个参数,比如head_commit.message 其实 gogs 推过来的根本没这玩意,而且都是数组,不知道咋取,烂文档,不过总比搭个 drone 好一点就忍了。补充一点就是在 debug 的时候需要看下问题出在哪,看看脚本有没有执行,所以需要在前面的 json 里加这个参数"include-command-output-in-response": true, 就能输出来脚本执行结果

    ]]>
    - Java - Maven + 持续集成 - Java - Maven + Gogs + Webhook - Number of 1 Bits - /2015/03/11/Number-Of-1-Bits/ - Number of 1 Bits

    Write a function that takes an unsigned integer and returns the number of ’1’ bits it has (also known as the Hamming weight). For example, the 32-bit integer ‘11’ has binary representation 00000000000000000000000000001011, so the function should return 3.

    - -

    分析

    从1位到2位到4位逐步的交换

    -
    -

    code

    int hammingWeight(uint32_t n) {
    -        const uint32_t m1  = 0x55555555; //binary: 0101...  
    -        const uint32_t m2  = 0x33333333; //binary: 00110011..  
    -        const uint32_t m4  = 0x0f0f0f0f; //binary:  4 zeros,  4 ones ...  
    -        const uint32_t m8  = 0x00ff00ff; //binary:  8 zeros,  8 ones ...  
    -        const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...  
    -        
    -        n = (n & m1 ) + ((n >>  1) & m1 ); //put count of each  2 bits into those  2 bits   
    -        n = (n & m2 ) + ((n >>  2) & m2 ); //put count of each  4 bits into those  4 bits   
    -        n = (n & m4 ) + ((n >>  4) & m4 ); //put count of each  8 bits into those  8 bits   
    -        n = (n & m8 ) + ((n >>  8) & m8 ); //put count of each 16 bits into those 16 bits   
    -        n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits   
    -        return n; 
    -
    -}
    ]]>
    + invert-binary-tree + /2015/06/22/invert-binary-tree/ + Invert a binary tree

    +
         4
    +   /   \
    +  2     7
    + / \   / \
    +1   3 6   9
    +
    +

    to

    +
         4
    +   /   \
    +  7     2
    + / \   / \
    +9   6 3   1
    +
    +

    Trivia:
    This problem was inspired by this original tweet by Max Howell:

    +
    +

    Google: 90% of our engineers use the software you wrote (Homebrew),
    but you can’t invert a binary tree on a whiteboard so fuck off.

    +
    +
    /**
    + * Definition for a binary tree node.
    + * struct TreeNode {
    + *     int val;
    + *     TreeNode *left;
    + *     TreeNode *right;
    + *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    + * };
    + */
    +class Solution {
    +public:
    +    TreeNode* invertTree(TreeNode* root) {
    +        if(root == NULL) return root;
    +        TreeNode* temp;
    +        temp = invertTree(root->left);
    +        root->left = invertTree(root->right);
    +        root->right = temp;
    +        return root;
    +    }
    +};
    ]]>
    leetcode @@ -5048,48 +5117,34 @@ OS name: "mac os x", version: "10.14.6", arch: "x86_64&
    - Path Sum - /2015/01/04/Path-Sum/ - problem

    Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

    - -

    For example:
    Given the below binary tree and sum = 22,

    -
          5
    -     / \
    -    4   8
    -   /   / \
    -  11  13  4
    - /  \      \
    -7    2      1
    -

    return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.

    -

    Analysis

    using simple deep first search

    -

    code

    /*
    -  Definition for binary tree
    -  struct TreeNode {
    -      int val;
    -      TreeNode *left;
    -      TreeNode *right;
    -      TreeNode(int x) : val(x), left(NULL), right(NULL)}
    -  };
    - */
    -class Solution {
    +    minimum-size-subarray-sum-209
    +    /2016/10/11/minimum-size-subarray-sum-209/
    +    problem

    Given an array of n positive integers and a positive integer s, find the minimal length of a subarray of which the sum ≥ s. If there isn’t one, return 0 instead.

    +

    For example, given the array [2,3,1,2,4,3] and s = 7,
    the subarray [4,3] has the minimal length under the problem constraint.

    +

    题解

    参考,滑动窗口,跟之前Data Structure课上的online算法有点像,链接

    +

    Code

    class Solution {
     public:
    -    bool deep_first_search(TreeNode *node, int sum, int curSum)
    -    {
    -        if (node == NULL)
    -            return false;
    +    int minSubArrayLen(int s, vector<int>& nums) {
    +        int len = nums.size();
    +        if(len == 0) return 0;
    +        int minlen = INT_MAX;
    +        int sum = 0;
             
    -        if (node->left == NULL && node->right == NULL)
    -            return curSum + node->val == sum;
    -               
    -        return deep_first_search(node->left, sum, curSum + node->val) || deep_first_search(node->right, sum, curSum + node->val);
    -    }
    -    
    -    bool hasPathSum(TreeNode *root, int sum) {
    -        // Start typing your C/C++ solution below
    -        // DO NOT write int main() function
    -        return deep_first_search(root, sum, 0);
    +        int left = 0;
    +        int right = -1;
    +        while(right < len)
    +        {
    +            while(sum < s && right < len)
    +                sum += nums[++right];
    +            if(sum >= s)
    +            {
    +                minlen = minlen < right - left + 1 ? minlen : right - left + 1;
    +                sum -= nums[left++];
    +            }
    +        }
    +        return minlen > len ? 0 : minlen;
         }
    -};
    +};
    ]]>
    leetcode @@ -5100,40 +5155,37 @@ public:
    - Reverse Integer - /2015/03/13/Reverse-Integer/ - Reverse Integer

    Reverse digits of an integer.

    Example1: x = 123, return 321
    Example2: x = -123, return -321

    + C++ 指针使用中的一个小问题 + /2014/12/23/my-new-post/ + 在工作中碰到的一点C++指针上的一点小问题
    +

    在C++中,应该是从C语言就开始了,除了void型指针之外都是需要有分配对应的内存才可以使用,同时mallocfree成对使用,newdelete成对使用,否则造成内存泄漏。

    +]]>
    + + C++ + + + 博客,文章 + +
    + + leetcode no.3 + /2015/04/15/Leetcode-No-3/ + **Longest Substring Without Repeating Characters **

    -

    spoilers

    Have you thought about this?
    Here are some good questions to ask before coding. Bonus points for you if you have already thought through this!

    -

    If the integer’s last digit is 0, what should the output be? ie, cases such as 10, 100.

    -

    Did you notice that the reversed integer might overflow? Assume the input is a 32-bit integer, then the reverse of 1000000003 overflows. How should you handle such cases?

    -

    For the purpose of this problem, assume that your function returns 0 when the reversed integer overflows.

    -
    -

    code

    class Solution {
    -public:
    -    int reverse(int x) {
    -
    -        int max = 1 << 31 - 1;
    -        int ret = 0;
    -        max = (max - 1) * 2 + 1;
    -        int min = 1 << 31;
    -        if(x < 0)
    -            while(x != 0){
    -                if(ret < (min - x % 10) / 10)
    -                    return 0;
    -                ret = ret * 10 + x % 10;
    -                x = x / 10;
    -            }
    -        else
    -            while(x != 0){
    -               if(ret > (max -x % 10) / 10)
    -                    return 0;
    -                ret = ret * 10 + x % 10;
    -                x = x / 10;
    -            }
    -        return ret;
    -    }
    -};
    +

    description

    Given a string, find the length of the longest substring without repeating characters.
    For example, the longest substring without repeating letters for “abcabcbb” is “abc”,
    which the length is 3. For “bbbbb” the longest substring is “b”, with the length of 1.

    +

    分析

    源码这次是参考了这个代码,
    tail 表示的当前子串的起始点位置,tail从-1开始就包括的串的长度是1的边界。其实我
    也是猜的(逃

    +
    int ct[256];
    +    memset(ct, -1, sizeof(ct));
    +	int tail = -1;
    +	int max = 0;
    +	for (int i = 0; i < s.size(); i++){
    +		if (ct[s[i]] > tail)
    +			tail = ct[s[i]];
    +		if (i - tail > max)
    +			max = i - tail;
    +		ct[s[i]] = i;
    +	}
    +	return max;
    ]]>
    leetcode @@ -5144,1802 +5196,1716 @@ public:
    - Reverse Bits - /2015/03/11/Reverse-Bits/ - Reverse Bits

    Reverse bits of a given 32 bits unsigned integer.

    For example, given input 43261596 (represented in binary as 00000010100101000001111010011100), return 964176192 (represented in binary as 00111001011110000010100101000000).

    - -

    Follow up:
    If this function is called many times, how would you optimize it?

    -
    -

    code

    class Solution {
    -public:
    -    uint32_t reverseBits(uint32_t n) {
    -        n = ((n >> 1) & 0x55555555) | ((n & 0x55555555) << 1);
    -        n = ((n >> 2) & 0x33333333) | ((n & 0x33333333) << 2);
    -        n = ((n >> 4) & 0x0f0f0f0f) | ((n & 0x0f0f0f0f) << 4);
    -        n = ((n >> 8) & 0x00ff00ff) | ((n & 0x00ff00ff) << 8);
    -        n = ((n >> 16) & 0x0000ffff) | ((n & 0x0000ffff) << 16);
    -        return n;
    -    }
    -};
    -]]>
    + mybatis 的 foreach 使用的注意点 + /2022/07/09/mybatis-%E7%9A%84-foreach-%E4%BD%BF%E7%94%A8%E7%9A%84%E6%B3%A8%E6%84%8F%E7%82%B9/ + mybatis 在作为轻量级 orm 框架,如果要使用类似于 in 查询的语句,除了直接替换字符串,还可以使用 foreach 标签
    在mybatis的 dtd 文件中可以看到可以配置这些字段,

    +
    <!ELEMENT foreach (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
    +<!ATTLIST foreach
    +collection CDATA #REQUIRED
    +item CDATA #IMPLIED
    +index CDATA #IMPLIED
    +open CDATA #IMPLIED
    +close CDATA #IMPLIED
    +separator CDATA #IMPLIED
    +>
    +

    collection 表示需要使用 foreach 的集合,item 表示进行迭代的变量名,index 就是索引值,而 open 跟 close
    代表拼接的起始和结束符号,一般就是左右括号,separator 则是每个 item 直接的分隔符

    +

    例如写了一个简单的 sql 查询

    +
    <select id="search" parameterType="list" resultMap="StudentMap">
    +    select * from student
    +    <where>
    +        id in
    +        <foreach collection="list" item="item" open="(" close=")" separator=",">
    +            #{item}
    +        </foreach>
    +    </where>
    +</select>
    +

    这里就发现了一个问题,collection 对应的这个值,如果传入的参数是个 HashMap,collection 的这个值就是以此作为
    key 从这个 HashMap 获取对应的集合,但是这里有几个特殊的小技巧,
    在上面的这个方法对应的接口方法定义中

    +
    public List<Student> search(List<Long> userIds);
    +

    我是这么定义的,而 collection 的值是list,这里就有一点不能理解了,但其实是 mybatis 考虑到使用的方便性,
    帮我们做了一点点小转换,我们翻一下 mybatis 的DefaultSqlSession 中的代码可以看到

    +
    @Override
    +public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    +  try {
    +    MappedStatement ms = configuration.getMappedStatement(statement);
    +    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    +  } catch (Exception e) {
    +    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    +  } finally {
    +    ErrorContext.instance().reset();
    +  }
    +}
    +// 就是在这帮我们做了转换
    +  private Object wrapCollection(final Object object) {
    +  if (object instanceof Collection) {
    +    StrictMap<Object> map = new StrictMap<Object>();
    +    map.put("collection", object);
    +    if (object instanceof List) {
    +      // 如果类型是list 就会转成以 list 为 key 的 map
    +      map.put("list", object);
    +    }
    +    return map;
    +  } else if (object != null && object.getClass().isArray()) {
    +    StrictMap<Object> map = new StrictMap<Object>();
    +    map.put("array", object);
    +    return map;
    +  }
    +  return object;
    +  }
    ]]>
    - leetcode + Java + Mybatis + Mysql - leetcode - c++ + Java + Mysql + Mybatis
    - two sum - /2015/01/14/Two-Sum/ - problem

    Given an array of integers, find two numbers such that they add up to a specific target number.

    -

    The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.

    - -

    You may assume that each input would have exactly one solution.

    -

    Input: numbers={2, 7, 11, 15}, target=9
    Output: index1=1, index2=2

    -

    code

    struct Node
    -{
    -    int num, pos;
    -};
    -bool cmp(Node a, Node b)
    -{
    -    return a.num < b.num;
    -}
    -class Solution {
    -public:
    -    vector<int> twoSum(vector<int> &numbers, int target) {
    -        // Start typing your C/C++ solution below
    -        // DO NOT write int main() function
    -        vector<int> result;
    -        vector<Node> array;
    -        for (int i = 0; i < numbers.size(); i++)
    -        {
    -            Node temp;
    -            temp.num = numbers[i];
    -            temp.pos = i;
    -            array.push_back(temp);
    +    mybatis 的 $ 和 # 是有啥区别
    +    /2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/
    +    这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
    对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
    在这里做了替换,将#{}替换成了?

    前面说的是否 dynamic 就是在这里进行判断

    +
    // org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
    +public SqlSource parseScriptNode() {
    +    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    +    SqlSource sqlSource;
    +    if (isDynamic) {
    +      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    +    } else {
    +      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    +    }
    +    return sqlSource;
    +  }
    +// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
    +protected MixedSqlNode parseDynamicTags(XNode node) {
    +    List<SqlNode> contents = new ArrayList<>();
    +    NodeList children = node.getNode().getChildNodes();
    +    for (int i = 0; i < children.getLength(); i++) {
    +      XNode child = node.newXNode(children.item(i));
    +      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
    +        String data = child.getStringBody("");
    +        TextSqlNode textSqlNode = new TextSqlNode(data);
    +        if (textSqlNode.isDynamic()) {
    +          contents.add(textSqlNode);
    +          isDynamic = true;
    +        } else {
    +          contents.add(new StaticTextSqlNode(data));
             }
    -
    -        sort(array.begin(), array.end(), cmp);
    -        for (int i = 0, j = array.size() - 1; i != j;)
    -        {
    -            int sum = array[i].num + array[j].num;
    -            if (sum == target)
    -            {
    -                if (array[i].pos < array[j].pos)
    -                {
    -                    result.push_back(array[i].pos + 1);
    -                    result.push_back(array[j].pos + 1);
    -                } else
    -                {
    -                    result.push_back(array[j].pos + 1);
    -                    result.push_back(array[i].pos + 1);
    -                }
    -                break;
    -            } else if (sum < target)
    -            {
    -                i++;
    -            } else if (sum > target)
    -            {
    -                j--;
    -            }
    +      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
    +        String nodeName = child.getNode().getNodeName();
    +        NodeHandler handler = nodeHandlerMap.get(nodeName);
    +        if (handler == null) {
    +          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
             }
    -        return result;
    +        handler.handleNode(child, contents);
    +        isDynamic = true;
    +      }
         }
    -};
    + return new MixedSqlNode(contents); + } +// org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic + public boolean isDynamic() { + DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser(); + GenericTokenParser parser = createParser(checker); + parser.parse(text); + return checker.isDynamic(); + } + private GenericTokenParser createParser(TokenHandler handler) { + return new GenericTokenParser("${", "}", handler); + }
    +

    可以看到其中一个条件就是是否有${}这种占位符,假如说上面的 sql 换成 ${},那么可以看到它会在这里创建一个 dynamicSqlSource,

    +
    // org.apache.ibatis.scripting.xmltags.DynamicSqlSource
    +public class DynamicSqlSource implements SqlSource {
     
    -

    Analysis

    sort the array, then test from head and end, until catch the right answer

    + private final Configuration configuration; + private final SqlNode rootSqlNode; + + public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { + this.configuration = configuration; + this.rootSqlNode = rootSqlNode; + } + + @Override + public BoundSql getBoundSql(Object parameterObject) { + DynamicContext context = new DynamicContext(configuration, parameterObject); + rootSqlNode.apply(context); + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); + Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + context.getBindings().forEach(boundSql::setAdditionalParameter); + return boundSql; + } + +} +
    +

    这里眼尖的同学可能就可以看出来了,RawSqlSource 在初始化的时候已经经过了 parse,把#{}替换成了?占位符,但是 DynamicSqlSource 并没有
    再看这个图,我们发现在这的时候还没有进行替换
    然后往里跟
    好像是这里了

    这里 rootSqlNode.apply 其实是一个对原来 sql 的解析结果的一个循环调用,不同类型的标签会构成不同的 node,像这里就是一个 textSqlNode

    可以发现到这我们的 sql 已经被替换了,而且是直接作为 string 类型替换的,所以可以明白了这个问题所在,就是注入,不过细心的同学发现其实这里是有个

    理论上还是可以做过滤的,不过好像现在没用起来。
    我们前面可以发现对于#{}是在启动扫描 mapper的 xml 文件就替换成了 ?,然后是在什么时候变成实际的值的呢

    发现到这的时候还是没有替换,其实说白了也就是 prepareStatement 那一套,

    在这里进行替换,会拿到 org.apache.ibatis.mapping.ParameterMapping,然后进行替换,因为会带着类型信息,所以不用担心注入咯

    ]]>
    - leetcode - - - leetcode - c++ - -
    - - binary-watch - /2016/09/29/binary-watch/ - problem

    A binary watch has 4 LEDs on the top which represent the hours (0-11), and the 6 LEDs on the bottom represent the minutes (0-59).

    -

    Each LED represents a zero or one, with the least significant bit on the right.

    -

    -

    For example, the above binary watch reads “3:25”.

    -

    Given a non-negative integer n which represents the number of LEDs that are currently on, return all possible times the watch could represent.

    -

    Example:

    Input: n = 1
    -Return: ["1:00", "2:00", "4:00", "8:00", "0:01", "0:02", "0:04", "0:08", "0:16", "0:32"]
    -

    Note:

      -
    • The order of output does not matter.
    • -
    • The hour must not contain a leading zero, for example “01:00” is not valid, it should be “1:00”.
    • -
    • The minute must be consist of two digits and may contain a leading zero, for example “10:2” is not valid, it should be “10:02”.
    • -
    -

    题解

    又是参(chao)考(xi)别人的代码,嗯,就是这么不要脸,链接

    -

    Code

    class Solution {
    -public:
    -    vector<string> readBinaryWatch(int num) {
    -        vector<string> res;
    -        for (int h = 0; h < 12; ++h) {
    -            for (int m = 0; m < 60; ++m) {
    -                if (bitset<10>((h << 6) + m).count() == num) {
    -                    res.push_back(to_string(h) + (m < 10 ? ":0" : ":") + to_string(m));
    -                }
    -            }
    -        }
    -        return res;
    -    }
    -};
    ]]>
    - - leetcode + Java + Mybatis + Spring + Mysql + Sql注入 + Mybatis - leetcode - c++ + Java + Mysql + Mybatis + Sql注入
    - Redis_分布式锁 - /2019/12/10/Redis-Part-1/ - 今天看了一下 redis 分布式锁 redlock 的实现,简单记录下,

    -

    加锁

    原先我对 redis 锁的概念就是加锁使用 setnx,解锁使用 lua 脚本,但是 setnx 具体是啥,lua 脚本是啥不是很清楚
    首先简单思考下这个问题,首先为啥不是先 get 一下 key 存不存在,然后再 set 一个 key value,因为加锁这个操作我们是要保证两点,一个是不能中途被打断,也就是说要原子性,如果是先 get 一下 key,如果不存在再 set 值的话,那就不是原子操作了;第二个是可不可以直接 set 值呢,显然不行,锁要保证唯一性,有且只能有一个线程或者其他应用单位获得该锁,正好 setnx 给了我们这种原子命令

    -

    然后是 setnx 的键和值分别是啥,键比较容易想到是要锁住的资源,比如 user_id, 这里有个我自己之前比较容易陷进去的误区,但是这个误区后
    面再说,这里其实是把user_id 作为要锁住的资源,在我获得锁的时候别的线程不允许操作,以此保证业务的正确性,不会被多个线程同时修改,确定了键,再来看看值是啥,其实原先我认为值是啥都没关系,我只要锁住了,光键就够我用了,但是考虑下多个线程的问题,如果我这个线程加了锁,然后我因为 gc 停顿等原因卡死了,这个时候redis 的锁或者说就是 redis 的缓存已经过期了,这时候另一个线程获得锁成功,然后我这个线程又活过来了,然后我就仍然认为我拿着锁,我去对数据进行修改或者释放锁,是不是就出现问题了,所以是不是我们还需要一个东西来区分这个锁是哪个线程加的,所以我们可以将值设置成为一个线程独有识别的值,至少在相对长的一段时间内不会重复。

    -

    上面其实还有两个问题,一个是当 gc 超时时,我这个线程如何知道我手里的锁已经过期了,一种方法是我在加好锁之后就维护一个超时时间,这里其实还有个问题,不过跟第二个问题相关,就一起说了,就是设置超时时间,有些对于不是锁的 redis 缓存操作可以是先设置好值,然后在设置过期时间,那么这就又有上面说到的不是原子性的问题,那么就需要在同一条指令里把超时时间也设置了,幸好 redis 提供了这种支持

    -
    SET resource_name my_random_value NX PX 30000
    -

    这里借鉴一下解释下,resource_name就是 key,代表要锁住的东西,my_random_value就是识别我这个线程的,NX代表只有在不存在的时候才设置,然后PX 30000表示超时时间是 30秒自动过期

    -

    PS:记录下我原先有的一个误区,是不是要用 key 来区分加锁的线程,这样只有一个用处,就是自身线程可以识别是否是自己加的锁,但是最大的问题是别的线程不知道,其实这个用户的出发点是我在担心前面提过的一个问题,就是当 gc 停顿后,我要去判断当前的这个锁是否是我加的,还有就是当释放锁的时候,如果保证不会错误释放了其他线程加的锁,但是这样附带很多其他问题,最大的就是其他线程怎么知道能不能加这个锁。

    -

    解锁

    当线程在锁过期之前就处理完了业务逻辑,那就可以提前释放这个锁,那么提前释放要怎么操作,直接del key显然是不行的,因为这样就是我前面想用线程随机值加资源名作为锁的初衷,我不能去释放别的线程加的锁,那么我要怎么办呢,先 get 一下看是不是我的?那又变成非原子的操作了,幸好redis 也考虑到了这个问题,给了lua 脚本来操作这种

    -
    if redis.call("get",KEYS[1]) == ARGV[1] then
    -    return redis.call("del",KEYS[1])
    -else
    -    return 0
    -end
    -

    这里的KEYS[1]就是前面加锁的resource_name,ARGV[1]就是线程的随机值my_random_value

    -

    多节点

    前面说的其实是单节点 redis 作为分布式锁的情况,那么当我们的 redis 有多节点的情况呢,如果多节点下处于加锁或者解锁或者锁有效情况下
    redis 的某个节点宕掉了怎么办,这里就有一些需要思考的地方,是否单独搞一个单节点的 redis作为分布式锁专用的,但是如果这个单节点的挂了呢,还有就是成本问题,所以我们需要一个多节点的分布式锁方案
    这里就引出了开头说到的redlock,这个可是 redis的作者写的, 他的加锁过程是分以下几步去做这个事情

    -
      -
    • 获取当前时间(毫秒数)。
    • -
    • 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
    • -
    • 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
    • -
    • 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
    • -
    • 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。
      释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。这里为什么要向所有的节点发送释放锁的操作呢,这里是因为有部分的节点的失败原因可能是加锁时阻塞,加锁成功的结果没有及时返回,所以为了防止这种情况还是需要向所有发起这个释放锁的操作。
      初步记录就先到这。
    • -
    -]]>
    + mybatis 的缓存是怎么回事 + /2020/10/03/mybatis-%E7%9A%84%E7%BC%93%E5%AD%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/ + Java 真的是任何一个中间件,比较常用的那种,都有很多内容值得深挖,比如这个缓存,慢慢有过一些感悟,比如如何提升性能,缓存无疑是一大重要手段,最底层开始 CPU 就有缓存,而且又小又贵,再往上一点内存一般作为硬盘存储在运行时的存储,一般在代码里也会用内存作为一些本地缓存,譬如数据库,像 mysql 这种也是有innodb_buffer_pool来提升查询效率,本质上理解就是用更快的存储作为相对慢存储的缓存,减少查询直接访问较慢的存储,并且这个都是相对的,比起 cpu 的缓存,那内存也是渣,但是与普通机械硬盘相比,那也是两个次元的水平。

    +

    闲扯这么多来说说 mybatis 的缓存,mybatis 一般作为一个轻量级的 orm 使用,相对应的就是比较重量级的 hibernate,不过不在这次讨论范围,上一次是主要讲了 mybatis 在解析 sql 过程中,对于两种占位符的不同替换实现策略,这次主要聊下 mybatis 的缓存,前面其实得了解下前置的东西,比如 sqlsession,先当做我们知道 sqlsession 是个什么玩意,可能或多或少的知道 mybatis 是有两级缓存,

    +

    一级缓存

    第一级的缓存是在 BaseExecutor 中的 PerpetualCache,它是个最基本的缓存实现类,使用了 HashMap 实现缓存功能,代码其实没几十行

    +
    public class PerpetualCache implements Cache {
    +
    +  private final String id;
    +
    +  private final Map<Object, Object> cache = new HashMap<>();
    +
    +  public PerpetualCache(String id) {
    +    this.id = id;
    +  }
    +
    +  @Override
    +  public String getId() {
    +    return id;
    +  }
    +
    +  @Override
    +  public int getSize() {
    +    return cache.size();
    +  }
    +
    +  @Override
    +  public void putObject(Object key, Object value) {
    +    cache.put(key, value);
    +  }
    +
    +  @Override
    +  public Object getObject(Object key) {
    +    return cache.get(key);
    +  }
    +
    +  @Override
    +  public Object removeObject(Object key) {
    +    return cache.remove(key);
    +  }
    +
    +  @Override
    +  public void clear() {
    +    cache.clear();
    +  }
    +
    +  @Override
    +  public boolean equals(Object o) {
    +    if (getId() == null) {
    +      throw new CacheException("Cache instances require an ID.");
    +    }
    +    if (this == o) {
    +      return true;
    +    }
    +    if (!(o instanceof Cache)) {
    +      return false;
    +    }
    +
    +    Cache otherCache = (Cache) o;
    +    return getId().equals(otherCache.getId());
    +  }
    +
    +  @Override
    +  public int hashCode() {
    +    if (getId() == null) {
    +      throw new CacheException("Cache instances require an ID.");
    +    }
    +    return getId().hashCode();
    +  }
    +
    +}
    +

    可以看一下BaseExecutor 的构造函数

    +
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
    +    this.transaction = transaction;
    +    this.deferredLoads = new ConcurrentLinkedQueue<>();
    +    this.localCache = new PerpetualCache("LocalCache");
    +    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    +    this.closed = false;
    +    this.configuration = configuration;
    +    this.wrapper = this;
    +  }
    +

    就是把 PerpetualCache 作为 localCache,然后怎么使用我看简单看一下,BaseExecutor 的查询首先是调用这个函数

    +
    @Override
    +  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    +    BoundSql boundSql = ms.getBoundSql(parameter);
    +    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    +    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    +  }
    +

    可以看到首先是调用了 createCacheKey 方法,这个方法呢,先不看怎么写的,如果我们自己要实现这么个缓存,首先这个缓存 key 的设计也是个问题,如果是以表名加主键作为 key,那么分页查询,或者没有主键的时候就不行,来看看 mybatis 是怎么设计的

    +
    @Override
    +  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    +    if (closed) {
    +      throw new ExecutorException("Executor was closed.");
    +    }
    +    CacheKey cacheKey = new CacheKey();
    +    cacheKey.update(ms.getId());
    +    cacheKey.update(rowBounds.getOffset());
    +    cacheKey.update(rowBounds.getLimit());
    +    cacheKey.update(boundSql.getSql());
    +    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    +    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    +    // mimic DefaultParameterHandler logic
    +    for (ParameterMapping parameterMapping : parameterMappings) {
    +      if (parameterMapping.getMode() != ParameterMode.OUT) {
    +        Object value;
    +        String propertyName = parameterMapping.getProperty();
    +        if (boundSql.hasAdditionalParameter(propertyName)) {
    +          value = boundSql.getAdditionalParameter(propertyName);
    +        } else if (parameterObject == null) {
    +          value = null;
    +        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    +          value = parameterObject;
    +        } else {
    +          MetaObject metaObject = configuration.newMetaObject(parameterObject);
    +          value = metaObject.getValue(propertyName);
    +        }
    +        cacheKey.update(value);
    +      }
    +    }
    +    if (configuration.getEnvironment() != null) {
    +      // issue #176
    +      cacheKey.update(configuration.getEnvironment().getId());
    +    }
    +    return cacheKey;
    +  }
    +
    +

    首先需要 id,这个 id 是 mapper 里方法的 id, 然后是偏移量跟返回行数,再就是 sql,然后是参数,基本上是会有影响的都加进去了,在这个 update 里面

    +
    public void update(Object object) {
    +    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    +
    +    count++;
    +    checksum += baseHashCode;
    +    baseHashCode *= count;
    +
    +    hashcode = multiplier * hashcode + baseHashCode;
    +
    +    updateList.add(object);
    +  }
    +

    其实是一个 hash 转换,具体不纠结,就是提高特异性,然后回来就是继续调用 query

    +
    @Override
    +  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    +    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    +    if (closed) {
    +      throw new ExecutorException("Executor was closed.");
    +    }
    +    if (queryStack == 0 && ms.isFlushCacheRequired()) {
    +      clearLocalCache();
    +    }
    +    List<E> list;
    +    try {
    +      queryStack++;
    +      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    +      if (list != null) {
    +        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    +      } else {
    +        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    +      }
    +    } finally {
    +      queryStack--;
    +    }
    +    if (queryStack == 0) {
    +      for (DeferredLoad deferredLoad : deferredLoads) {
    +        deferredLoad.load();
    +      }
    +      // issue #601
    +      deferredLoads.clear();
    +      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    +        // issue #482
    +        clearLocalCache();
    +      }
    +    }
    +    return list;
    +  }
    +

    可以看到是先从 localCache 里取,取不到再 queryFromDatabase,其实比较简单,这是一级缓存,考虑到 sqlsession 跟 BaseExecutor 的关系,其实是随着 sqlsession 来保证这个缓存不会出现脏数据幻读的情况,当然事务相关的后面可能再单独聊。

    +

    二级缓存

    其实这个一级二级顺序有点反过来,其实查询的是先走的二级缓存,当然二级的需要配置开启,默认不开,
    需要通过

    +
    <setting name="cacheEnabled" value="true"/>
    +

    来开启,然后我们的查询就会走到

    +
    public class CachingExecutor implements Executor {
    +
    +  private final Executor delegate;
    +  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
    +

    这个 Executor 中,这里我放了类里面的元素,发现没有一个 Cache 类,这就是一个特点了,往下看查询过程

    +
    @Override
    +  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    +    BoundSql boundSql = ms.getBoundSql(parameterObject);
    +    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    +    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    +  }
    +
    +  @Override
    +  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    +      throws SQLException {
    +    Cache cache = ms.getCache();
    +    if (cache != null) {
    +      flushCacheIfRequired(ms);
    +      if (ms.isUseCache() && resultHandler == null) {
    +        ensureNoOutParams(ms, boundSql);
    +        @SuppressWarnings("unchecked")
    +        List<E> list = (List<E>) tcm.getObject(cache, key);
    +        if (list == null) {
    +          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    +          tcm.putObject(cache, key, list); // issue #578 and #116
    +        }
    +        return list;
    +      }
    +    }
    +    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    +  }
    +

    看到没,其实缓存是从 tcm 这个成员变量里取,而这个是什么呢,事务性缓存(直译下),因为这个其实是用 MappedStatement 里的 Cache 作为key 从 tcm 的 map 取出来的

    +
    public class TransactionalCacheManager {
    +
    +  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
    +

    MappedStatement是被全局使用的,所以其实二级缓存是跟着 mapper 的 namespace 走的,可以被多个 CachingExecutor 获取到,就会出现线程安全问题,线程安全问题可以用SynchronizedCache来解决,就是加锁,但是对于事务中的脏读,使用了TransactionalCache来解决这个问题,

    +
    public class TransactionalCache implements Cache {
    +
    +  private static final Log log = LogFactory.getLog(TransactionalCache.class);
    +
    +  private final Cache delegate;
    +  private boolean clearOnCommit;
    +  private final Map<Object, Object> entriesToAddOnCommit;
    +  private final Set<Object> entriesMissedInCache;
    +

    在事务还没提交的时候,会把中间状态的数据放在 entriesToAddOnCommit 中,只有在提交后会放进共享缓存中,

    +
    public void commit() {
    +    if (clearOnCommit) {
    +      delegate.clear();
    +    }
    +    flushPendingEntries();
    +    reset();
    +  }
    ]]>
    - Redis - Distributed Lock - C - Redis + Java + Mybatis + Spring + Mybatis + 缓存 + Mybatis - C - Redis - Distributed Lock - 分布式锁 + Java + Mysql + Mybatis + 缓存
    - ambari-summary - /2017/05/09/ambari-summary/ - 初识ambari

    ambari是一个大数据平台的管理工具,包含了hadoop, yarn, hive, hbase, spark等大数据的基础架构和工具,简化了数据平台的搭建,之前只是在同事搭建好平台后的一些使用,这次有机会从头开始用ambari来搭建一个测试的数据平台,过程中也踩到不少坑,简单记录下。

    -

    简单过程

      -
    • 第一个坑
      在刚开始是按照官网的指南,用maven构建,因为GFW的原因,导致反复失败等待,也就是这个guide,因为对maven不熟悉导致有些按图索骥,浪费了很多时间,之后才知道可以直接加repo用yum安装,然而用yum安装马上就出现了第二个坑。
    • -
    • 第二个坑
      因为在线的repo还是因为网络原因很慢很慢,用proxychains勉强把ambari-server本身安装好了,ambari.repo将这个放进/etc/yum.repos.d/路径下,然后yum update && yum install ambari-server安装即可,如果有条件就用proxychains走下代理。
    • -
    • 第三步
      安装好ambari-server后先执行ambari-server setup做一些初始化设置,其中包含了JDK路径的设置,数据库设置,设置好就OK了,然后执行ambari-server start启动服务,这里有个小插曲,因为ambari-server涉及到这么多服务,所以管理控制监控之类的模块是必不可少的,这部分可以在ambari-server的web ui界面安装,也可以命令行提前安装,这部分被称为HDF Management Pack,运行ambari-server install-mpack \ --mpack=http://public-repo-1.hortonworks.com/HDF/centos7/2.x/updates/2.1.4.0/tars/hdf_ambari_mp/hdf-ambari-mpack-2.1.4.0-5.tar.gz \ --purge \ --verbose
      安装,当然这个压缩包可以下载之后指到本地路径安装,然后就可以重启ambari-server
    • -
    + nginx 日志小记 + /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ + nginx 默认的日志有特定的格式,我们也可以自定义,

    +

    默认的格式是预定义的 combined

    +
    log_format combined '$remote_addr - $remote_user [$time_local] '
    +                    '"$request" $status $body_bytes_sent '
    +                    '"$http_referer" "$http_user_agent"';
    + +

    配置的日志可以使用这个默认的,如果满足需求的话

    +
    Syntax:	access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
    +        access_log off;
    +Default: access_log logs/access.log combined;
    +Context: http, server, location, if in location, limit_except
    + +

    而如果需要额外的一些配置的话可以自己定义 log_format ,比如我想要给日志里加上请求时间,那就可以自己定义一个 log_format 比如添加下

    +
    $request_time
    +request processing time in seconds with a milliseconds resolution;   
    +time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client
    + +
    log_format combined_extend '$remote_addr - $remote_user [$time_local] '
    +                    '"$request" $status $body_bytes_sent '
    +                    '"$http_referer" "$http_user_agent" "$request_time"';
    + +

    然后其他的比如还有 gzip 压缩,可以设置压缩级别,flush 刷盘时间还有根据条件控制

    +

    这里的条件控制简单看了下还比较厉害

    +

    比如我想对2xx 跟 3xx 的访问不记录日志

    +
    map $status $loggable {
    +    ~^[23]  0;
    +    default 1;
    +}
    +
    +access_log /path/to/access.log combined if=$loggable;
    + +

    $loggable 是 0 或者空时表示 if 条件为否,上面的默认就是 1,只有当请求状态 status 是 2xx 或 3xx 时才是 0,代表不用记录,有了这个特性就可以更灵活地配置日志

    +

    文章主要参考了 nginx 的 log 模块的文档

    ]]>
    - data analysis + nginx - hadoop - cluster + nginx + 日志
    - docker-mysql-cluster - /2016/08/14/docker-mysql-cluster/ - docker-mysql-cluster

    基于docker搭了个mysql集群,稍微记一下,
    首先是新建mysql主库容

    docker run -d -e MYSQL_ROOT_PASSWORD=admin --name mysql-master -p 3307:3306 mysql:latest
    -d表示容器运行在后台,-e表示设置环境变量,即MYSQL_ROOT_PASSWORD=admin,设置了mysql的root密码,
    --name表示容器名,-p表示端口映射,将内部mysql:3306映射为外部的3307,最后的mysql:latest表示镜像名
    此外还可以用-v /local_path/my-master.cnf:/etc/mysql/my.cnf来映射配置文件
    然后同理启动从库
    docker run -d -e MYSQL_ROOT_PASSWORD=admin --name mysql-slave -p 3308:3306 mysql:latest
    然后进入主库改下配置文件
    docker-enter mysql-master如果无法进入就用docker ps -a看下容器是否在正常运行,如果status显示
    未正常运行,则用docker logs mysql-master看下日志哪里出错了。
    进入容器后,我这边使用的镜像的mysqld配置文件是在/etc/mysql下面,这个最新版本的mysql的配置文件包含
    三部分,/etc/mysql/my.cnf/etc/mysql/conf.d/mysql.cnf,还有/etc/mysql/mysql.conf.d/mysqld.cnf
    这里需要改的是最后一个,加上

    -
    log-bin = mysql-bin
    -server_id = 1
    -

    保存后退出容器重启主库容器,然后进入从库更改相同文件,

    -
    log-bin = mysql-bin
    -server_id = 2
    -

    主从配置

    同样退出重启容器,然后是配置主从,首先进入主库,用命令mysql -u root -pxxxx进入mysql,然后赋予一个同步
    权限GRANT REPLICATION SLAVE ON *.* to 'backup'@'%' identified by '123456';还是同样说明下,ON *.*表示了数
    据库全部的权限,如果要指定数据库/表的话可以使用类似testDb/testTable,然后是'backup'@'%'表示给予同步
    权限的用户名及其主机ip,%表示不限制ip,当然如果有防火墙的话还是会有限制的,最后的identified by '123456'
    表示同步用户的密码,然后就查看下主库的状态信息show master status,如下图:
    9G5FE[9%@7%G(B`Q7]E)5@R.png
    把file跟position记下来,然后再开一个terminal,进入从库容器,登陆mysql,然后设置主库

    -
    change master to
    -master_host='xxx.xxx.xxx.xxx',   //如果主从库的容器都在同一个宿主机上,这里的ip是docker容器的ip
    -master_user='backup',            //就是上面的赋予权限的用户
    -master_password='123456',
    -master_log_file='mysql-bin.000004',  //主库中查看到的file
    -master_log_pos=312,                  //主库中查看到的position
    -master_port=3306;                    //如果是同一宿主机,这里使用真实的3306端口,3308及主库的3307是给外部连接使用的
    -

    通过docker-ip mysql-master可以查看容器的ip
    S(GP)P(M$N3~N1764@OW3E0.png
    这里有一点是要注意的,也是我踩的坑,就是如果是同一宿主机下两个mysql容器互联,我这里只能通过docker-ip和真实
    的3306端口能够连接成功。
    本文参考了这位同学的文章

    + php-abstract-class-and-interface + /2016/11/10/php-abstract-class-and-interface/ + PHP抽象类和接口
      +
    • 抽象类与接口
    • +
    • 抽象类内可以包含非抽象函数,即可实现函数
    • +
    • 抽象类内必须包含至少一个抽象方法,抽象类和接口均不能实例化
    • +
    • 抽象类可以设置访问级别,接口默认都是public
    • +
    • 类可以实现多个接口但不能继承多个抽象类
    • +
    • 类必须实现抽象类和接口里的抽象方法,不一定要实现抽象类的非抽象方法
    • +
    • 接口内不能定义变量,但是可以定义常量
    • +
    +

    示例代码

    <?php
    +interface int1{
    +    const INTER1 = 111;
    +    function inter1();
    +}
    +interface int2{
    +    const INTER1 = 222;
    +    function inter2();
    +}
    +abstract class abst1{
    +    public function abstr1(){
    +        echo 1111;
    +    }
    +    abstract function abstra1(){
    +        echo 'ahahahha';
    +    }
    +}
    +abstract class abst2{
    +    public function abstr2(){
    +        echo 1111;
    +    }
    +    abstract function abstra2();
    +}
    +class normal1 extends abst1{
    +    protected function abstr2(){
    +        echo 222;
    +    }
    +}
    + +

    result

    PHP Fatal error:  Abstract function abst1::abstra1() cannot contain body in new.php on line 17
    +
    +Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
    ]]>
    - docker + php - docker - mysql + php
    - docker比一般多一点的初学者介绍三 - /2020/03/21/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%B8%89/ - 运行第一个 Dockerfile

    上一篇的 Dockerfile 我们停留在构建阶段,现在来把它跑起来

    -
    docker run -d -p 80 --name static_web nicksxs/static_web \
    -nginx -g "daemon off;"
    -

    这里的-d表示以分离模型运行docker (detached),然后-p 是表示将容器的 80 端口开放给宿主机,然后容器名就叫 static_web,使用了我们上次构建的 static_web 镜像,后面的是让 nginx 在前台运行

    可以看到返回了个容器 id,但是具体情况没出现,也没连上去,那我们想看看怎么访问在 Dockerfile 里写的静态页面,我们来看下docker 进程

    发现为我们随机分配了一个宿主机的端口,32768,去服务器的防火墙把这个外网端口开一下,看看是不是符合我们的预期呢

    好像不太对额,应该是 ubuntu 安装的 nginx 的默认工作目录不对,我们来进容器看看,再熟悉下命令docker exec -it 4792455ca2ed /bin/bash
    记得容器 id 换成自己的,进入容器后得找找 nginx 的配置文件,通常在/etc/nginx,/usr/local/etc等目录下,然后找到我们的目录是在这

    所以把刚才的内容复制过去再试试

    目标达成,give me five✌️

    -

    第二个 Dockerfile

    然后就想来动态一点的,毕竟写过 PHP,就来试试 PHP
    再建一个目录叫 dynamic_web,里面创建 src 目录,放一个 index.php
    内容是

    -
    <?php
    -echo "Hello World!";
    -

    然后在 dynamic_web 目录下创建 Dockerfile,

    -
    FROM trafex/alpine-nginx-php7:latest
    -COPY src/ /var/www/html
    -EXPOSE 80
    -

    Dockerfile 虽然只有三行,不过要着重说明下,这个底包其实不是docker 官方的,有两点考虑,一点是官方的基本都是 php apache 的镜像,还有就是 alpine这个,截取一段中文介绍

    -
    -

    Alpine 操作系统是一个面向安全的轻型 Linux 发行版。它不同于通常 Linux 发行版,Alpine 采用了 musl libc 和 busybox 以减小系统的体积和运行时资源消耗,但功能上比 busybox 又完善的多,因此得到开源社区越来越多的青睐。在保持瘦身的同时,Alpine 还提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过 apk 命令直接查询和安装各种软件。
    Alpine 由非商业组织维护的,支持广泛场景的 Linux发行版,它特别为资深/重度Linux用户而优化,关注安全,性能和资源效能。Alpine 镜像可以适用于更多常用场景,并且是一个优秀的可以适用于生产的基础系统/环境。

    + pcre-intro-and-a-simple-package + /2015/01/16/pcre-intro-and-a-simple-package/ + Pcre
    +

    Perl Compatible Regular Expressions (PCRE) is a regular
    expression C library inspired by the regular expression
    capabilities in the Perl programming language, written
    by Philip Hazel, starting in summer 1997.

    -
    -

    Alpine Docker 镜像也继承了 Alpine Linux 发行版的这些优势。相比于其他 Docker 镜像,它的容量非常小,仅仅只有 5 MB 左右(对比 Ubuntu 系列镜像接近 200 MB),且拥有非常友好的包管理机制。官方镜像来自 docker-alpine 项目。

    +

    因为最近工作内容的一部分需要做字符串的识别处理,所以就顺便用上了之前在PHP中用过的正则,在C/C++中本身不包含正则库,这里使用的pcre,对MFC开发,在这里提供了静态链接库,在引入lib跟.h文件后即可使用。

    + + +

    Regular Expression Syntax

    然后是一些正则语法,官方的语法文档比较科学严谨,特别是对类似于贪婪匹配等细节的说明,当然一般的使用可以在网上找到很多匹配语法,例如这个

    +

    PCRE函数介绍

    +

    pcre_compile
    原型:

    +
    #include <pcre.h>
    +pcre *pcre_compile(const char *pattern, int options, const char **errptr, int *erroffset, const unsigned char *tableptr);
    +

    功能:将一个正则表达式编译成一个内部表示,在匹配多个字符串时,可以加速匹配。其同pcre_compile2功能一样只是缺少一个参数errorcodeptr。
    参数:
    pattern 正则表达式
    options 为0,或者其他参数选项
    errptr 出错消息
    erroffset 出错位置
    tableptr 指向一个字符数组的指针,可以设置为空NULL

    -

    目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 做为基础镜像环境。这样会带来多个好处。包括镜像下载速度加快,镜像安全性提高,主机之间的切换更方便,占用更少磁盘空间等。

    +

    pcre_exec
    原型:

    -

    一方面在没有镜像的情况下,拉取 docker 镜像还是比较费力的,第二个就是也能节省硬盘空间,所以目前有大部分的 docker 镜像都将 alpine 作为基础镜像了
    然后再来构建下

    这里还有个点,就是上面的那个镜像我们也是 EXPOSE 80端口,然后外部宿主机会随机映射一个端口,为了偷个懒,我们就直接指定外部端口了
    docker run -d -p 80:80 dynamic_web打开浏览器发现访问不了,咋回事呢
    因为我们没看trafex/alpine-nginx-php7:latest这个镜像说明,它内部的服务是 8080 端口的,所以我们映射的暴露端口应该是 8080,再用 docker run -d -p 80:8080 dynamic_web这个启动,

    +
    #include <pcre.h>
    +int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, int length, int startoffset, int options, int *ovector, int ovecsize)
    +

    功能:使用编译好的模式进行匹配,采用与Perl相似的算法,返回匹配串的偏移位置。
    参数:
    code 编译好的模式
    extra 指向一个pcre_extra结构体,可以为NULL
    subject 需要匹配的字符串
    length 匹配的字符串长度(Byte)
    startoffset 匹配的开始位置
    options 选项位
    ovector 指向一个结果的整型数组
    ovecsize 数组大小。

    +

    这里是两个最常用的函数的简单说明,pcre的静态库提供了一系列的函数以供使用,可以参考这个博客说明,另外对于以上函数的具体参数详细说明可以参考官网此处

    +

    一个丑陋的封装

    void COcxDemoDlg::pcre_exec_all(const pcre * re, PCRE_SPTR src, vector<pair<int, int>> &vc)
    +{
    +	int rc;
    +	int ovector[30];
    +	int i = 0;
    +	pair<int, int> pr;
    +	rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
    +	for (; rc > 0;)
    +	{
    +		i = ovector[1];
    +		pr.first = ovector[2];
    +		pr.second = ovector[3];
    +		vc.push_back(pr);
    +		rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
    +	}
    +}
    +

    vector中是全文匹配后的索引对,只是简单地用下。

    ]]> - Docker - 介绍 + C++ - Docker - namespace - Dockerfile + c++ + mfc - docker比一般多一点的初学者介绍 - /2020/03/08/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D/ - 因为最近想搭一个phabricator用来做看板和任务管理,一开始了解这个是Easy大大有在微博推荐过,后来苏洋也在群里和博客里说到了,看上去还不错的样子,因为主角是docker所以就不介绍太多,后面有机会写一下。

    -

    docker最开始是之前在某位大佬的博客看到的,看上去有点神奇,感觉是一种轻量级的虚拟机,但是能做的事情好像差不多,那时候是在Ubuntu系统的vps里起一个Ubuntu的docker,然后在里面装个nginx,配置端口映射就可以访问了,后来也草草写过一篇使用docker搭建mysql集群,但是最近看了下好像是因为装docker的大佬做了一些别名还是什么操作,导致里面用的操作都不具有普遍性,而且主要是把搭的过程写了下,属于囫囵吞枣,没理解docker是干啥的,为啥用docker,就是操作了下,这几天借着搭phabricator的过程,把一些原来不理解,或者原来理解错误的地方重新理一下。

    -

    之前写的 mysql 集群,一主二备,这种架构在很多小型应用里都是这么配置的,而且一般是直接在三台 vps 里启动三个 mysql 实例,但是如果换成 docker 会有什么好处呢,其实就是方便部署,比如其中一台备库挂了,我要加一台,或者说备库的 qps 太高了,需要再加一个,如果要在 vps 上搭建的话,首先要买一台机器,等初始化,然后在上面修改源,更新,装 mysql ,然后配置主从,可能还要处理防火墙等等,如果把这些打包成一个 docker 镜像,并且放在自己的 docker registry,那就直接run 一下就可以了;还有比如在公司要给一个新同学整一套开发测试环境,以 Java 开发为例,要装 git,maven,jdk,配置 maven settings 和各种 rc,整合在一个镜像里的话,就会很方便了;再比如微服务的水平扩展。

    -

    但是为啥 docker 会有这种优势,听起来好像虚拟机也可以干这个事,但是虚拟机动辄上 G,而且需要 VMware,virtual box 等支持,不适合在Linux服务器环境使用,而且占用资源也会非常大。说得这么好,那么 docker 是啥呢

    -

    docker 主要使用 Linux 中已经存在的两种技术的一个整合升级,一个是 namespace,一个是cgroups,相比于虚拟机需要完整虚拟出一个操作系统运行基础,docker 基于宿主机内核,通过 namespace 和 cgroups 分隔进程,理念就是提供一个隔离的最小化运行依赖,这样子相对于虚拟机就有了巨大的便利性,具体的 namespace 和 cgroups 就先不展开讲,可以参考耗子叔的文章

    -

    安装

    那么我们先安装下 docker,参考官方的教程,安装,我的系统是 ubuntu 的,就贴了 ubuntu 的链接,用其他系统的可以找到对应的系统文档安装,安装完了的话看看 docker 的信息

    -
    sudo docker info
    - -

    输出以下信息

    -

    简单运行

    然后再来运行个 hello world 呗,

    -
    sudo docker run hello-world
    - -

    输出了这些

    -

    看看这个运行命令是怎么用的,一般都会看到这样子的,sudo docker run -it ubuntu bash, 前面的 docker run 反正就是运行一个容器的意思,-it是啥呢,还有这个什么 ubuntu bash,来看看docker run`的命令帮助信息

    -
    -i, --interactive                    Keep STDIN open even if not attached
    - -

    就是要有输入,我们运行的时候能输入

    -
    -t, --tty                            Allocate a pseudo-TTY
    - -

    要有个虚拟终端,

    -
    Usage:	docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
    -
    -Run a command in a new container
    - -

    镜像

    上面说的-it 就是这里的 options,后面那个 ubuntu 就是 image 辣,image 是啥呢

    -

    Docker 把应用程序及其依赖,打包在 image 文件里面,可以把它理解成为类似于虚拟机的镜像或者运行一个进程的代码,跑起来了的叫docker 容器或者进程,比如我们将要运行的docker run -it ubuntu bash的ubuntu 就是个 ubuntu 容器的镜像,将这个镜像运行起来后,我们可以进入容器像使用 ubuntu 一样使用它,来看下我们的镜像,使用sudo docker image ls就能列出我们宿主机上的 docker 镜像了

    -

    -

    一个 ubuntu 镜像才 64MB,非常小巧,然后是后面的bash,我通过交互式启动了一个 ubuntu 容器,然后在这个启动的容器里运行了 bash 命令,这样就可以在容器里玩一下了

    -

    在容器里看下进程,

    -

    只有刚才运行容器的 bash 进程和我刚执行的 ps,这里有个可以注意下的,bash 这个进程的 pid 是 1,其实这里就用到了 linux 中的PID Namespace,容器会隔离出一个 pid 的名字空间,这里面的进程跟外部的 pid 命名独立

    -

    查看宿主机上的容器

    sudo docker ps -a
    - -

    -

    如何进入一个正在运行中的 docker 容器

    这个应该是比较常用的,因为比如是一个微服务容器,有时候就像看下运行状态,日志啥的

    -
    sudo docker exec -it [containerID] bash
    - -

    -

    查看日志

    sudo docker logs [containerID]
    - -

    我在运行容器的终端里胡乱输入点啥,然后通过上面的命令就可以看到啦

    -

    -

    + rabbitmq-tips + /2017/04/25/rabbitmq-tips/ + rabbitmq 介绍

    接触了一下rabbitmq,原来在选型的时候是在rabbitmq跟kafka之间做选择,网上搜了一下之后发现kafka的优势在于吞吐量,而rabbitmq相对注重可靠性,因为应用在im上,需要保证消息不能丢失所以就暂时选定rabbitmq,
    Message Queue的需求由来已久,80年代最早在金融交易中,高盛等公司采用Teknekron公司的产品,当时的Message queuing软件叫做:the information bus(TIB)。 TIB被电信和通讯公司采用,路透社收购了Teknekron公司。之后,IBM开发了MQSeries,微软开发了Microsoft Message Queue(MSMQ)。这些商业MQ供应商的问题是厂商锁定,价格高昂。2001年,Java Message queuing试图解决锁定和交互性的问题,但对应用来说反而更加麻烦了。
    RabbitMQ采用Erlang语言开发。Erlang语言由Ericson设计,专门为开发concurrent和distribution系统的一种语言,在电信领域使用广泛。OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件/库/工具,如mnesia/SASL,极大方便了Erlang应用的开发。OTP就类似于Python语言中众多的module,用户借助这些module可以很方便的开发应用。
    于是2004年,摩根大通和iMatrix开始着手Advanced Message Queuing Protocol (AMQP)开放标准的开发。2006年,AMQP规范发布。2007年,Rabbit技术公司基于AMQP标准开发的RabbitMQ 1.0 发布。所有主要的编程语言均有与代理接口通讯的客户端库。

    +

    简单的使用经验

    通俗的理解

    这里介绍下其中的一些概念,connection表示和队列服务器的连接,一般情况下是tcp连接, channel表示通道,可以在一个连接上建立多个通道,这里主要是节省了tcp连接握手的成本,exchange可以理解成一个路由器,将消息推送给对应的队列queue,其实是像一个订阅的模式。

    +

    集群经验

    rabbitmqctl stop这个是关闭rabbitmq,在搭建集群时候先关闭服务,然后使用rabbitmq-server -detached静默启动,这时候使用rabbitmqctl cluster_status查看集群状态,因为还没将节点加入集群,所以只能看到类似

    +
    Cluster status of node rabbit@rabbit1 ...
    +[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]},
    + {running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}]
    +...done.
    +

    然后就可以把当前节点加入集群,

    +
    rabbit2$ rabbitmqctl stop_app #这个stop_app与stop的区别是前者停的是rabbitmq应用,保留erlang节点,
    +                              #后者是停止了rabbitmq和erlang节点
    +Stopping node rabbit@rabbit2 ...done.
    +rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1 #这里可以用--ram指定将当前节点作为内存节点加入集群
    +Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done.
    +rabbit2$ rabbitmqctl start_app
    +Starting node rabbit@rabbit2 ...done.
    +

    其他可以参考官方文档

    +

    一些坑

    消息丢失

    这里碰到过一个坑,对于使用exchange来做消息路由的,会有一个情况,就是在routing_key没被订阅的时候,会将该条找不到路由对应的queue的消息丢掉What happens if we break our contract and send a message with one or four words, like "orange" or "quick.orange.male.rabbit"? Well, these messages won't match any bindings and will be lost.对应链接,而当使用空的exchange时,会保留消息,当出现消费者的时候就可以将收到之前生产者所推送的消息对应链接,这里就是用了空的exchange。

    +

    集群搭建

    集群搭建的时候有个erlang vm生成的random cookie,这个是用来做集群之间认证的,相同的cookie才能连接,但是如果通过vim打开复制后在其他几点新建文件写入会多一个换行,导致集群建立是报错,所以这里最好使用scp等传输命令直接传输cookie文件,同时要注意下cookie的文件权限。
    另外在集群搭建的时候如果更改过hostname,那么要把rabbitmq的数据库删除,否则启动后会马上挂掉

    ]]>
    - Docker - 介绍 + php - Docker - namespace - cgroup + php + mq + im
    - dubbo 客户端配置的一个重要知识点 - /2022/06/11/dubbo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E4%B8%80%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9F%A5%E8%AF%86%E7%82%B9/ - 在配置项目中其实会留着比较多的问题,由于不同的项目没有比较统一的规划和框架模板,一般都是只有创建者会比较了解(可能也不了解),譬如前阵子在配置一个 springboot + dubbo 的项目,发现了dubbo 连接注册中间客户端的问题,这里可以结合下代码来看
    比如有的应用是用的这个

    -
    <dependency>
    -    <groupId>org.apache.curator</groupId>
    -    <artifactId>curator-client</artifactId>
    -    <version>${curator.version}</version>            
    -</dependency>
    -<dependency>
    -    <groupId>org.apache.curator</groupId>
    -    <artifactId>curator-recipes</artifactId>
    -    <version>${curator.version}</version>
    -</dependency>
    -

    有个别应用用的是这个

    -
    <dependency>
    -    <groupId>com.101tec</groupId>
    -    <artifactId>zkclient</artifactId>
    -    <version>0.11</version>
    -</dependency>
    -

    还有的应用是找不到相关的依赖,并且这些的使用没有个比较好的说明,为啥用前者,为啥用后者,有啥注意点,
    首先在使用 2.6.5 的 alibaba 的 dubbo 的时候,只使用后者是会报错的,至于为啥会报错,其实就是这篇文章想说明的点
    报错的内容其实很简单, 就是缺少这个 org.apache.curator.framework.CuratorFrameworkFactory
    这个类看着像是依赖上面的配置,但是应该不需要两个配置一块用的,所以还是需要去看代码
    通过找上面类被依赖的和 dubbo 连接注册中心相关的代码,看到了这段指点迷津的代码

    -
    @SPI("curator")
    -public interface ZookeeperTransporter {
    -
    -    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    -    ZookeeperClient connect(URL url);
    +    redis 的 rdb 和 COW 介绍
    +    /2021/08/15/redis-%E7%9A%84-rdb-%E5%92%8C-COW-%E4%BB%8B%E7%BB%8D/
    +    redis 在使用 rdb 策略进行备份时,rdb 的意思是会在开启备份的时候将开启时间点的内存数据进行备份,并且可以设置时间,这样子就是这个策略其实还是不完全可靠的,如果是在这个间隔中宕机了,或者间隔过长,不过这个不在这次的要说的内容中,如果自己去写这个 rdb 的策略可能就有点类似于 mvcc 的 redolog,需要知道这个时间点之前的数据是怎么样的,防止后面更改的干扰,但是这样一方面需要有比较复杂的 mvcc 实现,另一方面是很占用存储空间,所以 redis 在这里面使用了 COW (Copy On Write) 技术,这个技术呢以前听过,也大致了解是怎么个意思,这次稍微具体地来看下,其实 redis 的 copy-on-write 就是来自于 linux 的 cow

    +

    Linux中的CopyOnWrite

    fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。这个操作其实可以类比为写屏障,正常的读取是没问题的,当有写入时就会分裂。

    +

    CopyOnWrite的好处:

    1、减少分配和复制资源时带来的瞬时延迟;
    2、减少不必要的资源分配;
    CopyOnWrite的缺点:
    1、如果父子进程都需要进行大量的写操作,会产生大量的分页错误(页异常中断page-fault);

    +

    Redis中的CopyOnWrite

    Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
    总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
    而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。这里其实更巧妙了,在细节上去优化会产生大量页异常中断的情况。

    +]]>
    + + redis + + + redis + + + + redis数据结构介绍-第一部分 SDS,链表,字典 + /2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ + redis是现在服务端很常用的缓存中间件,其实原来还有memcache之类的竞品,但是现在貌似 redis 快一统江湖,这里当然不是在吹,只是个人角度的一个感觉,不权威只是主观感觉。
    redis 主要有五种数据结构,StringsListsSetsHashesSorted Sets,这五种数据结构先简单介绍下,Strings类型的其实就是我们最常用的 key-value,实际开发中也会用的最多;Lists是列表,这个有些会用来做队列,因为 redis 目前常用的版本支持丰富的列表操作;还有是Sets集合,这个主要的特点就是集合中元素不重复,可以用在有这类需求的场景里;Hashes是叫散列,类似于 Python 中的字典结构;还有就是Sorted Sets这个是个有序集合;一眼看这些其实没啥特别的,除了最后这个有序集合,不过去了解背后的实现方式还是比较有意思的。

    +

    SDS 简单动态字符串

    先从Strings开始说,了解过 C 语言的应该知道,C 语言中的字符串其实是个 char[] 字符数组,redis 也不例外,只是最开始的版本就对这个做了一丢丢的优化,而正是这一丢丢的优化,让这个 redis 的使用效率提升了数倍

    +
    struct sdshdr {
    +    // 字符串长度
    +    int len;
    +    // 字符串空余字符数
    +    int free;
    +    // 字符串内容
    +    char buf[];
    +};
    +

    这里引用了 redis 在 github 上最早的 2.2 版本的代码,代码路径是https://github.com/antirez/redis/blob/2.2/src/sds.h,可以看到这个结构体里只有仨元素,两个 int 型和一个 char 型数组,两个 int 型其实就是我说的优化,因为 C 语言本身的字符串数组,有两个问题,一个是要知道它实际已被占用的长度,需要去遍历这个数组,第二个就是比较容易踩坑的是遍历的时候要注意它有个以\0作为结尾的特点;通过上面的两个 int 型参数,一个是知道字符串目前的长度,一个是知道字符串还剩余多少位空间,这样子坐着两个操作从 O(N)简化到了O(1)了,还有第二个 free 还有个比较重要的作用就是能防止 C 字符串的溢出问题,在存储之前可以先判断 free 长度,如果长度不够就先扩容了,先介绍到这,这个系列可以写蛮多的,慢慢介绍吧

    +

    链表

    链表是比较常见的数据结构了,但是因为 redis 是用 C 写的,所以在不依赖第三方库的情况下只能自己写一个了,redis 的链表是个有头的链表,而且是无环的,具体的结构我也找了 github 上最早版本的代码

    +
    typedef struct listNode {
    +    // 前置节点
    +    struct listNode *prev;
    +    // 后置节点
    +    struct listNode *next;
    +    // 值
    +    void *value;
    +} listNode;
     
    -}
    -

    众所周知,dubbo 创造了叫自适应扩展点加载的神奇技术,这里的 adaptive 注解中的Constants.CLIENT_KEYConstants.TRANSPORTER_KEY 可以在配置 dubbo 的注册信息的时候进行配置,如果是通过 xml 配置的话,可以在 <dubbo:registry/> 这个 tag 中的以上两个 key 进行配置,
    具体在 dubbo.xsd 中有描述

    -
    <xsd:element name="registry" type="registryType">
    -        <xsd:annotation>
    -            <xsd:documentation><![CDATA[ The registry config ]]></xsd:documentation>
    -        </xsd:annotation>
    -    </xsd:element>
    +typedef struct list { + // 链表表头 + listNode *head; + // 当前节点,也可以说是最后节点 + listNode *tail; + // 节点复制函数 + void *(*dup)(void *ptr); + // 节点值释放函数 + void (*free)(void *ptr); + // 节点值比较函数 + int (*match)(void *ptr, void *key); + // 链表包含的节点数量 + unsigned int len; +} list;
    +

    代码地址是这个https://github.com/antirez/redis/blob/2.2/src/adlist.h
    可以看下节点是由listNode承载的,包括值和一个指向前节点跟一个指向后一节点的两个指针,然后值是 void 指针类型,所以可以承载不同类型的值
    然后是 list结构用来承载一个链表,包含了表头,和表尾,复制函数,释放函数和比较函数,还有链表长度,因为包含了前两个节点,找到表尾节点跟表头都是 O(1)的时间复杂度,还有节点数量,其实这个跟 SDS 是同一个做法,就是空间换时间,这也是写代码里比较常见的做法,以此让一些高频的操作提速。

    +

    字典

    字典也是个常用的数据结构,其实只是叫法不同,数据结构中叫 hash 散列,Java 中叫 Map,PHP 中是数组 array,Python 中也叫字典 dict,因为纯 C 语言本身不带这些数据结构,所以这也是个痛并快乐着的过程,享受 C 语言的高性能的同时也要接受它只提供了语言的基本功能的现实,各种轮子都需要自己造,redis 同样实现了自己的字典
    下面来看看代码

    +
    typedef struct dictEntry {
    +    void *key;
    +    void *val;
    +    struct dictEntry *next;
    +} dictEntry;
     
    -


    并且在 spi 的配置com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter 中可以看到

    -
    zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
    -curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
    +typedef struct dictType {
    +    unsigned int (*hashFunction)(const void *key);
    +    void *(*keyDup)(void *privdata, const void *key);
    +    void *(*valDup)(void *privdata, const void *obj);
    +    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    +    void (*keyDestructor)(void *privdata, void *key);
    +    void (*valDestructor)(void *privdata, void *obj);
    +} dictType;
     
    -zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
    -curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
    +/* 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;
    +    unsigned long size;
    +    unsigned long sizemask;
    +    unsigned long used;
    +} dictht;
     
    -zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
    -curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
    -

    而在上面的代码里默认的SPI 值是 curator,所以如果不配置,那就会报上面找不到类的问题,所以如果需要使用 zkclient 的,就需要在<dubbo:registry/> 配置中添加 client="zkclient"这个配置,所以有些地方还是需要懂一些更深层次的原理,但也不至于每个东西都要抠到每一行代码原理,除非就是专门做这一块的。
    还有一点是发现有些应用是碰运气,刚好有个三方包把这个类带进来了,但是这个应用就没有单独配置这块,如果不了解或者后续忘了再来查问题就会很奇怪

    +typedef struct dict { + dictType *type; + void *privdata; + dictht ht[2]; + int rehashidx; /* rehashing not in progress if rehashidx == -1 */ + int iterators; /* number of iterators currently running */ +} dict;
    +

    看了下这个 2.2 版本的代码跟最新版的其实也差的不是很多,所以还是照旧用老代码,可以看到上面四个结构体中,其实只有三个是存储数据用的,dictType 是用来放操作函数的,那么三个存放数据的结构体分别是干嘛的,这时候感觉需要一个图来说明比较好,稍等,我去画个图~

    这个图看着应该比较清楚这些都是用来干嘛的了,dict 是我们的主体结构,它有一个指向 dictType 的指针,这里面包含了字典的操作函数,然后是一个私有数据指针,接下来是一个 dictht 的数组,包含两个dictht,这个就是用来存数据的了,然后是 rehashidx 表示重哈希的状态,当是-1 的时候表示当前没有重哈希,iterators 表示正在遍历的迭代器的数量。
    首先说说为啥需要有两个 dictht,这是因为字典 dict 这个数据结构随着数据量的增减,会需要在中途做扩容或者缩容操作,如果只有一个的话,对它进行扩容缩容时会影响正常的访问和修改操作,或者说保证正常查询,修改的正确性会比较复杂,并且因为需要高效利用空间,不能一下子申请一个非常大的空间来存很少的数据。当 dict 中 dictht 中的数据量超过 size 的时候负载就超过了 1,就需要进行扩容,这里的其实跟 Java 中的 HashMap 比较类似,超过一定的负载之后进行扩容。这里为啥 size 会超过 1 呢,可能有部分不了解这类结构的同学会比较奇怪,其实就是上图中画的,在数据结构中对于散列的冲突有几类解决方法,比如转换成链表,二次散列,找下个空槽等,这里就使用了链表法,或者说拉链法。当一个新元素通过 hashFunction 得出的 key 跟 sizemask 取模之后的值相同了,那就将其放在原来的节点之前,变成链表挂在数组 dictht.table下面,放在原有节点前是考虑到可能会优先访问。
    忘了说明下 dictht 跟 dictEntry 的关系了,dictht 就是个哈希表,它里面是个dictEntry 的二维数组,而 dictEntry 是个包含了 key-value 结构之外还有一个 next 指针,因此可以将哈希冲突的以链表的形式保存下来。
    在重点说下重哈希,可能同样写 Java 的同学对这个比较有感觉,跟 HashMap 一样,会以 2 的 N 次方进行扩容,那么扩容的方法就会比较简单,每个键重哈希要不就在原来这个槽,要不就在原来的槽加原 dictht.size 的位置;然后是重头戏,具体是怎么做扩容呢,其实这里就把第二个 ht 用上了,其实这两个hashtable 的具体作用有点类似于 jvm 中的两个 survival 区,但是又不全一样,因为 redis 在扩容的时候是采用的渐进式地重哈希,什么叫渐进式的呢,就是它不是像 jvm 那种标记复制的模式直接将一个 eden 区和原来的 survival 区存活的对象复制到另一个 survival 区,而是在每一次添加,删除,查找或者更新操作时,都会额外的帮忙搬运一部分的原 dictht 中的数据,这里会根据 rehashidx 的值来判断,如果是-1 表示并没有在重哈希中,如果是 0 表示开始重哈希了,然后rehashidx 还会随着每次的帮忙搬运往上加,但全部被搬运完成后 rehashidx 又变回了-1,又可以扯到Java 中的 Concurrent HashMap, 他在扩容的时候也使用了类似的操作。

    ]]>
    - Java - Dubbo + Redis + 数据结构 + C + 源码 + Redis - Java - Dubbo + redis + 数据结构 + 源码
    - docker比一般多一点的初学者介绍二 - /2020/03/15/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%BA%8C/ - 限制下 docker 的 cpu 使用率

    这里我们开始玩一点有意思的,我们在容器里装下 vim 和 gcc,然后写这样一段 c 代码

    -
    #include <stdio.h>
    -int main(void)
    -{
    -    int i = 0;
    -    for(;;) i++;
    -    return 0;
    -}
    -

    就是一个最简单的死循环,然后在容器里跑起来

    -
    $ gcc 1.c 
    -$ ./a.out
    -

    然后我们来看下系统资源占用(CPU)
    Xs562iawhHyMxeO
    上图是在容器里的,可以看到 cpu 已经 100%了
    然后看看容器外面的
    ecqH8XJ4k7rKhzu
    可以看到一个核的 cpu 也被占满了,因为是个双核的机器,并且代码是单线程的
    然后呢我们要做点啥
    因为已经在这个 ubuntu 容器中装了 vim 和 gcc,考虑到国内的网络,所以我们先把这个容器 commit 一下,

    -
    docker commit -a "nick" -m "my ubuntu" f63c5607df06 my_ubuntu:v1
    -

    然后再运行起来

    -
    docker run -it --cpus=0.1 my_ubuntu:v1 bash
    -


    我们的代码跟可执行文件都还在,要的就是这个效果,然后再运行一下

    结果是这个样子的,有点神奇是不,关键就在于 run 的时候的--cpus=0.1这个参数,它其实就是基于我前一篇说的 cgroup 技术,能将进程之间的cpu,内存等资源进行隔离

    -

    开始第一个 Dockerfile

    上一面为了复用那个我装了 vim 跟 gcc 的容器,我把它提交到了本地,使用了docker commit命令,有点类似于 git 的 commit,但是这个不是个很好的操作方式,需要手动介入,这里更推荐使用 Dockerfile 来构建镜像

    -
    From ubuntu:latest
    -MAINTAINER Nicksxs "nicksxs@hotmail.com"
    -RUN  sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
    -RUN apt-get clean
    -RUN apt-get update && apt install -y nginx
    -RUN echo 'Hi, i am in container' \
    -    > /usr/share/nginx/html/index.html
    -EXPOSE 80
    -

    先解释下这是在干嘛,首先是这个From ubuntu:latest基于的 ubuntu 的最新版本的镜像,然后第二行是维护人的信息,第三四行么作为墙内人你懂的,把 ubuntu 的源换成阿里云的,不然就有的等了,然后就是装下 nginx,往默认的 nginx 的入口 html 文件里输入一行欢迎语,然后暴露 80 端口
    然后我们使用sudo docker build -t="nicksxs/static_web" .命令来基于这个 Dockerfile 构建我们自己的镜像,过程中是这样的


    可以看到图中,我的 Dockerfile 是 7 行,里面就执行了 7 步,并且每一步都有一个类似于容器 id 的层 id 出来,这里就是一个比较重要的东西,docker 在构建的时候其实是有这个层的概念,Dockerfile 里的每一行都会往上加一层,这里有还注意下命令后面的.,代表当前目录下会自行去寻找 Dockerfile 进行构建,构建完了之后我们再看下我们的本地镜像

    我们自己的镜像出现啦
    然后有个问题,如果这个构建中途报了错咋办呢,来试试看,我们把 nginx 改成随便的一个错误名,nginxx(不知道会不会运气好真的有这玩意),再来 build 一把

    找不到 nginxx 包,是不是这个镜像就完全不能用呢,当然也不是,因为前面说到了,docker 是基于层去构建的,可以看到前面的 4 个 step 都没报错,那我们基于最后的成功步骤创建下容器看看
    也就是sudo docker run -t -i bd26f991b6c8 /bin/bash
    答案是可以的,只是没装成功 nginx

    还有一点注意到没,前面的几个 step 都有一句 Using cache,说明 docker 在构建镜像的时候是有缓存的,这也更能说明 docker 是基于层去构建镜像,同样的底包,同样的步骤,这些层是可以被复用的,这就是 docker 的构建缓存,当然我们也可以在 build 的时候加上--no-cache去把构建缓存禁用掉。

    + redis数据结构介绍三-第三部分 整数集合 + /2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/ + redis中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

    +
    typedef struct intset {
    +    // 编码方式
    +    uint32_t encoding;
    +    // 集合包含的元素数量
    +    uint32_t length;
    +    // 保存元素的数组
    +    int8_t contents[];
    +} intset;
    +
    +/* Note that these encodings are ordered, so:
    + * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
    +#define INTSET_ENC_INT16 (sizeof(int16_t))
    +#define INTSET_ENC_INT32 (sizeof(int32_t))
    +#define INTSET_ENC_INT64 (sizeof(int64_t))
    +

    一眼看,为啥整型还需要编码,然后 int8_t 怎么能存下大整形呢,带着这些疑问,我们一步步分析下去,这里的编码其实指的是这个整型集合里存的究竟是多大的整型,16 位,还是 32 位,还是 64 位,结构体下面的宏定义就是表示了 encoding 的可能取值,INTSET_ENC_INT16 表示每个元素用2个字节存储,INTSET_ENC_INT32 表示每个元素用4个字节存储,INTSET_ENC_INT64 表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。length 就是正常的表示集合中元素的数量。最奇怪的应该就是这个 contents 了,是个 int8_t 的数组,那放毛线数据啊,最小的都有 16 位,这里我在看代码和《redis 设计与实现》的时候也有点懵逼,后来查了下发现这是个比较取巧的用法,这里我用自己的理解表述一下,先看看 8,16,32,64 的关系,一眼看就知道都是 2 的 N 次,并且呈两倍关系,而且 8 位刚好一个字节,所以呢其实这里的contents 不是个常规意义上的 int8_t 类型的数组,而是个柔性数组。看下 wiki 的定义

    +
    +

    Flexible array members1 were introduced in the C99 standard of the C programming language (in particular, in section §6.7.2.1, item 16, page 103).2 It is a member of a struct, which is an array without a given dimension. It must be the last member of such a struct and it must be accompanied by at least one other member, as in the following example:

    +
    +
    struct vectord {
    +    size_t len;
    +    double arr[]; // the flexible array member must be last
    +};
    +

    在初始化这个 intset 的时候,这个contents数组是不占用空间的,后面的反正用到了申请,那么这里就有一个问题,给出了三种可能的 encoding 值,他们能随便换吗,显然不行,首先在 intset 中数据的存放是有序的,这个有部分原因是方便二分查找,然后存放数据其实随着数据的大小不同会有一个升级的过程,看下图

    新创建的intset只有一个header,总共8个字节。其中encoding = 2, length = 0, 类型都是uint32_t,各占 4 字节,添加15, 5两个元素之后,因为它们是比较小的整数,都能使用2个字节表示,所以encoding不变,值还是2,也就是默认的 INTSET_ENC_INT16,当添加32768的时候,它不再能用2个字节来表示了(2个字节能表达的数据范围是-215~215-1,而32768等于215,超出范围了),因此encoding必须升级到INTSET_ENC_INT32(值为4),即用4个字节表示一个元素。在添加每个元素的过程中,intset始终保持从小到大有序。与ziplist类似,intset也是按小端(little endian)模式存储的(参见维基百科词条Endianness)。比如,在上图中intset添加完所有数据之后,表示encoding字段的4个字节应该解释成0x00000004,而第4个数据应该解释成0x00008000 = 32768

    ]]>
    - Docker - 介绍 + Redis + 数据结构 + C + 源码 + Redis - Docker - namespace - cgroup + redis + 数据结构 + 源码
    - docker使用中发现的echo命令的一个小技巧及其他 - /2020/03/29/echo%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E6%8A%80%E5%B7%A7/ - echo 实操技巧

    最近做 docker 系列,会经常需要进到 docker 内部,如上一篇介绍的,这些镜像一般都有用 ubuntu 或者alpine 这样的 Linux 系统作为底包,如果构建镜像的时候没有替换源的话,因为特殊的网络原因,在内部想编辑下东西要安装个类似于 vim 这样的编辑器就会很慢很慢,像视频里 two thousand years later~ 而且如果在容器内部想改源配置的话也要编辑器,就陷入了一个鸡生蛋,跟蛋生鸡的死锁问题中,对于 linux 大神来说应该有一万种方法解决这个问题,对于我这个渣渣来说可能只想到了这个土方法,先 cp backup 一下 sources.list, 再 echo “xxx” > sources.list, 这里就碰到了一个问题,这个 sources.list 一般不止一行,直接 echo 的话就解析不了了,不过 echo 可以支持”\n”转义,就是加-e看一下解释和示例,我这里使用了 tldr ,可以用 npm install -g tldr 安装,也可以直接用man, 或者–help 来查看使用方式

    -

    查看镜像底包

    还有一点也是在这个时候要安装 vim 之类的,得知道是什么镜像底包,如果是用 uname 指令,其实看到的是宿主机的系统,得用cat /etc/issue

    -


    这里稍稍记一下

    -

    寻找系统镜像源

    目前国内系统源用得比较多的是阿里云源,不过这里也推荐清华源, 中科大源, 浙大源 这里不要脸的推荐下母校的源,不过还不是很完善,尽情期待下。

    + redis数据结构介绍二-第二部分 跳表 + /2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ + 跳表 skiplist

    跳表是个在我们日常的代码中不太常用到的数据结构,相对来讲就没有像数组,链表,字典,散列,树等结构那么熟悉,所以就从头开始分析下,首先是链表,跳表跟链表都有个表字(太硬扯了我🤦‍♀️),注意这是个有序链表

    如上图,在这个链表里如果我要找到 23,是不是我需要从3,5,9开始一直往后找直到找到 23,也就是说时间复杂度是 O(N),N 的一次幂复杂度,那么我们来看看第二个

    这个结构跟原先有点不一样,它给链表中偶数位的节点又加了一个指针把它们链接起来,这样子当我们要寻找 23 的时候就可以从原来的一个个往下找变成跳着找,先找到 5,然后是 10,接着是 19,然后是 28,这时候发现 28 比 23 大了,那我在退回到 19,然后从下一层原来的链表往前找,

    这里毛估估是不是前面的节点我就少找了一半,有那么点二分法的意思。
    前面的其实是跳表的引子,真正的跳表其实不是这样,因为上面的其实有个比较大的问题,就是插入一个元素后需要调整每个元素的指针,在 redis 中的跳表其实是做了个随机层数的优化,因为沿着前面的例子,其实当数据量很大的时候,是不是层数越多,其查询效率越高,但是随着层数变多,要保持这种严格的层数规则其实也会增大处理复杂度,所以 redis 插入每个元素的时候都是使用随机的方式,看一眼代码

    +
    /* ZSETs use a specialized version of Skiplists */
    +typedef struct zskiplistNode {
    +    sds ele;
    +    double score;
    +    struct zskiplistNode *backward;
    +    struct zskiplistLevel {
    +        struct zskiplistNode *forward;
    +        unsigned long span;
    +    } level[];
    +} zskiplistNode;
    +
    +typedef struct zskiplist {
    +    struct zskiplistNode *header, *tail;
    +    unsigned long length;
    +    int level;
    +} zskiplist;
    +
    +typedef struct zset {
    +    dict *dict;
    +    zskiplist *zsl;
    +} zset;
    +

    忘了说了,redis 是把 skiplist 跳表用在 zset 里,zset 是个有序的集合,可以看到 zskiplist 就是个跳表的结构,里面用 header 保存跳表的表头,tail 保存表尾,还有长度和最大层级,具体的跳表节点元素使用 zskiplistNode 表示,里面包含了 sds 类型的元素值,double 类型的分值,用来排序,一个 backward 后向指针和一个 zskiplistLevel 数组,每个 level 包含了一个前向指针,和一个 span,span 表示的是跳表前向指针的跨度,这里再补充一点,前面说了为了灵活这个跳表的新增修改,redis 使用了随机层高的方式插入新节点,但是如果所有节点都随机到很高的层级或者所有都很低的话,跳表的效率优势就会减小,所以 redis 使用了个小技巧,贴下代码

    +
    #define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
    +int zslRandomLevel(void) {
    +    int level = 1;
    +    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
    +        level += 1;
    +    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    +}
    +

    当随机值跟0xFFFF进行与操作小于ZSKIPLIST_P * 0xFFFF时才会增大 level 的值,因此保持了一个相对递减的概率
    可以简单分析下,当 random() 的值小于 0xFFFF 的 1/4,才会 level + 1,就意味着当有 1 - 1/4也就是3/4的概率是直接跳出,所以一层的概率是3/4,也就是 1-P,二层的概率是 P*(1-P),三层的概率是 P² * (1-P) 依次递推。

    ]]>
    - Linux - Docker - 命令 - echo - 发行版本 + Redis + 数据结构 + C + 源码 + Redis - linux - Docker - echo - uname - 发行版 + redis + 数据结构 + 源码
    - invert-binary-tree - /2015/06/22/invert-binary-tree/ - Invert a binary tree

    -
         4
    -   /   \
    -  2     7
    - / \   / \
    -1   3 6   9
    -
    -

    to

    -
         4
    -   /   \
    -  7     2
    - / \   / \
    -9   6 3   1
    -
    -

    Trivia:
    This problem was inspired by this original tweet by Max Howell:

    -
    -

    Google: 90% of our engineers use the software you wrote (Homebrew),
    but you can’t invert a binary tree on a whiteboard so fuck off.

    -
    -
    /**
    - * Definition for a binary tree node.
    - * struct TreeNode {
    - *     int val;
    - *     TreeNode *left;
    - *     TreeNode *right;
    - *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    - * };
    - */
    -class Solution {
    -public:
    -    TreeNode* invertTree(TreeNode* root) {
    -        if(root == NULL) return root;
    -        TreeNode* temp;
    -        temp = invertTree(root->left);
    -        root->left = invertTree(root->right);
    -        root->right = temp;
    -        return root;
    -    }
    -};
    ]]>
    + redis数据结构介绍五-第五部分 对象 + /2020/01/20/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%94/ + 前面说了这么些数据结构,其实大家对于 redis 最初的印象应该就是个 key-value 的缓存,类似于 memcache,redis 其实也是个 key-value,key 还是一样的字符串,或者说就是用 redis 自己的动态字符串实现,但是 value 其实就是前面说的那些数据结构,差不多快说完了,还有个 quicklist 后面还有一篇,这里先介绍下 redis 对于这些不同类型的 value 是怎么实现的,首先看下 redisObject 的源码头文件

    +
    /* 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. */
    +/*
    + * 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_LINKEDLIST 4 /* No longer used: old list encoding. */
    +#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 */
    +
    +#define LRU_BITS 24
    +#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
    +#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
    +
    +#define OBJ_SHARED_REFCOUNT INT_MAX
    +typedef 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;
    +

    主体结构就是这个 redisObject,

    +
      +
    • type: 字段表示对象的类型,它对应的就是 redis 的对外暴露的,或者说用户可以使用的五种类型,OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
    • +
    • encoding: 字段表示这个对象在 redis 内部的编码方式,由OBJ_ENCODING_开头的 11 种
    • +
    • lru: 做LRU替换算法用,占24个bit
    • +
    • refcount: 引用计数。它允许robj对象在某些情况下被共享。
    • +
    • ptr: 指向底层实现数据结构的指针
      当 type 是 OBJ_STRING 时,表示类型是个 string,它的编码方式 encoding 可能有 OBJ_ENCODING_RAW,OBJ_ENCODING_INT,OBJ_ENCODING_EMBSTR 三种
      当 type 是 OBJ_LIST 时,表示类型是 list,它的编码方式 encoding 是 OBJ_ENCODING_QUICKLIST,对于早一些的版本,2.2这种可能还会使用 OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_LINKEDLIST
      当 type 是 OBJ_SET 时,是个集合,但是得看具体元素的类型,有可能使用整数集合,OBJ_ENCODING_INTSET, 如果元素不全是整型或者数量超过一定限制,那么编码就是 OBJ_ENCODING_HT hash table 了
      当 type 是 OBJ_ZSET 时,是个有序集合,它底层有可能使用的是 OBJ_ENCODING_ZIPLIST 或者 OBJ_ENCODING_SKIPLIST
      当 type 是 OBJ_HASH 时,一开始也是 OBJ_ENCODING_ZIPLIST,然后当数据量大于 hash_max_ziplist_entries 时会转成 OBJ_ENCODING_HT
    • +
    +]]>
    - leetcode + Redis + 数据结构 + C + 源码 + Redis - leetcode - c++ + redis + 数据结构 + 源码
    - minimum-size-subarray-sum-209 - /2016/10/11/minimum-size-subarray-sum-209/ - problem

    Given an array of n positive integers and a positive integer s, find the minimal length of a subarray of which the sum ≥ s. If there isn’t one, return 0 instead.

    -

    For example, given the array [2,3,1,2,4,3] and s = 7,
    the subarray [4,3] has the minimal length under the problem constraint.

    -

    题解

    参考,滑动窗口,跟之前Data Structure课上的online算法有点像,链接

    -

    Code

    class Solution {
    -public:
    -    int minSubArrayLen(int s, vector<int>& nums) {
    -        int len = nums.size();
    -        if(len == 0) return 0;
    -        int minlen = INT_MAX;
    -        int sum = 0;
    -        
    -        int left = 0;
    -        int right = -1;
    -        while(right < len)
    -        {
    -            while(sum < s && right < len)
    -                sum += nums[++right];
    -            if(sum >= s)
    -            {
    -                minlen = minlen < right - left + 1 ? minlen : right - left + 1;
    -                sum -= nums[left++];
    -            }
    -        }
    -        return minlen > len ? 0 : minlen;
    +    redis数据结构介绍六 快表
    +    /2020/01/22/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%85%AD/
    +    这应该是 redis 系列的最后一篇了,讲下快表,其实最前面讲的链表在早先的 redis 版本中也作为 list 的数据结构使用过,但是单纯的链表的缺陷之前也说了,插入便利,但是空间利用率低,并且不能进行二分查找等,检索效率低,ziplist 压缩表的产生也是同理,希望获得更好的性能,包括存储空间和访问性能等,原来我也不懂这个快表要怎么快,然后明白了一个道理,其实并没有什么银弹,只是大牛们会在适合的时候使用最适合的数据结构来实现性能的最大化,这里面有一招就是不同数据结构的组合调整,比如 Java 中的 HashMap,在链表节点数大于 8 时会转变成红黑树,以此提高访问效率,不费话了,回到快表,quicklist,这个数据结构主要使用在 list 类型中,如果我说其实这个 quicklist 就是个链表,可能大家不太会相信,但是事实上的确可以认为 quicklist 是个双向链表,看下代码

    +
    /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
    + * We use bit fields keep the quicklistNode at 32 bytes.
    + * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
    + * encoding: 2 bits, RAW=1, LZF=2.
    + * container: 2 bits, NONE=1, ZIPLIST=2.
    + * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
    + * attempted_compress: 1 bit, boolean, used for verifying during testing.
    + * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
    +typedef struct quicklistNode {
    +    struct quicklistNode *prev;
    +    struct quicklistNode *next;
    +    unsigned char *zl;
    +    unsigned int sz;             /* ziplist size in bytes */
    +    unsigned int count : 16;     /* count of items in ziplist */
    +    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    +    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    +    unsigned int recompress : 1; /* was this node previous compressed? */
    +    unsigned int attempted_compress : 1; /* node can't compress; too small */
    +    unsigned int extra : 10; /* more bits to steal for future usage */
    +} quicklistNode;
    +
    +/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
    + * 'sz' is byte length of 'compressed' field.
    + * 'compressed' is LZF data with total (compressed) length 'sz'
    + * NOTE: uncompressed length is stored in quicklistNode->sz.
    + * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
    +typedef struct quicklistLZF {
    +    unsigned int sz; /* LZF size in bytes*/
    +    char compressed[];
    +} quicklistLZF;
    +
    +/* 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. */
    +typedef struct quicklist {
    +    quicklistNode *head;
    +    quicklistNode *tail;
    +    unsigned long count;        /* total count of all entries in all ziplists */
    +    unsigned long len;          /* number of quicklistNodes */
    +    int fill : 16;              /* fill factor for individual nodes */
    +    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
    +} quicklist;
    +

    粗略看下,quicklist 里有 head,tail, quicklistNode里有 prev,next 指针,是不是有链表的基本轮廓了,那么为啥这玩意要称为快表呢,快在哪,关键就在这个unsigned char *zl;zl 是不是前面又看到过,就是 ziplist ,这是什么鬼,链表里用压缩表,这不套娃么,先别急,回顾下前面说的 ziplist,ziplist 有哪些特点,内存利用率高,可以从表头快速定位到尾节点,节点可以从后往前找,但是有个缺点,就是从中间插入的效率比较低,需要整体往后移,这个其实是普通数组的优化版,但还是有数组的一些劣势,所以要真的快,是不是可以将链表跟数组真的结合起来。

    +

    ziplist

    这里有两个 redis 的配置参数,list-max-ziplist-sizelist-compress-depth,先来说第一个,既然快表是将链表跟压缩表数组结合起来使用,那么具体怎么用呢,比如我有一个 10 个元素的 list,那具体怎么放,每个 quicklistNode 里放多大的 ziplist,假如每个快表节点的 ziplist 只放一个元素,那么其实这就退化成了一个链表,如果 10 个元素放在一个 quicklistNode 的 ziplist 里,那就退化成了一个 ziplist,所以有了这个 list-max-ziplist-size,而且它还比较牛,能取正负值,当是正值时,对应的就是每个 quicklistNode 的 ziplist 中的元素个数,比如配置了 list-max-ziplist-size = 5,那么我刚才的 10 个元素的 list 就是一个两个 quicklistNode 组成的快表,每个 quicklistNode 中的 ziplist 包含了五个元素,当 list-max-ziplist-size取负值的时候,它限制了 ziplist 的字节数

    +
    size_t offset = (-fill) - 1;
    +if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
    +    if (sz <= optimization_level[offset]) {
    +        return 1;
    +    } else {
    +        return 0;
    +    }
    +} else {
    +    return 0;
    +}
    +
    +/* Optimization levels for size-based filling */
    +static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
    +
    +/* Create a new quicklist.
    + * Free with quicklistRelease(). */
    +quicklist *quicklistCreate(void) {
    +    struct quicklist *quicklist;
    +
    +    quicklist = zmalloc(sizeof(*quicklist));
    +    quicklist->head = quicklist->tail = NULL;
    +    quicklist->len = 0;
    +    quicklist->count = 0;
    +    quicklist->compress = 0;
    +    quicklist->fill = -2;
    +    return quicklist;
    +}
    +

    这个 fill 就是传进来的 list-max-ziplist-size, 具体对应的就是

    +
      +
    • -5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
    • +
    • -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
    • +
    • -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
    • +
    • -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)也就是上面的 quicklist->fill = -2;
    • +
    • -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
    • +
    +

    压缩

    list-compress-depth这个参数呢是用来配置压缩的,等等压缩是为啥,不是里面已经是压缩表了么,大牛们就是为了性能殚精竭虑,这里考虑到的是一个场景,一般状况下,list 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示

    +
    /* depth of end nodes not to compress;0=off */
    +
      +
    • 0,代表不压缩,默认值
    • +
    • 1,两端各一个节点不压缩
    • +
    • 2,两端各两个节点不压缩
    • +
    • … 依次类推
      压缩后的 ziplist 就会变成 quicklistLZF,然后替换 zl 指针,这里使用的是 LZF 压缩算法,压缩后的 quicklistLZF 中的 compressed 也是个柔性数组,压缩后的 ziplist 整个就放进这个柔性数组
    • +
    +

    插入过程

    简单说下插入元素的过程

    +
    /* Wrapper to allow argument-based switching between HEAD/TAIL pop */
    +void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
    +                   int where) {
    +    if (where == QUICKLIST_HEAD) {
    +        quicklistPushHead(quicklist, value, sz);
    +    } else if (where == QUICKLIST_TAIL) {
    +        quicklistPushTail(quicklist, value, sz);
    +    }
    +}
    +
    +/* Add new entry to head node of quicklist.
    + *
    + * Returns 0 if used existing head.
    + * Returns 1 if new head created. */
    +int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    +    quicklistNode *orig_head = quicklist->head;
    +    if (likely(
    +            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
    +        quicklist->head->zl =
    +            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
    +        quicklistNodeUpdateSz(quicklist->head);
    +    } else {
    +        quicklistNode *node = quicklistCreateNode();
    +        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
    +
    +        quicklistNodeUpdateSz(node);
    +        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
         }
    -};
    -]]>
    - - leetcode - - - leetcode - c++ - - - - gogs使用webhook部署react单页应用 - /2020/02/22/gogs%E4%BD%BF%E7%94%A8webhook%E9%83%A8%E7%BD%B2react%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8/ - 众所周知,我是个前端彩笔,但是也想做点自己可以用的工具页面,所以就让朋友推荐了蚂蚁出品的 ant design,说基本可以直接 ctrl-c ctrl-v,实测对我这种来说还是有点难的,不过也能写点,但是现在碰到的问题是怎么部署到自己的服务器上去
    用 ant design 写的是个单页应用,实际来说就是一个 html 加 css 跟 js,最初的时候是直接 build 完就 scp 上去,也考虑过 rsync 之类的,但是都感觉不够自动化,正好自己还没这方面的经验就想折腾下,因为我自己搭的仓库应用是 gogs,搜了一下主要是跟 drones 配合做 ci/cd,研究了一下发现其实这个事情没必要这么搞(PS:drone 也不好用),整个 hook 就可以了, 但是实际上呢,这东西也不是那么简单
    首先是需要在服务器上装 webhook,这个我一开始用 snap 安装,但是出现问题,run 的时候会出现后面参数带的 hooks.json 文件找不到,然后索性就直接 github 上下最新版,放 /usr/local/bin 了,webhook 的原理呢其实也比较简单,就是起一个 http 服务,通过 post 请求调用,解析下参数,如果跟配置的参数一致,就调用对应的命令或者脚本。

    -

    配置 hooks.json

    webhook 的配置,需要的两个文件,一个是 hooks.json,这个是 webhook 服务的配置文件,像这样

    -
    [
    -  {
    -    "id": "redeploy-app",
    -    "execute-command": "/opt/scripts/redeploy.sh",
    -    "command-working-directory": "/opt/scripts",
    -    "pass-arguments-to-command":
    -    [
    -      {
    -        "source": "payload",
    -        "name": "head_commit.message"
    -      },
    -      {
    -        "source": "payload",
    -        "name": "pusher.name"
    -      },
    -      {
    -        "source": "payload",
    -        "name": "head_commit.id"
    -      }
    -    ],
    -    "trigger-rule":
    -    {
    -      "and":
    -      [
    -        {
    -          "match":
    -          {
    -            "type": "payload-hash-sha1",
    -            "secret": "your-github-secret",
    -            "parameter":
    -            {
    -              "source": "header",
    -              "name": "X-Hub-Signature"
    -            }
    -          }
    -        },
    -        {
    -          "match":
    -          {
    -            "type": "value",
    -            "value": "refs/heads/master",
    -            "parameter":
    -            {
    -              "source": "payload",
    -              "name": "ref"
    -            }
    -          }
    -        }
    -      ]
    -    }
    -  }
    -]
    + quicklist->count++; + quicklist->head->count++; + return (orig_head != quicklist->head); +} -

    这是个跟 github搭配的示例,首先 id 表示的是这个对应 hook 的识别 id,也可以看到这个 hooks.json 的结构是这样的一个数组,然后就是要执行的命令和命令执行的参数,值得注意的是这个trigger-rule,就是请求进来了回去匹配里面的,比如前一个是一个加密的,放在请求头里,第二个 match 表示请求里的 ref 是个 master 分支,就可以区分分支进行不同操作,但是前面的加密配合 gogs 使用的时候有个问题(PS: webhook 的文档是真的烂),gogs 设置 webhook 的加密是用的

    -
    -

    密钥文本将被用于计算推送内容的 SHA256 HMAC 哈希值,并设置为 X-Gogs-Signature 请求头的值。

    -
    -

    这种加密方式,所以 webhook 的这个示例的加密方式不行,但这货的文档里居然没有说明支持哪些加密,神TM,后来还是翻 issue 翻到了, 需要使用这个payload-hash-sha256

    -

    执行脚本 redeploy.sh

    脚本类似于这样

    -
    #!/bin/bash -e
    +/* Add new entry to tail node of quicklist.
    + *
    + * Returns 0 if used existing tail.
    + * Returns 1 if new tail created. */
    +int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    +    quicklistNode *orig_tail = quicklist->tail;
    +    if (likely(
    +            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
    +        quicklist->tail->zl =
    +            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
    +        quicklistNodeUpdateSz(quicklist->tail);
    +    } else {
    +        quicklistNode *node = quicklistCreateNode();
    +        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
     
    -function cleanup {
    -      echo "Error occoured"
    -}
    -trap cleanup ERR
    +        quicklistNodeUpdateSz(node);
    +        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    +    }
    +    quicklist->count++;
    +    quicklist->tail->count++;
    +    return (orig_tail != quicklist->tail);
    +}
     
    -commit_message=$1 # head_commit.message
    -pusher_name=$2 # pusher.name
    -commit_id=$3 # head_commit.id
    +/* Wrappers for node inserting around existing node. */
    +REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
    +                                             quicklistNode *old_node,
    +                                             quicklistNode *new_node) {
    +    __quicklistInsertNode(quicklist, old_node, new_node, 0);
    +}
     
    +REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
    +                                            quicklistNode *old_node,
    +                                            quicklistNode *new_node) {
    +    __quicklistInsertNode(quicklist, old_node, new_node, 1);
    +}
     
    -cd ~/do-react-example-app/
    -git pull origin master
    -yarn && yarn build
    +/* Insert 'new_node' after 'old_node' if 'after' is 1. + * Insert 'new_node' before 'old_node' if 'after' is 0. + * Note: 'new_node' is *always* uncompressed, so if we assign it to + * head or tail, we do not need to uncompress it. */ +REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist, + quicklistNode *old_node, + quicklistNode *new_node, int after) { + if (after) { + new_node->prev = old_node; + if (old_node) { + new_node->next = old_node->next; + if (old_node->next) + old_node->next->prev = new_node; + old_node->next = new_node; + } + if (quicklist->tail == old_node) + quicklist->tail = new_node; + } else { + new_node->next = old_node; + if (old_node) { + new_node->prev = old_node->prev; + if (old_node->prev) + old_node->prev->next = new_node; + old_node->prev = new_node; + } + if (quicklist->head == old_node) + quicklist->head = new_node; + } + /* If this insert creates the only element so far, initialize head/tail. */ + if (quicklist->len == 0) { + quicklist->head = quicklist->tail = new_node; + } -

    就是简单的拉代码,然后构建下,真实使用时可能不是这样,因为页面会部署在 nginx 的作用目录,还需要 rsync 过去,这部分可能还涉及到两个问题第一个是使用 rsync 还是其他的 cp,不过这个无所谓;第二个是目录权限的问题,以我的系统ubuntu 为例,默认用户是 ubuntu,nginx 部署的目录是 www,所以需要切换用户等操作,一开始是想用在shell 文件中直接写了密码,但是不知道咋传,查了下是类似于这样 echo "passwd" | sudo -S cmd,通过管道命令往后传,然后就是这个-S, 参数的解释是-S, --stdin read password from standard input,但是这样么也不是太安全的赶脚,又看了下还有两种方法,

    -
      -
    • 就是给root 设置一个不需要密码的命令类似于这样,

      -
      myusername ALL = (ALL) ALL
      -myusername ALL = (root) NOPASSWD: /path/to/my/program
      -
    • -
    • 另一种就是把默认用户跟 root 设置成同一个 group 的

      -
    • -
    -

    使用

    真正实操的时候其实还有不少问题,首先运行 webhook 就碰到了我前面说的,使用 snap 运行的时候会找不到前面的 hooks.json配置文件,执行snap run webhook -hooks /opt/hooks/hooks.json -verbose就碰到下面的couldn't load hooks from file! open /opt/hooks/hooks.json: no such file or directory,后来直接下了个官方最新的 release,就直接执行 webhook -hooks /opt/hooks/hooks.json -verbose 就可以了,然后是前面的示例配置文件里的几个参数,比如head_commit.message 其实 gogs 推过来的根本没这玩意,而且都是数组,不知道咋取,烂文档,不过总比搭个 drone 好一点就忍了。补充一点就是在 debug 的时候需要看下问题出在哪,看看脚本有没有执行,所以需要在前面的 json 里加这个参数"include-command-output-in-response": true, 就能输出来脚本执行结果

    -]]>
    - - 持续集成 - - - Gogs - Webhook - -
    - - C++ 指针使用中的一个小问题 - /2014/12/23/my-new-post/ - 在工作中碰到的一点C++指针上的一点小问题
    -

    在C++中,应该是从C语言就开始了,除了void型指针之外都是需要有分配对应的内存才可以使用,同时mallocfree成对使用,newdelete成对使用,否则造成内存泄漏。

    + if (old_node) + quicklistCompress(quicklist, old_node); + + quicklist->len++; +}
    +

    前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。

    ]]>
    - C++ - - - 博客,文章 - -
    - - mybatis 的 foreach 使用的注意点 - /2022/07/09/mybatis-%E7%9A%84-foreach-%E4%BD%BF%E7%94%A8%E7%9A%84%E6%B3%A8%E6%84%8F%E7%82%B9/ - mybatis 在作为轻量级 orm 框架,如果要使用类似于 in 查询的语句,除了直接替换字符串,还可以使用 foreach 标签
    在mybatis的 dtd 文件中可以看到可以配置这些字段,

    -
    <!ELEMENT foreach (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
    -<!ATTLIST foreach
    -collection CDATA #REQUIRED
    -item CDATA #IMPLIED
    -index CDATA #IMPLIED
    -open CDATA #IMPLIED
    -close CDATA #IMPLIED
    -separator CDATA #IMPLIED
    ->
    -

    collection 表示需要使用 foreach 的集合,item 表示进行迭代的变量名,index 就是索引值,而 open 跟 close
    代表拼接的起始和结束符号,一般就是左右括号,separator 则是每个 item 直接的分隔符

    -

    例如写了一个简单的 sql 查询

    -
    <select id="search" parameterType="list" resultMap="StudentMap">
    -    select * from student
    -    <where>
    -        id in
    -        <foreach collection="list" item="item" open="(" close=")" separator=",">
    -            #{item}
    -        </foreach>
    -    </where>
    -</select>
    -

    这里就发现了一个问题,collection 对应的这个值,如果传入的参数是个 HashMap,collection 的这个值就是以此作为
    key 从这个 HashMap 获取对应的集合,但是这里有几个特殊的小技巧,
    在上面的这个方法对应的接口方法定义中

    -
    public List<Student> search(List<Long> userIds);
    -

    我是这么定义的,而 collection 的值是list,这里就有一点不能理解了,但其实是 mybatis 考虑到使用的方便性,
    帮我们做了一点点小转换,我们翻一下 mybatis 的DefaultSqlSession 中的代码可以看到

    -
    @Override
    -public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    -  try {
    -    MappedStatement ms = configuration.getMappedStatement(statement);
    -    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    -  } catch (Exception e) {
    -    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    -  } finally {
    -    ErrorContext.instance().reset();
    -  }
    -}
    -// 就是在这帮我们做了转换
    -  private Object wrapCollection(final Object object) {
    -  if (object instanceof Collection) {
    -    StrictMap<Object> map = new StrictMap<Object>();
    -    map.put("collection", object);
    -    if (object instanceof List) {
    -      // 如果类型是list 就会转成以 list 为 key 的 map
    -      map.put("list", object);
    -    }
    -    return map;
    -  } else if (object != null && object.getClass().isArray()) {
    -    StrictMap<Object> map = new StrictMap<Object>();
    -    map.put("array", object);
    -    return map;
    -  }
    -  return object;
    -  }
    ]]>
    + Redis + 数据结构 + C + 源码 + Redis + + + redis + 数据结构 + 源码 + +
    + + redis数据结构介绍四-第四部分 压缩表 + /2020/01/19/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%9B%9B/ + 在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

    +
    +

    The ziplist is a specially encoded dually linked list that is designed to be very memory efficient.
    这是摘自 redis 源码中ziplist.c 文件的注释,也说明了原因,它的大概结构是这样子

    +
    +
    <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
    +

    其中
    <zlbytes>表示 ziplist 占用的字节总数,类型是uint32_t,32 位的无符号整型,当然表示的字节数也包含自己本身占用的 4 个
    <zltail> 类型也是是uint32_t,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。<zltail>的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。
    <uint16_t zllen> 表示ziplist 中的数据项个数,因为是 16 位,所以当数量超过所能表示的最大的数量,它的 16 位全会置为 1,但是真实的数量需要遍历整个 ziplist 才能知道
    <entry>是具体的数据项,后面解释
    <zlend> ziplist 的最后一个字节,固定是255。
    再看一下<entry>中的具体结构,

    +
    <prevlen> <encoding> <entry-data>
    +

    首先这个<prevlen>有两种情况,一种是前面的元素的长度,如果是小于等于 253的时候就用一个uint8_t 来表示前一元素的长度,如果大于的话他将占用五个字节,第一个字节是 254,即表示这个字节已经表示不下了,需要后面的四个字节帮忙表示
    <encoding>这个就比较复杂,把源码的注释放下面先看下

    +
    * |00pppppp| - 1 byte
    +*      String value with length less than or equal to 63 bytes (6 bits).
    +*      "pppppp" represents the unsigned 6 bit length.
    +* |01pppppp|qqqqqqqq| - 2 bytes
    +*      String value with length less than or equal to 16383 bytes (14 bits).
    +*      IMPORTANT: The 14 bit number is stored in big endian.
    +* |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
    +*      String value with length greater than or equal to 16384 bytes.
    +*      Only the 4 bytes following the first byte represents the length
    +*      up to 32^2-1. The 6 lower bits of the first byte are not used and
    +*      are set to zero.
    +*      IMPORTANT: The 32 bit number is stored in big endian.
    +* |11000000| - 3 bytes
    +*      Integer encoded as int16_t (2 bytes).
    +* |11010000| - 5 bytes
    +*      Integer encoded as int32_t (4 bytes).
    +* |11100000| - 9 bytes
    +*      Integer encoded as int64_t (8 bytes).
    +* |11110000| - 4 bytes
    +*      Integer encoded as 24 bit signed (3 bytes).
    +* |11111110| - 2 bytes
    +*      Integer encoded as 8 bit signed (1 byte).
    +* |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
    +*      Unsigned integer from 0 to 12. The encoded value is actually from
    +*      1 to 13 because 0000 and 1111 can not be used, so 1 should be
    +*      subtracted from the encoded 4 bit value to obtain the right value.
    +* |11111111| - End of ziplist special entry.
    +

    首先如果 encoding 的前两位是 00 的话代表这个元素是个 6 位的字符串,即直接将数据保存在 encoding 中,不消耗额外的<entry-data>,如果前两位是 01 的话表示是个 14 位的字符串,如果是 10 的话表示encoding 块之后的四个字节是存放字符串类型的数据,encoding 的剩余 6 位置 0。
    如果 encoding 的前两位是 11 的话表示这是个整型,具体的如果后两位是00的话,表示后面是个2字节的 int16_t 类型,如果是01的话,后面是个4字节的int32_t,如果是10的话后面是8字节的int64_t,如果是 11 的话后面是 3 字节的有符号整型,这些都要最后 4 位都是 0 的情况噢
    剩下当是11111110时,则表示是一个1 字节的有符号数,如果是 1111xxxx,其中xxxx在0000 到 1101 表示实际的 1 到 13,为啥呢,因为 0000 前面已经用过了,而 1110 跟 1111 也都有用了。
    看个具体的例子(上下有点对不齐,将就看)

    +
    [0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
    +|**zlbytes***|  |***zltail***|  |*zllen*|  |entry1 entry2|  |zlend|
    +

    第一部分代表整个 ziplist 有 15 个字节,zlbytes 自己占了 4 个 zltail 表示最后一个元素的偏移量,第 13 个字节起,zllen 表示有 2 个元素,第一个元素是00f3,00表示前一个元素长度是 0,本来前面就没元素(不过不知道这个能不能优化这一字节),然后是 f3,换成二进制就是11110011,对照上面的注释,是落在|1111xxxx|这个类型里,注意这个其实是用 0001 到 1101 也就是 1到 13 来表示 0到 12,所以 f3 应该就是 2,第一个元素是 2,第二个元素呢,02 代表前一个元素也就是刚才说的这个,占用 2 字节,f6 展开也是刚才的类型,实际是 5,ff 表示 ziplist 的结尾,所以这个 ziplist 里面是两个元素,2 跟 5

    +]]>
    - Java - Mybatis - Mysql + Redis + 数据结构 + C + 源码 + Redis - Java - Mysql - Mybatis + redis + 数据结构 + 源码
    - mybatis 的 $ 和 # 是有啥区别 - /2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/ - 这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
    对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
    在这里做了替换,将#{}替换成了?

    前面说的是否 dynamic 就是在这里进行判断

    -
    // org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
    -public SqlSource parseScriptNode() {
    -    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    -    SqlSource sqlSource;
    -    if (isDynamic) {
    -      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    +    JVM源码分析之G1垃圾收集器分析一
    +    /2019/12/07/JVM-G1-Part-1/
    +    对 Java 的 gc 实现比较感兴趣,原先一般都是看周志明的书,但其实并没有讲具体的 gc 源码,而是把整个思路和流程讲解了一下
    特别是 G1 的具体实现
    一般对 G1 的理解其实就是把原先整块的新生代老年代分成了以 region 为单位的小块内存,简而言之,就是原先对新生代老年代的收集会涉及到整个代的堆内存空间,而G1 把它变成了更细致的小块内存
    这带来了一个很明显的好处和一个很明显的坏处,好处是内存收集可以更灵活,耗时会变短,但整个收集的处理复杂度就变高了
    目前看了一点点关于 G1 收集的预期时间相关的代码

    +
    HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
    +                                               uint gc_count_before,
    +                                               bool* succeeded,
    +                                               GCCause::Cause gc_cause) {
    +  assert_heap_not_locked_and_not_at_safepoint();
    +  VM_G1CollectForAllocation op(word_size,
    +                               gc_count_before,
    +                               gc_cause,
    +                               false, /* should_initiate_conc_mark */
    +                               g1_policy()->max_pause_time_ms());
    +  VMThread::execute(&op);
    +
    +  HeapWord* result = op.result();
    +  bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
    +  assert(result == NULL || ret_succeeded,
    +         "the result should be NULL if the VM did not succeed");
    +  *succeeded = ret_succeeded;
    +
    +  assert_heap_not_locked();
    +  return result;
    +}
    +

    这里就是收集时需要停顿的,其中VMThread::execute(&op);是具体执行的,真正执行的是VM_G1CollectForAllocation::doit方法

    +
    void VM_G1CollectForAllocation::doit() {
    +  G1CollectedHeap* g1h = G1CollectedHeap::heap();
    +  assert(!_should_initiate_conc_mark || g1h->should_do_concurrent_full_gc(_gc_cause),
    +      "only a GC locker, a System.gc(), stats update, whitebox, or a hum allocation induced GC should start a cycle");
    +
    +  if (_word_size > 0) {
    +    // An allocation has been requested. So, try to do that first.
    +    _result = g1h->attempt_allocation_at_safepoint(_word_size,
    +                                                   false /* expect_null_cur_alloc_region */);
    +    if (_result != NULL) {
    +      // If we can successfully allocate before we actually do the
    +      // pause then we will consider this pause successful.
    +      _pause_succeeded = true;
    +      return;
    +    }
    +  }
    +
    +  GCCauseSetter x(g1h, _gc_cause);
    +  if (_should_initiate_conc_mark) {
    +    // It's safer to read old_marking_cycles_completed() here, given
    +    // that noone else will be updating it concurrently. Since we'll
    +    // only need it if we're initiating a marking cycle, no point in
    +    // setting it earlier.
    +    _old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();
    +
    +    // At this point we are supposed to start a concurrent cycle. We
    +    // will do so if one is not already in progress.
    +    bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);
    +
    +    // The above routine returns true if we were able to force the
    +    // next GC pause to be an initial mark; it returns false if a
    +    // marking cycle is already in progress.
    +    //
    +    // If a marking cycle is already in progress just return and skip the
    +    // pause below - if the reason for requesting this initial mark pause
    +    // was due to a System.gc() then the requesting thread should block in
    +    // doit_epilogue() until the marking cycle is complete.
    +    //
    +    // If this initial mark pause was requested as part of a humongous
    +    // allocation then we know that the marking cycle must just have
    +    // been started by another thread (possibly also allocating a humongous
    +    // object) as there was no active marking cycle when the requesting
    +    // thread checked before calling collect() in
    +    // attempt_allocation_humongous(). Retrying the GC, in this case,
    +    // will cause the requesting thread to spin inside collect() until the
    +    // just started marking cycle is complete - which may be a while. So
    +    // we do NOT retry the GC.
    +    if (!res) {
    +      assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating");
    +      if (_gc_cause != GCCause::_g1_humongous_allocation) {
    +        _should_retry_gc = true;
    +      }
    +      return;
    +    }
    +  }
    +
    +  // Try a partial collection of some kind.
    +  _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
    +
    +  if (_pause_succeeded) {
    +    if (_word_size > 0) {
    +      // An allocation had been requested. Do it, eventually trying a stronger
    +      // kind of GC.
    +      _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded);
         } else {
    -      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    +      bool should_upgrade_to_full = !g1h->should_do_concurrent_full_gc(_gc_cause) &&
    +                                    !g1h->has_regions_left_for_allocation();
    +      if (should_upgrade_to_full) {
    +        // There has been a request to perform a GC to free some space. We have no
    +        // information on how much memory has been asked for. In case there are
    +        // absolutely no regions left to allocate into, do a maximally compacting full GC.
    +        log_info(gc, ergo)("Attempting maximally compacting collection");
    +        _pause_succeeded = g1h->do_full_collection(false, /* explicit gc */
    +                                                   true   /* clear_all_soft_refs */);
    +      }
         }
    -    return sqlSource;
    +    guarantee(_pause_succeeded, "Elevated collections during the safepoint must always succeed.");
    +  } else {
    +    assert(_result == NULL, "invariant");
    +    // The only reason for the pause to not be successful is that, the GC locker is
    +    // active (or has become active since the prologue was executed). In this case
    +    // we should retry the pause after waiting for the GC locker to become inactive.
    +    _should_retry_gc = true;
       }
    -// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
    -protected MixedSqlNode parseDynamicTags(XNode node) {
    -    List<SqlNode> contents = new ArrayList<>();
    -    NodeList children = node.getNode().getChildNodes();
    -    for (int i = 0; i < children.getLength(); i++) {
    -      XNode child = node.newXNode(children.item(i));
    -      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
    -        String data = child.getStringBody("");
    -        TextSqlNode textSqlNode = new TextSqlNode(data);
    -        if (textSqlNode.isDynamic()) {
    -          contents.add(textSqlNode);
    -          isDynamic = true;
    -        } else {
    -          contents.add(new StaticTextSqlNode(data));
    -        }
    -      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
    -        String nodeName = child.getNode().getNodeName();
    -        NodeHandler handler = nodeHandlerMap.get(nodeName);
    -        if (handler == null) {
    -          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
    -        }
    -        handler.handleNode(child, contents);
    -        isDynamic = true;
    +}
    +

    这里可以看到核心的是G1CollectedHeap::do_collection_pause_at_safepoint这个方法,它带上了目标暂停时间的值

    +
    G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {
    +  assert_at_safepoint_on_vm_thread();
    +  guarantee(!is_gc_active(), "collection is not reentrant");
    +
    +  if (GCLocker::check_active_before_gc()) {
    +    return false;
    +  }
    +
    +  _gc_timer_stw->register_gc_start();
    +
    +  GCIdMark gc_id_mark;
    +  _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
    +
    +  SvcGCMarker sgcm(SvcGCMarker::MINOR);
    +  ResourceMark rm;
    +
    +  g1_policy()->note_gc_start();
    +
    +  wait_for_root_region_scanning();
    +
    +  print_heap_before_gc();
    +  print_heap_regions();
    +  trace_heap_before_gc(_gc_tracer_stw);
    +
    +  _verifier->verify_region_sets_optional();
    +  _verifier->verify_dirty_young_regions();
    +
    +  // We should not be doing initial mark unless the conc mark thread is running
    +  if (!_cm_thread->should_terminate()) {
    +    // This call will decide whether this pause is an initial-mark
    +    // pause. If it is, in_initial_mark_gc() will return true
    +    // for the duration of this pause.
    +    g1_policy()->decide_on_conc_mark_initiation();
    +  }
    +
    +  // We do not allow initial-mark to be piggy-backed on a mixed GC.
    +  assert(!collector_state()->in_initial_mark_gc() ||
    +          collector_state()->in_young_only_phase(), "sanity");
    +
    +  // We also do not allow mixed GCs during marking.
    +  assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
    +
    +  // Record whether this pause is an initial mark. When the current
    +  // thread has completed its logging output and it's safe to signal
    +  // the CM thread, the flag's value in the policy has been reset.
    +  bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
    +
    +  // Inner scope for scope based logging, timers, and stats collection
    +  {
    +    EvacuationInfo evacuation_info;
    +
    +    if (collector_state()->in_initial_mark_gc()) {
    +      // We are about to start a marking cycle, so we increment the
    +      // full collection counter.
    +      increment_old_marking_cycles_started();
    +      _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
    +    }
    +
    +    _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
    +
    +    GCTraceCPUTime tcpu;
    +
    +    G1HeapVerifier::G1VerifyType verify_type;
    +    FormatBuffer<> gc_string("Pause Young ");
    +    if (collector_state()->in_initial_mark_gc()) {
    +      gc_string.append("(Concurrent Start)");
    +      verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
    +    } else if (collector_state()->in_young_only_phase()) {
    +      if (collector_state()->in_young_gc_before_mixed()) {
    +        gc_string.append("(Prepare Mixed)");
    +      } else {
    +        gc_string.append("(Normal)");
           }
    +      verify_type = G1HeapVerifier::G1VerifyYoungNormal;
    +    } else {
    +      gc_string.append("(Mixed)");
    +      verify_type = G1HeapVerifier::G1VerifyMixed;
         }
    -    return new MixedSqlNode(contents);
    -  }
    -// org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
    -  public boolean isDynamic() {
    -    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    -    GenericTokenParser parser = createParser(checker);
    -    parser.parse(text);
    -    return checker.isDynamic();
    -  }
    -    private GenericTokenParser createParser(TokenHandler handler) {
    -    return new GenericTokenParser("${", "}", handler);
    -  }
    -

    可以看到其中一个条件就是是否有${}这种占位符,假如说上面的 sql 换成 ${},那么可以看到它会在这里创建一个 dynamicSqlSource,

    -
    // org.apache.ibatis.scripting.xmltags.DynamicSqlSource
    -public class DynamicSqlSource implements SqlSource {
    +    GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
     
    -  private final Configuration configuration;
    -  private final SqlNode rootSqlNode;
    +    uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
    +                                                                  workers()->active_workers(),
    +                                                                  Threads::number_of_non_daemon_threads());
    +    active_workers = workers()->update_active_workers(active_workers);
    +    log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
     
    -  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    -    this.configuration = configuration;
    -    this.rootSqlNode = rootSqlNode;
    -  }
    +    TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
    +    TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
    +                                collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
     
    -  @Override
    -  public BoundSql getBoundSql(Object parameterObject) {
    -    DynamicContext context = new DynamicContext(configuration, parameterObject);
    -    rootSqlNode.apply(context);
    -    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    -    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    -    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    -    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    -    context.getBindings().forEach(boundSql::setAdditionalParameter);
    -    return boundSql;
    -  }
    +    G1HeapTransition heap_transition(this);
    +    size_t heap_used_bytes_before_gc = used();
     
    -}
    -
    -

    这里眼尖的同学可能就可以看出来了,RawSqlSource 在初始化的时候已经经过了 parse,把#{}替换成了?占位符,但是 DynamicSqlSource 并没有
    再看这个图,我们发现在这的时候还没有进行替换
    然后往里跟
    好像是这里了

    这里 rootSqlNode.apply 其实是一个对原来 sql 的解析结果的一个循环调用,不同类型的标签会构成不同的 node,像这里就是一个 textSqlNode

    可以发现到这我们的 sql 已经被替换了,而且是直接作为 string 类型替换的,所以可以明白了这个问题所在,就是注入,不过细心的同学发现其实这里是有个

    理论上还是可以做过滤的,不过好像现在没用起来。
    我们前面可以发现对于#{}是在启动扫描 mapper的 xml 文件就替换成了 ?,然后是在什么时候变成实际的值的呢

    发现到这的时候还是没有替换,其实说白了也就是 prepareStatement 那一套,

    在这里进行替换,会拿到 org.apache.ibatis.mapping.ParameterMapping,然后进行替换,因为会带着类型信息,所以不用担心注入咯

    -]]>
    - - Java - Mybatis - Spring - Mysql - Sql注入 - Mybatis - - - Java - Mysql - Mybatis - Sql注入 - - - - nginx 日志小记 - /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ - nginx 默认的日志有特定的格式,我们也可以自定义,

    -

    默认的格式是预定义的 combined

    -
    log_format combined '$remote_addr - $remote_user [$time_local] '
    -                    '"$request" $status $body_bytes_sent '
    -                    '"$http_referer" "$http_user_agent"';
    + // Don't dynamically change the number of GC threads this early. A value of + // 0 is used to indicate serial work. When parallel work is done, + // it will be set. -

    配置的日志可以使用这个默认的,如果满足需求的话

    -
    Syntax:	access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
    -        access_log off;
    -Default: access_log logs/access.log combined;
    -Context: http, server, location, if in location, limit_except
    + { // Call to jvmpi::post_class_unload_events must occur outside of active GC + IsGCActiveMark x; -

    而如果需要额外的一些配置的话可以自己定义 log_format ,比如我想要给日志里加上请求时间,那就可以自己定义一个 log_format 比如添加下

    -
    $request_time
    -request processing time in seconds with a milliseconds resolution;   
    -time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client
    + gc_prologue(false); -
    log_format combined_extend '$remote_addr - $remote_user [$time_local] '
    -                    '"$request" $status $body_bytes_sent '
    -                    '"$http_referer" "$http_user_agent" "$request_time"';
    + if (VerifyRememberedSets) { + log_info(gc, verify)("[Verifying RemSets before GC]"); + VerifyRegionRemSetClosure v_cl; + heap_region_iterate(&v_cl); + } -

    然后其他的比如还有 gzip 压缩,可以设置压缩级别,flush 刷盘时间还有根据条件控制

    -

    这里的条件控制简单看了下还比较厉害

    -

    比如我想对2xx 跟 3xx 的访问不记录日志

    -
    map $status $loggable {
    -    ~^[23]  0;
    -    default 1;
    -}
    +      _verifier->verify_before_gc(verify_type);
     
    -access_log /path/to/access.log combined if=$loggable;
    + _verifier->check_bitmaps("GC Start"); -

    $loggable 是 0 或者空时表示 if 条件为否,上面的默认就是 1,只有当请求状态 status 是 2xx 或 3xx 时才是 0,代表不用记录,有了这个特性就可以更灵活地配置日志

    -

    文章主要参考了 nginx 的 log 模块的文档

    -]]>
    - - nginx - - - nginx - 日志 - -
    - - mybatis 的缓存是怎么回事 - /2020/10/03/mybatis-%E7%9A%84%E7%BC%93%E5%AD%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/ - Java 真的是任何一个中间件,比较常用的那种,都有很多内容值得深挖,比如这个缓存,慢慢有过一些感悟,比如如何提升性能,缓存无疑是一大重要手段,最底层开始 CPU 就有缓存,而且又小又贵,再往上一点内存一般作为硬盘存储在运行时的存储,一般在代码里也会用内存作为一些本地缓存,譬如数据库,像 mysql 这种也是有innodb_buffer_pool来提升查询效率,本质上理解就是用更快的存储作为相对慢存储的缓存,减少查询直接访问较慢的存储,并且这个都是相对的,比起 cpu 的缓存,那内存也是渣,但是与普通机械硬盘相比,那也是两个次元的水平。

    -

    闲扯这么多来说说 mybatis 的缓存,mybatis 一般作为一个轻量级的 orm 使用,相对应的就是比较重量级的 hibernate,不过不在这次讨论范围,上一次是主要讲了 mybatis 在解析 sql 过程中,对于两种占位符的不同替换实现策略,这次主要聊下 mybatis 的缓存,前面其实得了解下前置的东西,比如 sqlsession,先当做我们知道 sqlsession 是个什么玩意,可能或多或少的知道 mybatis 是有两级缓存,

    -

    一级缓存

    第一级的缓存是在 BaseExecutor 中的 PerpetualCache,它是个最基本的缓存实现类,使用了 HashMap 实现缓存功能,代码其实没几十行

    -
    public class PerpetualCache implements Cache {
    +#if COMPILER2_OR_JVMCI
    +      DerivedPointerTable::clear();
    +#endif
     
    -  private final String id;
    +      // Please see comment in g1CollectedHeap.hpp and
    +      // G1CollectedHeap::ref_processing_init() to see how
    +      // reference processing currently works in G1.
     
    -  private final Map<Object, Object> cache = new HashMap<>();
    +      // Enable discovery in the STW reference processor
    +      _ref_processor_stw->enable_discovery();
     
    -  public PerpetualCache(String id) {
    -    this.id = id;
    -  }
    +      {
    +        // We want to temporarily turn off discovery by the
    +        // CM ref processor, if necessary, and turn it back on
    +        // on again later if we do. Using a scoped
    +        // NoRefDiscovery object will do this.
    +        NoRefDiscovery no_cm_discovery(_ref_processor_cm);
     
    -  @Override
    -  public String getId() {
    -    return id;
    -  }
    +        // Forget the current alloc region (we might even choose it to be part
    +        // of the collection set!).
    +        _allocator->release_mutator_alloc_region();
     
    -  @Override
    -  public int getSize() {
    -    return cache.size();
    -  }
    +        // This timing is only used by the ergonomics to handle our pause target.
    +        // It is unclear why this should not include the full pause. We will
    +        // investigate this in CR 7178365.
    +        //
    +        // Preserving the old comment here if that helps the investigation:
    +        //
    +        // The elapsed time induced by the start time below deliberately elides
    +        // the possible verification above.
    +        double sample_start_time_sec = os::elapsedTime();
     
    -  @Override
    -  public void putObject(Object key, Object value) {
    -    cache.put(key, value);
    -  }
    +        g1_policy()->record_collection_pause_start(sample_start_time_sec);
     
    -  @Override
    -  public Object getObject(Object key) {
    -    return cache.get(key);
    -  }
    +        if (collector_state()->in_initial_mark_gc()) {
    +          concurrent_mark()->pre_initial_mark();
    +        }
     
    -  @Override
    -  public Object removeObject(Object key) {
    -    return cache.remove(key);
    -  }
    +        g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
     
    -  @Override
    -  public void clear() {
    -    cache.clear();
    -  }
    +        evacuation_info.set_collectionset_regions(collection_set()->region_length());
     
    -  @Override
    -  public boolean equals(Object o) {
    -    if (getId() == null) {
    -      throw new CacheException("Cache instances require an ID.");
    -    }
    -    if (this == o) {
    -      return true;
    -    }
    -    if (!(o instanceof Cache)) {
    -      return false;
    -    }
    +        // Make sure the remembered sets are up to date. This needs to be
    +        // done before register_humongous_regions_with_cset(), because the
    +        // remembered sets are used there to choose eager reclaim candidates.
    +        // If the remembered sets are not up to date we might miss some
    +        // entries that need to be handled.
    +        g1_rem_set()->cleanupHRRS();
    +
    +        register_humongous_regions_with_cset();
    +
    +        assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
    +
    +        // We call this after finalize_cset() to
    +        // ensure that the CSet has been finalized.
    +        _cm->verify_no_cset_oops();
    +
    +        if (_hr_printer.is_active()) {
    +          G1PrintCollectionSetClosure cl(&_hr_printer);
    +          _collection_set.iterate(&cl);
    +        }
    +
    +        // Initialize the GC alloc regions.
    +        _allocator->init_gc_alloc_regions(evacuation_info);
    +
    +        G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
    +        pre_evacuate_collection_set();
    +
    +        // Actually do the work...
    +        evacuate_collection_set(&per_thread_states);
    +
    +        post_evacuate_collection_set(evacuation_info, &per_thread_states);
    +
    +        const size_t* surviving_young_words = per_thread_states.surviving_young_words();
    +        free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
    +
    +        eagerly_reclaim_humongous_regions();
    +
    +        record_obj_copy_mem_stats();
    +        _survivor_evac_stats.adjust_desired_plab_sz();
    +        _old_evac_stats.adjust_desired_plab_sz();
     
    -    Cache otherCache = (Cache) o;
    -    return getId().equals(otherCache.getId());
    -  }
    +        double start = os::elapsedTime();
    +        start_new_collection_set();
    +        g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
     
    -  @Override
    -  public int hashCode() {
    -    if (getId() == null) {
    -      throw new CacheException("Cache instances require an ID.");
    -    }
    -    return getId().hashCode();
    -  }
    +        if (evacuation_failed()) {
    +          set_used(recalculate_used());
    +          if (_archive_allocator != NULL) {
    +            _archive_allocator->clear_used();
    +          }
    +          for (uint i = 0; i < ParallelGCThreads; i++) {
    +            if (_evacuation_failed_info_array[i].has_failed()) {
    +              _gc_tracer_stw->report_evacuation_failed(_evacuation_failed_info_array[i]);
    +            }
    +          }
    +        } else {
    +          // The "used" of the the collection set have already been subtracted
    +          // when they were freed.  Add in the bytes evacuated.
    +          increase_used(g1_policy()->bytes_copied_during_gc());
    +        }
     
    -}
    -

    可以看一下BaseExecutor 的构造函数

    -
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
    -    this.transaction = transaction;
    -    this.deferredLoads = new ConcurrentLinkedQueue<>();
    -    this.localCache = new PerpetualCache("LocalCache");
    -    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    -    this.closed = false;
    -    this.configuration = configuration;
    -    this.wrapper = this;
    -  }
    -

    就是把 PerpetualCache 作为 localCache,然后怎么使用我看简单看一下,BaseExecutor 的查询首先是调用这个函数

    -
    @Override
    -  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    -    BoundSql boundSql = ms.getBoundSql(parameter);
    -    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    -    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    -  }
    -

    可以看到首先是调用了 createCacheKey 方法,这个方法呢,先不看怎么写的,如果我们自己要实现这么个缓存,首先这个缓存 key 的设计也是个问题,如果是以表名加主键作为 key,那么分页查询,或者没有主键的时候就不行,来看看 mybatis 是怎么设计的

    -
    @Override
    -  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    -    if (closed) {
    -      throw new ExecutorException("Executor was closed.");
    -    }
    -    CacheKey cacheKey = new CacheKey();
    -    cacheKey.update(ms.getId());
    -    cacheKey.update(rowBounds.getOffset());
    -    cacheKey.update(rowBounds.getLimit());
    -    cacheKey.update(boundSql.getSql());
    -    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    -    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    -    // mimic DefaultParameterHandler logic
    -    for (ParameterMapping parameterMapping : parameterMappings) {
    -      if (parameterMapping.getMode() != ParameterMode.OUT) {
    -        Object value;
    -        String propertyName = parameterMapping.getProperty();
    -        if (boundSql.hasAdditionalParameter(propertyName)) {
    -          value = boundSql.getAdditionalParameter(propertyName);
    -        } else if (parameterObject == null) {
    -          value = null;
    -        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    -          value = parameterObject;
    -        } else {
    -          MetaObject metaObject = configuration.newMetaObject(parameterObject);
    -          value = metaObject.getValue(propertyName);
    -        }
    -        cacheKey.update(value);
    -      }
    -    }
    -    if (configuration.getEnvironment() != null) {
    -      // issue #176
    -      cacheKey.update(configuration.getEnvironment().getId());
    -    }
    -    return cacheKey;
    -  }
    -
    -

    首先需要 id,这个 id 是 mapper 里方法的 id, 然后是偏移量跟返回行数,再就是 sql,然后是参数,基本上是会有影响的都加进去了,在这个 update 里面

    -
    public void update(Object object) {
    -    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    +        if (collector_state()->in_initial_mark_gc()) {
    +          // We have to do this before we notify the CM threads that
    +          // they can start working to make sure that all the
    +          // appropriate initialization is done on the CM object.
    +          concurrent_mark()->post_initial_mark();
    +          // Note that we don't actually trigger the CM thread at
    +          // this point. We do that later when we're sure that
    +          // the current thread has completed its logging output.
    +        }
     
    -    count++;
    -    checksum += baseHashCode;
    -    baseHashCode *= count;
    +        allocate_dummy_regions();
     
    -    hashcode = multiplier * hashcode + baseHashCode;
    +        _allocator->init_mutator_alloc_region();
     
    -    updateList.add(object);
    -  }
    -

    其实是一个 hash 转换,具体不纠结,就是提高特异性,然后回来就是继续调用 query

    -
    @Override
    -  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    -    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    -    if (closed) {
    -      throw new ExecutorException("Executor was closed.");
    -    }
    -    if (queryStack == 0 && ms.isFlushCacheRequired()) {
    -      clearLocalCache();
    -    }
    -    List<E> list;
    -    try {
    -      queryStack++;
    -      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    -      if (list != null) {
    -        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    -      } else {
    -        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    -      }
    -    } finally {
    -      queryStack--;
    -    }
    -    if (queryStack == 0) {
    -      for (DeferredLoad deferredLoad : deferredLoads) {
    -        deferredLoad.load();
    -      }
    -      // issue #601
    -      deferredLoads.clear();
    -      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    -        // issue #482
    -        clearLocalCache();
    -      }
    -    }
    -    return list;
    -  }
    -

    可以看到是先从 localCache 里取,取不到再 queryFromDatabase,其实比较简单,这是一级缓存,考虑到 sqlsession 跟 BaseExecutor 的关系,其实是随着 sqlsession 来保证这个缓存不会出现脏数据幻读的情况,当然事务相关的后面可能再单独聊。

    -

    二级缓存

    其实这个一级二级顺序有点反过来,其实查询的是先走的二级缓存,当然二级的需要配置开启,默认不开,
    需要通过

    -
    <setting name="cacheEnabled" value="true"/>
    -

    来开启,然后我们的查询就会走到

    -
    public class CachingExecutor implements Executor {
    +        {
    +          size_t expand_bytes = _heap_sizing_policy->expansion_amount();
    +          if (expand_bytes > 0) {
    +            size_t bytes_before = capacity();
    +            // No need for an ergo logging here,
    +            // expansion_amount() does this when it returns a value > 0.
    +            double expand_ms;
    +            if (!expand(expand_bytes, _workers, &expand_ms)) {
    +              // We failed to expand the heap. Cannot do anything about it.
    +            }
    +            g1_policy()->phase_times()->record_expand_heap_time(expand_ms);
    +          }
    +        }
     
    -  private final Executor delegate;
    -  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
    -

    这个 Executor 中,这里我放了类里面的元素,发现没有一个 Cache 类,这就是一个特点了,往下看查询过程

    -
    @Override
    -  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    -    BoundSql boundSql = ms.getBoundSql(parameterObject);
    -    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    -    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    -  }
    +        // We redo the verification but now wrt to the new CSet which
    +        // has just got initialized after the previous CSet was freed.
    +        _cm->verify_no_cset_oops();
     
    -  @Override
    -  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    -      throws SQLException {
    -    Cache cache = ms.getCache();
    -    if (cache != null) {
    -      flushCacheIfRequired(ms);
    -      if (ms.isUseCache() && resultHandler == null) {
    -        ensureNoOutParams(ms, boundSql);
    -        @SuppressWarnings("unchecked")
    -        List<E> list = (List<E>) tcm.getObject(cache, key);
    -        if (list == null) {
    -          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    -          tcm.putObject(cache, key, list); // issue #578 and #116
    -        }
    -        return list;
    -      }
    -    }
    -    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    -  }
    -

    看到没,其实缓存是从 tcm 这个成员变量里取,而这个是什么呢,事务性缓存(直译下),因为这个其实是用 MappedStatement 里的 Cache 作为key 从 tcm 的 map 取出来的

    -
    public class TransactionalCacheManager {
    +        // This timing is only used by the ergonomics to handle our pause target.
    +        // It is unclear why this should not include the full pause. We will
    +        // investigate this in CR 7178365.
    +        double sample_end_time_sec = os::elapsedTime();
    +        double pause_time_ms = (sample_end_time_sec - sample_start_time_sec) * MILLIUNITS;
    +        size_t total_cards_scanned = g1_policy()->phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanRS, G1GCPhaseTimes::ScanRSScannedCards);
    +        g1_policy()->record_collection_pause_end(pause_time_ms, total_cards_scanned, heap_used_bytes_before_gc);
     
    -  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
    -

    MappedStatement是被全局使用的,所以其实二级缓存是跟着 mapper 的 namespace 走的,可以被多个 CachingExecutor 获取到,就会出现线程安全问题,线程安全问题可以用SynchronizedCache来解决,就是加锁,但是对于事务中的脏读,使用了TransactionalCache来解决这个问题,

    -
    public class TransactionalCache implements Cache {
    +        evacuation_info.set_collectionset_used_before(collection_set()->bytes_used_before());
    +        evacuation_info.set_bytes_copied(g1_policy()->bytes_copied_during_gc());
     
    -  private static final Log log = LogFactory.getLog(TransactionalCache.class);
    +        if (VerifyRememberedSets) {
    +          log_info(gc, verify)("[Verifying RemSets after GC]");
    +          VerifyRegionRemSetClosure v_cl;
    +          heap_region_iterate(&v_cl);
    +        }
     
    -  private final Cache delegate;
    -  private boolean clearOnCommit;
    -  private final Map<Object, Object> entriesToAddOnCommit;
    -  private final Set<Object> entriesMissedInCache;
    -

    在事务还没提交的时候,会把中间状态的数据放在 entriesToAddOnCommit 中,只有在提交后会放进共享缓存中,

    -
    public void commit() {
    -    if (clearOnCommit) {
    -      delegate.clear();
    -    }
    -    flushPendingEntries();
    -    reset();
    -  }
    ]]>
    - - Java - Mybatis - Spring - Mybatis - 缓存 - Mybatis - - - Java - Mysql - Mybatis - 缓存 - -
    - - openresty - /2019/06/18/openresty/ - 目前公司要对一些新的产品功能做灰度测试,因为在后端业务代码层面添加判断比较麻烦,所以想在nginx上做点手脚,就想到了openresty
    前后也踩了不少坑,这边先写一点

    -

    首先是日志
    error_log logs/error.log debug;
    需要nginx开启日志的debug才能看到日志

    -

    使用 lua_code_cache off即可, 另外注意只有使用 content_by_lua_file 才会生效

    -
    http {
    -  lua_code_cache off;
    -}
    +        _verifier->verify_after_gc(verify_type);
    +        _verifier->check_bitmaps("GC End");
     
    -location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
    -   default_type 'application/json;charset=utf-8';
    -   content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
    -}
    + assert(!_ref_processor_stw->discovery_enabled(), "Postcondition"); + _ref_processor_stw->verify_no_references_recorded(); + + // CM reference discovery will be re-enabled if necessary. + } -

    使用lua给nginx请求response头添加内容可以用这个

    -
    ngx.header['response'] = 'header'
    +#ifdef TRACESPINNING + ParallelTaskTerminator::print_termination_counts(); +#endif + gc_epilogue(false); + } -

    使用总结

    -

    后续:

    -
      -
    1. 一开始在本地环境的时候使用content_by_lua_file只关注了头,后来发到测试环境发现请求内容都没代理转发到后端服务上
      网上查了下发现content_by_lua_file是将请求的所有内容包括response都用这里面的lua脚本生成了,content这个词就表示是请求内容
      后来改成了access_by_lua_file就正常了,只是要去获取请求内容和修改响应头,并不是要完整的接管请求

      -
    2. -
    3. 后来又碰到了一个坑是nginx有个client_body_buffer_size的配置参数,nginx在32位和64位系统里有8K和16K两个默认值,当请求内容大于这两个值的时候,会把请求内容放到临时文件里,这个时候openresty里的ngx.req.get_post_args()就会报“failed to get post args: requesty body in temp file not supported”这个错误,将client_body_buffer_size这个参数配置调大一点就好了

      -
    4. -
    5. 还有就是lua的异常捕获,网上看一般是用pcall和xpcall来进行保护调用,因为问题主要出在cjson的decode,这里有两个解决方案,一个就是将cjson.decode使用pcall封装,

      -
      local decode = require("cjson").decode
      +    // Print the remainder of the GC log output.
      +    if (evacuation_failed()) {
      +      log_info(gc)("To-space exhausted");
      +    }
       
      -function json_decode( str )
      -    local ok, t = pcall(decode, str)
      -    if not ok then
      -      return nil
      -    end
      +    g1_policy()->print_phases();
      +    heap_transition.print();
       
      -    return t
      -end
      -

      这个是使用了pcall,称为保护调用,会在内部错误后返回两个参数,第一个是false,第二个是错误信息
      还有一种是使用cjson.safe包

      -
      local json = require("cjson.safe")
      -local str = [[ {"key:"value"} ]]
      +    // It is not yet to safe to tell the concurrent mark to
      +    // start as we have some optional output below. We don't want the
      +    // output from the concurrent mark thread interfering with this
      +    // logging output either.
       
      -local t = json.decode(str)
      -if t then
      -    ngx.say(" --> ", type(t))
      -end
      -

      cjson.safe包会在解析失败的时候返回nil

      -
    6. -
    7. 还有一个是redis链接时如果host使用的是域名的话会提示“failed to connect: no resolver defined to resolve “redis.xxxxxx.com””,这里需要使用nginx的resolver指令,
      resolver 8.8.8.8 valid=3600s;

      -
    8. -
    9. 还有一点补充下
      就是业务在使用redis的时候使用了db的特性,所以在lua访问redis的时候也需要执行db,这里lua的redis库也支持了这个特性,可以使用instance:select(config:get(‘db’))来切换db

      -
    10. -
    11. 性能优化tips
      建议是尽量少使用阶段钩子,例如content_by_lua_file,*_by_lua

      -
    12. -
    13. 发现一个不错的openresty站点
      地址

      -
    14. -
    -]]>
    - - nginx - - - nginx - openresty - -
    - - pcre-intro-and-a-simple-package - /2015/01/16/pcre-intro-and-a-simple-package/ - Pcre
    -

    Perl Compatible Regular Expressions (PCRE) is a regular
    expression C library inspired by the regular expression
    capabilities in the Perl programming language, written
    by Philip Hazel, starting in summer 1997.

    -
    -

    因为最近工作内容的一部分需要做字符串的识别处理,所以就顺便用上了之前在PHP中用过的正则,在C/C++中本身不包含正则库,这里使用的pcre,对MFC开发,在这里提供了静态链接库,在引入lib跟.h文件后即可使用。

    - + _hrm.verify_optional(); + _verifier->verify_region_sets_optional(); -

    Regular Expression Syntax

    然后是一些正则语法,官方的语法文档比较科学严谨,特别是对类似于贪婪匹配等细节的说明,当然一般的使用可以在网上找到很多匹配语法,例如这个

    -

    PCRE函数介绍

    -

    pcre_compile
    原型:

    -
    -
    #include <pcre.h>
    -pcre *pcre_compile(const char *pattern, int options, const char **errptr, int *erroffset, const unsigned char *tableptr);
    -

    功能:将一个正则表达式编译成一个内部表示,在匹配多个字符串时,可以加速匹配。其同pcre_compile2功能一样只是缺少一个参数errorcodeptr。
    参数:
    pattern 正则表达式
    options 为0,或者其他参数选项
    errptr 出错消息
    erroffset 出错位置
    tableptr 指向一个字符数组的指针,可以设置为空NULL

    -
    -

    pcre_exec
    原型:

    -
    -
    #include <pcre.h>
    -int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, int length, int startoffset, int options, int *ovector, int ovecsize)
    -

    功能:使用编译好的模式进行匹配,采用与Perl相似的算法,返回匹配串的偏移位置。
    参数:
    code 编译好的模式
    extra 指向一个pcre_extra结构体,可以为NULL
    subject 需要匹配的字符串
    length 匹配的字符串长度(Byte)
    startoffset 匹配的开始位置
    options 选项位
    ovector 指向一个结果的整型数组
    ovecsize 数组大小。

    -

    这里是两个最常用的函数的简单说明,pcre的静态库提供了一系列的函数以供使用,可以参考这个博客说明,另外对于以上函数的具体参数详细说明可以参考官网此处

    -

    一个丑陋的封装

    void COcxDemoDlg::pcre_exec_all(const pcre * re, PCRE_SPTR src, vector<pair<int, int>> &vc)
    -{
    -	int rc;
    -	int ovector[30];
    -	int i = 0;
    -	pair<int, int> pr;
    -	rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
    -	for (; rc > 0;)
    -	{
    -		i = ovector[1];
    -		pr.first = ovector[2];
    -		pr.second = ovector[3];
    -		vc.push_back(pr);
    -		rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
    -	}
    -}
    -

    vector中是全文匹配后的索引对,只是简单地用下。

    -]]>
    - - C++ - - - c++ - mfc - -
    - - php-abstract-class-and-interface - /2016/11/10/php-abstract-class-and-interface/ - PHP抽象类和接口
      -
    • 抽象类与接口
    • -
    • 抽象类内可以包含非抽象函数,即可实现函数
    • -
    • 抽象类内必须包含至少一个抽象方法,抽象类和接口均不能实例化
    • -
    • 抽象类可以设置访问级别,接口默认都是public
    • -
    • 类可以实现多个接口但不能继承多个抽象类
    • -
    • 类必须实现抽象类和接口里的抽象方法,不一定要实现抽象类的非抽象方法
    • -
    • 接口内不能定义变量,但是可以定义常量
    • -
    -

    示例代码

    <?php
    -interface int1{
    -    const INTER1 = 111;
    -    function inter1();
    -}
    -interface int2{
    -    const INTER1 = 222;
    -    function inter2();
    -}
    -abstract class abst1{
    -    public function abstr1(){
    -        echo 1111;
    -    }
    -    abstract function abstra1(){
    -        echo 'ahahahha';
    -    }
    -}
    -abstract class abst2{
    -    public function abstr2(){
    -        echo 1111;
    -    }
    -    abstract function abstra2();
    -}
    -class normal1 extends abst1{
    -    protected function abstr2(){
    -        echo 222;
    -    }
    -}
    + TASKQUEUE_STATS_ONLY(print_taskqueue_stats()); + TASKQUEUE_STATS_ONLY(reset_taskqueue_stats()); -

    result

    PHP Fatal error:  Abstract function abst1::abstra1() cannot contain body in new.php on line 17
    +    print_heap_after_gc();
    +    print_heap_regions();
    +    trace_heap_after_gc(_gc_tracer_stw);
     
    -Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
    -]]>
    - - php - - - php - -
    - - redis 的 rdb 和 COW 介绍 - /2021/08/15/redis-%E7%9A%84-rdb-%E5%92%8C-COW-%E4%BB%8B%E7%BB%8D/ - redis 在使用 rdb 策略进行备份时,rdb 的意思是会在开启备份的时候将开启时间点的内存数据进行备份,并且可以设置时间,这样子就是这个策略其实还是不完全可靠的,如果是在这个间隔中宕机了,或者间隔过长,不过这个不在这次的要说的内容中,如果自己去写这个 rdb 的策略可能就有点类似于 mvcc 的 redolog,需要知道这个时间点之前的数据是怎么样的,防止后面更改的干扰,但是这样一方面需要有比较复杂的 mvcc 实现,另一方面是很占用存储空间,所以 redis 在这里面使用了 COW (Copy On Write) 技术,这个技术呢以前听过,也大致了解是怎么个意思,这次稍微具体地来看下,其实 redis 的 copy-on-write 就是来自于 linux 的 cow

    -

    Linux中的CopyOnWrite

    fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。这个操作其实可以类比为写屏障,正常的读取是没问题的,当有写入时就会分裂。

    -

    CopyOnWrite的好处:

    1、减少分配和复制资源时带来的瞬时延迟;
    2、减少不必要的资源分配;
    CopyOnWrite的缺点:
    1、如果父子进程都需要进行大量的写操作,会产生大量的分页错误(页异常中断page-fault);

    -

    Redis中的CopyOnWrite

    Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
    总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
    而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。这里其实更巧妙了,在细节上去优化会产生大量页异常中断的情况。

    -]]>
    - - redis - - - redis - -
    - - redis数据结构介绍三-第三部分 整数集合 - /2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/ - redis中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

    -
    typedef struct intset {
    -    // 编码方式
    -    uint32_t encoding;
    -    // 集合包含的元素数量
    -    uint32_t length;
    -    // 保存元素的数组
    -    int8_t contents[];
    -} intset;
    +    // We must call G1MonitoringSupport::update_sizes() in the same scoping level
    +    // as an active TraceMemoryManagerStats object (i.e. before the destructor for the
    +    // TraceMemoryManagerStats is called) so that the G1 memory pools are updated
    +    // before any GC notifications are raised.
    +    g1mm()->update_sizes();
     
    -/* Note that these encodings are ordered, so:
    - * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
    -#define INTSET_ENC_INT16 (sizeof(int16_t))
    -#define INTSET_ENC_INT32 (sizeof(int32_t))
    -#define INTSET_ENC_INT64 (sizeof(int64_t))
    -

    一眼看,为啥整型还需要编码,然后 int8_t 怎么能存下大整形呢,带着这些疑问,我们一步步分析下去,这里的编码其实指的是这个整型集合里存的究竟是多大的整型,16 位,还是 32 位,还是 64 位,结构体下面的宏定义就是表示了 encoding 的可能取值,INTSET_ENC_INT16 表示每个元素用2个字节存储,INTSET_ENC_INT32 表示每个元素用4个字节存储,INTSET_ENC_INT64 表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。length 就是正常的表示集合中元素的数量。最奇怪的应该就是这个 contents 了,是个 int8_t 的数组,那放毛线数据啊,最小的都有 16 位,这里我在看代码和《redis 设计与实现》的时候也有点懵逼,后来查了下发现这是个比较取巧的用法,这里我用自己的理解表述一下,先看看 8,16,32,64 的关系,一眼看就知道都是 2 的 N 次,并且呈两倍关系,而且 8 位刚好一个字节,所以呢其实这里的contents 不是个常规意义上的 int8_t 类型的数组,而是个柔性数组。看下 wiki 的定义

    -
    -

    Flexible array members1 were introduced in the C99 standard of the C programming language (in particular, in section §6.7.2.1, item 16, page 103).2 It is a member of a struct, which is an array without a given dimension. It must be the last member of such a struct and it must be accompanied by at least one other member, as in the following example:

    -
    -
    struct vectord {
    -    size_t len;
    -    double arr[]; // the flexible array member must be last
    -};
    -

    在初始化这个 intset 的时候,这个contents数组是不占用空间的,后面的反正用到了申请,那么这里就有一个问题,给出了三种可能的 encoding 值,他们能随便换吗,显然不行,首先在 intset 中数据的存放是有序的,这个有部分原因是方便二分查找,然后存放数据其实随着数据的大小不同会有一个升级的过程,看下图

    新创建的intset只有一个header,总共8个字节。其中encoding = 2, length = 0, 类型都是uint32_t,各占 4 字节,添加15, 5两个元素之后,因为它们是比较小的整数,都能使用2个字节表示,所以encoding不变,值还是2,也就是默认的 INTSET_ENC_INT16,当添加32768的时候,它不再能用2个字节来表示了(2个字节能表达的数据范围是-215~215-1,而32768等于215,超出范围了),因此encoding必须升级到INTSET_ENC_INT32(值为4),即用4个字节表示一个元素。在添加每个元素的过程中,intset始终保持从小到大有序。与ziplist类似,intset也是按小端(little endian)模式存储的(参见维基百科词条Endianness)。比如,在上图中intset添加完所有数据之后,表示encoding字段的4个字节应该解释成0x00000004,而第4个数据应该解释成0x00008000 = 32768

    -]]>
    - - Redis - 数据结构 - C - 源码 - Redis - - - redis - 数据结构 - 源码 - -
    - - redis数据结构介绍-第一部分 SDS,链表,字典 - /2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ - redis是现在服务端很常用的缓存中间件,其实原来还有memcache之类的竞品,但是现在貌似 redis 快一统江湖,这里当然不是在吹,只是个人角度的一个感觉,不权威只是主观感觉。
    redis 主要有五种数据结构,StringsListsSetsHashesSorted Sets,这五种数据结构先简单介绍下,Strings类型的其实就是我们最常用的 key-value,实际开发中也会用的最多;Lists是列表,这个有些会用来做队列,因为 redis 目前常用的版本支持丰富的列表操作;还有是Sets集合,这个主要的特点就是集合中元素不重复,可以用在有这类需求的场景里;Hashes是叫散列,类似于 Python 中的字典结构;还有就是Sorted Sets这个是个有序集合;一眼看这些其实没啥特别的,除了最后这个有序集合,不过去了解背后的实现方式还是比较有意思的。

    -

    SDS 简单动态字符串

    先从Strings开始说,了解过 C 语言的应该知道,C 语言中的字符串其实是个 char[] 字符数组,redis 也不例外,只是最开始的版本就对这个做了一丢丢的优化,而正是这一丢丢的优化,让这个 redis 的使用效率提升了数倍

    -
    struct sdshdr {
    -    // 字符串长度
    -    int len;
    -    // 字符串空余字符数
    -    int free;
    -    // 字符串内容
    -    char buf[];
    -};
    -

    这里引用了 redis 在 github 上最早的 2.2 版本的代码,代码路径是https://github.com/antirez/redis/blob/2.2/src/sds.h,可以看到这个结构体里只有仨元素,两个 int 型和一个 char 型数组,两个 int 型其实就是我说的优化,因为 C 语言本身的字符串数组,有两个问题,一个是要知道它实际已被占用的长度,需要去遍历这个数组,第二个就是比较容易踩坑的是遍历的时候要注意它有个以\0作为结尾的特点;通过上面的两个 int 型参数,一个是知道字符串目前的长度,一个是知道字符串还剩余多少位空间,这样子坐着两个操作从 O(N)简化到了O(1)了,还有第二个 free 还有个比较重要的作用就是能防止 C 字符串的溢出问题,在存储之前可以先判断 free 长度,如果长度不够就先扩容了,先介绍到这,这个系列可以写蛮多的,慢慢介绍吧

    -

    链表

    链表是比较常见的数据结构了,但是因为 redis 是用 C 写的,所以在不依赖第三方库的情况下只能自己写一个了,redis 的链表是个有头的链表,而且是无环的,具体的结构我也找了 github 上最早版本的代码

    -
    typedef struct listNode {
    -    // 前置节点
    -    struct listNode *prev;
    -    // 后置节点
    -    struct listNode *next;
    -    // 值
    -    void *value;
    -} listNode;
    +    _gc_tracer_stw->report_evacuation_info(&evacuation_info);
    +    _gc_tracer_stw->report_tenuring_threshold(_g1_policy->tenuring_threshold());
    +    _gc_timer_stw->register_gc_end();
    +    _gc_tracer_stw->report_gc_end(_gc_timer_stw->gc_end(), _gc_timer_stw->time_partitions());
    +  }
    +  // It should now be safe to tell the concurrent mark thread to start
    +  // without its logging output interfering with the logging output
    +  // that came from the pause.
     
    -typedef struct list {
    -    // 链表表头
    -    listNode *head;
    -    // 当前节点,也可以说是最后节点
    -    listNode *tail;
    -    // 节点复制函数
    -    void *(*dup)(void *ptr);
    -    // 节点值释放函数
    -    void (*free)(void *ptr);
    -    // 节点值比较函数
    -    int (*match)(void *ptr, void *key);
    -    // 链表包含的节点数量
    -    unsigned int len;
    -} list;
    -

    代码地址是这个https://github.com/antirez/redis/blob/2.2/src/adlist.h
    可以看下节点是由listNode承载的,包括值和一个指向前节点跟一个指向后一节点的两个指针,然后值是 void 指针类型,所以可以承载不同类型的值
    然后是 list结构用来承载一个链表,包含了表头,和表尾,复制函数,释放函数和比较函数,还有链表长度,因为包含了前两个节点,找到表尾节点跟表头都是 O(1)的时间复杂度,还有节点数量,其实这个跟 SDS 是同一个做法,就是空间换时间,这也是写代码里比较常见的做法,以此让一些高频的操作提速。

    -

    字典

    字典也是个常用的数据结构,其实只是叫法不同,数据结构中叫 hash 散列,Java 中叫 Map,PHP 中是数组 array,Python 中也叫字典 dict,因为纯 C 语言本身不带这些数据结构,所以这也是个痛并快乐着的过程,享受 C 语言的高性能的同时也要接受它只提供了语言的基本功能的现实,各种轮子都需要自己造,redis 同样实现了自己的字典
    下面来看看代码

    -
    typedef struct dictEntry {
    -    void *key;
    -    void *val;
    -    struct dictEntry *next;
    -} dictEntry;
    +  if (should_start_conc_mark) {
    +    // CAUTION: after the doConcurrentMark() call below,
    +    // the concurrent marking thread(s) could be running
    +    // concurrently with us. Make sure that anything after
    +    // this point does not assume that we are the only GC thread
    +    // running. Note: of course, the actual marking work will
    +    // not start until the safepoint itself is released in
    +    // SuspendibleThreadSet::desynchronize().
    +    do_concurrent_mark();
    +  }
     
    -typedef struct dictType {
    -    unsigned int (*hashFunction)(const void *key);
    -    void *(*keyDup)(void *privdata, const void *key);
    -    void *(*valDup)(void *privdata, const void *obj);
    -    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    -    void (*keyDestructor)(void *privdata, void *key);
    -    void (*valDestructor)(void *privdata, void *obj);
    -} dictType;
    +  return true;
    +}
    +

    往下走就是这一步G1Policy::finalize_collection_set,去处理新生代和老年代

    +
    void G1Policy::finalize_collection_set(double target_pause_time_ms, G1SurvivorRegions* survivor) {
    +  double time_remaining_ms = _collection_set->finalize_young_part(target_pause_time_ms, survivor);
    +  _collection_set->finalize_old_part(time_remaining_ms);
    +}
    +

    这里分别调用了两个方法,可以看到剩余时间是往下传的,来看一下具体的方法

    +
    double G1CollectionSet::finalize_young_part(double target_pause_time_ms, G1SurvivorRegions* survivors) {
    +  double young_start_time_sec = os::elapsedTime();
     
    -/* 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;
    -    unsigned long size;
    -    unsigned long sizemask;
    -    unsigned long used;
    -} dictht;
    +  finalize_incremental_building();
     
    -typedef struct dict {
    -    dictType *type;
    -    void *privdata;
    -    dictht ht[2];
    -    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    -    int iterators; /* number of iterators currently running */
    -} dict;
    -

    看了下这个 2.2 版本的代码跟最新版的其实也差的不是很多,所以还是照旧用老代码,可以看到上面四个结构体中,其实只有三个是存储数据用的,dictType 是用来放操作函数的,那么三个存放数据的结构体分别是干嘛的,这时候感觉需要一个图来说明比较好,稍等,我去画个图~

    这个图看着应该比较清楚这些都是用来干嘛的了,dict 是我们的主体结构,它有一个指向 dictType 的指针,这里面包含了字典的操作函数,然后是一个私有数据指针,接下来是一个 dictht 的数组,包含两个dictht,这个就是用来存数据的了,然后是 rehashidx 表示重哈希的状态,当是-1 的时候表示当前没有重哈希,iterators 表示正在遍历的迭代器的数量。
    首先说说为啥需要有两个 dictht,这是因为字典 dict 这个数据结构随着数据量的增减,会需要在中途做扩容或者缩容操作,如果只有一个的话,对它进行扩容缩容时会影响正常的访问和修改操作,或者说保证正常查询,修改的正确性会比较复杂,并且因为需要高效利用空间,不能一下子申请一个非常大的空间来存很少的数据。当 dict 中 dictht 中的数据量超过 size 的时候负载就超过了 1,就需要进行扩容,这里的其实跟 Java 中的 HashMap 比较类似,超过一定的负载之后进行扩容。这里为啥 size 会超过 1 呢,可能有部分不了解这类结构的同学会比较奇怪,其实就是上图中画的,在数据结构中对于散列的冲突有几类解决方法,比如转换成链表,二次散列,找下个空槽等,这里就使用了链表法,或者说拉链法。当一个新元素通过 hashFunction 得出的 key 跟 sizemask 取模之后的值相同了,那就将其放在原来的节点之前,变成链表挂在数组 dictht.table下面,放在原有节点前是考虑到可能会优先访问。
    忘了说明下 dictht 跟 dictEntry 的关系了,dictht 就是个哈希表,它里面是个dictEntry 的二维数组,而 dictEntry 是个包含了 key-value 结构之外还有一个 next 指针,因此可以将哈希冲突的以链表的形式保存下来。
    在重点说下重哈希,可能同样写 Java 的同学对这个比较有感觉,跟 HashMap 一样,会以 2 的 N 次方进行扩容,那么扩容的方法就会比较简单,每个键重哈希要不就在原来这个槽,要不就在原来的槽加原 dictht.size 的位置;然后是重头戏,具体是怎么做扩容呢,其实这里就把第二个 ht 用上了,其实这两个hashtable 的具体作用有点类似于 jvm 中的两个 survival 区,但是又不全一样,因为 redis 在扩容的时候是采用的渐进式地重哈希,什么叫渐进式的呢,就是它不是像 jvm 那种标记复制的模式直接将一个 eden 区和原来的 survival 区存活的对象复制到另一个 survival 区,而是在每一次添加,删除,查找或者更新操作时,都会额外的帮忙搬运一部分的原 dictht 中的数据,这里会根据 rehashidx 的值来判断,如果是-1 表示并没有在重哈希中,如果是 0 表示开始重哈希了,然后rehashidx 还会随着每次的帮忙搬运往上加,但全部被搬运完成后 rehashidx 又变回了-1,又可以扯到Java 中的 Concurrent HashMap, 他在扩容的时候也使用了类似的操作。

    -]]>
    - - Redis - 数据结构 - C - 源码 - Redis - - - redis - 数据结构 - 源码 - -
    - - redis数据结构介绍二-第二部分 跳表 - /2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ - 跳表 skiplist

    跳表是个在我们日常的代码中不太常用到的数据结构,相对来讲就没有像数组,链表,字典,散列,树等结构那么熟悉,所以就从头开始分析下,首先是链表,跳表跟链表都有个表字(太硬扯了我🤦‍♀️),注意这是个有序链表

    如上图,在这个链表里如果我要找到 23,是不是我需要从3,5,9开始一直往后找直到找到 23,也就是说时间复杂度是 O(N),N 的一次幂复杂度,那么我们来看看第二个

    这个结构跟原先有点不一样,它给链表中偶数位的节点又加了一个指针把它们链接起来,这样子当我们要寻找 23 的时候就可以从原来的一个个往下找变成跳着找,先找到 5,然后是 10,接着是 19,然后是 28,这时候发现 28 比 23 大了,那我在退回到 19,然后从下一层原来的链表往前找,

    这里毛估估是不是前面的节点我就少找了一半,有那么点二分法的意思。
    前面的其实是跳表的引子,真正的跳表其实不是这样,因为上面的其实有个比较大的问题,就是插入一个元素后需要调整每个元素的指针,在 redis 中的跳表其实是做了个随机层数的优化,因为沿着前面的例子,其实当数据量很大的时候,是不是层数越多,其查询效率越高,但是随着层数变多,要保持这种严格的层数规则其实也会增大处理复杂度,所以 redis 插入每个元素的时候都是使用随机的方式,看一眼代码

    -
    /* ZSETs use a specialized version of Skiplists */
    -typedef struct zskiplistNode {
    -    sds ele;
    -    double score;
    -    struct zskiplistNode *backward;
    -    struct zskiplistLevel {
    -        struct zskiplistNode *forward;
    -        unsigned long span;
    -    } level[];
    -} zskiplistNode;
    +  guarantee(target_pause_time_ms > 0.0,
    +            "target_pause_time_ms = %1.6lf should be positive", target_pause_time_ms);
     
    -typedef struct zskiplist {
    -    struct zskiplistNode *header, *tail;
    -    unsigned long length;
    -    int level;
    -} zskiplist;
    +  size_t pending_cards = _policy->pending_cards();
    +  double base_time_ms = _policy->predict_base_elapsed_time_ms(pending_cards);
    +  double time_remaining_ms = MAX2(target_pause_time_ms - base_time_ms, 0.0);
     
    -typedef struct zset {
    -    dict *dict;
    -    zskiplist *zsl;
    -} zset;
    -

    忘了说了,redis 是把 skiplist 跳表用在 zset 里,zset 是个有序的集合,可以看到 zskiplist 就是个跳表的结构,里面用 header 保存跳表的表头,tail 保存表尾,还有长度和最大层级,具体的跳表节点元素使用 zskiplistNode 表示,里面包含了 sds 类型的元素值,double 类型的分值,用来排序,一个 backward 后向指针和一个 zskiplistLevel 数组,每个 level 包含了一个前向指针,和一个 span,span 表示的是跳表前向指针的跨度,这里再补充一点,前面说了为了灵活这个跳表的新增修改,redis 使用了随机层高的方式插入新节点,但是如果所有节点都随机到很高的层级或者所有都很低的话,跳表的效率优势就会减小,所以 redis 使用了个小技巧,贴下代码

    -
    #define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
    -int zslRandomLevel(void) {
    -    int level = 1;
    -    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
    -        level += 1;
    -    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    -}
    -

    当随机值跟0xFFFF进行与操作小于ZSKIPLIST_P * 0xFFFF时才会增大 level 的值,因此保持了一个相对递减的概率
    可以简单分析下,当 random() 的值小于 0xFFFF 的 1/4,才会 level + 1,就意味着当有 1 - 1/4也就是3/4的概率是直接跳出,所以一层的概率是3/4,也就是 1-P,二层的概率是 P*(1-P),三层的概率是 P² * (1-P) 依次递推。

    -]]>
    - - Redis - 数据结构 - C - 源码 - Redis - - - redis - 数据结构 - 源码 - -
    - - redis数据结构介绍五-第五部分 对象 - /2020/01/20/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%94/ - 前面说了这么些数据结构,其实大家对于 redis 最初的印象应该就是个 key-value 的缓存,类似于 memcache,redis 其实也是个 key-value,key 还是一样的字符串,或者说就是用 redis 自己的动态字符串实现,但是 value 其实就是前面说的那些数据结构,差不多快说完了,还有个 quicklist 后面还有一篇,这里先介绍下 redis 对于这些不同类型的 value 是怎么实现的,首先看下 redisObject 的源码头文件

    -
    /* 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. */
    -/*
    - * 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_LINKEDLIST 4 /* No longer used: old list encoding. */
    -#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 */
    +  log_trace(gc, ergo, cset)("Start choosing CSet. pending cards: " SIZE_FORMAT " predicted base time: %1.2fms remaining time: %1.2fms target pause time: %1.2fms",
    +                            pending_cards, base_time_ms, time_remaining_ms, target_pause_time_ms);
     
    -#define LRU_BITS 24
    -#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
    -#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
    +  // The young list is laid with the survivor regions from the previous
    +  // pause are appended to the RHS of the young list, i.e.
    +  //   [Newly Young Regions ++ Survivors from last pause].
     
    -#define OBJ_SHARED_REFCOUNT INT_MAX
    -typedef 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;
    -

    主体结构就是这个 redisObject,

    -
      -
    • type: 字段表示对象的类型,它对应的就是 redis 的对外暴露的,或者说用户可以使用的五种类型,OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
    • -
    • encoding: 字段表示这个对象在 redis 内部的编码方式,由OBJ_ENCODING_开头的 11 种
    • -
    • lru: 做LRU替换算法用,占24个bit
    • -
    • refcount: 引用计数。它允许robj对象在某些情况下被共享。
    • -
    • ptr: 指向底层实现数据结构的指针
      当 type 是 OBJ_STRING 时,表示类型是个 string,它的编码方式 encoding 可能有 OBJ_ENCODING_RAW,OBJ_ENCODING_INT,OBJ_ENCODING_EMBSTR 三种
      当 type 是 OBJ_LIST 时,表示类型是 list,它的编码方式 encoding 是 OBJ_ENCODING_QUICKLIST,对于早一些的版本,2.2这种可能还会使用 OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_LINKEDLIST
      当 type 是 OBJ_SET 时,是个集合,但是得看具体元素的类型,有可能使用整数集合,OBJ_ENCODING_INTSET, 如果元素不全是整型或者数量超过一定限制,那么编码就是 OBJ_ENCODING_HT hash table 了
      当 type 是 OBJ_ZSET 时,是个有序集合,它底层有可能使用的是 OBJ_ENCODING_ZIPLIST 或者 OBJ_ENCODING_SKIPLIST
      当 type 是 OBJ_HASH 时,一开始也是 OBJ_ENCODING_ZIPLIST,然后当数据量大于 hash_max_ziplist_entries 时会转成 OBJ_ENCODING_HT
    • -
    -]]>
    - - Redis - 数据结构 - C - 源码 - Redis - - - redis - 数据结构 - 源码 - -
    - - rabbitmq-tips - /2017/04/25/rabbitmq-tips/ - rabbitmq 介绍

    接触了一下rabbitmq,原来在选型的时候是在rabbitmq跟kafka之间做选择,网上搜了一下之后发现kafka的优势在于吞吐量,而rabbitmq相对注重可靠性,因为应用在im上,需要保证消息不能丢失所以就暂时选定rabbitmq,
    Message Queue的需求由来已久,80年代最早在金融交易中,高盛等公司采用Teknekron公司的产品,当时的Message queuing软件叫做:the information bus(TIB)。 TIB被电信和通讯公司采用,路透社收购了Teknekron公司。之后,IBM开发了MQSeries,微软开发了Microsoft Message Queue(MSMQ)。这些商业MQ供应商的问题是厂商锁定,价格高昂。2001年,Java Message queuing试图解决锁定和交互性的问题,但对应用来说反而更加麻烦了。
    RabbitMQ采用Erlang语言开发。Erlang语言由Ericson设计,专门为开发concurrent和distribution系统的一种语言,在电信领域使用广泛。OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件/库/工具,如mnesia/SASL,极大方便了Erlang应用的开发。OTP就类似于Python语言中众多的module,用户借助这些module可以很方便的开发应用。
    于是2004年,摩根大通和iMatrix开始着手Advanced Message Queuing Protocol (AMQP)开放标准的开发。2006年,AMQP规范发布。2007年,Rabbit技术公司基于AMQP标准开发的RabbitMQ 1.0 发布。所有主要的编程语言均有与代理接口通讯的客户端库。

    -

    简单的使用经验

    通俗的理解

    这里介绍下其中的一些概念,connection表示和队列服务器的连接,一般情况下是tcp连接, channel表示通道,可以在一个连接上建立多个通道,这里主要是节省了tcp连接握手的成本,exchange可以理解成一个路由器,将消息推送给对应的队列queue,其实是像一个订阅的模式。

    -

    集群经验

    rabbitmqctl stop这个是关闭rabbitmq,在搭建集群时候先关闭服务,然后使用rabbitmq-server -detached静默启动,这时候使用rabbitmqctl cluster_status查看集群状态,因为还没将节点加入集群,所以只能看到类似

    -
    Cluster status of node rabbit@rabbit1 ...
    -[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]},
    - {running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}]
    -...done.
    -

    然后就可以把当前节点加入集群,

    -
    rabbit2$ rabbitmqctl stop_app #这个stop_app与stop的区别是前者停的是rabbitmq应用,保留erlang节点,
    -                              #后者是停止了rabbitmq和erlang节点
    -Stopping node rabbit@rabbit2 ...done.
    -rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1 #这里可以用--ram指定将当前节点作为内存节点加入集群
    -Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done.
    -rabbit2$ rabbitmqctl start_app
    -Starting node rabbit@rabbit2 ...done.
    -

    其他可以参考官方文档

    -

    一些坑

    消息丢失

    这里碰到过一个坑,对于使用exchange来做消息路由的,会有一个情况,就是在routing_key没被订阅的时候,会将该条找不到路由对应的queue的消息丢掉What happens if we break our contract and send a message with one or four words, like "orange" or "quick.orange.male.rabbit"? Well, these messages won't match any bindings and will be lost.对应链接,而当使用空的exchange时,会保留消息,当出现消费者的时候就可以将收到之前生产者所推送的消息对应链接,这里就是用了空的exchange。

    -

    集群搭建

    集群搭建的时候有个erlang vm生成的random cookie,这个是用来做集群之间认证的,相同的cookie才能连接,但是如果通过vim打开复制后在其他几点新建文件写入会多一个换行,导致集群建立是报错,所以这里最好使用scp等传输命令直接传输cookie文件,同时要注意下cookie的文件权限。
    另外在集群搭建的时候如果更改过hostname,那么要把rabbitmq的数据库删除,否则启动后会马上挂掉

    -]]>
    - - php - - - php - mq - im - -
    - - redis数据结构介绍六 快表 - /2020/01/22/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%85%AD/ - 这应该是 redis 系列的最后一篇了,讲下快表,其实最前面讲的链表在早先的 redis 版本中也作为 list 的数据结构使用过,但是单纯的链表的缺陷之前也说了,插入便利,但是空间利用率低,并且不能进行二分查找等,检索效率低,ziplist 压缩表的产生也是同理,希望获得更好的性能,包括存储空间和访问性能等,原来我也不懂这个快表要怎么快,然后明白了一个道理,其实并没有什么银弹,只是大牛们会在适合的时候使用最适合的数据结构来实现性能的最大化,这里面有一招就是不同数据结构的组合调整,比如 Java 中的 HashMap,在链表节点数大于 8 时会转变成红黑树,以此提高访问效率,不费话了,回到快表,quicklist,这个数据结构主要使用在 list 类型中,如果我说其实这个 quicklist 就是个链表,可能大家不太会相信,但是事实上的确可以认为 quicklist 是个双向链表,看下代码

    -
    /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
    - * We use bit fields keep the quicklistNode at 32 bytes.
    - * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
    - * encoding: 2 bits, RAW=1, LZF=2.
    - * container: 2 bits, NONE=1, ZIPLIST=2.
    - * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
    - * attempted_compress: 1 bit, boolean, used for verifying during testing.
    - * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
    -typedef struct quicklistNode {
    -    struct quicklistNode *prev;
    -    struct quicklistNode *next;
    -    unsigned char *zl;
    -    unsigned int sz;             /* ziplist size in bytes */
    -    unsigned int count : 16;     /* count of items in ziplist */
    -    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    -    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    -    unsigned int recompress : 1; /* was this node previous compressed? */
    -    unsigned int attempted_compress : 1; /* node can't compress; too small */
    -    unsigned int extra : 10; /* more bits to steal for future usage */
    -} quicklistNode;
    +  uint survivor_region_length = survivors->length();
    +  uint eden_region_length = _g1h->eden_regions_count();
    +  init_region_lengths(eden_region_length, survivor_region_length);
    +
    +  verify_young_cset_indices();
     
    -/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
    - * 'sz' is byte length of 'compressed' field.
    - * 'compressed' is LZF data with total (compressed) length 'sz'
    - * NOTE: uncompressed length is stored in quicklistNode->sz.
    - * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
    -typedef struct quicklistLZF {
    -    unsigned int sz; /* LZF size in bytes*/
    -    char compressed[];
    -} quicklistLZF;
    +  // Clear the fields that point to the survivor list - they are all young now.
    +  survivors->convert_to_eden();
     
    -/* 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. */
    -typedef struct quicklist {
    -    quicklistNode *head;
    -    quicklistNode *tail;
    -    unsigned long count;        /* total count of all entries in all ziplists */
    -    unsigned long len;          /* number of quicklistNodes */
    -    int fill : 16;              /* fill factor for individual nodes */
    -    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
    -} quicklist;
    -

    粗略看下,quicklist 里有 head,tail, quicklistNode里有 prev,next 指针,是不是有链表的基本轮廓了,那么为啥这玩意要称为快表呢,快在哪,关键就在这个unsigned char *zl;zl 是不是前面又看到过,就是 ziplist ,这是什么鬼,链表里用压缩表,这不套娃么,先别急,回顾下前面说的 ziplist,ziplist 有哪些特点,内存利用率高,可以从表头快速定位到尾节点,节点可以从后往前找,但是有个缺点,就是从中间插入的效率比较低,需要整体往后移,这个其实是普通数组的优化版,但还是有数组的一些劣势,所以要真的快,是不是可以将链表跟数组真的结合起来。

    -

    ziplist

    这里有两个 redis 的配置参数,list-max-ziplist-sizelist-compress-depth,先来说第一个,既然快表是将链表跟压缩表数组结合起来使用,那么具体怎么用呢,比如我有一个 10 个元素的 list,那具体怎么放,每个 quicklistNode 里放多大的 ziplist,假如每个快表节点的 ziplist 只放一个元素,那么其实这就退化成了一个链表,如果 10 个元素放在一个 quicklistNode 的 ziplist 里,那就退化成了一个 ziplist,所以有了这个 list-max-ziplist-size,而且它还比较牛,能取正负值,当是正值时,对应的就是每个 quicklistNode 的 ziplist 中的元素个数,比如配置了 list-max-ziplist-size = 5,那么我刚才的 10 个元素的 list 就是一个两个 quicklistNode 组成的快表,每个 quicklistNode 中的 ziplist 包含了五个元素,当 list-max-ziplist-size取负值的时候,它限制了 ziplist 的字节数

    -
    size_t offset = (-fill) - 1;
    -if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
    -    if (sz <= optimization_level[offset]) {
    -        return 1;
    -    } else {
    -        return 0;
    -    }
    -} else {
    -    return 0;
    -}
    +  _bytes_used_before = _inc_bytes_used_before;
    +  time_remaining_ms = MAX2(time_remaining_ms - _inc_predicted_elapsed_time_ms, 0.0);
     
    -/* Optimization levels for size-based filling */
    -static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
    +  log_trace(gc, ergo, cset)("Add young regions to CSet. eden: %u regions, survivors: %u regions, predicted young region time: %1.2fms, target pause time: %1.2fms",
    +                            eden_region_length, survivor_region_length, _inc_predicted_elapsed_time_ms, target_pause_time_ms);
     
    -/* Create a new quicklist.
    - * Free with quicklistRelease(). */
    -quicklist *quicklistCreate(void) {
    -    struct quicklist *quicklist;
    +  // The number of recorded young regions is the incremental
    +  // collection set's current size
    +  set_recorded_rs_lengths(_inc_recorded_rs_lengths);
     
    -    quicklist = zmalloc(sizeof(*quicklist));
    -    quicklist->head = quicklist->tail = NULL;
    -    quicklist->len = 0;
    -    quicklist->count = 0;
    -    quicklist->compress = 0;
    -    quicklist->fill = -2;
    -    return quicklist;
    -}
    -

    这个 fill 就是传进来的 list-max-ziplist-size, 具体对应的就是

    -
      -
    • -5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
    • -
    • -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
    • -
    • -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
    • -
    • -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)也就是上面的 quicklist->fill = -2;
    • -
    • -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
    • -
    -

    压缩

    list-compress-depth这个参数呢是用来配置压缩的,等等压缩是为啥,不是里面已经是压缩表了么,大牛们就是为了性能殚精竭虑,这里考虑到的是一个场景,一般状况下,list 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示

    -
    /* depth of end nodes not to compress;0=off */
    -
      -
    • 0,代表不压缩,默认值
    • -
    • 1,两端各一个节点不压缩
    • -
    • 2,两端各两个节点不压缩
    • -
    • … 依次类推
      压缩后的 ziplist 就会变成 quicklistLZF,然后替换 zl 指针,这里使用的是 LZF 压缩算法,压缩后的 quicklistLZF 中的 compressed 也是个柔性数组,压缩后的 ziplist 整个就放进这个柔性数组
    • -
    -

    插入过程

    简单说下插入元素的过程

    -
    /* Wrapper to allow argument-based switching between HEAD/TAIL pop */
    -void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
    -                   int where) {
    -    if (where == QUICKLIST_HEAD) {
    -        quicklistPushHead(quicklist, value, sz);
    -    } else if (where == QUICKLIST_TAIL) {
    -        quicklistPushTail(quicklist, value, sz);
    -    }
    -}
    +  double young_end_time_sec = os::elapsedTime();
    +  phase_times()->record_young_cset_choice_time_ms((young_end_time_sec - young_start_time_sec) * 1000.0);
     
    -/* Add new entry to head node of quicklist.
    - *
    - * Returns 0 if used existing head.
    - * Returns 1 if new head created. */
    -int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    -    quicklistNode *orig_head = quicklist->head;
    -    if (likely(
    -            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
    -        quicklist->head->zl =
    -            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
    -        quicklistNodeUpdateSz(quicklist->head);
    -    } else {
    -        quicklistNode *node = quicklistCreateNode();
    -        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
    +  return time_remaining_ms;
    +}
    +

    下面是老年代的部分

    +
    void G1CollectionSet::finalize_old_part(double time_remaining_ms) {
    +  double non_young_start_time_sec = os::elapsedTime();
    +  double predicted_old_time_ms = 0.0;
     
    -        quicklistNodeUpdateSz(node);
    -        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    -    }
    -    quicklist->count++;
    -    quicklist->head->count++;
    -    return (orig_head != quicklist->head);
    -}
    +  if (collector_state()->in_mixed_phase()) {
    +    cset_chooser()->verify();
    +    const uint min_old_cset_length = _policy->calc_min_old_cset_length();
    +    const uint max_old_cset_length = _policy->calc_max_old_cset_length();
     
    -/* Add new entry to tail node of quicklist.
    - *
    - * Returns 0 if used existing tail.
    - * Returns 1 if new tail created. */
    -int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    -    quicklistNode *orig_tail = quicklist->tail;
    -    if (likely(
    -            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
    -        quicklist->tail->zl =
    -            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
    -        quicklistNodeUpdateSz(quicklist->tail);
    -    } else {
    -        quicklistNode *node = quicklistCreateNode();
    -        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
    +    uint expensive_region_num = 0;
    +    bool check_time_remaining = _policy->adaptive_young_list_length();
     
    -        quicklistNodeUpdateSz(node);
    -        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    -    }
    -    quicklist->count++;
    -    quicklist->tail->count++;
    -    return (orig_tail != quicklist->tail);
    -}
    +    HeapRegion* hr = cset_chooser()->peek();
    +    while (hr != NULL) {
    +      if (old_region_length() >= max_old_cset_length) {
    +        // Added maximum number of old regions to the CSet.
    +        log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached max). old %u regions, max %u regions",
    +                                  old_region_length(), max_old_cset_length);
    +        break;
    +      }
     
    -/* Wrappers for node inserting around existing node. */
    -REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
    -                                             quicklistNode *old_node,
    -                                             quicklistNode *new_node) {
    -    __quicklistInsertNode(quicklist, old_node, new_node, 0);
    -}
    +      // Stop adding regions if the remaining reclaimable space is
    +      // not above G1HeapWastePercent.
    +      size_t reclaimable_bytes = cset_chooser()->remaining_reclaimable_bytes();
    +      double reclaimable_percent = _policy->reclaimable_bytes_percent(reclaimable_bytes);
    +      double threshold = (double) G1HeapWastePercent;
    +      if (reclaimable_percent <= threshold) {
    +        // We've added enough old regions that the amount of uncollected
    +        // reclaimable space is at or below the waste threshold. Stop
    +        // adding old regions to the CSet.
    +        log_debug(gc, ergo, cset)("Finish adding old regions to CSet (reclaimable percentage not over threshold). "
    +                                  "old %u regions, max %u regions, reclaimable: " SIZE_FORMAT "B (%1.2f%%) threshold: " UINTX_FORMAT "%%",
    +                                  old_region_length(), max_old_cset_length, reclaimable_bytes, reclaimable_percent, G1HeapWastePercent);
    +        break;
    +      }
     
    -REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
    -                                            quicklistNode *old_node,
    -                                            quicklistNode *new_node) {
    -    __quicklistInsertNode(quicklist, old_node, new_node, 1);
    -}
    +      double predicted_time_ms = predict_region_elapsed_time_ms(hr);
    +      if (check_time_remaining) {
    +        if (predicted_time_ms > time_remaining_ms) {
    +          // Too expensive for the current CSet.
     
    -/* Insert 'new_node' after 'old_node' if 'after' is 1.
    - * Insert 'new_node' before 'old_node' if 'after' is 0.
    - * Note: 'new_node' is *always* uncompressed, so if we assign it to
    - *       head or tail, we do not need to uncompress it. */
    -REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
    -                                        quicklistNode *old_node,
    -                                        quicklistNode *new_node, int after) {
    -    if (after) {
    -        new_node->prev = old_node;
    -        if (old_node) {
    -            new_node->next = old_node->next;
    -            if (old_node->next)
    -                old_node->next->prev = new_node;
    -            old_node->next = new_node;
    +          if (old_region_length() >= min_old_cset_length) {
    +            // We have added the minimum number of old regions to the CSet,
    +            // we are done with this CSet.
    +            log_debug(gc, ergo, cset)("Finish adding old regions to CSet (predicted time is too high). "
    +                                      "predicted time: %1.2fms, remaining time: %1.2fms old %u regions, min %u regions",
    +                                      predicted_time_ms, time_remaining_ms, old_region_length(), min_old_cset_length);
    +            break;
    +          }
    +
    +          // We'll add it anyway given that we haven't reached the
    +          // minimum number of old regions.
    +          expensive_region_num += 1;
             }
    -        if (quicklist->tail == old_node)
    -            quicklist->tail = new_node;
    -    } else {
    -        new_node->next = old_node;
    -        if (old_node) {
    -            new_node->prev = old_node->prev;
    -            if (old_node->prev)
    -                old_node->prev->next = new_node;
    -            old_node->prev = new_node;
    +      } else {
    +        if (old_region_length() >= min_old_cset_length) {
    +          // In the non-auto-tuning case, we'll finish adding regions
    +          // to the CSet if we reach the minimum.
    +
    +          log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached min). old %u regions, min %u regions",
    +                                    old_region_length(), min_old_cset_length);
    +          break;
             }
    -        if (quicklist->head == old_node)
    -            quicklist->head = new_node;
    +      }
    +
    +      // We will add this region to the CSet.
    +      time_remaining_ms = MAX2(time_remaining_ms - predicted_time_ms, 0.0);
    +      predicted_old_time_ms += predicted_time_ms;
    +      cset_chooser()->pop(); // already have region via peek()
    +      _g1h->old_set_remove(hr);
    +      add_old_region(hr);
    +
    +      hr = cset_chooser()->peek();
         }
    -    /* If this insert creates the only element so far, initialize head/tail. */
    -    if (quicklist->len == 0) {
    -        quicklist->head = quicklist->tail = new_node;
    +    if (hr == NULL) {
    +      log_debug(gc, ergo, cset)("Finish adding old regions to CSet (candidate old regions not available)");
         }
     
    -    if (old_node)
    -        quicklistCompress(quicklist, old_node);
    +    if (expensive_region_num > 0) {
    +      // We print the information once here at the end, predicated on
    +      // whether we added any apparently expensive regions or not, to
    +      // avoid generating output per region.
    +      log_debug(gc, ergo, cset)("Added expensive regions to CSet (old CSet region num not reached min)."
    +                                "old: %u regions, expensive: %u regions, min: %u regions, remaining time: %1.2fms",
    +                                old_region_length(), expensive_region_num, min_old_cset_length, time_remaining_ms);
    +    }
    +
    +    cset_chooser()->verify();
    +  }
    +
    +  stop_incremental_building();
    +
    +  log_debug(gc, ergo, cset)("Finish choosing CSet. old: %u regions, predicted old region time: %1.2fms, time remaining: %1.2f",
    +                            old_region_length(), predicted_old_time_ms, time_remaining_ms);
     
    -    quicklist->len++;
    -}
    -

    前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。

    -]]>
    - - Redis - 数据结构 - C - 源码 - Redis - - - redis - 数据结构 - 源码 - -
    - - redis数据结构介绍四-第四部分 压缩表 - /2020/01/19/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%9B%9B/ - 在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

    -
    -

    The ziplist is a specially encoded dually linked list that is designed to be very memory efficient.
    这是摘自 redis 源码中ziplist.c 文件的注释,也说明了原因,它的大概结构是这样子

    -
    -
    <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
    -

    其中
    <zlbytes>表示 ziplist 占用的字节总数,类型是uint32_t,32 位的无符号整型,当然表示的字节数也包含自己本身占用的 4 个
    <zltail> 类型也是是uint32_t,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。<zltail>的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。
    <uint16_t zllen> 表示ziplist 中的数据项个数,因为是 16 位,所以当数量超过所能表示的最大的数量,它的 16 位全会置为 1,但是真实的数量需要遍历整个 ziplist 才能知道
    <entry>是具体的数据项,后面解释
    <zlend> ziplist 的最后一个字节,固定是255。
    再看一下<entry>中的具体结构,

    -
    <prevlen> <encoding> <entry-data>
    -

    首先这个<prevlen>有两种情况,一种是前面的元素的长度,如果是小于等于 253的时候就用一个uint8_t 来表示前一元素的长度,如果大于的话他将占用五个字节,第一个字节是 254,即表示这个字节已经表示不下了,需要后面的四个字节帮忙表示
    <encoding>这个就比较复杂,把源码的注释放下面先看下

    -
    * |00pppppp| - 1 byte
    -*      String value with length less than or equal to 63 bytes (6 bits).
    -*      "pppppp" represents the unsigned 6 bit length.
    -* |01pppppp|qqqqqqqq| - 2 bytes
    -*      String value with length less than or equal to 16383 bytes (14 bits).
    -*      IMPORTANT: The 14 bit number is stored in big endian.
    -* |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
    -*      String value with length greater than or equal to 16384 bytes.
    -*      Only the 4 bytes following the first byte represents the length
    -*      up to 32^2-1. The 6 lower bits of the first byte are not used and
    -*      are set to zero.
    -*      IMPORTANT: The 32 bit number is stored in big endian.
    -* |11000000| - 3 bytes
    -*      Integer encoded as int16_t (2 bytes).
    -* |11010000| - 5 bytes
    -*      Integer encoded as int32_t (4 bytes).
    -* |11100000| - 9 bytes
    -*      Integer encoded as int64_t (8 bytes).
    -* |11110000| - 4 bytes
    -*      Integer encoded as 24 bit signed (3 bytes).
    -* |11111110| - 2 bytes
    -*      Integer encoded as 8 bit signed (1 byte).
    -* |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
    -*      Unsigned integer from 0 to 12. The encoded value is actually from
    -*      1 to 13 because 0000 and 1111 can not be used, so 1 should be
    -*      subtracted from the encoded 4 bit value to obtain the right value.
    -* |11111111| - End of ziplist special entry.
    -

    首先如果 encoding 的前两位是 00 的话代表这个元素是个 6 位的字符串,即直接将数据保存在 encoding 中,不消耗额外的<entry-data>,如果前两位是 01 的话表示是个 14 位的字符串,如果是 10 的话表示encoding 块之后的四个字节是存放字符串类型的数据,encoding 的剩余 6 位置 0。
    如果 encoding 的前两位是 11 的话表示这是个整型,具体的如果后两位是00的话,表示后面是个2字节的 int16_t 类型,如果是01的话,后面是个4字节的int32_t,如果是10的话后面是8字节的int64_t,如果是 11 的话后面是 3 字节的有符号整型,这些都要最后 4 位都是 0 的情况噢
    剩下当是11111110时,则表示是一个1 字节的有符号数,如果是 1111xxxx,其中xxxx在0000 到 1101 表示实际的 1 到 13,为啥呢,因为 0000 前面已经用过了,而 1110 跟 1111 也都有用了。
    看个具体的例子(上下有点对不齐,将就看)

    -
    [0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
    -|**zlbytes***|  |***zltail***|  |*zllen*|  |entry1 entry2|  |zlend|
    -

    第一部分代表整个 ziplist 有 15 个字节,zlbytes 自己占了 4 个 zltail 表示最后一个元素的偏移量,第 13 个字节起,zllen 表示有 2 个元素,第一个元素是00f3,00表示前一个元素长度是 0,本来前面就没元素(不过不知道这个能不能优化这一字节),然后是 f3,换成二进制就是11110011,对照上面的注释,是落在|1111xxxx|这个类型里,注意这个其实是用 0001 到 1101 也就是 1到 13 来表示 0到 12,所以 f3 应该就是 2,第一个元素是 2,第二个元素呢,02 代表前一个元素也就是刚才说的这个,占用 2 字节,f6 展开也是刚才的类型,实际是 5,ff 表示 ziplist 的结尾,所以这个 ziplist 里面是两个元素,2 跟 5

    + double non_young_end_time_sec = os::elapsedTime(); + phase_times()->record_non_young_cset_choice_time_ms((non_young_end_time_sec - non_young_start_time_sec) * 1000.0); + + QuickSort::sort(_collection_set_regions, _collection_set_cur_length, compare_region_idx, true); +}
    +

    上面第三行是个判断,当前是否是 mixed 回收阶段,如果不是的话其实是没有老年代什么事的,所以可以看到代码基本是从这个 if 判断
    if (collector_state()->in_mixed_phase()) {开始往下走的
    先写到这,偏向于做笔记用,有错轻拍

    ]]>
    - Redis - 数据结构 - C - 源码 - Redis + Java + JVM + GC + C++ - redis - 数据结构 - 源码 + Java + JVM + C++
    @@ -6998,44 +6964,6 @@ REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist, Evict - - redis过期策略复习 - /2021/07/25/redis%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ - redis过期策略复习

    之前其实写过redis的过期的一些原理,这次主要是记录下,一些使用上的概念,主要是redis使用的过期策略是懒过期和定时清除,懒过期的其实比较简单,即是在key被访问的时候会顺带着判断下这个key是否已过期了,如果已经过期了,就不返回了,但是这种策略有个漏洞是如果有些key之后一直不会被访问了,就等于沉在池底了,所以需要有一个定时的清理机制,去从设置了过期的key池子(expires)里随机地捞key,具体的策略我们看下官网的解释

    -
      -
    1. Test 20 random keys from the set of keys with an associated expire.
    2. -
    3. Delete all the keys found expired.
    4. -
    5. If more than 25% of keys were expired, start again from step 1.
    6. -
    -

    从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件

    -
    # Redis calls an internal function to perform many background tasks, like
    -# closing connections of clients in timeout, purging expired keys that are
    -# never requested, and so forth.
    -#
    -# Not all tasks are performed with the same frequency, but Redis checks for
    -# tasks to perform according to the specified "hz" value.
    -#
    -# By default "hz" is set to 10. Raising the value will use more CPU when
    -# Redis is idle, but at the same time will make Redis more responsive when
    -# there are many keys expiring at the same time, and timeouts may be
    -# handled with more precision.
    -#
    -# The range is between 1 and 500, however a value over 100 is usually not
    -# a good idea. Most users should use the default of 10 and raise this up to
    -# 100 only in environments where very low latency is required.
    -hz 10
    - -

    可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略

    -]]>
    - - redis - - - redis - 应用 - 过期策略 - -
    redis系列介绍七-过期策略 /2020/04/12/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E4%B8%83/ @@ -7466,54 +7394,164 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100; - rust学习笔记-所有权三之切片 - /2021/05/16/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%B8%89%E4%B9%8B%E5%88%87%E7%89%87/ - 除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice)。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。
    例如代码

    -
    fn main() {
    -    let mut s = String::from("hello world");
    +    MFC 模态对话框
    +    /2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/
    +    void CTestDialog::OnBnClickedOk() 
    +{
    +	CString m_SrcTest;
    +	int nIndex = m_CbTest.GetCurSel(); 
    +	m_CbTest.GetLBText(nIndex, m_SrcTest);  
    +	OnOK();
    +}
    - let word = first_word(&s); +

    模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
    对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
    要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
    在主对话框类得到值。

    +]]>
    + + C++ + + + c++ + mfc + +
    + + openresty + /2019/06/18/openresty/ + 目前公司要对一些新的产品功能做灰度测试,因为在后端业务代码层面添加判断比较麻烦,所以想在nginx上做点手脚,就想到了openresty
    前后也踩了不少坑,这边先写一点

    +

    首先是日志
    error_log logs/error.log debug;
    需要nginx开启日志的debug才能看到日志

    +

    使用 lua_code_cache off即可, 另外注意只有使用 content_by_lua_file 才会生效

    +
    http {
    +  lua_code_cache off;
    +}
     
    -    s.clear();
    +location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
    +   default_type 'application/json;charset=utf-8';
    +   content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
    +}
    - // 这时候虽然 word 还是 5,但是 s 已经被清除了,所以就没存在的意义 -}
-

这里其实我们就需要关注 s 的存在性,代码的逻辑合理性就需要额外去维护,此时我们就可以用切片

-
let s = String::from("hello world")
+

使用lua给nginx请求response头添加内容可以用这个

+
ngx.header['response'] = 'header'
-let hello = &s[0..5]; -let world = &s[6..11];
-

其实跟 Python 的list 之类的语法有点类似,当然里面还有些语法糖,比如可以直接用省略后面的数字表示直接引用到结尾

-
let hello = &s[0..];
-

甚至再进一步

-
let hello = &s[..];
-

使用了切片之后

-
fn first_word(s: &String) -> &str {
-    let bytes = s.as_bytes();
 
-    for (i, &item) in bytes.iter().enumerate() {
-        if item == b' ' {
-            return &s[0..i];
-        }
-    }
+

使用总结

+

后续:

+
    +
  1. 一开始在本地环境的时候使用content_by_lua_file只关注了头,后来发到测试环境发现请求内容都没代理转发到后端服务上
    网上查了下发现content_by_lua_file是将请求的所有内容包括response都用这里面的lua脚本生成了,content这个词就表示是请求内容
    后来改成了access_by_lua_file就正常了,只是要去获取请求内容和修改响应头,并不是要完整的接管请求

    +
  2. +
  3. 后来又碰到了一个坑是nginx有个client_body_buffer_size的配置参数,nginx在32位和64位系统里有8K和16K两个默认值,当请求内容大于这两个值的时候,会把请求内容放到临时文件里,这个时候openresty里的ngx.req.get_post_args()就会报“failed to get post args: requesty body in temp file not supported”这个错误,将client_body_buffer_size这个参数配置调大一点就好了

    +
  4. +
  5. 还有就是lua的异常捕获,网上看一般是用pcall和xpcall来进行保护调用,因为问题主要出在cjson的decode,这里有两个解决方案,一个就是将cjson.decode使用pcall封装,

    +
    local decode = require("cjson").decode
     
    -    &s[..]
    +function json_decode( str )
    +    local ok, t = pcall(decode, str)
    +    if not ok then
    +      return nil
    +    end
    +
    +    return t
    +end
    +

    这个是使用了pcall,称为保护调用,会在内部错误后返回两个参数,第一个是false,第二个是错误信息
    还有一种是使用cjson.safe包

    +
    local json = require("cjson.safe")
    +local str = [[ {"key:"value"} ]]
    +
    +local t = json.decode(str)
    +if t then
    +    ngx.say(" --> ", type(t))
    +end
    +

    cjson.safe包会在解析失败的时候返回nil

    +
  6. +
  7. 还有一个是redis链接时如果host使用的是域名的话会提示“failed to connect: no resolver defined to resolve “redis.xxxxxx.com””,这里需要使用nginx的resolver指令,
    resolver 8.8.8.8 valid=3600s;

    +
  8. +
  9. 还有一点补充下
    就是业务在使用redis的时候使用了db的特性,所以在lua访问redis的时候也需要执行db,这里lua的redis库也支持了这个特性,可以使用instance:select(config:get(‘db’))来切换db

    +
  10. +
  11. 性能优化tips
    建议是尽量少使用阶段钩子,例如content_by_lua_file,*_by_lua

    +
  12. +
  13. 发现一个不错的openresty站点
    地址

    +
  14. +
+]]> + + nginx + + + nginx + openresty + + + + rust学习笔记-所有权二 + /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ + 这里需要说道函数和返回值了
可以看书上的这个例子

对于这种情况,当进入函数内部时,会把传入的变量的所有权转移进函数内部,如果最后还是要返回该变量,但是如果此时还要返回别的计算结果,就可能需要笨拙地使用元组

+

引用

此时我们就可以用引用来解决这个问题

+
fn main() {
+    let s1 = String::from("hello");
+    let len = calculate_length(&s1);
+
+    println!("The length of '{}' is {}", s1, len);
+}
+fn calculate_length(s: &String) -> usize {
+    s.len()
+}
+

这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值

由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃

+

可变引用

而当我们尝试对引用的字符串进行修改时

+
fn main() {
+    let s1 = String::from("hello");
+    change(&s1);
+}
+fn change(s: &String) {
+    s.push_str(", world");
+}
+

就会有以下报错,

其实也很容易发现,毕竟没有 mut 指出这是可变引用,同时需要将 s1 改成 mut 可变的

+
fn main() {
+    let mut s1 = String::from("hello");
+    change(&mut s1);
+}
+
+
+fn change(s: &mut String) {
+    s.push_str(", world");
+}
+

再看一个例子

+
fn main() {
+    let mut s1 = String::from("hello");
+    let r1 = &mut s1;
+    let r2 = &mut s1;
+}
+

这个例子在书里是会报错的,因为同时存在一个以上的可变引用,但是在我运行的版本里前面这段没有报错,只有当我真的要去更改的时候

+
fn main() {
+    let mut s1 = String::from("hello");
+    let mut r1 = &mut s1;
+    let mut r2 = &mut s1;
+    change(&mut r1);
+    change(&mut r2);
 }
-fn main() {
-    let mut s = String::from("hello world");
 
-    let word = first_word(&s);
 
-    s.clear(); // error!
+fn change(s: &mut String) {
+    s.push_str(", world");
+}
+


这里可能就是具体版本在实现上的一个差异,我用的 rustc 是 1.44.0 版本
其实上面的主要是由 rust 想要避免这类多重可变更导致的异常问题,总结下就是三个点

+
    +
  • 两个或两个以上的指针同时同时访问同一空间
  • +
  • 其中至少有一个指针会想空间中写入数据
  • +
  • 没有同步数据访问的机制
    并且我们不能在拥有不可变引用的情况下创建可变引用
  • +
+

悬垂引用

还有一点需要注意的就是悬垂引用

+
fn main() {
+    let reference_to_nothing = dangle();
+}
 
-    println!("the first word is: {}", word);
-}
-

那再执行 main 函数的时候就会抛错,因为 word 还是个切片,需要保证 s 的有效性,并且其实我们可以将函数申明成

-
fn first_word(s: &str) -> &str {
-

这样就既能处理&String 的情况,就是当成完整字符串的切片,也能处理普通的切片。
其他类型的切片

-
let a = [1, 2, 3, 4, 5];
-let slice = &a[1..3];
-

简单记录下,具体可以去看看这本书

+fn dangle() -> &String { + let s = String::from("hello"); + &s +}
+

这里可以看到其实在 dangle函数返回后,这里的 s 理论上就离开了作用域,但是由于返回了 s 的引用,在 main 函数中就会拿着这个引用,就会出现如下错误

+

总结

最后总结下

+
    +
  • 在任何一个段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
  • +
  • 引用总是有效的。
  • +
]]>
语言 @@ -7526,491 +7564,415 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;新语言 可变引用 不可变引用 - 切片
- Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析 - /2022/10/02/Leetcode-747-%E8%87%B3%E5%B0%91%E6%98%AF%E5%85%B6%E4%BB%96%E6%95%B0%E5%AD%97%E4%B8%A4%E5%80%8D%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0-Largest-Number-At-Least-Twice-of-Others-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

You are given an integer array nums where the largest integer is unique.

-

Determine whether the largest element in the array is at least twice as much as every other number in the array. If it is, return the index of the largest element, or return -1 otherwise.
确认在数组中的最大数是否是其余任意数的两倍大及以上,如果是返回索引,如果不是返回-1

-

示例

Example 1:

-

Input: nums = [3,6,1,0]
Output: 1
Explanation: 6 is the largest integer.
For every other number in the array x, 6 is at least twice as big as x.
The index of value 6 is 1, so we return 1.

-
-

Example 2:

-

Input: nums = [1,2,3,4]
Output: -1
Explanation: 4 is less than twice the value of 3, so we return -1.

-
-

提示:

    -
  • 2 <= nums.length <= 50
  • -
  • 0 <= nums[i] <= 100
  • -
  • The largest element in nums is unique.
  • -
-

简要解析

这个题easy是题意也比较简单,找最大值,并且最大值是其他任意值的两倍及以上,其实就是找最大值跟次大值,比较下就好了

-

代码

public int dominantIndex(int[] nums) {
-    int largest = Integer.MIN_VALUE;
-    int second = Integer.MIN_VALUE;
-    int largestIndex = -1;
-    for (int i = 0; i < nums.length; i++) {
-        // 如果有最大的就更新,同时更新最大值和第二大的
-        if (nums[i] > largest) {
-            second = largest;
-            largest = nums[i];
-            largestIndex = i;
-        } else if (nums[i] > second) {
-            // 没有超过最大的,但是比第二大的更大就更新第二大的
-            second = nums[i];
-        }
-    }
+    redis过期策略复习
+    /2021/07/25/redis%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/
+    redis过期策略复习

之前其实写过redis的过期的一些原理,这次主要是记录下,一些使用上的概念,主要是redis使用的过期策略是懒过期和定时清除,懒过期的其实比较简单,即是在key被访问的时候会顺带着判断下这个key是否已过期了,如果已经过期了,就不返回了,但是这种策略有个漏洞是如果有些key之后一直不会被访问了,就等于沉在池底了,所以需要有一个定时的清理机制,去从设置了过期的key池子(expires)里随机地捞key,具体的策略我们看下官网的解释

+
    +
  1. Test 20 random keys from the set of keys with an associated expire.
  2. +
  3. Delete all the keys found expired.
  4. +
  5. If more than 25% of keys were expired, start again from step 1.
  6. +
+

从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件

+
# Redis calls an internal function to perform many background tasks, like
+# closing connections of clients in timeout, purging expired keys that are
+# never requested, and so forth.
+#
+# Not all tasks are performed with the same frequency, but Redis checks for
+# tasks to perform according to the specified "hz" value.
+#
+# By default "hz" is set to 10. Raising the value will use more CPU when
+# Redis is idle, but at the same time will make Redis more responsive when
+# there are many keys expiring at the same time, and timeouts may be
+# handled with more precision.
+#
+# The range is between 1 and 500, however a value over 100 is usually not
+# a good idea. Most users should use the default of 10 and raise this up to
+# 100 only in environments where very low latency is required.
+hz 10
- // 判断下是否符合题目要求,要是所有值的两倍及以上 - if (largest >= 2 * second) { - return largestIndex; - } else { - return -1; - } -}
-

通过图

第一次错了是把第二大的情况只考虑第一种,也有可能最大值完全没经过替换就变成最大值了

+

可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略

]]>
- Java - leetcode + redis - leetcode - java - 题解 - -
- - redis系列介绍八-淘汰策略 - /2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/ - LRU

说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
redis 针对这个策略的改进做了一个实验,这里借用下图

首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

-
typedef 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;
-

对于 lru 策略来说,lru 字段记录的就是redisObj 的LRU time,
redis 在访问数据时,都会调用lookupKey方法

-
/* Low level key lookup API, not actually called directly from commands
- * implementations that should instead rely on lookupKeyRead(),
- * lookupKeyWrite() and lookupKeyReadWithFlags(). */
-robj *lookupKey(redisDb *db, robj *key, int flags) {
-    dictEntry *de = dictFind(db->dict,key->ptr);
-    if (de) {
-        robj *val = dictGetVal(de);
-
-        /* Update the access time for the ageing algorithm.
-         * Don't do it if we have a saving child, as this will trigger
-         * a copy on write madness. */
-        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
-            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
-                // 这个是后面一节的内容
-                updateLFU(val);
-            } else {
-                //  对于这个分支,访问时就会去更新 lru 值
-                val->lru = LRU_CLOCK();
-            }
-        }
-        return val;
-    } else {
-        return NULL;
-    }
-}
-/* This function is used to obtain the current LRU clock.
- * If the current resolution is lower than the frequency we refresh the
- * LRU clock (as it should be in production servers) we return the
- * precomputed value, otherwise we need to resort to a system call. */
-unsigned int LRU_CLOCK(void) {
-    unsigned int lruclock;
-    if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
-        // 如果服务器的频率server.hz大于 1 时就是用系统预设的 lruclock
-        lruclock = server.lruclock;
-    } else {
-        lruclock = getLRUClock();
-    }
-    return lruclock;
-}
-/* Return the LRU clock, based on the clock resolution. This is a time
- * in a reduced-bits format that can be used to set and check the
- * object->lru field of redisObject structures. */
-unsigned int getLRUClock(void) {
-    return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
-}
-

redis 处理命令是在这里processCommand

-
/* If this function gets called we already read a whole
- * command, arguments are in the client argv/argc fields.
- * processCommand() execute the command or prepare the
- * server for a bulk read from the client.
- *
- * If C_OK is returned the client is still alive and valid and
- * other operations can be performed by the caller. Otherwise
- * if C_ERR is returned the client was destroyed (i.e. after QUIT). */
-int processCommand(client *c) {
-    moduleCallCommandFilters(c);
-
-    
-
-    /* Handle the maxmemory directive.
-     *
-     * Note that we do not want to reclaim memory if we are here re-entering
-     * the event loop since there is a busy Lua script running in timeout
-     * condition, to avoid mixing the propagation of scripts with the
-     * propagation of DELs due to eviction. */
-    if (server.maxmemory && !server.lua_timedout) {
-        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
-        /* freeMemoryIfNeeded may flush slave output buffers. This may result
-         * into a slave, that may be the active client, to be freed. */
-        if (server.current_client == NULL) return C_ERR;
-
-        /* It was impossible to free enough memory, and the command the client
-         * is trying to execute is denied during OOM conditions or the client
-         * is in MULTI/EXEC context? Error. */
-        if (out_of_memory &&
-            (c->cmd->flags & CMD_DENYOOM ||
-             (c->flags & CLIENT_MULTI &&
-              c->cmd->proc != execCommand &&
-              c->cmd->proc != discardCommand)))
-        {
-            flagTransaction(c);
-            addReply(c, shared.oomerr);
-            return C_OK;
-        }
-    }
-}
-

这里只摘了部分,当需要清理内存时就会调用, 然后调用了freeMemoryIfNeededAndSafe

-
/* This is a wrapper for freeMemoryIfNeeded() that only really calls the
- * function if right now there are the conditions to do so safely:
- *
- * - There must be no script in timeout condition.
- * - Nor we are loading data right now.
- *
- */
-int freeMemoryIfNeededAndSafe(void) {
-    if (server.lua_timedout || server.loading) return C_OK;
-    return freeMemoryIfNeeded();
-}
-/* This function is periodically called to see if there is memory to free
- * according to the current "maxmemory" settings. In case we are over the
- * memory limit, the function will try to free some memory to return back
- * under the limit.
- *
- * The function returns C_OK if we are under the memory limit or if we
- * were over the limit, but the attempt to free memory was successful.
- * Otehrwise if we are over the memory limit, but not enough memory
- * was freed to return back under the limit, the function returns C_ERR. */
-int freeMemoryIfNeeded(void) {
-    int keys_freed = 0;
-    /* By default replicas should ignore maxmemory
-     * and just be masters exact copies. */
-    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
-
-    size_t mem_reported, mem_tofree, mem_freed;
-    mstime_t latency, eviction_latency;
-    long long delta;
-    int slaves = listLength(server.slaves);
+        redis
+        应用
+        过期策略
+      
+  
+  
+    rust学习笔记-所有权三之切片
+    /2021/05/16/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%B8%89%E4%B9%8B%E5%88%87%E7%89%87/
+    除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice)。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。
例如代码

+
fn main() {
+    let mut s = String::from("hello world");
 
-    /* When clients are paused the dataset should be static not just from the
-     * POV of clients not being able to write, but also from the POV of
-     * expires and evictions of keys not being performed. */
-    if (clientsArePaused()) return C_OK;
-    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
-        return C_OK;
+    let word = first_word(&s);
 
-    mem_freed = 0;
+    s.clear();
 
-    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
-        goto cant_free; /* We need to free memory, but policy forbids. */
+    // 这时候虽然 word 还是 5,但是 s 已经被清除了,所以就没存在的意义
+}
+

这里其实我们就需要关注 s 的存在性,代码的逻辑合理性就需要额外去维护,此时我们就可以用切片

+
let s = String::from("hello world")
 
-    latencyStartMonitor(latency);
-    while (mem_freed < mem_tofree) {
-        int j, k, i;
-        static unsigned int next_db = 0;
-        sds bestkey = NULL;
-        int bestdbid;
-        redisDb *db;
-        dict *dict;
-        dictEntry *de;
+let hello = &s[0..5];
+let world = &s[6..11];
+

其实跟 Python 的list 之类的语法有点类似,当然里面还有些语法糖,比如可以直接用省略后面的数字表示直接引用到结尾

+
let hello = &s[0..];
+

甚至再进一步

+
let hello = &s[..];
+

使用了切片之后

+
fn first_word(s: &String) -> &str {
+    let bytes = s.as_bytes();
 
-        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
-            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
-        {
-            struct evictionPoolEntry *pool = EvictionPoolLRU;
+    for (i, &item) in bytes.iter().enumerate() {
+        if item == b' ' {
+            return &s[0..i];
+        }
+    }
 
-            while(bestkey == NULL) {
-                unsigned long total_keys = 0, keys;
+    &s[..]
+}
+fn main() {
+    let mut s = String::from("hello world");
 
-                /* We don't want to make local-db choices when expiring keys,
-                 * so to start populate the eviction pool sampling keys from
-                 * every DB. */
-                for (i = 0; i < server.dbnum; i++) {
-                    db = server.db+i;
-                    dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
-                            db->dict : db->expires;
-                    if ((keys = dictSize(dict)) != 0) {
-                        evictionPoolPopulate(i, dict, db->dict, pool);
-                        total_keys += keys;
-                    }
-                }
-                if (!total_keys) break; /* No keys to evict. */
+    let word = first_word(&s);
 
-                /* Go backward from best to worst element to evict. */
-                for (k = EVPOOL_SIZE-1; k >= 0; k--) {
-                    if (pool[k].key == NULL) continue;
-                    bestdbid = pool[k].dbid;
+    s.clear(); // error!
 
-                    if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
-                        de = dictFind(server.db[pool[k].dbid].dict,
-                            pool[k].key);
-                    } else {
-                        de = dictFind(server.db[pool[k].dbid].expires,
-                            pool[k].key);
-                    }
+    println!("the first word is: {}", word);
+}
+

那再执行 main 函数的时候就会抛错,因为 word 还是个切片,需要保证 s 的有效性,并且其实我们可以将函数申明成

+
fn first_word(s: &str) -> &str {
+

这样就既能处理&String 的情况,就是当成完整字符串的切片,也能处理普通的切片。
其他类型的切片

+
let a = [1, 2, 3, 4, 5];
+let slice = &a[1..3];
+

简单记录下,具体可以去看看这本书

+]]>
+ + 语言 + Rust + + + Rust + 所有权 + 内存分布 + 新语言 + 可变引用 + 不可变引用 + 切片 + +
+ + rust学习笔记-所有权一 + /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 最近在看 《rust 权威指南》,还是难度比较大的,它里面的一些概念跟之前的用过的都有比较大的差别
比起有 gc 的虚拟机语言,跟像 C 和 C++这种主动释放内存的,rust 有他的独特点,主要是有三条

+
    +
  • Rust中的每一个值都有一个对应的变量作为它的所有者。
  • +
  • 在同一时间内,值有且只有一个所有者。
  • +
  • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

    这里有两个重点:
  • +
  • s 在进入作用域后才变得有效
  • +
  • 它会保持自己的有效性直到自己离开作用域为止
  • +
+

然后看个案例

+
let x = 5;
+let y = x;
+

这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
但是对于非基础类型

+
let s1 = String::from("hello");
+let s2 = s1;
 
-                    /* Remove the entry from the pool. */
-                    if (pool[k].key != pool[k].cached)
-                        sdsfree(pool[k].key);
-                    pool[k].key = NULL;
-                    pool[k].idle = 0;
+println!("{}, world!", s1);
+

有可能认为有两种内存分布可能
先看下 string 的内存结构

第一种可能是

第二种是

我们来尝试编译下

发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了

这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法

+
let s1 = String::from("hello");
+let s2 = s1.clone();
 
-                    /* If the key exists, is our pick. Otherwise it is
-                     * a ghost and we need to try the next element. */
-                    if (de) {
-                        bestkey = dictGetKey(de);
-                        break;
-                    } else {
-                        /* Ghost... Iterate again. */
-                    }
-                }
-            }
-        }
+println!("s1 = {}, s2 = {}", s1, s2);
+

这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配

+]]>
+ + 语言 + Rust + + + Rust + 所有权 + 内存分布 + 新语言 + +
+ + spring event 介绍 + /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ + spring框架中如果想使用一些一部操作,除了依赖第三方中间件的消息队列,还可以用spring自己的event,简单介绍下使用方法
首先我们可以建一个event,继承ApplicationEvent

+

+public class CustomSpringEvent extends ApplicationEvent {
 
-        /* volatile-random and allkeys-random policy */
-        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
-                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
-        {
-            /* When evicting a random key, we try to evict a key for
-             * each DB, so we use the static 'next_db' variable to
-             * incrementally visit all DBs. */
-            for (i = 0; i < server.dbnum; i++) {
-                j = (++next_db) % server.dbnum;
-                db = server.db+j;
-                dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
-                        db->dict : db->expires;
-                if (dictSize(dict) != 0) {
-                    de = dictGetRandomKey(dict);
-                    bestkey = dictGetKey(de);
-                    bestdbid = j;
-                    break;
-                }
-            }
-        }
+    private String message;
 
-        /* Finally remove the selected key. */
-        if (bestkey) {
-            db = server.db+bestdbid;
-            robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
-            propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
-            /* We compute the amount of memory freed by db*Delete() alone.
-             * It is possible that actually the memory needed to propagate
-             * the DEL in AOF and replication link is greater than the one
-             * we are freeing removing the key, but we can't account for
-             * that otherwise we would never exit the loop.
-             *
-             * AOF and Output buffer memory will be freed eventually so
-             * we only care about memory used by the key space. */
-            delta = (long long) zmalloc_used_memory();
-            latencyStartMonitor(eviction_latency);
-            if (server.lazyfree_lazy_eviction)
-                dbAsyncDelete(db,keyobj);
-            else
-                dbSyncDelete(db,keyobj);
-            latencyEndMonitor(eviction_latency);
-            latencyAddSampleIfNeeded("eviction-del",eviction_latency);
-            latencyRemoveNestedEvent(latency,eviction_latency);
-            delta -= (long long) zmalloc_used_memory();
-            mem_freed += delta;
-            server.stat_evictedkeys++;
-            notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
-                keyobj, db->id);
-            decrRefCount(keyobj);
-            keys_freed++;
+    public CustomSpringEvent(Object source, String message) {
+        super(source);
+        this.message = message;
+    }
+    public String getMessage() {
+        return message;
+    }
+}
+

这个 ApplicationEvent 其实也比较简单,内部就一个 Object 类型的 source,可以自行扩展,我们在自定义的这个 Event 里加了个 Message ,只是简单介绍下使用

+
public abstract class ApplicationEvent extends EventObject {
+    private static final long serialVersionUID = 7099057708183571937L;
+    private final long timestamp;
 
-            /* When the memory to free starts to be big enough, we may
-             * start spending so much time here that is impossible to
-             * deliver data to the slaves fast enough, so we force the
-             * transmission here inside the loop. */
-            if (slaves) flushSlavesOutputBuffers();
+    public ApplicationEvent(Object source) {
+        super(source);
+        this.timestamp = System.currentTimeMillis();
+    }
 
-            /* Normally our stop condition is the ability to release
-             * a fixed, pre-computed amount of memory. However when we
-             * are deleting objects in another thread, it's better to
-             * check, from time to time, if we already reached our target
-             * memory, since the "mem_freed" amount is computed only
-             * across the dbAsyncDelete() call, while the thread can
-             * release the memory all the time. */
-            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
-                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
-                    /* Let's satisfy our stop condition. */
-                    mem_freed = mem_tofree;
-                }
-            }
-        } else {
-            latencyEndMonitor(latency);
-            latencyAddSampleIfNeeded("eviction-cycle",latency);
-            goto cant_free; /* nothing to free... */
-        }
-    }
-    latencyEndMonitor(latency);
-    latencyAddSampleIfNeeded("eviction-cycle",latency);
-    return C_OK;
+    public ApplicationEvent(Object source, Clock clock) {
+        super(source);
+        this.timestamp = clock.millis();
+    }
 
-cant_free:
-    /* We are here if we are not able to reclaim memory. There is only one
-     * last thing we can try: check if the lazyfree thread has jobs in queue
-     * and wait... */
-    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
-        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
-            break;
-        usleep(1000);
-    }
-    return C_ERR;
-}
-

这里就是根据具体策略去淘汰 key,首先是要往 pool 更新 key,更新key 的方法是evictionPoolPopulate

-
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
-    int j, k, count;
-    dictEntry *samples[server.maxmemory_samples];
+    public final long getTimestamp() {
+        return this.timestamp;
+    }
+}
- count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples); - for (j = 0; j < count; j++) { - unsigned long long idle; - sds key; - robj *o; - dictEntry *de; +

然后就是事件生产者和监听消费者

+
@Component
+public class CustomSpringEventPublisher {
 
-        de = samples[j];
-        key = dictGetKey(de);
+    @Resource
+    private ApplicationEventPublisher applicationEventPublisher;
 
-        /* If the dictionary we are sampling from is not the main
-         * dictionary (but the expires one) we need to lookup the key
-         * again in the key dictionary to obtain the value object. */
-        if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
-            if (sampledict != keydict) de = dictFind(keydict, key);
-            o = dictGetVal(de);
-        }
+    public void publishCustomEvent(final String message) {
+        System.out.println("Publishing custom event. ");
+        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
+        applicationEventPublisher.publishEvent(customSpringEvent);
+    }
+}
+

这里的 ApplicationEventPublisher 是 Spring 的方法接口

+
@FunctionalInterface
+public interface ApplicationEventPublisher {
+    default void publishEvent(ApplicationEvent event) {
+        this.publishEvent((Object)event);
+    }
 
-        /* Calculate the idle time according to the policy. This is called
-         * idle just because the code initially handled LRU, but is in fact
-         * just a score where an higher score means better candidate. */
-        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
-            idle = estimateObjectIdleTime(o);
-        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
-            /* When we use an LRU policy, we sort the keys by idle time
-             * so that we expire keys starting from greater idle time.
-             * However when the policy is an LFU one, we have a frequency
-             * estimation, and we want to evict keys with lower frequency
-             * first. So inside the pool we put objects using the inverted
-             * frequency subtracting the actual frequency to the maximum
-             * frequency of 255. */
-            idle = 255-LFUDecrAndReturn(o);
-        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
-            /* In this case the sooner the expire the better. */
-            idle = ULLONG_MAX - (long)dictGetVal(de);
-        } else {
-            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
-        }
+    void publishEvent(Object var1);
+}
+

具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲

+

事件监听者:

+
@Component
+public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
+    @Override
+    public void onApplicationEvent(CustomSpringEvent event) {
+        System.out.println("Received spring custom event - " + event.getMessage());
+    }
+}
+

这里的也是 spring 的一个方法接口

+
@FunctionalInterface
+public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
+    void onApplicationEvent(E var1);
 
-        /* Insert the element inside the pool.
-         * First, find the first empty bucket or the first populated
-         * bucket that has an idle time smaller than our idle time. */
-        k = 0;
-        while (k < EVPOOL_SIZE &&
-               pool[k].key &&
-               pool[k].idle < idle) k++;
-        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
-            /* Can't insert if the element is < the worst element we have
-             * and there are no empty buckets. */
-            continue;
-        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
-            /* Inserting into empty position. No setup needed before insert. */
-        } else {
-            /* Inserting in the middle. Now k points to the first element
-             * greater than the element to insert.  */
-            if (pool[EVPOOL_SIZE-1].key == NULL) {
-                /* Free space on the right? Insert at k shifting
-                 * all the elements from k to end to the right. */
+    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
+        return (event) -> {
+            consumer.accept(event.getPayload());
+        };
+    }
+}
- /* Save SDS before overwriting. */ - sds cached = pool[EVPOOL_SIZE-1].cached; - memmove(pool+k+1,pool+k, - sizeof(pool[0])*(EVPOOL_SIZE-k-1)); - pool[k].cached = cached; - } else { - /* No free space on right? Insert at k-1 */ - k--; - /* Shift all elements on the left of k (included) to the - * left, so we discard the element with smaller idle time. */ - sds cached = pool[0].cached; /* Save SDS before overwriting. */ - if (pool[0].key != pool[0].cached) sdsfree(pool[0].key); - memmove(pool,pool+1,sizeof(pool[0])*k); - pool[k].cached = cached; - } +

然后简单包个请求

+

+@RequestMapping(value = "/event", method = RequestMethod.GET)
+@ResponseBody
+public void event() {
+    customSpringEventPublisher.publishCustomEvent("hello sprint event");
+}
+ +


就能看到接收到消息了。

+]]>
+ + Java + Spring + + + Java + Spring + Spring Event + +
+ + summary-ranges-228 + /2016/10/12/summary-ranges-228/ + problem

Given a sorted integer array without duplicates, return the summary of its ranges.

+

For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].

+

题解

每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考

+

Code

class Solution {
+public:
+    vector<string> summaryRanges(vector<int>& nums) {
+        int i = 0, j = 1, n;
+        vector<string> res;
+        n = nums.size();
+        while(i < n){
+            j = 1;
+            while(j < n && nums[i+j] - nums[i] == j) j++;
+            res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
+            i += j;
         }
+        return res;
+    }
+};
]]>
+ + leetcode + + + leetcode + c++ + +
+ + wordpress 忘记密码的一种解决方法 + /2021/12/05/wordpress-%E5%BF%98%E8%AE%B0%E5%AF%86%E7%A0%81%E7%9A%84%E4%B8%80%E7%A7%8D%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/ + 前阵子搭了个 WordPress,但是没怎么用,前两天发现忘了登录密码了,最近不知道是什么情况,chrome 的记住密码跟历史记录感觉有点问题,历史记录丢了不少东西,可能是时间太久了,但是理论上应该有 LRU 这种策略的,有些还比较常用,还有记住密码,因为个人域名都是用子域名分配给各个服务,有些记住了,有些又没记住密码,略蛋疼,所以就找了下这个方案。
当然这个方案不是最优的,有很多限制,首先就是要能够登陆 WordPress 的数据库,不然这个方法是没用的。
首先不管用什么方式(别违法)先登陆数据库,选择 WordPress 的数据库,可以看到里面有几个表,我们的目标就是 wp_users 表,用 select 查询看下可以看到有用户的数据,如果是像我这样搭着玩的没有创建其他用户的话应该就只有一个用户,那我们的表里的用户数据就只会有一条,当然多条的话可以通过用户名来找

然后可能我这个版本是这样,没有装额外的插件,密码只是经过了 MD5 的单向哈希,所以我们可以设定一个新密码,然后用 MD5 编码后直接更新进去

+
UPDATE wp_users SET user_pass = MD5('123456') WHERE ID = 1;
+ +

然后就能用自己的账户跟刚才更新的密码登录了。

+]]>
+ + 小技巧 + + + WordPress + 小技巧 + +
+ + spark-little-tips + /2017/03/28/spark-little-tips/ + spark 的一些粗浅使用经验

工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现

+

简单的mapreduce word count示例

textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
+data = textFile.reduceByKey(lambda x, y: x + y).collect()
+for _ in data:
+    print(_)
+ + +

结果

(3, 2)
+(1, 2)
+(4, 2)
+(2, 2)
+(5, 2)
+]]>
+ + data analysis + + + spark + python + +
+ + 《垃圾回收算法手册读书》笔记之整理算法 + /2021/03/07/%E3%80%8A%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%89%8B%E5%86%8C%E8%AF%BB%E4%B9%A6%E3%80%8B%E7%AC%94%E8%AE%B0%E4%B9%8B%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95/ + 最近看了下这本垃圾回收算法手册,看到了第三章的标记-整理回收算法,做个简单的读书笔记

+

双指针整理算法

对于一块待整理区域,通过两个指针,free 在区域的起始端,scan 指针在区域的末端,free 指针从前往后知道找到空闲区域,scan 从后往前一直找到存活对象,当 free 指针未与 scan 指针交叉时,会给 scan 位置的对象特定位置标记上 free 的地址,即将要转移的地址,不过这里有个限制,这种整理算法一般会用于对象大小统一的情况,否则 free 指针扫描时还需要匹配scan 指针扫描到的存活对象的大小。

+

Lisp 2 整理算法

需要三次完整遍历堆区域
第一遍是遍历后将计算出所有对象的最终地址(转发地址)
第二遍是使用转发地址更新赋值器线程根以及被标记对象中的引用,该操作将确保它们指向对象的新位置
第三次遍历是relocate最终将存活对象移动到其新的目标位置

+

引线整理算法

这个真的长见识了,

可以看到,原来是 A,B,C 对象引用了 N,这里会在第一次遍历的时候把这种引用反过来,让 N 的对象头部保存下 A 的地址,表示这类引用,然后在遍历到 B 的时候在链起来,到最后就会把所有引用了 N 对象的所有对象通过引线链起来,在第二次遍历的时候就把更新A,B,C 对象引用的 N 地址,并且移动 N 对象

+

单次遍历算法

这个一直提到过位图的实现方式,

可以看到在第一步会先通过位图标记,标记的方式是位图的每一位对应的堆内存的一个字(这里可能指的是 byte 吧),然后将一个存活对象的内存区域的第一个字跟最后一个字标记,这里如果在通过普通的方式就还需要一个地方在存转发地址,但是因为具体的位置可以通过位图算出来,也就不需要额外记录了

+]]>
+ + Java + gc + jvm + + + java + gc + 标记整理 + 垃圾回收 + jvm + +
+ + 《长安的荔枝》读后感 + /2022/07/17/%E3%80%8A%E9%95%BF%E5%AE%89%E7%9A%84%E8%8D%94%E6%9E%9D%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F/ + 断断续续地看完了马伯庸老师的《长安的荔枝》,一开始是看这本书在排行榜排得很高,又是马伯庸的,之前看过他的《古董局中局》,还是很有意思的,而且正好是比较短的,不过前后也拖了蛮久才看完,看完后读了下马老师自己写的后记,就特别有感触。
整个故事是围绕一个上林署监事李善德被委任一项给贵妃送荔枝的差事展开,“长安回望绣成堆,山顶千门次第开,一骑红尘妃子笑,无人知是荔枝来”,以前没细究过这个送荔枝的过程,但是以以前的运输速度和保鲜条件,感觉也不是太现实,所以主人公一开始就以为只是像以往一样是送荔枝干这种,能比较方便运输,不容易变质的,结果发现其实是同僚在坑他,这次是要在贵妃生辰的时候给贵妃送来新鲜的岭南荔枝,用比较时兴的词来说,这就是个送命题啊,鲜荔枝一日色变,两日香变,三日味变,同僚的还有杜甫跟韩承,都觉得老李可以直接写休书了,保全家人,不然就是全家送命,李善德也觉得基本算是判刑了,而且其实是这事被转了几次,最后到老李所在的上林署,主管为了骗他接下这个活还特意在文书上把荔枝鲜的“鲜”字贴住,那会叫做“贴黄”,变成了荔枝“煎”,所以说官场险恶,大家都想把这烫手山芋丢出去,结果丢到了我们老实的老李头上,但是从接到这个通知到贵妃的生辰六月初一还有挺长的时间,其实这个活虽然送命,但是在前期这个“荔枝使”也基本就是类似带着尚方宝剑,御赐黄马褂的职位,随便申请经费,不必像常规的部门费用需要定预算,申请后再层层审批,而是特事特批特办的耍赖做法,所以在这段时间是能够潇洒挥霍一下的。其实可以好好地捞一波给妻女,然后写下和离,在自己死后能让她们过的好一些,但最后还是在杜甫的一番劝导下做出了尝试一番的决定,因为也没其他办法,既是退无可退,何不向前拼死一搏,其实说到这,我觉得看这本书感觉有所收获的第一点,有时候总觉得事情没戏了,想躺平放弃了,但是这样其实这个结果是不会变好的,尝试努力,拼尽全力搏一搏,说不定会有所改观,至少不会变更坏了。

+]]>
+ + 读后感 + 生活 + + + 生活 + 读后感 + +
+ + 一个 nginx 的简单记忆点 + /2022/08/21/%E4%B8%80%E4%B8%AA-nginx-%E7%9A%84%E7%AE%80%E5%8D%95%E8%AE%B0%E5%BF%86%E7%82%B9/ + 上周在处理一个 nginx 配置的时候,发现了一个之前不理解的小点,说一个场景,就是我们一般的处理方式就是一个 ip 端口只能配置一个域名的服务,比如 https://nicksxs.me 对应配置到 127.0.0.1:443,如果我想要把 https://nicksxs.com 也解析到这个服务器,并转发到不同的下游,这里就需要借助所谓的 SNI 的功能

+

Server Name Indication

A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. SNI is currently supported by most modern browsers, though may not be used by some old or special clients.
来源
机翻一下:在单个 IP 地址上运行多个 HTTPS 服务器的更通用的解决方案是 TLS 服务器名称指示扩展(SNI,RFC 6066),它允许浏览器在 SSL 握手期间传递请求的服务器名称,因此,服务器将知道哪个 它应该用于连接的证书。 目前大多数现代浏览器都支持 SNI,但某些旧的或特殊的客户端可能不使用 SNI。

+

首先我们需要确认 sni 已被支持

在实际的配置中就可以这样

+
stream {
+  map $ssl_preread_server_name $stream_map {
+    nicksxs.me nme;
+    nicksxs.com ncom;
+  }
 
-        /* Try to reuse the cached SDS string allocated in the pool entry,
-         * because allocating and deallocating this object is costly
-         * (according to the profiler, not my fantasy. Remember:
-         * premature optimizbla bla bla bla. */
-        int klen = sdslen(key);
-        if (klen > EVPOOL_CACHED_SDS_SIZE) {
-            pool[k].key = sdsdup(key);
-        } else {
-            memcpy(pool[k].cached,key,klen+1);
-            sdssetlen(pool[k].cached,klen);
-            pool[k].key = pool[k].cached;
-        }
-        pool[k].idle = idle;
-        pool[k].dbid = dbid;
-    }
-}
-

Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些键的空闲时间还大)就可以进poolpool更新之后,就淘汰pool中空闲时间最大的键。

-

estimateObjectIdleTime用来计算Redis对象的空闲时间:

-
/* Given an object returns the min number of milliseconds the object was never
- * requested, using an approximated LRU algorithm. */
-unsigned long long estimateObjectIdleTime(robj *o) {
-    unsigned long long lruclock = LRU_CLOCK();
-    if (lruclock >= o->lru) {
-        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
-    } else {
-        return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
-                    LRU_CLOCK_RESOLUTION;
-    }
-}
-

空闲时间第一种是 lurclock 大于对象的 lru,那么就是减一下乘以精度,因为 lruclock 有可能是已经预生成的,所以会可能走下面这个

-

LFU

上面介绍了LRU 的算法,但是考虑一种场景

-
~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
-~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
-~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
-~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
-

可以发现,当采用 lru 的淘汰策略的时候,D 是最新的,会被认为是最值得保留的,但是事实上还不如 A 跟 B,然后 antirez 大神就想到了LFU (Least Frequently Used) 这个算法, 显然对于上面的四个 key 的访问频率,保留优先级应该是 B > A > C = D
那要怎么来实现这个 LFU 算法呢,其实像LRU,理想的情况就是维护个链表,把最新访问的放到头上去,但是这个会影响访问速度,注意到前面代码的应该可以看到,redisObject 的 lru 字段其实是两用的,当策略是 LFU 时,这个字段就另作他用了,它的 24 位长度被分成两部分

-
      16 bits      8 bits
-+----------------+--------+
-+ Last decr time | LOG_C  |
-+----------------+--------+
-

前16位字段是最后一次递减时间,因此Redis知道 上一次计数器递减,后8位是 计数器 counter。
LFU 的主体策略就是当这个 key 被访问的次数越多频率越高他就越容易被保留下来,并且是最近被访问的频率越高。这其实有两个事情要做,一个是在访问的时候增加计数值,在一定长时间不访问时进行衰减,所以这里用了两个值,前 16 位记录上一次衰减的时间,后 8 位记录具体的计数值。
Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:

-

volatile-lfu:对有过期时间的key采用LFU淘汰策略
allkeys-lfu:对全部key采用LFU淘汰策略
还有2个配置可以调整LFU算法:

-
lfu-log-factor 10
-lfu-decay-time 1
-```  
-`lfu-log-factor` 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
+  upstream nme {
+    server 127.0.0.1:8000;
+  }
+  upstream ncom {
+    server 127.0.0.1:8001;
+  }
 
-`lfu-decay-time`是一个以分钟为单位的数值,可以调整counter的减少速度
-这里有个问题是 8 位大小够计么,访问一次加 1 的话的确不够,不过大神就是大神,才不会这么简单的加一。往下看代码
-```C
-/* Low level key lookup API, not actually called directly from commands
+  server {
+    listen 443 reuseport;
+    proxy_pass $stream_map;
+    ssl_preread on;
+  }
+}
+

类似这样,但是这个理解是非常肤浅和不完善的,只是简单记忆下,后续再进行补充完整

+

还有一点就是我们在配置的时候经常配置就是 server_name,但是会看到直接在使用 ssl_server_name,
其实在listen 标识了 ssl, 对应的 ssl_server_name 就等于 server_name,不需要额外处理了。

+]]>
+ + nginx + + + nginx + +
+ + 上次的其他 外行聊国足 + /2022/03/06/%E4%B8%8A%E6%AC%A1%E7%9A%84%E5%85%B6%E4%BB%96-%E5%A4%96%E8%A1%8C%E8%81%8A%E5%9B%BD%E8%B6%B3/ + 上次本来想在换车牌后面聊下这个话题,为啥要聊这个话题呢,也很简单,在地铁上看到一对猜测是情侣或者比较关系好的男女同学在聊,因为是这位男同学是大学学的工科,然后自己爱好设计绘画相关的,可能还以此赚了点钱,在地铁上讨论男的要不要好好努力把大学课程完成好,大致的观点是没必要,本来就不适合,这一段我就不说了,恋爱人的嘴,信你个鬼。
后面男的说在家里又跟他爹吵了关于男足的,估计是那次输了越南,实话说我不是个足球迷,对各方面技术相关也不熟,只是对包括这个人的解释和网上一些观点的看法,纯主观,这次地铁上这位说的大概意思是足球这个训练什么的很难的,要想赢越南也很难的,不是我们能嘴炮的;在网上看到一个赞同数很多的一个回答,说什么中国是个体育弱国,但是由于有一些乒乓球,跳水等小众项目比较厉害,让民众给误解了,首先我先来反驳下这个偷换概念的观点,第一所谓的体育弱国,跟我们觉得足球不应该这么差没半毛钱关系,因为体育弱国,我们的足球本来就不是顶尖的,也并不是去跟顶尖的球队去争,以足球为例,跟巴西,阿根廷,英国,德国,西班牙,意大利,法国这些足球强国,去比较,我相信没有一个足球迷会这么去做对比,因为我们足球历史最高排名是 1998 年的 37 名,最差是 100 名,把能数出来的强队都数完,估计都还不会到 37,所以根本没有跟强队去做对比,第二体育弱国,我们的体育投入是在逐年降低吗,我们是因战乱没法好好训练踢球?还是这帮傻逼就不争气,前面也说了我们足球世界排名最高 37,最低 100,那么前阵子我们输的越南是第几,目前我们的排名 77 名,越南 92 名,看明白了么,轮排名我们都不至于输越南,然后就是这个排名,这也是我想回应那位地铁上的兄弟,我觉得除了造核弹这种高精尖技术,绝大部分包含足球这类运动,遵循类二八原则,比如满分是 100 分,从 80 提到 90 分或者 90 分提到 100 分非常难,30 分提到 40 分,50 分提到 60 分我觉得都是可以凭后天努力达成的,基本不受天赋限制,这里可以以篮球来类比下,相对足球的确篮球没有那么火,或者行业市值没法比,但是也算是相对大众了,中国在篮球方面相对比较好一点,在 08 年奥运会冲进过八强,那也不是唯一的巅峰,但是我说这个其实是想说明两方面的事情,第一,像篮球一样,状态是有起起伏伏,排名也会变动,但是我觉得至少能维持一个相对稳定的总体排名和持平或者上升的趋势,这恰恰是我们这种所谓的“体育弱国”应该走的路线,第二就是去支持我的类二八原则的,可以看到我们的篮球这两年也很垃圾,排名跌到 29 了,那问题我觉得跟足球是一样的,就是不能脚踏实地,如斯科拉说的,中国篮球太缺少竞争,打得好不好都是这些人打,打输了还是照样拿钱,相对足球,篮球的技术我还是懂一些的,对比 08 年的中国男篮,的确像姚明跟王治郅这样的天赋型+努力型球员少了以后竞争力下降在所难免,但是去对比下基本功,传球,投篮,罚球稳定性,也完全不是一个水平的,这些就是我说的,可以通过努力训练拿 80 分的,只要拿到 80 分,甚至只要拿到 60 分,我觉得应该就还算对得起球迷了,就像 NBA 里球队也会有核心球员的更替,战绩起起伏伏,但是基本功这东西,防守积极性,我觉得不随核心球员的变化而变化,就像姚明这样的天赋,其实他应该还有一些先天缺陷,大脚趾较长等,但是他从 CBA 到 NBA,在 NBA 适应并且打成顶尖中锋,离不开刻苦训练,任何的成功都不是纯天赋的,必须要付出足够的努力。
说回足球,如果像前面那么洗地(体育弱国),那能给我维持住一个稳定的排名我也能接受,问题是我们的经济物质资源比 2000 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
纯主观吐槽,勿喷。

+]]>
+ + 生活 + 运动 + + + 生活 + +
+ + redis系列介绍八-淘汰策略 + /2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/ + LRU

说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
redis 针对这个策略的改进做了一个实验,这里借用下图

首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

+
typedef 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;
+

对于 lru 策略来说,lru 字段记录的就是redisObj 的LRU time,
redis 在访问数据时,都会调用lookupKey方法

+
/* Low level key lookup API, not actually called directly from commands
  * implementations that should instead rely on lookupKeyRead(),
  * lookupKeyWrite() and lookupKeyReadWithFlags(). */
 robj *lookupKey(redisDb *db, robj *key, int flags) {
@@ -8023,9 +7985,10 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
          * a copy on write madness. */
         if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
             if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
-                // 当淘汰策略是 LFU 时,就会调用这个updateLFU
+                // 这个是后面一节的内容
                 updateLFU(val);
             } else {
+                //  对于这个分支,访问时就会去更新 lru 值
                 val->lru = LRU_CLOCK();
             }
         }
@@ -8033,305 +7996,495 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
     } else {
         return NULL;
     }
-}
-

updateLFU 这个其实个入口,调用了两个重要的方法

-
/* Update LFU when an object is accessed.
- * Firstly, decrement the counter if the decrement time is reached.
- * Then logarithmically increment the counter, and update the access time. */
-void updateLFU(robj *val) {
-    unsigned long counter = LFUDecrAndReturn(val);
-    counter = LFULogIncr(counter);
-    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
-}
-

首先来看看LFUDecrAndReturn,这个方法的作用是根据上一次衰减时间和系统配置的 lfu-decay-time 参数来确定需要将 counter 减去多少

-
/* If the object decrement time is reached decrement the LFU counter but
- * do not update LFU fields of the object, we update the access time
- * and counter in an explicit way when the object is really accessed.
- * And we will times halve the counter according to the times of
- * elapsed time than server.lfu_decay_time.
- * Return the object frequency counter.
+}
+/* This function is used to obtain the current LRU clock.
+ * If the current resolution is lower than the frequency we refresh the
+ * LRU clock (as it should be in production servers) we return the
+ * precomputed value, otherwise we need to resort to a system call. */
+unsigned int LRU_CLOCK(void) {
+    unsigned int lruclock;
+    if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
+        // 如果服务器的频率server.hz大于 1 时就是用系统预设的 lruclock
+        lruclock = server.lruclock;
+    } else {
+        lruclock = getLRUClock();
+    }
+    return lruclock;
+}
+/* Return the LRU clock, based on the clock resolution. This is a time
+ * in a reduced-bits format that can be used to set and check the
+ * object->lru field of redisObject structures. */
+unsigned int getLRUClock(void) {
+    return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
+}
+

redis 处理命令是在这里processCommand

+
/* If this function gets called we already read a whole
+ * command, arguments are in the client argv/argc fields.
+ * processCommand() execute the command or prepare the
+ * server for a bulk read from the client.
  *
- * This function is used in order to scan the dataset for the best object
- * to fit: as we check for the candidate, we incrementally decrement the
- * counter of the scanned objects if needed. */
-unsigned long LFUDecrAndReturn(robj *o) {
-    // 右移 8 位,拿到上次衰减时间
-    unsigned long ldt = o->lru >> 8;
-    // 对 255 做与操作,拿到 counter 值
-    unsigned long counter = o->lru & 255;
-    // 根据lfu_decay_time来算出过了多少个衰减周期
-    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
-    if (num_periods)
-        counter = (num_periods > counter) ? 0 : counter - num_periods;
-    return counter;
-}
-

然后是加,调用了LFULogIncr

-
/* Logarithmically increment a counter. The greater is the current counter value
- * the less likely is that it gets really implemented. Saturate it at 255. */
-uint8_t LFULogIncr(uint8_t counter) {
-    // 最大值就是 255,到顶了就不加了
-    if (counter == 255) return 255;
-    // 生成个随机小数
-    double r = (double)rand()/RAND_MAX;
-    // 减去个基础值,LFU_INIT_VAL = 5,防止刚进来就被逐出
-    double baseval = counter - LFU_INIT_VAL;
-    // 如果是小于 0,
-    if (baseval < 0) baseval = 0;
-    // 如果 baseval 是 0,那么 p 就是 1了,后面 counter 直接加一,如果不是的话,得看系统参数lfu_log_factor,这个越大,除出来的 p 越小,那么 counter++的可能性也越小,这样子就把前面的疑问给解决了,不是直接+1 的
-    double p = 1.0/(baseval*server.lfu_log_factor+1);
-    if (r < p) counter++;
-    return counter;
-}
-

大概的变化速度可以参考

-
+--------+------------+------------+------------+------------+------------+
-| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
-+--------+------------+------------+------------+------------+------------+
-| 0      | 104        | 255        | 255        | 255        | 255        |
-+--------+------------+------------+------------+------------+------------+
-| 1      | 18         | 49         | 255        | 255        | 255        |
-+--------+------------+------------+------------+------------+------------+
-| 10     | 10         | 18         | 142        | 255        | 255        |
-+--------+------------+------------+------------+------------+------------+
-| 100    | 8          | 11         | 49         | 143        | 255        |
-+--------+------------+------------+------------+------------+------------+
-

简而言之就是 lfu_log_factor 越大变化的越慢

-

总结

总结一下,redis 实现了近似的 lru 淘汰策略,通过增加了淘汰 key 的池子(pool),并且增大每次抽样的 key 的数量来将淘汰效果更进一步地接近于 lru,这是 lru 策略,但是对于前面举的一个例子,其实 lru 并不能保证 key 的淘汰就如我们预期,所以在后期又引入了 lfu 的策略,lfu的策略比较巧妙,复用了 redis 对象的 lru 字段,并且使用了factor 参数来控制计数器递增的速度,防止 8 位的计数器太早溢出。

-]]>
- - Redis - 数据结构 - C - 源码 - Redis - - - redis - 数据结构 - 源码 - -
- - rust学习笔记-所有权二 - /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ - 这里需要说道函数和返回值了
可以看书上的这个例子

对于这种情况,当进入函数内部时,会把传入的变量的所有权转移进函数内部,如果最后还是要返回该变量,但是如果此时还要返回别的计算结果,就可能需要笨拙地使用元组

-

引用

此时我们就可以用引用来解决这个问题

-
fn main() {
-    let s1 = String::from("hello");
-    let len = calculate_length(&s1);
+ * If C_OK is returned the client is still alive and valid and
+ * other operations can be performed by the caller. Otherwise
+ * if C_ERR is returned the client was destroyed (i.e. after QUIT). */
+int processCommand(client *c) {
+    moduleCallCommandFilters(c);
+
+    
+
+    /* Handle the maxmemory directive.
+     *
+     * Note that we do not want to reclaim memory if we are here re-entering
+     * the event loop since there is a busy Lua script running in timeout
+     * condition, to avoid mixing the propagation of scripts with the
+     * propagation of DELs due to eviction. */
+    if (server.maxmemory && !server.lua_timedout) {
+        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
+        /* freeMemoryIfNeeded may flush slave output buffers. This may result
+         * into a slave, that may be the active client, to be freed. */
+        if (server.current_client == NULL) return C_ERR;
+
+        /* It was impossible to free enough memory, and the command the client
+         * is trying to execute is denied during OOM conditions or the client
+         * is in MULTI/EXEC context? Error. */
+        if (out_of_memory &&
+            (c->cmd->flags & CMD_DENYOOM ||
+             (c->flags & CLIENT_MULTI &&
+              c->cmd->proc != execCommand &&
+              c->cmd->proc != discardCommand)))
+        {
+            flagTransaction(c);
+            addReply(c, shared.oomerr);
+            return C_OK;
+        }
+    }
+}
+

这里只摘了部分,当需要清理内存时就会调用, 然后调用了freeMemoryIfNeededAndSafe

+
/* This is a wrapper for freeMemoryIfNeeded() that only really calls the
+ * function if right now there are the conditions to do so safely:
+ *
+ * - There must be no script in timeout condition.
+ * - Nor we are loading data right now.
+ *
+ */
+int freeMemoryIfNeededAndSafe(void) {
+    if (server.lua_timedout || server.loading) return C_OK;
+    return freeMemoryIfNeeded();
+}
+/* This function is periodically called to see if there is memory to free
+ * according to the current "maxmemory" settings. In case we are over the
+ * memory limit, the function will try to free some memory to return back
+ * under the limit.
+ *
+ * The function returns C_OK if we are under the memory limit or if we
+ * were over the limit, but the attempt to free memory was successful.
+ * Otehrwise if we are over the memory limit, but not enough memory
+ * was freed to return back under the limit, the function returns C_ERR. */
+int freeMemoryIfNeeded(void) {
+    int keys_freed = 0;
+    /* By default replicas should ignore maxmemory
+     * and just be masters exact copies. */
+    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
 
-    println!("The length of '{}' is {}", s1, len);
-}
-fn calculate_length(s: &String) -> usize {
-    s.len()
-}
-

这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值

由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃

-

可变引用

而当我们尝试对引用的字符串进行修改时

-
fn main() {
-    let s1 = String::from("hello");
-    change(&s1);
-}
-fn change(s: &String) {
-    s.push_str(", world");
-}
-

就会有以下报错,

其实也很容易发现,毕竟没有 mut 指出这是可变引用,同时需要将 s1 改成 mut 可变的

-
fn main() {
-    let mut s1 = String::from("hello");
-    change(&mut s1);
-}
+    size_t mem_reported, mem_tofree, mem_freed;
+    mstime_t latency, eviction_latency;
+    long long delta;
+    int slaves = listLength(server.slaves);
 
+    /* When clients are paused the dataset should be static not just from the
+     * POV of clients not being able to write, but also from the POV of
+     * expires and evictions of keys not being performed. */
+    if (clientsArePaused()) return C_OK;
+    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
+        return C_OK;
 
-fn change(s: &mut String) {
-    s.push_str(", world");
-}
-

再看一个例子

-
fn main() {
-    let mut s1 = String::from("hello");
-    let r1 = &mut s1;
-    let r2 = &mut s1;
-}
-

这个例子在书里是会报错的,因为同时存在一个以上的可变引用,但是在我运行的版本里前面这段没有报错,只有当我真的要去更改的时候

-
fn main() {
-    let mut s1 = String::from("hello");
-    let mut r1 = &mut s1;
-    let mut r2 = &mut s1;
-    change(&mut r1);
-    change(&mut r2);
-}
+    mem_freed = 0;
 
+    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
+        goto cant_free; /* We need to free memory, but policy forbids. */
 
-fn change(s: &mut String) {
-    s.push_str(", world");
-}
-


这里可能就是具体版本在实现上的一个差异,我用的 rustc 是 1.44.0 版本
其实上面的主要是由 rust 想要避免这类多重可变更导致的异常问题,总结下就是三个点

-
    -
  • 两个或两个以上的指针同时同时访问同一空间
  • -
  • 其中至少有一个指针会想空间中写入数据
  • -
  • 没有同步数据访问的机制
    并且我们不能在拥有不可变引用的情况下创建可变引用
  • -
-

悬垂引用

还有一点需要注意的就是悬垂引用

-
fn main() {
-    let reference_to_nothing = dangle();
-}
+    latencyStartMonitor(latency);
+    while (mem_freed < mem_tofree) {
+        int j, k, i;
+        static unsigned int next_db = 0;
+        sds bestkey = NULL;
+        int bestdbid;
+        redisDb *db;
+        dict *dict;
+        dictEntry *de;
 
-fn dangle() -> &String {
-    let s = String::from("hello");
-    &s
-}
-

这里可以看到其实在 dangle函数返回后,这里的 s 理论上就离开了作用域,但是由于返回了 s 的引用,在 main 函数中就会拿着这个引用,就会出现如下错误

-

总结

最后总结下

-
    -
  • 在任何一个段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
  • -
  • 引用总是有效的。
  • -
-]]>
- - 语言 - Rust - - - Rust - 所有权 - 内存分布 - 新语言 - 可变引用 - 不可变引用 - -
- - rust学习笔记-所有权一 - /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ - 最近在看 《rust 权威指南》,还是难度比较大的,它里面的一些概念跟之前的用过的都有比较大的差别
比起有 gc 的虚拟机语言,跟像 C 和 C++这种主动释放内存的,rust 有他的独特点,主要是有三条

-
    -
  • Rust中的每一个值都有一个对应的变量作为它的所有者。
  • -
  • 在同一时间内,值有且只有一个所有者。
  • -
  • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

    这里有两个重点:
  • -
  • s 在进入作用域后才变得有效
  • -
  • 它会保持自己的有效性直到自己离开作用域为止
  • -
-

然后看个案例

-
let x = 5;
-let y = x;
-

这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
但是对于非基础类型

-
let s1 = String::from("hello");
-let s2 = s1;
+        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
+            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
+        {
+            struct evictionPoolEntry *pool = EvictionPoolLRU;
 
-println!("{}, world!", s1);
-

有可能认为有两种内存分布可能
先看下 string 的内存结构

第一种可能是

第二种是

我们来尝试编译下

发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了

这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法

-
let s1 = String::from("hello");
-let s2 = s1.clone();
+            while(bestkey == NULL) {
+                unsigned long total_keys = 0, keys;
 
-println!("s1 = {}, s2 = {}", s1, s2);
-

这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配

-]]>
- - 语言 - Rust - - - Rust - 所有权 - 内存分布 - 新语言 - -
- - spring event 介绍 - /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ - spring框架中如果想使用一些一部操作,除了依赖第三方中间件的消息队列,还可以用spring自己的event,简单介绍下使用方法
首先我们可以建一个event,继承ApplicationEvent

-

-public class CustomSpringEvent extends ApplicationEvent {
+                /* We don't want to make local-db choices when expiring keys,
+                 * so to start populate the eviction pool sampling keys from
+                 * every DB. */
+                for (i = 0; i < server.dbnum; i++) {
+                    db = server.db+i;
+                    dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
+                            db->dict : db->expires;
+                    if ((keys = dictSize(dict)) != 0) {
+                        evictionPoolPopulate(i, dict, db->dict, pool);
+                        total_keys += keys;
+                    }
+                }
+                if (!total_keys) break; /* No keys to evict. */
+
+                /* Go backward from best to worst element to evict. */
+                for (k = EVPOOL_SIZE-1; k >= 0; k--) {
+                    if (pool[k].key == NULL) continue;
+                    bestdbid = pool[k].dbid;
+
+                    if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
+                        de = dictFind(server.db[pool[k].dbid].dict,
+                            pool[k].key);
+                    } else {
+                        de = dictFind(server.db[pool[k].dbid].expires,
+                            pool[k].key);
+                    }
+
+                    /* Remove the entry from the pool. */
+                    if (pool[k].key != pool[k].cached)
+                        sdsfree(pool[k].key);
+                    pool[k].key = NULL;
+                    pool[k].idle = 0;
+
+                    /* If the key exists, is our pick. Otherwise it is
+                     * a ghost and we need to try the next element. */
+                    if (de) {
+                        bestkey = dictGetKey(de);
+                        break;
+                    } else {
+                        /* Ghost... Iterate again. */
+                    }
+                }
+            }
+        }
+
+        /* volatile-random and allkeys-random policy */
+        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
+                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
+        {
+            /* When evicting a random key, we try to evict a key for
+             * each DB, so we use the static 'next_db' variable to
+             * incrementally visit all DBs. */
+            for (i = 0; i < server.dbnum; i++) {
+                j = (++next_db) % server.dbnum;
+                db = server.db+j;
+                dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
+                        db->dict : db->expires;
+                if (dictSize(dict) != 0) {
+                    de = dictGetRandomKey(dict);
+                    bestkey = dictGetKey(de);
+                    bestdbid = j;
+                    break;
+                }
+            }
+        }
+
+        /* Finally remove the selected key. */
+        if (bestkey) {
+            db = server.db+bestdbid;
+            robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
+            propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
+            /* We compute the amount of memory freed by db*Delete() alone.
+             * It is possible that actually the memory needed to propagate
+             * the DEL in AOF and replication link is greater than the one
+             * we are freeing removing the key, but we can't account for
+             * that otherwise we would never exit the loop.
+             *
+             * AOF and Output buffer memory will be freed eventually so
+             * we only care about memory used by the key space. */
+            delta = (long long) zmalloc_used_memory();
+            latencyStartMonitor(eviction_latency);
+            if (server.lazyfree_lazy_eviction)
+                dbAsyncDelete(db,keyobj);
+            else
+                dbSyncDelete(db,keyobj);
+            latencyEndMonitor(eviction_latency);
+            latencyAddSampleIfNeeded("eviction-del",eviction_latency);
+            latencyRemoveNestedEvent(latency,eviction_latency);
+            delta -= (long long) zmalloc_used_memory();
+            mem_freed += delta;
+            server.stat_evictedkeys++;
+            notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
+                keyobj, db->id);
+            decrRefCount(keyobj);
+            keys_freed++;
 
-    private String message;
+            /* When the memory to free starts to be big enough, we may
+             * start spending so much time here that is impossible to
+             * deliver data to the slaves fast enough, so we force the
+             * transmission here inside the loop. */
+            if (slaves) flushSlavesOutputBuffers();
 
-    public CustomSpringEvent(Object source, String message) {
-        super(source);
-        this.message = message;
-    }
-    public String getMessage() {
-        return message;
-    }
-}
-

这个 ApplicationEvent 其实也比较简单,内部就一个 Object 类型的 source,可以自行扩展,我们在自定义的这个 Event 里加了个 Message ,只是简单介绍下使用

-
public abstract class ApplicationEvent extends EventObject {
-    private static final long serialVersionUID = 7099057708183571937L;
-    private final long timestamp;
+            /* Normally our stop condition is the ability to release
+             * a fixed, pre-computed amount of memory. However when we
+             * are deleting objects in another thread, it's better to
+             * check, from time to time, if we already reached our target
+             * memory, since the "mem_freed" amount is computed only
+             * across the dbAsyncDelete() call, while the thread can
+             * release the memory all the time. */
+            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
+                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
+                    /* Let's satisfy our stop condition. */
+                    mem_freed = mem_tofree;
+                }
+            }
+        } else {
+            latencyEndMonitor(latency);
+            latencyAddSampleIfNeeded("eviction-cycle",latency);
+            goto cant_free; /* nothing to free... */
+        }
+    }
+    latencyEndMonitor(latency);
+    latencyAddSampleIfNeeded("eviction-cycle",latency);
+    return C_OK;
 
-    public ApplicationEvent(Object source) {
-        super(source);
-        this.timestamp = System.currentTimeMillis();
-    }
+cant_free:
+    /* We are here if we are not able to reclaim memory. There is only one
+     * last thing we can try: check if the lazyfree thread has jobs in queue
+     * and wait... */
+    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
+        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
+            break;
+        usleep(1000);
+    }
+    return C_ERR;
+}
+

这里就是根据具体策略去淘汰 key,首先是要往 pool 更新 key,更新key 的方法是evictionPoolPopulate

+
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
+    int j, k, count;
+    dictEntry *samples[server.maxmemory_samples];
 
-    public ApplicationEvent(Object source, Clock clock) {
-        super(source);
-        this.timestamp = clock.millis();
-    }
+    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
+    for (j = 0; j < count; j++) {
+        unsigned long long idle;
+        sds key;
+        robj *o;
+        dictEntry *de;
 
-    public final long getTimestamp() {
-        return this.timestamp;
-    }
-}
+ de = samples[j]; + key = dictGetKey(de); -

然后就是事件生产者和监听消费者

-
@Component
-public class CustomSpringEventPublisher {
+        /* If the dictionary we are sampling from is not the main
+         * dictionary (but the expires one) we need to lookup the key
+         * again in the key dictionary to obtain the value object. */
+        if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
+            if (sampledict != keydict) de = dictFind(keydict, key);
+            o = dictGetVal(de);
+        }
 
-    @Resource
-    private ApplicationEventPublisher applicationEventPublisher;
+        /* Calculate the idle time according to the policy. This is called
+         * idle just because the code initially handled LRU, but is in fact
+         * just a score where an higher score means better candidate. */
+        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
+            idle = estimateObjectIdleTime(o);
+        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
+            /* When we use an LRU policy, we sort the keys by idle time
+             * so that we expire keys starting from greater idle time.
+             * However when the policy is an LFU one, we have a frequency
+             * estimation, and we want to evict keys with lower frequency
+             * first. So inside the pool we put objects using the inverted
+             * frequency subtracting the actual frequency to the maximum
+             * frequency of 255. */
+            idle = 255-LFUDecrAndReturn(o);
+        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
+            /* In this case the sooner the expire the better. */
+            idle = ULLONG_MAX - (long)dictGetVal(de);
+        } else {
+            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
+        }
 
-    public void publishCustomEvent(final String message) {
-        System.out.println("Publishing custom event. ");
-        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
-        applicationEventPublisher.publishEvent(customSpringEvent);
-    }
-}
-

这里的 ApplicationEventPublisher 是 Spring 的方法接口

-
@FunctionalInterface
-public interface ApplicationEventPublisher {
-    default void publishEvent(ApplicationEvent event) {
-        this.publishEvent((Object)event);
-    }
+        /* Insert the element inside the pool.
+         * First, find the first empty bucket or the first populated
+         * bucket that has an idle time smaller than our idle time. */
+        k = 0;
+        while (k < EVPOOL_SIZE &&
+               pool[k].key &&
+               pool[k].idle < idle) k++;
+        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
+            /* Can't insert if the element is < the worst element we have
+             * and there are no empty buckets. */
+            continue;
+        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
+            /* Inserting into empty position. No setup needed before insert. */
+        } else {
+            /* Inserting in the middle. Now k points to the first element
+             * greater than the element to insert.  */
+            if (pool[EVPOOL_SIZE-1].key == NULL) {
+                /* Free space on the right? Insert at k shifting
+                 * all the elements from k to end to the right. */
 
-    void publishEvent(Object var1);
-}
-

具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲

-

事件监听者:

-
@Component
-public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
-    @Override
-    public void onApplicationEvent(CustomSpringEvent event) {
-        System.out.println("Received spring custom event - " + event.getMessage());
-    }
-}
-

这里的也是 spring 的一个方法接口

-
@FunctionalInterface
-public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
-    void onApplicationEvent(E var1);
+                /* Save SDS before overwriting. */
+                sds cached = pool[EVPOOL_SIZE-1].cached;
+                memmove(pool+k+1,pool+k,
+                    sizeof(pool[0])*(EVPOOL_SIZE-k-1));
+                pool[k].cached = cached;
+            } else {
+                /* No free space on right? Insert at k-1 */
+                k--;
+                /* Shift all elements on the left of k (included) to the
+                 * left, so we discard the element with smaller idle time. */
+                sds cached = pool[0].cached; /* Save SDS before overwriting. */
+                if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
+                memmove(pool,pool+1,sizeof(pool[0])*k);
+                pool[k].cached = cached;
+            }
+        }
 
-    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
-        return (event) -> {
-            consumer.accept(event.getPayload());
-        };
-    }
-}
+ /* Try to reuse the cached SDS string allocated in the pool entry, + * because allocating and deallocating this object is costly + * (according to the profiler, not my fantasy. Remember: + * premature optimizbla bla bla bla. */ + int klen = sdslen(key); + if (klen > EVPOOL_CACHED_SDS_SIZE) { + pool[k].key = sdsdup(key); + } else { + memcpy(pool[k].cached,key,klen+1); + sdssetlen(pool[k].cached,klen); + pool[k].key = pool[k].cached; + } + pool[k].idle = idle; + pool[k].dbid = dbid; + } +}
+

Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些键的空闲时间还大)就可以进poolpool更新之后,就淘汰pool中空闲时间最大的键。

+

estimateObjectIdleTime用来计算Redis对象的空闲时间:

+
/* Given an object returns the min number of milliseconds the object was never
+ * requested, using an approximated LRU algorithm. */
+unsigned long long estimateObjectIdleTime(robj *o) {
+    unsigned long long lruclock = LRU_CLOCK();
+    if (lruclock >= o->lru) {
+        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
+    } else {
+        return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
+                    LRU_CLOCK_RESOLUTION;
+    }
+}
+

空闲时间第一种是 lurclock 大于对象的 lru,那么就是减一下乘以精度,因为 lruclock 有可能是已经预生成的,所以会可能走下面这个

+

LFU

上面介绍了LRU 的算法,但是考虑一种场景

+
~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
+~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
+~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
+~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
+

可以发现,当采用 lru 的淘汰策略的时候,D 是最新的,会被认为是最值得保留的,但是事实上还不如 A 跟 B,然后 antirez 大神就想到了LFU (Least Frequently Used) 这个算法, 显然对于上面的四个 key 的访问频率,保留优先级应该是 B > A > C = D
那要怎么来实现这个 LFU 算法呢,其实像LRU,理想的情况就是维护个链表,把最新访问的放到头上去,但是这个会影响访问速度,注意到前面代码的应该可以看到,redisObject 的 lru 字段其实是两用的,当策略是 LFU 时,这个字段就另作他用了,它的 24 位长度被分成两部分

+
      16 bits      8 bits
++----------------+--------+
++ Last decr time | LOG_C  |
++----------------+--------+
+

前16位字段是最后一次递减时间,因此Redis知道 上一次计数器递减,后8位是 计数器 counter。
LFU 的主体策略就是当这个 key 被访问的次数越多频率越高他就越容易被保留下来,并且是最近被访问的频率越高。这其实有两个事情要做,一个是在访问的时候增加计数值,在一定长时间不访问时进行衰减,所以这里用了两个值,前 16 位记录上一次衰减的时间,后 8 位记录具体的计数值。
Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:

+

volatile-lfu:对有过期时间的key采用LFU淘汰策略
allkeys-lfu:对全部key采用LFU淘汰策略
还有2个配置可以调整LFU算法:

+
lfu-log-factor 10
+lfu-decay-time 1
+```  
+`lfu-log-factor` 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
 
-

然后简单包个请求

-

-@RequestMapping(value = "/event", method = RequestMethod.GET)
-@ResponseBody
-public void event() {
-    customSpringEventPublisher.publishCustomEvent("hello sprint event");
-}
+`lfu-decay-time`是一个以分钟为单位的数值,可以调整counter的减少速度 +这里有个问题是 8 位大小够计么,访问一次加 1 的话的确不够,不过大神就是大神,才不会这么简单的加一。往下看代码 +```C +/* Low level key lookup API, not actually called directly from commands + * implementations that should instead rely on lookupKeyRead(), + * lookupKeyWrite() and lookupKeyReadWithFlags(). */ +robj *lookupKey(redisDb *db, robj *key, int flags) { + dictEntry *de = dictFind(db->dict,key->ptr); + if (de) { + robj *val = dictGetVal(de); -


就能看到接收到消息了。

+ /* Update the access time for the ageing algorithm. + * Don't do it if we have a saving child, as this will trigger + * a copy on write madness. */ + if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){ + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { + // 当淘汰策略是 LFU 时,就会调用这个updateLFU + updateLFU(val); + } else { + val->lru = LRU_CLOCK(); + } + } + return val; + } else { + return NULL; + } +}
+

updateLFU 这个其实个入口,调用了两个重要的方法

+
/* Update LFU when an object is accessed.
+ * Firstly, decrement the counter if the decrement time is reached.
+ * Then logarithmically increment the counter, and update the access time. */
+void updateLFU(robj *val) {
+    unsigned long counter = LFUDecrAndReturn(val);
+    counter = LFULogIncr(counter);
+    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
+}
+

首先来看看LFUDecrAndReturn,这个方法的作用是根据上一次衰减时间和系统配置的 lfu-decay-time 参数来确定需要将 counter 减去多少

+
/* If the object decrement time is reached decrement the LFU counter but
+ * do not update LFU fields of the object, we update the access time
+ * and counter in an explicit way when the object is really accessed.
+ * And we will times halve the counter according to the times of
+ * elapsed time than server.lfu_decay_time.
+ * Return the object frequency counter.
+ *
+ * This function is used in order to scan the dataset for the best object
+ * to fit: as we check for the candidate, we incrementally decrement the
+ * counter of the scanned objects if needed. */
+unsigned long LFUDecrAndReturn(robj *o) {
+    // 右移 8 位,拿到上次衰减时间
+    unsigned long ldt = o->lru >> 8;
+    // 对 255 做与操作,拿到 counter 值
+    unsigned long counter = o->lru & 255;
+    // 根据lfu_decay_time来算出过了多少个衰减周期
+    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
+    if (num_periods)
+        counter = (num_periods > counter) ? 0 : counter - num_periods;
+    return counter;
+}
+

然后是加,调用了LFULogIncr

+
/* Logarithmically increment a counter. The greater is the current counter value
+ * the less likely is that it gets really implemented. Saturate it at 255. */
+uint8_t LFULogIncr(uint8_t counter) {
+    // 最大值就是 255,到顶了就不加了
+    if (counter == 255) return 255;
+    // 生成个随机小数
+    double r = (double)rand()/RAND_MAX;
+    // 减去个基础值,LFU_INIT_VAL = 5,防止刚进来就被逐出
+    double baseval = counter - LFU_INIT_VAL;
+    // 如果是小于 0,
+    if (baseval < 0) baseval = 0;
+    // 如果 baseval 是 0,那么 p 就是 1了,后面 counter 直接加一,如果不是的话,得看系统参数lfu_log_factor,这个越大,除出来的 p 越小,那么 counter++的可能性也越小,这样子就把前面的疑问给解决了,不是直接+1 的
+    double p = 1.0/(baseval*server.lfu_log_factor+1);
+    if (r < p) counter++;
+    return counter;
+}
+

大概的变化速度可以参考

+
+--------+------------+------------+------------+------------+------------+
+| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
++--------+------------+------------+------------+------------+------------+
+| 0      | 104        | 255        | 255        | 255        | 255        |
++--------+------------+------------+------------+------------+------------+
+| 1      | 18         | 49         | 255        | 255        | 255        |
++--------+------------+------------+------------+------------+------------+
+| 10     | 10         | 18         | 142        | 255        | 255        |
++--------+------------+------------+------------+------------+------------+
+| 100    | 8          | 11         | 49         | 143        | 255        |
++--------+------------+------------+------------+------------+------------+
+

简而言之就是 lfu_log_factor 越大变化的越慢

+

总结

总结一下,redis 实现了近似的 lru 淘汰策略,通过增加了淘汰 key 的池子(pool),并且增大每次抽样的 key 的数量来将淘汰效果更进一步地接近于 lru,这是 lru 策略,但是对于前面举的一个例子,其实 lru 并不能保证 key 的淘汰就如我们预期,所以在后期又引入了 lfu 的策略,lfu的策略比较巧妙,复用了 redis 对象的 lru 字段,并且使用了factor 参数来控制计数器递增的速度,防止 8 位的计数器太早溢出。

]]>
- Java - Spring + Redis + 数据结构 + C + 源码 + Redis - Java - Spring - Spring Event + redis + 数据结构 + 源码
@@ -8459,146 +8612,6 @@ user3: swoole - - 一个 nginx 的简单记忆点 - /2022/08/21/%E4%B8%80%E4%B8%AA-nginx-%E7%9A%84%E7%AE%80%E5%8D%95%E8%AE%B0%E5%BF%86%E7%82%B9/ - 上周在处理一个 nginx 配置的时候,发现了一个之前不理解的小点,说一个场景,就是我们一般的处理方式就是一个 ip 端口只能配置一个域名的服务,比如 https://nicksxs.me 对应配置到 127.0.0.1:443,如果我想要把 https://nicksxs.com 也解析到这个服务器,并转发到不同的下游,这里就需要借助所谓的 SNI 的功能

-

Server Name Indication

A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. SNI is currently supported by most modern browsers, though may not be used by some old or special clients.
来源
机翻一下:在单个 IP 地址上运行多个 HTTPS 服务器的更通用的解决方案是 TLS 服务器名称指示扩展(SNI,RFC 6066),它允许浏览器在 SSL 握手期间传递请求的服务器名称,因此,服务器将知道哪个 它应该用于连接的证书。 目前大多数现代浏览器都支持 SNI,但某些旧的或特殊的客户端可能不使用 SNI。

-

首先我们需要确认 sni 已被支持

在实际的配置中就可以这样

-
stream {
-  map $ssl_preread_server_name $stream_map {
-    nicksxs.me nme;
-    nicksxs.com ncom;
-  }
-
-  upstream nme {
-    server 127.0.0.1:8000;
-  }
-  upstream ncom {
-    server 127.0.0.1:8001;
-  }
-
-  server {
-    listen 443 reuseport;
-    proxy_pass $stream_map;
-    ssl_preread on;
-  }
-}
-

类似这样,但是这个理解是非常肤浅和不完善的,只是简单记忆下,后续再进行补充完整

-

还有一点就是我们在配置的时候经常配置就是 server_name,但是会看到直接在使用 ssl_server_name,
其实在listen 标识了 ssl, 对应的 ssl_server_name 就等于 server_name,不需要额外处理了。

-]]>
- - nginx - - - nginx - -
- - spark-little-tips - /2017/03/28/spark-little-tips/ - spark 的一些粗浅使用经验

工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现

-

简单的mapreduce word count示例

textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
-data = textFile.reduceByKey(lambda x, y: x + y).collect()
-for _ in data:
-    print(_)
- - -

结果

(3, 2)
-(1, 2)
-(4, 2)
-(2, 2)
-(5, 2)
-]]>
- - data analysis - - - spark - python - -
- - summary-ranges-228 - /2016/10/12/summary-ranges-228/ - problem

Given a sorted integer array without duplicates, return the summary of its ranges.

-

For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].

-

题解

每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考

-

Code

class Solution {
-public:
-    vector<string> summaryRanges(vector<int>& nums) {
-        int i = 0, j = 1, n;
-        vector<string> res;
-        n = nums.size();
-        while(i < n){
-            j = 1;
-            while(j < n && nums[i+j] - nums[i] == j) j++;
-            res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
-            i += j;
-        }
-        return res;
-    }
-};
]]>
- - leetcode - - - leetcode - c++ - -
- - 《长安的荔枝》读后感 - /2022/07/17/%E3%80%8A%E9%95%BF%E5%AE%89%E7%9A%84%E8%8D%94%E6%9E%9D%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F/ - 断断续续地看完了马伯庸老师的《长安的荔枝》,一开始是看这本书在排行榜排得很高,又是马伯庸的,之前看过他的《古董局中局》,还是很有意思的,而且正好是比较短的,不过前后也拖了蛮久才看完,看完后读了下马老师自己写的后记,就特别有感触。
整个故事是围绕一个上林署监事李善德被委任一项给贵妃送荔枝的差事展开,“长安回望绣成堆,山顶千门次第开,一骑红尘妃子笑,无人知是荔枝来”,以前没细究过这个送荔枝的过程,但是以以前的运输速度和保鲜条件,感觉也不是太现实,所以主人公一开始就以为只是像以往一样是送荔枝干这种,能比较方便运输,不容易变质的,结果发现其实是同僚在坑他,这次是要在贵妃生辰的时候给贵妃送来新鲜的岭南荔枝,用比较时兴的词来说,这就是个送命题啊,鲜荔枝一日色变,两日香变,三日味变,同僚的还有杜甫跟韩承,都觉得老李可以直接写休书了,保全家人,不然就是全家送命,李善德也觉得基本算是判刑了,而且其实是这事被转了几次,最后到老李所在的上林署,主管为了骗他接下这个活还特意在文书上把荔枝鲜的“鲜”字贴住,那会叫做“贴黄”,变成了荔枝“煎”,所以说官场险恶,大家都想把这烫手山芋丢出去,结果丢到了我们老实的老李头上,但是从接到这个通知到贵妃的生辰六月初一还有挺长的时间,其实这个活虽然送命,但是在前期这个“荔枝使”也基本就是类似带着尚方宝剑,御赐黄马褂的职位,随便申请经费,不必像常规的部门费用需要定预算,申请后再层层审批,而是特事特批特办的耍赖做法,所以在这段时间是能够潇洒挥霍一下的。其实可以好好地捞一波给妻女,然后写下和离,在自己死后能让她们过的好一些,但最后还是在杜甫的一番劝导下做出了尝试一番的决定,因为也没其他办法,既是退无可退,何不向前拼死一搏,其实说到这,我觉得看这本书感觉有所收获的第一点,有时候总觉得事情没戏了,想躺平放弃了,但是这样其实这个结果是不会变好的,尝试努力,拼尽全力搏一搏,说不定会有所改观,至少不会变更坏了。

-]]>
- - 读后感 - 生活 - - - 生活 - 读后感 - -
- - wordpress 忘记密码的一种解决方法 - /2021/12/05/wordpress-%E5%BF%98%E8%AE%B0%E5%AF%86%E7%A0%81%E7%9A%84%E4%B8%80%E7%A7%8D%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/ - 前阵子搭了个 WordPress,但是没怎么用,前两天发现忘了登录密码了,最近不知道是什么情况,chrome 的记住密码跟历史记录感觉有点问题,历史记录丢了不少东西,可能是时间太久了,但是理论上应该有 LRU 这种策略的,有些还比较常用,还有记住密码,因为个人域名都是用子域名分配给各个服务,有些记住了,有些又没记住密码,略蛋疼,所以就找了下这个方案。
当然这个方案不是最优的,有很多限制,首先就是要能够登陆 WordPress 的数据库,不然这个方法是没用的。
首先不管用什么方式(别违法)先登陆数据库,选择 WordPress 的数据库,可以看到里面有几个表,我们的目标就是 wp_users 表,用 select 查询看下可以看到有用户的数据,如果是像我这样搭着玩的没有创建其他用户的话应该就只有一个用户,那我们的表里的用户数据就只会有一条,当然多条的话可以通过用户名来找

然后可能我这个版本是这样,没有装额外的插件,密码只是经过了 MD5 的单向哈希,所以我们可以设定一个新密码,然后用 MD5 编码后直接更新进去

-
UPDATE wp_users SET user_pass = MD5('123456') WHERE ID = 1;
- -

然后就能用自己的账户跟刚才更新的密码登录了。

-]]>
- - 小技巧 - - - WordPress - 小技巧 - -
- - 《垃圾回收算法手册读书》笔记之整理算法 - /2021/03/07/%E3%80%8A%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%89%8B%E5%86%8C%E8%AF%BB%E4%B9%A6%E3%80%8B%E7%AC%94%E8%AE%B0%E4%B9%8B%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95/ - 最近看了下这本垃圾回收算法手册,看到了第三章的标记-整理回收算法,做个简单的读书笔记

-

双指针整理算法

对于一块待整理区域,通过两个指针,free 在区域的起始端,scan 指针在区域的末端,free 指针从前往后知道找到空闲区域,scan 从后往前一直找到存活对象,当 free 指针未与 scan 指针交叉时,会给 scan 位置的对象特定位置标记上 free 的地址,即将要转移的地址,不过这里有个限制,这种整理算法一般会用于对象大小统一的情况,否则 free 指针扫描时还需要匹配scan 指针扫描到的存活对象的大小。

-

Lisp 2 整理算法

需要三次完整遍历堆区域
第一遍是遍历后将计算出所有对象的最终地址(转发地址)
第二遍是使用转发地址更新赋值器线程根以及被标记对象中的引用,该操作将确保它们指向对象的新位置
第三次遍历是relocate最终将存活对象移动到其新的目标位置

-

引线整理算法

这个真的长见识了,

可以看到,原来是 A,B,C 对象引用了 N,这里会在第一次遍历的时候把这种引用反过来,让 N 的对象头部保存下 A 的地址,表示这类引用,然后在遍历到 B 的时候在链起来,到最后就会把所有引用了 N 对象的所有对象通过引线链起来,在第二次遍历的时候就把更新A,B,C 对象引用的 N 地址,并且移动 N 对象

-

单次遍历算法

这个一直提到过位图的实现方式,

可以看到在第一步会先通过位图标记,标记的方式是位图的每一位对应的堆内存的一个字(这里可能指的是 byte 吧),然后将一个存活对象的内存区域的第一个字跟最后一个字标记,这里如果在通过普通的方式就还需要一个地方在存转发地址,但是因为具体的位置可以通过位图算出来,也就不需要额外记录了

-]]>
- - Java - gc - jvm - - - java - gc - 标记整理 - 垃圾回收 - jvm - -
介绍下最近比较实用的端口转发 /2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ @@ -8620,41 +8633,8 @@ public: 技巧 - ssh - 端口转发 - - - - 从清华美院学姐聊聊我们身边的恶人 - /2020/11/29/%E4%BB%8E%E6%B8%85%E5%8D%8E%E7%BE%8E%E9%99%A2%E5%AD%A6%E5%A7%90%E8%81%8A%E8%81%8A%E6%88%91%E4%BB%AC%E8%BA%AB%E8%BE%B9%E7%9A%84%E6%81%B6%E4%BA%BA/ - 前几天清华美院学姐的热点火了,然后仔细看了下,其实是个学姐诬陷以为其貌不扬的男同学摸她屁股

然后还在朋友圈发文想让他社死,我也是挺晚才知道这个词什么意思,然后后面我看到了这个图片,挺有意思的

本来其实也没什么想聊这个的,是在 B 站看了个吐槽这个的,然后刚好晚上乘公交的时候又碰到了有点类似的问题
故事描述下,我们从始发站做了公交,这辆公交司机上次碰到过一回,就是会比较关注乘客的佩戴情况,主要考虑到目前国内疫情,然后这次在差不多人都坐满的情况下,可能在提示了三次让车内乘客戴好口罩,但是他指的那个中年女性还是没有反应,司机就转头比较大声指着这个乘客(中年女性)让戴好口罩,然后这个乘客(中年女性)就大声的说“我口罩是滑下来了,你指着我干嘛,你态度这么差,要吃了我一样,我要投诉你”等等,然后可能跟她一块的一个中年女性也是这么帮腔指责司机,比较基本的理解,车子里这么多乘客,假如是处于这位乘客口罩滑下来了而不自知的情况下,司机在提示了三次以后回头指着她说,我想的是没什么问题的,但是这位却反而指责这位司机指着她,并且说是态度差,要吃了她,完全是不可理喻的,并且一直喋喋不休说她口罩滑掉了有什么错,要投诉这个司机,让他可以提前退休了,在其他乘客的劝说下司机准备继续开车时,又口吐芬芳“你个傻,你来打我呀”,真的是让我再次体会到了所谓的恶人先告状的又一完美呈现,后面还有个乘客还是表示要打死司机这个傻,让我有点不明所以,俗话说有人是得理不饶人,前提是得理,这种理亏不饶人真的是挺让人长见识的,试想下,司机在提示三次后,这位乘客还是没有把口罩戴好,如何在不指着这位乘客的情况下能准确的提示到她呢,并且觉得语气态度不好,司机要载着一车的人,因为你这一个乘客不戴好口罩而不能正常出发,有些着急应该很正常吧,可能是平时自己在家里耀武扬威的使唤别人习惯了吧,别人不敢这么大声跟她说话,其实想想这位中年女性应该年纪不会很大,还比较时髦的吧,像一些常见的中年杭州本地人可能是不会说傻*这个词的吧。
杭州的公交可能是在二月份疫情还比较严重的时候是要求上车出示健康码,后面比较缓和以后只要求佩戴好口罩,但是在我们小绍兴,目前还是一律要求检验健康码和佩戴口罩,对于疫情中,并且目前阶段国内也时有报出小范围的疫情的情况下,司机尽职要求佩戴好口罩其实也是为了乘客着想,另一种情况如果司机不严格要求,万一车上有个感染者,这位中年女性被传染了,如果能找到这个司机的话,是不是想“打死”这个司机,竟然让感染者上了车,反正她自己是不可能有错的,上来就是对方态度差,要投诉,自己不戴好口罩啥都没错,我就想知道如果因为自己没戴好口罩被感染了,是不是也是司机的错,毕竟没有像仆人那样点头哈腰求着她戴好口罩。
再说回来,整个车就她一个人没戴好口罩,并且还有个细节,其实这个乘客是上了车之后就没戴好了,本来上车的时候是戴好的,这种比较有可能是觉得上车的时候司机那看一眼就好了,如果好好戴着口罩,一点事情都没有,唉,纯粹是太气愤了,调理逻辑什么的就忽略吧

-]]>
- - 生活 - 吐槽 - 疫情 - 口罩 - - - 生活 - 吐槽 - 疫情 - 公交车 - 口罩 - 杀人诛心 - -
- - 上次的其他 外行聊国足 - /2022/03/06/%E4%B8%8A%E6%AC%A1%E7%9A%84%E5%85%B6%E4%BB%96-%E5%A4%96%E8%A1%8C%E8%81%8A%E5%9B%BD%E8%B6%B3/ - 上次本来想在换车牌后面聊下这个话题,为啥要聊这个话题呢,也很简单,在地铁上看到一对猜测是情侣或者比较关系好的男女同学在聊,因为是这位男同学是大学学的工科,然后自己爱好设计绘画相关的,可能还以此赚了点钱,在地铁上讨论男的要不要好好努力把大学课程完成好,大致的观点是没必要,本来就不适合,这一段我就不说了,恋爱人的嘴,信你个鬼。
后面男的说在家里又跟他爹吵了关于男足的,估计是那次输了越南,实话说我不是个足球迷,对各方面技术相关也不熟,只是对包括这个人的解释和网上一些观点的看法,纯主观,这次地铁上这位说的大概意思是足球这个训练什么的很难的,要想赢越南也很难的,不是我们能嘴炮的;在网上看到一个赞同数很多的一个回答,说什么中国是个体育弱国,但是由于有一些乒乓球,跳水等小众项目比较厉害,让民众给误解了,首先我先来反驳下这个偷换概念的观点,第一所谓的体育弱国,跟我们觉得足球不应该这么差没半毛钱关系,因为体育弱国,我们的足球本来就不是顶尖的,也并不是去跟顶尖的球队去争,以足球为例,跟巴西,阿根廷,英国,德国,西班牙,意大利,法国这些足球强国,去比较,我相信没有一个足球迷会这么去做对比,因为我们足球历史最高排名是 1998 年的 37 名,最差是 100 名,把能数出来的强队都数完,估计都还不会到 37,所以根本没有跟强队去做对比,第二体育弱国,我们的体育投入是在逐年降低吗,我们是因战乱没法好好训练踢球?还是这帮傻逼就不争气,前面也说了我们足球世界排名最高 37,最低 100,那么前阵子我们输的越南是第几,目前我们的排名 77 名,越南 92 名,看明白了么,轮排名我们都不至于输越南,然后就是这个排名,这也是我想回应那位地铁上的兄弟,我觉得除了造核弹这种高精尖技术,绝大部分包含足球这类运动,遵循类二八原则,比如满分是 100 分,从 80 提到 90 分或者 90 分提到 100 分非常难,30 分提到 40 分,50 分提到 60 分我觉得都是可以凭后天努力达成的,基本不受天赋限制,这里可以以篮球来类比下,相对足球的确篮球没有那么火,或者行业市值没法比,但是也算是相对大众了,中国在篮球方面相对比较好一点,在 08 年奥运会冲进过八强,那也不是唯一的巅峰,但是我说这个其实是想说明两方面的事情,第一,像篮球一样,状态是有起起伏伏,排名也会变动,但是我觉得至少能维持一个相对稳定的总体排名和持平或者上升的趋势,这恰恰是我们这种所谓的“体育弱国”应该走的路线,第二就是去支持我的类二八原则的,可以看到我们的篮球这两年也很垃圾,排名跌到 29 了,那问题我觉得跟足球是一样的,就是不能脚踏实地,如斯科拉说的,中国篮球太缺少竞争,打得好不好都是这些人打,打输了还是照样拿钱,相对足球,篮球的技术我还是懂一些的,对比 08 年的中国男篮,的确像姚明跟王治郅这样的天赋型+努力型球员少了以后竞争力下降在所难免,但是去对比下基本功,传球,投篮,罚球稳定性,也完全不是一个水平的,这些就是我说的,可以通过努力训练拿 80 分的,只要拿到 80 分,甚至只要拿到 60 分,我觉得应该就还算对得起球迷了,就像 NBA 里球队也会有核心球员的更替,战绩起起伏伏,但是基本功这东西,防守积极性,我觉得不随核心球员的变化而变化,就像姚明这样的天赋,其实他应该还有一些先天缺陷,大脚趾较长等,但是他从 CBA 到 NBA,在 NBA 适应并且打成顶尖中锋,离不开刻苦训练,任何的成功都不是纯天赋的,必须要付出足够的努力。
说回足球,如果像前面那么洗地(体育弱国),那能给我维持住一个稳定的排名我也能接受,问题是我们的经济物质资源比 2000 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
纯主观吐槽,勿喷。

-]]>
- - 生活 - 运动 - - - 生活 + ssh + 端口转发
@@ -8726,6 +8706,26 @@ public: 中间件 + + 从清华美院学姐聊聊我们身边的恶人 + /2020/11/29/%E4%BB%8E%E6%B8%85%E5%8D%8E%E7%BE%8E%E9%99%A2%E5%AD%A6%E5%A7%90%E8%81%8A%E8%81%8A%E6%88%91%E4%BB%AC%E8%BA%AB%E8%BE%B9%E7%9A%84%E6%81%B6%E4%BA%BA/ + 前几天清华美院学姐的热点火了,然后仔细看了下,其实是个学姐诬陷以为其貌不扬的男同学摸她屁股

然后还在朋友圈发文想让他社死,我也是挺晚才知道这个词什么意思,然后后面我看到了这个图片,挺有意思的

本来其实也没什么想聊这个的,是在 B 站看了个吐槽这个的,然后刚好晚上乘公交的时候又碰到了有点类似的问题
故事描述下,我们从始发站做了公交,这辆公交司机上次碰到过一回,就是会比较关注乘客的佩戴情况,主要考虑到目前国内疫情,然后这次在差不多人都坐满的情况下,可能在提示了三次让车内乘客戴好口罩,但是他指的那个中年女性还是没有反应,司机就转头比较大声指着这个乘客(中年女性)让戴好口罩,然后这个乘客(中年女性)就大声的说“我口罩是滑下来了,你指着我干嘛,你态度这么差,要吃了我一样,我要投诉你”等等,然后可能跟她一块的一个中年女性也是这么帮腔指责司机,比较基本的理解,车子里这么多乘客,假如是处于这位乘客口罩滑下来了而不自知的情况下,司机在提示了三次以后回头指着她说,我想的是没什么问题的,但是这位却反而指责这位司机指着她,并且说是态度差,要吃了她,完全是不可理喻的,并且一直喋喋不休说她口罩滑掉了有什么错,要投诉这个司机,让他可以提前退休了,在其他乘客的劝说下司机准备继续开车时,又口吐芬芳“你个傻,你来打我呀”,真的是让我再次体会到了所谓的恶人先告状的又一完美呈现,后面还有个乘客还是表示要打死司机这个傻,让我有点不明所以,俗话说有人是得理不饶人,前提是得理,这种理亏不饶人真的是挺让人长见识的,试想下,司机在提示三次后,这位乘客还是没有把口罩戴好,如何在不指着这位乘客的情况下能准确的提示到她呢,并且觉得语气态度不好,司机要载着一车的人,因为你这一个乘客不戴好口罩而不能正常出发,有些着急应该很正常吧,可能是平时自己在家里耀武扬威的使唤别人习惯了吧,别人不敢这么大声跟她说话,其实想想这位中年女性应该年纪不会很大,还比较时髦的吧,像一些常见的中年杭州本地人可能是不会说傻*这个词的吧。
杭州的公交可能是在二月份疫情还比较严重的时候是要求上车出示健康码,后面比较缓和以后只要求佩戴好口罩,但是在我们小绍兴,目前还是一律要求检验健康码和佩戴口罩,对于疫情中,并且目前阶段国内也时有报出小范围的疫情的情况下,司机尽职要求佩戴好口罩其实也是为了乘客着想,另一种情况如果司机不严格要求,万一车上有个感染者,这位中年女性被传染了,如果能找到这个司机的话,是不是想“打死”这个司机,竟然让感染者上了车,反正她自己是不可能有错的,上来就是对方态度差,要投诉,自己不戴好口罩啥都没错,我就想知道如果因为自己没戴好口罩被感染了,是不是也是司机的错,毕竟没有像仆人那样点头哈腰求着她戴好口罩。
再说回来,整个车就她一个人没戴好口罩,并且还有个细节,其实这个乘客是上了车之后就没戴好了,本来上车的时候是戴好的,这种比较有可能是觉得上车的时候司机那看一眼就好了,如果好好戴着口罩,一点事情都没有,唉,纯粹是太气愤了,调理逻辑什么的就忽略吧

+]]>
+ + 生活 + 吐槽 + 疫情 + 口罩 + + + 生活 + 吐槽 + 疫情 + 公交车 + 口罩 + 杀人诛心 + +
从丁仲礼被美国制裁聊点啥 /2020/12/20/%E4%BB%8E%E4%B8%81%E4%BB%B2%E7%A4%BC%E8%A2%AB%E7%BE%8E%E5%9B%BD%E5%88%B6%E8%A3%81%E8%81%8A%E7%82%B9%E5%95%A5/ @@ -8746,6 +8746,28 @@ public: 美国 + + 关于公共交通再吐个槽 + /2021/03/21/%E5%85%B3%E4%BA%8E%E5%85%AC%E5%85%B1%E4%BA%A4%E9%80%9A%E5%86%8D%E5%90%90%E4%B8%AA%E6%A7%BD/ + 事情源于周末来回家发生的两件事情,先是回去的时候从高铁下车要坐公交,现在算是有个比较好的临时候车点了,但是可能由于疫情好转,晚上都不用检查健康码就可以进候车点,但是上公交的时候还是需要看健康码,一般情况下从高铁下来的,各个地方的人都有,而且也不太清楚这边上公交车需要查验健康码,我的一个看法是候车的时候就应该放个横幅告示,或者再配合喇叭循环播放,请提前准备好健康码,上车前需要查验,因为像这周的情况,我乘坐的那辆车是间隔时间比较长,而且终点是工业开发区,可能是比较多外来务工人员的目的地,正好这部分人可能对于操作手机检验健康码这个事情不太熟悉,所以结果就是头上几个不知道怎么搞出来健康码,然后让几乎有满满一整车的人在后面堵着,司机又非常厌烦那些没有提前出示健康码的,有位乘客搞得久了点,还被误以为没刷卡买票,差点吵起来,其实公共交通或者高铁站负责的在公交指引路线上多写一句上车前需要查验健康码,可能就能改善比较多,还有就是那个积水的路,这个吐槽起来就一大坨了,整个绍兴像 dayuejin 一样到处都是破路。
第二个就是来杭州的时候,经过人行横道,远处车道的公交车停下来等我们了,为了少添麻烦总想快点穿过去,但是这时靠近我们的车道(晚上光线较暗,可见度不佳)有一辆从远处来的奥迪 A4 还是 A5 这种的车反而想加速冲过去,如果少看一下可能我已经残了,交规考的靠近人行道要减速好像基本都是个摆设了,杭州也只有公交车因为一些考核指标原因会主动礼让,人其实需要有同理心,虽然可能有些人是开车多于骑车走路的,但是总也不可能永远不穿人行道吧,甚至这些人可能还会在人行道红灯的时候走过去。这个事情不是吐槽公共交通的,只是也有些许关系,想起来还有一件事也是刚才来杭州的时候看到的,等公交的时候看到有辆路虎要加塞,而目标车道刚好是辆大货车,大货车看到按了喇叭,路虎犹豫了下还是挤进去了,可能是对路虎的扛撞性能非常自信吧,反正我是挺后怕的,这种级别的车,被撞了的话估计还是鸡蛋撞石头,吨位惯性在那,这里再延伸下,挺多开豪车的人好像都觉得这路上路权更大一些,谁谁都得让着他,可能实际吃亏的不多,所以越加巩固了这种思维,当真的碰到不管的可能就会明白了,路权这个事情在天朝也基本没啥人重视,也没想说个结论,就到这吧

+]]>
+ + 生活 + 公交 + + + 生活 + 开车 + 加塞 + 糟心事 + 规则 + 公交 + 路政规划 + 基础设施 + 杭州 + 健康码 + +
关于读书打卡与分享 /2021/02/07/%E5%85%B3%E4%BA%8E%E8%AF%BB%E4%B9%A6%E6%89%93%E5%8D%A1%E4%B8%8E%E5%88%86%E4%BA%AB/ @@ -8765,25 +8787,6 @@ public: 足球 - - 分享记录一下一个 git 操作方法 - /2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-git-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/ - 前阵子一个同事因为发现某个分支上的代码好像有缺失导致无法正常运行,然后就对比了下把缺失的代码从另一个分支上拷了过来,可能有所欠考虑,不过主要是说下操作过程和最后的处理方法,这位同学的操作是改一些代码commit 一下,这样的 commit 了大概五六次,并且已经 push 到了远端,然后就在想要怎么去处理,在本地可以 reset,已经到远端了,一个很不优雅的操作就是本地 reset 了用 force push,这个当然是不可取的,然后就是 revert 了,但是又已经 commit 了好几次了,网上看了下,好像处理方法还挺成熟的,git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。可以理解为就是提交一个反向的操作,这里其实我们可以用range revert来进行 git revert, 用法就是

-
git revert OLDER_COMMIT^..NEWER_COMMIT
-

这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明

然后就可以用 -n 参数,表示不自动提交

-
git revert -n OLDER_COMMIT^..NEWER_COMMIT
-git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"
- - -]]>
- - git - 小技巧 - - - git - -
分享记录一下一个 scp 操作方法 /2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-scp-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/ @@ -8803,28 +8806,6 @@ public: scp - - 关于公共交通再吐个槽 - /2021/03/21/%E5%85%B3%E4%BA%8E%E5%85%AC%E5%85%B1%E4%BA%A4%E9%80%9A%E5%86%8D%E5%90%90%E4%B8%AA%E6%A7%BD/ - 事情源于周末来回家发生的两件事情,先是回去的时候从高铁下车要坐公交,现在算是有个比较好的临时候车点了,但是可能由于疫情好转,晚上都不用检查健康码就可以进候车点,但是上公交的时候还是需要看健康码,一般情况下从高铁下来的,各个地方的人都有,而且也不太清楚这边上公交车需要查验健康码,我的一个看法是候车的时候就应该放个横幅告示,或者再配合喇叭循环播放,请提前准备好健康码,上车前需要查验,因为像这周的情况,我乘坐的那辆车是间隔时间比较长,而且终点是工业开发区,可能是比较多外来务工人员的目的地,正好这部分人可能对于操作手机检验健康码这个事情不太熟悉,所以结果就是头上几个不知道怎么搞出来健康码,然后让几乎有满满一整车的人在后面堵着,司机又非常厌烦那些没有提前出示健康码的,有位乘客搞得久了点,还被误以为没刷卡买票,差点吵起来,其实公共交通或者高铁站负责的在公交指引路线上多写一句上车前需要查验健康码,可能就能改善比较多,还有就是那个积水的路,这个吐槽起来就一大坨了,整个绍兴像 dayuejin 一样到处都是破路。
第二个就是来杭州的时候,经过人行横道,远处车道的公交车停下来等我们了,为了少添麻烦总想快点穿过去,但是这时靠近我们的车道(晚上光线较暗,可见度不佳)有一辆从远处来的奥迪 A4 还是 A5 这种的车反而想加速冲过去,如果少看一下可能我已经残了,交规考的靠近人行道要减速好像基本都是个摆设了,杭州也只有公交车因为一些考核指标原因会主动礼让,人其实需要有同理心,虽然可能有些人是开车多于骑车走路的,但是总也不可能永远不穿人行道吧,甚至这些人可能还会在人行道红灯的时候走过去。这个事情不是吐槽公共交通的,只是也有些许关系,想起来还有一件事也是刚才来杭州的时候看到的,等公交的时候看到有辆路虎要加塞,而目标车道刚好是辆大货车,大货车看到按了喇叭,路虎犹豫了下还是挤进去了,可能是对路虎的扛撞性能非常自信吧,反正我是挺后怕的,这种级别的车,被撞了的话估计还是鸡蛋撞石头,吨位惯性在那,这里再延伸下,挺多开豪车的人好像都觉得这路上路权更大一些,谁谁都得让着他,可能实际吃亏的不多,所以越加巩固了这种思维,当真的碰到不管的可能就会明白了,路权这个事情在天朝也基本没啥人重视,也没想说个结论,就到这吧

-]]>
- - 生活 - 公交 - - - 生活 - 开车 - 加塞 - 糟心事 - 规则 - 公交 - 路政规划 - 基础设施 - 杭州 - 健康码 - -
周末我在老丈人家打了天小工 /2020/08/16/%E5%91%A8%E6%9C%AB%E6%88%91%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/ @@ -8845,23 +8826,22 @@ public: - 在老丈人家的小工记五 - /2020/10/18/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%BA%94/ - 终于回忆起来了,年纪大了写这种东西真的要立马写,不然很容易想不起来,那天应该是 9 月 12 日,也就是上周六,因为我爸也去了,而且娘亲(丈母娘,LD 这么叫,我也就随了她这么叫,当然是背后,当面就叫妈)也在那,早上一到那二爹就给我爸指挥了活,要挖一条院子的出水道,自己想出来的词,因为觉得下水道是竖的,在那稍稍帮了一会会忙,然后我还是比较惯例的跟着 LD 还有娘亲去住的家里,主要是老丈人可能也不太想让我干太累的活,因为上次已经差不多把三楼都整理干净了,然后就是二楼了,二楼说实话我也帮不上什么忙,主要是衣服被子什么的,正好是有张以前小孩子睡过的那种摇篮床,看上去虽然有一些破损,整体还是不错的,所以打算拿过去,我就负责把它拆掉了,比较简单的是只要拧螺丝就行了,但是其实是用了好多好多工具才搞定的,一开始只要螺丝刀就行了,但是因为年代久了,后面的螺帽也有点锈住或者本身就会串着会一起动,所以拿来了个扳手,大部分的其实都被这两个工具给搞定了,但是后期大概还剩下四分之一的时候,有一颗完全锈住,并且螺纹跟之前那些都不一样,但是这个已经是最大的螺丝刀了,也没办法换个大的了,所以又去找来个一字的,因为十字的不是也可以用一字的拧嘛,结果可能是我买的工具箱里的一字螺丝刀太新了,口子那很锋利,直接把螺丝花纹给划掉了,大的小的都划掉,然后真的变成凹进去一个圆柱体了,然后就想能不能隔一层布去拧,然而因为的确是已经变成圆柱体了,布也不太给力,不放弃的我又去找来了个老虎钳,妄图把划掉的螺丝用老虎钳钳住,另一端用扳手拧开螺帽,但是这个螺丝跟螺帽真的是生锈的太严重了,外加上钳不太牢,完全是两边一起转,实在是没办法了,在征得同意之后,直接掰断了,火死了,一颗螺丝折腾得比我拆一张床还久,那天因为早上去的也比较晚了,然后就快吃午饭了,正好想着带一点东西过去,就把一些脸盆,泡脚桶啥的拿过去了,先是去吃了饭,还是在那家快餐店,菜的口味还是依然不错,就是人比较多,我爸旁边都是素菜,都没怎么吃远一点的荤菜,下次要早点去,把荤菜放我爸旁边😄(PS:他们家饭还是依然尴尬,需要等),吃完就开到在修的房子那把东西拿了出来,我爸已经动作很快的打了一小半的地沟了,说实话那玩意真的是很重,我之前把它从三楼拿下来,就觉得这个太重了,这次还要用起来,感觉我的手会分分钟废掉,不过一开始我还是跟着LD去了住的家里,惯例睡了午觉,那天睡得比较踏实,竟然睡了一个小时,醒了想了下,其实LD她们收拾也用不上我(没啥力气活),我还是去帮我爸他们,跟LD说了下就去了在修的老房子那,两位老爹在一起钻地,看着就很累,我连忙上去想换一会他们,因为刚好是钻到混凝土地线,特别难,力道不够就会滑开,用蛮力就是钻进去拔不出来,原理是因为本身浇的时候就是很紧实的,需要边钻边动,那家伙实在是太重了,真的是汗如雨下,基本是三个人轮流来,我是个添乱的,经常卡住,然后把地线,其实就是一条混凝土横梁,里面还有14跟18的钢筋,需要割断,这个割断也是很有技巧,钢筋本身在里面是受到挤压的,直接用切割的,到快断掉的时候就会崩一下,非常危险,还是老丈人比较有经验,要留一点点,然后直接用榔头敲断就好了,本来以为这个是最难的了,结果下面是一块非常大的青基石,而且也是石头跟石头挤一块,边上一点点打钻有点杯水车薪,后来是用那种螺旋的钻,钻四个洞,相对位置大概是个长方形,这样子把中间这个长方形钻出来就比较容易地能拿出来了,后面的也容易搞出来了,后面的其实难度不是特别大了,主要是地沟打好之后得看看高低是不是符合要求的,不能本来是往外排水的反而外面高,这个怎么看就又很有技巧了,一般在地上的只要侧着看一下就好了,考究点就用下水平尺,但是在地下的,不用水平尺,其实可以借助于地沟里正要放进去的水管,放点水进去,看水往哪流就行了,铺好水管后,就剩填埋的活了,不是太麻烦了,那天真的是累到了,打那个混凝土的时候我真的是把我整个人压上去了,不过也挺爽的,有点把平时无处发泄的蛮力发泄出去了。

+ 分享记录一下一个 git 操作方法 + /2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-git-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/ + 前阵子一个同事因为发现某个分支上的代码好像有缺失导致无法正常运行,然后就对比了下把缺失的代码从另一个分支上拷了过来,可能有所欠考虑,不过主要是说下操作过程和最后的处理方法,这位同学的操作是改一些代码commit 一下,这样的 commit 了大概五六次,并且已经 push 到了远端,然后就在想要怎么去处理,在本地可以 reset,已经到远端了,一个很不优雅的操作就是本地 reset 了用 force push,这个当然是不可取的,然后就是 revert 了,但是又已经 commit 了好几次了,网上看了下,好像处理方法还挺成熟的,git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。可以理解为就是提交一个反向的操作,这里其实我们可以用range revert来进行 git revert, 用法就是

+
git revert OLDER_COMMIT^..NEWER_COMMIT
+

这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明

然后就可以用 -n 参数,表示不自动提交

+
git revert -n OLDER_COMMIT^..NEWER_COMMIT
+git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"
+ + ]]>
- 生活 - 运动 - 跑步 - 干活 + git + 小技巧 - 生活 - 小技巧 - 运动 - 减肥 - 跑步 - 干活 + git
@@ -8885,23 +8865,24 @@ public: - 在老丈人家的小工记四 - /2020/09/26/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E5%9B%9B/ - 小工记四

第四周去的时候让我们去了现在在住的房子里,去三楼整理东西了,蛮多的东西需要收拾整理,有些需要丢一下,以前往往是把不太要用的东西就放三楼了,但是后面就不会再去收拾整理,LD 跟丈母娘负责收拾,我不太知道哪些还要的,哪些不要了,而且本来也不擅长这种收拾🤦‍♂️,然后就变成她们收拾出来废纸箱,我负责拆掉,压平,这时候终于觉得体重还算是有点作用,总体来说这个事情我其实也不擅长,不擅长的主要是捆起来,可能我总是小题大做,因为纸箱大小不一,如果不做一下分类,然后把大的折小一些的话,直接绑起来,容易拎起来就散掉了,而且一些鞋盒子这种小件的纸盒会比较薄,冰箱这种大件的比较厚,厚的比较不容易变形,需要大力踩踏,而且扎的时候需要用体重压住捆实了之后那样子才是真的捆实的,不然待会又是松松垮垮容易滑出来散架,因为压住了捆好后,下来了之后箱子就会弹开了把绳子崩紧实,感觉又是掌握到生活小技巧了😃,我这里其实比较单调无聊,然后 LD 那可以说非常厉害了,一共理出来 11 把旧电扇,还有好多没用过全新的不锈钢脸盆大大小小的,感觉比店里在卖的还多,还有是有比较多小时候的东西,特别多小时候的衣服,其实这种对我来说最难了,可能需要读一下断舍离,蛮多东西都舍不得扔,但是其实是没啥用了,然后还占地方,这天应该算是比较轻松的一天了,上午主要是把收拾出来要的和不要的搬下楼,然后下午要去把纸板给卖掉。中午还是去小快餐店吃的,在住的家里理东西还有个好处就是中午吃完饭可以小憩一下,因为我个人是非常依赖午休的,不然下午完全没精神,而且心态也会比较烦躁,一方面是客观的的确比较疲惫,另一方面应该主观心理作用也有点影响,就像上班的时候也是觉得不午睡就会很难受,心理作用也有一点,不过总之能睡还是睡一会,真的没办法就心态好点,吃完午饭之后我们就推着小平板车去收废品的地方卖掉了上午我收拾捆起来的纸板,好像卖了一百多,都是直接过地磅了,不用一捆一捆地称,不过有个小插曲,那里另外一个大爷在倒他的三轮车的时候撞了我一下,还好车速慢,屁股上肉垫后,接下来就比较麻烦了,是LD 她们两姐妹从小到大的书,也要去卖掉,小平板车就载不下了,而且着实也不太好推,轮子不太平,导致推着很累,书有好多箱,本来是想去亲戚家借电动三轮车,因为不会开摩托的那种,摩托的那种 LD 邻居家就有,可是到了那发现那个也是很大,而且刹车是用脚踩的那种,俺爹不太放心,就说第二天周日他有空会帮忙去载了卖掉的,然后比较搞笑的来了,丈母娘看错了时间,以为已经快五点了,就让我们顺便在车里带点东西去在修的房子,放到那边三楼去,到了那还跟老丈人说已经这么迟了要赶紧去菜场买菜了,结果我们回来以后才发现看错了一个小时🤦‍♂️。
前面可以没提,前三周去的我们一般就周六去一天,然后周日因为要早点回杭州,而且可能想让我们周日能休息下,但是这周就因为周日的时候我爸要去帮忙载书,然后 LD 姐姐也会过来收拾东西,我们周日就又去整理收拾了,周日由于俺爹去的很早,我过去的时候书已经木有了,主要是去收拾东西了,把一些有用没用的继续整理,基本上三楼的就处理完毕了,舒了一大口气,毕竟让丈母娘一个人收拾实在是太累了,但是要扔掉的衣服比较棘手,附近知道的青蛙回收桶被推倒了,其他地方也不知道哪里有,我们就先载了一些东西去在修的房子那,然后去找青蛙桶,结果一个小区可以进,但是已经满了,另一个不让进,后来只能让 LD 姐姐带去她们小区扔了,塞了满满一车。因为要赶回杭州的车就没有等我爸一起回来,他还在那帮忙搞卫生间的墙缝。
虽然这两天不太热,活也不算很吃力,不过我这个体重和易出汗的体质,还是让短袖不知道湿透了多少次,灌了好多水和冰红茶(下午能提提神),回来周一早上称体重也比较喜人,差一点就达到阶段目标,可以想想去哪里吃之前想好的烤肉跟火锅了(估计吃完立马回到解放前)。

+ 寄生虫观后感 + /2020/03/01/%E5%AF%84%E7%94%9F%E8%99%AB%E8%A7%82%E5%90%8E%E6%84%9F/ + 寄生虫这部电影在获得奥斯卡之前就有关注了,豆瓣评分很高,一开始看到这个片名以为是像《铁线虫入侵》那种灾难片,后来看到男主,宋康昊,也是老面孔了,从高中时候在学校操场组织看的《汉江怪物》,有点二的感觉,后来在大学寝室电脑上重新看的时候,室友跟我说是韩国国宝级演员,真人不可貌相,感觉是个呆子的形象。

+

但是你说这不是个灾难片,而是个反映社会问题的,就业比较容易往这个方向猜,只是剧情会是怎么样的,一时也没啥头绪,后来不知道哪里看了下一个剧情透露,是一个穷人给富人做家教,然后把自己一家都带进富人家,如果是这样的话可能会把这个怎么带进去作为一个主线,不过事实告诉我,这没那么重要,从第一步朋友的介绍,就显得无比顺利,要去当家教了,作为一个穷成这样的人,瞬间转变成一个衣着得体,言行举止都没让富人家看出破绽的延世大学学生,这真的挺难让人理解,所谓江山易改,本性难移,还有就是这人也正好有那么好能力去辅导,并且诡异的是,多惠也是瞬间就喜欢上了男主,多惠跟将男主介绍给她做家教,也就是多惠原来的家教敏赫,应该也有不少的相处时间,这变了有点大了吧,当然这里也可能因为时长需要,如果说这一点是因为时长,那可能我所有的槽点都是因为这个吧,因为我理解的应该是把家里的人如何一步步地带进富人家,这应该是整个剧情的一个需要更多铺垫去克服这个矛盾点,有时候也想过如果我去当导演,是能拍出个啥,没这个机会,可能有也会是很扯淡的,当然这也不能阻拦我谈谈对这个点的一些看法,毕竟评价一台电冰箱不是说我必须得自己会制冷对吧,这大概是我觉得这个电影的第一个槽点,接下去接二连三的,就是我说的这个最核心的矛盾点,不知道谁说过,这种影视剧应该是源自于生活又高于生活,越是好的作品,越要接近生活,这样子才更能有感同身受。

+

接下去的点是金基宇介绍金基婷去给多颂当美术家教,这一步又是我理解的败笔吧,就怎么说呢,没什么铺垫,突然从一个社会底层的穷姑娘,转变成一个气场爆表,把富人家太太唬得一愣一愣的,如果说富太太是比较简单无脑的,那富人自己应该是比较有见识而且是做 IT 的,给自己儿子女儿做家教的,查查底细也很正常吧,但是啥都没有,然后呢,她又开始耍司机的心机了,真的是莫名其妙了,司机真的很惨,窈窕淑女君子好逑,而且这个操作也让我摸不着头脑,这是多腹黑并且有经验才会这么操作,脱内裤真的是让我看得一愣愣的,更看得我一愣一愣的,富人竟然也完全按着这个思路去想了,完全没有别的可能呢,甚至可以去查下行车记录仪或者怎样的,或者有没有毛发体液啥的去检验下,毕竟金基婷也乘坐过这辆车,但是最最让我不懂的还是脱内裤这个操作,究竟是什么样的人才会的呢,值得思考。

+

金基泽和忠淑的点也是比较奇怪,首先是金基泽,引起最后那个杀人事件的一个由头,大部分观点都是人为朴社长在之前跟老婆啪啪啪的时候说金基泽的身上有股乘地铁的人的味道,简而言之就是穷人的味道,还有去雯光丈夫身下拿钥匙是对金基泽和雯光丈夫身上的味道的鄙夷,可是这个原因真的站不住脚,即使是同样经济水平,如果身上有比较重的异味,背后讨论下,或者闻到了比较重的味道,有不适的表情和动作很正常吧,像雯光丈夫,在地下室里呆了那么久,身上有异味并且比较重太正常了,就跟在厕所呆久了不会觉得味道大,但是从没味道的地方一进有点味道的厕所就会觉得异样,略尴尬的理由;再说忠淑呢,感觉是太厉害了,能胜任这么一家有钱人的各种挑剔的饮食口味要求的保姆职位,也是让人看懵逼了,看到了不禁想到一个问题,这家人开头是那么地穷,不堪,突然转变成这么地像骗子家族,如果有这么好的骗人能力,应该不会到这种地步吧,如果真的是那么穷,没能力,没志气,又怎么会突然变成这么厉害呢,一家人各司其职,把富人家唬得团团转,而这个前提是,这些人的确能胜任这四个位置,这就是我非常不能理解的点。

+

然后说回这个标题,寄生虫,不知道是不是翻译过来不准确,如果真的是叫寄生虫的话,这个寄生虫智商未免也太低了,没有像新冠那样机制,致死率低一点,传染能力强一点,潜伏期也能传染,这个寄生虫第一次受到免疫系统的攻击就自爆了;还有呢,作为一个社会比较低层的打工者,乡下人,对这个审题也是不太审的清,是指这一家人是社会的寄生虫,不思进取,并且死的应该,富人是傻白甜,又有钱又善良,这是给有钱人洗地了还是啥,这个奥斯卡真不知道是怎么得的,总觉得奥斯卡,甚至低一点,豆瓣,得奖的,评分高的都是被一群“精英党”把持的,有黑人主角的,得分高;有同性恋的,得分高;结局惨的,得分高;看不懂的,得分高;就像肖申克的救赎,真不知道是哪里好了,最近看了关于明朝那些事的三杨,杨溥的经历应该比这个厉害吧,可是外国人看不懂,就像外国人不懂中国为什么有反分裂国家法,经历了鸦片战争,八国联军,抗日战争等等,其实跟外国对于黑人的权益的问题,因为有南北战争,所以极度重视这个问题,相应的中国也有自己的历史,请理解。

+

简而言之我对寄生虫的评分大概 5~6 分吧。

]]>
生活 - 运动 - 跑步 - 干活 + 影评 + 2020 生活 - 小技巧 - 运动 - 减肥 - 跑步 - 干活 + 影评 + 寄生虫
@@ -8939,199 +8920,59 @@ public: - 寄生虫观后感 - /2020/03/01/%E5%AF%84%E7%94%9F%E8%99%AB%E8%A7%82%E5%90%8E%E6%84%9F/ - 寄生虫这部电影在获得奥斯卡之前就有关注了,豆瓣评分很高,一开始看到这个片名以为是像《铁线虫入侵》那种灾难片,后来看到男主,宋康昊,也是老面孔了,从高中时候在学校操场组织看的《汉江怪物》,有点二的感觉,后来在大学寝室电脑上重新看的时候,室友跟我说是韩国国宝级演员,真人不可貌相,感觉是个呆子的形象。

-

但是你说这不是个灾难片,而是个反映社会问题的,就业比较容易往这个方向猜,只是剧情会是怎么样的,一时也没啥头绪,后来不知道哪里看了下一个剧情透露,是一个穷人给富人做家教,然后把自己一家都带进富人家,如果是这样的话可能会把这个怎么带进去作为一个主线,不过事实告诉我,这没那么重要,从第一步朋友的介绍,就显得无比顺利,要去当家教了,作为一个穷成这样的人,瞬间转变成一个衣着得体,言行举止都没让富人家看出破绽的延世大学学生,这真的挺难让人理解,所谓江山易改,本性难移,还有就是这人也正好有那么好能力去辅导,并且诡异的是,多惠也是瞬间就喜欢上了男主,多惠跟将男主介绍给她做家教,也就是多惠原来的家教敏赫,应该也有不少的相处时间,这变了有点大了吧,当然这里也可能因为时长需要,如果说这一点是因为时长,那可能我所有的槽点都是因为这个吧,因为我理解的应该是把家里的人如何一步步地带进富人家,这应该是整个剧情的一个需要更多铺垫去克服这个矛盾点,有时候也想过如果我去当导演,是能拍出个啥,没这个机会,可能有也会是很扯淡的,当然这也不能阻拦我谈谈对这个点的一些看法,毕竟评价一台电冰箱不是说我必须得自己会制冷对吧,这大概是我觉得这个电影的第一个槽点,接下去接二连三的,就是我说的这个最核心的矛盾点,不知道谁说过,这种影视剧应该是源自于生活又高于生活,越是好的作品,越要接近生活,这样子才更能有感同身受。

-

接下去的点是金基宇介绍金基婷去给多颂当美术家教,这一步又是我理解的败笔吧,就怎么说呢,没什么铺垫,突然从一个社会底层的穷姑娘,转变成一个气场爆表,把富人家太太唬得一愣一愣的,如果说富太太是比较简单无脑的,那富人自己应该是比较有见识而且是做 IT 的,给自己儿子女儿做家教的,查查底细也很正常吧,但是啥都没有,然后呢,她又开始耍司机的心机了,真的是莫名其妙了,司机真的很惨,窈窕淑女君子好逑,而且这个操作也让我摸不着头脑,这是多腹黑并且有经验才会这么操作,脱内裤真的是让我看得一愣愣的,更看得我一愣一愣的,富人竟然也完全按着这个思路去想了,完全没有别的可能呢,甚至可以去查下行车记录仪或者怎样的,或者有没有毛发体液啥的去检验下,毕竟金基婷也乘坐过这辆车,但是最最让我不懂的还是脱内裤这个操作,究竟是什么样的人才会的呢,值得思考。

-

金基泽和忠淑的点也是比较奇怪,首先是金基泽,引起最后那个杀人事件的一个由头,大部分观点都是人为朴社长在之前跟老婆啪啪啪的时候说金基泽的身上有股乘地铁的人的味道,简而言之就是穷人的味道,还有去雯光丈夫身下拿钥匙是对金基泽和雯光丈夫身上的味道的鄙夷,可是这个原因真的站不住脚,即使是同样经济水平,如果身上有比较重的异味,背后讨论下,或者闻到了比较重的味道,有不适的表情和动作很正常吧,像雯光丈夫,在地下室里呆了那么久,身上有异味并且比较重太正常了,就跟在厕所呆久了不会觉得味道大,但是从没味道的地方一进有点味道的厕所就会觉得异样,略尴尬的理由;再说忠淑呢,感觉是太厉害了,能胜任这么一家有钱人的各种挑剔的饮食口味要求的保姆职位,也是让人看懵逼了,看到了不禁想到一个问题,这家人开头是那么地穷,不堪,突然转变成这么地像骗子家族,如果有这么好的骗人能力,应该不会到这种地步吧,如果真的是那么穷,没能力,没志气,又怎么会突然变成这么厉害呢,一家人各司其职,把富人家唬得团团转,而这个前提是,这些人的确能胜任这四个位置,这就是我非常不能理解的点。

-

然后说回这个标题,寄生虫,不知道是不是翻译过来不准确,如果真的是叫寄生虫的话,这个寄生虫智商未免也太低了,没有像新冠那样机制,致死率低一点,传染能力强一点,潜伏期也能传染,这个寄生虫第一次受到免疫系统的攻击就自爆了;还有呢,作为一个社会比较低层的打工者,乡下人,对这个审题也是不太审的清,是指这一家人是社会的寄生虫,不思进取,并且死的应该,富人是傻白甜,又有钱又善良,这是给有钱人洗地了还是啥,这个奥斯卡真不知道是怎么得的,总觉得奥斯卡,甚至低一点,豆瓣,得奖的,评分高的都是被一群“精英党”把持的,有黑人主角的,得分高;有同性恋的,得分高;结局惨的,得分高;看不懂的,得分高;就像肖申克的救赎,真不知道是哪里好了,最近看了关于明朝那些事的三杨,杨溥的经历应该比这个厉害吧,可是外国人看不懂,就像外国人不懂中国为什么有反分裂国家法,经历了鸦片战争,八国联军,抗日战争等等,其实跟外国对于黑人的权益的问题,因为有南北战争,所以极度重视这个问题,相应的中国也有自己的历史,请理解。

-

简而言之我对寄生虫的评分大概 5~6 分吧。

-]]>
- - 生活 - 影评 - 2020 - - - 生活 - 影评 - 寄生虫 - -
- - 搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答 - /2022/01/16/%E6%90%AC%E8%BF%90%E4%B8%A4%E4%B8%AA-StackOverflow-%E4%B8%8A%E7%9A%84-Mysql-%E7%BC%96%E7%A0%81%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94/ - Mysql 字符编码和排序规则

这个一直是属于一知半解的状态,知道 utf8 跟 utf8mb4 的区别主要是能不能支持 emoji,但是具体后面配置的排序规则是用来干嘛,或者有什么区别,应该使用哪个,所以在 stackoverflow 上找了下,有两个比较不错的解答,就搬过来并且配合机翻做了点修改

-

原文

For those people still arriving at this question in 2020 or later, there are newer options that may be better than both of these. For example, utf8mb4_0900_ai_ci.

-

All these collations are for the UTF-8 character encoding. The differences are in how text is sorted and compared.

-

_unicode_ci and _general_ci are two different sets of rules for sorting and comparing text according to the way we expect. Newer versions of MySQL introduce new sets of rules, too, such as _0900_ai_ci for equivalent rules based on Unicode 9.0 - and with no equivalent _general_ci variant. People reading this now should probably use one of these newer collations instead of either _unicode_ci or _general_ci. The description of those older collations below is provided for interest only.

-

MySQL is currently transitioning away from an older, flawed UTF-8 implementation. For now, you need to use utf8mb4 instead of utf8 for the character encoding part, to ensure you are getting the fixed version. The flawed version remains for backward compatibility, though it is being deprecated.

-

Key differences

-

utf8mb4_unicode_ci is based on the official Unicode rules for universal sorting and comparison, which sorts accurately in a wide range of languages.

-

utf8mb4_general_ci is a simplified set of sorting rules which aims to do as well as it can while taking many short-cuts designed to improve speed. It does not follow the Unicode rules and will result in undesirable sorting or comparison in some situations, such as when using particular languages or characters.

-

On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.

-

Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci

-

utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.

-

As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:

-

For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).

-

Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.

-

In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.

-

What should you use?

-

There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.

-

In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.

-

There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.

-

One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.

-

What the parts mean

-

Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.

-

Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.

-

And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.

-

翻译

对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci

-

所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。

-

_unicode_ci_general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci_general_ci。下面对那些较旧的排序规则的描述仅供参考。

-

MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。

-

主要区别

-

utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。

-

utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。

-

在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。

-

utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势

-

utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。

-

就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:

-

例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。

-

一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。

-

在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。

-

你应该用什么?

-

几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。

-

过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。

-

有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。

-

我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。

-

其余各个部分是什么意思

-

首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。

-

接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。

-

最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。

-

utf8 和 utf8mb4 编码有什么区别

原文

UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.

-

So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.

-

This is what (a previous version of the same page at)the MySQL documentationhas to say about it:

-
-

The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:

-
    -
  • For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
  • -
  • For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
  • -
-
-

So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.

-

译文

UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。

-

所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较

-

这就是(同一页面的先前版本)MySQL 文档 不得不说的:

-
-

名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:

-
    -
  • 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
  • -
  • 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
  • -
-
-

因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅

-

实际使用中最常见的非 BMP Unicode 字符是什么?

-]]>
- - Mysql - - - mysql - 字符集 - 编码 - utf8 - utf8mb4 - utf8mb4_0900_ai_ci - utf8mb4_unicode_ci - utf8mb4_general_ci - -
- - 看完了扫黑风暴,聊聊感想 - /2021/10/24/%E7%9C%8B%E5%AE%8C%E4%BA%86%E6%89%AB%E9%BB%91%E9%A3%8E%E6%9A%B4-%E8%81%8A%E8%81%8A%E6%84%9F%E6%83%B3/ - 一直在想这篇怎么写,看了这部剧其实对我的一些观念是有影响的,应该是在 9 月份就看完了,到现在可能才会稍微平静一点,一开始是没有想看这部剧,因为同期有一部差不多同名的电影,被投诉了对湖南埋尸案家属伤害很大,我以为就是投诉的这部电视剧,后来同事跟我说不是,所以就想着看一下,但是没有马上看,因为一直不喜欢追这种比较纠结的剧,当时看人民的名义,就是后面等不了了直接看了小说,所以差不多是等到更完了才看的。

-

尝试保持一个比较冷静的状态来聊聊,在看的时候有一点感想就是如果要剧里的坏人排个名,因为明眼看都是孙兴是个穷凶极恶的坏人,干尽了坏事,而且可能是演员表演地好,让人真的恨的牙痒痒,但是更多地还是停留在那些剧情中的表现和他的表情,其实对应的真实案例有更多的,这里尽量不展开,有兴趣可以自行搜索关键字,所以其实我想排个名的话,孙兴的母亲应该是我心目中是造成这个结果的比较大占比的始作俑者,因为是方方面面的,包括对林汉的栽赃迫害,最后串起来是因为他看到了孙兴又出来了,就是那句老话,撒了一个谎以后就要用无数个谎来圆,贺芸为了孙兴,作了第一个恶以后就用了一系列的丧心病狂的操作来保护孙兴,而且这之后所做的事情一件比一件可怕,并且如果不是督导组各种想方设法地去破解谜题,这个事情还可以一直被通过各种操作瞒下去,而孙兴还可以继续地为虎作伥,当然其他的包括高明远以及后面的王政,当然是为了这个操作也提供的各种方式的帮助,甚至是主导了这些操作,但是这里贺芸还是在这个位子上能够通过权力做出非常关键的动作,包括栽赃林汉,并且搞掉了李成阳。其中还有一点是我对剧情设计的质疑,也是我前面提到过一点,因为里面孙兴好像是很爱他的母亲贺芸,似乎想表达的是孙兴作的恶是因为得不到母爱,并且个人感觉如果是一个比较敬爱自己母亲的儿子,似乎应该有所畏惧,对他的行为也会有所限制,不应该变成这样一个无恶不作的恶霸,这也是我一直以来的观点,很多人作恶太多可能是因为没有信仰,不管是信基督耶稣还是信道教佛教,总归有一些制约,当然不是说就绝对不会作恶,只是偏向于有所畏惧敬畏,除了某绿哈。

-

而对于其他的人感觉演技都不错,只是最后有一些虎头蛇尾吧,不知道是不是审核的原因,也不细说了怕被请喝茶,还有提一点就是麦佳的这个事情,她其实是里面很惨的一个人,把高明远当成最亲近的人,而其实真相令人感觉不寒而栗,杀父杀母的仇人,对于麦佳这个演员,一直觉得印象深刻,后来才想起来就是在爱情公寓里演被关谷救了要以身相遇的那个女孩,长相其实蛮令人印象深刻的,但好像也一直不温不火,不过也不能说演技很好吧,只是在这里演的任务真的是很可怜了,剧情设计里也应该是个很重要的串联人物,最终被高明远献给了大佬,这里扯开一点,好像有的观点说贺芸之前也是这样的,只是一种推测了。

-

看完这部剧其实有很多想说的,但是也为了不被请喝茶,尽量少说了,只想说珍爱生命,还是自己小心吧

+ 在老丈人家的小工记五 + /2020/10/18/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%BA%94/ + 终于回忆起来了,年纪大了写这种东西真的要立马写,不然很容易想不起来,那天应该是 9 月 12 日,也就是上周六,因为我爸也去了,而且娘亲(丈母娘,LD 这么叫,我也就随了她这么叫,当然是背后,当面就叫妈)也在那,早上一到那二爹就给我爸指挥了活,要挖一条院子的出水道,自己想出来的词,因为觉得下水道是竖的,在那稍稍帮了一会会忙,然后我还是比较惯例的跟着 LD 还有娘亲去住的家里,主要是老丈人可能也不太想让我干太累的活,因为上次已经差不多把三楼都整理干净了,然后就是二楼了,二楼说实话我也帮不上什么忙,主要是衣服被子什么的,正好是有张以前小孩子睡过的那种摇篮床,看上去虽然有一些破损,整体还是不错的,所以打算拿过去,我就负责把它拆掉了,比较简单的是只要拧螺丝就行了,但是其实是用了好多好多工具才搞定的,一开始只要螺丝刀就行了,但是因为年代久了,后面的螺帽也有点锈住或者本身就会串着会一起动,所以拿来了个扳手,大部分的其实都被这两个工具给搞定了,但是后期大概还剩下四分之一的时候,有一颗完全锈住,并且螺纹跟之前那些都不一样,但是这个已经是最大的螺丝刀了,也没办法换个大的了,所以又去找来个一字的,因为十字的不是也可以用一字的拧嘛,结果可能是我买的工具箱里的一字螺丝刀太新了,口子那很锋利,直接把螺丝花纹给划掉了,大的小的都划掉,然后真的变成凹进去一个圆柱体了,然后就想能不能隔一层布去拧,然而因为的确是已经变成圆柱体了,布也不太给力,不放弃的我又去找来了个老虎钳,妄图把划掉的螺丝用老虎钳钳住,另一端用扳手拧开螺帽,但是这个螺丝跟螺帽真的是生锈的太严重了,外加上钳不太牢,完全是两边一起转,实在是没办法了,在征得同意之后,直接掰断了,火死了,一颗螺丝折腾得比我拆一张床还久,那天因为早上去的也比较晚了,然后就快吃午饭了,正好想着带一点东西过去,就把一些脸盆,泡脚桶啥的拿过去了,先是去吃了饭,还是在那家快餐店,菜的口味还是依然不错,就是人比较多,我爸旁边都是素菜,都没怎么吃远一点的荤菜,下次要早点去,把荤菜放我爸旁边😄(PS:他们家饭还是依然尴尬,需要等),吃完就开到在修的房子那把东西拿了出来,我爸已经动作很快的打了一小半的地沟了,说实话那玩意真的是很重,我之前把它从三楼拿下来,就觉得这个太重了,这次还要用起来,感觉我的手会分分钟废掉,不过一开始我还是跟着LD去了住的家里,惯例睡了午觉,那天睡得比较踏实,竟然睡了一个小时,醒了想了下,其实LD她们收拾也用不上我(没啥力气活),我还是去帮我爸他们,跟LD说了下就去了在修的老房子那,两位老爹在一起钻地,看着就很累,我连忙上去想换一会他们,因为刚好是钻到混凝土地线,特别难,力道不够就会滑开,用蛮力就是钻进去拔不出来,原理是因为本身浇的时候就是很紧实的,需要边钻边动,那家伙实在是太重了,真的是汗如雨下,基本是三个人轮流来,我是个添乱的,经常卡住,然后把地线,其实就是一条混凝土横梁,里面还有14跟18的钢筋,需要割断,这个割断也是很有技巧,钢筋本身在里面是受到挤压的,直接用切割的,到快断掉的时候就会崩一下,非常危险,还是老丈人比较有经验,要留一点点,然后直接用榔头敲断就好了,本来以为这个是最难的了,结果下面是一块非常大的青基石,而且也是石头跟石头挤一块,边上一点点打钻有点杯水车薪,后来是用那种螺旋的钻,钻四个洞,相对位置大概是个长方形,这样子把中间这个长方形钻出来就比较容易地能拿出来了,后面的也容易搞出来了,后面的其实难度不是特别大了,主要是地沟打好之后得看看高低是不是符合要求的,不能本来是往外排水的反而外面高,这个怎么看就又很有技巧了,一般在地上的只要侧着看一下就好了,考究点就用下水平尺,但是在地下的,不用水平尺,其实可以借助于地沟里正要放进去的水管,放点水进去,看水往哪流就行了,铺好水管后,就剩填埋的活了,不是太麻烦了,那天真的是累到了,打那个混凝土的时候我真的是把我整个人压上去了,不过也挺爽的,有点把平时无处发泄的蛮力发泄出去了。

]]>
生活 + 运动 + 跑步 + 干活 生活 - 影评 + 小技巧 + 运动 + 减肥 + 跑步 + 干活
- 给小电驴上牌 - /2022/03/20/%E7%BB%99%E5%B0%8F%E7%94%B5%E9%A9%B4%E4%B8%8A%E7%89%8C/ - 三八节活动的时候下决心买了个小电驴,主要是上下班路上现在通勤条件越来越恶劣了,之前都是觉得坐公交就行了,实际路程就比较短,但是现在或者说大概是年前那两个月差不多就开始了,基本是堵一路,个人感觉是天目山路那边在修地铁,而且蚂蚁的几个空间都在那,上班的时间点都差不多,前一个修地铁感觉挺久了,机动车保有量也越来越多,总体是古墩路就越来越堵,还有个原因就是早上上班的点共享单车都被骑走了,有时候整整走一路都没一辆,有时候孤零零地有一辆基本都是破的;走路其实也是一种选择,但是因为要赶着上班,走得太慢就要很久,可能要 45 分钟这样,走得比较快就一身汗挺难受的。所以考虑自行车和电动车,这里还有一点就是不管是乘公交还是骑共享单车,其实都要从楼下走出去蛮远,公司回来也是,也就是这种通勤方式在准备阶段就花了比较多时间,比如总的从下班到到家的时间是半小时,可能在骑共享单车和公交车上的时间都不到十分钟,就比较难受。觉得这种比例太浪费时间,如果能有这种比较点对点的方式,估计能省时省力不少,前面说的骑共享单车的方式其实在之前是比较可行的,但是后来越来越少车,基本都是每周的前几天,周一到周三都是没有车,走路到公司再冷的天都是走出一身的汗,下雨天就更难受,本来下雨天应该是优先选择坐公交,但是一般下雨天堵车会更严重,而且车子到我上车的那个站,下雨天就挤得不行,总体说下来感觉事情都不打,但是几年下来,还是会挺不爽的。

-

电驴看的比较草率,主要是考虑续航,然后锂电池外加 48v 和 24AH,这样一般来讲还是价格比较高的,只是原来没预料到这个限速,以为现在的车子都比较快,但是现在的新国标车子都是 25km/h 的限速,然后 15km/h 都是会要提醒,虽然说有一些特殊的解除限速的方法,但是解了也就 35km/h ,差距不是特别大,而且现在的车子都是比较小,也不太能载东西,特别是上下班路程也不远的情况下,其实不是那么需要速度,就像我朋友说的,可能骑车的时间还不如等红绿灯多,所以就还好,也不打算解除限速,只是品牌上也仔细看,后来选了绿源,目前大部分还是雅迪,爱玛,台羚,绿源,小牛等,路上看的话还是雅迪比较多,不过价格也比较贵一点,还有就是小牛了,是比较新兴的品牌,手机 App 什么的做得比较好,而且也比较贵,最后以相对比较便宜的价格买了个锂电 48V24AH 的小车子,后来发现还是有点不方便的点就是没有比较大的筐,也不好装,这样就是下雨天雨衣什么的比较不方便放。

-

聊回来主题上牌这个事情,这个事情也是颇费心力,提车的时候店里的让我跟他早上一起去,但是因为不确定时间,也比较远就没跟着去,因为我是线上买的,线下自提,线下的店可能没啥利润可以拿,就不肯帮忙代上牌,朋友说在线下店里买是可以代上的,自己上牌过程也比较曲折,一开始是头盔没到,然后是等开发票,主要的东西就是需要骑着车子去车管所,不能只自己去,然后需要预约,附近比较近的都是提前一周就预约完了号了,要提前在支付宝上进行预约,比较空的就是店里推荐的景区大队,但是随之而来就是比较蛋疼的,这个景区大队太远了,看下骑车距离有十几公里,所以就有点拖延症,但是总归要上的,不然一直不能开是白买了,上牌的材料主要是车辆合格证,发票,然后车子上的浙品码,在车架上和电池上,然后车架号什么的都要跟合格证上完全对应,整体车子要跟合格证上一毛一样,如果有额外的反光镜,后面副座都需要拆掉,脚踏板要装上,到了那其实还比较顺利,就是十几公里外加那天比较冷,吹得头疼。

+ 在老丈人家的小工记四 + /2020/09/26/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E5%9B%9B/ + 小工记四

第四周去的时候让我们去了现在在住的房子里,去三楼整理东西了,蛮多的东西需要收拾整理,有些需要丢一下,以前往往是把不太要用的东西就放三楼了,但是后面就不会再去收拾整理,LD 跟丈母娘负责收拾,我不太知道哪些还要的,哪些不要了,而且本来也不擅长这种收拾🤦‍♂️,然后就变成她们收拾出来废纸箱,我负责拆掉,压平,这时候终于觉得体重还算是有点作用,总体来说这个事情我其实也不擅长,不擅长的主要是捆起来,可能我总是小题大做,因为纸箱大小不一,如果不做一下分类,然后把大的折小一些的话,直接绑起来,容易拎起来就散掉了,而且一些鞋盒子这种小件的纸盒会比较薄,冰箱这种大件的比较厚,厚的比较不容易变形,需要大力踩踏,而且扎的时候需要用体重压住捆实了之后那样子才是真的捆实的,不然待会又是松松垮垮容易滑出来散架,因为压住了捆好后,下来了之后箱子就会弹开了把绳子崩紧实,感觉又是掌握到生活小技巧了😃,我这里其实比较单调无聊,然后 LD 那可以说非常厉害了,一共理出来 11 把旧电扇,还有好多没用过全新的不锈钢脸盆大大小小的,感觉比店里在卖的还多,还有是有比较多小时候的东西,特别多小时候的衣服,其实这种对我来说最难了,可能需要读一下断舍离,蛮多东西都舍不得扔,但是其实是没啥用了,然后还占地方,这天应该算是比较轻松的一天了,上午主要是把收拾出来要的和不要的搬下楼,然后下午要去把纸板给卖掉。中午还是去小快餐店吃的,在住的家里理东西还有个好处就是中午吃完饭可以小憩一下,因为我个人是非常依赖午休的,不然下午完全没精神,而且心态也会比较烦躁,一方面是客观的的确比较疲惫,另一方面应该主观心理作用也有点影响,就像上班的时候也是觉得不午睡就会很难受,心理作用也有一点,不过总之能睡还是睡一会,真的没办法就心态好点,吃完午饭之后我们就推着小平板车去收废品的地方卖掉了上午我收拾捆起来的纸板,好像卖了一百多,都是直接过地磅了,不用一捆一捆地称,不过有个小插曲,那里另外一个大爷在倒他的三轮车的时候撞了我一下,还好车速慢,屁股上肉垫后,接下来就比较麻烦了,是LD 她们两姐妹从小到大的书,也要去卖掉,小平板车就载不下了,而且着实也不太好推,轮子不太平,导致推着很累,书有好多箱,本来是想去亲戚家借电动三轮车,因为不会开摩托的那种,摩托的那种 LD 邻居家就有,可是到了那发现那个也是很大,而且刹车是用脚踩的那种,俺爹不太放心,就说第二天周日他有空会帮忙去载了卖掉的,然后比较搞笑的来了,丈母娘看错了时间,以为已经快五点了,就让我们顺便在车里带点东西去在修的房子,放到那边三楼去,到了那还跟老丈人说已经这么迟了要赶紧去菜场买菜了,结果我们回来以后才发现看错了一个小时🤦‍♂️。
前面可以没提,前三周去的我们一般就周六去一天,然后周日因为要早点回杭州,而且可能想让我们周日能休息下,但是这周就因为周日的时候我爸要去帮忙载书,然后 LD 姐姐也会过来收拾东西,我们周日就又去整理收拾了,周日由于俺爹去的很早,我过去的时候书已经木有了,主要是去收拾东西了,把一些有用没用的继续整理,基本上三楼的就处理完毕了,舒了一大口气,毕竟让丈母娘一个人收拾实在是太累了,但是要扔掉的衣服比较棘手,附近知道的青蛙回收桶被推倒了,其他地方也不知道哪里有,我们就先载了一些东西去在修的房子那,然后去找青蛙桶,结果一个小区可以进,但是已经满了,另一个不让进,后来只能让 LD 姐姐带去她们小区扔了,塞了满满一车。因为要赶回杭州的车就没有等我爸一起回来,他还在那帮忙搞卫生间的墙缝。
虽然这两天不太热,活也不算很吃力,不过我这个体重和易出汗的体质,还是让短袖不知道湿透了多少次,灌了好多水和冰红茶(下午能提提神),回来周一早上称体重也比较喜人,差一点就达到阶段目标,可以想想去哪里吃之前想好的烤肉跟火锅了(估计吃完立马回到解放前)。

]]>
生活 + 运动 + 跑步 + 干活 生活 + 小技巧 + 运动 + 减肥 + 跑步 + 干活
- 聊一下 RocketMQ 的消息存储之 MMAP - /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ - 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

-
read(file, tmp_buf, len);
-write(socket, tmp_buf, len);
- - - -

vms95Z

-

如上面的图显示的,要在用户态跟内核态进行切换,数据还需要在内核缓冲跟用户缓冲之间拷贝多次,

-
-
    -
  1. 第一步是调用 read,需要在用户态切换成内核态,DMA模块从磁盘中读取文件,并存储在内核缓冲区,相当于是第一次复制
  2. -
  3. 数据从内核缓冲区被拷贝到用户缓冲区,read 调用返回,伴随着内核态又切换成用户态,完成了第二次复制
  4. -
  5. 然后是write 写入,这里也会伴随着用户态跟内核态的切换,数据从用户缓冲区被复制到内核空间缓冲区,完成了第三次复制,这次有点不一样的是数据不是在内核缓冲区了,会复制到 socket buffer 中。
  6. -
  7. write 系统调用返回,又切换回了用户态,然后数据由 DMA 拷贝到协议引擎。
  8. -
-
-

如此就能看出其实默认的读写操作代价是非常大的,而在 rocketmq 等高性能中间件中都有使用的零拷贝技术,其中 rocketmq 使用的是 mmap

-

mmap

mmap基于 OS 的 mmap 的内存映射技术,通过MMU 映射文件,将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作,使随机读写文件和读写内存相似的速度。

-
-

mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

-
-
tmp_buf = mmap(file, len);
-write(socket, tmp_buf, len);
- -

I68mFx

-
-

第一步:mmap系统调用使得文件内容被DMA引擎复制到内核缓冲区。然后该缓冲区与用户进程共享,在内核和用户内存空间之间不进行任何拷贝。

-

第二步:写系统调用使得内核将数据从原来的内核缓冲区复制到与套接字相关的内核缓冲区。

-

第三步:第三次拷贝发生在DMA引擎将数据从内核套接字缓冲区传递给协议引擎时。

-

通过使用mmap而不是read,我们将内核需要拷贝的数据量减少了一半。当大量的数据被传输时,这将有很好的效果。然而,这种改进并不是没有代价的;在使用mmap+write方法时,有一些隐藏的陷阱。例如当你对一个文件进行内存映射,然后在另一个进程截断同一文件时调用写。你的写系统调用将被总线错误信号SIGBUS打断,因为你执行了一个错误的内存访问。该信号的默认行为是杀死进程并dumpcore–这对网络服务器来说不是最理想的操作。

-

有两种方法可以解决这个问题。

-

第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用返回。通过这样做,写系统调用会返回它在被打断之前所写的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,一个治标不治本的解决方案。因为SIGBUS预示着进程出了严重的问题,所以不鼓励使用这种解决方案。

-

第二个解决方案涉及内核的文件租赁(在Windows中称为 “机会锁”)。这是解决这个问题的正确方法。通过在文件描述符上使用租赁,你与内核在一个特定的文件上达成租约。然后你可以向内核请求一个读/写租约。当另一个进程试图截断你正在传输的文件时,内核会向你发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉你内核即将终止你对该文件的写或读租约。在你的程序访问一个无效的地址和被SIGBUS信号杀死之前,你的写调用会被打断了。写入调用的返回值是中断前写入的字节数,errno将被设置为成功。下面是一些示例代码,显示了如何从内核中获得租约。

-
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
-    perror("kernel lease set signal");
-    return -1;
-}
-/* l_type can be F_RDLCK F_WRLCK */
-if(fcntl(fd, F_SETLEASE, l_type)){
-    perror("kernel lease set type");
-    return -1;
-}
+ 看完了扫黑风暴,聊聊感想 + /2021/10/24/%E7%9C%8B%E5%AE%8C%E4%BA%86%E6%89%AB%E9%BB%91%E9%A3%8E%E6%9A%B4-%E8%81%8A%E8%81%8A%E6%84%9F%E6%83%B3/ + 一直在想这篇怎么写,看了这部剧其实对我的一些观念是有影响的,应该是在 9 月份就看完了,到现在可能才会稍微平静一点,一开始是没有想看这部剧,因为同期有一部差不多同名的电影,被投诉了对湖南埋尸案家属伤害很大,我以为就是投诉的这部电视剧,后来同事跟我说不是,所以就想着看一下,但是没有马上看,因为一直不喜欢追这种比较纠结的剧,当时看人民的名义,就是后面等不了了直接看了小说,所以差不多是等到更完了才看的。

+

尝试保持一个比较冷静的状态来聊聊,在看的时候有一点感想就是如果要剧里的坏人排个名,因为明眼看都是孙兴是个穷凶极恶的坏人,干尽了坏事,而且可能是演员表演地好,让人真的恨的牙痒痒,但是更多地还是停留在那些剧情中的表现和他的表情,其实对应的真实案例有更多的,这里尽量不展开,有兴趣可以自行搜索关键字,所以其实我想排个名的话,孙兴的母亲应该是我心目中是造成这个结果的比较大占比的始作俑者,因为是方方面面的,包括对林汉的栽赃迫害,最后串起来是因为他看到了孙兴又出来了,就是那句老话,撒了一个谎以后就要用无数个谎来圆,贺芸为了孙兴,作了第一个恶以后就用了一系列的丧心病狂的操作来保护孙兴,而且这之后所做的事情一件比一件可怕,并且如果不是督导组各种想方设法地去破解谜题,这个事情还可以一直被通过各种操作瞒下去,而孙兴还可以继续地为虎作伥,当然其他的包括高明远以及后面的王政,当然是为了这个操作也提供的各种方式的帮助,甚至是主导了这些操作,但是这里贺芸还是在这个位子上能够通过权力做出非常关键的动作,包括栽赃林汉,并且搞掉了李成阳。其中还有一点是我对剧情设计的质疑,也是我前面提到过一点,因为里面孙兴好像是很爱他的母亲贺芸,似乎想表达的是孙兴作的恶是因为得不到母爱,并且个人感觉如果是一个比较敬爱自己母亲的儿子,似乎应该有所畏惧,对他的行为也会有所限制,不应该变成这样一个无恶不作的恶霸,这也是我一直以来的观点,很多人作恶太多可能是因为没有信仰,不管是信基督耶稣还是信道教佛教,总归有一些制约,当然不是说就绝对不会作恶,只是偏向于有所畏惧敬畏,除了某绿哈。

+

而对于其他的人感觉演技都不错,只是最后有一些虎头蛇尾吧,不知道是不是审核的原因,也不细说了怕被请喝茶,还有提一点就是麦佳的这个事情,她其实是里面很惨的一个人,把高明远当成最亲近的人,而其实真相令人感觉不寒而栗,杀父杀母的仇人,对于麦佳这个演员,一直觉得印象深刻,后来才想起来就是在爱情公寓里演被关谷救了要以身相遇的那个女孩,长相其实蛮令人印象深刻的,但好像也一直不温不火,不过也不能说演技很好吧,只是在这里演的任务真的是很可怜了,剧情设计里也应该是个很重要的串联人物,最终被高明远献给了大佬,这里扯开一点,好像有的观点说贺芸之前也是这样的,只是一种推测了。

+

看完这部剧其实有很多想说的,但是也为了不被请喝茶,尽量少说了,只想说珍爱生命,还是自己小心吧

]]>
- MQ - RocketMQ - 消息队列 + 生活 - MQ - 消息队列 - RocketMQ + 生活 + 影评
@@ -9147,779 +8988,613 @@ public: - 聊一下 RocketMQ 的 DefaultMQPushConsumer 源码 - /2020/06/26/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-Consumer/ - 首先看下官方的小 demo

-
public static void main(String[] args) throws InterruptedException, MQClientException {
-
-        /*
-         * Instantiate with specified consumer group name.
-         * 首先是new 一个对象出来,然后指定 Consumer 的 Group
-         * 同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
-         */
-        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
-
-        /*
-         * Specify name server addresses.
-         * <p/>
-         * 这里可以通知指定环境变量或者设置对象参数的形式指定名字空间服务的地址
-         *
-         * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
-         * <pre>
-         * {@code
-         * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
-         * }
-         * </pre>
-         */
-
-        /*
-         * Specify where to start in case the specified consumer group is a brand new one.
-         * 指定消费起始点
-         */
-        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
-
-        /*
-         * Subscribe one more more topics to consume.
-         * 指定订阅的 topic 跟 tag,注意后面的是个表达式,可以以 tag1 || tag2 || tag3 传入
-         */
-        consumer.subscribe("TopicTest", "*");
-
-        /*
-         *  Register callback to execute on arrival of messages fetched from brokers.
-         *  注册具体获得消息后的处理方法
-         */
-        consumer.registerMessageListener(new MessageListenerConcurrently() {
+    聊一下 RocketMQ 的 NameServer 源码
+    /2020/07/05/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-NameServer-%E6%BA%90%E7%A0%81/
+    前面介绍了,nameserver相当于dubbo的注册中心,用与管理broker,broker会在启动的时候注册到nameserver,并且会发送心跳给namaserver,nameserver负责保存活跃的broker,包括master和slave,同时保存topic和topic下的队列,以及filter列表,然后为producer和consumer的请求提供服务。

+

启动过程

public static void main(String[] args) {
+        main0(args);
+    }
 
-            @Override
-            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
-                ConsumeConcurrentlyContext context) {
-                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
-                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
-            }
-        });
+    public static NamesrvController main0(String[] args) {
 
-        /*
-         *  Launch the consumer instance.
-         * 启动消费者
-         */
-        consumer.start();
+        try {
+            NamesrvController controller = createNamesrvController(args);
+            start(controller);
+            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
+            log.info(tip);
+            System.out.printf("%s%n", tip);
+            return controller;
+        } catch (Throwable e) {
+            e.printStackTrace();
+            System.exit(-1);
+        }
 
-        System.out.printf("Consumer Started.%n");
-    }
+ return null; + }
-

然后就是看看 start 的过程了

-
/**
-     * This method gets internal infrastructure readily to serve. Instances must call this method after configuration.
-     *
-     * @throws MQClientException if there is any client error.
-     */
-    @Override
-    public void start() throws MQClientException {
-        setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
-        this.defaultMQPushConsumerImpl.start();
-        if (null != traceDispatcher) {
-            try {
-                traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
-            } catch (MQClientException e) {
-                log.warn("trace dispatcher start failed ", e);
-            }
-        }
-    }
-

具体的逻辑在this.defaultMQPushConsumerImpl.start(),这个 defaultMQPushConsumerImpl 就是

-
/**
-     * Internal implementation. Most of the functions herein are delegated to it.
-     */
-    protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
+

入口的代码时这样子,其实主要的逻辑在createNamesrvController和start方法,来看下这两个的实现

+
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
+        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
+        //PackageConflictDetect.detectFastjson();
 
-
public synchronized void start() throws MQClientException {
-        switch (this.serviceState) {
-            case CREATE_JUST:
-                log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
-                    this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
-                // 这里比较巧妙,相当于想设立了个屏障,防止并发启动,不过这里并不是悲观锁,也不算个严格的乐观锁
-                this.serviceState = ServiceState.START_FAILED;
+        Options options = ServerUtil.buildCommandlineOptions(new Options());
+        commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
+        if (null == commandLine) {
+            System.exit(-1);
+            return null;
+        }
 
-                this.checkConfig();
+        final NamesrvConfig namesrvConfig = new NamesrvConfig();
+        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
+        nettyServerConfig.setListenPort(9876);
+        if (commandLine.hasOption('c')) {
+            String file = commandLine.getOptionValue('c');
+            if (file != null) {
+                InputStream in = new BufferedInputStream(new FileInputStream(file));
+                properties = new Properties();
+                properties.load(in);
+                MixAll.properties2Object(properties, namesrvConfig);
+                MixAll.properties2Object(properties, nettyServerConfig);
 
-                this.copySubscription();
+                namesrvConfig.setConfigStorePath(file);
 
-                if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
-                    this.defaultMQPushConsumer.changeInstanceNameToPID();
-                }
+                System.out.printf("load config properties file OK, %s%n", file);
+                in.close();
+            }
+        }
 
-                // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成
-                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
+        if (commandLine.hasOption('p')) {
+            InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
+            MixAll.printObjectProperties(console, namesrvConfig);
+            MixAll.printObjectProperties(console, nettyServerConfig);
+            System.exit(0);
+        }
 
-                // 这个 rebalanceImpl 主要负责决定,当前的consumer应该从哪些Queue中消费消息;
-                this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
-                this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
-                this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
-                this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
+        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
 
-                // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑
-                this.pullAPIWrapper = new PullAPIWrapper(
-                    mQClientFactory,
-                    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
-                this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
+        if (null == namesrvConfig.getRocketmqHome()) {
+            System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
+            System.exit(-2);
+        }
 
-                if (this.defaultMQPushConsumer.getOffsetStore() != null) {
-                    this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
-                } else {
-                    switch (this.defaultMQPushConsumer.getMessageModel()) {
-                        case BROADCASTING:
-                            this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
-                            break;
-                        case CLUSTERING:
-                            this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
-                            break;
-                        default:
-                            break;
-                    }
-                    this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
-                }
-                // offsetStore 维护当前consumer的消费记录(offset);有两种实现,Local和Rmote,Local存储在本地磁盘上,适用于BROADCASTING广播消费模式;而Remote则将消费进度存储在Broker上,适用于CLUSTERING集群消费模式;
-                this.offsetStore.load();
+        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+        JoranConfigurator configurator = new JoranConfigurator();
+        configurator.setContext(lc);
+        lc.reset();
+        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
 
-                if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
-                    this.consumeOrderly = true;
-                    this.consumeMessageService =
-                        new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
-                } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
-                    this.consumeOrderly = false;
-                    this.consumeMessageService =
-                        new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
-                }
+        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
 
-                // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
-                this.consumeMessageService.start();
+        MixAll.printObjectProperties(log, namesrvConfig);
+        MixAll.printObjectProperties(log, nettyServerConfig);
 
-                boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
-                if (!registerOK) {
-                    this.serviceState = ServiceState.CREATE_JUST;
-                    this.consumeMessageService.shutdown();
-                    throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
-                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
-                        null);
-                }
+        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
 
-                mQClientFactory.start();
-                log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
-                this.serviceState = ServiceState.RUNNING;
-                break;
-            case RUNNING:
-            case START_FAILED:
-            case SHUTDOWN_ALREADY:
-                throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
-                    + this.serviceState
-                    + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
-                    null);
-            default:
-                break;
-        }
+        // remember all configs to prevent discard
+        controller.getConfiguration().registerConfig(properties);
 
-        this.updateTopicSubscribeInfoWhenSubscriptionChanged();
-        this.mQClientFactory.checkClientInBroker();
-        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
-        this.mQClientFactory.rebalanceImmediately();
-    }
-

然后我们往下看主要的目光聚焦mQClientFactory.start()

-
public void start() throws MQClientException {
+        return controller;
+    }
- synchronized (this) { - switch (this.serviceState) { - case CREATE_JUST: - this.serviceState = ServiceState.START_FAILED; - // If not specified,looking address from name server - if (null == this.clientConfig.getNamesrvAddr()) { - this.mQClientAPIImpl.fetchNameServerAddr(); - } - // Start request-response channel - // 这里主要是初始化了个网络客户端 - this.mQClientAPIImpl.start(); - // Start various schedule tasks - // 定时任务 - this.startScheduledTask(); - // Start pull service - // 这里重点说下 - this.pullMessageService.start(); - // Start rebalance service - this.rebalanceService.start(); - // Start push service - this.defaultMQProducer.getDefaultMQProducerImpl().start(false); - log.info("the client factory [{}] start OK", this.clientId); - this.serviceState = ServiceState.RUNNING; - break; - case START_FAILED: - throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null); - default: - break; - } - } - }
-

我们来看下这个 pullMessageService,org.apache.rocketmq.client.impl.consumer.PullMessageService,

实现了 runnable 接口,
然后可以看到 run 方法

-
public void run() {
-        log.info(this.getServiceName() + " service started");
+

这个方法里其实主要是读取一些配置啥的,不是很复杂,

+
public static NamesrvController start(final NamesrvController controller) throws Exception {
 
-        while (!this.isStopped()) {
-            try {
-                PullRequest pullRequest = this.pullRequestQueue.take();
-                this.pullMessage(pullRequest);
-            } catch (InterruptedException ignored) {
-            } catch (Exception e) {
-                log.error("Pull Message Service Run Method exception", e);
-            }
-        }
+        if (null == controller) {
+            throw new IllegalArgumentException("NamesrvController is null");
+        }
 
-        log.info(this.getServiceName() + " service end");
-    }
-

接着在看 pullMessage 方法

-
private void pullMessage(final PullRequest pullRequest) {
-        final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
-        if (consumer != null) {
-            DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
-            impl.pullMessage(pullRequest);
-        } else {
-            log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
-        }
-    }
-

实际上调用了这个方法,这个方法很长,我在代码里注释下下每一段的功能

-
public void pullMessage(final PullRequest pullRequest) {
-        final ProcessQueue processQueue = pullRequest.getProcessQueue();
-        // 这里开始就是检查状态,确定是否往下执行
-        if (processQueue.isDropped()) {
-            log.info("the pull request[{}] is dropped.", pullRequest.toString());
-            return;
-        }
+        boolean initResult = controller.initialize();
+        if (!initResult) {
+            controller.shutdown();
+            System.exit(-3);
+        }
 
-        pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
+        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                controller.shutdown();
+                return null;
+            }
+        }));
 
-        try {
-            this.makeSureStateOK();
-        } catch (MQClientException e) {
-            log.warn("pullMessage exception, consumer state not ok", e);
-            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
-            return;
-        }
+        controller.start();
 
-        if (this.isPause()) {
-            log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
-            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
-            return;
-        }
+        return controller;
+    }
- // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制 - long cachedMessageCount = processQueue.getMsgCount().get(); - long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024); +

这个start里主要关注initialize方法,后面就是一个停机的hook,来看下initialize方法

+
public boolean initialize() {
 
-        if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
-            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
-            if ((queueFlowControlTimes++ % 1000) == 0) {
-                log.warn(
-                    "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
-                    this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
-            }
-            return;
-        }
+        this.kvConfigManager.load();
 
-        if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
-            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
-            if ((queueFlowControlTimes++ % 1000) == 0) {
-                log.warn(
-                    "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
-                    this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
-            }
-            return;
-        }
+        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
 
-        // 若不是顺序消费(即DefaultMQPushConsumerImpl.consumeOrderly等于false),则检查ProcessQueue对象的msgTreeMap:TreeMap<Long,MessageExt>变量的第一个key值与最后一个key值之间的差额,该key值表示查询的队列偏移量queueoffset;若差额大于阈值(由DefaultMQPushConsumer. consumeConcurrentlyMaxSpan指定,默认是2000),则调用PullMessageService.executePullRequestLater方法,在50毫秒之后重新将该PullRequest请求放入PullMessageService.pullRequestQueue队列中;并跳出该方法;这里的意思主要就是消息有堆积了,等会再来拉取
-        if (!this.consumeOrderly) {
-            if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
-                this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
-                if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
-                    log.warn(
-                        "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
-                        processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
-                        pullRequest, queueMaxSpanFlowControlTimes);
-                }
-                return;
-            }
-        } else {
-            if (processQueue.isLocked()) {
-                if (!pullRequest.isLockedFirst()) {
-                    final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
-                    boolean brokerBusy = offset < pullRequest.getNextOffset();
-                    log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
-                        pullRequest, offset, brokerBusy);
-                    if (brokerBusy) {
-                        log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
-                            pullRequest, offset);
-                    }
+        this.remotingExecutor =
+            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
 
-                    pullRequest.setLockedFirst(true);
-                    pullRequest.setNextOffset(offset);
-                }
-            } else {
-                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
-                log.info("pull message later because not locked in broker, {}", pullRequest);
-                return;
-            }
-        }
+        this.registerProcessor();
+
+        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+            @Override
+            public void run() {
+                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
+            }
+        }, 5, 10, TimeUnit.SECONDS);
+
+        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+            @Override
+            public void run() {
+                NamesrvController.this.kvConfigManager.printAllPeriodically();
+            }
+        }, 1, 10, TimeUnit.MINUTES);
+
+        if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
+            // Register a listener to reload SslContext
+            try {
+                fileWatchService = new FileWatchService(
+                    new String[] {
+                        TlsSystemConfig.tlsServerCertPath,
+                        TlsSystemConfig.tlsServerKeyPath,
+                        TlsSystemConfig.tlsServerTrustCertPath
+                    },
+                    new FileWatchService.Listener() {
+                        boolean certChanged, keyChanged = false;
+                        @Override
+                        public void onChanged(String path) {
+                            if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
+                                log.info("The trust certificate changed, reload the ssl context");
+                                reloadServerSslContext();
+                            }
+                            if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
+                                certChanged = true;
+                            }
+                            if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
+                                keyChanged = true;
+                            }
+                            if (certChanged && keyChanged) {
+                                log.info("The certificate and private key changed, reload the ssl context");
+                                certChanged = keyChanged = false;
+                                reloadServerSslContext();
+                            }
+                        }
+                        private void reloadServerSslContext() {
+                            ((NettyRemotingServer) remotingServer).loadSslContext();
+                        }
+                    });
+            } catch (Exception e) {
+                log.warn("FileWatchService created error, can't load the certificate dynamically");
+            }
+        }
 
-        // 以PullRequest.messageQueue对象的topic值为参数从RebalanceImpl.subscriptionInner: ConcurrentHashMap, SubscriptionData>中获取对应的SubscriptionData对象,若该对象为null,考虑到并发的关系,调用executePullRequestLater方法,稍后重试;并跳出该方法;
-        final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
-        if (null == subscriptionData) {
-            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
-            log.warn("find the consumer's subscription failed, {}", pullRequest);
-            return;
-        }
+        return true;
+    }
- final long beginTimestamp = System.currentTimeMillis(); +

这里的kvConfigManager主要是来加载NameServer的配置参数,存到org.apache.rocketmq.namesrv.kvconfig.KVConfigManager#configTable中,然后是以BrokerHousekeepingService对象为参数初始化NettyRemotingServer对象,BrokerHousekeepingService对象作为该Netty连接中Socket链接的监听器(ChannelEventListener);监听与Broker建立的渠道的状态(空闲、关闭、异常三个状态),并调用BrokerHousekeepingService的相应onChannel方法。其中渠道的空闲、关闭、异常状态均调用RouteInfoManager.onChannelDestory方法处理。这个BrokerHousekeepingService可以字面化地理解为broker的管家服务,这个类内部三个状态方法其实都是调用的org.apache.rocketmq.namesrv.NamesrvController#getRouteInfoManager方法,而这个RouteInfoManager里面的对象有这些

+
public class RouteInfoManager {
+    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
+    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+  // topic与queue的对应关系
+    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
+  // Broker名称与broker属性的map
+    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
+  // 集群与broker集合的对应关系
+    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
+  // 活跃的broker信息
+    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
+  // Broker地址与过滤器
+    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
- // 异步拉取回调,先不讨论细节 - PullCallback pullCallback = new PullCallback() { - @Override - public void onSuccess(PullResult pullResult) { - if (pullResult != null) { - pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, - subscriptionData); +

然后接下去就是初始化了一个线程池,然后注册默认的处理类this.registerProcessor();默认都是这个处理器去处理请求 org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#DefaultRequestProcessor然后是初始化两个定时任务

+

第一是每10秒检查一遍Broker的状态的定时任务,调用scanNotActiveBroker方法;遍历brokerLiveTable集合,查看每个broker的最后更新时间(BrokerLiveInfo.lastUpdateTimestamp)是否超过2分钟,若超过则关闭该broker的渠道并调用RouteInfoManager.onChannelDestory方法清理RouteInfoManager类的topicQueueTable、brokerAddrTable、clusterAddrTable、filterServerTable成员变量。

+
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 
-                    switch (pullResult.getPullStatus()) {
-                        case FOUND:
-                            long prevRequestOffset = pullRequest.getNextOffset();
-                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
-                            long pullRT = System.currentTimeMillis() - beginTimestamp;
-                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
-                                pullRequest.getMessageQueue().getTopic(), pullRT);
+            @Override
+            public void run() {
+                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
+            }
+        }, 5, 10, TimeUnit.SECONDS);
+public void scanNotActiveBroker() {
+        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
+        while (it.hasNext()) {
+            Entry<String, BrokerLiveInfo> next = it.next();
+            long last = next.getValue().getLastUpdateTimestamp();
+            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
+                RemotingUtil.closeChannel(next.getValue().getChannel());
+                it.remove();
+                log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
+                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
+            }
+        }
+    }
 
-                            long firstMsgOffset = Long.MAX_VALUE;
-                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
-                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                            } else {
-                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
+    public void onChannelDestroy(String remoteAddr, Channel channel) {
+        String brokerAddrFound = null;
+        if (channel != null) {
+            try {
+                try {
+                    this.lock.readLock().lockInterruptibly();
+                    Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
+                        this.brokerLiveTable.entrySet().iterator();
+                    while (itBrokerLiveTable.hasNext()) {
+                        Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
+                        if (entry.getValue().getChannel() == channel) {
+                            brokerAddrFound = entry.getKey();
+                            break;
+                        }
+                    }
+                } finally {
+                    this.lock.readLock().unlock();
+                }
+            } catch (Exception e) {
+                log.error("onChannelDestroy Exception", e);
+            }
+        }
 
-                                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
-                                    pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
+        if (null == brokerAddrFound) {
+            brokerAddrFound = remoteAddr;
+        } else {
+            log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
+        }
 
-                                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
-                                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
-                                    pullResult.getMsgFoundList(),
-                                    processQueue,
-                                    pullRequest.getMessageQueue(),
-                                    dispatchToConsume);
+        if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
 
-                                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
-                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
-                                        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
-                                } else {
-                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                                }
-                            }
+            try {
+                try {
+                    this.lock.writeLock().lockInterruptibly();
+                    this.brokerLiveTable.remove(brokerAddrFound);
+                    this.filterServerTable.remove(brokerAddrFound);
+                    String brokerNameFound = null;
+                    boolean removeBrokerName = false;
+                    Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
+                        this.brokerAddrTable.entrySet().iterator();
+                    while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
+                        BrokerData brokerData = itBrokerAddrTable.next().getValue();
 
-                            if (pullResult.getNextBeginOffset() < prevRequestOffset
-                                || firstMsgOffset < prevRequestOffset) {
-                                log.warn(
-                                    "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
-                                    pullResult.getNextBeginOffset(),
-                                    firstMsgOffset,
-                                    prevRequestOffset);
-                            }
+                        Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
+                        while (it.hasNext()) {
+                            Entry<Long, String> entry = it.next();
+                            Long brokerId = entry.getKey();
+                            String brokerAddr = entry.getValue();
+                            if (brokerAddr.equals(brokerAddrFound)) {
+                                brokerNameFound = brokerData.getBrokerName();
+                                it.remove();
+                                log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
+                                    brokerId, brokerAddr);
+                                break;
+                            }
+                        }
 
-                            break;
-                        case NO_NEW_MSG:
-                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+                        if (brokerData.getBrokerAddrs().isEmpty()) {
+                            removeBrokerName = true;
+                            itBrokerAddrTable.remove();
+                            log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
+                                brokerData.getBrokerName());
+                        }
+                    }
 
-                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
+                    if (brokerNameFound != null && removeBrokerName) {
+                        Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
+                        while (it.hasNext()) {
+                            Entry<String, Set<String>> entry = it.next();
+                            String clusterName = entry.getKey();
+                            Set<String> brokerNames = entry.getValue();
+                            boolean removed = brokerNames.remove(brokerNameFound);
+                            if (removed) {
+                                log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
+                                    brokerNameFound, clusterName);
 
-                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                            break;
-                        case NO_MATCHED_MSG:
-                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+                                if (brokerNames.isEmpty()) {
+                                    log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
+                                        clusterName);
+                                    it.remove();
+                                }
 
-                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
+                                break;
+                            }
+                        }
+                    }
 
-                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                            break;
-                        case OFFSET_ILLEGAL:
-                            log.warn("the pull request offset illegal, {} {}",
-                                pullRequest.toString(), pullResult.toString());
-                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+                    if (removeBrokerName) {
+                        Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
+                            this.topicQueueTable.entrySet().iterator();
+                        while (itTopicQueueTable.hasNext()) {
+                            Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
+                            String topic = entry.getKey();
+                            List<QueueData> queueDataList = entry.getValue();
 
-                            pullRequest.getProcessQueue().setDropped(true);
-                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
+                            Iterator<QueueData> itQueueData = queueDataList.iterator();
+                            while (itQueueData.hasNext()) {
+                                QueueData queueData = itQueueData.next();
+                                if (queueData.getBrokerName().equals(brokerNameFound)) {
+                                    itQueueData.remove();
+                                    log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
+                                        topic, queueData);
+                                }
+                            }
 
-                                @Override
-                                public void run() {
-                                    try {
-                                        DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
-                                            pullRequest.getNextOffset(), false);
+                            if (queueDataList.isEmpty()) {
+                                itTopicQueueTable.remove();
+                                log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
+                                    topic);
+                            }
+                        }
+                    }
+                } finally {
+                    this.lock.writeLock().unlock();
+                }
+            } catch (Exception e) {
+                log.error("onChannelDestroy Exception", e);
+            }
+        }
+    }
- DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue()); +

第二个是每10分钟打印一次NameServer的配置参数。即KVConfigManager.configTable变量的内容。

+
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 
-                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
+            @Override
+            public void run() {
+                NamesrvController.this.kvConfigManager.printAllPeriodically();
+            }
+        }, 1, 10, TimeUnit.MINUTES);
- log.warn("fix the pull request offset, {}", pullRequest); - } catch (Throwable e) { - log.error("executeTaskLater Exception", e); - } - } - }, 10000); - break; - default: - break; - } - } - } +

然后这个初始化就差不多完成了,后面只需要把remotingServer start一下就好了

+

处理请求

直接上代码,其实主体是swtich case去判断

+
@Override
+    public RemotingCommand processRequest(ChannelHandlerContext ctx,
+        RemotingCommand request) throws RemotingCommandException {
+
+        if (ctx != null) {
+            log.debug("receive request, {} {} {}",
+                request.getCode(),
+                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
+                request);
+        }
 
-            @Override
-            public void onException(Throwable e) {
-                if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
-                    log.warn("execute the pull request exception", e);
-                }
 
-                DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
-            }
-        };
-        // 如果为集群模式,即可置commitOffsetEnable为 true
-        boolean commitOffsetEnable = false;
-        long commitOffsetValue = 0L;
-        if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
-            commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
-            if (commitOffsetValue > 0) {
-                commitOffsetEnable = true;
-            }
-        }
+        switch (request.getCode()) {
+            case RequestCode.PUT_KV_CONFIG:
+                return this.putKVConfig(ctx, request);
+            case RequestCode.GET_KV_CONFIG:
+                return this.getKVConfig(ctx, request);
+            case RequestCode.DELETE_KV_CONFIG:
+                return this.deleteKVConfig(ctx, request);
+            case RequestCode.QUERY_DATA_VERSION:
+                return queryBrokerTopicConfig(ctx, request);
+            case RequestCode.REGISTER_BROKER:
+                Version brokerVersion = MQVersion.value2Version(request.getVersion());
+                if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
+                    return this.registerBrokerWithFilterServer(ctx, request);
+                } else {
+                    return this.registerBroker(ctx, request);
+                }
+            case RequestCode.UNREGISTER_BROKER:
+                return this.unregisterBroker(ctx, request);
+            case RequestCode.GET_ROUTEINTO_BY_TOPIC:
+                return this.getRouteInfoByTopic(ctx, request);
+            case RequestCode.GET_BROKER_CLUSTER_INFO:
+                return this.getBrokerClusterInfo(ctx, request);
+            case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
+                return this.wipeWritePermOfBroker(ctx, request);
+            case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
+                return getAllTopicListFromNameserver(ctx, request);
+            case RequestCode.DELETE_TOPIC_IN_NAMESRV:
+                return deleteTopicInNamesrv(ctx, request);
+            case RequestCode.GET_KVLIST_BY_NAMESPACE:
+                return this.getKVListByNamespace(ctx, request);
+            case RequestCode.GET_TOPICS_BY_CLUSTER:
+                return this.getTopicsByCluster(ctx, request);
+            case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
+                return this.getSystemTopicListFromNs(ctx, request);
+            case RequestCode.GET_UNIT_TOPIC_LIST:
+                return this.getUnitTopicList(ctx, request);
+            case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
+                return this.getHasUnitSubTopicList(ctx, request);
+            case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
+                return this.getHasUnitSubUnUnitTopicList(ctx, request);
+            case RequestCode.UPDATE_NAMESRV_CONFIG:
+                return this.updateConfig(ctx, request);
+            case RequestCode.GET_NAMESRV_CONFIG:
+                return this.getConfig(ctx, request);
+            default:
+                break;
+        }
+        return null;
+    }
- // 将上面获得的commitOffsetEnable更新到订阅关系里 - String subExpression = null; - boolean classFilter = false; - SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic()); - if (sd != null) { - if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) { - subExpression = sd.getSubString(); - } +

以broker注册为例,

+
case RequestCode.REGISTER_BROKER:
+                Version brokerVersion = MQVersion.value2Version(request.getVersion());
+                if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
+                    return this.registerBrokerWithFilterServer(ctx, request);
+                } else {
+                    return this.registerBroker(ctx, request);
+                }
- classFilter = sd.isClassFilterMode(); - } +

做了个简单的版本管理,我们看下前面一个的代码

+
public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
+    throws RemotingCommandException {
+    final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
+    final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
+    final RegisterBrokerRequestHeader requestHeader =
+        (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);
 
-        // 组成 sysFlag
-        int sysFlag = PullSysFlag.buildSysFlag(
-            commitOffsetEnable, // commitOffset
-            true, // suspend
-            subExpression != null, // subscription
-            classFilter // class filter
-        );
-        // 调用真正的拉取消息接口
-        try {
-            this.pullAPIWrapper.pullKernelImpl(
-                pullRequest.getMessageQueue(),
-                subExpression,
-                subscriptionData.getExpressionType(),
-                subscriptionData.getSubVersion(),
-                pullRequest.getNextOffset(),
-                this.defaultMQPushConsumer.getPullBatchSize(),
-                sysFlag,
-                commitOffsetValue,
-                BROKER_SUSPEND_MAX_TIME_MILLIS,
-                CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
-                CommunicationMode.ASYNC,
-                pullCallback
-            );
-        } catch (Exception e) {
-            log.error("pullKernelImpl exception", e);
-            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
-        }
-    }
-

以下就是拉取消息的底层 api,不够不是特别复杂,主要是在找 broker,和设置请求参数

-
public PullResult pullKernelImpl(
-    final MessageQueue mq,
-    final String subExpression,
-    final String expressionType,
-    final long subVersion,
-    final long offset,
-    final int maxNums,
-    final int sysFlag,
-    final long commitOffset,
-    final long brokerSuspendMaxTimeMillis,
-    final long timeoutMillis,
-    final CommunicationMode communicationMode,
-    final PullCallback pullCallback
-) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
-    FindBrokerResult findBrokerResult =
-        this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
-            this.recalculatePullFromWhichNode(mq), false);
-    if (null == findBrokerResult) {
-        this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
-        findBrokerResult =
-            this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
-                this.recalculatePullFromWhichNode(mq), false);
-    }
+    if (!checksum(ctx, request, requestHeader)) {
+        response.setCode(ResponseCode.SYSTEM_ERROR);
+        response.setRemark("crc32 not match");
+        return response;
+    }
 
-    if (findBrokerResult != null) {
-        {
-            // check version
-            if (!ExpressionType.isTagType(expressionType)
-                && findBrokerResult.getBrokerVersion() < MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) {
-                throw new MQClientException("The broker[" + mq.getBrokerName() + ", "
-                    + findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null);
-            }
-        }
-        int sysFlagInner = sysFlag;
+    RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
 
-        if (findBrokerResult.isSlave()) {
-            sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
-        }
+    if (request.getBody() != null) {
+        try {
+            registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
+        } catch (Exception e) {
+            throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
+        }
+    } else {
+        registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
+        registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
+    }
 
-        PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
-        requestHeader.setConsumerGroup(this.consumerGroup);
-        requestHeader.setTopic(mq.getTopic());
-        requestHeader.setQueueId(mq.getQueueId());
-        requestHeader.setQueueOffset(offset);
-        requestHeader.setMaxMsgNums(maxNums);
-        requestHeader.setSysFlag(sysFlagInner);
-        requestHeader.setCommitOffset(commitOffset);
-        requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);
-        requestHeader.setSubscription(subExpression);
-        requestHeader.setSubVersion(subVersion);
-        requestHeader.setExpressionType(expressionType);
+    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
+        requestHeader.getClusterName(),
+        requestHeader.getBrokerAddr(),
+        requestHeader.getBrokerName(),
+        requestHeader.getBrokerId(),
+        requestHeader.getHaServerAddr(),
+        registerBrokerBody.getTopicConfigSerializeWrapper(),
+        registerBrokerBody.getFilterServerList(),
+        ctx.channel());
 
-        String brokerAddr = findBrokerResult.getBrokerAddr();
-        if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
-            brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
-        }
+    responseHeader.setHaServerAddr(result.getHaServerAddr());
+    responseHeader.setMasterAddr(result.getMasterAddr());
 
-        PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
-            brokerAddr,
-            requestHeader,
-            timeoutMillis,
-            communicationMode,
-            pullCallback);
+    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
+    response.setBody(jsonValue);
 
-        return pullResult;
-    }
+    response.setCode(ResponseCode.SUCCESS);
+    response.setRemark(null);
+    return response;
+}
- throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); -}
-

再看下一步的

-
public PullResult pullMessage(
-    final String addr,
-    final PullMessageRequestHeader requestHeader,
-    final long timeoutMillis,
-    final CommunicationMode communicationMode,
-    final PullCallback pullCallback
-) throws RemotingException, MQBrokerException, InterruptedException {
-    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);
+

可以看到主要的逻辑还是在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker这个方法里

+
public RegisterBrokerResult registerBroker(
+        final String clusterName,
+        final String brokerAddr,
+        final String brokerName,
+        final long brokerId,
+        final String haServerAddr,
+        final TopicConfigSerializeWrapper topicConfigWrapper,
+        final List<String> filterServerList,
+        final Channel channel) {
+        RegisterBrokerResult result = new RegisterBrokerResult();
+        try {
+            try {
+                this.lock.writeLock().lockInterruptibly();
 
-    switch (communicationMode) {
-        case ONEWAY:
-            assert false;
-            return null;
-        case ASYNC:
-            this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
-            return null;
-        case SYNC:
-            return this.pullMessageSync(addr, request, timeoutMillis);
-        default:
-            assert false;
-            break;
-    }
+              // 更新这个clusterAddrTable
+                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
+                if (null == brokerNames) {
+                    brokerNames = new HashSet<String>();
+                    this.clusterAddrTable.put(clusterName, brokerNames);
+                }
+                brokerNames.add(brokerName);
 
-    return null;
-}
-

通过 communicationMode 判断是同步拉取还是异步拉取,异步就调用

-
private void pullMessageAsync(
-        final String addr,
-        final RemotingCommand request,
-        final long timeoutMillis,
-        final PullCallback pullCallback
-    ) throws RemotingException, InterruptedException {
-        this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
-            @Override
-            public void operationComplete(ResponseFuture responseFuture) {
-                异步
-                RemotingCommand response = responseFuture.getResponseCommand();
-                if (response != null) {
-                    try {
-                        PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response);
-                        assert pullResult != null;
-                        pullCallback.onSuccess(pullResult);
-                    } catch (Exception e) {
-                        pullCallback.onException(e);
-                    }
-                } else {
-                    if (!responseFuture.isSendRequestOK()) {
-                        pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause()));
-                    } else if (responseFuture.isTimeout()) {
-                        pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request,
-                            responseFuture.getCause()));
-                    } else {
-                        pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause()));
-                    }
-                }
-            }
-        });
-    }
-

并且会调用前面 pullCallback 的onSuccess和onException方法,同步的就是调用

-
private PullResult pullMessageSync(
-        final String addr,
-        final RemotingCommand request,
-        final long timeoutMillis
-    ) throws RemotingException, InterruptedException, MQBrokerException {
-        RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
-        assert response != null;
-        return this.processPullResponse(response);
-    }
-

然后就是这个 remotingClient 的 invokeAsync 跟 invokeSync 方法

-
@Override
-    public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
-        throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
-        RemotingSendRequestException {
-        long beginStartTime = System.currentTimeMillis();
-        final Channel channel = this.getAndCreateChannel(addr);
-        if (channel != null && channel.isActive()) {
-            try {
-                doBeforeRpcHooks(addr, request);
-                long costTime = System.currentTimeMillis() - beginStartTime;
-                if (timeoutMillis < costTime) {
-                    throw new RemotingTooMuchRequestException("invokeAsync call timeout");
-                }
-                this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, invokeCallback);
-            } catch (RemotingSendRequestException e) {
-                log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
-                this.closeChannel(addr, channel);
-                throw e;
-            }
-        } else {
-            this.closeChannel(addr, channel);
-            throw new RemotingConnectException(addr);
-        }
-    }
-@Override
-    public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
-        throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
-        long beginStartTime = System.currentTimeMillis();
-        final Channel channel = this.getAndCreateChannel(addr);
-        if (channel != null && channel.isActive()) {
-            try {
-                doBeforeRpcHooks(addr, request);
-                long costTime = System.currentTimeMillis() - beginStartTime;
-                if (timeoutMillis < costTime) {
-                    throw new RemotingTimeoutException("invokeSync call timeout");
-                }
-                RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
-                doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
-                return response;
-            } catch (RemotingSendRequestException e) {
-                log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
-                this.closeChannel(addr, channel);
-                throw e;
-            } catch (RemotingTimeoutException e) {
-                if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
-                    this.closeChannel(addr, channel);
-                    log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
-                }
-                log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
-                throw e;
-            }
-        } else {
-            this.closeChannel(addr, channel);
-            throw new RemotingConnectException(addr);
-        }
-    }
-

再往下看

-
public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
-        final long timeoutMillis)
-        throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
-        final int opaque = request.getOpaque();
+                boolean registerFirst = false;
 
-        try {
-            同步跟异步都是会把结果用ResponseFuture抱起来
-            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
-            this.responseTable.put(opaque, responseFuture);
-            final SocketAddress addr = channel.remoteAddress();
-            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
-                @Override
-                public void operationComplete(ChannelFuture f) throws Exception {
-                    if (f.isSuccess()) {
-                        responseFuture.setSendRequestOK(true);
-                        return;
-                    } else {
-                        responseFuture.setSendRequestOK(false);
-                    }
+              // 更新brokerAddrTable
+                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
+                if (null == brokerData) {
+                    registerFirst = true;
+                    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
+                    this.brokerAddrTable.put(brokerName, brokerData);
+                }
+                Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
+                //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
+                //The same IP:PORT must only have one record in brokerAddrTable
+                Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
+                while (it.hasNext()) {
+                    Entry<Long, String> item = it.next();
+                    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
+                        it.remove();
+                    }
+                }
 
-                    responseTable.remove(opaque);
-                    responseFuture.setCause(f.cause());
-                    responseFuture.putResponse(null);
-                    log.warn("send a request command to channel <" + addr + "> failed.");
-                }
-            });
-            // 区别是同步的是在这等待
-            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
-            if (null == responseCommand) {
-                if (responseFuture.isSendRequestOK()) {
-                    throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
-                        responseFuture.getCause());
-                } else {
-                    throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
-                }
-            }
+                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
+                registerFirst = registerFirst || (null == oldAddr);
 
-            return responseCommand;
-        } finally {
-            this.responseTable.remove(opaque);
-        }
-    }
+              // 更新了org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable中的数据
+                if (null != topicConfigWrapper
+                    && MixAll.MASTER_ID == brokerId) {
+                    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
+                        || registerFirst) {
+                        ConcurrentMap<String, TopicConfig> tcTable =
+                            topicConfigWrapper.getTopicConfigTable();
+                        if (tcTable != null) {
+                            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
+                                this.createAndUpdateQueueData(brokerName, entry.getValue());
+                            }
+                        }
+                    }
+                }
 
-    public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
-        final InvokeCallback invokeCallback)
-        throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
-        long beginStartTime = System.currentTimeMillis();
-        final int opaque = request.getOpaque();
-        boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
-        if (acquired) {
-            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
-            long costTime = System.currentTimeMillis() - beginStartTime;
-            if (timeoutMillis < costTime) {
-                once.release();
-                throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
-            }
+              // 更新活跃broker信息
+                BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
+                    new BrokerLiveInfo(
+                        System.currentTimeMillis(),
+                        topicConfigWrapper.getDataVersion(),
+                        channel,
+                        haServerAddr));
+                if (null == prevBrokerLiveInfo) {
+                    log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
+                }
 
-            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
-            this.responseTable.put(opaque, responseFuture);
-            try {
-                channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
-                    @Override
-                    public void operationComplete(ChannelFuture f) throws Exception {
-                        if (f.isSuccess()) {
-                            responseFuture.setSendRequestOK(true);
-                            return;
-                        }
-                        requestFail(opaque);
-                        log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
-                    }
-                });
-            } catch (Exception e) {
-                responseFuture.release();
-                log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
-                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
-            }
-        } else {
-            if (timeoutMillis <= 0) {
-                throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
-            } else {
-                String info =
-                    String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
-                        timeoutMillis,
-                        this.semaphoreAsync.getQueueLength(),
-                        this.semaphoreAsync.availablePermits()
-                    );
-                log.warn(info);
-                throw new RemotingTimeoutException(info);
-            }
-        }
-    }
+ // 处理filter + if (filterServerList != null) { + if (filterServerList.isEmpty()) { + this.filterServerTable.remove(brokerAddr); + } else { + this.filterServerTable.put(brokerAddr, filterServerList); + } + } + + // 当当前broker非master时返回master信息 + if (MixAll.MASTER_ID != brokerId) { + String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (masterAddr != null) { + BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr); + if (brokerLiveInfo != null) { + result.setHaServerAddr(brokerLiveInfo.getHaServerAddr()); + result.setMasterAddr(masterAddr); + } + } + } + } finally { + this.lock.writeLock().unlock(); + } + } catch (Exception e) { + log.error("registerBroker Exception", e); + } + + return result; + }
+ +

这个是注册 broker 的逻辑,再看下根据 topic 获取 broker 信息和 topic 信息,org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic 主要是这个方法的逻辑

+
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
+        RemotingCommand request) throws RemotingCommandException {
+        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
+        final GetRouteInfoRequestHeader requestHeader =
+            (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
+
+        TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
+
+        if (topicRouteData != null) {
+            if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
+                String orderTopicConf =
+                    this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
+                        requestHeader.getTopic());
+                topicRouteData.setOrderTopicConf(orderTopicConf);
+            }
 
+            byte[] content = topicRouteData.encode();
+            response.setBody(content);
+            response.setCode(ResponseCode.SUCCESS);
+            response.setRemark(null);
+            return response;
+        }
 
+        response.setCode(ResponseCode.TOPIC_NOT_EXIST);
+        response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
+            + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
+        return response;
+    }
+

首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteDataorg.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。

+

简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。

]]>
MQ @@ -9935,618 +9610,798 @@ public: RocketMQ 削峰填谷 中间件 - DefaultMQPushConsumer + NameServer 源码解析
- 聊一下 RocketMQ 的 NameServer 源码 - /2020/07/05/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-NameServer-%E6%BA%90%E7%A0%81/ - 前面介绍了,nameserver相当于dubbo的注册中心,用与管理broker,broker会在启动的时候注册到nameserver,并且会发送心跳给namaserver,nameserver负责保存活跃的broker,包括master和slave,同时保存topic和topic下的队列,以及filter列表,然后为producer和consumer的请求提供服务。

-

启动过程

public static void main(String[] args) {
-        main0(args);
-    }
-
-    public static NamesrvController main0(String[] args) {
-
-        try {
-            NamesrvController controller = createNamesrvController(args);
-            start(controller);
-            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
-            log.info(tip);
-            System.out.printf("%s%n", tip);
-            return controller;
-        } catch (Throwable e) {
-            e.printStackTrace();
-            System.exit(-1);
-        }
-
-        return null;
-    }
- -

入口的代码时这样子,其实主要的逻辑在createNamesrvController和start方法,来看下这两个的实现

-
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
-        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
-        //PackageConflictDetect.detectFastjson();
-
-        Options options = ServerUtil.buildCommandlineOptions(new Options());
-        commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
-        if (null == commandLine) {
-            System.exit(-1);
-            return null;
-        }
-
-        final NamesrvConfig namesrvConfig = new NamesrvConfig();
-        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
-        nettyServerConfig.setListenPort(9876);
-        if (commandLine.hasOption('c')) {
-            String file = commandLine.getOptionValue('c');
-            if (file != null) {
-                InputStream in = new BufferedInputStream(new FileInputStream(file));
-                properties = new Properties();
-                properties.load(in);
-                MixAll.properties2Object(properties, namesrvConfig);
-                MixAll.properties2Object(properties, nettyServerConfig);
-
-                namesrvConfig.setConfigStorePath(file);
-
-                System.out.printf("load config properties file OK, %s%n", file);
-                in.close();
-            }
-        }
-
-        if (commandLine.hasOption('p')) {
-            InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
-            MixAll.printObjectProperties(console, namesrvConfig);
-            MixAll.printObjectProperties(console, nettyServerConfig);
-            System.exit(0);
-        }
+    给小电驴上牌
+    /2022/03/20/%E7%BB%99%E5%B0%8F%E7%94%B5%E9%A9%B4%E4%B8%8A%E7%89%8C/
+    三八节活动的时候下决心买了个小电驴,主要是上下班路上现在通勤条件越来越恶劣了,之前都是觉得坐公交就行了,实际路程就比较短,但是现在或者说大概是年前那两个月差不多就开始了,基本是堵一路,个人感觉是天目山路那边在修地铁,而且蚂蚁的几个空间都在那,上班的时间点都差不多,前一个修地铁感觉挺久了,机动车保有量也越来越多,总体是古墩路就越来越堵,还有个原因就是早上上班的点共享单车都被骑走了,有时候整整走一路都没一辆,有时候孤零零地有一辆基本都是破的;走路其实也是一种选择,但是因为要赶着上班,走得太慢就要很久,可能要 45 分钟这样,走得比较快就一身汗挺难受的。所以考虑自行车和电动车,这里还有一点就是不管是乘公交还是骑共享单车,其实都要从楼下走出去蛮远,公司回来也是,也就是这种通勤方式在准备阶段就花了比较多时间,比如总的从下班到到家的时间是半小时,可能在骑共享单车和公交车上的时间都不到十分钟,就比较难受。觉得这种比例太浪费时间,如果能有这种比较点对点的方式,估计能省时省力不少,前面说的骑共享单车的方式其实在之前是比较可行的,但是后来越来越少车,基本都是每周的前几天,周一到周三都是没有车,走路到公司再冷的天都是走出一身的汗,下雨天就更难受,本来下雨天应该是优先选择坐公交,但是一般下雨天堵车会更严重,而且车子到我上车的那个站,下雨天就挤得不行,总体说下来感觉事情都不打,但是几年下来,还是会挺不爽的。

+

电驴看的比较草率,主要是考虑续航,然后锂电池外加 48v 和 24AH,这样一般来讲还是价格比较高的,只是原来没预料到这个限速,以为现在的车子都比较快,但是现在的新国标车子都是 25km/h 的限速,然后 15km/h 都是会要提醒,虽然说有一些特殊的解除限速的方法,但是解了也就 35km/h ,差距不是特别大,而且现在的车子都是比较小,也不太能载东西,特别是上下班路程也不远的情况下,其实不是那么需要速度,就像我朋友说的,可能骑车的时间还不如等红绿灯多,所以就还好,也不打算解除限速,只是品牌上也仔细看,后来选了绿源,目前大部分还是雅迪,爱玛,台羚,绿源,小牛等,路上看的话还是雅迪比较多,不过价格也比较贵一点,还有就是小牛了,是比较新兴的品牌,手机 App 什么的做得比较好,而且也比较贵,最后以相对比较便宜的价格买了个锂电 48V24AH 的小车子,后来发现还是有点不方便的点就是没有比较大的筐,也不好装,这样就是下雨天雨衣什么的比较不方便放。

+

聊回来主题上牌这个事情,这个事情也是颇费心力,提车的时候店里的让我跟他早上一起去,但是因为不确定时间,也比较远就没跟着去,因为我是线上买的,线下自提,线下的店可能没啥利润可以拿,就不肯帮忙代上牌,朋友说在线下店里买是可以代上的,自己上牌过程也比较曲折,一开始是头盔没到,然后是等开发票,主要的东西就是需要骑着车子去车管所,不能只自己去,然后需要预约,附近比较近的都是提前一周就预约完了号了,要提前在支付宝上进行预约,比较空的就是店里推荐的景区大队,但是随之而来就是比较蛋疼的,这个景区大队太远了,看下骑车距离有十几公里,所以就有点拖延症,但是总归要上的,不然一直不能开是白买了,上牌的材料主要是车辆合格证,发票,然后车子上的浙品码,在车架上和电池上,然后车架号什么的都要跟合格证上完全对应,整体车子要跟合格证上一毛一样,如果有额外的反光镜,后面副座都需要拆掉,脚踏板要装上,到了那其实还比较顺利,就是十几公里外加那天比较冷,吹得头疼。

+]]>
+ + 生活 + + + 生活 + + + + 聊一下 RocketMQ 的 DefaultMQPushConsumer 源码 + /2020/06/26/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-Consumer/ + 首先看下官方的小 demo

+
public static void main(String[] args) throws InterruptedException, MQClientException {
 
-        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
+        /*
+         * Instantiate with specified consumer group name.
+         * 首先是new 一个对象出来,然后指定 Consumer 的 Group
+         * 同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
+         */
+        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
 
-        if (null == namesrvConfig.getRocketmqHome()) {
-            System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
-            System.exit(-2);
-        }
+        /*
+         * Specify name server addresses.
+         * <p/>
+         * 这里可以通知指定环境变量或者设置对象参数的形式指定名字空间服务的地址
+         *
+         * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
+         * <pre>
+         * {@code
+         * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
+         * }
+         * </pre>
+         */
 
-        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
-        JoranConfigurator configurator = new JoranConfigurator();
-        configurator.setContext(lc);
-        lc.reset();
-        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
+        /*
+         * Specify where to start in case the specified consumer group is a brand new one.
+         * 指定消费起始点
+         */
+        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
 
-        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
+        /*
+         * Subscribe one more more topics to consume.
+         * 指定订阅的 topic 跟 tag,注意后面的是个表达式,可以以 tag1 || tag2 || tag3 传入
+         */
+        consumer.subscribe("TopicTest", "*");
 
-        MixAll.printObjectProperties(log, namesrvConfig);
-        MixAll.printObjectProperties(log, nettyServerConfig);
+        /*
+         *  Register callback to execute on arrival of messages fetched from brokers.
+         *  注册具体获得消息后的处理方法
+         */
+        consumer.registerMessageListener(new MessageListenerConcurrently() {
 
-        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
+            @Override
+            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
+                ConsumeConcurrentlyContext context) {
+                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
+                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
+            }
+        });
 
-        // remember all configs to prevent discard
-        controller.getConfiguration().registerConfig(properties);
+        /*
+         *  Launch the consumer instance.
+         * 启动消费者
+         */
+        consumer.start();
 
-        return controller;
-    }
+ System.out.printf("Consumer Started.%n"); + }
-

这个方法里其实主要是读取一些配置啥的,不是很复杂,

-
public static NamesrvController start(final NamesrvController controller) throws Exception {
+

然后就是看看 start 的过程了

+
/**
+     * This method gets internal infrastructure readily to serve. Instances must call this method after configuration.
+     *
+     * @throws MQClientException if there is any client error.
+     */
+    @Override
+    public void start() throws MQClientException {
+        setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
+        this.defaultMQPushConsumerImpl.start();
+        if (null != traceDispatcher) {
+            try {
+                traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
+            } catch (MQClientException e) {
+                log.warn("trace dispatcher start failed ", e);
+            }
+        }
+    }
+

具体的逻辑在this.defaultMQPushConsumerImpl.start(),这个 defaultMQPushConsumerImpl 就是

+
/**
+     * Internal implementation. Most of the functions herein are delegated to it.
+     */
+    protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
- if (null == controller) { - throw new IllegalArgumentException("NamesrvController is null"); - } +
public synchronized void start() throws MQClientException {
+        switch (this.serviceState) {
+            case CREATE_JUST:
+                log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
+                    this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
+                // 这里比较巧妙,相当于想设立了个屏障,防止并发启动,不过这里并不是悲观锁,也不算个严格的乐观锁
+                this.serviceState = ServiceState.START_FAILED;
 
-        boolean initResult = controller.initialize();
-        if (!initResult) {
-            controller.shutdown();
-            System.exit(-3);
-        }
+                this.checkConfig();
 
-        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
-            @Override
-            public Void call() throws Exception {
-                controller.shutdown();
-                return null;
-            }
-        }));
+                this.copySubscription();
 
-        controller.start();
+                if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
+                    this.defaultMQPushConsumer.changeInstanceNameToPID();
+                }
 
-        return controller;
-    }
+ // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成 + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook); -

这个start里主要关注initialize方法,后面就是一个停机的hook,来看下initialize方法

-
public boolean initialize() {
+                // 这个 rebalanceImpl 主要负责决定,当前的consumer应该从哪些Queue中消费消息;
+                this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
+                this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
+                this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
+                this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
 
-        this.kvConfigManager.load();
+                // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑
+                this.pullAPIWrapper = new PullAPIWrapper(
+                    mQClientFactory,
+                    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
+                this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
 
-        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
+                if (this.defaultMQPushConsumer.getOffsetStore() != null) {
+                    this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
+                } else {
+                    switch (this.defaultMQPushConsumer.getMessageModel()) {
+                        case BROADCASTING:
+                            this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
+                            break;
+                        case CLUSTERING:
+                            this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
+                            break;
+                        default:
+                            break;
+                    }
+                    this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
+                }
+                // offsetStore 维护当前consumer的消费记录(offset);有两种实现,Local和Rmote,Local存储在本地磁盘上,适用于BROADCASTING广播消费模式;而Remote则将消费进度存储在Broker上,适用于CLUSTERING集群消费模式;
+                this.offsetStore.load();
 
-        this.remotingExecutor =
-            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
+                if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
+                    this.consumeOrderly = true;
+                    this.consumeMessageService =
+                        new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
+                } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
+                    this.consumeOrderly = false;
+                    this.consumeMessageService =
+                        new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
+                }
 
-        this.registerProcessor();
+                // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
+                this.consumeMessageService.start();
 
-        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+                boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
+                if (!registerOK) {
+                    this.serviceState = ServiceState.CREATE_JUST;
+                    this.consumeMessageService.shutdown();
+                    throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
+                        null);
+                }
 
-            @Override
-            public void run() {
-                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
-            }
-        }, 5, 10, TimeUnit.SECONDS);
+                mQClientFactory.start();
+                log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
+                this.serviceState = ServiceState.RUNNING;
+                break;
+            case RUNNING:
+            case START_FAILED:
+            case SHUTDOWN_ALREADY:
+                throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
+                    + this.serviceState
+                    + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
+                    null);
+            default:
+                break;
+        }
 
-        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+        this.updateTopicSubscribeInfoWhenSubscriptionChanged();
+        this.mQClientFactory.checkClientInBroker();
+        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
+        this.mQClientFactory.rebalanceImmediately();
+    }
+

然后我们往下看主要的目光聚焦mQClientFactory.start()

+
public void start() throws MQClientException {
 
-            @Override
-            public void run() {
-                NamesrvController.this.kvConfigManager.printAllPeriodically();
-            }
-        }, 1, 10, TimeUnit.MINUTES);
+        synchronized (this) {
+            switch (this.serviceState) {
+                case CREATE_JUST:
+                    this.serviceState = ServiceState.START_FAILED;
+                    // If not specified,looking address from name server
+                    if (null == this.clientConfig.getNamesrvAddr()) {
+                        this.mQClientAPIImpl.fetchNameServerAddr();
+                    }
+                    // Start request-response channel
+                    // 这里主要是初始化了个网络客户端
+                    this.mQClientAPIImpl.start();
+                    // Start various schedule tasks
+                    // 定时任务
+                    this.startScheduledTask();
+                    // Start pull service
+                    // 这里重点说下
+                    this.pullMessageService.start();
+                    // Start rebalance service
+                    this.rebalanceService.start();
+                    // Start push service
+                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
+                    log.info("the client factory [{}] start OK", this.clientId);
+                    this.serviceState = ServiceState.RUNNING;
+                    break;
+                case START_FAILED:
+                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
+                default:
+                    break;
+            }
+        }
+    }
+

我们来看下这个 pullMessageService,org.apache.rocketmq.client.impl.consumer.PullMessageService,

实现了 runnable 接口,
然后可以看到 run 方法

+
public void run() {
+        log.info(this.getServiceName() + " service started");
 
-        if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
-            // Register a listener to reload SslContext
-            try {
-                fileWatchService = new FileWatchService(
-                    new String[] {
-                        TlsSystemConfig.tlsServerCertPath,
-                        TlsSystemConfig.tlsServerKeyPath,
-                        TlsSystemConfig.tlsServerTrustCertPath
-                    },
-                    new FileWatchService.Listener() {
-                        boolean certChanged, keyChanged = false;
-                        @Override
-                        public void onChanged(String path) {
-                            if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
-                                log.info("The trust certificate changed, reload the ssl context");
-                                reloadServerSslContext();
-                            }
-                            if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
-                                certChanged = true;
-                            }
-                            if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
-                                keyChanged = true;
-                            }
-                            if (certChanged && keyChanged) {
-                                log.info("The certificate and private key changed, reload the ssl context");
-                                certChanged = keyChanged = false;
-                                reloadServerSslContext();
-                            }
-                        }
-                        private void reloadServerSslContext() {
-                            ((NettyRemotingServer) remotingServer).loadSslContext();
-                        }
-                    });
-            } catch (Exception e) {
-                log.warn("FileWatchService created error, can't load the certificate dynamically");
-            }
-        }
+        while (!this.isStopped()) {
+            try {
+                PullRequest pullRequest = this.pullRequestQueue.take();
+                this.pullMessage(pullRequest);
+            } catch (InterruptedException ignored) {
+            } catch (Exception e) {
+                log.error("Pull Message Service Run Method exception", e);
+            }
+        }
 
-        return true;
-    }
+ log.info(this.getServiceName() + " service end"); + }
+

接着在看 pullMessage 方法

+
private void pullMessage(final PullRequest pullRequest) {
+        final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
+        if (consumer != null) {
+            DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
+            impl.pullMessage(pullRequest);
+        } else {
+            log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
+        }
+    }
+

实际上调用了这个方法,这个方法很长,我在代码里注释下下每一段的功能

+
public void pullMessage(final PullRequest pullRequest) {
+        final ProcessQueue processQueue = pullRequest.getProcessQueue();
+        // 这里开始就是检查状态,确定是否往下执行
+        if (processQueue.isDropped()) {
+            log.info("the pull request[{}] is dropped.", pullRequest.toString());
+            return;
+        }
 
-

这里的kvConfigManager主要是来加载NameServer的配置参数,存到org.apache.rocketmq.namesrv.kvconfig.KVConfigManager#configTable中,然后是以BrokerHousekeepingService对象为参数初始化NettyRemotingServer对象,BrokerHousekeepingService对象作为该Netty连接中Socket链接的监听器(ChannelEventListener);监听与Broker建立的渠道的状态(空闲、关闭、异常三个状态),并调用BrokerHousekeepingService的相应onChannel方法。其中渠道的空闲、关闭、异常状态均调用RouteInfoManager.onChannelDestory方法处理。这个BrokerHousekeepingService可以字面化地理解为broker的管家服务,这个类内部三个状态方法其实都是调用的org.apache.rocketmq.namesrv.NamesrvController#getRouteInfoManager方法,而这个RouteInfoManager里面的对象有这些

-
public class RouteInfoManager {
-    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
-    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
-    private final ReadWriteLock lock = new ReentrantReadWriteLock();
-  // topic与queue的对应关系
-    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
-  // Broker名称与broker属性的map
-    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
-  // 集群与broker集合的对应关系
-    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
-  // 活跃的broker信息
-    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
-  // Broker地址与过滤器
-    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
+ pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis()); -

然后接下去就是初始化了一个线程池,然后注册默认的处理类this.registerProcessor();默认都是这个处理器去处理请求 org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#DefaultRequestProcessor然后是初始化两个定时任务

-

第一是每10秒检查一遍Broker的状态的定时任务,调用scanNotActiveBroker方法;遍历brokerLiveTable集合,查看每个broker的最后更新时间(BrokerLiveInfo.lastUpdateTimestamp)是否超过2分钟,若超过则关闭该broker的渠道并调用RouteInfoManager.onChannelDestory方法清理RouteInfoManager类的topicQueueTable、brokerAddrTable、clusterAddrTable、filterServerTable成员变量。

-
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+        try {
+            this.makeSureStateOK();
+        } catch (MQClientException e) {
+            log.warn("pullMessage exception, consumer state not ok", e);
+            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+            return;
+        }
 
-            @Override
-            public void run() {
-                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
-            }
-        }, 5, 10, TimeUnit.SECONDS);
-public void scanNotActiveBroker() {
-        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
-        while (it.hasNext()) {
-            Entry<String, BrokerLiveInfo> next = it.next();
-            long last = next.getValue().getLastUpdateTimestamp();
-            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
-                RemotingUtil.closeChannel(next.getValue().getChannel());
-                it.remove();
-                log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
-                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
-            }
-        }
-    }
+        if (this.isPause()) {
+            log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
+            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
+            return;
+        }
 
-    public void onChannelDestroy(String remoteAddr, Channel channel) {
-        String brokerAddrFound = null;
-        if (channel != null) {
-            try {
-                try {
-                    this.lock.readLock().lockInterruptibly();
-                    Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
-                        this.brokerLiveTable.entrySet().iterator();
-                    while (itBrokerLiveTable.hasNext()) {
-                        Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
-                        if (entry.getValue().getChannel() == channel) {
-                            brokerAddrFound = entry.getKey();
-                            break;
-                        }
-                    }
-                } finally {
-                    this.lock.readLock().unlock();
-                }
-            } catch (Exception e) {
-                log.error("onChannelDestroy Exception", e);
-            }
-        }
+        // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制
+        long cachedMessageCount = processQueue.getMsgCount().get();
+        long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
 
-        if (null == brokerAddrFound) {
-            brokerAddrFound = remoteAddr;
-        } else {
-            log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
-        }
+        if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
+            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
+            if ((queueFlowControlTimes++ % 1000) == 0) {
+                log.warn(
+                    "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
+                    this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
+            }
+            return;
+        }
 
-        if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
+        if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
+            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
+            if ((queueFlowControlTimes++ % 1000) == 0) {
+                log.warn(
+                    "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
+                    this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
+            }
+            return;
+        }
 
-            try {
-                try {
-                    this.lock.writeLock().lockInterruptibly();
-                    this.brokerLiveTable.remove(brokerAddrFound);
-                    this.filterServerTable.remove(brokerAddrFound);
-                    String brokerNameFound = null;
-                    boolean removeBrokerName = false;
-                    Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
-                        this.brokerAddrTable.entrySet().iterator();
-                    while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
-                        BrokerData brokerData = itBrokerAddrTable.next().getValue();
+        // 若不是顺序消费(即DefaultMQPushConsumerImpl.consumeOrderly等于false),则检查ProcessQueue对象的msgTreeMap:TreeMap<Long,MessageExt>变量的第一个key值与最后一个key值之间的差额,该key值表示查询的队列偏移量queueoffset;若差额大于阈值(由DefaultMQPushConsumer. consumeConcurrentlyMaxSpan指定,默认是2000),则调用PullMessageService.executePullRequestLater方法,在50毫秒之后重新将该PullRequest请求放入PullMessageService.pullRequestQueue队列中;并跳出该方法;这里的意思主要就是消息有堆积了,等会再来拉取
+        if (!this.consumeOrderly) {
+            if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
+                this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
+                if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
+                    log.warn(
+                        "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
+                        processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
+                        pullRequest, queueMaxSpanFlowControlTimes);
+                }
+                return;
+            }
+        } else {
+            if (processQueue.isLocked()) {
+                if (!pullRequest.isLockedFirst()) {
+                    final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
+                    boolean brokerBusy = offset < pullRequest.getNextOffset();
+                    log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
+                        pullRequest, offset, brokerBusy);
+                    if (brokerBusy) {
+                        log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
+                            pullRequest, offset);
+                    }
 
-                        Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
-                        while (it.hasNext()) {
-                            Entry<Long, String> entry = it.next();
-                            Long brokerId = entry.getKey();
-                            String brokerAddr = entry.getValue();
-                            if (brokerAddr.equals(brokerAddrFound)) {
-                                brokerNameFound = brokerData.getBrokerName();
-                                it.remove();
-                                log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
-                                    brokerId, brokerAddr);
-                                break;
-                            }
-                        }
+                    pullRequest.setLockedFirst(true);
+                    pullRequest.setNextOffset(offset);
+                }
+            } else {
+                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+                log.info("pull message later because not locked in broker, {}", pullRequest);
+                return;
+            }
+        }
 
-                        if (brokerData.getBrokerAddrs().isEmpty()) {
-                            removeBrokerName = true;
-                            itBrokerAddrTable.remove();
-                            log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
-                                brokerData.getBrokerName());
-                        }
-                    }
+        // 以PullRequest.messageQueue对象的topic值为参数从RebalanceImpl.subscriptionInner: ConcurrentHashMap, SubscriptionData>中获取对应的SubscriptionData对象,若该对象为null,考虑到并发的关系,调用executePullRequestLater方法,稍后重试;并跳出该方法;
+        final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
+        if (null == subscriptionData) {
+            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+            log.warn("find the consumer's subscription failed, {}", pullRequest);
+            return;
+        }
 
-                    if (brokerNameFound != null && removeBrokerName) {
-                        Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
-                        while (it.hasNext()) {
-                            Entry<String, Set<String>> entry = it.next();
-                            String clusterName = entry.getKey();
-                            Set<String> brokerNames = entry.getValue();
-                            boolean removed = brokerNames.remove(brokerNameFound);
-                            if (removed) {
-                                log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
-                                    brokerNameFound, clusterName);
+        final long beginTimestamp = System.currentTimeMillis();
 
-                                if (brokerNames.isEmpty()) {
-                                    log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
-                                        clusterName);
-                                    it.remove();
-                                }
+        // 异步拉取回调,先不讨论细节
+        PullCallback pullCallback = new PullCallback() {
+            @Override
+            public void onSuccess(PullResult pullResult) {
+                if (pullResult != null) {
+                    pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
+                        subscriptionData);
 
-                                break;
-                            }
-                        }
-                    }
+                    switch (pullResult.getPullStatus()) {
+                        case FOUND:
+                            long prevRequestOffset = pullRequest.getNextOffset();
+                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+                            long pullRT = System.currentTimeMillis() - beginTimestamp;
+                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
+                                pullRequest.getMessageQueue().getTopic(), pullRT);
 
-                    if (removeBrokerName) {
-                        Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
-                            this.topicQueueTable.entrySet().iterator();
-                        while (itTopicQueueTable.hasNext()) {
-                            Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
-                            String topic = entry.getKey();
-                            List<QueueData> queueDataList = entry.getValue();
+                            long firstMsgOffset = Long.MAX_VALUE;
+                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
+                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+                            } else {
+                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
 
-                            Iterator<QueueData> itQueueData = queueDataList.iterator();
-                            while (itQueueData.hasNext()) {
-                                QueueData queueData = itQueueData.next();
-                                if (queueData.getBrokerName().equals(brokerNameFound)) {
-                                    itQueueData.remove();
-                                    log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
-                                        topic, queueData);
-                                }
-                            }
+                                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
+                                    pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
 
-                            if (queueDataList.isEmpty()) {
-                                itTopicQueueTable.remove();
-                                log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
-                                    topic);
-                            }
-                        }
-                    }
-                } finally {
-                    this.lock.writeLock().unlock();
-                }
-            } catch (Exception e) {
-                log.error("onChannelDestroy Exception", e);
-            }
-        }
-    }
+ boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); + DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( + pullResult.getMsgFoundList(), + processQueue, + pullRequest.getMessageQueue(), + dispatchToConsume); -

第二个是每10分钟打印一次NameServer的配置参数。即KVConfigManager.configTable变量的内容。

-
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+                                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
+                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
+                                        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
+                                } else {
+                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+                                }
+                            }
 
-            @Override
-            public void run() {
-                NamesrvController.this.kvConfigManager.printAllPeriodically();
-            }
-        }, 1, 10, TimeUnit.MINUTES);
+ if (pullResult.getNextBeginOffset() < prevRequestOffset + || firstMsgOffset < prevRequestOffset) { + log.warn( + "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}", + pullResult.getNextBeginOffset(), + firstMsgOffset, + prevRequestOffset); + } -

然后这个初始化就差不多完成了,后面只需要把remotingServer start一下就好了

-

处理请求

直接上代码,其实主体是swtich case去判断

-
@Override
-    public RemotingCommand processRequest(ChannelHandlerContext ctx,
-        RemotingCommand request) throws RemotingCommandException {
+                            break;
+                        case NO_NEW_MSG:
+                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 
-        if (ctx != null) {
-            log.debug("receive request, {} {} {}",
-                request.getCode(),
-                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
-                request);
-        }
+                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
 
+                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+                            break;
+                        case NO_MATCHED_MSG:
+                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 
-        switch (request.getCode()) {
-            case RequestCode.PUT_KV_CONFIG:
-                return this.putKVConfig(ctx, request);
-            case RequestCode.GET_KV_CONFIG:
-                return this.getKVConfig(ctx, request);
-            case RequestCode.DELETE_KV_CONFIG:
-                return this.deleteKVConfig(ctx, request);
-            case RequestCode.QUERY_DATA_VERSION:
-                return queryBrokerTopicConfig(ctx, request);
-            case RequestCode.REGISTER_BROKER:
-                Version brokerVersion = MQVersion.value2Version(request.getVersion());
-                if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
-                    return this.registerBrokerWithFilterServer(ctx, request);
-                } else {
-                    return this.registerBroker(ctx, request);
-                }
-            case RequestCode.UNREGISTER_BROKER:
-                return this.unregisterBroker(ctx, request);
-            case RequestCode.GET_ROUTEINTO_BY_TOPIC:
-                return this.getRouteInfoByTopic(ctx, request);
-            case RequestCode.GET_BROKER_CLUSTER_INFO:
-                return this.getBrokerClusterInfo(ctx, request);
-            case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
-                return this.wipeWritePermOfBroker(ctx, request);
-            case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
-                return getAllTopicListFromNameserver(ctx, request);
-            case RequestCode.DELETE_TOPIC_IN_NAMESRV:
-                return deleteTopicInNamesrv(ctx, request);
-            case RequestCode.GET_KVLIST_BY_NAMESPACE:
-                return this.getKVListByNamespace(ctx, request);
-            case RequestCode.GET_TOPICS_BY_CLUSTER:
-                return this.getTopicsByCluster(ctx, request);
-            case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
-                return this.getSystemTopicListFromNs(ctx, request);
-            case RequestCode.GET_UNIT_TOPIC_LIST:
-                return this.getUnitTopicList(ctx, request);
-            case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
-                return this.getHasUnitSubTopicList(ctx, request);
-            case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
-                return this.getHasUnitSubUnUnitTopicList(ctx, request);
-            case RequestCode.UPDATE_NAMESRV_CONFIG:
-                return this.updateConfig(ctx, request);
-            case RequestCode.GET_NAMESRV_CONFIG:
-                return this.getConfig(ctx, request);
-            default:
-                break;
-        }
-        return null;
-    }
+ DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest); -

以broker注册为例,

-
case RequestCode.REGISTER_BROKER:
-                Version brokerVersion = MQVersion.value2Version(request.getVersion());
-                if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
-                    return this.registerBrokerWithFilterServer(ctx, request);
-                } else {
-                    return this.registerBroker(ctx, request);
-                }
+ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); + break; + case OFFSET_ILLEGAL: + log.warn("the pull request offset illegal, {} {}", + pullRequest.toString(), pullResult.toString()); + pullRequest.setNextOffset(pullResult.getNextBeginOffset()); -

做了个简单的版本管理,我们看下前面一个的代码

-
public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
-    throws RemotingCommandException {
-    final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
-    final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
-    final RegisterBrokerRequestHeader requestHeader =
-        (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);
+                            pullRequest.getProcessQueue().setDropped(true);
+                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
 
-    if (!checksum(ctx, request, requestHeader)) {
-        response.setCode(ResponseCode.SYSTEM_ERROR);
-        response.setRemark("crc32 not match");
-        return response;
-    }
+                                @Override
+                                public void run() {
+                                    try {
+                                        DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
+                                            pullRequest.getNextOffset(), false);
 
-    RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
+                                        DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
 
-    if (request.getBody() != null) {
-        try {
-            registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
-        } catch (Exception e) {
-            throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
-        }
-    } else {
-        registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
-        registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
-    }
+                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
 
-    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
-        requestHeader.getClusterName(),
-        requestHeader.getBrokerAddr(),
-        requestHeader.getBrokerName(),
-        requestHeader.getBrokerId(),
-        requestHeader.getHaServerAddr(),
-        registerBrokerBody.getTopicConfigSerializeWrapper(),
-        registerBrokerBody.getFilterServerList(),
-        ctx.channel());
+                                        log.warn("fix the pull request offset, {}", pullRequest);
+                                    } catch (Throwable e) {
+                                        log.error("executeTaskLater Exception", e);
+                                    }
+                                }
+                            }, 10000);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
 
-    responseHeader.setHaServerAddr(result.getHaServerAddr());
-    responseHeader.setMasterAddr(result.getMasterAddr());
+            @Override
+            public void onException(Throwable e) {
+                if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
+                    log.warn("execute the pull request exception", e);
+                }
 
-    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
-    response.setBody(jsonValue);
+                DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+            }
+        };
+        // 如果为集群模式,即可置commitOffsetEnable为 true
+        boolean commitOffsetEnable = false;
+        long commitOffsetValue = 0L;
+        if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
+            commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
+            if (commitOffsetValue > 0) {
+                commitOffsetEnable = true;
+            }
+        }
 
-    response.setCode(ResponseCode.SUCCESS);
-    response.setRemark(null);
-    return response;
-}
+ // 将上面获得的commitOffsetEnable更新到订阅关系里 + String subExpression = null; + boolean classFilter = false; + SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic()); + if (sd != null) { + if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) { + subExpression = sd.getSubString(); + } -

可以看到主要的逻辑还是在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker这个方法里

-
public RegisterBrokerResult registerBroker(
-        final String clusterName,
-        final String brokerAddr,
-        final String brokerName,
-        final long brokerId,
-        final String haServerAddr,
-        final TopicConfigSerializeWrapper topicConfigWrapper,
-        final List<String> filterServerList,
-        final Channel channel) {
-        RegisterBrokerResult result = new RegisterBrokerResult();
-        try {
-            try {
-                this.lock.writeLock().lockInterruptibly();
+            classFilter = sd.isClassFilterMode();
+        }
 
-              // 更新这个clusterAddrTable
-                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
-                if (null == brokerNames) {
-                    brokerNames = new HashSet<String>();
-                    this.clusterAddrTable.put(clusterName, brokerNames);
-                }
-                brokerNames.add(brokerName);
+        // 组成 sysFlag
+        int sysFlag = PullSysFlag.buildSysFlag(
+            commitOffsetEnable, // commitOffset
+            true, // suspend
+            subExpression != null, // subscription
+            classFilter // class filter
+        );
+        // 调用真正的拉取消息接口
+        try {
+            this.pullAPIWrapper.pullKernelImpl(
+                pullRequest.getMessageQueue(),
+                subExpression,
+                subscriptionData.getExpressionType(),
+                subscriptionData.getSubVersion(),
+                pullRequest.getNextOffset(),
+                this.defaultMQPushConsumer.getPullBatchSize(),
+                sysFlag,
+                commitOffsetValue,
+                BROKER_SUSPEND_MAX_TIME_MILLIS,
+                CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
+                CommunicationMode.ASYNC,
+                pullCallback
+            );
+        } catch (Exception e) {
+            log.error("pullKernelImpl exception", e);
+            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+        }
+    }
+

以下就是拉取消息的底层 api,不够不是特别复杂,主要是在找 broker,和设置请求参数

+
public PullResult pullKernelImpl(
+    final MessageQueue mq,
+    final String subExpression,
+    final String expressionType,
+    final long subVersion,
+    final long offset,
+    final int maxNums,
+    final int sysFlag,
+    final long commitOffset,
+    final long brokerSuspendMaxTimeMillis,
+    final long timeoutMillis,
+    final CommunicationMode communicationMode,
+    final PullCallback pullCallback
+) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
+    FindBrokerResult findBrokerResult =
+        this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
+            this.recalculatePullFromWhichNode(mq), false);
+    if (null == findBrokerResult) {
+        this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
+        findBrokerResult =
+            this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
+                this.recalculatePullFromWhichNode(mq), false);
+    }
 
-                boolean registerFirst = false;
+    if (findBrokerResult != null) {
+        {
+            // check version
+            if (!ExpressionType.isTagType(expressionType)
+                && findBrokerResult.getBrokerVersion() < MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) {
+                throw new MQClientException("The broker[" + mq.getBrokerName() + ", "
+                    + findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null);
+            }
+        }
+        int sysFlagInner = sysFlag;
 
-              // 更新brokerAddrTable
-                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
-                if (null == brokerData) {
-                    registerFirst = true;
-                    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
-                    this.brokerAddrTable.put(brokerName, brokerData);
-                }
-                Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
-                //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
-                //The same IP:PORT must only have one record in brokerAddrTable
-                Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
-                while (it.hasNext()) {
-                    Entry<Long, String> item = it.next();
-                    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
-                        it.remove();
-                    }
-                }
+        if (findBrokerResult.isSlave()) {
+            sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
+        }
 
-                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
-                registerFirst = registerFirst || (null == oldAddr);
+        PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
+        requestHeader.setConsumerGroup(this.consumerGroup);
+        requestHeader.setTopic(mq.getTopic());
+        requestHeader.setQueueId(mq.getQueueId());
+        requestHeader.setQueueOffset(offset);
+        requestHeader.setMaxMsgNums(maxNums);
+        requestHeader.setSysFlag(sysFlagInner);
+        requestHeader.setCommitOffset(commitOffset);
+        requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);
+        requestHeader.setSubscription(subExpression);
+        requestHeader.setSubVersion(subVersion);
+        requestHeader.setExpressionType(expressionType);
 
-              // 更新了org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable中的数据
-                if (null != topicConfigWrapper
-                    && MixAll.MASTER_ID == brokerId) {
-                    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
-                        || registerFirst) {
-                        ConcurrentMap<String, TopicConfig> tcTable =
-                            topicConfigWrapper.getTopicConfigTable();
-                        if (tcTable != null) {
-                            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
-                                this.createAndUpdateQueueData(brokerName, entry.getValue());
-                            }
-                        }
-                    }
-                }
+        String brokerAddr = findBrokerResult.getBrokerAddr();
+        if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
+            brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
+        }
+
+        PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
+            brokerAddr,
+            requestHeader,
+            timeoutMillis,
+            communicationMode,
+            pullCallback);
+
+        return pullResult;
+    }
+
+    throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
+}
+

再看下一步的

+
public PullResult pullMessage(
+    final String addr,
+    final PullMessageRequestHeader requestHeader,
+    final long timeoutMillis,
+    final CommunicationMode communicationMode,
+    final PullCallback pullCallback
+) throws RemotingException, MQBrokerException, InterruptedException {
+    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);
 
-              // 更新活跃broker信息
-                BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
-                    new BrokerLiveInfo(
-                        System.currentTimeMillis(),
-                        topicConfigWrapper.getDataVersion(),
-                        channel,
-                        haServerAddr));
-                if (null == prevBrokerLiveInfo) {
-                    log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
-                }
+    switch (communicationMode) {
+        case ONEWAY:
+            assert false;
+            return null;
+        case ASYNC:
+            this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
+            return null;
+        case SYNC:
+            return this.pullMessageSync(addr, request, timeoutMillis);
+        default:
+            assert false;
+            break;
+    }
 
-              // 处理filter
-                if (filterServerList != null) {
-                    if (filterServerList.isEmpty()) {
-                        this.filterServerTable.remove(brokerAddr);
-                    } else {
-                        this.filterServerTable.put(brokerAddr, filterServerList);
-                    }
-                }
+    return null;
+}
+

通过 communicationMode 判断是同步拉取还是异步拉取,异步就调用

+
private void pullMessageAsync(
+        final String addr,
+        final RemotingCommand request,
+        final long timeoutMillis,
+        final PullCallback pullCallback
+    ) throws RemotingException, InterruptedException {
+        this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
+            @Override
+            public void operationComplete(ResponseFuture responseFuture) {
+                异步
+                RemotingCommand response = responseFuture.getResponseCommand();
+                if (response != null) {
+                    try {
+                        PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response);
+                        assert pullResult != null;
+                        pullCallback.onSuccess(pullResult);
+                    } catch (Exception e) {
+                        pullCallback.onException(e);
+                    }
+                } else {
+                    if (!responseFuture.isSendRequestOK()) {
+                        pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause()));
+                    } else if (responseFuture.isTimeout()) {
+                        pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request,
+                            responseFuture.getCause()));
+                    } else {
+                        pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause()));
+                    }
+                }
+            }
+        });
+    }
+

并且会调用前面 pullCallback 的onSuccess和onException方法,同步的就是调用

+
private PullResult pullMessageSync(
+        final String addr,
+        final RemotingCommand request,
+        final long timeoutMillis
+    ) throws RemotingException, InterruptedException, MQBrokerException {
+        RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
+        assert response != null;
+        return this.processPullResponse(response);
+    }
+

然后就是这个 remotingClient 的 invokeAsync 跟 invokeSync 方法

+
@Override
+    public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
+        throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
+        RemotingSendRequestException {
+        long beginStartTime = System.currentTimeMillis();
+        final Channel channel = this.getAndCreateChannel(addr);
+        if (channel != null && channel.isActive()) {
+            try {
+                doBeforeRpcHooks(addr, request);
+                long costTime = System.currentTimeMillis() - beginStartTime;
+                if (timeoutMillis < costTime) {
+                    throw new RemotingTooMuchRequestException("invokeAsync call timeout");
+                }
+                this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, invokeCallback);
+            } catch (RemotingSendRequestException e) {
+                log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
+                this.closeChannel(addr, channel);
+                throw e;
+            }
+        } else {
+            this.closeChannel(addr, channel);
+            throw new RemotingConnectException(addr);
+        }
+    }
+@Override
+    public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
+        throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
+        long beginStartTime = System.currentTimeMillis();
+        final Channel channel = this.getAndCreateChannel(addr);
+        if (channel != null && channel.isActive()) {
+            try {
+                doBeforeRpcHooks(addr, request);
+                long costTime = System.currentTimeMillis() - beginStartTime;
+                if (timeoutMillis < costTime) {
+                    throw new RemotingTimeoutException("invokeSync call timeout");
+                }
+                RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
+                doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
+                return response;
+            } catch (RemotingSendRequestException e) {
+                log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
+                this.closeChannel(addr, channel);
+                throw e;
+            } catch (RemotingTimeoutException e) {
+                if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
+                    this.closeChannel(addr, channel);
+                    log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
+                }
+                log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
+                throw e;
+            }
+        } else {
+            this.closeChannel(addr, channel);
+            throw new RemotingConnectException(addr);
+        }
+    }
+

再往下看

+
public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
+        final long timeoutMillis)
+        throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
+        final int opaque = request.getOpaque();
 
-              // 当当前broker非master时返回master信息
-                if (MixAll.MASTER_ID != brokerId) {
-                    String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
-                    if (masterAddr != null) {
-                        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
-                        if (brokerLiveInfo != null) {
-                            result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
-                            result.setMasterAddr(masterAddr);
-                        }
-                    }
-                }
-            } finally {
-                this.lock.writeLock().unlock();
-            }
-        } catch (Exception e) {
-            log.error("registerBroker Exception", e);
-        }
+        try {
+            同步跟异步都是会把结果用ResponseFuture抱起来
+            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
+            this.responseTable.put(opaque, responseFuture);
+            final SocketAddress addr = channel.remoteAddress();
+            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
+                @Override
+                public void operationComplete(ChannelFuture f) throws Exception {
+                    if (f.isSuccess()) {
+                        responseFuture.setSendRequestOK(true);
+                        return;
+                    } else {
+                        responseFuture.setSendRequestOK(false);
+                    }
 
-        return result;
-    }
+ responseTable.remove(opaque); + responseFuture.setCause(f.cause()); + responseFuture.putResponse(null); + log.warn("send a request command to channel <" + addr + "> failed."); + } + }); + // 区别是同步的是在这等待 + RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis); + if (null == responseCommand) { + if (responseFuture.isSendRequestOK()) { + throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis, + responseFuture.getCause()); + } else { + throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause()); + } + } -

这个是注册 broker 的逻辑,再看下根据 topic 获取 broker 信息和 topic 信息,org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic 主要是这个方法的逻辑

-
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
-        RemotingCommand request) throws RemotingCommandException {
-        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
-        final GetRouteInfoRequestHeader requestHeader =
-            (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
+            return responseCommand;
+        } finally {
+            this.responseTable.remove(opaque);
+        }
+    }
 
-        TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
+    public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
+        final InvokeCallback invokeCallback)
+        throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
+        long beginStartTime = System.currentTimeMillis();
+        final int opaque = request.getOpaque();
+        boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
+        if (acquired) {
+            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
+            long costTime = System.currentTimeMillis() - beginStartTime;
+            if (timeoutMillis < costTime) {
+                once.release();
+                throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
+            }
 
-        if (topicRouteData != null) {
-            if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
-                String orderTopicConf =
-                    this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
-                        requestHeader.getTopic());
-                topicRouteData.setOrderTopicConf(orderTopicConf);
-            }
+            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
+            this.responseTable.put(opaque, responseFuture);
+            try {
+                channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
+                    @Override
+                    public void operationComplete(ChannelFuture f) throws Exception {
+                        if (f.isSuccess()) {
+                            responseFuture.setSendRequestOK(true);
+                            return;
+                        }
+                        requestFail(opaque);
+                        log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
+                    }
+                });
+            } catch (Exception e) {
+                responseFuture.release();
+                log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
+                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
+            }
+        } else {
+            if (timeoutMillis <= 0) {
+                throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
+            } else {
+                String info =
+                    String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
+                        timeoutMillis,
+                        this.semaphoreAsync.getQueueLength(),
+                        this.semaphoreAsync.availablePermits()
+                    );
+                log.warn(info);
+                throw new RemotingTimeoutException(info);
+            }
+        }
+    }
- byte[] content = topicRouteData.encode(); - response.setBody(content); - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - return response; - } - response.setCode(ResponseCode.TOPIC_NOT_EXIST); - response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic() - + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); - return response; - }
-

首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteDataorg.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。

-

简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。

]]>
MQ @@ -10563,7 +10418,7 @@ public: 削峰填谷 中间件 源码解析 - NameServer + DefaultMQPushConsumer
@@ -10702,6 +10557,63 @@ public:

这里也可以看到 ConsumeQueue 的存储格式,

AA6Tve

偏移量,消息大小,跟 tag 的 hashCode

+]]> + + MQ + RocketMQ + 消息队列 + + + MQ + 消息队列 + RocketMQ + +
+ + 聊一下 RocketMQ 的消息存储之 MMAP + /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

+
read(file, tmp_buf, len);
+write(socket, tmp_buf, len);
+ + + +

vms95Z

+

如上面的图显示的,要在用户态跟内核态进行切换,数据还需要在内核缓冲跟用户缓冲之间拷贝多次,

+
+
    +
  1. 第一步是调用 read,需要在用户态切换成内核态,DMA模块从磁盘中读取文件,并存储在内核缓冲区,相当于是第一次复制
  2. +
  3. 数据从内核缓冲区被拷贝到用户缓冲区,read 调用返回,伴随着内核态又切换成用户态,完成了第二次复制
  4. +
  5. 然后是write 写入,这里也会伴随着用户态跟内核态的切换,数据从用户缓冲区被复制到内核空间缓冲区,完成了第三次复制,这次有点不一样的是数据不是在内核缓冲区了,会复制到 socket buffer 中。
  6. +
  7. write 系统调用返回,又切换回了用户态,然后数据由 DMA 拷贝到协议引擎。
  8. +
+
+

如此就能看出其实默认的读写操作代价是非常大的,而在 rocketmq 等高性能中间件中都有使用的零拷贝技术,其中 rocketmq 使用的是 mmap

+

mmap

mmap基于 OS 的 mmap 的内存映射技术,通过MMU 映射文件,将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作,使随机读写文件和读写内存相似的速度。

+
+

mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

+
+
tmp_buf = mmap(file, len);
+write(socket, tmp_buf, len);
+ +

I68mFx

+
+

第一步:mmap系统调用使得文件内容被DMA引擎复制到内核缓冲区。然后该缓冲区与用户进程共享,在内核和用户内存空间之间不进行任何拷贝。

+

第二步:写系统调用使得内核将数据从原来的内核缓冲区复制到与套接字相关的内核缓冲区。

+

第三步:第三次拷贝发生在DMA引擎将数据从内核套接字缓冲区传递给协议引擎时。

+

通过使用mmap而不是read,我们将内核需要拷贝的数据量减少了一半。当大量的数据被传输时,这将有很好的效果。然而,这种改进并不是没有代价的;在使用mmap+write方法时,有一些隐藏的陷阱。例如当你对一个文件进行内存映射,然后在另一个进程截断同一文件时调用写。你的写系统调用将被总线错误信号SIGBUS打断,因为你执行了一个错误的内存访问。该信号的默认行为是杀死进程并dumpcore–这对网络服务器来说不是最理想的操作。

+

有两种方法可以解决这个问题。

+

第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用返回。通过这样做,写系统调用会返回它在被打断之前所写的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,一个治标不治本的解决方案。因为SIGBUS预示着进程出了严重的问题,所以不鼓励使用这种解决方案。

+

第二个解决方案涉及内核的文件租赁(在Windows中称为 “机会锁”)。这是解决这个问题的正确方法。通过在文件描述符上使用租赁,你与内核在一个特定的文件上达成租约。然后你可以向内核请求一个读/写租约。当另一个进程试图截断你正在传输的文件时,内核会向你发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉你内核即将终止你对该文件的写或读租约。在你的程序访问一个无效的地址和被SIGBUS信号杀死之前,你的写调用会被打断了。写入调用的返回值是中断前写入的字节数,errno将被设置为成功。下面是一些示例代码,显示了如何从内核中获得租约。

+
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
+    perror("kernel lease set signal");
+    return -1;
+}
+/* l_type can be F_RDLCK F_WRLCK */
+if(fcntl(fd, F_SETLEASE, l_type)){
+    perror("kernel lease set type");
+    return -1;
+}
]]>
MQ @@ -11096,185 +11008,91 @@ public:
- 聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点 - /2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ - 这个话题是由一次组内同学分享引出来的,首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现,这里也稍微讲一下,在一个 1.x 的老项目里,可以看到AopAutoConfiguration 是这样的

-
@Configuration
-@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
-@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
-public class AopAutoConfiguration {
-
-	@Configuration
-	@EnableAspectJAutoProxy(proxyTargetClass = false)
-	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
-	public static class JdkDynamicAutoProxyConfiguration {
-	}
-
-	@Configuration
-	@EnableAspectJAutoProxy(proxyTargetClass = true)
-	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
-	public static class CglibAutoProxyConfiguration {
-	}
-
-}
- -

而在 2.x 中变成了这样

-
@Configuration(proxyBeanMethods = false)
-@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
-public class AopAutoConfiguration {
-
-	@Configuration(proxyBeanMethods = false)
-	@ConditionalOnClass(Advice.class)
-	static class AspectJAutoProxyingConfiguration {
-
-		@Configuration(proxyBeanMethods = false)
-		@EnableAspectJAutoProxy(proxyTargetClass = false)
-		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
-		static class JdkDynamicAutoProxyConfiguration {
-
-		}
-
-		@Configuration(proxyBeanMethods = false)
-		@EnableAspectJAutoProxy(proxyTargetClass = true)
-		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
-				matchIfMissing = true)
-		static class CglibAutoProxyConfiguration {
-
-		}
-
-	}
- -

为何会加载 AopAutoConfiguration 在前面的文章聊聊 SpringBoot 自动装配里已经介绍过,有兴趣的可以看下,可以发现 springboot 在 2.x 版本开始使用 cglib 作为默认的动态代理实现。

-

然后就是出现的问题了,代码是这样的,一个简单的基于 springboot 的带有数据库的插入,对插入代码加了事务注解,

-
@Mapper
-public interface StudentMapper {
-		// 就是插入一条数据
-    @Insert("insert into student(name, age)" + "values ('nick', '18')")
-    public Long insert();
-}
-
-@Component
-public class StudentManager {
-
-    @Resource
-    private StudentMapper studentMapper;
-    
-    public Long createStudent() {
-        return studentMapper.insert();
-    }
-}
-
-@Component
-public class StudentServiceImpl implements StudentService {
-
-    @Resource
-    private StudentManager studentManager;
-
-    // 自己引用
-    @Resource
-    private StudentServiceImpl studentService;
-
-    @Override
-    @Transactional
-    public Long createStudent() {
-        Long id = studentManager.createStudent();
-        Long id2 = studentService.createStudent2();
-        return 1L;
-    }
-
-    @Transactional
-    private Long createStudent2() {
-//        Integer t = Integer.valueOf("aaa");
-        return studentManager.createStudent();
-    }
-}
- -

第一个公有方法 createStudent 首先调用了 manager 层的创建方法,然后再通过引入的 studentService 调用了createStudent2,我们先跑一下看看会出现啥情况,果不其然报错了,正是这个报错让我纠结了很久

-

EdR7oB

-

报了个空指针,而且是在 createStudent2 已经被调用到了,在它的内部,报的 studentManager 是 null,首先 cglib 作为动态代理它是通过继承的方式来实现的,相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类,具体的代理切面逻辑在子类实现,然后在调用目标对象的目标方法,但是继承的方式对于 final 和私有方法其实是没法进行代理的,因为没法继承,所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了,也就是不会进入这个方法内部,后面才发现犯了个特别二的错误,继承的方式去调用父类的私有方法,对于 Java 来说是可以调用到的,父类的私有方法并不由子类的InstanceKlass维护,只能通过子类的InstanceKlass找到Java类对应的_super,这样间接地访问。也就是说子类其实是可以访问的,那为啥访问了会报空指针呢,这里报的是studentManager 是空的,可以往依赖注入方面去想,如果忽略依赖注入,我这个studentManager 的确是 null,那是不是就没有被依赖注入呢,但是为啥前面那个可以呢

-

这个问题着实查了很久,不废话来看代码

-
@Override
-		protected Object invokeJoinpoint() throws Throwable {
-			if (this.methodProxy != null) {
-        // 这里的 target 就是被代理的 bean
-				return this.methodProxy.invoke(this.target, this.arguments);
-			}
-			else {
-				return super.invokeJoinpoint();
-			}
-		}
- - - -

这个是org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation的代码,其实它在这里不是直接调用 super 也就是父类的方法,而是通过 methodProxy 调用 target 目标对象的方法,也就是原始的 studentService bean 的方法,这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用,否则就会出现上面的问题,因为 cglib 其实是通过继承来实现,通过将调用转移到子类上加入代理逻辑,我们在简单使用的时候会直接 invokeSuper() 调用父类的方法,但是在这里 spring 的场景里需要去支持 spring 的功能逻辑,所以上面的问题就可以开始来解释了,因为 createStudent 是公共方法,cglib 可以对其进行继承代理,但是在执行逻辑的时候其实是通过调用目标对象,也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent,而对于下面的 createStudent2 方法因为是私有方法,不会走代理逻辑,也就不会有调用回目标对象的逻辑,只是通过继承关系,在子类中没有这个方法,所以会通过子类的InstanceKlass找到这个类对应的_super,然后调用父类的这个私有方法,这里要搞清楚一个点,从这个代理类直接找到其父类然后调用这个私有方法,这个类是由 cglib 生成的,不是被 spring 管理起来经过依赖注入的 bean,所以是没有 studentManager 这个依赖的,也就出现了前面的问题

-

而在前面提到的cglib通过methodProxy调用到目标对象,目标对象是在什么时候设置的呢,其实是在bean的生命周期中,org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中,会调用实现了这个接口的方法,

-
@Override
-public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
-	if (bean != null) {
-		Object cacheKey = getCacheKey(bean.getClass(), beanName);
-		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
-			return wrapIfNecessary(bean, beanName, cacheKey);
-		}
-	}
-	return bean;
-}
- -

具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里

-
protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
-		if (StringUtils.hasLength(beanName)) {
-			return (FactoryBean.class.isAssignableFrom(beanClass) ?
-					BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
-		}
-		else {
-			return beanClass;
-		}
-	}
-
-	/**
-	 * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
-	 * @param bean the raw bean instance
-	 * @param beanName the name of the bean
-	 * @param cacheKey the cache key for metadata access
-	 * @return a proxy wrapping the bean, or the raw bean instance as-is
-	 */
-	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
-		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
-			return bean;
-		}
-		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
-			return bean;
-		}
-		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
-			this.advisedBeans.put(cacheKey, Boolean.FALSE);
-			return bean;
-		}
-
-		// Create proxy if we have advice.
-		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
-		if (specificInterceptors != DO_NOT_PROXY) {
-			this.advisedBeans.put(cacheKey, Boolean.TRUE);
-			Object proxy = createProxy(
-					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
-			this.proxyTypes.put(cacheKey, proxy.getClass());
-			return proxy;
-		}
-
-		this.advisedBeans.put(cacheKey, Boolean.FALSE);
-		return bean;
-	}
- -

然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类

+ 搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答 + /2022/01/16/%E6%90%AC%E8%BF%90%E4%B8%A4%E4%B8%AA-StackOverflow-%E4%B8%8A%E7%9A%84-Mysql-%E7%BC%96%E7%A0%81%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94/ + Mysql 字符编码和排序规则

这个一直是属于一知半解的状态,知道 utf8 跟 utf8mb4 的区别主要是能不能支持 emoji,但是具体后面配置的排序规则是用来干嘛,或者有什么区别,应该使用哪个,所以在 stackoverflow 上找了下,有两个比较不错的解答,就搬过来并且配合机翻做了点修改

+

原文

For those people still arriving at this question in 2020 or later, there are newer options that may be better than both of these. For example, utf8mb4_0900_ai_ci.

+

All these collations are for the UTF-8 character encoding. The differences are in how text is sorted and compared.

+

_unicode_ci and _general_ci are two different sets of rules for sorting and comparing text according to the way we expect. Newer versions of MySQL introduce new sets of rules, too, such as _0900_ai_ci for equivalent rules based on Unicode 9.0 - and with no equivalent _general_ci variant. People reading this now should probably use one of these newer collations instead of either _unicode_ci or _general_ci. The description of those older collations below is provided for interest only.

+

MySQL is currently transitioning away from an older, flawed UTF-8 implementation. For now, you need to use utf8mb4 instead of utf8 for the character encoding part, to ensure you are getting the fixed version. The flawed version remains for backward compatibility, though it is being deprecated.

+

Key differences

+

utf8mb4_unicode_ci is based on the official Unicode rules for universal sorting and comparison, which sorts accurately in a wide range of languages.

+

utf8mb4_general_ci is a simplified set of sorting rules which aims to do as well as it can while taking many short-cuts designed to improve speed. It does not follow the Unicode rules and will result in undesirable sorting or comparison in some situations, such as when using particular languages or characters.

+

On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.

+

Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci

+

utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.

+

As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:

+

For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).

+

Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.

+

In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.

+

What should you use?

+

There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.

+

In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.

+

There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.

+

One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.

+

What the parts mean

+

Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.

+

Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.

+

And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.

+

翻译

对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci

+

所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。

+

_unicode_ci_general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci_general_ci。下面对那些较旧的排序规则的描述仅供参考。

+

MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。

+

主要区别

+

utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。

+

utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。

+

在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。

+

utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势

+

utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。

+

就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:

+

例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。

+

一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。

+

在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。

+

你应该用什么?

+

几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。

+

过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。

+

有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。

+

我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。

+

其余各个部分是什么意思

+

首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。

+

接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。

+

最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。

+

utf8 和 utf8mb4 编码有什么区别

原文

UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.

+

So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.

+

This is what (a previous version of the same page at)the MySQL documentationhas to say about it:

+
+

The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:

+
    +
  • For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
  • +
  • For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
  • +
+
+

So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.

+

译文

UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。

+

所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较

+

这就是(同一页面的先前版本)MySQL 文档 不得不说的:

+
+

名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:

+
    +
  • 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
  • +
  • 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
  • +
+
+

因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅

+

实际使用中最常见的非 BMP Unicode 字符是什么?

]]>
- Java - SpringBoot + Mysql - Java - Spring - SpringBoot - cglib - 事务 + mysql + 字符集 + 编码 + utf8 + utf8mb4 + utf8mb4_0900_ai_ci + utf8mb4_unicode_ci + utf8mb4_general_ci
@@ -11930,8 +11748,190 @@ app.setWebAp Java Spring SpringBoot - 自动装配 - AutoConfiguration + 自动装配 + AutoConfiguration + + + + 聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点 + /2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ + 这个话题是由一次组内同学分享引出来的,首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现,这里也稍微讲一下,在一个 1.x 的老项目里,可以看到AopAutoConfiguration 是这样的

+
@Configuration
+@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
+@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
+public class AopAutoConfiguration {
+
+	@Configuration
+	@EnableAspectJAutoProxy(proxyTargetClass = false)
+	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
+	public static class JdkDynamicAutoProxyConfiguration {
+	}
+
+	@Configuration
+	@EnableAspectJAutoProxy(proxyTargetClass = true)
+	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
+	public static class CglibAutoProxyConfiguration {
+	}
+
+}
+ +

而在 2.x 中变成了这样

+
@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
+public class AopAutoConfiguration {
+
+	@Configuration(proxyBeanMethods = false)
+	@ConditionalOnClass(Advice.class)
+	static class AspectJAutoProxyingConfiguration {
+
+		@Configuration(proxyBeanMethods = false)
+		@EnableAspectJAutoProxy(proxyTargetClass = false)
+		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
+		static class JdkDynamicAutoProxyConfiguration {
+
+		}
+
+		@Configuration(proxyBeanMethods = false)
+		@EnableAspectJAutoProxy(proxyTargetClass = true)
+		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
+				matchIfMissing = true)
+		static class CglibAutoProxyConfiguration {
+
+		}
+
+	}
+ +

为何会加载 AopAutoConfiguration 在前面的文章聊聊 SpringBoot 自动装配里已经介绍过,有兴趣的可以看下,可以发现 springboot 在 2.x 版本开始使用 cglib 作为默认的动态代理实现。

+

然后就是出现的问题了,代码是这样的,一个简单的基于 springboot 的带有数据库的插入,对插入代码加了事务注解,

+
@Mapper
+public interface StudentMapper {
+		// 就是插入一条数据
+    @Insert("insert into student(name, age)" + "values ('nick', '18')")
+    public Long insert();
+}
+
+@Component
+public class StudentManager {
+
+    @Resource
+    private StudentMapper studentMapper;
+    
+    public Long createStudent() {
+        return studentMapper.insert();
+    }
+}
+
+@Component
+public class StudentServiceImpl implements StudentService {
+
+    @Resource
+    private StudentManager studentManager;
+
+    // 自己引用
+    @Resource
+    private StudentServiceImpl studentService;
+
+    @Override
+    @Transactional
+    public Long createStudent() {
+        Long id = studentManager.createStudent();
+        Long id2 = studentService.createStudent2();
+        return 1L;
+    }
+
+    @Transactional
+    private Long createStudent2() {
+//        Integer t = Integer.valueOf("aaa");
+        return studentManager.createStudent();
+    }
+}
+ +

第一个公有方法 createStudent 首先调用了 manager 层的创建方法,然后再通过引入的 studentService 调用了createStudent2,我们先跑一下看看会出现啥情况,果不其然报错了,正是这个报错让我纠结了很久

+

EdR7oB

+

报了个空指针,而且是在 createStudent2 已经被调用到了,在它的内部,报的 studentManager 是 null,首先 cglib 作为动态代理它是通过继承的方式来实现的,相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类,具体的代理切面逻辑在子类实现,然后在调用目标对象的目标方法,但是继承的方式对于 final 和私有方法其实是没法进行代理的,因为没法继承,所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了,也就是不会进入这个方法内部,后面才发现犯了个特别二的错误,继承的方式去调用父类的私有方法,对于 Java 来说是可以调用到的,父类的私有方法并不由子类的InstanceKlass维护,只能通过子类的InstanceKlass找到Java类对应的_super,这样间接地访问。也就是说子类其实是可以访问的,那为啥访问了会报空指针呢,这里报的是studentManager 是空的,可以往依赖注入方面去想,如果忽略依赖注入,我这个studentManager 的确是 null,那是不是就没有被依赖注入呢,但是为啥前面那个可以呢

+

这个问题着实查了很久,不废话来看代码

+
@Override
+		protected Object invokeJoinpoint() throws Throwable {
+			if (this.methodProxy != null) {
+        // 这里的 target 就是被代理的 bean
+				return this.methodProxy.invoke(this.target, this.arguments);
+			}
+			else {
+				return super.invokeJoinpoint();
+			}
+		}
+ + + +

这个是org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation的代码,其实它在这里不是直接调用 super 也就是父类的方法,而是通过 methodProxy 调用 target 目标对象的方法,也就是原始的 studentService bean 的方法,这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用,否则就会出现上面的问题,因为 cglib 其实是通过继承来实现,通过将调用转移到子类上加入代理逻辑,我们在简单使用的时候会直接 invokeSuper() 调用父类的方法,但是在这里 spring 的场景里需要去支持 spring 的功能逻辑,所以上面的问题就可以开始来解释了,因为 createStudent 是公共方法,cglib 可以对其进行继承代理,但是在执行逻辑的时候其实是通过调用目标对象,也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent,而对于下面的 createStudent2 方法因为是私有方法,不会走代理逻辑,也就不会有调用回目标对象的逻辑,只是通过继承关系,在子类中没有这个方法,所以会通过子类的InstanceKlass找到这个类对应的_super,然后调用父类的这个私有方法,这里要搞清楚一个点,从这个代理类直接找到其父类然后调用这个私有方法,这个类是由 cglib 生成的,不是被 spring 管理起来经过依赖注入的 bean,所以是没有 studentManager 这个依赖的,也就出现了前面的问题

+

而在前面提到的cglib通过methodProxy调用到目标对象,目标对象是在什么时候设置的呢,其实是在bean的生命周期中,org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中,会调用实现了这个接口的方法,

+
@Override
+public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
+	if (bean != null) {
+		Object cacheKey = getCacheKey(bean.getClass(), beanName);
+		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
+			return wrapIfNecessary(bean, beanName, cacheKey);
+		}
+	}
+	return bean;
+}
+ +

具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里

+
protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
+		if (StringUtils.hasLength(beanName)) {
+			return (FactoryBean.class.isAssignableFrom(beanClass) ?
+					BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
+		}
+		else {
+			return beanClass;
+		}
+	}
+
+	/**
+	 * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
+	 * @param bean the raw bean instance
+	 * @param beanName the name of the bean
+	 * @param cacheKey the cache key for metadata access
+	 * @return a proxy wrapping the bean, or the raw bean instance as-is
+	 */
+	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
+		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
+			return bean;
+		}
+		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
+			return bean;
+		}
+		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
+			this.advisedBeans.put(cacheKey, Boolean.FALSE);
+			return bean;
+		}
+
+		// Create proxy if we have advice.
+		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
+		if (specificInterceptors != DO_NOT_PROXY) {
+			this.advisedBeans.put(cacheKey, Boolean.TRUE);
+			Object proxy = createProxy(
+					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
+			this.proxyTypes.put(cacheKey, proxy.getClass());
+			return proxy;
+		}
+
+		this.advisedBeans.put(cacheKey, Boolean.FALSE);
+		return bean;
+	}
+ +

然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类

+]]>
+ + Java + SpringBoot + + + Java + Spring + SpringBoot + cglib + 事务
@@ -11954,6 +11954,26 @@ app.setWebAp 射击 + + 聊在东京奥运会闭幕式这天 + /2021/08/08/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9/ + 这届奥运会有可能是我除了 08 年之外关注度最高的一届奥运会,原因可能是因为最近也没什么电影综艺啥的比较好看,前面看跑男倒还行,不是说多好,也就图一乐,最开始看向往的生活觉得也挺不错的,后面变成了统一来了就看黄磊做饭,然后夸黄磊做饭好吃,然后无聊的说这种生活多么多么美好,单调无聊,差不多弃了,这里面还包括大华不在了,大华其实个人还是有点呱噪的,但是挺能搞气氛,并且也有才华,彭彭跟子枫人是不讨厌,但是撑不起来,所以也导致了前面说的结果,都变成了黄磊彩虹屁现场,虽然偶尔怀疑他是否做得好吃,但是整体还是承认的,可对于一个这么多季了的综艺来说,这样也有点单调了。

+

还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。

+

这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

+

乒乓球的说的有点多,就分篇说了,第一篇先到这。

+]]>
+ + 生活 + 运动 + + + 生活 + 运动 + 东京奥运会 + 乒乓球 + 跳水 + +
聊聊 Dubbo 的 SPI 续之自适应拓展 /2020/06/06/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI-%E7%BB%AD%E4%B9%8B%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ @@ -12102,26 +12122,6 @@ app.setWebAp 自适应拓展 - - 聊在东京奥运会闭幕式这天 - /2021/08/08/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9/ - 这届奥运会有可能是我除了 08 年之外关注度最高的一届奥运会,原因可能是因为最近也没什么电影综艺啥的比较好看,前面看跑男倒还行,不是说多好,也就图一乐,最开始看向往的生活觉得也挺不错的,后面变成了统一来了就看黄磊做饭,然后夸黄磊做饭好吃,然后无聊的说这种生活多么多么美好,单调无聊,差不多弃了,这里面还包括大华不在了,大华其实个人还是有点呱噪的,但是挺能搞气氛,并且也有才华,彭彭跟子枫人是不讨厌,但是撑不起来,所以也导致了前面说的结果,都变成了黄磊彩虹屁现场,虽然偶尔怀疑他是否做得好吃,但是整体还是承认的,可对于一个这么多季了的综艺来说,这样也有点单调了。

-

还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。

-

这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

-

乒乓球的说的有点多,就分篇说了,第一篇先到这。

-]]>
- - 生活 - 运动 - - - 生活 - 运动 - 东京奥运会 - 乒乓球 - 跳水 - -
聊聊 Dubbo 的 SPI /2020/05/31/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI/ @@ -12430,261 +12430,92 @@ public Result invoke(final Invocation invocation) throws RpcException {

看下运行结果

https://img.nicksxs.com/uPic/V2l78m.png

可以看到是加上了 101 = 5 也就是偏向锁,后面是线程 id

-

当我再使用一个线程来竞争这个锁的时候

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-				Thread thread1 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread1 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                    try {
-                        TimeUnit.SECONDS.sleep(5L);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        };
-				thread1.start();
-		}
-}
- -

https://img.nicksxs.com/uPic/bRMvlR.png

-

可以看到变成了轻量级锁,在线程没有争抢,只是进行了切换,就会使用轻量级锁,当两个线程在竞争了,就又会升级成重量级锁

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-        Thread thread1 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread1 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                    try {
-                        TimeUnit.SECONDS.sleep(5L);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        };
-        Thread thread2 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread2 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                }
-            }
-        };
-        thread1.start();
-        thread2.start();
-    }
-}
-
-class L {
-    private boolean myboolean = true;
-}
- -

https://img.nicksxs.com/uPic/LMzMtR.png

-

可以看到变成了重量级锁。

-]]> - - Java - - - Java - Synchronized - 偏向锁 - 轻量级锁 - 重量级锁 - 自旋 - -
- - 聊聊 Java 中绕不开的 Synchronized 关键字 - /2021/06/20/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97/ - Synchronized 关键字在 Java 的并发体系里也是非常重要的一个内容,首先比较常规的是知道它使用的方式,可以锁对象,可以锁代码块,也可以锁方法,看一个简单的 demo

-
public class SynchronizedDemo {
-
-    public static void main(String[] args) {
-        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
-        synchronizedDemo.lockMethod();
-    }
-
-    public synchronized void lockMethod() {
-        System.out.println("here i'm locked");
-    }
-
-    public void lockSynchronizedDemo() {
-        synchronized (this) {
-            System.out.println("here lock class");
-        }
-    }
-}
- -

然后来查看反编译结果,其实代码(日光)之下并无新事,即使是完全不懂的也可以通过一些词义看出一些意义

-
  Last modified 2021620; size 729 bytes
-  MD5 checksum dd9c529863bd7ff839a95481db578ad9
-  Compiled from "SynchronizedDemo.java"
-public class SynchronizedDemo
-  minor version: 0
-  major version: 53
-  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
-  this_class: #2                          // SynchronizedDemo
-  super_class: #9                         // java/lang/Object
-  interfaces: 0, fields: 0, methods: 4, attributes: 1
-Constant pool:
-   #1 = Methodref          #9.#22         // java/lang/Object."<init>":()V
-   #2 = Class              #23            // SynchronizedDemo
-   #3 = Methodref          #2.#22         // SynchronizedDemo."<init>":()V
-   #4 = Methodref          #2.#24         // SynchronizedDemo.lockMethod:()V
-   #5 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
-   #6 = String             #27            // here i\'m locked
-   #7 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
-   #8 = String             #30            // here lock class
-   #9 = Class              #31            // java/lang/Object
-  #10 = Utf8               <init>
-  #11 = Utf8               ()V
-  #12 = Utf8               Code
-  #13 = Utf8               LineNumberTable
-  #14 = Utf8               main
-  #15 = Utf8               ([Ljava/lang/String;)V
-  #16 = Utf8               lockMethod
-  #17 = Utf8               lockSynchronizedDemo
-  #18 = Utf8               StackMapTable
-  #19 = Class              #32            // java/lang/Throwable
-  #20 = Utf8               SourceFile
-  #21 = Utf8               SynchronizedDemo.java
-  #22 = NameAndType        #10:#11        // "<init>":()V
-  #23 = Utf8               SynchronizedDemo
-  #24 = NameAndType        #16:#11        // lockMethod:()V
-  #25 = Class              #33            // java/lang/System
-  #26 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
-  #27 = Utf8               here i\'m locked
-  #28 = Class              #36            // java/io/PrintStream
-  #29 = NameAndType        #37:#38        // println:(Ljava/lang/String;)V
-  #30 = Utf8               here lock class
-  #31 = Utf8               java/lang/Object
-  #32 = Utf8               java/lang/Throwable
-  #33 = Utf8               java/lang/System
-  #34 = Utf8               out
-  #35 = Utf8               Ljava/io/PrintStream;
-  #36 = Utf8               java/io/PrintStream
-  #37 = Utf8               println
-  #38 = Utf8               (Ljava/lang/String;)V
-{
-  public SynchronizedDemo();
-    descriptor: ()V
-    flags: (0x0001) ACC_PUBLIC
-    Code:
-      stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
-         4: return
-      LineNumberTable:
-        line 5: 0
-
-  public static void main(java.lang.String[]);
-    descriptor: ([Ljava/lang/String;)V
-    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
-    Code:
-      stack=2, locals=2, args_size=1
-         0: new           #2                  // class SynchronizedDemo
-         3: dup
-         4: invokespecial #3                  // Method "<init>":()V
-         7: astore_1
-         8: aload_1
-         9: invokevirtual #4                  // Method lockMethod:()V
-        12: return
-      LineNumberTable:
-        line 8: 0
-        line 9: 8
-        line 10: 12
-
-  public synchronized void lockMethod();
-    descriptor: ()V
-    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
-    Code:
-      stack=2, locals=1, args_size=1
-         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
-         3: ldc           #6                  // String here i\'m locked
-         5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
-         8: return
-      LineNumberTable:
-        line 13: 0
-        line 14: 8
+

当我再使用一个线程来竞争这个锁的时候

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+				Thread thread1 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread1 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                    try {
+                        TimeUnit.SECONDS.sleep(5L);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+				thread1.start();
+		}
+}
- public void lockSynchronizedDemo(); - descriptor: ()V - flags: (0x0001) ACC_PUBLIC - Code: - stack=2, locals=3, args_size=1 - 0: aload_0 - 1: dup - 2: astore_1 - 3: monitorenter - 4: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; - 7: ldc #8 // String here lock class - 9: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V - 12: aload_1 - 13: monitorexit - 14: goto 22 - 17: astore_2 - 18: aload_1 - 19: monitorexit - 20: aload_2 - 21: athrow - 22: return - Exception table: - from to target type - 4 14 17 any - 17 20 17 any - LineNumberTable: - line 17: 0 - line 18: 4 - line 19: 12 - line 20: 22 - StackMapTable: number_of_entries = 2 - frame_type = 255 /* full_frame */ - offset_delta = 17 - locals = [ class SynchronizedDemo, class java/lang/Object ] - stack = [ class java/lang/Throwable ] - frame_type = 250 /* chop */ - offset_delta = 4 +

https://img.nicksxs.com/uPic/bRMvlR.png

+

可以看到变成了轻量级锁,在线程没有争抢,只是进行了切换,就会使用轻量级锁,当两个线程在竞争了,就又会升级成重量级锁

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+        Thread thread1 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread1 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                    try {
+                        TimeUnit.SECONDS.sleep(5L);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+        Thread thread2 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread2 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                }
+            }
+        };
+        thread1.start();
+        thread2.start();
+    }
 }
-SourceFile: "SynchronizedDemo.java"
-

其中lockMethod中可以看到是通过 ACC_SYNCHRONIZED flag 来标记是被 synchronized 修饰,前面的 ACC 应该是 access 的意思,并且通过 ACC_PUBLIC 也可以看出来他们是同一类访问权限关键字来控制的,而修饰类则是通过3: monitorenter13: monitorexit来控制并发,这个是原来就知道,后来看了下才知道修饰方法是不一样的,但是在前期都比较诟病是 synchronized 的性能,像 monitor 也是通过操作系统的mutex lock互斥锁来实现的,相对是比较重的锁,于是在 JDK 1.6 之后对 synchronized 做了一系列优化,包括偏向锁,轻量级锁,并且包括像 ConcurrentHashMap 这类并发集合都有在使用 synchronized 关键字配合 cas 来做并发保护,

-

jdk 对于 synchronized 的优化主要在于多重状态锁的升级,最初会使用偏向锁,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
而当出现线程尝试进入同步块时发现已有偏向锁,并且是其他线程时,会将锁升级成轻量级锁,并且自旋尝试获取锁,如果自旋成功则表示获取轻量级锁成功,否则将会升级成重量级锁进行阻塞,当然这里具体的还很复杂,说的比较浅薄主体还是想将原先的阻塞互斥锁进行轻量化,区分特殊情况进行加锁。

+class L { + private boolean myboolean = true; +}
+ +

https://img.nicksxs.com/uPic/LMzMtR.png

+

可以看到变成了重量级锁。

]]>
Java @@ -12736,6 +12567,44 @@ public Result invoke(final Invocation invocation) throws RpcException { 类加载
+ + 聊聊 Java 的 equals 和 hashCode 方法 + /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/ + Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
简单看下代码
java.lang.Object#equals

+
public boolean equals(Object obj) {
+        return (this == obj);
+    }
+

对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了
java.lang.String#equals

+
public boolean equals(Object anObject) {
+        if (this == anObject) {
+            return true;
+        }
+        if (anObject instanceof String) {
+            String anotherString = (String)anObject;
+            int n = value.length;
+            if (n == anotherString.value.length) {
+                char v1[] = value;
+                char v2[] = anotherString.value;
+                int i = 0;
+                while (n-- != 0) {
+                    if (v1[i] != v2[i])
+                        return false;
+                    i++;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+

然后呢就是为啥一些书或者 effective java 中写了 equalshashCode 要一起重写,这里涉及到当对象作为 HashMapkey 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 objecthashCodenative 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.

+]]>
+ + java + + + java + +
聊聊 Java 的类加载机制二 /2021/06/13/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%BA%8C/ @@ -13032,107 +12901,21 @@ public Result invoke(final Invocation invocation) throws RpcException { return c; } }
-

破坏双亲委派

关于破坏双亲委派模型,第一次是在 JDK1.2 之后引入了双亲委派模型之前,那么在那之前已经有了类加载器,所以java.lang.ClassLoader 中添加了一个 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。这个跟上面的逻辑其实类似,当父类加载失败,会调用 findClass()来完成加载;第二次是因为这个模型本身还有一些不足之处,比如 SPI 这种,所以有设计了线程下上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 java.lang.Thread#setContextClassLoader() 进行设置,然后第三种是为了追求程序动态性,这里有涉及到了 osgi 等概念,就不展开了

-]]> - - Java - - - Java - 类加载 - 加载 - 验证 - 准备 - 解析 - 初始化 - 链接 - 双亲委派 - - - - 聊聊 Java 的 equals 和 hashCode 方法 - /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/ - Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
简单看下代码
java.lang.Object#equals

-
public boolean equals(Object obj) {
-        return (this == obj);
-    }
-

对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了
java.lang.String#equals

-
public boolean equals(Object anObject) {
-        if (this == anObject) {
-            return true;
-        }
-        if (anObject instanceof String) {
-            String anotherString = (String)anObject;
-            int n = value.length;
-            if (n == anotherString.value.length) {
-                char v1[] = value;
-                char v2[] = anotherString.value;
-                int i = 0;
-                while (n-- != 0) {
-                    if (v1[i] != v2[i])
-                        return false;
-                    i++;
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-

然后呢就是为啥一些书或者 effective java 中写了 equalshashCode 要一起重写,这里涉及到当对象作为 HashMapkey 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 objecthashCodenative 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.

-]]>
- - java - - - java - -
- - 聊聊 Linux 下的 top 命令 - /2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ - top 命令在日常的 Linux 使用中,特别是做一些服务器的简单状态查看,排查故障都起了比较大的作用,但是由于这个命令看到的东西比较多,一般只会看部分,或者说像我这样就会比较片面地看一些信息,比如默认是进程维度的,可以在启动命令的时候加-H进入线程模式

-
-H  :Threads-mode operation
-            Instructs top to display individual threads.  Without this command-line option a summation of all threads in each process  is  shown.   Later
-            this can be changed with the `H' interactive command.
-

这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样

执行后是这样

第二个就是排序了

-
SORTING of task window
-
-          For  compatibility,  this top supports most of the former top sort keys.  Since this is primarily a service to former top users, these commands
-          do not appear on any help screen.
-                command   sorted-field                  supported
-                A         start time (non-display)      No
-                M         %MEM                          Yes
-                N         PID                           Yes
-                P         %CPU                          Yes
-                T         TIME+                         Yes
-
-          Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
-          mand.  That will help ensure that the actual sort environment matches your intent.
-
-          The following interactive commands will only be honored when the current sort field is visible.  The sort field might not be visible because:
-                1) there is insufficient Screen Width
-                2) the `f' interactive command turned it Off
-
-             <  :Move-Sort-Field-Left
-                 Moves the sort column to the left unless the current sort field is the first field being displayed.
-
-             >  :Move-Sort-Field-Right
-                 Moves the sort column to the right unless the current sort field is the last field being displayed.
-

查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<>直接左右移动排序列

-]]>
- - Linux - 命令 - 小技巧 - top - top - 排序 +

破坏双亲委派

关于破坏双亲委派模型,第一次是在 JDK1.2 之后引入了双亲委派模型之前,那么在那之前已经有了类加载器,所以java.lang.ClassLoader 中添加了一个 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。这个跟上面的逻辑其实类似,当父类加载失败,会调用 findClass()来完成加载;第二次是因为这个模型本身还有一些不足之处,比如 SPI 这种,所以有设计了线程下上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 java.lang.Thread#setContextClassLoader() 进行设置,然后第三种是为了追求程序动态性,这里有涉及到了 osgi 等概念,就不展开了

+]]> + + Java - 排序 - linux - 小技巧 - top + Java + 类加载 + 加载 + 验证 + 准备 + 解析 + 初始化 + 链接 + 双亲委派
@@ -13657,6 +13440,54 @@ public Result invoke(final Invocation invocation) throws RpcException { JMap + + 聊聊 Linux 下的 top 命令 + /2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ + top 命令在日常的 Linux 使用中,特别是做一些服务器的简单状态查看,排查故障都起了比较大的作用,但是由于这个命令看到的东西比较多,一般只会看部分,或者说像我这样就会比较片面地看一些信息,比如默认是进程维度的,可以在启动命令的时候加-H进入线程模式

+
-H  :Threads-mode operation
+            Instructs top to display individual threads.  Without this command-line option a summation of all threads in each process  is  shown.   Later
+            this can be changed with the `H' interactive command.
+

这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样

执行后是这样

第二个就是排序了

+
SORTING of task window
+
+          For  compatibility,  this top supports most of the former top sort keys.  Since this is primarily a service to former top users, these commands
+          do not appear on any help screen.
+                command   sorted-field                  supported
+                A         start time (non-display)      No
+                M         %MEM                          Yes
+                N         PID                           Yes
+                P         %CPU                          Yes
+                T         TIME+                         Yes
+
+          Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
+          mand.  That will help ensure that the actual sort environment matches your intent.
+
+          The following interactive commands will only be honored when the current sort field is visible.  The sort field might not be visible because:
+                1) there is insufficient Screen Width
+                2) the `f' interactive command turned it Off
+
+             <  :Move-Sort-Field-Left
+                 Moves the sort column to the left unless the current sort field is the first field being displayed.
+
+             >  :Move-Sort-Field-Right
+                 Moves the sort column to the right unless the current sort field is the last field being displayed.
+

查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<>直接左右移动排序列

+]]>
+ + Linux + 命令 + 小技巧 + top + top + 排序 + + + 排序 + linux + 小技巧 + top + +
聊聊 RocketMQ 的 Broker 源码 /2020/07/19/%E8%81%8A%E8%81%8A-RocketMQ-%E7%9A%84-Broker-%E6%BA%90%E7%A0%81/ @@ -14272,6 +14103,67 @@ t2 Sharding-Jdbc + + 聊聊 dubbo 的线程池 + /2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + 之前没注意到这一块,只是比较模糊的印象 dubbo 自己基于 ThreadPoolExecutor 定义了几个线程池,但是没具体看过,主要是觉得就是为了避免使用 jdk 自带的那几个(java.util.concurrent.Executors),防止出现那些问题
看下代码目录主要是这几个

+
    +
  • FixedThreadPool:创建一个复用固定个数线程的线程池。
    简单看下代码
    public Executor getExecutor(URL url) {
    +        String name = url.getParameter("threadname", "Dubbo");
    +        int threads = url.getParameter("threads", 200);
    +        int queues = url.getParameter("queues", 0);
    +        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    +    }
    +可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
  • +
  • LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
    public Executor getExecutor(URL url) {
    +        String name = url.getParameter("threadname", "Dubbo");
    +        int cores = url.getParameter("corethreads", 0);
    +        int threads = url.getParameter("threads", 200);
    +        int queues = url.getParameter("queues", 0);
    +        return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    +    }
    +这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
  • +
  • EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
    public Executor getExecutor(URL url) {
    +        String name = url.getParameter("threadname", "Dubbo");
    +        int cores = url.getParameter("corethreads", 0);
    +        int threads = url.getParameter("threads", 2147483647);
    +        int queues = url.getParameter("queues", 0);
    +        int alive = url.getParameter("alive", 60000);
    +        TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
    +        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    +        taskQueue.setExecutor(executor);
    +        return executor;
    +    }
    +这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
  • +
  • CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
    public Executor getExecutor(URL url) {
    +        String name = url.getParameter("threadname", "Dubbo");
    +        int cores = url.getParameter("corethreads", 0);
    +        int threads = url.getParameter("threads", 2147483647);
    +        int queues = url.getParameter("queues", 0);
    +        int alive = url.getParameter("alive", 60000);
    +        return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    +    }
    +这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
    只是非常简略的介绍下,有兴趣可以自行阅读代码。
  • +
+]]>
+ + Java + Dubbo - 线程池 + Dubbo + 线程池 + ThreadPool + + + Java + Dubbo + ThreadPool + 线程池 + FixedThreadPool + LimitedThreadPool + EagerThreadPool + CachedThreadPool + +
聊聊 Sharding-Jdbc 的简单使用 /2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ @@ -14378,234 +14270,224 @@ t2 - 聊聊 Sharding-Jdbc 的简单原理初篇 - /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ - 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
就是我在执行 query 的时候犯过一个比较难发现的错误,

-
ResultSet resultSet = ps.executeQuery(sql);
-

实际上应该是

-
ResultSet resultSet = ps.executeQuery();
-

而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里

-
@Override
-public final ResultSet executeQuery(final String sql) throws SQLException {
-    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
-}
-

这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

在前一篇的 demo 中的

-
Connection conn = dataSource.getConnection();
-

其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement

-
PreparedStatement ps = conn.prepareStatement(sql)
-

就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行

-
ResultSet resultSet = ps.executeQuery();
-

然后获得结果
org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

-

其实像 mybatis 也是基于这样去实现的

-]]>
- - Java - - - Java - Sharding-Jdbc - -
- - 聊聊 dubbo 的线程池 - /2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 之前没注意到这一块,只是比较模糊的印象 dubbo 自己基于 ThreadPoolExecutor 定义了几个线程池,但是没具体看过,主要是觉得就是为了避免使用 jdk 自带的那几个(java.util.concurrent.Executors),防止出现那些问题
看下代码目录主要是这几个

-
    -
  • FixedThreadPool:创建一个复用固定个数线程的线程池。
    简单看下代码
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int threads = url.getParameter("threads", 200);
    -        int queues = url.getParameter("queues", 0);
    -        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -    }
    -可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
  • -
  • LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int cores = url.getParameter("corethreads", 0);
    -        int threads = url.getParameter("threads", 200);
    -        int queues = url.getParameter("queues", 0);
    -        return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -    }
    -这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
  • -
  • EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int cores = url.getParameter("corethreads", 0);
    -        int threads = url.getParameter("threads", 2147483647);
    -        int queues = url.getParameter("queues", 0);
    -        int alive = url.getParameter("alive", 60000);
    -        TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
    -        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -        taskQueue.setExecutor(executor);
    -        return executor;
    -    }
    -这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
  • -
  • CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int cores = url.getParameter("corethreads", 0);
    -        int threads = url.getParameter("threads", 2147483647);
    -        int queues = url.getParameter("queues", 0);
    -        int alive = url.getParameter("alive", 60000);
    -        return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -    }
    -这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
    只是非常简略的介绍下,有兴趣可以自行阅读代码。
  • -
-]]>
- - Java - Dubbo - 线程池 - Dubbo - 线程池 - ThreadPool - - - Java - Dubbo - ThreadPool - 线程池 - FixedThreadPool - LimitedThreadPool - EagerThreadPool - CachedThreadPool - -
- - 聊聊 mysql 的 MVCC 续篇 - /2020/05/02/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%AF%87/ - 上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

-
/**
-Opens a read view where exactly the transactions serialized before this
-point in time are seen in the view.
-@param id		Creator transaction id */
-
-void ReadView::prepare(trx_id_t id) {
-  ut_ad(mutex_own(&trx_sys->mutex));
-
-  m_creator_trx_id = id;
-
-  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;
-

m_low_limit_id赋的值是trx_sys->max_trx_id,代表的是当前系统最小的未分配的事务 id,所以呢,举个例子,当前有三个活跃事务,事务 id 分别是 100,200,300,而 m_up_limit_id = 100, m_low_limit_id = 301,当事务 id 是 200 的提交之后,它的更新就是可以被 100 和 300 看到,而不是说 m_ids 里没了 200,并且 200 比 100 大就应该不可见了

-

幻读

还有一个问题是幻读的问题,这貌似也是个高频面试题,啥意思呢,或者说跟它最常拿来比较的脏读,脏读是指读到了别的事务未提交的数据,因为未提交,严格意义上来讲,不一定是会被最后落到库里,可能会回滚,也就是在 read uncommitted 级别下会出现的问题,但是幻读不太一样,幻读是指两次查询的结果数量不一样,比如我查了第一次是 select * from table1 where id < 10 for update,查出来了一条结果 id 是 5,然后再查一下发现出来了一条 id 是 5,一条 id 是 7,那是不是有点尴尬了,其实呢这个点我觉得脏读跟幻读也比较是从原理层面来命名,如果第一次接触的同学发觉有点不理解也比较正常,因为从逻辑上讲总之都是数据不符合预期,但是基于源码层面其实是不同的情况,幻读是在原先的 read view 无法完全解决的,怎么解决呢,简单的来说就是锁咯,我们知道innodb 是基于 record lock 行锁的,但是貌似没有办法解决这种问题,那么 innodb 就引入了 gap lock 间隙锁,比如上面说的情况下,id 小于 10 的情况下,是都应该锁住的,gap lock 其实是基于索引结构来锁的,因为索引树除了树形结构之外,还有一个next record 的指针,gap lock 也是基于这个来锁的
看一下 mysql 的文档

-
-

SELECT … FOR UPDATE sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

-
-

对于一个 for update 查询,在 RR 级别下,会设置一个 next-key lock在每一条被查询到的记录上,next-lock 又是啥呢,其实就是 gap 锁和 record 锁的结合体,比如我在数据库里有 id 是 1,3,5,7,10,对于上面那条查询,查出来的结果就是 1,3,5,7,那么按照文档里描述的,对于这几条记录都会加上next-key lock,也就是(-∞, 1], (1, 3], (3, 5], (5, 7], (7, 10) 这些区间和记录会被锁起来,不让插入,再唠叨一下呢,就是其实如果是只读的事务,光 read view 一致性读就够了,如果是有写操作的呢,就需要锁了。

-]]>
- - Mysql - C - 数据结构 - 源码 - Mysql - - - mysql - 数据结构 - 源码 - mvcc - read view - gap lock - next-key lock - 幻读 - -
- - 聊聊 mysql 的 MVCC 续续篇之锁分析 - /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/ - 看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档

-
-

SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

-
-

纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类

-
    -
  • select * from table where ? lock in share mode;
  • -
  • select * from table where ? for update;
  • -
  • insert into table values (...);
  • -
  • update table set ? where ?;
  • -
  • delete from table where ?;
  • -
-

除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了

其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了

+ 聊聊 Sharding-Jdbc 的简单原理初篇 + /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
就是我在执行 query 的时候犯过一个比较难发现的错误,

+
ResultSet resultSet = ps.executeQuery(sql);
+

实际上应该是

+
ResultSet resultSet = ps.executeQuery();
+

而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里

+
@Override
+public final ResultSet executeQuery(final String sql) throws SQLException {
+    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
+}
+

这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

在前一篇的 demo 中的

+
Connection conn = dataSource.getConnection();
+

其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement

+
PreparedStatement ps = conn.prepareStatement(sql)
+

就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行

+
ResultSet resultSet = ps.executeQuery();
+

然后获得结果
org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

+

其实像 mybatis 也是基于这样去实现的

]]>
- Mysql - C - 数据结构 - 源码 - Mysql + Java - mysql - 数据结构 - 源码 - mvcc - read view - gap lock - next-key lock - 幻读 + Java + Sharding-Jdbc
- 聊聊 mysql 的 MVCC - /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/ - 很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢

-

其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行

-

腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下

-
/* Precise data types for system columns and the length of those columns;
-NOTE: the values must run from 0 up in the order given! All codes must
-be less than 256 */
-#define DATA_ROW_ID 0     /* row id: a 48-bit integer */
-#define DATA_ROW_ID_LEN 6 /* stored length for row id */
+    聊聊 Java 中绕不开的 Synchronized 关键字
+    /2021/06/20/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97/
+    Synchronized 关键字在 Java 的并发体系里也是非常重要的一个内容,首先比较常规的是知道它使用的方式,可以锁对象,可以锁代码块,也可以锁方法,看一个简单的 demo

+
public class SynchronizedDemo {
 
-/** Transaction id: 6 bytes */
-constexpr size_t DATA_TRX_ID = 1;
+    public static void main(String[] args) {
+        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
+        synchronizedDemo.lockMethod();
+    }
 
-/** Transaction ID type size in bytes. */
-constexpr size_t DATA_TRX_ID_LEN = 6;
+    public synchronized void lockMethod() {
+        System.out.println("here i'm locked");
+    }
 
-/** Rollback data pointer: 7 bytes */
-constexpr size_t DATA_ROLL_PTR = 2;
+    public void lockSynchronizedDemo() {
+        synchronized (this) {
+            System.out.println("here lock class");
+        }
+    }
+}
-/** Rollback data pointer type size in bytes. */ -constexpr size_t DATA_ROLL_PTR_LEN = 7;
+

然后来查看反编译结果,其实代码(日光)之下并无新事,即使是完全不懂的也可以通过一些词义看出一些意义

+
  Last modified 2021620; size 729 bytes
+  MD5 checksum dd9c529863bd7ff839a95481db578ad9
+  Compiled from "SynchronizedDemo.java"
+public class SynchronizedDemo
+  minor version: 0
+  major version: 53
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #2                          // SynchronizedDemo
+  super_class: #9                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 4, attributes: 1
+Constant pool:
+   #1 = Methodref          #9.#22         // java/lang/Object."<init>":()V
+   #2 = Class              #23            // SynchronizedDemo
+   #3 = Methodref          #2.#22         // SynchronizedDemo."<init>":()V
+   #4 = Methodref          #2.#24         // SynchronizedDemo.lockMethod:()V
+   #5 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
+   #6 = String             #27            // here i\'m locked
+   #7 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
+   #8 = String             #30            // here lock class
+   #9 = Class              #31            // java/lang/Object
+  #10 = Utf8               <init>
+  #11 = Utf8               ()V
+  #12 = Utf8               Code
+  #13 = Utf8               LineNumberTable
+  #14 = Utf8               main
+  #15 = Utf8               ([Ljava/lang/String;)V
+  #16 = Utf8               lockMethod
+  #17 = Utf8               lockSynchronizedDemo
+  #18 = Utf8               StackMapTable
+  #19 = Class              #32            // java/lang/Throwable
+  #20 = Utf8               SourceFile
+  #21 = Utf8               SynchronizedDemo.java
+  #22 = NameAndType        #10:#11        // "<init>":()V
+  #23 = Utf8               SynchronizedDemo
+  #24 = NameAndType        #16:#11        // lockMethod:()V
+  #25 = Class              #33            // java/lang/System
+  #26 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
+  #27 = Utf8               here i\'m locked
+  #28 = Class              #36            // java/io/PrintStream
+  #29 = NameAndType        #37:#38        // println:(Ljava/lang/String;)V
+  #30 = Utf8               here lock class
+  #31 = Utf8               java/lang/Object
+  #32 = Utf8               java/lang/Throwable
+  #33 = Utf8               java/lang/System
+  #34 = Utf8               out
+  #35 = Utf8               Ljava/io/PrintStream;
+  #36 = Utf8               java/io/PrintStream
+  #37 = Utf8               println
+  #38 = Utf8               (Ljava/lang/String;)V
+{
+  public SynchronizedDemo();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=1, locals=1, args_size=1
+         0: aload_0
+         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+         4: return
+      LineNumberTable:
+        line 5: 0
 
-

一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了

-

一个是 DATA_TRX_ID,这个表示这条记录的事务 ID

-

还有一个是 DATA_ROLL_PTR 指向回滚段的指针

-

指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,

-

m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id

-

m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id

-

m_ids, 这个是 read view 创建时所有的活跃事务 id 数组

-

m_creator_trx_id 这个是当前记录的创建事务 id

-

判断事务的可见性主要的逻辑是这样,

-
    -
  1. 当记录的事务 id 小于最小活跃事务 id,说明是可见的,
  2. -
  3. 如果记录的事务 id 等于当前事务 id,说明是自己的更改,可见
  4. -
  5. 如果记录的事务 id 大于最大的活跃事务 id, 不可见
  6. -
  7. 如果记录的事务 id 介于 m_low_limit_idm_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
    具体的代码捞一下看看
    /** Check whether the changes by id are visible.
    -  @param[in]	id	transaction id to check against the view
    -  @param[in]	name	table name
    -  @return whether the view sees the modifications of id. */
    -  bool changes_visible(trx_id_t id, const table_name_t &name) const
    -      MY_ATTRIBUTE((warn_unused_result)) {
    -    ut_ad(id > 0);
    +  public static void main(java.lang.String[]);
    +    descriptor: ([Ljava/lang/String;)V
    +    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    +    Code:
    +      stack=2, locals=2, args_size=1
    +         0: new           #2                  // class SynchronizedDemo
    +         3: dup
    +         4: invokespecial #3                  // Method "<init>":()V
    +         7: astore_1
    +         8: aload_1
    +         9: invokevirtual #4                  // Method lockMethod:()V
    +        12: return
    +      LineNumberTable:
    +        line 8: 0
    +        line 9: 8
    +        line 10: 12
     
    -    if (id < m_up_limit_id || id == m_creator_trx_id) {
    -      return (true);
    -    }
    +  public synchronized void lockMethod();
    +    descriptor: ()V
    +    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    +    Code:
    +      stack=2, locals=1, args_size=1
    +         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
    +         3: ldc           #6                  // String here i\'m locked
    +         5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    +         8: return
    +      LineNumberTable:
    +        line 13: 0
    +        line 14: 8
     
    -    check_trx_id_sanity(id, name);
    +  public void lockSynchronizedDemo();
    +    descriptor: ()V
    +    flags: (0x0001) ACC_PUBLIC
    +    Code:
    +      stack=2, locals=3, args_size=1
    +         0: aload_0
    +         1: dup
    +         2: astore_1
    +         3: monitorenter
    +         4: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
    +         7: ldc           #8                  // String here lock class
    +         9: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    +        12: aload_1
    +        13: monitorexit
    +        14: goto          22
    +        17: astore_2
    +        18: aload_1
    +        19: monitorexit
    +        20: aload_2
    +        21: athrow
    +        22: return
    +      Exception table:
    +         from    to  target type
    +             4    14    17   any
    +            17    20    17   any
    +      LineNumberTable:
    +        line 17: 0
    +        line 18: 4
    +        line 19: 12
    +        line 20: 22
    +      StackMapTable: number_of_entries = 2
    +        frame_type = 255 /* full_frame */
    +          offset_delta = 17
    +          locals = [ class SynchronizedDemo, class java/lang/Object ]
    +          stack = [ class java/lang/Throwable ]
    +        frame_type = 250 /* chop */
    +          offset_delta = 4
    +}
    +SourceFile: "SynchronizedDemo.java"
    - if (id >= m_low_limit_id) { - return (false); +

    其中lockMethod中可以看到是通过 ACC_SYNCHRONIZED flag 来标记是被 synchronized 修饰,前面的 ACC 应该是 access 的意思,并且通过 ACC_PUBLIC 也可以看出来他们是同一类访问权限关键字来控制的,而修饰类则是通过3: monitorenter13: monitorexit来控制并发,这个是原来就知道,后来看了下才知道修饰方法是不一样的,但是在前期都比较诟病是 synchronized 的性能,像 monitor 也是通过操作系统的mutex lock互斥锁来实现的,相对是比较重的锁,于是在 JDK 1.6 之后对 synchronized 做了一系列优化,包括偏向锁,轻量级锁,并且包括像 ConcurrentHashMap 这类并发集合都有在使用 synchronized 关键字配合 cas 来做并发保护,

    +

    jdk 对于 synchronized 的优化主要在于多重状态锁的升级,最初会使用偏向锁,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
    而当出现线程尝试进入同步块时发现已有偏向锁,并且是其他线程时,会将锁升级成轻量级锁,并且自旋尝试获取锁,如果自旋成功则表示获取轻量级锁成功,否则将会升级成重量级锁进行阻塞,当然这里具体的还很复杂,说的比较浅薄主体还是想将原先的阻塞互斥锁进行轻量化,区分特殊情况进行加锁。

    +]]> + + Java + + + Java + Synchronized + 偏向锁 + 轻量级锁 + 重量级锁 + 自旋 + + + + 聊聊 mysql 的 MVCC 续篇 + /2020/05/02/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%AF%87/ + 上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

    +
    /**
    +Opens a read view where exactly the transactions serialized before this
    +point in time are seen in the view.
    +@param id		Creator transaction id */
     
    -    } else if (m_ids.empty()) {
    -      return (true);
    -    }
    +void ReadView::prepare(trx_id_t id) {
    +  ut_ad(mutex_own(&trx_sys->mutex));
     
    -    const ids_t::value_type *p = m_ids.data();
    +  m_creator_trx_id = id;
     
    -    return (!std::binary_search(p, p + m_ids.size(), id));
    -  }
    -剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据
  8. -
+ m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;
+

m_low_limit_id赋的值是trx_sys->max_trx_id,代表的是当前系统最小的未分配的事务 id,所以呢,举个例子,当前有三个活跃事务,事务 id 分别是 100,200,300,而 m_up_limit_id = 100, m_low_limit_id = 301,当事务 id 是 200 的提交之后,它的更新就是可以被 100 和 300 看到,而不是说 m_ids 里没了 200,并且 200 比 100 大就应该不可见了

+

幻读

还有一个问题是幻读的问题,这貌似也是个高频面试题,啥意思呢,或者说跟它最常拿来比较的脏读,脏读是指读到了别的事务未提交的数据,因为未提交,严格意义上来讲,不一定是会被最后落到库里,可能会回滚,也就是在 read uncommitted 级别下会出现的问题,但是幻读不太一样,幻读是指两次查询的结果数量不一样,比如我查了第一次是 select * from table1 where id < 10 for update,查出来了一条结果 id 是 5,然后再查一下发现出来了一条 id 是 5,一条 id 是 7,那是不是有点尴尬了,其实呢这个点我觉得脏读跟幻读也比较是从原理层面来命名,如果第一次接触的同学发觉有点不理解也比较正常,因为从逻辑上讲总之都是数据不符合预期,但是基于源码层面其实是不同的情况,幻读是在原先的 read view 无法完全解决的,怎么解决呢,简单的来说就是锁咯,我们知道innodb 是基于 record lock 行锁的,但是貌似没有办法解决这种问题,那么 innodb 就引入了 gap lock 间隙锁,比如上面说的情况下,id 小于 10 的情况下,是都应该锁住的,gap lock 其实是基于索引结构来锁的,因为索引树除了树形结构之外,还有一个next record 的指针,gap lock 也是基于这个来锁的
看一下 mysql 的文档

+
+

SELECT … FOR UPDATE sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

+
+

对于一个 for update 查询,在 RR 级别下,会设置一个 next-key lock在每一条被查询到的记录上,next-lock 又是啥呢,其实就是 gap 锁和 record 锁的结合体,比如我在数据库里有 id 是 1,3,5,7,10,对于上面那条查询,查出来的结果就是 1,3,5,7,那么按照文档里描述的,对于这几条记录都会加上next-key lock,也就是(-∞, 1], (1, 3], (3, 5], (5, 7], (7, 10) 这些区间和记录会被锁起来,不让插入,再唠叨一下呢,就是其实如果是只读的事务,光 read view 一致性读就够了,如果是有写操作的呢,就需要锁了。

]]>
Mysql @@ -14620,6 +14502,9 @@ constexpr size_t DATA_ROLL_PTR_LEN 源码 mvcc read view + gap lock + next-key lock + 幻读
@@ -14687,156 +14572,6 @@ $ procedure - - 聊聊 redis 缓存的应用问题 - /2021/01/31/%E8%81%8A%E8%81%8A-redis-%E7%BC%93%E5%AD%98%E7%9A%84%E5%BA%94%E7%94%A8%E9%97%AE%E9%A2%98/ - 前面写过一系列的 redis 源码分析的,但是实际上很多的问题还是需要结合实际的使用,然后其实就避不开缓存使用的三个著名问题,穿透,击穿和雪崩,这三个概念也是有着千丝万缕的关系,

-

缓存穿透

缓存穿透是指当数据库中本身就不存在这个数据的时候,使用一般的缓存策略时访问不到缓存后就访问数据库,但是因为数据库也没数据,所以如果不做任何策略优化的话,这类数据就每次都会访问一次数据库,对数据库压力也会比较大。

-

缓存击穿

缓存击穿跟穿透比较类似的,都是访问缓存不在,然后去访问数据库,与穿透不一样的是击穿是在数据库中存在数据,但是可能由于第一次访问,或者缓存过期了,需要访问到数据库,这对于访问量小的情况其实算是个正常情况,但是随着请求量变高就会引发一些性能隐患。

-

缓存雪崩

缓存雪崩就是击穿的大规模集群效应,当大量的缓存过期失效的时候,这些请求都是直接访问到数据库了,会对数据库造成很大的压力。

-

对于以上三种场景也有一些比较常见的解决方案,但也不能说是万无一失的,需要随着业务去寻找合适的方案

-

解决缓存穿透

对于数据库中就没这个数据的时候,一种是可以对这个 key 设置下空值,即以一个特定的表示是数据库不存在的,这种情况需要合理地调整过期时间,当这个 key 在数据库中有数据了的话,也需要有策略去更新这个值,并且如果这类 key 非常多,这个方法就会不太合适,就可以使用第二种方法,就是布隆过滤器,bloom filter,前置一个布隆过滤器,当这个 key 在数据库不存在的话,先用布隆过滤器挡一道,如果不在的话就直接返回了,当然布隆过滤器不是绝对的准确的

-

解决缓存击穿

当一个 key 的缓存过期了,如果大量请求过来访问这个 key,请求都会落在数据库里,这个时候就可以使用一些类似于互斥锁的方式去让一个线程去访问数据库,更新缓存,但是这里其实也有个问题,就是如果是热点 key 其实这种方式也比较危险,万一更新失败,或者更新操作的时候耗时比较久,就会有一大堆请求卡在那,这种情况可能需要有一些异步提前刷新缓存,可以结合具体场景选择方式

-

解决缓存雪崩

雪崩的情况是指大批量的 key 都一起过期了,击穿的放大版,大批量的请求都打到数据库上了,一方面有可能直接缓存不可用了,就需要用集群化高可用的缓存服务,然后对于实际使用中也可以使用本地缓存结合 redis 缓存,去提高可用性,再配合一些限流措施,然后就是缓存使用过程总的过期时间最好能加一些随机值,防止在同一时间过期而导致雪崩,结合互斥锁防止大量请求打到数据库。

-]]>
- - Redis - 应用 - 缓存 - 缓存 - 穿透 - 击穿 - 雪崩 - - - Redis - 缓存穿透 - 缓存击穿 - 缓存雪崩 - 布隆过滤器 - bloom filter - 互斥锁 - -
- - 聊聊传说中的 ThreadLocal - /2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/ - 说来也惭愧,这个 ThreadLocal 其实一直都是一知半解,而且看了一下之后还发现记错了,所以还是记录下
原先记忆里的都是反过来,一个 ThreadLocal 是里面按照 thread 作为 key,存储线程内容的,真的是半解都米有,完全是错的,这样就得用 concurrentHashMap 这种去存储并且要锁定线程了,然后内容也只能存一个了,想想简直智障

-

究竟是啥结构

比如我们在代码中 new 一个 ThreadLocal,

-
public static void main(String[] args) {
-        ThreadLocal<Man> tl = new ThreadLocal<>();
-
-        new Thread(() -> {
-            try {
-                TimeUnit.SECONDS.sleep(2);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-            System.out.println(tl.get());
-        }).start();
-        new Thread(() -> {
-            try {
-                TimeUnit.SECONDS.sleep(1);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-            tl.set(new Man());
-        }).start();
-    }
-
-    static class Man {
-        String name = "nick";
-    }
-

这里构造了两个线程,一个先往里设值,一个后从里取,运行看下结果,

知道这个用法的话肯定知道是取不到值的,只是具体的原理原来搞错了,我们来看下设值 set 方法

-
public void set(T value) {
-    Thread t = Thread.currentThread();
-    ThreadLocalMap map = getMap(t);
-    if (map != null)
-        map.set(this, value);
-    else
-        createMap(t, value);
-}
-

写博客这会我才明白我原来咋会错得这么离谱,看到第一行代码 t 就是当前线程,然后第二行就是用这个线程去getMap,然后我是把这个当成从 map 里取值了,其实这里是

-
ThreadLocalMap getMap(Thread t) {
-    return t.threadLocals;
-}
-

获取 t 的 threadLocals 成员变量,那这个 threadLocals 又是啥呢

它其实是线程 Thread 中的一个类型是java.lang.ThreadLocal.ThreadLocalMap的成员变量
这是 ThreadLocal 的一个静态成员变量

-
static class ThreadLocalMap {
-
-        /**
-         * The entries in this hash map extend WeakReference, using
-         * its main ref field as the key (which is always a
-         * ThreadLocal object).  Note that null keys (i.e. entry.get()
-         * == null) mean that the key is no longer referenced, so the
-         * entry can be expunged from table.  Such entries are referred to
-         * as "stale entries" in the code that follows.
-         */
-        static class Entry extends WeakReference<ThreadLocal<?>> {
-            /** The value associated with this ThreadLocal. */
-            Object value;
-
-            Entry(ThreadLocal<?> k, Object v) {
-                super(k);
-                value = v;
-            }
-        }
-    }
-

全部代码有点长,只截取了一小部分,然后我们再回头来分析前面说的 set 过程,再 copy 下代码

-
public void set(T value) {
-    Thread t = Thread.currentThread();
-    ThreadLocalMap map = getMap(t);
-    if (map != null)
-        map.set(this, value);
-    else
-        createMap(t, value);
-}
-

获取到 map 以后呢,如果 map 不为空,就往 map 里 set,这里注意 key 是啥,其实是当前这个 ThreadLocal,这里就比较明白了究竟是啥结构,每个线程都会维护自身的 ThreadLocalMap,它是线程的一个成员变量,当创建 ThreadLocal 的时候,进行设值的时候其实是往这个 map 里以 ThreadLocal 作为 key,往里设 value。

-

内存泄漏是什么鬼

这里又要看下前面的 ThreadLocalMap 结构了,类似 HashMap,它有个 Entry 结构,在设置的时候会先包装成一个 Entry

-
private void set(ThreadLocal<?> key, Object value) {
-
-        // We don't use a fast path as with get() because it is at
-        // least as common to use set() to create new entries as
-        // it is to replace existing ones, in which case, a fast
-        // path would fail more often than not.
-
-        Entry[] tab = table;
-        int len = tab.length;
-        int i = key.threadLocalHashCode & (len-1);
-
-        for (Entry e = tab[i];
-             e != null;
-             e = tab[i = nextIndex(i, len)]) {
-            ThreadLocal<?> k = e.get();
-
-            if (k == key) {
-                e.value = value;
-                return;
-            }
-
-            if (k == null) {
-                replaceStaleEntry(key, value, i);
-                return;
-            }
-        }
-
-        tab[i] = new Entry(key, value);
-        int sz = ++size;
-        if (!cleanSomeSlots(i, sz) && sz >= threshold)
-            rehash();
-}
-

这里其实比较重要的就是前面的 Entry 的构造方法,Entry 是个 WeakReference 的子类,然后在构造方法里可以看到 key 会被包装成一个弱引用,这里为什么使用弱引用,其实是方便这个 key 被回收,如果前面的 ThreadLocal tl实例被设置成 null 了,如果这里是直接的强引用的话,就只能等到线程整个回收了,但是其实是弱引用也会有问题,主要是因为这个 value,如果在 ThreadLocal tl 被设置成 null 了,那么其实这个 value 就会没法被访问到,所以最好的操作还是在使用完了就 remove 掉

-]]>
- - Java - - - Java - ThreadLocal - 弱引用 - 内存泄漏 - WeakReference - -
聊聊Java中的单例模式 /2019/12/21/%E8%81%8A%E8%81%8AJava%E4%B8%AD%E7%9A%84%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/ @@ -14913,86 +14648,118 @@ $ - 聊聊一次 brew update 引发的血案 - /2020/06/13/%E8%81%8A%E8%81%8A%E4%B8%80%E6%AC%A1-brew-update-%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/ - 熟悉我的人(谁熟悉你啊🙄)知道我以前写过 PHP,虽然现在在工作中没用到了,但是自己的一些小工具还是会用 PHP 来写,但是在 Mac 碰到了一个环境相关的问题,因为我也是个更新狂魔,用了 brew 之后因为 gfw 的原因,如果长时间不更新,有时候要装一个用它装一个软件的话,前置的更新耗时就会让人非常头大,所以我基本会隔天 update 一下,但是这样会带来一个很心烦的问题,就是像这样,因为我是要用一个固定版本的 PHP,如果一直升需要一直配扩展啥的也很麻烦,如果一直升级 PHP 到最新版可能会比较少碰到这个问题

-
dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
-

这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下

-
-

ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。

-
-

然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
第一步,我们到 brew的目录下

-
cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
-

这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
第二步看看它的提交历史

-
git log --follow icu4c.rb
-

在 git log 的海洋中寻找,寻找它的(64版本)的身影

第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支

-
git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
-

其实注意 commit id 旁边的红框,这个是有tag 的,可以直接

-
git checkout icu4c-64
-

PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

-
brew reinstall ./icu4c.rb
-

然后就是第五步,切换版本

-
brew switch icu4c 64.2
-

最后把分支切回来

-
git checkout master
-

是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

-
# zsh
-function hiicu64() {
-  local last_dir=$(pwd)
+    聊聊 mysql 的 MVCC
+    /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/
+    很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢

+

其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行

+

腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下

+
/* Precise data types for system columns and the length of those columns;
+NOTE: the values must run from 0 up in the order given! All codes must
+be less than 256 */
+#define DATA_ROW_ID 0     /* row id: a 48-bit integer */
+#define DATA_ROW_ID_LEN 6 /* stored length for row id */
+
+/** Transaction id: 6 bytes */
+constexpr size_t DATA_TRX_ID = 1;
+
+/** Transaction ID type size in bytes. */
+constexpr size_t DATA_TRX_ID_LEN = 6;
+
+/** Rollback data pointer: 7 bytes */
+constexpr size_t DATA_ROLL_PTR = 2;
+
+/** Rollback data pointer type size in bytes. */
+constexpr size_t DATA_ROLL_PTR_LEN = 7;
+ +

一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了

+

一个是 DATA_TRX_ID,这个表示这条记录的事务 ID

+

还有一个是 DATA_ROLL_PTR 指向回滚段的指针

+

指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,

+

m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id

+

m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id

+

m_ids, 这个是 read view 创建时所有的活跃事务 id 数组

+

m_creator_trx_id 这个是当前记录的创建事务 id

+

判断事务的可见性主要的逻辑是这样,

+
    +
  1. 当记录的事务 id 小于最小活跃事务 id,说明是可见的,
  2. +
  3. 如果记录的事务 id 等于当前事务 id,说明是自己的更改,可见
  4. +
  5. 如果记录的事务 id 大于最大的活跃事务 id, 不可见
  6. +
  7. 如果记录的事务 id 介于 m_low_limit_idm_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
    具体的代码捞一下看看
    /** Check whether the changes by id are visible.
    +  @param[in]	id	transaction id to check against the view
    +  @param[in]	name	table name
    +  @return whether the view sees the modifications of id. */
    +  bool changes_visible(trx_id_t id, const table_name_t &name) const
    +      MY_ATTRIBUTE((warn_unused_result)) {
    +    ut_ad(id > 0);
    +
    +    if (id < m_up_limit_id || id == m_creator_trx_id) {
    +      return (true);
    +    }
    +
    +    check_trx_id_sanity(id, name);
    +
    +    if (id >= m_low_limit_id) {
    +      return (false);
    +
    +    } else if (m_ids.empty()) {
    +      return (true);
    +    }
     
    -  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
    -  git checkout icu4c-4
    -  brew reinstall ./icu4c.rb
    -  brew switch icu4c 64.2
    -  git checkout master
    +    const ids_t::value_type *p = m_ids.data();
     
    -  cd $last_dir
    -}
    -

    对应自己的版本改改版本号就可以了,非常好用。

    + return (!std::binary_search(p, p + m_ids.size(), id)); + }
+剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据 + ]]>
- Mac - PHP - Homebrew - PHP - icu4c + Mysql + C + 数据结构 + 源码 + Mysql - Mac - PHP - Homebrew - icu4c - zsh + mysql + 数据结构 + 源码 + mvcc + read view
- 聊聊厦门旅游的好与不好 - /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ - 这几天去了趟厦门,原来几年前就想去了,本来都请好假了,后面因为一些事情没去成,这次刚好公司组织,就跟 LD 一起去了厦门,也不洋洋洒洒地写游记了,后面可能会有,今天先来总结下好的地方和比较坑的地方。
这次主要去了中山路、鼓浪屿、曾厝(cuo)垵、植物园、灵玲马戏团,因为住的离环岛路比较近,还有幸现场看了下厦门马拉松,其中

-

中山路

这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。

-

鼓浪屿

鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆

-

曾厝垵

一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆

-

植物园

植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆

-

灵玲马戏团

对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。

-

总结

厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦‍♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。

+ 聊聊 mysql 的 MVCC 续续篇之锁分析 + /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/ + 看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档

+
+

SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

+
+

纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类

+
    +
  • select * from table where ? lock in share mode;
  • +
  • select * from table where ? for update;
  • +
  • insert into table values (...);
  • +
  • update table set ? where ?;
  • +
  • delete from table where ?;
  • +
+

除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了

其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了

]]>
- 生活 - 旅游 + Mysql + C + 数据结构 + 源码 + Mysql - 生活 - 杭州 - 旅游 - 厦门 - 中山路 - 局口街 - 鼓浪屿 - 曾厝垵 - 植物园 - 马戏团 - 沙茶面 - 海蛎煎 + mysql + 数据结构 + 源码 + mvcc + read view + gap lock + next-key lock + 幻读
@@ -15308,41 +15075,274 @@ $ org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\ org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration -# Failure analyzers -org.springframework.boot.diagnostics.FailureAnalyzer=\ -org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\ -org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ -org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ -org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ -org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ -org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\ -org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer +# Failure analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\ +org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ +org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ +org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer + +# Template availability providers +org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ +org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\ +org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\ +org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\ +org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\ +org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider + +# DataSource initializer detectors +org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\ +org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector +
+ +

上面根据 org.springframework.boot.autoconfigure.EnableAutoConfiguration 获取的各个配置类,在通过反射加载就能得到一堆 JavaConfig配置类,然后再根据 ConditionalOnProperty等条件配置加载具体的 bean,大致就是这么个逻辑

+]]> + + Java + SpringBoot + + + Java + Spring + SpringBoot + 自动装配 + AutoConfiguration + + + + 聊聊 redis 缓存的应用问题 + /2021/01/31/%E8%81%8A%E8%81%8A-redis-%E7%BC%93%E5%AD%98%E7%9A%84%E5%BA%94%E7%94%A8%E9%97%AE%E9%A2%98/ + 前面写过一系列的 redis 源码分析的,但是实际上很多的问题还是需要结合实际的使用,然后其实就避不开缓存使用的三个著名问题,穿透,击穿和雪崩,这三个概念也是有着千丝万缕的关系,

+

缓存穿透

缓存穿透是指当数据库中本身就不存在这个数据的时候,使用一般的缓存策略时访问不到缓存后就访问数据库,但是因为数据库也没数据,所以如果不做任何策略优化的话,这类数据就每次都会访问一次数据库,对数据库压力也会比较大。

+

缓存击穿

缓存击穿跟穿透比较类似的,都是访问缓存不在,然后去访问数据库,与穿透不一样的是击穿是在数据库中存在数据,但是可能由于第一次访问,或者缓存过期了,需要访问到数据库,这对于访问量小的情况其实算是个正常情况,但是随着请求量变高就会引发一些性能隐患。

+

缓存雪崩

缓存雪崩就是击穿的大规模集群效应,当大量的缓存过期失效的时候,这些请求都是直接访问到数据库了,会对数据库造成很大的压力。

+

对于以上三种场景也有一些比较常见的解决方案,但也不能说是万无一失的,需要随着业务去寻找合适的方案

+

解决缓存穿透

对于数据库中就没这个数据的时候,一种是可以对这个 key 设置下空值,即以一个特定的表示是数据库不存在的,这种情况需要合理地调整过期时间,当这个 key 在数据库中有数据了的话,也需要有策略去更新这个值,并且如果这类 key 非常多,这个方法就会不太合适,就可以使用第二种方法,就是布隆过滤器,bloom filter,前置一个布隆过滤器,当这个 key 在数据库不存在的话,先用布隆过滤器挡一道,如果不在的话就直接返回了,当然布隆过滤器不是绝对的准确的

+

解决缓存击穿

当一个 key 的缓存过期了,如果大量请求过来访问这个 key,请求都会落在数据库里,这个时候就可以使用一些类似于互斥锁的方式去让一个线程去访问数据库,更新缓存,但是这里其实也有个问题,就是如果是热点 key 其实这种方式也比较危险,万一更新失败,或者更新操作的时候耗时比较久,就会有一大堆请求卡在那,这种情况可能需要有一些异步提前刷新缓存,可以结合具体场景选择方式

+

解决缓存雪崩

雪崩的情况是指大批量的 key 都一起过期了,击穿的放大版,大批量的请求都打到数据库上了,一方面有可能直接缓存不可用了,就需要用集群化高可用的缓存服务,然后对于实际使用中也可以使用本地缓存结合 redis 缓存,去提高可用性,再配合一些限流措施,然后就是缓存使用过程总的过期时间最好能加一些随机值,防止在同一时间过期而导致雪崩,结合互斥锁防止大量请求打到数据库。

+]]>
+ + Redis + 应用 + 缓存 + 缓存 + 穿透 + 击穿 + 雪崩 + + + Redis + 缓存穿透 + 缓存击穿 + 缓存雪崩 + 布隆过滤器 + bloom filter + 互斥锁 + +
+ + 聊聊一次 brew update 引发的血案 + /2020/06/13/%E8%81%8A%E8%81%8A%E4%B8%80%E6%AC%A1-brew-update-%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/ + 熟悉我的人(谁熟悉你啊🙄)知道我以前写过 PHP,虽然现在在工作中没用到了,但是自己的一些小工具还是会用 PHP 来写,但是在 Mac 碰到了一个环境相关的问题,因为我也是个更新狂魔,用了 brew 之后因为 gfw 的原因,如果长时间不更新,有时候要装一个用它装一个软件的话,前置的更新耗时就会让人非常头大,所以我基本会隔天 update 一下,但是这样会带来一个很心烦的问题,就是像这样,因为我是要用一个固定版本的 PHP,如果一直升需要一直配扩展啥的也很麻烦,如果一直升级 PHP 到最新版可能会比较少碰到这个问题

+
dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
+

这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下

+
+

ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。

+
+

然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
第一步,我们到 brew的目录下

+
cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+

这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
第二步看看它的提交历史

+
git log --follow icu4c.rb
+

在 git log 的海洋中寻找,寻找它的(64版本)的身影

第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支

+
git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
+

其实注意 commit id 旁边的红框,这个是有tag 的,可以直接

+
git checkout icu4c-64
+

PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

+
brew reinstall ./icu4c.rb
+

然后就是第五步,切换版本

+
brew switch icu4c 64.2
+

最后把分支切回来

+
git checkout master
+

是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

+
# zsh
+function hiicu64() {
+  local last_dir=$(pwd)
+
+  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+  git checkout icu4c-4
+  brew reinstall ./icu4c.rb
+  brew switch icu4c 64.2
+  git checkout master
+
+  cd $last_dir
+}
+

对应自己的版本改改版本号就可以了,非常好用。

+]]>
+ + Mac + PHP + Homebrew + PHP + icu4c + + + Mac + PHP + Homebrew + icu4c + zsh + +
+ + 聊聊传说中的 ThreadLocal + /2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/ + 说来也惭愧,这个 ThreadLocal 其实一直都是一知半解,而且看了一下之后还发现记错了,所以还是记录下
原先记忆里的都是反过来,一个 ThreadLocal 是里面按照 thread 作为 key,存储线程内容的,真的是半解都米有,完全是错的,这样就得用 concurrentHashMap 这种去存储并且要锁定线程了,然后内容也只能存一个了,想想简直智障

+

究竟是啥结构

比如我们在代码中 new 一个 ThreadLocal,

+
public static void main(String[] args) {
+        ThreadLocal<Man> tl = new ThreadLocal<>();
+
+        new Thread(() -> {
+            try {
+                TimeUnit.SECONDS.sleep(2);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            System.out.println(tl.get());
+        }).start();
+        new Thread(() -> {
+            try {
+                TimeUnit.SECONDS.sleep(1);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            tl.set(new Man());
+        }).start();
+    }
+
+    static class Man {
+        String name = "nick";
+    }
+

这里构造了两个线程,一个先往里设值,一个后从里取,运行看下结果,

知道这个用法的话肯定知道是取不到值的,只是具体的原理原来搞错了,我们来看下设值 set 方法

+
public void set(T value) {
+    Thread t = Thread.currentThread();
+    ThreadLocalMap map = getMap(t);
+    if (map != null)
+        map.set(this, value);
+    else
+        createMap(t, value);
+}
+

写博客这会我才明白我原来咋会错得这么离谱,看到第一行代码 t 就是当前线程,然后第二行就是用这个线程去getMap,然后我是把这个当成从 map 里取值了,其实这里是

+
ThreadLocalMap getMap(Thread t) {
+    return t.threadLocals;
+}
+

获取 t 的 threadLocals 成员变量,那这个 threadLocals 又是啥呢

它其实是线程 Thread 中的一个类型是java.lang.ThreadLocal.ThreadLocalMap的成员变量
这是 ThreadLocal 的一个静态成员变量

+
static class ThreadLocalMap {
+
+        /**
+         * The entries in this hash map extend WeakReference, using
+         * its main ref field as the key (which is always a
+         * ThreadLocal object).  Note that null keys (i.e. entry.get()
+         * == null) mean that the key is no longer referenced, so the
+         * entry can be expunged from table.  Such entries are referred to
+         * as "stale entries" in the code that follows.
+         */
+        static class Entry extends WeakReference<ThreadLocal<?>> {
+            /** The value associated with this ThreadLocal. */
+            Object value;
+
+            Entry(ThreadLocal<?> k, Object v) {
+                super(k);
+                value = v;
+            }
+        }
+    }
+

全部代码有点长,只截取了一小部分,然后我们再回头来分析前面说的 set 过程,再 copy 下代码

+
public void set(T value) {
+    Thread t = Thread.currentThread();
+    ThreadLocalMap map = getMap(t);
+    if (map != null)
+        map.set(this, value);
+    else
+        createMap(t, value);
+}
+

获取到 map 以后呢,如果 map 不为空,就往 map 里 set,这里注意 key 是啥,其实是当前这个 ThreadLocal,这里就比较明白了究竟是啥结构,每个线程都会维护自身的 ThreadLocalMap,它是线程的一个成员变量,当创建 ThreadLocal 的时候,进行设值的时候其实是往这个 map 里以 ThreadLocal 作为 key,往里设 value。

+

内存泄漏是什么鬼

这里又要看下前面的 ThreadLocalMap 结构了,类似 HashMap,它有个 Entry 结构,在设置的时候会先包装成一个 Entry

+
private void set(ThreadLocal<?> key, Object value) {
 
-# Template availability providers
-org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
-org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
+        // We don't use a fast path as with get() because it is at
+        // least as common to use set() to create new entries as
+        // it is to replace existing ones, in which case, a fast
+        // path would fail more often than not.
 
-# DataSource initializer detectors
-org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
-org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector
-
+ Entry[] tab = table; + int len = tab.length; + int i = key.threadLocalHashCode & (len-1); -

上面根据 org.springframework.boot.autoconfigure.EnableAutoConfiguration 获取的各个配置类,在通过反射加载就能得到一堆 JavaConfig配置类,然后再根据 ConditionalOnProperty等条件配置加载具体的 bean,大致就是这么个逻辑

+ for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + ThreadLocal<?> k = e.get(); + + if (k == key) { + e.value = value; + return; + } + + if (k == null) { + replaceStaleEntry(key, value, i); + return; + } + } + + tab[i] = new Entry(key, value); + int sz = ++size; + if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +} +

这里其实比较重要的就是前面的 Entry 的构造方法,Entry 是个 WeakReference 的子类,然后在构造方法里可以看到 key 会被包装成一个弱引用,这里为什么使用弱引用,其实是方便这个 key 被回收,如果前面的 ThreadLocal tl实例被设置成 null 了,如果这里是直接的强引用的话,就只能等到线程整个回收了,但是其实是弱引用也会有问题,主要是因为这个 value,如果在 ThreadLocal tl 被设置成 null 了,那么其实这个 value 就会没法被访问到,所以最好的操作还是在使用完了就 remove 掉

]]>
Java - SpringBoot Java - Spring - SpringBoot - 自动装配 - AutoConfiguration + ThreadLocal + 弱引用 + 内存泄漏 + WeakReference + +
+ + 聊聊厦门旅游的好与不好 + /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ + 这几天去了趟厦门,原来几年前就想去了,本来都请好假了,后面因为一些事情没去成,这次刚好公司组织,就跟 LD 一起去了厦门,也不洋洋洒洒地写游记了,后面可能会有,今天先来总结下好的地方和比较坑的地方。
这次主要去了中山路、鼓浪屿、曾厝(cuo)垵、植物园、灵玲马戏团,因为住的离环岛路比较近,还有幸现场看了下厦门马拉松,其中

+

中山路

这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。

+

鼓浪屿

鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆

+

曾厝垵

一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆

+

植物园

植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆

+

灵玲马戏团

对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。

+

总结

厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦‍♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。

+]]>
+ + 生活 + 旅游 + + + 生活 + 杭州 + 旅游 + 厦门 + 中山路 + 局口街 + 鼓浪屿 + 曾厝垵 + 植物园 + 马戏团 + 沙茶面 + 海蛎煎
@@ -15477,20 +15477,16 @@ $ - 聊聊那些加塞狗 - /2021/01/17/%E8%81%8A%E8%81%8A%E9%82%A3%E4%BA%9B%E5%8A%A0%E5%A1%9E%E7%8B%97/ - 今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~

+ 聊聊最近平淡的生活之看《神探狄仁杰》 + /2021/12/19/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E3%80%8A%E7%A5%9E%E6%8E%A2%E7%8B%84%E4%BB%81%E6%9D%B0%E3%80%8B/ + 其实最近看的不止这一部,前面看了《继承者们》,《少年包青天》这些,就一起聊下,其中看《继承者们》算是个人比较喜欢,以前就有这种看剧的习惯,这个跟《一生一世》里任嘉伦说自己看《寻秦记》看了几十遍一样,我看喜欢的剧也基本上会看不止五遍,继承者们是有帅哥美女看,而且印象中剧情也挺甜的,一般情况下最好是已经有点遗忘剧情了,因为我个人觉得看剧分两种,无聊了又心情不太好,可以看些这类轻松又看过的剧,可以不完全专心地看剧,另外有心情专心看的时候,可以看一些需要思考,一些探案类的或者烧脑类。
最近看了《神探狄仁杰》,因为跟前面看的《少年包青天》都是这类古装探案剧,正好有些感想,《少年包青天》算是儿时阴影,本来是不太会去看的,正好有一次有机会跟 LD 一起看了会就也觉得比较有意思就看了下去,不得不说,以前的这些剧还是很不错的,包括剧情和演员,第一部一共是 40 集,看的过程中也发现了大概是五个案子,平均八集一个案子,整体节奏还是比较慢的,但是基本每个案子其实都是构思得很巧妙,很久以前看过但是现在基本不太记得剧情了,每个案子在前面几集的时候基本都猜不到犯案逻辑,但是在看了狄仁杰之后,发现两部剧也有比较大的差别,少年包青天相对来说逻辑性会更强一些,个人主观觉得推理的严谨性更高,可能剧本打磨上更将就一下,而狄仁杰因为要提现他的个人强项,不比少年包青天中有公孙策一时瑜亮的情节,狄仁杰中分工明确,李元芳是个武力担当,曾泰是捧哏的,相对来说是狄仁杰在案子里从始至终地推进案情,有些甚至有些玄乎,会有一些跳脱跟不合理,有些像是狄仁杰的奇遇,不过这些想法是私人的观点,并不是想要评孰优孰劣;第二个感受是不知道是不是年代关系,特别是少年包青天,每个案件的大 boss 基本都不是个完全的坏人,甚至都是比较情有可原的可怜人,因为一些特殊原因,而好几个都是包拯身边的人,这一点其实跟狄仁杰里第一个使团惊魂案件比较类似,虎敬晖也是个人物形象比较丰满的角色,不是个标签化的淡薄形象,跟金木兰的感情和反叛行动在最后都说明的缘由,而且也有随着跟狄仁杰一起办案被其影响感化,最终为了救狄仁杰而被金木兰所杀,只是这样金木兰这个角色就会有些偏执和符号化,当然剧本肯定不是能面面俱到,这样的剧本已经比现在很多流量剧的好很多了。还想到了前阵子看的《指环王》中的白袍萨鲁曼在剧中也是个比较单薄的角色,这样的电影彪炳影史也没办法把个个人物都设计得完整有血有肉,或者说这本来也是应该有侧重点,当然其实我也不觉得指环王就是绝对的最好的,因为相对来说故事情节的复杂性等真的不如西游记,只是在 86 版之后的各种乱七八糟的翻牌和乱拍已经让这个真正的王者神话故事有点力不从心,这里边有部西游记后传是个人还比较喜欢的,虽然武打动作比较鬼畜,但是剧情基本是无敌的,在西游的架构上衍生出来这么完整丰富的故事,人物角色也都有各自的出彩点。
说回狄仁杰,在这之前也看过徐克拍的几部狄仁杰的电影版,第一部刘德华拍得相对完成度更高,故事性也可圈可点,后面几部就是剧情拉胯,靠特效拉回来一点分,虽说这个也是所谓的狄仁杰宇宙的构思在里面但是现在来看基本是跟西游那些差不多,完全没有整体性可言,打一枪换一个地方,演员也没有延续性,剧情也是前后跳脱,没什么关联跟承上启下,导致质量层次不一,更不用谈什么狄仁杰宇宙了,不过这个事情也是难说,原因很多,现在资本都是更加趋利的,一些需要更长久时间才能有回报的投资是很难获得资本青睐,所以只能将重心投给选择一些流量明星,而本来应该将资源投给剧本打磨的基本就没了,再深入说也没意义了,社会现状就是这样。
还有一点感想是,以前的剧里的拍摄环境还是比较惨的,看着一些房子,甚至皇宫都是比较破旧的,地面还是石板这种,想想以前的演员的环境再想想现在的,比如成龙说的,以前他拍剧就是啪摔了,问这条有没有过,过了就直接送医院,而不是现在可能手蹭破点皮就大叫,甚至还有饭圈这些破事。

]]>
生活 - 开车 生活 - 开车 - 加塞 - 糟心事 - 规则 + 看剧
@@ -15511,52 +15507,50 @@ $ - 聊聊给亲戚朋友的老电脑重装系统那些事儿 - /2021/05/09/%E8%81%8A%E8%81%8A%E7%BB%99%E4%BA%B2%E6%88%9A%E6%9C%8B%E5%8F%8B%E7%9A%84%E8%80%81%E7%94%B5%E8%84%91%E9%87%8D%E8%A3%85%E7%B3%BB%E7%BB%9F%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF/ - 前面这个五一回去之前,LD 姐姐跟我说电脑很卡了,想让我重装系统,问了下 LD 可能是那个 09 年买的笔记本,想想有点害怕[捂脸],前年有一次好像让我帮忙装了她同事的一个三星的笔记本,本着一些系统洁癖,所以就从开始找纯净版的 win7 家庭版,因为之前那些本基本都自带 win7 的家庭版,而且把激活码就贴在机器下面,然后从三星官网去找官方驱动,还好这个机型的驱动还在,先做了系统镜像,其实感觉这种情况需要两个 U 盘,一个 U 盘装系统作为安装启动盘,一个放驱动,毕竟不是专业装系统的,然后因为官方驱动需要一个个下载一个个安装,然后驱动文件下载的地方还没标明是 32 位还是 64 位的,结果还被 LD 姐姐催着,一直问好没好,略尴尬,索性还是找个一键安装的

-

这次甚至更夸张,上次还让带回去,我准备好了系统镜像啥的,第二天装,这次直接带了两个老旧笔记本过来说让当天就装好,感觉有点像被当修电脑的使,又说这些电脑其实都不用了的,都是为了她们当医生的要每年看会课,然后只能用电脑浏览器看,结果都在用 360 浏览器,真的是万恶的 360,其实以前对 360 没啥坏印象,毕竟以前也经常用,只是对于这些老电脑,360 全家桶真的就是装了就废了,2G 的内存,开机就开着 360 安全卫士,360 杀毒,有一个还装了腾讯电脑管家,然后腾讯视频跟爱奇艺也开机启动了,然后还打开 360 浏览器看课,就算再好的系统也吃不消这么用,重装了系统,还是这么装这些东西,也是分分钟变卡,可惜他们都没啥这类概念。

-

对于他们要看的课,更搞笑的是,明明在页面上注明了说要使用 IE 浏览器,结果他们都在用 360 浏览器看,但是这个也不能完全怪他们,因为实在是现在的 IE 啥的也有开始不兼容 flash 的配置,需要开启兼容配置,但是只要开启了之后就可以直接用 IE 看,比 360 靠谱很多, 资源占用也比较少,360 估计是基于 chromium 加了很多内置的插件,本身 chromium 也是内存大户,但是说这些其实他们也不懂,总觉得找我免费装下系统能撑一段时间,反正对我来说也应该很简单(他们觉得),实际上开始工作以后,我自己想装个双系统都是上淘宝买别人的服务装的,台式机更是几年没动过系统了,因为要重装一大堆软件,数据备份啥的,还有驱动什么的,分区格式,那些驱动精灵啥的也都是越来越坑,一装就给你带一堆垃圾软件。

-

感悟是,总觉得学计算机的就应该会装系统,会修电脑,之前亲戚还拿着一个完全开不起来的笔记本让我来修,这真的是,我说可以找官方维修的,结果我说我搞不定,她直接觉得是修不好了,直接电脑都懒得拿回去了,后面又一次反复解释了才明白,另外就是 360 全家桶,别说老电脑了,新机器都不太吃得消。

+ 聊聊这次换车牌及其他 + /2022/02/20/%E8%81%8A%E8%81%8A%E8%BF%99%E6%AC%A1%E6%8D%A2%E8%BD%A6%E7%89%8C%E5%8F%8A%E5%85%B6%E4%BB%96/ + 去年 8 月份运气比较好,摇到了车牌,本来其实应该很早就开始摇的,前面第一次换工作没注意社保断缴了一个月,也是大意失荆州,后面到了 17 年社保满两年了,好像只摇了一次,还是就没摇过,有点忘了,好像是什么原因导致那次也没摇成功,但是后面暂住证就取消了,需要居住证,居住证又要一年及以上的租房合同,并且那会买车以后也不怎么开,住的地方车位还好,但是公司车位一个月要两三千,甚至还是打车上下班比较实惠,所以也没放在心上,后面摇到房以后,也觉得应该准备起来车子,就开始办了居住证,居住证其实还可以用劳动合同,而且办起来也挺快,大概是三四月份开始摇,到 8 月份的某一天收到短信说摇到了,一开始还挺开心,不过心里抱着也不怎么开,也没怎么大放在心上,不过这里有一点就是我把那个照片直接发出去,上面有着我的身份证号,被 LD 说了一顿,以后也应该小心点,但是后面不知道是哪里看了下,说杭州上牌已经需要国六标准的车了,瞬间感觉是空欢喜了,可是有同事说是可以的,我就又打了官方的电话,结果说可以的,要先转籍,然后再做上牌。

+

转籍其实是很方便的,在交警 12123 App 上申请就行了,在转籍以后,需要去实地验车,验车的话,在支付宝-杭州交警生活号里进行预约,找就近的车管所就好,需要准备一些东西,首先是行驶证,机动车登记证书,身份证,居住证,还有车上需要准备的东西是要有三脚架和反光背心,反光背心是最近几个月开始要的,问过之前去验车的只需要三脚架就好了,预约好了的话建议是赶上班时间越早越好,不然过去排队时间要很久,而且人多了以后会很乱,各种插队,而且有很多都是汽车销售,一个销售带着一堆车,我们附近那个进去的小路没一会就堵满车,进去需要先排队,然后扫码,接着交资料,这两个都排着队,如果去晚了就要排很久的队,交完资料才是排队等验车,验车就是打开引擎盖,有人会帮忙拓印发动机车架号,然后验车的会各种检查一下,车里面,还有后备箱,建议车内整理干净点,后备箱不要放杂物,检验完了之后,需要把三脚架跟反光背心放在后备箱盖子上,人在旁边拍个照,然后需要把车牌遮住后再拍个车子的照片,再之后就是去把车牌卸了,这个多吐槽下,那边应该是本来那边师傅帮忙卸车牌,结果他就说是教我们拆,虽然也不算难,但是不排除师傅有在偷懒,完了之后就是把旧车牌交回去,然后需要在手机上(警察叔叔 App)提交各种资料,包括身份证,行驶证,机动车登记证书,提交了之后就等寄车牌过来了。

+

这里面缺失的一个环节就是选号了,选号杭州有两个方式,一种就是根据交管局定期发布的选号号段,可以自定义拼 20 个号,在手机上的交警 12123 App 上可以三个一组的形式提交,如果有没被选走的,就可以预选到这个了,但是这种就是也需要有一定策略,最新出的号段能选中的概率大一点,然后数字全是 8,6 这种的肯定会一早就被选走,然后如果跟我一样可以提前选下尾号,因为尾号数字影响限号,我比较有可能周五回家,所以得避开 5,0 的,第二种就是 50 选一跟以前新车选号一样,就不介绍了。第一种选中了以后可以在前面交还旧车牌的时候填上等着寄过来了,因为我是第一种选中的,第二种也可以在手机上选,也在可以在交还车牌的时候现场选。

+

总体过程其实是 LD 在各种查资料跟帮我跑来跑去,要不是 LD,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。

]]>
生活 生活 - 装电脑 - 老电脑 - 360 全家桶 - 修电脑的 + 换车牌
- 聊聊部分公交车的设计bug - /2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ - 今天惯例坐公交回住的地方,不小心撞了头,原因是我们想坐倒数第二排,然后LD 走在我后面,我就走到最后一排中间等着,但是最后一排是高一截的,等 LD 坐进去以后,我就往前走,结果撞到了车顶的扶手杆子的一端,差点撞昏了去,这里我觉得其实杆子长度应该短一点,不然从最后一排出来,还是有比较大概率因为没注意看而撞到头,特别是没注意看的情况,发力其实会比较大,一头撞上就会像我这样,眼前一黑,又痛得要死。
还有一点就是座位设计了,先来看个图

图里大致画了两条线,因为可能是轮胎还是什么原因,后排中间会有那么大的突起,但是看两条红线可以发现,靠近过道的座位边缘跟地面突起的边缘不是一样宽的,这样导致的结果就是坐着的时候有一个脚没地儿搁,要不就得侧着斜着坐,或者就是一个脚悬空,短程的可能还好,路程远一点还是比较难受的,特别是像我现在这样,大腿外侧有点难受的情况,就会更难受。
虽然说这两个点,基本是屁用没有,但是我也是在自己这个博客说说,也当是个树洞了。

+ 聊聊那些加塞狗 + /2021/01/17/%E8%81%8A%E8%81%8A%E9%82%A3%E4%BA%9B%E5%8A%A0%E5%A1%9E%E7%8B%97/ + 今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~

]]>
生活 + 开车 生活 - 公交 - 杭州 + 开车 + 加塞 + 糟心事 + 规则
- 聊聊这次换车牌及其他 - /2022/02/20/%E8%81%8A%E8%81%8A%E8%BF%99%E6%AC%A1%E6%8D%A2%E8%BD%A6%E7%89%8C%E5%8F%8A%E5%85%B6%E4%BB%96/ - 去年 8 月份运气比较好,摇到了车牌,本来其实应该很早就开始摇的,前面第一次换工作没注意社保断缴了一个月,也是大意失荆州,后面到了 17 年社保满两年了,好像只摇了一次,还是就没摇过,有点忘了,好像是什么原因导致那次也没摇成功,但是后面暂住证就取消了,需要居住证,居住证又要一年及以上的租房合同,并且那会买车以后也不怎么开,住的地方车位还好,但是公司车位一个月要两三千,甚至还是打车上下班比较实惠,所以也没放在心上,后面摇到房以后,也觉得应该准备起来车子,就开始办了居住证,居住证其实还可以用劳动合同,而且办起来也挺快,大概是三四月份开始摇,到 8 月份的某一天收到短信说摇到了,一开始还挺开心,不过心里抱着也不怎么开,也没怎么大放在心上,不过这里有一点就是我把那个照片直接发出去,上面有着我的身份证号,被 LD 说了一顿,以后也应该小心点,但是后面不知道是哪里看了下,说杭州上牌已经需要国六标准的车了,瞬间感觉是空欢喜了,可是有同事说是可以的,我就又打了官方的电话,结果说可以的,要先转籍,然后再做上牌。

-

转籍其实是很方便的,在交警 12123 App 上申请就行了,在转籍以后,需要去实地验车,验车的话,在支付宝-杭州交警生活号里进行预约,找就近的车管所就好,需要准备一些东西,首先是行驶证,机动车登记证书,身份证,居住证,还有车上需要准备的东西是要有三脚架和反光背心,反光背心是最近几个月开始要的,问过之前去验车的只需要三脚架就好了,预约好了的话建议是赶上班时间越早越好,不然过去排队时间要很久,而且人多了以后会很乱,各种插队,而且有很多都是汽车销售,一个销售带着一堆车,我们附近那个进去的小路没一会就堵满车,进去需要先排队,然后扫码,接着交资料,这两个都排着队,如果去晚了就要排很久的队,交完资料才是排队等验车,验车就是打开引擎盖,有人会帮忙拓印发动机车架号,然后验车的会各种检查一下,车里面,还有后备箱,建议车内整理干净点,后备箱不要放杂物,检验完了之后,需要把三脚架跟反光背心放在后备箱盖子上,人在旁边拍个照,然后需要把车牌遮住后再拍个车子的照片,再之后就是去把车牌卸了,这个多吐槽下,那边应该是本来那边师傅帮忙卸车牌,结果他就说是教我们拆,虽然也不算难,但是不排除师傅有在偷懒,完了之后就是把旧车牌交回去,然后需要在手机上(警察叔叔 App)提交各种资料,包括身份证,行驶证,机动车登记证书,提交了之后就等寄车牌过来了。

-

这里面缺失的一个环节就是选号了,选号杭州有两个方式,一种就是根据交管局定期发布的选号号段,可以自定义拼 20 个号,在手机上的交警 12123 App 上可以三个一组的形式提交,如果有没被选走的,就可以预选到这个了,但是这种就是也需要有一定策略,最新出的号段能选中的概率大一点,然后数字全是 8,6 这种的肯定会一早就被选走,然后如果跟我一样可以提前选下尾号,因为尾号数字影响限号,我比较有可能周五回家,所以得避开 5,0 的,第二种就是 50 选一跟以前新车选号一样,就不介绍了。第一种选中了以后可以在前面交还旧车牌的时候填上等着寄过来了,因为我是第一种选中的,第二种也可以在手机上选,也在可以在交还车牌的时候现场选。

-

总体过程其实是 LD 在各种查资料跟帮我跑来跑去,要不是 LD,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。

+ 聊聊部分公交车的设计bug + /2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ + 今天惯例坐公交回住的地方,不小心撞了头,原因是我们想坐倒数第二排,然后LD 走在我后面,我就走到最后一排中间等着,但是最后一排是高一截的,等 LD 坐进去以后,我就往前走,结果撞到了车顶的扶手杆子的一端,差点撞昏了去,这里我觉得其实杆子长度应该短一点,不然从最后一排出来,还是有比较大概率因为没注意看而撞到头,特别是没注意看的情况,发力其实会比较大,一头撞上就会像我这样,眼前一黑,又痛得要死。
还有一点就是座位设计了,先来看个图

图里大致画了两条线,因为可能是轮胎还是什么原因,后排中间会有那么大的突起,但是看两条红线可以发现,靠近过道的座位边缘跟地面突起的边缘不是一样宽的,这样导致的结果就是坐着的时候有一个脚没地儿搁,要不就得侧着斜着坐,或者就是一个脚悬空,短程的可能还好,路程远一点还是比较难受的,特别是像我现在这样,大腿外侧有点难受的情况,就会更难受。
虽然说这两个点,基本是屁用没有,但是我也是在自己这个博客说说,也当是个树洞了。

]]>
生活 生活 - 换车牌 + 公交 + 杭州
@@ -15629,16 +15623,22 @@ $ - 聊聊最近平淡的生活之看《神探狄仁杰》 - /2021/12/19/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E3%80%8A%E7%A5%9E%E6%8E%A2%E7%8B%84%E4%BB%81%E6%9D%B0%E3%80%8B/ - 其实最近看的不止这一部,前面看了《继承者们》,《少年包青天》这些,就一起聊下,其中看《继承者们》算是个人比较喜欢,以前就有这种看剧的习惯,这个跟《一生一世》里任嘉伦说自己看《寻秦记》看了几十遍一样,我看喜欢的剧也基本上会看不止五遍,继承者们是有帅哥美女看,而且印象中剧情也挺甜的,一般情况下最好是已经有点遗忘剧情了,因为我个人觉得看剧分两种,无聊了又心情不太好,可以看些这类轻松又看过的剧,可以不完全专心地看剧,另外有心情专心看的时候,可以看一些需要思考,一些探案类的或者烧脑类。
最近看了《神探狄仁杰》,因为跟前面看的《少年包青天》都是这类古装探案剧,正好有些感想,《少年包青天》算是儿时阴影,本来是不太会去看的,正好有一次有机会跟 LD 一起看了会就也觉得比较有意思就看了下去,不得不说,以前的这些剧还是很不错的,包括剧情和演员,第一部一共是 40 集,看的过程中也发现了大概是五个案子,平均八集一个案子,整体节奏还是比较慢的,但是基本每个案子其实都是构思得很巧妙,很久以前看过但是现在基本不太记得剧情了,每个案子在前面几集的时候基本都猜不到犯案逻辑,但是在看了狄仁杰之后,发现两部剧也有比较大的差别,少年包青天相对来说逻辑性会更强一些,个人主观觉得推理的严谨性更高,可能剧本打磨上更将就一下,而狄仁杰因为要提现他的个人强项,不比少年包青天中有公孙策一时瑜亮的情节,狄仁杰中分工明确,李元芳是个武力担当,曾泰是捧哏的,相对来说是狄仁杰在案子里从始至终地推进案情,有些甚至有些玄乎,会有一些跳脱跟不合理,有些像是狄仁杰的奇遇,不过这些想法是私人的观点,并不是想要评孰优孰劣;第二个感受是不知道是不是年代关系,特别是少年包青天,每个案件的大 boss 基本都不是个完全的坏人,甚至都是比较情有可原的可怜人,因为一些特殊原因,而好几个都是包拯身边的人,这一点其实跟狄仁杰里第一个使团惊魂案件比较类似,虎敬晖也是个人物形象比较丰满的角色,不是个标签化的淡薄形象,跟金木兰的感情和反叛行动在最后都说明的缘由,而且也有随着跟狄仁杰一起办案被其影响感化,最终为了救狄仁杰而被金木兰所杀,只是这样金木兰这个角色就会有些偏执和符号化,当然剧本肯定不是能面面俱到,这样的剧本已经比现在很多流量剧的好很多了。还想到了前阵子看的《指环王》中的白袍萨鲁曼在剧中也是个比较单薄的角色,这样的电影彪炳影史也没办法把个个人物都设计得完整有血有肉,或者说这本来也是应该有侧重点,当然其实我也不觉得指环王就是绝对的最好的,因为相对来说故事情节的复杂性等真的不如西游记,只是在 86 版之后的各种乱七八糟的翻牌和乱拍已经让这个真正的王者神话故事有点力不从心,这里边有部西游记后传是个人还比较喜欢的,虽然武打动作比较鬼畜,但是剧情基本是无敌的,在西游的架构上衍生出来这么完整丰富的故事,人物角色也都有各自的出彩点。
说回狄仁杰,在这之前也看过徐克拍的几部狄仁杰的电影版,第一部刘德华拍得相对完成度更高,故事性也可圈可点,后面几部就是剧情拉胯,靠特效拉回来一点分,虽说这个也是所谓的狄仁杰宇宙的构思在里面但是现在来看基本是跟西游那些差不多,完全没有整体性可言,打一枪换一个地方,演员也没有延续性,剧情也是前后跳脱,没什么关联跟承上启下,导致质量层次不一,更不用谈什么狄仁杰宇宙了,不过这个事情也是难说,原因很多,现在资本都是更加趋利的,一些需要更长久时间才能有回报的投资是很难获得资本青睐,所以只能将重心投给选择一些流量明星,而本来应该将资源投给剧本打磨的基本就没了,再深入说也没意义了,社会现状就是这样。
还有一点感想是,以前的剧里的拍摄环境还是比较惨的,看着一些房子,甚至皇宫都是比较破旧的,地面还是石板这种,想想以前的演员的环境再想想现在的,比如成龙说的,以前他拍剧就是啪摔了,问这条有没有过,过了就直接送医院,而不是现在可能手蹭破点皮就大叫,甚至还有饭圈这些破事。

+ 这周末我又在老丈人家打了天小工 + /2020/08/30/%E8%BF%99%E5%91%A8%E6%9C%AB%E6%88%91%E5%8F%88%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/ + 因为活实在比较多,也不太好叫大工(活比较杂散),相比上一次我跟 LD 俩人晚起了一点,我真的是只要有事,早上就醒的很早,准备八点出发的,六点就醒了,然后想继续睡就一直做梦🤦‍♂️,差不多八点半多到的丈人家,他们应该已经干了有一会了,我们到了以后就分配给我撬地板的活,上次说的那个敲掉柜子的房间里,还铺着质地还不错的木地板,但是也不想要了,得撬掉重新铺。
拿着撬棍和榔头就上楼去干了,浙江这几天的天气,最高温度一般 38、9,楼上那个房间也没风扇,有了也不能用,都是灰尘,撬了两下,我感觉我体内的水就像真气爆发一样变成汗炸了出来,眼睛全被汗糊住了,可能大部分人不太了解地板是怎么铺的,一般是在地面先铺一层混凝土,混凝土中间嵌进去规则的长条木条,然后真正的地板一块块的都是钉在那个木条上,用那种气枪钉和普通的钉子,并且块跟块之前还有一个木头的槽结构相互耦合,然后边缘的一圈在用较薄的木板将整个木地板封边(这些词都是我现造的),边缘的用的钉子会更多,所以那几下真的很用力,而且撬地板,得蹲下起来,如此反复,对于我这个体重快超过身高的中年人来说的确是非常大的挑战,接下来继续撬了几个,已经有种要虚脱晕倒的感觉了,及时去喝水擦了汗,又歇了一会,为啥一上来就这么拼呢,主要是因为那个房间丈人在干活的时候是直接看得到的🤦‍♂️,后来被 LD 一顿教育,本来就是去帮忙的,又不是专业做这个的,急啥。
喝了水之后,又稍稍歇了一会,就开始继续撬了,本来觉得这个地板撬着好像还行,房间不大,没多久就撬完了,撬完之后喝了点饮料(补充点糖分,早餐吃得少,有点低血糖),然后看到 LD 在撬下面的木条了,这个动作开始了那天最大的经验值收集行动,前面说了这个木条一般是跟混凝土一块铺上去的,但是谁也没想到,这个混凝土铺上去的时候竟然处理的这么随意,根本没考虑跟下面的贴合,所以撬木条的时候直接把木条跟木条中间大块大块的混凝土一块撬起来了,想想那重量,于是我这靠蛮力干活的,就用力把木条带着混凝土一块撬了起来,还沾沾自喜,但是发现结果是撬起来一块之后,体力值瞬间归零,上一篇我也提到了,其实干这类活也是很有技巧性的,但是上次的是我没学会,可能需要花时间学的,但是这次是LD 用她的纤细胳膊教会我的,我在撬的时候,屏住一口气,双手用力,起,大概是吃好几口奶的力气都用出来了,但是 LD 在我休息的时候,慢慢悠悠的,先把撬棍挤到木条或者混凝土跟下层的缝里,然后往下垫一小块混凝土碎石,然后轻轻松松的扳两下,就撬开了,亏我高中的时候引以为傲的物理成绩,作为物理课代表,这么浅显易懂的杠杆原理都完全不会用到生活里,后面在用这个技巧撬的过程中,真的觉得自己蠢到家了,当然在明白了用点杠杆原理之后,撬地板的活就变得慢慢悠悠,悠哉悠哉的了(其实还是很热的,披着毛巾擦眼睛)。
上午的活差不多完了,后面就是把撬出来的混凝土和地板条丢下去,地上铺着不用了的被子,然后就是午饭和午休环节了,午饭换了一家快餐,味道非常可以,下午的活就比较单调了,帮忙清理了上去扔下来的混凝土碎块跟木条,然后稍微打扫了下,老丈人就让我们回家了,接着上次说的,还是觉得比跑步啥的消耗大太多了,那汗流的,一口就能喝完一瓶 500 毫升左右的矿泉水。

]]>
生活 + 运动 + 跑步 + 干活 生活 - 看剧 + 运动 + 减肥 + 跑步 + 干活
@@ -15689,6 +15689,22 @@ zk3 192.168.2.3 + + 重看了下《蛮荒记》说说感受 + /2021/10/10/%E9%87%8D%E7%9C%8B%E4%BA%86%E4%B8%8B%E3%80%8A%E8%9B%AE%E8%8D%92%E8%AE%B0%E3%80%8B%E8%AF%B4%E8%AF%B4%E6%84%9F%E5%8F%97/ + 周末把《蛮荒记》看完了,前面是发现微信读书有《搜神记》和《蛮荒记》,但是《搜神记》看了会发现很多都是跳段了,不知道为啥,貌似也没什么少儿不宜的情节,所以就上网找了原版来看,为什么看这个呢,主要还是高中的时候看过,觉得写得很不错,属于那时候的玄幻小说里的独一档,基于山海经创造了一个半架空的大荒宇宙,五族帝尊,人物名都是听说过的,而且又能契合部分历史,整个故事布局非常宏大,并且情节矛盾埋得很深,这里就不对具体情节作介绍了,只是聊聊对书中的一些人物和情节的看法感受。

+

乌丝兰玛是个贯穿两部,甚至在蛮荒的最后还要再搞事情,极其坚定的自以为是的大 boss,其实除了最后被我们的主人公打败,前面几乎就是无所不能,下了一盘无比巨大的棋,主人公都只是其中一个棋子和意外,但是正如很多反派,一直以来都是背着一个信念,并且这个所谓的信念是比较正义的,只是为了这个正义的信念和目标却做了各种丧尽天良的事情,说起来跟灭霸有点像,为了环保哈哈,相对来说感觉姬远玄也只是个最大牌的工具人,或者说是中间人,深爱的妹妹冰夷也意外被蚩尤怒拿一血。

+

但是中间那个赤霞仙子一定要给烈烟石的心上锁,导致最后认不出来蚩尤,也间接导致了蚩尤被杀,如果不考虑最后情节或者推动故事的需求,这个还是我很讨厌的,有点类似于《驴得水》里那个校长,看着貌似是个正常的,做的事情也是正派,但是其实是害人不浅,即使南阳仙子因此被抛进了火山,那也是有贱人在那挑食,并且赤松子是赤飚怒的儿子,烈烟石跟蚩尤又没这层关系,就很像倚天屠龙记里的灭绝师太和极品家丁里的那个玉德仙坊的院主,后者还好一些,前者几乎就是导致周芷若一生悲剧的始作俑者,自己偏执的善恶观,还要给徒弟灌输如此恶毒的理念和让她立下像紧箍咒似的誓言,在人一生中本来就有很多不能如愿的,又被最亲最尊敬的人下了这样的紧箍咒,人生的不幸也加倍了。

+

似乎习惯了总要有个总结的,想说的应该是我觉得这些剧也好,书也好,我觉得最坏的人可能是大部分人眼中的一些次要人物,或者至少大 boss 才是最坏的人,当然这个坏也不是严格的二分法,只是我觉得最让我觉得负面的人物,这些人可能看起来情景出现的不多,只是说了很少的话,做了很少的事,但是在我看来却做了最大的恶。

+]]>
+ + 生活 + + + 生活 + 看书 + +
闲聊下乘公交的用户体验 /2021/02/28/%E9%97%B2%E8%81%8A%E4%B8%8B%E4%B9%98%E5%85%AC%E4%BA%A4%E7%9A%84%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C/ @@ -15712,19 +15728,18 @@ zk3 192.168.2.3 - 重看了下《蛮荒记》说说感受 - /2021/10/10/%E9%87%8D%E7%9C%8B%E4%BA%86%E4%B8%8B%E3%80%8A%E8%9B%AE%E8%8D%92%E8%AE%B0%E3%80%8B%E8%AF%B4%E8%AF%B4%E6%84%9F%E5%8F%97/ - 周末把《蛮荒记》看完了,前面是发现微信读书有《搜神记》和《蛮荒记》,但是《搜神记》看了会发现很多都是跳段了,不知道为啥,貌似也没什么少儿不宜的情节,所以就上网找了原版来看,为什么看这个呢,主要还是高中的时候看过,觉得写得很不错,属于那时候的玄幻小说里的独一档,基于山海经创造了一个半架空的大荒宇宙,五族帝尊,人物名都是听说过的,而且又能契合部分历史,整个故事布局非常宏大,并且情节矛盾埋得很深,这里就不对具体情节作介绍了,只是聊聊对书中的一些人物和情节的看法感受。

-

乌丝兰玛是个贯穿两部,甚至在蛮荒的最后还要再搞事情,极其坚定的自以为是的大 boss,其实除了最后被我们的主人公打败,前面几乎就是无所不能,下了一盘无比巨大的棋,主人公都只是其中一个棋子和意外,但是正如很多反派,一直以来都是背着一个信念,并且这个所谓的信念是比较正义的,只是为了这个正义的信念和目标却做了各种丧尽天良的事情,说起来跟灭霸有点像,为了环保哈哈,相对来说感觉姬远玄也只是个最大牌的工具人,或者说是中间人,深爱的妹妹冰夷也意外被蚩尤怒拿一血。

-

但是中间那个赤霞仙子一定要给烈烟石的心上锁,导致最后认不出来蚩尤,也间接导致了蚩尤被杀,如果不考虑最后情节或者推动故事的需求,这个还是我很讨厌的,有点类似于《驴得水》里那个校长,看着貌似是个正常的,做的事情也是正派,但是其实是害人不浅,即使南阳仙子因此被抛进了火山,那也是有贱人在那挑食,并且赤松子是赤飚怒的儿子,烈烟石跟蚩尤又没这层关系,就很像倚天屠龙记里的灭绝师太和极品家丁里的那个玉德仙坊的院主,后者还好一些,前者几乎就是导致周芷若一生悲剧的始作俑者,自己偏执的善恶观,还要给徒弟灌输如此恶毒的理念和让她立下像紧箍咒似的誓言,在人一生中本来就有很多不能如愿的,又被最亲最尊敬的人下了这样的紧箍咒,人生的不幸也加倍了。

-

似乎习惯了总要有个总结的,想说的应该是我觉得这些剧也好,书也好,我觉得最坏的人可能是大部分人眼中的一些次要人物,或者至少大 boss 才是最坏的人,当然这个坏也不是严格的二分法,只是我觉得最让我觉得负面的人物,这些人可能看起来情景出现的不多,只是说了很少的话,做了很少的事,但是在我看来却做了最大的恶。

+ 闲话篇-路遇神逻辑骑车带娃爹 + /2022/05/08/%E9%97%B2%E8%AF%9D%E7%AF%87-%E8%B7%AF%E9%81%87%E7%A5%9E%E9%80%BB%E8%BE%91%E9%AA%91%E8%BD%A6%E5%B8%A6%E5%A8%83%E7%88%B9/ + 周末吃完中饭去买菜,没想到碰到这个神(zhi)奇(zhang)大哥带着两个娃,在非机动车道虽然没有像上班高峰车那么多,但是有送外卖,各种叮咚买菜和普通像我这样骑电驴,骑自行车的人,我的情况可能还特殊点,前面说过电驴买了以后本来网上找到过怎么解除限速的,后面看了下,限速 25 虽然慢,但还是对安全很有好处的,我上下班也不赶这个时间,所以就没解除,其他路上的电瓶车包括这位带娃的大哥可能有不少都不符合国标的限速要求或者解除了限速,这些算是铺垫。

+

那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。

+

现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。

+

说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。

]]>
生活 生活 - 看书
@@ -15742,31 +15757,35 @@ zk3 192.168.2.3 - 闲话篇-路遇神逻辑骑车带娃爹 - /2022/05/08/%E9%97%B2%E8%AF%9D%E7%AF%87-%E8%B7%AF%E9%81%87%E7%A5%9E%E9%80%BB%E8%BE%91%E9%AA%91%E8%BD%A6%E5%B8%A6%E5%A8%83%E7%88%B9/ - 周末吃完中饭去买菜,没想到碰到这个神(zhi)奇(zhang)大哥带着两个娃,在非机动车道虽然没有像上班高峰车那么多,但是有送外卖,各种叮咚买菜和普通像我这样骑电驴,骑自行车的人,我的情况可能还特殊点,前面说过电驴买了以后本来网上找到过怎么解除限速的,后面看了下,限速 25 虽然慢,但还是对安全很有好处的,我上下班也不赶这个时间,所以就没解除,其他路上的电瓶车包括这位带娃的大哥可能有不少都不符合国标的限速要求或者解除了限速,这些算是铺垫。

-

那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。

-

现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。

-

说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。

+ 难得的大扫除 + /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/ + 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 LD 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。

]]>
生活 生活 + 大扫除
- 难得的大扫除 - /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/ - 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 LD 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。

+ 聊聊给亲戚朋友的老电脑重装系统那些事儿 + /2021/05/09/%E8%81%8A%E8%81%8A%E7%BB%99%E4%BA%B2%E6%88%9A%E6%9C%8B%E5%8F%8B%E7%9A%84%E8%80%81%E7%94%B5%E8%84%91%E9%87%8D%E8%A3%85%E7%B3%BB%E7%BB%9F%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF/ + 前面这个五一回去之前,LD 姐姐跟我说电脑很卡了,想让我重装系统,问了下 LD 可能是那个 09 年买的笔记本,想想有点害怕[捂脸],前年有一次好像让我帮忙装了她同事的一个三星的笔记本,本着一些系统洁癖,所以就从开始找纯净版的 win7 家庭版,因为之前那些本基本都自带 win7 的家庭版,而且把激活码就贴在机器下面,然后从三星官网去找官方驱动,还好这个机型的驱动还在,先做了系统镜像,其实感觉这种情况需要两个 U 盘,一个 U 盘装系统作为安装启动盘,一个放驱动,毕竟不是专业装系统的,然后因为官方驱动需要一个个下载一个个安装,然后驱动文件下载的地方还没标明是 32 位还是 64 位的,结果还被 LD 姐姐催着,一直问好没好,略尴尬,索性还是找个一键安装的

+

这次甚至更夸张,上次还让带回去,我准备好了系统镜像啥的,第二天装,这次直接带了两个老旧笔记本过来说让当天就装好,感觉有点像被当修电脑的使,又说这些电脑其实都不用了的,都是为了她们当医生的要每年看会课,然后只能用电脑浏览器看,结果都在用 360 浏览器,真的是万恶的 360,其实以前对 360 没啥坏印象,毕竟以前也经常用,只是对于这些老电脑,360 全家桶真的就是装了就废了,2G 的内存,开机就开着 360 安全卫士,360 杀毒,有一个还装了腾讯电脑管家,然后腾讯视频跟爱奇艺也开机启动了,然后还打开 360 浏览器看课,就算再好的系统也吃不消这么用,重装了系统,还是这么装这些东西,也是分分钟变卡,可惜他们都没啥这类概念。

+

对于他们要看的课,更搞笑的是,明明在页面上注明了说要使用 IE 浏览器,结果他们都在用 360 浏览器看,但是这个也不能完全怪他们,因为实在是现在的 IE 啥的也有开始不兼容 flash 的配置,需要开启兼容配置,但是只要开启了之后就可以直接用 IE 看,比 360 靠谱很多, 资源占用也比较少,360 估计是基于 chromium 加了很多内置的插件,本身 chromium 也是内存大户,但是说这些其实他们也不懂,总觉得找我免费装下系统能撑一段时间,反正对我来说也应该很简单(他们觉得),实际上开始工作以后,我自己想装个双系统都是上淘宝买别人的服务装的,台式机更是几年没动过系统了,因为要重装一大堆软件,数据备份啥的,还有驱动什么的,分区格式,那些驱动精灵啥的也都是越来越坑,一装就给你带一堆垃圾软件。

+

感悟是,总觉得学计算机的就应该会装系统,会修电脑,之前亲戚还拿着一个完全开不起来的笔记本让我来修,这真的是,我说可以找官方维修的,结果我说我搞不定,她直接觉得是修不好了,直接电脑都懒得拿回去了,后面又一次反复解释了才明白,另外就是 360 全家桶,别说老电脑了,新机器都不太吃得消。

]]>
生活 生活 - 大扫除 + 装电脑 + 老电脑 + 360 全家桶 + 修电脑的
@@ -15903,23 +15922,4 @@ zk3 192.168.2.3 - - 这周末我又在老丈人家打了天小工 - /2020/08/30/%E8%BF%99%E5%91%A8%E6%9C%AB%E6%88%91%E5%8F%88%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/ - 因为活实在比较多,也不太好叫大工(活比较杂散),相比上一次我跟 LD 俩人晚起了一点,我真的是只要有事,早上就醒的很早,准备八点出发的,六点就醒了,然后想继续睡就一直做梦🤦‍♂️,差不多八点半多到的丈人家,他们应该已经干了有一会了,我们到了以后就分配给我撬地板的活,上次说的那个敲掉柜子的房间里,还铺着质地还不错的木地板,但是也不想要了,得撬掉重新铺。
拿着撬棍和榔头就上楼去干了,浙江这几天的天气,最高温度一般 38、9,楼上那个房间也没风扇,有了也不能用,都是灰尘,撬了两下,我感觉我体内的水就像真气爆发一样变成汗炸了出来,眼睛全被汗糊住了,可能大部分人不太了解地板是怎么铺的,一般是在地面先铺一层混凝土,混凝土中间嵌进去规则的长条木条,然后真正的地板一块块的都是钉在那个木条上,用那种气枪钉和普通的钉子,并且块跟块之前还有一个木头的槽结构相互耦合,然后边缘的一圈在用较薄的木板将整个木地板封边(这些词都是我现造的),边缘的用的钉子会更多,所以那几下真的很用力,而且撬地板,得蹲下起来,如此反复,对于我这个体重快超过身高的中年人来说的确是非常大的挑战,接下来继续撬了几个,已经有种要虚脱晕倒的感觉了,及时去喝水擦了汗,又歇了一会,为啥一上来就这么拼呢,主要是因为那个房间丈人在干活的时候是直接看得到的🤦‍♂️,后来被 LD 一顿教育,本来就是去帮忙的,又不是专业做这个的,急啥。
喝了水之后,又稍稍歇了一会,就开始继续撬了,本来觉得这个地板撬着好像还行,房间不大,没多久就撬完了,撬完之后喝了点饮料(补充点糖分,早餐吃得少,有点低血糖),然后看到 LD 在撬下面的木条了,这个动作开始了那天最大的经验值收集行动,前面说了这个木条一般是跟混凝土一块铺上去的,但是谁也没想到,这个混凝土铺上去的时候竟然处理的这么随意,根本没考虑跟下面的贴合,所以撬木条的时候直接把木条跟木条中间大块大块的混凝土一块撬起来了,想想那重量,于是我这靠蛮力干活的,就用力把木条带着混凝土一块撬了起来,还沾沾自喜,但是发现结果是撬起来一块之后,体力值瞬间归零,上一篇我也提到了,其实干这类活也是很有技巧性的,但是上次的是我没学会,可能需要花时间学的,但是这次是LD 用她的纤细胳膊教会我的,我在撬的时候,屏住一口气,双手用力,起,大概是吃好几口奶的力气都用出来了,但是 LD 在我休息的时候,慢慢悠悠的,先把撬棍挤到木条或者混凝土跟下层的缝里,然后往下垫一小块混凝土碎石,然后轻轻松松的扳两下,就撬开了,亏我高中的时候引以为傲的物理成绩,作为物理课代表,这么浅显易懂的杠杆原理都完全不会用到生活里,后面在用这个技巧撬的过程中,真的觉得自己蠢到家了,当然在明白了用点杠杆原理之后,撬地板的活就变得慢慢悠悠,悠哉悠哉的了(其实还是很热的,披着毛巾擦眼睛)。
上午的活差不多完了,后面就是把撬出来的混凝土和地板条丢下去,地上铺着不用了的被子,然后就是午饭和午休环节了,午饭换了一家快餐,味道非常可以,下午的活就比较单调了,帮忙清理了上去扔下来的混凝土碎块跟木条,然后稍微打扫了下,老丈人就让我们回家了,接着上次说的,还是觉得比跑步啥的消耗大太多了,那汗流的,一口就能喝完一瓶 500 毫升左右的矿泉水。

-]]>
- - 生活 - 运动 - 跑步 - 干活 - - - 生活 - 运动 - 减肥 - 跑步 - 干活 - -
diff --git a/sitemap.xml b/sitemap.xml index ce343baa0e..c4d0285f45 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -199,15 +199,6 @@ 0.6
- - https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ - - 2022-06-11 - - monthly - 0.6 - - https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ @@ -280,6 +271,15 @@ 0.6 + + https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ + + 2022-06-11 + + monthly + 0.6 + + https://nicksxs.me/2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/ @@ -362,7 +362,7 @@ - https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ 2022-06-11 @@ -371,7 +371,7 @@ - https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ 2022-06-11 @@ -389,7 +389,7 @@ - https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ + https://nicksxs.me/2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/ 2022-06-11 @@ -398,7 +398,7 @@ - https://nicksxs.me/2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/ + https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ 2022-06-11 @@ -407,7 +407,7 @@ - https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ + https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ 2022-06-11 @@ -443,7 +443,7 @@ - https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/ 2022-06-11 @@ -452,7 +452,7 @@ - https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/ + https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ 2022-06-11 @@ -470,7 +470,7 @@ - https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ + https://nicksxs.me/2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ 2022-06-11 @@ -479,7 +479,7 @@ - https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ 2022-06-11 @@ -488,7 +488,7 @@ - https://nicksxs.me/2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ 2022-06-11 @@ -1388,7 +1388,7 @@ - https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 @@ -1397,7 +1397,7 @@ - https://nicksxs.me/2015/03/13/Reverse-Integer/ + https://nicksxs.me/2015/03/11/Reverse-Bits/ 2020-01-12 @@ -1406,7 +1406,7 @@ - https://nicksxs.me/2015/03/11/Reverse-Bits/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 @@ -1433,7 +1433,7 @@ - https://nicksxs.me/2019/12/10/Redis-Part-1/ + https://nicksxs.me/2017/05/09/ambari-summary/ 2020-01-12 @@ -1442,7 +1442,7 @@ - https://nicksxs.me/2017/05/09/ambari-summary/ + https://nicksxs.me/2016/08/14/docker-mysql-cluster/ 2020-01-12 @@ -1451,7 +1451,7 @@ - https://nicksxs.me/2016/08/14/docker-mysql-cluster/ + https://nicksxs.me/2016/10/11/minimum-size-subarray-sum-209/ 2020-01-12 @@ -1460,7 +1460,7 @@ - https://nicksxs.me/2016/10/11/minimum-size-subarray-sum-209/ + https://nicksxs.me/2016/11/10/php-abstract-class-and-interface/ 2020-01-12 @@ -1469,7 +1469,7 @@ - https://nicksxs.me/2019/06/18/openresty/ + https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ 2020-01-12 @@ -1478,7 +1478,7 @@ - https://nicksxs.me/2016/11/10/php-abstract-class-and-interface/ + https://nicksxs.me/2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/ 2020-01-12 @@ -1487,7 +1487,7 @@ - https://nicksxs.me/2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/ + https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ 2020-01-12 @@ -1496,7 +1496,7 @@ - https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ 2020-01-12 @@ -1505,7 +1505,7 @@ - https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ + https://nicksxs.me/2019/06/18/openresty/ 2020-01-12 @@ -1514,7 +1514,7 @@ - https://nicksxs.me/baidu_verify_Gl8jtoDV4z.html + https://nicksxs.me/tags/index.html 2020-01-12 @@ -1523,7 +1523,7 @@ - https://nicksxs.me/tags/index.html + https://nicksxs.me/baidu_verify_Gl8jtoDV4z.html 2020-01-12 @@ -1559,7 +1559,7 @@ - https://nicksxs.me/2015/04/15/Leetcode-No-3/ + https://nicksxs.me/2015/03/11/Number-Of-1-Bits/ 2020-01-12 @@ -1568,7 +1568,7 @@ - https://nicksxs.me/2015/03/11/Number-Of-1-Bits/ + https://nicksxs.me/2015/01/04/Path-Sum/ 2020-01-12 @@ -1577,7 +1577,7 @@ - https://nicksxs.me/2015/01/04/Path-Sum/ + https://nicksxs.me/2015/06/22/invert-binary-tree/ 2020-01-12 @@ -1586,7 +1586,7 @@ - https://nicksxs.me/2015/06/22/invert-binary-tree/ + https://nicksxs.me/2014/12/23/my-new-post/ 2020-01-12 @@ -1595,7 +1595,7 @@ - https://nicksxs.me/2014/12/23/my-new-post/ + https://nicksxs.me/2015/04/15/Leetcode-No-3/ 2020-01-12 @@ -1622,7 +1622,7 @@ - https://nicksxs.me/2016/07/13/swoole-websocket-test/ + https://nicksxs.me/2016/10/12/summary-ranges-228/ 2020-01-12 @@ -1640,7 +1640,7 @@ - https://nicksxs.me/2016/10/12/summary-ranges-228/ + https://nicksxs.me/2016/07/13/swoole-websocket-test/ 2020-01-12 @@ -1687,7 +1687,7 @@ https://nicksxs.me/ - 2022-10-12 + 2022-10-13 daily 1.0 @@ -1695,1988 +1695,1988 @@ https://nicksxs.me/tags/%E7%94%9F%E6%B4%BB/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 2022-10-12 + https://nicksxs.me/tags/Java/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/2020/ - 2022-10-12 + https://nicksxs.me/tags/JVM/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/2021/ - 2022-10-12 + https://nicksxs.me/tags/C/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%8B%96%E6%9B%B4/ - 2022-10-12 + https://nicksxs.me/tags/leetcode/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Java/ - 2022-10-12 + https://nicksxs.me/tags/java/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/JVM/ - 2022-10-12 + https://nicksxs.me/tags/Binary-Tree/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/C/ - 2022-10-12 + https://nicksxs.me/tags/DFS/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/leetcode/ - 2022-10-12 + https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/java/ - 2022-10-12 + https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Binary-Tree/ - 2022-10-12 + https://nicksxs.me/tags/Linked-List/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/DFS/ - 2022-10-12 + https://nicksxs.me/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ - 2022-10-12 + https://nicksxs.me/tags/2020/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/ - 2022-10-12 + https://nicksxs.me/tags/2021/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Linked-List/ - 2022-10-12 + https://nicksxs.me/tags/%E6%8B%96%E6%9B%B4/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/2019/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/c/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B6%E5%8F%91/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/j-u-c/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/aqs/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/condition/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/await/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/signal/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/lock/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/unlock/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Apollo/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/environment/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/value/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ - 2022-10-12 - weekly - 0.2 - - - - https://nicksxs.me/tags/Disruptor/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Stream/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Comparator/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%8E%92%E5%BA%8F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/sort/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/nullsfirst/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Dubbo/ - 2022-10-12 + https://nicksxs.me/tags/Disruptor/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/RPC/ - 2022-10-12 + https://nicksxs.me/tags/Dubbo/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ - 2022-10-12 + https://nicksxs.me/tags/RPC/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/G1/ - 2022-10-12 + https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/GC/ - 2022-10-12 + https://nicksxs.me/tags/Filter/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Garbage-First-Collector/ - 2022-10-12 + https://nicksxs.me/tags/Interceptor/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Filter/ - 2022-10-12 + https://nicksxs.me/tags/AOP/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Interceptor/ - 2022-10-12 + https://nicksxs.me/tags/Spring/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/AOP/ - 2022-10-12 + https://nicksxs.me/tags/Tomcat/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Spring/ - 2022-10-12 + https://nicksxs.me/tags/Servlet/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Tomcat/ - 2022-10-12 + https://nicksxs.me/tags/Web/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Servlet/ - 2022-10-12 + https://nicksxs.me/tags/G1/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Web/ - 2022-10-12 + https://nicksxs.me/tags/GC/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Print-FooBar-Alternately/ - 2022-10-12 + https://nicksxs.me/tags/Garbage-First-Collector/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%80%92%E5%BD%92/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Preorder-Traversal/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Inorder-Traversal/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%89%8D%E5%BA%8F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%AD%E5%BA%8F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/DP/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/3Sum-Closest/ - 2022-10-12 + https://nicksxs.me/tags/Print-FooBar-Alternately/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Shift-2D-Grid/ - 2022-10-12 + https://nicksxs.me/tags/stack/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/stack/ - 2022-10-12 + https://nicksxs.me/tags/min-stack/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/min-stack/ - 2022-10-12 + https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ - 2022-10-12 + https://nicksxs.me/tags/leetcode-155/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/leetcode-155/ - 2022-10-12 + https://nicksxs.me/tags/Shift-2D-Grid/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ - 2022-10-12 + https://nicksxs.me/tags/3Sum-Closest/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/linked-list/ - 2022-10-12 + 2022-10-13 + weekly + 0.2 + + + + https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/First-Bad-Version/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/string/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Intersection-of-Two-Arrays/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/dp/ - 2022-10-12 + https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ - 2022-10-12 + https://nicksxs.me/tags/dp/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Trapping-Rain-Water/ - 2022-10-12 + https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ - 2022-10-12 + https://nicksxs.me/tags/Trapping-Rain-Water/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Leetcode-42/ - 2022-10-12 + https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ - 2022-10-12 + https://nicksxs.me/tags/Leetcode-42/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Rotate-Image/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/linux/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/grep/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%BD%AC%E4%B9%89/ - 2022-10-12 - weekly - 0.2 - - - - https://nicksxs.me/tags/mfc/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Maven/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/C/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Redis/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Distributed-Lock/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/hadoop/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/cluster/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/docker/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/mysql/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Docker/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/namespace/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Dockerfile/ - 2022-10-12 + https://nicksxs.me/tags/cgroup/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/cgroup/ - 2022-10-12 + https://nicksxs.me/tags/Dockerfile/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/echo/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/uname/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%8F%91%E8%A1%8C%E7%89%88/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Gogs/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Webhook/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Mysql/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Mybatis/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/nginx/ - 2022-10-12 + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ - 2022-10-12 + https://nicksxs.me/tags/nginx/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ - 2022-10-12 + https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/openresty/ - 2022-10-12 + https://nicksxs.me/tags/php/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/php/ - 2022-10-12 + https://nicksxs.me/tags/mfc/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/redis/ - 2022-10-12 + https://nicksxs.me/tags/mq/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ - 2022-10-12 + https://nicksxs.me/tags/im/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%BA%90%E7%A0%81/ - 2022-10-12 + https://nicksxs.me/tags/redis/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/mq/ - 2022-10-12 + https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/im/ - 2022-10-12 + https://nicksxs.me/tags/%E6%BA%90%E7%A0%81/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%BA%94%E7%94%A8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Evict/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/ - 2022-10-12 + https://nicksxs.me/tags/openresty/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Rust/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%89%80%E6%9C%89%E6%9D%83/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%96%B0%E8%AF%AD%E8%A8%80/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8/ - 2022-10-12 + 2022-10-13 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%88%87%E7%89%87/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Spring-Event/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/websocket/ - 2022-10-12 + https://nicksxs.me/tags/WordPress/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/swoole/ - 2022-10-12 + https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/spark/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/python/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/WordPress/ - 2022-10-12 + https://nicksxs.me/tags/gc/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-10-12 + https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/gc/ - 2022-10-12 + https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ - 2022-10-12 + https://nicksxs.me/tags/jvm/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ - 2022-10-12 + https://nicksxs.me/tags/websocket/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/jvm/ - 2022-10-12 + https://nicksxs.me/tags/swoole/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/ssh/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ - 2022-10-12 + https://nicksxs.me/tags/MQ/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ - 2022-10-12 + https://nicksxs.me/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/ - 2022-10-12 + https://nicksxs.me/tags/RocketMQ/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/ - 2022-10-12 + https://nicksxs.me/tags/%E5%89%8A%E5%B3%B0%E5%A1%AB%E8%B0%B7/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/ - 2022-10-12 + https://nicksxs.me/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/MQ/ - 2022-10-12 + https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ - 2022-10-12 + https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/RocketMQ/ - 2022-10-12 + https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%89%8A%E5%B3%B0%E5%A1%AB%E8%B0%B7/ - 2022-10-12 + https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/ - 2022-10-12 + https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ - 2022-10-12 + https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ - 2022-10-12 + https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ - 2022-10-12 + https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/git/ - 2022-10-12 + https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/scp/ - 2022-10-12 - weekly - 0.2 - - - - https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ - 2022-10-12 + https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ - 2022-10-12 + https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ - 2022-10-12 + https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ - 2022-10-12 + https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ - 2022-10-12 + https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ - 2022-10-12 + https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ - 2022-10-12 + https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ - 2022-10-12 + https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ - 2022-10-12 + https://nicksxs.me/tags/scp/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%BF%90%E5%8A%A8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%87%8F%E8%82%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%B7%91%E6%AD%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B2%E6%B4%BB/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/ - 2022-10-12 + https://nicksxs.me/tags/git/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%BD%B1%E8%AF%84/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%AF%84%E7%94%9F%E8%99%AB/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ - 2022-10-12 + https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/ - 2022-10-12 + https://nicksxs.me/tags/NameServer/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/utf8/ - 2022-10-12 + https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/utf8mb4/ - 2022-10-12 + https://nicksxs.me/tags/DefaultMQPushConsumer/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-0900-ai-ci/ - 2022-10-12 + https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-unicode-ci/ - 2022-10-12 + https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-general-ci/ - 2022-10-12 + https://nicksxs.me/tags/utf8/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/DefaultMQPushConsumer/ - 2022-10-12 + https://nicksxs.me/tags/utf8mb4/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/ - 2022-10-12 + https://nicksxs.me/tags/utf8mb4-0900-ai-ci/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/NameServer/ - 2022-10-12 + https://nicksxs.me/tags/utf8mb4-unicode-ci/ + 2022-10-13 + weekly + 0.2 + + + + https://nicksxs.me/tags/utf8mb4-general-ci/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/SpringBoot/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/cglib/ - 2022-10-12 + https://nicksxs.me/tags/Druid/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ - 2022-10-12 + https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Druid/ - 2022-10-12 + https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ - 2022-10-12 + https://nicksxs.me/tags/AutoConfiguration/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ - 2022-10-12 + https://nicksxs.me/tags/cglib/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/AutoConfiguration/ - 2022-10-12 + https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%BE%E9%87%8D/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%B0%84%E5%87%BB/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/SPI/ - 2022-10-12 + https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Adaptive/ - 2022-10-12 + https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ - 2022-10-12 + https://nicksxs.me/tags/SPI/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/ - 2022-10-12 + https://nicksxs.me/tags/Adaptive/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/ - 2022-10-12 + https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Synchronized/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%81%8F%E5%90%91%E9%94%81/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%87%AA%E6%97%8B/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%8A%A0%E8%BD%BD/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%AA%8C%E8%AF%81/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%87%86%E5%A4%87/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%A7%A3%E6%9E%90/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%88%9D%E5%A7%8B%E5%8C%96/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%93%BE%E6%8E%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/top/ - 2022-10-12 + https://nicksxs.me/tags/JPS/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/JPS/ - 2022-10-12 + https://nicksxs.me/tags/JStack/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/JStack/ - 2022-10-12 + https://nicksxs.me/tags/JMap/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/JMap/ - 2022-10-12 + https://nicksxs.me/tags/top/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Broker/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Sharding-Jdbc/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/ThreadPool/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/FixedThreadPool/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/LimitedThreadPool/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/EagerThreadPool/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/CachedThreadPool/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/mvcc/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/read-view/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/gap-lock/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/next-key-lock/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/is-null/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/is-not-null/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/procedure/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F/ - 2022-10-12 + https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF/ - 2022-10-12 + https://nicksxs.me/tags/Design-Patterns/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/ - 2022-10-12 + https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/ - 2022-10-12 + https://nicksxs.me/tags/Singleton/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/bloom-filter/ - 2022-10-12 + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%92%E6%96%A5%E9%94%81/ - 2022-10-12 + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/ThreadLocal/ - 2022-10-12 + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ - 2022-10-12 + https://nicksxs.me/tags/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ - 2022-10-12 + https://nicksxs.me/tags/bloom-filter/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/WeakReference/ - 2022-10-12 + https://nicksxs.me/tags/%E4%BA%92%E6%96%A5%E9%94%81/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ - 2022-10-12 + https://nicksxs.me/tags/Mac/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Design-Patterns/ - 2022-10-12 + https://nicksxs.me/tags/PHP/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/ - 2022-10-12 + https://nicksxs.me/tags/Homebrew/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Singleton/ - 2022-10-12 + https://nicksxs.me/tags/icu4c/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Mac/ - 2022-10-12 + https://nicksxs.me/tags/zsh/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/PHP/ - 2022-10-12 + https://nicksxs.me/tags/ThreadLocal/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/Homebrew/ - 2022-10-12 + https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/icu4c/ - 2022-10-12 + https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/zsh/ - 2022-10-12 + https://nicksxs.me/tags/WeakReference/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/Thread-dump/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/2PC/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/3PC/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/%E7%9C%8B%E5%89%A7/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ - 2022-10-12 + https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ - 2022-10-12 + https://nicksxs.me/tags/stream/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ - 2022-10-12 + https://nicksxs.me/tags/zookeeper/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ - 2022-10-12 + https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ - 2022-10-12 + https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/stream/ - 2022-10-12 + https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/zookeeper/ - 2022-10-12 + https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ - 2022-10-12 + https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ - 2022-10-12 + https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ - 2022-10-12 + https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ + 2022-10-13 weekly 0.2 https://nicksxs.me/tags/dubbo/ - 2022-10-12 + 2022-10-13 weekly 0.2 @@ -3685,1036 +3685,1036 @@ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/ - 2022-10-12 - weekly - 0.2 - - - - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/ - 2022-10-12 + https://nicksxs.me/categories/Java/JVM/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/JVM/ - 2022-10-12 + https://nicksxs.me/categories/leetcode/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/leetcode/ - 2022-10-12 + https://nicksxs.me/categories/Java/leetcode/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/GC/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Linked-List/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Binary-Tree/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ - 2022-10-12 + https://nicksxs.me/categories/C/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/C/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/leetcode/java/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ - 2022-10-12 + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/java/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E6%9D%91%E4%B8%8A%E6%98%A5%E6%A0%91/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Apollo/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ - 2022-10-12 + https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Dubbo/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/leetcode/java/Linked-List/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Filter/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/leetcode/java/Binary-Tree/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/DP/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/stack/ - 2022-10-12 + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/Lowest-Common-Ancestor-of-a-Binary-Tree/ - 2022-10-12 + https://nicksxs.me/categories/stack/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Apollo/value/ - 2022-10-12 + 2022-10-13 + weekly + 0.2 + + + + https://nicksxs.me/categories/Java/leetcode/Lowest-Common-Ancestor-of-a-Binary-Tree/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/linked-list/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/leetcode/Rotate-Image/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Linux/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Interceptor-AOP/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Maven/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ - 2022-10-12 + https://nicksxs.me/categories/Redis/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Redis/ - 2022-10-12 + https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/data-analysis/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/docker/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/leetcode/java/DP/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Docker/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/stack/ - 2022-10-12 + https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ - 2022-10-12 + https://nicksxs.me/categories/leetcode/java/stack/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Mybatis/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/nginx/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/linked-list/ - 2022-10-12 + https://nicksxs.me/categories/php/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/php/ - 2022-10-12 + https://nicksxs.me/categories/redis/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/redis/ - 2022-10-12 + https://nicksxs.me/categories/leetcode/java/linked-list/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/leetcode/java/string/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Spring/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Spring/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Redis/Distributed-Lock/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ - 2022-10-12 + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-10-12 + https://nicksxs.me/categories/Java/gc/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/gc/ - 2022-10-12 + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/ssh/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ - 2022-10-12 + https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ - 2022-10-12 + https://nicksxs.me/categories/ssh/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/MQ/ - 2022-10-12 + 2022-10-13 + weekly + 0.2 + + + + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/git/ - 2022-10-12 + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/shell/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ - 2022-10-12 + https://nicksxs.me/categories/git/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Mysql/ - 2022-10-12 + https://nicksxs.me/categories/Java/Mybatis/Mysql/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/Mybatis/Mysql/ - 2022-10-12 + https://nicksxs.me/categories/Mysql/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Mybatis/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/SpringBoot/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/Dubbo/RPC/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ - 2022-10-12 + https://nicksxs.me/categories/Dubbo-RPC/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Dubbo-RPC/ - 2022-10-12 + https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ - 2022-10-12 + https://nicksxs.me/categories/Thread-dump/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Thread-dump/ - 2022-10-12 + https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ - 2022-10-12 + https://nicksxs.me/categories/Java/Design-Patterns/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 2022-10-12 + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/Design-Patterns/ - 2022-10-12 + https://nicksxs.me/categories/SpringBoot/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Mac/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Spring/Servlet/ - 2022-10-12 - weekly - 0.2 - - - - https://nicksxs.me/categories/SpringBoot/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ - 2022-10-12 - weekly - 0.2 - - - - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Rust/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/C/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Java/gc/jvm/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ - 2022-10-12 + https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/ - 2022-10-12 + https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/MQ/RocketMQ/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-10-12 + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ - 2022-10-12 + https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ - 2022-10-12 + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ - 2022-10-12 + https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/ - 2022-10-12 + https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ - 2022-10-12 + https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Dubbo/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/ - 2022-10-12 + https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ - 2022-10-12 + https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/ - 2022-10-12 + https://nicksxs.me/categories/Java/Singleton/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/ - 2022-10-12 + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Java/Singleton/ - 2022-10-12 + https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Mac/PHP/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Spring/Servlet/Interceptor/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/C/Redis/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E5%8F%A3%E7%BD%A9/ - 2022-10-12 + https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ - 2022-10-12 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E5%8F%A3%E7%BD%A9/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E7%BE%8E%E5%9B%BD/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/%E5%B9%B2%E6%B4%BB/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/MQ/RocketMQ/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Spring/Mybatis/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Dubbo/SPI/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/ - 2022-10-12 + https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/ - 2022-10-12 + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 2022-10-12 + https://nicksxs.me/categories/C/Mysql/ + 2022-10-13 weekly 0.2 - https://nicksxs.me/categories/C/Mysql/ - 2022-10-12 + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/ + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Mac/Homebrew/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/RocketMQ/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ThreadPool/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/PHP/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/%E5%87%BB%E7%A9%BF/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/PHP/icu4c/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/RocketMQ/ - 2022-10-12 + 2022-10-13 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/%E5%87%BB%E7%A9%BF/%E9%9B%AA%E5%B4%A9/ - 2022-10-12 + 2022-10-13 + weekly + 0.2 + + + + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ + 2022-10-13 weekly 0.2 diff --git a/tags/DP/index.html b/tags/DP/index.html index 14f7ed3747..ddd2c740ed 100644 --- a/tags/DP/index.html +++ b/tags/DP/index.html @@ -1 +1 @@ -标签: dp | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
squs":"disqus"}} \ No newline at end of file +标签: dp | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/tags/JVM/index.html b/tags/JVM/index.html index fdf723caa5..b8adc80dd8 100644 --- a/tags/JVM/index.html +++ b/tags/JVM/index.html @@ -1 +1 @@ -标签: jvm | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
rity":true,"url":"https://nicksxs.me/tags/JVM/"} \ No newline at end of file +标签: jvm | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/tags/linked-list/index.html b/tags/linked-list/index.html index 45e3557b61..87171d4836 100644 --- a/tags/linked-list/index.html +++ b/tags/linked-list/index.html @@ -1 +1 @@ -标签: linked list | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

linked list 标签

2020
n-analytics.js"> \ No newline at end of file +标签: linked list | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

linked list 标签

2020
\ No newline at end of file diff --git a/tags/mysql/index.html b/tags/mysql/index.html index 11376dc1e8..efe326f10b 100644 --- a/tags/mysql/index.html +++ b/tags/mysql/index.html @@ -1 +1 @@ -标签: Mysql | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
.js" integrity="sha256-vXZMYLEqsROAXkEw93GGIvaB2ab+QW6w3+1ahD9nXXA=" crossorigin="anonymous"> \ No newline at end of file +标签: Mysql | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file diff --git a/tags/php/index.html b/tags/php/index.html index a4ba2083e0..da898a7394 100644 --- a/tags/php/index.html +++ b/tags/php/index.html @@ -1 +1 @@ -标签: PHP | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
,"home":false,"archive":false,"delay":true,"timeout":3000,"priority":true,"url":"https://nicksxs.me/tags/php/"} \ No newline at end of file +标签: PHP | Nicksxs's Blog

Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%
\ No newline at end of file