diff --git a/2020/04/26/聊聊-mysql-的-MVCC/index.html b/2020/04/26/聊聊-mysql-的-MVCC/index.html index 22e1630c0f..4f14ed9ac2 100644 --- a/2020/04/26/聊聊-mysql-的-MVCC/index.html +++ b/2020/04/26/聊聊-mysql-的-MVCC/index.html @@ -1,4 +1,4 @@ -聊聊 mysql 的 MVCC | Nicksxs's Blog

Nicksxs's Blog

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

聊聊 mysql 的 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;
+聊聊 mysql 的 MVCC | Nicksxs's Blog

Nicksxs's Blog

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

聊聊 mysql 的 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 */
diff --git a/2020/05/02/聊聊-mysql-的-MVCC-续篇/index.html b/2020/05/02/聊聊-mysql-的-MVCC-续篇/index.html
index 040eac74b0..01e0a6360e 100644
--- a/2020/05/02/聊聊-mysql-的-MVCC-续篇/index.html
+++ b/2020/05/02/聊聊-mysql-的-MVCC-续篇/index.html
@@ -1,4 +1,4 @@
-聊聊 mysql 的 MVCC 续篇 | Nicksxs's Blog

Nicksxs's Blog

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

聊聊 mysql 的 MVCC 续篇

上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

/**
+聊聊 mysql 的 MVCC 续篇 | Nicksxs's Blog

Nicksxs's Blog

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

聊聊 mysql 的 MVCC 续篇

上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

/**
 Opens a read view where exactly the transactions serialized before this
 point in time are seen in the view.
 @param id		Creator transaction id */
diff --git a/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/index.html b/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/index.html
index 7cb39bb6d5..3a7824adb7 100644
--- a/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/index.html
+++ b/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/index.html
@@ -1 +1 @@
-聊聊 mysql 的 MVCC 续续篇之锁分析 | Nicksxs's Blog

Nicksxs's Blog

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

聊聊 mysql 的 MVCC 续续篇之锁分析

看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档

SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert into table values (...);
  • update table set ? where ?;
  • delete from table where ?;

除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了

其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了

0%
\ No newline at end of file +聊聊 mysql 的 MVCC 续续篇之锁分析 | Nicksxs's Blog

Nicksxs's Blog

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

聊聊 mysql 的 MVCC 续续篇之锁分析

看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档

SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert into table values (...);
  • update table set ? where ?;
  • delete from table where ?;

除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了

其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了

0%
\ 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 bbbad49d97..8fd7c34ca4 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?

聊一下 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?

聊一下 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);
             }
         }
-    }
0%
\ No newline at end of file + }
0%
\ 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 71f999d24b..81d832b73c 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?

聊一下 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?

聊一下 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 的代码,比较粗粒度。

0%
\ 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 的代码,比较粗粒度。

0%
\ No newline at end of file diff --git a/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html b/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html index d72e063694..d3989cf2da 100644 --- a/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html +++ b/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html @@ -1,4 +1,4 @@ -Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析

题目介绍

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

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

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

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

  3
+Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析

题目介绍

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

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

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

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

  3
  / \
 9  20
   /  \
@@ -20,4 +20,4 @@
     }
     // 前面返回后,左右取大者
     return Math.max(left + 1, right + 1);
-}

分析

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

0%
\ No newline at end of file +}

分析

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

0%
\ No newline at end of file diff --git a/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html b/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html index 12cbcd439a..c4f1b6b781 100644 --- a/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html +++ b/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html @@ -1,4 +1,4 @@ -Leetcode 234 回文链表(Palindrome Linked List) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 234 回文链表(Palindrome Linked List) 题解分析

题目介绍

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

例一 Example 1:

Input: 1->2
Output: false

例二 Example 2:

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

挑战下自己

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

简要分析

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

/**
+Leetcode 234 回文链表(Palindrome Linked List) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 234 回文链表(Palindrome Linked List) 题解分析

题目介绍

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

例一 Example 1:

Input: 1->2
Output: false

例二 Example 2:

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

挑战下自己

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

简要分析

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

/**
  * Definition for singly-linked list.
  * public class ListNode {
  *     int val;
@@ -31,4 +31,4 @@
         }
         return true;
     }
-}
0%
\ No newline at end of file +}
0%
\ No newline at end of file diff --git a/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html b/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html index ebd7044506..b3e1fd9e6c 100644 --- a/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html +++ b/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html @@ -1,4 +1,4 @@ -Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析

题目介绍

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

注意

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

例子:

preorder = [3,9,20,15,7]
+Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析

题目介绍

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

注意

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

例子:

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

返回的二叉树

  3
  / \
 9  20
@@ -32,4 +32,4 @@ inorder = [9,3,15,20,7]
0%
\ No newline at end of file +}
0%
\ No newline at end of file diff --git a/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html b/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html index 35809910bf..efe98a09ef 100644 --- a/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html +++ b/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html @@ -1,4 +1,4 @@ -Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析

题目介绍

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

For example, the following two linked lists:

begin to intersect at node c1.

Example 1:

Input: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
+Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析

题目介绍

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

For example, the following two linked lists:

begin to intersect at node c1.

Example 1:

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

分析题解

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

代码

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
         if (headA == null || headB == null) {
@@ -42,4 +42,4 @@ Input Explanation: The intersected node's value is 8 (note that this must not be
             }
         }
         return null;
-    }

总结

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

0%
\ No newline at end of file + }

总结

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

0%
\ No newline at end of file diff --git a/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/index.html b/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/index.html index ed103e5131..fbe53fa87b 100644 --- a/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/index.html +++ b/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/index.html @@ -1,4 +1,4 @@ -Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析

题目介绍

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

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

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

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

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

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

简要分析

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

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

代码

int ansNew = Integer.MIN_VALUE;
+Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析

题目介绍

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

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

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

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

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

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

简要分析

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

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

代码

int ansNew = Integer.MIN_VALUE;
 public int maxPathSum(TreeNode root) {
         maxSumNew(root);
         return ansNew;
@@ -21,4 +21,4 @@
     int res = Math.max(left + right + root.val, currentSum);
     ans = Math.max(res, ans);
     return currentSum;
-}

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

结果图

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

0%
\ No newline at end of file +}

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

结果图

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

0%
\ No newline at end of file diff --git a/2021/07/18/2021-年中总结/index.html b/2021/07/18/2021-年中总结/index.html index d899efa1ec..7e7ae534ea 100644 --- a/2021/07/18/2021-年中总结/index.html +++ b/2021/07/18/2021-年中总结/index.html @@ -1 +1 @@ -2021 年中总结 | Nicksxs's Blog

Nicksxs's Blog

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

2021 年中总结

又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《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 分以上评价比较多的就可以试试。

0%
\ No newline at end of file +2021 年中总结 | Nicksxs's Blog

Nicksxs's Blog

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

2021 年中总结

又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《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 分以上评价比较多的就可以试试。

0%
\ No newline at end of file diff --git a/baidusitemap.xml b/baidusitemap.xml index ca0bd9f693..c729860902 100644 --- a/baidusitemap.xml +++ b/baidusitemap.xml @@ -173,11 +173,11 @@ 2022-06-11 - https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ + https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ 2022-06-11 - https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ + https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ 2022-06-11 @@ -201,11 +201,11 @@ 2022-06-11 - https://nicksxs.me/2022/03/13/Leetcode-83-%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0-Remove-Duplicates-from-Sorted-List-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-06-11 - https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/2022/03/13/Leetcode-83-%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0-Remove-Duplicates-from-Sorted-List-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ 2022-06-11 @@ -213,23 +213,23 @@ 2022-06-11 - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ 2022-06-11 - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ 2022-06-11 - https://nicksxs.me/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/ + https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ 2022-06-11 - https://nicksxs.me/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/ + https://nicksxs.me/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/ 2022-06-11 - https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/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/ 2022-06-11 @@ -245,15 +245,15 @@ 2022-06-11 - https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ 2022-06-11 - https://nicksxs.me/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/ + https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ 2022-06-11 - https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + https://nicksxs.me/2021/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/ 2022-06-11 @@ -273,11 +273,11 @@ 2022-06-11 - https://nicksxs.me/2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ + https://nicksxs.me/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/ 2022-06-11 - https://nicksxs.me/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/ + https://nicksxs.me/2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ 2022-06-11 @@ -289,15 +289,15 @@ 2022-06-11 - https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + https://nicksxs.me/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/ 2022-06-11 - https://nicksxs.me/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/ + https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ 2022-06-11 - https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ + https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ 2022-06-11 @@ -689,7 +689,7 @@ 2020-01-12 - https://nicksxs.me/2015/03/13/Reverse-Integer/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 @@ -697,7 +697,7 @@ 2020-01-12 - https://nicksxs.me/2019/12/10/Redis-Part-1/ + https://nicksxs.me/2015/01/14/Two-Sum/ 2020-01-12 @@ -705,7 +705,7 @@ 2020-01-12 - https://nicksxs.me/2015/01/14/Two-Sum/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 16183a164d..9dac6ea974 100644 --- a/leancloud_counter_security_urls.json +++ b/leancloud_counter_security_urls.json @@ -1 +1 @@ -[{"title":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2020 年终总结","url":"/2021/03/31/2020-年终总结/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"Apollo 客户端启动过程分析","url":"/2022/09/18/Apollo-客户端启动过程分析/"},{"title":"Apollo 如何获取当前环境","url":"/2022/09/04/Apollo-如何获取当前环境/"},{"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/27/Disruptor-系列二/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"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 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Disruptor 系列三","url":"/2022/09/25/Disruptor-系列三/"},{"title":"Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"title":"2022 年终总结","url":"/2023/01/15/2022-年终总结/"},{"title":"Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析","url":"/2022/07/22/Leetcode-1260-二维网格迁移-Shift-2D-Grid-Easy-题解分析/"},{"title":"Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"title":"Headscale初体验以及踩坑记","url":"/2023/01/22/Headscale初体验以及踩坑记/"},{"title":"Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析","url":"/2022/09/11/Leetcode-1862-向下取整数对和-Sum-of-Floored-Pairs-Hard-题解分析/"},{"title":"Leetcode 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"title":"Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析","url":"/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/"},{"title":"Leetcode 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"title":"Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"title":"Leetcode 3 Longest Substring Without Repeating Characters 题解分析","url":"/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-题解分析/"},{"title":"Leetcode 155 最小栈(Min Stack) 题解分析","url":"/2020/12/06/Leetcode-155-最小栈-Min-Stack-题解分析/"},{"title":"Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析","url":"/2022/03/07/Leetcode-349-两个数组的交集-Intersection-of-Two-Arrays-Easy-题解分析/"},{"title":"Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"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 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"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":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析","url":"/2022/10/02/Leetcode-747-至少是其他数字两倍的最大数-Largest-Number-At-Least-Twice-of-Others-Easy-题解分析/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"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":"docker比一般多一点的初学者介绍四","url":"/2022/12/25/docker比一般多一点的初学者介绍四/"},{"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":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"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系列-connection连接池解析","url":"/2023/02/19/mybatis系列-connection连接池解析/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"mybatis系列-mybatis是如何初始化mapper的","url":"/2022/12/04/mybatis是如何初始化mapper的/"},{"title":"mybatis系列-dataSource解析","url":"/2023/01/08/mybatis系列-dataSource解析/"},{"title":"mybatis系列-入门篇","url":"/2022/11/27/mybatis系列-入门篇/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"title":"openresty","url":"/2019/06/18/openresty/"},{"title":"mybatis系列-typeAliases系统","url":"/2023/01/01/mybatis系列-typeAliases系统/"},{"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":"mybatis系列-第一条sql的细节","url":"/2022/12/11/mybatis系列-第一条sql的细节/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis数据结构介绍-第一部分 SDS,链表,字典","url":"/2019/12/26/redis数据结构介绍/"},{"title":"redis数据结构介绍三-第三部分 整数集合","url":"/2020/01/10/redis数据结构介绍三/"},{"title":"mybatis系列-第一条sql的更多细节","url":"/2022/12/18/mybatis系列-第一条sql的更多细节/"},{"title":"redis数据结构介绍五-第五部分 对象","url":"/2020/01/20/redis数据结构介绍五/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/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":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"powershell 初体验","url":"/2022/11/13/powershell-初体验/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"分享一次折腾老旧笔记本的体验-续篇","url":"/2023/02/12/分享一次折腾老旧笔记本的体验-续篇/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"分享一次折腾老旧笔记本的体验-续续篇","url":"/2023/02/26/分享一次折腾老旧笔记本的体验-续续篇/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"分享一次折腾老旧笔记本的体验","url":"/2023/02/05/分享一次折腾老旧笔记本的体验/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"powershell 初体验二","url":"/2022/11/20/powershell-初体验二/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"分享一次比较诡异的 Windows 下 U盘无法退出的经历","url":"/2023/01/29/分享一次比较诡异的-Windows-下-U盘无法退出的经历/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-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":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊一下关于怎么陪伴学习","url":"/2022/11/06/聊一下关于怎么陪伴学习/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"记录下 phpunit 的入门使用方法之setUp和tearDown","url":"/2022/10/23/记录下-phpunit-的入门使用方法之setUp和tearDown/"},{"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/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"记录下 phpunit 的入门使用方法","url":"/2022/10/16/记录下-phpunit-的入门使用方法/"},{"title":"记一个容器中 dubbo 注册的小知识点","url":"/2022/10/09/记一个容器中-dubbo-注册的小知识点/"},{"title":"记录下 redis 的一些使用方法","url":"/2022/10/30/记录下-redis-的一些使用方法/"}] \ 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":"/2021/07/18/2021-年中总结/"},{"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":"2022 年终总结","url":"/2023/01/15/2022-年终总结/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"Apollo 客户端启动过程分析","url":"/2022/09/18/Apollo-客户端启动过程分析/"},{"title":"Apollo 如何获取当前环境","url":"/2022/09/04/Apollo-如何获取当前环境/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"Disruptor 系列三","url":"/2022/09/25/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":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"Headscale初体验以及踩坑记","url":"/2023/01/22/Headscale初体验以及踩坑记/"},{"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 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 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"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 160 相交链表(intersection-of-two-linked-lists) 题解分析","url":"/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/"},{"title":"Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"title":"Leetcode 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析","url":"/2022/09/11/Leetcode-1862-向下取整数对和-Sum-of-Floored-Pairs-Hard-题解分析/"},{"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 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"title":"Leetcode 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 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 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析","url":"/2022/10/02/Leetcode-747-至少是其他数字两倍的最大数-Largest-Number-At-Least-Twice-of-Others-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":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"binary-watch","url":"/2016/09/29/binary-watch/"},{"title":"docker-mysql-cluster","url":"/2016/08/14/docker-mysql-cluster/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"docker比一般多一点的初学者介绍","url":"/2020/03/08/docker比一般多一点的初学者介绍/"},{"title":"docker比一般多一点的初学者介绍三","url":"/2020/03/21/docker比一般多一点的初学者介绍三/"},{"title":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"docker比一般多一点的初学者介绍四","url":"/2022/12/25/docker比一般多一点的初学者介绍四/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"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/10/03/mybatis-的缓存是怎么回事/"},{"title":"mybatis系列-mybatis是如何初始化mapper的","url":"/2022/12/04/mybatis是如何初始化mapper的/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"mybatis系列-dataSource解析","url":"/2023/01/08/mybatis系列-dataSource解析/"},{"title":"mybatis系列-typeAliases系统","url":"/2023/01/01/mybatis系列-typeAliases系统/"},{"title":"mybatis系列-入门篇","url":"/2022/11/27/mybatis系列-入门篇/"},{"title":"mybatis系列-connection连接池解析","url":"/2023/02/19/mybatis系列-connection连接池解析/"},{"title":"mybatis系列-第一条sql的更多细节","url":"/2022/12/18/mybatis系列-第一条sql的更多细节/"},{"title":"mybatis系列-第一条sql的细节","url":"/2022/12/11/mybatis系列-第一条sql的细节/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"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":"powershell 初体验","url":"/2022/11/13/powershell-初体验/"},{"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":"powershell 初体验二","url":"/2022/11/20/powershell-初体验二/"},{"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/18/redis系列介绍八/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/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":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"分享一次折腾老旧笔记本的体验-续篇","url":"/2023/02/12/分享一次折腾老旧笔记本的体验-续篇/"},{"title":"分享一次折腾老旧笔记本的体验-续续篇","url":"/2023/02/26/分享一次折腾老旧笔记本的体验-续续篇/"},{"title":"分享一次折腾老旧笔记本的体验","url":"/2023/02/05/分享一次折腾老旧笔记本的体验/"},{"title":"分享一次比较诡异的 Windows 下 U盘无法退出的经历","url":"/2023/01/29/分享一次比较诡异的-Windows-下-U盘无法退出的经历/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"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":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"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":"/2022/11/06/聊一下关于怎么陪伴学习/"},{"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/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 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":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"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":"聊聊 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":"记一个容器中 dubbo 注册的小知识点","url":"/2022/10/09/记一个容器中-dubbo-注册的小知识点/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"记录下 phpunit 的入门使用方法","url":"/2022/10/16/记录下-phpunit-的入门使用方法/"},{"title":"记录下 redis 的一些使用方法","url":"/2022/10/30/记录下-redis-的一些使用方法/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"记录下 phpunit 的入门使用方法之setUp和tearDown","url":"/2022/10/23/记录下-phpunit-的入门使用方法之setUp和tearDown/"},{"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 diff --git a/page/31/index.html b/page/31/index.html index c76f2d4912..731a2ad8f9 100644 --- a/page/31/index.html +++ b/page/31/index.html @@ -1,4 +1,4 @@ -Nicksxs's Blog - What hurts more, the pain of hard work or the pain of regret?

Nicksxs's Blog

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

前面说了mysql数据库的事务相关的,那事务是用来干嘛的,这里得补一下ACID,

ACID,是指数据库管理系统DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

  • Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。[1]

  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束触发器级联回滚等。[1]

  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。[1]

  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。[1]

在mysql中,借助于MVCC,各种级别的锁,日志等特性来实现了事务的ACID,但是这个我们通常是对于一个数据库服务的定义,常见的情况下我们的数据库随着业务发展也会从单实例变成多实例,组成主从Master-Slave架构,这个时候其实会有一些问题随之出现,比如说主从同步延迟,假如在业务代码中做了读写分离,对于一些敏感度较低的数据其实问题不是很大,只要主从延迟不到特别夸张的地步一般都是可以忍受的,但是对于一些核心的业务数据,比如订单之类的,不能忍受数据不一致,下了单了,付了款了,一刷订单列表,发现这个订单还没支付,甚至订单都没在,这对于用户来讲是恨不能容忍的错误,那么这里就需要一些措施,要不就不读写分离,要不就在 redis 这类缓存下订单,或者支付后加个延时等,这些都是一些补偿措施,并且这也是一个不太切当的例子,比较合适的例子也可以用这个下单来说,一般在电商平台下单会有挺多要做的事情,比如像下面这个图

下单的是后要冻结核销优惠券,如果账户里有钱要冻结扣除账户里的钱,如果使用了J 豆也一样,可能还有 E 卡,忽略我借用的平台,因为目前一般后台服务化之后,可能每一项都是对应的一个后台服务,我们期望的执行过程是要不全成功,要不就全保持执行前状态,不能是部分扣减核销成功了,部分还不行,所以我们处理这种情况会引入一些通用的方案,第一种叫二阶段提交,

二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

对于上面的例子,我们将整个过程分成两个阶段,首先是提交请求阶段,这个阶段大概需要做的是确定资源存在,锁定资源,可能还要做好失败后回滚的准备,如果这些都 ok 了那么就响应成功,这里其实用到了一个叫事务的协调者的角色,类似于裁判员,每个节点都反馈第一阶段成功后,开始执行第二阶段,也就是实际执行操作,这里也是需要所有节点都反馈成功后才是执行成功,要不就是失败回滚。其实常用的分布式事务的解决方案主要也是基于此方案的改进,比如后面介绍的三阶段提交,有三阶段提交就是因为二阶段提交比较尴尬的几个点,

  • 第一是对于两阶段提交,其中默认只有协调者有超时时间,当一个参与者进入卡死状态时只能依赖协调者的超时来结束任务,这中间的时间参与者都是锁定着资源
  • 第二是协调者的单点问题,万一挂了,参与者就会在那傻等着

所以三阶段提交引入了各节点的超时机制和一个准备阶段,首先是一个can commit阶段,询问下各个节点有没有资源,能不能进行操作,这个阶段不阻塞,只是提前做个摸底,这个阶段其实人畜无害,但是能提高成功率,在这个阶段如果就有节点反馈是不接受的,那就不用执行下去了,也没有锁资源,然后第二阶段是 pre commit ,这个阶段做的事情跟原来的 第一阶段比较类似,然后是第三阶段do commit,其实三阶段提交我个人觉得只是加了个超时,和准备阶段,好像木有根本性的解决的两阶段提交的问题,后续可以再看看一些论文来思考讨论下。

2020年05月24日22:11 更新
这里跟朋友讨论了下,好像想通了最核心的一点,对于前面说的那个场景,如果是两阶段提交,如果各个节点中有一个没回应,并且协调者也挂了,这个时候会有什么情况呢,再加一个假设,其实比如这个一阶段其实是检验就失败的,理论上应该大家都释放资源,那么对于这种异常情况,其他的参与者就不知所措了,就傻傻地锁着资源阻塞着,那么三阶段提交的意义就出现了,把第一阶段拆开,那么即使在这个阶段出现上述的异常,即也不会锁定资源,同时参与者也有超时机制,在第二阶段锁定资源出现异常是,其他参与者节点等超时后就自动释放资源了,也就没啥问题了,不过对于那种异常恢复后的一些情况还是没有很好地解决,需要借助 zk 等,后面有空可以讲讲 paxos 跟 raft 等

看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档

SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert into table values (...);
  • update table set ? where ?;
  • delete from table where ?;

除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了

其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了

上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

/**
+Nicksxs's Blog - What hurts more, the pain of hard work or the pain of regret?

Nicksxs's Blog

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

前面说了mysql数据库的事务相关的,那事务是用来干嘛的,这里得补一下ACID,

ACID,是指数据库管理系统DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

  • Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。[1]

  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束触发器级联回滚等。[1]

  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。[1]

  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。[1]

在mysql中,借助于MVCC,各种级别的锁,日志等特性来实现了事务的ACID,但是这个我们通常是对于一个数据库服务的定义,常见的情况下我们的数据库随着业务发展也会从单实例变成多实例,组成主从Master-Slave架构,这个时候其实会有一些问题随之出现,比如说主从同步延迟,假如在业务代码中做了读写分离,对于一些敏感度较低的数据其实问题不是很大,只要主从延迟不到特别夸张的地步一般都是可以忍受的,但是对于一些核心的业务数据,比如订单之类的,不能忍受数据不一致,下了单了,付了款了,一刷订单列表,发现这个订单还没支付,甚至订单都没在,这对于用户来讲是恨不能容忍的错误,那么这里就需要一些措施,要不就不读写分离,要不就在 redis 这类缓存下订单,或者支付后加个延时等,这些都是一些补偿措施,并且这也是一个不太切当的例子,比较合适的例子也可以用这个下单来说,一般在电商平台下单会有挺多要做的事情,比如像下面这个图

下单的是后要冻结核销优惠券,如果账户里有钱要冻结扣除账户里的钱,如果使用了J 豆也一样,可能还有 E 卡,忽略我借用的平台,因为目前一般后台服务化之后,可能每一项都是对应的一个后台服务,我们期望的执行过程是要不全成功,要不就全保持执行前状态,不能是部分扣减核销成功了,部分还不行,所以我们处理这种情况会引入一些通用的方案,第一种叫二阶段提交,

二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

对于上面的例子,我们将整个过程分成两个阶段,首先是提交请求阶段,这个阶段大概需要做的是确定资源存在,锁定资源,可能还要做好失败后回滚的准备,如果这些都 ok 了那么就响应成功,这里其实用到了一个叫事务的协调者的角色,类似于裁判员,每个节点都反馈第一阶段成功后,开始执行第二阶段,也就是实际执行操作,这里也是需要所有节点都反馈成功后才是执行成功,要不就是失败回滚。其实常用的分布式事务的解决方案主要也是基于此方案的改进,比如后面介绍的三阶段提交,有三阶段提交就是因为二阶段提交比较尴尬的几个点,

  • 第一是对于两阶段提交,其中默认只有协调者有超时时间,当一个参与者进入卡死状态时只能依赖协调者的超时来结束任务,这中间的时间参与者都是锁定着资源
  • 第二是协调者的单点问题,万一挂了,参与者就会在那傻等着

所以三阶段提交引入了各节点的超时机制和一个准备阶段,首先是一个can commit阶段,询问下各个节点有没有资源,能不能进行操作,这个阶段不阻塞,只是提前做个摸底,这个阶段其实人畜无害,但是能提高成功率,在这个阶段如果就有节点反馈是不接受的,那就不用执行下去了,也没有锁资源,然后第二阶段是 pre commit ,这个阶段做的事情跟原来的 第一阶段比较类似,然后是第三阶段do commit,其实三阶段提交我个人觉得只是加了个超时,和准备阶段,好像木有根本性的解决的两阶段提交的问题,后续可以再看看一些论文来思考讨论下。

2020年05月24日22:11 更新
这里跟朋友讨论了下,好像想通了最核心的一点,对于前面说的那个场景,如果是两阶段提交,如果各个节点中有一个没回应,并且协调者也挂了,这个时候会有什么情况呢,再加一个假设,其实比如这个一阶段其实是检验就失败的,理论上应该大家都释放资源,那么对于这种异常情况,其他的参与者就不知所措了,就傻傻地锁着资源阻塞着,那么三阶段提交的意义就出现了,把第一阶段拆开,那么即使在这个阶段出现上述的异常,即也不会锁定资源,同时参与者也有超时机制,在第二阶段锁定资源出现异常是,其他参与者节点等超时后就自动释放资源了,也就没啥问题了,不过对于那种异常恢复后的一些情况还是没有很好地解决,需要借助 zk 等,后面有空可以讲讲 paxos 跟 raft 等

看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档

SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert into table values (...);
  • update table set ? where ?;
  • delete from table where ?;

除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了

其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了

上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

/**
 Opens a read view where exactly the transactions serialized before this
 point in time are seen in the view.
 @param id		Creator transaction id */
@@ -8,7 +8,7 @@ void ReadView::prepare(trx_id_t id) {
 
   m_creator_trx_id = id;
 
-  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;

m_low_limit_id赋的值是trx_sys->max_trx_id,代表的是当前系统最小的未分配的事务 id,所以呢,举个例子,当前有三个活跃事务,事务 id 分别是 100,200,300,而 m_up_limit_id = 100, m_low_limit_id = 301,当事务 id 是 200 的提交之后,它的更新就是可以被 100 和 300 看到,而不是说 m_ids 里没了 200,并且 200 比 100 大就应该不可见了

幻读

还有一个问题是幻读的问题,这貌似也是个高频面试题,啥意思呢,或者说跟它最常拿来比较的脏读,脏读是指读到了别的事务未提交的数据,因为未提交,严格意义上来讲,不一定是会被最后落到库里,可能会回滚,也就是在 read uncommitted 级别下会出现的问题,但是幻读不太一样,幻读是指两次查询的结果数量不一样,比如我查了第一次是 select * from table1 where id < 10 for update,查出来了一条结果 id 是 5,然后再查一下发现出来了一条 id 是 5,一条 id 是 7,那是不是有点尴尬了,其实呢这个点我觉得脏读跟幻读也比较是从原理层面来命名,如果第一次接触的同学发觉有点不理解也比较正常,因为从逻辑上讲总之都是数据不符合预期,但是基于源码层面其实是不同的情况,幻读是在原先的 read view 无法完全解决的,怎么解决呢,简单的来说就是锁咯,我们知道innodb 是基于 record lock 行锁的,但是貌似没有办法解决这种问题,那么 innodb 就引入了 gap lock 间隙锁,比如上面说的情况下,id 小于 10 的情况下,是都应该锁住的,gap lock 其实是基于索引结构来锁的,因为索引树除了树形结构之外,还有一个next record 的指针,gap lock 也是基于这个来锁的
看一下 mysql 的文档

SELECT … FOR UPDATE sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

对于一个 for update 查询,在 RR 级别下,会设置一个 next-key lock在每一条被查询到的记录上,next-lock 又是啥呢,其实就是 gap 锁和 record 锁的结合体,比如我在数据库里有 id 是 1,3,5,7,10,对于上面那条查询,查出来的结果就是 1,3,5,7,那么按照文档里描述的,对于这几条记录都会加上next-key lock,也就是(-∞, 1], (1, 3], (3, 5], (5, 7], (7, 10) 这些区间和记录会被锁起来,不让插入,再唠叨一下呢,就是其实如果是只读的事务,光 read view 一致性读就够了,如果是有写操作的呢,就需要锁了。

很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 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;
+  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;

m_low_limit_id赋的值是trx_sys->max_trx_id,代表的是当前系统最小的未分配的事务 id,所以呢,举个例子,当前有三个活跃事务,事务 id 分别是 100,200,300,而 m_up_limit_id = 100, m_low_limit_id = 301,当事务 id 是 200 的提交之后,它的更新就是可以被 100 和 300 看到,而不是说 m_ids 里没了 200,并且 200 比 100 大就应该不可见了

幻读

还有一个问题是幻读的问题,这貌似也是个高频面试题,啥意思呢,或者说跟它最常拿来比较的脏读,脏读是指读到了别的事务未提交的数据,因为未提交,严格意义上来讲,不一定是会被最后落到库里,可能会回滚,也就是在 read uncommitted 级别下会出现的问题,但是幻读不太一样,幻读是指两次查询的结果数量不一样,比如我查了第一次是 select * from table1 where id < 10 for update,查出来了一条结果 id 是 5,然后再查一下发现出来了一条 id 是 5,一条 id 是 7,那是不是有点尴尬了,其实呢这个点我觉得脏读跟幻读也比较是从原理层面来命名,如果第一次接触的同学发觉有点不理解也比较正常,因为从逻辑上讲总之都是数据不符合预期,但是基于源码层面其实是不同的情况,幻读是在原先的 read view 无法完全解决的,怎么解决呢,简单的来说就是锁咯,我们知道innodb 是基于 record lock 行锁的,但是貌似没有办法解决这种问题,那么 innodb 就引入了 gap lock 间隙锁,比如上面说的情况下,id 小于 10 的情况下,是都应该锁住的,gap lock 其实是基于索引结构来锁的,因为索引树除了树形结构之外,还有一个next record 的指针,gap lock 也是基于这个来锁的
看一下 mysql 的文档

SELECT … FOR UPDATE sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

对于一个 for update 查询,在 RR 级别下,会设置一个 next-key lock在每一条被查询到的记录上,next-lock 又是啥呢,其实就是 gap 锁和 record 锁的结合体,比如我在数据库里有 id 是 1,3,5,7,10,对于上面那条查询,查出来的结果就是 1,3,5,7,那么按照文档里描述的,对于这几条记录都会加上next-key lock,也就是(-∞, 1], (1, 3], (3, 5], (5, 7], (7, 10) 这些区间和记录会被锁起来,不让插入,再唠叨一下呢,就是其实如果是只读的事务,光 read view 一致性读就够了,如果是有写操作的呢,就需要锁了。

很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 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 */
diff --git a/search.xml b/search.xml
index fda22b5527..e5895a123b 100644
--- a/search.xml
+++ b/search.xml
@@ -20,6 +20,35 @@
         读后感
       
   
+  
+    2019年终总结
+    /2020/02/01/2019%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
+    今天是农历初八了,年前一个月的时候就准备做下今年的年终总结,可是写了一点觉得太情绪化了,希望后面写个平淡点的,正好最近技术方面还没有看到一个完整成文的内容,就来写一下这一年的总结,尽量少写一点太情绪化的东西。

+

跳槽

年初换了个公司,也算换了个环境,跟前公司不太一样,做的事情方向也不同,可能是侧重点不同,一开始有些不适应,主要是压力上,会觉得压力比较大,但是总体来说与人相处的部分还是不错的,做的技术方向还是Java,这里也感谢前东家让我有机会转了Java,个人感觉杭州整个市场还是Java比较有优势,不过在开始的时候总觉得对Java有点不适应,应该值得深究的东西还是很多的,而且对于面试来说,也是有很多可以问的,后面慢慢发现除开某里等一线超一线互联网公司之外,大部分的面试还是有大概的套路跟大纲的,不过更细致的则因人而异了,面试有时候也还看缘分,面试官关注的点跟应试者比较契合的话就很容易通过面试,不然的话总会有能刁难或者理性化地说比较难回答的问题。这个后面可以单独说一下,先按下不表。
刚进公司没多久就负责比较重要的项目,工期也比较紧张,整体来说那段时间的压力的确是比较大的,不过总算最后结果不坏,这里应该说对一些原来在前东家都是掌握的不太好的部分,比如maven,其实maven对于java程序员来说还是很重要的,但是我碰到过的面试基本没问过这个,我自己也在后面的面试中没问过相关的,不知道咋问,比如dependence分析、冲突解决,比如对bean的理解,这个算是我一直以来的疑问点,因为以前刚开始学Java学spring,上来就是bean,但是bean到底是啥,IOC是啥,可能网上的文章跟大多数书籍跟我的理解思路不太match,导致一直不能很好的理解这玩意,到后面才理解,要理解这个bean,需要有两个基本概念,一个是面向对象,一个是对象容器跟依赖反转,还是只说到这,后面可以有专题说一下,总之自认为技术上有了不小的长进了,方向上应该是偏实用的。这个重要的项目完成后慢慢能喘口气了,后面也有一些比较紧急且工作量大的,不过在我TL的帮助下还是能尽量协调好资源。

+

面试

后面因为项目比较多,缺少开发,所以也参与帮忙做一些面试,这里总体感觉是面的候选人还是比较多样的,有些工作了蛮多年但是一些基础问题回答的不好,有些还是在校学生,但是面试技巧不错,针对常见的面试题都有不错的准备,不过还是觉得光靠这些面试题不能完全说明问题,真正工作了需要的是解决问题的人,而不是会背题的,退一步来说能好好准备面试还是比较重要的,也是双向选择中的基本尊重,印象比较深刻的是参加了去杭州某高校的校招面试,感觉参加校招的同学还是很多的,大部分是20年将毕业的研究生,挺多都是基础很扎实,对比起我刚要毕业时还是很汗颜,挺多来面试的同学都非常不错,那天强度也很大,从下午到那开始一直面到六七点,在这祝福那些来面试的同学,也都不容易的,能找到心仪的工作。

+

技术方向

这一年前大半部分还是比较焦虑不能恢复那种主动找时间学习的状态,可能换了公司是主要的原因,初期有个适应的过程也比较正常,总体来说可能是到九十月份开始慢慢有所改善,对这些方面有学习了下,

+
    +
  • spring方向,spring真的是个庞然大物,但是还是要先抓住根本,慢慢发散去了解其他的细节,抓住bean的生命周期,当然也不是死记硬背,让我一个个背下来我也不行,但是知道它究竟是干嘛的,有啥用,并且在工作中能用起来是最重要的
  • +
  • mysql数据库,这部分主要是关注了mvcc,知道了个大概,源码实现细节还没具体研究,有时间可以来个专题(一大堆待写的内容)
  • +
  • java的一些源码,比如aqs这种,结合文章看了下源码,一开始总感觉静不下心来看,然后有一次被LD刺激了下就看完了,包括conditionObject等
  • +
  • redis的源码,这里包括了Redis分布式锁和redis的数据结构源码,已经写成文章,不过比较着急成文,所以质量不是特别好,希望后面再来补补
  • +
  • jvm源码,这部分正好是想了解下g1收集器,大概把周志明的书看完了,但是还没完整的理解掌握,还有就是g1收集器的部分,一是概念部分大概理解了,后面是就是想从源码层面去学习理解,这也是新一年的主要计划
  • +
  • mq的部分是了解了zero copy,sendfile等,跟消息队列主题关系不大🤦‍♂️
    这么看还是学了点东西的,希望新一年再接再厉。
  • +
+

生活

住的地方没变化,主要是周边设施比较方便,暂时没找到更好的就没打算换,主要的问题是没电梯,一开始没觉得有啥,真正住起来还是觉得比较累的,希望后面租的可以有电梯,或者楼层低一点,还有就是要通下水道,第一次让师傅上门,花了两百大洋,后来自学成才了,让师傅通了一次才撑了一个月就不行了,后面自己通的差不多可以撑半年,还是比较有成就感的😀,然后就是跑步了,年初的时候去了紫金港跑步,后面因为工作的原因没去了,但是公司的跑步机倒是让我重拾起这个唯一的运动健身项目,后面因为肠胃问题,体重也需要控制,所以就周末回来也在家这边坚持跑步,下半年的话基本保持每周一次以上,比较那些跑马拉松的大牛还是差距很大,不过也是突破自我了,有一次跑了12公里,最远的距离,而且后面感觉跑十公里也不是特别吃不消了,这一年达成了300公里的目标,体重也稍有下降,比较满意的结果。

+

期待

希望工作方面技术方面能有所长进,生活上能多点时间陪家人,继续跑步减肥,家人健健康康的,嗯

+]]>
+ + 生活 + 年终总结 + 2019 + + + 生活 + 年终总结 + 2019 + +
2020 年终总结 /2021/03/31/2020-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ @@ -45,6 +74,25 @@ 拖更 + + 2020年中总结 + /2020/07/11/2020%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + 很快2020 年就过了一半了,而且是今年这么特殊的一年,很多事情都发生的出乎意料,疫情这个绕不过去的话题,之前写了点比较愤青的文字,感觉不太适合发出来就烂在草稿箱里吧,这个目前一大影响估计是今年都没办法完全摘下口罩了,前面几个月来回杭州都开车,因为彭埠大桥不通行了,实在是非常不方便,每条路都灰常堵,心累,吐槽下杭州的交通规划和交警同志,工作实在做的不咋地。

+

另外一件是就是蜗壳,从前不知道黝黑蜗壳是啥意思,只是经常会在他的视频里看到,大学的时候在缘网下了一个集锦,炒鸡帅气,各种空接扣篮,越来越能明白那句“你永远不知道意外和明天不知道哪个会先来,且行且珍惜”的含义,只是听了很多道理,依然活不好这一生,知易行难,王阳明真的是这方面的大师,有空可以看看这方面的书,一直想写写我跟篮球跟蜗壳的这十几年,争取能早日写好吧,不过得找个静得下来的时候写。

+

正事方面上半年还是挺让人失望的,没有达成一些目标,应该还是能力不足吧,技术方面分析一下还是停留在看的表面层,有些实操的,或者结合业务场景的能力不太行,算是在坚持写写 blog,主要是被这个每周一篇的目标推着走,有时会比较焦虑,内容产出也还比较差,希望能在后面有些改善,可能会降低频率,只是觉得降低了也不一定能有比较好的提升,无法战胜自己的惰性,所以暂时还是坚持下这个目标吧,还有就是 coding 能力,有时候也应该刷刷题,提升思维敏捷度,大脑用太少可能生锈了,况且本来就不是很有优势,虽然失望也只能继续努力吧,日拱一卒,来日方长,加油吧~😔

+

还有就是跑步减肥了,截止今天,上半年跑了 136 公里了,因为疫情影响,农历年后是从 4 月 17 号开始跑的,去年跑到了 300 公里,奖励自己了一个手表(真的挺后悔的,还不如 200 块买个手表),今年希望可以能在这个基础上再进一步,一直跟领导说,跑步算是我坚持下来的唯一一个好习惯了,618 买了个跑步机,周末回家了可以不受天气影响的多跑跑,不过如果天气好可能还是会出去跑跑,跑步机跑道多少还是有点拘束,只是感觉可能是我还是吃得太多了🤦‍♂️,效果不是很明显,还在 80 这个坎徘徊,等于浪费了大半年,可能是年初的项目太费心力,压力比较大,吃得更多,是不是可以算工伤😄,这方面也需要好好调整,可以放得开一点,虽然不太可能一下子到位,但是总要去努力下,随着年龄成长总要承担更多,也要看得开一点,没法事事如愿,尽力就好了,减肥这个事情还在结合一些俯卧撑啥的,希望也能坚持下去,加油吧,不知道原话怎么说的,意思是人类最大的勇敢就是看透了人世间的苦难,仍然热爱生活。我当然没可能让内心变得这么强大,试着去努力吧,奥力给!

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

+

工作篇

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

+]]>
+ + 生活 + 年终总结 + + + 生活 + 年终总结 + 2021 + 拖更
@@ -114,71 +179,6 @@ public: c++ - - 2019年终总结 - /2020/02/01/2019%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 今天是农历初八了,年前一个月的时候就准备做下今年的年终总结,可是写了一点觉得太情绪化了,希望后面写个平淡点的,正好最近技术方面还没有看到一个完整成文的内容,就来写一下这一年的总结,尽量少写一点太情绪化的东西。

-

跳槽

年初换了个公司,也算换了个环境,跟前公司不太一样,做的事情方向也不同,可能是侧重点不同,一开始有些不适应,主要是压力上,会觉得压力比较大,但是总体来说与人相处的部分还是不错的,做的技术方向还是Java,这里也感谢前东家让我有机会转了Java,个人感觉杭州整个市场还是Java比较有优势,不过在开始的时候总觉得对Java有点不适应,应该值得深究的东西还是很多的,而且对于面试来说,也是有很多可以问的,后面慢慢发现除开某里等一线超一线互联网公司之外,大部分的面试还是有大概的套路跟大纲的,不过更细致的则因人而异了,面试有时候也还看缘分,面试官关注的点跟应试者比较契合的话就很容易通过面试,不然的话总会有能刁难或者理性化地说比较难回答的问题。这个后面可以单独说一下,先按下不表。
刚进公司没多久就负责比较重要的项目,工期也比较紧张,整体来说那段时间的压力的确是比较大的,不过总算最后结果不坏,这里应该说对一些原来在前东家都是掌握的不太好的部分,比如maven,其实maven对于java程序员来说还是很重要的,但是我碰到过的面试基本没问过这个,我自己也在后面的面试中没问过相关的,不知道咋问,比如dependence分析、冲突解决,比如对bean的理解,这个算是我一直以来的疑问点,因为以前刚开始学Java学spring,上来就是bean,但是bean到底是啥,IOC是啥,可能网上的文章跟大多数书籍跟我的理解思路不太match,导致一直不能很好的理解这玩意,到后面才理解,要理解这个bean,需要有两个基本概念,一个是面向对象,一个是对象容器跟依赖反转,还是只说到这,后面可以有专题说一下,总之自认为技术上有了不小的长进了,方向上应该是偏实用的。这个重要的项目完成后慢慢能喘口气了,后面也有一些比较紧急且工作量大的,不过在我TL的帮助下还是能尽量协调好资源。

-

面试

后面因为项目比较多,缺少开发,所以也参与帮忙做一些面试,这里总体感觉是面的候选人还是比较多样的,有些工作了蛮多年但是一些基础问题回答的不好,有些还是在校学生,但是面试技巧不错,针对常见的面试题都有不错的准备,不过还是觉得光靠这些面试题不能完全说明问题,真正工作了需要的是解决问题的人,而不是会背题的,退一步来说能好好准备面试还是比较重要的,也是双向选择中的基本尊重,印象比较深刻的是参加了去杭州某高校的校招面试,感觉参加校招的同学还是很多的,大部分是20年将毕业的研究生,挺多都是基础很扎实,对比起我刚要毕业时还是很汗颜,挺多来面试的同学都非常不错,那天强度也很大,从下午到那开始一直面到六七点,在这祝福那些来面试的同学,也都不容易的,能找到心仪的工作。

-

技术方向

这一年前大半部分还是比较焦虑不能恢复那种主动找时间学习的状态,可能换了公司是主要的原因,初期有个适应的过程也比较正常,总体来说可能是到九十月份开始慢慢有所改善,对这些方面有学习了下,

-
    -
  • spring方向,spring真的是个庞然大物,但是还是要先抓住根本,慢慢发散去了解其他的细节,抓住bean的生命周期,当然也不是死记硬背,让我一个个背下来我也不行,但是知道它究竟是干嘛的,有啥用,并且在工作中能用起来是最重要的
  • -
  • mysql数据库,这部分主要是关注了mvcc,知道了个大概,源码实现细节还没具体研究,有时间可以来个专题(一大堆待写的内容)
  • -
  • java的一些源码,比如aqs这种,结合文章看了下源码,一开始总感觉静不下心来看,然后有一次被LD刺激了下就看完了,包括conditionObject等
  • -
  • redis的源码,这里包括了Redis分布式锁和redis的数据结构源码,已经写成文章,不过比较着急成文,所以质量不是特别好,希望后面再来补补
  • -
  • jvm源码,这部分正好是想了解下g1收集器,大概把周志明的书看完了,但是还没完整的理解掌握,还有就是g1收集器的部分,一是概念部分大概理解了,后面是就是想从源码层面去学习理解,这也是新一年的主要计划
  • -
  • mq的部分是了解了zero copy,sendfile等,跟消息队列主题关系不大🤦‍♂️
    这么看还是学了点东西的,希望新一年再接再厉。
  • -
-

生活

住的地方没变化,主要是周边设施比较方便,暂时没找到更好的就没打算换,主要的问题是没电梯,一开始没觉得有啥,真正住起来还是觉得比较累的,希望后面租的可以有电梯,或者楼层低一点,还有就是要通下水道,第一次让师傅上门,花了两百大洋,后来自学成才了,让师傅通了一次才撑了一个月就不行了,后面自己通的差不多可以撑半年,还是比较有成就感的😀,然后就是跑步了,年初的时候去了紫金港跑步,后面因为工作的原因没去了,但是公司的跑步机倒是让我重拾起这个唯一的运动健身项目,后面因为肠胃问题,体重也需要控制,所以就周末回来也在家这边坚持跑步,下半年的话基本保持每周一次以上,比较那些跑马拉松的大牛还是差距很大,不过也是突破自我了,有一次跑了12公里,最远的距离,而且后面感觉跑十公里也不是特别吃不消了,这一年达成了300公里的目标,体重也稍有下降,比较满意的结果。

-

期待

希望工作方面技术方面能有所长进,生活上能多点时间陪家人,继续跑步减肥,家人健健康康的,嗯

-]]>
- - 生活 - 年终总结 - 2019 - - - 生活 - 年终总结 - 2019 - -
- - 2020年中总结 - /2020/07/11/2020%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 很快2020 年就过了一半了,而且是今年这么特殊的一年,很多事情都发生的出乎意料,疫情这个绕不过去的话题,之前写了点比较愤青的文字,感觉不太适合发出来就烂在草稿箱里吧,这个目前一大影响估计是今年都没办法完全摘下口罩了,前面几个月来回杭州都开车,因为彭埠大桥不通行了,实在是非常不方便,每条路都灰常堵,心累,吐槽下杭州的交通规划和交警同志,工作实在做的不咋地。

-

另外一件是就是蜗壳,从前不知道黝黑蜗壳是啥意思,只是经常会在他的视频里看到,大学的时候在缘网下了一个集锦,炒鸡帅气,各种空接扣篮,越来越能明白那句“你永远不知道意外和明天不知道哪个会先来,且行且珍惜”的含义,只是听了很多道理,依然活不好这一生,知易行难,王阳明真的是这方面的大师,有空可以看看这方面的书,一直想写写我跟篮球跟蜗壳的这十几年,争取能早日写好吧,不过得找个静得下来的时候写。

-

正事方面上半年还是挺让人失望的,没有达成一些目标,应该还是能力不足吧,技术方面分析一下还是停留在看的表面层,有些实操的,或者结合业务场景的能力不太行,算是在坚持写写 blog,主要是被这个每周一篇的目标推着走,有时会比较焦虑,内容产出也还比较差,希望能在后面有些改善,可能会降低频率,只是觉得降低了也不一定能有比较好的提升,无法战胜自己的惰性,所以暂时还是坚持下这个目标吧,还有就是 coding 能力,有时候也应该刷刷题,提升思维敏捷度,大脑用太少可能生锈了,况且本来就不是很有优势,虽然失望也只能继续努力吧,日拱一卒,来日方长,加油吧~😔

-

还有就是跑步减肥了,截止今天,上半年跑了 136 公里了,因为疫情影响,农历年后是从 4 月 17 号开始跑的,去年跑到了 300 公里,奖励自己了一个手表(真的挺后悔的,还不如 200 块买个手表),今年希望可以能在这个基础上再进一步,一直跟领导说,跑步算是我坚持下来的唯一一个好习惯了,618 买了个跑步机,周末回家了可以不受天气影响的多跑跑,不过如果天气好可能还是会出去跑跑,跑步机跑道多少还是有点拘束,只是感觉可能是我还是吃得太多了🤦‍♂️,效果不是很明显,还在 80 这个坎徘徊,等于浪费了大半年,可能是年初的项目太费心力,压力比较大,吃得更多,是不是可以算工伤😄,这方面也需要好好调整,可以放得开一点,虽然不太可能一下子到位,但是总要去努力下,随着年龄成长总要承担更多,也要看得开一点,没法事事如愿,尽力就好了,减肥这个事情还在结合一些俯卧撑啥的,希望也能坚持下去,加油吧,不知道原话怎么说的,意思是人类最大的勇敢就是看透了人世间的苦难,仍然热爱生活。我当然没可能让内心变得这么强大,试着去努力吧,奥力给!

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

-

工作篇

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

-]]>
- - 生活 - 年终总结 - - - 生活 - 年终总结 - 2021 - 拖更 - -
AQS篇二 之 Condition 浅析笔记 /2021/02/21/AQS-%E4%B9%8B-Condition-%E6%B5%85%E6%9E%90%E7%AC%94%E8%AE%B0/ @@ -904,6 +904,125 @@ public: aqs + + 2022 年终总结 + /2023/01/15/2022-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 一年又一年,时间匆匆,这一年过得不太容易,很多事情都是来得猝不及防,很多规划也照例是没有完成,今年更多了一些,又是比较丧的一篇总结
工作上的变化让我多理解了一些社会跟职场的现实吧,可能的确是我不够优秀,也可能是其他,说回我自身,在工作中今年应该是收获比较一般的一年,不能说没有,对原先不熟悉的业务的掌握程度有了比较大的提升,只是问题依旧存在,也挺难推动完全改变,只能尽自己所能,而这一点也主要是在团队中的定位因为前面说的一些原因,在前期不明确,限制比较大,虽然现在并没有完全解决,但也有了一些明显的改善,如果明年继续为这家公司服务,希望能有所突破,在人心沟通上的技巧总是比较反感,可也是不得不使用或者说被迫学习使用的,LD说我的对错观太强了,拗不过来,希望能有所改变。
长远的规划上没有什么明确的想法,很容易否定原来的各种想法,见识过各种现实的残酷,明白以前的一些想法不够全面或者比较幼稚,想有更上一层楼的机会,只是不希望是通过自己不认可的方式。比较能接受的是通过提升自己的技术和执行力,能够有更进一步的可能。
技术上是挺失败的去年跟前年还是能看一些书,学一些东西,今年少了很多,可能对原来比较熟悉的都有些遗忘,最近有在改善博客的内容,能更多的是系列化的,由浅入深,只是还很不完善,没什么规划,体系上也还不完整,不过还是以mybatis作为一个开头,后续新开始的内容或者原先写过的相关的都能做个整理,不再是想到啥就写点啥。最近的一个重点是在k8s上,学习方式跟一些特别优秀的人比起来还是会慢一些,不过也是自己的方法,能够更深入的理解整个体系,并讲解出来,可能会尝试采用视频的方式,对一些比较好的内容做尝试,看看会不会有比较好的数据和反馈,在22年还苟着周更的独立技术博客也算是比较稀有了的,其他站的发布也要勤一些,形成所谓的“矩阵”。
跑步减肥这个么还是比较惨,22年只跑了368公里,比21年少了85公里,有一些客观但很多是主观的原因,还是需要跑起来,只是减肥也很迫切,体重比较大跑步还是有些压力的,买了动感单车,就是时间稍长屁股痛这个目前比较难解决,骑还是每天在骑就是强度跟时间不太够,要保证每天30分钟的量可能会比较好。
加油吧,愿23年家人和自己都健康,顺遂。大家也一样。

+]]>
+ + 生活 + 年终总结 + + + 生活 + 年终总结 + 2022 + 2023 + +
+ + add-two-number + /2015/04/14/Add-Two-Number/ + problem

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

+

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

+

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

+ +

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

+

正确代码

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

失败的代码

/**
+ * Definition for singly-linked list.
+ * struct ListNode {
+ *     int val;
+ *     ListNode *next;
+ *     ListNode(int x) : val(x), next(NULL) {}
+ * };
+ */
+class Solution {
+public:
+    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
+        ListNode dummy(0);
+        ListNode* p = &dummy;
+
+        int cn = 0;
+        int flag = 0;
+        while(l1 || l2){
+            int val = cn + (l1 ? l1->val : 0) + (l2 ? l2->val : 0);
+            cn = val / 10;
+            val = val % 10;
+            p->next = new ListNode(val);
+            p = p->next;
+            if(!l1 && cn == 0){
+                flag = 1;
+                break;
+            }
+            if(!l2 && cn == 0){
+                flag = 1;
+                break;
+            }
+            if(l1){
+                l1 = l1->next;
+            }
+            if(l2){
+                l2 = l2->next;
+            }
+        }
+        if(!l1 && cn == 0 && flag == 1){
+            p->next = l2->next;
+        }
+        if(!l2 && cn == 0 && flag == 1){
+            p->next = l1->next;
+        }
+        if(cn != 0){
+            p->next = new ListNode(cn);
+            p = p->next;
+        }
+        return dummy.next;
+    }
+};
+]]>
+ + leetcode + + + leetcode + c++ + +
AbstractQueuedSynchronizer /2019/09/23/AbstractQueuedSynchronizer/ @@ -1357,11 +1476,64 @@ public: - Apollo 的 value 注解是怎么自动更新的 - /2020/11/01/Apollo-%E7%9A%84-value-%E6%B3%A8%E8%A7%A3%E6%98%AF%E6%80%8E%E4%B9%88%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%E7%9A%84/ - 在前司和目前公司,用的配置中心都是使用的 Apollo,经过了业界验证,比较强大的配置管理系统,特别是在0.10 后开始支持对使用 value 注解的配置值进行自动更新,今天刚好有个同学问到我,就顺便写篇文章记录下,其实也是借助于 spring 强大的 bean 生命周期管理,可以实现BeanPostProcessor接口,使用postProcessBeforeInitialization方法,来对bean 内部的属性和方法进行判断,是否有 value 注解,如果有就是将它注册到一个 map 中,可以看到这个方法com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField

-
@Override
-  protected void processField(Object bean, String beanName, Field field) {
+    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 + + + + Apollo 的 value 注解是怎么自动更新的 + /2020/11/01/Apollo-%E7%9A%84-value-%E6%B3%A8%E8%A7%A3%E6%98%AF%E6%80%8E%E4%B9%88%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%E7%9A%84/ + 在前司和目前公司,用的配置中心都是使用的 Apollo,经过了业界验证,比较强大的配置管理系统,特别是在0.10 后开始支持对使用 value 注解的配置值进行自动更新,今天刚好有个同学问到我,就顺便写篇文章记录下,其实也是借助于 spring 强大的 bean 生命周期管理,可以实现BeanPostProcessor接口,使用postProcessBeforeInitialization方法,来对bean 内部的属性和方法进行判断,是否有 value 注解,如果有就是将它注册到一个 map 中,可以看到这个方法com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField

+
@Override
+  protected void processField(Object bean, String beanName, Field field) {
     // register @Value on field
     Value value = field.getAnnotation(Value.class);
     if (value == null) {
@@ -1444,56 +1616,92 @@ 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;
-}
+ public void set(long value) { + this.value = value; + } - -

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.

+ public long getValue() { + return value; + } + + public void setValue(long value) { + this.value = value; + } +}
+

事件生产

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

事件处理器

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

主方法代码

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

运行下可以看到运行结果

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

]]>
- leetcode + Java - C++ - leetcode + Java + Disruptor
@@ -1598,124 +1806,179 @@ Node *clone(Node *graph) { - Disruptor 系列二 - /2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ - 这里开始慢慢深入的讲一下 disruptor,首先是 lock free , 相比于前面介绍的两个阻塞队列,
disruptor 本身是不直接使用锁的,因为本身的设计是单个线程去生产,通过 cas 来维护头指针,
不直接维护尾指针,这样就减少了锁的使用,提升了性能;第二个是这次介绍的重点,
减少 false sharing 的情况,也就是常说的 伪共享 问题,那么什么叫 伪共享 呢,
这里要扯到一些 cpu 缓存的知识,

譬如我在用的这个笔记本

这里就可能看到 L2 Cache 就是针对每个核的

这里可以看到现代 CPU 的结构里,分为三级缓存,越靠近 cpu 的速度越快,存储容量越小,
而 L1 跟 L2 是 CPU 核专属的每个核都有自己的 L1 和 L2 的,其中 L1 还分为数据和指令,
像我上面的图中显示的 L1 Cache 只有 64KB 大小,其中数据 32KB,指令 32KB,
而 L2 则有 256KB,L3 有 4MB,其中的 Line Size 是我们这里比较重要的一个值,
CPU 其实会就近地从 Cache 中读取数据,碰到 Cache Miss 就再往下一级 Cache 读取,
每次读取是按照缓存行 Cache Line 读取,并且也遵循了“就近原则”,
也就是相近的数据有可能也会马上被读取,所以以行的形式读取,然而这也造成了 false sharing
因为类似于 ArrayBlockingQueue,需要有 takeIndex , putIndex , count , 因为在同一个类中,
很有可能存在于同一个 Cache Line 中,但是这几个值会被不同的线程修改,
导致从 Cache 取出来以后立马就会被失效,所谓的就近原则也就没用了,
因为需要反复地标记 dirty 脏位,然后把 Cache 刷掉,就造成了false sharing这种情况
而在 disruptor 中则使用了填充的方式,让我的头指针能够不产生false sharing

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

+
EventHandlerGroup<T> createEventProcessors(
+        final Sequence[] barrierSequences,
+        final EventHandler<? super T>[] eventHandlers)
+    {
+        checkNotStarted();
 
-class Value extends LhsPadding
-{
-    protected volatile long value;
-}
+        final Sequence[] processorSequences = new Sequence[eventHandlers.length];
+        final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);
 
-class RhsPadding extends Value
-{
-    protected long p9, p10, p11, p12, p13, p14, p15;
-}
+        for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
+        {
+            final EventHandler<? super T> eventHandler = eventHandlers[i];
 
-/**
- * <p>Concurrent sequence class used for tracking the progress of
- * the ring buffer and event processors.  Support a number
- * of concurrent operations including CAS and order writes.
- *
- * <p>Also attempts to be more efficient with regards to false
- * sharing by adding padding around the volatile field.
- */
-public class Sequence extends RhsPadding
-{
-

通过代码可以看到,sequence 中其实真正有意义的是 value 字段,因为需要在多线程环境下可见也
使用了volatile 关键字,而 LhsPaddingRhsPadding 分别在value 前后填充了各
7 个 long 型的变量,long 型的变量在 Java 中是占用 8 bytes,这样就相当于不管怎么样,
value 都会单独使用一个缓存行,使得其不会产生 false sharing 的问题。

-]]>
- - Java - - - Java - Disruptor - - - - 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;
+            // 这里将 handler 包装成一个 BatchEventProcessor
+            final BatchEventProcessor<T> batchEventProcessor =
+                new BatchEventProcessor<>(ringBuffer, barrier, eventHandler);
 
-    public void set(long value) {
-        this.value = value;
-    }
+            if (exceptionHandler != null)
+            {
+                batchEventProcessor.setExceptionHandler(exceptionHandler);
+            }
 
-    public long getValue() {
-        return value;
-    }
+            consumerRepository.add(batchEventProcessor, eventHandler, barrier);
+            processorSequences[i] = batchEventProcessor.getSequence();
+        }
 
-    public void setValue(long value) {
-        this.value = value;
-    }
-}
-

事件生产

-
public class LongEventFactory implements EventFactory<LongEvent>
-{
-    public LongEvent newInstance()
+        updateGatingSequencesForNextInChain(barrierSequences, processorSequences);
+
+        return new EventHandlerGroup<>(this, consumerRepository, processorSequences);
+    }
+ +

BatchEventProcessor 在类内有个定义 sequence

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

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

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

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

+
private void updateGatingSequencesForNextInChain(final Sequence[] barrierSequences, final Sequence[] processorSequences)
     {
-        return new LongEvent();
-    }
-}
-

事件处理器

-
public class LongEventHandler implements EventHandler<LongEvent> {
+        if (processorSequences.length > 0)
+        {
+            // 然后在这里添加
+            ringBuffer.addGatingSequences(processorSequences);
+            for (final Sequence barrierSequence : barrierSequences)
+            {
+                ringBuffer.removeGatingSequence(barrierSequence);
+            }
+            consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
+        }
+    }
- // event 事件, - // sequence 当前的序列 - // 是否当前批次最后一个数据 - public void onEvent(LongEvent event, long sequence, boolean endOfBatch) +

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

+
public void run()
     {
-        String str = String.format("long event : %s l:%s b:%s", event.getValue(), sequence, endOfBatch);
-        System.out.println(str);
-    }
-}
-
-

主方法代码

-
package disruptor;
+        if (running.compareAndSet(IDLE, RUNNING))
+        {
+            sequenceBarrier.clearAlert();
 
-import com.lmax.disruptor.RingBuffer;
-import com.lmax.disruptor.dsl.Disruptor;
-import com.lmax.disruptor.util.DaemonThreadFactory;
+            notifyStart();
+            try
+            {
+                if (running.get() == RUNNING)
+                {
+                    processEvents();
+                }
+            }
+            finally
+            {
+                notifyShutdown();
+                running.set(IDLE);
+            }
+        }
+        else
+        {
+            // This is a little bit of guess work.  The running state could of changed to HALTED by
+            // this point.  However, Java does not have compareAndExchange which is the only way
+            // to get it exactly correct.
+            if (running.get() == RUNNING)
+            {
+                throw new IllegalStateException("Thread is already running");
+            }
+            else
+            {
+                earlyExit();
+            }
+        }
+    }
+

然后是

+
private void processEvents()
+    {
+        T event = null;
+        long nextSequence = sequence.get() + 1L;
 
-import java.nio.ByteBuffer;
+        while (true)
+        {
+            try
+            {
+                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
+                if (batchStartAware != null)
+                {
+                    batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
+                }
 
-public class LongEventMain
+                while (nextSequence <= availableSequence)
+                {
+                    event = dataProvider.get(nextSequence);
+                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
+                    nextSequence++;
+                }
+                // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
+                sequence.set(availableSequence);
+            }
+            catch (final TimeoutException e)
+            {
+                notifyTimeout(sequence.get());
+            }
+            catch (final AlertException ex)
+            {
+                if (running.get() != RUNNING)
+                {
+                    break;
+                }
+            }
+            catch (final Throwable ex)
+            {
+                handleEventException(ex, nextSequence, event);
+                // 如果是异常就只是 nextSequence
+                sequence.set(nextSequence);
+                nextSequence++;
+            }
+        }
+    }
+]]>
+ + Java + + + Java + Disruptor + +
+ + Disruptor 系列二 + /2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ + 这里开始慢慢深入的讲一下 disruptor,首先是 lock free , 相比于前面介绍的两个阻塞队列,
disruptor 本身是不直接使用锁的,因为本身的设计是单个线程去生产,通过 cas 来维护头指针,
不直接维护尾指针,这样就减少了锁的使用,提升了性能;第二个是这次介绍的重点,
减少 false sharing 的情况,也就是常说的 伪共享 问题,那么什么叫 伪共享 呢,
这里要扯到一些 cpu 缓存的知识,

譬如我在用的这个笔记本

这里就可能看到 L2 Cache 就是针对每个核的

这里可以看到现代 CPU 的结构里,分为三级缓存,越靠近 cpu 的速度越快,存储容量越小,
而 L1 跟 L2 是 CPU 核专属的每个核都有自己的 L1 和 L2 的,其中 L1 还分为数据和指令,
像我上面的图中显示的 L1 Cache 只有 64KB 大小,其中数据 32KB,指令 32KB,
而 L2 则有 256KB,L3 有 4MB,其中的 Line Size 是我们这里比较重要的一个值,
CPU 其实会就近地从 Cache 中读取数据,碰到 Cache Miss 就再往下一级 Cache 读取,
每次读取是按照缓存行 Cache Line 读取,并且也遵循了“就近原则”,
也就是相近的数据有可能也会马上被读取,所以以行的形式读取,然而这也造成了 false sharing
因为类似于 ArrayBlockingQueue,需要有 takeIndex , putIndex , count , 因为在同一个类中,
很有可能存在于同一个 Cache Line 中,但是这几个值会被不同的线程修改,
导致从 Cache 取出来以后立马就会被失效,所谓的就近原则也就没用了,
因为需要反复地标记 dirty 脏位,然后把 Cache 刷掉,就造成了false sharing这种情况
而在 disruptor 中则使用了填充的方式,让我的头指针能够不产生false sharing

+
class LhsPadding
 {
-    public static void main(String[] args) throws Exception
-    {
-        // 这个需要是 2 的幂次,这样在定位的时候只需要位移操作,也能减少各种计算操作
-        int bufferSize = 1024; 
-
-        Disruptor<LongEvent> disruptor = 
-                new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
+    protected long p1, p2, p3, p4, p5, p6, p7;
+}
 
-        // 类似于注册处理器
-        disruptor.handleEventsWith(new LongEventHandler());
-        // 或者直接用 lambda
-        disruptor.handleEventsWith((event, sequence, endOfBatch) ->
-                System.out.println("Event: " + event));
-        // 启动我们的 disruptor
-        disruptor.start(); 
+class Value extends LhsPadding
+{
+    protected volatile long value;
+}
 
+class RhsPadding extends Value
+{
+    protected long p9, p10, p11, p12, p13, p14, p15;
+}
 
-        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); 
-        ByteBuffer bb = ByteBuffer.allocate(8);
-        for (long l = 0; true; l++)
-        {
-            bb.putLong(0, l);
-            // 生产事件
-            ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
-            Thread.sleep(1000);
-        }
-    }
-}
-

运行下可以看到运行结果

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

+/** + * <p>Concurrent sequence class used for tracking the progress of + * the ring buffer and event processors. Support a number + * of concurrent operations including CAS and order writes. + * + * <p>Also attempts to be more efficient with regards to false + * sharing by adding padding around the volatile field. + */ +public class Sequence extends RhsPadding +{
+

通过代码可以看到,sequence 中其实真正有意义的是 value 字段,因为需要在多线程环境下可见也
使用了volatile 关键字,而 LhsPaddingRhsPadding 分别在value 前后填充了各
7 个 long 型的变量,long 型的变量在 Java 中是占用 8 bytes,这样就相当于不管怎么样,
value 都会单独使用一个缓存行,使得其不会产生 false sharing 的问题。

]]>
Java @@ -1726,116 +1989,35 @@ Node *clone(Node *graph) {
- add-two-number - /2015/04/14/Add-Two-Number/ - problem

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

-

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

-

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

- -

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

-

正确代码

/**
- * Definition for singly-linked list.
- * struct ListNode {
- *     int val;
- *     ListNode *next;
- *     ListNode(int x) : val(x), next(NULL) {}
- * };
- */
-class Solution {
-public:
-    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
-        ListNode dummy(0);
-        ListNode* p = &dummy;
+    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/ + 本来是想取个像现在那些公众号转了又转的文章标题,”面试官再问你xxxxx,就把这篇文章甩给他看”这种标题,但是觉得实在太 low 了,还是用一部我比较喜欢的电影里的一句台词,《人在囧途》里王宝强对着那张老板给他的欠条,看不懂字时候说的那句,这些都是些啥(第四声)
当我刚开始面 Java 的时候,其实我真的没注意这方面的东西,实话说就是不知道这些是啥,开发中用过 Interceptor和 Aop,了解 aop 的实现原理,但是不知道 Java web 中的 Filter 是怎么回事,知道 dubbo 的 filter,就这样,所以被问到了的确是回答不出来,可能就觉得这个渣渣,这么简单的都不会,所以还是花点时间来看看这个是个啥,为了避免我口吐芬芳,还是耐下性子来简单说下这几个东西
首先是 servlet,怎么去解释这个呢,因为之前是 PHPer,所以比较喜欢用它来举例子,在普通的 PHP 的 web 应用中一般有几部分组成,接受 HTTP 请求的是前置的 nginx 或者 apache,但是这俩玩意都是只能处理静态的请求,远古时代 PHP 和 HTML 混编是通过 apache 的 php module,跟后来 nginx 使用 php-fpm 其实道理类似,就是把请求中需要 PHP 处理的转发给 PHP,在 Java 中呢,是有个比较牛叉的叫 Tomcat 的,它可以把请求转成 servlet,而 servlet 其实就是一种实现了特定接口的 Java 代码,

+

+package javax.servlet;
 
-        int cn = 0;
-        while(l1 || l2){
-            int val = cn + (l1 ? l1->val : 0) + (l2 ? l2->val : 0);
-            cn = val / 10;
-            val = val % 10;
-            p->next = new ListNode(val);
-            p = p->next;
-            if(l1){
-                l1 = l1->next;
-            }
-            if(l2){
-                l2 = l2->next;
-            }
-        }
-        if(cn != 0){
-            p->next = new ListNode(cn);
-            p = p->next;
-        }
-        return dummy.next;
-    }
-};
- -

失败的代码

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

-

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

-
HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
-                                               uint gc_count_before,
-                                               bool* succeeded,
-                                               GCCause::Cause gc_cause) {
-  assert_heap_not_locked_and_not_at_safepoint();
-  VM_G1CollectForAllocation op(word_size,
-                               gc_count_before,
-                               gc_cause,
-                               false, /* should_initiate_conc_mark */
-                               g1_policy()->max_pause_time_ms());
-  VMThread::execute(&op);
-
-  HeapWord* result = op.result();
-  bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
-  assert(result == NULL || ret_succeeded,
-         "the result should be NULL if the VM did not succeed");
-  *succeeded = ret_succeeded;
+    Headscale初体验以及踩坑记
+    /2023/01/22/Headscale%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BB%A5%E5%8F%8A%E8%B8%A9%E5%9D%91%E8%AE%B0/
+    最近或者说很久以前就想着能够把几个散装服务器以及家里的网络连起来,譬如一些remote desktop的访问,之前搞了下frp,因为家里电脑没怎么注意安全性就被搞了一下,所以还是想用相对更安全的方式,比如限定ip和端口进行访问,但是感觉ip也不固定就比较难搞,后来看到了 TailscaleHeadscale 的方式,就想着试试看,没想到一开始就踩了几个比较莫名其妙的坑。
可以按官方文档去搭建,也可以在网上找一些其他人搭建的教程。我碰到的主要是关于配置文件的问题

+

第一个问题

Error initializing error="failed to read or create private key: failed to save private key to disk: open /etc/headscale/private.key: read-only file system"
+

其实一开始看到这个我都有点懵了,咋回事呢,read-only file system一般有可能是文件系统出问题了,不可写入,需要重启或者修改挂载方式,被这个错误的错误日志给误导了,后面才知道是配置文件,在另一个教程中也有个类似的回复,一开始没注意,其实就是同一个问题。
默认的配置文件是这样的

+
---
+# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
+#
+# - `/etc/headscale`
+# - `~/.headscale`
+# - current working directory
 
-  assert_heap_not_locked();
-  return result;
-}
-

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

-
void VM_G1CollectForAllocation::doit() {
-  G1CollectedHeap* g1h = G1CollectedHeap::heap();
-  assert(!_should_initiate_conc_mark || g1h->should_do_concurrent_full_gc(_gc_cause),
-      "only a GC locker, a System.gc(), stats update, whitebox, or a hum allocation induced GC should start a cycle");
+# The url clients will connect to.
+# Typically this will be a domain like:
+#
+# https://myheadscale.example.com:443
+#
+server_url: http://127.0.0.1:8080
 
-  if (_word_size > 0) {
-    // An allocation has been requested. So, try to do that first.
-    _result = g1h->attempt_allocation_at_safepoint(_word_size,
-                                                   false /* expect_null_cur_alloc_region */);
-    if (_result != NULL) {
-      // If we can successfully allocate before we actually do the
-      // pause then we will consider this pause successful.
-      _pause_succeeded = true;
-      return;
-    }
-  }
+# Address to listen to / bind to on the server
+#
+# For production:
+# listen_addr: 0.0.0.0:8080
+listen_addr: 127.0.0.1:8080
 
-  GCCauseSetter x(g1h, _gc_cause);
-  if (_should_initiate_conc_mark) {
-    // It's safer to read old_marking_cycles_completed() here, given
-    // that noone else will be updating it concurrently. Since we'll
-    // only need it if we're initiating a marking cycle, no point in
-    // setting it earlier.
-    _old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();
+# Address to listen to /metrics, you may want
+# to keep this endpoint private to your internal
+# network
+#
+metrics_listen_addr: 127.0.0.1:9090
 
-    // At this point we are supposed to start a concurrent cycle. We
-    // will do so if one is not already in progress.
-    bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);
+# Address to listen for gRPC.
+# gRPC is used for controlling a headscale server
+# remotely with the CLI
+# Note: Remote access _only_ works if you have
+# valid certificates.
+#
+# For production:
+# grpc_listen_addr: 0.0.0.0:50443
+grpc_listen_addr: 127.0.0.1:50443
 
-    // The above routine returns true if we were able to force the
-    // next GC pause to be an initial mark; it returns false if a
-    // marking cycle is already in progress.
-    //
-    // If a marking cycle is already in progress just return and skip the
-    // pause below - if the reason for requesting this initial mark pause
-    // was due to a System.gc() then the requesting thread should block in
-    // doit_epilogue() until the marking cycle is complete.
-    //
-    // If this initial mark pause was requested as part of a humongous
-    // allocation then we know that the marking cycle must just have
-    // been started by another thread (possibly also allocating a humongous
-    // object) as there was no active marking cycle when the requesting
-    // thread checked before calling collect() in
-    // attempt_allocation_humongous(). Retrying the GC, in this case,
-    // will cause the requesting thread to spin inside collect() until the
-    // just started marking cycle is complete - which may be a while. So
-    // we do NOT retry the GC.
-    if (!res) {
-      assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating");
-      if (_gc_cause != GCCause::_g1_humongous_allocation) {
-        _should_retry_gc = true;
-      }
-      return;
-    }
-  }
+# Allow the gRPC admin interface to run in INSECURE
+# mode. This is not recommended as the traffic will
+# be unencrypted. Only enable if you know what you
+# are doing.
+grpc_allow_insecure: false
 
-  // Try a partial collection of some kind.
-  _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
+# Private key used to encrypt the traffic between headscale
+# and Tailscale clients.
+# The private key file will be autogenerated if it's missing.
+#
+# For production:
+# /var/lib/headscale/private.key
+private_key_path: ./private.key
 
-  if (_pause_succeeded) {
-    if (_word_size > 0) {
-      // An allocation had been requested. Do it, eventually trying a stronger
-      // kind of GC.
-      _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded);
-    } else {
-      bool should_upgrade_to_full = !g1h->should_do_concurrent_full_gc(_gc_cause) &&
-                                    !g1h->has_regions_left_for_allocation();
-      if (should_upgrade_to_full) {
-        // There has been a request to perform a GC to free some space. We have no
-        // information on how much memory has been asked for. In case there are
-        // absolutely no regions left to allocate into, do a maximally compacting full GC.
-        log_info(gc, ergo)("Attempting maximally compacting collection");
-        _pause_succeeded = g1h->do_full_collection(false, /* explicit gc */
-                                                   true   /* clear_all_soft_refs */);
-      }
-    }
-    guarantee(_pause_succeeded, "Elevated collections during the safepoint must always succeed.");
-  } else {
-    assert(_result == NULL, "invariant");
-    // The only reason for the pause to not be successful is that, the GC locker is
-    // active (or has become active since the prologue was executed). In this case
-    // we should retry the pause after waiting for the GC locker to become inactive.
-    _should_retry_gc = true;
-  }
-}
-

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

-
G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {
-  assert_at_safepoint_on_vm_thread();
-  guarantee(!is_gc_active(), "collection is not reentrant");
+# The Noise section includes specific configuration for the
+# TS2021 Noise protocol
+noise:
+  # The Noise private key is used to encrypt the
+  # traffic between headscale and Tailscale clients when
+  # using the new Noise-based protocol. It must be different
+  # from the legacy private key.
+  #
+  # For production:
+  # private_key_path: /var/lib/headscale/noise_private.key
+  private_key_path: ./noise_private.key
 
-  if (GCLocker::check_active_before_gc()) {
-    return false;
-  }
+# List of IP prefixes to allocate tailaddresses from.
+# Each prefix consists of either an IPv4 or IPv6 address,
+# and the associated prefix length, delimited by a slash.
+# While this looks like it can take arbitrary values, it
+# needs to be within IP ranges supported by the Tailscale
+# client.
+# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
+# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
+ip_prefixes:
+  - fd7a:115c:a1e0::/48
+  - 100.64.0.0/10
 
-  _gc_timer_stw->register_gc_start();
+# DERP is a relay system that Tailscale uses when a direct
+# connection cannot be established.
+# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
+#
+# headscale needs a list of DERP servers that can be presented
+# to the clients.
+derp:
+  server:
+    # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
+    # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
+    enabled: false
 
-  GCIdMark gc_id_mark;
-  _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
+    # Region ID to use for the embedded DERP server.
+    # The local DERP prevails if the region ID collides with other region ID coming from
+    # the regular DERP config.
+    region_id: 999
 
-  SvcGCMarker sgcm(SvcGCMarker::MINOR);
-  ResourceMark rm;
+    # Region code and name are displayed in the Tailscale UI to identify a DERP region
+    region_code: "headscale"
+    region_name: "Headscale Embedded DERP"
 
-  g1_policy()->note_gc_start();
+    # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
+    # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
+    #
+    # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
+    stun_listen_addr: "0.0.0.0:3478"
 
-  wait_for_root_region_scanning();
+  # List of externally available DERP maps encoded in JSON
+  urls:
+    - https://controlplane.tailscale.com/derpmap/default
 
-  print_heap_before_gc();
-  print_heap_regions();
-  trace_heap_before_gc(_gc_tracer_stw);
+  # Locally available DERP map files encoded in YAML
+  #
+  # This option is mostly interesting for people hosting
+  # their own DERP servers:
+  # https://tailscale.com/kb/1118/custom-derp-servers/
+  #
+  # paths:
+  #   - /etc/headscale/derp-example.yaml
+  paths: []
 
-  _verifier->verify_region_sets_optional();
-  _verifier->verify_dirty_young_regions();
+  # If enabled, a worker will be set up to periodically
+  # refresh the given sources and update the derpmap
+  # will be set up.
+  auto_update_enabled: true
 
-  // We should not be doing initial mark unless the conc mark thread is running
-  if (!_cm_thread->should_terminate()) {
-    // This call will decide whether this pause is an initial-mark
-    // pause. If it is, in_initial_mark_gc() will return true
-    // for the duration of this pause.
-    g1_policy()->decide_on_conc_mark_initiation();
-  }
+  # How often should we check for DERP updates?
+  update_frequency: 24h
 
-  // We do not allow initial-mark to be piggy-backed on a mixed GC.
-  assert(!collector_state()->in_initial_mark_gc() ||
-          collector_state()->in_young_only_phase(), "sanity");
+# Disables the automatic check for headscale updates on startup
+disable_check_updates: false
 
-  // We also do not allow mixed GCs during marking.
-  assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
+# Time before an inactive ephemeral node is deleted?
+ephemeral_node_inactivity_timeout: 30m
 
-  // Record whether this pause is an initial mark. When the current
-  // thread has completed its logging output and it's safe to signal
-  // the CM thread, the flag's value in the policy has been reset.
-  bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
+# Period to check for node updates within the tailnet. A value too low will severely affect
+# CPU consumption of Headscale. A value too high (over 60s) will cause problems
+# for the nodes, as they won't get updates or keep alive messages frequently enough.
+# In case of doubts, do not touch the default 10s.
+node_update_check_interval: 10s
 
-  // Inner scope for scope based logging, timers, and stats collection
-  {
-    EvacuationInfo evacuation_info;
+# SQLite config
+db_type: sqlite3
 
-    if (collector_state()->in_initial_mark_gc()) {
-      // We are about to start a marking cycle, so we increment the
-      // full collection counter.
-      increment_old_marking_cycles_started();
-      _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
-    }
+# For production:
+# db_path: /var/lib/headscale/db.sqlite
+db_path: ./db.sqlite
 
-    _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
+# # Postgres config
+# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
+# db_type: postgres
+# db_host: localhost
+# db_port: 5432
+# db_name: headscale
+# db_user: foo
+# db_pass: bar
 
-    GCTraceCPUTime tcpu;
+# If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
+# in the 'db_ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
+# db_ssl: false
 
-    G1HeapVerifier::G1VerifyType verify_type;
-    FormatBuffer<> gc_string("Pause Young ");
-    if (collector_state()->in_initial_mark_gc()) {
-      gc_string.append("(Concurrent Start)");
-      verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
-    } else if (collector_state()->in_young_only_phase()) {
-      if (collector_state()->in_young_gc_before_mixed()) {
-        gc_string.append("(Prepare Mixed)");
-      } else {
-        gc_string.append("(Normal)");
-      }
-      verify_type = G1HeapVerifier::G1VerifyYoungNormal;
-    } else {
-      gc_string.append("(Mixed)");
-      verify_type = G1HeapVerifier::G1VerifyMixed;
-    }
-    GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
+### TLS configuration
+#
+## Let's encrypt / ACME
+#
+# headscale supports automatically requesting and setting up
+# TLS for a domain with Let's Encrypt.
+#
+# URL to ACME directory
+acme_url: https://acme-v02.api.letsencrypt.org/directory
 
-    uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
-                                                                  workers()->active_workers(),
-                                                                  Threads::number_of_non_daemon_threads());
-    active_workers = workers()->update_active_workers(active_workers);
-    log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
+# Email to register with ACME provider
+acme_email: ""
 
-    TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
-    TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
-                                collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
+# Domain name to request a TLS certificate for:
+tls_letsencrypt_hostname: ""
 
-    G1HeapTransition heap_transition(this);
-    size_t heap_used_bytes_before_gc = used();
+# Path to store certificates and metadata needed by
+# letsencrypt
+# For production:
+# tls_letsencrypt_cache_dir: /var/lib/headscale/cache
+tls_letsencrypt_cache_dir: ./cache
 
-    // Don't dynamically change the number of GC threads this early.  A value of
-    // 0 is used to indicate serial work.  When parallel work is done,
-    // it will be set.
+# Type of ACME challenge to use, currently supported types:
+# HTTP-01 or TLS-ALPN-01
+# See [docs/tls.md](docs/tls.md) for more information
+tls_letsencrypt_challenge_type: HTTP-01
+# When HTTP-01 challenge is chosen, letsencrypt must set up a
+# verification endpoint, and it will be listening on:
+# :http = port 80
+tls_letsencrypt_listen: ":http"
 
-    { // Call to jvmpi::post_class_unload_events must occur outside of active GC
-      IsGCActiveMark x;
+## Use already defined certificates:
+tls_cert_path: ""
+tls_key_path: ""
 
-      gc_prologue(false);
+log:
+  # Output formatting for logs: text or json
+  format: text
+  level: info
 
-      if (VerifyRememberedSets) {
-        log_info(gc, verify)("[Verifying RemSets before GC]");
-        VerifyRegionRemSetClosure v_cl;
-        heap_region_iterate(&v_cl);
-      }
+# Path to a file containg ACL policies.
+# ACLs can be defined as YAML or HUJSON.
+# https://tailscale.com/kb/1018/acls/
+acl_policy_path: ""
 
-      _verifier->verify_before_gc(verify_type);
+## DNS
+#
+# headscale supports Tailscale's DNS configuration and MagicDNS.
+# Please have a look to their KB to better understand the concepts:
+#
+# - https://tailscale.com/kb/1054/dns/
+# - https://tailscale.com/kb/1081/magicdns/
+# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
+#
+dns_config:
+  # Whether to prefer using Headscale provided DNS or use local.
+  override_local_dns: true
 
-      _verifier->check_bitmaps("GC Start");
+  # List of DNS servers to expose to clients.
+  nameservers:
+    - 1.1.1.1
 
-#if COMPILER2_OR_JVMCI
-      DerivedPointerTable::clear();
-#endif
+  # NextDNS (see https://tailscale.com/kb/1218/nextdns/).
+  # "abc123" is example NextDNS ID, replace with yours.
+  #
+  # With metadata sharing:
+  # nameservers:
+  #   - https://dns.nextdns.io/abc123
+  #
+  # Without metadata sharing:
+  # nameservers:
+  #   - 2a07:a8c0::ab:c123
+  #   - 2a07:a8c1::ab:c123
 
-      // Please see comment in g1CollectedHeap.hpp and
-      // G1CollectedHeap::ref_processing_init() to see how
-      // reference processing currently works in G1.
+  # Split DNS (see https://tailscale.com/kb/1054/dns/),
+  # list of search domains and the DNS to query for each one.
+  #
+  # restricted_nameservers:
+  #   foo.bar.com:
+  #     - 1.1.1.1
+  #   darp.headscale.net:
+  #     - 1.1.1.1
+  #     - 8.8.8.8
 
-      // Enable discovery in the STW reference processor
-      _ref_processor_stw->enable_discovery();
+  # Search domains to inject.
+  domains: []
 
-      {
-        // We want to temporarily turn off discovery by the
-        // CM ref processor, if necessary, and turn it back on
-        // on again later if we do. Using a scoped
-        // NoRefDiscovery object will do this.
-        NoRefDiscovery no_cm_discovery(_ref_processor_cm);
+  # Extra DNS records
+  # so far only A-records are supported (on the tailscale side)
+  # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
+  # extra_records:
+  #   - name: "grafana.myvpn.example.com"
+  #     type: "A"
+  #     value: "100.64.0.3"
+  #
+  #   # you can also put it in one line
+  #   - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
 
-        // Forget the current alloc region (we might even choose it to be part
-        // of the collection set!).
-        _allocator->release_mutator_alloc_region();
+  # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
+  # Only works if there is at least a nameserver defined.
+  magic_dns: true
 
-        // This timing is only used by the ergonomics to handle our pause target.
-        // It is unclear why this should not include the full pause. We will
-        // investigate this in CR 7178365.
-        //
-        // Preserving the old comment here if that helps the investigation:
-        //
-        // The elapsed time induced by the start time below deliberately elides
-        // the possible verification above.
-        double sample_start_time_sec = os::elapsedTime();
+  # Defines the base domain to create the hostnames for MagicDNS.
+  # `base_domain` must be a FQDNs, without the trailing dot.
+  # The FQDN of the hosts will be
+  # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_).
+  base_domain: example.com
 
-        g1_policy()->record_collection_pause_start(sample_start_time_sec);
+# Unix socket used for the CLI to connect without authentication
+# Note: for production you will want to set this to something like:
+# unix_socket: /var/run/headscale.sock
+unix_socket: ./headscale.sock
+unix_socket_permission: "0770"
+#
+# headscale supports experimental OpenID connect support,
+# it is still being tested and might have some bugs, please
+# help us test it.
+# OpenID Connect
+# oidc:
+#   only_start_if_oidc_is_available: true
+#   issuer: "https://your-oidc.issuer.com/path"
+#   client_id: "your-oidc-client-id"
+#   client_secret: "your-oidc-client-secret"
+#   # Alternatively, set `client_secret_path` to read the secret from the file.
+#   # It resolves environment variables, making integration to systemd's
+#   # `LoadCredential` straightforward:
+#   client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
+#   # client_secret and client_secret_path are mutually exclusive.
+#
+#   Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
+#   parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
+#
+#   scope: ["openid", "profile", "email", "custom"]
+#   extra_params:
+#     domain_hint: example.com
+#
+#   List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
+#   authentication request will be rejected.
+#
+#   allowed_domains:
+#     - example.com
+# Groups from keycloak have a leading '/'
+#   allowed_groups:
+#     - /headscale
+#   allowed_users:
+#     - alice@example.com
+#
+#   If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
+#   This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
+#   If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
+#   user: `first-name.last-name.example.com`
+#
+#   strip_email_domain: true
 
-        if (collector_state()->in_initial_mark_gc()) {
-          concurrent_mark()->pre_initial_mark();
-        }
+# Logtail configuration
+# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
+# to instruct tailscale nodes to log their activity to a remote server.
+logtail:
+  # Enable logtail for this headscales clients.
+  # As there is currently no support for overriding the log server in headscale, this is
+  # disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
+  enabled: false
 
-        g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
+# Enabling this option makes devices prefer a random port for WireGuard traffic over the
+# default static port 41641. This option is intended as a workaround for some buggy
+# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
+randomize_client_port: false
- evacuation_info.set_collectionset_regions(collection_set()->region_length()); +

问题就是出在几个文件路径的配置,默认都是当前目录,也就是headscale的可执行文件所在目录,需要按它配置说明中的生产配置进行修改

+
# For production:
+# /var/lib/headscale/private.key
+private_key_path: /var/lib/headscale/private.key
+

直接改成绝对路径就好了,还有两个文件路径
另一个也是个秘钥的路径问题

+
noise:
+  # The Noise private key is used to encrypt the
+  # traffic between headscale and Tailscale clients when
+  # using the new Noise-based protocol. It must be different
+  # from the legacy private key.
+  #
+  # For production:
+  # private_key_path: /var/lib/headscale/noise_private.key
+  private_key_path: /var/lib/headscale/noise_private.key
+

第二个问题

这个问题也是一种误导,
错误信息是

+
Error initializing error="unable to open database file: out of memory (14)"
+

这就是个文件,内存也完全没有被占满的迹象,原来也是文件路径的问题

+
# For production:
+# db_path: /var/lib/headscale/db.sqlite
+db_path: /var/lib/headscale/db.sqlite
+

都改成绝对路径就可以了,然后这里还有个就是要对/var/lib/headscale//etc/headscale/等路径赋予headscale用户权限,有时候对这类问题的排查真的蛮头疼,日志报错都不是真实的错误信息,开源项目对这些错误的提示真的也需要优化,后续的譬如mac也加入节点等后面再开篇讲

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

+
HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
+                                               uint gc_count_before,
+                                               bool* succeeded,
+                                               GCCause::Cause gc_cause) {
+  assert_heap_not_locked_and_not_at_safepoint();
+  VM_G1CollectForAllocation op(word_size,
+                               gc_count_before,
+                               gc_cause,
+                               false, /* should_initiate_conc_mark */
+                               g1_policy()->max_pause_time_ms());
+  VMThread::execute(&op);
 
-        // Make sure the remembered sets are up to date. This needs to be
-        // done before register_humongous_regions_with_cset(), because the
-        // remembered sets are used there to choose eager reclaim candidates.
-        // If the remembered sets are not up to date we might miss some
-        // entries that need to be handled.
-        g1_rem_set()->cleanupHRRS();
+  HeapWord* result = op.result();
+  bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
+  assert(result == NULL || ret_succeeded,
+         "the result should be NULL if the VM did not succeed");
+  *succeeded = ret_succeeded;
 
-        register_humongous_regions_with_cset();
+  assert_heap_not_locked();
+  return result;
+}
+

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

+
void VM_G1CollectForAllocation::doit() {
+  G1CollectedHeap* g1h = G1CollectedHeap::heap();
+  assert(!_should_initiate_conc_mark || g1h->should_do_concurrent_full_gc(_gc_cause),
+      "only a GC locker, a System.gc(), stats update, whitebox, or a hum allocation induced GC should start a cycle");
 
-        assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
+  if (_word_size > 0) {
+    // An allocation has been requested. So, try to do that first.
+    _result = g1h->attempt_allocation_at_safepoint(_word_size,
+                                                   false /* expect_null_cur_alloc_region */);
+    if (_result != NULL) {
+      // If we can successfully allocate before we actually do the
+      // pause then we will consider this pause successful.
+      _pause_succeeded = true;
+      return;
+    }
+  }
 
-        // We call this after finalize_cset() to
-        // ensure that the CSet has been finalized.
-        _cm->verify_no_cset_oops();
+  GCCauseSetter x(g1h, _gc_cause);
+  if (_should_initiate_conc_mark) {
+    // It's safer to read old_marking_cycles_completed() here, given
+    // that noone else will be updating it concurrently. Since we'll
+    // only need it if we're initiating a marking cycle, no point in
+    // setting it earlier.
+    _old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();
 
-        if (_hr_printer.is_active()) {
-          G1PrintCollectionSetClosure cl(&_hr_printer);
-          _collection_set.iterate(&cl);
-        }
+    // At this point we are supposed to start a concurrent cycle. We
+    // will do so if one is not already in progress.
+    bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);
 
-        // Initialize the GC alloc regions.
-        _allocator->init_gc_alloc_regions(evacuation_info);
+    // The above routine returns true if we were able to force the
+    // next GC pause to be an initial mark; it returns false if a
+    // marking cycle is already in progress.
+    //
+    // If a marking cycle is already in progress just return and skip the
+    // pause below - if the reason for requesting this initial mark pause
+    // was due to a System.gc() then the requesting thread should block in
+    // doit_epilogue() until the marking cycle is complete.
+    //
+    // If this initial mark pause was requested as part of a humongous
+    // allocation then we know that the marking cycle must just have
+    // been started by another thread (possibly also allocating a humongous
+    // object) as there was no active marking cycle when the requesting
+    // thread checked before calling collect() in
+    // attempt_allocation_humongous(). Retrying the GC, in this case,
+    // will cause the requesting thread to spin inside collect() until the
+    // just started marking cycle is complete - which may be a while. So
+    // we do NOT retry the GC.
+    if (!res) {
+      assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating");
+      if (_gc_cause != GCCause::_g1_humongous_allocation) {
+        _should_retry_gc = true;
+      }
+      return;
+    }
+  }
 
-        G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
-        pre_evacuate_collection_set();
+  // Try a partial collection of some kind.
+  _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
 
-        // Actually do the work...
-        evacuate_collection_set(&per_thread_states);
+  if (_pause_succeeded) {
+    if (_word_size > 0) {
+      // An allocation had been requested. Do it, eventually trying a stronger
+      // kind of GC.
+      _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded);
+    } else {
+      bool should_upgrade_to_full = !g1h->should_do_concurrent_full_gc(_gc_cause) &&
+                                    !g1h->has_regions_left_for_allocation();
+      if (should_upgrade_to_full) {
+        // There has been a request to perform a GC to free some space. We have no
+        // information on how much memory has been asked for. In case there are
+        // absolutely no regions left to allocate into, do a maximally compacting full GC.
+        log_info(gc, ergo)("Attempting maximally compacting collection");
+        _pause_succeeded = g1h->do_full_collection(false, /* explicit gc */
+                                                   true   /* clear_all_soft_refs */);
+      }
+    }
+    guarantee(_pause_succeeded, "Elevated collections during the safepoint must always succeed.");
+  } else {
+    assert(_result == NULL, "invariant");
+    // The only reason for the pause to not be successful is that, the GC locker is
+    // active (or has become active since the prologue was executed). In this case
+    // we should retry the pause after waiting for the GC locker to become inactive.
+    _should_retry_gc = true;
+  }
+}
+

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

+
G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {
+  assert_at_safepoint_on_vm_thread();
+  guarantee(!is_gc_active(), "collection is not reentrant");
 
-        post_evacuate_collection_set(evacuation_info, &per_thread_states);
+  if (GCLocker::check_active_before_gc()) {
+    return false;
+  }
 
-        const size_t* surviving_young_words = per_thread_states.surviving_young_words();
-        free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
+  _gc_timer_stw->register_gc_start();
 
-        eagerly_reclaim_humongous_regions();
+  GCIdMark gc_id_mark;
+  _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
 
-        record_obj_copy_mem_stats();
-        _survivor_evac_stats.adjust_desired_plab_sz();
-        _old_evac_stats.adjust_desired_plab_sz();
+  SvcGCMarker sgcm(SvcGCMarker::MINOR);
+  ResourceMark rm;
 
-        double start = os::elapsedTime();
-        start_new_collection_set();
+  g1_policy()->note_gc_start();
+
+  wait_for_root_region_scanning();
+
+  print_heap_before_gc();
+  print_heap_regions();
+  trace_heap_before_gc(_gc_tracer_stw);
+
+  _verifier->verify_region_sets_optional();
+  _verifier->verify_dirty_young_regions();
+
+  // We should not be doing initial mark unless the conc mark thread is running
+  if (!_cm_thread->should_terminate()) {
+    // This call will decide whether this pause is an initial-mark
+    // pause. If it is, in_initial_mark_gc() will return true
+    // for the duration of this pause.
+    g1_policy()->decide_on_conc_mark_initiation();
+  }
+
+  // We do not allow initial-mark to be piggy-backed on a mixed GC.
+  assert(!collector_state()->in_initial_mark_gc() ||
+          collector_state()->in_young_only_phase(), "sanity");
+
+  // We also do not allow mixed GCs during marking.
+  assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
+
+  // Record whether this pause is an initial mark. When the current
+  // thread has completed its logging output and it's safe to signal
+  // the CM thread, the flag's value in the policy has been reset.
+  bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
+
+  // Inner scope for scope based logging, timers, and stats collection
+  {
+    EvacuationInfo evacuation_info;
+
+    if (collector_state()->in_initial_mark_gc()) {
+      // We are about to start a marking cycle, so we increment the
+      // full collection counter.
+      increment_old_marking_cycles_started();
+      _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
+    }
+
+    _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
+
+    GCTraceCPUTime tcpu;
+
+    G1HeapVerifier::G1VerifyType verify_type;
+    FormatBuffer<> gc_string("Pause Young ");
+    if (collector_state()->in_initial_mark_gc()) {
+      gc_string.append("(Concurrent Start)");
+      verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
+    } else if (collector_state()->in_young_only_phase()) {
+      if (collector_state()->in_young_gc_before_mixed()) {
+        gc_string.append("(Prepare Mixed)");
+      } else {
+        gc_string.append("(Normal)");
+      }
+      verify_type = G1HeapVerifier::G1VerifyYoungNormal;
+    } else {
+      gc_string.append("(Mixed)");
+      verify_type = G1HeapVerifier::G1VerifyMixed;
+    }
+    GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
+
+    uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
+                                                                  workers()->active_workers(),
+                                                                  Threads::number_of_non_daemon_threads());
+    active_workers = workers()->update_active_workers(active_workers);
+    log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
+
+    TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
+    TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
+                                collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
+
+    G1HeapTransition heap_transition(this);
+    size_t heap_used_bytes_before_gc = used();
+
+    // Don't dynamically change the number of GC threads this early.  A value of
+    // 0 is used to indicate serial work.  When parallel work is done,
+    // it will be set.
+
+    { // Call to jvmpi::post_class_unload_events must occur outside of active GC
+      IsGCActiveMark x;
+
+      gc_prologue(false);
+
+      if (VerifyRememberedSets) {
+        log_info(gc, verify)("[Verifying RemSets before GC]");
+        VerifyRegionRemSetClosure v_cl;
+        heap_region_iterate(&v_cl);
+      }
+
+      _verifier->verify_before_gc(verify_type);
+
+      _verifier->check_bitmaps("GC Start");
+
+#if COMPILER2_OR_JVMCI
+      DerivedPointerTable::clear();
+#endif
+
+      // Please see comment in g1CollectedHeap.hpp and
+      // G1CollectedHeap::ref_processing_init() to see how
+      // reference processing currently works in G1.
+
+      // Enable discovery in the STW reference processor
+      _ref_processor_stw->enable_discovery();
+
+      {
+        // We want to temporarily turn off discovery by the
+        // CM ref processor, if necessary, and turn it back on
+        // on again later if we do. Using a scoped
+        // NoRefDiscovery object will do this.
+        NoRefDiscovery no_cm_discovery(_ref_processor_cm);
+
+        // Forget the current alloc region (we might even choose it to be part
+        // of the collection set!).
+        _allocator->release_mutator_alloc_region();
+
+        // This timing is only used by the ergonomics to handle our pause target.
+        // It is unclear why this should not include the full pause. We will
+        // investigate this in CR 7178365.
+        //
+        // Preserving the old comment here if that helps the investigation:
+        //
+        // The elapsed time induced by the start time below deliberately elides
+        // the possible verification above.
+        double sample_start_time_sec = os::elapsedTime();
+
+        g1_policy()->record_collection_pause_start(sample_start_time_sec);
+
+        if (collector_state()->in_initial_mark_gc()) {
+          concurrent_mark()->pre_initial_mark();
+        }
+
+        g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
+
+        evacuation_info.set_collectionset_regions(collection_set()->region_length());
+
+        // Make sure the remembered sets are up to date. This needs to be
+        // done before register_humongous_regions_with_cset(), because the
+        // remembered sets are used there to choose eager reclaim candidates.
+        // If the remembered sets are not up to date we might miss some
+        // entries that need to be handled.
+        g1_rem_set()->cleanupHRRS();
+
+        register_humongous_regions_with_cset();
+
+        assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
+
+        // We call this after finalize_cset() to
+        // ensure that the CSet has been finalized.
+        _cm->verify_no_cset_oops();
+
+        if (_hr_printer.is_active()) {
+          G1PrintCollectionSetClosure cl(&_hr_printer);
+          _collection_set.iterate(&cl);
+        }
+
+        // Initialize the GC alloc regions.
+        _allocator->init_gc_alloc_regions(evacuation_info);
+
+        G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
+        pre_evacuate_collection_set();
+
+        // Actually do the work...
+        evacuate_collection_set(&per_thread_states);
+
+        post_evacuate_collection_set(evacuation_info, &per_thread_states);
+
+        const size_t* surviving_young_words = per_thread_states.surviving_young_words();
+        free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
+
+        eagerly_reclaim_humongous_regions();
+
+        record_obj_copy_mem_stats();
+        _survivor_evac_stats.adjust_desired_plab_sz();
+        _old_evac_stats.adjust_desired_plab_sz();
+
+        double start = os::elapsedTime();
+        start_new_collection_set();
         g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
 
         if (evacuation_failed()) {
@@ -3078,81 +3596,6 @@ public:
         题解
       
   
-  
-    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 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 /2020/10/25/Leetcode-104-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6-Maximum-Depth-of-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ @@ -3198,10 +3641,10 @@ Output: 0 @@ -3243,6 +3686,81 @@ Output: 0 + + 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 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/ @@ -3300,9 +3818,9 @@ inorder = [9,3,15,20,7] - - Disruptor 系列三 - /2022/09/25/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%89/ - 原来一直有点被误导,
gatingSequences用来标识每个 processer 的操作位点,但是怎么记录更新有点搞不清楚
其实问题在于 gatingSequences 是个 Sequence 数组,首先要看下怎么加进去的,
可以看到是在 com.lmax.disruptor.RingBuffer#addGatingSequences 这个方法里添加
首先是 com.lmax.disruptor.dsl.Disruptor#handleEventsWith(com.lmax.disruptor.EventHandler<? super T>...)
然后执行 com.lmax.disruptor.dsl.Disruptor#createEventProcessors(com.lmax.disruptor.Sequence[], com.lmax.disruptor.EventHandler<? super T>[])

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

BatchEventProcessor 在类内有个定义 sequence

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

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

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

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

-
private void updateGatingSequencesForNextInChain(final Sequence[] barrierSequences, final Sequence[] processorSequences)
-    {
-        if (processorSequences.length > 0)
-        {
-            // 然后在这里添加
-            ringBuffer.addGatingSequences(processorSequences);
-            for (final Sequence barrierSequence : barrierSequences)
-            {
-                ringBuffer.removeGatingSequence(barrierSequence);
-            }
-            consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
-        }
-    }
- -

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

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

然后是

-
private void processEvents()
-    {
-        T event = null;
-        long nextSequence = sequence.get() + 1L;
-
-        while (true)
-        {
-            try
-            {
-                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
-                if (batchStartAware != null)
-                {
-                    batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
-                }
-
-                while (nextSequence <= availableSequence)
-                {
-                    event = dataProvider.get(nextSequence);
-                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
-                    nextSequence++;
-                }
-                // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
-                sequence.set(availableSequence);
-            }
-            catch (final TimeoutException e)
-            {
-                notifyTimeout(sequence.get());
-            }
-            catch (final AlertException ex)
-            {
-                if (running.get() != RUNNING)
-                {
-                    break;
-                }
-            }
-            catch (final Throwable ex)
-            {
-                handleEventException(ex, nextSequence, event);
-                // 如果是异常就只是 nextSequence
-                sequence.set(nextSequence);
-                nextSequence++;
-            }
-        }
-    }
-]]>
- - Java - - - Java - Disruptor - -
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/ @@ -3575,40 +3949,77 @@ inorder = [9,3,15,20,7] - 2022 年终总结 - /2023/01/15/2022-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 一年又一年,时间匆匆,这一年过得不太容易,很多事情都是来得猝不及防,很多规划也照例是没有完成,今年更多了一些,又是比较丧的一篇总结
工作上的变化让我多理解了一些社会跟职场的现实吧,可能的确是我不够优秀,也可能是其他,说回我自身,在工作中今年应该是收获比较一般的一年,不能说没有,对原先不熟悉的业务的掌握程度有了比较大的提升,只是问题依旧存在,也挺难推动完全改变,只能尽自己所能,而这一点也主要是在团队中的定位因为前面说的一些原因,在前期不明确,限制比较大,虽然现在并没有完全解决,但也有了一些明显的改善,如果明年继续为这家公司服务,希望能有所突破,在人心沟通上的技巧总是比较反感,可也是不得不使用或者说被迫学习使用的,LD说我的对错观太强了,拗不过来,希望能有所改变。
长远的规划上没有什么明确的想法,很容易否定原来的各种想法,见识过各种现实的残酷,明白以前的一些想法不够全面或者比较幼稚,想有更上一层楼的机会,只是不希望是通过自己不认可的方式。比较能接受的是通过提升自己的技术和执行力,能够有更进一步的可能。
技术上是挺失败的去年跟前年还是能看一些书,学一些东西,今年少了很多,可能对原来比较熟悉的都有些遗忘,最近有在改善博客的内容,能更多的是系列化的,由浅入深,只是还很不完善,没什么规划,体系上也还不完整,不过还是以mybatis作为一个开头,后续新开始的内容或者原先写过的相关的都能做个整理,不再是想到啥就写点啥。最近的一个重点是在k8s上,学习方式跟一些特别优秀的人比起来还是会慢一些,不过也是自己的方法,能够更深入的理解整个体系,并讲解出来,可能会尝试采用视频的方式,对一些比较好的内容做尝试,看看会不会有比较好的数据和反馈,在22年还苟着周更的独立技术博客也算是比较稀有了的,其他站的发布也要勤一些,形成所谓的“矩阵”。
跑步减肥这个么还是比较惨,22年只跑了368公里,比21年少了85公里,有一些客观但很多是主观的原因,还是需要跑起来,只是减肥也很迫切,体重比较大跑步还是有些压力的,买了动感单车,就是时间稍长屁股痛这个目前比较难解决,骑还是每天在骑就是强度跟时间不太够,要保证每天30分钟的量可能会比较好。
加油吧,愿23年家人和自己都健康,顺遂。大家也一样。

-]]>
- - 生活 - 年终总结 - - - 生活 - 年终总结 - 2022 - 2023 - -
- - 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
  • + Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析 + /2021/01/24/Leetcode-124-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C-Binary-Tree-Maximum-Path-Sum-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

    简要分析

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

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

    +

    代码

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

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

    +

    结果图

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

    +]]>
    + + Java + leetcode + Binary Tree + java + Binary Tree + + + leetcode + java + Binary Tree + 二叉树 + 题解 + + + + Leetcode 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
    • @@ -3671,6 +4082,162 @@ inorder = [9,3,15,20,7]

Analysis

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

+]]>
+ + leetcode + + + leetcode + c++ + +
+ + ambari-summary + /2017/05/09/ambari-summary/ + 初识ambari

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

+

简单过程

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

Reverse digits of an integer.

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

+ +

spoilers

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

+

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

+

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

+

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

+
+

code

class Solution {
+public:
+    int reverse(int x) {
+
+        int max = 1 << 31 - 1;
+        int ret = 0;
+        max = (max - 1) * 2 + 1;
+        int min = 1 << 31;
+        if(x < 0)
+            while(x != 0){
+                if(ret < (min - x % 10) / 10)
+                    return 0;
+                ret = ret * 10 + x % 10;
+                x = x / 10;
+            }
+        else
+            while(x != 0){
+               if(ret > (max -x % 10) / 10)
+                    return 0;
+                ret = ret * 10 + x % 10;
+                x = x / 10;
+            }
+        return ret;
+    }
+};
]]>
leetcode @@ -5780,11 +5750,41 @@ master_port=3306; //如果是同一宿主机

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

]]> - docker + docker + + + docker + mysql + +
+ + Number of 1 Bits + /2015/03/11/Number-Of-1-Bits/ + Number of 1 Bits

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

+ +

分析

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

+
+

code

int hammingWeight(uint32_t n) {
+        const uint32_t m1  = 0x55555555; //binary: 0101...  
+        const uint32_t m2  = 0x33333333; //binary: 00110011..  
+        const uint32_t m4  = 0x0f0f0f0f; //binary:  4 zeros,  4 ones ...  
+        const uint32_t m8  = 0x00ff00ff; //binary:  8 zeros,  8 ones ...  
+        const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...  
+        
+        n = (n & m1 ) + ((n >>  1) & m1 ); //put count of each  2 bits into those  2 bits   
+        n = (n & m2 ) + ((n >>  2) & m2 ); //put count of each  4 bits into those  4 bits   
+        n = (n & m4 ) + ((n >>  4) & m4 ); //put count of each  8 bits into those  8 bits   
+        n = (n & m8 ) + ((n >>  8) & m8 ); //put count of each 16 bits into those 16 bits   
+        n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits   
+        return n; 
+
+}
]]>
+ + leetcode - docker - mysql + leetcode + c++
@@ -5937,249 +5937,12 @@ EXPOSE 80 - - docker使用中发现的echo命令的一个小技巧及其他 - /2020/03/29/echo%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E6%8A%80%E5%B7%A7/ - echo 实操技巧

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

-

查看镜像底包

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

-


这里稍稍记一下

-

寻找系统镜像源

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

-]]>
- - Linux - Docker - 命令 - echo - 发行版本 - - - linux - Docker - echo - uname - 发行版 - -
- - gogs使用webhook部署react单页应用 - /2020/02/22/gogs%E4%BD%BF%E7%94%A8webhook%E9%83%A8%E7%BD%B2react%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8/ - 众所周知,我是个前端彩笔,但是也想做点自己可以用的工具页面,所以就让朋友推荐了蚂蚁出品的 ant design,说基本可以直接 ctrl-c ctrl-v,实测对我这种来说还是有点难的,不过也能写点,但是现在碰到的问题是怎么部署到自己的服务器上去
用 ant design 写的是个单页应用,实际来说就是一个 html 加 css 跟 js,最初的时候是直接 build 完就 scp 上去,也考虑过 rsync 之类的,但是都感觉不够自动化,正好自己还没这方面的经验就想折腾下,因为我自己搭的仓库应用是 gogs,搜了一下主要是跟 drones 配合做 ci/cd,研究了一下发现其实这个事情没必要这么搞(PS:drone 也不好用),整个 hook 就可以了, 但是实际上呢,这东西也不是那么简单
首先是需要在服务器上装 webhook,这个我一开始用 snap 安装,但是出现问题,run 的时候会出现后面参数带的 hooks.json 文件找不到,然后索性就直接 github 上下最新版,放 /usr/local/bin 了,webhook 的原理呢其实也比较简单,就是起一个 http 服务,通过 post 请求调用,解析下参数,如果跟配置的参数一致,就调用对应的命令或者脚本。

-

配置 hooks.json

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

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

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

-
-

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

-
-

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

-

执行脚本 redeploy.sh

脚本类似于这样

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

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

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

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

    -
  • -
-

使用

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

-]]>
- - 持续集成 - - - Gogs - Webhook - -
- - invert-binary-tree - /2015/06/22/invert-binary-tree/ - Invert a binary tree

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

to

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

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

-
-

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

-
-
/**
- * Definition for a binary tree node.
- * struct TreeNode {
- *     int val;
- *     TreeNode *left;
- *     TreeNode *right;
- *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
- * };
- */
-class Solution {
-public:
-    TreeNode* invertTree(TreeNode* root) {
-        if(root == NULL) return root;
-        TreeNode* temp;
-        temp = invertTree(root->left);
-        root->left = invertTree(root->right);
-        root->right = temp;
-        return root;
-    }
-};
]]>
- - leetcode - - - leetcode - c++ - -
- - mybatis 的 foreach 使用的注意点 - /2022/07/09/mybatis-%E7%9A%84-foreach-%E4%BD%BF%E7%94%A8%E7%9A%84%E6%B3%A8%E6%84%8F%E7%82%B9/ - mybatis 在作为轻量级 orm 框架,如果要使用类似于 in 查询的语句,除了直接替换字符串,还可以使用 foreach 标签
在mybatis的 dtd 文件中可以看到可以配置这些字段,

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

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

-

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

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

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

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

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

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

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

-

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

-

题解

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

-

Code

class Solution {
-public:
-    int minSubArrayLen(int s, vector<int>& nums) {
-        int len = nums.size();
-        if(len == 0) return 0;
-        int minlen = INT_MAX;
-        int sum = 0;
-        
-        int left = 0;
-        int right = -1;
-        while(right < len)
-        {
-            while(sum < s && right < len)
-                sum += nums[++right];
-            if(sum >= s)
-            {
-                minlen = minlen < right - left + 1 ? minlen : right - left + 1;
-                sum -= nums[left++];
-            }
-        }
-        return minlen > len ? 0 : minlen;
-    }
-};
-]]>
- - leetcode - - - leetcode - c++ - -
- - C++ 指针使用中的一个小问题 - /2014/12/23/my-new-post/ - 在工作中碰到的一点C++指针上的一点小问题
-

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

+ docker使用中发现的echo命令的一个小技巧及其他 + /2020/03/29/echo%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E6%8A%80%E5%B7%A7/ + echo 实操技巧

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

+

查看镜像底包

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

+


这里稍稍记一下

+

寻找系统镜像源

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

]]>
- C++ + Linux + Docker + 命令 + echo + 发行版本 - 博客,文章 + linux + Docker + echo + uname + 发行版
- mybatis系列-connection连接池解析 - /2023/02/19/mybatis%E7%B3%BB%E5%88%97-connection%E8%BF%9E%E6%8E%A5%E6%B1%A0%E8%A7%A3%E6%9E%90/ - 连接池主要是两个逻辑,首先是获取连接的逻辑,结合代码来讲一讲

-
private PooledConnection popConnection(String username, String password) throws SQLException {
-    boolean countedWait = false;
-    PooledConnection conn = null;
-    long t = System.currentTimeMillis();
-    int localBadConnectionCount = 0;
-
-    // 循环获取连接
-    while (conn == null) {
-      // 加锁
-      lock.lock();
-      try {
-        // 如果闲置的连接列表不为空
-        if (!state.idleConnections.isEmpty()) {
-          // Pool has available connection
-          // 连接池有可用的连接
-          conn = state.idleConnections.remove(0);
-          if (log.isDebugEnabled()) {
-            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
-          }
-        } else {
-          // Pool does not have available connection
-          // 进入这个分支表示没有空闲连接,但是活跃连接数还没达到最大活跃连接数上限,那么这时候就可以创建一个新连接
-          if (state.activeConnections.size() < poolMaximumActiveConnections) {
-            // Can create new connection
-            // 这里创建连接我们之前讲过,
-            conn = new PooledConnection(dataSource.getConnection(), this);
-            if (log.isDebugEnabled()) {
-              log.debug("Created connection " + conn.getRealHashCode() + ".");
-            }
-          } else {
-            // Cannot create new connection
-            // 进到这个分支了就表示没法创建新连接了,那么怎么办呢,这里引入了一个 poolMaximumCheckoutTime,这代表了我去控制连接一次被使用的最长时间,如果超过这个时间了,我就要去关闭失效它
-            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
-            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
-            if (longestCheckoutTime > poolMaximumCheckoutTime) {
-              // Can claim overdue connection
-              // 所有超时连接从池中被借出的次数+1
-              state.claimedOverdueConnectionCount++;
-              // 所有超时连接从池中被借出并归还的时间总和 + 当前连接借出时间
-              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
-              // 所有连接从池中被借出并归还的时间总和 + 当前连接借出时间
-              state.accumulatedCheckoutTime += longestCheckoutTime;
-              // 从活跃连接数中移除此连接
-              state.activeConnections.remove(oldestActiveConnection);
-              // 如果该连接不是自动提交的,则尝试回滚
-              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
-                try {
-                  oldestActiveConnection.getRealConnection().rollback();
-                } catch (SQLException e) {
-                  /*
-                     Just log a message for debug and continue to execute the following
-                     statement like nothing happened.
-                     Wrap the bad connection with a new PooledConnection, this will help
-                     to not interrupt current executing thread and give current thread a
-                     chance to join the next competition for another valid/good database
-                     connection. At the end of this loop, bad {@link @conn} will be set as null.
-                   */
-                  log.debug("Bad connection. Could not roll back");
-                }
-              }
-              // 用此连接的真实连接再创建一个连接,并设置时间
-              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
-              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
-              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
-              oldestActiveConnection.invalidate();
-              if (log.isDebugEnabled()) {
-                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
-              }
-            } else {
-              // Must wait
-              // 这样还是获取不到连接就只能等待了
-              try {
-                // 标记状态,然后把等待计数+1
-                if (!countedWait) {
-                  state.hadToWaitCount++;
-                  countedWait = true;
-                }
-                if (log.isDebugEnabled()) {
-                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
-                }
-                long wt = System.currentTimeMillis();
-                // 等待 poolTimeToWait 时间
-                condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
-                // 记录等待时间
-                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
-              } catch (InterruptedException e) {
-                // set interrupt flag
-                Thread.currentThread().interrupt();
-                break;
-              }
-            }
-          }
-        }
-        // 如果连接不为空
-        if (conn != null) {
-          // ping to server and check the connection is valid or not
-          // 判断是否有效
-          if (conn.isValid()) {
-            if (!conn.getRealConnection().getAutoCommit()) {
-              // 回滚未提交的
-              conn.getRealConnection().rollback();
-            }
-            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
-            // 设置时间
-            conn.setCheckoutTimestamp(System.currentTimeMillis());
-            conn.setLastUsedTimestamp(System.currentTimeMillis());
-            // 添加进活跃连接
-            state.activeConnections.add(conn);
-            state.requestCount++;
-            state.accumulatedRequestTime += System.currentTimeMillis() - t;
-          } else {
-            if (log.isDebugEnabled()) {
-              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
-            }
-            // 连接无效,坏连接+1
-            state.badConnectionCount++;
-            localBadConnectionCount++;
-            conn = null;
-            // 如果坏连接已经超过了容忍上限,就抛异常
-            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
-              if (log.isDebugEnabled()) {
-                log.debug("PooledDataSource: Could not get a good connection to the database.");
-              }
-              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
-            }
-          }
-        }
-      } finally {
-        // 释放锁
-        lock.unlock();
-      }
-
-    }
-
-    if (conn == null) {
-      // 连接仍为空
-      if (log.isDebugEnabled()) {
-        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
-      }
-      // 抛出异常
-      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
-    }
-    // fanhui 
-    return conn;
-  }
- -

然后是还回连接

-
protected void pushConnection(PooledConnection conn) throws SQLException {
-    // 加锁
-    lock.lock();
-    try {
-      // 从活跃连接中移除当前连接
-      state.activeConnections.remove(conn);
-      if (conn.isValid()) {
-        // 当前的空闲连接数小于连接池中允许的最大空闲连接数
-        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
-          // 记录借出时间
-          state.accumulatedCheckoutTime += conn.getCheckoutTime();
-          if (!conn.getRealConnection().getAutoCommit()) {
-            // 同样是做回滚
-            conn.getRealConnection().rollback();
-          }
-          // 新建一个连接
-          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
-          // 加入到空闲连接列表中
-          state.idleConnections.add(newConn);
-          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
-          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
-          // 原连接失效
-          conn.invalidate();
-          if (log.isDebugEnabled()) {
-            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
-          }
-          // 提醒前面等待的
-          condition.signal();
-        } else {
-          // 上面是相同的,就是这里是空闲连接数已经超过上限
-          state.accumulatedCheckoutTime += conn.getCheckoutTime();
-          if (!conn.getRealConnection().getAutoCommit()) {
-            conn.getRealConnection().rollback();
+    gogs使用webhook部署react单页应用
+    /2020/02/22/gogs%E4%BD%BF%E7%94%A8webhook%E9%83%A8%E7%BD%B2react%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8/
+    众所周知,我是个前端彩笔,但是也想做点自己可以用的工具页面,所以就让朋友推荐了蚂蚁出品的 ant design,说基本可以直接 ctrl-c ctrl-v,实测对我这种来说还是有点难的,不过也能写点,但是现在碰到的问题是怎么部署到自己的服务器上去
用 ant design 写的是个单页应用,实际来说就是一个 html 加 css 跟 js,最初的时候是直接 build 完就 scp 上去,也考虑过 rsync 之类的,但是都感觉不够自动化,正好自己还没这方面的经验就想折腾下,因为我自己搭的仓库应用是 gogs,搜了一下主要是跟 drones 配合做 ci/cd,研究了一下发现其实这个事情没必要这么搞(PS:drone 也不好用),整个 hook 就可以了, 但是实际上呢,这东西也不是那么简单
首先是需要在服务器上装 webhook,这个我一开始用 snap 安装,但是出现问题,run 的时候会出现后面参数带的 hooks.json 文件找不到,然后索性就直接 github 上下最新版,放 /usr/local/bin 了,webhook 的原理呢其实也比较简单,就是起一个 http 服务,通过 post 请求调用,解析下参数,如果跟配置的参数一致,就调用对应的命令或者脚本。

+

配置 hooks.json

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

+
[
+  {
+    "id": "redeploy-app",
+    "execute-command": "/opt/scripts/redeploy.sh",
+    "command-working-directory": "/opt/scripts",
+    "pass-arguments-to-command":
+    [
+      {
+        "source": "payload",
+        "name": "head_commit.message"
+      },
+      {
+        "source": "payload",
+        "name": "pusher.name"
+      },
+      {
+        "source": "payload",
+        "name": "head_commit.id"
+      }
+    ],
+    "trigger-rule":
+    {
+      "and":
+      [
+        {
+          "match":
+          {
+            "type": "payload-hash-sha1",
+            "secret": "your-github-secret",
+            "parameter":
+            {
+              "source": "header",
+              "name": "X-Hub-Signature"
+            }
           }
-          conn.getRealConnection().close();
-          if (log.isDebugEnabled()) {
-            log.debug("Closed connection " + conn.getRealHashCode() + ".");
+        },
+        {
+          "match":
+          {
+            "type": "value",
+            "value": "refs/heads/master",
+            "parameter":
+            {
+              "source": "payload",
+              "name": "ref"
+            }
           }
-          conn.invalidate();
-        }
-      } else {
-        if (log.isDebugEnabled()) {
-          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
         }
-        state.badConnectionCount++;
-      }
-    } finally {
-      lock.unlock();
+      ]
     }
-  }
+ } +]
+ +

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

+
+

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

+
+

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

+

执行脚本 redeploy.sh

脚本类似于这样

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

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

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

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

    +
  • +
+

使用

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

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

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

-
// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
-public SqlSource parseScriptNode() {
-    MixedSqlNode rootSqlNode = parseDynamicTags(context);
-    SqlSource sqlSource;
-    if (isDynamic) {
-      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
-    } else {
-      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+    invert-binary-tree
+    /2015/06/22/invert-binary-tree/
+    Invert a binary tree

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

to

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

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

+
+

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

+
+
/**
+ * Definition for a binary tree node.
+ * struct TreeNode {
+ *     int val;
+ *     TreeNode *left;
+ *     TreeNode *right;
+ *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
+ * };
+ */
+class Solution {
+public:
+    TreeNode* invertTree(TreeNode* root) {
+        if(root == NULL) return root;
+        TreeNode* temp;
+        temp = invertTree(root->left);
+        root->left = invertTree(root->right);
+        root->right = temp;
+        return root;
     }
-    return sqlSource;
-  }
-// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
-protected MixedSqlNode parseDynamicTags(XNode node) {
-    List<SqlNode> contents = new ArrayList<>();
-    NodeList children = node.getNode().getChildNodes();
-    for (int i = 0; i < children.getLength(); i++) {
-      XNode child = node.newXNode(children.item(i));
-      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
-        String data = child.getStringBody("");
-        TextSqlNode textSqlNode = new TextSqlNode(data);
-        if (textSqlNode.isDynamic()) {
-          contents.add(textSqlNode);
-          isDynamic = true;
-        } else {
-          contents.add(new StaticTextSqlNode(data));
-        }
-      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
-        String nodeName = child.getNode().getNodeName();
-        NodeHandler handler = nodeHandlerMap.get(nodeName);
-        if (handler == null) {
-          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+};
]]>
+ + leetcode + + + leetcode + c++ + + + + minimum-size-subarray-sum-209 + /2016/10/11/minimum-size-subarray-sum-209/ + problem

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

+

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

+

题解

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

+

Code

class Solution {
+public:
+    int minSubArrayLen(int s, vector<int>& nums) {
+        int len = nums.size();
+        if(len == 0) return 0;
+        int minlen = INT_MAX;
+        int sum = 0;
+        
+        int left = 0;
+        int right = -1;
+        while(right < len)
+        {
+            while(sum < s && right < len)
+                sum += nums[++right];
+            if(sum >= s)
+            {
+                minlen = minlen < right - left + 1 ? minlen : right - left + 1;
+                sum -= nums[left++];
+            }
         }
-        handler.handleNode(child, contents);
-        isDynamic = true;
-      }
+        return minlen > len ? 0 : minlen;
     }
-    return new MixedSqlNode(contents);
-  }
-// org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
-  public boolean isDynamic() {
-    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
-    GenericTokenParser parser = createParser(checker);
-    parser.parse(text);
-    return checker.isDynamic();
-  }
-    private GenericTokenParser createParser(TokenHandler handler) {
-    return new GenericTokenParser("${", "}", handler);
-  }
-

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

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

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

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

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

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

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

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

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

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

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

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

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

+

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

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

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

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

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

+
@Override
+public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
+  try {
+    MappedStatement ms = configuration.getMappedStatement(statement);
+    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
+  } catch (Exception e) {
+    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
+  } finally {
+    ErrorContext.instance().reset();
+  }
+}
+// 就是在这帮我们做了转换
+  private Object wrapCollection(final Object object) {
+  if (object instanceof Collection) {
+    StrictMap<Object> map = new StrictMap<Object>();
+    map.put("collection", object);
+    if (object instanceof List) {
+      // 如果类型是list 就会转成以 list 为 key 的 map
+      map.put("list", object);
+    }
+    return map;
+  } else if (object != null && object.getClass().isArray()) {
+    StrictMap<Object> map = new StrictMap<Object>();
+    map.put("array", object);
+    return map;
+  }
+  return object;
+  }
]]>
Java Mybatis - Spring Mysql - Sql注入 - Mybatis Java Mysql Mybatis - Sql注入
@@ -7197,6 +6890,101 @@ public class DynamicSqlSource implements SqlSource { Mybatis + + mybatis 的 $ 和 # 是有啥区别 + /2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/ + 这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
在这里做了替换,将#{}替换成了?

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

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

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

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

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

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

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

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

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

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

+]]>
+ + Java + Mybatis + Spring + Mysql + Sql注入 + Mybatis + + + Java + Mysql + Mybatis + Sql注入 + +
mybatis系列-dataSource解析 /2023/01/08/mybatis%E7%B3%BB%E5%88%97-dataSource%E8%A7%A3%E6%9E%90/ @@ -7475,92 +7263,8 @@ public class DynamicSqlSource implements SqlSource {
@Override
 public Connection getConnection() throws SQLException {
   return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-}
-

对于具体怎么获取连接我们可以下一篇具体讲下

-]]> - - Java - Mybatis - - - Java - Mysql - Mybatis - -
- - mybatis系列-入门篇 - /2022/11/27/mybatis%E7%B3%BB%E5%88%97-%E5%85%A5%E9%97%A8%E7%AF%87/ - mybatis是我们比较常用的orm框架,下面是官网的介绍

-
-

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

-
-

mybatis一大特点,或者说比较为人熟知的应该就是比 hibernate 是更轻量化,为国人所爱好的orm框架,对于hibernate目前还没有深入的拆解过,后续可以也写一下,在使用体验上觉得是个比较精巧的框架,看代码也比较容易,所以就想写个系列,第一篇先是介绍下使用
根据官网的文档上我们先来尝试一下简单使用
首先我们有个简单的配置,这个文件是mybatis-config.xml

-
<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE configuration
-        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
-        "https://mybatis.org/dtd/mybatis-3-config.dtd">
-<configuration>
-    <!-- 需要加入的properties-->
-    <properties resource="application-development.properties"/>
-    <!-- 指出使用哪个环境,默认是development-->
-    <environments default="development">
-        <environment id="development">
-        <!-- 指定事务管理器类型-->
-            <transactionManager type="JDBC"/>
-            <!-- 指定数据源类型-->
-            <dataSource type="POOLED">
-                <!-- 下面就是具体的参数占位了-->
-                <property name="driver" value="${driver}"/>
-                <property name="url" value="${url}"/>
-                <property name="username" value="${username}"/>
-                <property name="password" value="${password}"/>
-            </dataSource>
-        </environment>
-    </environments>
-    <mappers>
-        <!-- 指定mapper xml的位置或文件-->
-        <mapper resource="mapper/StudentMapper.xml"/>
-    </mappers>
-</configuration>
-

在代码里创建mybatis里重要入口

-
String resource = "mybatis-config.xml";
-InputStream inputStream = Resources.getResourceAsStream(resource);
-SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-

然后我们上面的StudentMapper.xml

-
<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper
-        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.nicksxs.mybatisdemo.StudentMapper">
-    <select id="selectStudent" resultType="com.nicksxs.mybatisdemo.StudentDO">
-        select * from student where id = #{id}
-    </select>
-</mapper>
-

那么我们就要使用这个mapper,

-
String resource = "mybatis-config.xml";
-InputStream inputStream = Resources.getResourceAsStream(resource);
-SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-try (SqlSession session = sqlSessionFactory.openSession()) {
-    StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
-    System.out.println("id is " + studentDO.getId() + " name is " +studentDO.getName());
-} catch (Exception e) {
-    e.printStackTrace();
-}
-

sqlSessionFactory是sqlSession的工厂,我们可以通过sqlSessionFactory来创建sqlSession,而SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。可以看到mapper.xml中有定义mapper的namespace,就可以通过session.selectOne()传入namespace+id来调用这个方法
但是这样调用比较不合理的点,或者说按后面mybatis优化之后我们可以指定mapper接口

-
public interface StudentMapper {
-
-    public StudentDO selectStudent(Long id);
-}
-

就可以可以通过mapper接口获取方法,这样就不用涉及到未知的变量转换等异常

-
try (SqlSession session = sqlSessionFactory.openSession()) {
-    StudentMapper mapper = session.getMapper(StudentMapper.class);
-    StudentDO studentDO = mapper.selectStudent(1L);
-    System.out.println("id is " + studentDO.getId() + " name is " +studentDO.getName());
-} catch (Exception e) {
-    e.printStackTrace();
-}
-

这一篇咱们先介绍下简单的使用,后面可以先介绍下这些的原理。

+}
+

对于具体怎么获取连接我们可以下一篇具体讲下

]]>
Java @@ -7572,116 +7276,6 @@ public class DynamicSqlSource implements SqlSource { Mybatis
- - nginx 日志小记 - /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ - nginx 默认的日志有特定的格式,我们也可以自定义,

-

默认的格式是预定义的 combined

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

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

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

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

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

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

-

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

-

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

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

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

-

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

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

-

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

-

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

-
http {
-  lua_code_cache off;
-}
-
-location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
-   default_type 'application/json;charset=utf-8';
-   content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
-}
- -

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

-
ngx.header['response'] = 'header'
- - -

使用总结

-

后续:

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

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

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

    -
    local decode = require("cjson").decode
    -
    -function json_decode( str )
    -    local ok, t = pcall(decode, str)
    -    if not ok then
    -      return nil
    -    end
    -
    -    return t
    -end
    -

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

    -
    local json = require("cjson.safe")
    -local str = [[ {"key:"value"} ]]
    -
    -local t = json.decode(str)
    -if t then
    -    ngx.say(" --> ", type(t))
    -end
    -

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

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

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

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

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

    -
  14. -
-]]>
- - nginx - - - nginx - openresty - -
mybatis系列-typeAliases系统 /2023/01/01/mybatis%E7%B3%BB%E5%88%97-typeAliases%E7%B3%BB%E7%BB%9F/ @@ -7788,343 +7382,497 @@ location ~* - pcre-intro-and-a-simple-package - /2015/01/16/pcre-intro-and-a-simple-package/ - Pcre
-

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

-
-

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

- - -

Regular Expression Syntax

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

-

PCRE函数介绍

-

pcre_compile
原型:

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

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

+ mybatis系列-入门篇 + /2022/11/27/mybatis%E7%B3%BB%E5%88%97-%E5%85%A5%E9%97%A8%E7%AF%87/ + mybatis是我们比较常用的orm框架,下面是官网的介绍

-

pcre_exec
原型:

+

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

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

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

-

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

-

一个丑陋的封装

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

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

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

示例代码

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

result

PHP Fatal error:  Abstract function abst1::abstra1() cannot contain body in new.php on line 17
+

mybatis一大特点,或者说比较为人熟知的应该就是比 hibernate 是更轻量化,为国人所爱好的orm框架,对于hibernate目前还没有深入的拆解过,后续可以也写一下,在使用体验上觉得是个比较精巧的框架,看代码也比较容易,所以就想写个系列,第一篇先是介绍下使用
根据官网的文档上我们先来尝试一下简单使用
首先我们有个简单的配置,这个文件是mybatis-config.xml

+
<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+        "https://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+    <!-- 需要加入的properties-->
+    <properties resource="application-development.properties"/>
+    <!-- 指出使用哪个环境,默认是development-->
+    <environments default="development">
+        <environment id="development">
+        <!-- 指定事务管理器类型-->
+            <transactionManager type="JDBC"/>
+            <!-- 指定数据源类型-->
+            <dataSource type="POOLED">
+                <!-- 下面就是具体的参数占位了-->
+                <property name="driver" value="${driver}"/>
+                <property name="url" value="${url}"/>
+                <property name="username" value="${username}"/>
+                <property name="password" value="${password}"/>
+            </dataSource>
+        </environment>
+    </environments>
+    <mappers>
+        <!-- 指定mapper xml的位置或文件-->
+        <mapper resource="mapper/StudentMapper.xml"/>
+    </mappers>
+</configuration>
+

在代码里创建mybatis里重要入口

+
String resource = "mybatis-config.xml";
+InputStream inputStream = Resources.getResourceAsStream(resource);
+SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
+

然后我们上面的StudentMapper.xml

+
<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.nicksxs.mybatisdemo.StudentMapper">
+    <select id="selectStudent" resultType="com.nicksxs.mybatisdemo.StudentDO">
+        select * from student where id = #{id}
+    </select>
+</mapper>
+

那么我们就要使用这个mapper,

+
String resource = "mybatis-config.xml";
+InputStream inputStream = Resources.getResourceAsStream(resource);
+SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
+try (SqlSession session = sqlSessionFactory.openSession()) {
+    StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
+    System.out.println("id is " + studentDO.getId() + " name is " +studentDO.getName());
+} catch (Exception e) {
+    e.printStackTrace();
+}
+

sqlSessionFactory是sqlSession的工厂,我们可以通过sqlSessionFactory来创建sqlSession,而SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。可以看到mapper.xml中有定义mapper的namespace,就可以通过session.selectOne()传入namespace+id来调用这个方法
但是这样调用比较不合理的点,或者说按后面mybatis优化之后我们可以指定mapper接口

+
public interface StudentMapper {
 
-Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
+ public StudentDO selectStudent(Long id); +}
+

就可以可以通过mapper接口获取方法,这样就不用涉及到未知的变量转换等异常

+
try (SqlSession session = sqlSessionFactory.openSession()) {
+    StudentMapper mapper = session.getMapper(StudentMapper.class);
+    StudentDO studentDO = mapper.selectStudent(1L);
+    System.out.println("id is " + studentDO.getId() + " name is " +studentDO.getName());
+} catch (Exception e) {
+    e.printStackTrace();
+}
+

这一篇咱们先介绍下简单的使用,后面可以先介绍下这些的原理。

]]>
- php + Java + Mybatis - php + Java + Mysql + Mybatis
- mybatis系列-第一条sql的细节 - /2022/12/11/mybatis%E7%B3%BB%E5%88%97-%E7%AC%AC%E4%B8%80%E6%9D%A1sql%E7%9A%84%E7%BB%86%E8%8A%82/ - 先补充两个点,
第一是前面我们说了
使用org.apache.ibatis.builder.xml.XMLConfigBuilder 创建了parser解析器,那么解析的结果是什么
看这个方法的返回值

-
public Configuration parse() {
-  if (parsed) {
-    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
-  }
-  parsed = true;
-  parseConfiguration(parser.evalNode("/configuration"));
-  return configuration;
-}
- -

返回的是 org.apache.ibatis.session.Configuration , 而这个 Configuration 也是 mybatis 中特别重要的配置核心类,贴一下里面的成员变量,

-
public class Configuration {
-
-  protected Environment environment;
+    mybatis系列-connection连接池解析
+    /2023/02/19/mybatis%E7%B3%BB%E5%88%97-connection%E8%BF%9E%E6%8E%A5%E6%B1%A0%E8%A7%A3%E6%9E%90/
+    连接池主要是两个逻辑,首先是获取连接的逻辑,结合代码来讲一讲

+
private PooledConnection popConnection(String username, String password) throws SQLException {
+    boolean countedWait = false;
+    PooledConnection conn = null;
+    long t = System.currentTimeMillis();
+    int localBadConnectionCount = 0;
 
-  protected boolean safeRowBoundsEnabled;
-  protected boolean safeResultHandlerEnabled = true;
-  protected boolean mapUnderscoreToCamelCase;
-  protected boolean aggressiveLazyLoading;
-  protected boolean multipleResultSetsEnabled = true;
-  protected boolean useGeneratedKeys;
-  protected boolean useColumnLabel = true;
-  protected boolean cacheEnabled = true;
-  protected boolean callSettersOnNulls;
-  protected boolean useActualParamName = true;
-  protected boolean returnInstanceForEmptyRow;
-  protected boolean shrinkWhitespacesInSql;
-  protected boolean nullableOnForEach;
-  protected boolean argNameBasedConstructorAutoMapping;
+    // 循环获取连接
+    while (conn == null) {
+      // 加锁
+      lock.lock();
+      try {
+        // 如果闲置的连接列表不为空
+        if (!state.idleConnections.isEmpty()) {
+          // Pool has available connection
+          // 连接池有可用的连接
+          conn = state.idleConnections.remove(0);
+          if (log.isDebugEnabled()) {
+            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
+          }
+        } else {
+          // Pool does not have available connection
+          // 进入这个分支表示没有空闲连接,但是活跃连接数还没达到最大活跃连接数上限,那么这时候就可以创建一个新连接
+          if (state.activeConnections.size() < poolMaximumActiveConnections) {
+            // Can create new connection
+            // 这里创建连接我们之前讲过,
+            conn = new PooledConnection(dataSource.getConnection(), this);
+            if (log.isDebugEnabled()) {
+              log.debug("Created connection " + conn.getRealHashCode() + ".");
+            }
+          } else {
+            // Cannot create new connection
+            // 进到这个分支了就表示没法创建新连接了,那么怎么办呢,这里引入了一个 poolMaximumCheckoutTime,这代表了我去控制连接一次被使用的最长时间,如果超过这个时间了,我就要去关闭失效它
+            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
+            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
+            if (longestCheckoutTime > poolMaximumCheckoutTime) {
+              // Can claim overdue connection
+              // 所有超时连接从池中被借出的次数+1
+              state.claimedOverdueConnectionCount++;
+              // 所有超时连接从池中被借出并归还的时间总和 + 当前连接借出时间
+              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
+              // 所有连接从池中被借出并归还的时间总和 + 当前连接借出时间
+              state.accumulatedCheckoutTime += longestCheckoutTime;
+              // 从活跃连接数中移除此连接
+              state.activeConnections.remove(oldestActiveConnection);
+              // 如果该连接不是自动提交的,则尝试回滚
+              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
+                try {
+                  oldestActiveConnection.getRealConnection().rollback();
+                } catch (SQLException e) {
+                  /*
+                     Just log a message for debug and continue to execute the following
+                     statement like nothing happened.
+                     Wrap the bad connection with a new PooledConnection, this will help
+                     to not interrupt current executing thread and give current thread a
+                     chance to join the next competition for another valid/good database
+                     connection. At the end of this loop, bad {@link @conn} will be set as null.
+                   */
+                  log.debug("Bad connection. Could not roll back");
+                }
+              }
+              // 用此连接的真实连接再创建一个连接,并设置时间
+              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
+              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
+              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
+              oldestActiveConnection.invalidate();
+              if (log.isDebugEnabled()) {
+                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
+              }
+            } else {
+              // Must wait
+              // 这样还是获取不到连接就只能等待了
+              try {
+                // 标记状态,然后把等待计数+1
+                if (!countedWait) {
+                  state.hadToWaitCount++;
+                  countedWait = true;
+                }
+                if (log.isDebugEnabled()) {
+                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
+                }
+                long wt = System.currentTimeMillis();
+                // 等待 poolTimeToWait 时间
+                condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
+                // 记录等待时间
+                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
+              } catch (InterruptedException e) {
+                // set interrupt flag
+                Thread.currentThread().interrupt();
+                break;
+              }
+            }
+          }
+        }
+        // 如果连接不为空
+        if (conn != null) {
+          // ping to server and check the connection is valid or not
+          // 判断是否有效
+          if (conn.isValid()) {
+            if (!conn.getRealConnection().getAutoCommit()) {
+              // 回滚未提交的
+              conn.getRealConnection().rollback();
+            }
+            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
+            // 设置时间
+            conn.setCheckoutTimestamp(System.currentTimeMillis());
+            conn.setLastUsedTimestamp(System.currentTimeMillis());
+            // 添加进活跃连接
+            state.activeConnections.add(conn);
+            state.requestCount++;
+            state.accumulatedRequestTime += System.currentTimeMillis() - t;
+          } else {
+            if (log.isDebugEnabled()) {
+              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
+            }
+            // 连接无效,坏连接+1
+            state.badConnectionCount++;
+            localBadConnectionCount++;
+            conn = null;
+            // 如果坏连接已经超过了容忍上限,就抛异常
+            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
+              if (log.isDebugEnabled()) {
+                log.debug("PooledDataSource: Could not get a good connection to the database.");
+              }
+              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
+            }
+          }
+        }
+      } finally {
+        // 释放锁
+        lock.unlock();
+      }
 
-  protected String logPrefix;
-  protected Class<? extends Log> logImpl;
-  protected Class<? extends VFS> vfsImpl;
-  protected Class<?> defaultSqlProviderType;
-  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
-  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
-  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
-  protected Integer defaultStatementTimeout;
-  protected Integer defaultFetchSize;
-  protected ResultSetType defaultResultSetType;
-  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
-  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
-  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
+    }
 
-  protected Properties variables = new Properties();
-  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
-  protected ObjectFactory objectFactory = new DefaultObjectFactory();
-  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
+    if (conn == null) {
+      // 连接仍为空
+      if (log.isDebugEnabled()) {
+        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
+      }
+      // 抛出异常
+      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
+    }
+    // fanhui 
+    return conn;
+  }
- protected boolean lazyLoadingEnabled = false; - protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL +

然后是还回连接

+
protected void pushConnection(PooledConnection conn) throws SQLException {
+    // 加锁
+    lock.lock();
+    try {
+      // 从活跃连接中移除当前连接
+      state.activeConnections.remove(conn);
+      if (conn.isValid()) {
+        // 当前的空闲连接数小于连接池中允许的最大空闲连接数
+        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
+          // 记录借出时间
+          state.accumulatedCheckoutTime += conn.getCheckoutTime();
+          if (!conn.getRealConnection().getAutoCommit()) {
+            // 同样是做回滚
+            conn.getRealConnection().rollback();
+          }
+          // 新建一个连接
+          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
+          // 加入到空闲连接列表中
+          state.idleConnections.add(newConn);
+          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
+          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
+          // 原连接失效
+          conn.invalidate();
+          if (log.isDebugEnabled()) {
+            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
+          }
+          // 提醒前面等待的
+          condition.signal();
+        } else {
+          // 上面是相同的,就是这里是空闲连接数已经超过上限
+          state.accumulatedCheckoutTime += conn.getCheckoutTime();
+          if (!conn.getRealConnection().getAutoCommit()) {
+            conn.getRealConnection().rollback();
+          }
+          conn.getRealConnection().close();
+          if (log.isDebugEnabled()) {
+            log.debug("Closed connection " + conn.getRealHashCode() + ".");
+          }
+          conn.invalidate();
+        }
+      } else {
+        if (log.isDebugEnabled()) {
+          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
+        }
+        state.badConnectionCount++;
+      }
+    } finally {
+      lock.unlock();
+    }
+  }
+]]>
+ + Java + Mybatis + + + Java + Mysql + Mybatis + + + + mybatis系列-第一条sql的更多细节 + /2022/12/18/mybatis%E7%B3%BB%E5%88%97-%E7%AC%AC%E4%B8%80%E6%9D%A1sql%E7%9A%84%E6%9B%B4%E5%A4%9A%E7%BB%86%E8%8A%82/ + 执行细节
首先设置了默认的languageDriver
org/mybatis/mybatis/3.5.11/mybatis-3.5.11-sources.jar!/org/apache/ibatis/session/Configuration.java:215
configuration的构造方法里

+
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
- protected String databaseId; - /** - * Configuration factory class. - * Used to create Configuration for loading deserialized unread properties. - * - * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a> - */ - protected Class<?> configurationFactory; +

而在
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
中,创建了sqlSource,这里就会根据前面的 LanguageDriver 的实现选择对应的 sqlSource

+
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
- protected final MapperRegistry mapperRegistry = new MapperRegistry(this); - protected final InterceptorChain interceptorChain = new InterceptorChain(); - protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this); - protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); - protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry(); +

createSqlSource 就会调用

+
@Override
+public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
+  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
+  return builder.parseScriptNode();
+}
- protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection") - .conflictMessageProducer((savedValue, targetValue) -> - ". please check " + savedValue.getResource() + " and " + targetValue.getResource()); - protected final Map<String, Cache> caches = new StrictMap<>("Caches collection"); - protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection"); - protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection"); - protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection"); +

再往下的逻辑在 parseScriptNode 中,org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode

+
public SqlSource parseScriptNode() {
+  MixedSqlNode rootSqlNode = parseDynamicTags(context);
+  SqlSource sqlSource;
+  if (isDynamic) {
+    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+  } else {
+    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+  }
+  return sqlSource;
+}
- protected final Set<String> loadedResources = new HashSet<>(); - protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); +

首先要解析dynamicTag,调用了org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags

+
protected MixedSqlNode parseDynamicTags(XNode node) {
+    List<SqlNode> contents = new ArrayList<>();
+    NodeList children = node.getNode().getChildNodes();
+    for (int i = 0; i < children.getLength(); i++) {
+      XNode child = node.newXNode(children.item(i));
+      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
+        String data = child.getStringBody("");
+        TextSqlNode textSqlNode = new TextSqlNode(data);
+        // ---------> 主要是这边的逻辑
+        if (textSqlNode.isDynamic()) {
+          contents.add(textSqlNode);
+          isDynamic = true;
+        } else {
+          contents.add(new StaticTextSqlNode(data));
+        }
+      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
+        String nodeName = child.getNode().getNodeName();
+        NodeHandler handler = nodeHandlerMap.get(nodeName);
+        if (handler == null) {
+          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+        }
+        handler.handleNode(child, contents);
+        isDynamic = true;
+      }
+    }
+    return new MixedSqlNode(contents);
+  }
- protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>(); - protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>(); - protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>(); - protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
+

判断是否是动态sql,调用了org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic

+
public boolean isDynamic() {
+  DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
+  // ----------> 主要是这里的方法
+  GenericTokenParser parser = createParser(checker);
+  parser.parse(text);
+  return checker.isDynamic();
+}
-

这么多成员变量,先不一一解释作用,但是其中的几个参数我们应该是已经知道了的,第一个就是 mappedStatements ,上一篇我们知道被解析的mapper就是放在这里,后面的 resultMapsparameterMaps 也比较常用的就是我们参数和结果的映射map,这里跟我之前有一篇解释为啥我们一些变量的使用会比较特殊,比如list,可以参考这篇keyGenerators是在我们需要定义主键生成器的时候使用。
然后第二点是我们创建的 org.apache.ibatis.session.SqlSessionFactory 是哪个,

-
public SqlSessionFactory build(Configuration config) {
-  return new DefaultSqlSessionFactory(config);
+

创建parser的时候可以看到这个parser是干了啥,其实就是找有没有${ , }

+
private GenericTokenParser createParser(TokenHandler handler) {
+  return new GenericTokenParser("${", "}", handler);
 }
-

是这个 DefaultSqlSessionFactory ,这是其中一个 SqlSessionFactory 的实现
接下来我们看看 openSession 里干了啥

-
public SqlSession openSession() {
-  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
-}
+

如果是的话,就在上面把 isDynamic 设置为true 如果是true 的话就创建 DynamicSqlSource

+
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
-

这边有几个参数,第一个是默认的执行器类型,往上找找上面贴着的 Configuration 的成员变量里可以看到默认是
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

-

因为没有指明特殊的执行逻辑,所以默认我们也就用简单类型的,第二个参数是是事务级别,第三个是是否自动提交

-
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
-  Transaction tx = null;
-  try {
-    final Environment environment = configuration.getEnvironment();
-    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
-    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
-    // --------> 先关注这里
-    final Executor executor = configuration.newExecutor(tx, execType);
-    return new DefaultSqlSession(configuration, executor, autoCommit);
-  } catch (Exception e) {
-    closeTransaction(tx); // may have fetched a connection so lets call close()
-    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
-  } finally {
-    ErrorContext.instance().reset();
-  }
-}
+

如果不是的话就创建RawSqlSource

+
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+```java
 
-

具体是调用了 Configuration 的这个方法

-
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
-  executorType = executorType == null ? defaultExecutorType : executorType;
-  Executor executor;
-  if (ExecutorType.BATCH == executorType) {
-    executor = new BatchExecutor(this, transaction);
-  } else if (ExecutorType.REUSE == executorType) {
-    executor = new ReuseExecutor(this, transaction);
-  } else {
-    // ---------> 会走到这个分支
-    executor = new SimpleExecutor(this, transaction);
-  }
-  if (cacheEnabled) {
-    executor = new CachingExecutor(executor);
+但是这不是一个真实可用的 `sqlSource` ,
+实际创建的时候会走到这
+```java
+public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
+    this(configuration, getSql(configuration, rootSqlNode), parameterType);
   }
-  executor = (Executor) interceptorChain.pluginAll(executor);
-  return executor;
-}
-

上面传入的 executorTypeConfiguration 的默认类型,也就是 simple 类型,并且 cacheEnabledConfiguration 默认为 true,所以会包装成CachingExecutor ,然后后面就是插件了,这块我们先不展开
然后我们的openSession返回的就是创建了DefaultSqlSession

-
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
-    this.configuration = configuration;
-    this.executor = executor;
-    this.dirty = false;
-    this.autoCommit = autoCommit;
-  }
+ public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); + Class<?> clazz = parameterType == null ? Object.class : parameterType; + sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); + }
-

然后就是调用 selectOne, 因为前面已经把这部分代码说过了,就直接跳转过来
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

-
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
-  try {
-    MappedStatement ms = configuration.getMappedStatement(statement);
-    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
-  } catch (Exception e) {
-    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
-  } finally {
-    ErrorContext.instance().reset();
+

具体的sqlSource是通过org.apache.ibatis.builder.SqlSourceBuilder#parse 创建的
具体的代码逻辑是

+
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
+  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
+  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
+  String sql;
+  if (configuration.isShrinkWhitespacesInSql()) {
+    sql = parser.parse(removeExtraWhitespaces(originalSql));
+  } else {
+    sql = parser.parse(originalSql);
   }
-}
- -

因为前面说了 executor 包装了 CachingExecutor ,所以会先调用

-
@Override
-public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
-  BoundSql boundSql = ms.getBoundSql(parameterObject);
-  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
-  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
-}
+ return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); +}
-

然后是调用的真实的query方法

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

这里创建的其实是StaticSqlSource ,多带一句前面的parser是将原来这样select * from student where id = #{id}sql 解析成了select * from student where id = ? 然后创建了StaticSqlSource

+
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
+  this.sql = sql;
+  this.parameterMappings = parameterMappings;
+  this.configuration = configuration;
+}
-

这里是第一次查询,没有缓存就先到最后一行,继续是调用到 org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

+

为什么前面要讲这么多好像没什么关系的代码呢,其实在最开始我们执行sql的代码中

@Override
-  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
-    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
-    if (closed) {
-      throw new ExecutorException("Executor was closed.");
-    }
-    if (queryStack == 0 && ms.isFlushCacheRequired()) {
-      clearLocalCache();
-    }
-    List<E> list;
-    try {
-      queryStack++;
-      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
-      if (list != null) {
-        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
-      } else {
-        // ----------->会走到这里
-        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
-      }
-    } finally {
-      queryStack--;
+  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
+    BoundSql boundSql = ms.getBoundSql(parameterObject);
+    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
+    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+  }
+ +

这里获取了BoundSql,而BoundSql是怎么来的呢,首先调用了org.apache.ibatis.mapping.MappedStatement#getBoundSql

+
public BoundSql getBoundSql(Object parameterObject) {
+    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
+    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
+    if (parameterMappings == null || parameterMappings.isEmpty()) {
+      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
     }
-    if (queryStack == 0) {
-      for (DeferredLoad deferredLoad : deferredLoads) {
-        deferredLoad.load();
-      }
-      // issue #601
-      deferredLoads.clear();
-      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
-        // issue #482
-        clearLocalCache();
+
+    // check for nested result maps in parameter mappings (issue #30)
+    for (ParameterMapping pm : boundSql.getParameterMappings()) {
+      String rmId = pm.getResultMapId();
+      if (rmId != null) {
+        ResultMap rm = configuration.getResultMap(rmId);
+        if (rm != null) {
+          hasNestedResultMaps |= rm.hasNestedResultMaps();
+        }
       }
     }
-    return list;
-  }
-

然后是

-
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
-  List<E> list;
-  localCache.putObject(key, EXECUTION_PLACEHOLDER);
+    return boundSql;
+  }
+ +

而我们从上面的解析中可以看到这里的sqlSource是一层RawSqlSource , 它的getBoundSql又是调用内部的sqlSource的方法

+
@Override
+public BoundSql getBoundSql(Object parameterObject) {
+  return sqlSource.getBoundSql(parameterObject);
+}
+ +

内部的sqlSource 就是StaticSqlSource

+
@Override
+public BoundSql getBoundSql(Object parameterObject) {
+  return new BoundSql(configuration, sql, parameterMappings, parameterObject);
+}
+ +

这个BoundSql的内容也比较简单

+
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
+  this.sql = sql;
+  this.parameterMappings = parameterMappings;
+  this.parameterObject = parameterObject;
+  this.additionalParameters = new HashMap<>();
+  this.metaParameters = configuration.newMetaObject(additionalParameters);
+}
+ +

而上次在这边org.apache.ibatis.executor.SimpleExecutor#doQuery 的时候落了个东西,就是StatementHandler的逻辑

+
@Override
+public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+  Statement stmt = null;
   try {
-    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
+    Configuration configuration = ms.getConfiguration();
+    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
+    stmt = prepareStatement(handler, ms.getStatementLog());
+    return handler.query(stmt, resultHandler);
   } finally {
-    localCache.removeObject(key);
+    closeStatement(stmt);
   }
-  localCache.putObject(key, list);
-  if (ms.getStatementType() == StatementType.CALLABLE) {
-    localOutputParameterCache.putObject(key, parameter);
+}
+ +

它是通过statementType来区分应该使用哪个statementHandler,我们这使用的就是PreparedStatementHandler

+
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+
+  switch (ms.getStatementType()) {
+    case STATEMENT:
+      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
+      break;
+    case PREPARED:
+      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
+      break;
+    case CALLABLE:
+      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
+      break;
+    default:
+      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
   }
-  return list;
-}
-

然后就是 simpleExecutor 的执行过程

+}
+ +

所以上次有个细节可以补充,这边的doQuery里面的handler.query 应该是调用了PreparedStatementHandler 的query方法

@Override
 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
   Statement stmt = null;
@@ -8138,7 +7886,8 @@ Fatal error: Abstract function abst1::abstra1() cannot contain body in php on li
   }
 }
-

接下去其实就是跟jdbc交互了

+ +

因为上面prepareStatement中getConnection拿到connection是com.mysql.cj.jdbc.ConnectionImpl#ConnectionImpl(com.mysql.cj.conf.HostInfo)

@Override
 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
   PreparedStatement ps = (PreparedStatement) statement;
@@ -8146,961 +7895,1215 @@ Fatal error: Abstract function abst1::abstra1() cannot contain body in php on li
   return resultSetHandler.handleResultSets(ps);
 }
-

com.mysql.cj.jdbc.ClientPreparedStatement#execute

-
public boolean execute() throws SQLException {
-        try {
-            synchronized(this.checkClosed().getConnectionMutex()) {
-                JdbcConnection locallyScopedConn = this.connection;
-                if (!this.doPingInstead && !this.checkReadOnlySafeStatement()) {
-                    throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.exceptionInterceptor);
-                } else {
-                    ResultSetInternalMethods rs = null;
-                    this.lastQueryIsOnDupKeyUpdate = false;
-                    if (this.retrieveGeneratedKeys) {
-                        this.lastQueryIsOnDupKeyUpdate = this.containsOnDuplicateKeyUpdate();
-                    }
-
-                    this.batchedGeneratedKeys = null;
-                    this.resetCancelledState();
-                    this.implicitlyCloseAllOpenResults();
-                    this.clearWarnings();
-                    if (this.doPingInstead) {
-                        this.doPingInstead();
-                        return true;
-                    } else {
-                        this.setupStreamingTimeout(locallyScopedConn);
-                        Message sendPacket = ((PreparedQuery)this.query).fillSendPacket(((PreparedQuery)this.query).getQueryBindings());
-                        String oldDb = null;
-                        if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
-                            oldDb = locallyScopedConn.getDatabase();
-                            locallyScopedConn.setDatabase(this.getCurrentDatabase());
-                        }
-
-                        CachedResultSetMetaData cachedMetadata = null;
-                        boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
-                        if (cacheResultSetMetadata) {
-                            cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql());
-                        }
-
-                        locallyScopedConn.setSessionMaxRows(this.getQueryInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
-                        rs = this.executeInternal(this.maxRows, sendPacket, this.createStreamingResultSet(), this.getQueryInfo().getFirstStmtChar() == 'S', cachedMetadata, false);
-                        if (cachedMetadata != null) {
-                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), cachedMetadata, rs);
-                        } else if (rs.hasRows() && cacheResultSetMetadata) {
-                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), (CachedResultSetMetaData)null, rs);
-                        }
-
-                        if (this.retrieveGeneratedKeys) {
-                            rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
-                        }
-
-                        if (oldDb != null) {
-                            locallyScopedConn.setDatabase(oldDb);
-                        }
-
-                        if (rs != null) {
-                            this.lastInsertId = rs.getUpdateID();
-                            this.results = rs;
-                        }
-
-                        return rs != null && rs.hasRows();
-                    }
-                }
-            }
-        } catch (CJException var11) {
-            throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
-        }
-    }
+

那又为什么是这个呢,可以在网上找,我们在mybatis-config.xml里配置的

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

-

Linux中的CopyOnWrite

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

-

CopyOnWrite的好处:

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

-

Redis中的CopyOnWrite

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

-]]>
- - redis - - - redis - -
- - rabbitmq-tips - /2017/04/25/rabbitmq-tips/ - rabbitmq 介绍

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

-

简单的使用经验

通俗的理解

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

-

集群经验

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

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

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

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

其他可以参考官方文档

-

一些坑

消息丢失

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

-

集群搭建

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

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

-

SDS 简单动态字符串

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

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

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

-

链表

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

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

因此在parseConfiguration中配置environment时

+
private void parseConfiguration(XNode root) {
+    try {
+      // issue #117 read properties first
+      propertiesElement(root.evalNode("properties"));
+      Properties settings = settingsAsProperties(root.evalNode("settings"));
+      loadCustomVfs(settings);
+      loadCustomLogImpl(settings);
+      typeAliasesElement(root.evalNode("typeAliases"));
+      pluginElement(root.evalNode("plugins"));
+      objectFactoryElement(root.evalNode("objectFactory"));
+      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
+      reflectorFactoryElement(root.evalNode("reflectorFactory"));
+      settingsElement(settings);
+      // read it after objectFactory and objectWrapperFactory issue #631
+      // ----------> 就是这里
+      environmentsElement(root.evalNode("environments"));
+      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
+      typeHandlerElement(root.evalNode("typeHandlers"));
+      mapperElement(root.evalNode("mappers"));
+    } catch (Exception e) {
+      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
+    }
+  }
-typedef struct list { - // 链表表头 - listNode *head; - // 当前节点,也可以说是最后节点 - listNode *tail; - // 节点复制函数 - void *(*dup)(void *ptr); - // 节点值释放函数 - void (*free)(void *ptr); - // 节点值比较函数 - int (*match)(void *ptr, void *key); - // 链表包含的节点数量 - unsigned int len; -} list;
-

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

-

字典

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

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

调用的这个方法通过获取xml中的transactionManager 配置的类型,也就是JDBC

+
private void environmentsElement(XNode context) throws Exception {
+  if (context != null) {
+    if (environment == null) {
+      environment = context.getStringAttribute("default");
+    }
+    for (XNode child : context.getChildren()) {
+      String id = child.getStringAttribute("id");
+      if (isSpecifiedEnvironment(id)) {
+        // -------> 找到这里
+        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
+        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
+        DataSource dataSource = dsFactory.getDataSource();
+        Environment.Builder environmentBuilder = new Environment.Builder(id)
+            .transactionFactory(txFactory)
+            .dataSource(dataSource);
+        configuration.setEnvironment(environmentBuilder.build());
+        break;
+      }
+    }
+  }
+}
-typedef struct dictType { - unsigned int (*hashFunction)(const void *key); - void *(*keyDup)(void *privdata, const void *key); - void *(*valDup)(void *privdata, const void *obj); - int (*keyCompare)(void *privdata, const void *key1, const void *key2); - void (*keyDestructor)(void *privdata, void *key); - void (*valDestructor)(void *privdata, void *obj); -} dictType; +

是通过以下方法获取的,

+
// 方法全限定名 org.apache.ibatis.builder.xml.XMLConfigBuilder#transactionManagerElement
+private TransactionFactory transactionManagerElement(XNode context) throws Exception {
+    if (context != null) {
+      String type = context.getStringAttribute("type");
+      Properties props = context.getChildrenAsProperties();
+      TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
+      factory.setProperties(props);
+      return factory;
+    }
+    throw new BuilderException("Environment declaration requires a TransactionFactory.");
+  }
 
-/* This is our hash table structure. Every dictionary has two of this as we
- * implement incremental rehashing, for the old to the new table. */
-typedef struct dictht {
-    dictEntry **table;
-    unsigned long size;
-    unsigned long sizemask;
-    unsigned long used;
-} dictht;
+// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveClass
+protected <T> Class<? extends T> resolveClass(String alias) {
+    if (alias == null) {
+      return null;
+    }
+    try {
+      return resolveAlias(alias);
+    } catch (Exception e) {
+      throw new BuilderException("Error resolving class. Cause: " + e, e);
+    }
+  }
 
-typedef struct dict {
-    dictType *type;
-    void *privdata;
-    dictht ht[2];
-    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
-    int iterators; /* number of iterators currently running */
-} dict;
-

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

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

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

-
typedef struct intset {
-    // 编码方式
-    uint32_t encoding;
-    // 集合包含的元素数量
-    uint32_t length;
-    // 保存元素的数组
-    int8_t contents[];
-} intset;
+// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveAlias
+  protected <T> Class<? extends T> resolveAlias(String alias) {
+    return typeAliasRegistry.resolveAlias(alias);
+  }
+// 方法全限定名 org.apache.ibatis.type.TypeAliasRegistry#resolveAlias
+  public <T> Class<T> resolveAlias(String string) {
+    try {
+      if (string == null) {
+        return null;
+      }
+      // issue #748
+      String key = string.toLowerCase(Locale.ENGLISH);
+      Class<T> value;
+      if (typeAliases.containsKey(key)) {
+        value = (Class<T>) typeAliases.get(key);
+      } else {
+        value = (Class<T>) Resources.classForName(string);
+      }
+      return value;
+    } catch (ClassNotFoundException e) {
+      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
+    }
+  }
+

而通过JDBC获取得是啥的,就是在Configuration的构造方法里写了的JdbcTransactionFactory

+
public Configuration() {
+  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
-/* Note that these encodings are ordered, so: - * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */ -#define INTSET_ENC_INT16 (sizeof(int16_t)) -#define INTSET_ENC_INT32 (sizeof(int32_t)) -#define INTSET_ENC_INT64 (sizeof(int64_t))
-

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

-
-

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

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

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

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

-]]>
- - Redis - 数据结构 - 源码 - C - Redis - - - redis - 数据结构 - 源码 - -
- - mybatis系列-第一条sql的更多细节 - /2022/12/18/mybatis%E7%B3%BB%E5%88%97-%E7%AC%AC%E4%B8%80%E6%9D%A1sql%E7%9A%84%E6%9B%B4%E5%A4%9A%E7%BB%86%E8%8A%82/ - 执行细节
首先设置了默认的languageDriver
org/mybatis/mybatis/3.5.11/mybatis-3.5.11-sources.jar!/org/apache/ibatis/session/Configuration.java:215
configuration的构造方法里

-
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
+

所以我们在这

+
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
+  Transaction tx = null;
+  try {
+    final Environment environment = configuration.getEnvironment();
+    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
-

而在
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
中,创建了sqlSource,这里就会根据前面的 LanguageDriver 的实现选择对应的 sqlSource

-
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
+

获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后

+
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
+```java
 
-

createSqlSource 就会调用

-
@Override
-public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
-  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
-  return builder.parseScriptNode();
-}
+创建的transaction就是JdbcTransaction +```java + @Override + public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) { + return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose); + }
-

再往下的逻辑在 parseScriptNode 中,org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode

-
public SqlSource parseScriptNode() {
-  MixedSqlNode rootSqlNode = parseDynamicTags(context);
-  SqlSource sqlSource;
-  if (isDynamic) {
-    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+

然后我们再会上去看代码getConnection ,

+
protected Connection getConnection(Log statementLog) throws SQLException {
+  // -------> 这里的transaction就是JdbcTransaction
+  Connection connection = transaction.getConnection();
+  if (statementLog.isDebugEnabled()) {
+    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
   } else {
-    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+    return connection;
   }
-  return sqlSource;
-}
+}
-

首先要解析dynamicTag,调用了org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags

-
protected MixedSqlNode parseDynamicTags(XNode node) {
-    List<SqlNode> contents = new ArrayList<>();
-    NodeList children = node.getNode().getChildNodes();
-    for (int i = 0; i < children.getLength(); i++) {
-      XNode child = node.newXNode(children.item(i));
-      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
-        String data = child.getStringBody("");
-        TextSqlNode textSqlNode = new TextSqlNode(data);
-        // ---------> 主要是这边的逻辑
-        if (textSqlNode.isDynamic()) {
-          contents.add(textSqlNode);
-          isDynamic = true;
+

即调用了

+
  @Override
+  public Connection getConnection() throws SQLException {
+    if (connection == null) {
+      openConnection();
+    }
+    return connection;
+  }
+
+  protected void openConnection() throws SQLException {
+    if (log.isDebugEnabled()) {
+      log.debug("Opening JDBC Connection");
+    }
+    connection = dataSource.getConnection();
+    if (level != null) {
+      connection.setTransactionIsolation(level.getLevel());
+    }
+    setDesiredAutoCommit(autoCommit);
+  }
+  @Override
+  public Connection getConnection() throws SQLException {
+    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+  }
+
+private PooledConnection popConnection(String username, String password) throws SQLException {
+    boolean countedWait = false;
+    PooledConnection conn = null;
+    long t = System.currentTimeMillis();
+    int localBadConnectionCount = 0;
+
+    while (conn == null) {
+      lock.lock();
+      try {
+        if (!state.idleConnections.isEmpty()) {
+          // Pool has available connection
+          conn = state.idleConnections.remove(0);
+          if (log.isDebugEnabled()) {
+            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
+          }
         } else {
-          contents.add(new StaticTextSqlNode(data));
+          // Pool does not have available connection
+          if (state.activeConnections.size() < poolMaximumActiveConnections) {
+            // Can create new connection
+            // ------------> 走到这里会创建PooledConnection,但是里面会先调用dataSource.getConnection()
+            conn = new PooledConnection(dataSource.getConnection(), this);
+            if (log.isDebugEnabled()) {
+              log.debug("Created connection " + conn.getRealHashCode() + ".");
+            }
+          } else {
+            // Cannot create new connection
+            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
+            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
+            if (longestCheckoutTime > poolMaximumCheckoutTime) {
+              // Can claim overdue connection
+              state.claimedOverdueConnectionCount++;
+              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
+              state.accumulatedCheckoutTime += longestCheckoutTime;
+              state.activeConnections.remove(oldestActiveConnection);
+              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
+                try {
+                  oldestActiveConnection.getRealConnection().rollback();
+                } catch (SQLException e) {
+                  /*
+                     Just log a message for debug and continue to execute the following
+                     statement like nothing happened.
+                     Wrap the bad connection with a new PooledConnection, this will help
+                     to not interrupt current executing thread and give current thread a
+                     chance to join the next competition for another valid/good database
+                     connection. At the end of this loop, bad {@link @conn} will be set as null.
+                   */
+                  log.debug("Bad connection. Could not roll back");
+                }
+              }
+              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
+              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
+              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
+              oldestActiveConnection.invalidate();
+              if (log.isDebugEnabled()) {
+                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
+              }
+            } else {
+              // Must wait
+              try {
+                if (!countedWait) {
+                  state.hadToWaitCount++;
+                  countedWait = true;
+                }
+                if (log.isDebugEnabled()) {
+                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
+                }
+                long wt = System.currentTimeMillis();
+                condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
+                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
+              } catch (InterruptedException e) {
+                // set interrupt flag
+                Thread.currentThread().interrupt();
+                break;
+              }
+            }
+          }
         }
-      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
-        String nodeName = child.getNode().getNodeName();
-        NodeHandler handler = nodeHandlerMap.get(nodeName);
-        if (handler == null) {
-          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+        if (conn != null) {
+          // ping to server and check the connection is valid or not
+          if (conn.isValid()) {
+            if (!conn.getRealConnection().getAutoCommit()) {
+              conn.getRealConnection().rollback();
+            }
+            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
+            conn.setCheckoutTimestamp(System.currentTimeMillis());
+            conn.setLastUsedTimestamp(System.currentTimeMillis());
+            state.activeConnections.add(conn);
+            state.requestCount++;
+            state.accumulatedRequestTime += System.currentTimeMillis() - t;
+          } else {
+            if (log.isDebugEnabled()) {
+              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
+            }
+            state.badConnectionCount++;
+            localBadConnectionCount++;
+            conn = null;
+            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
+              if (log.isDebugEnabled()) {
+                log.debug("PooledDataSource: Could not get a good connection to the database.");
+              }
+              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
+            }
+          }
         }
-        handler.handleNode(child, contents);
-        isDynamic = true;
+      } finally {
+        lock.unlock();
       }
-    }
-    return new MixedSqlNode(contents);
-  }
-

判断是否是动态sql,调用了org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic

-
public boolean isDynamic() {
-  DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
-  // ----------> 主要是这里的方法
-  GenericTokenParser parser = createParser(checker);
-  parser.parse(text);
-  return checker.isDynamic();
-}
+ } -

创建parser的时候可以看到这个parser是干了啥,其实就是找有没有${ , }

-
private GenericTokenParser createParser(TokenHandler handler) {
-  return new GenericTokenParser("${", "}", handler);
-}
+ if (conn == null) { + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } -

如果是的话,就在上面把 isDynamic 设置为true 如果是true 的话就创建 DynamicSqlSource

-
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ return conn; + }
-

如果不是的话就创建RawSqlSource

-
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+

其实就是调用的

+
// org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
+  @Override
+  public Connection getConnection() throws SQLException {
+    return doGetConnection(username, password);
+  }
 ```java
 
-但是这不是一个真实可用的 `sqlSource` ,
-实际创建的时候会走到这
+然后就是
 ```java
-public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
-    this(configuration, getSql(configuration, rootSqlNode), parameterType);
+private Connection doGetConnection(String username, String password) throws SQLException {
+    Properties props = new Properties();
+    if (driverProperties != null) {
+      props.putAll(driverProperties);
+    }
+    if (username != null) {
+      props.setProperty("user", username);
+    }
+    if (password != null) {
+      props.setProperty("password", password);
+    }
+    return doGetConnection(props);
+  }
+ +

继续这个逻辑

+
  private Connection doGetConnection(Properties properties) throws SQLException {
+    initializeDriver();
+    Connection connection = DriverManager.getConnection(url, properties);
+    configureConnection(connection);
+    return connection;
   }
+    @CallerSensitive
+    public static Connection getConnection(String url,
+        java.util.Properties info) throws SQLException {
 
-  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
-    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
-    Class<?> clazz = parameterType == null ? Object.class : parameterType;
-    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
-  }
+ return (getConnection(url, info, Reflection.getCallerClass())); + } +private static Connection getConnection( + String url, java.util.Properties info, Class<?> caller) throws SQLException { + /* + * When callerCl is null, we should check the application's + * (which is invoking this class indirectly) + * classloader, so that the JDBC driver class outside rt.jar + * can be loaded from here. + */ + ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; + synchronized(DriverManager.class) { + // synchronize loading of the correct classloader. + if (callerCL == null) { + callerCL = Thread.currentThread().getContextClassLoader(); + } + } -

具体的sqlSource是通过org.apache.ibatis.builder.SqlSourceBuilder#parse 创建的
具体的代码逻辑是

-
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
-  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
-  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
-  String sql;
-  if (configuration.isShrinkWhitespacesInSql()) {
-    sql = parser.parse(removeExtraWhitespaces(originalSql));
-  } else {
-    sql = parser.parse(originalSql);
-  }
-  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
-}
+ if(url == null) { + throw new SQLException("The url cannot be null", "08001"); + } -

这里创建的其实是StaticSqlSource ,多带一句前面的parser是将原来这样select * from student where id = #{id}sql 解析成了select * from student where id = ? 然后创建了StaticSqlSource

-
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
-  this.sql = sql;
-  this.parameterMappings = parameterMappings;
-  this.configuration = configuration;
-}
+ println("DriverManager.getConnection(\"" + url + "\")"); -

为什么前面要讲这么多好像没什么关系的代码呢,其实在最开始我们执行sql的代码中

-
@Override
-  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
-    BoundSql boundSql = ms.getBoundSql(parameterObject);
-    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
-    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
-  }
+ // Walk through the loaded registeredDrivers attempting to make a connection. + // Remember the first exception that gets raised so we can reraise it. + SQLException reason = null; + + for(DriverInfo aDriver : registeredDrivers) { + // If the caller does not have permission to load the driver then + // skip it. + if(isDriverAllowed(aDriver.driver, callerCL)) { + try { + // ----------> driver[className=com.mysql.cj.jdbc.Driver@64030b91] + println(" trying " + aDriver.driver.getClass().getName()); + Connection con = aDriver.driver.connect(url, info); + if (con != null) { + // Success! + println("getConnection returning " + aDriver.driver.getClass().getName()); + return (con); + } + } catch (SQLException ex) { + if (reason == null) { + reason = ex; + } + } + + } else { + println(" skipping: " + aDriver.getClass().getName()); + } -

这里获取了BoundSql,而BoundSql是怎么来的呢,首先调用了org.apache.ibatis.mapping.MappedStatement#getBoundSql

-
public BoundSql getBoundSql(Object parameterObject) {
-    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
-    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
-    if (parameterMappings == null || parameterMappings.isEmpty()) {
-      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
-    }
+        }
 
-    // check for nested result maps in parameter mappings (issue #30)
-    for (ParameterMapping pm : boundSql.getParameterMappings()) {
-      String rmId = pm.getResultMapId();
-      if (rmId != null) {
-        ResultMap rm = configuration.getResultMap(rmId);
-        if (rm != null) {
-          hasNestedResultMaps |= rm.hasNestedResultMaps();
+        // if we got here nobody could connect.
+        if (reason != null)    {
+            println("getConnection failed: " + reason);
+            throw reason;
         }
-      }
-    }
 
-    return boundSql;
-  }
+ println("getConnection: no suitable driver found for "+ url); + throw new SQLException("No suitable driver found for "+ url, "08001"); + }
-

而我们从上面的解析中可以看到这里的sqlSource是一层RawSqlSource , 它的getBoundSql又是调用内部的sqlSource的方法

-
@Override
-public BoundSql getBoundSql(Object parameterObject) {
-  return sqlSource.getBoundSql(parameterObject);
-}
-

内部的sqlSource 就是StaticSqlSource

-
@Override
-public BoundSql getBoundSql(Object parameterObject) {
-  return new BoundSql(configuration, sql, parameterMappings, parameterObject);
-}
+

上面的driver就是driver[className=com.mysql.cj.jdbc.Driver@64030b91]

+
// com.mysql.cj.jdbc.NonRegisteringDriver#connect
+public Connection connect(String url, Properties info) throws SQLException {
+        try {
+            try {
+                if (!ConnectionUrl.acceptsUrl(url)) {
+                    return null;
+                } else {
+                    ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
+                    switch (conStr.getType()) {
+                        case SINGLE_CONNECTION:
+                            return ConnectionImpl.getInstance(conStr.getMainHost());
+                        case FAILOVER_CONNECTION:
+                        case FAILOVER_DNS_SRV_CONNECTION:
+                            return FailoverConnectionProxy.createProxyInstance(conStr);
+                        case LOADBALANCE_CONNECTION:
+                        case LOADBALANCE_DNS_SRV_CONNECTION:
+                            return LoadBalancedConnectionProxy.createProxyInstance(conStr);
+                        case REPLICATION_CONNECTION:
+                        case REPLICATION_DNS_SRV_CONNECTION:
+                            return ReplicationConnectionProxy.createProxyInstance(conStr);
+                        default:
+                            return null;
+                    }
+                }
+            } catch (UnsupportedConnectionStringException var5) {
+                return null;
+            } catch (CJException var6) {
+                throw (UnableToConnectException)ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("NonRegisteringDriver.17", new Object[]{var6.toString()}), var6);
+            }
+        } catch (CJException var7) {
+            throw SQLExceptionsMapping.translateException(var7);
+        }
+    }
-

这个BoundSql的内容也比较简单

-
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
-  this.sql = sql;
-  this.parameterMappings = parameterMappings;
-  this.parameterObject = parameterObject;
-  this.additionalParameters = new HashMap<>();
-  this.metaParameters = configuration.newMetaObject(additionalParameters);
-}
+

这是个 SINGLE_CONNECTION ,所以调用的就是 return ConnectionImpl.getInstance(conStr.getMainHost());
然后在这里设置了代理类

+
public PooledConnection(Connection connection, PooledDataSource dataSource) {
+    this.hashCode = connection.hashCode();
+    this.realConnection = connection;
+    this.dataSource = dataSource;
+    this.createdTimestamp = System.currentTimeMillis();
+    this.lastUsedTimestamp = System.currentTimeMillis();
+    this.valid = true;
+    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
+  }
-

而上次在这边org.apache.ibatis.executor.SimpleExecutor#doQuery 的时候落了个东西,就是StatementHandler的逻辑

+

结合这个

@Override
-public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
-  Statement stmt = null;
-  try {
-    Configuration configuration = ms.getConfiguration();
-    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
-    stmt = prepareStatement(handler, ms.getStatementLog());
-    return handler.query(stmt, resultHandler);
-  } finally {
-    closeStatement(stmt);
-  }
-}
- -

它是通过statementType来区分应该使用哪个statementHandler,我们这使用的就是PreparedStatementHandler

-
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+public Connection getConnection() throws SQLException {
+  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+}
- switch (ms.getStatementType()) { - case STATEMENT: - delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); - break; - case PREPARED: - delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); - break; - case CALLABLE: - delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); - break; - default: - throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); +

所以最终的connection就是com.mysql.cj.jdbc.ConnectionImpl@358ab600

+]]>
+ + Java + Mybatis + + + Java + Mysql + Mybatis + +
+ + mybatis系列-第一条sql的细节 + /2022/12/11/mybatis%E7%B3%BB%E5%88%97-%E7%AC%AC%E4%B8%80%E6%9D%A1sql%E7%9A%84%E7%BB%86%E8%8A%82/ + 先补充两个点,
第一是前面我们说了
使用org.apache.ibatis.builder.xml.XMLConfigBuilder 创建了parser解析器,那么解析的结果是什么
看这个方法的返回值

+
public Configuration parse() {
+  if (parsed) {
+    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
   }
+  parsed = true;
+  parseConfiguration(parser.evalNode("/configuration"));
+  return configuration;
+}
-}
+

返回的是 org.apache.ibatis.session.Configuration , 而这个 Configuration 也是 mybatis 中特别重要的配置核心类,贴一下里面的成员变量,

+
public class Configuration {
 
-

所以上次有个细节可以补充,这边的doQuery里面的handler.query 应该是调用了PreparedStatementHandler 的query方法

-
@Override
-public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
-  Statement stmt = null;
-  try {
-    Configuration configuration = ms.getConfiguration();
-    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
-    stmt = prepareStatement(handler, ms.getStatementLog());
-    return handler.query(stmt, resultHandler);
-  } finally {
-    closeStatement(stmt);
-  }
-}
+ protected Environment environment; + protected boolean safeRowBoundsEnabled; + protected boolean safeResultHandlerEnabled = true; + protected boolean mapUnderscoreToCamelCase; + protected boolean aggressiveLazyLoading; + protected boolean multipleResultSetsEnabled = true; + protected boolean useGeneratedKeys; + protected boolean useColumnLabel = true; + protected boolean cacheEnabled = true; + protected boolean callSettersOnNulls; + protected boolean useActualParamName = true; + protected boolean returnInstanceForEmptyRow; + protected boolean shrinkWhitespacesInSql; + protected boolean nullableOnForEach; + protected boolean argNameBasedConstructorAutoMapping; -

因为上面prepareStatement中getConnection拿到connection是com.mysql.cj.jdbc.ConnectionImpl#ConnectionImpl(com.mysql.cj.conf.HostInfo)

-
@Override
-public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
-  PreparedStatement ps = (PreparedStatement) statement;
-  ps.execute();
-  return resultSetHandler.handleResultSets(ps);
-}
+ protected String logPrefix; + protected Class<? extends Log> logImpl; + protected Class<? extends VFS> vfsImpl; + protected Class<?> defaultSqlProviderType; + protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION; + protected JdbcType jdbcTypeForNull = JdbcType.OTHER; + protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString")); + protected Integer defaultStatementTimeout; + protected Integer defaultFetchSize; + protected ResultSetType defaultResultSetType; + protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; + protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL; + protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE; -

那又为什么是这个呢,可以在网上找,我们在mybatis-config.xml里配置的

-
<transactionManager type="JDBC"/>
+ protected Properties variables = new Properties(); + protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); + protected ObjectFactory objectFactory = new DefaultObjectFactory(); + protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); -

因此在parseConfiguration中配置environment时

-
private void parseConfiguration(XNode root) {
-    try {
-      // issue #117 read properties first
-      propertiesElement(root.evalNode("properties"));
-      Properties settings = settingsAsProperties(root.evalNode("settings"));
-      loadCustomVfs(settings);
-      loadCustomLogImpl(settings);
-      typeAliasesElement(root.evalNode("typeAliases"));
-      pluginElement(root.evalNode("plugins"));
-      objectFactoryElement(root.evalNode("objectFactory"));
-      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
-      reflectorFactoryElement(root.evalNode("reflectorFactory"));
-      settingsElement(settings);
-      // read it after objectFactory and objectWrapperFactory issue #631
-      // ----------> 就是这里
-      environmentsElement(root.evalNode("environments"));
-      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
-      typeHandlerElement(root.evalNode("typeHandlers"));
-      mapperElement(root.evalNode("mappers"));
-    } catch (Exception e) {
-      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
-    }
-  }
+ protected boolean lazyLoadingEnabled = false; + protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL -

调用的这个方法通过获取xml中的transactionManager 配置的类型,也就是JDBC

-
private void environmentsElement(XNode context) throws Exception {
-  if (context != null) {
-    if (environment == null) {
-      environment = context.getStringAttribute("default");
-    }
-    for (XNode child : context.getChildren()) {
-      String id = child.getStringAttribute("id");
-      if (isSpecifiedEnvironment(id)) {
-        // -------> 找到这里
-        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
-        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
-        DataSource dataSource = dsFactory.getDataSource();
-        Environment.Builder environmentBuilder = new Environment.Builder(id)
-            .transactionFactory(txFactory)
-            .dataSource(dataSource);
-        configuration.setEnvironment(environmentBuilder.build());
-        break;
-      }
-    }
-  }
-}
+ protected String databaseId; + /** + * Configuration factory class. + * Used to create Configuration for loading deserialized unread properties. + * + * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a> + */ + protected Class<?> configurationFactory; + + protected final MapperRegistry mapperRegistry = new MapperRegistry(this); + protected final InterceptorChain interceptorChain = new InterceptorChain(); + protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this); + protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); + protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry(); -

是通过以下方法获取的,

-
// 方法全限定名 org.apache.ibatis.builder.xml.XMLConfigBuilder#transactionManagerElement
-private TransactionFactory transactionManagerElement(XNode context) throws Exception {
-    if (context != null) {
-      String type = context.getStringAttribute("type");
-      Properties props = context.getChildrenAsProperties();
-      TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
-      factory.setProperties(props);
-      return factory;
-    }
-    throw new BuilderException("Environment declaration requires a TransactionFactory.");
-  }
+  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
+      .conflictMessageProducer((savedValue, targetValue) ->
+          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
+  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
+  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
+  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
+  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
 
-// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveClass
-protected <T> Class<? extends T> resolveClass(String alias) {
-    if (alias == null) {
-      return null;
-    }
-    try {
-      return resolveAlias(alias);
-    } catch (Exception e) {
-      throw new BuilderException("Error resolving class. Cause: " + e, e);
-    }
-  }
+  protected final Set<String> loadedResources = new HashSet<>();
+  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
 
-// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveAlias
-  protected <T> Class<? extends T> resolveAlias(String alias) {
-    return typeAliasRegistry.resolveAlias(alias);
-  }
-// 方法全限定名 org.apache.ibatis.type.TypeAliasRegistry#resolveAlias
-  public <T> Class<T> resolveAlias(String string) {
-    try {
-      if (string == null) {
-        return null;
-      }
-      // issue #748
-      String key = string.toLowerCase(Locale.ENGLISH);
-      Class<T> value;
-      if (typeAliases.containsKey(key)) {
-        value = (Class<T>) typeAliases.get(key);
-      } else {
-        value = (Class<T>) Resources.classForName(string);
-      }
-      return value;
-    } catch (ClassNotFoundException e) {
-      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
-    }
-  }
-

而通过JDBC获取得是啥的,就是在Configuration的构造方法里写了的JdbcTransactionFactory

-
public Configuration() {
-  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
+ protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>(); + protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>(); + protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>(); + protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
-

所以我们在这

+

这么多成员变量,先不一一解释作用,但是其中的几个参数我们应该是已经知道了的,第一个就是 mappedStatements ,上一篇我们知道被解析的mapper就是放在这里,后面的 resultMapsparameterMaps 也比较常用的就是我们参数和结果的映射map,这里跟我之前有一篇解释为啥我们一些变量的使用会比较特殊,比如list,可以参考这篇keyGenerators是在我们需要定义主键生成器的时候使用。
然后第二点是我们创建的 org.apache.ibatis.session.SqlSessionFactory 是哪个,

+
public SqlSessionFactory build(Configuration config) {
+  return new DefaultSqlSessionFactory(config);
+}
+ +

是这个 DefaultSqlSessionFactory ,这是其中一个 SqlSessionFactory 的实现
接下来我们看看 openSession 里干了啥

+
public SqlSession openSession() {
+  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
+}
+ +

这边有几个参数,第一个是默认的执行器类型,往上找找上面贴着的 Configuration 的成员变量里可以看到默认是
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

+

因为没有指明特殊的执行逻辑,所以默认我们也就用简单类型的,第二个参数是是事务级别,第三个是是否自动提交

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
   Transaction tx = null;
   try {
     final Environment environment = configuration.getEnvironment();
-    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
- -

获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后

-
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
-```java
-
-创建的transaction就是JdbcTransaction 
-```java
-  @Override
-  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
-    return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose);
-  }
- -

然后我们再会上去看代码getConnection ,

-
protected Connection getConnection(Log statementLog) throws SQLException {
-  // -------> 这里的transaction就是JdbcTransaction
-  Connection connection = transaction.getConnection();
-  if (statementLog.isDebugEnabled()) {
-    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
-  } else {
-    return connection;
-  }
-}
- -

即调用了

-
  @Override
-  public Connection getConnection() throws SQLException {
-    if (connection == null) {
-      openConnection();
-    }
-    return connection;
+    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
+    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
+    // --------> 先关注这里
+    final Executor executor = configuration.newExecutor(tx, execType);
+    return new DefaultSqlSession(configuration, executor, autoCommit);
+  } catch (Exception e) {
+    closeTransaction(tx); // may have fetched a connection so lets call close()
+    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
+  } finally {
+    ErrorContext.instance().reset();
   }
+}
- protected void openConnection() throws SQLException { - if (log.isDebugEnabled()) { - log.debug("Opening JDBC Connection"); - } - connection = dataSource.getConnection(); - if (level != null) { - connection.setTransactionIsolation(level.getLevel()); - } - setDesiredAutoCommit(autoCommit); +

具体是调用了 Configuration 的这个方法

+
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
+  executorType = executorType == null ? defaultExecutorType : executorType;
+  Executor executor;
+  if (ExecutorType.BATCH == executorType) {
+    executor = new BatchExecutor(this, transaction);
+  } else if (ExecutorType.REUSE == executorType) {
+    executor = new ReuseExecutor(this, transaction);
+  } else {
+    // ---------> 会走到这个分支
+    executor = new SimpleExecutor(this, transaction);
   }
-  @Override
-  public Connection getConnection() throws SQLException {
-    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+  if (cacheEnabled) {
+    executor = new CachingExecutor(executor);
   }
+  executor = (Executor) interceptorChain.pluginAll(executor);
+  return executor;
+}
-private PooledConnection popConnection(String username, String password) throws SQLException { - boolean countedWait = false; - PooledConnection conn = null; - long t = System.currentTimeMillis(); - int localBadConnectionCount = 0; - - while (conn == null) { - lock.lock(); - try { - if (!state.idleConnections.isEmpty()) { - // Pool has available connection - conn = state.idleConnections.remove(0); - if (log.isDebugEnabled()) { - log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); - } - } else { - // Pool does not have available connection - if (state.activeConnections.size() < poolMaximumActiveConnections) { - // Can create new connection - // ------------> 走到这里会创建PooledConnection,但是里面会先调用dataSource.getConnection() - conn = new PooledConnection(dataSource.getConnection(), this); - if (log.isDebugEnabled()) { - log.debug("Created connection " + conn.getRealHashCode() + "."); - } - } else { - // Cannot create new connection - PooledConnection oldestActiveConnection = state.activeConnections.get(0); - long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); - if (longestCheckoutTime > poolMaximumCheckoutTime) { - // Can claim overdue connection - state.claimedOverdueConnectionCount++; - state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; - state.accumulatedCheckoutTime += longestCheckoutTime; - state.activeConnections.remove(oldestActiveConnection); - if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { - try { - oldestActiveConnection.getRealConnection().rollback(); - } catch (SQLException e) { - /* - Just log a message for debug and continue to execute the following - statement like nothing happened. - Wrap the bad connection with a new PooledConnection, this will help - to not interrupt current executing thread and give current thread a - chance to join the next competition for another valid/good database - connection. At the end of this loop, bad {@link @conn} will be set as null. - */ - log.debug("Bad connection. Could not roll back"); - } - } - conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); - conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); - conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); - oldestActiveConnection.invalidate(); - if (log.isDebugEnabled()) { - log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); - } - } else { - // Must wait - try { - if (!countedWait) { - state.hadToWaitCount++; - countedWait = true; - } - if (log.isDebugEnabled()) { - log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); - } - long wt = System.currentTimeMillis(); - condition.await(poolTimeToWait, TimeUnit.MILLISECONDS); - state.accumulatedWaitTime += System.currentTimeMillis() - wt; - } catch (InterruptedException e) { - // set interrupt flag - Thread.currentThread().interrupt(); - break; - } - } - } - } - if (conn != null) { - // ping to server and check the connection is valid or not - if (conn.isValid()) { - if (!conn.getRealConnection().getAutoCommit()) { - conn.getRealConnection().rollback(); - } - conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); - conn.setCheckoutTimestamp(System.currentTimeMillis()); - conn.setLastUsedTimestamp(System.currentTimeMillis()); - state.activeConnections.add(conn); - state.requestCount++; - state.accumulatedRequestTime += System.currentTimeMillis() - t; - } else { - if (log.isDebugEnabled()) { - log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); - } - state.badConnectionCount++; - localBadConnectionCount++; - conn = null; - if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { - if (log.isDebugEnabled()) { - log.debug("PooledDataSource: Could not get a good connection to the database."); - } - throw new SQLException("PooledDataSource: Could not get a good connection to the database."); - } - } - } - } finally { - lock.unlock(); - } - - } +

上面传入的 executorTypeConfiguration 的默认类型,也就是 simple 类型,并且 cacheEnabledConfiguration 默认为 true,所以会包装成CachingExecutor ,然后后面就是插件了,这块我们先不展开
然后我们的openSession返回的就是创建了DefaultSqlSession

+
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
+    this.configuration = configuration;
+    this.executor = executor;
+    this.dirty = false;
+    this.autoCommit = autoCommit;
+  }
- if (conn == null) { - if (log.isDebugEnabled()) { - log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); - } - throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); - } +

然后就是调用 selectOne, 因为前面已经把这部分代码说过了,就直接跳转过来
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

+
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
+  try {
+    MappedStatement ms = configuration.getMappedStatement(statement);
+    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
+  } catch (Exception e) {
+    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
+  } finally {
+    ErrorContext.instance().reset();
+  }
+}
- return conn; - }
+

因为前面说了 executor 包装了 CachingExecutor ,所以会先调用

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

其实就是调用的

-
// org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
-  @Override
-  public Connection getConnection() throws SQLException {
-    return doGetConnection(username, password);
+

然后是调用的真实的query方法

+
@Override
+public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
+    throws SQLException {
+  Cache cache = ms.getCache();
+  if (cache != null) {
+    flushCacheIfRequired(ms);
+    if (ms.isUseCache() && resultHandler == null) {
+      ensureNoOutParams(ms, boundSql);
+      @SuppressWarnings("unchecked")
+      List<E> list = (List<E>) tcm.getObject(cache, key);
+      if (list == null) {
+        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+        tcm.putObject(cache, key, list); // issue #578 and #116
+      }
+      return list;
+    }
   }
-```java
+  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+}
-然后就是 -```java -private Connection doGetConnection(String username, String password) throws SQLException { - Properties props = new Properties(); - if (driverProperties != null) { - props.putAll(driverProperties); +

这里是第一次查询,没有缓存就先到最后一行,继续是调用到 org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

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

继续这个逻辑

-
  private Connection doGetConnection(Properties properties) throws SQLException {
-    initializeDriver();
-    Connection connection = DriverManager.getConnection(url, properties);
-    configureConnection(connection);
-    return connection;
+

然后是

+
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
+  List<E> list;
+  localCache.putObject(key, EXECUTION_PLACEHOLDER);
+  try {
+    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
+  } finally {
+    localCache.removeObject(key);
   }
-    @CallerSensitive
-    public static Connection getConnection(String url,
-        java.util.Properties info) throws SQLException {
+  localCache.putObject(key, list);
+  if (ms.getStatementType() == StatementType.CALLABLE) {
+    localOutputParameterCache.putObject(key, parameter);
+  }
+  return list;
+}
- return (getConnection(url, info, Reflection.getCallerClass())); - } -private static Connection getConnection( - String url, java.util.Properties info, Class<?> caller) throws SQLException { - /* - * When callerCl is null, we should check the application's - * (which is invoking this class indirectly) - * classloader, so that the JDBC driver class outside rt.jar - * can be loaded from here. - */ - ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; - synchronized(DriverManager.class) { - // synchronize loading of the correct classloader. - if (callerCL == null) { - callerCL = Thread.currentThread().getContextClassLoader(); +

然后就是 simpleExecutor 的执行过程

+
@Override
+public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+  Statement stmt = null;
+  try {
+    Configuration configuration = ms.getConfiguration();
+    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
+    stmt = prepareStatement(handler, ms.getStatementLog());
+    return handler.query(stmt, resultHandler);
+  } finally {
+    closeStatement(stmt);
+  }
+}
+ +

接下去其实就是跟jdbc交互了

+
@Override
+public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
+  PreparedStatement ps = (PreparedStatement) statement;
+  ps.execute();
+  return resultSetHandler.handleResultSets(ps);
+}
+ +

com.mysql.cj.jdbc.ClientPreparedStatement#execute

+
public boolean execute() throws SQLException {
+        try {
+            synchronized(this.checkClosed().getConnectionMutex()) {
+                JdbcConnection locallyScopedConn = this.connection;
+                if (!this.doPingInstead && !this.checkReadOnlySafeStatement()) {
+                    throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.exceptionInterceptor);
+                } else {
+                    ResultSetInternalMethods rs = null;
+                    this.lastQueryIsOnDupKeyUpdate = false;
+                    if (this.retrieveGeneratedKeys) {
+                        this.lastQueryIsOnDupKeyUpdate = this.containsOnDuplicateKeyUpdate();
+                    }
+
+                    this.batchedGeneratedKeys = null;
+                    this.resetCancelledState();
+                    this.implicitlyCloseAllOpenResults();
+                    this.clearWarnings();
+                    if (this.doPingInstead) {
+                        this.doPingInstead();
+                        return true;
+                    } else {
+                        this.setupStreamingTimeout(locallyScopedConn);
+                        Message sendPacket = ((PreparedQuery)this.query).fillSendPacket(((PreparedQuery)this.query).getQueryBindings());
+                        String oldDb = null;
+                        if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
+                            oldDb = locallyScopedConn.getDatabase();
+                            locallyScopedConn.setDatabase(this.getCurrentDatabase());
+                        }
+
+                        CachedResultSetMetaData cachedMetadata = null;
+                        boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
+                        if (cacheResultSetMetadata) {
+                            cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql());
+                        }
+
+                        locallyScopedConn.setSessionMaxRows(this.getQueryInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
+                        rs = this.executeInternal(this.maxRows, sendPacket, this.createStreamingResultSet(), this.getQueryInfo().getFirstStmtChar() == 'S', cachedMetadata, false);
+                        if (cachedMetadata != null) {
+                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), cachedMetadata, rs);
+                        } else if (rs.hasRows() && cacheResultSetMetadata) {
+                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), (CachedResultSetMetaData)null, rs);
+                        }
+
+                        if (this.retrieveGeneratedKeys) {
+                            rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
+                        }
+
+                        if (oldDb != null) {
+                            locallyScopedConn.setDatabase(oldDb);
+                        }
+
+                        if (rs != null) {
+                            this.lastInsertId = rs.getUpdateID();
+                            this.results = rs;
+                        }
+
+                        return rs != null && rs.hasRows();
+                    }
+                }
             }
+        } catch (CJException var11) {
+            throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
         }
+    }
+ +]]> + + Java + Mybatis + + + Java + Mysql + Mybatis + + + + nginx 日志小记 + /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ + nginx 默认的日志有特定的格式,我们也可以自定义,

+

默认的格式是预定义的 combined

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

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

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

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

+
$request_time
+request processing time in seconds with a milliseconds resolution;   
+time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client
- if(url == null) { - throw new SQLException("The url cannot be null", "08001"); - } +
log_format combined_extend '$remote_addr - $remote_user [$time_local] '
+                    '"$request" $status $body_bytes_sent '
+                    '"$http_referer" "$http_user_agent" "$request_time"';
- println("DriverManager.getConnection(\"" + url + "\")"); +

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

+

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

+

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

+
map $status $loggable {
+    ~^[23]  0;
+    default 1;
+}
 
-        // Walk through the loaded registeredDrivers attempting to make a connection.
-        // Remember the first exception that gets raised so we can reraise it.
-        SQLException reason = null;
+access_log /path/to/access.log combined if=$loggable;
- for(DriverInfo aDriver : registeredDrivers) { - // If the caller does not have permission to load the driver then - // skip it. - if(isDriverAllowed(aDriver.driver, callerCL)) { - try { - // ----------> driver[className=com.mysql.cj.jdbc.Driver@64030b91] - println(" trying " + aDriver.driver.getClass().getName()); - Connection con = aDriver.driver.connect(url, info); - if (con != null) { - // Success! - println("getConnection returning " + aDriver.driver.getClass().getName()); - return (con); - } - } catch (SQLException ex) { - if (reason == null) { - reason = ex; - } - } +

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

+

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

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

+

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

+

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

+
http {
+  lua_code_cache off;
+}
 
-            } else {
-                println("    skipping: " + aDriver.getClass().getName());
-            }
+location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
+   default_type 'application/json;charset=utf-8';
+   content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
+}
- } +

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

+
ngx.header['response'] = 'header'
- // if we got here nobody could connect. - if (reason != null) { - println("getConnection failed: " + reason); - throw reason; - } - println("getConnection: no suitable driver found for "+ url); - throw new SQLException("No suitable driver found for "+ url, "08001"); - }
+

使用总结

+

后续:

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

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

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

    +
    local decode = require("cjson").decode
     
    +function json_decode( str )
    +    local ok, t = pcall(decode, str)
    +    if not ok then
    +      return nil
    +    end
     
    -

    上面的driver就是driver[className=com.mysql.cj.jdbc.Driver@64030b91]

    -
    // com.mysql.cj.jdbc.NonRegisteringDriver#connect
    -public Connection connect(String url, Properties info) throws SQLException {
    -        try {
    -            try {
    -                if (!ConnectionUrl.acceptsUrl(url)) {
    -                    return null;
    -                } else {
    -                    ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
    -                    switch (conStr.getType()) {
    -                        case SINGLE_CONNECTION:
    -                            return ConnectionImpl.getInstance(conStr.getMainHost());
    -                        case FAILOVER_CONNECTION:
    -                        case FAILOVER_DNS_SRV_CONNECTION:
    -                            return FailoverConnectionProxy.createProxyInstance(conStr);
    -                        case LOADBALANCE_CONNECTION:
    -                        case LOADBALANCE_DNS_SRV_CONNECTION:
    -                            return LoadBalancedConnectionProxy.createProxyInstance(conStr);
    -                        case REPLICATION_CONNECTION:
    -                        case REPLICATION_DNS_SRV_CONNECTION:
    -                            return ReplicationConnectionProxy.createProxyInstance(conStr);
    -                        default:
    -                            return null;
    -                    }
    -                }
    -            } catch (UnsupportedConnectionStringException var5) {
    -                return null;
    -            } catch (CJException var6) {
    -                throw (UnableToConnectException)ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("NonRegisteringDriver.17", new Object[]{var6.toString()}), var6);
    -            }
    -        } catch (CJException var7) {
    -            throw SQLExceptionsMapping.translateException(var7);
    -        }
    -    }
    + return t +end
    +

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

    +
    local json = require("cjson.safe")
    +local str = [[ {"key:"value"} ]]
     
    -

    这是个 SINGLE_CONNECTION ,所以调用的就是 return ConnectionImpl.getInstance(conStr.getMainHost());
    然后在这里设置了代理类

    -
    public PooledConnection(Connection connection, PooledDataSource dataSource) {
    -    this.hashCode = connection.hashCode();
    -    this.realConnection = connection;
    -    this.dataSource = dataSource;
    -    this.createdTimestamp = System.currentTimeMillis();
    -    this.lastUsedTimestamp = System.currentTimeMillis();
    -    this.valid = true;
    -    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
    -  }
    +local t = json.decode(str) +if t then + ngx.say(" --> ", type(t)) +end
    +

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

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

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

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

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

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

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

+
+

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

+ + +

Regular Expression Syntax

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

+

PCRE函数介绍

+

pcre_compile
原型:

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

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

+
+

pcre_exec
原型:

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

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

+

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

+

一个丑陋的封装

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

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

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

示例代码

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

result

PHP Fatal error:  Abstract function abst1::abstra1() cannot contain body in new.php on line 17
 
-

结合这个

-
@Override
-public Connection getConnection() throws SQLException {
-  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-}
+Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
+]]>
+ + php + + + php + +
+ + powershell 初体验 + /2022/11/13/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C/ + powershell变量

变量命名类似于php

+
PS C:\Users\Nicks> $a=1
+PS C:\Users\Nicks> $b=2
+PS C:\Users\Nicks> $a*$b
+2
+

有一个比较好用的是变量交换
一般的语言做两个变量交换一般需要一个临时变量

+
$tmp=$a
+$a=$b
+$b=$tmp
+

而在powershell中可以这样

+
$a,$b=$b,$a
+PS C:\Users\Nicks> $a,$b=$b,$a
+PS C:\Users\Nicks> $a
+2
+PS C:\Users\Nicks> $b
+1
+

还可以通过这个

+
PS C:\Users\Nicks> ls variable:
 
-

所以最终的connection就是com.mysql.cj.jdbc.ConnectionImpl@358ab600

+Name Value +---- ----- +$ $b +? True +^ $b +a 2 +args {} +b 1
+

查看现存的变量
当然一般脚本都是动态类型的,
可以通过
gettype方法

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

-
/* The actual Redis Object */
-#define OBJ_STRING 0    /* String object. */
-#define OBJ_LIST 1      /* List object. */
-#define OBJ_SET 2       /* Set object. */
-#define OBJ_ZSET 3      /* Sorted set object. */
-#define OBJ_HASH 4      /* Hash object. */
-/*
- * Objects encoding. Some kind of objects like Strings and Hashes can be
- * internally represented in multiple ways. The 'encoding' field of the object
- * is set to one of this fields for this object. */
-#define OBJ_ENCODING_RAW 0     /* Raw representation */
-#define OBJ_ENCODING_INT 1     /* Encoded as integer */
-#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
-#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
-#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
-#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
-#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
-#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
-#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
-#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
-#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
+    rabbitmq-tips
+    /2017/04/25/rabbitmq-tips/
+    rabbitmq 介绍

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

+

简单的使用经验

通俗的理解

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

+

集群经验

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

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

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

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

其他可以参考官方文档

+

一些坑

消息丢失

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

+

集群搭建

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

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

+

Linux中的CopyOnWrite

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

+

CopyOnWrite的好处:

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

+

Redis中的CopyOnWrite

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

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

+

SDS 简单动态字符串

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

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

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

+

链表

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

+
typedef struct listNode {
+    // 前置节点
+    struct listNode *prev;
+    // 后置节点
+    struct listNode *next;
+    // 值
+    void *value;
+} listNode;
 
-#define LRU_BITS 24
-#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
-#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
+typedef struct list {
+    // 链表表头
+    listNode *head;
+    // 当前节点,也可以说是最后节点
+    listNode *tail;
+    // 节点复制函数
+    void *(*dup)(void *ptr);
+    // 节点值释放函数
+    void (*free)(void *ptr);
+    // 节点值比较函数
+    int (*match)(void *ptr, void *key);
+    // 链表包含的节点数量
+    unsigned int len;
+} list;
+

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

+

字典

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

+
typedef struct dictEntry {
+    void *key;
+    void *val;
+    struct dictEntry *next;
+} dictEntry;
 
-#define OBJ_SHARED_REFCOUNT INT_MAX
-typedef struct redisObject {
-    unsigned type:4;
-    unsigned encoding:4;
-    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
-                            * LFU data (least significant 8 bits frequency
-                            * and most significant 16 bits access time). */
-    int refcount;
-    void *ptr;
-} robj;
-

主体结构就是这个 redisObject,

-
    -
  • type: 字段表示对象的类型,它对应的就是 redis 的对外暴露的,或者说用户可以使用的五种类型,OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
  • -
  • encoding: 字段表示这个对象在 redis 内部的编码方式,由OBJ_ENCODING_开头的 11 种
  • -
  • lru: 做LRU替换算法用,占24个bit
  • -
  • refcount: 引用计数。它允许robj对象在某些情况下被共享。
  • -
  • ptr: 指向底层实现数据结构的指针
    当 type 是 OBJ_STRING 时,表示类型是个 string,它的编码方式 encoding 可能有 OBJ_ENCODING_RAW,OBJ_ENCODING_INT,OBJ_ENCODING_EMBSTR 三种
    当 type 是 OBJ_LIST 时,表示类型是 list,它的编码方式 encoding 是 OBJ_ENCODING_QUICKLIST,对于早一些的版本,2.2这种可能还会使用 OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_LINKEDLIST
    当 type 是 OBJ_SET 时,是个集合,但是得看具体元素的类型,有可能使用整数集合,OBJ_ENCODING_INTSET, 如果元素不全是整型或者数量超过一定限制,那么编码就是 OBJ_ENCODING_HT hash table 了
    当 type 是 OBJ_ZSET 时,是个有序集合,它底层有可能使用的是 OBJ_ENCODING_ZIPLIST 或者 OBJ_ENCODING_SKIPLIST
    当 type 是 OBJ_HASH 时,一开始也是 OBJ_ENCODING_ZIPLIST,然后当数据量大于 hash_max_ziplist_entries 时会转成 OBJ_ENCODING_HT
  • -
+typedef struct dictType { + unsigned int (*hashFunction)(const void *key); + void *(*keyDup)(void *privdata, const void *key); + void *(*valDup)(void *privdata, const void *obj); + int (*keyCompare)(void *privdata, const void *key1, const void *key2); + void (*keyDestructor)(void *privdata, void *key); + void (*valDestructor)(void *privdata, void *obj); +} dictType; + +/* This is our hash table structure. Every dictionary has two of this as we + * implement incremental rehashing, for the old to the new table. */ +typedef struct dictht { + dictEntry **table; + unsigned long size; + unsigned long sizemask; + unsigned long used; +} dictht; + +typedef struct dict { + dictType *type; + void *privdata; + dictht ht[2]; + int rehashidx; /* rehashing not in progress if rehashidx == -1 */ + int iterators; /* number of iterators currently running */ +} dict;
+

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

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

+]]>
+ + Redis + 数据结构 + 源码 + C + Redis + + + redis + 数据结构 + 源码 + +
+ + powershell 初体验二 + /2022/11/20/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BA%8C/ + powershell创建数组也很方便
可以这样

+
$nums=2,0,1,2
+

顺便可以用下我们上次学到的gettype()

+

如果是想创建连续数字的数组还可以用这个方便的方法

+
$nums=1..5
+


而且数组还可以存放各种类型的数据

+
$array=1,"哈哈",([System.Guid]::NewGuid()),(get-date)
+


还有判断类型可以用-is

创建一个空数组

+
$array=@()
+


数组添加元素

+
$array+="a"
+


数组删除元素

+
$a=1..4
+$a=$a[0..1]+$a[3]
+

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

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

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

+
+

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

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

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

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

]]>
Redis @@ -9164,47 +9167,53 @@ int zslRandomLevel(void) {
- redis数据结构介绍四-第四部分 压缩表 - /2020/01/19/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%9B%9B/ - 在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

-
-

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

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

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

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

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

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

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

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

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

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

+
/* The actual Redis Object */
+#define OBJ_STRING 0    /* String object. */
+#define OBJ_LIST 1      /* List object. */
+#define OBJ_SET 2       /* Set object. */
+#define OBJ_ZSET 3      /* Sorted set object. */
+#define OBJ_HASH 4      /* Hash object. */
+/*
+ * Objects encoding. Some kind of objects like Strings and Hashes can be
+ * internally represented in multiple ways. The 'encoding' field of the object
+ * is set to one of this fields for this object. */
+#define OBJ_ENCODING_RAW 0     /* Raw representation */
+#define OBJ_ENCODING_INT 1     /* Encoded as integer */
+#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
+#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
+#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
+#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
+#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
+#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
+#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
+#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
+#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
+
+#define LRU_BITS 24
+#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
+#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
+
+#define OBJ_SHARED_REFCOUNT INT_MAX
+typedef struct redisObject {
+    unsigned type:4;
+    unsigned encoding:4;
+    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
+                            * LFU data (least significant 8 bits frequency
+                            * and most significant 16 bits access time). */
+    int refcount;
+    void *ptr;
+} robj;
+

主体结构就是这个 redisObject,

+
    +
  • type: 字段表示对象的类型,它对应的就是 redis 的对外暴露的,或者说用户可以使用的五种类型,OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
  • +
  • encoding: 字段表示这个对象在 redis 内部的编码方式,由OBJ_ENCODING_开头的 11 种
  • +
  • lru: 做LRU替换算法用,占24个bit
  • +
  • refcount: 引用计数。它允许robj对象在某些情况下被共享。
  • +
  • ptr: 指向底层实现数据结构的指针
    当 type 是 OBJ_STRING 时,表示类型是个 string,它的编码方式 encoding 可能有 OBJ_ENCODING_RAW,OBJ_ENCODING_INT,OBJ_ENCODING_EMBSTR 三种
    当 type 是 OBJ_LIST 时,表示类型是 list,它的编码方式 encoding 是 OBJ_ENCODING_QUICKLIST,对于早一些的版本,2.2这种可能还会使用 OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_LINKEDLIST
    当 type 是 OBJ_SET 时,是个集合,但是得看具体元素的类型,有可能使用整数集合,OBJ_ENCODING_INTSET, 如果元素不全是整型或者数量超过一定限制,那么编码就是 OBJ_ENCODING_HT hash table 了
    当 type 是 OBJ_ZSET 时,是个有序集合,它底层有可能使用的是 OBJ_ENCODING_ZIPLIST 或者 OBJ_ENCODING_SKIPLIST
    当 type 是 OBJ_HASH 时,一开始也是 OBJ_ENCODING_ZIPLIST,然后当数据量大于 hash_max_ziplist_entries 时会转成 OBJ_ENCODING_HT
  • +
]]>
Redis @@ -9436,6 +9445,118 @@ REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist, 源码
+ + redis数据结构介绍四-第四部分 压缩表 + /2020/01/19/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%9B%9B/ + 在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

+
+

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

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

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

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

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

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

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

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

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

+]]>
+ + Redis + 数据结构 + 源码 + C + Redis + + + redis + 数据结构 + 源码 + +
+ + redis淘汰策略复习 + /2021/08/01/redis%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ + 前面复习了 redis 的过期策略,这里再复习下淘汰策略,淘汰跟过期的区别有时候会被混淆了,过期主要针对那些设置了过期时间的 key,应该说是一种逻辑策略,是主动的还是被动的加定时的,两种有各自的取舍,而淘汰也可以看成是一种保持系统稳定的策略,因为如果内存满了,不采取任何策略处理,那大概率会导致系统故障,之前其实主要从源码角度分析过redis 的 LRU 和 LFU,但这个是偏底层的实现,抠得比较细,那么具体的系统层面的配置是有哪些策略,来看下 redis labs 的介绍

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PolicyDescription
noeviction 不逐出Returns an error if the memory limit has been reached when trying to insert more data,插入更多数据时,如果内存达到上限了,返回错误
allkeys-lru 所有的 key 使用 lru 逐出Evicts the least recently used keys out of all keys 在所有 key 中逐出最近最少使用的
allkeys-lfu 所有的 key 使用 lfu 逐出Evicts the least frequently used keys out of all keys 在所有 key 中逐出最近最不频繁使用的
allkeys-random 所有的 key 中随机逐出Randomly evicts keys out of all keys 在所有 key 中随机逐出
volatile-lruEvicts the least recently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lru 策略逐出
volatile-lfuEvicts the least frequently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lfu 策略逐出
volatile-randomRandomly evicts keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中随机逐出
volatile-ttlEvicts the shortest time-to-live keys out of all keys with an “expire” field set.在设置了过期时间的 key 空间 expire 中逐出更早过期的
+

而在这其中默认使用的策略是 volatile-lru,对 lru 跟 lfu 想有更多的了解可以看下我之前的文章redis系列介绍八-淘汰策略

+]]>
+ + redis + + + redis + 淘汰策略 + 应用 + Evict + +
redis系列介绍八-淘汰策略 /2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/ @@ -9953,16 +10074,54 @@ uint8_t LFULogIncr(uint8_t counter) {

总结

总结一下,redis 实现了近似的 lru 淘汰策略,通过增加了淘汰 key 的池子(pool),并且增大每次抽样的 key 的数量来将淘汰效果更进一步地接近于 lru,这是 lru 策略,但是对于前面举的一个例子,其实 lru 并不能保证 key 的淘汰就如我们预期,所以在后期又引入了 lfu 的策略,lfu的策略比较巧妙,复用了 redis 对象的 lru 字段,并且使用了factor 参数来控制计数器递增的速度,防止 8 位的计数器太早溢出。

]]> - Redis - 数据结构 - 源码 - C - Redis + Redis + 数据结构 + 源码 + C + Redis + + + redis + 数据结构 + 源码 + +
+ + redis过期策略复习 + /2021/07/25/redis%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ + redis过期策略复习

之前其实写过redis的过期的一些原理,这次主要是记录下,一些使用上的概念,主要是redis使用的过期策略是懒过期和定时清除,懒过期的其实比较简单,即是在key被访问的时候会顺带着判断下这个key是否已过期了,如果已经过期了,就不返回了,但是这种策略有个漏洞是如果有些key之后一直不会被访问了,就等于沉在池底了,所以需要有一个定时的清理机制,去从设置了过期的key池子(expires)里随机地捞key,具体的策略我们看下官网的解释

+
    +
  1. Test 20 random keys from the set of keys with an associated expire.
  2. +
  3. Delete all the keys found expired.
  4. +
  5. If more than 25% of keys were expired, start again from step 1.
  6. +
+

从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件

+
# Redis calls an internal function to perform many background tasks, like
+# closing connections of clients in timeout, purging expired keys that are
+# never requested, and so forth.
+#
+# Not all tasks are performed with the same frequency, but Redis checks for
+# tasks to perform according to the specified "hz" value.
+#
+# By default "hz" is set to 10. Raising the value will use more CPU when
+# Redis is idle, but at the same time will make Redis more responsive when
+# there are many keys expiring at the same time, and timeouts may be
+# handled with more precision.
+#
+# The range is between 1 and 500, however a value over 100 is usually not
+# a good idea. Most users should use the default of 10 and raise this up to
+# 100 only in environments where very low latency is required.
+hz 10
+ +

可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略

+]]>
+ + redis redis - 数据结构 - 源码 + 应用 + 过期策略
@@ -10394,100 +10553,6 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;源码 - - redis淘汰策略复习 - /2021/08/01/redis%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ - 前面复习了 redis 的过期策略,这里再复习下淘汰策略,淘汰跟过期的区别有时候会被混淆了,过期主要针对那些设置了过期时间的 key,应该说是一种逻辑策略,是主动的还是被动的加定时的,两种有各自的取舍,而淘汰也可以看成是一种保持系统稳定的策略,因为如果内存满了,不采取任何策略处理,那大概率会导致系统故障,之前其实主要从源码角度分析过redis 的 LRU 和 LFU,但这个是偏底层的实现,抠得比较细,那么具体的系统层面的配置是有哪些策略,来看下 redis labs 的介绍

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PolicyDescription
noeviction 不逐出Returns an error if the memory limit has been reached when trying to insert more data,插入更多数据时,如果内存达到上限了,返回错误
allkeys-lru 所有的 key 使用 lru 逐出Evicts the least recently used keys out of all keys 在所有 key 中逐出最近最少使用的
allkeys-lfu 所有的 key 使用 lfu 逐出Evicts the least frequently used keys out of all keys 在所有 key 中逐出最近最不频繁使用的
allkeys-random 所有的 key 中随机逐出Randomly evicts keys out of all keys 在所有 key 中随机逐出
volatile-lruEvicts the least recently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lru 策略逐出
volatile-lfuEvicts the least frequently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lfu 策略逐出
volatile-randomRandomly evicts keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中随机逐出
volatile-ttlEvicts the shortest time-to-live keys out of all keys with an “expire” field set.在设置了过期时间的 key 空间 expire 中逐出更早过期的
-

而在这其中默认使用的策略是 volatile-lru,对 lru 跟 lfu 想有更多的了解可以看下我之前的文章redis系列介绍八-淘汰策略

-]]>
- - redis - - - redis - 淘汰策略 - 应用 - Evict - -
- - redis过期策略复习 - /2021/07/25/redis%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ - redis过期策略复习

之前其实写过redis的过期的一些原理,这次主要是记录下,一些使用上的概念,主要是redis使用的过期策略是懒过期和定时清除,懒过期的其实比较简单,即是在key被访问的时候会顺带着判断下这个key是否已过期了,如果已经过期了,就不返回了,但是这种策略有个漏洞是如果有些key之后一直不会被访问了,就等于沉在池底了,所以需要有一个定时的清理机制,去从设置了过期的key池子(expires)里随机地捞key,具体的策略我们看下官网的解释

-
    -
  1. Test 20 random keys from the set of keys with an associated expire.
  2. -
  3. Delete all the keys found expired.
  4. -
  5. If more than 25% of keys were expired, start again from step 1.
  6. -
-

从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件

-
# Redis calls an internal function to perform many background tasks, like
-# closing connections of clients in timeout, purging expired keys that are
-# never requested, and so forth.
-#
-# Not all tasks are performed with the same frequency, but Redis checks for
-# tasks to perform according to the specified "hz" value.
-#
-# By default "hz" is set to 10. Raising the value will use more CPU when
-# Redis is idle, but at the same time will make Redis more responsive when
-# there are many keys expiring at the same time, and timeouts may be
-# handled with more precision.
-#
-# The range is between 1 and 500, however a value over 100 is usually not
-# a good idea. Most users should use the default of 10 and raise this up to
-# 100 only in environments where very low latency is required.
-hz 10
- -

可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略

-]]>
- - redis - - - redis - 应用 - 过期策略 - -
rust学习笔记-所有权三之切片 /2021/05/16/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%B8%89%E4%B9%8B%E5%88%87%E7%89%87/ @@ -10552,6 +10617,43 @@ hz 10 rust学习笔记-所有权二 /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ @@ -10640,64 +10742,127 @@ hz 10 - spark-little-tips - /2017/03/28/spark-little-tips/ - spark 的一些粗浅使用经验

工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现

-

简单的mapreduce word count示例

textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
-data = textFile.reduceByKey(lambda x, y: x + y).collect()
-for _ in data:
-    print(_)
+ 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();
+    }
+
+    public final long getTimestamp() {
+        return this.timestamp;
+    }
+}
+ +

然后就是事件生产者和监听消费者

+
@Component
+public class CustomSpringEventPublisher {
+
+    @Resource
+    private ApplicationEventPublisher applicationEventPublisher;
+
+    public void publishCustomEvent(final String message) {
+        System.out.println("Publishing custom event. ");
+        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
+        applicationEventPublisher.publishEvent(customSpringEvent);
+    }
+}
+

这里的 ApplicationEventPublisher 是 Spring 的方法接口

+
@FunctionalInterface
+public interface ApplicationEventPublisher {
+    default void publishEvent(ApplicationEvent event) {
+        this.publishEvent((Object)event);
+    }
+
+    void publishEvent(Object var1);
+}
+

具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲

+

事件监听者:

+
@Component
+public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
+    @Override
+    public void onApplicationEvent(CustomSpringEvent event) {
+        System.out.println("Received spring custom event - " + event.getMessage());
+    }
+}
+

这里的也是 spring 的一个方法接口

+
@FunctionalInterface
+public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
+    void onApplicationEvent(E var1);
 
+    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
+        return (event) -> {
+            consumer.accept(event.getPayload());
+        };
+    }
+}
-

结果

(3, 2)
-(1, 2)
-(4, 2)
-(2, 2)
-(5, 2)
+

然后简单包个请求

+

+@RequestMapping(value = "/event", method = RequestMethod.GET)
+@ResponseBody
+public void event() {
+    customSpringEventPublisher.publishCustomEvent("hello sprint event");
+}
+ +


就能看到接收到消息了。

]]>
- data analysis + Java + Spring - spark - python + Java + Spring + Spring Event
@@ -10854,45 +11019,6 @@ user3: swoole - - powershell 初体验 - /2022/11/13/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C/ - powershell变量

变量命名类似于php

-
PS C:\Users\Nicks> $a=1
-PS C:\Users\Nicks> $b=2
-PS C:\Users\Nicks> $a*$b
-2
-

有一个比较好用的是变量交换
一般的语言做两个变量交换一般需要一个临时变量

-
$tmp=$a
-$a=$b
-$b=$tmp
-

而在powershell中可以这样

-
$a,$b=$b,$a
-PS C:\Users\Nicks> $a,$b=$b,$a
-PS C:\Users\Nicks> $a
-2
-PS C:\Users\Nicks> $b
-1
-

还可以通过这个

-
PS C:\Users\Nicks> ls variable:
-
-Name                           Value
-----                           -----
-$                              $b
-?                              True
-^                              $b
-a                              2
-args                           {}
-b                              1
-

查看现存的变量
当然一般脚本都是动态类型的,
可以通过
gettype方法

-]]>
- - 语言 - - - powershell - -
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/ @@ -10931,20 +11057,6 @@ b 1
-

这个 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();
-    }
-
-    public final long getTimestamp() {
-        return this.timestamp;
-    }
-}
- -

然后就是事件生产者和监听消费者

-
@Component
-public class CustomSpringEventPublisher {
-
-    @Resource
-    private ApplicationEventPublisher applicationEventPublisher;
-
-    public void publishCustomEvent(final String message) {
-        System.out.println("Publishing custom event. ");
-        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
-        applicationEventPublisher.publishEvent(customSpringEvent);
-    }
-}
-

这里的 ApplicationEventPublisher 是 Spring 的方法接口

-
@FunctionalInterface
-public interface ApplicationEventPublisher {
-    default void publishEvent(ApplicationEvent event) {
-        this.publishEvent((Object)event);
-    }
-
-    void publishEvent(Object var1);
-}
-

具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲

-

事件监听者:

-
@Component
-public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
-    @Override
-    public void onApplicationEvent(CustomSpringEvent event) {
-        System.out.println("Received spring custom event - " + event.getMessage());
-    }
-}
-

这里的也是 spring 的一个方法接口

-
@FunctionalInterface
-public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
-    void onApplicationEvent(E var1);
-
-    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
-        return (event) -> {
-            consumer.accept(event.getPayload());
-        };
-    }
-}
- -

然后简单包个请求

-

-@RequestMapping(value = "/event", method = RequestMethod.GET)
-@ResponseBody
-public void event() {
-    customSpringEventPublisher.publishCustomEvent("hello sprint event");
-}
- -


就能看到接收到消息了。

-]]> - - Java - Spring - - - Java - Spring - Spring Event - - 介绍一下 RocketMQ /2020/06/21/%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B-RocketMQ/ @@ -11163,23 +11175,17 @@ b 1
-

这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明

然后就可以用 -n 参数,表示不自动提交

-
git revert -n OLDER_COMMIT^..NEWER_COMMIT
-git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"
- - -]]> - - git - 小技巧 - - - git - - - - powershell 初体验二 - /2022/11/20/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BA%8C/ - powershell创建数组也很方便
可以这样

-
$nums=2,0,1,2
-

顺便可以用下我们上次学到的gettype()

-

如果是想创建连续数字的数组还可以用这个方便的方法

-
$nums=1..5
-


而且数组还可以存放各种类型的数据

-
$array=1,"哈哈",([System.Guid]::NewGuid()),(get-date)
-


还有判断类型可以用-is

创建一个空数组

-
$array=@()
-


数组添加元素

-
$array+="a"
-


数组删除元素

-
$a=1..4
-$a=$a[0..1]+$a[3]
-

-]]>
- - 语言 - - - powershell - -
周末我在老丈人家打了天小工 /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/ @@ -11430,43 +11428,6 @@ b 1 - - 分享一次比较诡异的 Windows 下 U盘无法退出的经历 - /2023/01/29/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%AF%94%E8%BE%83%E8%AF%A1%E5%BC%82%E7%9A%84-Windows-%E4%B8%8B-U%E7%9B%98%E6%97%A0%E6%B3%95%E9%80%80%E5%87%BA%E7%9A%84%E7%BB%8F%E5%8E%86/ - 作为一个 Windows 的老用户,并且也算是 Windows 系统的半个粉丝,但是秉承一贯的优缺点都应该说的原则,Windows 系统有一点缺点是真的挺难受,相信 Windows 用过比较久的都会经历过,就是 U盘无法退出的问题,在比较远古时代,这个问题似乎能采取的措施不多,关机再拔 U盘的方式是一种比较保险的方式,其他貌似有 360这种可以解除占用的,但是需要安装 360 软件,对于目前的使用环境来说有点得不偿失,也是比较流氓的一类软件了,目前在 Windows 环境我主要就安装了个火绒,或者就用 Windows 自带的 defender。

-

第一种

最近这次经历也是有火绒的一定责任,在我尝试推出 U盘的时候提示了我被另一个大流氓软件,XlibabaProtect.exe 占用了,这个流氓软件真的是充分展示了某里的技术实力,试过 N 多种办法都关不掉也删不掉,尝试了很多种办法也没办法删除,但是后面换了种思路,一般这种情况肯定是有进程在占用 U盘里的内容,最新版本的 Powertoys 会在文件的右键菜单里添加一个叫 File Locksmith 的功能,可以用于检查正在使用哪些文件以及由哪些进程使用,但是可能是我的使用姿势不对,没有仔细看文档,它里面有个”以管理员身份重启”,可能会有用。
这算是第一种方式,

-

第二种

第二种方式是 Windows 任务管理器中性能 tab 下的”打开资源监视器”,
,假如我的 U 盘的盘符是F:
就可以搜索到占用这个盘符下文件的进程,这里千万小心‼️‼️,不可轻易杀掉这些进程,有些系统进程如果轻易杀掉会导致蓝屏等问题,不可轻易尝试,除非能确认这些进程的作用。
对于前两种方式对我来说都无效,

-

第三种

所以尝试了第三种,
就是磁盘脱机的方式,在”计算机”右键管理,点击”磁盘管理”,可以找到 U 盘盘符右键,点击”脱机”,然后再”推出”,这个对我来说也不行

-

第四种

这种是唯一对我有效的,在开始菜单搜索”event”,可以搜到”事件查看器”,
,这个可以看到当前最近 Windows 发生的事件,打开这个后就点击U盘推出,因为推不出来也是一种错误事件,点击下刷新就能在这看到具体是因为什么推出不了,具体的进程信息
最后发现是英特尔的驱动管理程序的一个进程,关掉就退出了,虽然前面说的某里的进程是流氓,但这边是真的冤枉它了

-]]>
- - Windows - 小技巧 - - - Windows - -
寄生虫观后感 /2020/03/01/%E5%AF%84%E7%94%9F%E8%99%AB%E8%A7%82%E5%90%8E%E6%84%9F/ @@ -11501,18 +11462,6 @@ b 1 + + 聊一下 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;
+        }
+
+        final NamesrvConfig namesrvConfig = new NamesrvConfig();
+        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
+        nettyServerConfig.setListenPort(9876);
+        if (commandLine.hasOption('c')) {
+            String file = commandLine.getOptionValue('c');
+            if (file != null) {
+                InputStream in = new BufferedInputStream(new FileInputStream(file));
+                properties = new Properties();
+                properties.load(in);
+                MixAll.properties2Object(properties, namesrvConfig);
+                MixAll.properties2Object(properties, nettyServerConfig);
+
+                namesrvConfig.setConfigStorePath(file);
 
-            @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;
-            }
-        });
+                System.out.printf("load config properties file OK, %s%n", file);
+                in.close();
+            }
+        }
 
-        /*
-         *  Launch the consumer instance.
-         * 启动消费者
-         */
-        consumer.start();
+        if (commandLine.hasOption('p')) {
+            InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
+            MixAll.printObjectProperties(console, namesrvConfig);
+            MixAll.printObjectProperties(console, nettyServerConfig);
+            System.exit(0);
+        }
 
-        System.out.printf("Consumer Started.%n");
-    }
+ MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig); -

然后就是看看 start 的过程了

-
/**
-     * This method gets internal infrastructure readily to serve. Instances must call this method after configuration.
-     *
-     * @throws MQClientException if there is any client error.
-     */
-    @Override
-    public void start() throws MQClientException {
-        setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
-        this.defaultMQPushConsumerImpl.start();
-        if (null != traceDispatcher) {
-            try {
-                traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
-            } catch (MQClientException e) {
-                log.warn("trace dispatcher start failed ", e);
-            }
-        }
-    }
-

具体的逻辑在this.defaultMQPushConsumerImpl.start(),这个 defaultMQPushConsumerImpl 就是

-
/**
-     * Internal implementation. Most of the functions herein are delegated to it.
-     */
-    protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
+ if (null == 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); + } -
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;
+        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+        JoranConfigurator configurator = new JoranConfigurator();
+        configurator.setContext(lc);
+        lc.reset();
+        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
 
-                this.checkConfig();
+        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
 
-                this.copySubscription();
+        MixAll.printObjectProperties(log, namesrvConfig);
+        MixAll.printObjectProperties(log, nettyServerConfig);
 
-                if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
-                    this.defaultMQPushConsumer.changeInstanceNameToPID();
-                }
+        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
 
-                // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成
-                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
+        // remember all configs to prevent discard
+        controller.getConfiguration().registerConfig(properties);
 
-                // 这个 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);
+        return controller;
+    }
- // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑 - this.pullAPIWrapper = new PullAPIWrapper( - mQClientFactory, - this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode()); - this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList); +

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

+
public static NamesrvController start(final NamesrvController controller) throws Exception {
 
-                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 (null == controller) {
+            throw new IllegalArgumentException("NamesrvController is null");
+        }
 
-                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());
-                }
+        boolean initResult = controller.initialize();
+        if (!initResult) {
+            controller.shutdown();
+            System.exit(-3);
+        }
 
-                // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
-                this.consumeMessageService.start();
+        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                controller.shutdown();
+                return null;
+            }
+        }));
 
-                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);
-                }
+        controller.start();
 
-                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 {
+

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

+
public boolean initialize() {
 
-        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.kvConfigManager.load();
 
-        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);
-            }
-        }
+        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
 
-        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;
-        }
+        this.remotingExecutor =
+            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
 
-        pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
+        this.registerProcessor();
 
-        try {
-            this.makeSureStateOK();
-        } catch (MQClientException e) {
-            log.warn("pullMessage exception, consumer state not ok", e);
-            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
-            return;
-        }
+        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 
-        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;
-        }
+            @Override
+            public void run() {
+                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
+            }
+        }, 5, 10, TimeUnit.SECONDS);
 
-        // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制
-        long cachedMessageCount = processQueue.getMsgCount().get();
-        long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
+        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 
-        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;
-        }
+            @Override
+            public void run() {
+                NamesrvController.this.kvConfigManager.printAllPeriodically();
+            }
+        }, 1, 10, TimeUnit.MINUTES);
 
-        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 (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");
+            }
+        }
 
-        // 若不是顺序消费(即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);
-                    }
+        return true;
+    }
- pullRequest.setLockedFirst(true); - pullRequest.setNextOffset(offset); - } - } else { - this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); - log.info("pull message later because not locked in broker, {}", pullRequest); - return; - } - } +

这里的kvConfigManager主要是来加载NameServer的配置参数,存到org.apache.rocketmq.namesrv.kvconfig.KVConfigManager#configTable中,然后是以BrokerHousekeepingService对象为参数初始化NettyRemotingServer对象,BrokerHousekeepingService对象作为该Netty连接中Socket链接的监听器(ChannelEventListener);监听与Broker建立的渠道的状态(空闲、关闭、异常三个状态),并调用BrokerHousekeepingService的相应onChannel方法。其中渠道的空闲、关闭、异常状态均调用RouteInfoManager.onChannelDestory方法处理。这个BrokerHousekeepingService可以字面化地理解为broker的管家服务,这个类内部三个状态方法其实都是调用的org.apache.rocketmq.namesrv.NamesrvController#getRouteInfoManager方法,而这个RouteInfoManager里面的对象有这些

+
public class RouteInfoManager {
+    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
+    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+  // topic与queue的对应关系
+    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
+  // Broker名称与broker属性的map
+    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
+  // 集群与broker集合的对应关系
+    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
+  // 活跃的broker信息
+    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
+  // Broker地址与过滤器
+    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
- // 以PullRequest.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; - } +

然后接下去就是初始化了一个线程池,然后注册默认的处理类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() {
 
-        final long beginTimestamp = System.currentTimeMillis();
+            @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());
+            }
+        }
+    }
 
-        // 异步拉取回调,先不讨论细节
-        PullCallback pullCallback = new PullCallback() {
-            @Override
-            public void onSuccess(PullResult pullResult) {
-                if (pullResult != null) {
-                    pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
-                        subscriptionData);
+    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);
+            }
+        }
 
-                    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 (null == brokerAddrFound) {
+            brokerAddrFound = remoteAddr;
+        } else {
+            log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
+        }
 
-                            long firstMsgOffset = Long.MAX_VALUE;
-                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
-                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                            } else {
-                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
+        if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
 
-                                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
-                                    pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
+            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();
 
-                                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
-                                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
-                                    pullResult.getMsgFoundList(),
-                                    processQueue,
-                                    pullRequest.getMessageQueue(),
-                                    dispatchToConsume);
+                        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 (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
-                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
-                                        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
-                                } else {
-                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                                }
-                            }
+                        if (brokerData.getBrokerAddrs().isEmpty()) {
+                            removeBrokerName = true;
+                            itBrokerAddrTable.remove();
+                            log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
+                                brokerData.getBrokerName());
+                        }
+                    }
 
-                            if (pullResult.getNextBeginOffset() < prevRequestOffset
-                                || firstMsgOffset < prevRequestOffset) {
-                                log.warn(
-                                    "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
-                                    pullResult.getNextBeginOffset(),
-                                    firstMsgOffset,
-                                    prevRequestOffset);
-                            }
+                    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);
+
+                                if (brokerNames.isEmpty()) {
+                                    log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
+                                        clusterName);
+                                    it.remove();
+                                }
 
-                            break;
-                        case NO_NEW_MSG:
-                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+                                break;
+                            }
+                        }
+                    }
 
-                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
+                    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();
 
-                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
-                            break;
-                        case NO_MATCHED_MSG:
-                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+                            Iterator<QueueData> itQueueData = queueDataList.iterator();
+                            while (itQueueData.hasNext()) {
+                                QueueData queueData = itQueueData.next();
+                                if (queueData.getBrokerName().equals(brokerNameFound)) {
+                                    itQueueData.remove();
+                                    log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
+                                        topic, queueData);
+                                }
+                            }
 
-                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
+                            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.executePullRequestImmediately(pullRequest); - break; - case OFFSET_ILLEGAL: - log.warn("the pull request offset illegal, {} {}", - pullRequest.toString(), pullResult.toString()); - pullRequest.setNextOffset(pullResult.getNextBeginOffset()); +

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

+
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 
-                            pullRequest.getProcessQueue().setDropped(true);
-                            DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
+            @Override
+            public void run() {
+                NamesrvController.this.kvConfigManager.printAllPeriodically();
+            }
+        }, 1, 10, TimeUnit.MINUTES);
- @Override - public void run() { - try { - DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(), - pullRequest.getNextOffset(), false); +

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

+

处理请求

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

+
@Override
+    public RemotingCommand processRequest(ChannelHandlerContext ctx,
+        RemotingCommand request) throws RemotingCommandException {
 
-                                        DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
+        if (ctx != null) {
+            log.debug("receive request, {} {} {}",
+                request.getCode(),
+                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
+                request);
+        }
 
-                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
 
-                                        log.warn("fix the pull request offset, {}", pullRequest);
-                                    } catch (Throwable e) {
-                                        log.error("executeTaskLater Exception", e);
-                                    }
-                                }
-                            }, 10000);
-                            break;
-                        default:
-                            break;
-                    }
-                }
-            }
+        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;
+    }
- @Override - public void onException(Throwable e) { - if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { - log.warn("execute the pull request exception", e); - } +

以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.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; - } - } +

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

+
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);
 
-        // 将上面获得的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();
-            }
+    if (!checksum(ctx, request, requestHeader)) {
+        response.setCode(ResponseCode.SYSTEM_ERROR);
+        response.setRemark("crc32 not match");
+        return response;
+    }
 
-            classFilter = sd.isClassFilterMode();
-        }
+    RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
 
-        // 组成 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 (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);
+    }
+
+    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
+        requestHeader.getClusterName(),
+        requestHeader.getBrokerAddr(),
+        requestHeader.getBrokerName(),
+        requestHeader.getBrokerId(),
+        requestHeader.getHaServerAddr(),
+        registerBrokerBody.getTopicConfigSerializeWrapper(),
+        registerBrokerBody.getFilterServerList(),
+        ctx.channel());
 
-    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;
+    responseHeader.setHaServerAddr(result.getHaServerAddr());
+    responseHeader.setMasterAddr(result.getMasterAddr());
 
-        if (findBrokerResult.isSlave()) {
-            sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
-        }
+    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
+    response.setBody(jsonValue);
 
-        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);
+    response.setCode(ResponseCode.SUCCESS);
+    response.setRemark(null);
+    return response;
+}
- String brokerAddr = findBrokerResult.getBrokerAddr(); - if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) { - brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr); - } +

可以看到主要的逻辑还是在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();
 
-        PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
-            brokerAddr,
-            requestHeader,
-            timeoutMillis,
-            communicationMode,
-            pullCallback);
+              // 更新这个clusterAddrTable
+                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
+                if (null == brokerNames) {
+                    brokerNames = new HashSet<String>();
+                    this.clusterAddrTable.put(clusterName, brokerNames);
+                }
+                brokerNames.add(brokerName);
 
-        return pullResult;
-    }
+                boolean registerFirst = false;
 
-    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);
+              // 更新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();
+                    }
+                }
 
-    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;
-    }
+                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
+                registerFirst = registerFirst || (null == oldAddr);
 
-    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();
+              // 更新了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());
+                            }
+                        }
+                    }
+                }
+
+              // 更新活跃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);
+                }
+
+              // 处理filter
+                if (filterServerList != null) {
+                    if (filterServerList.isEmpty()) {
+                        this.filterServerTable.remove(brokerAddr);
+                    } else {
+                        this.filterServerTable.put(brokerAddr, filterServerList);
+                    }
+                }
 
-        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);
-                    }
+              // 当当前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);
+        }
 
-                    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 result;
+    }
- return responseCommand; - } finally { - this.responseTable.remove(opaque); - } - } +

这个是注册 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);
 
-    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");
-            }
+        TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
 
-            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);
-            }
-        }
-    }
+ if (topicRouteData != null) { + if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) { + String orderTopicConf = + this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, + requestHeader.getTopic()); + topicRouteData.setOrderTopicConf(orderTopicConf); + } + byte[] content = topicRouteData.encode(); + response.setBody(content); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic() + + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); + return response; + }
+

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

+

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

]]> MQ @@ -13055,21 +13069,64 @@ b 1
- - - - -]]> - - MQ - RocketMQ - 消息队列 - - - MQ - 消息队列 - RocketMQ - - - - 聊一下 RocketMQ 的消息存储之 MMAP - /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ - 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

-
read(file, tmp_buf, len);
-write(socket, tmp_buf, len);
- - - -

vms95Z

-

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

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

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

-

mmap

mmap基于 OS 的 mmap 的内存映射技术,通过MMU 映射文件,将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作,使随机读写文件和读写内存相似的速度。

-
-

mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

-
-
tmp_buf = mmap(file, len);
-write(socket, tmp_buf, len);
- -

I68mFx

-
-

第一步:mmap系统调用使得文件内容被DMA引擎复制到内核缓冲区。然后该缓冲区与用户进程共享,在内核和用户内存空间之间不进行任何拷贝。

-

第二步:写系统调用使得内核将数据从原来的内核缓冲区复制到与套接字相关的内核缓冲区。

-

第三步:第三次拷贝发生在DMA引擎将数据从内核套接字缓冲区传递给协议引擎时。

-

通过使用mmap而不是read,我们将内核需要拷贝的数据量减少了一半。当大量的数据被传输时,这将有很好的效果。然而,这种改进并不是没有代价的;在使用mmap+write方法时,有一些隐藏的陷阱。例如当你对一个文件进行内存映射,然后在另一个进程截断同一文件时调用写。你的写系统调用将被总线错误信号SIGBUS打断,因为你执行了一个错误的内存访问。该信号的默认行为是杀死进程并dumpcore–这对网络服务器来说不是最理想的操作。

-

有两种方法可以解决这个问题。

-

第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用返回。通过这样做,写系统调用会返回它在被打断之前所写的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,一个治标不治本的解决方案。因为SIGBUS预示着进程出了严重的问题,所以不鼓励使用这种解决方案。

-

第二个解决方案涉及内核的文件租赁(在Windows中称为 “机会锁”)。这是解决这个问题的正确方法。通过在文件描述符上使用租赁,你与内核在一个特定的文件上达成租约。然后你可以向内核请求一个读/写租约。当另一个进程试图截断你正在传输的文件时,内核会向你发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉你内核即将终止你对该文件的写或读租约。在你的程序访问一个无效的地址和被SIGBUS信号杀死之前,你的写调用会被打断了。写入调用的返回值是中断前写入的字节数,errno将被设置为成功。下面是一些示例代码,显示了如何从内核中获得租约。

-
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
-    perror("kernel lease set signal");
-    return -1;
-}
-/* l_type can be F_RDLCK F_WRLCK */
-if(fcntl(fd, F_SETLEASE, l_type)){
-    perror("kernel lease set type");
-    return -1;
-}
+ } + } finally { + if (req != null && isSuccess) + // 通知前面等待的 + req.getCountDownLatch().countDown(); + } + return true; + }
+ + + + ]]> MQ @@ -14497,6 +14497,18 @@ app.setWebAp AutoConfiguration + + 聊一下关于怎么陪伴学习 + /2022/11/06/%E8%81%8A%E4%B8%80%E4%B8%8B%E5%85%B3%E4%BA%8E%E6%80%8E%E4%B9%88%E9%99%AA%E4%BC%B4%E5%AD%A6%E4%B9%A0/ + 这是一次开车过程中结合网上的一些微博想到的,开车是之前LD买了车后,陪领导练车,其实在一开始练车的时候,我们已经是找了相对很空的封闭路段,路上基本很少有车,偶尔有一辆车,但是LD还是很害怕,车速还只有十几的时候,还很远的对面来车的时候就觉得很慌了,这个时候如果以常理肯定会说这样子完全不用怕,如果克服恐惧真的这么容易的话,问题就不会那么纠结了,人生是很难完全感同身受的,唯有降低预设的基准让事情从头理清楚,害怕了我们就先休息,有车了我们就停下,先适应完全没车的情况,变得更慢一点,如果这时候着急一点,反而会起到反效果,比如只是说不要怕,接着开,甚至有点厌烦了,那基本这个练车也不太成得了了,而正好是有耐心的一起慢慢练习,还有就是第二件是切身体会,就是当道路本来是两条道,但是封了一条的时候,这时候开车如果是像我这样的新手,如果开车时左右边看着的话,车肯定开不好,因为那样会一直左右调整,反而更容易控制不好左右的距离,蹭到旁边的隔离栏,正确的方式应该是专注于正前方的路,这样才能保证左右边距离尽可能均匀,而不是顾左失右或者顾右失左,所以很多陪伴学习需要注意的是方式和耐心,能够识别到关键点那是最好的,但是有时候更需要的是耐心,纯靠耐心不一定能解决问题,但是可能会找到问题关键点。

+]]>
+ + 生活 + + + 生活 + +
聊在东京奥运会闭幕式这天-二 /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/ @@ -14517,6 +14529,26 @@ app.setWebAp 射击 + + 聊在东京奥运会闭幕式这天 + /2021/08/08/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9/ + 这届奥运会有可能是我除了 08 年之外关注度最高的一届奥运会,原因可能是因为最近也没什么电影综艺啥的比较好看,前面看跑男倒还行,不是说多好,也就图一乐,最开始看向往的生活觉得也挺不错的,后面变成了统一来了就看黄磊做饭,然后夸黄磊做饭好吃,然后无聊的说这种生活多么多么美好,单调无聊,差不多弃了,这里面还包括大华不在了,大华其实个人还是有点呱噪的,但是挺能搞气氛,并且也有才华,彭彭跟子枫人是不讨厌,但是撑不起来,所以也导致了前面说的结果,都变成了黄磊彩虹屁现场,虽然偶尔怀疑他是否做得好吃,但是整体还是承认的,可对于一个这么多季了的综艺来说,这样也有点单调了。

+

还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。

+

这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

+

乒乓球的说的有点多,就分篇说了,第一篇先到这。

+]]>
+ + 生活 + 运动 + + + 生活 + 运动 + 东京奥运会 + 乒乓球 + 跳水 + +
聊聊 Dubbo 的 SPI 续之自适应拓展 /2020/06/06/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI-%E7%BB%AD%E4%B9%8B%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ @@ -14665,26 +14697,6 @@ app.setWebAp 自适应拓展 - - 聊在东京奥运会闭幕式这天 - /2021/08/08/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9/ - 这届奥运会有可能是我除了 08 年之外关注度最高的一届奥运会,原因可能是因为最近也没什么电影综艺啥的比较好看,前面看跑男倒还行,不是说多好,也就图一乐,最开始看向往的生活觉得也挺不错的,后面变成了统一来了就看黄磊做饭,然后夸黄磊做饭好吃,然后无聊的说这种生活多么多么美好,单调无聊,差不多弃了,这里面还包括大华不在了,大华其实个人还是有点呱噪的,但是挺能搞气氛,并且也有才华,彭彭跟子枫人是不讨厌,但是撑不起来,所以也导致了前面说的结果,都变成了黄磊彩虹屁现场,虽然偶尔怀疑他是否做得好吃,但是整体还是承认的,可对于一个这么多季了的综艺来说,这样也有点单调了。

-

还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。

-

这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

-

乒乓球的说的有点多,就分篇说了,第一篇先到这。

-]]>
- - 生活 - 运动 - - - 生活 - 运动 - 东京奥运会 - 乒乓球 - 跳水 - -
聊聊 Dubbo 的 SPI /2020/05/31/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI/ @@ -14880,216 +14892,41 @@ public Result invoke(final Invocation invocation) throws RpcException { if (e.isBiz()) { // biz exception. throw e; } - le = e; - } catch (Throwable e) { - le = new RpcException(e.getMessage(), e); - } finally { - providers.add(invoker.getUrl().getAddress()); - } - } - throw new RpcException(le.getCode(), "Failed to invoke the method " - + methodName + " in the service " + getInterface().getName() - + ". Tried " + len + " times of the providers " + providers - + " (" + providers.size() + "/" + copyInvokers.size() - + ") from the registry " + directory.getUrl().getAddress() - + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " - + Version.getVersion() + ". Last error is: " - + le.getMessage(), le.getCause() != null ? le.getCause() : le); - }
- - -

Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

-

Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

-

Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

-

Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。

-

Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。

-]]> - - Java - Dubbo - RPC - Dubbo - 容错机制 - - - Java - Dubbo - RPC - 容错机制 - - - - 聊聊 Java 中绕不开的 Synchronized 关键字-二 - /2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ - Java并发

synchronized 的一些学习记录

-

jdk1.6 以后对 synchronized 进行了一些优化,包括偏向锁,轻量级锁,重量级锁等

-

这些锁的加锁方式大多跟对象头有关,我们可以查看 jdk 代码

-

首先对象头的位置注释

-
// Bit-format of an object header (most significant first, big endian layout below):
-//
-//  32 bits:
-//  --------
-//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
-//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
-//             size:32 ------------------------------------------>| (CMS free block)
-//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
-//
-//  64 bits:
-//  --------
-//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
-//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
-//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
-//  size:64 ----------------------------------------------------->| (CMS free block)
-//
-//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
-//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
-//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
-//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
- -
enum { locked_value             = 0,
-         unlocked_value           = 1,
-         monitor_value            = 2,
-         marked_value             = 3,
-         biased_lock_pattern      = 5
-};
-
- -

我们可以用 java jol库来查看对象头,通过一段简单的代码来看下

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-		}
-}
- -

Untitled

-

然后可以看到打印输出,当然这里因为对齐方式,我们看到的其实顺序是反过来的,按最后三位去看,我们这是 001,好像偏向锁都没开,这里使用的是 jdk1.8,默认开始偏向锁的,其实这里有涉及到了一个配置,jdk1.8 中偏向锁会延迟 4 秒开启,可以通过添加启动参数 -XX:+PrintFlagsFinal,看到

-

偏向锁延迟

-

因为在初始化的时候防止线程竞争有大量的偏向锁撤销升级,所以会延迟 4s 开启

-

我们再来延迟 5s 看看

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-				TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-		}
-} 
- -

https://img.nicksxs.com/uPic/2LBKpX.jpg

-

可以看到偏向锁设置已经开启了,我们来是一下加个偏向锁

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-        synchronized (l) {
-            System.out.println("2\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-		}
-}
- -

看下运行结果

-

https://img.nicksxs.com/uPic/V2l78m.png

-

可以看到是加上了 101 = 5 也就是偏向锁,后面是线程 id

-

当我再使用一个线程来竞争这个锁的时候

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-				Thread thread1 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread1 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                    try {
-                        TimeUnit.SECONDS.sleep(5L);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        };
-				thread1.start();
-		}
-}
- -

https://img.nicksxs.com/uPic/bRMvlR.png

-

可以看到变成了轻量级锁,在线程没有争抢,只是进行了切换,就会使用轻量级锁,当两个线程在竞争了,就又会升级成重量级锁

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-        Thread thread1 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread1 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                    try {
-                        TimeUnit.SECONDS.sleep(5L);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        };
-        Thread thread2 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread2 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                }
-            }
-        };
-        thread1.start();
-        thread2.start();
-    }
-}
+                le = e;
+            } catch (Throwable e) {
+                le = new RpcException(e.getMessage(), e);
+            } finally {
+                providers.add(invoker.getUrl().getAddress());
+            }
+        }
+        throw new RpcException(le.getCode(), "Failed to invoke the method "
+                + methodName + " in the service " + getInterface().getName()
+                + ". Tried " + len + " times of the providers " + providers
+                + " (" + providers.size() + "/" + copyInvokers.size()
+                + ") from the registry " + directory.getUrl().getAddress()
+                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
+                + Version.getVersion() + ". Last error is: "
+                + le.getMessage(), le.getCause() != null ? le.getCause() : le);
+    }
-class L { - private boolean myboolean = true; -}
-

https://img.nicksxs.com/uPic/LMzMtR.png

-

可以看到变成了重量级锁。

+

Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

+

Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

+

Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

+

Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。

+

Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。

]]> Java + Dubbo - RPC + Dubbo + 容错机制 Java - Synchronized - 偏向锁 - 轻量级锁 - 重量级锁 - 自旋 + Dubbo + RPC + 容错机制 @@ -15299,18 +15136,6 @@ public Result invoke(final Invocation invocation) throws RpcException { java - - 聊一下关于怎么陪伴学习 - /2022/11/06/%E8%81%8A%E4%B8%80%E4%B8%8B%E5%85%B3%E4%BA%8E%E6%80%8E%E4%B9%88%E9%99%AA%E4%BC%B4%E5%AD%A6%E4%B9%A0/ - 这是一次开车过程中结合网上的一些微博想到的,开车是之前LD买了车后,陪领导练车,其实在一开始练车的时候,我们已经是找了相对很空的封闭路段,路上基本很少有车,偶尔有一辆车,但是LD还是很害怕,车速还只有十几的时候,还很远的对面来车的时候就觉得很慌了,这个时候如果以常理肯定会说这样子完全不用怕,如果克服恐惧真的这么容易的话,问题就不会那么纠结了,人生是很难完全感同身受的,唯有降低预设的基准让事情从头理清楚,害怕了我们就先休息,有车了我们就停下,先适应完全没车的情况,变得更慢一点,如果这时候着急一点,反而会起到反效果,比如只是说不要怕,接着开,甚至有点厌烦了,那基本这个练车也不太成得了了,而正好是有耐心的一起慢慢练习,还有就是第二件是切身体会,就是当道路本来是两条道,但是封了一条的时候,这时候开车如果是像我这样的新手,如果开车时左右边看着的话,车肯定开不好,因为那样会一直左右调整,反而更容易控制不好左右的距离,蹭到旁边的隔离栏,正确的方式应该是专注于正前方的路,这样才能保证左右边距离尽可能均匀,而不是顾左失右或者顾右失左,所以很多陪伴学习需要注意的是方式和耐心,能够识别到关键点那是最好的,但是有时候更需要的是耐心,纯靠耐心不一定能解决问题,但是可能会找到问题关键点。

-]]>
- - 生活 - - - 生活 - -
聊聊 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/ @@ -15624,6 +15449,181 @@ public Result invoke(final Invocation invocation) throws RpcException { 双亲委派 + + 聊聊 Java 中绕不开的 Synchronized 关键字-二 + /2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ + Java并发

synchronized 的一些学习记录

+

jdk1.6 以后对 synchronized 进行了一些优化,包括偏向锁,轻量级锁,重量级锁等

+

这些锁的加锁方式大多跟对象头有关,我们可以查看 jdk 代码

+

首先对象头的位置注释

+
// Bit-format of an object header (most significant first, big endian layout below):
+//
+//  32 bits:
+//  --------
+//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
+//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
+//             size:32 ------------------------------------------>| (CMS free block)
+//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
+//
+//  64 bits:
+//  --------
+//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
+//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
+//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
+//  size:64 ----------------------------------------------------->| (CMS free block)
+//
+//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
+//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
+//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
+//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
+ +
enum { locked_value             = 0,
+         unlocked_value           = 1,
+         monitor_value            = 2,
+         marked_value             = 3,
+         biased_lock_pattern      = 5
+};
+
+ +

我们可以用 java jol库来查看对象头,通过一段简单的代码来看下

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+		}
+}
+ +

Untitled

+

然后可以看到打印输出,当然这里因为对齐方式,我们看到的其实顺序是反过来的,按最后三位去看,我们这是 001,好像偏向锁都没开,这里使用的是 jdk1.8,默认开始偏向锁的,其实这里有涉及到了一个配置,jdk1.8 中偏向锁会延迟 4 秒开启,可以通过添加启动参数 -XX:+PrintFlagsFinal,看到

+

偏向锁延迟

+

因为在初始化的时候防止线程竞争有大量的偏向锁撤销升级,所以会延迟 4s 开启

+

我们再来延迟 5s 看看

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+				TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+		}
+} 
+ +

https://img.nicksxs.com/uPic/2LBKpX.jpg

+

可以看到偏向锁设置已经开启了,我们来是一下加个偏向锁

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+        synchronized (l) {
+            System.out.println("2\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+		}
+}
+ +

看下运行结果

+

https://img.nicksxs.com/uPic/V2l78m.png

+

可以看到是加上了 101 = 5 也就是偏向锁,后面是线程 id

+

当我再使用一个线程来竞争这个锁的时候

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+				Thread thread1 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread1 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                    try {
+                        TimeUnit.SECONDS.sleep(5L);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+				thread1.start();
+		}
+}
+ +

https://img.nicksxs.com/uPic/bRMvlR.png

+

可以看到变成了轻量级锁,在线程没有争抢,只是进行了切换,就会使用轻量级锁,当两个线程在竞争了,就又会升级成重量级锁

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+        Thread thread1 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread1 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                    try {
+                        TimeUnit.SECONDS.sleep(5L);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+        Thread thread2 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread2 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                }
+            }
+        };
+        thread1.start();
+        thread2.start();
+    }
+}
+
+class L {
+    private boolean myboolean = true;
+}
+ +

https://img.nicksxs.com/uPic/LMzMtR.png

+

可以看到变成了重量级锁。

+]]>
+ + Java + + + Java + Synchronized + 偏向锁 + 轻量级锁 + 重量级锁 + 自旋 + +
聊聊 Java 自带的那些*逆天*工具 /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/ @@ -16714,38 +16714,9 @@ result = result 消息队列 RocketMQ 削峰填谷 - 中间件 - 源码解析 - Broker - - - - 聊聊 Sharding-Jdbc 的简单原理初篇 - /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ - 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
就是我在执行 query 的时候犯过一个比较难发现的错误,

-
ResultSet resultSet = ps.executeQuery(sql);
-

实际上应该是

-
ResultSet resultSet = ps.executeQuery();
-

而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里

-
@Override
-public final ResultSet executeQuery(final String sql) throws SQLException {
-    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
-}
-

这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

在前一篇的 demo 中的

-
Connection conn = dataSource.getConnection();
-

其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement

-
PreparedStatement ps = conn.prepareStatement(sql)
-

就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行

-
ResultSet resultSet = ps.executeQuery();
-

然后获得结果
org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

-

其实像 mybatis 也是基于这样去实现的

-]]>
- - Java - - - Java - Sharding-Jdbc + 中间件 + 源码解析 + Broker
@@ -16944,42 +16915,33 @@ t2 - 聊聊 Java 的类加载机制一 - /2020/11/08/%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/ - 一说到这个主题,想到的应该是双亲委派模型,不过讲的包括但不限于这个,主要内容是参考深入理解 Java 虚拟机书中的介绍,
一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。

-
    -
  • 加载

  • -
-
    -
  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. -
  3. 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  4. -
  5. 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  6. -
-
    -
  • 验证

  • -
-
    -
  1. 文件格式验证
  2. -
  3. 元数据验证
  4. -
  5. 字节码验证
  6. -
  7. 符号引用验证
  8. -
-
    -
  • 准备

    准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

    -
  • -
  • 解析

    解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程

    -
  • -
-

以上验证准备解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

-
    -
  • 初始化

    类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
  • -
-

最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

+ 聊聊 Sharding-Jdbc 的简单原理初篇 + /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
就是我在执行 query 的时候犯过一个比较难发现的错误,

+
ResultSet resultSet = ps.executeQuery(sql);
+

实际上应该是

+
ResultSet resultSet = ps.executeQuery();
+

而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里

+
@Override
+public final ResultSet executeQuery(final String sql) throws SQLException {
+    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
+}
+

这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

在前一篇的 demo 中的

+
Connection conn = dataSource.getConnection();
+

其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement

+
PreparedStatement ps = conn.prepareStatement(sql)
+

就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行

+
ResultSet resultSet = ps.executeQuery();
+

然后获得结果
org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

+

其实像 mybatis 也是基于这样去实现的

]]>
Java - 类加载 + + Java + Sharding-Jdbc +
聊聊 dubbo 的线程池 @@ -17043,70 +17005,83 @@ t2 - 聊聊 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/ - 前几天同事问了我个 mysql 索引的问题,虽然大概知道,但是还是想来实践下,就是 is null,is not null 这类查询是否能用索引,可能之前有些网上的文章说都是不能用索引,但是其实不是,我们来看个小试验

-
CREATE TABLE `null_index_t` (
-  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
-  `null_key` varchar(255) DEFAULT NULL,
-  `null_key1` varchar(255) DEFAULT NULL,
-  `null_key2` varchar(255) DEFAULT NULL,
-  PRIMARY KEY (`id`),
-  KEY `idx_1` (`null_key`) USING BTREE,
-  KEY `idx_2` (`null_key1`) USING BTREE,
-  KEY `idx_3` (`null_key2`) USING BTREE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-

用个存储过程来插入数据

-

-delimiter $	#以delimiter来标记用$表示存储过程结束
-create procedure nullIndex1()
-begin
-declare i int;	
-declare j int;	
-set i=1;
-set j=1;
-while(i<=100) do	
-	while(j<=100) do	
-		IF (i % 3 = 0) THEN
-	     INSERT INTO null_index_t ( `null_key`, `null_key1`, `null_key2` ) VALUES (null , LEFT(MD5(RAND()), 8), LEFT(MD5(RAND()), 8));
-    ELSEIF (i % 3 = 1) THEN
-			 INSERT INTO null_index_t ( `null_key`, `null_key1`, `null_key2` ) VALUES (LEFT(MD5(RAND()), 8), NULL, LEFT(MD5(RAND()), 8));
-	  ELSE
-			 INSERT INTO null_index_t ( `null_key`, `null_key1`, `null_key2` ) VALUES (LEFT(MD5(RAND()), 8), LEFT(MD5(RAND()), 8), NULL);
-    END IF;
-		set j=j+1;
-	end while;
-	set i=i+1;
-	set j=1;	
-end while;
-end 
-$
-call nullIndex1();
-

然后看下我们的 is null 查询

-
EXPLAIN select * from null_index_t WHERE null_key is null;
-


再来看看另一个

-
EXPLAIN select * from null_index_t WHERE null_key is not null;
-


从这里能看出来啥呢,可以思考下

-

从上面可以发现,is null应该是用上了索引了,所以至少不是一刀切不能用,但是看着is not null好像不太行额
我们在做一点小改动,把这个表里的数据改成 9100 条是 null,剩下 900 条是有值的,然后再执行下

然后再来看看执行结果

-
EXPLAIN select * from null_index_t WHERE null_key is null;
-

-
EXPLAIN select * from null_index_t WHERE null_key is not null;
-


是不是不一样了,这里再补充下我试验使用的 mysql 是 5.7 的,不保证在其他版本的一致性,
其实可以看出随着数据量的变化,mysql 会不会使用索引是会变化的,不是说 is not null 一定会使用,也不是一定不会使用,而是优化器会根据查询成本做个预判,这个预判尽可能会减小查询成本,主要包括回表啥的,但是也不一定完全准确。

+ 聊聊 mysql 的 MVCC 续篇 + /2020/05/02/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%AF%87/ + 上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

+
/**
+Opens a read view where exactly the transactions serialized before this
+point in time are seen in the view.
+@param id		Creator transaction id */
+
+void ReadView::prepare(trx_id_t id) {
+  ut_ad(mutex_own(&trx_sys->mutex));
+
+  m_creator_trx_id = id;
+
+  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;
+

m_low_limit_id赋的值是trx_sys->max_trx_id,代表的是当前系统最小的未分配的事务 id,所以呢,举个例子,当前有三个活跃事务,事务 id 分别是 100,200,300,而 m_up_limit_id = 100, m_low_limit_id = 301,当事务 id 是 200 的提交之后,它的更新就是可以被 100 和 300 看到,而不是说 m_ids 里没了 200,并且 200 比 100 大就应该不可见了

+

幻读

还有一个问题是幻读的问题,这貌似也是个高频面试题,啥意思呢,或者说跟它最常拿来比较的脏读,脏读是指读到了别的事务未提交的数据,因为未提交,严格意义上来讲,不一定是会被最后落到库里,可能会回滚,也就是在 read uncommitted 级别下会出现的问题,但是幻读不太一样,幻读是指两次查询的结果数量不一样,比如我查了第一次是 select * from table1 where id < 10 for update,查出来了一条结果 id 是 5,然后再查一下发现出来了一条 id 是 5,一条 id 是 7,那是不是有点尴尬了,其实呢这个点我觉得脏读跟幻读也比较是从原理层面来命名,如果第一次接触的同学发觉有点不理解也比较正常,因为从逻辑上讲总之都是数据不符合预期,但是基于源码层面其实是不同的情况,幻读是在原先的 read view 无法完全解决的,怎么解决呢,简单的来说就是锁咯,我们知道innodb 是基于 record lock 行锁的,但是貌似没有办法解决这种问题,那么 innodb 就引入了 gap lock 间隙锁,比如上面说的情况下,id 小于 10 的情况下,是都应该锁住的,gap lock 其实是基于索引结构来锁的,因为索引树除了树形结构之外,还有一个next record 的指针,gap lock 也是基于这个来锁的
看一下 mysql 的文档

+
+

SELECT … FOR UPDATE sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

+
+

对于一个 for update 查询,在 RR 级别下,会设置一个 next-key lock在每一条被查询到的记录上,next-lock 又是啥呢,其实就是 gap 锁和 record 锁的结合体,比如我在数据库里有 id 是 1,3,5,7,10,对于上面那条查询,查出来的结果就是 1,3,5,7,那么按照文档里描述的,对于这几条记录都会加上next-key lock,也就是(-∞, 1], (1, 3], (3, 5], (5, 7], (7, 10) 这些区间和记录会被锁起来,不让插入,再唠叨一下呢,就是其实如果是只读的事务,光 read view 一致性读就够了,如果是有写操作的呢,就需要锁了。

]]>
Mysql C - 索引 + 数据结构 + 源码 Mysql mysql - 索引 - is null - is not null - procedure + 数据结构 + 源码 + mvcc + read view + gap lock + next-key lock + 幻读
+ + 聊聊 Java 的类加载机制一 + /2020/11/08/%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/ + 一说到这个主题,想到的应该是双亲委派模型,不过讲的包括但不限于这个,主要内容是参考深入理解 Java 虚拟机书中的介绍,
一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。

+
    +
  • 加载

  • +
+
    +
  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. +
  3. 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  4. +
  5. 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  6. +
+
    +
  • 验证

  • +
+
    +
  1. 文件格式验证
  2. +
  3. 元数据验证
  4. +
  5. 字节码验证
  6. +
  7. 符号引用验证
  8. +
+
    +
  • 准备

    准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

    +
  • +
  • 解析

    解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程

    +
  • +
+

以上验证准备解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

+
    +
  • 初始化

    类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
  • +
+

最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

+]]>
+ + Java + 类加载 + +
聊聊 mysql 的 MVCC 续续篇之锁分析 /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/ @@ -17128,8 +17103,8 @@ $ Mysql C 数据结构 - Mysql 源码 + Mysql mysql @@ -17211,8 +17186,8 @@ constexpr size_t DATA_ROLL_PTR_LEN Mysql C 数据结构 - Mysql 源码 + Mysql mysql @@ -17222,6 +17197,71 @@ constexpr size_t DATA_ROLL_PTR_LEN 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/ + 前几天同事问了我个 mysql 索引的问题,虽然大概知道,但是还是想来实践下,就是 is null,is not null 这类查询是否能用索引,可能之前有些网上的文章说都是不能用索引,但是其实不是,我们来看个小试验

+
CREATE TABLE `null_index_t` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `null_key` varchar(255) DEFAULT NULL,
+  `null_key1` varchar(255) DEFAULT NULL,
+  `null_key2` varchar(255) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_1` (`null_key`) USING BTREE,
+  KEY `idx_2` (`null_key1`) USING BTREE,
+  KEY `idx_3` (`null_key2`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+

用个存储过程来插入数据

+

+delimiter $	#以delimiter来标记用$表示存储过程结束
+create procedure nullIndex1()
+begin
+declare i int;	
+declare j int;	
+set i=1;
+set j=1;
+while(i<=100) do	
+	while(j<=100) do	
+		IF (i % 3 = 0) THEN
+	     INSERT INTO null_index_t ( `null_key`, `null_key1`, `null_key2` ) VALUES (null , LEFT(MD5(RAND()), 8), LEFT(MD5(RAND()), 8));
+    ELSEIF (i % 3 = 1) THEN
+			 INSERT INTO null_index_t ( `null_key`, `null_key1`, `null_key2` ) VALUES (LEFT(MD5(RAND()), 8), NULL, LEFT(MD5(RAND()), 8));
+	  ELSE
+			 INSERT INTO null_index_t ( `null_key`, `null_key1`, `null_key2` ) VALUES (LEFT(MD5(RAND()), 8), LEFT(MD5(RAND()), 8), NULL);
+    END IF;
+		set j=j+1;
+	end while;
+	set i=i+1;
+	set j=1;	
+end while;
+end 
+$
+call nullIndex1();
+

然后看下我们的 is null 查询

+
EXPLAIN select * from null_index_t WHERE null_key is null;
+


再来看看另一个

+
EXPLAIN select * from null_index_t WHERE null_key is not null;
+


从这里能看出来啥呢,可以思考下

+

从上面可以发现,is null应该是用上了索引了,所以至少不是一刀切不能用,但是看着is not null好像不太行额
我们在做一点小改动,把这个表里的数据改成 9100 条是 null,剩下 900 条是有值的,然后再执行下

然后再来看看执行结果

+
EXPLAIN select * from null_index_t WHERE null_key is null;
+

+
EXPLAIN select * from null_index_t WHERE null_key is not null;
+


是不是不一样了,这里再补充下我试验使用的 mysql 是 5.7 的,不保证在其他版本的一致性,
其实可以看出随着数据量的变化,mysql 会不会使用索引是会变化的,不是说 is not null 一定会使用,也不是一定不会使用,而是优化器会根据查询成本做个预判,这个预判尽可能会减小查询成本,主要包括回表啥的,但是也不一定完全准确。

+]]>
+ + Mysql + C + 索引 + Mysql + + + mysql + 索引 + is null + is not null + procedure + +
聊聊 redis 缓存的应用问题 /2021/01/31/%E8%81%8A%E8%81%8A-redis-%E7%BC%93%E5%AD%98%E7%9A%84%E5%BA%94%E7%94%A8%E9%97%AE%E9%A2%98/ @@ -17254,43 +17294,78 @@ constexpr size_t DATA_ROLL_PTR_LEN - 聊聊 mysql 的 MVCC 续篇 - /2020/05/02/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%AF%87/ - 上一篇聊了mysql 的 innodb 引擎基于 read view 实现的 mvcc 和事务隔离级别,可能有些细心的小伙伴会发现一些问题,第一个是在 RC 级别下的事务提交后的可见性,这里涉及到了三个参数,m_low_limit_id,m_up_limit_id,m_ids,之前看到知乎的一篇写的非常不错的文章,但是就在这一点上似乎有点疑惑,这里基于源码和注释来解释下这个问题

-
/**
-Opens a read view where exactly the transactions serialized before this
-point in time are seen in the view.
-@param id		Creator transaction id */
+    聊聊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();
 
-void ReadView::prepare(trx_id_t id) {
-  ut_ad(mutex_own(&trx_sys->mutex));
+    // 使用这个对象都是通过这个 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;
 
-  m_creator_trx_id = id;
+    private int m = 9;
 
-  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;
-

m_low_limit_id赋的值是trx_sys->max_trx_id,代表的是当前系统最小的未分配的事务 id,所以呢,举个例子,当前有三个活跃事务,事务 id 分别是 100,200,300,而 m_up_limit_id = 100, m_low_limit_id = 301,当事务 id 是 200 的提交之后,它的更新就是可以被 100 和 300 看到,而不是说 m_ids 里没了 200,并且 200 比 100 大就应该不可见了

-

幻读

还有一个问题是幻读的问题,这貌似也是个高频面试题,啥意思呢,或者说跟它最常拿来比较的脏读,脏读是指读到了别的事务未提交的数据,因为未提交,严格意义上来讲,不一定是会被最后落到库里,可能会回滚,也就是在 read uncommitted 级别下会出现的问题,但是幻读不太一样,幻读是指两次查询的结果数量不一样,比如我查了第一次是 select * from table1 where id < 10 for update,查出来了一条结果 id 是 5,然后再查一下发现出来了一条 id 是 5,一条 id 是 7,那是不是有点尴尬了,其实呢这个点我觉得脏读跟幻读也比较是从原理层面来命名,如果第一次接触的同学发觉有点不理解也比较正常,因为从逻辑上讲总之都是数据不符合预期,但是基于源码层面其实是不同的情况,幻读是在原先的 read view 无法完全解决的,怎么解决呢,简单的来说就是锁咯,我们知道innodb 是基于 record lock 行锁的,但是貌似没有办法解决这种问题,那么 innodb 就引入了 gap lock 间隙锁,比如上面说的情况下,id 小于 10 的情况下,是都应该锁住的,gap lock 其实是基于索引结构来锁的,因为索引树除了树形结构之外,还有一个next record 的指针,gap lock 也是基于这个来锁的
看一下 mysql 的文档

-
-

SELECT … FOR UPDATE sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

-
-

对于一个 for update 查询,在 RR 级别下,会设置一个 next-key lock在每一条被查询到的记录上,next-lock 又是啥呢,其实就是 gap 锁和 record 锁的结合体,比如我在数据库里有 id 是 1,3,5,7,10,对于上面那条查询,查出来的结果就是 1,3,5,7,那么按照文档里描述的,对于这几条记录都会加上next-key lock,也就是(-∞, 1], (1, 3], (3, 5], (5, 7], (7, 10) 这些区间和记录会被锁起来,不让插入,再唠叨一下呢,就是其实如果是只读的事务,光 read view 一致性读就够了,如果是有写操作的呢,就需要锁了。

+ 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 保证了它们不会再被实例化,所以它天生就是单例的。

]]>
- Mysql - C - 数据结构 - Mysql - 源码 + Java + Design Patterns + Singleton - mysql - 数据结构 - 源码 - mvcc - read view - gap lock - next-key lock - 幻读 + 设计模式 + Design Patterns + 单例 + Singleton
@@ -17643,81 +17718,6 @@ void ReadView::prepare(trx_id_t id) { AutoConfiguration - - 聊聊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;
-
-    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 保证了它们不会再被实例化,所以它天生就是单例的。

-]]>
- - Java - Design Patterns - Singleton - - - 设计模式 - Design Patterns - 单例 - Singleton - -
聊聊一次 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/ @@ -17735,111 +17735,40 @@ void ReadView::prepare(trx_id_t id) {
git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7

其实注意 commit id 旁边的红框,这个是有tag 的,可以直接

git checkout icu4c-64
-

PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

-
brew reinstall ./icu4c.rb
-

然后就是第五步,切换版本

-
brew switch icu4c 64.2
-

最后把分支切回来

-
git checkout master
-

是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

-
# zsh
-function hiicu64() {
-  local last_dir=$(pwd)
-
-  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
-  git checkout icu4c-4
-  brew reinstall ./icu4c.rb
-  brew switch icu4c 64.2
-  git checkout master
-
-  cd $last_dir
-}
-

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

-]]> - - Mac - PHP - Homebrew - PHP - icu4c - - - Mac - PHP - Homebrew - icu4c - zsh - -
- - 聊聊厦门旅游的好与不好 - /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ - 这几天去了趟厦门,原来几年前就想去了,本来都请好假了,后面因为一些事情没去成,这次刚好公司组织,就跟 LD 一起去了厦门,也不洋洋洒洒地写游记了,后面可能会有,今天先来总结下好的地方和比较坑的地方。
这次主要去了中山路、鼓浪屿、曾厝(cuo)垵、植物园、灵玲马戏团,因为住的离环岛路比较近,还有幸现场看了下厦门马拉松,其中

-

中山路

这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。

-

鼓浪屿

鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆

-

曾厝垵

一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆

-

植物园

植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆

-

灵玲马戏团

对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。

-

总结

厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦‍♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。

-]]>
- - 生活 - 旅游 - - - 生活 - 杭州 - 旅游 - 厦门 - 中山路 - 局口街 - 鼓浪屿 - 曾厝垵 - 植物园 - 马戏团 - 沙茶面 - 海蛎煎 - -
- - 聊聊我刚学会的应用诊断方法 - /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/ - 因为传说中的出身问题,我以前写的是PHP,在使用 swoole 之前,基本的应用调试手段就是简单粗暴的 var_dump,exit,对于单进程模型的 PHP 也是简单有效,技术栈换成 Java 之后,就变得没那么容易,一方面是需要编译,另一方面是一般都是基于 spring 的项目,如果问题定位比较模糊,那框架层的是很难靠简单的 System.out.println 或者打 log 解决,(PS:我觉得可能我写的东西比较适合从 PHP 这种弱类型语言转到 Java 的小白同学)这个时候一方面因为是 Java,有了非常好用的 idea IDE 的支持,可以各种花式调试,条件断点尤其牛叉,但是又因为有 Spring+Java 的双重原因,有些情况下单步调试可以把手按废掉,这也是我之前一直比较困惑苦逼的点,后来随着慢慢精(jiang)进(you)之后,比如对于一个 oom 的情况,我们可以通过启动参数加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xx/xx 来配置溢出时的堆dump 日志,获取到这个文件后,我们可以通过像 Memory Analyzer (MAT)[https://www.eclipse.org/mat/] (The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.)来查看诊断问题所在,之前用到的时候是因为有个死循环一直往链表里塞数据,属于比较简单的,后来一次是由于运维进行应用迁移时按默认的统一配置了堆内存大小,导致内存的确不够用,所以溢出了,
今天想说的其实主要是我们的 thread dump,这也是我最近才真正用的一个方法,可能真的很小白了,用过 ide 的单步调试其实都知道会有一个一层层的玩意,比如函数从 A,调用了 B,再从 B 调用了 C,一直往下(因为是 Java,所以还有很多🤦‍♂️),这个其实也是大部分语言的调用模型,利用了栈这个数据结构,通过这个结构我们可以知道代码的调用链路,由于对于一个 spring 应用,在本身框架代码量非常庞大的情况下,外加如果应用代码也是非常多的时候,有时候通过单步调试真的很难短时间定位到问题,需要非常大的耐心和仔细观察,当然不是说完全不行,举个例子当我的应用经常启动需要非常长的时间,因为本身应用有非常多个 bean,比较难说究竟是 bean 的加载的确很慢还是有什么异常原因,这种时候就可以使用 thread dump 了,具体怎么操作呢

如果在idea 中运行或者调试时,可以直接点击这个照相机一样的按钮,右边就会出现了左边会显示所有的线程,右边会显示线程栈,

-
"main@1" prio=5 tid=0x1 nid=NA runnable
-  java.lang.Thread.State: RUNNABLE
-	  at TreeDistance.treeDist(TreeDistance.java:64)
-	  at TreeDistance.treeDist(TreeDistance.java:65)
-	  at TreeDistance.treeDist(TreeDistance.java:65)
-	  at TreeDistance.treeDist(TreeDistance.java:65)
-	  at TreeDistance.main(TreeDistance.java:45)
-

这就是我们主线程的堆栈信息了,main 表示这个线程名,prio表示优先级,默认是 5,tid 表示线程 id,nid 表示对应的系统线程,后面的runnable 表示目前线程状态,因为是被我打了断点,所以是就许状态,然后下面就是对应的线程栈内容了,在TreeDistance类的 treeDist方法中,对应的文件行数是 64 行。
这里使用 thread dump一般也不会是上面我截图代码里的这种代码量很少的,一般是大型项目,有时候跑着跑着没反应,又不知道跑到哪了,特别是一些刚接触的大项目或者需要定位一个大项目的一个疑难问题,一时没思路时,可以使用这个方法,个人觉得非常有帮助。

-]]>
- - Java - Thread dump - 问题排查 - 工具 - - - Java - Thread dump - -
- - 聊聊如何识别和意识到日常生活中的各类危险 - /2021/06/06/%E8%81%8A%E8%81%8A%E5%A6%82%E4%BD%95%E8%AF%86%E5%88%AB%E5%92%8C%E6%84%8F%E8%AF%86%E5%88%B0%E6%97%A5%E5%B8%B8%E7%94%9F%E6%B4%BB%E4%B8%AD%E7%9A%84%E5%90%84%E7%B1%BB%E5%8D%B1%E9%99%A9/ - 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。

+

PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

+
brew reinstall ./icu4c.rb
+

然后就是第五步,切换版本

+
brew switch icu4c 64.2
+

最后把分支切回来

+
git checkout master
+

是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

+
# zsh
+function hiicu64() {
+  local last_dir=$(pwd)
+
+  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+  git checkout icu4c-4
+  brew reinstall ./icu4c.rb
+  brew switch icu4c 64.2
+  git checkout master
+
+  cd $last_dir
+}
+

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

]]>
- 生活 + Mac + PHP + Homebrew + PHP + icu4c - 生活 - 糟心事 - 扶梯 - 踩踏 - 安全 - 电瓶车 + Mac + PHP + Homebrew + icu4c + zsh
@@ -17961,6 +17890,77 @@ void ReadView::prepare(trx_id_t id) { WeakReference + + 聊聊厦门旅游的好与不好 + /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ + 这几天去了趟厦门,原来几年前就想去了,本来都请好假了,后面因为一些事情没去成,这次刚好公司组织,就跟 LD 一起去了厦门,也不洋洋洒洒地写游记了,后面可能会有,今天先来总结下好的地方和比较坑的地方。
这次主要去了中山路、鼓浪屿、曾厝(cuo)垵、植物园、灵玲马戏团,因为住的离环岛路比较近,还有幸现场看了下厦门马拉松,其中

+

中山路

这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。

+

鼓浪屿

鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆

+

曾厝垵

一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆

+

植物园

植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆

+

灵玲马戏团

对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。

+

总结

厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦‍♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。

+]]>
+ + 生活 + 旅游 + + + 生活 + 杭州 + 旅游 + 厦门 + 中山路 + 局口街 + 鼓浪屿 + 曾厝垵 + 植物园 + 马戏团 + 沙茶面 + 海蛎煎 + +
+ + 聊聊如何识别和意识到日常生活中的各类危险 + /2021/06/06/%E8%81%8A%E8%81%8A%E5%A6%82%E4%BD%95%E8%AF%86%E5%88%AB%E5%92%8C%E6%84%8F%E8%AF%86%E5%88%B0%E6%97%A5%E5%B8%B8%E7%94%9F%E6%B4%BB%E4%B8%AD%E7%9A%84%E5%90%84%E7%B1%BB%E5%8D%B1%E9%99%A9/ + 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。

+]]>
+ + 生活 + + + 生活 + 糟心事 + 扶梯 + 踩踏 + 安全 + 电瓶车 + +
+ + 聊聊我刚学会的应用诊断方法 + /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/ + 因为传说中的出身问题,我以前写的是PHP,在使用 swoole 之前,基本的应用调试手段就是简单粗暴的 var_dump,exit,对于单进程模型的 PHP 也是简单有效,技术栈换成 Java 之后,就变得没那么容易,一方面是需要编译,另一方面是一般都是基于 spring 的项目,如果问题定位比较模糊,那框架层的是很难靠简单的 System.out.println 或者打 log 解决,(PS:我觉得可能我写的东西比较适合从 PHP 这种弱类型语言转到 Java 的小白同学)这个时候一方面因为是 Java,有了非常好用的 idea IDE 的支持,可以各种花式调试,条件断点尤其牛叉,但是又因为有 Spring+Java 的双重原因,有些情况下单步调试可以把手按废掉,这也是我之前一直比较困惑苦逼的点,后来随着慢慢精(jiang)进(you)之后,比如对于一个 oom 的情况,我们可以通过启动参数加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xx/xx 来配置溢出时的堆dump 日志,获取到这个文件后,我们可以通过像 Memory Analyzer (MAT)[https://www.eclipse.org/mat/] (The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.)来查看诊断问题所在,之前用到的时候是因为有个死循环一直往链表里塞数据,属于比较简单的,后来一次是由于运维进行应用迁移时按默认的统一配置了堆内存大小,导致内存的确不够用,所以溢出了,
今天想说的其实主要是我们的 thread dump,这也是我最近才真正用的一个方法,可能真的很小白了,用过 ide 的单步调试其实都知道会有一个一层层的玩意,比如函数从 A,调用了 B,再从 B 调用了 C,一直往下(因为是 Java,所以还有很多🤦‍♂️),这个其实也是大部分语言的调用模型,利用了栈这个数据结构,通过这个结构我们可以知道代码的调用链路,由于对于一个 spring 应用,在本身框架代码量非常庞大的情况下,外加如果应用代码也是非常多的时候,有时候通过单步调试真的很难短时间定位到问题,需要非常大的耐心和仔细观察,当然不是说完全不行,举个例子当我的应用经常启动需要非常长的时间,因为本身应用有非常多个 bean,比较难说究竟是 bean 的加载的确很慢还是有什么异常原因,这种时候就可以使用 thread dump 了,具体怎么操作呢

如果在idea 中运行或者调试时,可以直接点击这个照相机一样的按钮,右边就会出现了左边会显示所有的线程,右边会显示线程栈,

+
"main@1" prio=5 tid=0x1 nid=NA runnable
+  java.lang.Thread.State: RUNNABLE
+	  at TreeDistance.treeDist(TreeDistance.java:64)
+	  at TreeDistance.treeDist(TreeDistance.java:65)
+	  at TreeDistance.treeDist(TreeDistance.java:65)
+	  at TreeDistance.treeDist(TreeDistance.java:65)
+	  at TreeDistance.main(TreeDistance.java:45)
+

这就是我们主线程的堆栈信息了,main 表示这个线程名,prio表示优先级,默认是 5,tid 表示线程 id,nid 表示对应的系统线程,后面的runnable 表示目前线程状态,因为是被我打了断点,所以是就许状态,然后下面就是对应的线程栈内容了,在TreeDistance类的 treeDist方法中,对应的文件行数是 64 行。
这里使用 thread dump一般也不会是上面我截图代码里的这种代码量很少的,一般是大型项目,有时候跑着跑着没反应,又不知道跑到哪了,特别是一些刚接触的大项目或者需要定位一个大项目的一个疑难问题,一时没思路时,可以使用这个方法,个人觉得非常有帮助。

+]]>
+ + Java + Thread dump + 问题排查 + 工具 + + + Java + Thread dump + +
聊聊我理解的分布式事务 /2020/05/17/%E8%81%8A%E8%81%8A%E6%88%91%E7%90%86%E8%A7%A3%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ @@ -18018,19 +18018,6 @@ void ReadView::prepare(trx_id_t id) { 远程办公 - - 聊聊最近平淡的生活之看《神探狄仁杰》 - /2021/12/19/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E3%80%8A%E7%A5%9E%E6%8E%A2%E7%8B%84%E4%BB%81%E6%9D%B0%E3%80%8B/ - 其实最近看的不止这一部,前面看了《继承者们》,《少年包青天》这些,就一起聊下,其中看《继承者们》算是个人比较喜欢,以前就有这种看剧的习惯,这个跟《一生一世》里任嘉伦说自己看《寻秦记》看了几十遍一样,我看喜欢的剧也基本上会看不止五遍,继承者们是有帅哥美女看,而且印象中剧情也挺甜的,一般情况下最好是已经有点遗忘剧情了,因为我个人觉得看剧分两种,无聊了又心情不太好,可以看些这类轻松又看过的剧,可以不完全专心地看剧,另外有心情专心看的时候,可以看一些需要思考,一些探案类的或者烧脑类。
最近看了《神探狄仁杰》,因为跟前面看的《少年包青天》都是这类古装探案剧,正好有些感想,《少年包青天》算是儿时阴影,本来是不太会去看的,正好有一次有机会跟 LD 一起看了会就也觉得比较有意思就看了下去,不得不说,以前的这些剧还是很不错的,包括剧情和演员,第一部一共是 40 集,看的过程中也发现了大概是五个案子,平均八集一个案子,整体节奏还是比较慢的,但是基本每个案子其实都是构思得很巧妙,很久以前看过但是现在基本不太记得剧情了,每个案子在前面几集的时候基本都猜不到犯案逻辑,但是在看了狄仁杰之后,发现两部剧也有比较大的差别,少年包青天相对来说逻辑性会更强一些,个人主观觉得推理的严谨性更高,可能剧本打磨上更将就一下,而狄仁杰因为要提现他的个人强项,不比少年包青天中有公孙策一时瑜亮的情节,狄仁杰中分工明确,李元芳是个武力担当,曾泰是捧哏的,相对来说是狄仁杰在案子里从始至终地推进案情,有些甚至有些玄乎,会有一些跳脱跟不合理,有些像是狄仁杰的奇遇,不过这些想法是私人的观点,并不是想要评孰优孰劣;第二个感受是不知道是不是年代关系,特别是少年包青天,每个案件的大 boss 基本都不是个完全的坏人,甚至都是比较情有可原的可怜人,因为一些特殊原因,而好几个都是包拯身边的人,这一点其实跟狄仁杰里第一个使团惊魂案件比较类似,虎敬晖也是个人物形象比较丰满的角色,不是个标签化的淡薄形象,跟金木兰的感情和反叛行动在最后都说明的缘由,而且也有随着跟狄仁杰一起办案被其影响感化,最终为了救狄仁杰而被金木兰所杀,只是这样金木兰这个角色就会有些偏执和符号化,当然剧本肯定不是能面面俱到,这样的剧本已经比现在很多流量剧的好很多了。还想到了前阵子看的《指环王》中的白袍萨鲁曼在剧中也是个比较单薄的角色,这样的电影彪炳影史也没办法把个个人物都设计得完整有血有肉,或者说这本来也是应该有侧重点,当然其实我也不觉得指环王就是绝对的最好的,因为相对来说故事情节的复杂性等真的不如西游记,只是在 86 版之后的各种乱七八糟的翻牌和乱拍已经让这个真正的王者神话故事有点力不从心,这里边有部西游记后传是个人还比较喜欢的,虽然武打动作比较鬼畜,但是剧情基本是无敌的,在西游的架构上衍生出来这么完整丰富的故事,人物角色也都有各自的出彩点。
说回狄仁杰,在这之前也看过徐克拍的几部狄仁杰的电影版,第一部刘德华拍得相对完成度更高,故事性也可圈可点,后面几部就是剧情拉胯,靠特效拉回来一点分,虽说这个也是所谓的狄仁杰宇宙的构思在里面但是现在来看基本是跟西游那些差不多,完全没有整体性可言,打一枪换一个地方,演员也没有延续性,剧情也是前后跳脱,没什么关联跟承上启下,导致质量层次不一,更不用谈什么狄仁杰宇宙了,不过这个事情也是难说,原因很多,现在资本都是更加趋利的,一些需要更长久时间才能有回报的投资是很难获得资本青睐,所以只能将重心投给选择一些流量明星,而本来应该将资源投给剧本打磨的基本就没了,再深入说也没意义了,社会现状就是这样。
还有一点感想是,以前的剧里的拍摄环境还是比较惨的,看着一些房子,甚至皇宫都是比较破旧的,地面还是石板这种,想想以前的演员的环境再想想现在的,比如成龙说的,以前他拍剧就是啪摔了,问这条有没有过,过了就直接送医院,而不是现在可能手蹭破点皮就大叫,甚至还有饭圈这些破事。

-]]>
- - 生活 - - - 生活 - 看剧 - -
聊聊最近平淡的生活之又聊通勤 /2021/11/07/%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/ @@ -18050,6 +18037,33 @@ void ReadView::prepare(trx_id_t id) { 骑车 + + 聊聊最近平淡的生活之《花束般的恋爱》观后感 + /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/ + 周末在领导的提议下看了豆瓣的年度榜单,本来感觉没啥心情看的,看到主演有有村架纯就觉得可以看一下,颜值即正义嘛,男主小麦跟女主小娟(后面简称小麦跟小娟)是两个在一次非常偶然的没赶上地铁末班车事件中相识,这里得说下日本这种通宵营业的店好像挺不错的,看着也挺正常,国内估计只有酒吧之类的可以。晚上去的地方是有点暗暗的,好像也有点类似酒吧,旁边有类似于 dj 那种,然后同桌的还有除了男女主的另外一对男女,也是因为没赶上地铁末班车的,但也是陌生人,然后小麦突然看到了有个非常有名的电影人,小娟竟然也认识,然后旁边那对完全不认识,还在那吹自己看过很多电影,比如《肖申克的救赎》,于是男女主都特别鄙夷地看着他们,然后他们又去了另一个有点像泡澡的地方席地而坐,他们发现了自己的鞋子都是一样的,然后在女的去上厕所的时候,小麦暗恋的学姐也来了,然后小麦就去跟学姐他们一起坐了,小娟回来后有点不开心就说去朋友家睡,幸好小麦看出来了(他竟然看出来了,本来以为应该是没填过恋爱很木讷的),就追出去,然后就去了小麦家,到了家小娟发现小麦家的书柜上的书简直就跟她自己家的一模一样,小麦还给小娟吹了头发,一起吃烤饭团,看电影,第二天送小娟上了公交,还约好了一起看木乃伊展,然而并没有交换联系方式,但是他们还是约上了一起看了木乃伊展,在餐馆就出现了片头那一幕的来源,因为餐馆他们想一起听歌,就用有线耳机一人一个耳朵听,但是旁边就有个大叔说“你们是不是不爱音乐,左右耳朵是不一样的,只有一起听才是真正的音乐”这样的话,然后的剧情有点跳,因为是指他们一直在这家餐馆吃饭,中间有他们一起出去玩情节穿插着,也是在这他们确立了关系,可以说主体就是体现了他们非常的合拍和默契,就像一些影评说的,这部电影是说如何跟百分百合拍的人分手,然后就是正常的恋爱开始啪啪啪,一直腻在床上,也没去就业说明会,后面也有讲了一点小麦带着小娟去认识他的朋友,也把小娟介绍给了他们认识,这里算是个小伏笔,后面他们分手也有这里的人的一些关系,接下去的剧情说实话我是不太喜欢的,如果一部八分的电影只是说恋爱被现实打败的话,我觉得在我这是不合格的,但是事实也是这样,小麦其实是有家里的资助,所以后面还是按自己的喜好给一些机构画点插画,小娟则要出去工作,因为小娟家庭观念也是要让她出去有正经工作,用脚指头想也能知道肯定不顺利,然后就是暂时在一家蛋糕店工作,小麦就每天去接小娟,日子过得甜甜蜜蜜,后面小娟在自己的努力下考了个什么资格证,去了一家医院还是什么做前台行政,这中间当然就有父母来见面吃饭了,他们在开始恋爱不久就同居合租了,然后小娟父母就是来说要让她有个正经工作,对男的说的话就是人生就是责任这类的话,而小麦爸爸算是个导火索,因为小麦家里是做烟花生意的,他爸让他就做烟花生意,因为要回老家,并且小麦也不想做,所以就拒绝了,然后他爸就说不给他每个月五万的资助,这也导致了小麦需要去找工作,这个过程也是很辛苦,本来想要年前找好工作,然后事与愿违,后面有一次小娟被同事吐槽怎么从来不去团建,于是她就去了(我以为会拒绝),正在团建的时候小麦给她电话,说找到工作了,是一个创业物流公司这种,这里剧情就是我觉得比较俗套的,小麦各种被虐,累成狗,但是就像小娟爸爸说的话,人生就是责任,所以一直在坚持,但是这样也导致了跟小娟的交流也越来越少,他们原来最爱的漫画,爱玩的游戏,也只剩小娟一个人看,一个人玩,而正是这个时候,小娟说她辞掉了工作,去做一个不是太靠谱的漫画改造的密室逃脱,然后这里其实有一点后面争议很大的,就是这个工作其实是前面小麦介绍给小娟的那些朋友中一个的女朋友介绍的,而在有个剧情就是小娟有一次在这个密室逃脱的老板怀里醒过来,是在 KTV 那样的场景里,这就有很多人觉得小娟是不是出轨了,我觉得其实不那么重要,因为这个离职的事情已经让一切矛盾都摆在眼前,小麦其实是接受这种需要承担责任的生活,也想着要跟小娟结婚,但是小娟似乎还是想要过着那样理想的生活,做自己想做的事情,看自己爱看的漫画,也要小麦能像以前那样一直那么默契的有着相同的爱好,这里的触发点其实还有个是那个小麦的朋友(也就是他女朋友介绍小娟那个不靠谱工作的)的葬礼上,小麦在参加完葬礼后有挺多想倾诉的,而小娟只是想睡了,这个让小麦第二天起来都不想理小娟,只是这里我不太理解,难道这点闹情绪都不能接受吗,所谓的合拍也只是毫无限制的情况下的合拍吧,真正的生活怎么可能如此理想呢,即使没有物质生活的压力,也会有其他的各种压力和限制,在这之后其实小麦想说的是小娟是不是没有想跟自己继续在一起的想法了,而小娟觉得都不说话了,还怎么结婚呢,后面其实导演搞了个小 trick,突然放了异常婚礼,但是不是男女主的,我并不觉得这个桥段很好,在婚礼里男女主都觉得自己想要跟对方说分手了,但是当他们去了最开始一直去的餐馆的时候,一个算是一个现实映照的就是他们一直坐的位子被占了,可能也是导演想通过这个来说明他们已经回不去了,在餐馆交谈的时候,小麦其实是说他们结婚吧,并没有想前面婚礼上预设地要分手,但是小娟放弃了,不想结婚,因为不想过那样的生活了,而小麦觉得可能生活就是那样,不可能一直保持刚恋爱时候的那种感觉,生活就是责任,人生就意味着责任。

+

我的一些观点也在前面说了,恋爱到婚姻,即使物质没问题,经济没问题,也会有各种各样的问题,需要一起去解决,因为结婚就意味着需要相互扶持,而不是各取所需,可能我的要求比较高,后面男女主在分手后还一起住了一段时间,我原来还在想会不会通过这个方式让他们继续去磨合同步,只是我失望了,最后给个打分可能是 5 到 6 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。

+]]>
+ + 生活 + + + 生活 + 看剧 + +
+ + 聊聊最近平淡的生活之看《神探狄仁杰》 + /2021/12/19/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E3%80%8A%E7%A5%9E%E6%8E%A2%E7%8B%84%E4%BB%81%E6%9D%B0%E3%80%8B/ + 其实最近看的不止这一部,前面看了《继承者们》,《少年包青天》这些,就一起聊下,其中看《继承者们》算是个人比较喜欢,以前就有这种看剧的习惯,这个跟《一生一世》里任嘉伦说自己看《寻秦记》看了几十遍一样,我看喜欢的剧也基本上会看不止五遍,继承者们是有帅哥美女看,而且印象中剧情也挺甜的,一般情况下最好是已经有点遗忘剧情了,因为我个人觉得看剧分两种,无聊了又心情不太好,可以看些这类轻松又看过的剧,可以不完全专心地看剧,另外有心情专心看的时候,可以看一些需要思考,一些探案类的或者烧脑类。
最近看了《神探狄仁杰》,因为跟前面看的《少年包青天》都是这类古装探案剧,正好有些感想,《少年包青天》算是儿时阴影,本来是不太会去看的,正好有一次有机会跟 LD 一起看了会就也觉得比较有意思就看了下去,不得不说,以前的这些剧还是很不错的,包括剧情和演员,第一部一共是 40 集,看的过程中也发现了大概是五个案子,平均八集一个案子,整体节奏还是比较慢的,但是基本每个案子其实都是构思得很巧妙,很久以前看过但是现在基本不太记得剧情了,每个案子在前面几集的时候基本都猜不到犯案逻辑,但是在看了狄仁杰之后,发现两部剧也有比较大的差别,少年包青天相对来说逻辑性会更强一些,个人主观觉得推理的严谨性更高,可能剧本打磨上更将就一下,而狄仁杰因为要提现他的个人强项,不比少年包青天中有公孙策一时瑜亮的情节,狄仁杰中分工明确,李元芳是个武力担当,曾泰是捧哏的,相对来说是狄仁杰在案子里从始至终地推进案情,有些甚至有些玄乎,会有一些跳脱跟不合理,有些像是狄仁杰的奇遇,不过这些想法是私人的观点,并不是想要评孰优孰劣;第二个感受是不知道是不是年代关系,特别是少年包青天,每个案件的大 boss 基本都不是个完全的坏人,甚至都是比较情有可原的可怜人,因为一些特殊原因,而好几个都是包拯身边的人,这一点其实跟狄仁杰里第一个使团惊魂案件比较类似,虎敬晖也是个人物形象比较丰满的角色,不是个标签化的淡薄形象,跟金木兰的感情和反叛行动在最后都说明的缘由,而且也有随着跟狄仁杰一起办案被其影响感化,最终为了救狄仁杰而被金木兰所杀,只是这样金木兰这个角色就会有些偏执和符号化,当然剧本肯定不是能面面俱到,这样的剧本已经比现在很多流量剧的好很多了。还想到了前阵子看的《指环王》中的白袍萨鲁曼在剧中也是个比较单薄的角色,这样的电影彪炳影史也没办法把个个人物都设计得完整有血有肉,或者说这本来也是应该有侧重点,当然其实我也不觉得指环王就是绝对的最好的,因为相对来说故事情节的复杂性等真的不如西游记,只是在 86 版之后的各种乱七八糟的翻牌和乱拍已经让这个真正的王者神话故事有点力不从心,这里边有部西游记后传是个人还比较喜欢的,虽然武打动作比较鬼畜,但是剧情基本是无敌的,在西游的架构上衍生出来这么完整丰富的故事,人物角色也都有各自的出彩点。
说回狄仁杰,在这之前也看过徐克拍的几部狄仁杰的电影版,第一部刘德华拍得相对完成度更高,故事性也可圈可点,后面几部就是剧情拉胯,靠特效拉回来一点分,虽说这个也是所谓的狄仁杰宇宙的构思在里面但是现在来看基本是跟西游那些差不多,完全没有整体性可言,打一枪换一个地方,演员也没有延续性,剧情也是前后跳脱,没什么关联跟承上启下,导致质量层次不一,更不用谈什么狄仁杰宇宙了,不过这个事情也是难说,原因很多,现在资本都是更加趋利的,一些需要更长久时间才能有回报的投资是很难获得资本青睐,所以只能将重心投给选择一些流量明星,而本来应该将资源投给剧本打磨的基本就没了,再深入说也没意义了,社会现状就是这样。
还有一点感想是,以前的剧里的拍摄环境还是比较惨的,看着一些房子,甚至皇宫都是比较破旧的,地面还是石板这种,想想以前的演员的环境再想想现在的,比如成龙说的,以前他拍剧就是啪摔了,问这条有没有过,过了就直接送医院,而不是现在可能手蹭破点皮就大叫,甚至还有饭圈这些破事。

+]]>
+ + 生活 + + + 生活 + 看剧 + +
聊聊最近平淡的生活之看看老剧 /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/ @@ -18068,17 +18082,38 @@ void ReadView::prepare(trx_id_t id) { - 聊聊最近平淡的生活之《花束般的恋爱》观后感 - /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/ - 周末在领导的提议下看了豆瓣的年度榜单,本来感觉没啥心情看的,看到主演有有村架纯就觉得可以看一下,颜值即正义嘛,男主小麦跟女主小娟(后面简称小麦跟小娟)是两个在一次非常偶然的没赶上地铁末班车事件中相识,这里得说下日本这种通宵营业的店好像挺不错的,看着也挺正常,国内估计只有酒吧之类的可以。晚上去的地方是有点暗暗的,好像也有点类似酒吧,旁边有类似于 dj 那种,然后同桌的还有除了男女主的另外一对男女,也是因为没赶上地铁末班车的,但也是陌生人,然后小麦突然看到了有个非常有名的电影人,小娟竟然也认识,然后旁边那对完全不认识,还在那吹自己看过很多电影,比如《肖申克的救赎》,于是男女主都特别鄙夷地看着他们,然后他们又去了另一个有点像泡澡的地方席地而坐,他们发现了自己的鞋子都是一样的,然后在女的去上厕所的时候,小麦暗恋的学姐也来了,然后小麦就去跟学姐他们一起坐了,小娟回来后有点不开心就说去朋友家睡,幸好小麦看出来了(他竟然看出来了,本来以为应该是没填过恋爱很木讷的),就追出去,然后就去了小麦家,到了家小娟发现小麦家的书柜上的书简直就跟她自己家的一模一样,小麦还给小娟吹了头发,一起吃烤饭团,看电影,第二天送小娟上了公交,还约好了一起看木乃伊展,然而并没有交换联系方式,但是他们还是约上了一起看了木乃伊展,在餐馆就出现了片头那一幕的来源,因为餐馆他们想一起听歌,就用有线耳机一人一个耳朵听,但是旁边就有个大叔说“你们是不是不爱音乐,左右耳朵是不一样的,只有一起听才是真正的音乐”这样的话,然后的剧情有点跳,因为是指他们一直在这家餐馆吃饭,中间有他们一起出去玩情节穿插着,也是在这他们确立了关系,可以说主体就是体现了他们非常的合拍和默契,就像一些影评说的,这部电影是说如何跟百分百合拍的人分手,然后就是正常的恋爱开始啪啪啪,一直腻在床上,也没去就业说明会,后面也有讲了一点小麦带着小娟去认识他的朋友,也把小娟介绍给了他们认识,这里算是个小伏笔,后面他们分手也有这里的人的一些关系,接下去的剧情说实话我是不太喜欢的,如果一部八分的电影只是说恋爱被现实打败的话,我觉得在我这是不合格的,但是事实也是这样,小麦其实是有家里的资助,所以后面还是按自己的喜好给一些机构画点插画,小娟则要出去工作,因为小娟家庭观念也是要让她出去有正经工作,用脚指头想也能知道肯定不顺利,然后就是暂时在一家蛋糕店工作,小麦就每天去接小娟,日子过得甜甜蜜蜜,后面小娟在自己的努力下考了个什么资格证,去了一家医院还是什么做前台行政,这中间当然就有父母来见面吃饭了,他们在开始恋爱不久就同居合租了,然后小娟父母就是来说要让她有个正经工作,对男的说的话就是人生就是责任这类的话,而小麦爸爸算是个导火索,因为小麦家里是做烟花生意的,他爸让他就做烟花生意,因为要回老家,并且小麦也不想做,所以就拒绝了,然后他爸就说不给他每个月五万的资助,这也导致了小麦需要去找工作,这个过程也是很辛苦,本来想要年前找好工作,然后事与愿违,后面有一次小娟被同事吐槽怎么从来不去团建,于是她就去了(我以为会拒绝),正在团建的时候小麦给她电话,说找到工作了,是一个创业物流公司这种,这里剧情就是我觉得比较俗套的,小麦各种被虐,累成狗,但是就像小娟爸爸说的话,人生就是责任,所以一直在坚持,但是这样也导致了跟小娟的交流也越来越少,他们原来最爱的漫画,爱玩的游戏,也只剩小娟一个人看,一个人玩,而正是这个时候,小娟说她辞掉了工作,去做一个不是太靠谱的漫画改造的密室逃脱,然后这里其实有一点后面争议很大的,就是这个工作其实是前面小麦介绍给小娟的那些朋友中一个的女朋友介绍的,而在有个剧情就是小娟有一次在这个密室逃脱的老板怀里醒过来,是在 KTV 那样的场景里,这就有很多人觉得小娟是不是出轨了,我觉得其实不那么重要,因为这个离职的事情已经让一切矛盾都摆在眼前,小麦其实是接受这种需要承担责任的生活,也想着要跟小娟结婚,但是小娟似乎还是想要过着那样理想的生活,做自己想做的事情,看自己爱看的漫画,也要小麦能像以前那样一直那么默契的有着相同的爱好,这里的触发点其实还有个是那个小麦的朋友(也就是他女朋友介绍小娟那个不靠谱工作的)的葬礼上,小麦在参加完葬礼后有挺多想倾诉的,而小娟只是想睡了,这个让小麦第二天起来都不想理小娟,只是这里我不太理解,难道这点闹情绪都不能接受吗,所谓的合拍也只是毫无限制的情况下的合拍吧,真正的生活怎么可能如此理想呢,即使没有物质生活的压力,也会有其他的各种压力和限制,在这之后其实小麦想说的是小娟是不是没有想跟自己继续在一起的想法了,而小娟觉得都不说话了,还怎么结婚呢,后面其实导演搞了个小 trick,突然放了异常婚礼,但是不是男女主的,我并不觉得这个桥段很好,在婚礼里男女主都觉得自己想要跟对方说分手了,但是当他们去了最开始一直去的餐馆的时候,一个算是一个现实映照的就是他们一直坐的位子被占了,可能也是导演想通过这个来说明他们已经回不去了,在餐馆交谈的时候,小麦其实是说他们结婚吧,并没有想前面婚礼上预设地要分手,但是小娟放弃了,不想结婚,因为不想过那样的生活了,而小麦觉得可能生活就是那样,不可能一直保持刚恋爱时候的那种感觉,生活就是责任,人生就意味着责任。

-

我的一些观点也在前面说了,恋爱到婚姻,即使物质没问题,经济没问题,也会有各种各样的问题,需要一起去解决,因为结婚就意味着需要相互扶持,而不是各取所需,可能我的要求比较高,后面男女主在分手后还一起住了一段时间,我原来还在想会不会通过这个方式让他们继续去磨合同步,只是我失望了,最后给个打分可能是 5 到 6 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。

+ 聊聊给亲戚朋友的老电脑重装系统那些事儿 + /2021/05/09/%E8%81%8A%E8%81%8A%E7%BB%99%E4%BA%B2%E6%88%9A%E6%9C%8B%E5%8F%8B%E7%9A%84%E8%80%81%E7%94%B5%E8%84%91%E9%87%8D%E8%A3%85%E7%B3%BB%E7%BB%9F%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF/ + 前面这个五一回去之前,LD 姐姐跟我说电脑很卡了,想让我重装系统,问了下 LD 可能是那个 09 年买的笔记本,想想有点害怕[捂脸],前年有一次好像让我帮忙装了她同事的一个三星的笔记本,本着一些系统洁癖,所以就从开始找纯净版的 win7 家庭版,因为之前那些本基本都自带 win7 的家庭版,而且把激活码就贴在机器下面,然后从三星官网去找官方驱动,还好这个机型的驱动还在,先做了系统镜像,其实感觉这种情况需要两个 U 盘,一个 U 盘装系统作为安装启动盘,一个放驱动,毕竟不是专业装系统的,然后因为官方驱动需要一个个下载一个个安装,然后驱动文件下载的地方还没标明是 32 位还是 64 位的,结果还被 LD 姐姐催着,一直问好没好,略尴尬,索性还是找个一键安装的

+

这次甚至更夸张,上次还让带回去,我准备好了系统镜像啥的,第二天装,这次直接带了两个老旧笔记本过来说让当天就装好,感觉有点像被当修电脑的使,又说这些电脑其实都不用了的,都是为了她们当医生的要每年看会课,然后只能用电脑浏览器看,结果都在用 360 浏览器,真的是万恶的 360,其实以前对 360 没啥坏印象,毕竟以前也经常用,只是对于这些老电脑,360 全家桶真的就是装了就废了,2G 的内存,开机就开着 360 安全卫士,360 杀毒,有一个还装了腾讯电脑管家,然后腾讯视频跟爱奇艺也开机启动了,然后还打开 360 浏览器看课,就算再好的系统也吃不消这么用,重装了系统,还是这么装这些东西,也是分分钟变卡,可惜他们都没啥这类概念。

+

对于他们要看的课,更搞笑的是,明明在页面上注明了说要使用 IE 浏览器,结果他们都在用 360 浏览器看,但是这个也不能完全怪他们,因为实在是现在的 IE 啥的也有开始不兼容 flash 的配置,需要开启兼容配置,但是只要开启了之后就可以直接用 IE 看,比 360 靠谱很多, 资源占用也比较少,360 估计是基于 chromium 加了很多内置的插件,本身 chromium 也是内存大户,但是说这些其实他们也不懂,总觉得找我免费装下系统能撑一段时间,反正对我来说也应该很简单(他们觉得),实际上开始工作以后,我自己想装个双系统都是上淘宝买别人的服务装的,台式机更是几年没动过系统了,因为要重装一大堆软件,数据备份啥的,还有驱动什么的,分区格式,那些驱动精灵啥的也都是越来越坑,一装就给你带一堆垃圾软件。

+

感悟是,总觉得学计算机的就应该会装系统,会修电脑,之前亲戚还拿着一个完全开不起来的笔记本让我来修,这真的是,我说可以找官方维修的,结果我说我搞不定,她直接觉得是修不好了,直接电脑都懒得拿回去了,后面又一次反复解释了才明白,另外就是 360 全家桶,别说老电脑了,新机器都不太吃得消。

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

+]]>
+ + 生活 + + + 生活 + 换车牌
@@ -18099,33 +18134,151 @@ void ReadView::prepare(trx_id_t id) { - 聊聊这次换车牌及其他 - /2022/02/20/%E8%81%8A%E8%81%8A%E8%BF%99%E6%AC%A1%E6%8D%A2%E8%BD%A6%E7%89%8C%E5%8F%8A%E5%85%B6%E4%BB%96/ - 去年 8 月份运气比较好,摇到了车牌,本来其实应该很早就开始摇的,前面第一次换工作没注意社保断缴了一个月,也是大意失荆州,后面到了 17 年社保满两年了,好像只摇了一次,还是就没摇过,有点忘了,好像是什么原因导致那次也没摇成功,但是后面暂住证就取消了,需要居住证,居住证又要一年及以上的租房合同,并且那会买车以后也不怎么开,住的地方车位还好,但是公司车位一个月要两三千,甚至还是打车上下班比较实惠,所以也没放在心上,后面摇到房以后,也觉得应该准备起来车子,就开始办了居住证,居住证其实还可以用劳动合同,而且办起来也挺快,大概是三四月份开始摇,到 8 月份的某一天收到短信说摇到了,一开始还挺开心,不过心里抱着也不怎么开,也没怎么大放在心上,不过这里有一点就是我把那个照片直接发出去,上面有着我的身份证号,被 LD 说了一顿,以后也应该小心点,但是后面不知道是哪里看了下,说杭州上牌已经需要国六标准的车了,瞬间感觉是空欢喜了,可是有同事说是可以的,我就又打了官方的电话,结果说可以的,要先转籍,然后再做上牌。

-

转籍其实是很方便的,在交警 12123 App 上申请就行了,在转籍以后,需要去实地验车,验车的话,在支付宝-杭州交警生活号里进行预约,找就近的车管所就好,需要准备一些东西,首先是行驶证,机动车登记证书,身份证,居住证,还有车上需要准备的东西是要有三脚架和反光背心,反光背心是最近几个月开始要的,问过之前去验车的只需要三脚架就好了,预约好了的话建议是赶上班时间越早越好,不然过去排队时间要很久,而且人多了以后会很乱,各种插队,而且有很多都是汽车销售,一个销售带着一堆车,我们附近那个进去的小路没一会就堵满车,进去需要先排队,然后扫码,接着交资料,这两个都排着队,如果去晚了就要排很久的队,交完资料才是排队等验车,验车就是打开引擎盖,有人会帮忙拓印发动机车架号,然后验车的会各种检查一下,车里面,还有后备箱,建议车内整理干净点,后备箱不要放杂物,检验完了之后,需要把三脚架跟反光背心放在后备箱盖子上,人在旁边拍个照,然后需要把车牌遮住后再拍个车子的照片,再之后就是去把车牌卸了,这个多吐槽下,那边应该是本来那边师傅帮忙卸车牌,结果他就说是教我们拆,虽然也不算难,但是不排除师傅有在偷懒,完了之后就是把旧车牌交回去,然后需要在手机上(警察叔叔 App)提交各种资料,包括身份证,行驶证,机动车登记证书,提交了之后就等寄车牌过来了。

-

这里面缺失的一个环节就是选号了,选号杭州有两个方式,一种就是根据交管局定期发布的选号号段,可以自定义拼 20 个号,在手机上的交警 12123 App 上可以三个一组的形式提交,如果有没被选走的,就可以预选到这个了,但是这种就是也需要有一定策略,最新出的号段能选中的概率大一点,然后数字全是 8,6 这种的肯定会一早就被选走,然后如果跟我一样可以提前选下尾号,因为尾号数字影响限号,我比较有可能周五回家,所以得避开 5,0 的,第二种就是 50 选一跟以前新车选号一样,就不介绍了。第一种选中了以后可以在前面交还旧车牌的时候填上等着寄过来了,因为我是第一种选中的,第二种也可以在手机上选,也在可以在交还车牌的时候现场选。

-

总体过程其实是 LD 在各种查资料跟帮我跑来跑去,要不是 LD,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。

+ 聊聊部分公交车的设计bug + /2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ + 今天惯例坐公交回住的地方,不小心撞了头,原因是我们想坐倒数第二排,然后LD 走在我后面,我就走到最后一排中间等着,但是最后一排是高一截的,等 LD 坐进去以后,我就往前走,结果撞到了车顶的扶手杆子的一端,差点撞昏了去,这里我觉得其实杆子长度应该短一点,不然从最后一排出来,还是有比较大概率因为没注意看而撞到头,特别是没注意看的情况,发力其实会比较大,一头撞上就会像我这样,眼前一黑,又痛得要死。
还有一点就是座位设计了,先来看个图

图里大致画了两条线,因为可能是轮胎还是什么原因,后排中间会有那么大的突起,但是看两条红线可以发现,靠近过道的座位边缘跟地面突起的边缘不是一样宽的,这样导致的结果就是坐着的时候有一个脚没地儿搁,要不就得侧着斜着坐,或者就是一个脚悬空,短程的可能还好,路程远一点还是比较难受的,特别是像我现在这样,大腿外侧有点难受的情况,就会更难受。
虽然说这两个点,基本是屁用没有,但是我也是在自己这个博客说说,也当是个树洞了。

]]>
生活 生活 - 换车牌 + 公交 + 杭州
- 聊聊部分公交车的设计bug - /2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ - 今天惯例坐公交回住的地方,不小心撞了头,原因是我们想坐倒数第二排,然后LD 走在我后面,我就走到最后一排中间等着,但是最后一排是高一截的,等 LD 坐进去以后,我就往前走,结果撞到了车顶的扶手杆子的一端,差点撞昏了去,这里我觉得其实杆子长度应该短一点,不然从最后一排出来,还是有比较大概率因为没注意看而撞到头,特别是没注意看的情况,发力其实会比较大,一头撞上就会像我这样,眼前一黑,又痛得要死。
还有一点就是座位设计了,先来看个图

图里大致画了两条线,因为可能是轮胎还是什么原因,后排中间会有那么大的突起,但是看两条红线可以发现,靠近过道的座位边缘跟地面突起的边缘不是一样宽的,这样导致的结果就是坐着的时候有一个脚没地儿搁,要不就得侧着斜着坐,或者就是一个脚悬空,短程的可能还好,路程远一点还是比较难受的,特别是像我现在这样,大腿外侧有点难受的情况,就会更难受。
虽然说这两个点,基本是屁用没有,但是我也是在自己这个博客说说,也当是个树洞了。

+ 记一个容器中 dubbo 注册的小知识点 + /2022/10/09/%E8%AE%B0%E4%B8%80%E4%B8%AA%E5%AE%B9%E5%99%A8%E4%B8%AD-dubbo-%E6%B3%A8%E5%86%8C%E7%9A%84%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/ + 在目前环境下使用容器部署Java应用还是挺普遍的,但是有一些问题也是随之而来需要解决的,比如容器中应用的dubbo注册,在比较早的版本的dubbo中,就是简单地获取网卡的ip地址。
具体代码在这个方法里 com.alibaba.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol

+
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
+        String name = protocolConfig.getName();
+        if (name == null || name.length() == 0) {
+            name = "dubbo";
+        }
+
+        String host = protocolConfig.getHost();
+        if (provider != null && (host == null || host.length() == 0)) {
+            host = provider.getHost();
+        }
+        boolean anyhost = false;
+        if (NetUtils.isInvalidLocalHost(host)) {
+            anyhost = true;
+            try {
+                host = InetAddress.getLocalHost().getHostAddress();
+            } catch (UnknownHostException e) {
+                logger.warn(e.getMessage(), e);
+            }
+            if (NetUtils.isInvalidLocalHost(host)) {
+                if (registryURLs != null && registryURLs.size() > 0) {
+                    for (URL registryURL : registryURLs) {
+                        try {
+                            Socket socket = new Socket();
+                            try {
+                                SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort());
+                                socket.connect(addr, 1000);
+                                host = socket.getLocalAddress().getHostAddress();
+                                break;
+                            } finally {
+                                try {
+                                    socket.close();
+                                } catch (Throwable e) {}
+                            }
+                        } catch (Exception e) {
+                            logger.warn(e.getMessage(), e);
+                        }
+                    }
+                }
+                if (NetUtils.isInvalidLocalHost(host)) {
+                    host = NetUtils.getLocalHost();
+                }
+            }
+        }
+

通过jdk自带的方法 java.net.InetAddress#getLocalHost来获取本机地址,这样子对于容器来讲,获取到容器内部ip注册上去其实是没办法被调用到的,
而在之后的版本中例如dubbo 2.6.5,则可以通过在docker中设置环境变量的形式来注入docker所在的宿主机地址,
代码同样在com.alibaba.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol这个方法中,但是获取host的方法变成了 com.alibaba.dubbo.config.ServiceConfig#findConfigedHosts

+
private String findConfigedHosts(ProtocolConfig protocolConfig, List<URL> registryURLs, Map<String, String> map) {
+        boolean anyhost = false;
+
+        String hostToBind = getValueFromConfig(protocolConfig, Constants.DUBBO_IP_TO_BIND);
+        if (hostToBind != null && hostToBind.length() > 0 && isInvalidLocalHost(hostToBind)) {
+            throw new IllegalArgumentException("Specified invalid bind ip from property:" + Constants.DUBBO_IP_TO_BIND + ", value:" + hostToBind);
+        }
+
+        // if bind ip is not found in environment, keep looking up
+        if (hostToBind == null || hostToBind.length() == 0) {
+            hostToBind = protocolConfig.getHost();
+            if (provider != null && (hostToBind == null || hostToBind.length() == 0)) {
+                hostToBind = provider.getHost();
+            }
+            if (isInvalidLocalHost(hostToBind)) {
+                anyhost = true;
+                try {
+                    hostToBind = InetAddress.getLocalHost().getHostAddress();
+                } catch (UnknownHostException e) {
+                    logger.warn(e.getMessage(), e);
+                }
+                if (isInvalidLocalHost(hostToBind)) {
+                    if (registryURLs != null && !registryURLs.isEmpty()) {
+                        for (URL registryURL : registryURLs) {
+                            if (Constants.MULTICAST.equalsIgnoreCase(registryURL.getParameter("registry"))) {
+                                // skip multicast registry since we cannot connect to it via Socket
+                                continue;
+                            }
+                            try {
+                                Socket socket = new Socket();
+                                try {
+                                    SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort());
+                                    socket.connect(addr, 1000);
+                                    hostToBind = socket.getLocalAddress().getHostAddress();
+                                    break;
+                                } finally {
+                                    try {
+                                        socket.close();
+                                    } catch (Throwable e) {
+                                    }
+                                }
+                            } catch (Exception e) {
+                                logger.warn(e.getMessage(), e);
+                            }
+                        }
+                    }
+                    if (isInvalidLocalHost(hostToBind)) {
+                        hostToBind = getLocalHost();
+                    }
+                }
+            }
+        }
+
+        map.put(Constants.BIND_IP_KEY, hostToBind);
+
+        // registry ip is not used for bind ip by default
+        String hostToRegistry = getValueFromConfig(protocolConfig, Constants.DUBBO_IP_TO_REGISTRY);
+        if (hostToRegistry != null && hostToRegistry.length() > 0 && isInvalidLocalHost(hostToRegistry)) {
+            throw new IllegalArgumentException("Specified invalid registry ip from property:" + Constants.DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
+        } else if (hostToRegistry == null || hostToRegistry.length() == 0) {
+            // bind ip is used as registry ip by default
+            hostToRegistry = hostToBind;
+        }
+
+        map.put(Constants.ANYHOST_KEY, String.valueOf(anyhost));
+
+        return hostToRegistry;
+    }
+

String hostToRegistry = getValueFromConfig(protocolConfig, Constants.DUBBO_IP_TO_REGISTRY);
就是这一行,

+
private String getValueFromConfig(ProtocolConfig protocolConfig, String key) {
+    String protocolPrefix = protocolConfig.getName().toUpperCase() + "_";
+    String port = ConfigUtils.getSystemProperty(protocolPrefix + key);
+    if (port == null || port.length() == 0) {
+        port = ConfigUtils.getSystemProperty(key);
+    }
+    return port;
+}
+

也就是配置了DUBBO_IP_TO_REGISTRY这个环境变量

]]>
- 生活 + java - 生活 - 公交 - 杭州 + dubbo
@@ -18198,68 +18351,83 @@ void ReadView::prepare(trx_id_t id) { - 聊聊给亲戚朋友的老电脑重装系统那些事儿 - /2021/05/09/%E8%81%8A%E8%81%8A%E7%BB%99%E4%BA%B2%E6%88%9A%E6%9C%8B%E5%8F%8B%E7%9A%84%E8%80%81%E7%94%B5%E8%84%91%E9%87%8D%E8%A3%85%E7%B3%BB%E7%BB%9F%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF/ - 前面这个五一回去之前,LD 姐姐跟我说电脑很卡了,想让我重装系统,问了下 LD 可能是那个 09 年买的笔记本,想想有点害怕[捂脸],前年有一次好像让我帮忙装了她同事的一个三星的笔记本,本着一些系统洁癖,所以就从开始找纯净版的 win7 家庭版,因为之前那些本基本都自带 win7 的家庭版,而且把激活码就贴在机器下面,然后从三星官网去找官方驱动,还好这个机型的驱动还在,先做了系统镜像,其实感觉这种情况需要两个 U 盘,一个 U 盘装系统作为安装启动盘,一个放驱动,毕竟不是专业装系统的,然后因为官方驱动需要一个个下载一个个安装,然后驱动文件下载的地方还没标明是 32 位还是 64 位的,结果还被 LD 姐姐催着,一直问好没好,略尴尬,索性还是找个一键安装的

-

这次甚至更夸张,上次还让带回去,我准备好了系统镜像啥的,第二天装,这次直接带了两个老旧笔记本过来说让当天就装好,感觉有点像被当修电脑的使,又说这些电脑其实都不用了的,都是为了她们当医生的要每年看会课,然后只能用电脑浏览器看,结果都在用 360 浏览器,真的是万恶的 360,其实以前对 360 没啥坏印象,毕竟以前也经常用,只是对于这些老电脑,360 全家桶真的就是装了就废了,2G 的内存,开机就开着 360 安全卫士,360 杀毒,有一个还装了腾讯电脑管家,然后腾讯视频跟爱奇艺也开机启动了,然后还打开 360 浏览器看课,就算再好的系统也吃不消这么用,重装了系统,还是这么装这些东西,也是分分钟变卡,可惜他们都没啥这类概念。

-

对于他们要看的课,更搞笑的是,明明在页面上注明了说要使用 IE 浏览器,结果他们都在用 360 浏览器看,但是这个也不能完全怪他们,因为实在是现在的 IE 啥的也有开始不兼容 flash 的配置,需要开启兼容配置,但是只要开启了之后就可以直接用 IE 看,比 360 靠谱很多, 资源占用也比较少,360 估计是基于 chromium 加了很多内置的插件,本身 chromium 也是内存大户,但是说这些其实他们也不懂,总觉得找我免费装下系统能撑一段时间,反正对我来说也应该很简单(他们觉得),实际上开始工作以后,我自己想装个双系统都是上淘宝买别人的服务装的,台式机更是几年没动过系统了,因为要重装一大堆软件,数据备份啥的,还有驱动什么的,分区格式,那些驱动精灵啥的也都是越来越坑,一装就给你带一堆垃圾软件。

-

感悟是,总觉得学计算机的就应该会装系统,会修电脑,之前亲戚还拿着一个完全开不起来的笔记本让我来修,这真的是,我说可以找官方维修的,结果我说我搞不定,她直接觉得是修不好了,直接电脑都懒得拿回去了,后面又一次反复解释了才明白,另外就是 360 全家桶,别说老电脑了,新机器都不太吃得消。

-]]>
- - 生活 - - - 生活 - 装电脑 - 老电脑 - 360 全家桶 - 修电脑的 - -
- - 记录下 phpunit 的入门使用方法之setUp和tearDown - /2022/10/23/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E4%B9%8BsetUp%E5%92%8CtearDown/ - 可能是太久没写单测了,写个单测发现不符合预期,后来验证下才反应过来
我们来看下demo

-
class RenameTest extends TestCase
+    记录下 phpunit 的入门使用方法
+    /2022/10/16/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/
+    这周开始打算写个比较简单的php工具包,然后顺带学习使用下php的单元测试,通过phpunit还是比较方便的,首先就composer require phpunit/phpunit
安装下 phpunit, 前面包就是通过 composer init 创建,装完依赖后就可以把自动加载代码生成下 composer dump-autoload
目录结构差不多这样

+
.
+├── composer.json
+├── composer.lock
+├── oldfile.txt
+├── phpunit.xml
+├── src
+│   └── Rename.php
+└── tests
+    └── RenameTest.php
+
+2 directories, 6 files
+

src/是源码,tests/是放的单测,比较重要的是phpunit.xml

+
<?xml version="1.0" encoding="UTF-8"?>
+<phpunit colors="true" bootstrap="vendor/autoload.php">
+    <testsuites>
+        <testsuite name="php-rename">
+            <directory>./tests/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
+

其中bootstrap就是需要把依赖包的自动加载入口配上,因为这个作为一个package,也会指出命名空间
然后就是testsuite的路径,源码中

+
<?php
+namespace Nicksxs\PhpRename;
+
+class Rename
 {
-    public function setUp(): void
+    public static function renameSingleFile($file, $newFileName): bool
     {
-        var_dump("setUp");
+        if(!is_file($file)) {
+            echo "it's not a file";
+            return false;
+        }
+        $fileInfo = pathinfo($file);
+        return rename($file, $fileInfo["dirname"] . DIRECTORY_SEPARATOR . $newFileName . "." . $fileInfo["extension"]);
     }
+}
+

就是一个简单的重命名
然后test代码是这样,

+
<?php
 
-    public function test1()
+// require_once 'vendor/autoload.php';
+
+use PHPUnit\Framework\TestCase;
+use Nicksxs\PhpRename\Rename;
+use function PHPUnit\Framework\assertEquals;
+
+class RenameTest extends TestCase 
+{
+    public function setUp() :void
     {
-        var_dump("test1");
-        assertEquals(1, 1);
+        $myfile = fopen(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "w") or die("Unable to open file!");
+        $txt = "file test1\n";
+        fwrite($myfile, $txt);
+        fclose($myfile);
     }
-
-    public function test2()
+    public function testRename()
     {
-        var_dump("test2");
-        assertEquals(1, 1);
+        Rename::renameSingleFile(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "newfile");
+        assertEquals(is_file(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt"), true);
     }
 
     protected function tearDown(): void
     {
-        var_dump("tearDown");
+        unlink(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt");
     }
-}
-

因为我是想写个重命名的小工具,希望通过setUptearDown做一些文件初始化和清理工作,但是我把两个case的初始化跟清理工作写到了单个setUptearDown中,这样就出现了异常的错误
通过上面的示例代码,可以看到执行结果

+}
+

setUptearDown 就是初始化跟结束清理的,但是注意如果不指明 __DIR__ ,待会的目录就会在执行 vendor/bin/phpunit 下面,
或者也可以指定在一个 tmp/ 目录下
最后就可以通过vendor/bin/phpunit 来执行测试
执行结果

❯ vendor/bin/phpunit
 PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
 
-.string(5) "setUp"
-string(5) "test1"
-string(8) "tearDown"
-.                                                                  2 / 2 (100%)string(5) "setUp"
-string(5) "test2"
-string(8) "tearDown"
-
+.                                                                   1 / 1 (100%)
 
 Time: 00:00.005, Memory: 6.00 MB
 
-OK (2 tests, 2 assertions)
-

其实就是很简单的会在每个test方法前后都执行setUptearDown

+OK (1 test, 1 assertion)
]]> php @@ -18268,6 +18436,20 @@ OK (2 t php + + 记录下 redis 的一些使用方法 + /2022/10/30/%E8%AE%B0%E5%BD%95%E4%B8%8B-redis-%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ + 虽然说之前讲解过一些redis源码相关的,但是说实话,redis的各种使用其实有时候有点生疏,或者在一些特定的使用场景中,一些使用方法还是需要学习和记录的

+

获取所有数据

获取list类型的所有元素,可以使用 lrange , 直接用lrange key 0 -1
比如

这里有一些方便的就是可以不用知道长度,直接全返回,或者如果想拿到特定区间的就可以直接指定起止范围,

这样就不用一个个pop出来

+

裁剪list

前面用了lrange取得了一个范围的数据,如果想将数据直接移除,那可以用 ltrim ,

这两个命令就可以从list里取出批量数据,并且能从list里删除这部分数据

+]]>
+ + redis + + + redis + +
记录下 zookeeper 集群迁移和易错点 /2022/05/29/%E8%AE%B0%E5%BD%95%E4%B8%8B-zookeeper-%E9%9B%86%E7%BE%A4%E8%BF%81%E7%A7%BB/ @@ -18316,6 +18498,22 @@ zk3 192.168.2.3 + + 重看了下《蛮荒记》说说感受 + /2021/10/10/%E9%87%8D%E7%9C%8B%E4%BA%86%E4%B8%8B%E3%80%8A%E8%9B%AE%E8%8D%92%E8%AE%B0%E3%80%8B%E8%AF%B4%E8%AF%B4%E6%84%9F%E5%8F%97/ + 周末把《蛮荒记》看完了,前面是发现微信读书有《搜神记》和《蛮荒记》,但是《搜神记》看了会发现很多都是跳段了,不知道为啥,貌似也没什么少儿不宜的情节,所以就上网找了原版来看,为什么看这个呢,主要还是高中的时候看过,觉得写得很不错,属于那时候的玄幻小说里的独一档,基于山海经创造了一个半架空的大荒宇宙,五族帝尊,人物名都是听说过的,而且又能契合部分历史,整个故事布局非常宏大,并且情节矛盾埋得很深,这里就不对具体情节作介绍了,只是聊聊对书中的一些人物和情节的看法感受。

+

乌丝兰玛是个贯穿两部,甚至在蛮荒的最后还要再搞事情,极其坚定的自以为是的大 boss,其实除了最后被我们的主人公打败,前面几乎就是无所不能,下了一盘无比巨大的棋,主人公都只是其中一个棋子和意外,但是正如很多反派,一直以来都是背着一个信念,并且这个所谓的信念是比较正义的,只是为了这个正义的信念和目标却做了各种丧尽天良的事情,说起来跟灭霸有点像,为了环保哈哈,相对来说感觉姬远玄也只是个最大牌的工具人,或者说是中间人,深爱的妹妹冰夷也意外被蚩尤怒拿一血。

+

但是中间那个赤霞仙子一定要给烈烟石的心上锁,导致最后认不出来蚩尤,也间接导致了蚩尤被杀,如果不考虑最后情节或者推动故事的需求,这个还是我很讨厌的,有点类似于《驴得水》里那个校长,看着貌似是个正常的,做的事情也是正派,但是其实是害人不浅,即使南阳仙子因此被抛进了火山,那也是有贱人在那挑食,并且赤松子是赤飚怒的儿子,烈烟石跟蚩尤又没这层关系,就很像倚天屠龙记里的灭绝师太和极品家丁里的那个玉德仙坊的院主,后者还好一些,前者几乎就是导致周芷若一生悲剧的始作俑者,自己偏执的善恶观,还要给徒弟灌输如此恶毒的理念和让她立下像紧箍咒似的誓言,在人一生中本来就有很多不能如愿的,又被最亲最尊敬的人下了这样的紧箍咒,人生的不幸也加倍了。

+

似乎习惯了总要有个总结的,想说的应该是我觉得这些剧也好,书也好,我觉得最坏的人可能是大部分人眼中的一些次要人物,或者至少大 boss 才是最坏的人,当然这个坏也不是严格的二分法,只是我觉得最让我觉得负面的人物,这些人可能看起来情景出现的不多,只是说了很少的话,做了很少的事,但是在我看来却做了最大的恶。

+]]>
+ + 生活 + + + 生活 + 看书 + +
这周末我又在老丈人家打了天小工 /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/ @@ -18336,19 +18534,55 @@ zk3 192.168.2.3 - 重看了下《蛮荒记》说说感受 - /2021/10/10/%E9%87%8D%E7%9C%8B%E4%BA%86%E4%B8%8B%E3%80%8A%E8%9B%AE%E8%8D%92%E8%AE%B0%E3%80%8B%E8%AF%B4%E8%AF%B4%E6%84%9F%E5%8F%97/ - 周末把《蛮荒记》看完了,前面是发现微信读书有《搜神记》和《蛮荒记》,但是《搜神记》看了会发现很多都是跳段了,不知道为啥,貌似也没什么少儿不宜的情节,所以就上网找了原版来看,为什么看这个呢,主要还是高中的时候看过,觉得写得很不错,属于那时候的玄幻小说里的独一档,基于山海经创造了一个半架空的大荒宇宙,五族帝尊,人物名都是听说过的,而且又能契合部分历史,整个故事布局非常宏大,并且情节矛盾埋得很深,这里就不对具体情节作介绍了,只是聊聊对书中的一些人物和情节的看法感受。

-

乌丝兰玛是个贯穿两部,甚至在蛮荒的最后还要再搞事情,极其坚定的自以为是的大 boss,其实除了最后被我们的主人公打败,前面几乎就是无所不能,下了一盘无比巨大的棋,主人公都只是其中一个棋子和意外,但是正如很多反派,一直以来都是背着一个信念,并且这个所谓的信念是比较正义的,只是为了这个正义的信念和目标却做了各种丧尽天良的事情,说起来跟灭霸有点像,为了环保哈哈,相对来说感觉姬远玄也只是个最大牌的工具人,或者说是中间人,深爱的妹妹冰夷也意外被蚩尤怒拿一血。

-

但是中间那个赤霞仙子一定要给烈烟石的心上锁,导致最后认不出来蚩尤,也间接导致了蚩尤被杀,如果不考虑最后情节或者推动故事的需求,这个还是我很讨厌的,有点类似于《驴得水》里那个校长,看着貌似是个正常的,做的事情也是正派,但是其实是害人不浅,即使南阳仙子因此被抛进了火山,那也是有贱人在那挑食,并且赤松子是赤飚怒的儿子,烈烟石跟蚩尤又没这层关系,就很像倚天屠龙记里的灭绝师太和极品家丁里的那个玉德仙坊的院主,后者还好一些,前者几乎就是导致周芷若一生悲剧的始作俑者,自己偏执的善恶观,还要给徒弟灌输如此恶毒的理念和让她立下像紧箍咒似的誓言,在人一生中本来就有很多不能如愿的,又被最亲最尊敬的人下了这样的紧箍咒,人生的不幸也加倍了。

-

似乎习惯了总要有个总结的,想说的应该是我觉得这些剧也好,书也好,我觉得最坏的人可能是大部分人眼中的一些次要人物,或者至少大 boss 才是最坏的人,当然这个坏也不是严格的二分法,只是我觉得最让我觉得负面的人物,这些人可能看起来情景出现的不多,只是说了很少的话,做了很少的事,但是在我看来却做了最大的恶。

+ 记录下 phpunit 的入门使用方法之setUp和tearDown + /2022/10/23/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E4%B9%8BsetUp%E5%92%8CtearDown/ + 可能是太久没写单测了,写个单测发现不符合预期,后来验证下才反应过来
我们来看下demo

+
class RenameTest extends TestCase
+{
+    public function setUp(): void
+    {
+        var_dump("setUp");
+    }
+
+    public function test1()
+    {
+        var_dump("test1");
+        assertEquals(1, 1);
+    }
+
+    public function test2()
+    {
+        var_dump("test2");
+        assertEquals(1, 1);
+    }
+
+    protected function tearDown(): void
+    {
+        var_dump("tearDown");
+    }
+}
+

因为我是想写个重命名的小工具,希望通过setUptearDown做一些文件初始化和清理工作,但是我把两个case的初始化跟清理工作写到了单个setUptearDown中,这样就出现了异常的错误
通过上面的示例代码,可以看到执行结果

+
❯ vendor/bin/phpunit
+PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
+
+.string(5) "setUp"
+string(5) "test1"
+string(8) "tearDown"
+.                                                                  2 / 2 (100%)string(5) "setUp"
+string(5) "test2"
+string(8) "tearDown"
+
+
+Time: 00:00.005, Memory: 6.00 MB
+
+OK (2 tests, 2 assertions)
+

其实就是很简单的会在每个test方法前后都执行setUptearDown

]]>
- 生活 + php - 生活 - 看书 + php
@@ -18374,12 +18608,11 @@ zk3 192.168.2.3 - 闲话篇-路遇神逻辑骑车带娃爹 - /2022/05/08/%E9%97%B2%E8%AF%9D%E7%AF%87-%E8%B7%AF%E9%81%87%E7%A5%9E%E9%80%BB%E8%BE%91%E9%AA%91%E8%BD%A6%E5%B8%A6%E5%A8%83%E7%88%B9/ - 周末吃完中饭去买菜,没想到碰到这个神(zhi)奇(zhang)大哥带着两个娃,在非机动车道虽然没有像上班高峰车那么多,但是有送外卖,各种叮咚买菜和普通像我这样骑电驴,骑自行车的人,我的情况可能还特殊点,前面说过电驴买了以后本来网上找到过怎么解除限速的,后面看了下,限速 25 虽然慢,但还是对安全很有好处的,我上下班也不赶这个时间,所以就没解除,其他路上的电瓶车包括这位带娃的大哥可能有不少都不符合国标的限速要求或者解除了限速,这些算是铺垫。

-

那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。

-

现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。

-

说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。

+ 闲话篇-也算碰到了为老不尊和坏人变老了的典型案例 + /2022/05/22/%E9%97%B2%E8%AF%9D%E7%AF%87-%E4%B9%9F%E7%AE%97%E7%A2%B0%E5%88%B0%E4%BA%86%E4%B8%BA%E8%80%81%E4%B8%8D%E5%B0%8A%E5%92%8C%E5%9D%8F%E4%BA%BA%E5%8F%98%E8%80%81%E4%BA%86%E7%9A%84%E5%85%B8%E5%9E%8B%E6%A1%88%E4%BE%8B/ + 在目前的房子也差不多租了四五年了,楼下邻居换了两拨了,我们这栋楼装修了不知道多少次,因为是学区的原因,房子交易的频率还是比较高的,不过比较神奇的我们对门的没换过,而且一直也没什么交集(除了后面说的水管爆裂),就进出的时候偶尔看到应该是住着一对老夫妻,感觉年纪也有个七八十了。

+

对对面这户人家的印象,就是对面的老头子经常是我出门上班去了他回来,看着他颤颤巍巍地走楼梯,我看到了都靠边走,而且有几次还听见好像是他儿子在说他,”年假这么大了,还是少出去吧”,说实话除了这次的事情,之前就有一次水管阀门爆裂了,算是有点交集,那次大概是去年冬天,天气已经很冷了,我们周日下午回来看到楼梯有点湿,但是没什么特别的异常就没怎么注意,到晚上洗完澡,楼下的邻居就来敲门,说我们门外的水表那一直在流水,出门一看真的是懵了,外面水表那在哗哗哗地流水,导致楼梯那就跟水帘洞一样,仔细看看是对面家的水表阀门那在漏水,我只能先用塑料袋包一下,然后大冬天(刚洗完澡)穿着凉拖跑下去找物业保安,走到一楼的时候发现水一直流到一楼了,楼梯上都是水流下来,五楼那是最惨的,感觉门框周边都浸透了,五楼的也是态度比较差的让我一定要把水弄好了,但是前面也说了谁是从对门那户的水表阀那出来的,理论上应该让对面的处理,结果我敲门敲了半天对面都没反应,想着我放着不管也不太好,就去找了物业保安,保安上来看了只能先把总阀关了,我也打电话给维修自来水管的,自来水公司的人过了会也是真的来修了,我那会是挺怕不来修,自来水公司的师傅到了以后拿开一看是对面那户的有个阀门估计是自己换上去的,跟我们这的完全不一样,看上去就比较劣质,师傅也挺气的,大晚上被叫过来,我又尝试着去敲门也还是没人应,也没办法,对面老人家我敲太响到时候出来说我吓到他们啥的,第二天去说也没现场了。

+

前面的这件事是个重要铺垫,前几天 LD 下班后把厨余垃圾套好袋子放在门口,打算等我下班了因为要去做核酸(hz 48 小时核酸)顺便带下去,结果到了七点多,说对面的老太太在那疯狂砸门了,LD 被吓到了不敢开门,老太太在外面一边砸门一边骂,“你们年轻人怎么素质这么差”(他们家也经常在门口放垃圾,我们刚来住的时候在楼梯转角他们就放这个废弃的洗衣机,每次走楼梯带点东西都要小心翼翼地走,不然都过不去,然后我赶紧赶回去,结果她听到我回家了,还特意开门继续骂,“你们年轻人怎么素质这么差,垃圾放在这里”,我说我们刚才放在这,打算待会做核酸的时候去扔掉,结果他们家老头,都已经没了牙齿,在那瞪大眼睛说,“你们早上就放在这了的,”我说是LD 刚才下班了放的,争论了一会,我说这个事情我们门口放了垃圾,这会我就去扔掉了,但是你们家老太太这么砸门总不太好,像之前门口水管爆掉了,我敲了门没人应,我也没要砸门一定把你们叫醒,结果老头老太说我们的水管从来没换过,不可能破的(其实到这,再往后说就没意思了,跟这么不要脸的人说多了也只是瞎扯),一会又回到这个垃圾的问题,那个老头说“你们昨天就放在这里了的”,睁着眼说瞎话可真是 666,感觉不是老太太拦着点他马上就要冲上来揍我了一样,事后我想想,这种情况我大概只能躺地上装死了,当这个事情发生之前我真的快把前面说的事情(水管阀坏了)给忘了,虽然这是理论上不该我来处理,除非是老头老太太请求我帮忙,这事后面我也从没说起过,本来完全没交集,对他们的是怎么样的人也没概念,总觉得年纪大了可能还比较心宽和蔼点,结果没想到就是一典型的坏人变老了,我说你们这么砸门,我老婆都被吓得不敢开门,结果对面老头老太太的儿子也出来了说,“我们就是敲下门,我母亲是机关单位退休的,所以肯定不会敲门很大声的,你老婆觉得吓到了是你们人生观价值观有问题”,听到这话我差点笑出来,连着两个可笑至极的脑残逻辑,无语他妈给无语开门,无语到家了。对门家我们之前有个印象就是因为我们都是顶楼,这边老小区以前都是把前后阳台包进来的,然后社区就来咨询大家的意见是不是统一把包进来的违建拆掉,还特地上来六楼跟他们说,结果对面的老头就说,“我要去住建局投诉你们”,本来这个事情是违法的,但是社区的意思也是征求各位业主的意见,结果感觉是社区上门强拆了一样,为老不尊,坏人变老了的典范了。

]]>
生活 @@ -18389,11 +18622,12 @@ zk3 192.168.2.3
- 闲话篇-也算碰到了为老不尊和坏人变老了的典型案例 - /2022/05/22/%E9%97%B2%E8%AF%9D%E7%AF%87-%E4%B9%9F%E7%AE%97%E7%A2%B0%E5%88%B0%E4%BA%86%E4%B8%BA%E8%80%81%E4%B8%8D%E5%B0%8A%E5%92%8C%E5%9D%8F%E4%BA%BA%E5%8F%98%E8%80%81%E4%BA%86%E7%9A%84%E5%85%B8%E5%9E%8B%E6%A1%88%E4%BE%8B/ - 在目前的房子也差不多租了四五年了,楼下邻居换了两拨了,我们这栋楼装修了不知道多少次,因为是学区的原因,房子交易的频率还是比较高的,不过比较神奇的我们对门的没换过,而且一直也没什么交集(除了后面说的水管爆裂),就进出的时候偶尔看到应该是住着一对老夫妻,感觉年纪也有个七八十了。

-

对对面这户人家的印象,就是对面的老头子经常是我出门上班去了他回来,看着他颤颤巍巍地走楼梯,我看到了都靠边走,而且有几次还听见好像是他儿子在说他,”年假这么大了,还是少出去吧”,说实话除了这次的事情,之前就有一次水管阀门爆裂了,算是有点交集,那次大概是去年冬天,天气已经很冷了,我们周日下午回来看到楼梯有点湿,但是没什么特别的异常就没怎么注意,到晚上洗完澡,楼下的邻居就来敲门,说我们门外的水表那一直在流水,出门一看真的是懵了,外面水表那在哗哗哗地流水,导致楼梯那就跟水帘洞一样,仔细看看是对面家的水表阀门那在漏水,我只能先用塑料袋包一下,然后大冬天(刚洗完澡)穿着凉拖跑下去找物业保安,走到一楼的时候发现水一直流到一楼了,楼梯上都是水流下来,五楼那是最惨的,感觉门框周边都浸透了,五楼的也是态度比较差的让我一定要把水弄好了,但是前面也说了谁是从对门那户的水表阀那出来的,理论上应该让对面的处理,结果我敲门敲了半天对面都没反应,想着我放着不管也不太好,就去找了物业保安,保安上来看了只能先把总阀关了,我也打电话给维修自来水管的,自来水公司的人过了会也是真的来修了,我那会是挺怕不来修,自来水公司的师傅到了以后拿开一看是对面那户的有个阀门估计是自己换上去的,跟我们这的完全不一样,看上去就比较劣质,师傅也挺气的,大晚上被叫过来,我又尝试着去敲门也还是没人应,也没办法,对面老人家我敲太响到时候出来说我吓到他们啥的,第二天去说也没现场了。

-

前面的这件事是个重要铺垫,前几天 LD 下班后把厨余垃圾套好袋子放在门口,打算等我下班了因为要去做核酸(hz 48 小时核酸)顺便带下去,结果到了七点多,说对面的老太太在那疯狂砸门了,LD 被吓到了不敢开门,老太太在外面一边砸门一边骂,“你们年轻人怎么素质这么差”(他们家也经常在门口放垃圾,我们刚来住的时候在楼梯转角他们就放这个废弃的洗衣机,每次走楼梯带点东西都要小心翼翼地走,不然都过不去,然后我赶紧赶回去,结果她听到我回家了,还特意开门继续骂,“你们年轻人怎么素质这么差,垃圾放在这里”,我说我们刚才放在这,打算待会做核酸的时候去扔掉,结果他们家老头,都已经没了牙齿,在那瞪大眼睛说,“你们早上就放在这了的,”我说是LD 刚才下班了放的,争论了一会,我说这个事情我们门口放了垃圾,这会我就去扔掉了,但是你们家老太太这么砸门总不太好,像之前门口水管爆掉了,我敲了门没人应,我也没要砸门一定把你们叫醒,结果老头老太说我们的水管从来没换过,不可能破的(其实到这,再往后说就没意思了,跟这么不要脸的人说多了也只是瞎扯),一会又回到这个垃圾的问题,那个老头说“你们昨天就放在这里了的”,睁着眼说瞎话可真是 666,感觉不是老太太拦着点他马上就要冲上来揍我了一样,事后我想想,这种情况我大概只能躺地上装死了,当这个事情发生之前我真的快把前面说的事情(水管阀坏了)给忘了,虽然这是理论上不该我来处理,除非是老头老太太请求我帮忙,这事后面我也从没说起过,本来完全没交集,对他们的是怎么样的人也没概念,总觉得年纪大了可能还比较心宽和蔼点,结果没想到就是一典型的坏人变老了,我说你们这么砸门,我老婆都被吓得不敢开门,结果对面老头老太太的儿子也出来了说,“我们就是敲下门,我母亲是机关单位退休的,所以肯定不会敲门很大声的,你老婆觉得吓到了是你们人生观价值观有问题”,听到这话我差点笑出来,连着两个可笑至极的脑残逻辑,无语他妈给无语开门,无语到家了。对门家我们之前有个印象就是因为我们都是顶楼,这边老小区以前都是把前后阳台包进来的,然后社区就来咨询大家的意见是不是统一把包进来的违建拆掉,还特地上来六楼跟他们说,结果对面的老头就说,“我要去住建局投诉你们”,本来这个事情是违法的,但是社区的意思也是征求各位业主的意见,结果感觉是社区上门强拆了一样,为老不尊,坏人变老了的典范了。

+ 闲话篇-路遇神逻辑骑车带娃爹 + /2022/05/08/%E9%97%B2%E8%AF%9D%E7%AF%87-%E8%B7%AF%E9%81%87%E7%A5%9E%E9%80%BB%E8%BE%91%E9%AA%91%E8%BD%A6%E5%B8%A6%E5%A8%83%E7%88%B9/ + 周末吃完中饭去买菜,没想到碰到这个神(zhi)奇(zhang)大哥带着两个娃,在非机动车道虽然没有像上班高峰车那么多,但是有送外卖,各种叮咚买菜和普通像我这样骑电驴,骑自行车的人,我的情况可能还特殊点,前面说过电驴买了以后本来网上找到过怎么解除限速的,后面看了下,限速 25 虽然慢,但还是对安全很有好处的,我上下班也不赶这个时间,所以就没解除,其他路上的电瓶车包括这位带娃的大哥可能有不少都不符合国标的限速要求或者解除了限速,这些算是铺垫。

+

那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。

+

现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。

+

说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。

]]>
生活 @@ -18415,238 +18649,4 @@ zk3 192.168.2.3
- - 记录下 phpunit 的入门使用方法 - /2022/10/16/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ - 这周开始打算写个比较简单的php工具包,然后顺带学习使用下php的单元测试,通过phpunit还是比较方便的,首先就composer require phpunit/phpunit
安装下 phpunit, 前面包就是通过 composer init 创建,装完依赖后就可以把自动加载代码生成下 composer dump-autoload
目录结构差不多这样

-
.
-├── composer.json
-├── composer.lock
-├── oldfile.txt
-├── phpunit.xml
-├── src
-│   └── Rename.php
-└── tests
-    └── RenameTest.php
-
-2 directories, 6 files
-

src/是源码,tests/是放的单测,比较重要的是phpunit.xml

-
<?xml version="1.0" encoding="UTF-8"?>
-<phpunit colors="true" bootstrap="vendor/autoload.php">
-    <testsuites>
-        <testsuite name="php-rename">
-            <directory>./tests/</directory>
-        </testsuite>
-    </testsuites>
-</phpunit>
-

其中bootstrap就是需要把依赖包的自动加载入口配上,因为这个作为一个package,也会指出命名空间
然后就是testsuite的路径,源码中

-
<?php
-namespace Nicksxs\PhpRename;
-
-class Rename
-{
-    public static function renameSingleFile($file, $newFileName): bool
-    {
-        if(!is_file($file)) {
-            echo "it's not a file";
-            return false;
-        }
-        $fileInfo = pathinfo($file);
-        return rename($file, $fileInfo["dirname"] . DIRECTORY_SEPARATOR . $newFileName . "." . $fileInfo["extension"]);
-    }
-}
-

就是一个简单的重命名
然后test代码是这样,

-
<?php
-
-// require_once 'vendor/autoload.php';
-
-use PHPUnit\Framework\TestCase;
-use Nicksxs\PhpRename\Rename;
-use function PHPUnit\Framework\assertEquals;
-
-class RenameTest extends TestCase 
-{
-    public function setUp() :void
-    {
-        $myfile = fopen(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "w") or die("Unable to open file!");
-        $txt = "file test1\n";
-        fwrite($myfile, $txt);
-        fclose($myfile);
-    }
-    public function testRename()
-    {
-        Rename::renameSingleFile(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "newfile");
-        assertEquals(is_file(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt"), true);
-    }
-
-    protected function tearDown(): void
-    {
-        unlink(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt");
-    }
-}
-

setUptearDown 就是初始化跟结束清理的,但是注意如果不指明 __DIR__ ,待会的目录就会在执行 vendor/bin/phpunit 下面,
或者也可以指定在一个 tmp/ 目录下
最后就可以通过vendor/bin/phpunit 来执行测试
执行结果

-
❯ vendor/bin/phpunit
-PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
-
-.                                                                   1 / 1 (100%)
-
-Time: 00:00.005, Memory: 6.00 MB
-
-OK (1 test, 1 assertion)
-]]>
- - php - - - php - -
- - 记一个容器中 dubbo 注册的小知识点 - /2022/10/09/%E8%AE%B0%E4%B8%80%E4%B8%AA%E5%AE%B9%E5%99%A8%E4%B8%AD-dubbo-%E6%B3%A8%E5%86%8C%E7%9A%84%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/ - 在目前环境下使用容器部署Java应用还是挺普遍的,但是有一些问题也是随之而来需要解决的,比如容器中应用的dubbo注册,在比较早的版本的dubbo中,就是简单地获取网卡的ip地址。
具体代码在这个方法里 com.alibaba.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol

-
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
-        String name = protocolConfig.getName();
-        if (name == null || name.length() == 0) {
-            name = "dubbo";
-        }
-
-        String host = protocolConfig.getHost();
-        if (provider != null && (host == null || host.length() == 0)) {
-            host = provider.getHost();
-        }
-        boolean anyhost = false;
-        if (NetUtils.isInvalidLocalHost(host)) {
-            anyhost = true;
-            try {
-                host = InetAddress.getLocalHost().getHostAddress();
-            } catch (UnknownHostException e) {
-                logger.warn(e.getMessage(), e);
-            }
-            if (NetUtils.isInvalidLocalHost(host)) {
-                if (registryURLs != null && registryURLs.size() > 0) {
-                    for (URL registryURL : registryURLs) {
-                        try {
-                            Socket socket = new Socket();
-                            try {
-                                SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort());
-                                socket.connect(addr, 1000);
-                                host = socket.getLocalAddress().getHostAddress();
-                                break;
-                            } finally {
-                                try {
-                                    socket.close();
-                                } catch (Throwable e) {}
-                            }
-                        } catch (Exception e) {
-                            logger.warn(e.getMessage(), e);
-                        }
-                    }
-                }
-                if (NetUtils.isInvalidLocalHost(host)) {
-                    host = NetUtils.getLocalHost();
-                }
-            }
-        }
-

通过jdk自带的方法 java.net.InetAddress#getLocalHost来获取本机地址,这样子对于容器来讲,获取到容器内部ip注册上去其实是没办法被调用到的,
而在之后的版本中例如dubbo 2.6.5,则可以通过在docker中设置环境变量的形式来注入docker所在的宿主机地址,
代码同样在com.alibaba.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol这个方法中,但是获取host的方法变成了 com.alibaba.dubbo.config.ServiceConfig#findConfigedHosts

-
private String findConfigedHosts(ProtocolConfig protocolConfig, List<URL> registryURLs, Map<String, String> map) {
-        boolean anyhost = false;
-
-        String hostToBind = getValueFromConfig(protocolConfig, Constants.DUBBO_IP_TO_BIND);
-        if (hostToBind != null && hostToBind.length() > 0 && isInvalidLocalHost(hostToBind)) {
-            throw new IllegalArgumentException("Specified invalid bind ip from property:" + Constants.DUBBO_IP_TO_BIND + ", value:" + hostToBind);
-        }
-
-        // if bind ip is not found in environment, keep looking up
-        if (hostToBind == null || hostToBind.length() == 0) {
-            hostToBind = protocolConfig.getHost();
-            if (provider != null && (hostToBind == null || hostToBind.length() == 0)) {
-                hostToBind = provider.getHost();
-            }
-            if (isInvalidLocalHost(hostToBind)) {
-                anyhost = true;
-                try {
-                    hostToBind = InetAddress.getLocalHost().getHostAddress();
-                } catch (UnknownHostException e) {
-                    logger.warn(e.getMessage(), e);
-                }
-                if (isInvalidLocalHost(hostToBind)) {
-                    if (registryURLs != null && !registryURLs.isEmpty()) {
-                        for (URL registryURL : registryURLs) {
-                            if (Constants.MULTICAST.equalsIgnoreCase(registryURL.getParameter("registry"))) {
-                                // skip multicast registry since we cannot connect to it via Socket
-                                continue;
-                            }
-                            try {
-                                Socket socket = new Socket();
-                                try {
-                                    SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort());
-                                    socket.connect(addr, 1000);
-                                    hostToBind = socket.getLocalAddress().getHostAddress();
-                                    break;
-                                } finally {
-                                    try {
-                                        socket.close();
-                                    } catch (Throwable e) {
-                                    }
-                                }
-                            } catch (Exception e) {
-                                logger.warn(e.getMessage(), e);
-                            }
-                        }
-                    }
-                    if (isInvalidLocalHost(hostToBind)) {
-                        hostToBind = getLocalHost();
-                    }
-                }
-            }
-        }
-
-        map.put(Constants.BIND_IP_KEY, hostToBind);
-
-        // registry ip is not used for bind ip by default
-        String hostToRegistry = getValueFromConfig(protocolConfig, Constants.DUBBO_IP_TO_REGISTRY);
-        if (hostToRegistry != null && hostToRegistry.length() > 0 && isInvalidLocalHost(hostToRegistry)) {
-            throw new IllegalArgumentException("Specified invalid registry ip from property:" + Constants.DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
-        } else if (hostToRegistry == null || hostToRegistry.length() == 0) {
-            // bind ip is used as registry ip by default
-            hostToRegistry = hostToBind;
-        }
-
-        map.put(Constants.ANYHOST_KEY, String.valueOf(anyhost));
-
-        return hostToRegistry;
-    }
-

String hostToRegistry = getValueFromConfig(protocolConfig, Constants.DUBBO_IP_TO_REGISTRY);
就是这一行,

-
private String getValueFromConfig(ProtocolConfig protocolConfig, String key) {
-    String protocolPrefix = protocolConfig.getName().toUpperCase() + "_";
-    String port = ConfigUtils.getSystemProperty(protocolPrefix + key);
-    if (port == null || port.length() == 0) {
-        port = ConfigUtils.getSystemProperty(key);
-    }
-    return port;
-}
-

也就是配置了DUBBO_IP_TO_REGISTRY这个环境变量

-]]>
- - java - - - dubbo - -
- - 记录下 redis 的一些使用方法 - /2022/10/30/%E8%AE%B0%E5%BD%95%E4%B8%8B-redis-%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ - 虽然说之前讲解过一些redis源码相关的,但是说实话,redis的各种使用其实有时候有点生疏,或者在一些特定的使用场景中,一些使用方法还是需要学习和记录的

-

获取所有数据

获取list类型的所有元素,可以使用 lrange , 直接用lrange key 0 -1
比如

这里有一些方便的就是可以不用知道长度,直接全返回,或者如果想拿到特定区间的就可以直接指定起止范围,

这样就不用一个个pop出来

-

裁剪list

前面用了lrange取得了一个范围的数据,如果想将数据直接移除,那可以用 ltrim ,

这两个命令就可以从list里取出批量数据,并且能从list里删除这部分数据

-]]>
- - redis - - - redis - -
diff --git a/sitemap.xml b/sitemap.xml index 73eed2d2de..ea5d22bf7e 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -389,7 +389,7 @@ - https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ + https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ 2022-06-11 @@ -398,7 +398,7 @@ - https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/ + https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ 2022-06-11 @@ -452,7 +452,7 @@ - https://nicksxs.me/2022/03/13/Leetcode-83-%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0-Remove-Duplicates-from-Sorted-List-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-06-11 @@ -461,7 +461,7 @@ - https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/2022/03/13/Leetcode-83-%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0-Remove-Duplicates-from-Sorted-List-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ 2022-06-11 @@ -479,7 +479,7 @@ - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ 2022-06-11 @@ -488,7 +488,7 @@ - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ 2022-06-11 @@ -497,7 +497,7 @@ - https://nicksxs.me/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/ + https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ 2022-06-11 @@ -506,7 +506,7 @@ - https://nicksxs.me/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/ + https://nicksxs.me/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/ 2022-06-11 @@ -515,7 +515,7 @@ - https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/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/ 2022-06-11 @@ -551,7 +551,7 @@ - https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ 2022-06-11 @@ -560,7 +560,7 @@ - https://nicksxs.me/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/ + https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ 2022-06-11 @@ -569,7 +569,7 @@ - https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + https://nicksxs.me/2021/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/ 2022-06-11 @@ -614,7 +614,7 @@ - https://nicksxs.me/2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ + https://nicksxs.me/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/ 2022-06-11 @@ -623,7 +623,7 @@ - https://nicksxs.me/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/ + https://nicksxs.me/2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ 2022-06-11 @@ -650,7 +650,7 @@ - https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + https://nicksxs.me/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/ 2022-06-11 @@ -659,7 +659,7 @@ - https://nicksxs.me/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/ + https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ 2022-06-11 @@ -668,7 +668,7 @@ - https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ + https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ 2022-06-11 @@ -1577,7 +1577,7 @@ - https://nicksxs.me/2015/03/13/Reverse-Integer/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 @@ -1595,7 +1595,7 @@ - https://nicksxs.me/2019/12/10/Redis-Part-1/ + https://nicksxs.me/2015/01/14/Two-Sum/ 2020-01-12 @@ -1613,7 +1613,7 @@ - https://nicksxs.me/2015/01/14/Two-Sum/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 @@ -1694,7 +1694,7 @@ - https://nicksxs.me/baidu_verify_Gl8jtoDV4z.html + https://nicksxs.me/categories/index.html 2020-01-12 @@ -1703,7 +1703,7 @@ - https://nicksxs.me/categories/index.html + https://nicksxs.me/baidu_verify_Gl8jtoDV4z.html 2020-01-12 @@ -1944,35 +1944,35 @@ - https://nicksxs.me/tags/Linked-List/ + https://nicksxs.me/tags/Binary-Tree/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/ + https://nicksxs.me/tags/DFS/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Binary-Tree/ + https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/DFS/ + https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ + https://nicksxs.me/tags/Linked-List/ 2023-02-26 weekly 0.2 @@ -1986,35 +1986,35 @@ - https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ + https://nicksxs.me/tags/2019/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ + https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/c/ + https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/2019/ + https://nicksxs.me/tags/c/ 2023-02-26 weekly 0.2 @@ -2076,6 +2076,20 @@ 0.2 + + https://nicksxs.me/tags/2022/ + 2023-02-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/2023/ + 2023-02-26 + weekly + 0.2 + + https://nicksxs.me/tags/Apollo/ 2023-02-26 @@ -2104,6 +2118,13 @@ 0.2 + + https://nicksxs.me/tags/Disruptor/ + 2023-02-26 + weekly + 0.2 + + https://nicksxs.me/tags/Stream/ 2023-02-26 @@ -2140,98 +2161,98 @@ - https://nicksxs.me/tags/Disruptor/ + https://nicksxs.me/tags/Dubbo/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Filter/ + https://nicksxs.me/tags/RPC/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Interceptor/ + https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/AOP/ + https://nicksxs.me/tags/Filter/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Spring/ + https://nicksxs.me/tags/Interceptor/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Tomcat/ + https://nicksxs.me/tags/AOP/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Servlet/ + https://nicksxs.me/tags/Spring/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Web/ + https://nicksxs.me/tags/Tomcat/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Dubbo/ + https://nicksxs.me/tags/Servlet/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/RPC/ + https://nicksxs.me/tags/Web/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ + https://nicksxs.me/tags/G1/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/G1/ + https://nicksxs.me/tags/GC/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/GC/ + https://nicksxs.me/tags/Garbage-First-Collector/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Garbage-First-Collector/ + https://nicksxs.me/tags/headscale/ 2023-02-26 weekly 0.2 @@ -2286,20 +2307,6 @@ 0.2 - - https://nicksxs.me/tags/2022/ - 2023-02-26 - weekly - 0.2 - - - - https://nicksxs.me/tags/2023/ - 2023-02-26 - weekly - 0.2 - - https://nicksxs.me/tags/Shift-2D-Grid/ 2023-02-26 @@ -2308,77 +2315,77 @@ - https://nicksxs.me/tags/3Sum-Closest/ + https://nicksxs.me/tags/stack/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/headscale/ + https://nicksxs.me/tags/min-stack/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/linked-list/ + https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ + https://nicksxs.me/tags/leetcode-155/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/First-Bad-Version/ + https://nicksxs.me/tags/3Sum-Closest/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/string/ + https://nicksxs.me/tags/linked-list/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/stack/ + https://nicksxs.me/tags/First-Bad-Version/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/min-stack/ + https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ + https://nicksxs.me/tags/string/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/leetcode-155/ + https://nicksxs.me/tags/Intersection-of-Two-Arrays/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Intersection-of-Two-Arrays/ + https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ 2023-02-26 weekly 0.2 @@ -2433,20 +2440,6 @@ 0.2 - - https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/ - 2023-02-26 - weekly - 0.2 - - - - https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ - 2023-02-26 - weekly - 0.2 - - https://nicksxs.me/tags/linux/ 2023-02-26 @@ -2469,14 +2462,14 @@ - https://nicksxs.me/tags/mfc/ + https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Maven/ + https://nicksxs.me/tags/mfc/ 2023-02-26 weekly 0.2 @@ -2510,6 +2503,13 @@ 0.2 + + https://nicksxs.me/tags/Maven/ + 2023-02-26 + weekly + 0.2 + + https://nicksxs.me/tags/hadoop/ 2023-02-26 @@ -2602,35 +2602,35 @@ - https://nicksxs.me/tags/Mysql/ + https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Mybatis/ + https://nicksxs.me/tags/Mysql/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/ + https://nicksxs.me/tags/Mybatis/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ 2023-02-26 weekly 0.2 @@ -2665,7 +2665,7 @@ - https://nicksxs.me/tags/redis/ + https://nicksxs.me/tags/powershell/ 2023-02-26 weekly 0.2 @@ -2685,6 +2685,13 @@ 0.2 + + https://nicksxs.me/tags/redis/ + 2023-02-26 + weekly + 0.2 + + https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ 2023-02-26 @@ -2791,21 +2798,21 @@ - https://nicksxs.me/tags/websocket/ + https://nicksxs.me/tags/Spring-Event/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/swoole/ + https://nicksxs.me/tags/websocket/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/powershell/ + https://nicksxs.me/tags/swoole/ 2023-02-26 weekly 0.2 @@ -2853,13 +2860,6 @@ 0.2 - - https://nicksxs.me/tags/Spring-Event/ - 2023-02-26 - weekly - 0.2 - - https://nicksxs.me/tags/MQ/ 2023-02-26 @@ -2896,35 +2896,35 @@ - https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ + https://nicksxs.me/tags/ssh/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ + https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ + https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/ssh/ + https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ + https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ 2023-02-26 weekly 0.2 @@ -2952,91 +2952,91 @@ - https://nicksxs.me/tags/Windows/ + https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ + https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ + https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ + https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ + https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ + https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ + https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ + https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ + https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ + https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ + https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ + https://nicksxs.me/tags/Windows/ 2023-02-26 weekly 0.2 @@ -3049,13 +3049,6 @@ 0.2 - - https://nicksxs.me/tags/git/ - 2023-02-26 - weekly - 0.2 - - https://nicksxs.me/tags/%E8%BF%90%E5%8A%A8/ 2023-02-26 @@ -3155,7 +3148,14 @@ - https://nicksxs.me/tags/NameServer/ + https://nicksxs.me/tags/git/ + 2023-02-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/DefaultMQPushConsumer/ 2023-02-26 weekly 0.2 @@ -3169,7 +3169,7 @@ - https://nicksxs.me/tags/DefaultMQPushConsumer/ + https://nicksxs.me/tags/NameServer/ 2023-02-26 weekly 0.2 @@ -3246,35 +3246,35 @@ - https://nicksxs.me/tags/SPI/ + https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Adaptive/ + https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ + https://nicksxs.me/tags/SPI/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/ + https://nicksxs.me/tags/Adaptive/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/ + https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ 2023-02-26 weekly 0.2 @@ -3463,63 +3463,63 @@ - https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/ + https://nicksxs.me/tags/mvcc/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/is-null/ + https://nicksxs.me/tags/read-view/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/is-not-null/ + https://nicksxs.me/tags/gap-lock/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/procedure/ + https://nicksxs.me/tags/next-key-lock/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/mvcc/ + https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/read-view/ + https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/gap-lock/ + https://nicksxs.me/tags/is-null/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/next-key-lock/ + https://nicksxs.me/tags/is-not-null/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/ + https://nicksxs.me/tags/procedure/ 2023-02-26 weekly 0.2 @@ -3631,133 +3631,133 @@ - https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ + https://nicksxs.me/tags/ThreadLocal/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ + https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ + https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ + https://nicksxs.me/tags/WeakReference/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ + https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ + https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ + https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ + https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ + https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ + https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/Thread-dump/ + https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ + https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ + https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ + https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ + https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/ThreadLocal/ + https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ + https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ + https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/WeakReference/ + https://nicksxs.me/tags/Thread-dump/ 2023-02-26 weekly 0.2 @@ -3806,91 +3806,91 @@ - https://nicksxs.me/tags/%E7%9C%8B%E5%89%A7/ + https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/ + https://nicksxs.me/tags/%E7%9C%8B%E5%89%A7/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ + https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/stream/ + https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ + https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ + https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ + https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ + https://nicksxs.me/tags/dubbo/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/zookeeper/ + https://nicksxs.me/tags/stream/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ + https://nicksxs.me/tags/zookeeper/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ + https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ + https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ 2023-02-26 weekly 0.2 - https://nicksxs.me/tags/dubbo/ + https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ 2023-02-26 weekly 0.2 @@ -3906,28 +3906,28 @@ - https://nicksxs.me/categories/Java/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ 2023-02-26 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/ + https://nicksxs.me/categories/Java/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Java/JVM/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + https://nicksxs.me/categories/Java/JVM/ 2023-02-26 weekly 0.2 @@ -3948,14 +3948,14 @@ - https://nicksxs.me/categories/Java/GC/ + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + https://nicksxs.me/categories/Java/GC/ 2023-02-26 weekly 0.2 @@ -3983,28 +3983,28 @@ - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ 2023-02-26 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/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ 2023-02-26 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/ + https://nicksxs.me/categories/C/ 2023-02-26 weekly 0.2 @@ -4046,7 +4046,7 @@ - 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/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ 2023-02-26 weekly 0.2 @@ -4066,6 +4066,13 @@ 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/ + 2023-02-26 + weekly + 0.2 + + https://nicksxs.me/categories/Filter/ 2023-02-26 @@ -4074,7 +4081,7 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ + https://nicksxs.me/categories/headscale/ 2023-02-26 weekly 0.2 @@ -4102,7 +4109,7 @@ - https://nicksxs.me/categories/headscale/ + https://nicksxs.me/categories/stack/ 2023-02-26 weekly 0.2 @@ -4137,21 +4144,21 @@ - https://nicksxs.me/categories/stack/ + https://nicksxs.me/categories/Java/leetcode/Rotate-Image/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/Rotate-Image/ + https://nicksxs.me/categories/Interceptor-AOP/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Interceptor-AOP/ + https://nicksxs.me/categories/Linux/ 2023-02-26 weekly 0.2 @@ -4165,7 +4172,7 @@ - https://nicksxs.me/categories/Linux/ + https://nicksxs.me/categories/Redis/ 2023-02-26 weekly 0.2 @@ -4179,49 +4186,49 @@ - https://nicksxs.me/categories/Redis/ + https://nicksxs.me/categories/data-analysis/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/DP/ + https://nicksxs.me/categories/docker/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/data-analysis/ + https://nicksxs.me/categories/leetcode/java/DP/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/docker/ + https://nicksxs.me/categories/Docker/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Docker/ + https://nicksxs.me/categories/leetcode/java/stack/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/linked-list/ + https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ + https://nicksxs.me/categories/leetcode/java/linked-list/ 2023-02-26 weekly 0.2 @@ -4242,21 +4249,21 @@ - https://nicksxs.me/categories/leetcode/java/stack/ + https://nicksxs.me/categories/nginx/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/nginx/ + https://nicksxs.me/categories/Spring/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Spring/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ 2023-02-26 weekly 0.2 @@ -4270,7 +4277,7 @@ - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ 2023-02-26 weekly 0.2 @@ -4298,14 +4305,14 @@ - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ + https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/categories/Java/Spring/ 2023-02-26 weekly 0.2 @@ -4326,28 +4333,28 @@ - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ + https://nicksxs.me/categories/MQ/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Java/Spring/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/MQ/ + https://nicksxs.me/categories/ssh/ 2023-02-26 weekly 0.2 @@ -4361,21 +4368,21 @@ - https://nicksxs.me/categories/ssh/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Windows/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ + https://nicksxs.me/categories/Windows/ 2023-02-26 weekly 0.2 @@ -4389,21 +4396,21 @@ - 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/shell/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/shell/ + https://nicksxs.me/categories/Mybatis/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/git/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ 2023-02-26 weekly 0.2 @@ -4417,14 +4424,14 @@ - https://nicksxs.me/categories/Mybatis/ + https://nicksxs.me/categories/git/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ + https://nicksxs.me/categories/Java/SpringBoot/ 2023-02-26 weekly 0.2 @@ -4437,13 +4444,6 @@ 0.2 - - https://nicksxs.me/categories/Java/SpringBoot/ - 2023-02-26 - weekly - 0.2 - - https://nicksxs.me/categories/Java/Dubbo/RPC/ 2023-02-26 @@ -4480,7 +4480,7 @@ - https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ + https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ 2023-02-26 weekly 0.2 @@ -4494,21 +4494,21 @@ - https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ + https://nicksxs.me/categories/C/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ + https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ 2023-02-26 weekly 0.2 @@ -4522,14 +4522,14 @@ - https://nicksxs.me/categories/SpringBoot/ + https://nicksxs.me/categories/Mac/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Mac/ + https://nicksxs.me/categories/SpringBoot/ 2023-02-26 weekly 0.2 @@ -4550,14 +4550,14 @@ - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ 2023-02-26 weekly 0.2 @@ -4578,42 +4578,42 @@ - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ + https://nicksxs.me/categories/MQ/RocketMQ/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/MQ/RocketMQ/ + https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ 2023-02-26 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/Linux/%E5%91%BD%E4%BB%A4/echo/ 2023-02-26 weekly 0.2 - 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/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Windows/%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/ 2023-02-26 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/Windows/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2023-02-26 weekly 0.2 @@ -4627,35 +4627,35 @@ - https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ + https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2023-02-26 weekly 0.2 @@ -4704,28 +4704,28 @@ - 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/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + https://nicksxs.me/categories/C/Redis/ 2023-02-26 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/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/C/Redis/ + https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/ 2023-02-26 weekly 0.2 @@ -4753,14 +4753,14 @@ - https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/ + https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ 2023-02-26 weekly 0.2 - https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ + https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/ 2023-02-26 weekly 0.2 @@ -4844,7 +4844,7 @@ - https://nicksxs.me/categories/C/Mysql/ + https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/ 2023-02-26 weekly 0.2 @@ -4858,7 +4858,7 @@ - https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/ + https://nicksxs.me/categories/C/Mysql/ 2023-02-26 weekly 0.2 diff --git a/tags/java/page/7/index.html b/tags/java/page/7/index.html index 6521000d61..780011d2ef 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%
\ 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