diff --git a/2019/06/18/openresty/index.html b/2019/06/18/openresty/index.html index 63813653ec..29a1f6ea8a 100644 --- a/2019/06/18/openresty/index.html +++ b/2019/06/18/openresty/index.html @@ -1,4 +1,4 @@ -openresty | Nicksxs's Blog

Nicksxs's Blog

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

0%

openresty

目前公司要对一些新的产品功能做灰度测试,因为在后端业务代码层面添加判断比较麻烦,所以想在nginx上做点手脚,就想到了openresty
前后也踩了不少坑,这边先写一点

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

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

http {
+openresty | Nicksxs's Blog

Nicksxs's Blog

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

0%

openresty

目前公司要对一些新的产品功能做灰度测试,因为在后端业务代码层面添加判断比较麻烦,所以想在nginx上做点手脚,就想到了openresty
前后也踩了不少坑,这边先写一点

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

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

http {
   lua_code_cache off;
 }
 
@@ -20,4 +20,4 @@ location ~*local t = json.decode(str)
 if t then
     ngx.say(" --> ", type(t))
-end

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

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

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

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

  • 发现一个不错的openresty站点
    地址

  • \ No newline at end of file +end

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

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

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

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

  • 发现一个不错的openresty站点
    地址

  • \ No newline at end of file diff --git a/2020/06/26/聊一下-RocketMQ-的-Consumer/index.html b/2020/06/26/聊一下-RocketMQ-的-Consumer/index.html index 7ba50281c8..bc1ceaefbc 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 a36f692966..ad73660cf5 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 750f7a67be..2ffc2e7d54 100644 --- a/baidusitemap.xml +++ b/baidusitemap.xml @@ -581,19 +581,19 @@ 2020-01-12 - https://nicksxs.me/2019/12/10/Redis-Part-1/ + 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/2015/03/11/Reverse-Bits/ + 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/01/14/Two-Sum/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 @@ -601,7 +601,7 @@ 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/2015/01/14/Two-Sum/ 2020-01-12 diff --git a/categories/docker/index.html b/categories/docker/index.html index aacbf631d4..74a010b104 100644 --- a/categories/docker/index.html +++ b/categories/docker/index.html @@ -1 +1 @@ -分类: docker | Nicksxs's Blog

    Nicksxs's Blog

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

    0%

    docker 分类

    2016
    \ No newline at end of file +分类: Docker | 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/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index e05e3a5eda..8693287271 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":"/2021/07/18/2021-年中总结/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"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":"JVM源码分析之G1垃圾收集器分析一","url":"/2019/12/07/JVM-G1-Part-1/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"title":"Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析","url":"/2021/10/07/Leetcode-021-合并两个有序链表-Merge-Two-Sorted-Lists-题解分析/"},{"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 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"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 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析","url":"/2022/07/22/Leetcode-1260-二维网格迁移-Shift-2D-Grid-Easy-题解分析/"},{"title":"Leetcode 155 最小栈(Min Stack) 题解分析","url":"/2020/12/06/Leetcode-155-最小栈-Min-Stack-题解分析/"},{"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 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"title":"Leetcode 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"title":"Leetcode 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"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 3 Longest Substring Without Repeating Characters 题解分析","url":"/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"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 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 42 接雨水 (Trapping Rain Water) 题解分析","url":"/2021/07/04/Leetcode-42-接雨水-Trapping-Rain-Water-题解分析/"},{"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 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析","url":"/2022/08/23/Leetcode-885-螺旋矩阵-III-Spiral-Matrix-III-Medium-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"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":"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":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"binary-watch","url":"/2016/09/29/binary-watch/"},{"title":"docker比一般多一点的初学者介绍","url":"/2020/03/08/docker比一般多一点的初学者介绍/"},{"title":"docker比一般多一点的初学者介绍三","url":"/2020/03/21/docker比一般多一点的初学者介绍三/"},{"title":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"docker-mysql-cluster","url":"/2016/08/14/docker-mysql-cluster/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"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":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"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":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"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":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"聊一下 RocketMQ 的消息存储四","url":"/2021/10/17/聊一下-RocketMQ-的消息存储四/"},{"title":"聊一下 RocketMQ 的顺序消息","url":"/2021/08/29/聊一下-RocketMQ-的顺序消息/"},{"title":"聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点","url":"/2021/09/19/聊一下-SpringBoot-中使用的-cglib-作为动态代理中的一个注意点/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊一下 SpringBoot 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"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 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-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":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"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":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"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":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"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":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"}] \ No newline at end of file +[{"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":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"Disruptor 系列二","url":"/2022/02/27/Disruptor-系列二/"},{"title":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"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 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"title":"Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"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":"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 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"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 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"title":"Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"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 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"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":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"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":"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":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"binary-watch","url":"/2016/09/29/binary-watch/"},{"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":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"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":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"title":"openresty","url":"/2019/06/18/openresty/"},{"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/20/redis数据结构介绍五/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储四","url":"/2021/10/17/聊一下-RocketMQ-的消息存储四/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点","url":"/2021/09/19/聊一下-SpringBoot-中使用的-cglib-作为动态代理中的一个注意点/"},{"title":"聊一下 RocketMQ 的顺序消息","url":"/2021/08/29/聊一下-RocketMQ-的顺序消息/"},{"title":"聊一下 SpringBoot 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"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 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"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 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"}] \ No newline at end of file diff --git a/search.xml b/search.xml index a3bd2e7da6..c7cc55b11e 100644 --- a/search.xml +++ b/search.xml @@ -93,257 +93,6 @@ 年中总结 - - 2021 年中总结 - /2021/07/18/2021-%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

    -

    这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。

    -

    技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。

    -

    另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。

    -

    还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。

    -

    下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。

    -]]>
    - - 生活 - 年中总结 - 2021 - - - 生活 - 2021 - 年中总结 - 技术 - 读书 - -
    - - AQS篇一 - /2021/02/14/AQS%E7%AF%87%E4%B8%80/ - 很多东西都是时看时新,而且时间长了也会忘,所以再来复习下,也会有一些新的角度看法这次来聊下AQS的内容,主要是这几个点,

    -

    第一个线程

    第一个线程抢到锁了,此时state跟阻塞队列是怎么样的,其实这里是之前没理解对的地方

    -
    /**
    -         * Fair version of tryAcquire.  Don't grant access unless
    -         * recursive call or no waiters or is first.
    -         */
    -        protected final boolean tryAcquire(int acquires) {
    -            final Thread current = Thread.currentThread();
    -            int c = getState();
    -            // 这里如果state还是0说明锁还空着
    -            if (c == 0) {
    -                // 因为是公平锁版本的,先去看下是否阻塞队列里有排着队的
    -                if (!hasQueuedPredecessors() &&
    -                    compareAndSetState(0, acquires)) {
    -                    // 没有排队的,并且state使用cas设置成功的就标记当前占有锁的线程是我
    -                    setExclusiveOwnerThread(current);
    -                    // 然后其实就返回了,包括阻塞队列的head和tail节点和waitStatus都没有设置
    -                    return true;
    -                }
    -            }
    -            else if (current == getExclusiveOwnerThread()) {
    -                int nextc = c + acquires;
    -                if (nextc < 0)
    -                    throw new Error("Maximum lock count exceeded");
    -                setState(nextc);
    -                return true;
    -            }
    -            // 这里就是第二个线程会返回false
    -            return false;
    -        }
    -    }
    - -

    第二个线程

    当第二个线程进来的时候应该是怎么样,结合代码来看

    -
    /**
    -     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    -     * by invoking at least once {@link #tryAcquire},
    -     * returning on success.  Otherwise the thread is queued, possibly
    -     * repeatedly blocking and unblocking, invoking {@link
    -     * #tryAcquire} until success.  This method can be used
    -     * to implement method {@link Lock#lock}.
    -     *
    -     * @param arg the acquire argument.  This value is conveyed to
    -     *        {@link #tryAcquire} but is otherwise uninterpreted and
    -     *        can represent anything you like.
    -     */
    -    public final void acquire(int arg) {
    -        // 前面第一种情况是tryAcquire直接成功了,这个if判断第一个条件就是false,就不往下执行了
    -        // 如果是第二个线程,第一个条件获取锁不成功,条件判断!tryAcquire(arg) == true,就会走
    -        // acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    -        if (!tryAcquire(arg) &&
    -            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    -            selfInterrupt();
    -    }
    - -

    然后来看下addWaiter的逻辑

    -
    /**
    -     * Creates and enqueues node for current thread and given mode.
    -     *
    -     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
    -     * @return the new node
    -     */
    -    private Node addWaiter(Node mode) {
    -        // 这里是包装成一个node
    -        Node node = new Node(Thread.currentThread(), mode);
    -        // Try the fast path of enq; backup to full enq on failure
    -        // 最快的方式就是把当前线程的节点放在阻塞队列的最后
    -        Node pred = tail;
    -        // 只有当tail,也就是pred不为空的时候可以直接接上
    -        if (pred != null) {
    -            node.prev = pred;
    -            // 如果这里cas成功了,就直接接上返回了
    -            if (compareAndSetTail(pred, node)) {
    -                pred.next = node;
    -                return node;
    -            }
    -        }
    -        // 不然就会继续走到这里
    -        enq(node);
    -        return node;
    -    }
    - -

    然后就是enq的逻辑了

    -
    /**
    -     * Inserts node into queue, initializing if necessary. See picture above.
    -     * @param node the node to insert
    -     * @return node's predecessor
    -     */
    -    private Node enq(final Node node) {
    -        for (;;) {
    -            // 如果状态没变化的话,tail这时还是null的
    -            Node t = tail;
    -            if (t == null) { // Must initialize
    -                // 这里就会初始化头结点,就是个空节点
    -                if (compareAndSetHead(new Node()))
    -                    // tail也赋值成head
    -                    tail = head;
    -            } else {
    -                // 这里就设置tail了
    -                node.prev = t;
    -                if (compareAndSetTail(t, node)) {
    -                    t.next = node;
    -                    return t;
    -                }
    -            }
    -        }
    -    }
    - -

    所以从这里可以看出来,其实head头结点不是个真实的带有线程的节点,并且不是在第一个线程进来的时候设置的

    -

    解锁

    通过代码来看下

    -
    /**
    -     * Attempts to release this lock.
    -     *
    -     * <p>If the current thread is the holder of this lock then the hold
    -     * count is decremented.  If the hold count is now zero then the lock
    -     * is released.  If the current thread is not the holder of this
    -     * lock then {@link IllegalMonitorStateException} is thrown.
    -     *
    -     * @throws IllegalMonitorStateException if the current thread does not
    -     *         hold this lock
    -     */
    -    public void unlock() {
    -        // 释放锁
    -        sync.release(1);
    -    }
    -/**
    -     * Releases in exclusive mode.  Implemented by unblocking one or
    -     * more threads if {@link #tryRelease} returns true.
    -     * This method can be used to implement method {@link Lock#unlock}.
    -     *
    -     * @param arg the release argument.  This value is conveyed to
    -     *        {@link #tryRelease} but is otherwise uninterpreted and
    -     *        can represent anything you like.
    -     * @return the value returned from {@link #tryRelease}
    -     */
    -    public final boolean release(int arg) {
    -        // 尝试去释放
    -        if (tryRelease(arg)) {
    -            Node h = head;
    -            if (h != null && h.waitStatus != 0)
    -                unparkSuccessor(h);
    -            return true;
    -        }
    -        return false;
    -    }
    -protected final boolean tryRelease(int releases) {
    -            int c = getState() - releases;
    -            if (Thread.currentThread() != getExclusiveOwnerThread())
    -                throw new IllegalMonitorStateException();
    -            boolean free = false;
    -    		// 判断是否完全释放锁,因为可重入
    -            if (c == 0) {
    -                free = true;
    -                setExclusiveOwnerThread(null);
    -            }
    -            setState(c);
    -            return free;
    -        }
    -// 这段代码和上面的一致,只是为了顺序性,又拷下来看下
    -
    -public final boolean release(int arg) {
    -        // 尝试去释放,如果是完全释放,返回的就是true,否则是false
    -        if (tryRelease(arg)) {
    -            Node h = head;
    -            // 这里判断头结点是否为空以及waitStatus的状态,前面说了head节点其实是
    -            // 在第二个线程进来的时候初始化的,如果是空的话说明没后续节点,并且waitStatus
    -            // 也表示了后续的等待状态
    -            if (h != null && h.waitStatus != 0)
    -                unparkSuccessor(h);
    -            return true;
    -        }
    -        return false;
    -    }
    -
    -/**
    -     * Wakes up node's successor, if one exists.
    -     *
    -     * @param node the node
    -     */
    -// 唤醒后继节点
    -    private void unparkSuccessor(Node node) {
    -        /*
    -         * If status is negative (i.e., possibly needing signal) try
    -         * to clear in anticipation of signalling.  It is OK if this
    -         * fails or if status is changed by waiting thread.
    -         */
    -        int ws = node.waitStatus;
    -        if (ws < 0)
    -            compareAndSetWaitStatus(node, ws, 0);
    -
    -        /*
    -         * Thread to unpark is held in successor, which is normally
    -         * just the next node.  But if cancelled or apparently null,
    -         * traverse backwards from tail to find the actual
    -         * non-cancelled successor.
    -         */
    -        Node s = node.next;
    -        // 如果后继节点是空或者当前节点取消等待了
    -        if (s == null || s.waitStatus > 0) {
    -            s = null;
    -            // 从后往前找,找到非取消的节点,注意这里不是找到就退出,而是一直找到头
    -            // 所以不必担心中间有取消的
    -            for (Node t = tail; t != null && t != node; t = t.prev)
    -                if (t.waitStatus <= 0)
    -                    s = t;
    -        }
    -        if (s != null)
    -            // 将其唤醒
    -            LockSupport.unpark(s.thread);
    -    }
    - - - - -]]>
    - - Java - 并发 - - - java - 并发 - j.u.c - aqs - -
    2021 年终总结 /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ @@ -362,11 +111,57 @@ - AQS篇二 之 Condition 浅析笔记 - /2021/02/21/AQS-%E4%B9%8B-Condition-%E6%B5%85%E6%9E%90%E7%AC%94%E8%AE%B0/ - Condition也是 AQS 中很重要的一块内容,可以先看段示例代码,这段代码应该来自于Doug Lea大大,可以在 javadoc 中的 condition 部分找到,其实大大原来写过基于 synchronized 实现的,后面我也贴下代码

    -
    import java.util.concurrent.locks.Condition;
    -import java.util.concurrent.locks.Lock;
    +    34_Search_for_a_Range
    +    /2016/08/14/34-Search-for-a-Range/
    +    question

    34. Search for a Range

    Original Page

    +

    Given a sorted array of integers, find the starting and ending position of a given target value.

    +

    Your algorithm’s runtime complexity must be in the order of O(log n).

    +

    If the target is not found in the array, return [-1, -1].

    +

    For example,
    Given [5, 7, 7, 8, 8, 10] and target value 8,
    return [3, 4].

    +

    analysis

    一开始就想到了二分查找,但是原来做二分查找的时候一般都是找到确定的那个数就完成了,
    这里的情况比较特殊,需要找到整个区间,所以需要两遍查找,并且一个是找到小于target
    的最大索引,一个是找到大于target的最大索引,代码参考leetcode discuss,这位仁
    兄也做了详细的分析解释。

    +

    code

    class Solution {
    +public:
    +    vector<int> searchRange(vector<int>& nums, int target) {
    +        vector<int> ret(2, -1);
    +        int i = 0, j = nums.size() - 1;
    +        int mid;
    +        while(i < j){
    +            mid = (i + j) / 2;
    +            if(nums[mid] < target) i = mid + 1;
    +            else j = mid;
    +        }
    +        if(nums[i] != target) return ret;
    +        else {
    +            ret[0] = i;
    +            if((i+1) < (nums.size() - 1) && nums[i+1] > target){
    +                ret[1] = i;
    +                return ret;
    +            }
    +        }   //一点小优化
    +        j = nums.size() - 1;
    +        while(i < j){
    +            mid = (i + j) / 2 + 1;
    +            if(nums[mid] > target) j = mid - 1;
    +            else i = mid;
    +        }
    +        ret[1] = j;
    +        return ret;
    +    }
    +};
    ]]>
    + + leetcode + + + leetcode + c++ + + + + AQS篇二 之 Condition 浅析笔记 + /2021/02/21/AQS-%E4%B9%8B-Condition-%E6%B5%85%E6%9E%90%E7%AC%94%E8%AE%B0/ + Condition也是 AQS 中很重要的一块内容,可以先看段示例代码,这段代码应该来自于Doug Lea大大,可以在 javadoc 中的 condition 部分找到,其实大大原来写过基于 synchronized 实现的,后面我也贴下代码

    +
    import java.util.concurrent.locks.Condition;
    +import java.util.concurrent.locks.Lock;
     import java.util.concurrent.locks.ReentrantLock;
     
     class BoundedBuffer {
    @@ -859,112 +654,294 @@
           
       
       
    -    34_Search_for_a_Range
    -    /2016/08/14/34-Search-for-a-Range/
    -    question

    34. Search for a Range

    Original Page

    -

    Given a sorted array of integers, find the starting and ending position of a given target value.

    -

    Your algorithm’s runtime complexity must be in the order of O(log n).

    -

    If the target is not found in the array, return [-1, -1].

    -

    For example,
    Given [5, 7, 7, 8, 8, 10] and target value 8,
    return [3, 4].

    -

    analysis

    一开始就想到了二分查找,但是原来做二分查找的时候一般都是找到确定的那个数就完成了,
    这里的情况比较特殊,需要找到整个区间,所以需要两遍查找,并且一个是找到小于target
    的最大索引,一个是找到大于target的最大索引,代码参考leetcode discuss,这位仁
    兄也做了详细的分析解释。

    -

    code

    class Solution {
    -public:
    -    vector<int> searchRange(vector<int>& nums, int target) {
    -        vector<int> ret(2, -1);
    -        int i = 0, j = nums.size() - 1;
    -        int mid;
    -        while(i < j){
    -            mid = (i + j) / 2;
    -            if(nums[mid] < target) i = mid + 1;
    -            else j = mid;
    -        }
    -        if(nums[i] != target) return ret;
    -        else {
    -            ret[0] = i;
    -            if((i+1) < (nums.size() - 1) && nums[i+1] > target){
    -                ret[1] = i;
    -                return ret;
    -            }
    -        }   //一点小优化
    -        j = nums.size() - 1;
    -        while(i < j){
    -            mid = (i + j) / 2 + 1;
    -            if(nums[mid] > target) j = mid - 1;
    -            else i = mid;
    -        }
    -        ret[1] = j;
    -        return ret;
    -    }
    -};
    ]]>
    - - leetcode - - - leetcode - c++ - -
    - - AbstractQueuedSynchronizer - /2019/09/23/AbstractQueuedSynchronizer/ - 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
    首先 AQS 只要有这些属性

    -
    // 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
    -private transient volatile Node head;
    -
    -// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
    -private transient volatile Node tail;
    -
    -// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
    -// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
    -private volatile int state;
    -
    -// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
    -// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
    -// if (currentThread == getExclusiveOwnerThread()) {state++}
    -private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
    -

    大概了解了 aqs 底层的双向等待队列,
    结构是这样的

    每个 node 里面主要是的代码结构也比较简单

    -
    static final class Node {
    -    // 标识节点当前在共享模式下
    -    static final Node SHARED = new Node();
    -    // 标识节点当前在独占模式下
    -    static final Node EXCLUSIVE = null;
    +    AQS篇一
    +    /2021/02/14/AQS%E7%AF%87%E4%B8%80/
    +    很多东西都是时看时新,而且时间长了也会忘,所以再来复习下,也会有一些新的角度看法这次来聊下AQS的内容,主要是这几个点,

    +

    第一个线程

    第一个线程抢到锁了,此时state跟阻塞队列是怎么样的,其实这里是之前没理解对的地方

    +
    /**
    +         * Fair version of tryAcquire.  Don't grant access unless
    +         * recursive call or no waiters or is first.
    +         */
    +        protected final boolean tryAcquire(int acquires) {
    +            final Thread current = Thread.currentThread();
    +            int c = getState();
    +            // 这里如果state还是0说明锁还空着
    +            if (c == 0) {
    +                // 因为是公平锁版本的,先去看下是否阻塞队列里有排着队的
    +                if (!hasQueuedPredecessors() &&
    +                    compareAndSetState(0, acquires)) {
    +                    // 没有排队的,并且state使用cas设置成功的就标记当前占有锁的线程是我
    +                    setExclusiveOwnerThread(current);
    +                    // 然后其实就返回了,包括阻塞队列的head和tail节点和waitStatus都没有设置
    +                    return true;
    +                }
    +            }
    +            else if (current == getExclusiveOwnerThread()) {
    +                int nextc = c + acquires;
    +                if (nextc < 0)
    +                    throw new Error("Maximum lock count exceeded");
    +                setState(nextc);
    +                return true;
    +            }
    +            // 这里就是第二个线程会返回false
    +            return false;
    +        }
    +    }
    - // ======== 下面的几个int常量是给waitStatus用的 =========== - /** waitStatus value to indicate thread has cancelled */ - // 代码此线程取消了争抢这个锁 - static final int CANCELLED = 1; - /** waitStatus value to indicate successor's thread needs unparking */ - // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒 - static final int SIGNAL = -1; - /** waitStatus value to indicate thread is waiting on condition */ - // 本文不分析condition,所以略过吧,下一篇文章会介绍这个 - static final int CONDITION = -2; - /** - * waitStatus value to indicate the next acquireShared should - * unconditionally propagate +

    第二个线程

    当第二个线程进来的时候应该是怎么样,结合代码来看

    +
    /**
    +     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    +     * by invoking at least once {@link #tryAcquire},
    +     * returning on success.  Otherwise the thread is queued, possibly
    +     * repeatedly blocking and unblocking, invoking {@link
    +     * #tryAcquire} until success.  This method can be used
    +     * to implement method {@link Lock#lock}.
    +     *
    +     * @param arg the acquire argument.  This value is conveyed to
    +     *        {@link #tryAcquire} but is otherwise uninterpreted and
    +     *        can represent anything you like.
          */
    -    // 同样的不分析,略过吧
    -    static final int PROPAGATE = -3;
    -    // =====================================================
    +    public final void acquire(int arg) {
    +        // 前面第一种情况是tryAcquire直接成功了,这个if判断第一个条件就是false,就不往下执行了
    +        // 如果是第二个线程,第一个条件获取锁不成功,条件判断!tryAcquire(arg) == true,就会走
    +        // acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    +        if (!tryAcquire(arg) &&
    +            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    +            selfInterrupt();
    +    }
    +

    然后来看下addWaiter的逻辑

    +
    /**
    +     * Creates and enqueues node for current thread and given mode.
    +     *
    +     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
    +     * @return the new node
    +     */
    +    private Node addWaiter(Node mode) {
    +        // 这里是包装成一个node
    +        Node node = new Node(Thread.currentThread(), mode);
    +        // Try the fast path of enq; backup to full enq on failure
    +        // 最快的方式就是把当前线程的节点放在阻塞队列的最后
    +        Node pred = tail;
    +        // 只有当tail,也就是pred不为空的时候可以直接接上
    +        if (pred != null) {
    +            node.prev = pred;
    +            // 如果这里cas成功了,就直接接上返回了
    +            if (compareAndSetTail(pred, node)) {
    +                pred.next = node;
    +                return node;
    +            }
    +        }
    +        // 不然就会继续走到这里
    +        enq(node);
    +        return node;
    +    }
    - // 取值为上面的1、-1、-2、-3,或者0(以后会讲到) - // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待, - // ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。 - volatile int waitStatus; - // 前驱节点的引用 - volatile Node prev; - // 后继节点的引用 - volatile Node next; - // 这个就是线程本尊 - volatile Thread thread; +

    然后就是enq的逻辑了

    +
    /**
    +     * Inserts node into queue, initializing if necessary. See picture above.
    +     * @param node the node to insert
    +     * @return node's predecessor
    +     */
    +    private Node enq(final Node node) {
    +        for (;;) {
    +            // 如果状态没变化的话,tail这时还是null的
    +            Node t = tail;
    +            if (t == null) { // Must initialize
    +                // 这里就会初始化头结点,就是个空节点
    +                if (compareAndSetHead(new Node()))
    +                    // tail也赋值成head
    +                    tail = head;
    +            } else {
    +                // 这里就设置tail了
    +                node.prev = t;
    +                if (compareAndSetTail(t, node)) {
    +                    t.next = node;
    +                    return t;
    +                }
    +            }
    +        }
    +    }
    -}
    -

    其实可以主要关注这个 waitStatus 因为这个是后面的节点给前面的节点设置的,等于-1 的时候代表后面有节点等待,需要去唤醒,
    这里使用了一个变种的 CLH 队列实现,CLH 队列相关内容可以查看这篇 自旋锁、排队自旋锁、MCS锁、CLH锁

    -]]>
    - - java - +

    所以从这里可以看出来,其实head头结点不是个真实的带有线程的节点,并且不是在第一个线程进来的时候设置的

    +

    解锁

    通过代码来看下

    +
    /**
    +     * Attempts to release this lock.
    +     *
    +     * <p>If the current thread is the holder of this lock then the hold
    +     * count is decremented.  If the hold count is now zero then the lock
    +     * is released.  If the current thread is not the holder of this
    +     * lock then {@link IllegalMonitorStateException} is thrown.
    +     *
    +     * @throws IllegalMonitorStateException if the current thread does not
    +     *         hold this lock
    +     */
    +    public void unlock() {
    +        // 释放锁
    +        sync.release(1);
    +    }
    +/**
    +     * Releases in exclusive mode.  Implemented by unblocking one or
    +     * more threads if {@link #tryRelease} returns true.
    +     * This method can be used to implement method {@link Lock#unlock}.
    +     *
    +     * @param arg the release argument.  This value is conveyed to
    +     *        {@link #tryRelease} but is otherwise uninterpreted and
    +     *        can represent anything you like.
    +     * @return the value returned from {@link #tryRelease}
    +     */
    +    public final boolean release(int arg) {
    +        // 尝试去释放
    +        if (tryRelease(arg)) {
    +            Node h = head;
    +            if (h != null && h.waitStatus != 0)
    +                unparkSuccessor(h);
    +            return true;
    +        }
    +        return false;
    +    }
    +protected final boolean tryRelease(int releases) {
    +            int c = getState() - releases;
    +            if (Thread.currentThread() != getExclusiveOwnerThread())
    +                throw new IllegalMonitorStateException();
    +            boolean free = false;
    +    		// 判断是否完全释放锁,因为可重入
    +            if (c == 0) {
    +                free = true;
    +                setExclusiveOwnerThread(null);
    +            }
    +            setState(c);
    +            return free;
    +        }
    +// 这段代码和上面的一致,只是为了顺序性,又拷下来看下
    +
    +public final boolean release(int arg) {
    +        // 尝试去释放,如果是完全释放,返回的就是true,否则是false
    +        if (tryRelease(arg)) {
    +            Node h = head;
    +            // 这里判断头结点是否为空以及waitStatus的状态,前面说了head节点其实是
    +            // 在第二个线程进来的时候初始化的,如果是空的话说明没后续节点,并且waitStatus
    +            // 也表示了后续的等待状态
    +            if (h != null && h.waitStatus != 0)
    +                unparkSuccessor(h);
    +            return true;
    +        }
    +        return false;
    +    }
    +
    +/**
    +     * Wakes up node's successor, if one exists.
    +     *
    +     * @param node the node
    +     */
    +// 唤醒后继节点
    +    private void unparkSuccessor(Node node) {
    +        /*
    +         * If status is negative (i.e., possibly needing signal) try
    +         * to clear in anticipation of signalling.  It is OK if this
    +         * fails or if status is changed by waiting thread.
    +         */
    +        int ws = node.waitStatus;
    +        if (ws < 0)
    +            compareAndSetWaitStatus(node, ws, 0);
    +
    +        /*
    +         * Thread to unpark is held in successor, which is normally
    +         * just the next node.  But if cancelled or apparently null,
    +         * traverse backwards from tail to find the actual
    +         * non-cancelled successor.
    +         */
    +        Node s = node.next;
    +        // 如果后继节点是空或者当前节点取消等待了
    +        if (s == null || s.waitStatus > 0) {
    +            s = null;
    +            // 从后往前找,找到非取消的节点,注意这里不是找到就退出,而是一直找到头
    +            // 所以不必担心中间有取消的
    +            for (Node t = tail; t != null && t != node; t = t.prev)
    +                if (t.waitStatus <= 0)
    +                    s = t;
    +        }
    +        if (s != null)
    +            // 将其唤醒
    +            LockSupport.unpark(s.thread);
    +    }
    + + + + +]]> + + Java + 并发 + + + java + 并发 + j.u.c + aqs + +
    + + AbstractQueuedSynchronizer + /2019/09/23/AbstractQueuedSynchronizer/ + 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
    首先 AQS 只要有这些属性

    +
    // 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
    +private transient volatile Node head;
    +
    +// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
    +private transient volatile Node tail;
    +
    +// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
    +// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
    +private volatile int state;
    +
    +// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
    +// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
    +// if (currentThread == getExclusiveOwnerThread()) {state++}
    +private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
    +

    大概了解了 aqs 底层的双向等待队列,
    结构是这样的

    每个 node 里面主要是的代码结构也比较简单

    +
    static final class Node {
    +    // 标识节点当前在共享模式下
    +    static final Node SHARED = new Node();
    +    // 标识节点当前在独占模式下
    +    static final Node EXCLUSIVE = null;
    +
    +    // ======== 下面的几个int常量是给waitStatus用的 ===========
    +    /** waitStatus value to indicate thread has cancelled */
    +    // 代码此线程取消了争抢这个锁
    +    static final int CANCELLED =  1;
    +    /** waitStatus value to indicate successor's thread needs unparking */
    +    // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
    +    static final int SIGNAL    = -1;
    +    /** waitStatus value to indicate thread is waiting on condition */
    +    // 本文不分析condition,所以略过吧,下一篇文章会介绍这个
    +    static final int CONDITION = -2;
    +    /**
    +     * waitStatus value to indicate the next acquireShared should
    +     * unconditionally propagate
    +     */
    +    // 同样的不分析,略过吧
    +    static final int PROPAGATE = -3;
    +    // =====================================================
    +
    +
    +    // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
    +    // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
    +    //    ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
    +    volatile int waitStatus;
    +    // 前驱节点的引用
    +    volatile Node prev;
    +    // 后继节点的引用
    +    volatile Node next;
    +    // 这个就是线程本尊
    +    volatile Thread thread;
    +
    +}
    +

    其实可以主要关注这个 waitStatus 因为这个是后面的节点给前面的节点设置的,等于-1 的时候代表后面有节点等待,需要去唤醒,
    这里使用了一个变种的 CLH 队列实现,CLH 队列相关内容可以查看这篇 自旋锁、排队自旋锁、MCS锁、CLH锁

    +]]>
    + + java + java aqs @@ -1160,6 +1137,59 @@ public: environment
    + + Clone Graph Part I + /2014/12/30/Clone-Graph-Part-I/ + problem
    Clone a graph. Input is a Node pointer. Return the Node pointer of the cloned graph.
    +
    +A graph is defined below:
    +struct Node {
    +vector neighbors;
    +}
    + + +

    code

    typedef unordered_map<Node *, Node *> Map;
    + 
    +Node *clone(Node *graph) {
    +    if (!graph) return NULL;
    + 
    +    Map map;
    +    queue<Node *> q;
    +    q.push(graph);
    + 
    +    Node *graphCopy = new Node();
    +    map[graph] = graphCopy;
    + 
    +    while (!q.empty()) {
    +        Node *node = q.front();
    +        q.pop();
    +        int n = node->neighbors.size();
    +        for (int i = 0; i < n; i++) {
    +            Node *neighbor = node->neighbors[i];
    +            // no copy exists
    +            if (map.find(neighbor) == map.end()) {
    +                Node *p = new Node();
    +                map[node]->neighbors.push_back(p);
    +                map[neighbor] = p;
    +                q.push(neighbor);
    +            } else {     // a copy already exists
    +                map[node]->neighbors.push_back(map[neighbor]);
    +            }
    +        }
    +    }
    + 
    +    return graphCopy;
    +}
    +

    anlysis

    using the Breadth-first traversal
    and use a map to save the neighbors not to be duplicated.

    +]]>
    + + leetcode + + + C++ + leetcode + +
    Comparator使用小记 /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/ @@ -1262,68 +1292,15 @@ public: - Clone Graph Part I - /2014/12/30/Clone-Graph-Part-I/ - problem
    Clone a graph. Input is a Node pointer. Return the Node pointer of the cloned 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;
     
    -A graph is defined below:
    -struct Node {
    -vector neighbors;
    -}
    - - -

    code

    typedef unordered_map<Node *, Node *> Map;
    - 
    -Node *clone(Node *graph) {
    -    if (!graph) return NULL;
    - 
    -    Map map;
    -    queue<Node *> q;
    -    q.push(graph);
    - 
    -    Node *graphCopy = new Node();
    -    map[graph] = graphCopy;
    - 
    -    while (!q.empty()) {
    -        Node *node = q.front();
    -        q.pop();
    -        int n = node->neighbors.size();
    -        for (int i = 0; i < n; i++) {
    -            Node *neighbor = node->neighbors[i];
    -            // no copy exists
    -            if (map.find(neighbor) == map.end()) {
    -                Node *p = new Node();
    -                map[node]->neighbors.push_back(p);
    -                map[neighbor] = p;
    -                q.push(neighbor);
    -            } else {     // a copy already exists
    -                map[node]->neighbors.push_back(map[neighbor]);
    -            }
    -        }
    -    }
    - 
    -    return graphCopy;
    -}
    -

    anlysis

    using the Breadth-first traversal
    and use a map to save the neighbors not to be duplicated.

    -]]>
    - - leetcode - - - C++ - leetcode - - - - 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 void set(long value) {
    +        this.value = value;
    +    }
     
         public long getValue() {
             return value;
    @@ -1442,28 +1419,6 @@ Node *clone(Node *graph) {
             Disruptor
           
       
    -  
    -    Dubbo 使用的几个记忆点
    -    /2022/04/02/Dubbo-%E4%BD%BF%E7%94%A8%E7%9A%84%E5%87%A0%E4%B8%AA%E8%AE%B0%E5%BF%86%E7%82%B9/
    -    因为后台使用的 dubbo 作为 rpc 框架,并且会有一些日常使用情景有一些小的技巧,在这里做下记录作笔记用

    -

    dubbo 只拉取不注册

    <dubbo:registry address="zookeeper://127.0.0.1:2181" register="false" />
    -

    就是只要 register="false" 就可以了,这样比如我们在开发环境想运行服务,但又不想让开发环境正常的请求调用到本地来,当然这不是唯一的方式,通过 dubbo 2.7 以上的 tag 路由也可以实现或者自行改造拉取和注册服务的逻辑,因为注册到注册中心的其实是一串带参数的 url,还是比较方便改造的。相反的就是只注册,不拉取

    -

    dubbo 只注册不拉取

    <dubbo:registry address="zookeeper://127.0.0.1:2181" subscribe="false" />
    -

    这个使用场景就是如果我这个服务只作为 provider,没有任何调用其他的服务,其实就可以这么设置

    -

    权重配置

    <dubbo:provider loadbalance="random" weight="50"/>
    -

    首先这是在使用了随机的负载均衡策略的时候可以进行配置,并且是对于多个 provider 的情况下,这样其实也可以部分解决上面的只拉取不注册的问题,我把自己的权重调成 0 或者很低

    -]]>
    - - Java - Dubbo - - - Java - Dubbo - RPC - 负载均衡 - -
    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/ @@ -1931,6 +1886,111 @@ Node *clone(Node *graph) { Web + + Dubbo 使用的几个记忆点 + /2022/04/02/Dubbo-%E4%BD%BF%E7%94%A8%E7%9A%84%E5%87%A0%E4%B8%AA%E8%AE%B0%E5%BF%86%E7%82%B9/ + 因为后台使用的 dubbo 作为 rpc 框架,并且会有一些日常使用情景有一些小的技巧,在这里做下记录作笔记用

    +

    dubbo 只拉取不注册

    <dubbo:registry address="zookeeper://127.0.0.1:2181" register="false" />
    +

    就是只要 register="false" 就可以了,这样比如我们在开发环境想运行服务,但又不想让开发环境正常的请求调用到本地来,当然这不是唯一的方式,通过 dubbo 2.7 以上的 tag 路由也可以实现或者自行改造拉取和注册服务的逻辑,因为注册到注册中心的其实是一串带参数的 url,还是比较方便改造的。相反的就是只注册,不拉取

    +

    dubbo 只注册不拉取

    <dubbo:registry address="zookeeper://127.0.0.1:2181" subscribe="false" />
    +

    这个使用场景就是如果我这个服务只作为 provider,没有任何调用其他的服务,其实就可以这么设置

    +

    权重配置

    <dubbo:provider loadbalance="random" weight="50"/>
    +

    首先这是在使用了随机的负载均衡策略的时候可以进行配置,并且是对于多个 provider 的情况下,这样其实也可以部分解决上面的只拉取不注册的问题,我把自己的权重调成 0 或者很低

    +]]>
    + + Java + Dubbo + + + Java + Dubbo + RPC + 负载均衡 + +
    + + 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,
    +
    +   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 + +
    JVM源码分析之G1垃圾收集器分析一 /2019/12/07/JVM-G1-Part-1/ @@ -2537,164 +2597,6 @@ Node *clone(Node *graph) { C++ - - 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,
    -
    -   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 - -
    - - 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
    - -

    题解

    字符串比较其实是写代码里永恒的主题,底层的编译器等处理肯定需要字符串对比,像 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 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/ @@ -2768,34 +2670,70 @@ Output: 0 - 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);
    +    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
    + +

    题解

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

    +

    code

    public int strStr(String haystack, String needle) {
    +        // 如果两个字符串都为空,返回 -1
    +        if (haystack == null || needle == null) {
    +            return -1;
             }
    -        return max;
    -    }
    ]]>
    + // 如果 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 @@ -2857,6 +2795,45 @@ Output: 0
    + + 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 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/ @@ -2924,6 +2901,55 @@ inorder = [9,3,15,20,7] + + 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;
    +}
    + +

    总结下

    一开始看到 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/ @@ -2996,52 +3022,84 @@ inorder = [9,3,15,20,7] - 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);
    +    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]]

    +
    +

    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]]

    +
    +

    提示

      +
    • 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 maxSofar;
    -}
    + return matrix; + }
    -

    总结下

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

    +

    结果数据


    比较慢

    ]]>
    Java leetcode - java - DP - DP leetcode java 题解 - DP + Shift 2D Grid
    @@ -3098,74 +3156,62 @@ inorder = [9,3,15,20,7] - 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 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 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: nums = [0,0,0], target = 1
    Output: 0

    -

    提示

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

      Constraints:

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

      解析

      这个题主要是矩阵或者说数组的操作,并且题目要返回的是个 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;
      +

      简单解析

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

      +

      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;
      +                    }
                       }
      -                // 根据矩阵列数 n 算出在原来矩阵的位置
      -                int last = (currentNum - 1) % n;
      -                int passLine = (currentNum - 1) / n;
      -
      -                line.add(grid[passLine][last]);
                   }
      -            matrix.add(line);
               }
      -        return matrix;
      -    }
      + return closestSum; + }
      -

      结果数据


      比较慢

      +

      结果

      ]]> Java @@ -3175,7 +3221,7 @@ inorder = [9,3,15,20,7]
    -

    这个 ApplicationEvent 其实也比较简单,内部就一个 Object 类型的 source,可以自行扩展,我们在自定义的这个 Event 里加了个 Message ,只是简单介绍下使用

    -
    public abstract class ApplicationEvent extends EventObject {
    -    private static final long serialVersionUID = 7099057708183571937L;
    -    private final long timestamp;
    -
    -    public ApplicationEvent(Object source) {
    -        super(source);
    -        this.timestamp = System.currentTimeMillis();
    -    }
    -
    -    public ApplicationEvent(Object source, Clock clock) {
    -        super(source);
    +    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++ + + + + spring event 介绍 + /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ + spring框架中如果想使用一些一部操作,除了依赖第三方中间件的消息队列,还可以用spring自己的event,简单介绍下使用方法
    首先我们可以建一个event,继承ApplicationEvent

    +
    
    +public class CustomSpringEvent extends ApplicationEvent {
    +
    +    private String message;
    +
    +    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;
    +
    +    public ApplicationEvent(Object source) {
    +        super(source);
    +        this.timestamp = System.currentTimeMillis();
    +    }
    +
    +    public ApplicationEvent(Object source, Clock clock) {
    +        super(source);
             this.timestamp = clock.millis();
         }
     
    @@ -7715,32 +7696,19 @@ for _ in data:
           
       
       
    -    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;
    -    }
    -};
    ]]>
    + 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;
    + +

    然后就能用自己的账户跟刚才更新的密码登录了。

    +]]>
    - leetcode + 小技巧 - leetcode - c++ + WordPress + 小技巧
    @@ -7868,22 +7836,6 @@ user3: swoole - - 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/ @@ -7920,19 +7872,6 @@ user3: 读后感 - - 上次的其他 外行聊国足 - /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 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
    纯主观吐槽,勿喷。

    -]]>
    - - 生活 - 运动 - - - 生活 - -
    一个 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/ @@ -8038,28 +7977,16 @@ user3: - 介绍下最近比较实用的端口转发 - /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/ - vscode 扩展转发

    在日常使用云服务器的时候,如果要访问上面自建的 mysql,一般要不直接开对应的端口,然后需要对本地 ip 进行授权,但是这个方案会有比较多的限制,比如本地 ip 变了,比如是非固定出口 ip 的家用宽带,或者要在家里跟公司都要访问,如果对所有 ip 都授权的话会不安全,这个时候其实是用 ssh 端口转发是个比较安全方便的方式。
    原来在这个之前其实对这块内容不太了解,后面是听朋友说的,vscode 的 Remote - SSH 扩展可以很方便的使用端口转发,在使用该扩展的时候,会在控制台位置里都出现一个”端口” tab

    如图中所示,我就是将一个服务器上的 mysql 的 3306 端口转发到本地的 3307 端口,至于为什么不用 3306 是因为本地我也有个 mysql 已经使用了 3306 端口,这个方法是使用的 vscode 的这个扩展,

    -

    ssh 命令转发

    还有个方式是直接使用 ssh 命令
    命令可以如此

    -
    ssh -CfNg -L 3307:127.0.0.1:3306 user1@199.199.199.199
    -

    简单介绍下这个命令
    -C 表示的是压缩数据包
    -f 表示后台执行命令
    -N 是表示不执行具体命令只用于端口转发
    -g 表示允许远程主机连接本地转发端口
    -L 则是具体端口转发的映射配置
    上面的命令就是将远程主机的 127.0.0.1:3306 对应转发到本地 3307
    而后面的用户则就是登录主机的用户名user1和ip地址199.199.199.199,当然这个配置也不是唯一的

    -

    ssh config 配置转发

    还可以在ssh 的 config 配置中加对应的配置

    -
    Host host1
    -  HostName 199.199.199.199
    -  User user1
    -  IdentityFile  /Users/user1/.ssh/id_rsa
    -  ServerAliveInterval 60
    -  LocalForward 3310 127.0.0.1:3306
    -

    然后通过 ssh host1 连接服务器的时候就能顺带做端口转发

    + 上次的其他 外行聊国足 + /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 - 技巧 + 生活 + 运动 - ssh - 端口转发 + 生活
    @@ -8082,6 +8009,31 @@ user3: 美国 + + 介绍下最近比较实用的端口转发 + /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/ + vscode 扩展转发

    在日常使用云服务器的时候,如果要访问上面自建的 mysql,一般要不直接开对应的端口,然后需要对本地 ip 进行授权,但是这个方案会有比较多的限制,比如本地 ip 变了,比如是非固定出口 ip 的家用宽带,或者要在家里跟公司都要访问,如果对所有 ip 都授权的话会不安全,这个时候其实是用 ssh 端口转发是个比较安全方便的方式。
    原来在这个之前其实对这块内容不太了解,后面是听朋友说的,vscode 的 Remote - SSH 扩展可以很方便的使用端口转发,在使用该扩展的时候,会在控制台位置里都出现一个”端口” tab

    如图中所示,我就是将一个服务器上的 mysql 的 3306 端口转发到本地的 3307 端口,至于为什么不用 3306 是因为本地我也有个 mysql 已经使用了 3306 端口,这个方法是使用的 vscode 的这个扩展,

    +

    ssh 命令转发

    还有个方式是直接使用 ssh 命令
    命令可以如此

    +
    ssh -CfNg -L 3307:127.0.0.1:3306 user1@199.199.199.199
    +

    简单介绍下这个命令
    -C 表示的是压缩数据包
    -f 表示后台执行命令
    -N 是表示不执行具体命令只用于端口转发
    -g 表示允许远程主机连接本地转发端口
    -L 则是具体端口转发的映射配置
    上面的命令就是将远程主机的 127.0.0.1:3306 对应转发到本地 3307
    而后面的用户则就是登录主机的用户名user1和ip地址199.199.199.199,当然这个配置也不是唯一的

    +

    ssh config 配置转发

    还可以在ssh 的 config 配置中加对应的配置

    +
    Host host1
    +  HostName 199.199.199.199
    +  User user1
    +  IdentityFile  /Users/user1/.ssh/id_rsa
    +  ServerAliveInterval 60
    +  LocalForward 3310 127.0.0.1:3306
    +

    然后通过 ssh host1 连接服务器的时候就能顺带做端口转发

    +]]>
    + + ssh + 技巧 + + + 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/ @@ -8124,6 +8076,25 @@ user3: 健康码 + + 关于读书打卡与分享 + /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/ + 最近群里大佬发起了一个读书打卡活动,需要每天读一会书,在群里打卡分享感悟,争取一个月能读完一本书,说实话一天十分钟的读书时间倒是问题不大,不过每天都要打卡,而且一个月要读完一本书,其实难度还是有点大的,不过也想试试看。
    之前某某老大给自己立了个 flag,说要读一百本书,这对我来说挺难实现的,一则我也不喜欢书只读一小半,二则感觉对于喜欢看的内容范围还是比较有限制,可能也算是比较矫情,不爱追热门的各类东西,因为往往会有一些跟大众不一致的观点看法,显得格格不入。所以还是这个打卡活动可能会比较适合我,书是人类进步的阶梯。
    到现在是打卡了三天了,读的主要是白岩松的《幸福了吗》,对于白岩松,我们这一代人是比较熟悉,并且整体印象比较不错的一个央视主持人,从《焦点访谈》开始,到疫情期间的各类一线节目,可能对我来说是个三观比较正,敢于说一些真话的主持人,这中间其实是有个空档期,没怎么看电视,也不太关注了,只是在疫情期间的节目,还是一如既往地给人一种可靠的感觉,正好有一次偶然微信读书推荐了白岩松的这本书,就看了一部分,正好这次继续往下看,因为相对来讲不会很晦涩,并且从这位知名央视主持人的角度分享他的过往和想法看法,还是比较有意思的。
    从对汶川地震,08 年奥运等往事的回忆和一些细节的呈现,也让我了解比较多当时所不知道的,特别是汶川地震,那时的我还在读高中,真的是看着电视,作为“猛男”都忍不住泪目了,共和国之殇,多难兴邦,但是这对于当事人来说,都是一场醒不过来的噩梦。
    然后是对于足球的热爱,其实光这个就能掰扯很多,因为我不爱足球,只爱篮球,其中原因有的没的也挺多可以说的,但是看了他的书,才能比较深入的了解一个足球迷,对足球,对中国足球,对世界杯,对阿根廷的感情。
    接下去还是想能继续坚持下去,加油!

    +]]>
    + + 生活 + 读后感 + 白岩松 + 幸福了吗 + + + 读后感 + 读书 + 打卡 + 幸福了吗 + 足球 + +
    分享记录一下一个 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/ @@ -8143,6 +8114,25 @@ user3: git + + 周末我在老丈人家打了天小工 + /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/ + 这周回家提前约好了要去老丈人家帮下忙,因为在翻修下老房子,活不是特别整的那种,所以大部分都是自己干,或者找个大工临时干几天(我们这那种比较专业的泥工匠叫做大工),像我这样去帮忙的,就是干点小工(把给大工帮忙的,干些偏体力活的叫做小工)的活。从大学毕业以后真的蛮少帮家里干活了,以前上学的时候放假还是帮家里淘个米,简单的扫地拖地啥的,当然刚高考完的时候,还去我爸厂里帮忙干了几天的活,实在是比较累,不过现在想着是觉得自己那时候比较牛,而不是特别排斥这个活,相对于现在的工作来说,导致了一系列的职业病,颈椎腰背都很僵硬,眼镜也不好,还有反流,像我爸那种活反而是脑力加体力的比较好的结合。
    这一天的活前半部分主要是在清理厨房,瓷砖上的油污和墙上天花板上即将脱落的石灰或者白色涂料层,这种活特别是瓷砖上的油污,之前在自己家里也干活,还是比较熟悉的,不过前面主要是LD 在干,我主要是先搞墙上和天花板上的,干活还是很需要技巧的,如果直接去铲,那基本我会变成一个灰人,而且吸一鼻子灰,老丈人比较专业,先接上软管用水冲,一冲效果特别好,有些石灰涂料层直接就冲掉了,冲完之后先用带加长杆的刀片铲铲了一圈墙面,说实话因为老房子之前租出去了,所以墙面什么的被糟蹋的比较难看,一层一层的,不过这还算还好,后面主要是天花板上的,这可难倒我了,从小我爸妈是比较把我当小孩管着,爬上爬下的基本都是我爸搞定,但是到了老丈人家也只得硬着头皮上了,爬到跳(一种建筑工地用的架子)上,还有点晃,小心脏扑通扑通跳,而且带加长杆的铲子还是比较重的,铲一会手也有点累,不过坚持着铲完了,上面还是比较平整的,不过下来的时候又把我难住了🤦‍♂️,往下爬的时候有根杆子要跨过去,由于裤子比较紧,强行一把跨过去怕抽筋,所以以一个非常尴尬的姿势停留休息了一会,再跨了过去,幸好事后问 LD,他们都没看到,哈哈哈,然后就是帮忙一起搞瓷砖上的油污,这个太有经验了,不过老丈人更有意思,一会试试啤酒,一会用用沙子,后面在午饭前基本就弄的比较干净了,就坐着等吃饭了,下午午休了会,就继续干活了。
    下午是我这次体验的重点了,因为要清理以前贴的墙纸,真的是个很麻烦的活,只能说贴墙纸的师傅活干得太好了,基本不可能整个撕下来,想用铲子一点点铲下来也不行,太轻了就只铲掉表面一层,太重了就把墙纸跟墙面的石灰啥的整个铲下来了,而且手又累又酸,后来想着是不是继续用水冲一下,对着一小面墙试验了下,效果还不错,但是又发现了个问题,那一面墙又有一块是后面糊上去的,铲掉外层的石灰后不平,然后就是最最重头的,也是让我后遗症持续到第二天的,要把那一块糊上去的水泥敲下来,毛估下大概是敲了80%左右,剩下的我的手已经不会用力了,因为那一块应该是要糊上去的始作俑者,就一块里面凹进去的,我拿着榔头敲到我手已经没法使劲了,而且大下午,感觉没五分钟,我的汗已经糊满脸,眼睛也睁不开,不然就流到眼睛里了,此处获得成就一:用榔头敲墙壁,也是个技术加体力的活,而且需要非常好的技巧,否则手马上就废了,敲下去的反作用力,没一会就不行了,然后是看着老丈人兄弟帮忙拆一个柜子,在我看来是个几天都搞不定的活,他轻轻松松在我敲墙的那会就搞定了,以前总觉得我干的活非常有技术含量,可是这个事情真的也是很有技巧啊,它是个把一间房间分隔开的柜子,从底到顶上,还带着门,我还在旁边帮忙撬一下脚踢,一根木条撬半天,唉,成就二:专业的人就是不一样。
    最后就是成就三了:我之前沾沾自喜的跑了多少步,做了什么锻炼,其实都是渣渣,像这样干一天活,没经历过的,基本大半天就废了,反过来说,如果能经常去这么干一天活,跑步啥的都是渣渣,消耗的能量远远超过跑个十公里啥的。

    +]]>
    + + 生活 + 运动 + 跑步 + 干活 + + + 生活 + 运动 + 减肥 + 跑步 + 干活 + +
    分享记录一下一个 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/ @@ -8183,42 +8173,51 @@ user3: - 周末我在老丈人家打了天小工 - /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/ - 这周回家提前约好了要去老丈人家帮下忙,因为在翻修下老房子,活不是特别整的那种,所以大部分都是自己干,或者找个大工临时干几天(我们这那种比较专业的泥工匠叫做大工),像我这样去帮忙的,就是干点小工(把给大工帮忙的,干些偏体力活的叫做小工)的活。从大学毕业以后真的蛮少帮家里干活了,以前上学的时候放假还是帮家里淘个米,简单的扫地拖地啥的,当然刚高考完的时候,还去我爸厂里帮忙干了几天的活,实在是比较累,不过现在想着是觉得自己那时候比较牛,而不是特别排斥这个活,相对于现在的工作来说,导致了一系列的职业病,颈椎腰背都很僵硬,眼镜也不好,还有反流,像我爸那种活反而是脑力加体力的比较好的结合。
    这一天的活前半部分主要是在清理厨房,瓷砖上的油污和墙上天花板上即将脱落的石灰或者白色涂料层,这种活特别是瓷砖上的油污,之前在自己家里也干活,还是比较熟悉的,不过前面主要是LD 在干,我主要是先搞墙上和天花板上的,干活还是很需要技巧的,如果直接去铲,那基本我会变成一个灰人,而且吸一鼻子灰,老丈人比较专业,先接上软管用水冲,一冲效果特别好,有些石灰涂料层直接就冲掉了,冲完之后先用带加长杆的刀片铲铲了一圈墙面,说实话因为老房子之前租出去了,所以墙面什么的被糟蹋的比较难看,一层一层的,不过这还算还好,后面主要是天花板上的,这可难倒我了,从小我爸妈是比较把我当小孩管着,爬上爬下的基本都是我爸搞定,但是到了老丈人家也只得硬着头皮上了,爬到跳(一种建筑工地用的架子)上,还有点晃,小心脏扑通扑通跳,而且带加长杆的铲子还是比较重的,铲一会手也有点累,不过坚持着铲完了,上面还是比较平整的,不过下来的时候又把我难住了🤦‍♂️,往下爬的时候有根杆子要跨过去,由于裤子比较紧,强行一把跨过去怕抽筋,所以以一个非常尴尬的姿势停留休息了一会,再跨了过去,幸好事后问 LD,他们都没看到,哈哈哈,然后就是帮忙一起搞瓷砖上的油污,这个太有经验了,不过老丈人更有意思,一会试试啤酒,一会用用沙子,后面在午饭前基本就弄的比较干净了,就坐着等吃饭了,下午午休了会,就继续干活了。
    下午是我这次体验的重点了,因为要清理以前贴的墙纸,真的是个很麻烦的活,只能说贴墙纸的师傅活干得太好了,基本不可能整个撕下来,想用铲子一点点铲下来也不行,太轻了就只铲掉表面一层,太重了就把墙纸跟墙面的石灰啥的整个铲下来了,而且手又累又酸,后来想着是不是继续用水冲一下,对着一小面墙试验了下,效果还不错,但是又发现了个问题,那一面墙又有一块是后面糊上去的,铲掉外层的石灰后不平,然后就是最最重头的,也是让我后遗症持续到第二天的,要把那一块糊上去的水泥敲下来,毛估下大概是敲了80%左右,剩下的我的手已经不会用力了,因为那一块应该是要糊上去的始作俑者,就一块里面凹进去的,我拿着榔头敲到我手已经没法使劲了,而且大下午,感觉没五分钟,我的汗已经糊满脸,眼睛也睁不开,不然就流到眼睛里了,此处获得成就一:用榔头敲墙壁,也是个技术加体力的活,而且需要非常好的技巧,否则手马上就废了,敲下去的反作用力,没一会就不行了,然后是看着老丈人兄弟帮忙拆一个柜子,在我看来是个几天都搞不定的活,他轻轻松松在我敲墙的那会就搞定了,以前总觉得我干的活非常有技术含量,可是这个事情真的也是很有技巧啊,它是个把一间房间分隔开的柜子,从底到顶上,还带着门,我还在旁边帮忙撬一下脚踢,一根木条撬半天,唉,成就二:专业的人就是不一样。
    最后就是成就三了:我之前沾沾自喜的跑了多少步,做了什么锻炼,其实都是渣渣,像这样干一天活,没经历过的,基本大半天就废了,反过来说,如果能经常去这么干一天活,跑步啥的都是渣渣,消耗的能量远远超过跑个十公里啥的。

    + 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) 依次递推。

    ]]>
    - 生活 - 运动 - 跑步 - 干活 + Redis + 数据结构 + C + 源码 + Redis - 生活 - 运动 - 减肥 - 跑步 - 干活 - -
    - - 在老丈人家的小工记五 - /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的钢筋,需要割断,这个割断也是很有技巧,钢筋本身在里面是受到挤压的,直接用切割的,到快断掉的时候就会崩一下,非常危险,还是老丈人比较有经验,要留一点点,然后直接用榔头敲断就好了,本来以为这个是最难的了,结果下面是一块非常大的青基石,而且也是石头跟石头挤一块,边上一点点打钻有点杯水车薪,后来是用那种螺旋的钻,钻四个洞,相对位置大概是个长方形,这样子把中间这个长方形钻出来就比较容易地能拿出来了,后面的也容易搞出来了,后面的其实难度不是特别大了,主要是地沟打好之后得看看高低是不是符合要求的,不能本来是往外排水的反而外面高,这个怎么看就又很有技巧了,一般在地上的只要侧着看一下就好了,考究点就用下水平尺,但是在地下的,不用水平尺,其实可以借助于地沟里正要放进去的水管,放点水进去,看水往哪流就行了,铺好水管后,就剩填埋的活了,不是太麻烦了,那天真的是累到了,打那个混凝土的时候我真的是把我整个人压上去了,不过也挺爽的,有点把平时无处发泄的蛮力发泄出去了。

    -]]>
    - - 生活 - 运动 - 跑步 - 干活 - - - 生活 - 小技巧 - 运动 - 减肥 - 跑步 - 干活 + redis + 数据结构 + 源码
    @@ -8242,16 +8241,24 @@ user3: - 屯菜惊魂记 - /2022/04/24/%E5%B1%AF%E8%8F%9C%E6%83%8A%E9%AD%82%E8%AE%B0/ - 因某国际大都市的给力表现,昨儿旁边行政区启动应急响应,同事早上就在群里说要去超市买菜了,到了超市人还特别多,由于来的就是我们经常去的那家超市,一方面为了安全,另一方面是怕已经抢不到了,就去了另一家比较远的超市,开车怕没车位就骑了小电驴,还下着小雨,结果到了超市差不多 12 点多,超市里出来的人都是推着一整车一整车的物资,有些比较像我,整箱的泡面,好几提纸巾,还有各种吃的,都是整箱整箱的,进了超市发现结账包括自助结账的都排很长的队,到了蔬菜货架附近,差点哼起那首歌“空空如也~”,新鲜蔬菜基本已被抢空,只剩下一些卖相不太好的土豆番薯之类的,也算是意料之外情理之中了,本来以为这家超市稍微离封控区远一些会空一点,结果就是所谓的某大都市封控了等物资,杭州市是屯了物资等封控,新鲜蔬菜没了我们也只能买点其他的,神奇的是水果基本都在,可能困难时期水果不算必需品了?还是水果基本人人都已经储备了很多,不太能理解,虽然水果还在,但是称重的地方也还有好多人排队,我们采取了并行策略,LD 在那排队,遥控指挥我去拿其他物资,拿了点碱水面,黑米,那黑米的时候还闹了个乌龙,因为前面就是散装鸡蛋的堆货的地方,结果我们以为是在那后面排队,结果称重那个在那散步了,我们还在那排队,看到后面排队,那几个挑的人也该提醒下吧,几个鸡蛋挑了半天,看看人家大妈,直接拿了四盘,看了下牛奶货架也比较空,不过还有致优跟优倍,不过不算很实惠,本来想买,只是后来赶着去结账,就给忘了,称好了黑米去看了下肉,结果肉也没了,都在买猪蹄,我们也不太爱吃猪蹄,就买了点鸡胸肉,整体看起来我们买的东西真的有点格格不入,不买泡面(因为 LD 不让买了),也不屯啥米和鸡蛋,其实鸡蛋已经买了,米也买了,其他的本身冰箱小也放不下太多东西,我是觉得还可能在屯一点这那的,LD 觉得太多了,基本的米面油有了,其他调味品什么也有了。后面就是排队结账,我去排的时候刚好前面一个小伙子跟大妈在争执,大妈说我们差不多时间来的,你要排前面就前面,小伙子有点不高兴,觉得她就是插队,哈哈,平时一般这种剧情都是发生在我身上的,这会看着前面的吵起来还是很开心的,终于有跟我一样较真的人了,有时候总觉得我是个很纠结,很较真的人,但是我现在慢慢认可了这种较真,如果没有人指出来这种是插队行为,是不对的,就会有越来越多的人觉得是可以随意插队的,正确的事应该要坚持,很多情况大家总是觉得多一事不如少一事,鸡毛蒜皮的没什么好计较的,正是这种想法,那么多人才不管任何规则,反而搞得像遵守规则都是傻 X 似的。回到屯物资,后面结账排到队了也没来得及买原来想买的花生牛奶什么的,毕竟那么多人排着队,回家后因为没有蔬菜,结果就只能吃干菜汤和饭了

    + 寄生虫观后感 + /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 生活 - 囤物资 + 影评 + 寄生虫
    @@ -8275,6 +8282,34 @@ user3: 跑步 + + 是何原因竟让两人深夜奔袭十公里 + /2022/06/05/%E6%98%AF%E4%BD%95%E5%8E%9F%E5%9B%A0%E7%AB%9F%E8%AE%A9%E4%B8%A4%E4%BA%BA%E6%B7%B1%E5%A4%9C%E5%A5%94%E8%A2%AD%E5%8D%81%E5%85%AC%E9%87%8C/ + 偶尔来个标题党,不过也是一次比较神奇的经历
    上周五下班后跟 LD 约好去吃牛蛙,某个朋友好像对这类都不太能接受,我以前小时候也不常吃,但是这类其实都是口味比较重,没有那种肉本身的腥味,而且肉质比较特殊,吃过几次以后就有点爱上了,这次刚好是 LD 买的新店开业券,比较优惠(我们俩都是有点勤俭持家的,想着小电驴还有三格电,这家店又有点远,骑车单趟大概要 10 公里左右,有点担心,LD 说应该可以的,就一起骑了过去(跟她轮换着骑电驴和共享单车),结果大概离吃牛蛙的店还有一辆公里的时候,电量就报警了,只有最后一个红色的了,一共是五格,最后一格是红色的,提示我们该充电了,这样子是真的有点慌了,之前开了几个月都是还有一两格电的时候就充电了,没有试验过究竟这最后一格电能开多远,总之先到了再说。
    这家牛蛙没想到还挺热闹的,我们到那已经快八点了,还有十几个排队的,有个人还想插队(向来是不惯着这种,一边去),旁边刚好是有些商店就逛了下,就跟常规的商业中心差不多,开业的比较早也算是这一边比较核心的商业综合体了,各种品牌都有,而且还有彩票售卖点的,只是不太理解现在的彩票都是兑图案的,而且要 10 块钱一张,我的概念里还是以前 2 块钱一张的双色球,偶尔能中个五块十块的。排队还剩四五个的时候我们就去门口坐着等了,又等了大概二十分钟才排到我们,靠近我们等的里面的位置,好像好几个小女生在那还叫了外卖奶茶,然后各种拍照,小朋友的生活还是丰富多彩的,我们到了就点了蒜蓉的,没有点传说中紫苏的,菜单上画了 N 个🌶,LD 还是想体验下说下次人多点可以试试,我们俩吃怕太辣了吃不消,口味还是不错的,这家貌似是 LD 闺蜜推荐的,口碑有保证。两个人光吃一个蛙锅就差不多了,本来还想再点个其他的,后面实在吃不下了就没点,吃完还是惯例点了个奶茶,不过是真的不好找,太大了。
    本来是就回个家的事了,结果就因为前面铺垫的小电驴已经只有一格电了,标题的深夜奔袭十公里就出现了,这个电驴估计续航也虚标挺严重的,电量也是这样,骑的时候显示只有一格电,关掉再开起来又有三格,然后我们回去骑了没一公里就没电了,这下是真的完球了,觉得车子也比较新,直接停外面也不放心,就开始了深夜的十公里推电驴奔袭,LD 看我太累还帮我中间推了一段,虽然是跑过十公里的,但是推着个没电的电驴,还是着实不容易的,LD 也是陪我推着车走,中间好几次说我们把电驴停着打车回去,把电池带回去充满了明天再过来骑车,可能是心态已经转变了,这应该算是一次很特殊的体验,从我们吃完出来大概十点,到最后我们推到小区,大概是过了两个小时的样子,说句深夜也不太过分,把这次这么推车看成了一种意志力的考验,很多事情也都是怕坚持,或者说怕不能坚持,想走得远,没有持续的努力坚持肯定是不行的,所以还是坚持着把车推回来(好吧,我其实主要是怕车被偷,毕竟刚来杭州上学没多久就被偷了自行车留下了阴影),中间感谢 LD,跟我轮着推了一段路,有些下坡的时候还在那坐着用脚蹬一下,离家里大概还有一公里的时候,有个骑电瓶车的大叔还停下来问我们是车破了还是没电了,应该是出于好意吧,最后快到的时候真的非常渴,买了2.5 升的水被我一口气喝了大半瓶,奶茶已经不能起到解渴的作用了,本来以为这样能消耗很多,结果第二天一称还重了,(我的称一定有问题 233

    +]]>
    + + 生活 + + + 生活 + +
    + + 看完了扫黑风暴,聊聊感想 + /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 月份就看完了,到现在可能才会稍微平静一点,一开始是没有想看这部剧,因为同期有一部差不多同名的电影,被投诉了对湖南埋尸案家属伤害很大,我以为就是投诉的这部电视剧,后来同事跟我说不是,所以就想着看一下,但是没有马上看,因为一直不喜欢追这种比较纠结的剧,当时看人民的名义,就是后面等不了了直接看了小说,所以差不多是等到更完了才看的。

    +

    尝试保持一个比较冷静的状态来聊聊,在看的时候有一点感想就是如果要剧里的坏人排个名,因为明眼看都是孙兴是个穷凶极恶的坏人,干尽了坏事,而且可能是演员表演地好,让人真的恨的牙痒痒,但是更多地还是停留在那些剧情中的表现和他的表情,其实对应的真实案例有更多的,这里尽量不展开,有兴趣可以自行搜索关键字,所以其实我想排个名的话,孙兴的母亲应该是我心目中是造成这个结果的比较大占比的始作俑者,因为是方方面面的,包括对林汉的栽赃迫害,最后串起来是因为他看到了孙兴又出来了,就是那句老话,撒了一个谎以后就要用无数个谎来圆,贺芸为了孙兴,作了第一个恶以后就用了一系列的丧心病狂的操作来保护孙兴,而且这之后所做的事情一件比一件可怕,并且如果不是督导组各种想方设法地去破解谜题,这个事情还可以一直被通过各种操作瞒下去,而孙兴还可以继续地为虎作伥,当然其他的包括高明远以及后面的王政,当然是为了这个操作也提供的各种方式的帮助,甚至是主导了这些操作,但是这里贺芸还是在这个位子上能够通过权力做出非常关键的动作,包括栽赃林汉,并且搞掉了李成阳。其中还有一点是我对剧情设计的质疑,也是我前面提到过一点,因为里面孙兴好像是很爱他的母亲贺芸,似乎想表达的是孙兴作的恶是因为得不到母爱,并且个人感觉如果是一个比较敬爱自己母亲的儿子,似乎应该有所畏惧,对他的行为也会有所限制,不应该变成这样一个无恶不作的恶霸,这也是我一直以来的观点,很多人作恶太多可能是因为没有信仰,不管是信基督耶稣还是信道教佛教,总归有一些制约,当然不是说就绝对不会作恶,只是偏向于有所畏惧敬畏,除了某绿哈。

    +

    而对于其他的人感觉演技都不错,只是最后有一些虎头蛇尾吧,不知道是不是审核的原因,也不细说了怕被请喝茶,还有提一点就是麦佳的这个事情,她其实是里面很惨的一个人,把高明远当成最亲近的人,而其实真相令人感觉不寒而栗,杀父杀母的仇人,对于麦佳这个演员,一直觉得印象深刻,后来才想起来就是在爱情公寓里演被关谷救了要以身相遇的那个女孩,长相其实蛮令人印象深刻的,但好像也一直不温不火,不过也不能说演技很好吧,只是在这里演的任务真的是很可怜了,剧情设计里也应该是个很重要的串联人物,最终被高明远献给了大佬,这里扯开一点,好像有的观点说贺芸之前也是这样的,只是一种推测了。

    +

    看完这部剧其实有很多想说的,但是也为了不被请喝茶,尽量少说了,只想说珍爱生命,还是自己小心吧

    +]]>
    + + 生活 + + + 生活 + 影评 + +
    搬运两个 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/ @@ -8363,34 +8398,6 @@ user3: utf8mb4_general_ci - - 是何原因竟让两人深夜奔袭十公里 - /2022/06/05/%E6%98%AF%E4%BD%95%E5%8E%9F%E5%9B%A0%E7%AB%9F%E8%AE%A9%E4%B8%A4%E4%BA%BA%E6%B7%B1%E5%A4%9C%E5%A5%94%E8%A2%AD%E5%8D%81%E5%85%AC%E9%87%8C/ - 偶尔来个标题党,不过也是一次比较神奇的经历
    上周五下班后跟 LD 约好去吃牛蛙,某个朋友好像对这类都不太能接受,我以前小时候也不常吃,但是这类其实都是口味比较重,没有那种肉本身的腥味,而且肉质比较特殊,吃过几次以后就有点爱上了,这次刚好是 LD 买的新店开业券,比较优惠(我们俩都是有点勤俭持家的,想着小电驴还有三格电,这家店又有点远,骑车单趟大概要 10 公里左右,有点担心,LD 说应该可以的,就一起骑了过去(跟她轮换着骑电驴和共享单车),结果大概离吃牛蛙的店还有一辆公里的时候,电量就报警了,只有最后一个红色的了,一共是五格,最后一格是红色的,提示我们该充电了,这样子是真的有点慌了,之前开了几个月都是还有一两格电的时候就充电了,没有试验过究竟这最后一格电能开多远,总之先到了再说。
    这家牛蛙没想到还挺热闹的,我们到那已经快八点了,还有十几个排队的,有个人还想插队(向来是不惯着这种,一边去),旁边刚好是有些商店就逛了下,就跟常规的商业中心差不多,开业的比较早也算是这一边比较核心的商业综合体了,各种品牌都有,而且还有彩票售卖点的,只是不太理解现在的彩票都是兑图案的,而且要 10 块钱一张,我的概念里还是以前 2 块钱一张的双色球,偶尔能中个五块十块的。排队还剩四五个的时候我们就去门口坐着等了,又等了大概二十分钟才排到我们,靠近我们等的里面的位置,好像好几个小女生在那还叫了外卖奶茶,然后各种拍照,小朋友的生活还是丰富多彩的,我们到了就点了蒜蓉的,没有点传说中紫苏的,菜单上画了 N 个🌶,LD 还是想体验下说下次人多点可以试试,我们俩吃怕太辣了吃不消,口味还是不错的,这家貌似是 LD 闺蜜推荐的,口碑有保证。两个人光吃一个蛙锅就差不多了,本来还想再点个其他的,后面实在吃不下了就没点,吃完还是惯例点了个奶茶,不过是真的不好找,太大了。
    本来是就回个家的事了,结果就因为前面铺垫的小电驴已经只有一格电了,标题的深夜奔袭十公里就出现了,这个电驴估计续航也虚标挺严重的,电量也是这样,骑的时候显示只有一格电,关掉再开起来又有三格,然后我们回去骑了没一公里就没电了,这下是真的完球了,觉得车子也比较新,直接停外面也不放心,就开始了深夜的十公里推电驴奔袭,LD 看我太累还帮我中间推了一段,虽然是跑过十公里的,但是推着个没电的电驴,还是着实不容易的,LD 也是陪我推着车走,中间好几次说我们把电驴停着打车回去,把电池带回去充满了明天再过来骑车,可能是心态已经转变了,这应该算是一次很特殊的体验,从我们吃完出来大概十点,到最后我们推到小区,大概是过了两个小时的样子,说句深夜也不太过分,把这次这么推车看成了一种意志力的考验,很多事情也都是怕坚持,或者说怕不能坚持,想走得远,没有持续的努力坚持肯定是不行的,所以还是坚持着把车推回来(好吧,我其实主要是怕车被偷,毕竟刚来杭州上学没多久就被偷了自行车留下了阴影),中间感谢 LD,跟我轮着推了一段路,有些下坡的时候还在那坐着用脚蹬一下,离家里大概还有一公里的时候,有个骑电瓶车的大叔还停下来问我们是车破了还是没电了,应该是出于好意吧,最后快到的时候真的非常渴,买了2.5 升的水被我一口气喝了大半瓶,奶茶已经不能起到解渴的作用了,本来以为这样能消耗很多,结果第二天一称还重了,(我的称一定有问题 233

    -]]>
    - - 生活 - - - 生活 - -
    - - 看完了扫黑风暴,聊聊感想 - /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 月份就看完了,到现在可能才会稍微平静一点,一开始是没有想看这部剧,因为同期有一部差不多同名的电影,被投诉了对湖南埋尸案家属伤害很大,我以为就是投诉的这部电视剧,后来同事跟我说不是,所以就想着看一下,但是没有马上看,因为一直不喜欢追这种比较纠结的剧,当时看人民的名义,就是后面等不了了直接看了小说,所以差不多是等到更完了才看的。

    -

    尝试保持一个比较冷静的状态来聊聊,在看的时候有一点感想就是如果要剧里的坏人排个名,因为明眼看都是孙兴是个穷凶极恶的坏人,干尽了坏事,而且可能是演员表演地好,让人真的恨的牙痒痒,但是更多地还是停留在那些剧情中的表现和他的表情,其实对应的真实案例有更多的,这里尽量不展开,有兴趣可以自行搜索关键字,所以其实我想排个名的话,孙兴的母亲应该是我心目中是造成这个结果的比较大占比的始作俑者,因为是方方面面的,包括对林汉的栽赃迫害,最后串起来是因为他看到了孙兴又出来了,就是那句老话,撒了一个谎以后就要用无数个谎来圆,贺芸为了孙兴,作了第一个恶以后就用了一系列的丧心病狂的操作来保护孙兴,而且这之后所做的事情一件比一件可怕,并且如果不是督导组各种想方设法地去破解谜题,这个事情还可以一直被通过各种操作瞒下去,而孙兴还可以继续地为虎作伥,当然其他的包括高明远以及后面的王政,当然是为了这个操作也提供的各种方式的帮助,甚至是主导了这些操作,但是这里贺芸还是在这个位子上能够通过权力做出非常关键的动作,包括栽赃林汉,并且搞掉了李成阳。其中还有一点是我对剧情设计的质疑,也是我前面提到过一点,因为里面孙兴好像是很爱他的母亲贺芸,似乎想表达的是孙兴作的恶是因为得不到母爱,并且个人感觉如果是一个比较敬爱自己母亲的儿子,似乎应该有所畏惧,对他的行为也会有所限制,不应该变成这样一个无恶不作的恶霸,这也是我一直以来的观点,很多人作恶太多可能是因为没有信仰,不管是信基督耶稣还是信道教佛教,总归有一些制约,当然不是说就绝对不会作恶,只是偏向于有所畏惧敬畏,除了某绿哈。

    -

    而对于其他的人感觉演技都不错,只是最后有一些虎头蛇尾吧,不知道是不是审核的原因,也不细说了怕被请喝茶,还有提一点就是麦佳的这个事情,她其实是里面很惨的一个人,把高明远当成最亲近的人,而其实真相令人感觉不寒而栗,杀父杀母的仇人,对于麦佳这个演员,一直觉得印象深刻,后来才想起来就是在爱情公寓里演被关谷救了要以身相遇的那个女孩,长相其实蛮令人印象深刻的,但好像也一直不温不火,不过也不能说演技很好吧,只是在这里演的任务真的是很可怜了,剧情设计里也应该是个很重要的串联人物,最终被高明远献给了大佬,这里扯开一点,好像有的观点说贺芸之前也是这样的,只是一种推测了。

    -

    看完这部剧其实有很多想说的,但是也为了不被请喝茶,尽量少说了,只想说珍爱生命,还是自己小心吧

    -]]>
    - - 生活 - - - 生活 - 影评 - -
    给小电驴上牌 /2022/03/20/%E7%BB%99%E5%B0%8F%E7%94%B5%E9%A9%B4%E4%B8%8A%E7%89%8C/ @@ -8406,1445 +8413,1445 @@ user3: - 聊一下 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 {
    +    聊一下 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);
    +    }
     
    -        /*
    -         * 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");
    +    public static NamesrvController main0(String[] args) {
     
    -        /*
    -         * 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>
    -         */
    +        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);
    +        }
     
    -        /*
    -         * Specify where to start in case the specified consumer group is a brand new one.
    -         * 指定消费起始点
    -         */
    -        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
    +        return null;
    +    }
    - /* - * Subscribe one more more topics to consume. - * 指定订阅的 topic 跟 tag,注意后面的是个表达式,可以以 tag1 || tag2 || tag3 传入 - */ - consumer.subscribe("TopicTest", "*"); +

    入口的代码时这样子,其实主要的逻辑在createNamesrvController和start方法,来看下这两个的实现

    +
    public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
    +        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
    +        //PackageConflictDetect.detectFastjson();
     
    -        /*
    -         *  Register callback to execute on arrival of messages fetched from brokers.
    -         *  注册具体获得消息后的处理方法
    -         */
    -        consumer.registerMessageListener(new MessageListenerConcurrently() {
    +        Options options = ServerUtil.buildCommandlineOptions(new Options());
    +        commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
    +        if (null == commandLine) {
    +            System.exit(-1);
    +            return null;
    +        }
     
    -            @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;
    -            }
    -        });
    +        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);
     
    -        /*
    -         *  Launch the consumer instance.
    -         * 启动消费者
    -         */
    -        consumer.start();
    +                namesrvConfig.setConfigStorePath(file);
     
    -        System.out.printf("Consumer Started.%n");
    -    }
    + System.out.printf("load config properties file OK, %s%n", file); + in.close(); + } + } -

    然后就是看看 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 (commandLine.hasOption('p')) { + InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME); + MixAll.printObjectProperties(console, namesrvConfig); + MixAll.printObjectProperties(console, nettyServerConfig); + System.exit(0); + } -
    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;
    +        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
     
    -                this.checkConfig();
    +        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);
    +        }
     
    -                this.copySubscription();
    +        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
    +        JoranConfigurator configurator = new JoranConfigurator();
    +        configurator.setContext(lc);
    +        lc.reset();
    +        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
     
    -                if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
    -                    this.defaultMQPushConsumer.changeInstanceNameToPID();
    -                }
    +        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
     
    -                // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成
    -                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
    +        MixAll.printObjectProperties(log, namesrvConfig);
    +        MixAll.printObjectProperties(log, nettyServerConfig);
     
    -                // 这个 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);
    +        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
     
    -                // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑
    -                this.pullAPIWrapper = new PullAPIWrapper(
    -                    mQClientFactory,
    -                    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
    -                this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
    -
    -                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();
    -
    -                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());
    -                }
    -
    -                // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
    -                this.consumeMessageService.start();
    -
    -                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);
    -                }
    +        // remember all configs to prevent discard
    +        controller.getConfiguration().registerConfig(properties);
     
    -                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;
    -        }
    +        return controller;
    +    }
    - this.updateTopicSubscribeInfoWhenSubscriptionChanged(); - this.mQClientFactory.checkClientInBroker(); - this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); - this.mQClientFactory.rebalanceImmediately(); - }
    -

    然后我们往下看主要的目光聚焦mQClientFactory.start()

    -
    public void start() throws MQClientException {
    +

    这个方法里其实主要是读取一些配置啥的,不是很复杂,

    +
    public static NamesrvController start(final NamesrvController controller) throws Exception {
     
    -        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 (null == controller) {
    +            throw new IllegalArgumentException("NamesrvController is null");
    +        }
     
    -        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);
    -            }
    -        }
    +        boolean initResult = controller.initialize();
    +        if (!initResult) {
    +            controller.shutdown();
    +            System.exit(-3);
    +        }
     
    -        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;
    -        }
    +        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
    +            @Override
    +            public Void call() throws Exception {
    +                controller.shutdown();
    +                return null;
    +            }
    +        }));
     
    -        pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
    +        controller.start();
     
    -        try {
    -            this.makeSureStateOK();
    -        } catch (MQClientException e) {
    -            log.warn("pullMessage exception, consumer state not ok", e);
    -            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    -            return;
    -        }
    +        return controller;
    +    }
    - 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; - } +

    这个start里主要关注initialize方法,后面就是一个停机的hook,来看下initialize方法

    +
    public boolean initialize() {
     
    -        // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制
    -        long cachedMessageCount = processQueue.getMsgCount().get();
    -        long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
    +        this.kvConfigManager.load();
     
    -        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.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
     
    -        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.remotingExecutor =
    +            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
     
    -        // 若不是顺序消费(即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.registerProcessor();
     
    -                    pullRequest.setLockedFirst(true);
    -                    pullRequest.setNextOffset(offset);
    -                }
    -            } else {
    -                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    -                log.info("pull message later because not locked in broker, {}", pullRequest);
    -                return;
    -            }
    -        }
    +        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
     
    -        // 以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;
    -        }
    +            @Override
    +            public void run() {
    +                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    +            }
    +        }, 5, 10, TimeUnit.SECONDS);
     
    -        final long beginTimestamp = System.currentTimeMillis();
    +        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
     
    -        // 异步拉取回调,先不讨论细节
    -        PullCallback pullCallback = new PullCallback() {
    -            @Override
    -            public void onSuccess(PullResult pullResult) {
    -                if (pullResult != null) {
    -                    pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
    -                        subscriptionData);
    +            @Override
    +            public void run() {
    +                NamesrvController.this.kvConfigManager.printAllPeriodically();
    +            }
    +        }, 1, 10, TimeUnit.MINUTES);
     
    -                    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 (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");
    +            }
    +        }
     
    -                            long firstMsgOffset = Long.MAX_VALUE;
    -                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
    -                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    -                            } else {
    -                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
    +        return true;
    +    }
    - DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), - pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); +

    这里的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;
    - boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); - DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( - pullResult.getMsgFoundList(), - processQueue, - pullRequest.getMessageQueue(), - dispatchToConsume); +

    然后接下去就是初始化了一个线程池,然后注册默认的处理类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() {
     
    -                                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.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 (pullResult.getNextBeginOffset() < prevRequestOffset
    -                                || firstMsgOffset < prevRequestOffset) {
    -                                log.warn(
    -                                    "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
    -                                    pullResult.getNextBeginOffset(),
    -                                    firstMsgOffset,
    -                                    prevRequestOffset);
    -                            }
    +    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);
    +            }
    +        }
     
    -                            break;
    -                        case NO_NEW_MSG:
    -                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
    +        if (null == brokerAddrFound) {
    +            brokerAddrFound = remoteAddr;
    +        } else {
    +            log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
    +        }
     
    -                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
    +        if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
     
    -                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    -                            break;
    -                        case NO_MATCHED_MSG:
    -                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
    +            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.this.correctTagsOffset(pullRequest);
    +                        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;
    +                            }
    +                        }
     
    -                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    -                            break;
    -                        case OFFSET_ILLEGAL:
    -                            log.warn("the pull request offset illegal, {} {}",
    -                                pullRequest.toString(), pullResult.toString());
    -                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
    +                        if (brokerData.getBrokerAddrs().isEmpty()) {
    +                            removeBrokerName = true;
    +                            itBrokerAddrTable.remove();
    +                            log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
    +                                brokerData.getBrokerName());
    +                        }
    +                    }
     
    -                            pullRequest.getProcessQueue().setDropped(true);
    -                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
    +                    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);
     
    -                                @Override
    -                                public void run() {
    -                                    try {
    -                                        DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
    -                                            pullRequest.getNextOffset(), false);
    +                                if (brokerNames.isEmpty()) {
    +                                    log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
    +                                        clusterName);
    +                                    it.remove();
    +                                }
     
    -                                        DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
    +                                break;
    +                            }
    +                        }
    +                    }
     
    -                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
    +                    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();
     
    -                                        log.warn("fix the pull request offset, {}", pullRequest);
    -                                    } catch (Throwable e) {
    -                                        log.error("executeTaskLater Exception", e);
    -                                    }
    -                                }
    -                            }, 10000);
    -                            break;
    -                        default:
    -                            break;
    -                    }
    -                }
    -            }
    +                            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 onException(Throwable e) {
    -                if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    -                    log.warn("execute the pull request exception", e);
    -                }
    +                            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.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; - } - } +

    第二个是每10分钟打印一次NameServer的配置参数。即KVConfigManager.configTable变量的内容。

    +
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
     
    -        // 将上面获得的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();
    -            }
    +            @Override
    +            public void run() {
    +                NamesrvController.this.kvConfigManager.printAllPeriodically();
    +            }
    +        }, 1, 10, TimeUnit.MINUTES);
    - classFilter = sd.isClassFilterMode(); - } +

    然后这个初始化就差不多完成了,后面只需要把remotingServer start一下就好了

    +

    处理请求

    直接上代码,其实主体是swtich case去判断

    +
    @Override
    +    public RemotingCommand processRequest(ChannelHandlerContext ctx,
    +        RemotingCommand request) throws RemotingCommandException {
     
    -        // 组成 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 (ctx != null) {
    +            log.debug("receive request, {} {} {}",
    +                request.getCode(),
    +                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
    +                request);
    +        }
     
    -    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;
     
    -        if (findBrokerResult.isSlave()) {
    -            sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
    -        }
    +        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;
    +    }
    - 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); +

    以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);
    +                }
    - String brokerAddr = findBrokerResult.getBrokerAddr(); - if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) { - brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr); - } +

    做了个简单的版本管理,我们看下前面一个的代码

    +
    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);
     
    -        PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
    -            brokerAddr,
    -            requestHeader,
    -            timeoutMillis,
    -            communicationMode,
    -            pullCallback);
    +    if (!checksum(ctx, request, requestHeader)) {
    +        response.setCode(ResponseCode.SYSTEM_ERROR);
    +        response.setRemark("crc32 not match");
    +        return response;
    +    }
     
    -        return pullResult;
    -    }
    +    RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
     
    -    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);
    +    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);
    +    }
     
    -    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;
    -    }
    +    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
    +        requestHeader.getClusterName(),
    +        requestHeader.getBrokerAddr(),
    +        requestHeader.getBrokerName(),
    +        requestHeader.getBrokerId(),
    +        requestHeader.getHaServerAddr(),
    +        registerBrokerBody.getTopicConfigSerializeWrapper(),
    +        registerBrokerBody.getFilterServerList(),
    +        ctx.channel());
     
    -    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();
    -
    -        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);
    -                    }
    -
    -                    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());
    -                }
    -            }
    +    responseHeader.setHaServerAddr(result.getHaServerAddr());
    +    responseHeader.setMasterAddr(result.getMasterAddr());
     
    -            return responseCommand;
    -        } finally {
    -            this.responseTable.remove(opaque);
    -        }
    -    }
    +    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
    +    response.setBody(jsonValue);
     
    -    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");
    -            }
    +    response.setCode(ResponseCode.SUCCESS);
    +    response.setRemark(null);
    +    return response;
    +}
    - 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); - } - } - }
    +

    可以看到主要的逻辑还是在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();
     
    +              // 更新这个clusterAddrTable
    +                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
    +                if (null == brokerNames) {
    +                    brokerNames = new HashSet<String>();
    +                    this.clusterAddrTable.put(clusterName, brokerNames);
    +                }
    +                brokerNames.add(brokerName);
     
    +                boolean registerFirst = false;
     
    -]]>
    -      
    -        MQ
    -        RocketMQ
    -        消息队列
    -        RocketMQ
    -        中间件
    -        RocketMQ
    -      
    -      
    -        MQ
    -        消息队列
    -        RocketMQ
    -        削峰填谷
    -        中间件
    -        DefaultMQPushConsumer
    -        源码解析
    -      
    -  
    -  
    -    聊一下 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);
    -    }
    +              // 更新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();
    +                    }
    +                }
     
    -    public static NamesrvController main0(String[] args) {
    +                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
    +                registerFirst = registerFirst || (null == oldAddr);
     
    -        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);
    -        }
    +              // 更新了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());
    +                            }
    +                        }
    +                    }
    +                }
     
    -        return null;
    -    }
    + // 更新活跃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); + } -

    入口的代码时这样子,其实主要的逻辑在createNamesrvController和start方法,来看下这两个的实现

    -
    public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
    -        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
    -        //PackageConflictDetect.detectFastjson();
    +              // 处理filter
    +                if (filterServerList != null) {
    +                    if (filterServerList.isEmpty()) {
    +                        this.filterServerTable.remove(brokerAddr);
    +                    } else {
    +                        this.filterServerTable.put(brokerAddr, filterServerList);
    +                    }
    +                }
     
    -        Options options = ServerUtil.buildCommandlineOptions(new Options());
    -        commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
    -        if (null == commandLine) {
    -            System.exit(-1);
    -            return null;
    +              // 当当前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);
             }
     
    -        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);
    +        return result;
    +    }
    - namesrvConfig.setConfigStorePath(file); +

    这个是注册 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);
     
    -                System.out.printf("load config properties file OK, %s%n", file);
    -                in.close();
    +        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);
                 }
    -        }
     
    -        if (commandLine.hasOption('p')) {
    -            InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
    -            MixAll.printObjectProperties(console, namesrvConfig);
    -            MixAll.printObjectProperties(console, nettyServerConfig);
    -            System.exit(0);
    +            byte[] content = topicRouteData.encode();
    +            response.setBody(content);
    +            response.setCode(ResponseCode.SUCCESS);
    +            response.setRemark(null);
    +            return response;
             }
     
    -        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
    +        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;
    +    }
    - 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); - } +

    首先调用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 + RocketMQ + 消息队列 + RocketMQ + 中间件 + RocketMQ + + + MQ + 消息队列 + RocketMQ + 削峰填谷 + 中间件 + NameServer + 源码解析 + +
    + + 聊一下 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 {
     
    -        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
    -        JoranConfigurator configurator = new JoranConfigurator();
    -        configurator.setContext(lc);
    -        lc.reset();
    -        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
    +        /*
    +         * 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");
     
    -        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
    +        /*
    +         * 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>
    +         */
     
    -        MixAll.printObjectProperties(log, namesrvConfig);
    -        MixAll.printObjectProperties(log, nettyServerConfig);
    +        /*
    +         * Specify where to start in case the specified consumer group is a brand new one.
    +         * 指定消费起始点
    +         */
    +        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
     
    -        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
    +        /*
    +         * Subscribe one more more topics to consume.
    +         * 指定订阅的 topic 跟 tag,注意后面的是个表达式,可以以 tag1 || tag2 || tag3 传入
    +         */
    +        consumer.subscribe("TopicTest", "*");
     
    -        // remember all configs to prevent discard
    -        controller.getConfiguration().registerConfig(properties);
    +        /*
    +         *  Register callback to execute on arrival of messages fetched from brokers.
    +         *  注册具体获得消息后的处理方法
    +         */
    +        consumer.registerMessageListener(new MessageListenerConcurrently() {
     
    -        return controller;
    -    }
    + @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 start(final NamesrvController controller) throws Exception {
    +        /*
    +         *  Launch the consumer instance.
    +         * 启动消费者
    +         */
    +        consumer.start();
     
    -        if (null == controller) {
    -            throw new IllegalArgumentException("NamesrvController is null");
    -        }
    +        System.out.printf("Consumer Started.%n");
    +    }
    - boolean initResult = controller.initialize(); - if (!initResult) { - controller.shutdown(); - System.exit(-3); - } +

    然后就是看看 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;
    - Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() { - @Override - public Void call() throws Exception { - controller.shutdown(); - return 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;
     
    -        controller.start();
    +                this.checkConfig();
     
    -        return controller;
    -    }
    + this.copySubscription(); -

    这个start里主要关注initialize方法,后面就是一个停机的hook,来看下initialize方法

    -
    public boolean initialize() {
    -
    -        this.kvConfigManager.load();
    +                if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
    +                    this.defaultMQPushConsumer.changeInstanceNameToPID();
    +                }
     
    -        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
    +                // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成
    +                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
     
    -        this.remotingExecutor =
    -            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
    +                // 这个 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.registerProcessor();
    +                // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑
    +                this.pullAPIWrapper = new PullAPIWrapper(
    +                    mQClientFactory,
    +                    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
    +                this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
     
    -        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    +                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();
     
    -            @Override
    -            public void run() {
    -                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    -            }
    -        }, 5, 10, TimeUnit.SECONDS);
    +                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.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    +                // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
    +                this.consumeMessageService.start();
     
    -            @Override
    -            public void run() {
    -                NamesrvController.this.kvConfigManager.printAllPeriodically();
    -            }
    -        }, 1, 10, TimeUnit.MINUTES);
    +                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);
    +                }
     
    -        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");
    -            }
    -        }
    +                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;
    +        }
     
    -        return true;
    -    }
    + this.updateTopicSubscribeInfoWhenSubscriptionChanged(); + this.mQClientFactory.checkClientInBroker(); + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + this.mQClientFactory.rebalanceImmediately(); + }
    +

    然后我们往下看主要的目光聚焦mQClientFactory.start()

    +
    public void start() throws MQClientException {
     
    -

    这里的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;
    + 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");
     
    -

    然后接下去就是初始化了一个线程池,然后注册默认的处理类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() {
    +        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);
    +            }
    +        }
     
    -            @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());
    -            }
    -        }
    -    }
    +        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;
    +        }
     
    -    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);
    -            }
    -        }
    +        pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
     
    -        if (null == brokerAddrFound) {
    -            brokerAddrFound = remoteAddr;
    -        } else {
    -            log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
    -        }
    +        try {
    +            this.makeSureStateOK();
    +        } catch (MQClientException e) {
    +            log.warn("pullMessage exception, consumer state not ok", e);
    +            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    +            return;
    +        }
     
    -        if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
    +        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;
    +        }
     
    -            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();
    +        // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制
    +        long cachedMessageCount = processQueue.getMsgCount().get();
    +        long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
     
    -                        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;
    -                            }
    -                        }
    +        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 (brokerData.getBrokerAddrs().isEmpty()) {
    -                            removeBrokerName = true;
    -                            itBrokerAddrTable.remove();
    -                            log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
    -                                brokerData.getBrokerName());
    -                        }
    -                    }
    +        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;
    +        }
     
    -                    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.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);
    +                    }
     
    -                                if (brokerNames.isEmpty()) {
    -                                    log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
    -                                        clusterName);
    -                                    it.remove();
    -                                }
    +                    pullRequest.setLockedFirst(true);
    +                    pullRequest.setNextOffset(offset);
    +                }
    +            } else {
    +                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    +                log.info("pull message later because not locked in broker, {}", pullRequest);
    +                return;
    +            }
    +        }
     
    -                                break;
    -                            }
    -                        }
    -                    }
    +        // 以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 (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();
    +        final long beginTimestamp = System.currentTimeMillis();
     
    -                            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);
    -                                }
    -                            }
    +        // 异步拉取回调,先不讨论细节
    +        PullCallback pullCallback = new PullCallback() {
    +            @Override
    +            public void onSuccess(PullResult pullResult) {
    +                if (pullResult != null) {
    +                    pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
    +                        subscriptionData);
     
    -                            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);
    -            }
    -        }
    -    }
    + 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); -

    第二个是每10分钟打印一次NameServer的配置参数。即KVConfigManager.configTable变量的内容。

    -
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    +                            long firstMsgOffset = Long.MAX_VALUE;
    +                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
    +                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    +                            } else {
    +                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
     
    -            @Override
    -            public void run() {
    -                NamesrvController.this.kvConfigManager.printAllPeriodically();
    -            }
    -        }, 1, 10, TimeUnit.MINUTES);
    + DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), + pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); -

    然后这个初始化就差不多完成了,后面只需要把remotingServer start一下就好了

    -

    处理请求

    直接上代码,其实主体是swtich case去判断

    -
    @Override
    -    public RemotingCommand processRequest(ChannelHandlerContext ctx,
    -        RemotingCommand request) throws RemotingCommandException {
    +                                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
    +                                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
    +                                    pullResult.getMsgFoundList(),
    +                                    processQueue,
    +                                    pullRequest.getMessageQueue(),
    +                                    dispatchToConsume);
     
    -        if (ctx != null) {
    -            log.debug("receive request, {} {} {}",
    -                request.getCode(),
    -                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
    -                request);
    -        }
    +                                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    +                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
    +                                        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
    +                                } else {
    +                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    +                                }
    +                            }
     
    +                            if (pullResult.getNextBeginOffset() < prevRequestOffset
    +                                || firstMsgOffset < prevRequestOffset) {
    +                                log.warn(
    +                                    "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
    +                                    pullResult.getNextBeginOffset(),
    +                                    firstMsgOffset,
    +                                    prevRequestOffset);
    +                            }
     
    -        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;
    -    }
    + break; + case NO_NEW_MSG: + pullRequest.setNextOffset(pullResult.getNextBeginOffset()); -

    以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.correctTagsOffset(pullRequest); -

    做了个简单的版本管理,我们看下前面一个的代码

    -
    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);
    +                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    +                            break;
    +                        case NO_MATCHED_MSG:
    +                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
     
    -    if (!checksum(ctx, request, requestHeader)) {
    -        response.setCode(ResponseCode.SYSTEM_ERROR);
    -        response.setRemark("crc32 not match");
    -        return response;
    -    }
    +                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
     
    -    RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
    +                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    +                            break;
    +                        case OFFSET_ILLEGAL:
    +                            log.warn("the pull request offset illegal, {} {}",
    +                                pullRequest.toString(), pullResult.toString());
    +                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
     
    -    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);
    -    }
    +                            pullRequest.getProcessQueue().setDropped(true);
    +                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
     
    -    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
    -        requestHeader.getClusterName(),
    -        requestHeader.getBrokerAddr(),
    -        requestHeader.getBrokerName(),
    -        requestHeader.getBrokerId(),
    -        requestHeader.getHaServerAddr(),
    -        registerBrokerBody.getTopicConfigSerializeWrapper(),
    -        registerBrokerBody.getFilterServerList(),
    -        ctx.channel());
    +                                @Override
    +                                public void run() {
    +                                    try {
    +                                        DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
    +                                            pullRequest.getNextOffset(), false);
     
    -    responseHeader.setHaServerAddr(result.getHaServerAddr());
    -    responseHeader.setMasterAddr(result.getMasterAddr());
    +                                        DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
     
    -    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
    -    response.setBody(jsonValue);
    +                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
     
    -    response.setCode(ResponseCode.SUCCESS);
    -    response.setRemark(null);
    -    return response;
    -}
    + log.warn("fix the pull request offset, {}", pullRequest); + } catch (Throwable e) { + log.error("executeTaskLater Exception", e); + } + } + }, 10000); + break; + default: + break; + } + } + } -

    可以看到主要的逻辑还是在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();
    +            @Override
    +            public void onException(Throwable e) {
    +                if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    +                    log.warn("execute the pull request exception", e);
    +                }
     
    -              // 更新这个clusterAddrTable
    -                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
    -                if (null == brokerNames) {
    -                    brokerNames = new HashSet<String>();
    -                    this.clusterAddrTable.put(clusterName, brokerNames);
    -                }
    -                brokerNames.add(brokerName);
    +                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;
    +            }
    +        }
     
    -                boolean registerFirst = false;
    +        // 将上面获得的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();
    +            }
     
    -              // 更新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();
    -                    }
    -                }
    +            classFilter = sd.isClassFilterMode();
    +        }
     
    -                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
    -                registerFirst = registerFirst || (null == oldAddr);
    +        // 组成 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);
    +    }
     
    -              // 更新了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());
    -                            }
    -                        }
    -                    }
    -                }
    +    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;
     
    -              // 更新活跃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);
    -                }
    +        if (findBrokerResult.isSlave()) {
    +            sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
    +        }
     
    -              // 处理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);
    +        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);
     
    -        TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
    +        String brokerAddr = findBrokerResult.getBrokerAddr();
    +        if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
    +            brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
    +        }
     
    -        if (topicRouteData != null) {
    -            if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
    -                String orderTopicConf =
    -                    this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
    -                        requestHeader.getTopic());
    -                topicRouteData.setOrderTopicConf(orderTopicConf);
    -            }
    +        PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
    +            brokerAddr,
    +            requestHeader,
    +            timeoutMillis,
    +            communicationMode,
    +            pullCallback);
     
    -            byte[] content = topicRouteData.encode();
    -            response.setBody(content);
    -            response.setCode(ResponseCode.SUCCESS);
    -            response.setRemark(null);
    -            return response;
    -        }
    +        return pullResult;
    +    }
     
    -        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;
    -    }
    + 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#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 - RocketMQ - 消息队列 - RocketMQ - 中间件 - RocketMQ - - - MQ - 消息队列 - RocketMQ - 削峰填谷 - 中间件 - 源码解析 - NameServer - - - - 聊一下 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);
    + 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; + } + 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();
     
    +        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);
    +                    }
     
    -

    vms95Z

    -

    如上面的图显示的,要在用户态跟内核态进行切换,数据还需要在内核缓冲跟用户缓冲之间拷贝多次,

    -
    -
      -
    1. 第一步是调用 read,需要在用户态切换成内核态,DMA模块从磁盘中读取文件,并存储在内核缓冲区,相当于是第一次复制
    2. -
    3. 数据从内核缓冲区被拷贝到用户缓冲区,read 调用返回,伴随着内核态又切换成用户态,完成了第二次复制
    4. -
    5. 然后是write 写入,这里也会伴随着用户态跟内核态的切换,数据从用户缓冲区被复制到内核空间缓冲区,完成了第三次复制,这次有点不一样的是数据不是在内核缓冲区了,会复制到 socket buffer 中。
    6. -
    7. write 系统调用返回,又切换回了用户态,然后数据由 DMA 拷贝到协议引擎。
    8. -
    -
    -

    如此就能看出其实默认的读写操作代价是非常大的,而在 rocketmq 等高性能中间件中都有使用的零拷贝技术,其中 rocketmq 使用的是 mmap

    + 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()); + } + } + + return responseCommand; + } finally { + this.responseTable.remove(opaque); + } + } + + 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"); + } + + 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); + } + } + }
    + + + +]]>
    + + MQ + RocketMQ + 消息队列 + RocketMQ + 中间件 + RocketMQ + + + MQ + 消息队列 + RocketMQ + 削峰填谷 + 中间件 + 源码解析 + DefaultMQPushConsumer + +
    + + 聊一下 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 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

    @@ -10031,85 +10038,240 @@ user3: - 聊一下 RocketMQ 的消息存储二 - /2021/09/12/%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%BA%8C/ - CommitLog 结构

    CommitLog 是 rocketmq 的服务端,也就是 broker 存储消息的的文件,跟 kafka 一样,也是顺序写入,当然消息是变长的,生成的规则是每个文件的默认1G =1024 * 1024 * 1024,commitlog的文件名fileName,名字长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1 073 741 824Byte;当这个文件满了,第二个文件名字为00000000001073741824,起始偏移量为1073741824, 消息存储的时候会顺序写入文件,当文件满了则写入下一个文件,代码中的定义

    -
    // CommitLog file size,default is 1G
    -private int mapedFileSizeCommitLog = 1024 * 1024 * 1024;
    + 聊一下 RocketMQ 的消息存储四 + /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/ + IndexFile 结构 hash 结构能够通过 key 寻找到对应在 CommitLog 中的位置

    +

    IndexFile 的构建则是分发给这个进行处理

    +
    class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
     
    -

    kLahwW

    -

    本地跑个 demo 验证下,也是这样,这里奇妙有几个比较巧妙的点(个人观点),首先文件就刚好是 1G,并且按照大小偏移量去生成下一个文件,这样获取消息的时候按大小算一下就知道在哪个文件里了,

    -

    代码中写入 CommitLog 的逻辑可以从这开始看

    -
    public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    -        // Set the storage time
    -        msg.setStoreTimestamp(System.currentTimeMillis());
    -        // Set the message body BODY CRC (consider the most appropriate setting
    -        // on the client)
    -        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
    -        // Back to Results
    -        AppendMessageResult result = null;
    +    @Override
    +    public void dispatch(DispatchRequest request) {
    +        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    +            DefaultMessageStore.this.indexService.buildIndex(request);
    +        }
    +    }
    +}
    +public void buildIndex(DispatchRequest req) {
    +        IndexFile indexFile = retryGetAndCreateIndexFile();
    +        if (indexFile != null) {
    +            long endPhyOffset = indexFile.getEndPhyOffset();
    +            DispatchRequest msg = req;
    +            String topic = msg.getTopic();
    +            String keys = msg.getKeys();
    +            if (msg.getCommitLogOffset() < endPhyOffset) {
    +                return;
    +            }
     
    -        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
    +            final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    +            switch (tranType) {
    +                case MessageSysFlag.TRANSACTION_NOT_TYPE:
    +                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    +                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
    +                    break;
    +                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
    +                    return;
    +            }
     
    -        String topic = msg.getTopic();
    -        int queueId = msg.getQueueId();
    +            if (req.getUniqKey() != null) {
    +                indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
    +                if (indexFile == null) {
    +                    log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
    +                    return;
    +                }
    +            }
     
    -        final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    -        if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
    -            || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
    -            // Delay Delivery
    -            if (msg.getDelayTimeLevel() > 0) {
    -                if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
    -                    msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
    +            if (keys != null && keys.length() > 0) {
    +                String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
    +                for (int i = 0; i < keyset.length; i++) {
    +                    String key = keyset[i];
    +                    if (key.length() > 0) {
    +                        indexFile = putKey(indexFile, msg, buildKey(topic, key));
    +                        if (indexFile == null) {
    +                            log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
    +                            return;
    +                        }
    +                    }
                     }
    +            }
    +        } else {
    +            log.error("build index error, stop building index");
    +        }
    +    }
    - topic = ScheduleMessageService.SCHEDULE_TOPIC; - queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel()); +

    配置的数量

    +
    private boolean messageIndexEnable = true;
    +private int maxHashSlotNum = 5000000;
    +private int maxIndexNum = 5000000 * 4;
    - // Backup real topic, queueId - MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic()); - MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); - msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); +

    最核心的其实是 IndexFile 的结构和如何写入

    +
    public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    +        if (this.indexHeader.getIndexCount() < this.indexNum) {
    +          // 获取 key 的 hash
    +            int keyHash = indexKeyHashMethod(key);
    +          // 计算属于哪个 slot
    +            int slotPos = keyHash % this.hashSlotNum;
    +          // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
    +            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
     
    -                msg.setTopic(topic);
    -                msg.setQueueId(queueId);
    -            }
    -        }
    +            FileLock fileLock = null;
     
    -        long eclipseTimeInLock = 0;
    -        MappedFile unlockMappedFile = null;
    -        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
    +            try {
     
    -        putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
    -        try {
    -            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
    -            this.beginTimeInLock = beginLockTimestamp;
    +                // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
    +                // false);
    +                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    +                if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
    +                    slotValue = invalidIndex;
    +                }
     
    -            // Here settings are stored timestamp, in order to ensure an orderly
    -            // global
    -            msg.setStoreTimestamp(beginLockTimestamp);
    +                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
     
    -            if (null == mappedFile || mappedFile.isFull()) {
    -                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
    -            }
    -            if (null == mappedFile) {
    -                log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
    -                beginTimeInLock = 0;
    -                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
    -            }
    +                timeDiff = timeDiff / 1000;
     
    -            result = mappedFile.appendMessage(msg, this.appendMessageCallback);
    -            switch (result.getStatus()) {
    -                case PUT_OK:
    -                    break;
    -                case END_OF_FILE:
    -                    unlockMappedFile = mappedFile;
    -                    // Create a new file, re-write the message
    -                    mappedFile = this.mappedFileQueue.getLastMappedFile(0);
    -                    if (null == mappedFile) {
    -                        // XXX: warn and notify me
    -                        log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
    -                        beginTimeInLock = 0;
    +                if (this.indexHeader.getBeginTimestamp() <= 0) {
    +                    timeDiff = 0;
    +                } else if (timeDiff > Integer.MAX_VALUE) {
    +                    timeDiff = Integer.MAX_VALUE;
    +                } else if (timeDiff < 0) {
    +                    timeDiff = 0;
    +                }
    +
    +              // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
    +                int absIndexPos =
    +                    IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
    +                        + this.indexHeader.getIndexCount() * indexSize;
    +							
    +                this.mappedByteBuffer.putInt(absIndexPos, keyHash);
    +                this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
    +                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
    +                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
    +
    +              // 存放的是数量位移,不是绝对位置
    +                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    +
    +                if (this.indexHeader.getIndexCount() <= 1) {
    +                    this.indexHeader.setBeginPhyOffset(phyOffset);
    +                    this.indexHeader.setBeginTimestamp(storeTimestamp);
    +                }
    +
    +                this.indexHeader.incHashSlotCount();
    +                this.indexHeader.incIndexCount();
    +                this.indexHeader.setEndPhyOffset(phyOffset);
    +                this.indexHeader.setEndTimestamp(storeTimestamp);
    +
    +                return true;
    +            } catch (Exception e) {
    +                log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
    +            } finally {
    +                if (fileLock != null) {
    +                    try {
    +                        fileLock.release();
    +                    } catch (IOException e) {
    +                        log.error("Failed to release the lock", e);
    +                    }
    +                }
    +            }
    +        } else {
    +            log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
    +                + "; index max num = " + this.indexNum);
    +        }
    +
    +        return false;
    +    }
    + +

    具体可以看一下这个简略的示意图

    +]]> + + MQ + RocketMQ + 消息队列 + + + MQ + 消息队列 + RocketMQ + + + + 聊一下 RocketMQ 的消息存储二 + /2021/09/12/%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%BA%8C/ + CommitLog 结构

    CommitLog 是 rocketmq 的服务端,也就是 broker 存储消息的的文件,跟 kafka 一样,也是顺序写入,当然消息是变长的,生成的规则是每个文件的默认1G =1024 * 1024 * 1024,commitlog的文件名fileName,名字长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1 073 741 824Byte;当这个文件满了,第二个文件名字为00000000001073741824,起始偏移量为1073741824, 消息存储的时候会顺序写入文件,当文件满了则写入下一个文件,代码中的定义

    +
    // CommitLog file size,default is 1G
    +private int mapedFileSizeCommitLog = 1024 * 1024 * 1024;
    + +

    kLahwW

    +

    本地跑个 demo 验证下,也是这样,这里奇妙有几个比较巧妙的点(个人观点),首先文件就刚好是 1G,并且按照大小偏移量去生成下一个文件,这样获取消息的时候按大小算一下就知道在哪个文件里了,

    +

    代码中写入 CommitLog 的逻辑可以从这开始看

    +
    public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    +        // Set the storage time
    +        msg.setStoreTimestamp(System.currentTimeMillis());
    +        // Set the message body BODY CRC (consider the most appropriate setting
    +        // on the client)
    +        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
    +        // Back to Results
    +        AppendMessageResult result = null;
    +
    +        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
    +
    +        String topic = msg.getTopic();
    +        int queueId = msg.getQueueId();
    +
    +        final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    +        if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
    +            || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
    +            // Delay Delivery
    +            if (msg.getDelayTimeLevel() > 0) {
    +                if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
    +                    msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
    +                }
    +
    +                topic = ScheduleMessageService.SCHEDULE_TOPIC;
    +                queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
    +
    +                // Backup real topic, queueId
    +                MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
    +                MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
    +                msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
    +
    +                msg.setTopic(topic);
    +                msg.setQueueId(queueId);
    +            }
    +        }
    +
    +        long eclipseTimeInLock = 0;
    +        MappedFile unlockMappedFile = null;
    +        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
    +
    +        putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
    +        try {
    +            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
    +            this.beginTimeInLock = beginLockTimestamp;
    +
    +            // Here settings are stored timestamp, in order to ensure an orderly
    +            // global
    +            msg.setStoreTimestamp(beginLockTimestamp);
    +
    +            if (null == mappedFile || mappedFile.isFull()) {
    +                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
    +            }
    +            if (null == mappedFile) {
    +                log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
    +                beginTimeInLock = 0;
    +                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
    +            }
    +
    +            result = mappedFile.appendMessage(msg, this.appendMessageCallback);
    +            switch (result.getStatus()) {
    +                case PUT_OK:
    +                    break;
    +                case END_OF_FILE:
    +                    unlockMappedFile = mappedFile;
    +                    // Create a new file, re-write the message
    +                    mappedFile = this.mappedFileQueue.getLastMappedFile(0);
    +                    if (null == mappedFile) {
    +                        // XXX: warn and notify me
    +                        log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
    +                        beginTimeInLock = 0;
                             return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
                         }
                         result = mappedFile.appendMessage(msg, this.appendMessageCallback);
    @@ -10412,158 +10574,185 @@ user3:
           
       
       
    -    聊一下 RocketMQ 的消息存储四
    -    /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/
    -    IndexFile 结构 hash 结构能够通过 key 寻找到对应在 CommitLog 中的位置

    -

    IndexFile 的构建则是分发给这个进行处理

    -
    class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
    +    聊一下 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 {
     
    -    @Override
    -    public void dispatch(DispatchRequest request) {
    -        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    -            DefaultMessageStore.this.indexService.buildIndex(request);
    -        }
    -    }
    -}
    -public void buildIndex(DispatchRequest req) {
    -        IndexFile indexFile = retryGetAndCreateIndexFile();
    -        if (indexFile != null) {
    -            long endPhyOffset = indexFile.getEndPhyOffset();
    -            DispatchRequest msg = req;
    -            String topic = msg.getTopic();
    -            String keys = msg.getKeys();
    -            if (msg.getCommitLogOffset() < endPhyOffset) {
    -                return;
    -            }
    +	@Configuration
    +	@EnableAspectJAutoProxy(proxyTargetClass = false)
    +	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
    +	public static class JdkDynamicAutoProxyConfiguration {
    +	}
     
    -            final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    -            switch (tranType) {
    -                case MessageSysFlag.TRANSACTION_NOT_TYPE:
    -                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    -                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
    -                    break;
    -                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
    -                    return;
    -            }
    +	@Configuration
    +	@EnableAspectJAutoProxy(proxyTargetClass = true)
    +	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
    +	public static class CglibAutoProxyConfiguration {
    +	}
     
    -            if (req.getUniqKey() != null) {
    -                indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
    -                if (indexFile == null) {
    -                    log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
    -                    return;
    -                }
    -            }
    +}
    - if (keys != null && keys.length() > 0) { - String[] keyset = keys.split(MessageConst.KEY_SEPARATOR); - for (int i = 0; i < keyset.length; i++) { - String key = keyset[i]; - if (key.length() > 0) { - indexFile = putKey(indexFile, msg, buildKey(topic, key)); - if (indexFile == null) { - log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); - return; - } - } - } - } - } else { - log.error("build index error, stop building index"); - } - }
    +

    而在 2.x 中变成了这样

    +
    @Configuration(proxyBeanMethods = false)
    +@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    +public class AopAutoConfiguration {
     
    -

    配置的数量

    -
    private boolean messageIndexEnable = true;
    -private int maxHashSlotNum = 5000000;
    -private int maxIndexNum = 5000000 * 4;
    + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Advice.class) + static class AspectJAutoProxyingConfiguration { -

    最核心的其实是 IndexFile 的结构和如何写入

    -
    public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    -        if (this.indexHeader.getIndexCount() < this.indexNum) {
    -          // 获取 key 的 hash
    -            int keyHash = indexKeyHashMethod(key);
    -          // 计算属于哪个 slot
    -            int slotPos = keyHash % this.hashSlotNum;
    -          // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
    -            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    +		@Configuration(proxyBeanMethods = false)
    +		@EnableAspectJAutoProxy(proxyTargetClass = false)
    +		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
    +		static class JdkDynamicAutoProxyConfiguration {
     
    -            FileLock fileLock = null;
    +		}
     
    -            try {
    +		@Configuration(proxyBeanMethods = false)
    +		@EnableAspectJAutoProxy(proxyTargetClass = true)
    +		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
    +				matchIfMissing = true)
    +		static class CglibAutoProxyConfiguration {
     
    -                // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
    -                // false);
    -                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    -                if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
    -                    slotValue = invalidIndex;
    -                }
    +		}
     
    -                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
    +	}
    - timeDiff = timeDiff / 1000; +

    为何会加载 AopAutoConfiguration 在前面的文章聊聊 SpringBoot 自动装配里已经介绍过,有兴趣的可以看下,可以发现 springboot 在 2.x 版本开始使用 cglib 作为默认的动态代理实现。

    +

    然后就是出现的问题了,代码是这样的,一个简单的基于 springboot 的带有数据库的插入,对插入代码加了事务注解,

    +
    @Mapper
    +public interface StudentMapper {
    +		// 就是插入一条数据
    +    @Insert("insert into student(name, age)" + "values ('nick', '18')")
    +    public Long insert();
    +}
     
    -                if (this.indexHeader.getBeginTimestamp() <= 0) {
    -                    timeDiff = 0;
    -                } else if (timeDiff > Integer.MAX_VALUE) {
    -                    timeDiff = Integer.MAX_VALUE;
    -                } else if (timeDiff < 0) {
    -                    timeDiff = 0;
    -                }
    +@Component
    +public class StudentManager {
     
    -              // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
    -                int absIndexPos =
    -                    IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
    -                        + this.indexHeader.getIndexCount() * indexSize;
    -							
    -                this.mappedByteBuffer.putInt(absIndexPos, keyHash);
    -                this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
    -                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
    -                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
    +    @Resource
    +    private StudentMapper studentMapper;
    +    
    +    public Long createStudent() {
    +        return studentMapper.insert();
    +    }
    +}
     
    -              // 存放的是数量位移,不是绝对位置
    -                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    +@Component
    +public class StudentServiceImpl implements StudentService {
     
    -                if (this.indexHeader.getIndexCount() <= 1) {
    -                    this.indexHeader.setBeginPhyOffset(phyOffset);
    -                    this.indexHeader.setBeginTimestamp(storeTimestamp);
    -                }
    +    @Resource
    +    private StudentManager studentManager;
     
    -                this.indexHeader.incHashSlotCount();
    -                this.indexHeader.incIndexCount();
    -                this.indexHeader.setEndPhyOffset(phyOffset);
    -                this.indexHeader.setEndTimestamp(storeTimestamp);
    +    // 自己引用
    +    @Resource
    +    private StudentServiceImpl studentService;
     
    -                return true;
    -            } catch (Exception e) {
    -                log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
    -            } finally {
    -                if (fileLock != null) {
    -                    try {
    -                        fileLock.release();
    -                    } catch (IOException e) {
    -                        log.error("Failed to release the lock", e);
    -                    }
    -                }
    -            }
    -        } else {
    -            log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
    -                + "; index max num = " + this.indexNum);
    -        }
    +    @Override
    +    @Transactional
    +    public Long createStudent() {
    +        Long id = studentManager.createStudent();
    +        Long id2 = studentService.createStudent2();
    +        return 1L;
    +    }
     
    -        return false;
    -    }
    + @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 中创建了代理类

    ]]>
    - MQ - RocketMQ - 消息队列 + Java + SpringBoot - MQ - 消息队列 - RocketMQ + Java + Spring + SpringBoot + cglib + 事务
    @@ -10812,194 +11001,61 @@ user3: - 聊一下 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 {
    -	}
    +    聊一下 SpringBoot 设置非 web 应用的方法
    +    /2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/
    +    寻找原因

    这次碰到一个比较奇怪的问题,应该统一发布脚本统一给应用启动参数传了个 -Dserver.port=xxxx,其实这个端口会作为 dubbo 的服务端口,并且应用也不提供 web 服务,但是在启动的时候会报embedded servlet container failed to start. port xxxx was already in use就觉得有点奇怪,仔细看了启动参数猜测可能是这个问题,有可能是依赖的二方三方包带了 spring-web 的包,然后基于 springboot 的 auto configuration 会把这个自己加载,就在本地复现了下这个问题,结果的确是这个问题。

    +

    解决方案

    老版本 设置 spring 不带 web 功能

    比较老的 springboot 版本,可以使用

    +
    SpringApplication app = new SpringApplication(XXXXXApplication.class);
    +app.setWebEnvironment(false);
    +app.run(args);
    +

    新版本

    新版本的 springboot (>= 2.0.0)可以在 properties 里配置

    +
    spring.main.web-application-type=none
    +

    或者

    +
    SpringApplication app = new SpringApplication(XXXXXApplication.class);
    +app.setWebApplicationType(WebApplicationType.NONE);
    +

    这个枚举里还有其他两种配置

    +
    public enum WebApplicationType {
     
    -}
    + /** + * The application should not run as a web application and should not start an + * embedded web server. + */ + NONE, -

    而在 2.x 中变成了这样

    -
    @Configuration(proxyBeanMethods = false)
    -@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    -public class AopAutoConfiguration {
    +	/**
    +	 * The application should run as a servlet-based web application and should start an
    +	 * embedded servlet web server.
    +	 */
    +	SERVLET,
     
    -	@Configuration(proxyBeanMethods = false)
    -	@ConditionalOnClass(Advice.class)
    -	static class AspectJAutoProxyingConfiguration {
    +	/**
    +	 * The application should run as a reactive web application and should start an
    +	 * embedded reactive web server.
    +	 */
    +	REACTIVE
     
    -		@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 - 事务 - - - - 聊一下 SpringBoot 中动态切换数据源的方法 - /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/ - 其实这个表示有点不太对,应该是 Druid 动态切换数据源的方法,只是应用在了 springboot 框架中,准备代码准备了半天,之前在一次数据库迁移中使用了,发现 Druid 还是很强大的,用来做动态数据源切换很方便。

    -

    首先这里的场景跟我原来用的有点点区别,在项目中使用的是通过配置中心控制数据源切换,统一切换,而这里的例子多加了个可以根据接口注解配置

    -

    第一部分是最核心的,如何基于 Spring JDBC 和 Druid 来实现数据源切换,是继承了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 这个类,他的determineCurrentLookupKey方法会被调用来获得用来决定选择那个数据源的对象,也就是 lookupKey,也可以通过这个类看到就是通过这个 lookupKey 来路由找到数据源。

    -
    public class DynamicDataSource extends AbstractRoutingDataSource {
    +}
    +

    相当于是把none 的类型和包括 servlet 和 reactive 放进了枚举类进行控制。

    +]]>
    + + Java + SpringBoot + + + Java + Spring + SpringBoot + 自动装配 + AutoConfiguration + +
    + + 聊一下 SpringBoot 中动态切换数据源的方法 + /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/ + 其实这个表示有点不太对,应该是 Druid 动态切换数据源的方法,只是应用在了 springboot 框架中,准备代码准备了半天,之前在一次数据库迁移中使用了,发现 Druid 还是很强大的,用来做动态数据源切换很方便。

    +

    首先这里的场景跟我原来用的有点点区别,在项目中使用的是通过配置中心控制数据源切换,统一切换,而这里的例子多加了个可以根据接口注解配置

    +

    第一部分是最核心的,如何基于 Spring JDBC 和 Druid 来实现数据源切换,是继承了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 这个类,他的determineCurrentLookupKey方法会被调用来获得用来决定选择那个数据源的对象,也就是 lookupKey,也可以通过这个类看到就是通过这个 lookupKey 来路由找到数据源。

    +
    public class DynamicDataSource extends AbstractRoutingDataSource {
     
         @Override
         protected Object determineCurrentLookupKey() {
    @@ -11201,55 +11257,6 @@ user3:
             数据源动态切换
           
       
    -  
    -    聊一下 SpringBoot 设置非 web 应用的方法
    -    /2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/
    -    寻找原因

    这次碰到一个比较奇怪的问题,应该统一发布脚本统一给应用启动参数传了个 -Dserver.port=xxxx,其实这个端口会作为 dubbo 的服务端口,并且应用也不提供 web 服务,但是在启动的时候会报embedded servlet container failed to start. port xxxx was already in use就觉得有点奇怪,仔细看了启动参数猜测可能是这个问题,有可能是依赖的二方三方包带了 spring-web 的包,然后基于 springboot 的 auto configuration 会把这个自己加载,就在本地复现了下这个问题,结果的确是这个问题。

    -

    解决方案

    老版本 设置 spring 不带 web 功能

    比较老的 springboot 版本,可以使用

    -
    SpringApplication app = new SpringApplication(XXXXXApplication.class);
    -app.setWebEnvironment(false);
    -app.run(args);
    -

    新版本

    新版本的 springboot (>= 2.0.0)可以在 properties 里配置

    -
    spring.main.web-application-type=none
    -

    或者

    -
    SpringApplication app = new SpringApplication(XXXXXApplication.class);
    -app.setWebApplicationType(WebApplicationType.NONE);
    -

    这个枚举里还有其他两种配置

    -
    public enum WebApplicationType {
    -
    -	/**
    -	 * The application should not run as a web application and should not start an
    -	 * embedded web server.
    -	 */
    -	NONE,
    -
    -	/**
    -	 * The application should run as a servlet-based web application and should start an
    -	 * embedded servlet web server.
    -	 */
    -	SERVLET,
    -
    -	/**
    -	 * The application should run as a reactive web application and should start an
    -	 * embedded reactive web server.
    -	 */
    -	REACTIVE
    -
    -}
    -

    相当于是把none 的类型和包括 servlet 和 reactive 放进了枚举类进行控制。

    -]]>
    - - Java - SpringBoot - - - Java - Spring - SpringBoot - 自动装配 - AutoConfiguration - -
    聊在东京奥运会闭幕式这天-二 /2021/08/19/%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-%E4%BA%8C/ @@ -11290,6 +11297,19 @@ app.setWebAp 跳水 + + 屯菜惊魂记 + /2022/04/24/%E5%B1%AF%E8%8F%9C%E6%83%8A%E9%AD%82%E8%AE%B0/ + 因某国际大都市的给力表现,昨儿旁边行政区启动应急响应,同事早上就在群里说要去超市买菜了,到了超市人还特别多,由于来的就是我们经常去的那家超市,一方面为了安全,另一方面是怕已经抢不到了,就去了另一家比较远的超市,开车怕没车位就骑了小电驴,还下着小雨,结果到了超市差不多 12 点多,超市里出来的人都是推着一整车一整车的物资,有些比较像我,整箱的泡面,好几提纸巾,还有各种吃的,都是整箱整箱的,进了超市发现结账包括自助结账的都排很长的队,到了蔬菜货架附近,差点哼起那首歌“空空如也~”,新鲜蔬菜基本已被抢空,只剩下一些卖相不太好的土豆番薯之类的,也算是意料之外情理之中了,本来以为这家超市稍微离封控区远一些会空一点,结果就是所谓的某大都市封控了等物资,杭州市是屯了物资等封控,新鲜蔬菜没了我们也只能买点其他的,神奇的是水果基本都在,可能困难时期水果不算必需品了?还是水果基本人人都已经储备了很多,不太能理解,虽然水果还在,但是称重的地方也还有好多人排队,我们采取了并行策略,LD 在那排队,遥控指挥我去拿其他物资,拿了点碱水面,黑米,那黑米的时候还闹了个乌龙,因为前面就是散装鸡蛋的堆货的地方,结果我们以为是在那后面排队,结果称重那个在那散步了,我们还在那排队,看到后面排队,那几个挑的人也该提醒下吧,几个鸡蛋挑了半天,看看人家大妈,直接拿了四盘,看了下牛奶货架也比较空,不过还有致优跟优倍,不过不算很实惠,本来想买,只是后来赶着去结账,就给忘了,称好了黑米去看了下肉,结果肉也没了,都在买猪蹄,我们也不太爱吃猪蹄,就买了点鸡胸肉,整体看起来我们买的东西真的有点格格不入,不买泡面(因为 LD 不让买了),也不屯啥米和鸡蛋,其实鸡蛋已经买了,米也买了,其他的本身冰箱小也放不下太多东西,我是觉得还可能在屯一点这那的,LD 觉得太多了,基本的米面油有了,其他调味品什么也有了。后面就是排队结账,我去排的时候刚好前面一个小伙子跟大妈在争执,大妈说我们差不多时间来的,你要排前面就前面,小伙子有点不高兴,觉得她就是插队,哈哈,平时一般这种剧情都是发生在我身上的,这会看着前面的吵起来还是很开心的,终于有跟我一样较真的人了,有时候总觉得我是个很纠结,很较真的人,但是我现在慢慢认可了这种较真,如果没有人指出来这种是插队行为,是不对的,就会有越来越多的人觉得是可以随意插队的,正确的事应该要坚持,很多情况大家总是觉得多一事不如少一事,鸡毛蒜皮的没什么好计较的,正是这种想法,那么多人才不管任何规则,反而搞得像遵守规则都是傻 X 似的。回到屯物资,后面结账排到队了也没来得及买原来想买的花生牛奶什么的,毕竟那么多人排着队,回家后因为没有蔬菜,结果就只能吃干菜汤和饭了

    +]]>
    + + 生活 + + + 生活 + 囤物资 + +
    聊聊 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/ @@ -12091,117 +12111,430 @@ public Result invoke(final Invocation invocation) throws RpcException { - 聊聊 Java 自带的那些*逆天*工具 - /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/ - 原谅我的标题党,其实这些工具的确很厉害,之前其实介绍过一点相关的,是从我一次问题排查的过程中用到的,但是最近又有碰到一次排查问题,发现其实用 idea 直接 dump thread 是不现实的,毕竟服务器环境的没法这么操作,那就得用 Java 的那些工具了

    -

    jstack & jps

    譬如 jstack,这个命令其实不能更简单了
    看看 help 信息

    -l参数可以打出锁的额外信息,然后后面的 pid 就是进程 id 咯,机智的小伙伴会问了(就你这个小白才问这么蠢的问题🤦‍♂️),怎么看 Java 应用的进程呢
    那就是 jps 了,命令也很简单,一般直接用 jps命令就好了,不过也可以 help 看一下

    稍微解释下,-q是只显示进程 id,-m是输出给main 方法的参数,比如我在配置中加给参数

    然后用 jps -m查看

    -v加上小 v 的话就是打印 jvm 参数

    还是有点东西,然后就继续介绍 jstack 了,然后我们看看 jstack 出来是啥,为了加点内容我加了个死锁

    -
    public static void main(String[] args) throws InterruptedException {
    -        SpringApplication.run(ThreadDumpDemoApplication.class, args);
    -        ReentrantLock lock1 = new ReentrantLock();
    -        ReentrantLock lock2 = new ReentrantLock();
    -        Thread t1 = new Thread() {
    +    聊聊 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/
    +    类加载器

    类加载机制中说来说去其实也逃不开类加载器这个话题,我们就来说下类加载器这个话题,Java 在 jdk1.2 以后开始有了
    Java 虚拟机设计团队有意把加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader).
    其实在 Java 中类加载器有一个很常用的作用,比如一个类的唯一性,其实是由加载它的类加载器和这个类一起来确定这个类在虚拟机的唯一性,这里也参考下周志明书里的例子

    +
    public class ClassLoaderTest {
    +
    +    public static void main(String[] args) throws Exception {
    +        ClassLoader myLoader = new ClassLoader() {
                 @Override
    -            public void run() {
    +            public Class<?> loadClass(String name) throws ClassNotFoundException {
                     try {
    -                    lock1.lock();
    -                    TimeUnit.SECONDS.sleep(1);
    -                    lock2.lock();
    -                } catch (InterruptedException e) {
    -                    e.printStackTrace();
    +                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    +                    InputStream is = getClass().getResourceAsStream(fileName);
    +                    if (is == null) {
    +                        return super.loadClass(name);
    +                    }
    +                    byte[] b = new byte[is.available()];
    +                    is.read(b);
    +                    return defineClass(name, b, 0, b.length);
    +                } catch (IOException e) {
    +                    throw new ClassNotFoundException(name);
                     }
                 }
             };
    -        Thread t2 = new Thread() {
    -            @Override
    -            public void run() {
    -                try {
    -                    lock2.lock();
    -                    TimeUnit.SECONDS.sleep(1);
    -                    lock1.lock();
    -                } catch (InterruptedException e) {
    -                    e.printStackTrace();
    +        Object object = myLoader.loadClass("com.nicksxs.demo.ClassLoaderTest").newInstance();
    +        System.out.println(object.getClass());
    +        System.out.println(object instanceof ClassLoaderTest);
    +    }
    +}
    +

    可以看下结果

    这里说明了当一个是由虚拟机的应用程序类加载器所加载的和另一个由自己写的自定义类加载器加载的,虽然是同一个类,但是 instanceof 的结果就是 false 的

    +

    双亲委派

    自 JDK1.2 以来,Java 一直有些三层类加载器、双亲委派的类加载架构

    +

    启动类加载器

    首先是启动类加载器,Bootstrap Class Loader,这个类加载器负责加载放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中,也不会被加载)类库加载到虚拟机的内存中,启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把家在请求为派给引导类加载器去处理,那直接使用 null 代替即可,可以看下 java.lang.ClassLoader.getClassLoader()方法的代码片段

    +
    /**
    +     * Returns the class loader for the class.  Some implementations may use
    +     * null to represent the bootstrap class loader. This method will return
    +     * null in such implementations if this class was loaded by the bootstrap
    +     * class loader.
    +     *
    +     * <p> If a security manager is present, and the caller's class loader is
    +     * not null and the caller's class loader is not the same as or an ancestor of
    +     * the class loader for the class whose class loader is requested, then
    +     * this method calls the security manager's {@code checkPermission}
    +     * method with a {@code RuntimePermission("getClassLoader")}
    +     * permission to ensure it's ok to access the class loader for the class.
    +     *
    +     * <p>If this object
    +     * represents a primitive type or void, null is returned.
    +     *
    +     * @return  the class loader that loaded the class or interface
    +     *          represented by this object.
    +     * @throws SecurityException
    +     *    if a security manager exists and its
    +     *    {@code checkPermission} method denies
    +     *    access to the class loader for the class.
    +     * @see java.lang.ClassLoader
    +     * @see SecurityManager#checkPermission
    +     * @see java.lang.RuntimePermission
    +     */
    +    @CallerSensitive
    +    public ClassLoader getClassLoader() {
    +        ClassLoader cl = getClassLoader0();
    +        if (cl == null)
    +            return null;
    +        SecurityManager sm = System.getSecurityManager();
    +        if (sm != null) {
    +            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    +        }
    +        return cl;
    +    }
    +

    扩展类加载器

    这个类加载器是在类sun.misc.Launcher.ExtClassLoader中以 Java 代码的形式实现的,它负责在家\lib\ext 目录中,或者被 java.ext.dirs系统变量中所指定的路径中的所有类库,它其实目的是为了实现 Java 系统类库的扩展机制

    +

    应用程序类加载器

    这个类加载器是由sun.misc.Launcher.AppClassLoader实现,通过 java 代码,并且是 ClassLoader 类中的 getSystemClassLoader()方法的返回值,可以看一下代码

    +
    /**
    +     * Returns the system class loader for delegation.  This is the default
    +     * delegation parent for new <tt>ClassLoader</tt> instances, and is
    +     * typically the class loader used to start the application.
    +     *
    +     * <p> This method is first invoked early in the runtime's startup
    +     * sequence, at which point it creates the system class loader and sets it
    +     * as the context class loader of the invoking <tt>Thread</tt>.
    +     *
    +     * <p> The default system class loader is an implementation-dependent
    +     * instance of this class.
    +     *
    +     * <p> If the system property "<tt>java.system.class.loader</tt>" is defined
    +     * when this method is first invoked then the value of that property is
    +     * taken to be the name of a class that will be returned as the system
    +     * class loader.  The class is loaded using the default system class loader
    +     * and must define a public constructor that takes a single parameter of
    +     * type <tt>ClassLoader</tt> which is used as the delegation parent.  An
    +     * instance is then created using this constructor with the default system
    +     * class loader as the parameter.  The resulting class loader is defined
    +     * to be the system class loader.
    +     *
    +     * <p> If a security manager is present, and the invoker's class loader is
    +     * not <tt>null</tt> and the invoker's class loader is not the same as or
    +     * an ancestor of the system class loader, then this method invokes the
    +     * security manager's {@link
    +     * SecurityManager#checkPermission(java.security.Permission)
    +     * <tt>checkPermission</tt>} method with a {@link
    +     * RuntimePermission#RuntimePermission(String)
    +     * <tt>RuntimePermission("getClassLoader")</tt>} permission to verify
    +     * access to the system class loader.  If not, a
    +     * <tt>SecurityException</tt> will be thrown.  </p>
    +     *
    +     * @return  The system <tt>ClassLoader</tt> for delegation, or
    +     *          <tt>null</tt> if none
    +     *
    +     * @throws  SecurityException
    +     *          If a security manager exists and its <tt>checkPermission</tt>
    +     *          method doesn't allow access to the system class loader.
    +     *
    +     * @throws  IllegalStateException
    +     *          If invoked recursively during the construction of the class
    +     *          loader specified by the "<tt>java.system.class.loader</tt>"
    +     *          property.
    +     *
    +     * @throws  Error
    +     *          If the system property "<tt>java.system.class.loader</tt>"
    +     *          is defined but the named class could not be loaded, the
    +     *          provider class does not define the required constructor, or an
    +     *          exception is thrown by that constructor when it is invoked. The
    +     *          underlying cause of the error can be retrieved via the
    +     *          {@link Throwable#getCause()} method.
    +     *
    +     * @revised  1.4
    +     */
    +    @CallerSensitive
    +    public static ClassLoader getSystemClassLoader() {
    +        initSystemClassLoader();
    +        if (scl == null) {
    +            return null;
    +        }
    +        SecurityManager sm = System.getSecurityManager();
    +        if (sm != null) {
    +            checkClassLoaderPermission(scl, Reflection.getCallerClass());
    +        }
    +        return scl;
    +    }
    +    private static synchronized void initSystemClassLoader() {
    +        if (!sclSet) {
    +            if (scl != null)
    +                throw new IllegalStateException("recursive invocation");
    +            // 主要的第一步是这
    +            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
    +            if (l != null) {
    +                Throwable oops = null;
    +                // 然后是这
    +                scl = l.getClassLoader();
    +                try {
    +                    scl = AccessController.doPrivileged(
    +                        new SystemClassLoaderAction(scl));
    +                } catch (PrivilegedActionException pae) {
    +                    oops = pae.getCause();
    +                    if (oops instanceof InvocationTargetException) {
    +                        oops = oops.getCause();
    +                    }
    +                }
    +                if (oops != null) {
    +                    if (oops instanceof Error) {
    +                        throw (Error) oops;
    +                    } else {
    +                        // wrap the exception
    +                        throw new Error(oops);
    +                    }
                     }
                 }
    -        };
    -        t1.setName("mythread1");
    -        t2.setName("mythread2");
    -        t1.start();
    -        t2.start();
    -        Thread.sleep(10000);
    -    }
    -

    然后看看出来时怎么样的

    -
    2020-08-02 21:50:32
    -Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):
    -
    -"DestroyJavaVM" #147 prio=5 os_prio=31 tid=0x00007fc9dd807000 nid=0x2603 waiting on condition [0x0000000000000000]
    -   java.lang.Thread.State: RUNNABLE
    -
    -   Locked ownable synchronizers:
    -        - None
    -
    -"mythread2" #140 prio=5 os_prio=31 tid=0x00007fc9dd877000 nid=0x9903 waiting on condition [0x0000700006fb9000]
    -   java.lang.Thread.State: WAITING (parking)
    -        at sun.misc.Unsafe.park(Native Method)
    -        - parking to wait for  <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    -        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    -        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    -        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    -        at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$2.run(ThreadDumpDemoApplication.java:34)
    -
    -   Locked ownable synchronizers:
    -        - <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    -
    -"mythread1" #139 prio=5 os_prio=31 tid=0x00007fc9de873800 nid=0x9a03 waiting on condition [0x0000700006eb6000]
    -   java.lang.Thread.State: WAITING (parking)
    -        at sun.misc.Unsafe.park(Native Method)
    -        - parking to wait for  <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    -        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    -        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    -        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    -        at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$1.run(ThreadDumpDemoApplication.java:22)
    -
    -   Locked ownable synchronizers:
    -        - <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    +            sclSet = true;
    +        }
    +    }
    +// 接着跟到sun.misc.Launcher#getClassLoader
    +public ClassLoader getClassLoader() {
    +        return this.loader;
    +    }
    +// 然后看到这 sun.misc.Launcher#Launcher
    +public Launcher() {
    +        Launcher.ExtClassLoader var1;
    +        try {
    +            var1 = Launcher.ExtClassLoader.getExtClassLoader();
    +        } catch (IOException var10) {
    +            throw new InternalError("Could not create extension class loader", var10);
    +        }
     
    -"http-nio-8080-Acceptor" #137 daemon prio=5 os_prio=31 tid=0x00007fc9de1ac000 nid=0x9b03 runnable [0x0000700006db3000]
    -   java.lang.Thread.State: RUNNABLE
    -        at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
    -        at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
    -        at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
    -        - locked <0x000000076f1e4820> (a java.lang.Object)
    -        at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:469)
    -        at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:71)
    -        at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:95)
    -        at java.lang.Thread.run(Thread.java:748)
    +        try {
    +            // 可以看到 就是 AppClassLoader
    +            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    +        } catch (IOException var9) {
    +            throw new InternalError("Could not create application class loader", var9);
    +        }
     
    -   Locked ownable synchronizers:
    -        - None
    +        Thread.currentThread().setContextClassLoader(this.loader);
    +        String var2 = System.getProperty("java.security.manager");
    +        if (var2 != null) {
    +            SecurityManager var3 = null;
    +            if (!"".equals(var2) && !"default".equals(var2)) {
    +                try {
    +                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
    +                } catch (IllegalAccessException var5) {
    +                } catch (InstantiationException var6) {
    +                } catch (ClassNotFoundException var7) {
    +                } catch (ClassCastException var8) {
    +                }
    +            } else {
    +                var3 = new SecurityManager();
    +            }
     
    -"http-nio-8080-ClientPoller" #136 daemon prio=5 os_prio=31 tid=0x00007fc9dd876800 nid=0x6503 runnable [0x0000700006cb0000]
    -   java.lang.Thread.State: RUNNABLE
    -        at sun.nio.ch.KQueueArrayWrapper.kevent0(Native Method)
    -        at sun.nio.ch.KQueueArrayWrapper.poll(KQueueArrayWrapper.java:198)
    -        at sun.nio.ch.KQueueSelectorImpl.doSelect(KQueueSelectorImpl.java:117)
    -        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
    -        - locked <0x000000076f2978c8> (a sun.nio.ch.Util$3)
    -        - locked <0x000000076f2978b8> (a java.util.Collections$UnmodifiableSet)
    -        - locked <0x000000076f297798> (a sun.nio.ch.KQueueSelectorImpl)
    -        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
    -        at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:709)
    -        at java.lang.Thread.run(Thread.java:748)
    +            if (var3 == null) {
    +                throw new InternalError("Could not create SecurityManager: " + var2);
    +            }
     
    -   Locked ownable synchronizers:
    -        - None
    +            System.setSecurityManager(var3);
    +        }
     
    -"http-nio-8080-exec-10" #135 daemon prio=5 os_prio=31 tid=0x00007fc9de1af000 nid=0x9d03 waiting on condition [0x0000700006bad000]
    -   java.lang.Thread.State: WAITING (parking)
    -        at sun.misc.Unsafe.park(Native Method)
    +    }
    +

    它负责加载用户类路径(ClassPath)上所有的类库,我们可以直接在代码中使用这个类加载器,如果我们的代码中没有自定义的类在加载器,一般情况下这个就是程序中默认的类加载器

    +

    双亲委派模型


    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试家在这个类,而是把这个请求为派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的家在请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去完成加载。
    使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起举杯了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要家在这个类,最终都是委派给处于模型最顶层的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双薪委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
    可以来看下双亲委派模型的代码实现

    +
    /**
    +     * Loads the class with the specified <a href="#name">binary name</a>.  The
    +     * default implementation of this method searches for classes in the
    +     * following order:
    +     *
    +     * <ol>
    +     *
    +     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
    +     *   has already been loaded.  </p></li>
    +     *
    +     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
    +     *   on the parent class loader.  If the parent is <tt>null</tt> the class
    +     *   loader built-in to the virtual machine is used, instead.  </p></li>
    +     *
    +     *   <li><p> Invoke the {@link #findClass(String)} method to find the
    +     *   class.  </p></li>
    +     *
    +     * </ol>
    +     *
    +     * <p> If the class was found using the above steps, and the
    +     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
    +     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
    +     *
    +     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
    +     * #findClass(String)}, rather than this method.  </p>
    +     *
    +     * <p> Unless overridden, this method synchronizes on the result of
    +     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
    +     * during the entire class loading process.
    +     *
    +     * @param  name
    +     *         The <a href="#name">binary name</a> of the class
    +     *
    +     * @param  resolve
    +     *         If <tt>true</tt> then resolve the class
    +     *
    +     * @return  The resulting <tt>Class</tt> object
    +     *
    +     * @throws  ClassNotFoundException
    +     *          If the class could not be found
    +     */
    +    protected Class<?> loadClass(String name, boolean resolve)
    +        throws ClassNotFoundException
    +    {
    +        synchronized (getClassLoadingLock(name)) {
    +            // First, check if the class has already been loaded
    +            Class<?> c = findLoadedClass(name);
    +            if (c == null) {
    +                long t0 = System.nanoTime();
    +                try {
    +                    if (parent != null) {
    +                        // 委托父类加载
    +                        c = parent.loadClass(name, false);
    +                    } else {
    +                        // 使用启动类加载器
    +                        c = findBootstrapClassOrNull(name);
    +                    }
    +                } catch (ClassNotFoundException e) {
    +                    // ClassNotFoundException thrown if class not found
    +                    // from the non-null parent class loader
    +                }
    +
    +                if (c == null) {
    +                    // If still not found, then invoke findClass in order
    +                    // to find the class.
    +                    long t1 = System.nanoTime();
    +                    // 调用自己的 findClass() 方法尝试进行加载
    +                    c = findClass(name);
    +
    +                    // this is the defining class loader; record the stats
    +                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    +                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    +                    sun.misc.PerfCounter.getFindClasses().increment();
    +                }
    +            }
    +            if (resolve) {
    +                resolveClass(c);
    +            }
    +            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 自带的那些*逆天*工具 + /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/ + 原谅我的标题党,其实这些工具的确很厉害,之前其实介绍过一点相关的,是从我一次问题排查的过程中用到的,但是最近又有碰到一次排查问题,发现其实用 idea 直接 dump thread 是不现实的,毕竟服务器环境的没法这么操作,那就得用 Java 的那些工具了

    +

    jstack & jps

    譬如 jstack,这个命令其实不能更简单了
    看看 help 信息

    -l参数可以打出锁的额外信息,然后后面的 pid 就是进程 id 咯,机智的小伙伴会问了(就你这个小白才问这么蠢的问题🤦‍♂️),怎么看 Java 应用的进程呢
    那就是 jps 了,命令也很简单,一般直接用 jps命令就好了,不过也可以 help 看一下

    稍微解释下,-q是只显示进程 id,-m是输出给main 方法的参数,比如我在配置中加给参数

    然后用 jps -m查看

    -v加上小 v 的话就是打印 jvm 参数

    还是有点东西,然后就继续介绍 jstack 了,然后我们看看 jstack 出来是啥,为了加点内容我加了个死锁

    +
    public static void main(String[] args) throws InterruptedException {
    +        SpringApplication.run(ThreadDumpDemoApplication.class, args);
    +        ReentrantLock lock1 = new ReentrantLock();
    +        ReentrantLock lock2 = new ReentrantLock();
    +        Thread t1 = new Thread() {
    +            @Override
    +            public void run() {
    +                try {
    +                    lock1.lock();
    +                    TimeUnit.SECONDS.sleep(1);
    +                    lock2.lock();
    +                } catch (InterruptedException e) {
    +                    e.printStackTrace();
    +                }
    +            }
    +        };
    +        Thread t2 = new Thread() {
    +            @Override
    +            public void run() {
    +                try {
    +                    lock2.lock();
    +                    TimeUnit.SECONDS.sleep(1);
    +                    lock1.lock();
    +                } catch (InterruptedException e) {
    +                    e.printStackTrace();
    +                }
    +            }
    +        };
    +        t1.setName("mythread1");
    +        t2.setName("mythread2");
    +        t1.start();
    +        t2.start();
    +        Thread.sleep(10000);
    +    }
    +

    然后看看出来时怎么样的

    +
    2020-08-02 21:50:32
    +Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):
    +
    +"DestroyJavaVM" #147 prio=5 os_prio=31 tid=0x00007fc9dd807000 nid=0x2603 waiting on condition [0x0000000000000000]
    +   java.lang.Thread.State: RUNNABLE
    +
    +   Locked ownable synchronizers:
    +        - None
    +
    +"mythread2" #140 prio=5 os_prio=31 tid=0x00007fc9dd877000 nid=0x9903 waiting on condition [0x0000700006fb9000]
    +   java.lang.Thread.State: WAITING (parking)
    +        at sun.misc.Unsafe.park(Native Method)
    +        - parking to wait for  <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    +        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    +        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    +        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    +        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    +        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    +        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    +        at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$2.run(ThreadDumpDemoApplication.java:34)
    +
    +   Locked ownable synchronizers:
    +        - <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    +
    +"mythread1" #139 prio=5 os_prio=31 tid=0x00007fc9de873800 nid=0x9a03 waiting on condition [0x0000700006eb6000]
    +   java.lang.Thread.State: WAITING (parking)
    +        at sun.misc.Unsafe.park(Native Method)
    +        - parking to wait for  <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    +        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    +        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    +        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    +        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    +        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    +        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    +        at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$1.run(ThreadDumpDemoApplication.java:22)
    +
    +   Locked ownable synchronizers:
    +        - <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    +
    +"http-nio-8080-Acceptor" #137 daemon prio=5 os_prio=31 tid=0x00007fc9de1ac000 nid=0x9b03 runnable [0x0000700006db3000]
    +   java.lang.Thread.State: RUNNABLE
    +        at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
    +        at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
    +        at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
    +        - locked <0x000000076f1e4820> (a java.lang.Object)
    +        at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:469)
    +        at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:71)
    +        at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:95)
    +        at java.lang.Thread.run(Thread.java:748)
    +
    +   Locked ownable synchronizers:
    +        - None
    +
    +"http-nio-8080-ClientPoller" #136 daemon prio=5 os_prio=31 tid=0x00007fc9dd876800 nid=0x6503 runnable [0x0000700006cb0000]
    +   java.lang.Thread.State: RUNNABLE
    +        at sun.nio.ch.KQueueArrayWrapper.kevent0(Native Method)
    +        at sun.nio.ch.KQueueArrayWrapper.poll(KQueueArrayWrapper.java:198)
    +        at sun.nio.ch.KQueueSelectorImpl.doSelect(KQueueSelectorImpl.java:117)
    +        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
    +        - locked <0x000000076f2978c8> (a sun.nio.ch.Util$3)
    +        - locked <0x000000076f2978b8> (a java.util.Collections$UnmodifiableSet)
    +        - locked <0x000000076f297798> (a sun.nio.ch.KQueueSelectorImpl)
    +        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
    +        at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:709)
    +        at java.lang.Thread.run(Thread.java:748)
    +
    +   Locked ownable synchronizers:
    +        - None
    +
    +"http-nio-8080-exec-10" #135 daemon prio=5 os_prio=31 tid=0x00007fc9de1af000 nid=0x9d03 waiting on condition [0x0000700006bad000]
    +   java.lang.Thread.State: WAITING (parking)
    +        at sun.misc.Unsafe.park(Native Method)
             - parking to wait for  <0x000000076f26aa00> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
             at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
             at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
    @@ -12568,361 +12901,48 @@ JNI global references: for ownable synchronizer 0x000000076f5d4330, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
       which is held by "mythread1"
     "mythread1":
    -  waiting for ownable synchronizer 0x000000076f5d4360, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
    -  which is held by "mythread2"
    -
    -Java stack information for the threads listed above:
    -===================================================
    -"mythread2":
    -        at sun.misc.Unsafe.park(Native Method)
    -        - parking to wait for  <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    -        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    -        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    -        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    -        at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$2.run(ThreadDumpDemoApplication.java:34)
    -"mythread1":
    -        at sun.misc.Unsafe.park(Native Method)
    -        - parking to wait for  <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    -        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    -        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    -        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    -        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    -        at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$1.run(ThreadDumpDemoApplication.java:22)
    -
    -Found 1 deadlock.
    -

    前面的信息其实上次就看过了,后面就可以发现有个死锁了,

    上面比较长,把主要的截出来,就是这边的,这点就很强大了。

    -

    jmap

    惯例还是看一下帮助信息

    这个相对命令比较多,不过因为现在 dump 下来我们可能会用文件模式,然后将文件下载下来使用 mat 进行分析,所以可以使用
    jmap -dump:live,format=b,file=heap.bin <pid>
    命令照着上面看的就是打印活着的对象,然后以二进制格式,文件名叫 heap.bin 然后最后就是进程 id,打印出来以后可以用 mat 打开

    这样就可以很清晰的看到应用里的各种信息,jmap 直接在命令中还可以看很多信息,比如使用jmap -histo <pid>打印对象的实例数和对象占用的内存

    jmap -finalizerinfo <pid> 打印正在等候回收的对象

    -

    小tips

    对于一些应用内存已经占满了,jstack 和 jmap 可能会连不上的情况,可以使用-F参数强制打印线程或者 dump 文件,但是要注意这两者使用的用户必须与 java 进程启动用户一致,并且使用的 jdk 也要一致

    -]]>
    - - Java - Thread dump - 问题排查 - 工具 - - - Java - JPS - JStack - JMap - -
    - - 聊聊 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/ - 类加载器

    类加载机制中说来说去其实也逃不开类加载器这个话题,我们就来说下类加载器这个话题,Java 在 jdk1.2 以后开始有了
    Java 虚拟机设计团队有意把加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader).
    其实在 Java 中类加载器有一个很常用的作用,比如一个类的唯一性,其实是由加载它的类加载器和这个类一起来确定这个类在虚拟机的唯一性,这里也参考下周志明书里的例子

    -
    public class ClassLoaderTest {
    -
    -    public static void main(String[] args) throws Exception {
    -        ClassLoader myLoader = new ClassLoader() {
    -            @Override
    -            public Class<?> loadClass(String name) throws ClassNotFoundException {
    -                try {
    -                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    -                    InputStream is = getClass().getResourceAsStream(fileName);
    -                    if (is == null) {
    -                        return super.loadClass(name);
    -                    }
    -                    byte[] b = new byte[is.available()];
    -                    is.read(b);
    -                    return defineClass(name, b, 0, b.length);
    -                } catch (IOException e) {
    -                    throw new ClassNotFoundException(name);
    -                }
    -            }
    -        };
    -        Object object = myLoader.loadClass("com.nicksxs.demo.ClassLoaderTest").newInstance();
    -        System.out.println(object.getClass());
    -        System.out.println(object instanceof ClassLoaderTest);
    -    }
    -}
    -

    可以看下结果

    这里说明了当一个是由虚拟机的应用程序类加载器所加载的和另一个由自己写的自定义类加载器加载的,虽然是同一个类,但是 instanceof 的结果就是 false 的

    -

    双亲委派

    自 JDK1.2 以来,Java 一直有些三层类加载器、双亲委派的类加载架构

    -

    启动类加载器

    首先是启动类加载器,Bootstrap Class Loader,这个类加载器负责加载放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中,也不会被加载)类库加载到虚拟机的内存中,启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把家在请求为派给引导类加载器去处理,那直接使用 null 代替即可,可以看下 java.lang.ClassLoader.getClassLoader()方法的代码片段

    -
    /**
    -     * Returns the class loader for the class.  Some implementations may use
    -     * null to represent the bootstrap class loader. This method will return
    -     * null in such implementations if this class was loaded by the bootstrap
    -     * class loader.
    -     *
    -     * <p> If a security manager is present, and the caller's class loader is
    -     * not null and the caller's class loader is not the same as or an ancestor of
    -     * the class loader for the class whose class loader is requested, then
    -     * this method calls the security manager's {@code checkPermission}
    -     * method with a {@code RuntimePermission("getClassLoader")}
    -     * permission to ensure it's ok to access the class loader for the class.
    -     *
    -     * <p>If this object
    -     * represents a primitive type or void, null is returned.
    -     *
    -     * @return  the class loader that loaded the class or interface
    -     *          represented by this object.
    -     * @throws SecurityException
    -     *    if a security manager exists and its
    -     *    {@code checkPermission} method denies
    -     *    access to the class loader for the class.
    -     * @see java.lang.ClassLoader
    -     * @see SecurityManager#checkPermission
    -     * @see java.lang.RuntimePermission
    -     */
    -    @CallerSensitive
    -    public ClassLoader getClassLoader() {
    -        ClassLoader cl = getClassLoader0();
    -        if (cl == null)
    -            return null;
    -        SecurityManager sm = System.getSecurityManager();
    -        if (sm != null) {
    -            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    -        }
    -        return cl;
    -    }
    -

    扩展类加载器

    这个类加载器是在类sun.misc.Launcher.ExtClassLoader中以 Java 代码的形式实现的,它负责在家\lib\ext 目录中,或者被 java.ext.dirs系统变量中所指定的路径中的所有类库,它其实目的是为了实现 Java 系统类库的扩展机制

    -

    应用程序类加载器

    这个类加载器是由sun.misc.Launcher.AppClassLoader实现,通过 java 代码,并且是 ClassLoader 类中的 getSystemClassLoader()方法的返回值,可以看一下代码

    -
    /**
    -     * Returns the system class loader for delegation.  This is the default
    -     * delegation parent for new <tt>ClassLoader</tt> instances, and is
    -     * typically the class loader used to start the application.
    -     *
    -     * <p> This method is first invoked early in the runtime's startup
    -     * sequence, at which point it creates the system class loader and sets it
    -     * as the context class loader of the invoking <tt>Thread</tt>.
    -     *
    -     * <p> The default system class loader is an implementation-dependent
    -     * instance of this class.
    -     *
    -     * <p> If the system property "<tt>java.system.class.loader</tt>" is defined
    -     * when this method is first invoked then the value of that property is
    -     * taken to be the name of a class that will be returned as the system
    -     * class loader.  The class is loaded using the default system class loader
    -     * and must define a public constructor that takes a single parameter of
    -     * type <tt>ClassLoader</tt> which is used as the delegation parent.  An
    -     * instance is then created using this constructor with the default system
    -     * class loader as the parameter.  The resulting class loader is defined
    -     * to be the system class loader.
    -     *
    -     * <p> If a security manager is present, and the invoker's class loader is
    -     * not <tt>null</tt> and the invoker's class loader is not the same as or
    -     * an ancestor of the system class loader, then this method invokes the
    -     * security manager's {@link
    -     * SecurityManager#checkPermission(java.security.Permission)
    -     * <tt>checkPermission</tt>} method with a {@link
    -     * RuntimePermission#RuntimePermission(String)
    -     * <tt>RuntimePermission("getClassLoader")</tt>} permission to verify
    -     * access to the system class loader.  If not, a
    -     * <tt>SecurityException</tt> will be thrown.  </p>
    -     *
    -     * @return  The system <tt>ClassLoader</tt> for delegation, or
    -     *          <tt>null</tt> if none
    -     *
    -     * @throws  SecurityException
    -     *          If a security manager exists and its <tt>checkPermission</tt>
    -     *          method doesn't allow access to the system class loader.
    -     *
    -     * @throws  IllegalStateException
    -     *          If invoked recursively during the construction of the class
    -     *          loader specified by the "<tt>java.system.class.loader</tt>"
    -     *          property.
    -     *
    -     * @throws  Error
    -     *          If the system property "<tt>java.system.class.loader</tt>"
    -     *          is defined but the named class could not be loaded, the
    -     *          provider class does not define the required constructor, or an
    -     *          exception is thrown by that constructor when it is invoked. The
    -     *          underlying cause of the error can be retrieved via the
    -     *          {@link Throwable#getCause()} method.
    -     *
    -     * @revised  1.4
    -     */
    -    @CallerSensitive
    -    public static ClassLoader getSystemClassLoader() {
    -        initSystemClassLoader();
    -        if (scl == null) {
    -            return null;
    -        }
    -        SecurityManager sm = System.getSecurityManager();
    -        if (sm != null) {
    -            checkClassLoaderPermission(scl, Reflection.getCallerClass());
    -        }
    -        return scl;
    -    }
    -    private static synchronized void initSystemClassLoader() {
    -        if (!sclSet) {
    -            if (scl != null)
    -                throw new IllegalStateException("recursive invocation");
    -            // 主要的第一步是这
    -            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
    -            if (l != null) {
    -                Throwable oops = null;
    -                // 然后是这
    -                scl = l.getClassLoader();
    -                try {
    -                    scl = AccessController.doPrivileged(
    -                        new SystemClassLoaderAction(scl));
    -                } catch (PrivilegedActionException pae) {
    -                    oops = pae.getCause();
    -                    if (oops instanceof InvocationTargetException) {
    -                        oops = oops.getCause();
    -                    }
    -                }
    -                if (oops != null) {
    -                    if (oops instanceof Error) {
    -                        throw (Error) oops;
    -                    } else {
    -                        // wrap the exception
    -                        throw new Error(oops);
    -                    }
    -                }
    -            }
    -            sclSet = true;
    -        }
    -    }
    -// 接着跟到sun.misc.Launcher#getClassLoader
    -public ClassLoader getClassLoader() {
    -        return this.loader;
    -    }
    -// 然后看到这 sun.misc.Launcher#Launcher
    -public Launcher() {
    -        Launcher.ExtClassLoader var1;
    -        try {
    -            var1 = Launcher.ExtClassLoader.getExtClassLoader();
    -        } catch (IOException var10) {
    -            throw new InternalError("Could not create extension class loader", var10);
    -        }
    -
    -        try {
    -            // 可以看到 就是 AppClassLoader
    -            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    -        } catch (IOException var9) {
    -            throw new InternalError("Could not create application class loader", var9);
    -        }
    -
    -        Thread.currentThread().setContextClassLoader(this.loader);
    -        String var2 = System.getProperty("java.security.manager");
    -        if (var2 != null) {
    -            SecurityManager var3 = null;
    -            if (!"".equals(var2) && !"default".equals(var2)) {
    -                try {
    -                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
    -                } catch (IllegalAccessException var5) {
    -                } catch (InstantiationException var6) {
    -                } catch (ClassNotFoundException var7) {
    -                } catch (ClassCastException var8) {
    -                }
    -            } else {
    -                var3 = new SecurityManager();
    -            }
    -
    -            if (var3 == null) {
    -                throw new InternalError("Could not create SecurityManager: " + var2);
    -            }
    -
    -            System.setSecurityManager(var3);
    -        }
    -
    -    }
    -

    它负责加载用户类路径(ClassPath)上所有的类库,我们可以直接在代码中使用这个类加载器,如果我们的代码中没有自定义的类在加载器,一般情况下这个就是程序中默认的类加载器

    -

    双亲委派模型


    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试家在这个类,而是把这个请求为派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的家在请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去完成加载。
    使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起举杯了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要家在这个类,最终都是委派给处于模型最顶层的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双薪委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
    可以来看下双亲委派模型的代码实现

    -
    /**
    -     * Loads the class with the specified <a href="#name">binary name</a>.  The
    -     * default implementation of this method searches for classes in the
    -     * following order:
    -     *
    -     * <ol>
    -     *
    -     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
    -     *   has already been loaded.  </p></li>
    -     *
    -     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
    -     *   on the parent class loader.  If the parent is <tt>null</tt> the class
    -     *   loader built-in to the virtual machine is used, instead.  </p></li>
    -     *
    -     *   <li><p> Invoke the {@link #findClass(String)} method to find the
    -     *   class.  </p></li>
    -     *
    -     * </ol>
    -     *
    -     * <p> If the class was found using the above steps, and the
    -     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
    -     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
    -     *
    -     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
    -     * #findClass(String)}, rather than this method.  </p>
    -     *
    -     * <p> Unless overridden, this method synchronizes on the result of
    -     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
    -     * during the entire class loading process.
    -     *
    -     * @param  name
    -     *         The <a href="#name">binary name</a> of the class
    -     *
    -     * @param  resolve
    -     *         If <tt>true</tt> then resolve the class
    -     *
    -     * @return  The resulting <tt>Class</tt> object
    -     *
    -     * @throws  ClassNotFoundException
    -     *          If the class could not be found
    -     */
    -    protected Class<?> loadClass(String name, boolean resolve)
    -        throws ClassNotFoundException
    -    {
    -        synchronized (getClassLoadingLock(name)) {
    -            // First, check if the class has already been loaded
    -            Class<?> c = findLoadedClass(name);
    -            if (c == null) {
    -                long t0 = System.nanoTime();
    -                try {
    -                    if (parent != null) {
    -                        // 委托父类加载
    -                        c = parent.loadClass(name, false);
    -                    } else {
    -                        // 使用启动类加载器
    -                        c = findBootstrapClassOrNull(name);
    -                    }
    -                } catch (ClassNotFoundException e) {
    -                    // ClassNotFoundException thrown if class not found
    -                    // from the non-null parent class loader
    -                }
    -
    -                if (c == null) {
    -                    // If still not found, then invoke findClass in order
    -                    // to find the class.
    -                    long t1 = System.nanoTime();
    -                    // 调用自己的 findClass() 方法尝试进行加载
    -                    c = findClass(name);
    +  waiting for ownable synchronizer 0x000000076f5d4360, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
    +  which is held by "mythread2"
     
    -                    // this is the defining class loader; record the stats
    -                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    -                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    -                    sun.misc.PerfCounter.getFindClasses().increment();
    -                }
    -            }
    -            if (resolve) {
    -                resolveClass(c);
    -            }
    -            return c;
    -        }
    -    }
    -

    破坏双亲委派

    关于破坏双亲委派模型,第一次是在 JDK1.2 之后引入了双亲委派模型之前,那么在那之前已经有了类加载器,所以java.lang.ClassLoader 中添加了一个 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。这个跟上面的逻辑其实类似,当父类加载失败,会调用 findClass()来完成加载;第二次是因为这个模型本身还有一些不足之处,比如 SPI 这种,所以有设计了线程下上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 java.lang.Thread#setContextClassLoader() 进行设置,然后第三种是为了追求程序动态性,这里有涉及到了 osgi 等概念,就不展开了

    +Java stack information for the threads listed above: +=================================================== +"mythread2": + at sun.misc.Unsafe.park(Native Method) + - parking to wait for <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) + at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) + at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870) + at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199) + at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209) + at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285) + at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$2.run(ThreadDumpDemoApplication.java:34) +"mythread1": + at sun.misc.Unsafe.park(Native Method) + - parking to wait for <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) + at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) + at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870) + at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199) + at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209) + at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285) + at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$1.run(ThreadDumpDemoApplication.java:22) + +Found 1 deadlock.
    +

    前面的信息其实上次就看过了,后面就可以发现有个死锁了,

    上面比较长,把主要的截出来,就是这边的,这点就很强大了。

    +

    jmap

    惯例还是看一下帮助信息

    这个相对命令比较多,不过因为现在 dump 下来我们可能会用文件模式,然后将文件下载下来使用 mat 进行分析,所以可以使用
    jmap -dump:live,format=b,file=heap.bin <pid>
    命令照着上面看的就是打印活着的对象,然后以二进制格式,文件名叫 heap.bin 然后最后就是进程 id,打印出来以后可以用 mat 打开

    这样就可以很清晰的看到应用里的各种信息,jmap 直接在命令中还可以看很多信息,比如使用jmap -histo <pid>打印对象的实例数和对象占用的内存

    jmap -finalizerinfo <pid> 打印正在等候回收的对象

    +

    小tips

    对于一些应用内存已经占满了,jstack 和 jmap 可能会连不上的情况,可以使用-F参数强制打印线程或者 dump 文件,但是要注意这两者使用的用户必须与 java 进程启动用户一致,并且使用的 jdk 也要一致

    ]]>
    Java + Thread dump + 问题排查 + 工具 Java - 类加载 - 加载 - 验证 - 准备 - 解析 - 初始化 - 链接 - 双亲委派 + JPS + JStack + JMap
    @@ -12973,6 +12993,96 @@ JNI global references: CREATE TABLE `student_time_0` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `name` varchar(200) COLLATE utf8_bin DEFAULT NULL, + `age` tinyint(3) unsigned DEFAULT NULL, + `create_time` bigint(20) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=674 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
    +

    有这样的三个表,student_time_0, student_time_1, student_time_2, 以 user_id 作为分表键,根据表数量取模作为分表依据
    这里先构造点数据,

    +
    insert into student_time (`name`, `user_id`, `age`, `create_time`) values (?, ?, ?, ?)
    +

    主要是为了保证 create_time 唯一比较好说明问题,

    +
    int i = 0;
    +try (
    +        Connection conn = dataSource.getConnection();
    +        PreparedStatement ps = conn.prepareStatement(insertSql)) {
    +    do {
    +        ps.setString(1, localName + new Random().nextInt(100));
    +        ps.setLong(2, 10086L + (new Random().nextInt(100)));
    +        ps.setInt(3, 18);
    +        ps.setLong(4, new Date().getTime());
    +
    +
    +        int result = ps.executeUpdate();
    +        LOGGER.info("current execute result: {}", result);
    +        Thread.sleep(new Random().nextInt(100));
    +        i++;
    +    } while (i <= 2000);
    +

    三个表的数据分别是 673,678,650,说明符合预期了,各个表数据不一样,接下来比如我们想要做一个这样的分页查询

    +
    select * from student_time ORDER BY create_time ASC limit 1000, 5;
    +

    student_time 对于我们使用的 sharding-jdbc 来说当然是逻辑表,首先从一无所知去想这个查询如果我们自己来处理应该是怎么做,
    首先是不是可以每个表都从 333 开始取 5 条数据,类似于下面的查询,然后进行 15 条的合并重排序获取前面的 5 条

    +
    select * from student_time_0 ORDER BY create_time ASC limit 333, 5;
    +select * from student_time_1 ORDER BY create_time ASC limit 333, 5;
    +select * from student_time_2 ORDER BY create_time ASC limit 333, 5;
    +

    忽略前面 limit 差的 1,这个结果除非三个表的分布是绝对的均匀,否则结果肯定会出现一定的偏差,以为每个表的 333 这个位置对于其他表来说都不一定是一样的,这样对于最后整体的结果,就会出现偏差
    因为一直在纠结怎么让这个更直观的表现出来,所以尝试画了个图

    黑色的框代表我从每个表里按排序从 334 到 338 的 5 条数据, 他们在每个表里都是代表了各自正确的排序值,但是对于我们想要的其实是合表后的 1001,1005 这五条,然后我们假设总的排序值位于前 1000 的分布是第 0 个表是 320 条,第 1 个表是 340 条,第 2 个表是 340 条,那么可以明显地看出来我这么查的结果简单合并肯定是不对的。
    那么 sharding-jdbc 是如何保证这个结果的呢,其实就是我在每个表里都查分页偏移量和分页大小那么多的数据,在我这个例子里就是对于 0,1,2 三个分表每个都查 1005 条数据,即使我的数据不平衡到最极端的情况,前 1005 条数据都出在某个分表中,也可以正确获得最后的结果,但是明显的问题就是大分页,数据较多,就会导致非常大的问题,即使如 sharding-jdbc 对于合并排序的优化做得比较好,也还是需要传输那么大量的数据,并且查询也耗时,那么有没有解决方案呢,应该说有两个,或者说主要是想讲后者
    第一个办法是像这种查询,如果业务上不需要进行跳页,而是只给下一页,那么我们就能把前一次的最大偏移量的 create_time 记录下来,下一页就可以拿着这个偏移量进行查询,这个比较简单易懂,就不多说了
    第二个办法是看的58 沈剑的一篇文章,尝试理解讲述一下,
    这个办法的第一步跟前面那个错误的方法或者说不准确的方法一样,先是将分页偏移量平均后在三个表里进行查询

    +
    t0
    +334 10158 nick95  18  1641548941767
    +335 10098 nick11  18  1641548941879
    +336 10167 nick51  18  1641548942089
    +337 10167 nick3 18  1641548942119
    +338 10170 nick57  18  1641548942169
    +
    +
    +t1
    +334 10105 nick98  18  1641548939071   最小
    +335 10174 nick94  18  1641548939377
    +336 10129 nick85  18  1641548939442
    +337 10141 nick84  18  1641548939480
    +338 10096 nick74  18  1641548939668
    +
    +t2
    +334 10184 nick11  18  1641548945075
    +335 10109 nick93  18  1641548945382
    +336 10181 nick41  18  1641548945583
    +337 10130 nick80  18  1641548945993
    +338 10184 nick19  18  1641548946294  最大
    +

    然后要做什么呢,其实目标比较明白,因为前面那种方法其实就是我知道了前一页的偏移量,所以可以直接当做条件来进行查询,那这里我也想着拿到这个条件,所以我将第一遍查出来的最小的 create_time 和最大的 create_time 找出来,然后再去三个表里查询,其实主要是最小值,因为我拿着最小值去查以后我就能知道这个最小值在每个表里处在什么位置,

    +
    t0
    +322 10161 nick81  18  1641548939284
    +323 10113 nick16  18  1641548939393
    +324 10110 nick56  18  1641548939577
    +325 10116 nick69  18  1641548939588
    +326 10173 nick51  18  1641548939646
    +
    +t1
    +334 10105 nick98  18  1641548939071
    +335 10174 nick94  18  1641548939377
    +336 10129 nick85  18  1641548939442
    +337 10141 nick84  18  1641548939480
    +338 10096 nick74  18  1641548939668
    +
    +t2
    +297 10136 nick28  18  1641548939161
    +298 10142 nick68  18  1641548939177
    +299 10124 nick41  18  1641548939237
    +300 10148 nick87  18  1641548939510
    +301 10169 nick23  18  1641548939715
    +

    我只贴了前五条数据,为了方便知道偏移量,每个分表都使用了自增主键,我们可以看到前一次查询的最小值分别在其他两个表里的位置分别是 322-1 和 297-1,那么对于总体来说这个时间应该是在 322 - 1 + 333 + 297 - 1 = 951,那这样子我只要对后面的数据最多每个表查 1000 - 951 + 5 = 54 条数据再进行合并排序就可以获得最终正确的结果。
    这个就是传说中的二次查询法。

    +]]>
    + + Java + + + Java + Sharding-Jdbc + +
    聊聊 RocketMQ 的 Broker 源码 /2020/07/19/%E8%81%8A%E8%81%8A-RocketMQ-%E7%9A%84-Broker-%E6%BA%90%E7%A0%81/ @@ -13498,96 +13608,6 @@ result = result Broker - - 聊聊 Sharding-Jdbc 分库分表下的分页方案 - /2022/01/09/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E4%B8%8B%E7%9A%84%E5%88%86%E9%A1%B5%E6%96%B9%E6%A1%88/ - 前面在聊 Sharding-Jdbc 的时候看到了一篇文章,关于一个分页的查询,一直比较直接的想法就是分库分表下的分页是非常不合理的,一般我们的实操方案都是分表加上 ES 搜索做分页,或者通过合表读写分离的方案,因为对于 sharding-jdbc 如果没有带分表键,查询基本都是只能在所有分表都执行一遍,然后再加上分页,基本上是分页越大后续的查询越耗资源,但是仔细的去想这个细节还是这次,就简单说说
    首先就是我的分表结构

    -
    CREATE TABLE `student_time_0` (
    -  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    -  `user_id` int(11) NOT NULL,
    -  `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
    -  `age` tinyint(3) unsigned DEFAULT NULL,
    -  `create_time` bigint(20) DEFAULT NULL,
    -  PRIMARY KEY (`id`)
    -) ENGINE=InnoDB AUTO_INCREMENT=674 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
    -

    有这样的三个表,student_time_0, student_time_1, student_time_2, 以 user_id 作为分表键,根据表数量取模作为分表依据
    这里先构造点数据,

    -
    insert into student_time (`name`, `user_id`, `age`, `create_time`) values (?, ?, ?, ?)
    -

    主要是为了保证 create_time 唯一比较好说明问题,

    -
    int i = 0;
    -try (
    -        Connection conn = dataSource.getConnection();
    -        PreparedStatement ps = conn.prepareStatement(insertSql)) {
    -    do {
    -        ps.setString(1, localName + new Random().nextInt(100));
    -        ps.setLong(2, 10086L + (new Random().nextInt(100)));
    -        ps.setInt(3, 18);
    -        ps.setLong(4, new Date().getTime());
    -
    -
    -        int result = ps.executeUpdate();
    -        LOGGER.info("current execute result: {}", result);
    -        Thread.sleep(new Random().nextInt(100));
    -        i++;
    -    } while (i <= 2000);
    -

    三个表的数据分别是 673,678,650,说明符合预期了,各个表数据不一样,接下来比如我们想要做一个这样的分页查询

    -
    select * from student_time ORDER BY create_time ASC limit 1000, 5;
    -

    student_time 对于我们使用的 sharding-jdbc 来说当然是逻辑表,首先从一无所知去想这个查询如果我们自己来处理应该是怎么做,
    首先是不是可以每个表都从 333 开始取 5 条数据,类似于下面的查询,然后进行 15 条的合并重排序获取前面的 5 条

    -
    select * from student_time_0 ORDER BY create_time ASC limit 333, 5;
    -select * from student_time_1 ORDER BY create_time ASC limit 333, 5;
    -select * from student_time_2 ORDER BY create_time ASC limit 333, 5;
    -

    忽略前面 limit 差的 1,这个结果除非三个表的分布是绝对的均匀,否则结果肯定会出现一定的偏差,以为每个表的 333 这个位置对于其他表来说都不一定是一样的,这样对于最后整体的结果,就会出现偏差
    因为一直在纠结怎么让这个更直观的表现出来,所以尝试画了个图

    黑色的框代表我从每个表里按排序从 334 到 338 的 5 条数据, 他们在每个表里都是代表了各自正确的排序值,但是对于我们想要的其实是合表后的 1001,1005 这五条,然后我们假设总的排序值位于前 1000 的分布是第 0 个表是 320 条,第 1 个表是 340 条,第 2 个表是 340 条,那么可以明显地看出来我这么查的结果简单合并肯定是不对的。
    那么 sharding-jdbc 是如何保证这个结果的呢,其实就是我在每个表里都查分页偏移量和分页大小那么多的数据,在我这个例子里就是对于 0,1,2 三个分表每个都查 1005 条数据,即使我的数据不平衡到最极端的情况,前 1005 条数据都出在某个分表中,也可以正确获得最后的结果,但是明显的问题就是大分页,数据较多,就会导致非常大的问题,即使如 sharding-jdbc 对于合并排序的优化做得比较好,也还是需要传输那么大量的数据,并且查询也耗时,那么有没有解决方案呢,应该说有两个,或者说主要是想讲后者
    第一个办法是像这种查询,如果业务上不需要进行跳页,而是只给下一页,那么我们就能把前一次的最大偏移量的 create_time 记录下来,下一页就可以拿着这个偏移量进行查询,这个比较简单易懂,就不多说了
    第二个办法是看的58 沈剑的一篇文章,尝试理解讲述一下,
    这个办法的第一步跟前面那个错误的方法或者说不准确的方法一样,先是将分页偏移量平均后在三个表里进行查询

    -
    t0
    -334 10158 nick95  18  1641548941767
    -335 10098 nick11  18  1641548941879
    -336 10167 nick51  18  1641548942089
    -337 10167 nick3 18  1641548942119
    -338 10170 nick57  18  1641548942169
    -
    -
    -t1
    -334 10105 nick98  18  1641548939071   最小
    -335 10174 nick94  18  1641548939377
    -336 10129 nick85  18  1641548939442
    -337 10141 nick84  18  1641548939480
    -338 10096 nick74  18  1641548939668
    -
    -t2
    -334 10184 nick11  18  1641548945075
    -335 10109 nick93  18  1641548945382
    -336 10181 nick41  18  1641548945583
    -337 10130 nick80  18  1641548945993
    -338 10184 nick19  18  1641548946294  最大
    -

    然后要做什么呢,其实目标比较明白,因为前面那种方法其实就是我知道了前一页的偏移量,所以可以直接当做条件来进行查询,那这里我也想着拿到这个条件,所以我将第一遍查出来的最小的 create_time 和最大的 create_time 找出来,然后再去三个表里查询,其实主要是最小值,因为我拿着最小值去查以后我就能知道这个最小值在每个表里处在什么位置,

    -
    t0
    -322 10161 nick81  18  1641548939284
    -323 10113 nick16  18  1641548939393
    -324 10110 nick56  18  1641548939577
    -325 10116 nick69  18  1641548939588
    -326 10173 nick51  18  1641548939646
    -
    -t1
    -334 10105 nick98  18  1641548939071
    -335 10174 nick94  18  1641548939377
    -336 10129 nick85  18  1641548939442
    -337 10141 nick84  18  1641548939480
    -338 10096 nick74  18  1641548939668
    -
    -t2
    -297 10136 nick28  18  1641548939161
    -298 10142 nick68  18  1641548939177
    -299 10124 nick41  18  1641548939237
    -300 10148 nick87  18  1641548939510
    -301 10169 nick23  18  1641548939715
    -

    我只贴了前五条数据,为了方便知道偏移量,每个分表都使用了自增主键,我们可以看到前一次查询的最小值分别在其他两个表里的位置分别是 322-1 和 297-1,那么对于总体来说这个时间应该是在 322 - 1 + 333 + 297 - 1 = 951,那这样子我只要对后面的数据最多每个表查 1000 - 951 + 5 = 54 条数据再进行合并排序就可以获得最终正确的结果。
    这个就是传说中的二次查询法。

    -]]>
    - - Java - - - Java - Sharding-Jdbc - -
    聊聊 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/ @@ -13689,29 +13709,8 @@ t2 Java - Java - Sharding-Jdbc - - - - 寄生虫观后感 - /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 - - - 生活 - 影评 - 寄生虫 + Java + Sharding-Jdbc
    @@ -13879,86 +13878,6 @@ void ReadView::prepare(trx_id_t id) { 幻读 - - 聊聊 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);
      -    }
      -
      -    const ids_t::value_type *p = m_ids.data();
      -
      -    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. -
    -]]>
    - - Mysql - C - 数据结构 - 源码 - Mysql - - - mysql - 数据结构 - 源码 - mvcc - read view - -
    聊聊 mysql 索引的一些细节 /2020/12/27/%E8%81%8A%E8%81%8A-mysql-%E7%B4%A2%E5%BC%95%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%86%E8%8A%82/ @@ -14056,97 +13975,56 @@ $ - 聊聊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/ - 这是个 Java 面试的高频问题,我也遇到过,以往都是觉得这类题没意思,网上一搜一大堆,也不愿意记,其实说回来,主要还是没静下心来好好去理解,今天无意中看到一个课程,基本帮我把一些疑惑的点讲清楚了,首先单例是啥意思,这个其实是有范围一说,比如我起了个Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的

    -

    饿汉模式

    public class Singleton1 {
    -    // 首先,将构造方法变成私有的
    -    private Singleton1() {};
    -    // 创建私有静态实例,这样第一次使用的时候就会进行创建
    -    private static Singleton instance = new Singleton1();
    -
    -    // 使用这个对象都是通过这个 getInstance 来获取
    -    public static Singleton1 getInstance() {
    -        return instance;
    -    }
    -    // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
    -    // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
    -    public static Date getDate(String mode) {return new Date();}
    -}
    -

    上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中getDate方法里说的问题,有时候并没有想那这个对象,但是因为我调用了这个类的静态方法,导致对象已经生成了,可能这也是饿汉模式名字的来由,不管三七二十一给你生成个单例就完事了,不管有没有用,但是这种个人觉得也没啥大问题,如果是面试的话最好说出来它的缺点

    -

    饱汉模式

    public class Singleton2 {
    -    // 首先,也是先堵死 new Singleton() 这条路,将构造方法变成私有
    -    private Singleton2() {}
    -    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
    -    private static volatile Singleton2 instance = null;
    -
    -    private int m = 9;
    +    聊聊一次 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)
     
    -    public static Singleton getInstance() {
    -        if (instance == null) {
    -            // 加锁
    -            synchronized (Singleton2.class) {
    -                // 这一次判断也是必须的,不然会有并发问题
    -                if (instance == null) {
    -                    instance = new Singleton2();
    -                }
    -            }
    -        }
    -        return instance;
    -    }
    -}
    -

    这里容易错的有三点,理解了其实就比较好记了

    -

    第一点,为啥不在 getInstance 上整个代码块加 synchronized,这个其实比较容易理解,就是锁的力度太大,性能太差了,这点其实也要去理解,可以举个夸张的例子,比如我一个电商的服务,如果为了避免一个人的订单出现问题,是不是可以从请求入口就把他锁住,到请求结束释放,那么里面做的事情都有保障,然而这显然不可能,因为我们想要这种竞态条件抢占资源的时间尽量减少,防止其他线程等待。
    第二点,为啥synchronized之已经检查了 instance == null,还要在里面再检查一次,这个有个术语,叫 double check lock,但是为啥要这么做呢,其实很简单,想象当有两个线程,都过了第一步为空判断,这个时候只有一个线程能拿到这个锁,另一个线程就等待了,如果不再判断一次,那么第一个线程新建完对象释放锁之后,第二个线程又能拿到锁,再去创建一个对象。
    第三点,为啥要volatile关键字,原先对它的理解是它修饰的变量在 JMM 中能及时将变量值写到主存中,但是它还有个很重要的作用,就是防止指令重排序,instance = new Singleton();这行代码其实在底层是分成三条指令执行的,第一条是在堆上申请了一块内存放这个对象,但是对象的字段啥的都还是默认值,第二条是设置对象的值,比如上面的 m 是 9,然后第三条是将这个对象和虚拟机栈上的指针建立引用关联,那么如果我不用volatile关键字,这三条指令就有可能出现重排,比如变成了 1-3-2 这种顺序,当执行完第二步时,有个线程来访问这个对象了,先判断是不是空,发现不是空的,就拿去直接用了,是不是就出现问题了,所以这个volatile也是不可缺少的

    -

    嵌套类

    public class Singleton3 {
    +  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
     
    -    private Singleton3() {}
    -    // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
    -    private static class Holder {
    -        private static Singleton3 instance = new Singleton3();
    -    }
    -    public static Singleton3 getInstance() {
    -        return Holder.instance;
    -    }
    -}
    -

    这个我个人感觉是饿汉模式的升级版,可以在调用getInstance的时候去实例化对象,也是比较推荐的

    -

    枚举单例

    public enum Singleton {
    -    INSTANCE;
    -    
    -    public void doSomething(){
    -        //todo doSomething
    -    }
    -}
    -

    枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。

    -]]>
    - - Java - Design Patterns - Singleton - - - 设计模式 - Design Patterns - 单例 - Singleton - - - - 关于读书打卡与分享 - /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/ - 最近群里大佬发起了一个读书打卡活动,需要每天读一会书,在群里打卡分享感悟,争取一个月能读完一本书,说实话一天十分钟的读书时间倒是问题不大,不过每天都要打卡,而且一个月要读完一本书,其实难度还是有点大的,不过也想试试看。
    之前某某老大给自己立了个 flag,说要读一百本书,这对我来说挺难实现的,一则我也不喜欢书只读一小半,二则感觉对于喜欢看的内容范围还是比较有限制,可能也算是比较矫情,不爱追热门的各类东西,因为往往会有一些跟大众不一致的观点看法,显得格格不入。所以还是这个打卡活动可能会比较适合我,书是人类进步的阶梯。
    到现在是打卡了三天了,读的主要是白岩松的《幸福了吗》,对于白岩松,我们这一代人是比较熟悉,并且整体印象比较不错的一个央视主持人,从《焦点访谈》开始,到疫情期间的各类一线节目,可能对我来说是个三观比较正,敢于说一些真话的主持人,这中间其实是有个空档期,没怎么看电视,也不太关注了,只是在疫情期间的节目,还是一如既往地给人一种可靠的感觉,正好有一次偶然微信读书推荐了白岩松的这本书,就看了一部分,正好这次继续往下看,因为相对来讲不会很晦涩,并且从这位知名央视主持人的角度分享他的过往和想法看法,还是比较有意思的。
    从对汶川地震,08 年奥运等往事的回忆和一些细节的呈现,也让我了解比较多当时所不知道的,特别是汶川地震,那时的我还在读高中,真的是看着电视,作为“猛男”都忍不住泪目了,共和国之殇,多难兴邦,但是这对于当事人来说,都是一场醒不过来的噩梦。
    然后是对于足球的热爱,其实光这个就能掰扯很多,因为我不爱足球,只爱篮球,其中原因有的没的也挺多可以说的,但是看了他的书,才能比较深入的了解一个足球迷,对足球,对中国足球,对世界杯,对阿根廷的感情。
    接下去还是想能继续坚持下去,加油!

    + cd $last_dir +}
    +

    对应自己的版本改改版本号就可以了,非常好用。

    ]]>
    - 生活 - 读后感 - 白岩松 - 幸福了吗 + Mac + PHP + Homebrew + PHP + icu4c - 读后感 - 读书 - 打卡 - 幸福了吗 - 足球 + Mac + PHP + Homebrew + icu4c + zsh
    @@ -14500,56 +14378,78 @@ $ - 聊聊一次 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)
    +    聊聊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/
    +    这是个 Java 面试的高频问题,我也遇到过,以往都是觉得这类题没意思,网上一搜一大堆,也不愿意记,其实说回来,主要还是没静下心来好好去理解,今天无意中看到一个课程,基本帮我把一些疑惑的点讲清楚了,首先单例是啥意思,这个其实是有范围一说,比如我起了个Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的

    +

    饿汉模式

    public class Singleton1 {
    +    // 首先,将构造方法变成私有的
    +    private Singleton1() {};
    +    // 创建私有静态实例,这样第一次使用的时候就会进行创建
    +    private static Singleton instance = new Singleton1();
    +
    +    // 使用这个对象都是通过这个 getInstance 来获取
    +    public static Singleton1 getInstance() {
    +        return instance;
    +    }
    +    // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
    +    // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
    +    public static Date getDate(String mode) {return new Date();}
    +}
    +

    上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中getDate方法里说的问题,有时候并没有想那这个对象,但是因为我调用了这个类的静态方法,导致对象已经生成了,可能这也是饿汉模式名字的来由,不管三七二十一给你生成个单例就完事了,不管有没有用,但是这种个人觉得也没啥大问题,如果是面试的话最好说出来它的缺点

    +

    饱汉模式

    public class Singleton2 {
    +    // 首先,也是先堵死 new Singleton() 这条路,将构造方法变成私有
    +    private Singleton2() {}
    +    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
    +    private static volatile Singleton2 instance = null;
     
    -  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
    +    private int m = 9;
     
    -  cd $last_dir
    -}
    -

    对应自己的版本改改版本号就可以了,非常好用。

    + public static Singleton getInstance() { + if (instance == null) { + // 加锁 + synchronized (Singleton2.class) { + // 这一次判断也是必须的,不然会有并发问题 + if (instance == null) { + instance = new Singleton2(); + } + } + } + return instance; + } +}
    +

    这里容易错的有三点,理解了其实就比较好记了

    +

    第一点,为啥不在 getInstance 上整个代码块加 synchronized,这个其实比较容易理解,就是锁的力度太大,性能太差了,这点其实也要去理解,可以举个夸张的例子,比如我一个电商的服务,如果为了避免一个人的订单出现问题,是不是可以从请求入口就把他锁住,到请求结束释放,那么里面做的事情都有保障,然而这显然不可能,因为我们想要这种竞态条件抢占资源的时间尽量减少,防止其他线程等待。
    第二点,为啥synchronized之已经检查了 instance == null,还要在里面再检查一次,这个有个术语,叫 double check lock,但是为啥要这么做呢,其实很简单,想象当有两个线程,都过了第一步为空判断,这个时候只有一个线程能拿到这个锁,另一个线程就等待了,如果不再判断一次,那么第一个线程新建完对象释放锁之后,第二个线程又能拿到锁,再去创建一个对象。
    第三点,为啥要volatile关键字,原先对它的理解是它修饰的变量在 JMM 中能及时将变量值写到主存中,但是它还有个很重要的作用,就是防止指令重排序,instance = new Singleton();这行代码其实在底层是分成三条指令执行的,第一条是在堆上申请了一块内存放这个对象,但是对象的字段啥的都还是默认值,第二条是设置对象的值,比如上面的 m 是 9,然后第三条是将这个对象和虚拟机栈上的指针建立引用关联,那么如果我不用volatile关键字,这三条指令就有可能出现重排,比如变成了 1-3-2 这种顺序,当执行完第二步时,有个线程来访问这个对象了,先判断是不是空,发现不是空的,就拿去直接用了,是不是就出现问题了,所以这个volatile也是不可缺少的

    +

    嵌套类

    public class Singleton3 {
    +
    +    private Singleton3() {}
    +    // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
    +    private static class Holder {
    +        private static Singleton3 instance = new Singleton3();
    +    }
    +    public static Singleton3 getInstance() {
    +        return Holder.instance;
    +    }
    +}
    +

    这个我个人感觉是饿汉模式的升级版,可以在调用getInstance的时候去实例化对象,也是比较推荐的

    +

    枚举单例

    public enum Singleton {
    +    INSTANCE;
    +    
    +    public void doSomething(){
    +        //todo doSomething
    +    }
    +}
    +

    枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。

    ]]>
    - Mac - PHP - Homebrew - PHP - icu4c + Java + Design Patterns + Singleton - Mac - PHP - Homebrew - icu4c - zsh + 设计模式 + Design Patterns + 单例 + Singleton
    @@ -14718,6 +14618,86 @@ $ 电瓶车 + + 聊聊 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);
      +    }
      +
      +    const ids_t::value_type *p = m_ids.data();
      +
      +    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. +
    +]]>
    + + Mysql + C + 数据结构 + 源码 + Mysql + + + mysql + 数据结构 + 源码 + mvcc + read view + +
    聊聊我刚学会的应用诊断方法 /2020/05/22/%E8%81%8A%E8%81%8A%E6%88%91%E5%88%9A%E5%AD%A6%E4%BC%9A%E7%9A%84%E5%BA%94%E7%94%A8%E8%AF%8A%E6%96%AD%E6%96%B9%E6%B3%95/ @@ -14818,6 +14798,23 @@ $ 骑车 + + 聊聊最近平淡的生活之看看老剧 + /2021/11/21/%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%E7%9C%8B%E8%80%81%E5%89%A7/ + 最近因为也没什么好看的新剧和综艺所以就看看一些以前看过的老剧,我是个非常念旧的人吧,很多剧都会反反复复地看,一方面之前看过觉得好看的的确是一直记着,还有就是平时工作完了回来就想能放松下,剧情太纠结的,太烧脑的都不喜欢,也就是我常挂在口头的不喜欢看费脑子的剧,跟我不喜欢狼人杀的原因也类似。

    +

    前面其实是看的太阳的后裔,跟 LD 一起看的,之前其实算是看过一点,但是没有看的很完整,并且很多剧情也忘了,只是这个我我可能看得更少一点,因为最开始的时候觉得男主应该是男二,可能对长得这样的男主并且是这样的人设有点失望,感觉不是特别像个特种兵,但是由于本来也比较火,而且 LD 比较喜欢就从这个开始看了,有两个点是比较想说的

    +

    韩剧虽然被吐槽的很多,但是很多剧的质量,情节把控还是优于目前非常多国内剧的,相对来说剧情发展的前后承接不是那么硬凹出来的,而且人设都立得住,这个是非常重要的,很多国内剧怎么说呢,就是当爹的看起来就比儿子没大几岁,三四十岁的人去演一个十岁出头的小姑娘,除非容貌异常,比如刘晓庆这种,不然就会觉得导演在把我们观众当傻子。瞬间就没有想看下去的欲望了。

    +

    再一点就是情节是大众都能接受度比较高的,现在有很多普遍会找一些新奇的视角,比如卖腐,想某某令,两部都叫某某令,这其实是一个点,延伸出去就是跟前面说的一点有点类似,xx 老祖,人看着就二三十,叫 xx 老祖,(喜欢的人轻喷哈)然后名字有一堆,同一个人物一会叫这个名字,一会又叫另一个名字,然后一堆死表情。

    +

    因为今天有个特殊的事情发生,所以简短的写(shui)一篇了

    +]]>
    + + 生活 + + + 生活 + 看剧 + +
    聊聊最近平淡的生活之《花束般的恋爱》观后感 /2021/12/31/%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%E3%80%8A%E8%8A%B1%E6%9D%9F%E8%88%AC%E7%9A%84%E6%81%8B%E7%88%B1%E3%80%8B%E8%A7%82%E5%90%8E%E6%84%9F/ @@ -14846,20 +14843,19 @@ $ - 聊聊最近平淡的生活之看看老剧 - /2021/11/21/%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%E7%9C%8B%E8%80%81%E5%89%A7/ - 最近因为也没什么好看的新剧和综艺所以就看看一些以前看过的老剧,我是个非常念旧的人吧,很多剧都会反反复复地看,一方面之前看过觉得好看的的确是一直记着,还有就是平时工作完了回来就想能放松下,剧情太纠结的,太烧脑的都不喜欢,也就是我常挂在口头的不喜欢看费脑子的剧,跟我不喜欢狼人杀的原因也类似。

    -

    前面其实是看的太阳的后裔,跟 LD 一起看的,之前其实算是看过一点,但是没有看的很完整,并且很多剧情也忘了,只是这个我我可能看得更少一点,因为最开始的时候觉得男主应该是男二,可能对长得这样的男主并且是这样的人设有点失望,感觉不是特别像个特种兵,但是由于本来也比较火,而且 LD 比较喜欢就从这个开始看了,有两个点是比较想说的

    -

    韩剧虽然被吐槽的很多,但是很多剧的质量,情节把控还是优于目前非常多国内剧的,相对来说剧情发展的前后承接不是那么硬凹出来的,而且人设都立得住,这个是非常重要的,很多国内剧怎么说呢,就是当爹的看起来就比儿子没大几岁,三四十岁的人去演一个十岁出头的小姑娘,除非容貌异常,比如刘晓庆这种,不然就会觉得导演在把我们观众当傻子。瞬间就没有想看下去的欲望了。

    -

    再一点就是情节是大众都能接受度比较高的,现在有很多普遍会找一些新奇的视角,比如卖腐,想某某令,两部都叫某某令,这其实是一个点,延伸出去就是跟前面说的一点有点类似,xx 老祖,人看着就二三十,叫 xx 老祖,(喜欢的人轻喷哈)然后名字有一堆,同一个人物一会叫这个名字,一会又叫另一个名字,然后一堆死表情。

    -

    因为今天有个特殊的事情发生,所以简短的写(shui)一篇了

    + 聊聊这次换车牌及其他 + /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,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。

    ]]>
    生活 生活 - 看剧 + 换车牌
    @@ -14881,22 +14877,6 @@ $ 修电脑的 - - 聊聊这次换车牌及其他 - /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,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。

    -]]>
    - - 生活 - - - 生活 - 换车牌 - -
    聊聊那些加塞狗 /2021/01/17/%E8%81%8A%E8%81%8A%E9%82%A3%E4%BA%9B%E5%8A%A0%E5%A1%9E%E7%8B%97/ @@ -15045,6 +15025,22 @@ server.6=192.168< zookeeper + + 重看了下《蛮荒记》说说感受 + /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 才是最坏的人,当然这个坏也不是严格的二分法,只是我觉得最让我觉得负面的人物,这些人可能看起来情景出现的不多,只是说了很少的话,做了很少的事,但是在我看来却做了最大的恶。

    +]]>
    + + 生活 + + + 生活 + 看书 + +
    这周末我又在老丈人家打了天小工 /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/ @@ -15064,22 +15060,6 @@ server.6=192.168< 干活 - - 重看了下《蛮荒记》说说感受 - /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/ @@ -15116,6 +15096,19 @@ server.6=192.168< 生活 + + 难得的大扫除 + /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/ + 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
    大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
    这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 LD 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。

    +]]>
    + + 生活 + + + 生活 + 大扫除 + +
    闲话篇-路遇神逻辑骑车带娃爹 /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/ @@ -15132,16 +15125,23 @@ server.6=192.168< - 难得的大扫除 - /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/ - 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
    大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
    这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 LD 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。

    + 在老丈人家的小工记五 + /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的钢筋,需要割断,这个割断也是很有技巧,钢筋本身在里面是受到挤压的,直接用切割的,到快断掉的时候就会崩一下,非常危险,还是老丈人比较有经验,要留一点点,然后直接用榔头敲断就好了,本来以为这个是最难的了,结果下面是一块非常大的青基石,而且也是石头跟石头挤一块,边上一点点打钻有点杯水车薪,后来是用那种螺旋的钻,钻四个洞,相对位置大概是个长方形,这样子把中间这个长方形钻出来就比较容易地能拿出来了,后面的也容易搞出来了,后面的其实难度不是特别大了,主要是地沟打好之后得看看高低是不是符合要求的,不能本来是往外排水的反而外面高,这个怎么看就又很有技巧了,一般在地上的只要侧着看一下就好了,考究点就用下水平尺,但是在地下的,不用水平尺,其实可以借助于地沟里正要放进去的水管,放点水进去,看水往哪流就行了,铺好水管后,就剩填埋的活了,不是太麻烦了,那天真的是累到了,打那个混凝土的时候我真的是把我整个人压上去了,不过也挺爽的,有点把平时无处发泄的蛮力发泄出去了。

    ]]>
    生活 + 运动 + 跑步 + 干活 生活 - 大扫除 + 小技巧 + 运动 + 减肥 + 跑步 + 干活
    diff --git a/sitemap.xml b/sitemap.xml index 9a40b30543..888ada2abf 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1334,7 +1334,7 @@ - https://nicksxs.me/2019/12/10/Redis-Part-1/ + 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 @@ -1343,7 +1343,7 @@ - https://nicksxs.me/2015/03/11/Reverse-Bits/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 @@ -1352,7 +1352,7 @@ - https://nicksxs.me/2015/03/13/Reverse-Integer/ + https://nicksxs.me/2015/03/11/Reverse-Bits/ 2020-01-12 @@ -1361,7 +1361,7 @@ - https://nicksxs.me/2015/01/14/Two-Sum/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 @@ -1379,7 +1379,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/2015/01/14/Two-Sum/ 2020-01-12 @@ -1766,14 +1766,7 @@ - https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ - 2022-08-23 - weekly - 0.2 - - - - https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ + https://nicksxs.me/tags/c/ 2022-08-23 weekly 0.2 @@ -1835,13 +1828,6 @@ 0.2 - - https://nicksxs.me/tags/c/ - 2022-08-23 - weekly - 0.2 - - https://nicksxs.me/tags/Apollo/ 2022-08-23 @@ -1913,70 +1899,70 @@ - https://nicksxs.me/tags/Dubbo/ + https://nicksxs.me/tags/Filter/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/RPC/ + https://nicksxs.me/tags/Interceptor/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ + https://nicksxs.me/tags/AOP/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Filter/ + https://nicksxs.me/tags/Spring/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Interceptor/ + https://nicksxs.me/tags/Tomcat/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/AOP/ + https://nicksxs.me/tags/Servlet/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Spring/ + https://nicksxs.me/tags/Web/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Tomcat/ + https://nicksxs.me/tags/Dubbo/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Servlet/ + https://nicksxs.me/tags/RPC/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Web/ + https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ 2022-08-23 weekly 0.2 @@ -2039,14 +2025,14 @@ - https://nicksxs.me/tags/Print-FooBar-Alternately/ + https://nicksxs.me/tags/DP/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/DP/ + https://nicksxs.me/tags/Print-FooBar-Alternately/ 2022-08-23 weekly 0.2 @@ -2060,35 +2046,35 @@ - https://nicksxs.me/tags/stack/ + https://nicksxs.me/tags/3Sum-Closest/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/min-stack/ + https://nicksxs.me/tags/stack/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ + https://nicksxs.me/tags/min-stack/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/leetcode-155/ + https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/3Sum-Closest/ + https://nicksxs.me/tags/leetcode-155/ 2022-08-23 weekly 0.2 @@ -2109,14 +2095,14 @@ - https://nicksxs.me/tags/string/ + https://nicksxs.me/tags/First-Bad-Version/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/First-Bad-Version/ + https://nicksxs.me/tags/string/ 2022-08-23 weekly 0.2 @@ -2137,49 +2123,49 @@ - https://nicksxs.me/tags/Rotate-Image/ + https://nicksxs.me/tags/dp/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/ + https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/dp/ + https://nicksxs.me/tags/Trapping-Rain-Water/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ + https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Trapping-Rain-Water/ + https://nicksxs.me/tags/Leetcode-42/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ + https://nicksxs.me/tags/Rotate-Image/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Leetcode-42/ + https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/ 2022-08-23 weekly 0.2 @@ -2213,6 +2199,13 @@ 0.2 + + https://nicksxs.me/tags/mfc/ + 2022-08-23 + weekly + 0.2 + + https://nicksxs.me/tags/Maven/ 2022-08-23 @@ -2263,49 +2256,42 @@ - https://nicksxs.me/tags/mfc/ - 2022-08-23 - weekly - 0.2 - - - - https://nicksxs.me/tags/Docker/ + https://nicksxs.me/tags/docker/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/namespace/ + https://nicksxs.me/tags/mysql/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/cgroup/ + https://nicksxs.me/tags/Docker/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Dockerfile/ + https://nicksxs.me/tags/namespace/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/docker/ + https://nicksxs.me/tags/cgroup/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/mysql/ + https://nicksxs.me/tags/Dockerfile/ 2022-08-23 weekly 0.2 @@ -2368,63 +2354,63 @@ - https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/openresty/ + https://nicksxs.me/tags/nginx/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/nginx/ + https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/php/ + https://nicksxs.me/tags/openresty/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/mq/ + https://nicksxs.me/tags/php/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/im/ + https://nicksxs.me/tags/mq/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/redis/ + https://nicksxs.me/tags/im/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ + https://nicksxs.me/tags/redis/ 2022-08-23 weekly 0.2 @@ -2521,6 +2507,20 @@ 0.2 + + https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ + 2022-08-23 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ + 2022-08-23 + weekly + 0.2 + + https://nicksxs.me/tags/spark/ 2022-08-23 @@ -2543,28 +2543,28 @@ - https://nicksxs.me/tags/websocket/ + https://nicksxs.me/tags/WordPress/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/swoole/ + https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/WordPress/ + https://nicksxs.me/tags/websocket/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/tags/swoole/ 2022-08-23 weekly 0.2 @@ -2634,35 +2634,35 @@ - https://nicksxs.me/tags/ssh/ + https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ + https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ + https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ + https://nicksxs.me/tags/ssh/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ + https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ 2022-08-23 weekly 0.2 @@ -2753,14 +2753,28 @@ - https://nicksxs.me/tags/git/ + https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/scp/ + https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + 2022-08-23 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ + 2022-08-23 + weekly + 0.2 + + + + https://nicksxs.me/tags/git/ 2022-08-23 weekly 0.2 @@ -2795,7 +2809,21 @@ - https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/ + https://nicksxs.me/tags/scp/ + 2022-08-23 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E5%BD%B1%E8%AF%84/ + 2022-08-23 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E5%AF%84%E7%94%9F%E8%99%AB/ 2022-08-23 weekly 0.2 @@ -2851,14 +2879,7 @@ - https://nicksxs.me/tags/%E5%BD%B1%E8%AF%84/ - 2022-08-23 - weekly - 0.2 - - - - https://nicksxs.me/tags/DefaultMQPushConsumer/ + https://nicksxs.me/tags/NameServer/ 2022-08-23 weekly 0.2 @@ -2872,7 +2893,7 @@ - https://nicksxs.me/tags/NameServer/ + https://nicksxs.me/tags/DefaultMQPushConsumer/ 2022-08-23 weekly 0.2 @@ -2900,28 +2921,28 @@ - https://nicksxs.me/tags/Druid/ + https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ 2022-08-23 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/ + https://nicksxs.me/tags/AutoConfiguration/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ + https://nicksxs.me/tags/Druid/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/AutoConfiguration/ + 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-08-23 weekly 0.2 @@ -2962,6 +2983,13 @@ 0.2 + + https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/ + 2022-08-23 + weekly + 0.2 + + https://nicksxs.me/tags/SPI/ 2022-08-23 @@ -3026,77 +3054,77 @@ - https://nicksxs.me/tags/JPS/ + https://nicksxs.me/tags/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/JStack/ + https://nicksxs.me/tags/%E5%8A%A0%E8%BD%BD/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/JMap/ + https://nicksxs.me/tags/%E9%AA%8C%E8%AF%81/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ + https://nicksxs.me/tags/%E5%87%86%E5%A4%87/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%8A%A0%E8%BD%BD/ + https://nicksxs.me/tags/%E8%A7%A3%E6%9E%90/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E9%AA%8C%E8%AF%81/ + https://nicksxs.me/tags/%E5%88%9D%E5%A7%8B%E5%8C%96/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%87%86%E5%A4%87/ + https://nicksxs.me/tags/%E9%93%BE%E6%8E%A5/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E8%A7%A3%E6%9E%90/ + https://nicksxs.me/tags/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%88%9D%E5%A7%8B%E5%8C%96/ + https://nicksxs.me/tags/JPS/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E9%93%BE%E6%8E%A5/ + https://nicksxs.me/tags/JStack/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE/ + https://nicksxs.me/tags/JMap/ 2022-08-23 weekly 0.2 @@ -3109,13 +3137,6 @@ 0.2 - - https://nicksxs.me/tags/Broker/ - 2022-08-23 - weekly - 0.2 - - https://nicksxs.me/tags/Sharding-Jdbc/ 2022-08-23 @@ -3124,7 +3145,7 @@ - https://nicksxs.me/tags/%E5%AF%84%E7%94%9F%E8%99%AB/ + https://nicksxs.me/tags/Broker/ 2022-08-23 weekly 0.2 @@ -3278,84 +3299,63 @@ - https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ - 2022-08-23 - weekly - 0.2 - - - - https://nicksxs.me/tags/Design-Patterns/ - 2022-08-23 - weekly - 0.2 - - - - https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/ - 2022-08-23 - weekly - 0.2 - - - - https://nicksxs.me/tags/Singleton/ + https://nicksxs.me/tags/Mac/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ + https://nicksxs.me/tags/PHP/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + https://nicksxs.me/tags/Homebrew/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ + https://nicksxs.me/tags/icu4c/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Mac/ + https://nicksxs.me/tags/zsh/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/PHP/ + https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/Homebrew/ + https://nicksxs.me/tags/Design-Patterns/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/icu4c/ + https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/zsh/ + https://nicksxs.me/tags/Singleton/ 2022-08-23 weekly 0.2 @@ -3551,35 +3551,35 @@ - https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ + https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ + https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ + https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ + https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ 2022-08-23 weekly 0.2 - https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ + https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ 2022-08-23 weekly 0.2 @@ -3665,14 +3665,14 @@ - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + https://nicksxs.me/categories/Java/leetcode/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/ + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ 2022-08-23 weekly 0.2 @@ -3699,13 +3699,6 @@ 0.2 - - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ - 2022-08-23 - 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-08-23 @@ -3721,21 +3714,21 @@ - https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ + https://nicksxs.me/categories/Linked-List/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Linked-List/ + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ 2022-08-23 weekly 0.2 @@ -3749,7 +3742,7 @@ - https://nicksxs.me/categories/leetcode/java/ + https://nicksxs.me/categories/C/ 2022-08-23 weekly 0.2 @@ -3763,21 +3756,21 @@ - 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/ + https://nicksxs.me/categories/leetcode/java/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ + 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-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/ + https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ 2022-08-23 weekly 0.2 @@ -3791,14 +3784,14 @@ - https://nicksxs.me/categories/Filter/ + https://nicksxs.me/categories/Java/Dubbo/ 2022-08-23 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/ + https://nicksxs.me/categories/Filter/ 2022-08-23 weekly 0.2 @@ -3826,35 +3819,35 @@ - https://nicksxs.me/categories/leetcode/java/Linked-List/ + https://nicksxs.me/categories/Java/Apollo/value/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/ + https://nicksxs.me/categories/linked-list/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/linked-list/ + https://nicksxs.me/categories/leetcode/java/Binary-Tree/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/Apollo/value/ + https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ + https://nicksxs.me/categories/leetcode/java/Linked-List/ 2022-08-23 weekly 0.2 @@ -3902,6 +3895,13 @@ 0.2 + + https://nicksxs.me/categories/docker/ + 2022-08-23 + weekly + 0.2 + + https://nicksxs.me/categories/Docker/ 2022-08-23 @@ -3910,7 +3910,7 @@ - https://nicksxs.me/categories/docker/ + https://nicksxs.me/categories/leetcode/java/DP/ 2022-08-23 weekly 0.2 @@ -3924,7 +3924,7 @@ - https://nicksxs.me/categories/leetcode/java/DP/ + https://nicksxs.me/categories/leetcode/java/stack/ 2022-08-23 weekly 0.2 @@ -3938,21 +3938,21 @@ - https://nicksxs.me/categories/leetcode/java/stack/ + https://nicksxs.me/categories/nginx/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/nginx/ + https://nicksxs.me/categories/php/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/php/ + https://nicksxs.me/categories/leetcode/java/linked-list/ 2022-08-23 weekly 0.2 @@ -3973,14 +3973,14 @@ - https://nicksxs.me/categories/leetcode/java/linked-list/ + https://nicksxs.me/categories/leetcode/java/string/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/string/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ 2022-08-23 weekly 0.2 @@ -3994,7 +3994,14 @@ - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/ + 2022-08-23 + weekly + 0.2 + + + + https://nicksxs.me/categories/Spring/ 2022-08-23 weekly 0.2 @@ -4015,7 +4022,7 @@ - https://nicksxs.me/categories/Spring/ + https://nicksxs.me/categories/Redis/Distributed-Lock/ 2022-08-23 weekly 0.2 @@ -4029,14 +4036,14 @@ - https://nicksxs.me/categories/Redis/Distributed-Lock/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ + https://nicksxs.me/categories/MQ/ 2022-08-23 weekly 0.2 @@ -4050,7 +4057,7 @@ - https://nicksxs.me/categories/MQ/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ 2022-08-23 weekly 0.2 @@ -4071,14 +4078,14 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ 2022-08-23 weekly 0.2 @@ -4099,70 +4106,70 @@ - https://nicksxs.me/categories/Mysql/ + https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/Mybatis/Mysql/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/SpringBoot/ + https://nicksxs.me/categories/Mysql/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/RPC/ + https://nicksxs.me/categories/Mybatis/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mybatis/ + https://nicksxs.me/categories/Java/SpringBoot/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Dubbo-RPC/ + https://nicksxs.me/categories/Java/Mybatis/Mysql/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ + https://nicksxs.me/categories/Java/Dubbo/RPC/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Thread-dump/ + https://nicksxs.me/categories/Dubbo-RPC/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ + https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + https://nicksxs.me/categories/Thread-dump/ 2022-08-23 weekly 0.2 @@ -4183,21 +4190,14 @@ - https://nicksxs.me/categories/Java/Design-Patterns/ - 2022-08-23 - 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/ + https://nicksxs.me/categories/Mac/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mac/ + https://nicksxs.me/categories/Java/Design-Patterns/ 2022-08-23 weekly 0.2 @@ -4232,14 +4232,14 @@ - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ 2022-08-23 weekly 0.2 @@ -4260,14 +4260,14 @@ - https://nicksxs.me/categories/Java/gc/jvm/ + https://nicksxs.me/categories/C/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/Java/gc/jvm/ 2022-08-23 weekly 0.2 @@ -4281,28 +4281,28 @@ - https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/ 2022-08-23 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/ + https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 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-08-23 weekly 0.2 - https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-08-23 weekly 0.2 @@ -4316,105 +4316,105 @@ - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ + https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Dubbo/ + https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ + https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ + https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ + https://nicksxs.me/categories/Dubbo/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ + https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/ + https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Java/Singleton/ + https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/ 2022-08-23 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/ + https://nicksxs.me/categories/Mac/PHP/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/Mac/PHP/ + https://nicksxs.me/categories/Java/Singleton/ 2022-08-23 weekly 0.2 @@ -4498,14 +4498,14 @@ - https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ + https://nicksxs.me/categories/Dubbo/SPI/ 2022-08-23 weekly 0.2 - https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/ + https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ 2022-08-23 weekly 0.2 @@ -4518,6 +4518,13 @@ 0.2 + + https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/ + 2022-08-23 + weekly + 0.2 + + https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/ 2022-08-23 @@ -4581,13 +4588,6 @@ 0.2 - - https://nicksxs.me/categories/Dubbo/SPI/ - 2022-08-23 - weekly - 0.2 - - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/ 2022-08-23 diff --git a/tags/DP/index.html b/tags/DP/index.html index aadf3534af..4e071133a1 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%
    \ 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%
    squs":"disqus"}} \ No newline at end of file diff --git a/tags/JVM/index.html b/tags/JVM/index.html index 595ceba23c..02628d3914 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%
    \ 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%
    rity":true,"url":"https://nicksxs.me/tags/JVM/"} \ No newline at end of file diff --git a/tags/docker/index.html b/tags/docker/index.html index 81daac3de1..19bf2344de 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -1 +1 @@ -标签: docker | Nicksxs's Blog

    Nicksxs's Blog

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

    0%

    docker 标签

    2016
    \ No newline at end of file +标签: Docker | 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/java/page/2/index.html b/tags/java/page/2/index.html index 581fe24ed3..8d95f049bf 100644 --- a/tags/java/page/2/index.html +++ b/tags/java/page/2/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%
    \ 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/tags/java/page/7/index.html b/tags/java/page/7/index.html index 5361ab37ea..ff2914066e 100644 --- a/tags/java/page/7/index.html +++ b/tags/java/page/7/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%

    java 标签

    2019
    \ 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%

    java 标签

    2019
    html> \ No newline at end of file diff --git a/tags/linked-list/index.html b/tags/linked-list/index.html index dc16c8dd91..6b735af962 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
    \ 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
    n-analytics.js"> \ No newline at end of file diff --git a/tags/mysql/index.html b/tags/mysql/index.html index 05384ccc13..3be2d2f6a5 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%
    \ 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%
    .js" integrity="sha256-vXZMYLEqsROAXkEw93GGIvaB2ab+QW6w3+1ahD9nXXA=" crossorigin="anonymous"> \ No newline at end of file diff --git a/tags/php/index.html b/tags/php/index.html index 8dd4cbec06..5c48b5d5d3 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%
    \ 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%
    ,"home":false,"archive":false,"delay":true,"timeout":3000,"priority":true,"url":"https://nicksxs.me/tags/php/"} \ No newline at end of file