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 d856235cde..8c3856aa70 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 @@ -
在前司和目前公司,用的配置中心都是使用的 Apollo,经过了业界验证,比较强大的配置管理系统,特别是在0.10 后开始支持对使用 value 注解的配置值进行自动更新,今天刚好有个同学问到我,就顺便写篇文章记录下,其实也是借助于 spring 强大的 bean 生命周期管理,可以实现BeanPostProcessor接口,使用postProcessBeforeInitialization方法,来对bean 内部的属性和方法进行判断,是否有 value 注解,如果有就是将它注册到一个 map 中,可以看到这个方法com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField
@Override
+Apollo 的 value 注解是怎么自动更新的 | Nicksxs's Blog
Apollo 的 value 注解是怎么自动更新的
在前司和目前公司,用的配置中心都是使用的 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);
@@ -61,4 +61,4 @@
updateSpringValue(val);
}
}
- }
其实原理很简单,就是得了解知道下
0%
\ No newline at end of file
+ }其实原理很简单,就是得了解知道下
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
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].right = buildTree(Arrays.copyOfRange(preorder, pos + 1, n), Arrays.copyOfRange(inorder, pos + 1, n));
return node;
}
-}
0%
\ No newline at end of file
+}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
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%,贴个图哈哈
这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。
这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。
一直以来过着特别平淡普通的生活,不过大多数人应该都这样吧,也许有些人可以把平凡的生活过得精彩,最简单的说明就是朋友圈吧,看我一年的盆友圈虽然在发,不过大概 90%的都是发发跑步的打卡,偶尔会有稀稀拉拉的点赞,天天上班,也不喜欢发什么状态,觉得没什么人关注,索性不发。
只是这么平淡的生活就有一些自己比较心烦纠结的,之前有提到过的交通,最近似乎又发现了一点,就真相总是让人跌破眼镜,以前觉得我可能是胆子比较小,所以会觉得怎么路上这些电瓶都是这么肆无忌惮的往我冲过来,后面慢慢有一种借用电视剧读心神探的概念,安全距离,觉得大部分人跟我一样,骑电瓶车什么的总还是有个安全距离,只是可能这个安全距离对于不同的人不一样,那些骑电瓶车的潜意识里的安全距离是非常短,所以经常会骑车离着你非常近才会刹车,但是这个安全距离理论最近又被推翻了,因为经历过几次电瓶车就是已经跟你有身体接触了,但是没到把人撞倒的程度,似乎这些骑电瓶车的觉得步行的行人在人行道上是空气,蹭一下也无所谓,反正不能挡我的路,总感觉要不是我在前面骑自行车太慢挡着电瓶车,不然他们都能起飞去干掉 F35 解放湾湾了;
另一个问题应该是说我们交通规则普及的太少,虽然我们没有路权这个名词概念,但是其实是有这个优先级的,包括像杭州是以公交车在人行道礼让行人闻名的,其实这个文明的行为只限于人行道在直行路中间的,大部分在十字路口,右转的公交车很少会让直行人行道的,前提是直行的绿灯的时候,特别是像公交车这样,车身特别长,右转的时候会有比较大的死角,如果是公交车先转,行人或者自行车很容易被卷进去,非常危险的,私家车就更不用说了,反正右转即使人行道上人非常多要转的也是一秒都不等,所以我自己在开车的时候是尽量在右转的时候等人行道上的行人或者骑车的走完,因为总会觉得我是不是有点双标,骑车走路的时候希望开车的能按规则让我,自己开车的时候又想赶紧开走,所以在开车的时候尽量做到让行车和骑车的。
还有个其实是写着写着想起来的,比如我骑车左转的时候,因为我是左转到对角那就到了,跟那些左转后要再直行的不一样,我们应该在学车的时候也学过,超车要从左边超,但是往往那些骑电瓶车的在左转的时候会从我右边超过来再往左边撇过去,如果留的空间大还好,有些电瓶车就是如果车头超过了就不管他的车屁股,如果我不减速,自行车就被刮倒了,可能的确是别人就不是人,只要不把你撞倒就无所谓,反正为了你自己不被撞倒你肯定会让的。
一直以来过着特别平淡普通的生活,不过大多数人应该都这样吧,也许有些人可以把平凡的生活过得精彩,最简单的说明就是朋友圈吧,看我一年的盆友圈虽然在发,不过大概 90%的都是发发跑步的打卡,偶尔会有稀稀拉拉的点赞,天天上班,也不喜欢发什么状态,觉得没什么人关注,索性不发。
只是这么平淡的生活就有一些自己比较心烦纠结的,之前有提到过的交通,最近似乎又发现了一点,就真相总是让人跌破眼镜,以前觉得我可能是胆子比较小,所以会觉得怎么路上这些电瓶都是这么肆无忌惮的往我冲过来,后面慢慢有一种借用电视剧读心神探的概念,安全距离,觉得大部分人跟我一样,骑电瓶车什么的总还是有个安全距离,只是可能这个安全距离对于不同的人不一样,那些骑电瓶车的潜意识里的安全距离是非常短,所以经常会骑车离着你非常近才会刹车,但是这个安全距离理论最近又被推翻了,因为经历过几次电瓶车就是已经跟你有身体接触了,但是没到把人撞倒的程度,似乎这些骑电瓶车的觉得步行的行人在人行道上是空气,蹭一下也无所谓,反正不能挡我的路,总感觉要不是我在前面骑自行车太慢挡着电瓶车,不然他们都能起飞去干掉 F35 解放湾湾了;
另一个问题应该是说我们交通规则普及的太少,虽然我们没有路权这个名词概念,但是其实是有这个优先级的,包括像杭州是以公交车在人行道礼让行人闻名的,其实这个文明的行为只限于人行道在直行路中间的,大部分在十字路口,右转的公交车很少会让直行人行道的,前提是直行的绿灯的时候,特别是像公交车这样,车身特别长,右转的时候会有比较大的死角,如果是公交车先转,行人或者自行车很容易被卷进去,非常危险的,私家车就更不用说了,反正右转即使人行道上人非常多要转的也是一秒都不等,所以我自己在开车的时候是尽量在右转的时候等人行道上的行人或者骑车的走完,因为总会觉得我是不是有点双标,骑车走路的时候希望开车的能按规则让我,自己开车的时候又想赶紧开走,所以在开车的时候尽量做到让行车和骑车的。
还有个其实是写着写着想起来的,比如我骑车左转的时候,因为我是左转到对角那就到了,跟那些左转后要再直行的不一样,我们应该在学车的时候也学过,超车要从左边超,但是往往那些骑电瓶车的在左转的时候会从我右边超过来再往左边撇过去,如果留的空间大还好,有些电瓶车就是如果车头超过了就不管他的车屁股,如果我不减速,自行车就被刮倒了,可能的确是别人就不是人,只要不把你撞倒就无所谓,反正为了你自己不被撞倒你肯定会让的。
底层的存储就是上一篇说的 mappingLookup 来存储信息
底层的存储就是上一篇说的 mappingLookup 来存储信息
springboot 的一个方便之处就是集成了 web server 进去,接着上一篇继续来看下这个 web server 的启动过程
基于 springboot 的 2.2.9.RELEASE 版本
整个 springboot 体系主体就是看 org.springframework.context.support.AbstractApplicationContext#refresh 刷新方法,
而启动 web server 的方法就是在其中的 OnRefresh
try {
+springboot web server 启动逻辑 | Nicksxs's Blog
springboot web server 启动逻辑
springboot 的一个方便之处就是集成了 web server 进去,接着上一篇继续来看下这个 web server 的启动过程
基于 springboot 的 2.2.9.RELEASE 版本
整个 springboot 体系主体就是看 org.springframework.context.support.AbstractApplicationContext#refresh 刷新方法,
而启动 web server 的方法就是在其中的 OnRefresh
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
@@ -166,4 +166,4 @@
this.server.addService(service);
return this.server;
}
- }
然后就是启动 server,后面可以继续看这个启动 TomcatServer 内部的逻辑
0%
\ No newline at end of file
+ }然后就是启动 server,后面可以继续看这个启动 TomcatServer 内部的逻辑
springboot 的一个方便之处就是集成了 web server 进去,接着上一篇继续来看下这个 web server 的启动过程
基于 springboot 的 2.2.9.RELEASE 版本
整个 springboot 体系主体就是看 org.springframework.context.support.AbstractApplicationContext#refresh 刷新方法,
而启动 web server 的方法就是在其中的 OnRefresh
try {
+Nicksxs's Blog - What hurts more, the pain of hard work or the pain of regret?
springboot web server 启动逻辑
springboot 的一个方便之处就是集成了 web server 进去,接着上一篇继续来看下这个 web server 的启动过程
基于 springboot 的 2.2.9.RELEASE 版本
整个 springboot 体系主体就是看 org.springframework.context.support.AbstractApplicationContext#refresh 刷新方法,
而启动 web server 的方法就是在其中的 OnRefresh
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
diff --git a/leancloud.memo b/leancloud.memo
index 4627c43b2d..223222395e 100644
--- a/leancloud.memo
+++ b/leancloud.memo
@@ -228,4 +228,5 @@
{"title":"java 中发起 http 请求时证书问题解决记录","url":"/2023/07/29/java-中发起-http-请求时证书问题解决记录/"},
{"title":"springboot 获取 web 应用中所有的接口 url","url":"/2023/08/06/springboot-获取-web-应用中所有的接口-url/"},
{"title":"springboot mappings 注册逻辑","url":"/2023/08/13/springboot-mappings-注册逻辑/"},
+{"title":"springboot web server 启动逻辑 - Java - SpringBoot","url":"/2023/08/20/springboot-web-server-启动逻辑/"},
]
\ No newline at end of file
diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json
index b5af2840ae..f93e22693b 100644
--- a/leancloud_counter_security_urls.json
+++ b/leancloud_counter_security_urls.json
@@ -1 +1 @@
-[{"title":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"2020 年终总结","url":"/2021/03/31/2020-年终总结/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"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 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Apollo 如何获取当前环境","url":"/2022/09/04/Apollo-如何获取当前环境/"},{"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":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"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":"2022 年终总结","url":"/2023/01/15/2022-年终总结/"},{"title":"Disruptor 系列三","url":"/2022/09/25/Disruptor-系列三/"},{"title":"Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"title":"Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析","url":"/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/"},{"title":"Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"title":"Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"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 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析","url":"/2022/09/11/Leetcode-1862-向下取整数对和-Sum-of-Floored-Pairs-Hard-题解分析/"},{"title":"Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"title":"Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"title":"Leetcode 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"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 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 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 42 接雨水 (Trapping Rain Water) 题解分析","url":"/2021/07/04/Leetcode-42-接雨水-Trapping-Rain-Water-题解分析/"},{"title":"Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"title":"Leetcode 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 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"Headscale初体验以及踩坑记","url":"/2023/01/22/Headscale初体验以及踩坑记/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析","url":"/2022/08/23/Leetcode-885-螺旋矩阵-III-Spiral-Matrix-III-Medium-题解分析/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"binary-watch","url":"/2016/09/29/binary-watch/"},{"title":"docker-mysql-cluster","url":"/2016/08/14/docker-mysql-cluster/"},{"title":"docker比一般多一点的初学者介绍三","url":"/2020/03/21/docker比一般多一点的初学者介绍三/"},{"title":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"docker比一般多一点的初学者介绍四","url":"/2022/12/25/docker比一般多一点的初学者介绍四/"},{"title":"docker比一般多一点的初学者介绍","url":"/2020/03/08/docker比一般多一点的初学者介绍/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"title":"dnsmasq的一个使用注意点","url":"/2023/04/16/dnsmasq的一个使用注意点/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"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":"C++ 指针使用中的一个小问题","url":"/2014/12/23/my-new-post/"},{"title":"minimum-size-subarray-sum-209","url":"/2016/10/11/minimum-size-subarray-sum-209/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"hexo 配置系列-接入Algolia搜索","url":"/2023/04/02/hexo-配置系列-接入Algolia搜索/"},{"title":"headscale 添加节点","url":"/2023/07/09/headscale-添加节点/"},{"title":"java 中发起 http 请求时证书问题解决记录","url":"/2023/07/29/java-中发起-http-请求时证书问题解决记录/"},{"title":"github 小技巧-更新 github host key","url":"/2023/03/28/github-小技巧-更新-github-host-key/"},{"title":"mybatis系列-connection连接池解析","url":"/2023/02/19/mybatis系列-connection连接池解析/"},{"title":"mybatis系列-sql 类的简单使用","url":"/2023/03/12/mybatis系列-sql-类的简单使用/"},{"title":"mybatis系列-foreach 解析","url":"/2023/06/11/mybatis系列-foreach-解析/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"mybatis系列-dataSource解析","url":"/2023/01/08/mybatis系列-dataSource解析/"},{"title":"mybatis系列-mybatis是如何初始化mapper的","url":"/2022/12/04/mybatis是如何初始化mapper的/"},{"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":"/2023/03/19/mybatis系列-sql-类的简要分析/"},{"title":"mybatis系列-第一条sql的细节","url":"/2022/12/11/mybatis系列-第一条sql的细节/"},{"title":"mybatis系列-第一条sql的更多细节","url":"/2022/12/18/mybatis系列-第一条sql的更多细节/"},{"title":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"redis数据结构介绍三-第三部分 整数集合","url":"/2020/01/10/redis数据结构介绍三/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"redis数据结构介绍-第一部分 SDS,链表,字典","url":"/2019/12/26/redis数据结构介绍/"},{"title":"redis数据结构介绍五-第五部分 对象","url":"/2020/01/20/redis数据结构介绍五/"},{"title":"mybatis系列-入门篇","url":"/2022/11/27/mybatis系列-入门篇/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"springboot mappings 注册逻辑","url":"/2023/08/13/springboot-mappings-注册逻辑/"},{"title":"powershell 初体验","url":"/2022/11/13/powershell-初体验/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"springboot 获取 web 应用中所有的接口 url","url":"/2023/08/06/springboot-获取-web-应用中所有的接口-url/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"springboot web server 启动逻辑 - Java - SpringBoot","url":"/2023/08/20/springboot-web-server-启动逻辑/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"spring boot中的 http 接口返回 json 形式的小注意点","url":"/2023/06/25/spring-boot中的-http-接口返回-json-形式的小注意点/"},{"title":"powershell 初体验二","url":"/2022/11/20/powershell-初体验二/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"win 下 vmware 虚拟机搭建黑裙 nas 的小思路","url":"/2023/06/04/win-下-vmware-虚拟机搭建黑裙-nas-的小思路/"},{"title":"ssh 小技巧-端口转发","url":"/2023/03/26/ssh-小技巧-端口转发/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"《寻羊历险记》读后感","url":"/2023/07/23/《寻羊历险记》读后感/"},{"title":"分享一次折腾老旧笔记本的体验-续续篇","url":"/2023/02/26/分享一次折腾老旧笔记本的体验-续续篇/"},{"title":"分享一次折腾老旧笔记本的体验","url":"/2023/02/05/分享一次折腾老旧笔记本的体验/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"nas 中使用 tmm 刮削视频","url":"/2023/07/02/使用-tmm-刮削视频/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"关于 npe 的一个小记忆点","url":"/2023/07/16/关于-npe-的一个小记忆点/"},{"title":"分享一次折腾老旧笔记本的体验-续篇","url":"/2023/02/12/分享一次折腾老旧笔记本的体验-续篇/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"在 wsl 2 中开启 ssh 连接","url":"/2023/04/23/在-wsl-2-中开启-ssh-连接/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"分享一次比较诡异的 Windows 下 U盘无法退出的经历","url":"/2023/01/29/分享一次比较诡异的-Windows-下-U盘无法退出的经历/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"小工周记一","url":"/2023/03/05/小工周记一/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"深度学习入门初认识","url":"/2023/04/30/深度学习入门初认识/"},{"title":"聊一下 RocketMQ 的顺序消息","url":"/2021/08/29/聊一下-RocketMQ-的顺序消息/"},{"title":"聊一下 RocketMQ 的消息存储四","url":"/2021/10/17/聊一下-RocketMQ-的消息存储四/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊一下 SpringBoot 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点","url":"/2021/09/19/聊一下-SpringBoot-中使用的-cglib-作为动态代理中的一个注意点/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊一下关于怎么陪伴学习","url":"/2022/11/06/聊一下关于怎么陪伴学习/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"记一个容器中 dubbo 注册的小知识点","url":"/2022/10/09/记一个容器中-dubbo-注册的小知识点/"},{"title":"记录一次折腾自组 nas 的失败经历-续续篇","url":"/2023/05/28/记录一次折腾自组-nas-的失败经历-续续篇/"},{"title":"记录一次折腾自组 nas 的失败经历-续篇","url":"/2023/05/14/记录一次折腾自组-nas-的失败经历-续篇/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"记录一次折腾自组 nas 的失败经历","url":"/2023/05/07/记录一次折腾自组-nas-的失败经历/"},{"title":"记录下 phpunit 的入门使用方法之setUp和tearDown","url":"/2022/10/23/记录下-phpunit-的入门使用方法之setUp和tearDown/"},{"title":"记录一次折腾自组 nas 的失败经历-续续续篇","url":"/2023/06/18/记录一次折腾自组-nas-的失败经历-续续续篇/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"解决 网络文件夹目前是以其他用户名和密码进行映射的 问题","url":"/2023/04/09/解决-网络文件夹目前是以其他用户名和密码进行映射的/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"记录下 phpunit 的入门使用方法","url":"/2022/10/16/记录下-phpunit-的入门使用方法/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"记录下 redis 的一些使用方法","url":"/2022/10/30/记录下-redis-的一些使用方法/"},{"title":"记录下把小米路由器 4A 千兆版刷成 openwrt 的过程","url":"/2023/05/21/记录下把小米路由器-4A-千兆版刷成-openwrt-的过程/"}]
\ No newline at end of file
+[{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"title":"2020 年终总结","url":"/2021/03/31/2020-年终总结/"},{"title":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"2022 年终总结","url":"/2023/01/15/2022-年终总结/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"Apollo 如何获取当前环境","url":"/2022/09/04/Apollo-如何获取当前环境/"},{"title":"Apollo 客户端启动过程分析","url":"/2022/09/18/Apollo-客户端启动过程分析/"},{"title":"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":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"Disruptor 系列三","url":"/2022/09/25/Disruptor-系列三/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"Disruptor 系列二","url":"/2022/02/27/Disruptor-系列二/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析","url":"/2021/10/07/Leetcode-021-合并两个有序链表-Merge-Two-Sorted-Lists-题解分析/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"JVM源码分析之G1垃圾收集器分析一","url":"/2019/12/07/JVM-G1-Part-1/"},{"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 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"title":"Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"title":"Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"title":"Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析","url":"/2022/07/22/Leetcode-1260-二维网格迁移-Shift-2D-Grid-Easy-题解分析/"},{"title":"Leetcode 155 最小栈(Min Stack) 题解分析","url":"/2020/12/06/Leetcode-155-最小栈-Min-Stack-题解分析/"},{"title":"Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析","url":"/2022/08/06/Leetcode-16-最接近的三数之和-3Sum-Closest-Medium-题解分析/"},{"title":"Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析","url":"/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/"},{"title":"Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"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 1862 向下取整数对和 ( Sum of Floored Pairs *Hard* ) 题解分析","url":"/2022/09/11/Leetcode-1862-向下取整数对和-Sum-of-Floored-Pairs-Hard-题解分析/"},{"title":"Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析","url":"/2022/03/07/Leetcode-349-两个数组的交集-Intersection-of-Two-Arrays-Easy-题解分析/"},{"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 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"title":"Leetcode 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 42 接雨水 (Trapping Rain Water) 题解分析","url":"/2021/07/04/Leetcode-42-接雨水-Trapping-Rain-Water-题解分析/"},{"title":"Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析","url":"/2022/06/19/Leetcode-698-划分为k个相等的子集-Partition-to-K-Equal-Sum-Subsets-Medium-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析","url":"/2022/08/23/Leetcode-885-螺旋矩阵-III-Spiral-Matrix-III-Medium-题解分析/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"Headscale初体验以及踩坑记","url":"/2023/01/22/Headscale初体验以及踩坑记/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析","url":"/2022/10/02/Leetcode-747-至少是其他数字两倍的最大数-Largest-Number-At-Least-Twice-of-Others-Easy-题解分析/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"ambari-summary","url":"/2017/05/09/ambari-summary/"},{"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":"two sum","url":"/2015/01/14/Two-Sum/"},{"title":"docker比一般多一点的初学者介绍四","url":"/2022/12/25/docker比一般多一点的初学者介绍四/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"headscale 添加节点","url":"/2023/07/09/headscale-添加节点/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"title":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"github 小技巧-更新 github host key","url":"/2023/03/28/github-小技巧-更新-github-host-key/"},{"title":"C++ 指针使用中的一个小问题","url":"/2014/12/23/my-new-post/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"minimum-size-subarray-sum-209","url":"/2016/10/11/minimum-size-subarray-sum-209/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"hexo 配置系列-接入Algolia搜索","url":"/2023/04/02/hexo-配置系列-接入Algolia搜索/"},{"title":"mybatis系列-dataSource解析","url":"/2023/01/08/mybatis系列-dataSource解析/"},{"title":"mybatis系列-sql 类的简单使用","url":"/2023/03/12/mybatis系列-sql-类的简单使用/"},{"title":"java 中发起 http 请求时证书问题解决记录","url":"/2023/07/29/java-中发起-http-请求时证书问题解决记录/"},{"title":"mybatis系列-sql 类的简要分析","url":"/2023/03/19/mybatis系列-sql-类的简要分析/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"title":"dnsmasq的一个使用注意点","url":"/2023/04/16/dnsmasq的一个使用注意点/"},{"title":"mybatis系列-mybatis是如何初始化mapper的","url":"/2022/12/04/mybatis是如何初始化mapper的/"},{"title":"mybatis系列-foreach 解析","url":"/2023/06/11/mybatis系列-foreach-解析/"},{"title":"mybatis系列-connection连接池解析","url":"/2023/02/19/mybatis系列-connection连接池解析/"},{"title":"mybatis系列-入门篇","url":"/2022/11/27/mybatis系列-入门篇/"},{"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":"mybatis系列-第一条sql的更多细节","url":"/2022/12/18/mybatis系列-第一条sql的更多细节/"},{"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":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"redis数据结构介绍-第一部分 SDS,链表,字典","url":"/2019/12/26/redis数据结构介绍/"},{"title":"redis数据结构介绍三-第三部分 整数集合","url":"/2020/01/10/redis数据结构介绍三/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"redis数据结构介绍五-第五部分 对象","url":"/2020/01/20/redis数据结构介绍五/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"mybatis系列-typeAliases系统","url":"/2023/01/01/mybatis系列-typeAliases系统/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"springboot mappings 注册逻辑","url":"/2023/08/13/springboot-mappings-注册逻辑/"},{"title":"powershell 初体验","url":"/2022/11/13/powershell-初体验/"},{"title":"springboot web server 启动逻辑","url":"/2023/08/20/springboot-web-server-启动逻辑/"},{"title":"powershell 初体验二","url":"/2022/11/20/powershell-初体验二/"},{"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":"win 下 vmware 虚拟机搭建黑裙 nas 的小思路","url":"/2023/06/04/win-下-vmware-虚拟机搭建黑裙-nas-的小思路/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"spring boot中的 http 接口返回 json 形式的小注意点","url":"/2023/06/25/spring-boot中的-http-接口返回-json-形式的小注意点/"},{"title":"springboot 获取 web 应用中所有的接口 url","url":"/2023/08/06/springboot-获取-web-应用中所有的接口-url/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"ssh 小技巧-端口转发","url":"/2023/03/26/ssh-小技巧-端口转发/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"《寻羊历险记》读后感","url":"/2023/07/23/《寻羊历险记》读后感/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"nas 中使用 tmm 刮削视频","url":"/2023/07/02/使用-tmm-刮削视频/"},{"title":"分享一次折腾老旧笔记本的体验","url":"/2023/02/05/分享一次折腾老旧笔记本的体验/"},{"title":"关于 npe 的一个小记忆点","url":"/2023/07/16/关于-npe-的一个小记忆点/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"分享一次折腾老旧笔记本的体验-续续篇","url":"/2023/02/26/分享一次折腾老旧笔记本的体验-续续篇/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"小工周记一","url":"/2023/03/05/小工周记一/"},{"title":"分享一次折腾老旧笔记本的体验-续篇","url":"/2023/02/12/分享一次折腾老旧笔记本的体验-续篇/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"分享一次比较诡异的 Windows 下 U盘无法退出的经历","url":"/2023/01/29/分享一次比较诡异的-Windows-下-U盘无法退出的经历/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"在 wsl 2 中开启 ssh 连接","url":"/2023/04/23/在-wsl-2-中开启-ssh-连接/"},{"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":"/2023/04/30/深度学习入门初认识/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊一下关于怎么陪伴学习","url":"/2022/11/06/聊一下关于怎么陪伴学习/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"记一个容器中 dubbo 注册的小知识点","url":"/2022/10/09/记一个容器中-dubbo-注册的小知识点/"},{"title":"记录一次折腾自组 nas 的失败经历-续篇","url":"/2023/05/14/记录一次折腾自组-nas-的失败经历-续篇/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"记录一次折腾自组 nas 的失败经历-续续篇","url":"/2023/05/28/记录一次折腾自组-nas-的失败经历-续续篇/"},{"title":"记录一次折腾自组 nas 的失败经历-续续续篇","url":"/2023/06/18/记录一次折腾自组-nas-的失败经历-续续续篇/"},{"title":"记录一次折腾自组 nas 的失败经历","url":"/2023/05/07/记录一次折腾自组-nas-的失败经历/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"记录下把小米路由器 4A 千兆版刷成 openwrt 的过程","url":"/2023/05/21/记录下把小米路由器-4A-千兆版刷成-openwrt-的过程/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"记录下 phpunit 的入门使用方法之setUp和tearDown","url":"/2022/10/23/记录下-phpunit-的入门使用方法之setUp和tearDown/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"记录下 redis 的一些使用方法","url":"/2022/10/30/记录下-redis-的一些使用方法/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"解决 网络文件夹目前是以其他用户名和密码进行映射的 问题","url":"/2023/04/09/解决-网络文件夹目前是以其他用户名和密码进行映射的/"},{"title":"记录下 phpunit 的入门使用方法","url":"/2022/10/16/记录下-phpunit-的入门使用方法/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"}]
\ No newline at end of file
diff --git a/search.xml b/search.xml
index 4f184ab27d..ec78d93937 100644
--- a/search.xml
+++ b/search.xml
@@ -1,25 +1,5 @@
-
- 村上春树《1Q84》读后感
- /2019/12/18/1Q84%E8%AF%BB%E5%90%8E%E6%84%9F/
- 看完了村上春树的《1Q84》,这应该是第五本看的他的书了,继 跑步,挪威的森林,刺杀骑士团长,海边的卡夫卡之后,不是其中最长的,好像是海边的卡夫卡还是刺杀骑士团长比较长一点,都是在微信读书上看的,比较方便,最开始在上面看的是高晓松的《鱼羊野史》,不知道为啥取这个名字,但是还是满吸引我的,不过由于去年的种种,没有很多心思把它看完,而且本身的组织形式就是比较松散的,看到哪算哪,其实一些野史部分是我比较喜欢,有些谈到人物的就不太有兴趣,而且类似于大祥哥吃的东西,反正都是哇,怎么这么好吃,嗯,太爱(niu)你(bi)了,高晓松就是这个人是我最喜欢的 xxx 家,我也没去细究过他有没有说重复过,反正是不太爱,后来因为这书还一度对战争史有了浓厚的兴趣,然而事实告诉我,大部头的战争史,其实正史我是真的啃不下去,我可能只对其中 10%的内容感兴趣,不过终于也在今年把它看完了,好像高晓松的晓说也最终季了,貌似其中讲朝鲜战争的还被和谐了,看样子是说出了一些故事(truth)。
-本来只是想把 《1Q84》的读后感写下,现在觉得还是把这篇当成我今年的读书总结吧,不过先从《1Q84》说起。
-严格来讲,这不是很书面化的读后感,可能我想写的也只是像聊天一样的说下我读过的书,包括的技术博客其实也是类似的,以后或许会转变,但是目前水平如此吧,写多了可能会变好,也可能不会。
-开始正文吧,这书有点类似于海边的卡夫卡,一开始是通过两条故事线,穿插着叙述,一条是青豆的,不算是个职业杀手的女杀手,要去解决一个经常家暴的斯文败类,穿着描述得比较性感吧,杀人方式是通过比较长的细针,从脖子后面一个精巧的位置插入,可以造成是未知原因死亡的假象,可能会推断成心梗之类的,这里有个前置的细节,就是青豆是乘坐一辆很高级的出租车,内饰什么的都非常有质感,有点不像一辆出租车,然后车里放了一首比较小众的歌,雅纳切克的《小交响曲》,但是青豆知道它,这跟后面的情节也有些许关系,这是女主人公青豆的出场;相应的男主的出场印象不是太深刻,男主叫天吾,是个不知名的作家,跟一个叫小松的编辑有比较好的关系,虽然天吾还没有拿到比较有分量的奖项,但是小松很看好他,也让他帮忙审校一个新作家奖的投稿文章,虽然天吾自身还没获得过这个奖,天吾还有个正式工作,是当数学老师,天吾在学生时代是个数学天才,但后面有对文学产生了兴趣,文学还不足以养活自己,靠着教课还是能保持温饱;
-接下来是正式故事的起点了,就是小松收到了一部小说投稿,名叫《空气蛹》,是个叫深绘里的女孩子投的稿,小松对他赋予了很高的评价,这里好像记岔了,好像是天吾对这部小说很有好感,但是小松比较怀疑,然后小松看了之后也有了浓厚的兴趣,这里就是开端了,小松想让天吾来重写润色这部《空气蛹》,因为故事本身很有分量,但是描写手法叙事方式等都很拙劣,而天吾正好擅长这个,小松对天吾的评价是,描写技巧无可挑剔,就是故事主体的火花还没际遇迸发,需要一个导火索,这个就可以类比我们程序员,很多比较初中级的程序员主要擅长在原来的代码上修修改改或者给他分配个小功能,比较高级的程序员就需要能做一些项目的架构设计,核心的技术方案设计,以前我也觉得写文档这个比较无聊,但是当一个项目真的比较庞大,复杂的时候,整体和核心部分的架构设计和方案还是需要有文档沉淀的,不然别人不知道没法接受,自己过段时间也会忘记。
-对于小松的这个建议,他的初衷是想搅一搅这个死气沉沉套路颇深的文坛,因为本身《空气蛹》这部小说的内容很吸引人,小松想通过天吾的润色补充让这部小说冲击新人奖,有种恶作剧的意图,天吾对此表示很多担心和顾虑,小松的这个建议其实也是一种文学作假,有两方面的担心,一方面是原作者深绘里是否同意如此操作,一方面是外界如果发现了这个事实会有什么样的后果,但是小松表示不用担心,前一步由小松牵线,让天吾跟原作者深绘里当面沟通这个代写是否被允许,结果当然是被允许了,这里有了对深绘里的初步描写,按我的理解是比较仙的感觉,然后语言沟通有些吃力,或者说有她自己的特色,当面沟通时貌似是让深绘里回去再考虑下,然后后面再由天吾去深绘里寄宿的戎野老师家沟通具体的细节。
-2019年12月18日23:37:19 更新
去到戎野老师家之后,天吾知道了关于深绘里的一些事情,深绘里的父亲与戎野老师应该是老友,深绘里的父亲在当初成立了一个叫”先驱”的公社,一个独立运行的社会组织,以运营农场作为物资来源,追求更为松散的共同体,即不过分激进地公有制,进行松散的共同生活,承认私有财产,简而言之就是这样一个能稳定存活下来的独立社会组织,但是随着稳定运行,内部的激进派和稳健派开始出现分歧,不可磨合,后来两派就分裂了,深绘里的父亲,深田保留在了稳健派,但是此时其实深田保内心是矛盾的,以为一开始其实是他倡导的独立革命才组织起了这群人,然而现在他又认清了现实社会已经不太相信能通过革命来独立的可能性,后来激进派便开始越加封闭,而且进行军事训练和思想教育,而后这个先驱的激进派别便有了新的名字”黎明”,深绘里也是在此时从先驱逃离来投靠戎野老师
暂时先写到这,未完待续~
-]]>
-
- 生活
- 读后感
- 村上春树
-
-
- 读后感
-
-
2019年终总结
/2020/02/01/2019%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
@@ -50,70 +30,67 @@
- 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 年终总结
+ /2021/03/31/2020-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
+ 拖更原因这篇年终总结本来应该在农历过完年就出来的,结果是对没有受疫情影响的春节放假时间空闲情况预估太良好,虽然公司调了几天假,但是因为春节期间疫情状况比较好,本来酒店都不让接待聚餐什么的,后来统统放开,结果就是从初一到初六每天要不就是去亲戚家,要不就是去酒店饭店吃饭,计划很丰满,现实很骨感,时间感觉一下就没了,然后年后感觉有点犯懒了,所以才拖到现在。
+生活-健身跑步
去年(19 年)的时候跑步突破了 300 公里,然后20 年给自己定了个 400 公里的目标,结果意料之中的没成功,原因可能疫情算一点吧,后面买了跑步机之后,基本周末回家都能跑一下,但是最后还是只跑了300 多公里,总的keep 记录跑量也没超过 1000 公里,所以跑步这个目标还是没成功的,不过还算是比去年多跑一点,这样也算后面好突破点,后面的目标就不定的太高了,每年能比前一年多一点就好,其实跑步已经从一种减肥方式变成一种习惯了,一周一次的跑步已经比较难有效减重了,但是对于保持精力和身体状态还是很有效和重要的,只是对于目前的体重还是要多减下去一些跑步才好,太重了对膝盖负担太大了,可惜还是时间呐,游泳骑车什么的都需要更苛刻的条件和时间,饮食呢控制起来比较难(贪吃
终于在 3 月底之前跑到了 1000 公里,迟了三个月,不过也总算达到了,只是体重控制还是不行,有试着走走楼梯,但是感觉对膝盖负担比较大,得再想想用什么方式
+![]()
+技术成长
一直提不起笔来写这篇年终总结还有个比较大的原因是觉得20 年的成长不如预期,大小目标都没怎么完成,比如深入了解 jvm,是想能有些深入的见解,而不再是某些点的比较片面的理解,系统性的归纳总结也比较少,每个方向或多或少有些看法和理解,但是不全面,一些东西看过了也会忘记,需要温故而知新,比如 AQS 的内容,第一次读其实理解比较浅,后面就强迫自己去读,去写,才有了一些比之前更深入的理解,因为很多文章都是带有作者思路的引导,适不适合自己都要看是否能从他的思路把它看懂,有些就差别很大,这个跟看书也一样,有些书大众一致推荐,一般情况下大多是经典的好的,但是也有可能是不太适合自己的,可能有时候机缘巧合看到的反而让人茅塞顿开,在 todo 里已经积攒了好多的点和面需要去学习实践,一方面是自己懒,一方面是时间也相对偏少,看看 21 年能不能有所提升,加强“时间管理”,哈哈
+技术上主要是看了 mysql 的 mvcc 相关内容,rocketmq 的,redis 的代码,还有 mybatis 等,其实每一个都能写很多,也有很多值得学习的,需要全面系统学习,之前想好好画一个思维导图,将整个技术体系都梳理下,还只做了一点点,方式也有点问题,应该从大到小,而不是深度优先,细节有很多,每一个方面都有自己比较熟悉擅长的,也有不太了解的,可以做一个评分,这个也是亟待改善的,希望今年能完成。
+博客
博客方面 20 年一年整是写了 53 篇,差不多是一周一篇的节奏,这个还是不错的,虽然博客质量参差不齐,但是这个更新频率还是比较好的,并且也定了个潜规则,可以一周技术一周生活,这样能缓解水文的频率,提高些技术文章的质量,虽然结果并没有好多少,不过感觉还是可以这么坚持的,能提高一些技术文章的质量那就更好了
]]>
生活
- 年中总结
+ 年终总结
+ 2020
+ 年终总结
2020
生活
+ 年终总结
2020
- 年中总结
+ 2021
+ 拖更
- 2021 年中总结
- /2021/07/18/2021-%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/
- 又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。
-这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。
-技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。
-另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。
-还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。
-下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。
+ 村上春树《1Q84》读后感
+ /2019/12/18/1Q84%E8%AF%BB%E5%90%8E%E6%84%9F/
+ 看完了村上春树的《1Q84》,这应该是第五本看的他的书了,继 跑步,挪威的森林,刺杀骑士团长,海边的卡夫卡之后,不是其中最长的,好像是海边的卡夫卡还是刺杀骑士团长比较长一点,都是在微信读书上看的,比较方便,最开始在上面看的是高晓松的《鱼羊野史》,不知道为啥取这个名字,但是还是满吸引我的,不过由于去年的种种,没有很多心思把它看完,而且本身的组织形式就是比较松散的,看到哪算哪,其实一些野史部分是我比较喜欢,有些谈到人物的就不太有兴趣,而且类似于大祥哥吃的东西,反正都是哇,怎么这么好吃,嗯,太爱(niu)你(bi)了,高晓松就是这个人是我最喜欢的 xxx 家,我也没去细究过他有没有说重复过,反正是不太爱,后来因为这书还一度对战争史有了浓厚的兴趣,然而事实告诉我,大部头的战争史,其实正史我是真的啃不下去,我可能只对其中 10%的内容感兴趣,不过终于也在今年把它看完了,好像高晓松的晓说也最终季了,貌似其中讲朝鲜战争的还被和谐了,看样子是说出了一些故事(truth)。
+本来只是想把 《1Q84》的读后感写下,现在觉得还是把这篇当成我今年的读书总结吧,不过先从《1Q84》说起。
+严格来讲,这不是很书面化的读后感,可能我想写的也只是像聊天一样的说下我读过的书,包括的技术博客其实也是类似的,以后或许会转变,但是目前水平如此吧,写多了可能会变好,也可能不会。
+开始正文吧,这书有点类似于海边的卡夫卡,一开始是通过两条故事线,穿插着叙述,一条是青豆的,不算是个职业杀手的女杀手,要去解决一个经常家暴的斯文败类,穿着描述得比较性感吧,杀人方式是通过比较长的细针,从脖子后面一个精巧的位置插入,可以造成是未知原因死亡的假象,可能会推断成心梗之类的,这里有个前置的细节,就是青豆是乘坐一辆很高级的出租车,内饰什么的都非常有质感,有点不像一辆出租车,然后车里放了一首比较小众的歌,雅纳切克的《小交响曲》,但是青豆知道它,这跟后面的情节也有些许关系,这是女主人公青豆的出场;相应的男主的出场印象不是太深刻,男主叫天吾,是个不知名的作家,跟一个叫小松的编辑有比较好的关系,虽然天吾还没有拿到比较有分量的奖项,但是小松很看好他,也让他帮忙审校一个新作家奖的投稿文章,虽然天吾自身还没获得过这个奖,天吾还有个正式工作,是当数学老师,天吾在学生时代是个数学天才,但后面有对文学产生了兴趣,文学还不足以养活自己,靠着教课还是能保持温饱;
+接下来是正式故事的起点了,就是小松收到了一部小说投稿,名叫《空气蛹》,是个叫深绘里的女孩子投的稿,小松对他赋予了很高的评价,这里好像记岔了,好像是天吾对这部小说很有好感,但是小松比较怀疑,然后小松看了之后也有了浓厚的兴趣,这里就是开端了,小松想让天吾来重写润色这部《空气蛹》,因为故事本身很有分量,但是描写手法叙事方式等都很拙劣,而天吾正好擅长这个,小松对天吾的评价是,描写技巧无可挑剔,就是故事主体的火花还没际遇迸发,需要一个导火索,这个就可以类比我们程序员,很多比较初中级的程序员主要擅长在原来的代码上修修改改或者给他分配个小功能,比较高级的程序员就需要能做一些项目的架构设计,核心的技术方案设计,以前我也觉得写文档这个比较无聊,但是当一个项目真的比较庞大,复杂的时候,整体和核心部分的架构设计和方案还是需要有文档沉淀的,不然别人不知道没法接受,自己过段时间也会忘记。
+对于小松的这个建议,他的初衷是想搅一搅这个死气沉沉套路颇深的文坛,因为本身《空气蛹》这部小说的内容很吸引人,小松想通过天吾的润色补充让这部小说冲击新人奖,有种恶作剧的意图,天吾对此表示很多担心和顾虑,小松的这个建议其实也是一种文学作假,有两方面的担心,一方面是原作者深绘里是否同意如此操作,一方面是外界如果发现了这个事实会有什么样的后果,但是小松表示不用担心,前一步由小松牵线,让天吾跟原作者深绘里当面沟通这个代写是否被允许,结果当然是被允许了,这里有了对深绘里的初步描写,按我的理解是比较仙的感觉,然后语言沟通有些吃力,或者说有她自己的特色,当面沟通时貌似是让深绘里回去再考虑下,然后后面再由天吾去深绘里寄宿的戎野老师家沟通具体的细节。
+2019年12月18日23:37:19 更新
去到戎野老师家之后,天吾知道了关于深绘里的一些事情,深绘里的父亲与戎野老师应该是老友,深绘里的父亲在当初成立了一个叫”先驱”的公社,一个独立运行的社会组织,以运营农场作为物资来源,追求更为松散的共同体,即不过分激进地公有制,进行松散的共同生活,承认私有财产,简而言之就是这样一个能稳定存活下来的独立社会组织,但是随着稳定运行,内部的激进派和稳健派开始出现分歧,不可磨合,后来两派就分裂了,深绘里的父亲,深田保留在了稳健派,但是此时其实深田保内心是矛盾的,以为一开始其实是他倡导的独立革命才组织起了这群人,然而现在他又认清了现实社会已经不太相信能通过革命来独立的可能性,后来激进派便开始越加封闭,而且进行军事训练和思想教育,而后这个先驱的激进派别便有了新的名字”黎明”,深绘里也是在此时从先驱逃离来投靠戎野老师
暂时先写到这,未完待续~
]]>
生活
- 年中总结
- 2021
+ 读后感
+ 村上春树
- 生活
- 2021
- 年中总结
- 技术
- 读书
+ 读后感
- 2020 年终总结
- /2021/03/31/2020-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
- 拖更原因这篇年终总结本来应该在农历过完年就出来的,结果是对没有受疫情影响的春节放假时间空闲情况预估太良好,虽然公司调了几天假,但是因为春节期间疫情状况比较好,本来酒店都不让接待聚餐什么的,后来统统放开,结果就是从初一到初六每天要不就是去亲戚家,要不就是去酒店饭店吃饭,计划很丰满,现实很骨感,时间感觉一下就没了,然后年后感觉有点犯懒了,所以才拖到现在。
-生活-健身跑步
去年(19 年)的时候跑步突破了 300 公里,然后20 年给自己定了个 400 公里的目标,结果意料之中的没成功,原因可能疫情算一点吧,后面买了跑步机之后,基本周末回家都能跑一下,但是最后还是只跑了300 多公里,总的keep 记录跑量也没超过 1000 公里,所以跑步这个目标还是没成功的,不过还算是比去年多跑一点,这样也算后面好突破点,后面的目标就不定的太高了,每年能比前一年多一点就好,其实跑步已经从一种减肥方式变成一种习惯了,一周一次的跑步已经比较难有效减重了,但是对于保持精力和身体状态还是很有效和重要的,只是对于目前的体重还是要多减下去一些跑步才好,太重了对膝盖负担太大了,可惜还是时间呐,游泳骑车什么的都需要更苛刻的条件和时间,饮食呢控制起来比较难(贪吃
终于在 3 月底之前跑到了 1000 公里,迟了三个月,不过也总算达到了,只是体重控制还是不行,有试着走走楼梯,但是感觉对膝盖负担比较大,得再想想用什么方式
-![]()
-技术成长
一直提不起笔来写这篇年终总结还有个比较大的原因是觉得20 年的成长不如预期,大小目标都没怎么完成,比如深入了解 jvm,是想能有些深入的见解,而不再是某些点的比较片面的理解,系统性的归纳总结也比较少,每个方向或多或少有些看法和理解,但是不全面,一些东西看过了也会忘记,需要温故而知新,比如 AQS 的内容,第一次读其实理解比较浅,后面就强迫自己去读,去写,才有了一些比之前更深入的理解,因为很多文章都是带有作者思路的引导,适不适合自己都要看是否能从他的思路把它看懂,有些就差别很大,这个跟看书也一样,有些书大众一致推荐,一般情况下大多是经典的好的,但是也有可能是不太适合自己的,可能有时候机缘巧合看到的反而让人茅塞顿开,在 todo 里已经积攒了好多的点和面需要去学习实践,一方面是自己懒,一方面是时间也相对偏少,看看 21 年能不能有所提升,加强“时间管理”,哈哈
-技术上主要是看了 mysql 的 mvcc 相关内容,rocketmq 的,redis 的代码,还有 mybatis 等,其实每一个都能写很多,也有很多值得学习的,需要全面系统学习,之前想好好画一个思维导图,将整个技术体系都梳理下,还只做了一点点,方式也有点问题,应该从大到小,而不是深度优先,细节有很多,每一个方面都有自己比较熟悉擅长的,也有不太了解的,可以做一个评分,这个也是亟待改善的,希望今年能完成。
-博客
博客方面 20 年一年整是写了 53 篇,差不多是一周一篇的节奏,这个还是不错的,虽然博客质量参差不齐,但是这个更新频率还是比较好的,并且也定了个潜规则,可以一周技术一周生活,这样能缓解水文的频率,提高些技术文章的质量,虽然结果并没有好多少,不过感觉还是可以这么坚持的,能提高一些技术文章的质量那就更好了
+ 2020年中总结
+ /2020/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
生活
- 年终总结
2020
- 2021
- 拖更
+ 年中总结
@@ -179,6 +156,22 @@ public:
c++
+
+ 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
+
+
AQS篇二 之 Condition 浅析笔记
/2021/02/21/AQS-%E4%B9%8B-Condition-%E6%B5%85%E6%9E%90%E7%AC%94%E8%AE%B0/
@@ -676,6 +669,72 @@ public:
unlock
+
+ AbstractQueuedSynchronizer
+ /2019/09/23/AbstractQueuedSynchronizer/
+ 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
首先 AQS 只要有这些属性
+// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
+private transient volatile Node head;
+
+// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
+private transient volatile Node tail;
+
+// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
+// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
+private volatile int state;
+
+// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
+// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
+// if (currentThread == getExclusiveOwnerThread()) {state++}
+private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
+大概了解了 aqs 底层的双向等待队列,
结构是这样的
![]()
每个 node 里面主要是的代码结构也比较简单
+static final class Node {
+ // 标识节点当前在共享模式下
+ static final Node SHARED = new Node();
+ // 标识节点当前在独占模式下
+ static final Node EXCLUSIVE = null;
+
+ // ======== 下面的几个int常量是给waitStatus用的 ===========
+ /** waitStatus value to indicate thread has cancelled */
+ // 代码此线程取消了争抢这个锁
+ static final int CANCELLED = 1;
+ /** waitStatus value to indicate successor's thread needs unparking */
+ // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
+ static final int SIGNAL = -1;
+ /** waitStatus value to indicate thread is waiting on condition */
+ // 本文不分析condition,所以略过吧,下一篇文章会介绍这个
+ static final int CONDITION = -2;
+ /**
+ * waitStatus value to indicate the next acquireShared should
+ * unconditionally propagate
+ */
+ // 同样的不分析,略过吧
+ static final int PROPAGATE = -3;
+ // =====================================================
+
+
+ // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
+ // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
+ // ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
+ volatile int waitStatus;
+ // 前驱节点的引用
+ volatile Node prev;
+ // 后继节点的引用
+ volatile Node next;
+ // 这个就是线程本尊
+ volatile Thread thread;
+
+}
+其实可以主要关注这个 waitStatus 因为这个是后面的节点给前面的节点设置的,等于-1 的时候代表后面有节点等待,需要去唤醒,
这里使用了一个变种的 CLH 队列实现,CLH 队列相关内容可以查看这篇 自旋锁、排队自旋锁、MCS锁、CLH锁
+]]>
+
+ java
+
+
+ java
+ aqs
+
+
AQS篇一
/2021/02/14/AQS%E7%AF%87%E4%B8%80/
@@ -1008,87 +1067,62 @@ public:
- AbstractQueuedSynchronizer
- /2019/09/23/AbstractQueuedSynchronizer/
- 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
首先 AQS 只要有这些属性
-// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
-private transient volatile Node head;
+ Apollo 如何获取当前环境
+ /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/
+ 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境
+public enum Env{
+ LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
-// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
-private transient volatile Node tail;
+ public static Env fromString(String env) {
+ Env environment = EnvUtils.transformEnv(env);
+ Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
+ return environment;
+ }
+}
+而这些解释
+/**
+ * Here is the brief description for all the predefined environments:
+ * <ul>
+ * <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
+ * <li>DEV: Development environment</li>
+ * <li>FWS: Feature Web Service Test environment</li>
+ * <li>FAT: Feature Acceptance Test environment</li>
+ * <li>UAT: User Acceptance Test environment</li>
+ * <li>LPT: Load and Performance Test environment</li>
+ * <li>PRO: Production environment</li>
+ * <li>TOOLS: Tooling environment, a special area in production environment which allows
+ * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
+ * </ul>
+ */
+那如果要在运行时知道 apollo 当前使用的环境可以用这个
+Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
+简单记录下。
+]]>
+
+ Java
+
+
+ Java
+ Apollo
+ environment
+
+
+
+ Apollo 客户端启动过程分析
+ /2022/09/18/Apollo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90/
+ 入口是可以在 springboot 的启动类上打上EnableApolloConfig 注解
+@Import(ApolloConfigRegistrar.class)
+public @interface EnableApolloConfig {
+这个 import 实现了
+public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
-// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
-// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
-private volatile int state;
+ private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);
-// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
-// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
-// if (currentThread == getExclusiveOwnerThread()) {state++}
-private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
-大概了解了 aqs 底层的双向等待队列,
结构是这样的
![]()
每个 node 里面主要是的代码结构也比较简单
-static final class Node {
- // 标识节点当前在共享模式下
- static final Node SHARED = new Node();
- // 标识节点当前在独占模式下
- static final Node EXCLUSIVE = null;
-
- // ======== 下面的几个int常量是给waitStatus用的 ===========
- /** waitStatus value to indicate thread has cancelled */
- // 代码此线程取消了争抢这个锁
- static final int CANCELLED = 1;
- /** waitStatus value to indicate successor's thread needs unparking */
- // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
- static final int SIGNAL = -1;
- /** waitStatus value to indicate thread is waiting on condition */
- // 本文不分析condition,所以略过吧,下一篇文章会介绍这个
- static final int CONDITION = -2;
- /**
- * waitStatus value to indicate the next acquireShared should
- * unconditionally propagate
- */
- // 同样的不分析,略过吧
- static final int PROPAGATE = -3;
- // =====================================================
-
-
- // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
- // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
- // ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
- volatile int waitStatus;
- // 前驱节点的引用
- volatile Node prev;
- // 后继节点的引用
- volatile Node next;
- // 这个就是线程本尊
- volatile Thread thread;
-
-}
-其实可以主要关注这个 waitStatus 因为这个是后面的节点给前面的节点设置的,等于-1 的时候代表后面有节点等待,需要去唤醒,
这里使用了一个变种的 CLH 队列实现,CLH 队列相关内容可以查看这篇 自旋锁、排队自旋锁、MCS锁、CLH锁
-]]>
-
- java
-
-
- java
- aqs
-
-
-
- Apollo 客户端启动过程分析
- /2022/09/18/Apollo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90/
- 入口是可以在 springboot 的启动类上打上EnableApolloConfig 注解
-@Import(ApolloConfigRegistrar.class)
-public @interface EnableApolloConfig {
-这个 import 实现了
-public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
-
- private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);
-
- @Override
- public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
- helper.registerBeanDefinitions(importingClassMetadata, registry);
- }
-}
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+ helper.registerBeanDefinitions(importingClassMetadata, registry);
+ }
+}
然后就调用了
com.ctrip.framework.apollo.spring.spi.DefaultApolloConfigRegistrarHelper#registerBeanDefinitions
@@ -1500,50 +1534,9 @@ public:
Java
Apollo
+ environment
value
注解
- environment
-
-
-
- Apollo 如何获取当前环境
- /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/
- 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境
-public enum Env{
- LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
-
- public static Env fromString(String env) {
- Env environment = EnvUtils.transformEnv(env);
- Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
- return environment;
- }
-}
-而这些解释
-/**
- * Here is the brief description for all the predefined environments:
- * <ul>
- * <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
- * <li>DEV: Development environment</li>
- * <li>FWS: Feature Web Service Test environment</li>
- * <li>FAT: Feature Acceptance Test environment</li>
- * <li>UAT: User Acceptance Test environment</li>
- * <li>LPT: Load and Performance Test environment</li>
- * <li>PRO: Production environment</li>
- * <li>TOOLS: Tooling environment, a special area in production environment which allows
- * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
- * </ul>
- */
-那如果要在运行时知道 apollo 当前使用的环境可以用这个
-Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
-简单记录下。
-]]>
-
- Java
-
-
- Java
- Apollo
- environment
@@ -1701,531 +1694,170 @@ 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;
-}
-
-class Value extends LhsPadding
-{
- protected volatile long value;
-}
-
-class RhsPadding extends Value
-{
- protected long p9, p10, p11, p12, p13, p14, p15;
-}
-
-/**
- * <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 关键字,而 LhsPadding 和 RhsPadding 分别在value 前后填充了各
7 个 long 型的变量,long 型的变量在 Java 中是占用 8 bytes,这样就相当于不管怎么样,
value 都会单独使用一个缓存行,使得其不会产生 false sharing 的问题。
+ 2021 年中总结
+ /2021/07/18/2021-%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/
+ 又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。
+这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。
+技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。
+另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。
+还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。
+下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。
]]>
- Java
+ 生活
+ 年中总结
+ 2021
- Java
- Disruptor
+ 生活
+ 2021
+ 年中总结
+ 技术
+ 读书
- 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;
+ 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();
-import java.io.IOException;
+ final Sequence[] processorSequences = new Sequence[eventHandlers.length];
+ final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);
-/**
- * Defines methods that all servlets must implement.
- *
- * <p>
- * A servlet is a small Java program that runs within a Web server. Servlets
- * receive and respond to requests from Web clients, usually across HTTP, the
- * HyperText Transfer Protocol.
- *
- * <p>
- * To implement this interface, you can write a generic servlet that extends
- * <code>javax.servlet.GenericServlet</code> or an HTTP servlet that extends
- * <code>javax.servlet.http.HttpServlet</code>.
- *
- * <p>
- * This interface defines methods to initialize a servlet, to service requests,
- * and to remove a servlet from the server. These are known as life-cycle
- * methods and are called in the following sequence:
- * <ol>
- * <li>The servlet is constructed, then initialized with the <code>init</code>
- * method.
- * <li>Any calls from clients to the <code>service</code> method are handled.
- * <li>The servlet is taken out of service, then destroyed with the
- * <code>destroy</code> method, then garbage collected and finalized.
- * </ol>
- *
- * <p>
- * In addition to the life-cycle methods, this interface provides the
- * <code>getServletConfig</code> method, which the servlet can use to get any
- * startup information, and the <code>getServletInfo</code> method, which allows
- * the servlet to return basic information about itself, such as author,
- * version, and copyright.
- *
- * @see GenericServlet
- * @see javax.servlet.http.HttpServlet
- */
-public interface Servlet {
+ for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
+ {
+ final EventHandler<? super T> eventHandler = eventHandlers[i];
- /**
- * Called by the servlet container to indicate to a servlet that the servlet
- * is being placed into service.
- *
- * <p>
- * The servlet container calls the <code>init</code> method exactly once
- * after instantiating the servlet. The <code>init</code> method must
- * complete successfully before the servlet can receive any requests.
- *
- * <p>
- * The servlet container cannot place the servlet into service if the
- * <code>init</code> method
- * <ol>
- * <li>Throws a <code>ServletException</code>
- * <li>Does not return within a time period defined by the Web server
- * </ol>
- *
- *
- * @param config
- * a <code>ServletConfig</code> object containing the servlet's
- * configuration and initialization parameters
- *
- * @exception ServletException
- * if an exception has occurred that interferes with the
- * servlet's normal operation
- *
- * @see UnavailableException
- * @see #getServletConfig
- */
- public void init(ServletConfig config) throws ServletException;
+ // 这里将 handler 包装成一个 BatchEventProcessor
+ final BatchEventProcessor<T> batchEventProcessor =
+ new BatchEventProcessor<>(ringBuffer, barrier, eventHandler);
- /**
- *
- * Returns a {@link ServletConfig} object, which contains initialization and
- * startup parameters for this servlet. The <code>ServletConfig</code>
- * object returned is the one passed to the <code>init</code> method.
- *
- * <p>
- * Implementations of this interface are responsible for storing the
- * <code>ServletConfig</code> object so that this method can return it. The
- * {@link GenericServlet} class, which implements this interface, already
- * does this.
- *
- * @return the <code>ServletConfig</code> object that initializes this
- * servlet
- *
- * @see #init
- */
- public ServletConfig getServletConfig();
+ if (exceptionHandler != null)
+ {
+ batchEventProcessor.setExceptionHandler(exceptionHandler);
+ }
- /**
- * Called by the servlet container to allow the servlet to respond to a
- * request.
- *
- * <p>
- * This method is only called after the servlet's <code>init()</code> method
- * has completed successfully.
- *
- * <p>
- * The status code of the response always should be set for a servlet that
- * throws or sends an error.
- *
- *
- * <p>
- * Servlets typically run inside multithreaded servlet containers that can
- * handle multiple requests concurrently. Developers must be aware to
- * synchronize access to any shared resources such as files, network
- * connections, and as well as the servlet's class and instance variables.
- * More information on multithreaded programming in Java is available in <a
- * href
- * ="http://java.sun.com/Series/Tutorial/java/threads/multithreaded.html">
- * the Java tutorial on multi-threaded programming</a>.
- *
- *
- * @param req
- * the <code>ServletRequest</code> object that contains the
- * client's request
- *
- * @param res
- * the <code>ServletResponse</code> object that contains the
- * servlet's response
- *
- * @exception ServletException
- * if an exception occurs that interferes with the servlet's
- * normal operation
- *
- * @exception IOException
- * if an input or output exception occurs
- */
- public void service(ServletRequest req, ServletResponse res)
- throws ServletException, IOException;
-
- /**
- * Returns information about the servlet, such as author, version, and
- * copyright.
- *
- * <p>
- * The string that this method returns should be plain text and not markup
- * of any kind (such as HTML, XML, etc.).
- *
- * @return a <code>String</code> containing servlet information
- */
- public String getServletInfo();
+ consumerRepository.add(batchEventProcessor, eventHandler, barrier);
+ processorSequences[i] = batchEventProcessor.getSequence();
+ }
- /**
- * Called by the servlet container to indicate to a servlet that the servlet
- * is being taken out of service. This method is only called once all
- * threads within the servlet's <code>service</code> method have exited or
- * after a timeout period has passed. After the servlet container calls this
- * method, it will not call the <code>service</code> method again on this
- * servlet.
- *
- * <p>
- * This method gives the servlet an opportunity to clean up any resources
- * that are being held (for example, memory, file handles, threads) and make
- * sure that any persistent state is synchronized with the servlet's current
- * state in memory.
- */
- public void destroy();
-}
-
-重点看 servlet 的 service方法,就是接受请求,处理完了给响应,不说细节,不然光 Tomcat 的能说半年,所以呢再进一步去理解,其实就能知道,就是一个先后的问题,盗个图
![]()
filter 跟后两者最大的不一样其实是一个基于 servlet,在非常外层做的处理,然后是 interceptor 的 prehandle 跟 posthandle,接着才是我们常规的 aop,就这么点事情,做个小试验吧(还是先补段代码吧)
-Filter
// ---------------------------------------------------- FilterChain Methods
+ updateGatingSequencesForNextInChain(barrierSequences, processorSequences);
- /**
- * Invoke the next filter in this chain, passing the specified request
- * and response. If there are no more filters in this chain, invoke
- * the <code>service()</code> method of the servlet itself.
- *
- * @param request The servlet request we are processing
- * @param response The servlet response we are creating
- *
- * @exception IOException if an input/output error occurs
- * @exception ServletException if a servlet exception occurs
- */
- @Override
- public void doFilter(ServletRequest request, ServletResponse response)
- throws IOException, ServletException {
+ return new EventHandlerGroup<>(this, consumerRepository, processorSequences);
+ }
- if( Globals.IS_SECURITY_ENABLED ) {
- final ServletRequest req = request;
- final ServletResponse res = response;
- try {
- java.security.AccessController.doPrivileged(
- new java.security.PrivilegedExceptionAction<Void>() {
- @Override
- public Void run()
- throws ServletException, IOException {
- internalDoFilter(req,res);
- return null;
- }
- }
- );
- } catch( PrivilegedActionException pe) {
- Exception e = pe.getException();
- if (e instanceof ServletException)
- throw (ServletException) e;
- else if (e instanceof IOException)
- throw (IOException) e;
- else if (e instanceof RuntimeException)
- throw (RuntimeException) e;
- else
- throw new ServletException(e.getMessage(), e);
+在 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);
}
- } else {
- internalDoFilter(request,response);
+ consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
}
- }
- private void internalDoFilter(ServletRequest request,
- ServletResponse response)
- throws IOException, ServletException {
-
- // Call the next filter if there is one
- if (pos < n) {
- ApplicationFilterConfig filterConfig = filters[pos++];
- try {
- Filter filter = filterConfig.getFilter();
+ }
- if (request.isAsyncSupported() && "false".equalsIgnoreCase(
- filterConfig.getFilterDef().getAsyncSupported())) {
- request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
- }
- if( Globals.IS_SECURITY_ENABLED ) {
- final ServletRequest req = request;
- final ServletResponse res = response;
- Principal principal =
- ((HttpServletRequest) req).getUserPrincipal();
+而如何更新则是在处理器 com.lmax.disruptor.BatchEventProcessor#run 中
+public void run()
+ {
+ if (running.compareAndSet(IDLE, RUNNING))
+ {
+ sequenceBarrier.clearAlert();
- Object[] args = new Object[]{req, res, this};
- SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
- } else {
- filter.doFilter(request, response, this);
+ notifyStart();
+ try
+ {
+ if (running.get() == RUNNING)
+ {
+ processEvents();
}
- } catch (IOException | ServletException | RuntimeException e) {
- throw e;
- } catch (Throwable e) {
- e = ExceptionUtils.unwrapInvocationTargetException(e);
- ExceptionUtils.handleThrowable(e);
- throw new ServletException(sm.getString("filterChain.filter"), e);
- }
- return;
- }
-
- // We fell off the end of the chain -- call the servlet instance
- try {
- if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
- lastServicedRequest.set(request);
- lastServicedResponse.set(response);
}
-
- if (request.isAsyncSupported() && !servletSupportsAsync) {
- request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
- Boolean.FALSE);
+ finally
+ {
+ notifyShutdown();
+ running.set(IDLE);
}
- // Use potentially wrapped request from this point
- if ((request instanceof HttpServletRequest) &&
- (response instanceof HttpServletResponse) &&
- Globals.IS_SECURITY_ENABLED ) {
- final ServletRequest req = request;
- final ServletResponse res = response;
- Principal principal =
- ((HttpServletRequest) req).getUserPrincipal();
- Object[] args = new Object[]{req, res};
- SecurityUtil.doAsPrivilege("service",
- servlet,
- classTypeUsedInService,
- args,
- principal);
- } else {
- servlet.service(request, response);
+ }
+ 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");
}
- } catch (IOException | ServletException | RuntimeException e) {
- throw e;
- } catch (Throwable e) {
- e = ExceptionUtils.unwrapInvocationTargetException(e);
- ExceptionUtils.handleThrowable(e);
- throw new ServletException(sm.getString("filterChain.servlet"), e);
- } finally {
- if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
- lastServicedRequest.set(null);
- lastServicedResponse.set(null);
+ else
+ {
+ earlyExit();
}
}
- }
-注意看这一行
filter.doFilter(request, response, this);
是不是看懂了,就是个 filter 链,但是这个代码在哪呢,org.apache.catalina.core.ApplicationFilterChain#doFilter
然后是interceptor,
-protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
- HttpServletRequest processedRequest = request;
- HandlerExecutionChain mappedHandler = null;
- boolean multipartRequestParsed = false;
- WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+ }
+然后是
+private void processEvents()
+ {
+ T event = null;
+ long nextSequence = sequence.get() + 1L;
- try {
- try {
- ModelAndView mv = null;
- Object dispatchException = null;
+ while (true)
+ {
+ try
+ {
+ final long availableSequence = sequenceBarrier.waitFor(nextSequence);
+ if (batchStartAware != null)
+ {
+ batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
+ }
- try {
- processedRequest = this.checkMultipart(request);
- multipartRequestParsed = processedRequest != request;
- mappedHandler = this.getHandler(processedRequest);
- if (mappedHandler == null) {
- this.noHandlerFound(processedRequest, response);
- return;
- }
-
- HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
- String method = request.getMethod();
- boolean isGet = "GET".equals(method);
- if (isGet || "HEAD".equals(method)) {
- long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
- if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
- return;
- }
- }
-
- /**
- * 看这里看这里‼️
- */
- if (!mappedHandler.applyPreHandle(processedRequest, response)) {
- return;
- }
-
- mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- if (asyncManager.isConcurrentHandlingStarted()) {
- return;
- }
-
- this.applyDefaultViewName(processedRequest, mv);
- /**
- * 再看这里看这里‼️
- */
- mappedHandler.applyPostHandle(processedRequest, response, mv);
- } catch (Exception var20) {
- dispatchException = var20;
- } catch (Throwable var21) {
- dispatchException = new NestedServletException("Handler dispatch failed", var21);
+ while (nextSequence <= availableSequence)
+ {
+ event = dataProvider.get(nextSequence);
+ eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
+ nextSequence++;
}
-
- this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
- } catch (Exception var22) {
- this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
- } catch (Throwable var23) {
- this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
+ // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
+ sequence.set(availableSequence);
}
-
- } finally {
- if (asyncManager.isConcurrentHandlingStarted()) {
- if (mappedHandler != null) {
- mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
+ catch (final TimeoutException e)
+ {
+ notifyTimeout(sequence.get());
+ }
+ catch (final AlertException ex)
+ {
+ if (running.get() != RUNNING)
+ {
+ break;
}
- } else if (multipartRequestParsed) {
- this.cleanupMultipart(processedRequest);
}
-
+ catch (final Throwable ex)
+ {
+ handleEventException(ex, nextSequence, event);
+ // 如果是异常就只是 nextSequence
+ sequence.set(nextSequence);
+ nextSequence++;
+ }
}
- }
-代码在哪呢,org.springframework.web.servlet.DispatcherServlet#doDispatch,然后才是我们自己写的 aop,是不是差不多明白了,嗯,接下来是例子
写个 filter
-public class DemoFilter extends HttpServlet implements Filter {
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- System.out.println("==>DemoFilter启动");
- }
-
- @Override
- public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
- // 将请求转换成HttpServletRequest 请求
- HttpServletRequest req = (HttpServletRequest) servletRequest;
- HttpServletResponse resp = (HttpServletResponse) servletResponse;
- System.out.println("before filter");
- filterChain.doFilter(req, resp);
- System.out.println("after filter");
- }
-
- @Override
- public void destroy() {
-
- }
-}
-因为用的springboot,所以就不写 web.xml 了,写个配置类
-@Configuration
-public class FilterConfiguration {
- @Bean
- public FilterRegistrationBean filterDemo4Registration() {
- FilterRegistrationBean registration = new FilterRegistrationBean();
- //注入过滤器
- registration.setFilter(new DemoFilter());
- //拦截规则
- registration.addUrlPatterns("/*");
- //过滤器名称
- registration.setName("DemoFilter");
- //是否自动注册 false 取消Filter的自动注册
- registration.setEnabled(true);
- //过滤器顺序
- registration.setOrder(1);
- return registration;
- }
-
-}
-然后再来个 interceptor 和 aop,以及一个简单的请求处理
-public class DemoInterceptor extends HandlerInterceptorAdapter {
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- System.out.println("preHandle test");
- return true;
- }
-
- @Override
- public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
- System.out.println("postHandle test");
- }
-}
-@Aspect
-@Component
-public class DemoAspect {
-
- @Pointcut("execution( public * com.nicksxs.springbootdemo.demo.DemoController.*())")
- public void point() {
-
- }
-
- @Before("point()")
- public void doBefore(){
- System.out.println("==doBefore==");
- }
-
- @After("point()")
- public void doAfter(){
- System.out.println("==doAfter==");
- }
-}
-@RestController
-public class DemoController {
-
- @RequestMapping("/hello")
- @ResponseBody
- public String hello() {
- return "hello world";
- }
-}
-好了,请求一下,看看 stdout,
![]()
搞定完事儿~
-]]>
-
- Java
- Filter
- Interceptor - AOP
- Spring
- Servlet
- Interceptor
- AOP
-
-
- Java
- Filter
- Interceptor
- AOP
- Spring
- Tomcat
- Servlet
- 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
- 负载均衡
+ Disruptor
@@ -2318,412 +1950,1012 @@ Node *clone(Node *graph) {
- G1收集器概述
- /2020/02/09/G1%E6%94%B6%E9%9B%86%E5%99%A8%E6%A6%82%E8%BF%B0/
- G1: The Garbage-First Collector, 垃圾回收优先的垃圾回收器,目标是用户多核 cpu 和大内存的机器,最大的特点就是可预测的停顿时间,官方给出的介绍是提供一个用户在大的堆内存情况下一个低延迟表现的解决方案,通常是 6GB 及以上的堆大小,有低于 0.5 秒稳定的可预测的停顿时间。
-这里主要介绍这个比较新的垃圾回收器,在 G1 之前的垃圾回收器都是基于如下图的内存结构分布,有新生代,老年代和永久代(jdk8 之前),然后G1 往前的那些垃圾回收器都有个分代,比如 serial,parallel 等,一般有个应用的组合,最初的 serial 和 serial old,因为新生代和老年代的收集方式不太一样,新生代主要是标记复制,所以有 eden 跟两个 survival区,老年代一般用标记整理方式,而 G1 对这个不太一样。
![]()
看一下 G1 的内存分布
![]()
可以看到这有很大的不同,G1 通过将内存分成大小相等的 region,每个region是存在于一个连续的虚拟内存范围,对于某个 region 来说其角色是类似于原来的收集器的Eden、Survivor、Old Generation,这个具体在代码层面
-// We encode the value of the heap region type so the generation can be
- // determined quickly. The tag is split into two parts:
- //
- // major type (young, old, humongous, archive) : top N-1 bits
- // minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
- //
- // If there's need to increase the number of minor types in the
- // future, we'll have to increase the size of the latter and hence
- // decrease the size of the former.
- //
- // 00000 0 [ 0] Free
- //
- // 00001 0 [ 2] Young Mask
- // 00001 0 [ 2] Eden
- // 00001 1 [ 3] Survivor
- //
- // 00010 0 [ 4] Humongous Mask
- // 00100 0 [ 8] Pinned Mask
- // 00110 0 [12] Starts Humongous
- // 00110 1 [13] Continues Humongous
- //
- // 01000 0 [16] Old Mask
- //
- // 10000 0 [32] Archive Mask
- // 11100 0 [56] Open Archive
- // 11100 1 [57] Closed Archive
- //
- typedef enum {
- FreeTag = 0,
-
- YoungMask = 2,
- EdenTag = YoungMask,
- SurvTag = YoungMask + 1,
+ 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;
+}
- HumongousMask = 4,
- PinnedMask = 8,
- StartsHumongousTag = HumongousMask | PinnedMask,
- ContinuesHumongousTag = HumongousMask | PinnedMask + 1,
+class Value extends LhsPadding
+{
+ protected volatile long value;
+}
- OldMask = 16,
- OldTag = OldMask,
+class RhsPadding extends Value
+{
+ protected long p9, p10, p11, p12, p13, p14, p15;
+}
- // Archive regions are regions with immutable content (i.e. not reclaimed, and
- // not allocated into during regular operation). They differ in the kind of references
- // allowed for the contained objects:
- // - Closed archive regions form a separate self-contained (closed) object graph
- // within the set of all of these regions. No references outside of closed
- // archive regions are allowed.
- // - Open archive regions have no restrictions on the references of their objects.
- // Objects within these regions are allowed to have references to objects
- // contained in any other kind of regions.
- ArchiveMask = 32,
- OpenArchiveTag = ArchiveMask | PinnedMask | OldMask,
- ClosedArchiveTag = ArchiveMask | PinnedMask | OldMask + 1
- } Tag;
-
-hotspot/share/gc/g1/heapRegionType.hpp
-当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。
-由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。
-需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。
-注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。
-在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。
+/**
+ * <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 关键字,而 LhsPadding 和 RhsPadding 分别在value 前后填充了各
7 个 long 型的变量,long 型的变量在 Java 中是占用 8 bytes,这样就相当于不管怎么样,
value 都会单独使用一个缓存行,使得其不会产生 false sharing 的问题。
]]>
Java
- JVM
- GC
- C++
Java
- JVM
- C++
- G1
- GC
- Garbage-First Collector
+ Disruptor
- 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);
+ 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;
- 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;
+import java.io.IOException;
- 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");
+/**
+ * Defines methods that all servlets must implement.
+ *
+ * <p>
+ * A servlet is a small Java program that runs within a Web server. Servlets
+ * receive and respond to requests from Web clients, usually across HTTP, the
+ * HyperText Transfer Protocol.
+ *
+ * <p>
+ * To implement this interface, you can write a generic servlet that extends
+ * <code>javax.servlet.GenericServlet</code> or an HTTP servlet that extends
+ * <code>javax.servlet.http.HttpServlet</code>.
+ *
+ * <p>
+ * This interface defines methods to initialize a servlet, to service requests,
+ * and to remove a servlet from the server. These are known as life-cycle
+ * methods and are called in the following sequence:
+ * <ol>
+ * <li>The servlet is constructed, then initialized with the <code>init</code>
+ * method.
+ * <li>Any calls from clients to the <code>service</code> method are handled.
+ * <li>The servlet is taken out of service, then destroyed with the
+ * <code>destroy</code> method, then garbage collected and finalized.
+ * </ol>
+ *
+ * <p>
+ * In addition to the life-cycle methods, this interface provides the
+ * <code>getServletConfig</code> method, which the servlet can use to get any
+ * startup information, and the <code>getServletInfo</code> method, which allows
+ * the servlet to return basic information about itself, such as author,
+ * version, and copyright.
+ *
+ * @see GenericServlet
+ * @see javax.servlet.http.HttpServlet
+ */
+public interface Servlet {
- 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;
- }
- }
+ /**
+ * Called by the servlet container to indicate to a servlet that the servlet
+ * is being placed into service.
+ *
+ * <p>
+ * The servlet container calls the <code>init</code> method exactly once
+ * after instantiating the servlet. The <code>init</code> method must
+ * complete successfully before the servlet can receive any requests.
+ *
+ * <p>
+ * The servlet container cannot place the servlet into service if the
+ * <code>init</code> method
+ * <ol>
+ * <li>Throws a <code>ServletException</code>
+ * <li>Does not return within a time period defined by the Web server
+ * </ol>
+ *
+ *
+ * @param config
+ * a <code>ServletConfig</code> object containing the servlet's
+ * configuration and initialization parameters
+ *
+ * @exception ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation
+ *
+ * @see UnavailableException
+ * @see #getServletConfig
+ */
+ public void init(ServletConfig config) throws ServletException;
- 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();
+ /**
+ *
+ * Returns a {@link ServletConfig} object, which contains initialization and
+ * startup parameters for this servlet. The <code>ServletConfig</code>
+ * object returned is the one passed to the <code>init</code> method.
+ *
+ * <p>
+ * Implementations of this interface are responsible for storing the
+ * <code>ServletConfig</code> object so that this method can return it. The
+ * {@link GenericServlet} class, which implements this interface, already
+ * does this.
+ *
+ * @return the <code>ServletConfig</code> object that initializes this
+ * servlet
+ *
+ * @see #init
+ */
+ public ServletConfig getServletConfig();
- // 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);
+ /**
+ * Called by the servlet container to allow the servlet to respond to a
+ * request.
+ *
+ * <p>
+ * This method is only called after the servlet's <code>init()</code> method
+ * has completed successfully.
+ *
+ * <p>
+ * The status code of the response always should be set for a servlet that
+ * throws or sends an error.
+ *
+ *
+ * <p>
+ * Servlets typically run inside multithreaded servlet containers that can
+ * handle multiple requests concurrently. Developers must be aware to
+ * synchronize access to any shared resources such as files, network
+ * connections, and as well as the servlet's class and instance variables.
+ * More information on multithreaded programming in Java is available in <a
+ * href
+ * ="http://java.sun.com/Series/Tutorial/java/threads/multithreaded.html">
+ * the Java tutorial on multi-threaded programming</a>.
+ *
+ *
+ * @param req
+ * the <code>ServletRequest</code> object that contains the
+ * client's request
+ *
+ * @param res
+ * the <code>ServletResponse</code> object that contains the
+ * servlet's response
+ *
+ * @exception ServletException
+ * if an exception occurs that interferes with the servlet's
+ * normal operation
+ *
+ * @exception IOException
+ * if an input or output exception occurs
+ */
+ public void service(ServletRequest req, ServletResponse res)
+ throws ServletException, IOException;
- // The above routine returns true if we were able to force the
- // next GC pause to be an initial mark; it returns false if a
- // marking cycle is already in progress.
- //
- // If a marking cycle is already in progress just return and skip the
- // pause below - if the reason for requesting this initial mark pause
- // was due to a System.gc() then the requesting thread should block in
- // doit_epilogue() until the marking cycle is complete.
- //
- // If this initial mark pause was requested as part of a humongous
- // allocation then we know that the marking cycle must just have
- // been started by another thread (possibly also allocating a humongous
- // object) as there was no active marking cycle when the requesting
- // thread checked before calling collect() in
- // attempt_allocation_humongous(). Retrying the GC, in this case,
- // will cause the requesting thread to spin inside collect() until the
- // just started marking cycle is complete - which may be a while. So
- // we do NOT retry the GC.
- if (!res) {
- assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating");
- if (_gc_cause != GCCause::_g1_humongous_allocation) {
- _should_retry_gc = true;
- }
- return;
- }
- }
-
- // Try a partial collection of some kind.
- _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
-
- if (_pause_succeeded) {
- if (_word_size > 0) {
- // An allocation had been requested. Do it, eventually trying a stronger
- // kind of GC.
- _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded);
- } else {
- 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");
+ /**
+ * Returns information about the servlet, such as author, version, and
+ * copyright.
+ *
+ * <p>
+ * The string that this method returns should be plain text and not markup
+ * of any kind (such as HTML, XML, etc.).
+ *
+ * @return a <code>String</code> containing servlet information
+ */
+ public String getServletInfo();
- if (GCLocker::check_active_before_gc()) {
- return false;
- }
+ /**
+ * Called by the servlet container to indicate to a servlet that the servlet
+ * is being taken out of service. This method is only called once all
+ * threads within the servlet's <code>service</code> method have exited or
+ * after a timeout period has passed. After the servlet container calls this
+ * method, it will not call the <code>service</code> method again on this
+ * servlet.
+ *
+ * <p>
+ * This method gives the servlet an opportunity to clean up any resources
+ * that are being held (for example, memory, file handles, threads) and make
+ * sure that any persistent state is synchronized with the servlet's current
+ * state in memory.
+ */
+ public void destroy();
+}
+
+重点看 servlet 的 service方法,就是接受请求,处理完了给响应,不说细节,不然光 Tomcat 的能说半年,所以呢再进一步去理解,其实就能知道,就是一个先后的问题,盗个图
![]()
filter 跟后两者最大的不一样其实是一个基于 servlet,在非常外层做的处理,然后是 interceptor 的 prehandle 跟 posthandle,接着才是我们常规的 aop,就这么点事情,做个小试验吧(还是先补段代码吧)
+Filter
// ---------------------------------------------------- FilterChain Methods
- _gc_timer_stw->register_gc_start();
+ /**
+ * Invoke the next filter in this chain, passing the specified request
+ * and response. If there are no more filters in this chain, invoke
+ * the <code>service()</code> method of the servlet itself.
+ *
+ * @param request The servlet request we are processing
+ * @param response The servlet response we are creating
+ *
+ * @exception IOException if an input/output error occurs
+ * @exception ServletException if a servlet exception occurs
+ */
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response)
+ throws IOException, ServletException {
- GCIdMark gc_id_mark;
- _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
+ if( Globals.IS_SECURITY_ENABLED ) {
+ final ServletRequest req = request;
+ final ServletResponse res = response;
+ try {
+ java.security.AccessController.doPrivileged(
+ new java.security.PrivilegedExceptionAction<Void>() {
+ @Override
+ public Void run()
+ throws ServletException, IOException {
+ internalDoFilter(req,res);
+ return null;
+ }
+ }
+ );
+ } catch( PrivilegedActionException pe) {
+ Exception e = pe.getException();
+ if (e instanceof ServletException)
+ throw (ServletException) e;
+ else if (e instanceof IOException)
+ throw (IOException) e;
+ else if (e instanceof RuntimeException)
+ throw (RuntimeException) e;
+ else
+ throw new ServletException(e.getMessage(), e);
+ }
+ } else {
+ internalDoFilter(request,response);
+ }
+ }
+ private void internalDoFilter(ServletRequest request,
+ ServletResponse response)
+ throws IOException, ServletException {
- SvcGCMarker sgcm(SvcGCMarker::MINOR);
- ResourceMark rm;
+ // Call the next filter if there is one
+ if (pos < n) {
+ ApplicationFilterConfig filterConfig = filters[pos++];
+ try {
+ Filter filter = filterConfig.getFilter();
- g1_policy()->note_gc_start();
+ if (request.isAsyncSupported() && "false".equalsIgnoreCase(
+ filterConfig.getFilterDef().getAsyncSupported())) {
+ request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
+ }
+ if( Globals.IS_SECURITY_ENABLED ) {
+ final ServletRequest req = request;
+ final ServletResponse res = response;
+ Principal principal =
+ ((HttpServletRequest) req).getUserPrincipal();
- wait_for_root_region_scanning();
+ Object[] args = new Object[]{req, res, this};
+ SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
+ } else {
+ filter.doFilter(request, response, this);
+ }
+ } catch (IOException | ServletException | RuntimeException e) {
+ throw e;
+ } catch (Throwable e) {
+ e = ExceptionUtils.unwrapInvocationTargetException(e);
+ ExceptionUtils.handleThrowable(e);
+ throw new ServletException(sm.getString("filterChain.filter"), e);
+ }
+ return;
+ }
- print_heap_before_gc();
- print_heap_regions();
- trace_heap_before_gc(_gc_tracer_stw);
+ // We fell off the end of the chain -- call the servlet instance
+ try {
+ if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
+ lastServicedRequest.set(request);
+ lastServicedResponse.set(response);
+ }
- _verifier->verify_region_sets_optional();
- _verifier->verify_dirty_young_regions();
+ if (request.isAsyncSupported() && !servletSupportsAsync) {
+ request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
+ Boolean.FALSE);
+ }
+ // Use potentially wrapped request from this point
+ if ((request instanceof HttpServletRequest) &&
+ (response instanceof HttpServletResponse) &&
+ Globals.IS_SECURITY_ENABLED ) {
+ final ServletRequest req = request;
+ final ServletResponse res = response;
+ Principal principal =
+ ((HttpServletRequest) req).getUserPrincipal();
+ Object[] args = new Object[]{req, res};
+ SecurityUtil.doAsPrivilege("service",
+ servlet,
+ classTypeUsedInService,
+ args,
+ principal);
+ } else {
+ servlet.service(request, response);
+ }
+ } catch (IOException | ServletException | RuntimeException e) {
+ throw e;
+ } catch (Throwable e) {
+ e = ExceptionUtils.unwrapInvocationTargetException(e);
+ ExceptionUtils.handleThrowable(e);
+ throw new ServletException(sm.getString("filterChain.servlet"), e);
+ } finally {
+ if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
+ lastServicedRequest.set(null);
+ lastServicedResponse.set(null);
+ }
+ }
+ }
+注意看这一行
filter.doFilter(request, response, this);
是不是看懂了,就是个 filter 链,但是这个代码在哪呢,org.apache.catalina.core.ApplicationFilterChain#doFilter
然后是interceptor,
+protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ HttpServletRequest processedRequest = request;
+ HandlerExecutionChain mappedHandler = null;
+ boolean multipartRequestParsed = false;
+ WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
- // 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();
- }
+ try {
+ try {
+ ModelAndView mv = null;
+ Object dispatchException = null;
- // 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");
+ try {
+ processedRequest = this.checkMultipart(request);
+ multipartRequestParsed = processedRequest != request;
+ mappedHandler = this.getHandler(processedRequest);
+ if (mappedHandler == null) {
+ this.noHandlerFound(processedRequest, response);
+ return;
+ }
- // We also do not allow mixed GCs during marking.
- assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
+ HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
+ String method = request.getMethod();
+ boolean isGet = "GET".equals(method);
+ if (isGet || "HEAD".equals(method)) {
+ long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
+ if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
+ return;
+ }
+ }
- // 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();
+ /**
+ * 看这里看这里‼️
+ */
+ if (!mappedHandler.applyPreHandle(processedRequest, response)) {
+ return;
+ }
- // Inner scope for scope based logging, timers, and stats collection
- {
- EvacuationInfo evacuation_info;
+ mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
+ if (asyncManager.isConcurrentHandlingStarted()) {
+ return;
+ }
- 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());
+ this.applyDefaultViewName(processedRequest, mv);
+ /**
+ * 再看这里看这里‼️
+ */
+ mappedHandler.applyPostHandle(processedRequest, response, mv);
+ } catch (Exception var20) {
+ dispatchException = var20;
+ } catch (Throwable var21) {
+ dispatchException = new NestedServletException("Handler dispatch failed", var21);
+ }
- TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
- TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
- collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
+ this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
+ } catch (Exception var22) {
+ this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
+ } catch (Throwable var23) {
+ this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
+ }
- G1HeapTransition heap_transition(this);
- size_t heap_used_bytes_before_gc = used();
+ } finally {
+ if (asyncManager.isConcurrentHandlingStarted()) {
+ if (mappedHandler != null) {
+ mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
+ }
+ } else if (multipartRequestParsed) {
+ this.cleanupMultipart(processedRequest);
+ }
- // 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.
+ }
+ }
+代码在哪呢,org.springframework.web.servlet.DispatcherServlet#doDispatch,然后才是我们自己写的 aop,是不是差不多明白了,嗯,接下来是例子
写个 filter
+public class DemoFilter extends HttpServlet implements Filter {
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ System.out.println("==>DemoFilter启动");
+ }
- { // Call to jvmpi::post_class_unload_events must occur outside of active GC
- IsGCActiveMark x;
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ // 将请求转换成HttpServletRequest 请求
+ HttpServletRequest req = (HttpServletRequest) servletRequest;
+ HttpServletResponse resp = (HttpServletResponse) servletResponse;
+ System.out.println("before filter");
+ filterChain.doFilter(req, resp);
+ System.out.println("after filter");
+ }
- gc_prologue(false);
+ @Override
+ public void destroy() {
- if (VerifyRememberedSets) {
- log_info(gc, verify)("[Verifying RemSets before GC]");
- VerifyRegionRemSetClosure v_cl;
- heap_region_iterate(&v_cl);
- }
+ }
+}
+因为用的springboot,所以就不写 web.xml 了,写个配置类
+@Configuration
+public class FilterConfiguration {
+ @Bean
+ public FilterRegistrationBean filterDemo4Registration() {
+ FilterRegistrationBean registration = new FilterRegistrationBean();
+ //注入过滤器
+ registration.setFilter(new DemoFilter());
+ //拦截规则
+ registration.addUrlPatterns("/*");
+ //过滤器名称
+ registration.setName("DemoFilter");
+ //是否自动注册 false 取消Filter的自动注册
+ registration.setEnabled(true);
+ //过滤器顺序
+ registration.setOrder(1);
+ return registration;
+ }
- _verifier->verify_before_gc(verify_type);
+}
+然后再来个 interceptor 和 aop,以及一个简单的请求处理
+public class DemoInterceptor extends HandlerInterceptorAdapter {
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ System.out.println("preHandle test");
+ return true;
+ }
- _verifier->check_bitmaps("GC Start");
+ @Override
+ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+ System.out.println("postHandle test");
+ }
+}
+@Aspect
+@Component
+public class DemoAspect {
-#if COMPILER2_OR_JVMCI
- DerivedPointerTable::clear();
-#endif
+ @Pointcut("execution( public * com.nicksxs.springbootdemo.demo.DemoController.*())")
+ public void point() {
- // 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();
+ @Before("point()")
+ public void doBefore(){
+ System.out.println("==doBefore==");
+ }
- {
- // 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);
+ @After("point()")
+ public void doAfter(){
+ System.out.println("==doAfter==");
+ }
+}
+@RestController
+public class DemoController {
- // Forget the current alloc region (we might even choose it to be part
- // of the collection set!).
- _allocator->release_mutator_alloc_region();
+ @RequestMapping("/hello")
+ @ResponseBody
+ public String hello() {
+ return "hello world";
+ }
+}
+好了,请求一下,看看 stdout,
![]()
搞定完事儿~
+]]>
+
+ Java
+ Filter
+ Interceptor - AOP
+ Spring
+ Servlet
+ Interceptor
+ AOP
+
+
+ Java
+ Filter
+ Interceptor
+ AOP
+ Spring
+ Tomcat
+ Servlet
+ Web
+
+
+
+ Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析
+ /2021/10/07/Leetcode-021-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8-Merge-Two-Sorted-Lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.
+将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
+示例 1
![]()
+
+输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
+
+示例 2
+输入: l1 = [], l2 = []
输出: []
+
+示例 3
+输入: l1 = [], l2 = [0]
输出: [0]
+
+简要分析
这题是 Easy 的,看着也挺简单,两个链表进行合并,就是比较下大小,可能将就点的话最好就在两个链表中原地合并
+题解代码
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
+ // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
+ if (l1 == null) {
+ return l2;
+ }
+ if (l2 == null) {
+ return l1;
+ }
+ // new 一个合并后的头结点
+ ListNode merged = new ListNode();
+ // 这个是当前节点
+ ListNode current = merged;
+ // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
+ // 因为内部前两个if就是跳出条件
+ while (true) {
+ if (l1 == null) {
+ // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
+ // 所以不能是直接current = l2,这样就是把后面的直接丢了
+ current.val = l2.val;
+ current.next = l2.next;
+ break;
+ }
+ if (l2 == null) {
+ current.val = l1.val;
+ current.next = l1.next;
+ break;
+ }
+ // 这里是两个链表都不为空的时候,就比较下大小
+ if (l1.val < l2.val) {
+ current.val = l1.val;
+ l1 = l1.next;
+ } else {
+ current.val = l2.val;
+ l2 = l2.next;
+ }
+ // 这里是new个新的,其实也可以放在循环头上
+ current.next = new ListNode();
+ current = current.next;
+ }
+ current = null;
+ // 返回这个头结点
+ return merged;
+ }
- // 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();
+结果
![]()
+]]>
+
+ Java
+ leetcode
+
+
+ leetcode
+ java
+ 题解
+
+
+
+ G1收集器概述
+ /2020/02/09/G1%E6%94%B6%E9%9B%86%E5%99%A8%E6%A6%82%E8%BF%B0/
+ G1: The Garbage-First Collector, 垃圾回收优先的垃圾回收器,目标是用户多核 cpu 和大内存的机器,最大的特点就是可预测的停顿时间,官方给出的介绍是提供一个用户在大的堆内存情况下一个低延迟表现的解决方案,通常是 6GB 及以上的堆大小,有低于 0.5 秒稳定的可预测的停顿时间。
+这里主要介绍这个比较新的垃圾回收器,在 G1 之前的垃圾回收器都是基于如下图的内存结构分布,有新生代,老年代和永久代(jdk8 之前),然后G1 往前的那些垃圾回收器都有个分代,比如 serial,parallel 等,一般有个应用的组合,最初的 serial 和 serial old,因为新生代和老年代的收集方式不太一样,新生代主要是标记复制,所以有 eden 跟两个 survival区,老年代一般用标记整理方式,而 G1 对这个不太一样。
![]()
看一下 G1 的内存分布
![]()
可以看到这有很大的不同,G1 通过将内存分成大小相等的 region,每个region是存在于一个连续的虚拟内存范围,对于某个 region 来说其角色是类似于原来的收集器的Eden、Survivor、Old Generation,这个具体在代码层面
+// We encode the value of the heap region type so the generation can be
+ // determined quickly. The tag is split into two parts:
+ //
+ // major type (young, old, humongous, archive) : top N-1 bits
+ // minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
+ //
+ // If there's need to increase the number of minor types in the
+ // future, we'll have to increase the size of the latter and hence
+ // decrease the size of the former.
+ //
+ // 00000 0 [ 0] Free
+ //
+ // 00001 0 [ 2] Young Mask
+ // 00001 0 [ 2] Eden
+ // 00001 1 [ 3] Survivor
+ //
+ // 00010 0 [ 4] Humongous Mask
+ // 00100 0 [ 8] Pinned Mask
+ // 00110 0 [12] Starts Humongous
+ // 00110 1 [13] Continues Humongous
+ //
+ // 01000 0 [16] Old Mask
+ //
+ // 10000 0 [32] Archive Mask
+ // 11100 0 [56] Open Archive
+ // 11100 1 [57] Closed Archive
+ //
+ typedef enum {
+ FreeTag = 0,
- g1_policy()->record_collection_pause_start(sample_start_time_sec);
+ YoungMask = 2,
+ EdenTag = YoungMask,
+ SurvTag = YoungMask + 1,
- if (collector_state()->in_initial_mark_gc()) {
- concurrent_mark()->pre_initial_mark();
- }
+ HumongousMask = 4,
+ PinnedMask = 8,
+ StartsHumongousTag = HumongousMask | PinnedMask,
+ ContinuesHumongousTag = HumongousMask | PinnedMask + 1,
- g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
+ OldMask = 16,
+ OldTag = OldMask,
- evacuation_info.set_collectionset_regions(collection_set()->region_length());
+ // Archive regions are regions with immutable content (i.e. not reclaimed, and
+ // not allocated into during regular operation). They differ in the kind of references
+ // allowed for the contained objects:
+ // - Closed archive regions form a separate self-contained (closed) object graph
+ // within the set of all of these regions. No references outside of closed
+ // archive regions are allowed.
+ // - Open archive regions have no restrictions on the references of their objects.
+ // Objects within these regions are allowed to have references to objects
+ // contained in any other kind of regions.
+ ArchiveMask = 32,
+ OpenArchiveTag = ArchiveMask | PinnedMask | OldMask,
+ ClosedArchiveTag = ArchiveMask | PinnedMask | OldMask + 1
+ } Tag;
- // 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();
+hotspot/share/gc/g1/heapRegionType.hpp
+当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。
+由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。
+需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。
+注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。
+在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。
+]]>
+
+ Java
+ JVM
+ GC
+ C++
+
+
+ Java
+ JVM
+ C++
+ G1
+ GC
+ Garbage-First Collector
+
+
+
+ JVM源码分析之G1垃圾收集器分析一
+ /2019/12/07/JVM-G1-Part-1/
+ 对 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);
- register_humongous_regions_with_cset();
+ HeapWord* result = op.result();
+ bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
+ assert(result == NULL || ret_succeeded,
+ "the result should be NULL if the VM did not succeed");
+ *succeeded = ret_succeeded;
- assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
+ 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");
- // We call this after finalize_cset() to
- // ensure that the CSet has been finalized.
- _cm->verify_no_cset_oops();
+ 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;
+ }
+ }
- if (_hr_printer.is_active()) {
- G1PrintCollectionSetClosure cl(&_hr_printer);
- _collection_set.iterate(&cl);
- }
+ 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();
- // Initialize the GC alloc regions.
- _allocator->init_gc_alloc_regions(evacuation_info);
+ // 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);
- G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
- pre_evacuate_collection_set();
+ // 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;
+ }
+ }
- // Actually do the work...
- evacuate_collection_set(&per_thread_states);
+ // Try a partial collection of some kind.
+ _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
- post_evacuate_collection_set(evacuation_info, &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");
- const size_t* surviving_young_words = per_thread_states.surviving_young_words();
- free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
+ if (GCLocker::check_active_before_gc()) {
+ return false;
+ }
- eagerly_reclaim_humongous_regions();
+ _gc_timer_stw->register_gc_start();
- record_obj_copy_mem_stats();
- _survivor_evac_stats.adjust_desired_plab_sz();
- _old_evac_stats.adjust_desired_plab_sz();
+ GCIdMark gc_id_mark;
+ _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
- double start = os::elapsedTime();
- start_new_collection_set();
- g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
+ SvcGCMarker sgcm(SvcGCMarker::MINOR);
+ ResourceMark rm;
- if (evacuation_failed()) {
- set_used(recalculate_used());
- if (_archive_allocator != NULL) {
- _archive_allocator->clear_used();
- }
- for (uint i = 0; i < ParallelGCThreads; i++) {
- if (_evacuation_failed_info_array[i].has_failed()) {
- _gc_tracer_stw->report_evacuation_failed(_evacuation_failed_info_array[i]);
- }
- }
- } else {
- // The "used" of the the collection set have already been subtracted
- // when they were freed. Add in the bytes evacuated.
- increase_used(g1_policy()->bytes_copied_during_gc());
+ g1_policy()->note_gc_start();
+
+ wait_for_root_region_scanning();
+
+ print_heap_before_gc();
+ print_heap_regions();
+ trace_heap_before_gc(_gc_tracer_stw);
+
+ _verifier->verify_region_sets_optional();
+ _verifier->verify_dirty_young_regions();
+
+ // We should not be doing initial mark unless the conc mark thread is running
+ if (!_cm_thread->should_terminate()) {
+ // This call will decide whether this pause is an initial-mark
+ // pause. If it is, in_initial_mark_gc() will return true
+ // for the duration of this pause.
+ g1_policy()->decide_on_conc_mark_initiation();
+ }
+
+ // We do not allow initial-mark to be piggy-backed on a mixed GC.
+ assert(!collector_state()->in_initial_mark_gc() ||
+ collector_state()->in_young_only_phase(), "sanity");
+
+ // We also do not allow mixed GCs during marking.
+ assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
+
+ // Record whether this pause is an initial mark. When the current
+ // thread has completed its logging output and it's safe to signal
+ // the CM thread, the flag's value in the policy has been reset.
+ bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
+
+ // Inner scope for scope based logging, timers, and stats collection
+ {
+ EvacuationInfo evacuation_info;
+
+ if (collector_state()->in_initial_mark_gc()) {
+ // We are about to start a marking cycle, so we increment the
+ // full collection counter.
+ increment_old_marking_cycles_started();
+ _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
+ }
+
+ _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
+
+ GCTraceCPUTime tcpu;
+
+ G1HeapVerifier::G1VerifyType verify_type;
+ FormatBuffer<> gc_string("Pause Young ");
+ if (collector_state()->in_initial_mark_gc()) {
+ gc_string.append("(Concurrent Start)");
+ verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
+ } else if (collector_state()->in_young_only_phase()) {
+ if (collector_state()->in_young_gc_before_mixed()) {
+ gc_string.append("(Prepare Mixed)");
+ } else {
+ gc_string.append("(Normal)");
+ }
+ verify_type = G1HeapVerifier::G1VerifyYoungNormal;
+ } else {
+ gc_string.append("(Mixed)");
+ verify_type = G1HeapVerifier::G1VerifyMixed;
+ }
+ GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
+
+ uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
+ workers()->active_workers(),
+ Threads::number_of_non_daemon_threads());
+ active_workers = workers()->update_active_workers(active_workers);
+ log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
+
+ TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
+ TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
+ collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
+
+ G1HeapTransition heap_transition(this);
+ size_t heap_used_bytes_before_gc = used();
+
+ // Don't dynamically change the number of GC threads this early. A value of
+ // 0 is used to indicate serial work. When parallel work is done,
+ // it will be set.
+
+ { // Call to jvmpi::post_class_unload_events must occur outside of active GC
+ IsGCActiveMark x;
+
+ gc_prologue(false);
+
+ if (VerifyRememberedSets) {
+ log_info(gc, verify)("[Verifying RemSets before GC]");
+ VerifyRegionRemSetClosure v_cl;
+ heap_region_iterate(&v_cl);
+ }
+
+ _verifier->verify_before_gc(verify_type);
+
+ _verifier->check_bitmaps("GC Start");
+
+#if COMPILER2_OR_JVMCI
+ DerivedPointerTable::clear();
+#endif
+
+ // Please see comment in g1CollectedHeap.hpp and
+ // G1CollectedHeap::ref_processing_init() to see how
+ // reference processing currently works in G1.
+
+ // Enable discovery in the STW reference processor
+ _ref_processor_stw->enable_discovery();
+
+ {
+ // We want to temporarily turn off discovery by the
+ // CM ref processor, if necessary, and turn it back on
+ // on again later if we do. Using a scoped
+ // NoRefDiscovery object will do this.
+ NoRefDiscovery no_cm_discovery(_ref_processor_cm);
+
+ // Forget the current alloc region (we might even choose it to be part
+ // of the collection set!).
+ _allocator->release_mutator_alloc_region();
+
+ // This timing is only used by the ergonomics to handle our pause target.
+ // It is unclear why this should not include the full pause. We will
+ // investigate this in CR 7178365.
+ //
+ // Preserving the old comment here if that helps the investigation:
+ //
+ // The elapsed time induced by the start time below deliberately elides
+ // the possible verification above.
+ double sample_start_time_sec = os::elapsedTime();
+
+ g1_policy()->record_collection_pause_start(sample_start_time_sec);
+
+ if (collector_state()->in_initial_mark_gc()) {
+ concurrent_mark()->pre_initial_mark();
+ }
+
+ g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
+
+ evacuation_info.set_collectionset_regions(collection_set()->region_length());
+
+ // Make sure the remembered sets are up to date. This needs to be
+ // done before register_humongous_regions_with_cset(), because the
+ // remembered sets are used there to choose eager reclaim candidates.
+ // If the remembered sets are not up to date we might miss some
+ // entries that need to be handled.
+ g1_rem_set()->cleanupHRRS();
+
+ register_humongous_regions_with_cset();
+
+ assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
+
+ // We call this after finalize_cset() to
+ // ensure that the CSet has been finalized.
+ _cm->verify_no_cset_oops();
+
+ if (_hr_printer.is_active()) {
+ G1PrintCollectionSetClosure cl(&_hr_printer);
+ _collection_set.iterate(&cl);
+ }
+
+ // Initialize the GC alloc regions.
+ _allocator->init_gc_alloc_regions(evacuation_info);
+
+ G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
+ pre_evacuate_collection_set();
+
+ // Actually do the work...
+ evacuate_collection_set(&per_thread_states);
+
+ post_evacuate_collection_set(evacuation_info, &per_thread_states);
+
+ const size_t* surviving_young_words = per_thread_states.surviving_young_words();
+ free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
+
+ eagerly_reclaim_humongous_regions();
+
+ record_obj_copy_mem_stats();
+ _survivor_evac_stats.adjust_desired_plab_sz();
+ _old_evac_stats.adjust_desired_plab_sz();
+
+ double start = os::elapsedTime();
+ start_new_collection_set();
+ g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
+
+ if (evacuation_failed()) {
+ set_used(recalculate_used());
+ if (_archive_allocator != NULL) {
+ _archive_allocator->clear_used();
+ }
+ for (uint i = 0; i < ParallelGCThreads; i++) {
+ if (_evacuation_failed_info_array[i].has_failed()) {
+ _gc_tracer_stw->report_evacuation_failed(_evacuation_failed_info_array[i]);
+ }
+ }
+ } else {
+ // The "used" of the the collection set have already been subtracted
+ // when they were freed. Add in the bytes evacuated.
+ increase_used(g1_policy()->bytes_copied_during_gc());
}
if (collector_state()->in_initial_mark_gc()) {
@@ -3007,66 +3239,219 @@ Node *clone(Node *graph) {
- Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析
- /2021/10/07/Leetcode-021-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8-Merge-Two-Sorted-Lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.
-将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
-示例 1
![]()
-
-输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
-
-示例 2
-输入: l1 = [], l2 = []
输出: []
-
-示例 3
-输入: l1 = [], l2 = [0]
输出: [0]
-
-简要分析
这题是 Easy 的,看着也挺简单,两个链表进行合并,就是比较下大小,可能将就点的话最好就在两个链表中原地合并
-题解代码
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
- // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
- if (l1 == null) {
- return l2;
+ Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析
+ /2020/12/13/Leetcode-105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given preorder and inorder traversal of a tree, construct the binary tree.
给定一棵树的前序和中序遍历,构造出一棵二叉树
+注意
You may assume that duplicates do not exist in the tree.
你可以假设树中没有重复的元素。(PS: 不然就没法做了呀)
+例子:
preorder = [3,9,20,15,7]
+inorder = [9,3,15,20,7]
+返回的二叉树
+ 3
+ / \
+9 20
+ / \
+ 15 7
+
+
+简要分析
看到这个题可以想到一个比较常规的解法就是递归拆树,前序就是根左右,中序就是左根右,然后就是通过前序已经确定的根在中序中找到,然后去划分左右子树,这个例子里是 3,找到中序中的位置,那么就可以确定,9 是左子树,15,20,7是右子树,然后对应的可以根据左右子树的元素数量在前序中划分左右子树,再继续递归就行
+class Solution {
+ public TreeNode buildTree(int[] preorder, int[] inorder) {
+ // 获取下数组长度
+ int n = preorder.length;
+ // 排除一下异常和边界
+ if (n != inorder.length) {
+ return null;
}
- if (l2 == null) {
- return l1;
+ if (n == 0) {
+ return null;
}
- // new 一个合并后的头结点
- ListNode merged = new ListNode();
- // 这个是当前节点
- ListNode current = merged;
- // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
- // 因为内部前两个if就是跳出条件
- while (true) {
- if (l1 == null) {
- // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
- // 所以不能是直接current = l2,这样就是把后面的直接丢了
- current.val = l2.val;
- current.next = l2.next;
- break;
- }
- if (l2 == null) {
- current.val = l1.val;
- current.next = l1.next;
+ if (n == 1) {
+ return new TreeNode(preorder[0]);
+ }
+ // 获得根节点
+ TreeNode node = new TreeNode(preorder[0]);
+ int pos = 0;
+ // 找到中序中的位置
+ for (int i = 0; i < inorder.length; i++) {
+ if (node.val == inorder[i]) {
+ pos = i;
break;
}
- // 这里是两个链表都不为空的时候,就比较下大小
- if (l1.val < l2.val) {
- current.val = l1.val;
- l1 = l1.next;
- } else {
- current.val = l2.val;
- l2 = l2.next;
- }
- // 这里是new个新的,其实也可以放在循环头上
- current.next = new ListNode();
- current = current.next;
}
- current = null;
- // 返回这个头结点
- return merged;
- }
+ // 划分左右再进行递归,注意下`Arrays.copyOfRange`的用法
+ node.left = buildTree(Arrays.copyOfRange(preorder, 1, pos + 1), Arrays.copyOfRange(inorder, 0, pos));
+ node.right = buildTree(Arrays.copyOfRange(preorder, pos + 1, n), Arrays.copyOfRange(inorder, pos + 1, n));
+ return node;
+ }
+}
]]>
+
+ Java
+ leetcode
+ Binary Tree
+ java
+ Binary Tree
+ DFS
+
+
+ leetcode
+ java
+ Binary Tree
+ 二叉树
+ 题解
+ 递归
+ Preorder Traversal
+ Inorder Traversal
+ 前序
+ 中序
+
+
+
+ Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析
+ /2021/11/28/Leetcode-053-%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C-Maximum-Subarray-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
+A subarray is a contiguous part of an array.
+示例
Example 1:
+
+Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
+
+Example 2:
+
+Input: nums = [1]
Output: 1
+
+Example 3:
+
+Input: nums = [5,4,-1,7,8]
Output: 23
+
+说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。
+代码
public int maxSubArray(int[] nums) {
+ int max = nums[0];
+ int sum = nums[0];
+ for (int i = 1; i < nums.length; i++) {
+ // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
+ sum = Math.max(nums[i], sum + nums[i]);
+ // max 是用来承载最大值的
+ max = Math.max(max, sum);
+ }
+ return max;
+ }
]]>
+
+ Java
+ leetcode
+
+
+ leetcode
+ java
+ 题解
+
+
+
+ Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析
+ /2021/03/14/Leetcode-121-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-Best-Time-to-Buy-and-Sell-Stock-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍You are given an array prices where prices[i] is the price of a given stock on the ith day.
+You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.
+Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.
+给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
+你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
+返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
+简单分析
其实这个跟二叉树的最长路径和有点类似,需要找到整体的最大收益,但是在迭代过程中需要一个当前的值
+int maxSofar = 0;
+public int maxProfit(int[] prices) {
+ if (prices.length <= 1) {
+ return 0;
+ }
+ int maxIn = prices[0];
+ int maxOut = prices[0];
+ for (int i = 1; i < prices.length; i++) {
+ if (maxIn > prices[i]) {
+ // 当循环当前值小于之前的买入值时就当成买入值,同时卖出也要更新
+ maxIn = prices[i];
+ maxOut = prices[i];
+ }
+ if (prices[i] > maxOut) {
+ // 表示一个可卖出点,即比买入值高时
+ maxOut = prices[i];
+ // 需要设置一个历史值
+ maxSofar = Math.max(maxSofar, maxOut - maxIn);
+ }
+ }
+ return maxSofar;
+}
-结果
![]()
+总结下
一开始看到 easy 就觉得是很简单,就没有 maxSofar ,但是一提交就出现问题了
对于[2, 4, 1]这种就会变成 0,所以还是需要一个历史值来存放历史最大值,这题有点动态规划的意思
+]]>
+
+ Java
+ leetcode
+ java
+ DP
+ DP
+
+
+ leetcode
+ java
+ 题解
+ DP
+
+
+
+ Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析
+ /2022/05/01/Leetcode-1115-%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0-FooBar-Print-FooBar-Alternately-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 无聊想去 roll 一题就看到了有并发题,就找到了这题,其实一眼看我的想法也是用信号量,但是用 condition 应该也是可以处理的,不过这类问题好像本地有点难调,因为它好像是抽取代码执行的,跟直观的逻辑比较不一样
Suppose you are given the following code:
+class FooBar {
+ public void foo() {
+ for (int i = 0; i < n; i++) {
+ print("foo");
+ }
+ }
+
+ public void bar() {
+ for (int i = 0; i < n; i++) {
+ print("bar");
+ }
+ }
+}
+The same instance of FooBar will be passed to two different threads:
+
+- thread
A will call foo(), while
+- thread
B will call bar().
Modify the given program to output "foobar" n times.
+
+示例
Example 1:
+Input: n = 1
Output: “foobar”
Explanation: There are two threads being fired asynchronously. One of them calls foo(), while the other calls bar().
“foobar” is being output 1 time.
+
+Example 2:
+Input: n = 2
Output: “foobarfoobar”
Explanation: “foobar” is being output 2 times.
+
+题解
简析
其实用信号量是很直观的,就是让打印 foo 的线程先拥有信号量,打印后就等待,给 bar 信号量 + 1,然后 bar 线程运行打印消耗 bar 信号量,再给 foo 信号量 + 1
+code
class FooBar {
+
+ private final Semaphore foo = new Semaphore(1);
+ private final Semaphore bar = new Semaphore(0);
+ private int n;
+
+ public FooBar(int n) {
+ this.n = n;
+ }
+
+ public void foo(Runnable printFoo) throws InterruptedException {
+
+ for (int i = 0; i < n; i++) {
+ foo.acquire();
+ // printFoo.run() outputs "foo". Do not change or remove this line.
+ printFoo.run();
+ bar.release();
+ }
+ }
+
+ public void bar(Runnable printBar) throws InterruptedException {
+
+ for (int i = 0; i < n; i++) {
+ bar.acquire();
+ // printBar.run() outputs "bar". Do not change or remove this line.
+ printBar.run();
+ foo.release();
+ }
+ }
+}
]]>
Java
@@ -3076,6 +3461,7 @@ Node *clone(Node *graph) {
leetcode
java
题解
+ Print FooBar Alternately
@@ -3154,194 +3540,128 @@ Output: 0<
- 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年家人和自己都健康,顺遂。大家也一样。
+ 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
- 生活
- 年终总结
- 2022
- 2023
-
-
-
- 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
+ java
+ Binary Tree
+ 二叉树
+ 题解
- Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析
- /2021/11/28/Leetcode-053-%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C-Maximum-Subarray-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
-A subarray is a contiguous part of an array.
-示例
Example 1:
+ Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析
+ /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given a 2D grid of size m x n and an integer k. You need to shift the grid k times.
+In one shift operation:
+Element at grid[i][j] moves to grid[i][j + 1].
Element at grid[i][n - 1] moves to grid[i + 1][0].
Element at grid[m - 1][n - 1] moves to grid[0][0].
Return the 2D grid after applying shift operation k times.
+示例
Example 1:
-Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
+Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 1
Output: [[9,1,2],[3,4,5],[6,7,8]]
-Example 2:
+Example 2:
-Input: nums = [1]
Output: 1
+Input: grid = [[3,8,1,9],[19,7,2,5],[4,6,11,10],[12,0,21,13]], k = 4
Output: [[12,0,21,13],[3,8,1,9],[19,7,2,5],[4,6,11,10]]
-Example 3:
-
-Input: nums = [5,4,-1,7,8]
Output: 23
+Example 3:
+Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 9
Output: [[1,2,3],[4,5,6],[7,8,9]]
-说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。
-代码
public int maxSubArray(int[] nums) {
- int max = nums[0];
- int sum = nums[0];
- for (int i = 1; i < nums.length; i++) {
- // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
- sum = Math.max(nums[i], sum + nums[i]);
- // max 是用来承载最大值的
- max = Math.max(max, sum);
+提示
+m == grid.length
+n == grid[i].length
+1 <= m <= 50
+1 <= n <= 50
+-1000 <= grid[i][j] <= 1000
+0 <= k <= 100
+
+解析
这个题主要是矩阵或者说数组的操作,并且题目要返回的是个 List,所以也不用原地操作,只需要找对位置就可以了,k 是多少就相当于让这个二维数组头尾衔接移动 k 个元素
+代码
public List<List<Integer>> shiftGrid(int[][] grid, int k) {
+ // 行数
+ int m = grid.length;
+ // 列数
+ int n = grid[0].length;
+ // 偏移值,取下模
+ k = k % (m * n);
+ // 反向取下数量,因为我打算直接从头填充新的矩阵
+ /*
+ * 比如
+ * 1 2 3
+ * 4 5 6
+ * 7 8 9
+ * 需要变成
+ * 9 1 2
+ * 3 4 5
+ * 6 7 8
+ * 就要从 9 开始填充
+ */
+ int reverseK = m * n - k;
+ List<List<Integer>> matrix = new ArrayList<>();
+ // 这类就是两层循环
+ for (int i = 0; i < m; i++) {
+ List<Integer> line = new ArrayList<>();
+ for (int j = 0; j < n; j++) {
+ // 数量会随着循环迭代增长, 确认是第几个
+ int currentNum = reverseK + i * n + (j + 1);
+ // 这里处理下到达矩阵末尾后减掉 m * n
+ if (currentNum > m * n) {
+ currentNum -= m * n;
+ }
+ // 根据矩阵列数 n 算出在原来矩阵的位置
+ int last = (currentNum - 1) % n;
+ int passLine = (currentNum - 1) / n;
+
+ line.add(grid[passLine][last]);
+ }
+ matrix.add(line);
}
- return max;
- }
]]>
+ return matrix;
+ }
+
+结果数据
![]()
比较慢
+]]>
Java
leetcode
@@ -3350,206 +3670,245 @@ Output: 0<
leetcode
java
题解
+ Shift 2D Grid
- Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析
- /2022/05/01/Leetcode-1115-%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0-FooBar-Print-FooBar-Alternately-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 无聊想去 roll 一题就看到了有并发题,就找到了这题,其实一眼看我的想法也是用信号量,但是用 condition 应该也是可以处理的,不过这类问题好像本地有点难调,因为它好像是抽取代码执行的,跟直观的逻辑比较不一样
Suppose you are given the following code:
-class FooBar {
- public void foo() {
- for (int i = 0; i < n; i++) {
- print("foo");
- }
- }
-
- public void bar() {
- for (int i = 0; i < n; i++) {
- print("bar");
- }
- }
-}
-The same instance of FooBar will be passed to two different threads:
+ Leetcode 155 最小栈(Min Stack) 题解分析
+ /2020/12/06/Leetcode-155-%E6%9C%80%E5%B0%8F%E6%A0%88-Min-Stack-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
设计一个栈,支持压栈,出站,获取栈顶元素,通过常数级复杂度获取栈中的最小元素
-- thread
A will call foo(), while
-- thread
B will call bar().
Modify the given program to output "foobar" n times.
-
-示例
Example 1:
-Input: n = 1
Output: “foobar”
Explanation: There are two threads being fired asynchronously. One of them calls foo(), while the other calls bar().
“foobar” is being output 1 time.
-
-Example 2:
-Input: n = 2
Output: “foobarfoobar”
Explanation: “foobar” is being output 2 times.
-
-题解
简析
其实用信号量是很直观的,就是让打印 foo 的线程先拥有信号量,打印后就等待,给 bar 信号量 + 1,然后 bar 线程运行打印消耗 bar 信号量,再给 foo 信号量 + 1
-code
class FooBar {
-
- private final Semaphore foo = new Semaphore(1);
- private final Semaphore bar = new Semaphore(0);
- private int n;
+push(x) – Push element x onto stack.
+pop() – Removes the element on top of the stack.
+top() – Get the top element.
+getMin() – Retrieve the minimum element in the stack.
+
+示例
Example 1:
+Input
+["MinStack","push","push","push","getMin","pop","top","getMin"]
+[[],[-2],[0],[-3],[],[],[],[]]
- public FooBar(int n) {
- this.n = n;
- }
+Output
+[null,null,null,null,-3,null,0,-2]
- public void foo(Runnable printFoo) throws InterruptedException {
-
- for (int i = 0; i < n; i++) {
- foo.acquire();
- // printFoo.run() outputs "foo". Do not change or remove this line.
- printFoo.run();
- bar.release();
- }
- }
+Explanation
+MinStack minStack = new MinStack();
+minStack.push(-2);
+minStack.push(0);
+minStack.push(-3);
+minStack.getMin(); // return -3
+minStack.pop();
+minStack.top(); // return 0
+minStack.getMin(); // return -2
- public void bar(Runnable printBar) throws InterruptedException {
-
- for (int i = 0; i < n; i++) {
- bar.acquire();
- // printBar.run() outputs "bar". Do not change or remove this line.
- printBar.run();
- foo.release();
- }
- }
-}
-]]>
-
- Java
- leetcode
-
-
- leetcode
- java
- 题解
- Print FooBar Alternately
-
-
-
- Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析
- /2020/12/13/Leetcode-105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given preorder and inorder traversal of a tree, construct the binary tree.
给定一棵树的前序和中序遍历,构造出一棵二叉树
-注意
You may assume that duplicates do not exist in the tree.
你可以假设树中没有重复的元素。(PS: 不然就没法做了呀)
-例子:
preorder = [3,9,20,15,7]
-inorder = [9,3,15,20,7]
-返回的二叉树
- 3
- / \
-9 20
- / \
- 15 7
+简要分析
其实现在大部分语言都自带类栈的数据结构,Java 也自带 stack 这个数据结构,所以这个题的主要难点的就是常数级的获取最小元素,最开始的想法是就一个栈外加一个记录最小值的变量就行了,但是仔细一想是不行的,因为随着元素被 pop 出去,这个最小值也可能需要梗着变化,就不太好判断了,所以后面是用了一个辅助栈。
+代码
class MinStack {
+ // 这个作为主栈
+ Stack<Integer> s1 = new Stack<>();
+ // 这个作为辅助栈,放最小值的栈
+ Stack<Integer> s2 = new Stack<>();
+ /** initialize your data structure here. */
+ public MinStack() {
+ }
-简要分析
看到这个题可以想到一个比较常规的解法就是递归拆树,前序就是根左右,中序就是左根右,然后就是通过前序已经确定的根在中序中找到,然后去划分左右子树,这个例子里是 3,找到中序中的位置,那么就可以确定,9 是左子树,15,20,7是右子树,然后对应的可以根据左右子树的元素数量在前序中划分左右子树,再继续递归就行
-class Solution {
- public TreeNode buildTree(int[] preorder, int[] inorder) {
- // 获取下数组长度
- int n = preorder.length;
- // 排除一下异常和边界
- if (n != inorder.length) {
- return null;
+ public void push(int x) {
+ // 放入主栈
+ s1.push(x);
+ // 当 s2 是空或者当前值是小于"等于" s2 栈顶时,压入辅助最小值的栈
+ // 注意这里的"等于"非常必要,因为当最小值有多个的情况下,也需要压入栈,否则在 pop 的时候就会不对等
+ if (s2.isEmpty() || x <= s2.peek()) {
+ s2.push(x);
+ }
}
- if (n == 0) {
- return null;
+
+ public void pop() {
+ // 首先就是主栈要 pop,然后就是第二个了,跟上面的"等于"很有关系,
+ // 因为如果有两个最小值,如果前面等于的情况没有压栈,那这边相等的时候 pop 就会少一个了,可能就导致最小值不对了
+ int x = s1.pop();
+ if (x == s2.peek()) {
+ s2.pop();
+ }
}
- if (n == 1) {
- return new TreeNode(preorder[0]);
+
+ public int top() {
+ // 栈顶的元素
+ return s1.peek();
}
- // 获得根节点
- TreeNode node = new TreeNode(preorder[0]);
- int pos = 0;
- // 找到中序中的位置
- for (int i = 0; i < inorder.length; i++) {
- if (node.val == inorder[i]) {
- pos = i;
- break;
- }
+
+ public int getMin() {
+ // 辅助最小栈的栈顶
+ return s2.peek();
}
- // 划分左右再进行递归,注意下`Arrays.copyOfRange`的用法
- node.left = buildTree(Arrays.copyOfRange(preorder, 1, pos + 1), Arrays.copyOfRange(inorder, 0, pos));
- node.right = buildTree(Arrays.copyOfRange(preorder, pos + 1, n), Arrays.copyOfRange(inorder, pos + 1, n));
- return node;
- }
-}
]]>
+ }
+
+]]>
Java
leetcode
- Binary Tree
java
- Binary Tree
- DFS
+ stack
+ stack
leetcode
java
题解
- Binary Tree
- 二叉树
- 递归
- Preorder Traversal
- Inorder Traversal
- 前序
- 中序
+ stack
+ min stack
+ 最小栈
+ leetcode 155
- Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析
- /2021/03/14/Leetcode-121-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-Best-Time-to-Buy-and-Sell-Stock-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍You are given an array prices where prices[i] is the price of a given stock on the ith day.
-You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.
-Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.
-给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
-你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
-返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
-简单分析
其实这个跟二叉树的最长路径和有点类似,需要找到整体的最大收益,但是在迭代过程中需要一个当前的值
-int maxSofar = 0;
-public int maxProfit(int[] prices) {
- if (prices.length <= 1) {
- return 0;
- }
- int maxIn = prices[0];
- int maxOut = prices[0];
- for (int i = 1; i < prices.length; i++) {
- if (maxIn > prices[i]) {
- // 当循环当前值小于之前的买入值时就当成买入值,同时卖出也要更新
- maxIn = prices[i];
- maxOut = prices[i];
- }
- if (prices[i] > maxOut) {
- // 表示一个可卖出点,即比买入值高时
- maxOut = prices[i];
- // 需要设置一个历史值
- maxSofar = Math.max(maxSofar, maxOut - maxIn);
+ Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析
+ /2022/08/06/Leetcode-16-%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C-3Sum-Closest-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given an integer array nums of length n and an integer target, find three integers in nums such that the sum is closest to target.
+Return the sum of the three integers.
+You may assume that each input would have exactly one solution.
+简单解释下就是之前是要三数之和等于目标值,现在是找到最接近的三数之和。
+示例
Example 1:
+Input: nums = [-1,2,1,-4], target = 1
Output: 2
Explanation: The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).
+
+Example 2:
+Input: nums = [0,0,0], target = 1
Output: 0
+
+Constraints:
+3 <= nums.length <= 1000
+-1000 <= nums[i] <= 1000
+-10^4 <= target <= 10^4
+
+简单解析
这个题思路上来讲不难,也是用原来三数之和的方式去做,利用”双指针法”或者其它描述法,但是需要简化逻辑
+code
public int threeSumClosest(int[] nums, int target) {
+ Arrays.sort(nums);
+ // 当前最近的和
+ int closestSum = nums[0] + nums[1] + nums[nums.length - 1];
+ for (int i = 0; i < nums.length - 2; i++) {
+ if (i == 0 || nums[i] != nums[i - 1]) {
+ // 左指针
+ int left = i + 1;
+ // 右指针
+ int right = nums.length - 1;
+ // 判断是否遍历完了
+ while (left < right) {
+ // 当前的和
+ int sum = nums[i] + nums[left] + nums[right];
+ // 小优化,相等就略过了
+ while (left < right && nums[left] == nums[left + 1]) {
+ left++;
+ }
+ while (left < right && nums[right] == nums[right - 1]) {
+ right--;
+ }
+ // 这里判断,其实也还是希望趋近目标值
+ if (sum < target) {
+ left++;
+ } else {
+ right--;
+ }
+ // 判断是否需要替换
+ if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
+ closestSum = sum;
+ }
+ }
+ }
}
- }
- return maxSofar;
-}
+ return closestSum;
+ }
-总结下
一开始看到 easy 就觉得是很简单,就没有 maxSofar ,但是一提交就出现问题了
对于[2, 4, 1]这种就会变成 0,所以还是需要一个历史值来存放历史最大值,这题有点动态规划的意思
+结果
![]()
]]>
Java
leetcode
- java
- DP
- DP
leetcode
java
题解
- DP
+ 3Sum Closest
- Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析
- /2020/10/25/Leetcode-104-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6-Maximum-Depth-of-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍给定一个二叉树,找出其最大深度。
-二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
-说明: 叶子节点是指没有子节点的节点。
-示例:
给定二叉树 [3,9,20,null,null,15,7],
- 3
- / \
-9 20
- / \
+ Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析
+ /2021/01/10/Leetcode-160-%E7%9B%B8%E4%BA%A4%E9%93%BE%E8%A1%A8-intersection-of-two-linked-lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍写一个程序找出两个单向链表的交叉起始点,可能是我英语不好,图里画的其实还有一点是交叉以后所有节点都是相同的
Write a program to find the node at which the intersection of two singly linked lists begins.
+For example, the following two linked lists:
![]()
begin to intersect at node c1.
+Example 1:
![]()
+Input: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
+Output: Reference of the node with value = 8
+Input Explanation: The intersected node's value is 8 (note that this must not be 0 if the two lists intersect). From the head of A, it reads as [4,1,8,4,5]. From the head of B, it reads as [5,6,1,8,4,5]. There are 2 nodes before the intersected node in A; There are 3 nodes before the intersected node in B.
+分析题解
一开始没什么头绪,感觉只能最原始的遍历,后来看了一些文章,发现比较简单的方式就是先找两个链表的长度差,因为从相交点开始肯定是长度一致的,这是个很好的解题突破口,找到长度差以后就是先跳过长链表的较长部分,然后开始同步遍历比较 A,B 链表;
+代码
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
+ if (headA == null || headB == null) {
+ return null;
+ }
+ // 算 A 的长度
+ int countA = 0;
+ ListNode tailA = headA;
+ while (tailA != null) {
+ tailA = tailA.next;
+ countA++;
+ }
+ // 算 B 的长度
+ int countB = 0;
+ ListNode tailB = headB;
+ while (tailB != null) {
+ tailB = tailB.next;
+ countB++;
+ }
+ tailA = headA;
+ tailB = headB;
+ // 依据长度差,先让长的链表 tail 指针往后移
+ if (countA > countB) {
+ while (countA > countB) {
+ tailA = tailA.next;
+ countA--;
+ }
+ } else if (countA < countB) {
+ while (countA < countB) {
+ tailB = tailB.next;
+ countB--;
+ }
+ }
+ // 然后以相同速度遍历两个链表比较
+ while (tailA != null) {
+ if (tailA == tailB) {
+ return tailA;
+ } else {
+ tailA = tailA.next;
+ tailB = tailB.next;
+ }
+ }
+ return null;
+ }
+总结
可能缺少这种思维,做的还是比较少,所以没法一下子反应过来,需要锻炼,我的第一反应是两重遍历,不过那样复杂度就高了,这里应该是只有 O(N) 的复杂度。
+]]>
+
+ Java
+ leetcode
+ Linked List
+ java
+ Linked List
+
+
+ leetcode
+ java
+ 题解
+ Linked List
+
+
+
+ Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析
+ /2020/10/25/Leetcode-104-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6-Maximum-Depth-of-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍给定一个二叉树,找出其最大深度。
+二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
+说明: 叶子节点是指没有子节点的节点。
+示例:
给定二叉树 [3,9,20,null,null,15,7],
+ 3
+ / \
+9 20
+ / \
15 7
返回它的最大深度 3 。
代码
// 主体是个递归的应用
@@ -3584,178 +3943,214 @@ inorder = [9,3,15,20,7]<
leetcode
java
- 题解
Binary Tree
DFS
二叉树
+ 题解
- Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析
- /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given a 2D grid of size m x n and an integer k. You need to shift the grid k times.
-In one shift operation:
-Element at grid[i][j] moves to grid[i][j + 1].
Element at grid[i][n - 1] moves to grid[i + 1][0].
Element at grid[m - 1][n - 1] moves to grid[0][0].
Return the 2D grid after applying shift operation k times.
-示例
Example 1:
-
-Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 1
Output: [[9,1,2],[3,4,5],[6,7,8]]
-
-Example 2:
-
-Input: grid = [[3,8,1,9],[19,7,2,5],[4,6,11,10],[12,0,21,13]], k = 4
Output: [[12,0,21,13],[3,8,1,9],[19,7,2,5],[4,6,11,10]]
-
-Example 3:
-Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 9
Output: [[1,2,3],[4,5,6],[7,8,9]]
-
-提示
-m == grid.length
-n == grid[i].length
-1 <= m <= 50
-1 <= n <= 50
--1000 <= grid[i][j] <= 1000
-0 <= k <= 100
-
-解析
这个题主要是矩阵或者说数组的操作,并且题目要返回的是个 List,所以也不用原地操作,只需要找对位置就可以了,k 是多少就相当于让这个二维数组头尾衔接移动 k 个元素
-代码
public List<List<Integer>> shiftGrid(int[][] grid, int k) {
- // 行数
- int m = grid.length;
- // 列数
- int n = grid[0].length;
- // 偏移值,取下模
- k = k % (m * n);
- // 反向取下数量,因为我打算直接从头填充新的矩阵
- /*
- * 比如
- * 1 2 3
- * 4 5 6
- * 7 8 9
- * 需要变成
- * 9 1 2
- * 3 4 5
- * 6 7 8
- * 就要从 9 开始填充
- */
- int reverseK = m * n - k;
- List<List<Integer>> matrix = new ArrayList<>();
- // 这类就是两层循环
- for (int i = 0; i < m; i++) {
- List<Integer> line = new ArrayList<>();
- for (int j = 0; j < n; j++) {
- // 数量会随着循环迭代增长, 确认是第几个
- int currentNum = reverseK + i * n + (j + 1);
- // 这里处理下到达矩阵末尾后减掉 m * n
- if (currentNum > m * n) {
- currentNum -= m * n;
- }
- // 根据矩阵列数 n 算出在原来矩阵的位置
- int last = (currentNum - 1) % n;
- int passLine = (currentNum - 1) / n;
+ Leetcode 2 Add Two Numbers 题解分析
+ /2020/10/11/Leetcode-2-Add-Two-Numbers-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 又 roll 到了一个以前做过的题,不过现在用 Java 也来写一下,是 easy 级别的,所以就简单说下
+简要介绍
You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.
+You may assume the two numbers do not contain any leading zero, except the number 0 itself.
就是给了两个链表,用来表示两个非负的整数,在链表中倒序放着,每个节点包含一位的数字,把他们加起来以后也按照原来的链表结构输出
+样例
example 1
Input: l1 = [2,4,3], l2 = [5,6,4]
+Output: [7,0,8]
+Explanation: 342 + 465 = 807.
- line.add(grid[passLine][last]);
+example 2
Input: l1 = [0], l2 = [0]
+Output: [0]
+
+example 3
Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
+Output: [8,9,9,9,0,0,0,1]
+
+题解
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
+ ListNode root = new ListNode();
+ if (l1 == null && l2 == null) {
+ return root;
+ }
+ ListNode tail = root;
+ int entered = 0;
+ // 这个条件加了 entered,就是还有进位的数
+ while (l1 != null || l2 != null || entered != 0) {
+ int temp = entered;
+ if (l1 != null) {
+ temp += l1.val;
+ l1 = l1.next;
+ }
+ if (l2 != null) {
+ temp += l2.val;
+ l2 = l2.next;
+ }
+ entered = (temp - temp % 10) / 10;
+ tail.val = temp % 10;
+ // 循环内部的控制是为了排除最后的空节点
+ if (l1 != null || l2 != null || entered != 0) {
+ tail.next = new ListNode();
+ tail = tail.next;
}
- matrix.add(line);
}
- return matrix;
- }
-
-结果数据
![]()
比较慢
+// tail = null;
+ return root;
+ }
+这里唯二需要注意的就是两个点,一个是循环条件需要包含进位值还存在的情况,还有一个是最后一个节点,如果是空的了,就不要在 new 一个出来了,写的比较挫
]]>
Java
leetcode
+ java
+ linked list
+ linked list
leetcode
java
题解
- Shift 2D Grid
+ linked list
- Leetcode 155 最小栈(Min Stack) 题解分析
- /2020/12/06/Leetcode-155-%E6%9C%80%E5%B0%8F%E6%A0%88-Min-Stack-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
设计一个栈,支持压栈,出站,获取栈顶元素,通过常数级复杂度获取栈中的最小元素
-
-- push(x) – Push element x onto stack.
-- pop() – Removes the element on top of the stack.
-- top() – Get the top element.
-- getMin() – Retrieve the minimum element in the stack.
+ Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析
+ /2022/07/02/Leetcode-20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7-Valid-Parentheses-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
+An input string is valid if:
+
+- Open brackets must be closed by the same type of brackets.
+- Open brackets must be closed in the correct order.
+
+示例
Example 1:
+Input: s = “()”
Output: true
+
+Example 2:
+Input: s = “()[]{}”
Output: true
+
+Example 3:
+Input: s = “(]”
Output: false
+
+Constraints:
+1 <= s.length <= 10^4
+s consists of parentheses only '()[]{}'.
-示例
Example 1:
-Input
-["MinStack","push","push","push","getMin","pop","top","getMin"]
-[[],[-2],[0],[-3],[],[],[],[]]
-
-Output
-[null,null,null,null,-3,null,0,-2]
-
-Explanation
-MinStack minStack = new MinStack();
-minStack.push(-2);
-minStack.push(0);
-minStack.push(-3);
-minStack.getMin(); // return -3
-minStack.pop();
-minStack.top(); // return 0
-minStack.getMin(); // return -2
-
-简要分析
其实现在大部分语言都自带类栈的数据结构,Java 也自带 stack 这个数据结构,所以这个题的主要难点的就是常数级的获取最小元素,最开始的想法是就一个栈外加一个记录最小值的变量就行了,但是仔细一想是不行的,因为随着元素被 pop 出去,这个最小值也可能需要梗着变化,就不太好判断了,所以后面是用了一个辅助栈。
-代码
class MinStack {
- // 这个作为主栈
- Stack<Integer> s1 = new Stack<>();
- // 这个作为辅助栈,放最小值的栈
- Stack<Integer> s2 = new Stack<>();
- /** initialize your data structure here. */
- public MinStack() {
+解析
easy题,并且看起来也是比较简单的,三种括号按对匹配,直接用栈来做,栈里面存的是括号的类型,如果是左括号,就放入栈中,如果是右括号,就把栈顶的元素弹出,如果弹出的元素不是左括号,就返回false,如果弹出的元素是左括号,就继续往下走,如果遍历完了,如果栈里面还有元素,就返回false,如果遍历完了,如果栈里面没有元素,就返回true
+代码
class Solution {
+ public boolean isValid(String s) {
+ if (s.length() % 2 != 0) {
+ return false;
}
-
- public void push(int x) {
- // 放入主栈
- s1.push(x);
- // 当 s2 是空或者当前值是小于"等于" s2 栈顶时,压入辅助最小值的栈
- // 注意这里的"等于"非常必要,因为当最小值有多个的情况下,也需要压入栈,否则在 pop 的时候就会不对等
- if (s2.isEmpty() || x <= s2.peek()) {
- s2.push(x);
+ Stack<String> stk = new Stack<>();
+ for (int i = 0; i < s.length(); i++) {
+ if (s.charAt(i) == '{' || s.charAt(i) == '(' || s.charAt(i) == '[') {
+ stk.push(String.valueOf(s.charAt(i)));
+ continue;
}
- }
-
- public void pop() {
- // 首先就是主栈要 pop,然后就是第二个了,跟上面的"等于"很有关系,
- // 因为如果有两个最小值,如果前面等于的情况没有压栈,那这边相等的时候 pop 就会少一个了,可能就导致最小值不对了
- int x = s1.pop();
- if (x == s2.peek()) {
- s2.pop();
+ if (s.charAt(i) == '}') {
+ if (stk.isEmpty()) {
+ return false;
+ }
+ String cur = stk.peek();
+ if (cur.charAt(0) != '{') {
+ return false;
+ } else {
+ stk.pop();
+ }
+ continue;
+ }
+ if (s.charAt(i) == ']') {
+ if (stk.isEmpty()) {
+ return false;
+ }
+ String cur = stk.peek();
+ if (cur.charAt(0) != '[') {
+ return false;
+ } else {
+ stk.pop();
+ }
+ continue;
+ }
+ if (s.charAt(i) == ')') {
+ if (stk.isEmpty()) {
+ return false;
+ }
+ String cur = stk.peek();
+ if (cur.charAt(0) != '(') {
+ return false;
+ } else {
+ stk.pop();
+ }
+ continue;
}
- }
-
- public int top() {
- // 栈顶的元素
- return s1.peek();
- }
- public int getMin() {
- // 辅助最小栈的栈顶
- return s2.peek();
}
- }
+ return stk.size() == 0;
+ }
+}
]]>
Java
leetcode
+
+
+ leetcode
+ java
+
+
+
+ Leetcode 234 回文链表(Palindrome Linked List) 题解分析
+ /2020/11/15/Leetcode-234-%E5%9B%9E%E6%96%87%E8%81%94%E8%A1%A8-Palindrome-Linked-List-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given a singly linked list, determine if it is a palindrome.
给定一个单向链表,判断是否是回文链表
+例一 Example 1:
Input: 1->2
Output: false
+例二 Example 2:
Input: 1->2->2->1
Output: true
+挑战下自己
Follow up:
Could you do it in O(n) time and O(1) space?
+简要分析
首先这是个单向链表,如果是双向的就可以一个从头到尾,一个从尾到头,显然那样就没啥意思了,然后想过要不找到中点,然后用一个栈,把前一半塞进栈里,但是这种其实也比较麻烦,比如长度是奇偶数,然后如何找到中点,这倒是可以借助于双指针,还是比较麻烦,再想一想,回文链表,就跟最开始的一样,链表只有单向的,我用个栈不就可以逆向了么,先把链表整个塞进栈里,然后在一个个 pop 出来跟链表从头开始比较,全对上了就是回文了
+/**
+ * Definition for singly-linked list.
+ * public class ListNode {
+ * int val;
+ * ListNode next;
+ * ListNode() {}
+ * ListNode(int val) { this.val = val; }
+ * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
+ * }
+ */
+class Solution {
+ public boolean isPalindrome(ListNode head) {
+ if (head == null) {
+ return true;
+ }
+ ListNode tail = head;
+ LinkedList<Integer> stack = new LinkedList<>();
+ // 这里就是一个循环,将所有元素依次压入栈
+ while (tail != null) {
+ stack.push(tail.val);
+ tail = tail.next;
+ }
+ // 在逐个 pop 出来,其实这个出来的顺序就等于链表从尾到头遍历,同时跟链表从头到尾遍历进行逐对对比
+ while (!stack.isEmpty()) {
+ if (stack.peekFirst() == head.val) {
+ stack.pollFirst();
+ head = head.next;
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+}
]]>
+
+ Java
+ leetcode
+ Linked List
java
- stack
- stack
+ Linked List
leetcode
java
题解
- stack
- min stack
- 最小栈
- leetcode 155
+ Linked List
@@ -3821,273 +4216,117 @@ minStack.getMin(); // return -2题目介绍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%,贴个图哈哈
![]()
+ Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析
+ /2021/05/23/Leetcode-236-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88-Lowest-Common-Ancestor-of-a-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
+According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”
+给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
+百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
+代码
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
+ // 如果当前节点就是 p 或者是 q 的时候,就直接返回了
+ // 当没找到,即 root == null 的时候也会返回 null,这是个重要的点
+ if (root == null || root == p || root == q) return root;
+ // 在左子树中找 p 和 q
+ TreeNode left = lowestCommonAncestor(root.left, p, q);
+ // 在右子树中找 p 和 q
+ TreeNode right = lowestCommonAncestor(root.right, p, q);
+ // 当左边是 null 就直接返回右子树,但是这里不表示右边不是 null,所以这个顺序是不影响的
+ // 考虑一种情况,如果一个节点的左右子树都是 null,那么其实对于这个节点来说首先两个子树分别调用
+ // lowestCommonAncestor会在开头就返回 null,那么就是上面 left 跟 right 都是 null,然后走下面的判断的时候
+ // 其实第一个 if 就返回了 null,如此递归返回就能达到当子树中没有找到 p 或者 q 的时候只返回 null
+ if (left == null) {
+ return right;
+ } else if (right == null) {
+ return left;
+ } else {
+ return root;
+ }
+// if (right == null) {
+// return left;
+// } else if (left == null) {
+// return right;
+// } else {
+// return root;
+// }
+// return left == null ? right : right == null ? left : root;
+ }
]]>
Java
leetcode
- Binary Tree
- java
- Binary Tree
+ Lowest Common Ancestor of a Binary Tree
leetcode
java
题解
- Binary Tree
- 二叉树
+ Lowest Common Ancestor of a Binary Tree
- Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析
- /2022/08/06/Leetcode-16-%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C-3Sum-Closest-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given an integer array nums of length n and an integer target, find three integers in nums such that the sum is closest to target.
-Return the sum of the three integers.
-You may assume that each input would have exactly one solution.
-简单解释下就是之前是要三数之和等于目标值,现在是找到最接近的三数之和。
-示例
Example 1:
-Input: nums = [-1,2,1,-4], target = 1
Output: 2
Explanation: The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).
+ Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析
+ /2022/03/07/Leetcode-349-%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86-Intersection-of-Two-Arrays-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
+
+示例
+示例 1:
+输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
-Example 2:
-Input: nums = [0,0,0], target = 1
Output: 0
+
+示例 2:
+输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
+
-Constraints:
-3 <= nums.length <= 1000
--1000 <= nums[i] <= 1000
--10^4 <= target <= 10^4
+提示:
+1 <= nums1.length, nums2.length <= 1000
+0 <= nums1[i], nums2[i] <= 1000
-简单解析
这个题思路上来讲不难,也是用原来三数之和的方式去做,利用”双指针法”或者其它描述法,但是需要简化逻辑
-code
public int threeSumClosest(int[] nums, int target) {
- Arrays.sort(nums);
- // 当前最近的和
- int closestSum = nums[0] + nums[1] + nums[nums.length - 1];
- for (int i = 0; i < nums.length - 2; i++) {
- if (i == 0 || nums[i] != nums[i - 1]) {
- // 左指针
- int left = i + 1;
- // 右指针
- int right = nums.length - 1;
- // 判断是否遍历完了
- while (left < right) {
- // 当前的和
- int sum = nums[i] + nums[left] + nums[right];
- // 小优化,相等就略过了
- while (left < right && nums[left] == nums[left + 1]) {
- left++;
- }
- while (left < right && nums[right] == nums[right - 1]) {
- right--;
- }
- // 这里判断,其实也还是希望趋近目标值
- if (sum < target) {
- left++;
- } else {
- right--;
- }
- // 判断是否需要替换
- if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
- closestSum = sum;
- }
- }
- }
- }
- return closestSum;
- }
-
-结果
![]()
-]]>
-
- Java
- leetcode
-
-
- leetcode
- java
- 题解
- 3Sum Closest
-
-
-
- Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析
- /2022/07/02/Leetcode-20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7-Valid-Parentheses-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
-An input string is valid if:
-
-- Open brackets must be closed by the same type of brackets.
-- Open brackets must be closed in the correct order.
-
-示例
Example 1:
-Input: s = “()”
Output: true
-
-Example 2:
-Input: s = “()[]{}”
Output: true
-
-Example 3:
-Input: s = “(]”
Output: false
-
-Constraints:
-1 <= s.length <= 10^4
-s consists of parentheses only '()[]{}'.
-
-解析
easy题,并且看起来也是比较简单的,三种括号按对匹配,直接用栈来做,栈里面存的是括号的类型,如果是左括号,就放入栈中,如果是右括号,就把栈顶的元素弹出,如果弹出的元素不是左括号,就返回false,如果弹出的元素是左括号,就继续往下走,如果遍历完了,如果栈里面还有元素,就返回false,如果遍历完了,如果栈里面没有元素,就返回true
-代码
class Solution {
- public boolean isValid(String s) {
-
- if (s.length() % 2 != 0) {
- return false;
+分析与题解
两个数组的交集,最简单就是两层循环了把两个都存在的找出来,不过还有个要去重的问题,稍微思考下可以使用集合 set 来处理,先把一个数组全丢进去,再对比另外一个,如果出现在第一个集合里就丢进一个新的集合,最后转换成数组,这次我稍微取了个巧,因为看到了提示里的条件,两个数组中的元素都是不大于 1000 的,所以就搞了个 1000 长度的数组,如果在第一个数组出现,就在对应的下标设置成 1,如果在第二个数组也出现了就加 1,
+code
public int[] intersection(int[] nums1, int[] nums2) {
+ // 大小是 1000 的数组,如果没有提示的条件就没法这么做
+ // define a array which size is 1000, and can not be done like this without the condition in notice
+ int[] inter = new int[1000];
+ int[] outer;
+ int m = 0;
+ for (int j : nums1) {
+ // 这里得是设置成 1,因为有可能 nums1 就出现了重复元素,如果直接++会造成结果重复
+ // need to be set 1, cause element in nums1 can be duplicated
+ inter[j] = 1;
}
- Stack<String> stk = new Stack<>();
- for (int i = 0; i < s.length(); i++) {
- if (s.charAt(i) == '{' || s.charAt(i) == '(' || s.charAt(i) == '[') {
- stk.push(String.valueOf(s.charAt(i)));
- continue;
- }
- if (s.charAt(i) == '}') {
- if (stk.isEmpty()) {
- return false;
- }
- String cur = stk.peek();
- if (cur.charAt(0) != '{') {
- return false;
- } else {
- stk.pop();
- }
- continue;
- }
- if (s.charAt(i) == ']') {
- if (stk.isEmpty()) {
- return false;
- }
- String cur = stk.peek();
- if (cur.charAt(0) != '[') {
- return false;
- } else {
- stk.pop();
- }
- continue;
- }
- if (s.charAt(i) == ')') {
- if (stk.isEmpty()) {
- return false;
- }
- String cur = stk.peek();
- if (cur.charAt(0) != '(') {
- return false;
- } else {
- stk.pop();
- }
- continue;
+ for (int j : nums2) {
+ if (inter[j] > 0) {
+ // 这里可以直接+1,因为后面判断只需要判断大于 1
+ // just plus 1, cause we can judge with condition that larger than 1
+ inter[j] += 1;
}
-
- }
- return stk.size() == 0;
- }
-}
-
-]]>
-
- Java
- leetcode
-
-
- leetcode
- java
-
-
-
- Leetcode 2 Add Two Numbers 题解分析
- /2020/10/11/Leetcode-2-Add-Two-Numbers-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 又 roll 到了一个以前做过的题,不过现在用 Java 也来写一下,是 easy 级别的,所以就简单说下
-简要介绍
You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.
-You may assume the two numbers do not contain any leading zero, except the number 0 itself.
就是给了两个链表,用来表示两个非负的整数,在链表中倒序放着,每个节点包含一位的数字,把他们加起来以后也按照原来的链表结构输出
-样例
example 1
Input: l1 = [2,4,3], l2 = [5,6,4]
-Output: [7,0,8]
-Explanation: 342 + 465 = 807.
-
-example 2
Input: l1 = [0], l2 = [0]
-Output: [0]
-
-example 3
Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
-Output: [8,9,9,9,0,0,0,1]
-
-题解
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
- ListNode root = new ListNode();
- if (l1 == null && l2 == null) {
- return root;
}
- ListNode tail = root;
- int entered = 0;
- // 这个条件加了 entered,就是还有进位的数
- while (l1 != null || l2 != null || entered != 0) {
- int temp = entered;
- if (l1 != null) {
- temp += l1.val;
- l1 = l1.next;
- }
- if (l2 != null) {
- temp += l2.val;
- l2 = l2.next;
+ for (int i = 0; i < inter.length; i++) {
+ // 统计下元素数量
+ // count distinct elements
+ if (inter[i] > 1) {
+ m++;
}
- entered = (temp - temp % 10) / 10;
- tail.val = temp % 10;
- // 循环内部的控制是为了排除最后的空节点
- if (l1 != null || l2 != null || entered != 0) {
- tail.next = new ListNode();
- tail = tail.next;
+ }
+ // initial a array of size m
+ outer = new int[m];
+ m = 0;
+ for (int i = 0; i < inter.length; i++) {
+ if (inter[i] > 1) {
+ // add to outer
+ outer[m++] = i;
}
}
-// tail = null;
- return root;
- }
-这里唯二需要注意的就是两个点,一个是循环条件需要包含进位值还存在的情况,还有一个是最后一个节点,如果是空的了,就不要在 new 一个出来了,写的比较挫
-]]>
+ return outer;
+ }]]>
Java
leetcode
- java
- linked list
- linked list
leetcode
java
题解
- linked list
+ Intersection of Two Arrays
@@ -4137,131 +4376,6 @@ Output: [8,9,9,9,0,0,0,1]First Bad Version
-
- Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析
- /2021/01/10/Leetcode-160-%E7%9B%B8%E4%BA%A4%E9%93%BE%E8%A1%A8-intersection-of-two-linked-lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍写一个程序找出两个单向链表的交叉起始点,可能是我英语不好,图里画的其实还有一点是交叉以后所有节点都是相同的
Write a program to find the node at which the intersection of two singly linked lists begins.
-For example, the following two linked lists:
![]()
begin to intersect at node c1.
-Example 1:
![]()
-Input: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
-Output: Reference of the node with value = 8
-Input Explanation: The intersected node's value is 8 (note that this must not be 0 if the two lists intersect). From the head of A, it reads as [4,1,8,4,5]. From the head of B, it reads as [5,6,1,8,4,5]. There are 2 nodes before the intersected node in A; There are 3 nodes before the intersected node in B.
-分析题解
一开始没什么头绪,感觉只能最原始的遍历,后来看了一些文章,发现比较简单的方式就是先找两个链表的长度差,因为从相交点开始肯定是长度一致的,这是个很好的解题突破口,找到长度差以后就是先跳过长链表的较长部分,然后开始同步遍历比较 A,B 链表;
-代码
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
- if (headA == null || headB == null) {
- return null;
- }
- // 算 A 的长度
- int countA = 0;
- ListNode tailA = headA;
- while (tailA != null) {
- tailA = tailA.next;
- countA++;
- }
- // 算 B 的长度
- int countB = 0;
- ListNode tailB = headB;
- while (tailB != null) {
- tailB = tailB.next;
- countB++;
- }
- tailA = headA;
- tailB = headB;
- // 依据长度差,先让长的链表 tail 指针往后移
- if (countA > countB) {
- while (countA > countB) {
- tailA = tailA.next;
- countA--;
- }
- } else if (countA < countB) {
- while (countA < countB) {
- tailB = tailB.next;
- countB--;
- }
- }
- // 然后以相同速度遍历两个链表比较
- while (tailA != null) {
- if (tailA == tailB) {
- return tailA;
- } else {
- tailA = tailA.next;
- tailB = tailB.next;
- }
- }
- return null;
- }
-总结
可能缺少这种思维,做的还是比较少,所以没法一下子反应过来,需要锻炼,我的第一反应是两重遍历,不过那样复杂度就高了,这里应该是只有 O(N) 的复杂度。
-]]>
-
- Java
- leetcode
- Linked List
- java
- Linked List
-
-
- leetcode
- java
- 题解
- Linked List
-
-
-
- Leetcode 234 回文链表(Palindrome Linked List) 题解分析
- /2020/11/15/Leetcode-234-%E5%9B%9E%E6%96%87%E8%81%94%E8%A1%A8-Palindrome-Linked-List-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given a singly linked list, determine if it is a palindrome.
给定一个单向链表,判断是否是回文链表
-例一 Example 1:
Input: 1->2
Output: false
-例二 Example 2:
Input: 1->2->2->1
Output: true
-挑战下自己
Follow up:
Could you do it in O(n) time and O(1) space?
-简要分析
首先这是个单向链表,如果是双向的就可以一个从头到尾,一个从尾到头,显然那样就没啥意思了,然后想过要不找到中点,然后用一个栈,把前一半塞进栈里,但是这种其实也比较麻烦,比如长度是奇偶数,然后如何找到中点,这倒是可以借助于双指针,还是比较麻烦,再想一想,回文链表,就跟最开始的一样,链表只有单向的,我用个栈不就可以逆向了么,先把链表整个塞进栈里,然后在一个个 pop 出来跟链表从头开始比较,全对上了就是回文了
-/**
- * Definition for singly-linked list.
- * public class ListNode {
- * int val;
- * ListNode next;
- * ListNode() {}
- * ListNode(int val) { this.val = val; }
- * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
- * }
- */
-class Solution {
- public boolean isPalindrome(ListNode head) {
- if (head == null) {
- return true;
- }
- ListNode tail = head;
- LinkedList<Integer> stack = new LinkedList<>();
- // 这里就是一个循环,将所有元素依次压入栈
- while (tail != null) {
- stack.push(tail.val);
- tail = tail.next;
- }
- // 在逐个 pop 出来,其实这个出来的顺序就等于链表从尾到头遍历,同时跟链表从头到尾遍历进行逐对对比
- while (!stack.isEmpty()) {
- if (stack.peekFirst() == head.val) {
- stack.pollFirst();
- head = head.next;
- } else {
- return false;
- }
- }
- return true;
- }
-}
]]>
-
- Java
- leetcode
- Linked List
- java
- Linked List
-
-
- leetcode
- java
- 题解
- Linked List
-
-
Leetcode 3 Longest Substring Without Repeating Characters 题解分析
/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
@@ -4332,60 +4446,65 @@ Output: 0<
- Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析
- /2022/03/07/Leetcode-349-%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86-Intersection-of-Two-Arrays-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
-
-示例
-示例 1:
-输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
+ Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析
+ /2022/03/27/Leetcode-4-%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0-Median-of-Two-Sorted-Arrays-Hard-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
+算法的时间复杂度应该为 O(log (m+n)) 。
+示例 1:
+输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
-
-示例 2:
-输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
-
+示例 2:
+输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
-提示:
-1 <= nums1.length, nums2.length <= 1000
-0 <= nums1[i], nums2[i] <= 1000
-
-分析与题解
两个数组的交集,最简单就是两层循环了把两个都存在的找出来,不过还有个要去重的问题,稍微思考下可以使用集合 set 来处理,先把一个数组全丢进去,再对比另外一个,如果出现在第一个集合里就丢进一个新的集合,最后转换成数组,这次我稍微取了个巧,因为看到了提示里的条件,两个数组中的元素都是不大于 1000 的,所以就搞了个 1000 长度的数组,如果在第一个数组出现,就在对应的下标设置成 1,如果在第二个数组也出现了就加 1,
-code
public int[] intersection(int[] nums1, int[] nums2) {
- // 大小是 1000 的数组,如果没有提示的条件就没法这么做
- // define a array which size is 1000, and can not be done like this without the condition in notice
- int[] inter = new int[1000];
- int[] outer;
- int m = 0;
- for (int j : nums1) {
- // 这里得是设置成 1,因为有可能 nums1 就出现了重复元素,如果直接++会造成结果重复
- // need to be set 1, cause element in nums1 can be duplicated
- inter[j] = 1;
- }
- for (int j : nums2) {
- if (inter[j] > 0) {
- // 这里可以直接+1,因为后面判断只需要判断大于 1
- // just plus 1, cause we can judge with condition that larger than 1
- inter[j] += 1;
- }
+分析与题解
这个题也是我随机出来的,之前都是随机到 easy 的,而且是序号这么靠前的,然后翻一下,之前应该是用 C++做过的,具体的方法其实可以从他的算法时间复杂度要求看出来,大概率是要二分法这种,后面就结合代码来讲了
+public double findMedianSortedArrays(int[] nums1, int[] nums2) {
+ int n1 = nums1.length;
+ int n2 = nums2.length;
+ if (n1 > n2) {
+ return findMedianSortedArrays(nums2, nums1);
}
- for (int i = 0; i < inter.length; i++) {
- // 统计下元素数量
- // count distinct elements
- if (inter[i] > 1) {
- m++;
+
+ // 找到两个数组的中点下标
+ int k = (n1 + n2 + 1 ) / 2;
+ // 使用一个类似于二分法的查找方法
+ // 起始值就是 num1 的头跟尾
+ int left = 0;
+ int right = n1;
+ while (left < right) {
+ // m1 表示我取的是 nums1 的中点,即二分法的方式
+ int m1 = left + (right - left) / 2;
+ // *** 这里是重点,因为这个问题也可以转换成找成 n1 + n2 那么多个数中的前 (n1 + n2 + 1) / 2 个
+ // *** 因为两个数组都是排好序的,那么我从 num1 中取了 m1 个,从 num2 中就是去 k - m1 个
+ // *** 但是不知道取出来大小是否正好是整体排序的第 (n1 + n2 + 1) / 2 个,所以需要二分法上下对比
+ int m2 = k - m1;
+ // 如果 nums1[m1] 小,那我在第一个数组 nums1 的二分查找就要把左端点改成前一次的中点 + 1 (不然就进死循环了
+ if (nums1[m1] < nums2[m2 - 1]) {
+ left = m1 + 1;
+ } else {
+ right = m1;
}
}
- // initial a array of size m
- outer = new int[m];
- m = 0;
- for (int i = 0; i < inter.length; i++) {
- if (inter[i] > 1) {
- // add to outer
- outer[m++] = i;
- }
+
+ // 因为对比后其实我们只是拿到了一个位置,具体哪个是第 k 个就需要继续判断
+ int m1 = left;
+ int m2 = k - left;
+ // 如果 m1 或者 m2 有小于等于 0 的,那这个值可以先抛弃
+ // m1 如果等于 0,就是 num1[0] 都比 nums2 中所有值都要大
+ // m2 等于 0 的话 刚好相反
+ // 可以这么推断,当其中一个是 0 的时候那么另一个 mx 值肯定是> 0 的,那么就是取的对应的这个下标的值
+ int c1 = Math.max( m1 <= 0 ? Integer.MIN_VALUE : nums1[m1 - 1] , m2 <= 0 ? Integer.MIN_VALUE : nums2[m2 - 1]);
+ // 如果两个数组的元素数量和是奇数,那就直接可以返回了,因为 m1 + m2 就是 k, 如果是一个数组,那这个元素其实就是 nums[k - 1]
+ // 如果 m1 或者 m2 是 0,那另一个就是 k,取 mx - 1的下标就等于是 k - 1
+ // 如果都不是 0,那就是取的了 nums1[m1 - 1] 与 nums2[m2 - 1]中的较大者,如果取得是后者,那么也就是 m1 + m2 - 1 的下标就是 k - 1
+ if ((n1 + n2) % 2 == 1) {
+ return c1;
}
- return outer;
- }
]]>
+ // 如果是偶数个,那还要取两个数组后面的较小者,然后求平均值
+ int c2 = Math.min(m1 >= n1 ? Integer.MAX_VALUE : nums1[m1], m2 >= n2 ? Integer.MAX_VALUE : nums2[m2]);
+ return (c1 + c2) / 2.0;
+ }
+前面考虑的方法还是比较繁琐,考虑了两个数组的各种交叉情况,后面这个参考了一些网上的解法,代码比较简洁,但是可能不容易一下子就搞明白,所以配合了比较多的注释。
+]]>
Java
leetcode
@@ -4394,55 +4513,65 @@ Output: 0<
leetcode
java
题解
- Intersection of Two Arrays
+ Median of Two Sorted Arrays
- Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析
- /2021/05/23/Leetcode-236-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88-Lowest-Common-Ancestor-of-a-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
-According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”
-给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
-百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
-代码
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
- // 如果当前节点就是 p 或者是 q 的时候,就直接返回了
- // 当没找到,即 root == null 的时候也会返回 null,这是个重要的点
- if (root == null || root == p || root == q) return root;
- // 在左子树中找 p 和 q
- TreeNode left = lowestCommonAncestor(root.left, p, q);
- // 在右子树中找 p 和 q
- TreeNode right = lowestCommonAncestor(root.right, p, q);
- // 当左边是 null 就直接返回右子树,但是这里不表示右边不是 null,所以这个顺序是不影响的
- // 考虑一种情况,如果一个节点的左右子树都是 null,那么其实对于这个节点来说首先两个子树分别调用
- // lowestCommonAncestor会在开头就返回 null,那么就是上面 left 跟 right 都是 null,然后走下面的判断的时候
- // 其实第一个 if 就返回了 null,如此递归返回就能达到当子树中没有找到 p 或者 q 的时候只返回 null
- if (left == null) {
- return right;
- } else if (right == null) {
- return left;
- } else {
- return root;
+ Leetcode 48 旋转图像(Rotate Image) 题解分析
+ /2021/05/01/Leetcode-48-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F-Rotate-Image-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
+You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.
![]()
如图,这道题以前做过,其实一看有点蒙,好像规则很容易描述,但是代码很难写,因为要类似于贪吃蛇那样,后来想着应该会有一些特殊的技巧,比如翻转等
+代码
直接上码
+public void rotate(int[][] matrix) {
+ // 这里真的傻了,长宽应该是一致的,所以取一次就够了
+ int lengthX = matrix[0].length;
+ int lengthY = matrix.length;
+ int temp;
+ System.out.println(lengthY - (lengthY % 2) / 2);
+ // 这里除错了,应该是减掉余数再除 2
+// for (int i = 0; i < lengthY - (lengthY % 2) / 2; i++) {
+ /**
+ * 1 2 3 7 8 9
+ * 4 5 6 => 4 5 6 先沿着 4 5 6 上下交换
+ * 7 8 9 1 2 3
+ */
+ for (int i = 0; i < (lengthY - (lengthY % 2)) / 2; i++) {
+ for (int j = 0; j < lengthX; j++) {
+ temp = matrix[i][j];
+ matrix[i][j] = matrix[lengthY-i-1][j];
+ matrix[lengthY-i-1][j] = temp;
+ }
}
-// if (right == null) {
-// return left;
-// } else if (left == null) {
-// return right;
-// } else {
-// return root;
-// }
-// return left == null ? right : right == null ? left : root;
- }
+
+ /**
+ * 7 8 9 7 4 1
+ * 4 5 6 => 8 5 2 这里再沿着 7 5 3 这条对角线交换
+ * 1 2 3 9 6 3
+ */
+ for (int i = 0; i < lengthX; i++) {
+ for (int j = 0; j <= i; j++) {
+ if (i == j) {
+ continue;
+ }
+ temp = matrix[i][j];
+ matrix[i][j] = matrix[j][i];
+ matrix[j][i] = temp;
+ }
+ }
+ }
+还没到可以直接归纳题目类型的水平,主要是几年前做过,可能有那么点模糊的记忆,当然应该也有直接转的方法
]]>
Java
leetcode
- Lowest Common Ancestor of a Binary Tree
+ Rotate Image
leetcode
java
题解
- Lowest Common Ancestor of a Binary Tree
+ Rotate Image
+ 矩阵
@@ -4520,83 +4649,12 @@ maxR[n -
- Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析
- /2022/03/27/Leetcode-4-%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0-Median-of-Two-Sorted-Arrays-Hard-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
-算法的时间复杂度应该为 O(log (m+n)) 。
-示例 1:
-输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
-
-示例 2:
-输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
-
-分析与题解
这个题也是我随机出来的,之前都是随机到 easy 的,而且是序号这么靠前的,然后翻一下,之前应该是用 C++做过的,具体的方法其实可以从他的算法时间复杂度要求看出来,大概率是要二分法这种,后面就结合代码来讲了
-public double findMedianSortedArrays(int[] nums1, int[] nums2) {
- int n1 = nums1.length;
- int n2 = nums2.length;
- if (n1 > n2) {
- return findMedianSortedArrays(nums2, nums1);
- }
-
- // 找到两个数组的中点下标
- int k = (n1 + n2 + 1 ) / 2;
- // 使用一个类似于二分法的查找方法
- // 起始值就是 num1 的头跟尾
- int left = 0;
- int right = n1;
- while (left < right) {
- // m1 表示我取的是 nums1 的中点,即二分法的方式
- int m1 = left + (right - left) / 2;
- // *** 这里是重点,因为这个问题也可以转换成找成 n1 + n2 那么多个数中的前 (n1 + n2 + 1) / 2 个
- // *** 因为两个数组都是排好序的,那么我从 num1 中取了 m1 个,从 num2 中就是去 k - m1 个
- // *** 但是不知道取出来大小是否正好是整体排序的第 (n1 + n2 + 1) / 2 个,所以需要二分法上下对比
- int m2 = k - m1;
- // 如果 nums1[m1] 小,那我在第一个数组 nums1 的二分查找就要把左端点改成前一次的中点 + 1 (不然就进死循环了
- if (nums1[m1] < nums2[m2 - 1]) {
- left = m1 + 1;
- } else {
- right = m1;
- }
- }
-
- // 因为对比后其实我们只是拿到了一个位置,具体哪个是第 k 个就需要继续判断
- int m1 = left;
- int m2 = k - left;
- // 如果 m1 或者 m2 有小于等于 0 的,那这个值可以先抛弃
- // m1 如果等于 0,就是 num1[0] 都比 nums2 中所有值都要大
- // m2 等于 0 的话 刚好相反
- // 可以这么推断,当其中一个是 0 的时候那么另一个 mx 值肯定是> 0 的,那么就是取的对应的这个下标的值
- int c1 = Math.max( m1 <= 0 ? Integer.MIN_VALUE : nums1[m1 - 1] , m2 <= 0 ? Integer.MIN_VALUE : nums2[m2 - 1]);
- // 如果两个数组的元素数量和是奇数,那就直接可以返回了,因为 m1 + m2 就是 k, 如果是一个数组,那这个元素其实就是 nums[k - 1]
- // 如果 m1 或者 m2 是 0,那另一个就是 k,取 mx - 1的下标就等于是 k - 1
- // 如果都不是 0,那就是取的了 nums1[m1 - 1] 与 nums2[m2 - 1]中的较大者,如果取得是后者,那么也就是 m1 + m2 - 1 的下标就是 k - 1
- if ((n1 + n2) % 2 == 1) {
- return c1;
- }
- // 如果是偶数个,那还要取两个数组后面的较小者,然后求平均值
- int c2 = Math.min(m1 >= n1 ? Integer.MAX_VALUE : nums1[m1], m2 >= n2 ? Integer.MAX_VALUE : nums2[m2]);
- return (c1 + c2) / 2.0;
- }
-前面考虑的方法还是比较繁琐,考虑了两个数组的各种交叉情况,后面这个参考了一些网上的解法,代码比较简洁,但是可能不容易一下子就搞明白,所以配合了比较多的注释。
-]]>
-
- Java
- leetcode
-
-
- leetcode
- java
- 题解
- Median of Two Sorted Arrays
-
-
-
- Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析
- /2022/06/19/Leetcode-698-%E5%88%92%E5%88%86%E4%B8%BAk%E4%B8%AA%E7%9B%B8%E7%AD%89%E7%9A%84%E5%AD%90%E9%9B%86-Partition-to-K-Equal-Sum-Subsets-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍Given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.
-示例
Example 1:
-
-Input: nums = [4,3,2,3,5,2,1], k = 4
Output: true
Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.
+ Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析
+ /2022/06/19/Leetcode-698-%E5%88%92%E5%88%86%E4%B8%BAk%E4%B8%AA%E7%9B%B8%E7%AD%89%E7%9A%84%E5%AD%90%E9%9B%86-Partition-to-K-Equal-Sum-Subsets-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.
+示例
Example 1:
+
+Input: nums = [4,3,2,3,5,2,1], k = 4
Output: true
Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.
Example 2:
@@ -4688,64 +4746,6 @@ maxR[n -java
-
- Leetcode 48 旋转图像(Rotate Image) 题解分析
- /2021/05/01/Leetcode-48-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F-Rotate-Image-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
-You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.
![]()
如图,这道题以前做过,其实一看有点蒙,好像规则很容易描述,但是代码很难写,因为要类似于贪吃蛇那样,后来想着应该会有一些特殊的技巧,比如翻转等
-代码
直接上码
-public void rotate(int[][] matrix) {
- // 这里真的傻了,长宽应该是一致的,所以取一次就够了
- int lengthX = matrix[0].length;
- int lengthY = matrix.length;
- int temp;
- System.out.println(lengthY - (lengthY % 2) / 2);
- // 这里除错了,应该是减掉余数再除 2
-// for (int i = 0; i < lengthY - (lengthY % 2) / 2; i++) {
- /**
- * 1 2 3 7 8 9
- * 4 5 6 => 4 5 6 先沿着 4 5 6 上下交换
- * 7 8 9 1 2 3
- */
- for (int i = 0; i < (lengthY - (lengthY % 2)) / 2; i++) {
- for (int j = 0; j < lengthX; j++) {
- temp = matrix[i][j];
- matrix[i][j] = matrix[lengthY-i-1][j];
- matrix[lengthY-i-1][j] = temp;
- }
- }
-
- /**
- * 7 8 9 7 4 1
- * 4 5 6 => 8 5 2 这里再沿着 7 5 3 这条对角线交换
- * 1 2 3 9 6 3
- */
- for (int i = 0; i < lengthX; i++) {
- for (int j = 0; j <= i; j++) {
- if (i == j) {
- continue;
- }
- temp = matrix[i][j];
- matrix[i][j] = matrix[j][i];
- matrix[j][i] = temp;
- }
- }
- }
-还没到可以直接归纳题目类型的水平,主要是几年前做过,可能有那么点模糊的记忆,当然应该也有直接转的方法
-]]>
-
- Java
- leetcode
- Rotate Image
-
-
- leetcode
- java
- 题解
- Rotate Image
- 矩阵
-
-
Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析
/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/
@@ -4798,6 +4798,114 @@ maxR[n -Remove Duplicates from Sorted List
+
+ Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析
+ /2022/08/23/Leetcode-885-%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5-III-Spiral-Matrix-III-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍You start at the cell (rStart, cStart) of an rows x cols grid facing east. The northwest corner is at the first row and column in the grid, and the southeast corner is at the last row and column.
+You will walk in a clockwise spiral shape to visit every position in this grid. Whenever you move outside the grid’s boundary, we continue our walk outside the grid (but may return to the grid boundary later.). Eventually, we reach all rows * cols spaces of the grid.
+Return an array of coordinates representing the positions of the grid in the order you visited them.
+Example 1:
+Input: rows = 1, cols = 4, rStart = 0, cStart = 0
Output: [[0,0],[0,1],[0,2],[0,3]]
+
+Example 2:
+Input: rows = 5, cols = 6, rStart = 1, cStart = 4
Output: [[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]]
+
+Constraints:
+
+1 <= rows, cols <= 100
+0 <= rStart < rows
+0 <= cStart < cols
+
+简析
这个题主要是要相同螺旋矩阵的转变方向的边界判断,已经相同步长会行进两次这个规律,写代码倒不复杂
+代码
public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
+ int size = rows * cols;
+ int x = rStart, y = cStart;
+ // 返回的二维矩阵
+ int[][] matrix = new int[size][2];
+ // 传入的参数就是入口第一个
+ matrix[0][0] = rStart;
+ matrix[0][1] = cStart;
+ // 作为数量
+ int z = 1;
+ // 步进,1,1,2,2,3,3,4 ... 螺旋矩阵的增长
+ int a = 1;
+ // 方向 1 表示右,2 表示下,3 表示左,4 表示上
+ int dir = 1;
+ while (z < size) {
+ for (int i = 0; i < 2; i++) {
+ for (int j= 0; j < a; j++) {
+ // 处理方向
+ if (dir % 4 == 1) {
+ y++;
+ } else if (dir % 4 == 2) {
+ x++;
+ } else if (dir % 4 == 3) {
+ y--;
+ } else {
+ x--;
+ }
+ // 如果在实际矩阵内
+ if (x < rows && y < cols && x >= 0 && y >= 0) {
+ matrix[z][0] = x;
+ matrix[z][1] = y;
+ z++;
+ }
+ }
+ // 转变方向
+ dir++;
+ }
+ // 步进++
+ a++;
+ }
+ return matrix;
+ }
+
+结果
![]()
+]]>
+
+ Java
+ leetcode
+
+
+ leetcode
+ java
+ 题解
+
+
+
+ Linux 下 grep 命令的一点小技巧
+ /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/
+ 用了比较久的 grep 命令,其实都只是用了最最基本的功能来查日志,
+譬如
+
+grep 'xxx' xxxx.log
+
+
+然后有挺多情况比如想要找日志里带一些符号什么的,就需要用到一些特殊的
+比如这样\"userId\":\"123456\",因为比如用户 ID 有时候会跟其他的 id 一样,只用具体的值 123456 来查的话干扰信息太多了,如果直接这样
+
+grep '\"userId\":\"123456\"' xxxx.log
+
+
+好像不行,盲猜就是符号的问题,特别是\和"这两个,
+之前一直是想试一下,但是没成功,昨天在排查一个问题的时候发现了,只要把这些都转义了就行了
+grep '\\\"userId\\\":\\\"123456\\\"' xxxx.log
+![]()
+]]>
+
+ Linux
+ 命令
+ 小技巧
+ grep
+ grep
+ 查日志
+
+
+ linux
+ grep
+ 转义
+
+
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/
@@ -5184,61 +5292,6 @@ maxR[n -c++
-
- Linux 下 grep 命令的一点小技巧
- /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/
- 用了比较久的 grep 命令,其实都只是用了最最基本的功能来查日志,
-譬如
-
-grep 'xxx' xxxx.log
-
-
-然后有挺多情况比如想要找日志里带一些符号什么的,就需要用到一些特殊的
-比如这样\"userId\":\"123456\",因为比如用户 ID 有时候会跟其他的 id 一样,只用具体的值 123456 来查的话干扰信息太多了,如果直接这样
-
-grep '\"userId\":\"123456\"' xxxx.log
-
-
-好像不行,盲猜就是符号的问题,特别是\和"这两个,
-之前一直是想试一下,但是没成功,昨天在排查一个问题的时候发现了,只要把这些都转义了就行了
-grep '\\\"userId\\\":\\\"123456\\\"' xxxx.log
-![]()
-]]>
-
- Linux
- 命令
- 小技巧
- grep
- grep
- 查日志
-
-
- linux
- grep
- 转义
-
-
-
- MFC 模态对话框
- /2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/
- void CTestDialog::OnBnClickedOk()
-{
- CString m_SrcTest;
- int nIndex = m_CbTest.GetCurSel();
- m_CbTest.GetLBText(nIndex, m_SrcTest);
- OnOK();
-}
-
-模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。
-]]>
-
- C++
-
-
- c++
- mfc
-
-
Maven实用小技巧
/2020/02/16/Maven%E5%AE%9E%E7%94%A8%E5%B0%8F%E6%8A%80%E5%B7%A7/
@@ -5317,6 +5370,88 @@ OS name: "mac os x", version: "10.14.6", arch: "x86_64&
Maven
+
+ 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
+
+
+ leetcode
+ c++
+
+
+
+ Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析
+ /2022/10/02/Leetcode-747-%E8%87%B3%E5%B0%91%E6%98%AF%E5%85%B6%E4%BB%96%E6%95%B0%E5%AD%97%E4%B8%A4%E5%80%8D%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0-Largest-Number-At-Least-Twice-of-Others-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍You are given an integer array nums where the largest integer is unique.
+Determine whether the largest element in the array is at least twice as much as every other number in the array. If it is, return the index of the largest element, or return -1 otherwise.
确认在数组中的最大数是否是其余任意数的两倍大及以上,如果是返回索引,如果不是返回-1
+示例
Example 1:
+Input: nums = [3,6,1,0]
Output: 1
Explanation: 6 is the largest integer.
For every other number in the array x, 6 is at least twice as big as x.
The index of value 6 is 1, so we return 1.
+
+Example 2:
+Input: nums = [1,2,3,4]
Output: -1
Explanation: 4 is less than twice the value of 3, so we return -1.
+
+提示:
+2 <= nums.length <= 50
+0 <= nums[i] <= 100
+- The largest element in
nums is unique.
+
+简要解析
这个题easy是题意也比较简单,找最大值,并且最大值是其他任意值的两倍及以上,其实就是找最大值跟次大值,比较下就好了
+代码
public int dominantIndex(int[] nums) {
+ int largest = Integer.MIN_VALUE;
+ int second = Integer.MIN_VALUE;
+ int largestIndex = -1;
+ for (int i = 0; i < nums.length; i++) {
+ // 如果有最大的就更新,同时更新最大值和第二大的
+ if (nums[i] > largest) {
+ second = largest;
+ largest = nums[i];
+ largestIndex = i;
+ } else if (nums[i] > second) {
+ // 没有超过最大的,但是比第二大的更大就更新第二大的
+ second = nums[i];
+ }
+ }
+
+ // 判断下是否符合题目要求,要是所有值的两倍及以上
+ if (largest >= 2 * second) {
+ return largestIndex;
+ } else {
+ return -1;
+ }
+}
+通过图
第一次错了是把第二大的情况只考虑第一种,也有可能最大值完全没经过替换就变成最大值了
![]()
+]]>
+
+ Java
+ leetcode
+
+
+ leetcode
+ java
+ 题解
+
+
Path Sum
/2015/01/04/Path-Sum/
@@ -5369,36 +5504,6 @@ public:
c++
-
- 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
-
-
- leetcode
- c++
-
-
Redis_分布式锁
/2019/12/10/Redis-Part-1/
@@ -5465,80 +5570,6 @@ public:
c++
-
- Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析
- /2022/08/23/Leetcode-885-%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5-III-Spiral-Matrix-III-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍You start at the cell (rStart, cStart) of an rows x cols grid facing east. The northwest corner is at the first row and column in the grid, and the southeast corner is at the last row and column.
-You will walk in a clockwise spiral shape to visit every position in this grid. Whenever you move outside the grid’s boundary, we continue our walk outside the grid (but may return to the grid boundary later.). Eventually, we reach all rows * cols spaces of the grid.
-Return an array of coordinates representing the positions of the grid in the order you visited them.
-Example 1:
-Input: rows = 1, cols = 4, rStart = 0, cStart = 0
Output: [[0,0],[0,1],[0,2],[0,3]]
-
-Example 2:
-Input: rows = 5, cols = 6, rStart = 1, cStart = 4
Output: [[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]]
-
-Constraints:
-
-1 <= rows, cols <= 100
-0 <= rStart < rows
-0 <= cStart < cols
-
-简析
这个题主要是要相同螺旋矩阵的转变方向的边界判断,已经相同步长会行进两次这个规律,写代码倒不复杂
-代码
public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
- int size = rows * cols;
- int x = rStart, y = cStart;
- // 返回的二维矩阵
- int[][] matrix = new int[size][2];
- // 传入的参数就是入口第一个
- matrix[0][0] = rStart;
- matrix[0][1] = cStart;
- // 作为数量
- int z = 1;
- // 步进,1,1,2,2,3,3,4 ... 螺旋矩阵的增长
- int a = 1;
- // 方向 1 表示右,2 表示下,3 表示左,4 表示上
- int dir = 1;
- while (z < size) {
- for (int i = 0; i < 2; i++) {
- for (int j= 0; j < a; j++) {
- // 处理方向
- if (dir % 4 == 1) {
- y++;
- } else if (dir % 4 == 2) {
- x++;
- } else if (dir % 4 == 3) {
- y--;
- } else {
- x--;
- }
- // 如果在实际矩阵内
- if (x < rows && y < cols && x >= 0 && y >= 0) {
- matrix[z][0] = x;
- matrix[z][1] = y;
- z++;
- }
- }
- // 转变方向
- dir++;
- }
- // 步进++
- a++;
- }
- return matrix;
- }
-
-结果
![]()
-]]>
-
- Java
- leetcode
-
-
- leetcode
- java
- 题解
-
-
Reverse Integer
/2015/03/13/Reverse-Integer/
@@ -5583,6 +5614,27 @@ public:
c++
+
+ MFC 模态对话框
+ /2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/
+ void CTestDialog::OnBnClickedOk()
+{
+ CString m_SrcTest;
+ int nIndex = m_CbTest.GetCurSel();
+ m_CbTest.GetLBText(nIndex, m_SrcTest);
+ OnOK();
+}
+
+模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。
+]]>
+
+ C++
+
+
+ c++
+ mfc
+
+
ambari-summary
/2017/05/09/ambari-summary/
@@ -5601,75 +5653,6 @@ public:
cluster
-
- two sum
- /2015/01/14/Two-Sum/
- problemGiven an array of integers, find two numbers such that they add up to a specific target number.
-The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.
-
-You may assume that each input would have exactly one solution.
-Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
-code
struct Node
-{
- int num, pos;
-};
-bool cmp(Node a, Node b)
-{
- return a.num < b.num;
-}
-class Solution {
-public:
- vector<int> twoSum(vector<int> &numbers, int target) {
- // Start typing your C/C++ solution below
- // DO NOT write int main() function
- vector<int> result;
- vector<Node> array;
- for (int i = 0; i < numbers.size(); i++)
- {
- Node temp;
- temp.num = numbers[i];
- temp.pos = i;
- array.push_back(temp);
- }
-
- sort(array.begin(), array.end(), cmp);
- for (int i = 0, j = array.size() - 1; i != j;)
- {
- int sum = array[i].num + array[j].num;
- if (sum == target)
- {
- if (array[i].pos < array[j].pos)
- {
- result.push_back(array[i].pos + 1);
- result.push_back(array[j].pos + 1);
- } else
- {
- result.push_back(array[j].pos + 1);
- result.push_back(array[i].pos + 1);
- }
- break;
- } else if (sum < target)
- {
- i++;
- } else if (sum > target)
- {
- j--;
- }
- }
- return result;
- }
-};
-
-Analysis
sort the array, then test from head and end, until catch the right answer
-]]>
-
- leetcode
-
-
- leetcode
- c++
-
-
binary-watch
/2016/09/29/binary-watch/
@@ -5735,6 +5718,62 @@ master_port=3306; //如果是同一宿主机
mysql
+
+ docker比一般多一点的初学者介绍
+ /2020/03/08/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D/
+ 因为最近想搭一个phabricator用来做看板和任务管理,一开始了解这个是Easy大大有在微博推荐过,后来苏洋也在群里和博客里说到了,看上去还不错的样子,因为主角是docker所以就不介绍太多,后面有机会写一下。
+docker最开始是之前在某位大佬的博客看到的,看上去有点神奇,感觉是一种轻量级的虚拟机,但是能做的事情好像差不多,那时候是在Ubuntu系统的vps里起一个Ubuntu的docker,然后在里面装个nginx,配置端口映射就可以访问了,后来也草草写过一篇使用docker搭建mysql集群,但是最近看了下好像是因为装docker的大佬做了一些别名还是什么操作,导致里面用的操作都不具有普遍性,而且主要是把搭的过程写了下,属于囫囵吞枣,没理解docker是干啥的,为啥用docker,就是操作了下,这几天借着搭phabricator的过程,把一些原来不理解,或者原来理解错误的地方重新理一下。
+之前写的 mysql 集群,一主二备,这种架构在很多小型应用里都是这么配置的,而且一般是直接在三台 vps 里启动三个 mysql 实例,但是如果换成 docker 会有什么好处呢,其实就是方便部署,比如其中一台备库挂了,我要加一台,或者说备库的 qps 太高了,需要再加一个,如果要在 vps 上搭建的话,首先要买一台机器,等初始化,然后在上面修改源,更新,装 mysql ,然后配置主从,可能还要处理防火墙等等,如果把这些打包成一个 docker 镜像,并且放在自己的 docker registry,那就直接run 一下就可以了;还有比如在公司要给一个新同学整一套开发测试环境,以 Java 开发为例,要装 git,maven,jdk,配置 maven settings 和各种 rc,整合在一个镜像里的话,就会很方便了;再比如微服务的水平扩展。
+但是为啥 docker 会有这种优势,听起来好像虚拟机也可以干这个事,但是虚拟机动辄上 G,而且需要 VMware,virtual box 等支持,不适合在Linux服务器环境使用,而且占用资源也会非常大。说得这么好,那么 docker 是啥呢
+docker 主要使用 Linux 中已经存在的两种技术的一个整合升级,一个是 namespace,一个是cgroups,相比于虚拟机需要完整虚拟出一个操作系统运行基础,docker 基于宿主机内核,通过 namespace 和 cgroups 分隔进程,理念就是提供一个隔离的最小化运行依赖,这样子相对于虚拟机就有了巨大的便利性,具体的 namespace 和 cgroups 就先不展开讲,可以参考耗子叔的文章
+安装
那么我们先安装下 docker,参考官方的教程,安装,我的系统是 ubuntu 的,就贴了 ubuntu 的链接,用其他系统的可以找到对应的系统文档安装,安装完了的话看看 docker 的信息
+sudo docker info
+
+输出以下信息
![]()
+简单运行
然后再来运行个 hello world 呗,
+sudo docker run hello-world
+
+输出了这些
![]()
+看看这个运行命令是怎么用的,一般都会看到这样子的,sudo docker run -it ubuntu bash, 前面的 docker run 反正就是运行一个容器的意思,-it是啥呢,还有这个什么 ubuntu bash,来看看docker run`的命令帮助信息
+-i, --interactive Keep STDIN open even if not attached
+
+就是要有输入,我们运行的时候能输入
+-t, --tty Allocate a pseudo-TTY
+
+要有个虚拟终端,
+Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
+
+Run a command in a new container
+
+镜像
上面说的-it 就是这里的 options,后面那个 ubuntu 就是 image 辣,image 是啥呢
+Docker 把应用程序及其依赖,打包在 image 文件里面,可以把它理解成为类似于虚拟机的镜像或者运行一个进程的代码,跑起来了的叫docker 容器或者进程,比如我们将要运行的docker run -it ubuntu bash的ubuntu 就是个 ubuntu 容器的镜像,将这个镜像运行起来后,我们可以进入容器像使用 ubuntu 一样使用它,来看下我们的镜像,使用sudo docker image ls就能列出我们宿主机上的 docker 镜像了
+![]()
+一个 ubuntu 镜像才 64MB,非常小巧,然后是后面的bash,我通过交互式启动了一个 ubuntu 容器,然后在这个启动的容器里运行了 bash 命令,这样就可以在容器里玩一下了
+在容器里看下进程,
![]()
+只有刚才运行容器的 bash 进程和我刚执行的 ps,这里有个可以注意下的,bash 这个进程的 pid 是 1,其实这里就用到了 linux 中的PID Namespace,容器会隔离出一个 pid 的名字空间,这里面的进程跟外部的 pid 命名独立
+查看宿主机上的容器
sudo docker ps -a
+
+![]()
+如何进入一个正在运行中的 docker 容器
这个应该是比较常用的,因为比如是一个微服务容器,有时候就像看下运行状态,日志啥的
+sudo docker exec -it [containerID] bash
+
+![]()
+查看日志
sudo docker logs [containerID]
+
+我在运行容器的终端里胡乱输入点啥,然后通过上面的命令就可以看到啦
+![]()
+![]()
+]]>
+
+ Docker
+ 介绍
+
+
+ Docker
+ namespace
+ cgroup
+
+
docker比一般多一点的初学者介绍三
/2020/03/21/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%B8%89/
@@ -5772,43 +5811,72 @@ EXPOSE 80<
- docker比一般多一点的初学者介绍二
- /2020/03/15/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%BA%8C/
- 限制下 docker 的 cpu 使用率这里我们开始玩一点有意思的,我们在容器里装下 vim 和 gcc,然后写这样一段 c 代码
-#include <stdio.h>
-int main(void)
+ two sum
+ /2015/01/14/Two-Sum/
+ problemGiven an array of integers, find two numbers such that they add up to a specific target number.
+The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.
+
+You may assume that each input would have exactly one solution.
+Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
+code
struct Node
{
- int i = 0;
- for(;;) i++;
- return 0;
-}
-就是一个最简单的死循环,然后在容器里跑起来
-$ gcc 1.c
-$ ./a.out
-然后我们来看下系统资源占用(CPU)
![Xs562iawhHyMxeO]()
上图是在容器里的,可以看到 cpu 已经 100%了
然后看看容器外面的
![ecqH8XJ4k7rKhzu]()
可以看到一个核的 cpu 也被占满了,因为是个双核的机器,并且代码是单线程的
然后呢我们要做点啥
因为已经在这个 ubuntu 容器中装了 vim 和 gcc,考虑到国内的网络,所以我们先把这个容器 commit 一下,
-docker commit -a "nick" -m "my ubuntu" f63c5607df06 my_ubuntu:v1
-然后再运行起来
-docker run -it --cpus=0.1 my_ubuntu:v1 bash
-![]()
我们的代码跟可执行文件都还在,要的就是这个效果,然后再运行一下
![]()
结果是这个样子的,有点神奇是不,关键就在于 run 的时候的--cpus=0.1这个参数,它其实就是基于我前一篇说的 cgroup 技术,能将进程之间的cpu,内存等资源进行隔离
-开始第一个 Dockerfile
上一面为了复用那个我装了 vim 跟 gcc 的容器,我把它提交到了本地,使用了docker commit命令,有点类似于 git 的 commit,但是这个不是个很好的操作方式,需要手动介入,这里更推荐使用 Dockerfile 来构建镜像
-From ubuntu:latest
-MAINTAINER Nicksxs "nicksxs@hotmail.com"
-RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
-RUN apt-get clean
-RUN apt-get update && apt install -y nginx
-RUN echo 'Hi, i am in container' \
- > /usr/share/nginx/html/index.html
-EXPOSE 80
-先解释下这是在干嘛,首先是这个From ubuntu:latest基于的 ubuntu 的最新版本的镜像,然后第二行是维护人的信息,第三四行么作为墙内人你懂的,把 ubuntu 的源换成阿里云的,不然就有的等了,然后就是装下 nginx,往默认的 nginx 的入口 html 文件里输入一行欢迎语,然后暴露 80 端口
然后我们使用sudo docker build -t="nicksxs/static_web" .命令来基于这个 Dockerfile 构建我们自己的镜像,过程中是这样的
![]()
![]()
可以看到图中,我的 Dockerfile 是 7 行,里面就执行了 7 步,并且每一步都有一个类似于容器 id 的层 id 出来,这里就是一个比较重要的东西,docker 在构建的时候其实是有这个层的概念,Dockerfile 里的每一行都会往上加一层,这里有还注意下命令后面的.,代表当前目录下会自行去寻找 Dockerfile 进行构建,构建完了之后我们再看下我们的本地镜像
![]()
我们自己的镜像出现啦
然后有个问题,如果这个构建中途报了错咋办呢,来试试看,我们把 nginx 改成随便的一个错误名,nginxx(不知道会不会运气好真的有这玩意),再来 build 一把
![]()
找不到 nginxx 包,是不是这个镜像就完全不能用呢,当然也不是,因为前面说到了,docker 是基于层去构建的,可以看到前面的 4 个 step 都没报错,那我们基于最后的成功步骤创建下容器看看
也就是sudo docker run -t -i bd26f991b6c8 /bin/bash
答案是可以的,只是没装成功 nginx
![]()
还有一点注意到没,前面的几个 step 都有一句 Using cache,说明 docker 在构建镜像的时候是有缓存的,这也更能说明 docker 是基于层去构建镜像,同样的底包,同样的步骤,这些层是可以被复用的,这就是 docker 的构建缓存,当然我们也可以在 build 的时候加上--no-cache去把构建缓存禁用掉。
+ int num, pos;
+};
+bool cmp(Node a, Node b)
+{
+ return a.num < b.num;
+}
+class Solution {
+public:
+ vector<int> twoSum(vector<int> &numbers, int target) {
+ // Start typing your C/C++ solution below
+ // DO NOT write int main() function
+ vector<int> result;
+ vector<Node> array;
+ for (int i = 0; i < numbers.size(); i++)
+ {
+ Node temp;
+ temp.num = numbers[i];
+ temp.pos = i;
+ array.push_back(temp);
+ }
+
+ sort(array.begin(), array.end(), cmp);
+ for (int i = 0, j = array.size() - 1; i != j;)
+ {
+ int sum = array[i].num + array[j].num;
+ if (sum == target)
+ {
+ if (array[i].pos < array[j].pos)
+ {
+ result.push_back(array[i].pos + 1);
+ result.push_back(array[j].pos + 1);
+ } else
+ {
+ result.push_back(array[j].pos + 1);
+ result.push_back(array[i].pos + 1);
+ }
+ break;
+ } else if (sum < target)
+ {
+ i++;
+ } else if (sum > target)
+ {
+ j--;
+ }
+ }
+ return result;
+ }
+};
+
+Analysis
sort the array, then test from head and end, until catch the right answer
]]>
- Docker
- 介绍
+ leetcode
- Docker
- namespace
- cgroup
+ leetcode
+ c++
@@ -5838,59 +5906,58 @@ EXPOSE 80<
- docker比一般多一点的初学者介绍
- /2020/03/08/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D/
- 因为最近想搭一个phabricator用来做看板和任务管理,一开始了解这个是Easy大大有在微博推荐过,后来苏洋也在群里和博客里说到了,看上去还不错的样子,因为主角是docker所以就不介绍太多,后面有机会写一下。
-docker最开始是之前在某位大佬的博客看到的,看上去有点神奇,感觉是一种轻量级的虚拟机,但是能做的事情好像差不多,那时候是在Ubuntu系统的vps里起一个Ubuntu的docker,然后在里面装个nginx,配置端口映射就可以访问了,后来也草草写过一篇使用docker搭建mysql集群,但是最近看了下好像是因为装docker的大佬做了一些别名还是什么操作,导致里面用的操作都不具有普遍性,而且主要是把搭的过程写了下,属于囫囵吞枣,没理解docker是干啥的,为啥用docker,就是操作了下,这几天借着搭phabricator的过程,把一些原来不理解,或者原来理解错误的地方重新理一下。
-之前写的 mysql 集群,一主二备,这种架构在很多小型应用里都是这么配置的,而且一般是直接在三台 vps 里启动三个 mysql 实例,但是如果换成 docker 会有什么好处呢,其实就是方便部署,比如其中一台备库挂了,我要加一台,或者说备库的 qps 太高了,需要再加一个,如果要在 vps 上搭建的话,首先要买一台机器,等初始化,然后在上面修改源,更新,装 mysql ,然后配置主从,可能还要处理防火墙等等,如果把这些打包成一个 docker 镜像,并且放在自己的 docker registry,那就直接run 一下就可以了;还有比如在公司要给一个新同学整一套开发测试环境,以 Java 开发为例,要装 git,maven,jdk,配置 maven settings 和各种 rc,整合在一个镜像里的话,就会很方便了;再比如微服务的水平扩展。
-但是为啥 docker 会有这种优势,听起来好像虚拟机也可以干这个事,但是虚拟机动辄上 G,而且需要 VMware,virtual box 等支持,不适合在Linux服务器环境使用,而且占用资源也会非常大。说得这么好,那么 docker 是啥呢
-docker 主要使用 Linux 中已经存在的两种技术的一个整合升级,一个是 namespace,一个是cgroups,相比于虚拟机需要完整虚拟出一个操作系统运行基础,docker 基于宿主机内核,通过 namespace 和 cgroups 分隔进程,理念就是提供一个隔离的最小化运行依赖,这样子相对于虚拟机就有了巨大的便利性,具体的 namespace 和 cgroups 就先不展开讲,可以参考耗子叔的文章
-安装
那么我们先安装下 docker,参考官方的教程,安装,我的系统是 ubuntu 的,就贴了 ubuntu 的链接,用其他系统的可以找到对应的系统文档安装,安装完了的话看看 docker 的信息
-sudo docker info
-
-输出以下信息
![]()
-简单运行
然后再来运行个 hello world 呗,
-sudo docker run hello-world
+ dubbo 客户端配置的一个重要知识点
+ /2022/06/11/dubbo-%E5%AE%A2%E6%88%B7%E7%AB%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E4%B8%80%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9F%A5%E8%AF%86%E7%82%B9/
+ 在配置项目中其实会留着比较多的问题,由于不同的项目没有比较统一的规划和框架模板,一般都是只有创建者会比较了解(可能也不了解),譬如前阵子在配置一个 springboot + dubbo 的项目,发现了dubbo 连接注册中间客户端的问题,这里可以结合下代码来看
比如有的应用是用的这个
+<dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-client</artifactId>
+ <version>${curator.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-recipes</artifactId>
+ <version>${curator.version}</version>
+</dependency>
+有个别应用用的是这个
+<dependency>
+ <groupId>com.101tec</groupId>
+ <artifactId>zkclient</artifactId>
+ <version>0.11</version>
+</dependency>
+还有的应用是找不到相关的依赖,并且这些的使用没有个比较好的说明,为啥用前者,为啥用后者,有啥注意点,
首先在使用 2.6.5 的 alibaba 的 dubbo 的时候,只使用后者是会报错的,至于为啥会报错,其实就是这篇文章想说明的点
报错的内容其实很简单, 就是缺少这个 org.apache.curator.framework.CuratorFrameworkFactory 类
这个类看着像是依赖上面的配置,但是应该不需要两个配置一块用的,所以还是需要去看代码
通过找上面类被依赖的和 dubbo 连接注册中心相关的代码,看到了这段指点迷津的代码
+@SPI("curator")
+public interface ZookeeperTransporter {
-输出了这些
![]()
-看看这个运行命令是怎么用的,一般都会看到这样子的,sudo docker run -it ubuntu bash, 前面的 docker run 反正就是运行一个容器的意思,-it是啥呢,还有这个什么 ubuntu bash,来看看docker run`的命令帮助信息
--i, --interactive Keep STDIN open even if not attached
+ @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
+ ZookeeperClient connect(URL url);
-就是要有输入,我们运行的时候能输入
--t, --tty Allocate a pseudo-TTY
+}
+众所周知,dubbo 创造了叫自适应扩展点加载的神奇技术,这里的 adaptive 注解中的Constants.CLIENT_KEY 和 Constants.TRANSPORTER_KEY 可以在配置 dubbo 的注册信息的时候进行配置,如果是通过 xml 配置的话,可以在 <dubbo:registry/> 这个 tag 中的以上两个 key 进行配置,
具体在 dubbo.xsd 中有描述
+<xsd:element name="registry" type="registryType">
+ <xsd:annotation>
+ <xsd:documentation><![CDATA[ The registry config ]]></xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
-要有个虚拟终端,
-Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
+![]()
并且在 spi 的配置com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter 中可以看到
+zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
+curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
-Run a command in a new container
-
-镜像
上面说的-it 就是这里的 options,后面那个 ubuntu 就是 image 辣,image 是啥呢
-Docker 把应用程序及其依赖,打包在 image 文件里面,可以把它理解成为类似于虚拟机的镜像或者运行一个进程的代码,跑起来了的叫docker 容器或者进程,比如我们将要运行的docker run -it ubuntu bash的ubuntu 就是个 ubuntu 容器的镜像,将这个镜像运行起来后,我们可以进入容器像使用 ubuntu 一样使用它,来看下我们的镜像,使用sudo docker image ls就能列出我们宿主机上的 docker 镜像了
-![]()
-一个 ubuntu 镜像才 64MB,非常小巧,然后是后面的bash,我通过交互式启动了一个 ubuntu 容器,然后在这个启动的容器里运行了 bash 命令,这样就可以在容器里玩一下了
-在容器里看下进程,
![]()
-只有刚才运行容器的 bash 进程和我刚执行的 ps,这里有个可以注意下的,bash 这个进程的 pid 是 1,其实这里就用到了 linux 中的PID Namespace,容器会隔离出一个 pid 的名字空间,这里面的进程跟外部的 pid 命名独立
-查看宿主机上的容器
sudo docker ps -a
-
-![]()
-如何进入一个正在运行中的 docker 容器
这个应该是比较常用的,因为比如是一个微服务容器,有时候就像看下运行状态,日志啥的
-sudo docker exec -it [containerID] bash
-
-![]()
-查看日志
sudo docker logs [containerID]
+zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
+curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
-我在运行容器的终端里胡乱输入点啥,然后通过上面的命令就可以看到啦
-![]()
-![]()
+zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
+curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
+而在上面的代码里默认的SPI 值是 curator,所以如果不配置,那就会报上面找不到类的问题,所以如果需要使用 zkclient 的,就需要在<dubbo:registry/> 配置中添加 client="zkclient"这个配置,所以有些地方还是需要懂一些更深层次的原理,但也不至于每个东西都要抠到每一行代码原理,除非就是专门做这一块的。
还有一点是发现有些应用是碰运气,刚好有个三方包把这个类带进来了,但是这个应用就没有单独配置这块,如果不了解或者后续忘了再来查问题就会很奇怪
]]>
- Docker
- 介绍
+ Java
+ Dubbo
- Docker
- namespace
- cgroup
+ Java
+ Dubbo
@@ -5917,58 +5984,21 @@ Run a command <dependency>
- <groupId>org.apache.curator</groupId>
- <artifactId>curator-client</artifactId>
- <version>${curator.version}</version>
-</dependency>
-<dependency>
- <groupId>org.apache.curator</groupId>
- <artifactId>curator-recipes</artifactId>
- <version>${curator.version}</version>
-</dependency>
-有个别应用用的是这个
-<dependency>
- <groupId>com.101tec</groupId>
- <artifactId>zkclient</artifactId>
- <version>0.11</version>
-</dependency>
-还有的应用是找不到相关的依赖,并且这些的使用没有个比较好的说明,为啥用前者,为啥用后者,有啥注意点,
首先在使用 2.6.5 的 alibaba 的 dubbo 的时候,只使用后者是会报错的,至于为啥会报错,其实就是这篇文章想说明的点
报错的内容其实很简单, 就是缺少这个 org.apache.curator.framework.CuratorFrameworkFactory 类
这个类看着像是依赖上面的配置,但是应该不需要两个配置一块用的,所以还是需要去看代码
通过找上面类被依赖的和 dubbo 连接注册中心相关的代码,看到了这段指点迷津的代码
-@SPI("curator")
-public interface ZookeeperTransporter {
-
- @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
- ZookeeperClient connect(URL url);
-
-}
-众所周知,dubbo 创造了叫自适应扩展点加载的神奇技术,这里的 adaptive 注解中的Constants.CLIENT_KEY 和 Constants.TRANSPORTER_KEY 可以在配置 dubbo 的注册信息的时候进行配置,如果是通过 xml 配置的话,可以在 <dubbo:registry/> 这个 tag 中的以上两个 key 进行配置,
具体在 dubbo.xsd 中有描述
-<xsd:element name="registry" type="registryType">
- <xsd:annotation>
- <xsd:documentation><![CDATA[ The registry config ]]></xsd:documentation>
- </xsd:annotation>
- </xsd:element>
-
-![]()
并且在 spi 的配置com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter 中可以看到
-zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
-curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
-
-zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
-curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
-
-zkclient=com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperTransporter
-curator=com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter
-而在上面的代码里默认的SPI 值是 curator,所以如果不配置,那就会报上面找不到类的问题,所以如果需要使用 zkclient 的,就需要在<dubbo:registry/> 配置中添加 client="zkclient"这个配置,所以有些地方还是需要懂一些更深层次的原理,但也不至于每个东西都要抠到每一行代码原理,除非就是专门做这一块的。
还有一点是发现有些应用是碰运气,刚好有个三方包把这个类带进来了,但是这个应用就没有单独配置这块,如果不了解或者后续忘了再来查问题就会很奇怪
+ headscale 添加节点
+ /2023/07/09/headscale-%E6%B7%BB%E5%8A%A0%E8%8A%82%E7%82%B9/
+ 添加节点添加节点非常简单,比如 app store 或者官网可以下载 mac 的安装包,
+安装包直接下载可以在这里,下载安装完后还需要做一些处理,才能让 Tailscale 使用 Headscale 作为控制服务器。当然,Headscale 已经给我们提供了详细的操作步骤,你只需要在浏览器中打开 URL:http://<HEADSCALE_PUB_IP>:<HEADSCALE_PUB_PORT>/apple,记得端口替换成自己的,就会看到这样的说明页
+![image]()
+然后对于像我这样自己下载的客户端安装包,也就是standalone client,就可以用下面的命令
+defaults write io.tailscale.ipn.macsys ControlURL http://<HEADSCALE_PUB_IP>:<HEADSCALE_PUB_PORT> 类似于 Windows 客户端需要写入注册表,就是把控制端的地址改成了我们自己搭建的 headscale 的,设置完以后就打开 tailscale 客户端右键点击 login,就会弹出一个浏览器地址
+![image]()
+按照这个里面的命令去 headscale 的机器上执行,注意要替换 namespace,对于最新的 headscale 已经把 namespace 废弃改成 user 了,这点要注意了,其他客户端也同理,现在还有个好消息,安卓和 iOS 客户端也已经都可以用了,后面可以在介绍下局域网怎么部分打通和自建 derper。
]]>
- Java
- Dubbo
+ headscale
- Java
- Dubbo
+ headscale
@@ -6071,120 +6101,73 @@ myusername ALL = resolv-file=/opt/homebrew/etc/dnsmasq.d/resolv.dnsmasq.conf
-结果发现 dnsmasq 就起不来了,因为是 brew 服务的形式起来,发现日志也没有, dnsmasq 配置文件本身也没什么日志,这个是最讨厌的,网上搜了一圈也都没有, brew services 的服务如果启动状态是 error,并且服务本身没有日志的话就是一头雾水,并且对于 plist 来说,即使我手动加了标准输出和错误输出,brew services restart 的时候也是会被重新覆盖,
后来仔细看了下这个问题,发现它下面有这么一行配置
-conf-dir=/opt/homebrew/etc/dnsmasq.d/,*.conf
-想了一下发现这个问题其实很简单,dnsmasq 应该是不支持同一配置文件加载两次,
我把 resolv 文件放在了同一个配置文件目录下,所以就被加载了两次,所以改掉目录就行了,但是目前看 dnsmasq 还不符合我的要求,也有可能我还没完全了解 dnsmasq 的使用方法,我想要的是比如按特定的域名后缀来配置对应的 dns 服务器,这样就不太会被影响,可以试试 AdGuard 看
+ docker比一般多一点的初学者介绍二
+ /2020/03/15/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%BA%8C/
+ 限制下 docker 的 cpu 使用率这里我们开始玩一点有意思的,我们在容器里装下 vim 和 gcc,然后写这样一段 c 代码
+#include <stdio.h>
+int main(void)
+{
+ int i = 0;
+ for(;;) i++;
+ return 0;
+}
+就是一个最简单的死循环,然后在容器里跑起来
+$ gcc 1.c
+$ ./a.out
+然后我们来看下系统资源占用(CPU)
![Xs562iawhHyMxeO]()
上图是在容器里的,可以看到 cpu 已经 100%了
然后看看容器外面的
![ecqH8XJ4k7rKhzu]()
可以看到一个核的 cpu 也被占满了,因为是个双核的机器,并且代码是单线程的
然后呢我们要做点啥
因为已经在这个 ubuntu 容器中装了 vim 和 gcc,考虑到国内的网络,所以我们先把这个容器 commit 一下,
+docker commit -a "nick" -m "my ubuntu" f63c5607df06 my_ubuntu:v1
+然后再运行起来
+docker run -it --cpus=0.1 my_ubuntu:v1 bash
+![]()
我们的代码跟可执行文件都还在,要的就是这个效果,然后再运行一下
![]()
结果是这个样子的,有点神奇是不,关键就在于 run 的时候的--cpus=0.1这个参数,它其实就是基于我前一篇说的 cgroup 技术,能将进程之间的cpu,内存等资源进行隔离
+开始第一个 Dockerfile
上一面为了复用那个我装了 vim 跟 gcc 的容器,我把它提交到了本地,使用了docker commit命令,有点类似于 git 的 commit,但是这个不是个很好的操作方式,需要手动介入,这里更推荐使用 Dockerfile 来构建镜像
+From ubuntu:latest
+MAINTAINER Nicksxs "nicksxs@hotmail.com"
+RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
+RUN apt-get clean
+RUN apt-get update && apt install -y nginx
+RUN echo 'Hi, i am in container' \
+ > /usr/share/nginx/html/index.html
+EXPOSE 80
+先解释下这是在干嘛,首先是这个From ubuntu:latest基于的 ubuntu 的最新版本的镜像,然后第二行是维护人的信息,第三四行么作为墙内人你懂的,把 ubuntu 的源换成阿里云的,不然就有的等了,然后就是装下 nginx,往默认的 nginx 的入口 html 文件里输入一行欢迎语,然后暴露 80 端口
然后我们使用sudo docker build -t="nicksxs/static_web" .命令来基于这个 Dockerfile 构建我们自己的镜像,过程中是这样的
![]()
![]()
可以看到图中,我的 Dockerfile 是 7 行,里面就执行了 7 步,并且每一步都有一个类似于容器 id 的层 id 出来,这里就是一个比较重要的东西,docker 在构建的时候其实是有这个层的概念,Dockerfile 里的每一行都会往上加一层,这里有还注意下命令后面的.,代表当前目录下会自行去寻找 Dockerfile 进行构建,构建完了之后我们再看下我们的本地镜像
![]()
我们自己的镜像出现啦
然后有个问题,如果这个构建中途报了错咋办呢,来试试看,我们把 nginx 改成随便的一个错误名,nginxx(不知道会不会运气好真的有这玩意),再来 build 一把
![]()
找不到 nginxx 包,是不是这个镜像就完全不能用呢,当然也不是,因为前面说到了,docker 是基于层去构建的,可以看到前面的 4 个 step 都没报错,那我们基于最后的成功步骤创建下容器看看
也就是sudo docker run -t -i bd26f991b6c8 /bin/bash
答案是可以的,只是没装成功 nginx
![]()
还有一点注意到没,前面的几个 step 都有一句 Using cache,说明 docker 在构建镜像的时候是有缓存的,这也更能说明 docker 是基于层去构建镜像,同样的底包,同样的步骤,这些层是可以被复用的,这就是 docker 的构建缓存,当然我们也可以在 build 的时候加上--no-cache去把构建缓存禁用掉。
]]>
- dns
+ Docker
+ 介绍
- dnsmasq
+ Docker
+ namespace
+ cgroup
- 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:
+ github 小技巧-更新 github host key
+ /2023/03/28/github-%E5%B0%8F%E6%8A%80%E5%B7%A7-%E6%9B%B4%E6%96%B0-github-host-key/
+ 最近一次推送博客,发现报了个错推不上去,
+WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
+
+IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
+Someone could be eavesdropping on you right now (man-in-the-middle attack)!
+It is also possible that a host key has just been changed.
+错误信息是这样,有点奇怪也没干啥,网上一搜发现是We updated our RSA SSH host key
简单翻一下就是
-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++
-
-
-
- Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析
- /2022/10/02/Leetcode-747-%E8%87%B3%E5%B0%91%E6%98%AF%E5%85%B6%E4%BB%96%E6%95%B0%E5%AD%97%E4%B8%A4%E5%80%8D%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0-Largest-Number-At-Least-Twice-of-Others-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
- 题目介绍You are given an integer array nums where the largest integer is unique.
-Determine whether the largest element in the array is at least twice as much as every other number in the array. If it is, return the index of the largest element, or return -1 otherwise.
确认在数组中的最大数是否是其余任意数的两倍大及以上,如果是返回索引,如果不是返回-1
-示例
Example 1:
-Input: nums = [3,6,1,0]
Output: 1
Explanation: 6 is the largest integer.
For every other number in the array x, 6 is at least twice as big as x.
The index of value 6 is 1, so we return 1.
+在3月24日协调世界时大约05:00时,出于谨慎,我们更换了用于保护 GitHub.com 的 Git 操作的 RSA SSH 主机密钥。我们这样做是为了保护我们的用户免受任何对手模仿 GitHub 或通过 SSH 窃听他们的 Git 操作的机会。此密钥不授予对 GitHub 基础设施或客户数据的访问权限。此更改仅影响通过使用 RSA 的 SSH 进行的 Git 操作。GitHub.com 和 HTTPS Git 操作的网络流量不受影响。
-Example 2:
-Input: nums = [1,2,3,4]
Output: -1
Explanation: 4 is less than twice the value of 3, so we return -1.
+要解决也比较简单就是重置下 host key,
+
+Host Key是服务器用来证明自己身份的一个永久性的非对称密钥
-提示:
-2 <= nums.length <= 50
-0 <= nums[i] <= 100
-- The largest element in
nums is unique.
-
-简要解析
这个题easy是题意也比较简单,找最大值,并且最大值是其他任意值的两倍及以上,其实就是找最大值跟次大值,比较下就好了
-代码
public int dominantIndex(int[] nums) {
- int largest = Integer.MIN_VALUE;
- int second = Integer.MIN_VALUE;
- int largestIndex = -1;
- for (int i = 0; i < nums.length; i++) {
- // 如果有最大的就更新,同时更新最大值和第二大的
- if (nums[i] > largest) {
- second = largest;
- largest = nums[i];
- largestIndex = i;
- } else if (nums[i] > second) {
- // 没有超过最大的,但是比第二大的更大就更新第二大的
- second = nums[i];
- }
- }
-
- // 判断下是否符合题目要求,要是所有值的两倍及以上
- if (largest >= 2 * second) {
- return largestIndex;
- } else {
- return -1;
- }
-}
-通过图
第一次错了是把第二大的情况只考虑第一种,也有可能最大值完全没经过替换就变成最大值了
![]()
+使用
+ssh-keygen -R github.com
+然后在首次建立连接的时候同意下就可以了
]]>
- Java
- leetcode
+ ssh
+ 技巧
- leetcode
- java
- 题解
+ ssh
+ 端口转发
@@ -6200,110 +6183,6 @@ public:
博客,文章
-
- minimum-size-subarray-sum-209
- /2016/10/11/minimum-size-subarray-sum-209/
- problemGiven 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++
-
-
-
- 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
-
-
- Java
- Mysql
- 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/
@@ -6400,150 +6279,574 @@ public class DynamicSqlSource implements SqlSource {
- hexo 配置系列-接入Algolia搜索
- /2023/04/02/hexo-%E9%85%8D%E7%BD%AE%E7%B3%BB%E5%88%97-%E6%8E%A5%E5%85%A5Algolia%E6%90%9C%E7%B4%A2/
- 博客之前使用的是 local search,最开始感觉使用体验还不错,速度也不慢,最近自己搜了下觉得效果差了很多,不知道是啥原因,所以接入有 next 主题支持的 Algolia 搜索,next 主题的文档已经介绍的很清楚了,这边就记录下,
首先要去 Algolia 开通下账户,创建一个索引
![]()
创建好后要去找一下 api key 的配置,这个跟 next 主题的说明已经有些不一样了
在设置里可以找到
![]()
这里默认会有两个 key
![]()
一个是 search only,一个是 admin key,需要再创建一个自定义 key
这个 key 需要有这些权限,称为 High-privilege API key, 后面有用
![]()
然后就是到博客目录下安装
-cd hexo-site
-npm install hexo-algolia
-然后在 hexo 站点配置中添加
-algolia:
- applicationID: "Application ID"
- apiKey: "Search-only API key"
- indexName: "indexName"
-包括应用 Id,只搜索的 api key(默认给创建好的那个),indexName 就是最开始创建的 index 名,
-export HEXO_ALGOLIA_INDEXING_KEY=High-privilege API key # Use Git Bash
-# set HEXO_ALGOLIA_INDEXING_KEY=High-privilege API key # Use Windows command line
-hexo clean
-hexo algolia
-然后再到 next 配置中开启 algolia_search
-# Algolia Search
-algolia_search:
- enable: true
- hits:
- per_page: 10
-搜索的界面其实跟 local 的差不多,就是搜索效果会好一些
![]()
也推荐可以搜搜过往的内容,已经左边有个热度的,做了个按阅读量排序的榜单。
-]]>
-
- hexo
- 技巧
-
-
- hexo
-
-
-
- headscale 添加节点
- /2023/07/09/headscale-%E6%B7%BB%E5%8A%A0%E8%8A%82%E7%82%B9/
- 添加节点添加节点非常简单,比如 app store 或者官网可以下载 mac 的安装包,
-安装包直接下载可以在这里,下载安装完后还需要做一些处理,才能让 Tailscale 使用 Headscale 作为控制服务器。当然,Headscale 已经给我们提供了详细的操作步骤,你只需要在浏览器中打开 URL:http://<HEADSCALE_PUB_IP>:<HEADSCALE_PUB_PORT>/apple,记得端口替换成自己的,就会看到这样的说明页
-![image]()
-然后对于像我这样自己下载的客户端安装包,也就是standalone client,就可以用下面的命令
-defaults write io.tailscale.ipn.macsys ControlURL http://<HEADSCALE_PUB_IP>:<HEADSCALE_PUB_PORT> 类似于 Windows 客户端需要写入注册表,就是把控制端的地址改成了我们自己搭建的 headscale 的,设置完以后就打开 tailscale 客户端右键点击 login,就会弹出一个浏览器地址
-![image]()
-按照这个里面的命令去 headscale 的机器上执行,注意要替换 namespace,对于最新的 headscale 已经把 namespace 废弃改成 user 了,这点要注意了,其他客户端也同理,现在还有个好消息,安卓和 iOS 客户端也已经都可以用了,后面可以在介绍下局域网怎么部分打通和自建 derper。
+ minimum-size-subarray-sum-209
+ /2016/10/11/minimum-size-subarray-sum-209/
+ problemGiven 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;
+ }
+};
]]>
- headscale
+ leetcode
- headscale
+ leetcode
+ c++
- java 中发起 http 请求时证书问题解决记录
- /2023/07/29/java-%E4%B8%AD%E5%8F%91%E8%B5%B7-http-%E8%AF%B7%E6%B1%82%E6%97%B6%E8%AF%81%E4%B9%A6%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%E8%AE%B0%E5%BD%95/
- 再一次环境部署是发现了个问题,就是在请求微信 https 请求的时候,出现了个错误
No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
一开始以为是环境问题,从 oracle 的 jdk 换成了基于 openjdk 的底包,没有 javax 的关系,
完整的提示包含了 javax 的异常
java.lang.RuntimeException: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
后面再看了下,是不是也可能是证书的问题,然后就去找了下是不是证书相关的,
可以看到在 /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security 路径下的 java.security 中
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA,
而正好在我们代码里 createSocketFactory 的时候使用了 TLSv1 这个证书协议
-SSLContext sslContext = SSLContext.getInstance("TLS");
-sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
-return new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1"}, null, new DefaultHostnameVerifier());
-所以就有两种方案,一个是使用更新版本的 TLS 或者另一个就是使用比较久的 jdk,这也说明其实即使都是 jdk8 的,不同的小版本差异还是会有些影响,有的时候对于这些错误还是需要更深入地学习,不能一概而之认为就是 jdk 用的是 oracle 还是 openjdk 的,不同的错误可能就需要仔细确认原因所在。
-]]>
+ 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
+ Java
+ Mybatis
+ Mysql
- java
+ Java
+ Mysql
+ Mybatis
- github 小技巧-更新 github host key
- /2023/03/28/github-%E5%B0%8F%E6%8A%80%E5%B7%A7-%E6%9B%B4%E6%96%B0-github-host-key/
- 最近一次推送博客,发现报了个错推不上去,
-WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
+ mybatis 的缓存是怎么回事
+ /2020/10/03/mybatis-%E7%9A%84%E7%BC%93%E5%AD%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/
+ Java 真的是任何一个中间件,比较常用的那种,都有很多内容值得深挖,比如这个缓存,慢慢有过一些感悟,比如如何提升性能,缓存无疑是一大重要手段,最底层开始 CPU 就有缓存,而且又小又贵,再往上一点内存一般作为硬盘存储在运行时的存储,一般在代码里也会用内存作为一些本地缓存,譬如数据库,像 mysql 这种也是有innodb_buffer_pool来提升查询效率,本质上理解就是用更快的存储作为相对慢存储的缓存,减少查询直接访问较慢的存储,并且这个都是相对的,比起 cpu 的缓存,那内存也是渣,但是与普通机械硬盘相比,那也是两个次元的水平。
+闲扯这么多来说说 mybatis 的缓存,mybatis 一般作为一个轻量级的 orm 使用,相对应的就是比较重量级的 hibernate,不过不在这次讨论范围,上一次是主要讲了 mybatis 在解析 sql 过程中,对于两种占位符的不同替换实现策略,这次主要聊下 mybatis 的缓存,前面其实得了解下前置的东西,比如 sqlsession,先当做我们知道 sqlsession 是个什么玩意,可能或多或少的知道 mybatis 是有两级缓存,
+一级缓存
第一级的缓存是在 BaseExecutor 中的 PerpetualCache,它是个最基本的缓存实现类,使用了 HashMap 实现缓存功能,代码其实没几十行
+public class PerpetualCache implements Cache {
-IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
-Someone could be eavesdropping on you right now (man-in-the-middle attack)!
-It is also possible that a host key has just been changed.
-错误信息是这样,有点奇怪也没干啥,网上一搜发现是We updated our RSA SSH host key
简单翻一下就是
-
-在3月24日协调世界时大约05:00时,出于谨慎,我们更换了用于保护 GitHub.com 的 Git 操作的 RSA SSH 主机密钥。我们这样做是为了保护我们的用户免受任何对手模仿 GitHub 或通过 SSH 窃听他们的 Git 操作的机会。此密钥不授予对 GitHub 基础设施或客户数据的访问权限。此更改仅影响通过使用 RSA 的 SSH 进行的 Git 操作。GitHub.com 和 HTTPS Git 操作的网络流量不受影响。
-
-要解决也比较简单就是重置下 host key,
-
-Host Key是服务器用来证明自己身份的一个永久性的非对称密钥
-
-使用
-ssh-keygen -R github.com
-然后在首次建立连接的时候同意下就可以了
-]]>
+ private final String id;
+
+ private final Map<Object, Object> cache = new HashMap<>();
+
+ public PerpetualCache(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public int getSize() {
+ return cache.size();
+ }
+
+ @Override
+ public void putObject(Object key, Object value) {
+ cache.put(key, value);
+ }
+
+ @Override
+ public Object getObject(Object key) {
+ return cache.get(key);
+ }
+
+ @Override
+ public Object removeObject(Object key) {
+ return cache.remove(key);
+ }
+
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (getId() == null) {
+ throw new CacheException("Cache instances require an ID.");
+ }
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Cache)) {
+ return false;
+ }
+
+ Cache otherCache = (Cache) o;
+ return getId().equals(otherCache.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ if (getId() == null) {
+ throw new CacheException("Cache instances require an ID.");
+ }
+ return getId().hashCode();
+ }
+
+}
+可以看一下BaseExecutor 的构造函数
+protected BaseExecutor(Configuration configuration, Transaction transaction) {
+ this.transaction = transaction;
+ this.deferredLoads = new ConcurrentLinkedQueue<>();
+ this.localCache = new PerpetualCache("LocalCache");
+ this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
+ this.closed = false;
+ this.configuration = configuration;
+ this.wrapper = this;
+ }
+就是把 PerpetualCache 作为 localCache,然后怎么使用我看简单看一下,BaseExecutor 的查询首先是调用这个函数
+@Override
+ public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
+ BoundSql boundSql = ms.getBoundSql(parameter);
+ CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
+ return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
+ }
+可以看到首先是调用了 createCacheKey 方法,这个方法呢,先不看怎么写的,如果我们自己要实现这么个缓存,首先这个缓存 key 的设计也是个问题,如果是以表名加主键作为 key,那么分页查询,或者没有主键的时候就不行,来看看 mybatis 是怎么设计的
+@Override
+ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
+ if (closed) {
+ throw new ExecutorException("Executor was closed.");
+ }
+ CacheKey cacheKey = new CacheKey();
+ cacheKey.update(ms.getId());
+ cacheKey.update(rowBounds.getOffset());
+ cacheKey.update(rowBounds.getLimit());
+ cacheKey.update(boundSql.getSql());
+ List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
+ TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
+ // mimic DefaultParameterHandler logic
+ for (ParameterMapping parameterMapping : parameterMappings) {
+ if (parameterMapping.getMode() != ParameterMode.OUT) {
+ Object value;
+ String propertyName = parameterMapping.getProperty();
+ if (boundSql.hasAdditionalParameter(propertyName)) {
+ value = boundSql.getAdditionalParameter(propertyName);
+ } else if (parameterObject == null) {
+ value = null;
+ } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
+ value = parameterObject;
+ } else {
+ MetaObject metaObject = configuration.newMetaObject(parameterObject);
+ value = metaObject.getValue(propertyName);
+ }
+ cacheKey.update(value);
+ }
+ }
+ if (configuration.getEnvironment() != null) {
+ // issue #176
+ cacheKey.update(configuration.getEnvironment().getId());
+ }
+ return cacheKey;
+ }
+
+首先需要 id,这个 id 是 mapper 里方法的 id, 然后是偏移量跟返回行数,再就是 sql,然后是参数,基本上是会有影响的都加进去了,在这个 update 里面
+public void update(Object object) {
+ int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
+
+ count++;
+ checksum += baseHashCode;
+ baseHashCode *= count;
+
+ hashcode = multiplier * hashcode + baseHashCode;
+
+ updateList.add(object);
+ }
+其实是一个 hash 转换,具体不纠结,就是提高特异性,然后回来就是继续调用 query
+@Override
+ public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
+ ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
+ if (closed) {
+ throw new ExecutorException("Executor was closed.");
+ }
+ if (queryStack == 0 && ms.isFlushCacheRequired()) {
+ clearLocalCache();
+ }
+ List<E> list;
+ try {
+ queryStack++;
+ list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
+ if (list != null) {
+ handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
+ } else {
+ list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
+ }
+ } finally {
+ queryStack--;
+ }
+ if (queryStack == 0) {
+ for (DeferredLoad deferredLoad : deferredLoads) {
+ deferredLoad.load();
+ }
+ // issue #601
+ deferredLoads.clear();
+ if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
+ // issue #482
+ clearLocalCache();
+ }
+ }
+ return list;
+ }
+可以看到是先从 localCache 里取,取不到再 queryFromDatabase,其实比较简单,这是一级缓存,考虑到 sqlsession 跟 BaseExecutor 的关系,其实是随着 sqlsession 来保证这个缓存不会出现脏数据幻读的情况,当然事务相关的后面可能再单独聊。
+二级缓存
其实这个一级二级顺序有点反过来,其实查询的是先走的二级缓存,当然二级的需要配置开启,默认不开,
需要通过
+<setting name="cacheEnabled" value="true"/>
+来开启,然后我们的查询就会走到
+public class CachingExecutor implements Executor {
+
+ private final Executor delegate;
+ private final TransactionalCacheManager tcm = new TransactionalCacheManager();
+这个 Executor 中,这里我放了类里面的元素,发现没有一个 Cache 类,这就是一个特点了,往下看查询过程
+@Override
+ public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
+ BoundSql boundSql = ms.getBoundSql(parameterObject);
+ CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
+ return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+ }
+
+ @Override
+ public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
+ throws SQLException {
+ Cache cache = ms.getCache();
+ if (cache != null) {
+ flushCacheIfRequired(ms);
+ if (ms.isUseCache() && resultHandler == null) {
+ ensureNoOutParams(ms, boundSql);
+ @SuppressWarnings("unchecked")
+ List<E> list = (List<E>) tcm.getObject(cache, key);
+ if (list == null) {
+ list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+ tcm.putObject(cache, key, list); // issue #578 and #116
+ }
+ return list;
+ }
+ }
+ return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+ }
+看到没,其实缓存是从 tcm 这个成员变量里取,而这个是什么呢,事务性缓存(直译下),因为这个其实是用 MappedStatement 里的 Cache 作为key 从 tcm 的 map 取出来的
+public class TransactionalCacheManager {
+
+ private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
+MappedStatement是被全局使用的,所以其实二级缓存是跟着 mapper 的 namespace 走的,可以被多个 CachingExecutor 获取到,就会出现线程安全问题,线程安全问题可以用SynchronizedCache来解决,就是加锁,但是对于事务中的脏读,使用了TransactionalCache来解决这个问题,
+public class TransactionalCache implements Cache {
+
+ private static final Log log = LogFactory.getLog(TransactionalCache.class);
+
+ private final Cache delegate;
+ private boolean clearOnCommit;
+ private final Map<Object, Object> entriesToAddOnCommit;
+ private final Set<Object> entriesMissedInCache;
+在事务还没提交的时候,会把中间状态的数据放在 entriesToAddOnCommit 中,只有在提交后会放进共享缓存中,
+public void commit() {
+ if (clearOnCommit) {
+ delegate.clear();
+ }
+ flushPendingEntries();
+ reset();
+ }
]]>
- ssh
- 技巧
+ Java
+ Mybatis
+ Spring
+ Mybatis
+ 缓存
+ Mybatis
- ssh
- 端口转发
+ Java
+ Mysql
+ Mybatis
+ 缓存
- 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 {
- // 如果闲置的连接列表不为空
+ hexo 配置系列-接入Algolia搜索
+ /2023/04/02/hexo-%E9%85%8D%E7%BD%AE%E7%B3%BB%E5%88%97-%E6%8E%A5%E5%85%A5Algolia%E6%90%9C%E7%B4%A2/
+ 博客之前使用的是 local search,最开始感觉使用体验还不错,速度也不慢,最近自己搜了下觉得效果差了很多,不知道是啥原因,所以接入有 next 主题支持的 Algolia 搜索,next 主题的文档已经介绍的很清楚了,这边就记录下,
首先要去 Algolia 开通下账户,创建一个索引
![]()
创建好后要去找一下 api key 的配置,这个跟 next 主题的说明已经有些不一样了
在设置里可以找到
![]()
这里默认会有两个 key
![]()
一个是 search only,一个是 admin key,需要再创建一个自定义 key
这个 key 需要有这些权限,称为 High-privilege API key, 后面有用
![]()
然后就是到博客目录下安装
+cd hexo-site
+npm install hexo-algolia
+然后在 hexo 站点配置中添加
+algolia:
+ applicationID: "Application ID"
+ apiKey: "Search-only API key"
+ indexName: "indexName"
+包括应用 Id,只搜索的 api key(默认给创建好的那个),indexName 就是最开始创建的 index 名,
+export HEXO_ALGOLIA_INDEXING_KEY=High-privilege API key # Use Git Bash
+# set HEXO_ALGOLIA_INDEXING_KEY=High-privilege API key # Use Windows command line
+hexo clean
+hexo algolia
+然后再到 next 配置中开启 algolia_search
+# Algolia Search
+algolia_search:
+ enable: true
+ hits:
+ per_page: 10
+搜索的界面其实跟 local 的差不多,就是搜索效果会好一些
![]()
也推荐可以搜搜过往的内容,已经左边有个热度的,做了个按阅读量排序的榜单。
+]]>
+
+ hexo
+ 技巧
+
+
+ hexo
+
+
+
+ mybatis系列-dataSource解析
+ /2023/01/08/mybatis%E7%B3%BB%E5%88%97-dataSource%E8%A7%A3%E6%9E%90/
+ DataSource 作为数据库查询的最重要的数据源,在 mybatis 中也展开来说下
首先是解析的过程
+SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
+
+在构建 SqlSessionFactory 也就是 DefaultSqlSessionFactory 的时候,
+public SqlSessionFactory build(InputStream inputStream) {
+ return build(inputStream, null, null);
+ }
+public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
+ try {
+ XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
+ return build(parser.parse());
+ } catch (Exception e) {
+ throw ExceptionFactory.wrapException("Error building SqlSession.", e);
+ } finally {
+ ErrorContext.instance().reset();
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ // Intentionally ignore. Prefer previous error.
+ }
+ }
+ }
+前面也说过,就是解析 mybatis-config.xml 成 Configuration
+public Configuration parse() {
+ if (parsed) {
+ throw new BuilderException("Each XMLConfigBuilder can only be used once.");
+ }
+ parsed = true;
+ parseConfiguration(parser.evalNode("/configuration"));
+ return configuration;
+}
+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
+ // -------------> 是在这里解析了DataSource
+ 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);
+ }
+}
+环境解析了这一块的内容
+<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>
+解析也是自上而下的,
+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;
+ }
+ }
+ }
+}
+前面第一步是解析事务管理器元素
+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.");
+}
+而这里的 resolveClass 其实就使用了上一篇的 typeAliases 系统,这里是使用了 JdbcTransactionFactory 作为事务管理器,
后面的就是 DataSourceFactory 的创建也是 DataSource 的创建
+private DataSourceFactory dataSourceElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties props = context.getChildrenAsProperties();
+ DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
+ factory.setProperties(props);
+ return factory;
+ }
+ throw new BuilderException("Environment declaration requires a DataSourceFactory.");
+}
+因为在config文件中设置了Pooled,所以对应创建的就是 PooledDataSourceFactory
但是这里其实有个比较需要注意的,mybatis 这里的其实是继承了 UnpooledDataSourceFactory
将基础方法都放在了 UnpooledDataSourceFactory 中
+public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
+
+ public PooledDataSourceFactory() {
+ this.dataSource = new PooledDataSource();
+ }
+
+}
+这里只保留了在构造方法里创建 DataSource
而这个 PooledDataSource 虽然没有直接继承 UnpooledDataSource,但其实
在构造方法里也是
+public PooledDataSource() {
+ dataSource = new UnpooledDataSource();
+}
+至于为什么这么做呢应该也是考虑到能比较多的复用代码,因为 Pooled 其实跟 Unpooled 最重要的差别就在于是不是每次都重开连接
使用连接池能够让应用在有大量查询的时候不用反复创建连接,省去了建联的网络等开销,Unpooled就是完成与数据库的连接,并可以获取该连接
主要的代码
+@Override
+public Connection getConnection() throws SQLException {
+ return doGetConnection(username, password);
+}
+
+@Override
+public Connection getConnection(String username, String password) throws SQLException {
+ return doGetConnection(username, password);
+}
+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;
+}
+而对于Pooled就会处理池化的逻辑
+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();
@@ -6559,7 +6862,6 @@ It is also possible that a host key has just
log.debug("Bad connection. Could not roll back");
}
}
- // 用此连接的真实连接再创建一个连接,并设置时间
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
@@ -6569,9 +6871,7 @@ It is also possible that a host key has just
}
} else {
// Must wait
- // 这样还是获取不到连接就只能等待了
try {
- // 标记状态,然后把等待计数+1
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
@@ -6580,9 +6880,7 @@ It is also possible that a host key has just
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
@@ -6592,20 +6890,15 @@ It is also possible that a host key has just
}
}
}
- // 如果连接不为空
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;
@@ -6613,11 +6906,9 @@ It is also possible that a host key has just
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.");
@@ -6627,75 +6918,26 @@ It is also possible that a host key has just
}
}
} 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();
- }
- 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();
- }
- }
+ return conn;
+ }
+它的入口不是个get方法,而是pop,从含义来来讲就不一样
org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection()
+@Override
+public Connection getConnection() throws SQLException {
+ return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+}
+对于具体怎么获取连接我们可以下一篇具体讲下
]]>
Java
@@ -6765,1330 +7007,1303 @@ WHERE (id = #{id})mapperElement(root.evalNode("mappers"));
-具体的代码会执行到这
-private void mapperElement(XNode parent) throws Exception {
- if (parent != null) {
- for (XNode child : parent.getChildren()) {
- if ("package".equals(child.getName())) {
- // 这里解析的不是 package
- String mapperPackage = child.getStringAttribute("name");
- configuration.addMappers(mapperPackage);
- } else {
- // 根据 resource 和 url 还有 mapperClass 判断
- String resource = child.getStringAttribute("resource");
- String url = child.getStringAttribute("url");
- String mapperClass = child.getStringAttribute("class");
- // resource 不为空其他为空的情况,就开始将 resource 读成输入流
- if (resource != null && url == null && mapperClass == null) {
- ErrorContext.instance().resource(resource);
- try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
- // 初始化 XMLMapperBuilder 来解析 mapper
- XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
- mapperParser.parse();
- }
-然后再是 parse 过程
-public void parse() {
- if (!configuration.isResourceLoaded(resource)) {
- // 解析 mapper 节点,也就是下图中的mapper
- configurationElement(parser.evalNode("/mapper"));
- configuration.addLoadedResource(resource);
- bindMapperForNamespace();
- }
-
- parsePendingResultMaps();
- parsePendingCacheRefs();
- parsePendingStatements();
-}
-![image]()
-继续往下走
-private void configurationElement(XNode context) {
- try {
- String namespace = context.getStringAttribute("namespace");
- if (namespace == null || namespace.isEmpty()) {
- throw new BuilderException("Mapper's namespace cannot be empty");
+ java 中发起 http 请求时证书问题解决记录
+ /2023/07/29/java-%E4%B8%AD%E5%8F%91%E8%B5%B7-http-%E8%AF%B7%E6%B1%82%E6%97%B6%E8%AF%81%E4%B9%A6%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%E8%AE%B0%E5%BD%95/
+ 再一次环境部署是发现了个问题,就是在请求微信 https 请求的时候,出现了个错误
No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
一开始以为是环境问题,从 oracle 的 jdk 换成了基于 openjdk 的底包,没有 javax 的关系,
完整的提示包含了 javax 的异常
java.lang.RuntimeException: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
后面再看了下,是不是也可能是证书的问题,然后就去找了下是不是证书相关的,
可以看到在 /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security 路径下的 java.security 中
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA,
而正好在我们代码里 createSocketFactory 的时候使用了 TLSv1 这个证书协议
+SSLContext sslContext = SSLContext.getInstance("TLS");
+sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
+return new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1"}, null, new DefaultHostnameVerifier());
+所以就有两种方案,一个是使用更新版本的 TLS 或者另一个就是使用比较久的 jdk,这也说明其实即使都是 jdk8 的,不同的小版本差异还是会有些影响,有的时候对于这些错误还是需要更深入地学习,不能一概而之认为就是 jdk 用的是 oracle 还是 openjdk 的,不同的错误可能就需要仔细确认原因所在。
+]]>
+
+ java
+
+
+ java
+
+
+
+ mybatis系列-sql 类的简要分析
+ /2023/03/19/mybatis%E7%B3%BB%E5%88%97-sql-%E7%B1%BB%E7%9A%84%E7%AE%80%E8%A6%81%E5%88%86%E6%9E%90/
+ 上次就比较简单的讲了使用,这块也比较简单,因为封装得不是很复杂,首先我们从 select 作为入口来看看,这个具体的实现,
+String selectSql = new SQL() {{
+ SELECT("id", "name");
+ FROM("student");
+ WHERE("id = #{id}");
+ }}.toString();
+SELECT 方法的实现,
+public T SELECT(String... columns) {
+ sql().statementType = SQLStatement.StatementType.SELECT;
+ sql().select.addAll(Arrays.asList(columns));
+ return getSelf();
+}
+statementType是个枚举
+public enum StatementType {
+ DELETE, INSERT, SELECT, UPDATE
+}
+那这个就是个 select 语句,然后会把参数转成 list 添加到 select 变量里,
然后是 from 语句,这个大概也能猜到就是设置下表名,
+public T FROM(String table) {
+ sql().tables.add(table);
+ return getSelf();
+}
+往 tables 里添加了 table,这个 tables 是什么呢
这里也可以看下所有的变量,
+StatementType statementType;
+List<String> sets = new ArrayList<>();
+List<String> select = new ArrayList<>();
+List<String> tables = new ArrayList<>();
+List<String> join = new ArrayList<>();
+List<String> innerJoin = new ArrayList<>();
+List<String> outerJoin = new ArrayList<>();
+List<String> leftOuterJoin = new ArrayList<>();
+List<String> rightOuterJoin = new ArrayList<>();
+List<String> where = new ArrayList<>();
+List<String> having = new ArrayList<>();
+List<String> groupBy = new ArrayList<>();
+List<String> orderBy = new ArrayList<>();
+List<String> lastList = new ArrayList<>();
+List<String> columns = new ArrayList<>();
+List<List<String>> valuesList = new ArrayList<>();
+可以看到是一堆 List 先暂存这些sql 片段,然后再拼装成 sql 语句,
因为它重写了 toString 方法
+@Override
+public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sql().sql(sb);
+ return sb.toString();
+}
+调用的 sql 方法是
+public String sql(Appendable a) {
+ SafeAppendable builder = new SafeAppendable(a);
+ if (statementType == null) {
+ return null;
}
- builderAssistant.setCurrentNamespace(namespace);
- // 处理cache 和 cache 应用
- cacheRefElement(context.evalNode("cache-ref"));
- cacheElement(context.evalNode("cache"));
- parameterMapElement(context.evalNodes("/mapper/parameterMap"));
- resultMapElements(context.evalNodes("/mapper/resultMap"));
- sqlElement(context.evalNodes("/mapper/sql"));
- // 因为我们是个 sql 查询,所以具体逻辑是在这里面
- buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
- } catch (Exception e) {
- throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
- }
- }
-然后是
-private void buildStatementFromContext(List<XNode> list) {
- if (configuration.getDatabaseId() != null) {
- buildStatementFromContext(list, configuration.getDatabaseId());
- }
- // 然后没有 databaseId 就走到这
- buildStatementFromContext(list, null);
-}
-继续
-private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
- for (XNode context : list) {
- // 创建语句解析器
- final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
- try {
- // 解析节点
- statementParser.parseStatementNode();
- } catch (IncompleteElementException e) {
- configuration.addIncompleteStatement(statementParser);
- }
- }
-}
-这个代码比较长,做下简略,只保留相关代码
-public void parseStatementNode() {
- String id = context.getStringAttribute("id");
- String databaseId = context.getStringAttribute("databaseId");
- if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
- return;
- }
+ String answer;
- String nodeName = context.getNode().getNodeName();
- SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
- boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
- boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
- boolean useCache = context.getBooleanAttribute("useCache", isSelect);
- boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
+ switch (statementType) {
+ case DELETE:
+ answer = deleteSQL(builder);
+ break;
+
+ case INSERT:
+ answer = insertSQL(builder);
+ break;
+ case SELECT:
+ answer = selectSQL(builder);
+ break;
- // 简略前后代码,主要看这里,创建 sqlSource
+ case UPDATE:
+ answer = updateSQL(builder);
+ break;
- SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
-
-
-然后根据 LanguageDriver,我们这用的 XMLLanguageDriver,先是初始化
- @Override
- public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
- XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
- return builder.parseScriptNode();
- }
-// 初始化有一些逻辑
- public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
- super(configuration);
- this.context = context;
- this.parameterType = parameterType;
- // 特别是这,我这次特意在 mapper 中加了 foreach,就是为了说下这一块的解析
- initNodeHandlerMap();
- }
-// 设置各种类型的处理器
- private void initNodeHandlerMap() {
- nodeHandlerMap.put("trim", new TrimHandler());
- nodeHandlerMap.put("where", new WhereHandler());
- nodeHandlerMap.put("set", new SetHandler());
- nodeHandlerMap.put("foreach", new ForEachHandler());
- nodeHandlerMap.put("if", new IfHandler());
- nodeHandlerMap.put("choose", new ChooseHandler());
- nodeHandlerMap.put("when", new IfHandler());
- nodeHandlerMap.put("otherwise", new OtherwiseHandler());
- nodeHandlerMap.put("bind", new BindHandler());
- }
-初始化解析器以后就开始解析了
-public SqlSource parseScriptNode() {
- // 先是解析 parseDynamicTags
- MixedSqlNode rootSqlNode = parseDynamicTags(context);
- SqlSource sqlSource;
- if (isDynamic) {
- sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ default:
+ answer = null;
+ }
+
+ return answer;
+ }
+根据上面的 statementType判断是个什么 sql,我们这个是 selectSQL 就走的 SELECT 这个分支
+private String selectSQL(SafeAppendable builder) {
+ if (distinct) {
+ sqlClause(builder, "SELECT DISTINCT", select, "", "", ", ");
} else {
- sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+ sqlClause(builder, "SELECT", select, "", "", ", ");
}
- return sqlSource;
-}
-但是这里可能做的事情比较多
-protected MixedSqlNode parseDynamicTags(XNode node) {
- List<SqlNode> contents = new ArrayList<>();
- // 获取子节点,这里可以把我 xml 中的 SELECT 语句分成三部分,第一部分是 select 到 in,然后是 foreach 部分,最后是\n结束符
- NodeList children = node.getNode().getChildNodes();
- for (int i = 0; i < children.getLength(); i++) {
- XNode child = node.newXNode(children.item(i));
- // 第一个节点是个纯 text 节点就会走到这
- 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 {
- // 在 content 中添加这个 node
- contents.add(new StaticTextSqlNode(data));
+
+ sqlClause(builder, "FROM", tables, "", "", ", ");
+ joins(builder);
+ sqlClause(builder, "WHERE", where, "(", ")", " AND ");
+ sqlClause(builder, "GROUP BY", groupBy, "", "", ", ");
+ sqlClause(builder, "HAVING", having, "(", ")", " AND ");
+ sqlClause(builder, "ORDER BY", orderBy, "", "", ", ");
+ limitingRowsStrategy.appendClause(builder, offset, limit);
+ return builder.toString();
+}
+上面的可以看出来就是按我们常规的 sql 理解顺序来处理
就是select ... from ... where ... 这样子
再看下 sqlClause 的代码
+private void sqlClause(SafeAppendable builder, String keyword, List<String> parts, String open, String close,
+ String conjunction) {
+ if (!parts.isEmpty()) {
+ if (!builder.isEmpty()) {
+ builder.append("\n");
}
- } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
- // 第二个节点是个带 foreach 的,是个内部元素节点
- String nodeName = child.getNode().getNodeName();
- // 通过 nodeName 获取处理器
- NodeHandler handler = nodeHandlerMap.get(nodeName);
- if (handler == null) {
- throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+ builder.append(keyword);
+ builder.append(" ");
+ builder.append(open);
+ String last = "________";
+ for (int i = 0, n = parts.size(); i < n; i++) {
+ String part = parts.get(i);
+ if (i > 0 && !part.equals(AND) && !part.equals(OR) && !last.equals(AND) && !last.equals(OR)) {
+ builder.append(conjunction);
+ }
+ builder.append(part);
+ last = part;
}
- // 调用处理器来处理
- handler.handleNode(child, contents);
- isDynamic = true;
+ builder.append(close);
}
- }
- // 然后返回这个混合 sql 节点
- return new MixedSqlNode(contents);
- }
-再看下 handleNode 的逻辑
- @Override
- public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
- // 又会套娃执行这里的 parseDynamicTags
- MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
- String collection = nodeToHandle.getStringAttribute("collection");
- Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
- String item = nodeToHandle.getStringAttribute("item");
- String index = nodeToHandle.getStringAttribute("index");
- String open = nodeToHandle.getStringAttribute("open");
- String close = nodeToHandle.getStringAttribute("close");
- String separator = nodeToHandle.getStringAttribute("separator");
- ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
- targetContents.add(forEachSqlNode);
- }
-// 这里走的逻辑不一样了
-protected MixedSqlNode parseDynamicTags(XNode node) {
- List<SqlNode> contents = new ArrayList<>();
- // 这里是 foreach 内部的,所以是个 text_node
- NodeList children = node.getNode().getChildNodes();
- for (int i = 0; i < children.getLength(); i++) {
- XNode child = node.newXNode(children.item(i));
- // 第一个节点是个纯 text 节点就会走到这
- 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 {
- // 所以还是会走到这
- // 在 content 中添加这个 node
- contents.add(new StaticTextSqlNode(data));
- }
-// 最后继续包装成 MixedSqlNode
-// 再回到这里
- @Override
- public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
- MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
- // 处理 foreach 内部的各个变量
- String collection = nodeToHandle.getStringAttribute("collection");
- Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
- String item = nodeToHandle.getStringAttribute("item");
- String index = nodeToHandle.getStringAttribute("index");
- String open = nodeToHandle.getStringAttribute("open");
- String close = nodeToHandle.getStringAttribute("close");
- String separator = nodeToHandle.getStringAttribute("separator");
- ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
- targetContents.add(forEachSqlNode);
- }
-再回过来
-public SqlSource parseScriptNode() {
- MixedSqlNode rootSqlNode = parseDynamicTags(context);
- SqlSource sqlSource;
- // 因为在 foreach 节点处理时直接是把 isDynamic 置成了 true
- if (isDynamic) {
- // 所以是个 DynamicSqlSource
- sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
- } else {
- sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
- }
- return sqlSource;
-}
-这里就做完了预处理工作,真正在执行的执行的时候还需要进一步解析
-因为前面讲过很多了,所以直接跳到这里
- @Override
- public <T> T selectOne(String statement, Object parameter) {
- // Popular vote was to return null on 0 results and throw exception on too many.
- // 都知道是在这进去
- List<T> list = this.selectList(statement, parameter);
- if (list.size() == 1) {
- return list.get(0);
- } else if (list.size() > 1) {
- throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
- } else {
- return null;
- }
- }
+ }
+这里的拼接方式还需要判断 AND 和 OR 的判断逻辑,其他就没什么特别的了,只是where 语句中的 lastList 不知道是干嘛的,好像只有添加跟赋值的操作,有知道的大神也可以评论指导下
+]]>
+
+ Java
+ Mybatis
+
+
+ Java
+ Mysql
+ Mybatis
+
+
+
+ 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++
+
+
+
+ dnsmasq的一个使用注意点
+ /2023/04/16/dnsmasq%E7%9A%84%E4%B8%80%E4%B8%AA%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9/
+ 在本地使用了 valet 做 php 的开发环境,因为可以指定自定义域名和证书,碰巧最近公司的网络环境比较糟糕,就想要在自定义 dns 上下点功夫,本来我们经常要在 dns 那配置个内部的 dns 地址,就想是不是可以通过 dnsmasq 来解决,
却在第一步碰到个低级的问题,在 dnsmasq 的主配置文件里
我配置了解析文件路径配置
像这样
+resolv-file=/opt/homebrew/etc/dnsmasq.d/resolv.dnsmasq.conf
+结果发现 dnsmasq 就起不来了,因为是 brew 服务的形式起来,发现日志也没有, dnsmasq 配置文件本身也没什么日志,这个是最讨厌的,网上搜了一圈也都没有, brew services 的服务如果启动状态是 error,并且服务本身没有日志的话就是一头雾水,并且对于 plist 来说,即使我手动加了标准输出和错误输出,brew services restart 的时候也是会被重新覆盖,
后来仔细看了下这个问题,发现它下面有这么一行配置
+conf-dir=/opt/homebrew/etc/dnsmasq.d/,*.conf
+想了一下发现这个问题其实很简单,dnsmasq 应该是不支持同一配置文件加载两次,
我把 resolv 文件放在了同一个配置文件目录下,所以就被加载了两次,所以改掉目录就行了,但是目前看 dnsmasq 还不符合我的要求,也有可能我还没完全了解 dnsmasq 的使用方法,我想要的是比如按特定的域名后缀来配置对应的 dns 服务器,这样就不太会被影响,可以试试 AdGuard 看
+]]>
+
+ dns
+
+
+ dnsmasq
+
+
+
+ mybatis系列-mybatis是如何初始化mapper的
+ /2022/12/04/mybatis%E6%98%AF%E5%A6%82%E4%BD%95%E5%88%9D%E5%A7%8B%E5%8C%96mapper%E7%9A%84/
+ 前一篇讲了mybatis的初始化使用,如果我第一次看到这个使用入门文档,比较会产生疑惑的是配置了mapper,怎么就能通过selectOne跟语句id就能执行sql了,那么第一个问题,就是mapper是怎么被解析的,存在哪里,怎么被拿出来的
+添加解析mapper
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
+public SqlSessionFactory build(InputStream inputStream) {
+ return build(inputStream, null, null);
+}
- @Override
- public <E> List<E> selectList(String statement, Object parameter) {
- return this.selectList(statement, parameter, RowBounds.DEFAULT);
- }
- @Override
- public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
- return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
- }
- private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
+通过读取mybatis-config.xml来构建SqlSessionFactory,
+public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
+ try {
+ // 创建下xml的解析器
+ XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
+ // 进行解析,后再构建
+ return build(parser.parse());
+ } catch (Exception e) {
+ throw ExceptionFactory.wrapException("Error building SqlSession.", e);
+ } finally {
+ ErrorContext.instance().reset();
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();
- }
- }
- // 包括这里,是调用的org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
- @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);
- }
-// 然后是获取 BoundSql
- 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 (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ // Intentionally ignore. Prefer previous error.
}
+ }
- // 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();
- }
- }
- }
+创建XMLConfigBuilder
+public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
+ // --------> 创建 XPathParser
+ this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
+}
- return boundSql;
+public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
+ commonConstructor(validation, variables, entityResolver);
+ this.document = createDocument(new InputSource(inputStream));
}
-// 因为前面讲了是生成的 DynamicSqlSource,所以也是调用这个的 getBoundSql
- @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;
+
+private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
+ super(new Configuration());
+ ErrorContext.instance().resource("SQL Mapper Configuration");
+ this.configuration.setVariables(props);
+ this.parsed = false;
+ this.environment = environment;
+ this.parser = parser;
+}
+
+这里主要是创建了Builder包含了Parser
然后调用parse方法
+public Configuration parse() {
+ if (parsed) {
+ throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
-// 继续是这个 DynamicSqlNode 的 apply
- public boolean apply(DynamicContext context) {
- contents.forEach(node -> node.apply(context));
- return true;
+ // 标记下是否已解析,但是这里是否有线程安全问题
+ parsed = true;
+ // --------> 解析配置
+ parseConfiguration(parser.evalNode("/configuration"));
+ return configuration;
+}
+
+实际的解析区分了各类标签
+private void parseConfiguration(XNode root) {
+ try {
+ // issue #117 read properties first
+ // 解析properties,这个不是spring自带的,需要额外配置,并且在config文件里应该放在最前
+ 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"));
+ // ----------> 我们需要关注的是mapper的处理
+ mapperElement(root.evalNode("mappers"));
+ } catch (Exception e) {
+ throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
-// 看下面的图
-![image]()
-我们重点看 foreach 的逻辑
-@Override
- public boolean apply(DynamicContext context) {
- Map<String, Object> bindings = context.getBindings();
- final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
- Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
- if (iterable == null || !iterable.iterator().hasNext()) {
- return true;
- }
- boolean first = true;
- // 开始符号
- applyOpen(context);
- int i = 0;
- for (Object o : iterable) {
- DynamicContext oldContext = context;
- if (first || separator == null) {
- context = new PrefixedContext(context, "");
- } else {
- context = new PrefixedContext(context, separator);
- }
- int uniqueNumber = context.getUniqueNumber();
- // Issue #709
- if (o instanceof Map.Entry) {
- @SuppressWarnings("unchecked")
- Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
- applyIndex(context, mapEntry.getKey(), uniqueNumber);
- applyItem(context, mapEntry.getValue(), uniqueNumber);
+}
+
+然后就是调用到mapperElement方法了
+private void mapperElement(XNode parent) throws Exception {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ if ("package".equals(child.getName())) {
+ String mapperPackage = child.getStringAttribute("name");
+ configuration.addMappers(mapperPackage);
} else {
- applyIndex(context, i, uniqueNumber);
- applyItem(context, o, uniqueNumber);
- }
- // 转换变量名,变成这种形式 select * from student where id in
- // (
- // #{__frch_id_0}
- // )
- contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
- if (first) {
- first = !((PrefixedContext) context).isPrefixApplied();
- }
- context = oldContext;
- i++;
- }
- applyClose(context);
- context.getBindings().remove(item);
- context.getBindings().remove(index);
- return true;
- }
-// 回到外层就会调用 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());
- }
-![image]()
-可以看到这里,然后再进行替换
-![image]()
-真实的从 ? 替换成具体的变量值,是在这里
org.apache.ibatis.executor.SimpleExecutor#doQuery
调用了
-private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
- Statement stmt;
- Connection connection = getConnection(statementLog);
- stmt = handler.prepare(connection, transaction.getTimeout());
- handler.parameterize(stmt);
- return stmt;
- }
- @Override
- public void parameterize(Statement statement) throws SQLException {
- parameterHandler.setParameters((PreparedStatement) statement);
- }
- @Override
- public void setParameters(PreparedStatement ps) {
- ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
- List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
- if (parameterMappings != null) {
- for (int i = 0; i < parameterMappings.size(); i++) {
- ParameterMapping parameterMapping = parameterMappings.get(i);
- if (parameterMapping.getMode() != ParameterMode.OUT) {
- Object value;
- String propertyName = parameterMapping.getProperty();
- if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
- value = boundSql.getAdditionalParameter(propertyName);
- } else if (parameterObject == null) {
- value = null;
- } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
- value = parameterObject;
- } else {
- MetaObject metaObject = configuration.newMetaObject(parameterObject);
- value = metaObject.getValue(propertyName);
- }
- TypeHandler typeHandler = parameterMapping.getTypeHandler();
- JdbcType jdbcType = parameterMapping.getJdbcType();
- if (value == null && jdbcType == null) {
- jdbcType = configuration.getJdbcTypeForNull();
+ String resource = child.getStringAttribute("resource");
+ String url = child.getStringAttribute("url");
+ String mapperClass = child.getStringAttribute("class");
+ if (resource != null && url == null && mapperClass == null) {
+ ErrorContext.instance().resource(resource);
+ try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
+ // --------> 我们这没有指定package,所以是走到这
+ mapperParser.parse();
}
- try {
- // -------------------------->
- // 替换变量
- typeHandler.setParameter(ps, i + 1, value, jdbcType);
- } catch (TypeException | SQLException e) {
- throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
+ } else if (resource == null && url != null && mapperClass == null) {
+ ErrorContext.instance().resource(url);
+ try(InputStream inputStream = Resources.getUrlAsStream(url)){
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
+ mapperParser.parse();
}
+ } else if (resource == null && url == null && mapperClass != null) {
+ Class<?> mapperInterface = Resources.classForName(mapperClass);
+ configuration.addMapper(mapperInterface);
+ } else {
+ throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
- }
-]]>
-
- Java
- Mybatis
-
-
- Java
- Mysql
- Mybatis
-
-
-
- mybatis 的缓存是怎么回事
- /2020/10/03/mybatis-%E7%9A%84%E7%BC%93%E5%AD%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/
- Java 真的是任何一个中间件,比较常用的那种,都有很多内容值得深挖,比如这个缓存,慢慢有过一些感悟,比如如何提升性能,缓存无疑是一大重要手段,最底层开始 CPU 就有缓存,而且又小又贵,再往上一点内存一般作为硬盘存储在运行时的存储,一般在代码里也会用内存作为一些本地缓存,譬如数据库,像 mysql 这种也是有innodb_buffer_pool来提升查询效率,本质上理解就是用更快的存储作为相对慢存储的缓存,减少查询直接访问较慢的存储,并且这个都是相对的,比起 cpu 的缓存,那内存也是渣,但是与普通机械硬盘相比,那也是两个次元的水平。
-闲扯这么多来说说 mybatis 的缓存,mybatis 一般作为一个轻量级的 orm 使用,相对应的就是比较重量级的 hibernate,不过不在这次讨论范围,上一次是主要讲了 mybatis 在解析 sql 过程中,对于两种占位符的不同替换实现策略,这次主要聊下 mybatis 的缓存,前面其实得了解下前置的东西,比如 sqlsession,先当做我们知道 sqlsession 是个什么玩意,可能或多或少的知道 mybatis 是有两级缓存,
-一级缓存
第一级的缓存是在 BaseExecutor 中的 PerpetualCache,它是个最基本的缓存实现类,使用了 HashMap 实现缓存功能,代码其实没几十行
-public class PerpetualCache implements Cache {
-
- private final String id;
+ }
+}
- private final Map<Object, Object> cache = new HashMap<>();
+核心就在这个parse()方法
+public void parse() {
+ if (!configuration.isResourceLoaded(resource)) {
+ // -------> 然后就是走到这里,配置xml的mapper节点的内容
+ configurationElement(parser.evalNode("/mapper"));
+ configuration.addLoadedResource(resource);
+ bindMapperForNamespace();
+ }
- public PerpetualCache(String id) {
- this.id = id;
- }
+ parsePendingResultMaps();
+ parsePendingCacheRefs();
+ parsePendingStatements();
+}
- @Override
- public String getId() {
- return id;
- }
+具体的处理逻辑
+private void configurationElement(XNode context) {
+ try {
+ String namespace = context.getStringAttribute("namespace");
+ if (namespace == null || namespace.isEmpty()) {
+ throw new BuilderException("Mapper's namespace cannot be empty");
+ }
+ builderAssistant.setCurrentNamespace(namespace);
+ cacheRefElement(context.evalNode("cache-ref"));
+ cacheElement(context.evalNode("cache"));
+ parameterMapElement(context.evalNodes("/mapper/parameterMap"));
+ resultMapElements(context.evalNodes("/mapper/resultMap"));
+ sqlElement(context.evalNodes("/mapper/sql"));
+ // -------> 走到这,从上下文构建statement
+ buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
+ } catch (Exception e) {
+ throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
+ }
+}
- @Override
- public int getSize() {
- return cache.size();
- }
+具体代码在这,从上下文构建statement,只不过区分了下databaseId
+private void buildStatementFromContext(List<XNode> list) {
+ if (configuration.getDatabaseId() != null) {
+ buildStatementFromContext(list, configuration.getDatabaseId());
+ }
+ // -----> 判断databaseId
+ buildStatementFromContext(list, null);
+}
- @Override
- public void putObject(Object key, Object value) {
- cache.put(key, value);
- }
+判断下databaseId
+private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
+ for (XNode context : list) {
+ final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
+ try {
+ // -------> 解析statement节点
+ statementParser.parseStatementNode();
+ } catch (IncompleteElementException e) {
+ configuration.addIncompleteStatement(statementParser);
+ }
+ }
+}
- @Override
- public Object getObject(Object key) {
- return cache.get(key);
- }
+接下来就是真正处理的xml语句内容的,各个节点的信息内容
+public void parseStatementNode() {
+ String id = context.getStringAttribute("id");
+ String databaseId = context.getStringAttribute("databaseId");
- @Override
- public Object removeObject(Object key) {
- return cache.remove(key);
- }
+ if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
+ return;
+ }
- @Override
- public void clear() {
- cache.clear();
- }
+ String nodeName = context.getNode().getNodeName();
+ SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
+ boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
+ boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
+ boolean useCache = context.getBooleanAttribute("useCache", isSelect);
+ boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
- @Override
- public boolean equals(Object o) {
- if (getId() == null) {
- throw new CacheException("Cache instances require an ID.");
- }
- if (this == o) {
- return true;
- }
- if (!(o instanceof Cache)) {
- return false;
- }
+ // Include Fragments before parsing
+ XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
+ includeParser.applyIncludes(context.getNode());
- Cache otherCache = (Cache) o;
- return getId().equals(otherCache.getId());
- }
+ String parameterType = context.getStringAttribute("parameterType");
+ Class<?> parameterTypeClass = resolveClass(parameterType);
- @Override
- public int hashCode() {
- if (getId() == null) {
- throw new CacheException("Cache instances require an ID.");
- }
- return getId().hashCode();
- }
+ String lang = context.getStringAttribute("lang");
+ LanguageDriver langDriver = getLanguageDriver(lang);
-}
-可以看一下BaseExecutor 的构造函数
-protected BaseExecutor(Configuration configuration, Transaction transaction) {
- this.transaction = transaction;
- this.deferredLoads = new ConcurrentLinkedQueue<>();
- this.localCache = new PerpetualCache("LocalCache");
- this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
- this.closed = false;
- this.configuration = configuration;
- this.wrapper = this;
- }
-就是把 PerpetualCache 作为 localCache,然后怎么使用我看简单看一下,BaseExecutor 的查询首先是调用这个函数
-@Override
- public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameter);
- CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
- return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
- }
-可以看到首先是调用了 createCacheKey 方法,这个方法呢,先不看怎么写的,如果我们自己要实现这么个缓存,首先这个缓存 key 的设计也是个问题,如果是以表名加主键作为 key,那么分页查询,或者没有主键的时候就不行,来看看 mybatis 是怎么设计的
-@Override
- public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
- if (closed) {
- throw new ExecutorException("Executor was closed.");
- }
- CacheKey cacheKey = new CacheKey();
- cacheKey.update(ms.getId());
- cacheKey.update(rowBounds.getOffset());
- cacheKey.update(rowBounds.getLimit());
- cacheKey.update(boundSql.getSql());
- List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
- TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
- // mimic DefaultParameterHandler logic
- for (ParameterMapping parameterMapping : parameterMappings) {
- if (parameterMapping.getMode() != ParameterMode.OUT) {
- Object value;
- String propertyName = parameterMapping.getProperty();
- if (boundSql.hasAdditionalParameter(propertyName)) {
- value = boundSql.getAdditionalParameter(propertyName);
- } else if (parameterObject == null) {
- value = null;
- } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
- value = parameterObject;
- } else {
- MetaObject metaObject = configuration.newMetaObject(parameterObject);
- value = metaObject.getValue(propertyName);
- }
- cacheKey.update(value);
- }
- }
- if (configuration.getEnvironment() != null) {
- // issue #176
- cacheKey.update(configuration.getEnvironment().getId());
- }
- return cacheKey;
+ // Parse selectKey after includes and remove them.
+ processSelectKeyNodes(id, parameterTypeClass, langDriver);
+
+ // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
+ KeyGenerator keyGenerator;
+ String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
+ keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
+ if (configuration.hasKeyGenerator(keyStatementId)) {
+ keyGenerator = configuration.getKeyGenerator(keyStatementId);
+ } else {
+ keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
+ configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
+ ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
-
-首先需要 id,这个 id 是 mapper 里方法的 id, 然后是偏移量跟返回行数,再就是 sql,然后是参数,基本上是会有影响的都加进去了,在这个 update 里面
-public void update(Object object) {
- int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
- count++;
- checksum += baseHashCode;
- baseHashCode *= count;
+ // 语句的主要参数解析
+ SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
+ StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
+ Integer fetchSize = context.getIntAttribute("fetchSize");
+ Integer timeout = context.getIntAttribute("timeout");
+ String parameterMap = context.getStringAttribute("parameterMap");
+ String resultType = context.getStringAttribute("resultType");
+ Class<?> resultTypeClass = resolveClass(resultType);
+ String resultMap = context.getStringAttribute("resultMap");
+ String resultSetType = context.getStringAttribute("resultSetType");
+ ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
+ if (resultSetTypeEnum == null) {
+ resultSetTypeEnum = configuration.getDefaultResultSetType();
+ }
+ String keyProperty = context.getStringAttribute("keyProperty");
+ String keyColumn = context.getStringAttribute("keyColumn");
+ String resultSets = context.getStringAttribute("resultSets");
- hashcode = multiplier * hashcode + baseHashCode;
+ // --------> 添加映射的statement
+ builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
+ fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
+ resultSetTypeEnum, flushCache, useCache, resultOrdered,
+ keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
+}
- updateList.add(object);
- }
-其实是一个 hash 转换,具体不纠结,就是提高特异性,然后回来就是继续调用 query
-@Override
- public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
- ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
- if (closed) {
- throw new ExecutorException("Executor was closed.");
- }
- if (queryStack == 0 && ms.isFlushCacheRequired()) {
- clearLocalCache();
- }
- List<E> list;
- try {
- queryStack++;
- list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
- if (list != null) {
- handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
- } else {
- list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
- }
- } finally {
- queryStack--;
- }
- if (queryStack == 0) {
- for (DeferredLoad deferredLoad : deferredLoads) {
- deferredLoad.load();
- }
- // issue #601
- deferredLoads.clear();
- if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
- // issue #482
- clearLocalCache();
- }
- }
- return list;
- }
-可以看到是先从 localCache 里取,取不到再 queryFromDatabase,其实比较简单,这是一级缓存,考虑到 sqlsession 跟 BaseExecutor 的关系,其实是随着 sqlsession 来保证这个缓存不会出现脏数据幻读的情况,当然事务相关的后面可能再单独聊。
-二级缓存
其实这个一级二级顺序有点反过来,其实查询的是先走的二级缓存,当然二级的需要配置开启,默认不开,
需要通过
-<setting name="cacheEnabled" value="true"/>
-来开启,然后我们的查询就会走到
-public class CachingExecutor implements Executor {
- private final Executor delegate;
- private final TransactionalCacheManager tcm = new TransactionalCacheManager();
-这个 Executor 中,这里我放了类里面的元素,发现没有一个 Cache 类,这就是一个特点了,往下看查询过程
-@Override
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameterObject);
- CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
- return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+添加的逻辑具体可以看下
+public MappedStatement addMappedStatement(
+ String id,
+ SqlSource sqlSource,
+ StatementType statementType,
+ SqlCommandType sqlCommandType,
+ Integer fetchSize,
+ Integer timeout,
+ String parameterMap,
+ Class<?> parameterType,
+ String resultMap,
+ Class<?> resultType,
+ ResultSetType resultSetType,
+ boolean flushCache,
+ boolean useCache,
+ boolean resultOrdered,
+ KeyGenerator keyGenerator,
+ String keyProperty,
+ String keyColumn,
+ String databaseId,
+ LanguageDriver lang,
+ String resultSets) {
+
+ if (unresolvedCacheRef) {
+ throw new IncompleteElementException("Cache-ref not yet resolved");
}
- @Override
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
- throws SQLException {
- Cache cache = ms.getCache();
- if (cache != null) {
- flushCacheIfRequired(ms);
- if (ms.isUseCache() && resultHandler == null) {
- ensureNoOutParams(ms, boundSql);
- @SuppressWarnings("unchecked")
- List<E> list = (List<E>) tcm.getObject(cache, key);
- if (list == null) {
- list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- tcm.putObject(cache, key, list); // issue #578 and #116
- }
- return list;
- }
- }
- return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
-看到没,其实缓存是从 tcm 这个成员变量里取,而这个是什么呢,事务性缓存(直译下),因为这个其实是用 MappedStatement 里的 Cache 作为key 从 tcm 的 map 取出来的
-public class TransactionalCacheManager {
+ id = applyCurrentNamespace(id, false);
+ boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
- private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
-MappedStatement是被全局使用的,所以其实二级缓存是跟着 mapper 的 namespace 走的,可以被多个 CachingExecutor 获取到,就会出现线程安全问题,线程安全问题可以用SynchronizedCache来解决,就是加锁,但是对于事务中的脏读,使用了TransactionalCache来解决这个问题,
-public class TransactionalCache implements Cache {
+ MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
+ .resource(resource)
+ .fetchSize(fetchSize)
+ .timeout(timeout)
+ .statementType(statementType)
+ .keyGenerator(keyGenerator)
+ .keyProperty(keyProperty)
+ .keyColumn(keyColumn)
+ .databaseId(databaseId)
+ .lang(lang)
+ .resultOrdered(resultOrdered)
+ .resultSets(resultSets)
+ .resultMaps(getStatementResultMaps(resultMap, resultType, id))
+ .resultSetType(resultSetType)
+ .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
+ .useCache(valueOrDefault(useCache, isSelect))
+ .cache(currentCache);
- private static final Log log = LogFactory.getLog(TransactionalCache.class);
+ ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
+ if (statementParameterMap != null) {
+ statementBuilder.parameterMap(statementParameterMap);
+ }
- private final Cache delegate;
- private boolean clearOnCommit;
- private final Map<Object, Object> entriesToAddOnCommit;
- private final Set<Object> entriesMissedInCache;
-在事务还没提交的时候,会把中间状态的数据放在 entriesToAddOnCommit 中,只有在提交后会放进共享缓存中,
-public void commit() {
- if (clearOnCommit) {
- delegate.clear();
- }
- flushPendingEntries();
- reset();
- }
]]>
+ MappedStatement statement = statementBuilder.build();
+ // ------> 正好是这里在configuration中添加了映射好的statement
+ configuration.addMappedStatement(statement);
+ return statement;
+}
+
+而里面就是往map里添加
+public void addMappedStatement(MappedStatement ms) {
+ mappedStatements.put(ms.getId(), ms);
+}
+
+获取mapper
StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
+
+就是调用了 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
+public <T> T selectOne(String statement, Object parameter) {
+ // Popular vote was to return null on 0 results and throw exception on too many.
+ List<T> list = this.selectList(statement, parameter);
+ if (list.size() == 1) {
+ return list.get(0);
+ } else if (list.size() > 1) {
+ throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
+ } else {
+ return null;
+ }
+}
+
+调用实际的实现方法
+public <E> List<E> selectList(String statement, Object parameter) {
+ return this.selectList(statement, parameter, RowBounds.DEFAULT);
+}
+
+这里还有一层
+public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
+ return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
+}
+
+
+根本的就是从configuration里获取了mappedStatement
+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();
+ }
+}
+]]>
Java
Mybatis
- Spring
- Mybatis
- 缓存
- Mybatis
Java
Mysql
Mybatis
- 缓存
- mybatis系列-dataSource解析
- /2023/01/08/mybatis%E7%B3%BB%E5%88%97-dataSource%E8%A7%A3%E6%9E%90/
- DataSource 作为数据库查询的最重要的数据源,在 mybatis 中也展开来说下
首先是解析的过程
-SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-
-在构建 SqlSessionFactory 也就是 DefaultSqlSessionFactory 的时候,
-public SqlSessionFactory build(InputStream inputStream) {
- return build(inputStream, null, null);
+ mybatis系列-foreach 解析
+ /2023/06/11/mybatis%E7%B3%BB%E5%88%97-foreach-%E8%A7%A3%E6%9E%90/
+ 在 org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration 中进行配置解析,其中这一行就是解析 mappers
+mapperElement(root.evalNode("mappers"));
+具体的代码会执行到这
+private void mapperElement(XNode parent) throws Exception {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ if ("package".equals(child.getName())) {
+ // 这里解析的不是 package
+ String mapperPackage = child.getStringAttribute("name");
+ configuration.addMappers(mapperPackage);
+ } else {
+ // 根据 resource 和 url 还有 mapperClass 判断
+ String resource = child.getStringAttribute("resource");
+ String url = child.getStringAttribute("url");
+ String mapperClass = child.getStringAttribute("class");
+ // resource 不为空其他为空的情况,就开始将 resource 读成输入流
+ if (resource != null && url == null && mapperClass == null) {
+ ErrorContext.instance().resource(resource);
+ try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
+ // 初始化 XMLMapperBuilder 来解析 mapper
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
+ mapperParser.parse();
+ }
+然后再是 parse 过程
+public void parse() {
+ if (!configuration.isResourceLoaded(resource)) {
+ // 解析 mapper 节点,也就是下图中的mapper
+ configurationElement(parser.evalNode("/mapper"));
+ configuration.addLoadedResource(resource);
+ bindMapperForNamespace();
}
-public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
+
+ parsePendingResultMaps();
+ parsePendingCacheRefs();
+ parsePendingStatements();
+}
+![image]()
+继续往下走
+private void configurationElement(XNode context) {
try {
- XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
- return build(parser.parse());
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error building SqlSession.", e);
- } finally {
- ErrorContext.instance().reset();
- try {
- if (inputStream != null) {
- inputStream.close();
- }
- } catch (IOException e) {
- // Intentionally ignore. Prefer previous error.
+ String namespace = context.getStringAttribute("namespace");
+ if (namespace == null || namespace.isEmpty()) {
+ throw new BuilderException("Mapper's namespace cannot be empty");
}
+ builderAssistant.setCurrentNamespace(namespace);
+ // 处理cache 和 cache 应用
+ cacheRefElement(context.evalNode("cache-ref"));
+ cacheElement(context.evalNode("cache"));
+ parameterMapElement(context.evalNodes("/mapper/parameterMap"));
+ resultMapElements(context.evalNodes("/mapper/resultMap"));
+ sqlElement(context.evalNodes("/mapper/sql"));
+ // 因为我们是个 sql 查询,所以具体逻辑是在这里面
+ buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
+ } catch (Exception e) {
+ throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
- }
-前面也说过,就是解析 mybatis-config.xml 成 Configuration
-public Configuration parse() {
- if (parsed) {
- throw new BuilderException("Each XMLConfigBuilder can only be used once.");
- }
- parsed = true;
- parseConfiguration(parser.evalNode("/configuration"));
- return configuration;
-}
-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
- // -------------> 是在这里解析了DataSource
- 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);
+ }
+然后是
+private void buildStatementFromContext(List<XNode> list) {
+ if (configuration.getDatabaseId() != null) {
+ buildStatementFromContext(list, configuration.getDatabaseId());
}
-}
-环境解析了这一块的内容
-<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>
-解析也是自上而下的,
-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;
- }
+ // 然后没有 databaseId 就走到这
+ buildStatementFromContext(list, null);
+}
+继续
+private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
+ for (XNode context : list) {
+ // 创建语句解析器
+ final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
+ try {
+ // 解析节点
+ statementParser.parseStatementNode();
+ } catch (IncompleteElementException e) {
+ configuration.addIncompleteStatement(statementParser);
}
}
-}
-前面第一步是解析事务管理器元素
-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.");
-}
-而这里的 resolveClass 其实就使用了上一篇的 typeAliases 系统,这里是使用了 JdbcTransactionFactory 作为事务管理器,
后面的就是 DataSourceFactory 的创建也是 DataSource 的创建
-private DataSourceFactory dataSourceElement(XNode context) throws Exception {
- if (context != null) {
- String type = context.getStringAttribute("type");
- Properties props = context.getChildrenAsProperties();
- DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
- factory.setProperties(props);
- return factory;
- }
- throw new BuilderException("Environment declaration requires a DataSourceFactory.");
-}
-因为在config文件中设置了Pooled,所以对应创建的就是 PooledDataSourceFactory
但是这里其实有个比较需要注意的,mybatis 这里的其实是继承了 UnpooledDataSourceFactory
将基础方法都放在了 UnpooledDataSourceFactory 中
-public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
+}
+这个代码比较长,做下简略,只保留相关代码
+public void parseStatementNode() {
+ String id = context.getStringAttribute("id");
+ String databaseId = context.getStringAttribute("databaseId");
- public PooledDataSourceFactory() {
- this.dataSource = new PooledDataSource();
- }
+ if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
+ return;
+ }
-}
-这里只保留了在构造方法里创建 DataSource
而这个 PooledDataSource 虽然没有直接继承 UnpooledDataSource,但其实
在构造方法里也是
-public PooledDataSource() {
- dataSource = new UnpooledDataSource();
-}
-至于为什么这么做呢应该也是考虑到能比较多的复用代码,因为 Pooled 其实跟 Unpooled 最重要的差别就在于是不是每次都重开连接
使用连接池能够让应用在有大量查询的时候不用反复创建连接,省去了建联的网络等开销,Unpooled就是完成与数据库的连接,并可以获取该连接
主要的代码
-@Override
-public Connection getConnection() throws SQLException {
- return doGetConnection(username, password);
-}
+ String nodeName = context.getNode().getNodeName();
+ SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
+ boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
+ boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
+ boolean useCache = context.getBooleanAttribute("useCache", isSelect);
+ boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
-@Override
-public Connection getConnection(String username, String password) throws SQLException {
- return doGetConnection(username, password);
-}
-private Connection doGetConnection(String username, String password) throws SQLException {
- Properties props = new Properties();
- if (driverProperties != null) {
- props.putAll(driverProperties);
+
+ // 简略前后代码,主要看这里,创建 sqlSource
+
+ SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
+
+
+然后根据 LanguageDriver,我们这用的 XMLLanguageDriver,先是初始化
+ @Override
+ public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
+ XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
+ return builder.parseScriptNode();
}
- if (username != null) {
- props.setProperty("user", username);
+// 初始化有一些逻辑
+ public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
+ super(configuration);
+ this.context = context;
+ this.parameterType = parameterType;
+ // 特别是这,我这次特意在 mapper 中加了 foreach,就是为了说下这一块的解析
+ initNodeHandlerMap();
}
- if (password != null) {
- props.setProperty("password", password);
+// 设置各种类型的处理器
+ private void initNodeHandlerMap() {
+ nodeHandlerMap.put("trim", new TrimHandler());
+ nodeHandlerMap.put("where", new WhereHandler());
+ nodeHandlerMap.put("set", new SetHandler());
+ nodeHandlerMap.put("foreach", new ForEachHandler());
+ nodeHandlerMap.put("if", new IfHandler());
+ nodeHandlerMap.put("choose", new ChooseHandler());
+ nodeHandlerMap.put("when", new IfHandler());
+ nodeHandlerMap.put("otherwise", new OtherwiseHandler());
+ nodeHandlerMap.put("bind", new BindHandler());
+ }
+初始化解析器以后就开始解析了
+public SqlSource parseScriptNode() {
+ // 先是解析 parseDynamicTags
+ MixedSqlNode rootSqlNode = parseDynamicTags(context);
+ SqlSource sqlSource;
+ if (isDynamic) {
+ sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ } else {
+ sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
- return doGetConnection(props);
-}
-private Connection doGetConnection(Properties properties) throws SQLException {
- initializeDriver();
- Connection connection = DriverManager.getConnection(url, properties);
- configureConnection(connection);
- return connection;
-}
-而对于Pooled就会处理池化的逻辑
-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.");
- }
+ return sqlSource;
+}
+但是这里可能做的事情比较多
+protected MixedSqlNode parseDynamicTags(XNode node) {
+ List<SqlNode> contents = new ArrayList<>();
+ // 获取子节点,这里可以把我 xml 中的 SELECT 语句分成三部分,第一部分是 select 到 in,然后是 foreach 部分,最后是\n结束符
+ NodeList children = node.getNode().getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ XNode child = node.newXNode(children.item(i));
+ // 第一个节点是个纯 text 节点就会走到这
+ 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 {
- // 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
- 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;
- }
- }
- }
+ // 在 content 中添加这个 node
+ contents.add(new StaticTextSqlNode(data));
}
- 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.");
- }
- }
+ } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
+ // 第二个节点是个带 foreach 的,是个内部元素节点
+ String nodeName = child.getNode().getNodeName();
+ // 通过 nodeName 获取处理器
+ NodeHandler handler = nodeHandlerMap.get(nodeName);
+ if (handler == null) {
+ throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
- } finally {
- lock.unlock();
+ // 调用处理器来处理
+ handler.handleNode(child, contents);
+ isDynamic = true;
}
-
}
-
- 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.");
+ // 然后返回这个混合 sql 节点
+ return new MixedSqlNode(contents);
+ }
+再看下 handleNode 的逻辑
+ @Override
+ public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
+ // 又会套娃执行这里的 parseDynamicTags
+ MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
+ String collection = nodeToHandle.getStringAttribute("collection");
+ Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
+ String item = nodeToHandle.getStringAttribute("item");
+ String index = nodeToHandle.getStringAttribute("index");
+ String open = nodeToHandle.getStringAttribute("open");
+ String close = nodeToHandle.getStringAttribute("close");
+ String separator = nodeToHandle.getStringAttribute("separator");
+ ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
+ targetContents.add(forEachSqlNode);
}
+// 这里走的逻辑不一样了
+protected MixedSqlNode parseDynamicTags(XNode node) {
+ List<SqlNode> contents = new ArrayList<>();
+ // 这里是 foreach 内部的,所以是个 text_node
+ NodeList children = node.getNode().getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ XNode child = node.newXNode(children.item(i));
+ // 第一个节点是个纯 text 节点就会走到这
+ 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 {
+ // 所以还是会走到这
+ // 在 content 中添加这个 node
+ contents.add(new StaticTextSqlNode(data));
+ }
+// 最后继续包装成 MixedSqlNode
+// 再回到这里
+ @Override
+ public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
+ MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
+ // 处理 foreach 内部的各个变量
+ String collection = nodeToHandle.getStringAttribute("collection");
+ Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
+ String item = nodeToHandle.getStringAttribute("item");
+ String index = nodeToHandle.getStringAttribute("index");
+ String open = nodeToHandle.getStringAttribute("open");
+ String close = nodeToHandle.getStringAttribute("close");
+ String separator = nodeToHandle.getStringAttribute("separator");
+ ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
+ targetContents.add(forEachSqlNode);
+ }
+再回过来
+public SqlSource parseScriptNode() {
+ MixedSqlNode rootSqlNode = parseDynamicTags(context);
+ SqlSource sqlSource;
+ // 因为在 foreach 节点处理时直接是把 isDynamic 置成了 true
+ if (isDynamic) {
+ // 所以是个 DynamicSqlSource
+ sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ } else {
+ sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+ }
+ return sqlSource;
+}
+这里就做完了预处理工作,真正在执行的执行的时候还需要进一步解析
+因为前面讲过很多了,所以直接跳到这里
+ @Override
+ public <T> T selectOne(String statement, Object parameter) {
+ // Popular vote was to return null on 0 results and throw exception on too many.
+ // 都知道是在这进去
+ List<T> list = this.selectList(statement, parameter);
+ if (list.size() == 1) {
+ return list.get(0);
+ } else if (list.size() > 1) {
+ throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
+ } else {
+ return null;
+ }
+ }
- return conn;
- }
-它的入口不是个get方法,而是pop,从含义来来讲就不一样
org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection()
-@Override
-public Connection getConnection() throws SQLException {
- return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-}
-对于具体怎么获取连接我们可以下一篇具体讲下
-]]>
-
- Java
- Mybatis
-
-
- Java
- Mysql
- Mybatis
-
-
-
- mybatis系列-mybatis是如何初始化mapper的
- /2022/12/04/mybatis%E6%98%AF%E5%A6%82%E4%BD%95%E5%88%9D%E5%A7%8B%E5%8C%96mapper%E7%9A%84/
- 前一篇讲了mybatis的初始化使用,如果我第一次看到这个使用入门文档,比较会产生疑惑的是配置了mapper,怎么就能通过selectOne跟语句id就能执行sql了,那么第一个问题,就是mapper是怎么被解析的,存在哪里,怎么被拿出来的
-添加解析mapper
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
-public SqlSessionFactory build(InputStream inputStream) {
- return build(inputStream, null, null);
-}
-
-通过读取mybatis-config.xml来构建SqlSessionFactory,
-public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
- try {
- // 创建下xml的解析器
- XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
- // 进行解析,后再构建
- return build(parser.parse());
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error building SqlSession.", e);
- } finally {
- ErrorContext.instance().reset();
+ @Override
+ public <E> List<E> selectList(String statement, Object parameter) {
+ return this.selectList(statement, parameter, RowBounds.DEFAULT);
+ }
+ @Override
+ public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
+ return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
+ }
+ private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
- if (inputStream != null) {
- inputStream.close();
- }
- } catch (IOException e) {
- // Intentionally ignore. Prefer previous error.
+ // 前面也讲过这个,
+ 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();
}
- }
-
-创建XMLConfigBuilder
-public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
- // --------> 创建 XPathParser
- this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
-}
-
-public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
- commonConstructor(validation, variables, entityResolver);
- this.document = createDocument(new InputSource(inputStream));
}
-
-private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
- super(new Configuration());
- ErrorContext.instance().resource("SQL Mapper Configuration");
- this.configuration.setVariables(props);
- this.parsed = false;
- this.environment = environment;
- this.parser = parser;
-}
-
-这里主要是创建了Builder包含了Parser
然后调用parse方法
-public Configuration parse() {
- if (parsed) {
- throw new BuilderException("Each XMLConfigBuilder can only be used once.");
+ // 包括这里,是调用的org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
+ @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);
}
- // 标记下是否已解析,但是这里是否有线程安全问题
- parsed = true;
- // --------> 解析配置
- parseConfiguration(parser.evalNode("/configuration"));
- return configuration;
-}
+// 然后是获取 BoundSql
+ 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);
+ }
-实际的解析区分了各类标签
-private void parseConfiguration(XNode root) {
- try {
- // issue #117 read properties first
- // 解析properties,这个不是spring自带的,需要额外配置,并且在config文件里应该放在最前
- 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"));
- // ----------> 我们需要关注的是mapper的处理
- mapperElement(root.evalNode("mappers"));
- } catch (Exception e) {
- throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
- }
-}
+ // 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();
+ }
+ }
+ }
-然后就是调用到mapperElement方法了
-private void mapperElement(XNode parent) throws Exception {
- if (parent != null) {
- for (XNode child : parent.getChildren()) {
- if ("package".equals(child.getName())) {
- String mapperPackage = child.getStringAttribute("name");
- configuration.addMappers(mapperPackage);
+ return boundSql;
+ }
+// 因为前面讲了是生成的 DynamicSqlSource,所以也是调用这个的 getBoundSql
+ @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;
+ }
+// 继续是这个 DynamicSqlNode 的 apply
+ public boolean apply(DynamicContext context) {
+ contents.forEach(node -> node.apply(context));
+ return true;
+ }
+// 看下面的图
+![image]()
+我们重点看 foreach 的逻辑
+@Override
+ public boolean apply(DynamicContext context) {
+ Map<String, Object> bindings = context.getBindings();
+ final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
+ Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
+ if (iterable == null || !iterable.iterator().hasNext()) {
+ return true;
+ }
+ boolean first = true;
+ // 开始符号
+ applyOpen(context);
+ int i = 0;
+ for (Object o : iterable) {
+ DynamicContext oldContext = context;
+ if (first || separator == null) {
+ context = new PrefixedContext(context, "");
} else {
- String resource = child.getStringAttribute("resource");
- String url = child.getStringAttribute("url");
- String mapperClass = child.getStringAttribute("class");
- if (resource != null && url == null && mapperClass == null) {
- ErrorContext.instance().resource(resource);
- try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
- XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
- // --------> 我们这没有指定package,所以是走到这
- mapperParser.parse();
- }
- } else if (resource == null && url != null && mapperClass == null) {
- ErrorContext.instance().resource(url);
- try(InputStream inputStream = Resources.getUrlAsStream(url)){
- XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
- mapperParser.parse();
- }
- } else if (resource == null && url == null && mapperClass != null) {
- Class<?> mapperInterface = Resources.classForName(mapperClass);
- configuration.addMapper(mapperInterface);
- } else {
- throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
- }
+ context = new PrefixedContext(context, separator);
+ }
+ int uniqueNumber = context.getUniqueNumber();
+ // Issue #709
+ if (o instanceof Map.Entry) {
+ @SuppressWarnings("unchecked")
+ Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
+ applyIndex(context, mapEntry.getKey(), uniqueNumber);
+ applyItem(context, mapEntry.getValue(), uniqueNumber);
+ } else {
+ applyIndex(context, i, uniqueNumber);
+ applyItem(context, o, uniqueNumber);
+ }
+ // 转换变量名,变成这种形式 select * from student where id in
+ // (
+ // #{__frch_id_0}
+ // )
+ contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
+ if (first) {
+ first = !((PrefixedContext) context).isPrefixApplied();
}
+ context = oldContext;
+ i++;
}
+ applyClose(context);
+ context.getBindings().remove(item);
+ context.getBindings().remove(index);
+ return true;
}
-}
-
-核心就在这个parse()方法
-public void parse() {
- if (!configuration.isResourceLoaded(resource)) {
- // -------> 然后就是走到这里,配置xml的mapper节点的内容
- configurationElement(parser.evalNode("/mapper"));
- configuration.addLoadedResource(resource);
- bindMapperForNamespace();
- }
-
- parsePendingResultMaps();
- parsePendingCacheRefs();
- parsePendingStatements();
-}
-
-具体的处理逻辑
-private void configurationElement(XNode context) {
- try {
- String namespace = context.getStringAttribute("namespace");
- if (namespace == null || namespace.isEmpty()) {
- throw new BuilderException("Mapper's namespace cannot be empty");
+// 回到外层就会调用 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);
}
- builderAssistant.setCurrentNamespace(namespace);
- cacheRefElement(context.evalNode("cache-ref"));
- cacheElement(context.evalNode("cache"));
- parameterMapElement(context.evalNodes("/mapper/parameterMap"));
- resultMapElements(context.evalNodes("/mapper/resultMap"));
- sqlElement(context.evalNodes("/mapper/sql"));
- // -------> 走到这,从上下文构建statement
- buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
- } catch (Exception e) {
- throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
+ return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
+ }
+![image]()
+可以看到这里,然后再进行替换
+![image]()
+真实的从 ? 替换成具体的变量值,是在这里
org.apache.ibatis.executor.SimpleExecutor#doQuery
调用了
+private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
+ Statement stmt;
+ Connection connection = getConnection(statementLog);
+ stmt = handler.prepare(connection, transaction.getTimeout());
+ handler.parameterize(stmt);
+ return stmt;
}
-}
-
-具体代码在这,从上下文构建statement,只不过区分了下databaseId
-private void buildStatementFromContext(List<XNode> list) {
- if (configuration.getDatabaseId() != null) {
- buildStatementFromContext(list, configuration.getDatabaseId());
+ @Override
+ public void parameterize(Statement statement) throws SQLException {
+ parameterHandler.setParameters((PreparedStatement) statement);
}
- // -----> 判断databaseId
- buildStatementFromContext(list, null);
-}
-
-判断下databaseId
-private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
- for (XNode context : list) {
- final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
- try {
- // -------> 解析statement节点
- statementParser.parseStatementNode();
- } catch (IncompleteElementException e) {
- configuration.addIncompleteStatement(statementParser);
+ @Override
+ public void setParameters(PreparedStatement ps) {
+ ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
+ List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
+ if (parameterMappings != null) {
+ for (int i = 0; i < parameterMappings.size(); i++) {
+ ParameterMapping parameterMapping = parameterMappings.get(i);
+ if (parameterMapping.getMode() != ParameterMode.OUT) {
+ Object value;
+ String propertyName = parameterMapping.getProperty();
+ if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
+ value = boundSql.getAdditionalParameter(propertyName);
+ } else if (parameterObject == null) {
+ value = null;
+ } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
+ value = parameterObject;
+ } else {
+ MetaObject metaObject = configuration.newMetaObject(parameterObject);
+ value = metaObject.getValue(propertyName);
+ }
+ TypeHandler typeHandler = parameterMapping.getTypeHandler();
+ JdbcType jdbcType = parameterMapping.getJdbcType();
+ if (value == null && jdbcType == null) {
+ jdbcType = configuration.getJdbcTypeForNull();
+ }
+ try {
+ // -------------------------->
+ // 替换变量
+ typeHandler.setParameter(ps, i + 1, value, jdbcType);
+ } catch (TypeException | SQLException e) {
+ throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
+ }
+ }
+ }
}
- }
-}
-
-接下来就是真正处理的xml语句内容的,各个节点的信息内容
-public void parseStatementNode() {
- String id = context.getStringAttribute("id");
- String databaseId = context.getStringAttribute("databaseId");
-
- if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
- return;
- }
-
- String nodeName = context.getNode().getNodeName();
- SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
- boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
- boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
- boolean useCache = context.getBooleanAttribute("useCache", isSelect);
- boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
-
- // Include Fragments before parsing
- XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
- includeParser.applyIncludes(context.getNode());
-
- String parameterType = context.getStringAttribute("parameterType");
- Class<?> parameterTypeClass = resolveClass(parameterType);
-
- String lang = context.getStringAttribute("lang");
- LanguageDriver langDriver = getLanguageDriver(lang);
-
- // Parse selectKey after includes and remove them.
- processSelectKeyNodes(id, parameterTypeClass, langDriver);
+ }
+]]>
+
+ Java
+ Mybatis
+
+
+ Java
+ Mysql
+ Mybatis
+
+
+
+ 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;
- // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
- KeyGenerator keyGenerator;
- String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
- keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
- if (configuration.hasKeyGenerator(keyStatementId)) {
- keyGenerator = configuration.getKeyGenerator(keyStatementId);
- } else {
- keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
- configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
- ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
- }
-
- // 语句的主要参数解析
- SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
- StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
- Integer fetchSize = context.getIntAttribute("fetchSize");
- Integer timeout = context.getIntAttribute("timeout");
- String parameterMap = context.getStringAttribute("parameterMap");
- String resultType = context.getStringAttribute("resultType");
- Class<?> resultTypeClass = resolveClass(resultType);
- String resultMap = context.getStringAttribute("resultMap");
- String resultSetType = context.getStringAttribute("resultSetType");
- ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
- if (resultSetTypeEnum == null) {
- resultSetTypeEnum = configuration.getDefaultResultSetType();
- }
- String keyProperty = context.getStringAttribute("keyProperty");
- String keyColumn = context.getStringAttribute("keyColumn");
- String resultSets = context.getStringAttribute("resultSets");
-
- // --------> 添加映射的statement
- builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
- fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
- resultSetTypeEnum, flushCache, useCache, resultOrdered,
- keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
-}
-
-
-添加的逻辑具体可以看下
-public MappedStatement addMappedStatement(
- String id,
- SqlSource sqlSource,
- StatementType statementType,
- SqlCommandType sqlCommandType,
- Integer fetchSize,
- Integer timeout,
- String parameterMap,
- Class<?> parameterType,
- String resultMap,
- Class<?> resultType,
- ResultSetType resultSetType,
- boolean flushCache,
- boolean useCache,
- boolean resultOrdered,
- KeyGenerator keyGenerator,
- String keyProperty,
- String keyColumn,
- String databaseId,
- LanguageDriver lang,
- String resultSets) {
-
- if (unresolvedCacheRef) {
- throw new IncompleteElementException("Cache-ref not yet resolved");
- }
-
- id = applyCurrentNamespace(id, false);
- boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
-
- MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
- .resource(resource)
- .fetchSize(fetchSize)
- .timeout(timeout)
- .statementType(statementType)
- .keyGenerator(keyGenerator)
- .keyProperty(keyProperty)
- .keyColumn(keyColumn)
- .databaseId(databaseId)
- .lang(lang)
- .resultOrdered(resultOrdered)
- .resultSets(resultSets)
- .resultMaps(getStatementResultMaps(resultMap, resultType, id))
- .resultSetType(resultSetType)
- .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
- .useCache(valueOrDefault(useCache, isSelect))
- .cache(currentCache);
-
- ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
- if (statementParameterMap != null) {
- statementBuilder.parameterMap(statementParameterMap);
- }
-
- MappedStatement statement = statementBuilder.build();
- // ------> 正好是这里在configuration中添加了映射好的statement
- configuration.addMappedStatement(statement);
- return statement;
-}
-
-而里面就是往map里添加
-public void addMappedStatement(MappedStatement ms) {
- mappedStatements.put(ms.getId(), ms);
-}
-
-获取mapper
StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
+ // 循环获取连接
+ 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();
+ }
-就是调用了 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
-public <T> T selectOne(String statement, Object parameter) {
- // Popular vote was to return null on 0 results and throw exception on too many.
- List<T> list = this.selectList(statement, parameter);
- if (list.size() == 1) {
- return list.get(0);
- } else if (list.size() > 1) {
- throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
- } else {
- return null;
- }
-}
+ }
-调用实际的实现方法
-public <E> List<E> selectList(String statement, Object parameter) {
- return this.selectList(statement, parameter, RowBounds.DEFAULT);
-}
-
-这里还有一层
-public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
- return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_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.");
+ }
+ // 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();
+ }
+ 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系列-入门篇
+ /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 {
-根本的就是从configuration里获取了mappedStatement
-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();
- }
-}
+ 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
@@ -8210,111 +8425,6 @@ location ~*openresty
-
- mybatis系列-typeAliases系统
- /2023/01/01/mybatis%E7%B3%BB%E5%88%97-typeAliases%E7%B3%BB%E7%BB%9F/
- 其实前面已经聊到过这个概念,在mybatis的配置中,以及一些初始化逻辑都是用了typeAliases,
-<typeAliases>
- <typeAlias alias="Author" type="domain.blog.Author"/>
- <typeAlias alias="Blog" type="domain.blog.Blog"/>
- <typeAlias alias="Comment" type="domain.blog.Comment"/>
- <typeAlias alias="Post" type="domain.blog.Post"/>
- <typeAlias alias="Section" type="domain.blog.Section"/>
- <typeAlias alias="Tag" type="domain.blog.Tag"/>
-</typeAliases>
-可以在这里注册类型别名,然后在mybatis中配置使用时,可以简化这些类型的使用,其底层逻辑主要是一个map,
-public class TypeAliasRegistry {
-
- private final Map<String, Class<?>> typeAliases = new HashMap<>();
-以string作为key,class对象作为value,比如我们在一开始使用的配置文件
-<dataSource type="POOLED">
- <property name="driver" value="${driver}"/>
- <property name="url" value="${url}"/>
- <property name="username" value="${username}"/>
- <property name="password" value="${password}"/>
-</dataSource>
-这里使用的dataSource是POOLED,那它肯定是个别名或者需要对应处理
而这个别名就是在Configuration的构造方法里初始化
-public Configuration() {
- typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
- typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
-
- typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
- typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
- typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
-
- typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
- typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
- typeAliasRegistry.registerAlias("LRU", LruCache.class);
- typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
- typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
-
- typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
-
- typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
- typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
-
- typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
- typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
- typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
- typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
- typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
- typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
- typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
-
- typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
- typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
-
- languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
- languageRegistry.register(RawLanguageDriver.class);
- }
-正是通过typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);这一行,注册了
POOLED对应的别名类型是PooledDataSourceFactory.class
具体的注册方法是在
-public void registerAlias(String alias, Class<?> value) {
- if (alias == null) {
- throw new TypeException("The parameter alias cannot be null");
- }
- // issue #748
- // 转换成小写,
- String key = alias.toLowerCase(Locale.ENGLISH);
- // 判断是否已经注册过了
- if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
- throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
- }
- // 放进map里
- typeAliases.put(key, value);
-}
-而获取的逻辑在这
-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);
- }
- }
-逻辑比较简单,但是在mybatis中也是不可或缺的一块概念
-]]>
-
- Java
- Mybatis
-
-
- Java
- Mysql
- Mybatis
-
-
pcre-intro-and-a-simple-package
/2015/01/16/pcre-intro-and-a-simple-package/
@@ -8365,1169 +8475,1038 @@ int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, in
- 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;
- }
-}
+ 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);
-result
PHP Fatal error: Abstract function abst1::abstra1() cannot contain body in new.php on line 17
+而在
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
中,创建了sqlSource,这里就会根据前面的 LanguageDriver 的实现选择对应的 sqlSource ,
+SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
-Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
-]]>
-
- php
-
-
- php
-
-
-
- mybatis系列-sql 类的简要分析
- /2023/03/19/mybatis%E7%B3%BB%E5%88%97-sql-%E7%B1%BB%E7%9A%84%E7%AE%80%E8%A6%81%E5%88%86%E6%9E%90/
- 上次就比较简单的讲了使用,这块也比较简单,因为封装得不是很复杂,首先我们从 select 作为入口来看看,这个具体的实现,
-String selectSql = new SQL() {{
- SELECT("id", "name");
- FROM("student");
- WHERE("id = #{id}");
- }}.toString();
-SELECT 方法的实现,
-public T SELECT(String... columns) {
- sql().statementType = SQLStatement.StatementType.SELECT;
- sql().select.addAll(Arrays.asList(columns));
- return getSelf();
-}
-statementType是个枚举
-public enum StatementType {
- DELETE, INSERT, SELECT, UPDATE
-}
-那这个就是个 select 语句,然后会把参数转成 list 添加到 select 变量里,
然后是 from 语句,这个大概也能猜到就是设置下表名,
-public T FROM(String table) {
- sql().tables.add(table);
- return getSelf();
-}
-往 tables 里添加了 table,这个 tables 是什么呢
这里也可以看下所有的变量,
-StatementType statementType;
-List<String> sets = new ArrayList<>();
-List<String> select = new ArrayList<>();
-List<String> tables = new ArrayList<>();
-List<String> join = new ArrayList<>();
-List<String> innerJoin = new ArrayList<>();
-List<String> outerJoin = new ArrayList<>();
-List<String> leftOuterJoin = new ArrayList<>();
-List<String> rightOuterJoin = new ArrayList<>();
-List<String> where = new ArrayList<>();
-List<String> having = new ArrayList<>();
-List<String> groupBy = new ArrayList<>();
-List<String> orderBy = new ArrayList<>();
-List<String> lastList = new ArrayList<>();
-List<String> columns = new ArrayList<>();
-List<List<String>> valuesList = new ArrayList<>();
-可以看到是一堆 List 先暂存这些sql 片段,然后再拼装成 sql 语句,
因为它重写了 toString 方法
+createSqlSource 就会调用
@Override
-public String toString() {
- StringBuilder sb = new StringBuilder();
- sql().sql(sb);
- return sb.toString();
-}
-调用的 sql 方法是
-public String sql(Appendable a) {
- SafeAppendable builder = new SafeAppendable(a);
- if (statementType == null) {
- return null;
+public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
+ XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
+ return builder.parseScriptNode();
+}
+
+再往下的逻辑在 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;
+}
+
+首先要解析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);
+ }
- String answer;
+判断是否是动态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();
+}
- switch (statementType) {
- case DELETE:
- answer = deleteSQL(builder);
- break;
+创建parser的时候可以看到这个parser是干了啥,其实就是找有没有${ , }
+private GenericTokenParser createParser(TokenHandler handler) {
+ return new GenericTokenParser("${", "}", handler);
+}
- case INSERT:
- answer = insertSQL(builder);
- break;
+如果是的话,就在上面把 isDynamic 设置为true 如果是true 的话就创建 DynamicSqlSource
+sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
- case SELECT:
- answer = selectSQL(builder);
- break;
+如果不是的话就创建RawSqlSource
+sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+```java
- case UPDATE:
- answer = updateSQL(builder);
- break;
+但是这不是一个真实可用的 `sqlSource` ,
+实际创建的时候会走到这
+```java
+public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
+ this(configuration, getSql(configuration, rootSqlNode), parameterType);
+ }
- default:
- answer = null;
- }
+ 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 answer;
- }
-根据上面的 statementType判断是个什么 sql,我们这个是 selectSQL 就走的 SELECT 这个分支
-private String selectSQL(SafeAppendable builder) {
- if (distinct) {
- sqlClause(builder, "SELECT DISTINCT", select, "", "", ", ");
+具体的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 {
- sqlClause(builder, "SELECT", select, "", "", ", ");
+ sql = parser.parse(originalSql);
}
+ return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
+}
- sqlClause(builder, "FROM", tables, "", "", ", ");
- joins(builder);
- sqlClause(builder, "WHERE", where, "(", ")", " AND ");
- sqlClause(builder, "GROUP BY", groupBy, "", "", ", ");
- sqlClause(builder, "HAVING", having, "(", ")", " AND ");
- sqlClause(builder, "ORDER BY", orderBy, "", "", ", ");
- limitingRowsStrategy.appendClause(builder, offset, limit);
- return builder.toString();
-}
-上面的可以看出来就是按我们常规的 sql 理解顺序来处理
就是select ... from ... where ... 这样子
再看下 sqlClause 的代码
-private void sqlClause(SafeAppendable builder, String keyword, List<String> parts, String open, String close,
- String conjunction) {
- if (!parts.isEmpty()) {
- if (!builder.isEmpty()) {
- builder.append("\n");
- }
- builder.append(keyword);
- builder.append(" ");
- builder.append(open);
- String last = "________";
- for (int i = 0, n = parts.size(); i < n; i++) {
- String part = parts.get(i);
- if (i > 0 && !part.equals(AND) && !part.equals(OR) && !last.equals(AND) && !last.equals(OR)) {
- builder.append(conjunction);
- }
- builder.append(part);
- last = part;
- }
- builder.append(close);
- }
- }
-这里的拼接方式还需要判断 AND 和 OR 的判断逻辑,其他就没什么特别的了,只是where 语句中的 lastList 不知道是干嘛的,好像只有添加跟赋值的操作,有知道的大神也可以评论指导下
-]]>
-
- 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 {
-
- 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;
-
- 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();
-
- protected boolean lazyLoadingEnabled = false;
- protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
+这里创建的其实是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;
+}
- 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;
+为什么前面要讲这么多好像没什么关系的代码呢,其实在最开始我们执行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);
+ }
- 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();
+这里获取了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);
+ }
- 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");
+ // 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();
+ }
+ }
+ }
- protected final Set<String> loadedResources = new HashSet<>();
- protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
+ return boundSql;
+ }
- 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<>();
+而我们从上面的解析中可以看到这里的sqlSource是一层RawSqlSource , 它的getBoundSql又是调用内部的sqlSource的方法
+@Override
+public BoundSql getBoundSql(Object parameterObject) {
+ return sqlSource.getBoundSql(parameterObject);
+}
-这么多成员变量,先不一一解释作用,但是其中的几个参数我们应该是已经知道了的,第一个就是 mappedStatements ,上一篇我们知道被解析的mapper就是放在这里,后面的 resultMaps ,parameterMaps 也比较常用的就是我们参数和结果的映射map,这里跟我之前有一篇解释为啥我们一些变量的使用会比较特殊,比如list,可以参考这篇,keyGenerators是在我们需要定义主键生成器的时候使用。
然后第二点是我们创建的 org.apache.ibatis.session.SqlSessionFactory 是哪个,
-public SqlSessionFactory build(Configuration config) {
- return new DefaultSqlSessionFactory(config);
-}
+内部的sqlSource 就是StaticSqlSource ,
+@Override
+public BoundSql getBoundSql(Object parameterObject) {
+ return new BoundSql(configuration, sql, parameterMappings, parameterObject);
+}
-是这个 DefaultSqlSessionFactory ,这是其中一个 SqlSessionFactory 的实现
接下来我们看看 openSession 里干了啥
-public SqlSession openSession() {
- return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
-}
+这个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);
+}
-这边有几个参数,第一个是默认的执行器类型,往上找找上面贴着的 Configuration 的成员变量里可以看到默认是
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
-因为没有指明特殊的执行逻辑,所以默认我们也就用简单类型的,第二个参数是是事务级别,第三个是是否自动提交
-private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
- Transaction tx = null;
+而上次在这边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 {
- 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);
+ 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 {
- ErrorContext.instance().reset();
+ closeStatement(stmt);
}
-}
+}
-具体是调用了 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);
+它是通过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());
}
- executor = (Executor) interceptorChain.pluginAll(executor);
- return executor;
-}
-上面传入的 executorType 是 Configuration 的默认类型,也就是 simple 类型,并且 cacheEnabled 在 Configuration 默认为 true,所以会包装成CachingExecutor ,然后后面就是插件了,这块我们先不展开
然后我们的openSession返回的就是创建了DefaultSqlSession
-public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
- this.configuration = configuration;
- this.executor = executor;
- this.dirty = false;
- this.autoCommit = autoCommit;
- }
+}
-然后就是调用 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) {
+所以上次有个细节可以补充,这边的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 {
- 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);
+ 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 {
- ErrorContext.instance().reset();
+ closeStatement(stmt);
}
-}
+}
-因为前面说了 executor 包装了 CachingExecutor ,所以会先调用
+
+因为上面prepareStatement中getConnection拿到connection是com.mysql.cj.jdbc.ConnectionImpl#ConnectionImpl(com.mysql.cj.conf.HostInfo)
@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);
+public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
+ PreparedStatement ps = (PreparedStatement) statement;
+ ps.execute();
+ return resultSetHandler.handleResultSets(ps);
}
-然后是调用的真实的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);
-}
+那又为什么是这个呢,可以在网上找,我们在mybatis-config.xml里配置的
+<transactionManager type="JDBC"/>
-这里是第一次查询,没有缓存就先到最后一行,继续是调用到 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 (queryStack == 0 && ms.isFlushCacheRequired()) {
- clearLocalCache();
- }
- List<E> list;
+因此在parseConfiguration中配置environment时
+private void parseConfiguration(XNode root) {
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--;
+ // 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);
}
- if (queryStack == 0) {
- for (DeferredLoad deferredLoad : deferredLoads) {
- deferredLoad.load();
- }
- // issue #601
- deferredLoads.clear();
- if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
- // issue #482
- clearLocalCache();
+ }
+
+调用的这个方法通过获取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;
}
}
- 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);
- try {
- list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
- } finally {
- localCache.removeObject(key);
}
- localCache.putObject(key, list);
- if (ms.getStatementType() == StatementType.CALLABLE) {
- localOutputParameterCache.putObject(key, parameter);
+}
+
+是通过以下方法获取的,
+// 方法全限定名 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.");
}
- return list;
-}
-然后就是 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);
+// 方法全限定名 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);
+ }
}
-}
-接下去其实就是跟jdbc交互了
-@Override
-public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
- PreparedStatement ps = (PreparedStatement) statement;
- ps.execute();
- return resultSetHandler.handleResultSets(ps);
-}
+// 方法全限定名 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);
-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();
- }
+所以我们在这
+private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
+ Transaction tx = null;
+ try {
+ final Environment environment = configuration.getEnvironment();
+ final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
- 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());
- }
+获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后
+tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
+```java
- CachedResultSetMetaData cachedMetadata = null;
- boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
- if (cacheResultSetMetadata) {
- cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql());
- }
+创建的transaction就是JdbcTransaction
+```java
+ @Override
+ public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
+ return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose);
+ }
- 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);
- }
+然后我们再会上去看代码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;
+ }
+}
- if (this.retrieveGeneratedKeys) {
- rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
- }
+即调用了
+ @Override
+ public Connection getConnection() throws SQLException {
+ if (connection == null) {
+ openConnection();
+ }
+ return connection;
+ }
- if (oldDb != null) {
- locallyScopedConn.setDatabase(oldDb);
- }
+ 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();
+ }
- if (rs != null) {
- this.lastInsertId = rs.getUpdateID();
- this.results = rs;
- }
+private PooledConnection popConnection(String username, String password) throws SQLException {
+ boolean countedWait = false;
+ PooledConnection conn = null;
+ long t = System.currentTimeMillis();
+ int localBadConnectionCount = 0;
- return rs != null && rs.hasRows();
- }
+ 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;
+ }
}
- } catch (CJException var11) {
- throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
+ }
}
- }
+ 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();
+ }
-]]>
-
- 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);
+ }
-而在
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
中,创建了sqlSource,这里就会根据前面的 LanguageDriver 的实现选择对应的 sqlSource ,
-SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
+ 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.");
+ }
-createSqlSource 就会调用
-@Override
-public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
- XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
- return builder.parseScriptNode();
-}
+ return conn;
+ }
-再往下的逻辑在 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);
+其实就是调用的
+// org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
+ @Override
+ public Connection getConnection() throws SQLException {
+ return doGetConnection(username, password);
}
- return sqlSource;
-}
+```java
-首先要解析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;
- }
+然后就是
+```java
+private Connection doGetConnection(String username, String password) throws SQLException {
+ Properties props = new Properties();
+ if (driverProperties != null) {
+ props.putAll(driverProperties);
}
- 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();
-}
+ if (username != null) {
+ props.setProperty("user", username);
+ }
+ if (password != null) {
+ props.setProperty("password", password);
+ }
+ return doGetConnection(props);
+ }
-创建parser的时候可以看到这个parser是干了啥,其实就是找有没有${ , }
-private GenericTokenParser createParser(TokenHandler handler) {
- return new GenericTokenParser("${", "}", handler);
-}
+继续这个逻辑
+ 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 {
-如果是的话,就在上面把 isDynamic 设置为true 如果是true 的话就创建 DynamicSqlSource
-sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ 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();
+ }
+ }
-如果不是的话就创建RawSqlSource
-sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
-```java
+ if(url == null) {
+ throw new SQLException("The url cannot be null", "08001");
+ }
-但是这不是一个真实可用的 `sqlSource` ,
-实际创建的时候会走到这
-```java
-public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
- this(configuration, getSql(configuration, rootSqlNode), parameterType);
- }
+ println("DriverManager.getConnection(\"" + url + "\")");
- 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<>());
- }
+ // 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;
-具体的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());
-}
+ 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;
+ }
+ }
-这里创建的其实是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;
-}
+ } else {
+ println(" skipping: " + aDriver.getClass().getName());
+ }
-为什么前面要讲这么多好像没什么关系的代码呢,其实在最开始我们执行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);
- }
+ }
-这里获取了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 we got here nobody could connect.
+ if (reason != null) {
+ println("getConnection failed: " + reason);
+ throw reason;
+ }
- // 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();
+ println("getConnection: no suitable driver found for "+ url);
+ throw new SQLException("No suitable driver found for "+ url, "08001");
+ }
+
+
+上面的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 boundSql;
- }
+这是个 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);
+ }
-而我们从上面的解析中可以看到这里的sqlSource是一层RawSqlSource , 它的getBoundSql又是调用内部的sqlSource的方法
+结合这个
@Override
-public BoundSql getBoundSql(Object parameterObject) {
- return sqlSource.getBoundSql(parameterObject);
+public Connection getConnection() throws SQLException {
+ return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
-内部的sqlSource 就是StaticSqlSource ,
-@Override
-public BoundSql getBoundSql(Object parameterObject) {
- return new BoundSql(configuration, sql, parameterMappings, parameterObject);
-}
+所以最终的connection就是com.mysql.cj.jdbc.ConnectionImpl@358ab600
+]]>
+
+ Java
+ Mybatis
+
+
+ Java
+ Mysql
+ Mybatis
+
+
+
+ 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;
+ }
+}
-这个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);
-}
+result
PHP Fatal error: Abstract function abst1::abstra1() cannot contain body in new.php on line 17
-而上次在这边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);
+Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
+]]>
+
+ php
+
+
+ php
+
+
+
+ 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;
+}
-它是通过statementType来区分应该使用哪个statementHandler,我们这使用的就是PreparedStatementHandler
-public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+返回的是 org.apache.ibatis.session.Configuration , 而这个 Configuration 也是 mybatis 中特别重要的配置核心类,贴一下里面的成员变量,
+public class Configuration {
- 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());
- }
+ 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;
-所以上次有个细节可以补充,这边的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 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();
-因为上面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 boolean lazyLoadingEnabled = false;
+ protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
-那又为什么是这个呢,可以在网上找,我们在mybatis-config.xml里配置的
-<transactionManager type="JDBC"/>
+ 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;
-因此在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 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();
-调用的这个方法通过获取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 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.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 Set<String> loadedResources = new HashSet<>();
+ protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
-// 方法全限定名 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 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<>();
-// 方法全限定名 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);
+这么多成员变量,先不一一解释作用,但是其中的几个参数我们应该是已经知道了的,第一个就是 mappedStatements ,上一篇我们知道被解析的mapper就是放在这里,后面的 resultMaps ,parameterMaps 也比较常用的就是我们参数和结果的映射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;
+上面传入的 executorType 是 Configuration 的默认类型,也就是 simple 类型,并且 cacheEnabled 在 Configuration 默认为 true,所以会包装成CachingExecutor ,然后后面就是插件了,这块我们先不展开
然后我们的openSession返回的就是创建了DefaultSqlSession
+public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
+ this.configuration = configuration;
+ this.executor = executor;
+ this.dirty = false;
+ this.autoCommit = autoCommit;
+ }
- 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();
- }
+然后就是调用 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();
+ }
+}
- }
+因为前面说了 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);
+}
- if (conn == null) {
- if (log.isDebugEnabled()) {
- log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+然后是调用的真实的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
}
- throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+ return list;
}
-
- return conn;
- }
-
-其实就是调用的
-// org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
- @Override
- public Connection getConnection() throws SQLException {
- return doGetConnection(username, password);
}
-```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);
+ }
+}
- if(url == null) {
- throw new SQLException("The url cannot be null", "08001");
- }
+接下去其实就是跟jdbc交互了
+@Override
+public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
+ PreparedStatement ps = (PreparedStatement) statement;
+ ps.execute();
+ return resultSetHandler.handleResultSets(ps);
+}
- println("DriverManager.getConnection(\"" + url + "\")");
-
- // 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;
+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();
}
- }
- } else {
- println(" skipping: " + aDriver.getClass().getName());
- }
+ 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());
+ }
- // if we got here nobody could connect.
- if (reason != null) {
- println("getConnection failed: " + reason);
- throw reason;
- }
+ 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);
+ }
- println("getConnection: no suitable driver found for "+ url);
- throw new SQLException("No suitable driver found for "+ url, "08001");
- }
+ if (this.retrieveGeneratedKeys) {
+ rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
+ }
+ if (oldDb != null) {
+ locallyScopedConn.setDatabase(oldDb);
+ }
-上面的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;
+ if (rs != null) {
+ this.lastInsertId = rs.getUpdateID();
+ this.results = rs;
+ }
+
+ return rs != null && rs.hasRows();
}
}
- } 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);
+ } catch (CJException var11) {
+ throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
}
- }
-
-这是个 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);
- }
-
-结合这个
-@Override
-public Connection getConnection() throws SQLException {
- return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-}
+ }
-所以最终的connection就是com.mysql.cj.jdbc.ConnectionImpl@358ab600
]]>
Java
@@ -9585,6 +9564,92 @@ Starting node rabbit@rabbit2 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;
+
+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;
+
+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
+ 数据结构
+ 源码
+
+
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/
@@ -9660,92 +9725,6 @@ int zslRandomLevel(void) {
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
当随机值跟0xFFFF进行与操作小于ZSKIPLIST_P * 0xFFFF时才会增大 level 的值,因此保持了一个相对递减的概率
可以简单分析下,当 random() 的值小于 0xFFFF 的 1/4,才会 level + 1,就意味着当有 1 - 1/4也就是3/4的概率是直接跳出,所以一层的概率是3/4,也就是 1-P,二层的概率是 P*(1-P),三层的概率是 P² * (1-P) 依次递推。
-]]>
-
- Redis
- 数据结构
- C
- 源码
- Redis
-
-
- redis
- 数据结构
- 源码
-
-
-
- redis数据结构介绍-第一部分 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 主要有五种数据结构,Strings,Lists,Sets,Hashes,Sorted 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;
-
-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;
-
-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
@@ -9823,187 +9802,208 @@ typedef struct redisObject {
- 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 {
+ redis数据结构介绍六 快表
+ /2020/01/22/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%85%AD/
+ 这应该是 redis 系列的最后一篇了,讲下快表,其实最前面讲的链表在早先的 redis 版本中也作为 list 的数据结构使用过,但是单纯的链表的缺陷之前也说了,插入便利,但是空间利用率低,并且不能进行二分查找等,检索效率低,ziplist 压缩表的产生也是同理,希望获得更好的性能,包括存储空间和访问性能等,原来我也不懂这个快表要怎么快,然后明白了一个道理,其实并没有什么银弹,只是大牛们会在适合的时候使用最适合的数据结构来实现性能的最大化,这里面有一招就是不同数据结构的组合调整,比如 Java 中的 HashMap,在链表节点数大于 8 时会转变成红黑树,以此提高访问效率,不费话了,回到快表,quicklist,这个数据结构主要使用在 list 类型中,如果我说其实这个 quicklist 就是个链表,可能大家不太会相信,但是事实上的确可以认为 quicklist 是个双向链表,看下代码
+/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
+ * We use bit fields keep the quicklistNode at 32 bytes.
+ * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
+ * encoding: 2 bits, RAW=1, LZF=2.
+ * container: 2 bits, NONE=1, ZIPLIST=2.
+ * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
+ * attempted_compress: 1 bit, boolean, used for verifying during testing.
+ * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
+typedef struct quicklistNode {
+ struct quicklistNode *prev;
+ struct quicklistNode *next;
+ unsigned char *zl;
+ unsigned int sz; /* ziplist size in bytes */
+ unsigned int count : 16; /* count of items in ziplist */
+ unsigned int encoding : 2; /* RAW==1 or LZF==2 */
+ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
+ unsigned int recompress : 1; /* was this node previous compressed? */
+ unsigned int attempted_compress : 1; /* node can't compress; too small */
+ unsigned int extra : 10; /* more bits to steal for future usage */
+} quicklistNode;
- 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
- Mybatis
-
-
- Java
- Mysql
- Mybatis
-
-
-
- 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 的介绍
-
-
-
-Policy
-Description
-
-
-
-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-lru
-Evicts the least recently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lru 策略逐出
-
-
-volatile-lfu
-Evicts the least frequently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lfu 策略逐出
-
-
-volatile-random
-Randomly evicts keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中随机逐出
-
-
-volatile-ttl
-Evicts 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/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
+/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
+ * 'sz' is byte length of 'compressed' field.
+ * 'compressed' is LZF data with total (compressed) length 'sz'
+ * NOTE: uncompressed length is stored in quicklistNode->sz.
+ * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
+typedef struct quicklistLZF {
+ unsigned int sz; /* LZF size in bytes*/
+ char compressed[];
+} quicklistLZF;
+
+/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
+ * 'count' is the number of total entries.
+ * 'len' is the number of quicklist nodes.
+ * 'compress' is: -1 if compression disabled, otherwise it's the number
+ * of quicklistNodes to leave uncompressed at ends of quicklist.
+ * 'fill' is the user-requested (or default) fill factor. */
+typedef struct quicklist {
+ quicklistNode *head;
+ quicklistNode *tail;
+ unsigned long count; /* total count of all entries in all ziplists */
+ unsigned long len; /* number of quicklistNodes */
+ int fill : 16; /* fill factor for individual nodes */
+ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
+} quicklist;
+粗略看下,quicklist 里有 head,tail, quicklistNode里有 prev,next 指针,是不是有链表的基本轮廓了,那么为啥这玩意要称为快表呢,快在哪,关键就在这个unsigned char *zl;zl 是不是前面又看到过,就是 ziplist ,这是什么鬼,链表里用压缩表,这不套娃么,先别急,回顾下前面说的 ziplist,ziplist 有哪些特点,内存利用率高,可以从表头快速定位到尾节点,节点可以从后往前找,但是有个缺点,就是从中间插入的效率比较低,需要整体往后移,这个其实是普通数组的优化版,但还是有数组的一些劣势,所以要真的快,是不是可以将链表跟数组真的结合起来。
+ziplist
这里有两个 redis 的配置参数,list-max-ziplist-size 和 list-compress-depth,先来说第一个,既然快表是将链表跟压缩表数组结合起来使用,那么具体怎么用呢,比如我有一个 10 个元素的 list,那具体怎么放,每个 quicklistNode 里放多大的 ziplist,假如每个快表节点的 ziplist 只放一个元素,那么其实这就退化成了一个链表,如果 10 个元素放在一个 quicklistNode 的 ziplist 里,那就退化成了一个 ziplist,所以有了这个 list-max-ziplist-size,而且它还比较牛,能取正负值,当是正值时,对应的就是每个 quicklistNode 的 ziplist 中的元素个数,比如配置了 list-max-ziplist-size = 5,那么我刚才的 10 个元素的 list 就是一个两个 quicklistNode 组成的快表,每个 quicklistNode 中的 ziplist 包含了五个元素,当 list-max-ziplist-size取负值的时候,它限制了 ziplist 的字节数
+size_t offset = (-fill) - 1;
+if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
+ if (sz <= optimization_level[offset]) {
+ return 1;
+ } else {
+ return 0;
+ }
+} else {
+ return 0;
+}
+
+/* Optimization levels for size-based filling */
+static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
+
+/* Create a new quicklist.
+ * Free with quicklistRelease(). */
+quicklist *quicklistCreate(void) {
+ struct quicklist *quicklist;
+
+ quicklist = zmalloc(sizeof(*quicklist));
+ quicklist->head = quicklist->tail = NULL;
+ quicklist->len = 0;
+ quicklist->count = 0;
+ quicklist->compress = 0;
+ quicklist->fill = -2;
+ return quicklist;
+}
+这个 fill 就是传进来的 list-max-ziplist-size, 具体对应的就是
+
+- -5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
+- -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
+- -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
+- -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)也就是上面的
quicklist->fill = -2;
+- -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
+
+压缩
list-compress-depth这个参数呢是用来配置压缩的,等等压缩是为啥,不是里面已经是压缩表了么,大牛们就是为了性能殚精竭虑,这里考虑到的是一个场景,一般状况下,list 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示
+/* depth of end nodes not to compress;0=off */
+
+- 0,代表不压缩,默认值
+- 1,两端各一个节点不压缩
+- 2,两端各两个节点不压缩
+- … 依次类推
压缩后的 ziplist 就会变成 quicklistLZF,然后替换 zl 指针,这里使用的是 LZF 压缩算法,压缩后的 quicklistLZF 中的 compressed 也是个柔性数组,压缩后的 ziplist 整个就放进这个柔性数组
+
+插入过程
简单说下插入元素的过程
+/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
+void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
+ int where) {
+ if (where == QUICKLIST_HEAD) {
+ quicklistPushHead(quicklist, value, sz);
+ } else if (where == QUICKLIST_TAIL) {
+ quicklistPushTail(quicklist, value, sz);
+ }
+}
+
+/* Add new entry to head node of quicklist.
+ *
+ * Returns 0 if used existing head.
+ * Returns 1 if new head created. */
+int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
+ quicklistNode *orig_head = quicklist->head;
+ if (likely(
+ _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
+ quicklist->head->zl =
+ ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
+ quicklistNodeUpdateSz(quicklist->head);
+ } else {
+ quicklistNode *node = quicklistCreateNode();
+ node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
+
+ quicklistNodeUpdateSz(node);
+ _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
+ }
+ quicklist->count++;
+ quicklist->head->count++;
+ return (orig_head != quicklist->head);
+}
+
+/* Add new entry to tail node of quicklist.
+ *
+ * Returns 0 if used existing tail.
+ * Returns 1 if new tail created. */
+int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
+ quicklistNode *orig_tail = quicklist->tail;
+ if (likely(
+ _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
+ quicklist->tail->zl =
+ ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
+ quicklistNodeUpdateSz(quicklist->tail);
+ } else {
+ quicklistNode *node = quicklistCreateNode();
+ node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
+
+ quicklistNodeUpdateSz(node);
+ _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
+ }
+ quicklist->count++;
+ quicklist->tail->count++;
+ return (orig_tail != quicklist->tail);
+}
+
+/* Wrappers for node inserting around existing node. */
+REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
+ quicklistNode *old_node,
+ quicklistNode *new_node) {
+ __quicklistInsertNode(quicklist, old_node, new_node, 0);
+}
+
+REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
+ quicklistNode *old_node,
+ quicklistNode *new_node) {
+ __quicklistInsertNode(quicklist, old_node, new_node, 1);
+}
+
+/* Insert 'new_node' after 'old_node' if 'after' is 1.
+ * Insert 'new_node' before 'old_node' if 'after' is 0.
+ * Note: 'new_node' is *always* uncompressed, so if we assign it to
+ * head or tail, we do not need to uncompress it. */
+REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
+ quicklistNode *old_node,
+ quicklistNode *new_node, int after) {
+ if (after) {
+ new_node->prev = old_node;
+ if (old_node) {
+ new_node->next = old_node->next;
+ if (old_node->next)
+ old_node->next->prev = new_node;
+ old_node->next = new_node;
+ }
+ if (quicklist->tail == old_node)
+ quicklist->tail = new_node;
+ } else {
+ new_node->next = old_node;
+ if (old_node) {
+ new_node->prev = old_node->prev;
+ if (old_node->prev)
+ old_node->prev->next = new_node;
+ old_node->prev = new_node;
+ }
+ if (quicklist->head == old_node)
+ quicklist->head = new_node;
+ }
+ /* If this insert creates the only element so far, initialize head/tail. */
+ if (quicklist->len == 0) {
+ quicklist->head = quicklist->tail = new_node;
+ }
+
+ if (old_node)
+ quicklistCompress(quicklist, old_node);
+
+ quicklist->len++;
+}
+前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。
]]>
Redis
@@ -10019,192 +10019,409 @@ typedef struct redisObject {
- redis系列介绍七-过期策略
- /2020/04/12/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E4%B8%83/
- 这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,
-redis过期策略
redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
主要代码是
-/* This function is called when we are going to perform some operation
- * in a given key, but such key may be already logically expired even if
- * it still exists in the database. The main way this function is called
- * is via lookupKey*() family of functions.
- *
- * The behavior of the function depends on the replication role of the
- * instance, because slave instances do not expire keys, they wait
- * for DELs from the master for consistency matters. However even
- * slaves will try to have a coherent return value for the function,
- * so that read commands executed in the slave side will be able to
- * behave like if the key is expired even if still present (because the
- * master has yet to propagate the DEL).
- *
- * In masters as a side effect of finding a key which is expired, such
- * key will be evicted from the database. Also this may trigger the
- * propagation of a DEL/UNLINK command in AOF / replication stream.
- *
- * The return value of the function is 0 if the key is still valid,
- * otherwise the function returns 1 if the key is expired. */
-int expireIfNeeded(redisDb *db, robj *key) {
- if (!keyIsExpired(db,key)) return 0;
-
- /* If we are running in the context of a slave, instead of
- * evicting the expired key from the database, we return ASAP:
- * the slave key expiration is controlled by the master that will
- * send us synthesized DEL operations for expired keys.
- *
- * Still we try to return the right information to the caller,
- * that is, 0 if we think the key should be still valid, 1 if
- * we think the key is expired at this time. */
- if (server.masterhost != NULL) return 1;
-
- /* Delete the key */
- server.stat_expiredkeys++;
- propagateExpire(db,key,server.lazyfree_lazy_expire);
- notifyKeyspaceEvent(NOTIFY_EXPIRED,
- "expired",key,db->id);
- return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
- dbSyncDelete(db,key);
-}
-
-/* Check if the key is expired. */
-int keyIsExpired(redisDb *db, robj *key) {
- mstime_t when = getExpire(db,key);
- mstime_t now;
-
- if (when < 0) return 0; /* No expire for this key */
-
- /* Don't expire anything while loading. It will be done later. */
- if (server.loading) return 0;
-
- /* If we are in the context of a Lua script, we pretend that time is
- * blocked to when the Lua script started. This way a key can expire
- * only the first time it is accessed and not in the middle of the
- * script execution, making propagation to slaves / AOF consistent.
- * See issue #1525 on Github for more information. */
- if (server.lua_caller) {
- now = server.lua_time_start;
- }
- /* If we are in the middle of a command execution, we still want to use
- * a reference time that does not change: in that case we just use the
- * cached time, that we update before each call in the call() function.
- * This way we avoid that commands such as RPOPLPUSH or similar, that
- * may re-open the same key multiple times, can invalidate an already
- * open object in a next call, if the next call will see the key expired,
- * while the first did not. */
- else if (server.fixed_time_expire > 0) {
- now = server.mstime;
- }
- /* For the other cases, we want to use the most fresh time we have. */
- else {
- now = mstime();
- }
-
- /* The key expired if the current (virtual or real) time is greater
- * than the expire time of the key. */
- return now > when;
-}
-/* Return the expire time of the specified key, or -1 if no expire
- * is associated with this key (i.e. the key is non volatile) */
-long long getExpire(redisDb *db, robj *key) {
- dictEntry *de;
+ 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 的介绍
+
+
+
+Policy
+Description
+
+
+
+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-lru
+Evicts the least recently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lru 策略逐出
+
+
+volatile-lfu
+Evicts the least frequently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lfu 策略逐出
+
+
+volatile-random
+Randomly evicts keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中随机逐出
+
+
+volatile-ttl
+Evicts 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
+
+
+
+ mybatis系列-typeAliases系统
+ /2023/01/01/mybatis%E7%B3%BB%E5%88%97-typeAliases%E7%B3%BB%E7%BB%9F/
+ 其实前面已经聊到过这个概念,在mybatis的配置中,以及一些初始化逻辑都是用了typeAliases,
+<typeAliases>
+ <typeAlias alias="Author" type="domain.blog.Author"/>
+ <typeAlias alias="Blog" type="domain.blog.Blog"/>
+ <typeAlias alias="Comment" type="domain.blog.Comment"/>
+ <typeAlias alias="Post" type="domain.blog.Post"/>
+ <typeAlias alias="Section" type="domain.blog.Section"/>
+ <typeAlias alias="Tag" type="domain.blog.Tag"/>
+</typeAliases>
+可以在这里注册类型别名,然后在mybatis中配置使用时,可以简化这些类型的使用,其底层逻辑主要是一个map,
+public class TypeAliasRegistry {
- /* No expire? return ASAP */
- if (dictSize(db->expires) == 0 ||
- (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
+ private final Map<String, Class<?>> typeAliases = new HashMap<>();
+以string作为key,class对象作为value,比如我们在一开始使用的配置文件
+<dataSource type="POOLED">
+ <property name="driver" value="${driver}"/>
+ <property name="url" value="${url}"/>
+ <property name="username" value="${username}"/>
+ <property name="password" value="${password}"/>
+</dataSource>
+这里使用的dataSource是POOLED,那它肯定是个别名或者需要对应处理
而这个别名就是在Configuration的构造方法里初始化
+public Configuration() {
+ typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
+ typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
- /* The entry was found in the expire dict, this means it should also
- * be present in the main dict (safety check). */
- serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
- return dictGetSignedIntegerVal(de);
-}
-这里有几点要注意的,第一是当惰性删除时会根据lazyfree_lazy_expire这个参数去判断是执行同步删除还是异步删除,另外一点是对于 slave,是不需要执行的,因为会在 master 过期时向 slave 发送 del 指令。
光采用这个策略会有什么问题呢,假如一些key 一直未被访问,那这些 key 就不会过期了,导致一直被占用着内存,所以 redis 采取了懒汉式过期加定期过期策略,定期策略是怎么执行的呢
-/* This function handles 'background' operations we are required to do
- * incrementally in Redis databases, such as active key expiring, resizing,
- * rehashing. */
-void databasesCron(void) {
- /* Expire keys by random sampling. Not required for slaves
- * as master will synthesize DELs for us. */
- if (server.active_expire_enabled) {
- if (server.masterhost == NULL) {
- activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
- } else {
- expireSlaveKeys();
- }
- }
+ typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
+ typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
+ typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
- /* Defrag keys gradually. */
- activeDefragCycle();
+ typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
+ typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
+ typeAliasRegistry.registerAlias("LRU", LruCache.class);
+ typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
+ typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
- /* Perform hash tables rehashing if needed, but only if there are no
- * other processes saving the DB on disk. Otherwise rehashing is bad
- * as will cause a lot of copy-on-write of memory pages. */
- if (!hasActiveChildProcess()) {
- /* We use global counters so if we stop the computation at a given
- * DB we'll be able to start from the successive in the next
- * cron loop iteration. */
- static unsigned int resize_db = 0;
- static unsigned int rehash_db = 0;
- int dbs_per_call = CRON_DBS_PER_CALL;
- int j;
+ typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
- /* Don't test more DBs than we have. */
- if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
+ typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
+ typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
- /* Resize */
- for (j = 0; j < dbs_per_call; j++) {
- tryResizeHashTables(resize_db % server.dbnum);
- resize_db++;
- }
+ typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
+ typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
+ typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
+ typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
+ typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
+ typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
+ typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
- /* Rehash */
- if (server.activerehashing) {
- for (j = 0; j < dbs_per_call; j++) {
- int work_done = incrementallyRehash(rehash_db);
- if (work_done) {
- /* If the function did some work, stop here, we'll do
- * more at the next cron loop. */
- break;
- } else {
- /* If this db didn't need rehash, we'll try the next one. */
- rehash_db++;
- rehash_db %= server.dbnum;
- }
- }
- }
- }
-}
-/* Try to expire a few timed out keys. The algorithm used is adaptive and
- * will use few CPU cycles if there are few expiring keys, otherwise
- * it will get more aggressive to avoid that too much memory is used by
- * keys that can be removed from the keyspace.
- *
- * Every expire cycle tests multiple databases: the next call will start
- * again from the next db, with the exception of exists for time limit: in that
- * case we restart again from the last database we were processing. Anyway
- * no more than CRON_DBS_PER_CALL databases are tested at every iteration.
- *
- * The function can perform more or less work, depending on the "type"
- * argument. It can execute a "fast cycle" or a "slow cycle". The slow
- * cycle is the main way we collect expired cycles: this happens with
- * the "server.hz" frequency (usually 10 hertz).
- *
- * However the slow cycle can exit for timeout, since it used too much time.
- * For this reason the function is also invoked to perform a fast cycle
- * at every event loop cycle, in the beforeSleep() function. The fast cycle
- * will try to perform less work, but will do it much more often.
- *
- * The following are the details of the two expire cycles and their stop
- * conditions:
- *
- * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
- * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
- * microseconds, and is not repeated again before the same amount of time.
- * The cycle will also refuse to run at all if the latest slow cycle did not
- * terminate because of a time limit condition.
- *
- * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
- * executed, where the time limit is a percentage of the REDIS_HZ period
- * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
- * fast cycle, the check of every database is interrupted once the number
- * of already expired keys in the database is estimated to be lower than
+ typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
+ typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
+
+ languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
+ languageRegistry.register(RawLanguageDriver.class);
+ }
+正是通过typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);这一行,注册了
POOLED对应的别名类型是PooledDataSourceFactory.class
具体的注册方法是在
+public void registerAlias(String alias, Class<?> value) {
+ if (alias == null) {
+ throw new TypeException("The parameter alias cannot be null");
+ }
+ // issue #748
+ // 转换成小写,
+ String key = alias.toLowerCase(Locale.ENGLISH);
+ // 判断是否已经注册过了
+ if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
+ throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
+ }
+ // 放进map里
+ typeAliases.put(key, value);
+}
+而获取的逻辑在这
+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);
+ }
+ }
+逻辑比较简单,但是在mybatis中也是不可或缺的一块概念
+]]>
+
+ Java
+ Mybatis
+
+
+ Java
+ Mysql
+ Mybatis
+
+
+
+ redis系列介绍七-过期策略
+ /2020/04/12/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E4%B8%83/
+ 这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,
+redis过期策略
redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
主要代码是
+/* This function is called when we are going to perform some operation
+ * in a given key, but such key may be already logically expired even if
+ * it still exists in the database. The main way this function is called
+ * is via lookupKey*() family of functions.
+ *
+ * The behavior of the function depends on the replication role of the
+ * instance, because slave instances do not expire keys, they wait
+ * for DELs from the master for consistency matters. However even
+ * slaves will try to have a coherent return value for the function,
+ * so that read commands executed in the slave side will be able to
+ * behave like if the key is expired even if still present (because the
+ * master has yet to propagate the DEL).
+ *
+ * In masters as a side effect of finding a key which is expired, such
+ * key will be evicted from the database. Also this may trigger the
+ * propagation of a DEL/UNLINK command in AOF / replication stream.
+ *
+ * The return value of the function is 0 if the key is still valid,
+ * otherwise the function returns 1 if the key is expired. */
+int expireIfNeeded(redisDb *db, robj *key) {
+ if (!keyIsExpired(db,key)) return 0;
+
+ /* If we are running in the context of a slave, instead of
+ * evicting the expired key from the database, we return ASAP:
+ * the slave key expiration is controlled by the master that will
+ * send us synthesized DEL operations for expired keys.
+ *
+ * Still we try to return the right information to the caller,
+ * that is, 0 if we think the key should be still valid, 1 if
+ * we think the key is expired at this time. */
+ if (server.masterhost != NULL) return 1;
+
+ /* Delete the key */
+ server.stat_expiredkeys++;
+ propagateExpire(db,key,server.lazyfree_lazy_expire);
+ notifyKeyspaceEvent(NOTIFY_EXPIRED,
+ "expired",key,db->id);
+ return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
+ dbSyncDelete(db,key);
+}
+
+/* Check if the key is expired. */
+int keyIsExpired(redisDb *db, robj *key) {
+ mstime_t when = getExpire(db,key);
+ mstime_t now;
+
+ if (when < 0) return 0; /* No expire for this key */
+
+ /* Don't expire anything while loading. It will be done later. */
+ if (server.loading) return 0;
+
+ /* If we are in the context of a Lua script, we pretend that time is
+ * blocked to when the Lua script started. This way a key can expire
+ * only the first time it is accessed and not in the middle of the
+ * script execution, making propagation to slaves / AOF consistent.
+ * See issue #1525 on Github for more information. */
+ if (server.lua_caller) {
+ now = server.lua_time_start;
+ }
+ /* If we are in the middle of a command execution, we still want to use
+ * a reference time that does not change: in that case we just use the
+ * cached time, that we update before each call in the call() function.
+ * This way we avoid that commands such as RPOPLPUSH or similar, that
+ * may re-open the same key multiple times, can invalidate an already
+ * open object in a next call, if the next call will see the key expired,
+ * while the first did not. */
+ else if (server.fixed_time_expire > 0) {
+ now = server.mstime;
+ }
+ /* For the other cases, we want to use the most fresh time we have. */
+ else {
+ now = mstime();
+ }
+
+ /* The key expired if the current (virtual or real) time is greater
+ * than the expire time of the key. */
+ return now > when;
+}
+/* Return the expire time of the specified key, or -1 if no expire
+ * is associated with this key (i.e. the key is non volatile) */
+long long getExpire(redisDb *db, robj *key) {
+ dictEntry *de;
+
+ /* No expire? return ASAP */
+ if (dictSize(db->expires) == 0 ||
+ (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
+
+ /* The entry was found in the expire dict, this means it should also
+ * be present in the main dict (safety check). */
+ serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
+ return dictGetSignedIntegerVal(de);
+}
+这里有几点要注意的,第一是当惰性删除时会根据lazyfree_lazy_expire这个参数去判断是执行同步删除还是异步删除,另外一点是对于 slave,是不需要执行的,因为会在 master 过期时向 slave 发送 del 指令。
光采用这个策略会有什么问题呢,假如一些key 一直未被访问,那这些 key 就不会过期了,导致一直被占用着内存,所以 redis 采取了懒汉式过期加定期过期策略,定期策略是怎么执行的呢
+/* This function handles 'background' operations we are required to do
+ * incrementally in Redis databases, such as active key expiring, resizing,
+ * rehashing. */
+void databasesCron(void) {
+ /* Expire keys by random sampling. Not required for slaves
+ * as master will synthesize DELs for us. */
+ if (server.active_expire_enabled) {
+ if (server.masterhost == NULL) {
+ activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
+ } else {
+ expireSlaveKeys();
+ }
+ }
+
+ /* Defrag keys gradually. */
+ activeDefragCycle();
+
+ /* Perform hash tables rehashing if needed, but only if there are no
+ * other processes saving the DB on disk. Otherwise rehashing is bad
+ * as will cause a lot of copy-on-write of memory pages. */
+ if (!hasActiveChildProcess()) {
+ /* We use global counters so if we stop the computation at a given
+ * DB we'll be able to start from the successive in the next
+ * cron loop iteration. */
+ static unsigned int resize_db = 0;
+ static unsigned int rehash_db = 0;
+ int dbs_per_call = CRON_DBS_PER_CALL;
+ int j;
+
+ /* Don't test more DBs than we have. */
+ if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
+
+ /* Resize */
+ for (j = 0; j < dbs_per_call; j++) {
+ tryResizeHashTables(resize_db % server.dbnum);
+ resize_db++;
+ }
+
+ /* Rehash */
+ if (server.activerehashing) {
+ for (j = 0; j < dbs_per_call; j++) {
+ int work_done = incrementallyRehash(rehash_db);
+ if (work_done) {
+ /* If the function did some work, stop here, we'll do
+ * more at the next cron loop. */
+ break;
+ } else {
+ /* If this db didn't need rehash, we'll try the next one. */
+ rehash_db++;
+ rehash_db %= server.dbnum;
+ }
+ }
+ }
+ }
+}
+/* Try to expire a few timed out keys. The algorithm used is adaptive and
+ * will use few CPU cycles if there are few expiring keys, otherwise
+ * it will get more aggressive to avoid that too much memory is used by
+ * keys that can be removed from the keyspace.
+ *
+ * Every expire cycle tests multiple databases: the next call will start
+ * again from the next db, with the exception of exists for time limit: in that
+ * case we restart again from the last database we were processing. Anyway
+ * no more than CRON_DBS_PER_CALL databases are tested at every iteration.
+ *
+ * The function can perform more or less work, depending on the "type"
+ * argument. It can execute a "fast cycle" or a "slow cycle". The slow
+ * cycle is the main way we collect expired cycles: this happens with
+ * the "server.hz" frequency (usually 10 hertz).
+ *
+ * However the slow cycle can exit for timeout, since it used too much time.
+ * For this reason the function is also invoked to perform a fast cycle
+ * at every event loop cycle, in the beforeSleep() function. The fast cycle
+ * will try to perform less work, but will do it much more often.
+ *
+ * The following are the details of the two expire cycles and their stop
+ * conditions:
+ *
+ * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
+ * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
+ * microseconds, and is not repeated again before the same amount of time.
+ * The cycle will also refuse to run at all if the latest slow cycle did not
+ * terminate because of a time limit condition.
+ *
+ * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
+ * executed, where the time limit is a percentage of the REDIS_HZ period
+ * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
+ * fast cycle, the check of every database is interrupted once the number
+ * of already expired keys in the database is estimated to be lower than
* a given percentage, in order to avoid doing too much work to gain too
* little memory.
*
@@ -10447,261 +10664,6 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;源码
-
- redis数据结构介绍六 快表
- /2020/01/22/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%85%AD/
- 这应该是 redis 系列的最后一篇了,讲下快表,其实最前面讲的链表在早先的 redis 版本中也作为 list 的数据结构使用过,但是单纯的链表的缺陷之前也说了,插入便利,但是空间利用率低,并且不能进行二分查找等,检索效率低,ziplist 压缩表的产生也是同理,希望获得更好的性能,包括存储空间和访问性能等,原来我也不懂这个快表要怎么快,然后明白了一个道理,其实并没有什么银弹,只是大牛们会在适合的时候使用最适合的数据结构来实现性能的最大化,这里面有一招就是不同数据结构的组合调整,比如 Java 中的 HashMap,在链表节点数大于 8 时会转变成红黑树,以此提高访问效率,不费话了,回到快表,quicklist,这个数据结构主要使用在 list 类型中,如果我说其实这个 quicklist 就是个链表,可能大家不太会相信,但是事实上的确可以认为 quicklist 是个双向链表,看下代码
-/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
- * We use bit fields keep the quicklistNode at 32 bytes.
- * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
- * encoding: 2 bits, RAW=1, LZF=2.
- * container: 2 bits, NONE=1, ZIPLIST=2.
- * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
- * attempted_compress: 1 bit, boolean, used for verifying during testing.
- * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
-typedef struct quicklistNode {
- struct quicklistNode *prev;
- struct quicklistNode *next;
- unsigned char *zl;
- unsigned int sz; /* ziplist size in bytes */
- unsigned int count : 16; /* count of items in ziplist */
- unsigned int encoding : 2; /* RAW==1 or LZF==2 */
- unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
- unsigned int recompress : 1; /* was this node previous compressed? */
- unsigned int attempted_compress : 1; /* node can't compress; too small */
- unsigned int extra : 10; /* more bits to steal for future usage */
-} quicklistNode;
-
-/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
- * 'sz' is byte length of 'compressed' field.
- * 'compressed' is LZF data with total (compressed) length 'sz'
- * NOTE: uncompressed length is stored in quicklistNode->sz.
- * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
-typedef struct quicklistLZF {
- unsigned int sz; /* LZF size in bytes*/
- char compressed[];
-} quicklistLZF;
-
-/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
- * 'count' is the number of total entries.
- * 'len' is the number of quicklist nodes.
- * 'compress' is: -1 if compression disabled, otherwise it's the number
- * of quicklistNodes to leave uncompressed at ends of quicklist.
- * 'fill' is the user-requested (or default) fill factor. */
-typedef struct quicklist {
- quicklistNode *head;
- quicklistNode *tail;
- unsigned long count; /* total count of all entries in all ziplists */
- unsigned long len; /* number of quicklistNodes */
- int fill : 16; /* fill factor for individual nodes */
- unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
-} quicklist;
-粗略看下,quicklist 里有 head,tail, quicklistNode里有 prev,next 指针,是不是有链表的基本轮廓了,那么为啥这玩意要称为快表呢,快在哪,关键就在这个unsigned char *zl;zl 是不是前面又看到过,就是 ziplist ,这是什么鬼,链表里用压缩表,这不套娃么,先别急,回顾下前面说的 ziplist,ziplist 有哪些特点,内存利用率高,可以从表头快速定位到尾节点,节点可以从后往前找,但是有个缺点,就是从中间插入的效率比较低,需要整体往后移,这个其实是普通数组的优化版,但还是有数组的一些劣势,所以要真的快,是不是可以将链表跟数组真的结合起来。
-ziplist
这里有两个 redis 的配置参数,list-max-ziplist-size 和 list-compress-depth,先来说第一个,既然快表是将链表跟压缩表数组结合起来使用,那么具体怎么用呢,比如我有一个 10 个元素的 list,那具体怎么放,每个 quicklistNode 里放多大的 ziplist,假如每个快表节点的 ziplist 只放一个元素,那么其实这就退化成了一个链表,如果 10 个元素放在一个 quicklistNode 的 ziplist 里,那就退化成了一个 ziplist,所以有了这个 list-max-ziplist-size,而且它还比较牛,能取正负值,当是正值时,对应的就是每个 quicklistNode 的 ziplist 中的元素个数,比如配置了 list-max-ziplist-size = 5,那么我刚才的 10 个元素的 list 就是一个两个 quicklistNode 组成的快表,每个 quicklistNode 中的 ziplist 包含了五个元素,当 list-max-ziplist-size取负值的时候,它限制了 ziplist 的字节数
-size_t offset = (-fill) - 1;
-if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
- if (sz <= optimization_level[offset]) {
- return 1;
- } else {
- return 0;
- }
-} else {
- return 0;
-}
-
-/* Optimization levels for size-based filling */
-static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
-
-/* Create a new quicklist.
- * Free with quicklistRelease(). */
-quicklist *quicklistCreate(void) {
- struct quicklist *quicklist;
-
- quicklist = zmalloc(sizeof(*quicklist));
- quicklist->head = quicklist->tail = NULL;
- quicklist->len = 0;
- quicklist->count = 0;
- quicklist->compress = 0;
- quicklist->fill = -2;
- return quicklist;
-}
-这个 fill 就是传进来的 list-max-ziplist-size, 具体对应的就是
-
-- -5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-- -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-- -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-- -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)也就是上面的
quicklist->fill = -2;
-- -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
-
-压缩
list-compress-depth这个参数呢是用来配置压缩的,等等压缩是为啥,不是里面已经是压缩表了么,大牛们就是为了性能殚精竭虑,这里考虑到的是一个场景,一般状况下,list 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示
-/* depth of end nodes not to compress;0=off */
-
-- 0,代表不压缩,默认值
-- 1,两端各一个节点不压缩
-- 2,两端各两个节点不压缩
-- … 依次类推
压缩后的 ziplist 就会变成 quicklistLZF,然后替换 zl 指针,这里使用的是 LZF 压缩算法,压缩后的 quicklistLZF 中的 compressed 也是个柔性数组,压缩后的 ziplist 整个就放进这个柔性数组
-
-插入过程
简单说下插入元素的过程
-/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
-void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
- int where) {
- if (where == QUICKLIST_HEAD) {
- quicklistPushHead(quicklist, value, sz);
- } else if (where == QUICKLIST_TAIL) {
- quicklistPushTail(quicklist, value, sz);
- }
-}
-
-/* Add new entry to head node of quicklist.
- *
- * Returns 0 if used existing head.
- * Returns 1 if new head created. */
-int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
- quicklistNode *orig_head = quicklist->head;
- if (likely(
- _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
- quicklist->head->zl =
- ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
- quicklistNodeUpdateSz(quicklist->head);
- } else {
- quicklistNode *node = quicklistCreateNode();
- node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
-
- quicklistNodeUpdateSz(node);
- _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
- }
- quicklist->count++;
- quicklist->head->count++;
- return (orig_head != quicklist->head);
-}
-
-/* Add new entry to tail node of quicklist.
- *
- * Returns 0 if used existing tail.
- * Returns 1 if new tail created. */
-int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
- quicklistNode *orig_tail = quicklist->tail;
- if (likely(
- _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
- quicklist->tail->zl =
- ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
- quicklistNodeUpdateSz(quicklist->tail);
- } else {
- quicklistNode *node = quicklistCreateNode();
- node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
-
- quicklistNodeUpdateSz(node);
- _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
- }
- quicklist->count++;
- quicklist->tail->count++;
- return (orig_tail != quicklist->tail);
-}
-
-/* Wrappers for node inserting around existing node. */
-REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
- quicklistNode *old_node,
- quicklistNode *new_node) {
- __quicklistInsertNode(quicklist, old_node, new_node, 0);
-}
-
-REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
- quicklistNode *old_node,
- quicklistNode *new_node) {
- __quicklistInsertNode(quicklist, old_node, new_node, 1);
-}
-
-/* Insert 'new_node' after 'old_node' if 'after' is 1.
- * Insert 'new_node' before 'old_node' if 'after' is 0.
- * Note: 'new_node' is *always* uncompressed, so if we assign it to
- * head or tail, we do not need to uncompress it. */
-REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
- quicklistNode *old_node,
- quicklistNode *new_node, int after) {
- if (after) {
- new_node->prev = old_node;
- if (old_node) {
- new_node->next = old_node->next;
- if (old_node->next)
- old_node->next->prev = new_node;
- old_node->next = new_node;
- }
- if (quicklist->tail == old_node)
- quicklist->tail = new_node;
- } else {
- new_node->next = old_node;
- if (old_node) {
- new_node->prev = old_node->prev;
- if (old_node->prev)
- old_node->prev->next = new_node;
- old_node->prev = new_node;
- }
- if (quicklist->head == old_node)
- quicklist->head = new_node;
- }
- /* If this insert creates the only element so far, initialize head/tail. */
- if (quicklist->len == 0) {
- quicklist->head = quicklist->tail = new_node;
- }
-
- if (old_node)
- quicklistCompress(quicklist, old_node);
-
- quicklist->len++;
-}
-前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。
-]]>
-
- 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,具体的策略我们看下官网的解释
-
-- Test 20 random keys from the set of keys with an associated expire.
-- Delete all the keys found expired.
-- If more than 25% of keys were expired, start again from step 1.
-
-从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件
-# Redis calls an internal function to perform many background tasks, like
-# closing connections of clients in timeout, purging expired keys that are
-# never requested, and so forth.
-#
-# Not all tasks are performed with the same frequency, but Redis checks for
-# tasks to perform according to the specified "hz" value.
-#
-# By default "hz" is set to 10. Raising the value will use more CPU when
-# Redis is idle, but at the same time will make Redis more responsive when
-# there are many keys expiring at the same time, and timeouts may be
-# handled with more precision.
-#
-# The range is between 1 and 500, however a value over 100 is usually not
-# a good idea. Most users should use the default of 10 and raise this up to
-# 100 only in environments where very low latency is required.
-hz 10
-
-可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略
-]]>
-
- redis
-
-
- redis
- 应用
- 过期策略
-
-
redis系列介绍八-淘汰策略
/2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/
@@ -11232,22 +11194,97 @@ uint8_t LFULogIncr(uint8_t counter) {
- rust学习笔记-所有权二
- /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/
- 这里需要说道函数和返回值了
可以看书上的这个例子
![]()
对于这种情况,当进入函数内部时,会把传入的变量的所有权转移进函数内部,如果最后还是要返回该变量,但是如果此时还要返回别的计算结果,就可能需要笨拙地使用元组
-引用
此时我们就可以用引用来解决这个问题
-fn main() {
- let s1 = String::from("hello");
- let len = calculate_length(&s1);
-
- println!("The length of '{}' is {}", s1, len);
-}
-fn calculate_length(s: &String) -> usize {
- s.len()
-}
-这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值
![]()
由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃
-可变引用
而当我们尝试对引用的字符串进行修改时
-fn main() {
+ 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,具体的策略我们看下官网的解释
+
+- Test 20 random keys from the set of keys with an associated expire.
+- Delete all the keys found expired.
+- If more than 25% of keys were expired, start again from step 1.
+
+从池子里随机获取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/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
+ 最近在看 《rust 权威指南》,还是难度比较大的,它里面的一些概念跟之前的用过的都有比较大的差别
比起有 gc 的虚拟机语言,跟像 C 和 C++这种主动释放内存的,rust 有他的独特点,主要是有三条
+
+- Rust中的每一个值都有一个对应的变量作为它的所有者。
+- 在同一时间内,值有且只有一个所有者。
+- 当所有者离开自己的作用域时,它持有的值就会被释放掉。
![]()
这里有两个重点:
+- s 在进入作用域后才变得有效
+- 它会保持自己的有效性直到自己离开作用域为止
+
+然后看个案例
+let x = 5;
+let y = x;
+这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
但是对于非基础类型
+let s1 = String::from("hello");
+let s2 = s1;
+
+println!("{}, world!", s1);
+有可能认为有两种内存分布可能
先看下 string 的内存结构
![]()
第一种可能是
![]()
第二种是
![]()
我们来尝试编译下
![]()
发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了
![]()
这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法
+let s1 = String::from("hello");
+let s2 = s1.clone();
+
+println!("s1 = {}, s2 = {}", s1, s2);
+这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配
+]]>
+
+ 语言
+ Rust
+
+
+ Rust
+ 所有权
+ 内存分布
+ 新语言
+
+
+
+ rust学习笔记-所有权二
+ /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/
+ 这里需要说道函数和返回值了
可以看书上的这个例子
![]()
对于这种情况,当进入函数内部时,会把传入的变量的所有权转移进函数内部,如果最后还是要返回该变量,但是如果此时还要返回别的计算结果,就可能需要笨拙地使用元组
+引用
此时我们就可以用引用来解决这个问题
+fn main() {
+ let s1 = String::from("hello");
+ let len = calculate_length(&s1);
+
+ println!("The length of '{}' is {}", s1, len);
+}
+fn calculate_length(s: &String) -> usize {
+ s.len()
+}
+这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值
![]()
由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃
+可变引用
而当我们尝试对引用的字符串进行修改时
+fn main() {
let s1 = String::from("hello");
change(&s1);
}
@@ -11318,6 +11355,30 @@ uint8_t LFULogIncr(uint8_t counter) {
不可变引用
+
+ spark-little-tips
+ /2017/03/28/spark-little-tips/
+ spark 的一些粗浅使用经验工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现
+简单的mapreduce word count示例
textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
+data = textFile.reduceByKey(lambda x, y: x + y).collect()
+for _ in data:
+ print(_)
+
+
+结果
(3, 2)
+(1, 2)
+(4, 2)
+(2, 2)
+(5, 2)
+]]>
+
+ data analysis
+
+
+ spark
+ python
+
+
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/
@@ -11382,30 +11443,6 @@ uint8_t LFULogIncr(uint8_t counter) {
切片
-
- spark-little-tips
- /2017/03/28/spark-little-tips/
- spark 的一些粗浅使用经验工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现
-简单的mapreduce word count示例
textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
-data = textFile.reduceByKey(lambda x, y: x + y).collect()
-for _ in data:
- print(_)
-
-
-结果
(3, 2)
-(1, 2)
-(4, 2)
-(2, 2)
-(5, 2)
-]]>
-
- data analysis
-
-
- spark
- python
-
-
spring event 介绍
/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/
@@ -11700,163 +11737,31 @@ b 1
这里有两个重点:
-s 在进入作用域后才变得有效
-它会保持自己的有效性直到自己离开作用域为止
-
-然后看个案例
-let x = 5;
-let y = x;
-这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
但是对于非基础类型
-let s1 = String::from("hello");
-let s2 = s1;
+ springboot web server 启动逻辑
+ /2023/08/20/springboot-web-server-%E5%90%AF%E5%8A%A8%E9%80%BB%E8%BE%91/
+ springboot 的一个方便之处就是集成了 web server 进去,接着上一篇继续来看下这个 web server 的启动过程
基于 springboot 的 2.2.9.RELEASE 版本
整个 springboot 体系主体就是看 org.springframework.context.support.AbstractApplicationContext#refresh 刷新方法,
而启动 web server 的方法就是在其中的 OnRefresh
+try {
+ // Allows post-processing of the bean factory in context subclasses.
+ postProcessBeanFactory(beanFactory);
-println!("{}, world!", s1);
-有可能认为有两种内存分布可能
先看下 string 的内存结构
![]()
第一种可能是
![]()
第二种是
![]()
我们来尝试编译下
![]()
发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了
![]()
这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法
-let s1 = String::from("hello");
-let s2 = s1.clone();
+ // Invoke factory processors registered as beans in the context.
+ invokeBeanFactoryPostProcessors(beanFactory);
-println!("s1 = {}, s2 = {}", s1, s2);
-这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配
-]]>
-
- 语言
- Rust
-
-
- Rust
- 所有权
- 内存分布
- 新语言
-
-
-
- springboot 获取 web 应用中所有的接口 url
- /2023/08/06/springboot-%E8%8E%B7%E5%8F%96-web-%E5%BA%94%E7%94%A8%E4%B8%AD%E6%89%80%E6%9C%89%E7%9A%84%E6%8E%A5%E5%8F%A3-url/
- 最近有个小需求,要把我们一个 springboot 应用的 request mapping 给导出来,这么说已经是转化过了的,应该是要整理这个应用所有的接口路径,比如我有一个 api.baidu1.com 作为接口域名,然后这个域名下有很多个接口提供服务,这些接口都是写在一个 springboot 应用里,如果本身有网关管理这些接口转发的其实可以通过网关的数据输出,这里就介绍下通过代码来获取
-RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
-Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
-for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
- Set<String> urlSet = entry.getKey().getPatternsCondition().getPatterns();
- for (String url : urlSet) {
- System.out.println(url);
- }
-}
-第一行
逐行来解析下,第一行就是从上下文中获取 RequestMappingHandlerMapping 这个 bean,
-第二行
然后调用了 getHandlerMethods,
这里面具体执行了
-public Map<T, HandlerMethod> getHandlerMethods() {
- this.mappingRegistry.acquireReadLock();
- try {
- return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
- }
- finally {
- this.mappingRegistry.releaseReadLock();
- }
- }
-前后加了锁,重要的就是从 mappingRegistry 中获取 mappings, 这里获取的就是
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#getMappings 具体的代码
-public Map<T, HandlerMethod> getMappings() {
- return this.mappingLookup;
-}
-而这个 mappingLookup 再来看下
-class MappingRegistry {
+ // Register bean processors that intercept bean creation.
+ registerBeanPostProcessors(beanFactory);
- private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
+ // Initialize message source for this context.
+ initMessageSource();
- private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
-可以看到就是在 MappingRegistry 中的一个变量,至于这个变量是怎么来的,简单的考虑下 springboot 处理请求的流程,就是从 Mapping 去找到对应的 Handler,所以就需要提前将这个对应关系存到这个变量里,
具体可以看这 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#register
-public void register(T mapping, Object handler, Method method) {
- // Assert that the handler method is not a suspending one.
- if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
- Class<?>[] parameterTypes = method.getParameterTypes();
- if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) {
- throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
- }
- }
- this.readWriteLock.writeLock().lock();
- try {
- HandlerMethod handlerMethod = createHandlerMethod(handler, method);
- validateMethodMapping(handlerMethod, mapping);
- this.mappingLookup.put(mapping, handlerMethod);
-就是在这里会把 mapping 和 handleMethod 对应关系存进去
-第三行
这里拿的是上面的 map 里的 key,也就是 RequestMappingInfo 也就是 org.springframework.web.servlet.mvc.method.RequestMappingInfo
而真的 url 就存在 org.springframework.web.servlet.mvc.condition.PatternsRequestCondition
最终这里面的patterns就是我们要的路径
-public class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
+ // Initialize event multicaster for this context.
+ initApplicationEventMulticaster();
- private final static Set<String> EMPTY_PATH_PATTERN = Collections.singleton("");
+ // Initialize other special beans in specific context subclasses.
+ // ------------------> ‼️就是这里
+ onRefresh();
-
- private final Set<String> patterns;
-写到这下一篇是不是可以介绍下 mapping 的具体注册逻辑
-]]>
-
- Java
- SpringBoot
-
-
- Java
- SpringBoot
-
-
-
- summary-ranges-228
- /2016/10/12/summary-ranges-228/
- problemGiven a sorted integer array without duplicates, return the summary of its ranges.
-For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].
-题解
每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考
-Code
class Solution {
-public:
- vector<string> summaryRanges(vector<int>& nums) {
- int i = 0, j = 1, n;
- vector<string> res;
- n = nums.size();
- while(i < n){
- j = 1;
- while(j < n && nums[i+j] - nums[i] == j) j++;
- res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
- i += j;
- }
- return res;
- }
-};
]]>
-
- leetcode
-
-
- leetcode
- c++
-
-
-
- springboot web server 启动逻辑 - Java - SpringBoot
- /2023/08/20/springboot-web-server-%E5%90%AF%E5%8A%A8%E9%80%BB%E8%BE%91/
- springboot 的一个方便之处就是集成了 web server 进去,接着上一篇继续来看下这个 web server 的启动过程
基于 springboot 的 2.2.9.RELEASE 版本
整个 springboot 体系主体就是看 org.springframework.context.support.AbstractApplicationContext#refresh 刷新方法,
而启动 web server 的方法就是在其中的 OnRefresh
-try {
- // Allows post-processing of the bean factory in context subclasses.
- postProcessBeanFactory(beanFactory);
-
- // Invoke factory processors registered as beans in the context.
- invokeBeanFactoryPostProcessors(beanFactory);
-
- // Register bean processors that intercept bean creation.
- registerBeanPostProcessors(beanFactory);
-
- // Initialize message source for this context.
- initMessageSource();
-
- // Initialize event multicaster for this context.
- initApplicationEventMulticaster();
-
- // Initialize other special beans in specific context subclasses.
- // ------------------> ‼️就是这里
- onRefresh();
-
- // Check for listener beans and register them.
- registerListeners();
+ // Check for listener beans and register them.
+ registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
@@ -12028,6 +11933,61 @@ public:
SpringBoot
+
+ 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
+
+
+
+ summary-ranges-228
+ /2016/10/12/summary-ranges-228/
+ problemGiven a sorted integer array without duplicates, return the summary of its ranges.
+For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].
+题解
每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考
+Code
class Solution {
+public:
+ vector<string> summaryRanges(vector<int>& nums) {
+ int i = 0, j = 1, n;
+ vector<string> res;
+ n = nums.size();
+ while(i < n){
+ j = 1;
+ while(j < n && nums[i+j] - nums[i] == j) j++;
+ res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
+ i += j;
+ }
+ return res;
+ }
+};
]]>
+
+ leetcode
+
+
+ leetcode
+ c++
+
+
swoole-websocket-test
/2016/07/13/swoole-websocket-test/
@@ -12169,6 +12129,18 @@ user3:
小技巧
+
+ win 下 vmware 虚拟机搭建黑裙 nas 的小思路
+ /2023/06/04/win-%E4%B8%8B-vmware-%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%90%AD%E5%BB%BA%E9%BB%91%E8%A3%99-nas-%E7%9A%84%E5%B0%8F%E6%80%9D%E8%B7%AF/
+ 上次说 nas 的方案我是在 win10 下使用vmware workstation 搭建的黑裙虚拟机,采用 sata 物理磁盘直通的方式,算是跑通了黑裙的基础使用模式,但是后来发现的一个问题是之前没考虑到的,我买了不带 f 的处理器就是为了核显能做硬解,但是因为 cpu 是通过 vmware 虚拟的,目前看来是没法直通核显的,我是使用的 jellyfin 套件,一开始使用是默认的刮削方式,而且把电视剧当成了电影在刮削,所以基本不能看,后面使用了 tmm 作为刮削工具,可以手动填写 imdb 的id 来进行搜索,一般比较正式的剧都可以在豆瓣上找到的,然后让 jellyfin 只作为媒体管理器,但是前面的问题还是没解决,所以考虑了下可以在win10 下直接运行 jellyfin,媒体目录使用挂载在 nas 里的盘,这样 jellyfin 就能直接调用核显了,也算是把 win10 本身给利用起来了,并且文件的管理还是在黑裙中。
现在想来其实我这个方案还是不太合理,cpu 性能有点过剩想通过虚拟机的形式进行隔离使用,但是购买带核显的 cpu 最大的目的却没有实现,如果是直接裸机部署黑裙的话,真的是觉得 cpu 有点太浪费了,毕竟 passmark评分有 1w3 的cpu,只用来跑黑裙,所以网上的很多建议也是合理的,不过我可能是 win10 用的比较多了,还是习惯有 win 的环境。
+]]>
+
+ nas
+
+
+ nas
+
+
《垃圾回收算法手册读书》笔记之整理算法
/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/
@@ -12217,29 +12189,69 @@ out.flush
- 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]
-![]()
+ springboot 获取 web 应用中所有的接口 url
+ /2023/08/06/springboot-%E8%8E%B7%E5%8F%96-web-%E5%BA%94%E7%94%A8%E4%B8%AD%E6%89%80%E6%9C%89%E7%9A%84%E6%8E%A5%E5%8F%A3-url/
+ 最近有个小需求,要把我们一个 springboot 应用的 request mapping 给导出来,这么说已经是转化过了的,应该是要整理这个应用所有的接口路径,比如我有一个 api.baidu1.com 作为接口域名,然后这个域名下有很多个接口提供服务,这些接口都是写在一个 springboot 应用里,如果本身有网关管理这些接口转发的其实可以通过网关的数据输出,这里就介绍下通过代码来获取
+RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
+Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
+for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
+ Set<String> urlSet = entry.getKey().getPatternsCondition().getPatterns();
+ for (String url : urlSet) {
+ System.out.println(url);
+ }
+}
+第一行
逐行来解析下,第一行就是从上下文中获取 RequestMappingHandlerMapping 这个 bean,
+第二行
然后调用了 getHandlerMethods,
这里面具体执行了
+public Map<T, HandlerMethod> getHandlerMethods() {
+ this.mappingRegistry.acquireReadLock();
+ try {
+ return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
+ }
+ finally {
+ this.mappingRegistry.releaseReadLock();
+ }
+ }
+前后加了锁,重要的就是从 mappingRegistry 中获取 mappings, 这里获取的就是
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#getMappings 具体的代码
+public Map<T, HandlerMethod> getMappings() {
+ return this.mappingLookup;
+}
+而这个 mappingLookup 再来看下
+class MappingRegistry {
+
+ private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
+
+ private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
+可以看到就是在 MappingRegistry 中的一个变量,至于这个变量是怎么来的,简单的考虑下 springboot 处理请求的流程,就是从 Mapping 去找到对应的 Handler,所以就需要提前将这个对应关系存到这个变量里,
具体可以看这 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#register
+public void register(T mapping, Object handler, Method method) {
+ // Assert that the handler method is not a suspending one.
+ if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
+ Class<?>[] parameterTypes = method.getParameterTypes();
+ if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) {
+ throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
+ }
+ }
+ this.readWriteLock.writeLock().lock();
+ try {
+ HandlerMethod handlerMethod = createHandlerMethod(handler, method);
+ validateMethodMapping(handlerMethod, mapping);
+ this.mappingLookup.put(mapping, handlerMethod);
+就是在这里会把 mapping 和 handleMethod 对应关系存进去
+第三行
这里拿的是上面的 map 里的 key,也就是 RequestMappingInfo 也就是 org.springframework.web.servlet.mvc.method.RequestMappingInfo
而真的 url 就存在 org.springframework.web.servlet.mvc.condition.PatternsRequestCondition
最终这里面的patterns就是我们要的路径
+public class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
+
+ private final static Set<String> EMPTY_PATH_PATTERN = Collections.singleton("");
+
+
+ private final Set<String> patterns;
+写到这下一篇是不是可以介绍下 mapping 的具体注册逻辑
]]>
- 语言
+ Java
+ SpringBoot
- powershell
+ Java
+ SpringBoot
@@ -12256,56 +12268,6 @@ out.flush读后感
-
- 上次的其他 外行聊国足
- /2022/03/06/%E4%B8%8A%E6%AC%A1%E7%9A%84%E5%85%B6%E4%BB%96-%E5%A4%96%E8%A1%8C%E8%81%8A%E5%9B%BD%E8%B6%B3/
- 上次本来想在换车牌后面聊下这个话题,为啥要聊这个话题呢,也很简单,在地铁上看到一对猜测是情侣或者比较关系好的男女同学在聊,因为是这位男同学是大学学的工科,然后自己爱好设计绘画相关的,可能还以此赚了点钱,在地铁上讨论男的要不要好好努力把大学课程完成好,大致的观点是没必要,本来就不适合,这一段我就不说了,恋爱人的嘴,信你个鬼。
后面男的说在家里又跟他爹吵了关于男足的,估计是那次输了越南,实话说我不是个足球迷,对各方面技术相关也不熟,只是对包括这个人的解释和网上一些观点的看法,纯主观,这次地铁上这位说的大概意思是足球这个训练什么的很难的,要想赢越南也很难的,不是我们能嘴炮的;在网上看到一个赞同数很多的一个回答,说什么中国是个体育弱国,但是由于有一些乒乓球,跳水等小众项目比较厉害,让民众给误解了,首先我先来反驳下这个偷换概念的观点,第一所谓的体育弱国,跟我们觉得足球不应该这么差没半毛钱关系,因为体育弱国,我们的足球本来就不是顶尖的,也并不是去跟顶尖的球队去争,以足球为例,跟巴西,阿根廷,英国,德国,西班牙,意大利,法国这些足球强国,去比较,我相信没有一个足球迷会这么去做对比,因为我们足球历史最高排名是 1998 年的 37 名,最差是 100 名,把能数出来的强队都数完,估计都还不会到 37,所以根本没有跟强队去做对比,第二体育弱国,我们的体育投入是在逐年降低吗,我们是因战乱没法好好训练踢球?还是这帮傻逼就不争气,前面也说了我们足球世界排名最高 37,最低 100,那么前阵子我们输的越南是第几,目前我们的排名 77 名,越南 92 名,看明白了么,轮排名我们都不至于输越南,然后就是这个排名,这也是我想回应那位地铁上的兄弟,我觉得除了造核弹这种高精尖技术,绝大部分包含足球这类运动,遵循类二八原则,比如满分是 100 分,从 80 提到 90 分或者 90 分提到 100 分非常难,30 分提到 40 分,50 分提到 60 分我觉得都是可以凭后天努力达成的,基本不受天赋限制,这里可以以篮球来类比下,相对足球的确篮球没有那么火,或者行业市值没法比,但是也算是相对大众了,中国在篮球方面相对比较好一点,在 08 年奥运会冲进过八强,那也不是唯一的巅峰,但是我说这个其实是想说明两方面的事情,第一,像篮球一样,状态是有起起伏伏,排名也会变动,但是我觉得至少能维持一个相对稳定的总体排名和持平或者上升的趋势,这恰恰是我们这种所谓的“体育弱国”应该走的路线,第二就是去支持我的类二八原则的,可以看到我们的篮球这两年也很垃圾,排名跌到 29 了,那问题我觉得跟足球是一样的,就是不能脚踏实地,如斯科拉说的,中国篮球太缺少竞争,打得好不好都是这些人打,打输了还是照样拿钱,相对足球,篮球的技术我还是懂一些的,对比 08 年的中国男篮,的确像姚明跟王治郅这样的天赋型+努力型球员少了以后竞争力下降在所难免,但是去对比下基本功,传球,投篮,罚球稳定性,也完全不是一个水平的,这些就是我说的,可以通过努力训练拿 80 分的,只要拿到 80 分,甚至只要拿到 60 分,我觉得应该就还算对得起球迷了,就像 NBA 里球队也会有核心球员的更替,战绩起起伏伏,但是基本功这东西,防守积极性,我觉得不随核心球员的变化而变化,就像姚明这样的天赋,其实他应该还有一些先天缺陷,大脚趾较长等,但是他从 CBA 到 NBA,在 NBA 适应并且打成顶尖中锋,离不开刻苦训练,任何的成功都不是纯天赋的,必须要付出足够的努力。
说回足球,如果像前面那么洗地(体育弱国),那能给我维持住一个稳定的排名我也能接受,问题是我们的经济物质资源比 2000 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
纯主观吐槽,勿喷。
-]]>
-
- 生活
- 运动
-
-
- 生活
-
-
-
- win 下 vmware 虚拟机搭建黑裙 nas 的小思路
- /2023/06/04/win-%E4%B8%8B-vmware-%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%90%AD%E5%BB%BA%E9%BB%91%E8%A3%99-nas-%E7%9A%84%E5%B0%8F%E6%80%9D%E8%B7%AF/
- 上次说 nas 的方案我是在 win10 下使用vmware workstation 搭建的黑裙虚拟机,采用 sata 物理磁盘直通的方式,算是跑通了黑裙的基础使用模式,但是后来发现的一个问题是之前没考虑到的,我买了不带 f 的处理器就是为了核显能做硬解,但是因为 cpu 是通过 vmware 虚拟的,目前看来是没法直通核显的,我是使用的 jellyfin 套件,一开始使用是默认的刮削方式,而且把电视剧当成了电影在刮削,所以基本不能看,后面使用了 tmm 作为刮削工具,可以手动填写 imdb 的id 来进行搜索,一般比较正式的剧都可以在豆瓣上找到的,然后让 jellyfin 只作为媒体管理器,但是前面的问题还是没解决,所以考虑了下可以在win10 下直接运行 jellyfin,媒体目录使用挂载在 nas 里的盘,这样 jellyfin 就能直接调用核显了,也算是把 win10 本身给利用起来了,并且文件的管理还是在黑裙中。
现在想来其实我这个方案还是不太合理,cpu 性能有点过剩想通过虚拟机的形式进行隔离使用,但是购买带核显的 cpu 最大的目的却没有实现,如果是直接裸机部署黑裙的话,真的是觉得 cpu 有点太浪费了,毕竟 passmark评分有 1w3 的cpu,只用来跑黑裙,所以网上的很多建议也是合理的,不过我可能是 win10 用的比较多了,还是习惯有 win 的环境。
-]]>
-
- nas
-
-
- nas
-
-
-
- ssh 小技巧-端口转发
- /2023/03/26/ssh-%E5%B0%8F%E6%8A%80%E5%B7%A7-%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
- 我们在使用 ssh 连接的使用有一个很好用功能,就是端口转发,而且使用的方式也很多样,比如我们经常用 vscode 来做远程开发的话,一般远程连接就可以基于 ssh,前面也介绍过 vscode 的端口转发,并且可以配置到 .ssh/config 配置文件里,只不过最近在一次使用的过程中发现了一个问题,就是在一台 Ubuntu 的某云服务器上想 ssh 到另一台服务器上,并且做下端口映射,但是发现报了个错,
-bind: Cannot assign requested address
-查了下这个问题,猜测是不是端口已经被占用了,查了下并不是,然后想到是不是端口是系统保留的,
-sysctl -a |grep port_range
-结果中
-net.ipv4.ip_local_port_range = 50000 65000 -----意味着50000~65000端口可用
-发现也不是,没有限制,最后才查到这个原因是默认如果有 ipv6 的话会使用 ipv6 的地址做映射
所以如果是命令连接做端口转发的话,
-ssh -4 -L 11234:localhost:1234 x.x.x.x
-使用-4来制定通过 ipv4 地址来做映射
如果是在 .ssh/config 中配置的话可以直接指定所有的连接都走 ipv4
-Host *
- AddressFamily inet
-inet代表 ipv4,inet6代表 ipv6
AddressFamily 的所有取值范围是:”any”(默认)、”inet”(仅IPv4)、”inet6”(仅IPv6)。
另外此类问题还可以通过 ssh -v 来打印更具体的信息
-]]>
-
- ssh
- 技巧
-
-
- ssh
- 端口转发
-
-
一个 nginx 的简单记忆点
/2022/08/21/%E4%B8%80%E4%B8%AA-nginx-%E7%9A%84%E7%AE%80%E5%8D%95%E8%AE%B0%E5%BF%86%E7%82%B9/
@@ -12342,28 +12304,16 @@ out.flush
- 介绍下最近比较实用的端口转发
- /2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
- vscode 扩展转发在日常使用云服务器的时候,如果要访问上面自建的 mysql,一般要不直接开对应的端口,然后需要对本地 ip 进行授权,但是这个方案会有比较多的限制,比如本地 ip 变了,比如是非固定出口 ip 的家用宽带,或者要在家里跟公司都要访问,如果对所有 ip 都授权的话会不安全,这个时候其实是用 ssh 端口转发是个比较安全方便的方式。
原来在这个之前其实对这块内容不太了解,后面是听朋友说的,vscode 的 Remote - SSH 扩展可以很方便的使用端口转发,在使用该扩展的时候,会在控制台位置里都出现一个”端口” tab
![]()
如图中所示,我就是将一个服务器上的 mysql 的 3306 端口转发到本地的 3307 端口,至于为什么不用 3306 是因为本地我也有个 mysql 已经使用了 3306 端口,这个方法是使用的 vscode 的这个扩展,
-ssh 命令转发
还有个方式是直接使用 ssh 命令
命令可以如此
-ssh -CfNg -L 3307:127.0.0.1:3306 user1@199.199.199.199
-简单介绍下这个命令
-C 表示的是压缩数据包
-f 表示后台执行命令
-N 是表示不执行具体命令只用于端口转发
-g 表示允许远程主机连接本地转发端口
-L 则是具体端口转发的映射配置
上面的命令就是将远程主机的 127.0.0.1:3306 对应转发到本地 3307
而后面的用户则就是登录主机的用户名user1和ip地址199.199.199.199,当然这个配置也不是唯一的
-ssh config 配置转发
还可以在ssh 的 config 配置中加对应的配置
-Host host1
- HostName 199.199.199.199
- User user1
- IdentityFile /Users/user1/.ssh/id_rsa
- ServerAliveInterval 60
- LocalForward 3310 127.0.0.1:3306
-然后通过 ssh host1 连接服务器的时候就能顺带做端口转发
+ 上次的其他 外行聊国足
+ /2022/03/06/%E4%B8%8A%E6%AC%A1%E7%9A%84%E5%85%B6%E4%BB%96-%E5%A4%96%E8%A1%8C%E8%81%8A%E5%9B%BD%E8%B6%B3/
+ 上次本来想在换车牌后面聊下这个话题,为啥要聊这个话题呢,也很简单,在地铁上看到一对猜测是情侣或者比较关系好的男女同学在聊,因为是这位男同学是大学学的工科,然后自己爱好设计绘画相关的,可能还以此赚了点钱,在地铁上讨论男的要不要好好努力把大学课程完成好,大致的观点是没必要,本来就不适合,这一段我就不说了,恋爱人的嘴,信你个鬼。
后面男的说在家里又跟他爹吵了关于男足的,估计是那次输了越南,实话说我不是个足球迷,对各方面技术相关也不熟,只是对包括这个人的解释和网上一些观点的看法,纯主观,这次地铁上这位说的大概意思是足球这个训练什么的很难的,要想赢越南也很难的,不是我们能嘴炮的;在网上看到一个赞同数很多的一个回答,说什么中国是个体育弱国,但是由于有一些乒乓球,跳水等小众项目比较厉害,让民众给误解了,首先我先来反驳下这个偷换概念的观点,第一所谓的体育弱国,跟我们觉得足球不应该这么差没半毛钱关系,因为体育弱国,我们的足球本来就不是顶尖的,也并不是去跟顶尖的球队去争,以足球为例,跟巴西,阿根廷,英国,德国,西班牙,意大利,法国这些足球强国,去比较,我相信没有一个足球迷会这么去做对比,因为我们足球历史最高排名是 1998 年的 37 名,最差是 100 名,把能数出来的强队都数完,估计都还不会到 37,所以根本没有跟强队去做对比,第二体育弱国,我们的体育投入是在逐年降低吗,我们是因战乱没法好好训练踢球?还是这帮傻逼就不争气,前面也说了我们足球世界排名最高 37,最低 100,那么前阵子我们输的越南是第几,目前我们的排名 77 名,越南 92 名,看明白了么,轮排名我们都不至于输越南,然后就是这个排名,这也是我想回应那位地铁上的兄弟,我觉得除了造核弹这种高精尖技术,绝大部分包含足球这类运动,遵循类二八原则,比如满分是 100 分,从 80 提到 90 分或者 90 分提到 100 分非常难,30 分提到 40 分,50 分提到 60 分我觉得都是可以凭后天努力达成的,基本不受天赋限制,这里可以以篮球来类比下,相对足球的确篮球没有那么火,或者行业市值没法比,但是也算是相对大众了,中国在篮球方面相对比较好一点,在 08 年奥运会冲进过八强,那也不是唯一的巅峰,但是我说这个其实是想说明两方面的事情,第一,像篮球一样,状态是有起起伏伏,排名也会变动,但是我觉得至少能维持一个相对稳定的总体排名和持平或者上升的趋势,这恰恰是我们这种所谓的“体育弱国”应该走的路线,第二就是去支持我的类二八原则的,可以看到我们的篮球这两年也很垃圾,排名跌到 29 了,那问题我觉得跟足球是一样的,就是不能脚踏实地,如斯科拉说的,中国篮球太缺少竞争,打得好不好都是这些人打,打输了还是照样拿钱,相对足球,篮球的技术我还是懂一些的,对比 08 年的中国男篮,的确像姚明跟王治郅这样的天赋型+努力型球员少了以后竞争力下降在所难免,但是去对比下基本功,传球,投篮,罚球稳定性,也完全不是一个水平的,这些就是我说的,可以通过努力训练拿 80 分的,只要拿到 80 分,甚至只要拿到 60 分,我觉得应该就还算对得起球迷了,就像 NBA 里球队也会有核心球员的更替,战绩起起伏伏,但是基本功这东西,防守积极性,我觉得不随核心球员的变化而变化,就像姚明这样的天赋,其实他应该还有一些先天缺陷,大脚趾较长等,但是他从 CBA 到 NBA,在 NBA 适应并且打成顶尖中锋,离不开刻苦训练,任何的成功都不是纯天赋的,必须要付出足够的努力。
说回足球,如果像前面那么洗地(体育弱国),那能给我维持住一个稳定的排名我也能接受,问题是我们的经济物质资源比 2000 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
纯主观吐槽,勿喷。
]]>
- ssh
- 技巧
+ 生活
+ 运动
- ssh
- 端口转发
+ 生活
@@ -12436,23 +12386,53 @@ out.flush
- 从丁仲礼被美国制裁聊点啥
- /2020/12/20/%E4%BB%8E%E4%B8%81%E4%BB%B2%E7%A4%BC%E8%A2%AB%E7%BE%8E%E5%9B%BD%E5%88%B6%E8%A3%81%E8%81%8A%E7%82%B9%E5%95%A5/
- 几年前看了柴静的《穹顶之下》觉得这个记者调查得很深入,挺有水平,然后再看到了她跟丁仲礼的采访,其实没看完整,也没试着去理解,就觉得环境问题挺严重的,为啥柴静这个对面的这位好像对这个很不屑的样子,最近因为丁仲礼上了美国制裁名单,B 站又有人把这个视频发了出来,就完整看了下,就觉得自己挺惭愧的,就抱着对柴静的好感而没来由的否定了丁老的看法和说法,所以人也需要不断地学习,改正之前错误的观点,当然不是说我现在说的就是百分百正确,只是个人的一些浅显的见解
-先聊聊这个事情,整体看下来我的一些理解,IPCC给中国的方案其实是个很大的陷阱,它里面有几个隐藏的点是容易被我们外行忽略的,第一点是基数,首先发达国家目前(指2010年采访或者IPCC方案时间)的人均碳排放量已经是远高于发展中国家的了,这也就导致了所谓的发达国家承诺减排80%是个非常有诚意的承诺其实就是忽悠;第二点是碳排放是个累计过程,从1900年开始到2050年,或者说到2010年,发达国家已经排的量是远超过发展中国家的,这是非常不公平的;第三点其实是通过前两点推导出来的,也就是即使发达国家这么有诚意地说减排80%,扒开这层虚伪的外衣,其实是给他们11亿人分走了48%的碳排放量,留给发展中国家55亿人口的只剩下了52%;第四点,人是否因为国家的发达与否而应受到不平等待遇,如果按国家维度,丁老说的,摩纳哥要跟中国分同样的排放量么,中国人还算不算人;第五点,这点算是我自己想的,也可能是柴静屁股决定脑袋想不到的点,她作为一个物质生活条件已经足够好了,那么对于那些生活在物质条件平均线以下的,他们是否能像城里人那样有空调地暖,洗澡有热水器浴霸,上下班能开车,这些其实都直接或者间接地导致了碳排放;他们有没有改善物质生活条件地权利呢,并且再说回来,其实丁老也给了我们觉得合理地方案,我们保证不管发达国家不管减排多少,我们都不会超过他们的80%,我觉得这是真正的诚意,而不是接着减排80%的噱头来忽悠人,也是像丁老这样的专家才能看破这个陷阱,碳排放权其实就是发展权,就是人权,中国人就不是人了么,或者说站在贫困线以下的人民是否有改善物质条件的权利,而不是说像柴静这样,只是管她自己,可能觉得小孩因为空气污染导致身体不好,所以做了穹顶之下这个纪录片,想去改善这个事情,空气污染不是说对的,只是每个国家都有这个过程,如果不发展,哪里有资源去让人活得好,活得好了是前提,然后再去各方面都改善。
-对于这个问题其实更想说的是人的认知偏差,之前总觉得美帝是更自由民主,公平啥的,或者说觉得美帝啥都好,有种无脑愤青的感觉,外国的月亮比较圆,但是经历了像川普当选美国总统以来的各种魔幻操作,还有对于疫情的种种不可思议的美国民众的反应,其实更让人明白第一是外国的月亮没比较圆,第二是事情总是没那么简单粗暴非黑即白,美国不像原先设想地那么领先优秀,但是的确有很多方面是全球领先的,天朝也有体制所带来的优势,不可妄自菲薄,也不能忙不自大,还是要多学习知识,提升认知水平。
+ 介绍下最近比较实用的端口转发
+ /2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
+ vscode 扩展转发在日常使用云服务器的时候,如果要访问上面自建的 mysql,一般要不直接开对应的端口,然后需要对本地 ip 进行授权,但是这个方案会有比较多的限制,比如本地 ip 变了,比如是非固定出口 ip 的家用宽带,或者要在家里跟公司都要访问,如果对所有 ip 都授权的话会不安全,这个时候其实是用 ssh 端口转发是个比较安全方便的方式。
原来在这个之前其实对这块内容不太了解,后面是听朋友说的,vscode 的 Remote - SSH 扩展可以很方便的使用端口转发,在使用该扩展的时候,会在控制台位置里都出现一个”端口” tab
![]()
如图中所示,我就是将一个服务器上的 mysql 的 3306 端口转发到本地的 3307 端口,至于为什么不用 3306 是因为本地我也有个 mysql 已经使用了 3306 端口,这个方法是使用的 vscode 的这个扩展,
+ssh 命令转发
还有个方式是直接使用 ssh 命令
命令可以如此
+ssh -CfNg -L 3307:127.0.0.1:3306 user1@199.199.199.199
+简单介绍下这个命令
-C 表示的是压缩数据包
-f 表示后台执行命令
-N 是表示不执行具体命令只用于端口转发
-g 表示允许远程主机连接本地转发端口
-L 则是具体端口转发的映射配置
上面的命令就是将远程主机的 127.0.0.1:3306 对应转发到本地 3307
而后面的用户则就是登录主机的用户名user1和ip地址199.199.199.199,当然这个配置也不是唯一的
+ssh config 配置转发
还可以在ssh 的 config 配置中加对应的配置
+Host host1
+ HostName 199.199.199.199
+ User user1
+ IdentityFile /Users/user1/.ssh/id_rsa
+ ServerAliveInterval 60
+ LocalForward 3310 127.0.0.1:3306
+然后通过 ssh host1 连接服务器的时候就能顺带做端口转发
]]>
- 生活
- 吐槽
- 疫情
- 美国
+ ssh
+ 技巧
- 生活
- 吐槽
- 疫情
- 美国
+ ssh
+ 端口转发
+
+
+
+ ssh 小技巧-端口转发
+ /2023/03/26/ssh-%E5%B0%8F%E6%8A%80%E5%B7%A7-%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
+ 我们在使用 ssh 连接的使用有一个很好用功能,就是端口转发,而且使用的方式也很多样,比如我们经常用 vscode 来做远程开发的话,一般远程连接就可以基于 ssh,前面也介绍过 vscode 的端口转发,并且可以配置到 .ssh/config 配置文件里,只不过最近在一次使用的过程中发现了一个问题,就是在一台 Ubuntu 的某云服务器上想 ssh 到另一台服务器上,并且做下端口映射,但是发现报了个错,
+bind: Cannot assign requested address
+查了下这个问题,猜测是不是端口已经被占用了,查了下并不是,然后想到是不是端口是系统保留的,
+sysctl -a |grep port_range
+结果中
+net.ipv4.ip_local_port_range = 50000 65000 -----意味着50000~65000端口可用
+发现也不是,没有限制,最后才查到这个原因是默认如果有 ipv6 的话会使用 ipv6 的地址做映射
所以如果是命令连接做端口转发的话,
+ssh -4 -L 11234:localhost:1234 x.x.x.x
+使用-4来制定通过 ipv4 地址来做映射
如果是在 .ssh/config 中配置的话可以直接指定所有的连接都走 ipv4
+Host *
+ AddressFamily inet
+inet代表 ipv4,inet6代表 ipv6
AddressFamily 的所有取值范围是:”any”(默认)、”inet”(仅IPv4)、”inet6”(仅IPv6)。
另外此类问题还可以通过 ssh -v 来打印更具体的信息
+]]>
+
+ ssh
+ 技巧
+
+
+ ssh
+ 端口转发
@@ -12476,22 +12456,23 @@ out.flush
- 关于读书打卡与分享
- /2021/02/07/%E5%85%B3%E4%BA%8E%E8%AF%BB%E4%B9%A6%E6%89%93%E5%8D%A1%E4%B8%8E%E5%88%86%E4%BA%AB/
- 最近群里大佬发起了一个读书打卡活动,需要每天读一会书,在群里打卡分享感悟,争取一个月能读完一本书,说实话一天十分钟的读书时间倒是问题不大,不过每天都要打卡,而且一个月要读完一本书,其实难度还是有点大的,不过也想试试看。
之前某某老大给自己立了个 flag,说要读一百本书,这对我来说挺难实现的,一则我也不喜欢书只读一小半,二则感觉对于喜欢看的内容范围还是比较有限制,可能也算是比较矫情,不爱追热门的各类东西,因为往往会有一些跟大众不一致的观点看法,显得格格不入。所以还是这个打卡活动可能会比较适合我,书是人类进步的阶梯。
到现在是打卡了三天了,读的主要是白岩松的《幸福了吗》,对于白岩松,我们这一代人是比较熟悉,并且整体印象比较不错的一个央视主持人,从《焦点访谈》开始,到疫情期间的各类一线节目,可能对我来说是个三观比较正,敢于说一些真话的主持人,这中间其实是有个空档期,没怎么看电视,也不太关注了,只是在疫情期间的节目,还是一如既往地给人一种可靠的感觉,正好有一次偶然微信读书推荐了白岩松的这本书,就看了一部分,正好这次继续往下看,因为相对来讲不会很晦涩,并且从这位知名央视主持人的角度分享他的过往和想法看法,还是比较有意思的。
从对汶川地震,08 年奥运等往事的回忆和一些细节的呈现,也让我了解比较多当时所不知道的,特别是汶川地震,那时的我还在读高中,真的是看着电视,作为“猛男”都忍不住泪目了,共和国之殇,多难兴邦,但是这对于当事人来说,都是一场醒不过来的噩梦。
然后是对于足球的热爱,其实光这个就能掰扯很多,因为我不爱足球,只爱篮球,其中原因有的没的也挺多可以说的,但是看了他的书,才能比较深入的了解一个足球迷,对足球,对中国足球,对世界杯,对阿根廷的感情。
接下去还是想能继续坚持下去,加油!
+ 从丁仲礼被美国制裁聊点啥
+ /2020/12/20/%E4%BB%8E%E4%B8%81%E4%BB%B2%E7%A4%BC%E8%A2%AB%E7%BE%8E%E5%9B%BD%E5%88%B6%E8%A3%81%E8%81%8A%E7%82%B9%E5%95%A5/
+ 几年前看了柴静的《穹顶之下》觉得这个记者调查得很深入,挺有水平,然后再看到了她跟丁仲礼的采访,其实没看完整,也没试着去理解,就觉得环境问题挺严重的,为啥柴静这个对面的这位好像对这个很不屑的样子,最近因为丁仲礼上了美国制裁名单,B 站又有人把这个视频发了出来,就完整看了下,就觉得自己挺惭愧的,就抱着对柴静的好感而没来由的否定了丁老的看法和说法,所以人也需要不断地学习,改正之前错误的观点,当然不是说我现在说的就是百分百正确,只是个人的一些浅显的见解
+先聊聊这个事情,整体看下来我的一些理解,IPCC给中国的方案其实是个很大的陷阱,它里面有几个隐藏的点是容易被我们外行忽略的,第一点是基数,首先发达国家目前(指2010年采访或者IPCC方案时间)的人均碳排放量已经是远高于发展中国家的了,这也就导致了所谓的发达国家承诺减排80%是个非常有诚意的承诺其实就是忽悠;第二点是碳排放是个累计过程,从1900年开始到2050年,或者说到2010年,发达国家已经排的量是远超过发展中国家的,这是非常不公平的;第三点其实是通过前两点推导出来的,也就是即使发达国家这么有诚意地说减排80%,扒开这层虚伪的外衣,其实是给他们11亿人分走了48%的碳排放量,留给发展中国家55亿人口的只剩下了52%;第四点,人是否因为国家的发达与否而应受到不平等待遇,如果按国家维度,丁老说的,摩纳哥要跟中国分同样的排放量么,中国人还算不算人;第五点,这点算是我自己想的,也可能是柴静屁股决定脑袋想不到的点,她作为一个物质生活条件已经足够好了,那么对于那些生活在物质条件平均线以下的,他们是否能像城里人那样有空调地暖,洗澡有热水器浴霸,上下班能开车,这些其实都直接或者间接地导致了碳排放;他们有没有改善物质生活条件地权利呢,并且再说回来,其实丁老也给了我们觉得合理地方案,我们保证不管发达国家不管减排多少,我们都不会超过他们的80%,我觉得这是真正的诚意,而不是接着减排80%的噱头来忽悠人,也是像丁老这样的专家才能看破这个陷阱,碳排放权其实就是发展权,就是人权,中国人就不是人了么,或者说站在贫困线以下的人民是否有改善物质条件的权利,而不是说像柴静这样,只是管她自己,可能觉得小孩因为空气污染导致身体不好,所以做了穹顶之下这个纪录片,想去改善这个事情,空气污染不是说对的,只是每个国家都有这个过程,如果不发展,哪里有资源去让人活得好,活得好了是前提,然后再去各方面都改善。
+对于这个问题其实更想说的是人的认知偏差,之前总觉得美帝是更自由民主,公平啥的,或者说觉得美帝啥都好,有种无脑愤青的感觉,外国的月亮比较圆,但是经历了像川普当选美国总统以来的各种魔幻操作,还有对于疫情的种种不可思议的美国民众的反应,其实更让人明白第一是外国的月亮没比较圆,第二是事情总是没那么简单粗暴非黑即白,美国不像原先设想地那么领先优秀,但是的确有很多方面是全球领先的,天朝也有体制所带来的优势,不可妄自菲薄,也不能忙不自大,还是要多学习知识,提升认知水平。
]]>
生活
- 读后感
- 白岩松
- 幸福了吗
+ 吐槽
+ 疫情
+ 美国
- 读后感
- 读书
- 打卡
- 幸福了吗
- 足球
+ 生活
+ 吐槽
+ 疫情
+ 美国
@@ -12530,16 +12511,34 @@ out.flush
- 分享一次折腾老旧笔记本的体验-续续篇
- /2023/02/26/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C-%E7%BB%AD%E7%BB%AD%E7%AF%87/
- 上周因为一些事情没回去,买好了内存条这周才回去用,结果不知道是 U 盘的问题还是什么其他原因原来装的那个系统起不来,然后想用之前一直用的 LD 的笔记本,结果 LD 的老笔记本也起不来了,一直卡在启动界面,猜测可能是本身也比较老了,用的机械硬盘会不会被我一直动而损坏了,所以就想搞个老毛桃跟新搞一个启动盘,结果这周最大的坑就在这了,在我的 15 寸 mbp 上用etcher做了个 win10 的启动盘,可能是分区表格式是 GUID,不是 MBR 的,老电脑不能识别,就像装个虚拟机来做启动盘,结果像老毛桃不支持这种用虚拟机的,需要本地磁盘,然后我就有用 bootcamp 安装了个 win10,又没网络,查了下网络需要用 bootcamp 下载 Windows Support 软件,下了好久一直没下完,后来又提示连不上苹果服务器,好不容易下完了,开启 win10 后,安装到一半就蓝屏了,真的是够操蛋的,在这个时候真的很需要有一个 Windows 机器的支持,LD 的那个笔记本也跟我差不多老了,虽然比较慢但之前好歹也还可以用,结果这下也被我弄坏了,家里就没有其他机器可以支持了,有点巧妇难为无米之炊的赶脚,而且还是想再折腾下我的老电脑,后面看看打算做几个 U 盘,分工职责明确,一个老毛桃的PE 盘,老毛桃的虽然不是很纯净的,但是是我一直以来用过内置工具比较全,修复启动功能比较强大的一个 pe,没用过 winpe,后续可以考虑使用下,一个 win7 的启动盘,因为是老电脑,如果后面是新的电脑可以考虑是 win10,一个 win to go 盘,这样我的 mbp 就可以直接用 win10 了,不过想来也还是要有一台 win 机器才比较好,不然很多时候都是在折腾工具,工欲善其事必先利其器,后面也可以考虑能够更深入的去了解主板的一些问题,我原来的那个主板可能是一些小的电阻等问题,如果能自己学习解决就更好了,有时候研究下还是有意思的。
+ 关于读书打卡与分享
+ /2021/02/07/%E5%85%B3%E4%BA%8E%E8%AF%BB%E4%B9%A6%E6%89%93%E5%8D%A1%E4%B8%8E%E5%88%86%E4%BA%AB/
+ 最近群里大佬发起了一个读书打卡活动,需要每天读一会书,在群里打卡分享感悟,争取一个月能读完一本书,说实话一天十分钟的读书时间倒是问题不大,不过每天都要打卡,而且一个月要读完一本书,其实难度还是有点大的,不过也想试试看。
之前某某老大给自己立了个 flag,说要读一百本书,这对我来说挺难实现的,一则我也不喜欢书只读一小半,二则感觉对于喜欢看的内容范围还是比较有限制,可能也算是比较矫情,不爱追热门的各类东西,因为往往会有一些跟大众不一致的观点看法,显得格格不入。所以还是这个打卡活动可能会比较适合我,书是人类进步的阶梯。
到现在是打卡了三天了,读的主要是白岩松的《幸福了吗》,对于白岩松,我们这一代人是比较熟悉,并且整体印象比较不错的一个央视主持人,从《焦点访谈》开始,到疫情期间的各类一线节目,可能对我来说是个三观比较正,敢于说一些真话的主持人,这中间其实是有个空档期,没怎么看电视,也不太关注了,只是在疫情期间的节目,还是一如既往地给人一种可靠的感觉,正好有一次偶然微信读书推荐了白岩松的这本书,就看了一部分,正好这次继续往下看,因为相对来讲不会很晦涩,并且从这位知名央视主持人的角度分享他的过往和想法看法,还是比较有意思的。
从对汶川地震,08 年奥运等往事的回忆和一些细节的呈现,也让我了解比较多当时所不知道的,特别是汶川地震,那时的我还在读高中,真的是看着电视,作为“猛男”都忍不住泪目了,共和国之殇,多难兴邦,但是这对于当事人来说,都是一场醒不过来的噩梦。
然后是对于足球的热爱,其实光这个就能掰扯很多,因为我不爱足球,只爱篮球,其中原因有的没的也挺多可以说的,但是看了他的书,才能比较深入的了解一个足球迷,对足球,对中国足球,对世界杯,对阿根廷的感情。
接下去还是想能继续坚持下去,加油!
]]>
- Windows
- 小技巧
+ 生活
+ 读后感
+ 白岩松
+ 幸福了吗
- Windows
+ 读后感
+ 读书
+ 打卡
+ 幸福了吗
+ 足球
+
+
+
+ nas 中使用 tmm 刮削视频
+ /2023/07/02/%E4%BD%BF%E7%94%A8-tmm-%E5%88%AE%E5%89%8A%E8%A7%86%E9%A2%91/
+ 最近折腾了个自建的 nas,为了使用 jellyfin 这样的影视应用需要对视频进行刮削,对于电视剧来说还是有些不一样的,
比如我要刮削这部经典电视剧纪晓岚
![]()
像这样的命名方式在 tmm 中是无法识别的,或者就得一集一集进行制定刮削,
![]()
所以第一步需要进行改名
比如这是第一季的,那就是 S01,然后按集数 E01,第一季第一集的文件名就是 S01E01.mkv
然后右键点击搜索刮削,默认会以文件夹名进行搜索,是在 tmdb 数据库进行搜索
![]()
这样除非文件夹名很符合要求,一般都刮削不出来,所以需要有两种刮削方式,一种就是比较标准的命名,这样的名字可以手动先去豆瓣或者 tmdb 搜索,另一个种也可以在豆瓣找到 imdb 的 id 进行搜索
![]()
但有时候也会搜不到,比如这个纪晓岚就是搜不到,但是之前的一些韩剧什么的很多都能搜到
![]()
这个其实是最基本的刮削,但是这样刮削了需要在 jellyfin 那取消元数据下载
,这样就不会被覆盖
+]]>
+
+ nas
+
+
+ nas
@@ -12555,6 +12554,24 @@ out.flushWindows
+
+ 关于 npe 的一个小记忆点
+ /2023/07/16/%E5%85%B3%E4%BA%8E-npe-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E8%AE%B0%E5%BF%86%E7%82%B9/
+ Java 中最常见的一类问题或者说异常就是 NullPointerException,而往往这种异常我们在查看日志的时候都是根据打印出来的异常堆栈找到对应的代码以确定问题,但是有一种情况会出现一个比较奇怪的情况,虽然我们在日志中打印了异常
+log.error("something error", e);
+这样是可以打印出来日志,譬如 slf4j 就可以把异常作为最后一个参数传入
+void error(String var1, Throwable var2);
+有的时候如果不小心把异常通过占位符传入就可能出现异常被吞的情况,不过这点不是这次的重点
即使我们正常作为最后一个参数传入了,也会出现只打印出来
+something error, java.lang.NullPointerException: null
+这样就导致了如果我们这个日志所代表的异常包含的代码范围比较大的话,就不确定具体是哪一行出现的异常,除非对这些代码逻辑非常清楚,这个是为什么呢,其实这也是 jvm 的一种优化机制,像我们目前主要还在用的 jdk8中使用的 jvm 虚拟机 hotspot 的一个热点异常机制,对于这样一直出现的相同异常,就认为是热点异常,后面只会打印异常名,不再打印异常堆栈,这样对于 io 性能能够提供一定的优化保证,这个可以通过 jvm 启动参数
-XX:-OmitStackTraceInFastThrow 来关闭优化,这个问题其实在一开始造成了一定困扰,找不准具体是哪一行代码,不过在知道这个之后简单的重启也能暂时解决问题。
+]]>
+
+ java
+
+
+ java
+
+
分享记录一下一个 git 操作方法
/2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-git-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/
@@ -12574,18 +12591,6 @@ out.flushgit
-
- nas 中使用 tmm 刮削视频
- /2023/07/02/%E4%BD%BF%E7%94%A8-tmm-%E5%88%AE%E5%89%8A%E8%A7%86%E9%A2%91/
- 最近折腾了个自建的 nas,为了使用 jellyfin 这样的影视应用需要对视频进行刮削,对于电视剧来说还是有些不一样的,
比如我要刮削这部经典电视剧纪晓岚
![]()
像这样的命名方式在 tmm 中是无法识别的,或者就得一集一集进行制定刮削,
![]()
所以第一步需要进行改名
比如这是第一季的,那就是 S01,然后按集数 E01,第一季第一集的文件名就是 S01E01.mkv
然后右键点击搜索刮削,默认会以文件夹名进行搜索,是在 tmdb 数据库进行搜索
![]()
这样除非文件夹名很符合要求,一般都刮削不出来,所以需要有两种刮削方式,一种就是比较标准的命名,这样的名字可以手动先去豆瓣或者 tmdb 搜索,另一个种也可以在豆瓣找到 imdb 的 id 进行搜索
![]()
但有时候也会搜不到,比如这个纪晓岚就是搜不到,但是之前的一些韩剧什么的很多都能搜到
![]()
这个其实是最基本的刮削,但是这样刮削了需要在 jellyfin 那取消元数据下载
,这样就不会被覆盖
-]]>
-
- nas
-
-
- nas
-
-
分享记录一下一个 scp 操作方法
/2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-scp-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/
@@ -12606,27 +12611,28 @@ out.flush
- 关于 npe 的一个小记忆点
- /2023/07/16/%E5%85%B3%E4%BA%8E-npe-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E8%AE%B0%E5%BF%86%E7%82%B9/
- Java 中最常见的一类问题或者说异常就是 NullPointerException,而往往这种异常我们在查看日志的时候都是根据打印出来的异常堆栈找到对应的代码以确定问题,但是有一种情况会出现一个比较奇怪的情况,虽然我们在日志中打印了异常
-log.error("something error", e);
-这样是可以打印出来日志,譬如 slf4j 就可以把异常作为最后一个参数传入
-void error(String var1, Throwable var2);
-有的时候如果不小心把异常通过占位符传入就可能出现异常被吞的情况,不过这点不是这次的重点
即使我们正常作为最后一个参数传入了,也会出现只打印出来
-something error, java.lang.NullPointerException: null
-这样就导致了如果我们这个日志所代表的异常包含的代码范围比较大的话,就不确定具体是哪一行出现的异常,除非对这些代码逻辑非常清楚,这个是为什么呢,其实这也是 jvm 的一种优化机制,像我们目前主要还在用的 jdk8中使用的 jvm 虚拟机 hotspot 的一个热点异常机制,对于这样一直出现的相同异常,就认为是热点异常,后面只会打印异常名,不再打印异常堆栈,这样对于 io 性能能够提供一定的优化保证,这个可以通过 jvm 启动参数
-XX:-OmitStackTraceInFastThrow 来关闭优化,这个问题其实在一开始造成了一定困扰,找不准具体是哪一行代码,不过在知道这个之后简单的重启也能暂时解决问题。
+ 周末我在老丈人家打了天小工
+ /2020/08/16/%E5%91%A8%E6%9C%AB%E6%88%91%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/
+ 这周回家提前约好了要去老丈人家帮下忙,因为在翻修下老房子,活不是特别整的那种,所以大部分都是自己干,或者找个大工临时干几天(我们这那种比较专业的泥工匠叫做大工),像我这样去帮忙的,就是干点小工(把给大工帮忙的,干些偏体力活的叫做小工)的活。从大学毕业以后真的蛮少帮家里干活了,以前上学的时候放假还是帮家里淘个米,简单的扫地拖地啥的,当然刚高考完的时候,还去我爸厂里帮忙干了几天的活,实在是比较累,不过现在想着是觉得自己那时候比较牛,而不是特别排斥这个活,相对于现在的工作来说,导致了一系列的职业病,颈椎腰背都很僵硬,眼镜也不好,还有反流,像我爸那种活反而是脑力加体力的比较好的结合。
这一天的活前半部分主要是在清理厨房,瓷砖上的油污和墙上天花板上即将脱落的石灰或者白色涂料层,这种活特别是瓷砖上的油污,之前在自己家里也干活,还是比较熟悉的,不过前面主要是LD 在干,我主要是先搞墙上和天花板上的,干活还是很需要技巧的,如果直接去铲,那基本我会变成一个灰人,而且吸一鼻子灰,老丈人比较专业,先接上软管用水冲,一冲效果特别好,有些石灰涂料层直接就冲掉了,冲完之后先用带加长杆的刀片铲铲了一圈墙面,说实话因为老房子之前租出去了,所以墙面什么的被糟蹋的比较难看,一层一层的,不过这还算还好,后面主要是天花板上的,这可难倒我了,从小我爸妈是比较把我当小孩管着,爬上爬下的基本都是我爸搞定,但是到了老丈人家也只得硬着头皮上了,爬到跳(一种建筑工地用的架子)上,还有点晃,小心脏扑通扑通跳,而且带加长杆的铲子还是比较重的,铲一会手也有点累,不过坚持着铲完了,上面还是比较平整的,不过下来的时候又把我难住了🤦♂️,往下爬的时候有根杆子要跨过去,由于裤子比较紧,强行一把跨过去怕抽筋,所以以一个非常尴尬的姿势停留休息了一会,再跨了过去,幸好事后问 LD,他们都没看到,哈哈哈,然后就是帮忙一起搞瓷砖上的油污,这个太有经验了,不过老丈人更有意思,一会试试啤酒,一会用用沙子,后面在午饭前基本就弄的比较干净了,就坐着等吃饭了,下午午休了会,就继续干活了。
下午是我这次体验的重点了,因为要清理以前贴的墙纸,真的是个很麻烦的活,只能说贴墙纸的师傅活干得太好了,基本不可能整个撕下来,想用铲子一点点铲下来也不行,太轻了就只铲掉表面一层,太重了就把墙纸跟墙面的石灰啥的整个铲下来了,而且手又累又酸,后来想着是不是继续用水冲一下,对着一小面墙试验了下,效果还不错,但是又发现了个问题,那一面墙又有一块是后面糊上去的,铲掉外层的石灰后不平,然后就是最最重头的,也是让我后遗症持续到第二天的,要把那一块糊上去的水泥敲下来,毛估下大概是敲了80%左右,剩下的我的手已经不会用力了,因为那一块应该是要糊上去的始作俑者,就一块里面凹进去的,我拿着榔头敲到我手已经没法使劲了,而且大下午,感觉没五分钟,我的汗已经糊满脸,眼睛也睁不开,不然就流到眼睛里了,此处获得成就一:用榔头敲墙壁,也是个技术加体力的活,而且需要非常好的技巧,否则手马上就废了,敲下去的反作用力,没一会就不行了,然后是看着老丈人兄弟帮忙拆一个柜子,在我看来是个几天都搞不定的活,他轻轻松松在我敲墙的那会就搞定了,以前总觉得我干的活非常有技术含量,可是这个事情真的也是很有技巧啊,它是个把一间房间分隔开的柜子,从底到顶上,还带着门,我还在旁边帮忙撬一下脚踢,一根木条撬半天,唉,成就二:专业的人就是不一样。
最后就是成就三了:我之前沾沾自喜的跑了多少步,做了什么锻炼,其实都是渣渣,像这样干一天活,没经历过的,基本大半天就废了,反过来说,如果能经常去这么干一天活,跑步啥的都是渣渣,消耗的能量远远超过跑个十公里啥的。
]]>
- java
+ 生活
+ 运动
+ 跑步
+ 干活
- java
+ 生活
+ 运动
+ 减肥
+ 跑步
+ 干活
- 分享一次折腾老旧笔记本的体验-续篇
- /2023/02/12/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C-%E7%BB%AD%E7%AF%87/
- 接着上一篇的折腾记,因为这周又尝试了一些新的措施和方法,想继续记录分享下,上周的整体情况大概是 Ubuntu 系统能进去了,但是 Windows 进不去,PE 也进不去,Windows 启动盘也进不去,因为我的机器加过一个 msata 的固态,Windows 是装在 msata 固态硬盘里的,Ubuntu 是装在机械硬盘里的,所以有了一种猜测就是可能这个固态硬盘有点问题,还有就是还是怀疑内存的问题,正好家里还有个msata 的固态硬盘,是以前想给LD 的旧笔记本换上的,因为买回来放在那没有及时装,后面会又找不到,直到很后面才找到,LD 也不怎么用那个笔记本了,所以就一直放着了,这次我就想拿来换上。
周末回家我就开始尝试了,换上了新的固态硬盘后,插上 Windows 启动 U 盘,这次一开始看起来有点顺利,在 BIOS 选择 U 盘启动,进入了 Windows 安装界面,但是装到一半,后面重启了之后就一直说硬盘有问题,让重启,但是重启并没有解决问题,变成了一直无效地重复重启,再想进 U 盘启动,就又进不去了,这时候怎么说呢,感觉硬盘不是没有问题,但是呢,问题应该不完全出在这,所以按着总体的逻辑来讲,主板带着cpu 跟显卡,都换掉了,硬盘也换掉了,剩下的就是内存了,可是内存我也尝试过把后面加的那条金士顿拔掉,可还是一样,也尝试过用橡皮擦金手指,这里感觉也很奇怪了,找了一圈了都感觉没啥明确的原因,比如其实我的猜测,主板电池的问题,一些电阻坏掉了,但是主板是换过的,那如果内存有问题,照理我用原装的那条应该会没问题,也有一种非常小的可能,就是两条内存都坏了,或者说这也是一种不太可能的可能,所以最后的办法就打算试试把两条内存都换掉,不过现在网上都找不到这个内存的确切信息了,只能根据大致的型号去买来试试,就怕买来的还是坏的,其实也怕是这个买来的主板因为也是别的拆机下来的,不一定保证完全没问题,要是有类似的问题或者也有别的问题导致开不起来就很尴尬,也没有很多专业的仪器可以排查原因,比如主板有没有什么短路的,对了还有一个就是电源问题,但是电源的问题也就可能是从充电器插的口子到主板的连线,因为 LD 的电源跟我的口子一样,也试过,但是结果还是一样,顺着正常逻辑排查,目前也没有剩下很明确的方向了,只能再尝试下看看。
+ 分享一次折腾老旧笔记本的体验-续续篇
+ /2023/02/26/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C-%E7%BB%AD%E7%BB%AD%E7%AF%87/
+ 上周因为一些事情没回去,买好了内存条这周才回去用,结果不知道是 U 盘的问题还是什么其他原因原来装的那个系统起不来,然后想用之前一直用的 LD 的笔记本,结果 LD 的老笔记本也起不来了,一直卡在启动界面,猜测可能是本身也比较老了,用的机械硬盘会不会被我一直动而损坏了,所以就想搞个老毛桃跟新搞一个启动盘,结果这周最大的坑就在这了,在我的 15 寸 mbp 上用etcher做了个 win10 的启动盘,可能是分区表格式是 GUID,不是 MBR 的,老电脑不能识别,就像装个虚拟机来做启动盘,结果像老毛桃不支持这种用虚拟机的,需要本地磁盘,然后我就有用 bootcamp 安装了个 win10,又没网络,查了下网络需要用 bootcamp 下载 Windows Support 软件,下了好久一直没下完,后来又提示连不上苹果服务器,好不容易下完了,开启 win10 后,安装到一半就蓝屏了,真的是够操蛋的,在这个时候真的很需要有一个 Windows 机器的支持,LD 的那个笔记本也跟我差不多老了,虽然比较慢但之前好歹也还可以用,结果这下也被我弄坏了,家里就没有其他机器可以支持了,有点巧妇难为无米之炊的赶脚,而且还是想再折腾下我的老电脑,后面看看打算做几个 U 盘,分工职责明确,一个老毛桃的PE 盘,老毛桃的虽然不是很纯净的,但是是我一直以来用过内置工具比较全,修复启动功能比较强大的一个 pe,没用过 winpe,后续可以考虑使用下,一个 win7 的启动盘,因为是老电脑,如果后面是新的电脑可以考虑是 win10,一个 win to go 盘,这样我的 mbp 就可以直接用 win10 了,不过想来也还是要有一台 win 机器才比较好,不然很多时候都是在折腾工具,工欲善其事必先利其器,后面也可以考虑能够更深入的去了解主板的一些问题,我原来的那个主板可能是一些小的电阻等问题,如果能自己学习解决就更好了,有时候研究下还是有意思的。
]]>
Windows
@@ -12637,9 +12643,9 @@ out.flush
- 在老丈人家的小工记三
- /2020/09/13/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%B8%89/
- 小工记三前面这两周周末也都去老丈人家帮忙了,上上周周六先是去了那个在装修的旧房子那,把三楼收拾了下,因为要搬进来住,来不及等二楼装修好,就要把三楼里的东西都整理干净,这个活感觉是比较 easy,原来是就准备把三楼当放东西仓储的地方了,我们乡下大部分三层楼都是这么用的,这次也是没办法,之前搬进来的木头什么的都搬出去,主要是这上面灰尘太多,后面清理鼻孔的时候都是黑色的了,把东西都搬出去以后主要是地还是很脏,就扫了地拖了地,因为是水泥地,灰尘又太多了,拖起来都是会灰尘扬起来,整个脱完了的确干净很多,然而这会就出了个大乌龙,我们清理的是三楼的西边一间,结果老丈人上来说要住东边那间的🤦♂️,不过其实西边的也得清理,因为还是要放被子什么的,不算是白费功夫,接着清理东边那间,之前这个房子做过群租房,里面有个高低铺的床,当时觉得可以用在放被子什么的就没扔,只是拆掉了放旁边,我们就把它擦干净了又装好,发现螺丝🔩少了几个,亘古不变的真理,拆了以后装要不就多几个要不就少几个,不是很牢靠,不过用来放放被子省得放地上总还是可以的,对了前面还做了个事情就是铺地毯,其实也不是地毯,就是类似于墙布雨篷布那种,别人不用了送给我们的,三楼水泥地也不会铺瓷砖地板了就放一下,干净好看点,不过大小不合适要裁一下,那把剪刀是真的太难用了,我手都要抽筋了,它就是刀口只有一小个点是能剪下来的,其他都是钝的,后来还是用刀片直接裁,铺好以后,真的感觉也不太一样了,焕然一新的感觉
差不多中午了就去吃饭了,之前两次是去了一家小饭店,还是还比较干净,但是店里菜不好吃,还死贵,这次去了一家小快餐店,口味好,便宜,味道是真的不错,带鱼跟黄鱼都好吃,一点都不腥,我对这类比较腥的鱼真的是很挑剔的,基本上除了家里做的很少吃外面的,那天抱着试试的态度吃了下,真的还不错,后来丈母娘说好像这家老板是给别人结婚喜事酒席当厨师的,怪不得做的好吃,其实本来是有一点小抗拒,怕不干净什么的,后来发现菜很好吃,而且可能是老丈人跟干活的师傅去吃的比较多,老板很客气,我们吃完饭,还给我们买了葡萄吃,不过这家店有一个槽点,就是饭比较不好吃,有时候会夹生,不过后面聊起来其实是这种小菜馆饭点的通病,烧的太早太多容易多出来浪费,烧的迟了不够吃,而且大的电饭锅比较不容易烧好。
下午前面还是在处理三楼的,窗户上各种钉子,实在是太多了,我后面在走廊上排了一排🤦♂️,有些是直接断了,有些是就撬了出来,感觉我在杭州租房也没有这样子各种钉钉子,挂下衣服什么的也不用这么多吧,比较不能理解,搞得到处都是钉子。那天我爸也去帮忙了,主要是在卫生间里做白缝,其实也是个技术活,印象中好像我小时候自己家里也做过这个事情,但是比较模糊了,后面我们三楼搞完了就去帮我爸了,前面是我老婆二爹在那先刷上白缝,这里叫白缝,有些考究的也叫美缝,就是瓷砖铺完之后的缝,如果不去弄的话,里面水泥的颜色就露出来了,而且容易渗水,所以就要用白水泥加胶水搅拌之后糊在缝上,但是也不是直接糊,先要把缝抠一抠,因为铺瓷砖的还不会仔细到每个缝里的水泥都是一样满,而且也需要一些空间糊上去,不然就太表面的一层很容易被水直接冲掉了,然后这次其实也不是用的白水泥,而是直接现成买来就已经配好的用来填缝的,兑水搅拌均匀就好了,后面就主要是我跟我爸在搞,那个时候真的觉得我实在是太胖了,蹲下去真的没一会就受不了了,膝盖什么的太难受了,后面我跪着刷,然后膝盖又疼,也是比较不容易,不过我爸动作很快,我中间跪累了休息一会,我爸就能搞一大片,后面其实我也没做多少(谦虚一下),总体来讲这次不是很累,就是蹲着跪着腿有点受不了,是应该好好减肥了。
+ 在老丈人家的小工记五
+ /2020/10/18/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%BA%94/
+ 终于回忆起来了,年纪大了写这种东西真的要立马写,不然很容易想不起来,那天应该是 9 月 12 日,也就是上周六,因为我爸也去了,而且娘亲(丈母娘,LD 这么叫,我也就随了她这么叫,当然是背后,当面就叫妈)也在那,早上一到那二爹就给我爸指挥了活,要挖一条院子的出水道,自己想出来的词,因为觉得下水道是竖的,在那稍稍帮了一会会忙,然后我还是比较惯例的跟着 LD 还有娘亲去住的家里,主要是老丈人可能也不太想让我干太累的活,因为上次已经差不多把三楼都整理干净了,然后就是二楼了,二楼说实话我也帮不上什么忙,主要是衣服被子什么的,正好是有张以前小孩子睡过的那种摇篮床,看上去虽然有一些破损,整体还是不错的,所以打算拿过去,我就负责把它拆掉了,比较简单的是只要拧螺丝就行了,但是其实是用了好多好多工具才搞定的,一开始只要螺丝刀就行了,但是因为年代久了,后面的螺帽也有点锈住或者本身就会串着会一起动,所以拿来了个扳手,大部分的其实都被这两个工具给搞定了,但是后期大概还剩下四分之一的时候,有一颗完全锈住,并且螺纹跟之前那些都不一样,但是这个已经是最大的螺丝刀了,也没办法换个大的了,所以又去找来个一字的,因为十字的不是也可以用一字的拧嘛,结果可能是我买的工具箱里的一字螺丝刀太新了,口子那很锋利,直接把螺丝花纹给划掉了,大的小的都划掉,然后真的变成凹进去一个圆柱体了,然后就想能不能隔一层布去拧,然而因为的确是已经变成圆柱体了,布也不太给力,不放弃的我又去找来了个老虎钳,妄图把划掉的螺丝用老虎钳钳住,另一端用扳手拧开螺帽,但是这个螺丝跟螺帽真的是生锈的太严重了,外加上钳不太牢,完全是两边一起转,实在是没办法了,在征得同意之后,直接掰断了,火死了,一颗螺丝折腾得比我拆一张床还久,那天因为早上去的也比较晚了,然后就快吃午饭了,正好想着带一点东西过去,就把一些脸盆,泡脚桶啥的拿过去了,先是去吃了饭,还是在那家快餐店,菜的口味还是依然不错,就是人比较多,我爸旁边都是素菜,都没怎么吃远一点的荤菜,下次要早点去,把荤菜放我爸旁边😄(PS:他们家饭还是依然尴尬,需要等),吃完就开到在修的房子那把东西拿了出来,我爸已经动作很快的打了一小半的地沟了,说实话那玩意真的是很重,我之前把它从三楼拿下来,就觉得这个太重了,这次还要用起来,感觉我的手会分分钟废掉,不过一开始我还是跟着LD去了住的家里,惯例睡了午觉,那天睡得比较踏实,竟然睡了一个小时,醒了想了下,其实LD她们收拾也用不上我(没啥力气活),我还是去帮我爸他们,跟LD说了下就去了在修的老房子那,两位老爹在一起钻地,看着就很累,我连忙上去想换一会他们,因为刚好是钻到混凝土地线,特别难,力道不够就会滑开,用蛮力就是钻进去拔不出来,原理是因为本身浇的时候就是很紧实的,需要边钻边动,那家伙实在是太重了,真的是汗如雨下,基本是三个人轮流来,我是个添乱的,经常卡住,然后把地线,其实就是一条混凝土横梁,里面还有14跟18的钢筋,需要割断,这个割断也是很有技巧,钢筋本身在里面是受到挤压的,直接用切割的,到快断掉的时候就会崩一下,非常危险,还是老丈人比较有经验,要留一点点,然后直接用榔头敲断就好了,本来以为这个是最难的了,结果下面是一块非常大的青基石,而且也是石头跟石头挤一块,边上一点点打钻有点杯水车薪,后来是用那种螺旋的钻,钻四个洞,相对位置大概是个长方形,这样子把中间这个长方形钻出来就比较容易地能拿出来了,后面的也容易搞出来了,后面的其实难度不是特别大了,主要是地沟打好之后得看看高低是不是符合要求的,不能本来是往外排水的反而外面高,这个怎么看就又很有技巧了,一般在地上的只要侧着看一下就好了,考究点就用下水平尺,但是在地下的,不用水平尺,其实可以借助于地沟里正要放进去的水管,放点水进去,看水往哪流就行了,铺好水管后,就剩填埋的活了,不是太麻烦了,那天真的是累到了,打那个混凝土的时候我真的是把我整个人压上去了,不过也挺爽的,有点把平时无处发泄的蛮力发泄出去了。
]]>
生活
@@ -12657,9 +12663,9 @@ out.flush
- 在老丈人家的小工记五
- /2020/10/18/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%BA%94/
- 终于回忆起来了,年纪大了写这种东西真的要立马写,不然很容易想不起来,那天应该是 9 月 12 日,也就是上周六,因为我爸也去了,而且娘亲(丈母娘,LD 这么叫,我也就随了她这么叫,当然是背后,当面就叫妈)也在那,早上一到那二爹就给我爸指挥了活,要挖一条院子的出水道,自己想出来的词,因为觉得下水道是竖的,在那稍稍帮了一会会忙,然后我还是比较惯例的跟着 LD 还有娘亲去住的家里,主要是老丈人可能也不太想让我干太累的活,因为上次已经差不多把三楼都整理干净了,然后就是二楼了,二楼说实话我也帮不上什么忙,主要是衣服被子什么的,正好是有张以前小孩子睡过的那种摇篮床,看上去虽然有一些破损,整体还是不错的,所以打算拿过去,我就负责把它拆掉了,比较简单的是只要拧螺丝就行了,但是其实是用了好多好多工具才搞定的,一开始只要螺丝刀就行了,但是因为年代久了,后面的螺帽也有点锈住或者本身就会串着会一起动,所以拿来了个扳手,大部分的其实都被这两个工具给搞定了,但是后期大概还剩下四分之一的时候,有一颗完全锈住,并且螺纹跟之前那些都不一样,但是这个已经是最大的螺丝刀了,也没办法换个大的了,所以又去找来个一字的,因为十字的不是也可以用一字的拧嘛,结果可能是我买的工具箱里的一字螺丝刀太新了,口子那很锋利,直接把螺丝花纹给划掉了,大的小的都划掉,然后真的变成凹进去一个圆柱体了,然后就想能不能隔一层布去拧,然而因为的确是已经变成圆柱体了,布也不太给力,不放弃的我又去找来了个老虎钳,妄图把划掉的螺丝用老虎钳钳住,另一端用扳手拧开螺帽,但是这个螺丝跟螺帽真的是生锈的太严重了,外加上钳不太牢,完全是两边一起转,实在是没办法了,在征得同意之后,直接掰断了,火死了,一颗螺丝折腾得比我拆一张床还久,那天因为早上去的也比较晚了,然后就快吃午饭了,正好想着带一点东西过去,就把一些脸盆,泡脚桶啥的拿过去了,先是去吃了饭,还是在那家快餐店,菜的口味还是依然不错,就是人比较多,我爸旁边都是素菜,都没怎么吃远一点的荤菜,下次要早点去,把荤菜放我爸旁边😄(PS:他们家饭还是依然尴尬,需要等),吃完就开到在修的房子那把东西拿了出来,我爸已经动作很快的打了一小半的地沟了,说实话那玩意真的是很重,我之前把它从三楼拿下来,就觉得这个太重了,这次还要用起来,感觉我的手会分分钟废掉,不过一开始我还是跟着LD去了住的家里,惯例睡了午觉,那天睡得比较踏实,竟然睡了一个小时,醒了想了下,其实LD她们收拾也用不上我(没啥力气活),我还是去帮我爸他们,跟LD说了下就去了在修的老房子那,两位老爹在一起钻地,看着就很累,我连忙上去想换一会他们,因为刚好是钻到混凝土地线,特别难,力道不够就会滑开,用蛮力就是钻进去拔不出来,原理是因为本身浇的时候就是很紧实的,需要边钻边动,那家伙实在是太重了,真的是汗如雨下,基本是三个人轮流来,我是个添乱的,经常卡住,然后把地线,其实就是一条混凝土横梁,里面还有14跟18的钢筋,需要割断,这个割断也是很有技巧,钢筋本身在里面是受到挤压的,直接用切割的,到快断掉的时候就会崩一下,非常危险,还是老丈人比较有经验,要留一点点,然后直接用榔头敲断就好了,本来以为这个是最难的了,结果下面是一块非常大的青基石,而且也是石头跟石头挤一块,边上一点点打钻有点杯水车薪,后来是用那种螺旋的钻,钻四个洞,相对位置大概是个长方形,这样子把中间这个长方形钻出来就比较容易地能拿出来了,后面的也容易搞出来了,后面的其实难度不是特别大了,主要是地沟打好之后得看看高低是不是符合要求的,不能本来是往外排水的反而外面高,这个怎么看就又很有技巧了,一般在地上的只要侧着看一下就好了,考究点就用下水平尺,但是在地下的,不用水平尺,其实可以借助于地沟里正要放进去的水管,放点水进去,看水往哪流就行了,铺好水管后,就剩填埋的活了,不是太麻烦了,那天真的是累到了,打那个混凝土的时候我真的是把我整个人压上去了,不过也挺爽的,有点把平时无处发泄的蛮力发泄出去了。
+ 在老丈人家的小工记三
+ /2020/09/13/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%B8%89/
+ 小工记三前面这两周周末也都去老丈人家帮忙了,上上周周六先是去了那个在装修的旧房子那,把三楼收拾了下,因为要搬进来住,来不及等二楼装修好,就要把三楼里的东西都整理干净,这个活感觉是比较 easy,原来是就准备把三楼当放东西仓储的地方了,我们乡下大部分三层楼都是这么用的,这次也是没办法,之前搬进来的木头什么的都搬出去,主要是这上面灰尘太多,后面清理鼻孔的时候都是黑色的了,把东西都搬出去以后主要是地还是很脏,就扫了地拖了地,因为是水泥地,灰尘又太多了,拖起来都是会灰尘扬起来,整个脱完了的确干净很多,然而这会就出了个大乌龙,我们清理的是三楼的西边一间,结果老丈人上来说要住东边那间的🤦♂️,不过其实西边的也得清理,因为还是要放被子什么的,不算是白费功夫,接着清理东边那间,之前这个房子做过群租房,里面有个高低铺的床,当时觉得可以用在放被子什么的就没扔,只是拆掉了放旁边,我们就把它擦干净了又装好,发现螺丝🔩少了几个,亘古不变的真理,拆了以后装要不就多几个要不就少几个,不是很牢靠,不过用来放放被子省得放地上总还是可以的,对了前面还做了个事情就是铺地毯,其实也不是地毯,就是类似于墙布雨篷布那种,别人不用了送给我们的,三楼水泥地也不会铺瓷砖地板了就放一下,干净好看点,不过大小不合适要裁一下,那把剪刀是真的太难用了,我手都要抽筋了,它就是刀口只有一小个点是能剪下来的,其他都是钝的,后来还是用刀片直接裁,铺好以后,真的感觉也不太一样了,焕然一新的感觉
差不多中午了就去吃饭了,之前两次是去了一家小饭店,还是还比较干净,但是店里菜不好吃,还死贵,这次去了一家小快餐店,口味好,便宜,味道是真的不错,带鱼跟黄鱼都好吃,一点都不腥,我对这类比较腥的鱼真的是很挑剔的,基本上除了家里做的很少吃外面的,那天抱着试试的态度吃了下,真的还不错,后来丈母娘说好像这家老板是给别人结婚喜事酒席当厨师的,怪不得做的好吃,其实本来是有一点小抗拒,怕不干净什么的,后来发现菜很好吃,而且可能是老丈人跟干活的师傅去吃的比较多,老板很客气,我们吃完饭,还给我们买了葡萄吃,不过这家店有一个槽点,就是饭比较不好吃,有时候会夹生,不过后面聊起来其实是这种小菜馆饭点的通病,烧的太早太多容易多出来浪费,烧的迟了不够吃,而且大的电饭锅比较不容易烧好。
下午前面还是在处理三楼的,窗户上各种钉子,实在是太多了,我后面在走廊上排了一排🤦♂️,有些是直接断了,有些是就撬了出来,感觉我在杭州租房也没有这样子各种钉钉子,挂下衣服什么的也不用这么多吧,比较不能理解,搞得到处都是钉子。那天我爸也去帮忙了,主要是在卫生间里做白缝,其实也是个技术活,印象中好像我小时候自己家里也做过这个事情,但是比较模糊了,后面我们三楼搞完了就去帮我爸了,前面是我老婆二爹在那先刷上白缝,这里叫白缝,有些考究的也叫美缝,就是瓷砖铺完之后的缝,如果不去弄的话,里面水泥的颜色就露出来了,而且容易渗水,所以就要用白水泥加胶水搅拌之后糊在缝上,但是也不是直接糊,先要把缝抠一抠,因为铺瓷砖的还不会仔细到每个缝里的水泥都是一样满,而且也需要一些空间糊上去,不然就太表面的一层很容易被水直接冲掉了,然后这次其实也不是用的白水泥,而是直接现成买来就已经配好的用来填缝的,兑水搅拌均匀就好了,后面就主要是我跟我爸在搞,那个时候真的觉得我实在是太胖了,蹲下去真的没一会就受不了了,膝盖什么的太难受了,后面我跪着刷,然后膝盖又疼,也是比较不容易,不过我爸动作很快,我中间跪累了休息一会,我爸就能搞一大片,后面其实我也没做多少(谦虚一下),总体来讲这次不是很累,就是蹲着跪着腿有点受不了,是应该好好减肥了。
]]>
生活
@@ -12697,34 +12703,33 @@ out.flush
- 在 wsl 2 中开启 ssh 连接
- /2023/04/23/%E5%9C%A8-wsl-2-%E4%B8%AD%E5%BC%80%E5%90%AF-ssh-%E8%BF%9E%E6%8E%A5/
- 之前在 wsl 1 中开启 ssh 其实很方便,只要把 sshd 服务起来就好了,但是在 wsl 2 中就不太一样了,
我这边使用的是 wsl 2 中的 Ubuntu 20.04,直接启动 sshd 服务是没法让其他机器连接的,而且都没有 ifconfig 命令可以查看 ip
不过可以用直接用 ip a来查看,可以看到这个 ip 是172网段的,而在 wsl 1 中可以看到 ip 就是 win 的 ip,
所以需要做一些操作,首先要安装 openssl-server
-sudo apt update
-sudo apt install openssh-server
-另外如果需要提高安全性,可以wsl 中配置 hosts.allow
-sshd:192.168.xx.
-先定一个子网段,然后对于ssh的配置,可以做以下修改
-Port 22
-PasswordAuthentication yes
-在进行重启
-sudo service ssh --full-restart
-配置以后发现上面的问题,没法远程登录,因为 wsl 2 是基于 hyper-v 虚拟机实现的,并且 ip 使用的是一个虚拟出来的子网 ip,所以需要在 Windows 这一层配置端口的转发,可以通过命令netsh interface portproxy show v4tov4看到
![]()
截图是我已经添加好了的,先把原来的删除,再进行添加
-netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=22
-netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=172.19.129.207 connectport=22
-也可以全量删除
-netsh int portproxy reset all
-但是这样也不能直接访问了,还需要开启防火墙
-netsh advfirewall firewall add rule name="WSL SSH" dir=in action=allow protocol=TCP localport=22
-
-
-
+ 小工周记一
+ /2023/03/05/%E5%B0%8F%E5%B7%A5%E5%91%A8%E8%AE%B0%E4%B8%80/
+ 开始修老房子又可以更新这个系列了,比较无聊,就是帮着干点零活的记录,这次过去起的比较早,前几天是在翻新瓦片,到这次周六是收尾了,到了的时候先是继续筛了沙子,上周也筛了,就是只筛了一点点,筛沙子的那个像纱窗一样的还是用一扇中空的中间有一根竖档的门钉上铁丝网做的,就是沙子一直放在外面,原来是有袋子装好的,后来是风吹雨打在上面的都已经破掉,还夹杂了很多树叶什么的,需要过下筛,并且前面都是下雨天,沙子都是湿的,不太像我以前看村里有人造房子筛沙子那样,用铲子铲上去就自己都下去的,湿的就是会在一坨,所以需要铲得比较少,然后撒的比较开,这个需要一点经验,然后如果有人一起的话就可以用扫把按住扫一下,这样就会筛得比较有效,不至于都滑下去,沙子本来大部分是可以筛出来的,还有一点就是这种情况网筛需要放得坡度小一点,不然就更容易直接往下调,袋子没破的就不用过筛了,只是湿掉了的是真的重,筛完了那些破掉了的袋子里的沙子,就没有特别的事情要做了,看到大工在那打墙,有一些敲下来的好的砖头就留着,需要削一下上面的混凝土,据大工说现在砖头要七八毛一块了,后面能够重新利用还是挺值钱的,我跟 LD 就用泥刀和铁锹的在那慢慢削,砖头上的泥灰有的比较牢固有的就像直接是沙子,泥刀刮一下就下来了,有的就结合得比较牢固,不过据说以前的砖头工艺上还比较落后,这个房子差不多是三十年前了的,砖头表面都是有点不平,甚至变形,那时候可能砖头是手工烧制的,现在的砖头比较工艺可好多了,不过可能也贵了很多,后来老丈人也过来了,指导了我们拌泥灰,就是水泥和黄沙混合,以前小时候可喜欢玩这个了,可是就也搞不清楚这个是怎么搅拌的,只看见是水泥跟黄沙围城一圈,中间放水,然后一点点搬进去,首先需要先把干的水泥跟黄沙进行混合,具体的比例是老丈人说的,拌的方式有点像堆沙堆,把水泥黄沙铲起来堆起来,就一直要往这个混合堆的尖尖上堆,这样子它自己滑下来能更好地混合,来回两趟就基本混合均匀了,然后就是跟以前看过的,中间扒拉出一个空间放水,然后慢慢把周围的混合好的泥沙推进去,需要注意不要太着急,旁边的推进去太多太快就会漏水出来,一个是会把旁边的地给弄脏,另一个也铲不回水,然后就是推完所有的都混合水了,就铲起来倒一下,再将铲子翻过来捣几下,后面我们就去吃饭了,去了一家叫金记的,又贵又不太好吃的店,就是离得比较近,六七个人只有六七个菜,吃完要四百多,也是有点离谱了。
下午是重头戏,其实本来倒没啥事,就说帮忙搞下靠背(就是踢脚线上面到窗台附近的用木头还有其他材料的装饰性的),都撬撬掉,但是真的是有点离谱了,首先是撬棒真的很重,20 斤的重量(网上查的,没有真的查过),抡起来还要用力地铲进去,因为就是破坏性的要把整个都撬掉,对于我这种又没技巧又没力气的非专业选手,抡起撬棍铲两下手就开始痛了,只是也比较犟,不想刚开始弄就说太重了要休息,后面都完全靠的一点倔强劲撑着,看着里面的工艺感觉也是不容易的,直着横着的木条有好多,竖的一整条,每隔三五十公分,横着的就是三五十公分,每根都要用钉子钉起来,然后外层好像是贴上去,在同一个面的开了头之后就能靠着蛮力往下撬,但是到了转角就又要重新开头,而且最上面一根横条跟紧邻的那一块,大概十几公分,是横着的三条钉在一起,真的是大力都出不了奇迹了,用撬棍的一头用力地敲打都很震手,要从下面往上铲进去撬开一点,然后再从上面往下敲打,这里比较重要的是要小心钉子,我这次运气比较好,踩下去已经扎到了,不过正好在脚趾缝里,没有扎到脚,还是要小心的,做完这个我的手真的是差不多废了,上臂的疼痛已经动一下就受不了了,后面有撬下了最下面当踢脚线的小瓷砖,这个房子估计中间修过一次,两批水泥糊的,新的那批粘的特别牢,敲敲打打了半天才下来一点点,锤子敲上去跟一整块石头一样,震手又没有进展,整个搞完,楼上又在敲墙了,下面的灰尘也是从没见过,我一直在那洒水都完全没有缓解,就上去跟 LD 一起拣砖头,手痛到只能抬两块砖头都会痛了。
回到家里开始越来越痛,两个手就完全没法动了,应该也是肌肉拉伤了,我这样是没足够的力气也不会什么技巧,像大工说的,他们也累也难,只是为了赚钱,不过他们有了经验跟技巧,会注意怎么使力不容易受伤,怎么样比较省力,还有一点就是即使这么累,他们一般也下午五点半就下班了,真的很累了,至少还有不少时间可以回家休息,而我们的职业呢,就像 LD 说的回家就像住酒店,就只是来洗澡睡个觉,希望能改善吧
]]>
- wsl
+ 生活
- wsl
+ 生活
+ 小技巧
+ 运动
+ 减肥
+ 跑步
+ 干活
+
+
+
+ 分享一次折腾老旧笔记本的体验-续篇
+ /2023/02/12/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C-%E7%BB%AD%E7%AF%87/
+ 接着上一篇的折腾记,因为这周又尝试了一些新的措施和方法,想继续记录分享下,上周的整体情况大概是 Ubuntu 系统能进去了,但是 Windows 进不去,PE 也进不去,Windows 启动盘也进不去,因为我的机器加过一个 msata 的固态,Windows 是装在 msata 固态硬盘里的,Ubuntu 是装在机械硬盘里的,所以有了一种猜测就是可能这个固态硬盘有点问题,还有就是还是怀疑内存的问题,正好家里还有个msata 的固态硬盘,是以前想给LD 的旧笔记本换上的,因为买回来放在那没有及时装,后面会又找不到,直到很后面才找到,LD 也不怎么用那个笔记本了,所以就一直放着了,这次我就想拿来换上。
周末回家我就开始尝试了,换上了新的固态硬盘后,插上 Windows 启动 U 盘,这次一开始看起来有点顺利,在 BIOS 选择 U 盘启动,进入了 Windows 安装界面,但是装到一半,后面重启了之后就一直说硬盘有问题,让重启,但是重启并没有解决问题,变成了一直无效地重复重启,再想进 U 盘启动,就又进不去了,这时候怎么说呢,感觉硬盘不是没有问题,但是呢,问题应该不完全出在这,所以按着总体的逻辑来讲,主板带着cpu 跟显卡,都换掉了,硬盘也换掉了,剩下的就是内存了,可是内存我也尝试过把后面加的那条金士顿拔掉,可还是一样,也尝试过用橡皮擦金手指,这里感觉也很奇怪了,找了一圈了都感觉没啥明确的原因,比如其实我的猜测,主板电池的问题,一些电阻坏掉了,但是主板是换过的,那如果内存有问题,照理我用原装的那条应该会没问题,也有一种非常小的可能,就是两条内存都坏了,或者说这也是一种不太可能的可能,所以最后的办法就打算试试把两条内存都换掉,不过现在网上都找不到这个内存的确切信息了,只能根据大致的型号去买来试试,就怕买来的还是坏的,其实也怕是这个买来的主板因为也是别的拆机下来的,不一定保证完全没问题,要是有类似的问题或者也有别的问题导致开不起来就很尴尬,也没有很多专业的仪器可以排查原因,比如主板有没有什么短路的,对了还有一个就是电源问题,但是电源的问题也就可能是从充电器插的口子到主板的连线,因为 LD 的电源跟我的口子一样,也试过,但是结果还是一样,顺着正常逻辑排查,目前也没有剩下很明确的方向了,只能再尝试下看看。
+]]>
+
+ Windows
+ 小技巧
+
+
+ Windows
@@ -12748,6 +12753,19 @@ netsh interface portproxy add v4tov4 listenaddress=0跑步
-
- 周末我在老丈人家打了天小工
- /2020/08/16/%E5%91%A8%E6%9C%AB%E6%88%91%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/
- 这周回家提前约好了要去老丈人家帮下忙,因为在翻修下老房子,活不是特别整的那种,所以大部分都是自己干,或者找个大工临时干几天(我们这那种比较专业的泥工匠叫做大工),像我这样去帮忙的,就是干点小工(把给大工帮忙的,干些偏体力活的叫做小工)的活。从大学毕业以后真的蛮少帮家里干活了,以前上学的时候放假还是帮家里淘个米,简单的扫地拖地啥的,当然刚高考完的时候,还去我爸厂里帮忙干了几天的活,实在是比较累,不过现在想着是觉得自己那时候比较牛,而不是特别排斥这个活,相对于现在的工作来说,导致了一系列的职业病,颈椎腰背都很僵硬,眼镜也不好,还有反流,像我爸那种活反而是脑力加体力的比较好的结合。
这一天的活前半部分主要是在清理厨房,瓷砖上的油污和墙上天花板上即将脱落的石灰或者白色涂料层,这种活特别是瓷砖上的油污,之前在自己家里也干活,还是比较熟悉的,不过前面主要是LD 在干,我主要是先搞墙上和天花板上的,干活还是很需要技巧的,如果直接去铲,那基本我会变成一个灰人,而且吸一鼻子灰,老丈人比较专业,先接上软管用水冲,一冲效果特别好,有些石灰涂料层直接就冲掉了,冲完之后先用带加长杆的刀片铲铲了一圈墙面,说实话因为老房子之前租出去了,所以墙面什么的被糟蹋的比较难看,一层一层的,不过这还算还好,后面主要是天花板上的,这可难倒我了,从小我爸妈是比较把我当小孩管着,爬上爬下的基本都是我爸搞定,但是到了老丈人家也只得硬着头皮上了,爬到跳(一种建筑工地用的架子)上,还有点晃,小心脏扑通扑通跳,而且带加长杆的铲子还是比较重的,铲一会手也有点累,不过坚持着铲完了,上面还是比较平整的,不过下来的时候又把我难住了🤦♂️,往下爬的时候有根杆子要跨过去,由于裤子比较紧,强行一把跨过去怕抽筋,所以以一个非常尴尬的姿势停留休息了一会,再跨了过去,幸好事后问 LD,他们都没看到,哈哈哈,然后就是帮忙一起搞瓷砖上的油污,这个太有经验了,不过老丈人更有意思,一会试试啤酒,一会用用沙子,后面在午饭前基本就弄的比较干净了,就坐着等吃饭了,下午午休了会,就继续干活了。
下午是我这次体验的重点了,因为要清理以前贴的墙纸,真的是个很麻烦的活,只能说贴墙纸的师傅活干得太好了,基本不可能整个撕下来,想用铲子一点点铲下来也不行,太轻了就只铲掉表面一层,太重了就把墙纸跟墙面的石灰啥的整个铲下来了,而且手又累又酸,后来想着是不是继续用水冲一下,对着一小面墙试验了下,效果还不错,但是又发现了个问题,那一面墙又有一块是后面糊上去的,铲掉外层的石灰后不平,然后就是最最重头的,也是让我后遗症持续到第二天的,要把那一块糊上去的水泥敲下来,毛估下大概是敲了80%左右,剩下的我的手已经不会用力了,因为那一块应该是要糊上去的始作俑者,就一块里面凹进去的,我拿着榔头敲到我手已经没法使劲了,而且大下午,感觉没五分钟,我的汗已经糊满脸,眼睛也睁不开,不然就流到眼睛里了,此处获得成就一:用榔头敲墙壁,也是个技术加体力的活,而且需要非常好的技巧,否则手马上就废了,敲下去的反作用力,没一会就不行了,然后是看着老丈人兄弟帮忙拆一个柜子,在我看来是个几天都搞不定的活,他轻轻松松在我敲墙的那会就搞定了,以前总觉得我干的活非常有技术含量,可是这个事情真的也是很有技巧啊,它是个把一间房间分隔开的柜子,从底到顶上,还带着门,我还在旁边帮忙撬一下脚踢,一根木条撬半天,唉,成就二:专业的人就是不一样。
最后就是成就三了:我之前沾沾自喜的跑了多少步,做了什么锻炼,其实都是渣渣,像这样干一天活,没经历过的,基本大半天就废了,反过来说,如果能经常去这么干一天活,跑步啥的都是渣渣,消耗的能量远远超过跑个十公里啥的。
-]]>
-
- 生活
- 运动
- 跑步
- 干活
-
-
- 生活
- 运动
- 减肥
- 跑步
- 干活
-
-
-
- 屯菜惊魂记
- /2022/04/24/%E5%B1%AF%E8%8F%9C%E6%83%8A%E9%AD%82%E8%AE%B0/
- 因某国际大都市的给力表现,昨儿旁边行政区启动应急响应,同事早上就在群里说要去超市买菜了,到了超市人还特别多,由于来的就是我们经常去的那家超市,一方面为了安全,另一方面是怕已经抢不到了,就去了另一家比较远的超市,开车怕没车位就骑了小电驴,还下着小雨,结果到了超市差不多 12 点多,超市里出来的人都是推着一整车一整车的物资,有些比较像我,整箱的泡面,好几提纸巾,还有各种吃的,都是整箱整箱的,进了超市发现结账包括自助结账的都排很长的队,到了蔬菜货架附近,差点哼起那首歌“空空如也~”,新鲜蔬菜基本已被抢空,只剩下一些卖相不太好的土豆番薯之类的,也算是意料之外情理之中了,本来以为这家超市稍微离封控区远一些会空一点,结果就是所谓的某大都市封控了等物资,杭州市是屯了物资等封控,新鲜蔬菜没了我们也只能买点其他的,神奇的是水果基本都在,可能困难时期水果不算必需品了?还是水果基本人人都已经储备了很多,不太能理解,虽然水果还在,但是称重的地方也还有好多人排队,我们采取了并行策略,LD 在那排队,遥控指挥我去拿其他物资,拿了点碱水面,黑米,那黑米的时候还闹了个乌龙,因为前面就是散装鸡蛋的堆货的地方,结果我们以为是在那后面排队,结果称重那个在那散步了,我们还在那排队,看到后面排队,那几个挑的人也该提醒下吧,几个鸡蛋挑了半天,看看人家大妈,直接拿了四盘,看了下牛奶货架也比较空,不过还有致优跟优倍,不过不算很实惠,本来想买,只是后来赶着去结账,就给忘了,称好了黑米去看了下肉,结果肉也没了,都在买猪蹄,我们也不太爱吃猪蹄,就买了点鸡胸肉,整体看起来我们买的东西真的有点格格不入,不买泡面(因为 LD 不让买了),也不屯啥米和鸡蛋,其实鸡蛋已经买了,米也买了,其他的本身冰箱小也放不下太多东西,我是觉得还可能在屯一点这那的,LD 觉得太多了,基本的米面油有了,其他调味品什么也有了。后面就是排队结账,我去排的时候刚好前面一个小伙子跟大妈在争执,大妈说我们差不多时间来的,你要排前面就前面,小伙子有点不高兴,觉得她就是插队,哈哈,平时一般这种剧情都是发生在我身上的,这会看着前面的吵起来还是很开心的,终于有跟我一样较真的人了,有时候总觉得我是个很纠结,很较真的人,但是我现在慢慢认可了这种较真,如果没有人指出来这种是插队行为,是不对的,就会有越来越多的人觉得是可以随意插队的,正确的事应该要坚持,很多情况大家总是觉得多一事不如少一事,鸡毛蒜皮的没什么好计较的,正是这种想法,那么多人才不管任何规则,反而搞得像遵守规则都是傻 X 似的。回到屯物资,后面结账排到队了也没来得及买原来想买的花生牛奶什么的,毕竟那么多人排着队,回家后因为没有蔬菜,结果就只能吃干菜汤和饭了
-]]>
-
- 生活
-
-
- 生活
- 囤物资
-
-
-
- 搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答
- /2022/01/16/%E6%90%AC%E8%BF%90%E4%B8%A4%E4%B8%AA-StackOverflow-%E4%B8%8A%E7%9A%84-Mysql-%E7%BC%96%E7%A0%81%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94/
- Mysql 字符编码和排序规则这个一直是属于一知半解的状态,知道 utf8 跟 utf8mb4 的区别主要是能不能支持 emoji,但是具体后面配置的排序规则是用来干嘛,或者有什么区别,应该使用哪个,所以在 stackoverflow 上找了下,有两个比较不错的解答,就搬过来并且配合机翻做了点修改
-原文
For those people still arriving at this question in 2020 or later, there are newer options that may be better than both of these. For example, utf8mb4_0900_ai_ci.
-All these collations are for the UTF-8 character encoding. The differences are in how text is sorted and compared.
-_unicode_ci and _general_ci are two different sets of rules for sorting and comparing text according to the way we expect. Newer versions of MySQL introduce new sets of rules, too, such as _0900_ai_ci for equivalent rules based on Unicode 9.0 - and with no equivalent _general_ci variant. People reading this now should probably use one of these newer collations instead of either _unicode_ci or _general_ci. The description of those older collations below is provided for interest only.
-MySQL is currently transitioning away from an older, flawed UTF-8 implementation. For now, you need to use utf8mb4 instead of utf8 for the character encoding part, to ensure you are getting the fixed version. The flawed version remains for backward compatibility, though it is being deprecated.
-Key differences
-utf8mb4_unicode_ci is based on the official Unicode rules for universal sorting and comparison, which sorts accurately in a wide range of languages.
-utf8mb4_general_ci is a simplified set of sorting rules which aims to do as well as it can while taking many short-cuts designed to improve speed. It does not follow the Unicode rules and will result in undesirable sorting or comparison in some situations, such as when using particular languages or characters.
-On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.
-Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci
-utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.
-As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:
-For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).
-Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.
-In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.
-What should you use?
-There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.
-In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.
-There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.
-One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.
-What the parts mean
-Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.
-Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.
-And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.
-翻译
对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci。
-所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。
-_unicode_ci和 _general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci或 _general_ci。下面对那些较旧的排序规则的描述仅供参考。
-MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。
-主要区别
-utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。
-utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。
-在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。
-utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势
-utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。
-就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:
-例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。
-一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。
-在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。
-你应该用什么?
-几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。
-过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。
-有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。
-我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。
-其余各个部分是什么意思
-首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。
-接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。
-最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。
-utf8 和 utf8mb4 编码有什么区别
原文
UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.
-So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.
-This is what (a previous version of the same page at)the MySQL documentationhas to say about it:
-
-The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:
-
-- For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
-- For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
-
-
-So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.
-译文
UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。
-所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较。
-这就是(同一页面的先前版本)MySQL 文档 不得不说的:
-
-名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:
-
-- 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
-- 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
-
-
-因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅
-
-]]>
-
- Mysql
-
-
- mysql
- 字符集
- 编码
- utf8
- utf8mb4
- utf8mb4_0900_ai_ci
- utf8mb4_unicode_ci
- utf8mb4_general_ci
-
-
是何原因竟让两人深夜奔袭十公里
/2022/06/05/%E6%98%AF%E4%BD%95%E5%8E%9F%E5%9B%A0%E7%AB%9F%E8%AE%A9%E4%B8%A4%E4%BA%BA%E6%B7%B1%E5%A4%9C%E5%A5%94%E8%A2%AD%E5%8D%81%E5%85%AC%E9%87%8C/
@@ -12901,23 +12799,6 @@ netsh interface portproxy add v4tov4 listenaddress=0第一种最近这次经历也是有火绒的一定责任,在我尝试推出 U盘的时候提示了我被另一个大流氓软件,XlibabaProtect.exe 占用了,这个流氓软件真的是充分展示了某里的技术实力,试过 N 多种办法都关不掉也删不掉,尝试了很多种办法也没办法删除,但是后面换了种思路,一般这种情况肯定是有进程在占用 U盘里的内容,最新版本的 Powertoys 会在文件的右键菜单里添加一个叫 File Locksmith 的功能,可以用于检查正在使用哪些文件以及由哪些进程使用,但是可能是我的使用姿势不对,没有仔细看文档,它里面有个”以管理员身份重启”,可能会有用。
这算是第一种方式,
-第二种
第二种方式是 Windows 任务管理器中性能 tab 下的”打开资源监视器”,
,假如我的 U 盘的盘符是F:
就可以搜索到占用这个盘符下文件的进程,这里千万小心‼️‼️,不可轻易杀掉这些进程,有些系统进程如果轻易杀掉会导致蓝屏等问题,不可轻易尝试,除非能确认这些进程的作用。
对于前两种方式对我来说都无效,
-第三种
所以尝试了第三种,
就是磁盘脱机的方式,在”计算机”右键管理,点击”磁盘管理”,可以找到 U 盘盘符右键,点击”脱机”,然后再”推出”,这个对我来说也不行
-第四种
这种是唯一对我有效的,在开始菜单搜索”event”,可以搜到”事件查看器”,
,这个可以看到当前最近 Windows 发生的事件,打开这个后就点击U盘推出,因为推不出来也是一种错误事件,点击下刷新就能在这看到具体是因为什么推出不了,具体的进程信息
最后发现是英特尔的驱动管理程序的一个进程,关掉就退出了,虽然前面说的某里的进程是流氓,但这边是真的冤枉它了
-]]>
-
- Windows
- 小技巧
-
-
- Windows
-
-
看完了扫黑风暴,聊聊感想
/2021/10/24/%E7%9C%8B%E5%AE%8C%E4%BA%86%E6%89%AB%E9%BB%91%E9%A3%8E%E6%9A%B4-%E8%81%8A%E8%81%8A%E6%84%9F%E6%83%B3/
@@ -12935,34 +12816,20 @@ netsh interface portproxy add v4tov4 listenaddress=0系列了,比较无聊,就是帮着干点零活的记录,这次过去起的比较早,前几天是在翻新瓦片,到这次周六是收尾了,到了的时候先是继续筛了沙子,上周也筛了,就是只筛了一点点,筛沙子的那个像纱窗一样的还是用一扇中空的中间有一根竖档的门钉上铁丝网做的,就是沙子一直放在外面,原来是有袋子装好的,后来是风吹雨打在上面的都已经破掉,还夹杂了很多树叶什么的,需要过下筛,并且前面都是下雨天,沙子都是湿的,不太像我以前看村里有人造房子筛沙子那样,用铲子铲上去就自己都下去的,湿的就是会在一坨,所以需要铲得比较少,然后撒的比较开,这个需要一点经验,然后如果有人一起的话就可以用扫把按住扫一下,这样就会筛得比较有效,不至于都滑下去,沙子本来大部分是可以筛出来的,还有一点就是这种情况网筛需要放得坡度小一点,不然就更容易直接往下调,袋子没破的就不用过筛了,只是湿掉了的是真的重,筛完了那些破掉了的袋子里的沙子,就没有特别的事情要做了,看到大工在那打墙,有一些敲下来的好的砖头就留着,需要削一下上面的混凝土,据大工说现在砖头要七八毛一块了,后面能够重新利用还是挺值钱的,我跟 LD 就用泥刀和铁锹的在那慢慢削,砖头上的泥灰有的比较牢固有的就像直接是沙子,泥刀刮一下就下来了,有的就结合得比较牢固,不过据说以前的砖头工艺上还比较落后,这个房子差不多是三十年前了的,砖头表面都是有点不平,甚至变形,那时候可能砖头是手工烧制的,现在的砖头比较工艺可好多了,不过可能也贵了很多,后来老丈人也过来了,指导了我们拌泥灰,就是水泥和黄沙混合,以前小时候可喜欢玩这个了,可是就也搞不清楚这个是怎么搅拌的,只看见是水泥跟黄沙围城一圈,中间放水,然后一点点搬进去,首先需要先把干的水泥跟黄沙进行混合,具体的比例是老丈人说的,拌的方式有点像堆沙堆,把水泥黄沙铲起来堆起来,就一直要往这个混合堆的尖尖上堆,这样子它自己滑下来能更好地混合,来回两趟就基本混合均匀了,然后就是跟以前看过的,中间扒拉出一个空间放水,然后慢慢把周围的混合好的泥沙推进去,需要注意不要太着急,旁边的推进去太多太快就会漏水出来,一个是会把旁边的地给弄脏,另一个也铲不回水,然后就是推完所有的都混合水了,就铲起来倒一下,再将铲子翻过来捣几下,后面我们就去吃饭了,去了一家叫金记的,又贵又不太好吃的店,就是离得比较近,六七个人只有六七个菜,吃完要四百多,也是有点离谱了。
下午是重头戏,其实本来倒没啥事,就说帮忙搞下靠背(就是踢脚线上面到窗台附近的用木头还有其他材料的装饰性的),都撬撬掉,但是真的是有点离谱了,首先是撬棒真的很重,20 斤的重量(网上查的,没有真的查过),抡起来还要用力地铲进去,因为就是破坏性的要把整个都撬掉,对于我这种又没技巧又没力气的非专业选手,抡起撬棍铲两下手就开始痛了,只是也比较犟,不想刚开始弄就说太重了要休息,后面都完全靠的一点倔强劲撑着,看着里面的工艺感觉也是不容易的,直着横着的木条有好多,竖的一整条,每隔三五十公分,横着的就是三五十公分,每根都要用钉子钉起来,然后外层好像是贴上去,在同一个面的开了头之后就能靠着蛮力往下撬,但是到了转角就又要重新开头,而且最上面一根横条跟紧邻的那一块,大概十几公分,是横着的三条钉在一起,真的是大力都出不了奇迹了,用撬棍的一头用力地敲打都很震手,要从下面往上铲进去撬开一点,然后再从上面往下敲打,这里比较重要的是要小心钉子,我这次运气比较好,踩下去已经扎到了,不过正好在脚趾缝里,没有扎到脚,还是要小心的,做完这个我的手真的是差不多废了,上臂的疼痛已经动一下就受不了了,后面有撬下了最下面当踢脚线的小瓷砖,这个房子估计中间修过一次,两批水泥糊的,新的那批粘的特别牢,敲敲打打了半天才下来一点点,锤子敲上去跟一整块石头一样,震手又没有进展,整个搞完,楼上又在敲墙了,下面的灰尘也是从没见过,我一直在那洒水都完全没有缓解,就上去跟 LD 一起拣砖头,手痛到只能抬两块砖头都会痛了。
回到家里开始越来越痛,两个手就完全没法动了,应该也是肌肉拉伤了,我这样是没足够的力气也不会什么技巧,像大工说的,他们也累也难,只是为了赚钱,不过他们有了经验跟技巧,会注意怎么使力不容易受伤,怎么样比较省力,还有一点就是即使这么累,他们一般也下午五点半就下班了,真的很累了,至少还有不少时间可以回家休息,而我们的职业呢,就像 LD 说的回家就像住酒店,就只是来洗澡睡个觉,希望能改善吧
-]]>
-
- 生活
-
-
- 生活
- 小技巧
- 运动
- 减肥
- 跑步
- 干活
-
-
-
- 给小电驴上牌
- /2022/03/20/%E7%BB%99%E5%B0%8F%E7%94%B5%E9%A9%B4%E4%B8%8A%E7%89%8C/
- 三八节活动的时候下决心买了个小电驴,主要是上下班路上现在通勤条件越来越恶劣了,之前都是觉得坐公交就行了,实际路程就比较短,但是现在或者说大概是年前那两个月差不多就开始了,基本是堵一路,个人感觉是天目山路那边在修地铁,而且蚂蚁的几个空间都在那,上班的时间点都差不多,前一个修地铁感觉挺久了,机动车保有量也越来越多,总体是古墩路就越来越堵,还有个原因就是早上上班的点共享单车都被骑走了,有时候整整走一路都没一辆,有时候孤零零地有一辆基本都是破的;走路其实也是一种选择,但是因为要赶着上班,走得太慢就要很久,可能要 45 分钟这样,走得比较快就一身汗挺难受的。所以考虑自行车和电动车,这里还有一点就是不管是乘公交还是骑共享单车,其实都要从楼下走出去蛮远,公司回来也是,也就是这种通勤方式在准备阶段就花了比较多时间,比如总的从下班到到家的时间是半小时,可能在骑共享单车和公交车上的时间都不到十分钟,就比较难受。觉得这种比例太浪费时间,如果能有这种比较点对点的方式,估计能省时省力不少,前面说的骑共享单车的方式其实在之前是比较可行的,但是后来越来越少车,基本都是每周的前几天,周一到周三都是没有车,走路到公司再冷的天都是走出一身的汗,下雨天就更难受,本来下雨天应该是优先选择坐公交,但是一般下雨天堵车会更严重,而且车子到我上车的那个站,下雨天就挤得不行,总体说下来感觉事情都不打,但是几年下来,还是会挺不爽的。
-电驴看的比较草率,主要是考虑续航,然后锂电池外加 48v 和 24AH,这样一般来讲还是价格比较高的,只是原来没预料到这个限速,以为现在的车子都比较快,但是现在的新国标车子都是 25km/h 的限速,然后 15km/h 都是会要提醒,虽然说有一些特殊的解除限速的方法,但是解了也就 35km/h ,差距不是特别大,而且现在的车子都是比较小,也不太能载东西,特别是上下班路程也不远的情况下,其实不是那么需要速度,就像我朋友说的,可能骑车的时间还不如等红绿灯多,所以就还好,也不打算解除限速,只是品牌上也仔细看,后来选了绿源,目前大部分还是雅迪,爱玛,台羚,绿源,小牛等,路上看的话还是雅迪比较多,不过价格也比较贵一点,还有就是小牛了,是比较新兴的品牌,手机 App 什么的做得比较好,而且也比较贵,最后以相对比较便宜的价格买了个锂电 48V24AH 的小车子,后来发现还是有点不方便的点就是没有比较大的筐,也不好装,这样就是下雨天雨衣什么的比较不方便放。
-聊回来主题上牌这个事情,这个事情也是颇费心力,提车的时候店里的让我跟他早上一起去,但是因为不确定时间,也比较远就没跟着去,因为我是线上买的,线下自提,线下的店可能没啥利润可以拿,就不肯帮忙代上牌,朋友说在线下店里买是可以代上的,自己上牌过程也比较曲折,一开始是头盔没到,然后是等开发票,主要的东西就是需要骑着车子去车管所,不能只自己去,然后需要预约,附近比较近的都是提前一周就预约完了号了,要提前在支付宝上进行预约,比较空的就是店里推荐的景区大队,但是随之而来就是比较蛋疼的,这个景区大队太远了,看下骑车距离有十几公里,所以就有点拖延症,但是总归要上的,不然一直不能开是白买了,上牌的材料主要是车辆合格证,发票,然后车子上的浙品码,在车架上和电池上,然后车架号什么的都要跟合格证上完全对应,整体车子要跟合格证上一毛一样,如果有额外的反光镜,后面副座都需要拆掉,脚踏板要装上,到了那其实还比较顺利,就是十几公里外加那天比较冷,吹得头疼。
+ 分享一次比较诡异的 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
@@ -14385,6 +14252,20 @@ netsh interface portproxy add v4tov4 listenaddress=0
- 聊一下 RocketMQ 的消息存储三
- /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/
- ConsumeQueue 其实是定位到一个 topic 下的消息在 CommitLog 下的偏移量,它也是固定大小的
-// ConsumeQueue file size,default is 30W
-private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
-
-public static final int CQ_STORE_UNIT_SIZE = 20;
-
-所以文件大小是5.7M 左右
-![5udpag]()
-ConsumeQueue 的构建是通过org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService运行后的 doReput 方法,而启动是的 reputFromOffset 则是通过org.apache.rocketmq.store.DefaultMessageStore#start中下面代码设置并启动
-log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
- maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
- this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
- this.reputMessageService.start();
-
-看一下 doReput 的逻辑
-private void doReput() {
- if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
- log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
- this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
- this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
- }
- for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
-
- if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
- && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
- break;
- }
-
- // 根据偏移量获取消息
- SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
- if (result != null) {
- try {
- this.reputFromOffset = result.getStartOffset();
-
- for (int readSize = 0; readSize < result.getSize() && doNext; ) {
- // 消息校验和转换
- DispatchRequest dispatchRequest =
- DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
- int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
-
- if (dispatchRequest.isSuccess()) {
- if (size > 0) {
- // 进行分发处理,包括 ConsumeQueue 和 IndexFile
- DefaultMessageStore.this.doDispatch(dispatchRequest);
-
- if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
- && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
- DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
- dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
- dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
- dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
- }
-
- this.reputFromOffset += size;
- readSize += size;
- if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
- DefaultMessageStore.this.storeStatsService
- .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
- DefaultMessageStore.this.storeStatsService
- .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
- .addAndGet(dispatchRequest.getMsgSize());
- }
- } else if (size == 0) {
- this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
- readSize = result.getSize();
- }
- } else if (!dispatchRequest.isSuccess()) {
-
- if (size > 0) {
- log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
- this.reputFromOffset += size;
+ 搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答
+ /2022/01/16/%E6%90%AC%E8%BF%90%E4%B8%A4%E4%B8%AA-StackOverflow-%E4%B8%8A%E7%9A%84-Mysql-%E7%BC%96%E7%A0%81%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94/
+ Mysql 字符编码和排序规则这个一直是属于一知半解的状态,知道 utf8 跟 utf8mb4 的区别主要是能不能支持 emoji,但是具体后面配置的排序规则是用来干嘛,或者有什么区别,应该使用哪个,所以在 stackoverflow 上找了下,有两个比较不错的解答,就搬过来并且配合机翻做了点修改
+原文
For those people still arriving at this question in 2020 or later, there are newer options that may be better than both of these. For example, utf8mb4_0900_ai_ci.
+All these collations are for the UTF-8 character encoding. The differences are in how text is sorted and compared.
+_unicode_ci and _general_ci are two different sets of rules for sorting and comparing text according to the way we expect. Newer versions of MySQL introduce new sets of rules, too, such as _0900_ai_ci for equivalent rules based on Unicode 9.0 - and with no equivalent _general_ci variant. People reading this now should probably use one of these newer collations instead of either _unicode_ci or _general_ci. The description of those older collations below is provided for interest only.
+MySQL is currently transitioning away from an older, flawed UTF-8 implementation. For now, you need to use utf8mb4 instead of utf8 for the character encoding part, to ensure you are getting the fixed version. The flawed version remains for backward compatibility, though it is being deprecated.
+Key differences
+utf8mb4_unicode_ci is based on the official Unicode rules for universal sorting and comparison, which sorts accurately in a wide range of languages.
+utf8mb4_general_ci is a simplified set of sorting rules which aims to do as well as it can while taking many short-cuts designed to improve speed. It does not follow the Unicode rules and will result in undesirable sorting or comparison in some situations, such as when using particular languages or characters.
+On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.
+Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci
+utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.
+As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:
+For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).
+Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.
+In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.
+What should you use?
+There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.
+In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.
+There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.
+One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.
+What the parts mean
+Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.
+Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.
+And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.
+翻译
对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci。
+所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。
+_unicode_ci和 _general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci或 _general_ci。下面对那些较旧的排序规则的描述仅供参考。
+MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。
+主要区别
+utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。
+utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。
+在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。
+utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势
+utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。
+就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:
+例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。
+一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。
+在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。
+你应该用什么?
+几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。
+过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。
+有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。
+我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。
+其余各个部分是什么意思
+首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。
+接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。
+最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。
+utf8 和 utf8mb4 编码有什么区别
原文
UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.
+So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.
+This is what (a previous version of the same page at)the MySQL documentationhas to say about it:
+
+The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:
+
+- For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
+- For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
+
+
+So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.
+译文
UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。
+所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较。
+这就是(同一页面的先前版本)MySQL 文档 不得不说的:
+
+名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:
+
+- 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
+- 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
+
+
+因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅
+
+]]>
+
+ Mysql
+
+
+ mysql
+ 字符集
+ 编码
+ utf8
+ utf8mb4
+ utf8mb4_0900_ai_ci
+ utf8mb4_unicode_ci
+ utf8mb4_general_ci
+
+
+
+ 在 wsl 2 中开启 ssh 连接
+ /2023/04/23/%E5%9C%A8-wsl-2-%E4%B8%AD%E5%BC%80%E5%90%AF-ssh-%E8%BF%9E%E6%8E%A5/
+ 之前在 wsl 1 中开启 ssh 其实很方便,只要把 sshd 服务起来就好了,但是在 wsl 2 中就不太一样了,
我这边使用的是 wsl 2 中的 Ubuntu 20.04,直接启动 sshd 服务是没法让其他机器连接的,而且都没有 ifconfig 命令可以查看 ip
不过可以用直接用 ip a来查看,可以看到这个 ip 是172网段的,而在 wsl 1 中可以看到 ip 就是 win 的 ip,
所以需要做一些操作,首先要安装 openssl-server
+sudo apt update
+sudo apt install openssh-server
+另外如果需要提高安全性,可以wsl 中配置 hosts.allow
+sshd:192.168.xx.
+先定一个子网段,然后对于ssh的配置,可以做以下修改
+Port 22
+PasswordAuthentication yes
+在进行重启
+sudo service ssh --full-restart
+配置以后发现上面的问题,没法远程登录,因为 wsl 2 是基于 hyper-v 虚拟机实现的,并且 ip 使用的是一个虚拟出来的子网 ip,所以需要在 Windows 这一层配置端口的转发,可以通过命令netsh interface portproxy show v4tov4看到
![]()
截图是我已经添加好了的,先把原来的删除,再进行添加
+netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=22
+netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=172.19.129.207 connectport=22
+也可以全量删除
+netsh int portproxy reset all
+但是这样也不能直接访问了,还需要开启防火墙
+netsh advfirewall firewall add rule name="WSL SSH" dir=in action=allow protocol=TCP localport=22
+
+
+
+]]>
+
+ wsl
+
+
+ wsl
+
+
+
+ 聊一下 RocketMQ 的消息存储三
+ /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/
+ ConsumeQueue 其实是定位到一个 topic 下的消息在 CommitLog 下的偏移量,它也是固定大小的
+// ConsumeQueue file size,default is 30W
+private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
+
+public static final int CQ_STORE_UNIT_SIZE = 20;
+
+所以文件大小是5.7M 左右
+![5udpag]()
+ConsumeQueue 的构建是通过org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService运行后的 doReput 方法,而启动是的 reputFromOffset 则是通过org.apache.rocketmq.store.DefaultMessageStore#start中下面代码设置并启动
+log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
+ maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
+ this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
+ this.reputMessageService.start();
+
+看一下 doReput 的逻辑
+private void doReput() {
+ if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
+ log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
+ this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
+ this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
+ }
+ for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
+
+ if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
+ && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
+ break;
+ }
+
+ // 根据偏移量获取消息
+ SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
+ if (result != null) {
+ try {
+ this.reputFromOffset = result.getStartOffset();
+
+ for (int readSize = 0; readSize < result.getSize() && doNext; ) {
+ // 消息校验和转换
+ DispatchRequest dispatchRequest =
+ DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
+ int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
+
+ if (dispatchRequest.isSuccess()) {
+ if (size > 0) {
+ // 进行分发处理,包括 ConsumeQueue 和 IndexFile
+ DefaultMessageStore.this.doDispatch(dispatchRequest);
+
+ if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
+ && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
+ DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
+ dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
+ dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
+ dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
+ }
+
+ this.reputFromOffset += size;
+ readSize += size;
+ if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
+ DefaultMessageStore.this.storeStatsService
+ .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
+ DefaultMessageStore.this.storeStatsService
+ .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
+ .addAndGet(dispatchRequest.getMsgSize());
+ }
+ } else if (size == 0) {
+ this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
+ readSize = result.getSize();
+ }
+ } else if (!dispatchRequest.isSuccess()) {
+
+ if (size > 0) {
+ log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
+ this.reputFromOffset += size;
} else {
doNext = false;
// If user open the dledger pattern or the broker is master node,
@@ -14972,50 +14972,193 @@ netsh interface portproxy add v4tov4 listenaddress=0SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
- @Override
- public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
- Long id = (Long) arg; //message queue is selected by #salesOrderID
- long index = id % mqs.size();
- return mqs.get((int) index);
- }
- }, orderList.get(i).getOrderId());
+ 聊一下 RocketMQ 的消息存储四
+ /2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/
+ IndexFile 结构 hash 结构能够通过 key 寻找到对应在 CommitLog 中的位置
+IndexFile 的构建则是分发给这个进行处理
+class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
-而在消费侧有几个点比较重要,首先我们要保证一个 MessageQueue只被一个消费者消费,消费队列存在broker端,要保证 MessageQueue 只被一个消费者消费,那么消费者在进行消息拉取消费时就必须向mq服务器申请队列锁,消费者申请队列锁的代码存在于RebalanceService消息队列负载的实现代码中。
-List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
- for (MessageQueue mq : mqSet) {
- if (!this.processQueueTable.containsKey(mq)) {
- // 判断是否顺序,如果是顺序消费的,则需要加锁
- if (isOrder && !this.lock(mq)) {
- log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
- continue;
- }
+ @Override
+ public void dispatch(DispatchRequest request) {
+ if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
+ DefaultMessageStore.this.indexService.buildIndex(request);
+ }
+ }
+}
+public void buildIndex(DispatchRequest req) {
+ IndexFile indexFile = retryGetAndCreateIndexFile();
+ if (indexFile != null) {
+ long endPhyOffset = indexFile.getEndPhyOffset();
+ DispatchRequest msg = req;
+ String topic = msg.getTopic();
+ String keys = msg.getKeys();
+ if (msg.getCommitLogOffset() < endPhyOffset) {
+ return;
+ }
- this.removeDirtyOffset(mq);
- ProcessQueue pq = new ProcessQueue();
- long nextOffset = this.computePullFromWhere(mq);
- if (nextOffset >= 0) {
- ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
- if (pre != null) {
- log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
+ final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
+ switch (tranType) {
+ case MessageSysFlag.TRANSACTION_NOT_TYPE:
+ case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
+ case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
+ break;
+ case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
+ return;
+ }
+
+ if (req.getUniqKey() != null) {
+ indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
+ if (indexFile == null) {
+ log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
+ return;
+ }
+ }
+
+ if (keys != null && keys.length() > 0) {
+ String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
+ for (int i = 0; i < keyset.length; i++) {
+ String key = keyset[i];
+ if (key.length() > 0) {
+ indexFile = putKey(indexFile, msg, buildKey(topic, key));
+ if (indexFile == null) {
+ log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
+ return;
+ }
+ }
+ }
+ }
+ } else {
+ log.error("build index error, stop building index");
+ }
+ }
+
+配置的数量
+private boolean messageIndexEnable = true;
+private int maxHashSlotNum = 5000000;
+private int maxIndexNum = 5000000 * 4;
+
+最核心的其实是 IndexFile 的结构和如何写入
+public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
+ if (this.indexHeader.getIndexCount() < this.indexNum) {
+ // 获取 key 的 hash
+ int keyHash = indexKeyHashMethod(key);
+ // 计算属于哪个 slot
+ int slotPos = keyHash % this.hashSlotNum;
+ // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
+ int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
+
+ FileLock fileLock = null;
+
+ try {
+
+ // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
+ // false);
+ int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
+ if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
+ slotValue = invalidIndex;
+ }
+
+ long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
+
+ timeDiff = timeDiff / 1000;
+
+ if (this.indexHeader.getBeginTimestamp() <= 0) {
+ timeDiff = 0;
+ } else if (timeDiff > Integer.MAX_VALUE) {
+ timeDiff = Integer.MAX_VALUE;
+ } else if (timeDiff < 0) {
+ timeDiff = 0;
+ }
+
+ // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
+ int absIndexPos =
+ IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
+ + this.indexHeader.getIndexCount() * indexSize;
+
+ this.mappedByteBuffer.putInt(absIndexPos, keyHash);
+ this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
+ this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
+ this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
+
+ // 存放的是数量位移,不是绝对位置
+ this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
+
+ if (this.indexHeader.getIndexCount() <= 1) {
+ this.indexHeader.setBeginPhyOffset(phyOffset);
+ this.indexHeader.setBeginTimestamp(storeTimestamp);
+ }
+
+ this.indexHeader.incHashSlotCount();
+ this.indexHeader.incIndexCount();
+ this.indexHeader.setEndPhyOffset(phyOffset);
+ this.indexHeader.setEndTimestamp(storeTimestamp);
+
+ return true;
+ } catch (Exception e) {
+ log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
+ } finally {
+ if (fileLock != null) {
+ try {
+ fileLock.release();
+ } catch (IOException e) {
+ log.error("Failed to release the lock", e);
+ }
+ }
+ }
+ } else {
+ log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
+ + "; index max num = " + this.indexNum);
+ }
+
+ return false;
+ }
+
+具体可以看一下这个简略的示意图
![]()
+]]>
+
+ MQ
+ RocketMQ
+ 消息队列
+
+
+ MQ
+ 消息队列
+ RocketMQ
+
+
+
+ 聊一下 RocketMQ 的顺序消息
+ /2021/08/29/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E9%A1%BA%E5%BA%8F%E6%B6%88%E6%81%AF/
+ rocketmq 里有一种比较特殊的用法,就是顺序消息,比如订单的生命周期里,在创建,支付,签收等状态轮转中,会发出来对应的消息,这里面就比较需要去保证他们的顺序,当然在处理的业务代码也可以做对应的处理,结合消息重投,但是如果这里消息就能保证顺序性了,那么业务代码就能更好的关注业务代码的处理。
+首先有一种情况是全局的有序,比如对于一个 topic 里就得发送线程保证只有一个,内部的 queue 也只有一个,消费线程也只有一个,这样就能比较容易的保证全局顺序性了,但是这里的问题就是完全限制了性能,不是很现实,在真实场景里很多都是比如对于同一个订单,需要去保证状态的轮转是按照预期的顺序来,而不必要全局的有序性。
+对于这类的有序性,需要在发送和接收方都有对应的处理,在发送消息中,需要去指定 selector,即MessageQueueSelector,能够以固定的方式是分配到对应的 MessageQueue
+比如像 RocketMQ 中的示例
+SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
+ @Override
+ public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
+ Long id = (Long) arg; //message queue is selected by #salesOrderID
+ long index = id % mqs.size();
+ return mqs.get((int) index);
+ }
+ }, orderList.get(i).getOrderId());
+
+而在消费侧有几个点比较重要,首先我们要保证一个 MessageQueue只被一个消费者消费,消费队列存在broker端,要保证 MessageQueue 只被一个消费者消费,那么消费者在进行消息拉取消费时就必须向mq服务器申请队列锁,消费者申请队列锁的代码存在于RebalanceService消息队列负载的实现代码中。
+List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
+ for (MessageQueue mq : mqSet) {
+ if (!this.processQueueTable.containsKey(mq)) {
+ // 判断是否顺序,如果是顺序消费的,则需要加锁
+ if (isOrder && !this.lock(mq)) {
+ log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
+ continue;
+ }
+
+ this.removeDirtyOffset(mq);
+ ProcessQueue pq = new ProcessQueue();
+ long nextOffset = this.computePullFromWhere(mq);
+ if (nextOffset >= 0) {
+ ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
+ if (pre != null) {
+ log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
} else {
log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
PullRequest pullRequest = new PullRequest();
@@ -15229,158 +15372,185 @@ netsh interface portproxy add v4tov4 listenaddress=0class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
+ 聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点
+ /2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/
+ 这个话题是由一次组内同学分享引出来的,首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现,这里也稍微讲一下,在一个 1.x 的老项目里,可以看到AopAutoConfiguration 是这样的
+@Configuration
+@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
+@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
+public class AopAutoConfiguration {
- @Override
- public void dispatch(DispatchRequest request) {
- if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
- DefaultMessageStore.this.indexService.buildIndex(request);
- }
- }
-}
-public void buildIndex(DispatchRequest req) {
- IndexFile indexFile = retryGetAndCreateIndexFile();
- if (indexFile != null) {
- long endPhyOffset = indexFile.getEndPhyOffset();
- DispatchRequest msg = req;
- String topic = msg.getTopic();
- String keys = msg.getKeys();
- if (msg.getCommitLogOffset() < endPhyOffset) {
- return;
- }
+ @Configuration
+ @EnableAspectJAutoProxy(proxyTargetClass = false)
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
+ public static class JdkDynamicAutoProxyConfiguration {
+ }
- final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
- switch (tranType) {
- case MessageSysFlag.TRANSACTION_NOT_TYPE:
- case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
- case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
- break;
- case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
- return;
- }
+ @Configuration
+ @EnableAspectJAutoProxy(proxyTargetClass = true)
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
+ public static class CglibAutoProxyConfiguration {
+ }
- if (req.getUniqKey() != null) {
- indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
- if (indexFile == null) {
- log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
- return;
- }
- }
+}
- if (keys != null && keys.length() > 0) {
- String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
- for (int i = 0; i < keyset.length; i++) {
- String key = keyset[i];
- if (key.length() > 0) {
- indexFile = putKey(indexFile, msg, buildKey(topic, key));
- if (indexFile == null) {
- log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
- return;
- }
- }
- }
- }
- } else {
- log.error("build index error, stop building index");
- }
- }
+而在 2.x 中变成了这样
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
+public class AopAutoConfiguration {
-配置的数量
-private boolean messageIndexEnable = true;
-private int maxHashSlotNum = 5000000;
-private int maxIndexNum = 5000000 * 4;
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(Advice.class)
+ static class AspectJAutoProxyingConfiguration {
-最核心的其实是 IndexFile 的结构和如何写入
-public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
- if (this.indexHeader.getIndexCount() < this.indexNum) {
- // 获取 key 的 hash
- int keyHash = indexKeyHashMethod(key);
- // 计算属于哪个 slot
- int slotPos = keyHash % this.hashSlotNum;
- // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
- int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
+ @Configuration(proxyBeanMethods = false)
+ @EnableAspectJAutoProxy(proxyTargetClass = false)
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
+ static class JdkDynamicAutoProxyConfiguration {
- FileLock fileLock = null;
+ }
- try {
+ @Configuration(proxyBeanMethods = false)
+ @EnableAspectJAutoProxy(proxyTargetClass = true)
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
+ matchIfMissing = true)
+ static class CglibAutoProxyConfiguration {
- // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
- // false);
- int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
- if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
- slotValue = invalidIndex;
- }
+ }
- long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
+ }
- timeDiff = timeDiff / 1000;
+为何会加载 AopAutoConfiguration 在前面的文章聊聊 SpringBoot 自动装配里已经介绍过,有兴趣的可以看下,可以发现 springboot 在 2.x 版本开始使用 cglib 作为默认的动态代理实现。
+然后就是出现的问题了,代码是这样的,一个简单的基于 springboot 的带有数据库的插入,对插入代码加了事务注解,
+@Mapper
+public interface StudentMapper {
+ // 就是插入一条数据
+ @Insert("insert into student(name, age)" + "values ('nick', '18')")
+ public Long insert();
+}
- if (this.indexHeader.getBeginTimestamp() <= 0) {
- timeDiff = 0;
- } else if (timeDiff > Integer.MAX_VALUE) {
- timeDiff = Integer.MAX_VALUE;
- } else if (timeDiff < 0) {
- timeDiff = 0;
- }
+@Component
+public class StudentManager {
- // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
- int absIndexPos =
- IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
- + this.indexHeader.getIndexCount() * indexSize;
-
- this.mappedByteBuffer.putInt(absIndexPos, keyHash);
- this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
- this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
- this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
+ @Resource
+ private StudentMapper studentMapper;
+
+ public Long createStudent() {
+ return studentMapper.insert();
+ }
+}
- // 存放的是数量位移,不是绝对位置
- this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
+@Component
+public class StudentServiceImpl implements StudentService {
- if (this.indexHeader.getIndexCount() <= 1) {
- this.indexHeader.setBeginPhyOffset(phyOffset);
- this.indexHeader.setBeginTimestamp(storeTimestamp);
- }
+ @Resource
+ private StudentManager studentManager;
- this.indexHeader.incHashSlotCount();
- this.indexHeader.incIndexCount();
- this.indexHeader.setEndPhyOffset(phyOffset);
- this.indexHeader.setEndTimestamp(storeTimestamp);
+ // 自己引用
+ @Resource
+ private StudentServiceImpl studentService;
- return true;
- } catch (Exception e) {
- log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
- } finally {
- if (fileLock != null) {
- try {
- fileLock.release();
- } catch (IOException e) {
- log.error("Failed to release the lock", e);
- }
- }
- }
- } else {
- log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
- + "; index max num = " + this.indexNum);
- }
+ @Override
+ @Transactional
+ public Long createStudent() {
+ Long id = studentManager.createStudent();
+ Long id2 = studentService.createStudent2();
+ return 1L;
+ }
- return false;
- }
+ @Transactional
+ private Long createStudent2() {
+// Integer t = Integer.valueOf("aaa");
+ return studentManager.createStudent();
+ }
+}
-具体可以看一下这个简略的示意图
![]()
+第一个公有方法 createStudent 首先调用了 manager 层的创建方法,然后再通过引入的 studentService 调用了createStudent2,我们先跑一下看看会出现啥情况,果不其然报错了,正是这个报错让我纠结了很久
+![EdR7oB]()
+报了个空指针,而且是在 createStudent2 已经被调用到了,在它的内部,报的 studentManager 是 null,首先 cglib 作为动态代理它是通过继承的方式来实现的,相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类,具体的代理切面逻辑在子类实现,然后在调用目标对象的目标方法,但是继承的方式对于 final 和私有方法其实是没法进行代理的,因为没法继承,所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了,也就是不会进入这个方法内部,后面才发现犯了个特别二的错误,继承的方式去调用父类的私有方法,对于 Java 来说是可以调用到的,父类的私有方法并不由子类的InstanceKlass维护,只能通过子类的InstanceKlass找到Java类对应的_super,这样间接地访问。也就是说子类其实是可以访问的,那为啥访问了会报空指针呢,这里报的是studentManager 是空的,可以往依赖注入方面去想,如果忽略依赖注入,我这个studentManager 的确是 null,那是不是就没有被依赖注入呢,但是为啥前面那个可以呢
+这个问题着实查了很久,不废话来看代码
+@Override
+ protected Object invokeJoinpoint() throws Throwable {
+ if (this.methodProxy != null) {
+ // 这里的 target 就是被代理的 bean
+ return this.methodProxy.invoke(this.target, this.arguments);
+ }
+ else {
+ return super.invokeJoinpoint();
+ }
+ }
+
+
+
+这个是org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation的代码,其实它在这里不是直接调用 super 也就是父类的方法,而是通过 methodProxy 调用 target 目标对象的方法,也就是原始的 studentService bean 的方法,这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用,否则就会出现上面的问题,因为 cglib 其实是通过继承来实现,通过将调用转移到子类上加入代理逻辑,我们在简单使用的时候会直接 invokeSuper() 调用父类的方法,但是在这里 spring 的场景里需要去支持 spring 的功能逻辑,所以上面的问题就可以开始来解释了,因为 createStudent 是公共方法,cglib 可以对其进行继承代理,但是在执行逻辑的时候其实是通过调用目标对象,也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent,而对于下面的 createStudent2 方法因为是私有方法,不会走代理逻辑,也就不会有调用回目标对象的逻辑,只是通过继承关系,在子类中没有这个方法,所以会通过子类的InstanceKlass找到这个类对应的_super,然后调用父类的这个私有方法,这里要搞清楚一个点,从这个代理类直接找到其父类然后调用这个私有方法,这个类是由 cglib 生成的,不是被 spring 管理起来经过依赖注入的 bean,所以是没有 studentManager 这个依赖的,也就出现了前面的问题
+而在前面提到的cglib通过methodProxy调用到目标对象,目标对象是在什么时候设置的呢,其实是在bean的生命周期中,org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中,会调用实现了这个接口的方法,
+@Override
+public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
+ if (bean != null) {
+ Object cacheKey = getCacheKey(bean.getClass(), beanName);
+ if (this.earlyProxyReferences.remove(cacheKey) != bean) {
+ return wrapIfNecessary(bean, beanName, cacheKey);
+ }
+ }
+ return bean;
+}
+
+具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里
+protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
+ if (StringUtils.hasLength(beanName)) {
+ return (FactoryBean.class.isAssignableFrom(beanClass) ?
+ BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
+ }
+ else {
+ return beanClass;
+ }
+ }
+
+ /**
+ * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
+ * @param bean the raw bean instance
+ * @param beanName the name of the bean
+ * @param cacheKey the cache key for metadata access
+ * @return a proxy wrapping the bean, or the raw bean instance as-is
+ */
+ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
+ if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
+ return bean;
+ }
+ if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
+ return bean;
+ }
+ if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
+ this.advisedBeans.put(cacheKey, Boolean.FALSE);
+ return bean;
+ }
+
+ // Create proxy if we have advice.
+ Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
+ if (specificInterceptors != DO_NOT_PROXY) {
+ this.advisedBeans.put(cacheKey, Boolean.TRUE);
+ Object proxy = createProxy(
+ bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
+ this.proxyTypes.put(cacheKey, proxy.getClass());
+ return proxy;
+ }
+
+ this.advisedBeans.put(cacheKey, Boolean.FALSE);
+ return bean;
+ }
+
+然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类
]]>
- MQ
- RocketMQ
- 消息队列
+ Java
+ SpringBoot
- MQ
- 消息队列
- RocketMQ
+ Java
+ Spring
+ SpringBoot
+ cglib
+ 事务
@@ -15640,6 +15810,18 @@ app.setWebAp
AutoConfiguration
+
+ 深度学习入门初认识
+ /2023/04/30/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%85%A5%E9%97%A8%E5%88%9D%E8%AE%A4%E8%AF%86/
+ 对于深度学习只能说我是个门外汉,开始学习,不过很多还搞不懂,做个记录和分享,基于《深度学习入门:基于 Python 的理论与实现》,
第一章 Python 入门就不介绍了,不是重点,不过完全没有 Python 基础的可以看下,我之前算是学过一点点,
第二章我觉得入门的方式比较不错,从感知机入手,有一些顾名思义,感知一些状态(输入),来做出反应,简单的就是比如通电了,我的灯就亮起来了,并且往后就是延伸到了计算机的最基础组成,与或门,与门与或门是最基础的,配合非门进行组成,可以作为计算机基础单元ALU 的组成基础,而更复杂的也可以由此进行搭建,这是个比较通俗的解释,根据书中的定义,感知机是具有输入和输出的算法。给定一个输入后,将输出一个既定的值,单层的感知机无法实现更复杂的异或门,但是可以通过 2 层感知机来实现,也就是一个与非门,一个或门,作为第一层的感知机,他们的输出作为与门的输入,就可以成为一个异或门。理论上多层感知机可以表示计算机。
而后第三章引出了激活函数,也就是在前面的与或门和基础的感知机的基础上加上了输出的条件,前面与门或门都是最基础的 0,1 游戏,现在可以加上更复杂的判断条件,在输入的基础上配以权重,再加上偏置参数,表示被激活的容易程度,这种激活函数可以被称为阶跃函数,如果超过了一定的值就代表被激活,没有则不激活,但是实际在神经元中被使用的主要是用 sigmoid 函数,相比阶跃函数,sigmoid 函数是一个平滑的曲线,随着输入变化而连续变化,因为相对感知机,神经元需要的信号是连续的实数值信号,再往后则是对输出层的介绍,如果是回归问题,也就是根据输入预测一个(连续的)数值的问题,属于回归问题,可以用恒等函数,而对于分类问题,则使用 softmax 函数,这个函数的一个重要的点在于也是区分于简单的二元分类器,softmax 是将多个结果概率进行数值处理(归一化),也叫做归一化指数函数,对于不同的结果概率是将概率最大的进行放大,凸显其中最大的值并抑制远低于最大值的其他分量。使得其他概率值也能够被使用,但是减弱其份额权重。
+]]>
+
+ 深度学习
+
+
+ 深度学习
+
+
聊在东京奥运会闭幕式这天
/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/
@@ -15926,187 +16108,218 @@ app.setWebAp
- 聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点
- /2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/
- 这个话题是由一次组内同学分享引出来的,首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现,这里也稍微讲一下,在一个 1.x 的老项目里,可以看到AopAutoConfiguration 是这样的
-@Configuration
-@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
-@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
-public class AopAutoConfiguration {
-
- @Configuration
- @EnableAspectJAutoProxy(proxyTargetClass = false)
- @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
- public static class JdkDynamicAutoProxyConfiguration {
- }
+ 聊聊 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)
- @Configuration
- @EnableAspectJAutoProxy(proxyTargetClass = true)
- @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
- public static class CglibAutoProxyConfiguration {
- }
-
-}
-
-而在 2.x 中变成了这样
-@Configuration(proxyBeanMethods = false)
-@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
-public class AopAutoConfiguration {
-
- @Configuration(proxyBeanMethods = false)
- @ConditionalOnClass(Advice.class)
- static class AspectJAutoProxyingConfiguration {
-
- @Configuration(proxyBeanMethods = false)
- @EnableAspectJAutoProxy(proxyTargetClass = false)
- @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
- static class JdkDynamicAutoProxyConfiguration {
-
- }
-
- @Configuration(proxyBeanMethods = false)
- @EnableAspectJAutoProxy(proxyTargetClass = true)
- @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
- matchIfMissing = true)
- static class CglibAutoProxyConfiguration {
+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());
}
+}
- }
-
-为何会加载 AopAutoConfiguration 在前面的文章聊聊 SpringBoot 自动装配里已经介绍过,有兴趣的可以看下,可以发现 springboot 在 2.x 版本开始使用 cglib 作为默认的动态代理实现。
-然后就是出现的问题了,代码是这样的,一个简单的基于 springboot 的带有数据库的插入,对插入代码加了事务注解,
-@Mapper
-public interface StudentMapper {
- // 就是插入一条数据
- @Insert("insert into student(name, age)" + "values ('nick', '18')")
- public Long insert();
-}
-
-@Component
-public class StudentManager {
-
- @Resource
- private StudentMapper studentMapper;
-
- public Long createStudent() {
- return studentMapper.insert();
- }
-}
-
-@Component
-public class StudentServiceImpl implements StudentService {
-
- @Resource
- private StudentManager studentManager;
-
- // 自己引用
- @Resource
- private StudentServiceImpl studentService;
-
- @Override
- @Transactional
- public Long createStudent() {
- Long id = studentManager.createStudent();
- Long id2 = studentService.createStudent2();
- return 1L;
- }
-
- @Transactional
- private Long createStudent2() {
-// Integer t = Integer.valueOf("aaa");
- return studentManager.createStudent();
- }
-}
-
-第一个公有方法 createStudent 首先调用了 manager 层的创建方法,然后再通过引入的 studentService 调用了createStudent2,我们先跑一下看看会出现啥情况,果不其然报错了,正是这个报错让我纠结了很久
-![EdR7oB]()
-报了个空指针,而且是在 createStudent2 已经被调用到了,在它的内部,报的 studentManager 是 null,首先 cglib 作为动态代理它是通过继承的方式来实现的,相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类,具体的代理切面逻辑在子类实现,然后在调用目标对象的目标方法,但是继承的方式对于 final 和私有方法其实是没法进行代理的,因为没法继承,所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了,也就是不会进入这个方法内部,后面才发现犯了个特别二的错误,继承的方式去调用父类的私有方法,对于 Java 来说是可以调用到的,父类的私有方法并不由子类的InstanceKlass维护,只能通过子类的InstanceKlass找到Java类对应的_super,这样间接地访问。也就是说子类其实是可以访问的,那为啥访问了会报空指针呢,这里报的是studentManager 是空的,可以往依赖注入方面去想,如果忽略依赖注入,我这个studentManager 的确是 null,那是不是就没有被依赖注入呢,但是为啥前面那个可以呢
-这个问题着实查了很久,不废话来看代码
-@Override
- protected Object invokeJoinpoint() throws Throwable {
- if (this.methodProxy != null) {
- // 这里的 target 就是被代理的 bean
- return this.methodProxy.invoke(this.target, this.arguments);
- }
- else {
- return super.invokeJoinpoint();
- }
- }
-
-
-
-这个是org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation的代码,其实它在这里不是直接调用 super 也就是父类的方法,而是通过 methodProxy 调用 target 目标对象的方法,也就是原始的 studentService bean 的方法,这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用,否则就会出现上面的问题,因为 cglib 其实是通过继承来实现,通过将调用转移到子类上加入代理逻辑,我们在简单使用的时候会直接 invokeSuper() 调用父类的方法,但是在这里 spring 的场景里需要去支持 spring 的功能逻辑,所以上面的问题就可以开始来解释了,因为 createStudent 是公共方法,cglib 可以对其进行继承代理,但是在执行逻辑的时候其实是通过调用目标对象,也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent,而对于下面的 createStudent2 方法因为是私有方法,不会走代理逻辑,也就不会有调用回目标对象的逻辑,只是通过继承关系,在子类中没有这个方法,所以会通过子类的InstanceKlass找到这个类对应的_super,然后调用父类的这个私有方法,这里要搞清楚一个点,从这个代理类直接找到其父类然后调用这个私有方法,这个类是由 cglib 生成的,不是被 spring 管理起来经过依赖注入的 bean,所以是没有 studentManager 这个依赖的,也就出现了前面的问题
-而在前面提到的cglib通过methodProxy调用到目标对象,目标对象是在什么时候设置的呢,其实是在bean的生命周期中,org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中,会调用实现了这个接口的方法,
-@Override
-public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
- if (bean != null) {
- Object cacheKey = getCacheKey(bean.getClass(), beanName);
- if (this.earlyProxyReferences.remove(cacheKey) != bean) {
- return wrapIfNecessary(bean, beanName, cacheKey);
+![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());
}
- }
- return bean;
-}
+}
-具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里
-protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
- if (StringUtils.hasLength(beanName)) {
- return (FactoryBean.class.isAssignableFrom(beanClass) ?
- BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
- }
- else {
- return beanClass;
+![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());
+ }
}
- }
+}
- /**
- * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
- * @param bean the raw bean instance
- * @param beanName the name of the bean
- * @param cacheKey the cache key for metadata access
- * @return a proxy wrapping the bean, or the raw bean instance as-is
- */
- protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
- if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
- return bean;
- }
- if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
- return bean;
- }
- if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
- this.advisedBeans.put(cacheKey, Boolean.FALSE);
- return bean;
+看下运行结果
+![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();
}
+}
- // Create proxy if we have advice.
- Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
- if (specificInterceptors != DO_NOT_PROXY) {
- this.advisedBeans.put(cacheKey, Boolean.TRUE);
- Object proxy = createProxy(
- bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
- this.proxyTypes.put(cacheKey, proxy.getClass());
- return proxy;
- }
+![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();
+ }
+}
- this.advisedBeans.put(cacheKey, Boolean.FALSE);
- return bean;
- }
+class L {
+ private boolean myboolean = true;
+}
-然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类
+![https://img.nicksxs.com/uPic/LMzMtR.png]()
+可以看到变成了重量级锁。
]]>
Java
- SpringBoot
Java
- Spring
- SpringBoot
- cglib
- 事务
+ Synchronized
+ 偏向锁
+ 轻量级锁
+ 重量级锁
+ 自旋
+
+ 聊聊 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 虚拟机书中的介绍,
一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。
+
+
+- 通过一个类的全限定名来获取定义此类的二进制字节流
+- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
+- 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
+
+
+
+- 文件格式验证
+- 元数据验证
+- 字节码验证
+- 符号引用验证
+
+
+以上验证、准备、解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
+
+初始化
类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
+
+最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。
+]]>
+
+ Java
+ 类加载
+
+
聊聊 Dubbo 的容错机制
/2020/11/22/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/
@@ -16243,251 +16456,26 @@ 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());
- }
-}
+ 聊聊 Java 中绕不开的 Synchronized 关键字
+ /2021/06/20/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97/
+ Synchronized 关键字在 Java 的并发体系里也是非常重要的一个内容,首先比较常规的是知道它使用的方式,可以锁对象,可以锁代码块,也可以锁方法,看一个简单的 demo
+public class SynchronizedDemo {
-![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());
- }
- }
-}
+ public static void main(String[] args) {
+ SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
+ synchronizedDemo.lockMethod();
+ }
-看下运行结果
-![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();
- }
-}
+ public synchronized void lockMethod() {
+ System.out.println("here i'm locked");
+ }
-![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());
+ public void lockSynchronizedDemo() {
+ synchronized (this) {
+ System.out.println("here lock class");
}
- 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
- 偏向锁
- 轻量级锁
- 重量级锁
- 自旋
-
-
-
- 聊一下关于怎么陪伴学习
- /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 的类加载机制一
- /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 虚拟机书中的介绍,
一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。
-
-
-- 通过一个类的全限定名来获取定义此类的二进制字节流
-- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
-- 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
-
-
-
-- 文件格式验证
-- 元数据验证
-- 字节码验证
-- 符号引用验证
-
-
-以上验证、准备、解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
-
-初始化
类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
-
-最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。
-]]>
-
- Java
- 类加载
-
-
-
- 聊聊 Java 中绕不开的 Synchronized 关键字
- /2021/06/20/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97/
- Synchronized 关键字在 Java 的并发体系里也是非常重要的一个内容,首先比较常规的是知道它使用的方式,可以锁对象,可以锁代码块,也可以锁方法,看一个简单的 demo
-public class SynchronizedDemo {
-
- public static void main(String[] args) {
- SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
- synchronizedDemo.lockMethod();
- }
-
- public synchronized void lockMethod() {
- System.out.println("here i'm locked");
- }
-
- public void lockSynchronizedDemo() {
- synchronized (this) {
- System.out.println("here lock class");
- }
- }
-}
+}
然后来查看反编译结果,其实代码(日光)之下并无新事,即使是完全不懂的也可以通过一些词义看出一些意义
Last modified 2021年6月20日; size 729 bytes
@@ -16637,360 +16625,47 @@ public Result invoke(final Invocation invocation) throws RpcException {
- 聊聊 Java 的类加载机制二
- /2021/06/13/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%BA%8C/
- 类加载器类加载机制中说来说去其实也逃不开类加载器这个话题,我们就来说下类加载器这个话题,Java 在 jdk1.2 以后开始有了
Java 虚拟机设计团队有意把加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader).
其实在 Java 中类加载器有一个很常用的作用,比如一个类的唯一性,其实是由加载它的类加载器和这个类一起来确定这个类在虚拟机的唯一性,这里也参考下周志明书里的例子
-public class ClassLoaderTest {
-
- public static void main(String[] args) throws Exception {
- ClassLoader myLoader = new ClassLoader() {
+ 聊聊 Java 自带的那些*逆天*工具
+ /2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/
+ 原谅我的标题党,其实这些工具的确很厉害,之前其实介绍过一点相关的,是从我一次问题排查的过程中用到的,但是最近又有碰到一次排查问题,发现其实用 idea 直接 dump thread 是不现实的,毕竟服务器环境的没法这么操作,那就得用 Java 的那些工具了
+jstack & jps
譬如 jstack,这个命令其实不能更简单了
看看 help 信息
![]()
用-l参数可以打出锁的额外信息,然后后面的 pid 就是进程 id 咯,机智的小伙伴会问了(就你这个小白才问这么蠢的问题🤦♂️),怎么看 Java 应用的进程呢
那就是 jps 了,命令也很简单,一般直接用 jps命令就好了,不过也可以 help 看一下
![]()
稍微解释下,-q是只显示进程 id,-m是输出给main 方法的参数,比如我在配置中加给参数
![]()
然后用 jps -m查看
![]()
-v加上小 v 的话就是打印 jvm 参数
![]()
还是有点东西,然后就继续介绍 jstack 了,然后我们看看 jstack 出来是啥,为了加点内容我加了个死锁
+public static void main(String[] args) throws InterruptedException {
+ SpringApplication.run(ThreadDumpDemoApplication.class, args);
+ ReentrantLock lock1 = new ReentrantLock();
+ ReentrantLock lock2 = new ReentrantLock();
+ Thread t1 = new Thread() {
@Override
- public Class<?> loadClass(String name) throws ClassNotFoundException {
+ public void run() {
try {
- String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
- InputStream is = getClass().getResourceAsStream(fileName);
- if (is == null) {
- return super.loadClass(name);
- }
- byte[] b = new byte[is.available()];
- is.read(b);
- return defineClass(name, b, 0, b.length);
- } catch (IOException e) {
- throw new ClassNotFoundException(name);
+ lock1.lock();
+ TimeUnit.SECONDS.sleep(1);
+ lock2.lock();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
}
};
- Object object = myLoader.loadClass("com.nicksxs.demo.ClassLoaderTest").newInstance();
- System.out.println(object.getClass());
- System.out.println(object instanceof ClassLoaderTest);
- }
-}
-可以看下结果
![]()
这里说明了当一个是由虚拟机的应用程序类加载器所加载的和另一个由自己写的自定义类加载器加载的,虽然是同一个类,但是 instanceof 的结果就是 false 的
-双亲委派
自 JDK1.2 以来,Java 一直有些三层类加载器、双亲委派的类加载架构
-启动类加载器
首先是启动类加载器,Bootstrap Class Loader,这个类加载器负责加载放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中,也不会被加载)类库加载到虚拟机的内存中,启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把家在请求为派给引导类加载器去处理,那直接使用 null 代替即可,可以看下 java.lang.ClassLoader.getClassLoader()方法的代码片段
-/**
- * Returns the class loader for the class. Some implementations may use
- * null to represent the bootstrap class loader. This method will return
- * null in such implementations if this class was loaded by the bootstrap
- * class loader.
- *
- * <p> If a security manager is present, and the caller's class loader is
- * not null and the caller's class loader is not the same as or an ancestor of
- * the class loader for the class whose class loader is requested, then
- * this method calls the security manager's {@code checkPermission}
- * method with a {@code RuntimePermission("getClassLoader")}
- * permission to ensure it's ok to access the class loader for the class.
- *
- * <p>If this object
- * represents a primitive type or void, null is returned.
- *
- * @return the class loader that loaded the class or interface
- * represented by this object.
- * @throws SecurityException
- * if a security manager exists and its
- * {@code checkPermission} method denies
- * access to the class loader for the class.
- * @see java.lang.ClassLoader
- * @see SecurityManager#checkPermission
- * @see java.lang.RuntimePermission
- */
- @CallerSensitive
- public ClassLoader getClassLoader() {
- ClassLoader cl = getClassLoader0();
- if (cl == null)
- return null;
- SecurityManager sm = System.getSecurityManager();
- if (sm != null) {
- ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
- }
- return cl;
- }
-扩展类加载器
这个类加载器是在类sun.misc.Launcher.ExtClassLoader中以 Java 代码的形式实现的,它负责在家\lib\ext 目录中,或者被 java.ext.dirs系统变量中所指定的路径中的所有类库,它其实目的是为了实现 Java 系统类库的扩展机制
-应用程序类加载器
这个类加载器是由sun.misc.Launcher.AppClassLoader实现,通过 java 代码,并且是 ClassLoader 类中的 getSystemClassLoader()方法的返回值,可以看一下代码
-/**
- * Returns the system class loader for delegation. This is the default
- * delegation parent for new <tt>ClassLoader</tt> instances, and is
- * typically the class loader used to start the application.
- *
- * <p> This method is first invoked early in the runtime's startup
- * sequence, at which point it creates the system class loader and sets it
- * as the context class loader of the invoking <tt>Thread</tt>.
- *
- * <p> The default system class loader is an implementation-dependent
- * instance of this class.
- *
- * <p> If the system property "<tt>java.system.class.loader</tt>" is defined
- * when this method is first invoked then the value of that property is
- * taken to be the name of a class that will be returned as the system
- * class loader. The class is loaded using the default system class loader
- * and must define a public constructor that takes a single parameter of
- * type <tt>ClassLoader</tt> which is used as the delegation parent. An
- * instance is then created using this constructor with the default system
- * class loader as the parameter. The resulting class loader is defined
- * to be the system class loader.
- *
- * <p> If a security manager is present, and the invoker's class loader is
- * not <tt>null</tt> and the invoker's class loader is not the same as or
- * an ancestor of the system class loader, then this method invokes the
- * security manager's {@link
- * SecurityManager#checkPermission(java.security.Permission)
- * <tt>checkPermission</tt>} method with a {@link
- * RuntimePermission#RuntimePermission(String)
- * <tt>RuntimePermission("getClassLoader")</tt>} permission to verify
- * access to the system class loader. If not, a
- * <tt>SecurityException</tt> will be thrown. </p>
- *
- * @return The system <tt>ClassLoader</tt> for delegation, or
- * <tt>null</tt> if none
- *
- * @throws SecurityException
- * If a security manager exists and its <tt>checkPermission</tt>
- * method doesn't allow access to the system class loader.
- *
- * @throws IllegalStateException
- * If invoked recursively during the construction of the class
- * loader specified by the "<tt>java.system.class.loader</tt>"
- * property.
- *
- * @throws Error
- * If the system property "<tt>java.system.class.loader</tt>"
- * is defined but the named class could not be loaded, the
- * provider class does not define the required constructor, or an
- * exception is thrown by that constructor when it is invoked. The
- * underlying cause of the error can be retrieved via the
- * {@link Throwable#getCause()} method.
- *
- * @revised 1.4
- */
- @CallerSensitive
- public static ClassLoader getSystemClassLoader() {
- initSystemClassLoader();
- if (scl == null) {
- return null;
- }
- SecurityManager sm = System.getSecurityManager();
- if (sm != null) {
- checkClassLoaderPermission(scl, Reflection.getCallerClass());
- }
- return scl;
- }
- private static synchronized void initSystemClassLoader() {
- if (!sclSet) {
- if (scl != null)
- throw new IllegalStateException("recursive invocation");
- // 主要的第一步是这
- sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
- if (l != null) {
- Throwable oops = null;
- // 然后是这
- scl = l.getClassLoader();
+ Thread t2 = new Thread() {
+ @Override
+ public void run() {
try {
- scl = AccessController.doPrivileged(
- new SystemClassLoaderAction(scl));
- } catch (PrivilegedActionException pae) {
- oops = pae.getCause();
- if (oops instanceof InvocationTargetException) {
- oops = oops.getCause();
- }
- }
- if (oops != null) {
- if (oops instanceof Error) {
- throw (Error) oops;
- } else {
- // wrap the exception
- throw new Error(oops);
- }
+ lock2.lock();
+ TimeUnit.SECONDS.sleep(1);
+ lock1.lock();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
}
- sclSet = true;
- }
- }
-// 接着跟到sun.misc.Launcher#getClassLoader
-public ClassLoader getClassLoader() {
- return this.loader;
- }
-// 然后看到这 sun.misc.Launcher#Launcher
-public Launcher() {
- Launcher.ExtClassLoader var1;
- try {
- var1 = Launcher.ExtClassLoader.getExtClassLoader();
- } catch (IOException var10) {
- throw new InternalError("Could not create extension class loader", var10);
- }
-
- try {
- // 可以看到 就是 AppClassLoader
- this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
- } catch (IOException var9) {
- throw new InternalError("Could not create application class loader", var9);
- }
-
- Thread.currentThread().setContextClassLoader(this.loader);
- String var2 = System.getProperty("java.security.manager");
- if (var2 != null) {
- SecurityManager var3 = null;
- if (!"".equals(var2) && !"default".equals(var2)) {
- try {
- var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
- } catch (IllegalAccessException var5) {
- } catch (InstantiationException var6) {
- } catch (ClassNotFoundException var7) {
- } catch (ClassCastException var8) {
- }
- } else {
- var3 = new SecurityManager();
- }
-
- if (var3 == null) {
- throw new InternalError("Could not create SecurityManager: " + var2);
- }
-
- System.setSecurityManager(var3);
- }
-
- }
-它负责加载用户类路径(ClassPath)上所有的类库,我们可以直接在代码中使用这个类加载器,如果我们的代码中没有自定义的类在加载器,一般情况下这个就是程序中默认的类加载器
-双亲委派模型
![]()
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试家在这个类,而是把这个请求为派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的家在请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起举杯了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要家在这个类,最终都是委派给处于模型最顶层的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双薪委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
可以来看下双亲委派模型的代码实现
-/**
- * Loads the class with the specified <a href="#name">binary name</a>. The
- * default implementation of this method searches for classes in the
- * following order:
- *
- * <ol>
- *
- * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
- * has already been loaded. </p></li>
- *
- * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
- * on the parent class loader. If the parent is <tt>null</tt> the class
- * loader built-in to the virtual machine is used, instead. </p></li>
- *
- * <li><p> Invoke the {@link #findClass(String)} method to find the
- * class. </p></li>
- *
- * </ol>
- *
- * <p> If the class was found using the above steps, and the
- * <tt>resolve</tt> flag is true, this method will then invoke the {@link
- * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
- *
- * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
- * #findClass(String)}, rather than this method. </p>
- *
- * <p> Unless overridden, this method synchronizes on the result of
- * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
- * during the entire class loading process.
- *
- * @param name
- * The <a href="#name">binary name</a> of the class
- *
- * @param resolve
- * If <tt>true</tt> then resolve the class
- *
- * @return The resulting <tt>Class</tt> object
- *
- * @throws ClassNotFoundException
- * If the class could not be found
- */
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // First, check if the class has already been loaded
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- if (parent != null) {
- // 委托父类加载
- c = parent.loadClass(name, false);
- } else {
- // 使用启动类加载器
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
-
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- long t1 = System.nanoTime();
- // 调用自己的 findClass() 方法尝试进行加载
- c = findClass(name);
-
- // this is the defining class loader; record the stats
- sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
- sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
- sun.misc.PerfCounter.getFindClasses().increment();
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
-破坏双亲委派
关于破坏双亲委派模型,第一次是在 JDK1.2 之后引入了双亲委派模型之前,那么在那之前已经有了类加载器,所以java.lang.ClassLoader 中添加了一个 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。这个跟上面的逻辑其实类似,当父类加载失败,会调用 findClass()来完成加载;第二次是因为这个模型本身还有一些不足之处,比如 SPI 这种,所以有设计了线程下上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 java.lang.Thread#setContextClassLoader() 进行设置,然后第三种是为了追求程序动态性,这里有涉及到了 osgi 等概念,就不展开了
-]]>
-
- Java
-
-
- Java
- 类加载
- 加载
- 验证
- 准备
- 解析
- 初始化
- 链接
- 双亲委派
-
-
-
- 聊聊 Java 自带的那些*逆天*工具
- /2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/
- 原谅我的标题党,其实这些工具的确很厉害,之前其实介绍过一点相关的,是从我一次问题排查的过程中用到的,但是最近又有碰到一次排查问题,发现其实用 idea 直接 dump thread 是不现实的,毕竟服务器环境的没法这么操作,那就得用 Java 的那些工具了
-jstack & jps
譬如 jstack,这个命令其实不能更简单了
看看 help 信息
![]()
用-l参数可以打出锁的额外信息,然后后面的 pid 就是进程 id 咯,机智的小伙伴会问了(就你这个小白才问这么蠢的问题🤦♂️),怎么看 Java 应用的进程呢
那就是 jps 了,命令也很简单,一般直接用 jps命令就好了,不过也可以 help 看一下
![]()
稍微解释下,-q是只显示进程 id,-m是输出给main 方法的参数,比如我在配置中加给参数
![]()
然后用 jps -m查看
![]()
-v加上小 v 的话就是打印 jvm 参数
![]()
还是有点东西,然后就继续介绍 jstack 了,然后我们看看 jstack 出来是啥,为了加点内容我加了个死锁
-public static void main(String[] args) throws InterruptedException {
- SpringApplication.run(ThreadDumpDemoApplication.class, args);
- ReentrantLock lock1 = new ReentrantLock();
- ReentrantLock lock2 = new ReentrantLock();
- Thread t1 = new Thread() {
- @Override
- public void run() {
- try {
- lock1.lock();
- TimeUnit.SECONDS.sleep(1);
- lock2.lock();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- Thread t2 = new Thread() {
- @Override
- public void run() {
- try {
- lock2.lock();
- TimeUnit.SECONDS.sleep(1);
- lock1.lock();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- t1.setName("mythread1");
- t2.setName("mythread2");
- t1.start();
- t2.start();
- Thread.sleep(10000);
- }
-然后看看出来时怎么样的
-2020-08-02 21:50:32
-Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):
+ };
+ t1.setName("mythread1");
+ t2.setName("mythread2");
+ t1.start();
+ t2.start();
+ Thread.sleep(10000);
+ }
+然后看看出来时怎么样的
+2020-08-02 21:50:32
+Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):
"DestroyJavaVM" #147 prio=5 os_prio=31 tid=0x00007fc9dd807000 nid=0x2603 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
@@ -17472,152 +17147,37 @@ public Result invoke(final Invocation invocation) throws RpcException {
- 聊聊 Java 的 equals 和 hashCode 方法
- /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/
- Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
简单看下代码
java.lang.Object#equals
-public boolean equals(Object obj) {
- return (this == obj);
- }
-对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了
java.lang.String#equals
-public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String)anObject;
- int n = value.length;
- if (n == anotherString.value.length) {
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = 0;
- while (n-- != 0) {
- if (v1[i] != v2[i])
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
-然后呢就是为啥一些书或者 effective java 中写了 equals 跟 hashCode 要一起重写,这里涉及到当对象作为 HashMap 的 key 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 object 的hashCode 是 native 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.
-]]>
-
- java
-
-
- java
-
-
-
- 聊聊 Linux 下的 top 命令
- /2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/
- top 命令在日常的 Linux 使用中,特别是做一些服务器的简单状态查看,排查故障都起了比较大的作用,但是由于这个命令看到的东西比较多,一般只会看部分,或者说像我这样就会比较片面地看一些信息,比如默认是进程维度的,可以在启动命令的时候加-H进入线程模式
--H :Threads-mode operation
- Instructs top to display individual threads. Without this command-line option a summation of all threads in each process is shown. Later
- this can be changed with the `H' interactive command.
-这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样
![]()
执行后是这样
![]()
第二个就是排序了
-SORTING of task window
-
- For compatibility, this top supports most of the former top sort keys. Since this is primarily a service to former top users, these commands
- do not appear on any help screen.
- command sorted-field supported
- A start time (non-display) No
- M %MEM Yes
- N PID Yes
- P %CPU Yes
- T TIME+ Yes
+ 聊聊 Sharding-Jdbc 的简单使用
+ /2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/
+ 我们在日常工作中还是使用比较多的分库分表组件的,其中比较优秀的就有 Sharding-Jdbc,一开始由当当开源,后来捐献给了 Apache,说一下简单使用,因为原来经常的使用都是基于 xml 跟 properties 组合起来使用,这里主要试下用 Java Config 来配置
首先是通过 Spring Initializr 创建个带 jdbc 的 Spring Boot 项目,然后引入主要的依赖
+<dependency>
+ <groupId>org.apache.shardingsphere</groupId>
+ <artifactId>shardingsphere-jdbc-core</artifactId>
+ <version>5.0.0-beta</version>
+</dependency>
+因为前面有聊过 Spring Boot 的自动加载,在这里 spring 就会自己去找 DataSource 的配置,所以要在入口把它干掉
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
+public class ShardingJdbcDemoApplication implements CommandLineRunner {
+然后因为想在入口跑代码,就实现了下 org.springframework.boot.CommandLineRunner 主要是后面的 Java Config 代码
+
+// 注意这里的注解,可以让 Spring 自动帮忙加载,也就是 Java Config 的核心
+@Configuration
+public class MysqlConfig {
- Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
- mand. That will help ensure that the actual sort environment matches your intent.
+ @Bean
+ public DataSource dataSource() throws SQLException {
+ // Configure actual data sources
+ Map<String, DataSource> dataSourceMap = new HashMap<>();
- The following interactive commands will only be honored when the current sort field is visible. The sort field might not be visible because:
- 1) there is insufficient Screen Width
- 2) the `f' interactive command turned it Off
-
- < :Move-Sort-Field-Left
- Moves the sort column to the left unless the current sort field is the first field being displayed.
-
- > :Move-Sort-Field-Right
- Moves the sort column to the right unless the current sort field is the last field being displayed.
-查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<和>直接左右移动排序列
-]]>
-
- Linux
- 命令
- 小技巧
- top
- top
- 排序
-
-
- 排序
- linux
- 小技巧
- top
-
-
-
- 聊聊 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
-
-
-
- 聊聊 Sharding-Jdbc 的简单使用
- /2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/
- 我们在日常工作中还是使用比较多的分库分表组件的,其中比较优秀的就有 Sharding-Jdbc,一开始由当当开源,后来捐献给了 Apache,说一下简单使用,因为原来经常的使用都是基于 xml 跟 properties 组合起来使用,这里主要试下用 Java Config 来配置
首先是通过 Spring Initializr 创建个带 jdbc 的 Spring Boot 项目,然后引入主要的依赖
-<dependency>
- <groupId>org.apache.shardingsphere</groupId>
- <artifactId>shardingsphere-jdbc-core</artifactId>
- <version>5.0.0-beta</version>
-</dependency>
-因为前面有聊过 Spring Boot 的自动加载,在这里 spring 就会自己去找 DataSource 的配置,所以要在入口把它干掉
-@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
-public class ShardingJdbcDemoApplication implements CommandLineRunner {
-然后因为想在入口跑代码,就实现了下 org.springframework.boot.CommandLineRunner 主要是后面的 Java Config 代码
-
-// 注意这里的注解,可以让 Spring 自动帮忙加载,也就是 Java Config 的核心
-@Configuration
-public class MysqlConfig {
-
- @Bean
- public DataSource dataSource() throws SQLException {
- // Configure actual data sources
- Map<String, DataSource> dataSourceMap = new HashMap<>();
-
-
- // Configure the first data source
- // 使用了默认的Hikari连接池的 DataSource
- HikariDataSource dataSource1 = new HikariDataSource();
- dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
- dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/sharding");
- dataSource1.setUsername("username");
- dataSource1.setPassword("password");
- dataSourceMap.put("ds0", dataSource1);
+
+ // Configure the first data source
+ // 使用了默认的Hikari连接池的 DataSource
+ HikariDataSource dataSource1 = new HikariDataSource();
+ dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
+ dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/sharding");
+ dataSource1.setUsername("username");
+ dataSource1.setPassword("password");
+ dataSourceMap.put("ds0", dataSource1);
// Configure student table rule
// 这里是配置分表逻辑,逻辑表是 student,对应真实的表是 student_0 到 student_1, 这个配置方式就是有多少表可以用 student_$->{0..n}
@@ -17692,679 +17252,505 @@ public Result invoke(final Invocation invocation) throws RpcException {
- 聊聊 dubbo 的线程池
- /2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/
- 之前没注意到这一块,只是比较模糊的印象 dubbo 自己基于 ThreadPoolExecutor 定义了几个线程池,但是没具体看过,主要是觉得就是为了避免使用 jdk 自带的那几个(java.util.concurrent.Executors),防止出现那些问题
看下代码目录主要是这几个
-
-- FixedThreadPool:创建一个复用固定个数线程的线程池。
简单看下代码public Executor getExecutor(URL url) {
- String name = url.getParameter("threadname", "Dubbo");
- int threads = url.getParameter("threads", 200);
- int queues = url.getParameter("queues", 0);
- return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
- }
-可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
-- LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
public Executor getExecutor(URL url) {
- String name = url.getParameter("threadname", "Dubbo");
- int cores = url.getParameter("corethreads", 0);
- int threads = url.getParameter("threads", 200);
- int queues = url.getParameter("queues", 0);
- return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
- }
-这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
-- EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
public Executor getExecutor(URL url) {
- String name = url.getParameter("threadname", "Dubbo");
- int cores = url.getParameter("corethreads", 0);
- int threads = url.getParameter("threads", 2147483647);
- int queues = url.getParameter("queues", 0);
- int alive = url.getParameter("alive", 60000);
- TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
- EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
- taskQueue.setExecutor(executor);
- return executor;
- }
-这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
-- CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
public Executor getExecutor(URL url) {
- String name = url.getParameter("threadname", "Dubbo");
- int cores = url.getParameter("corethreads", 0);
- int threads = url.getParameter("threads", 2147483647);
- int queues = url.getParameter("queues", 0);
- int alive = url.getParameter("alive", 60000);
- return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
- }
-这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
只是非常简略的介绍下,有兴趣可以自行阅读代码。
-
+ 聊聊 Java 的 equals 和 hashCode 方法
+ /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/
+ Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
简单看下代码
java.lang.Object#equals
+public boolean equals(Object obj) {
+ return (this == obj);
+ }
+对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了
java.lang.String#equals
+public boolean equals(Object anObject) {
+ if (this == anObject) {
+ return true;
+ }
+ if (anObject instanceof String) {
+ String anotherString = (String)anObject;
+ int n = value.length;
+ if (n == anotherString.value.length) {
+ char v1[] = value;
+ char v2[] = anotherString.value;
+ int i = 0;
+ while (n-- != 0) {
+ if (v1[i] != v2[i])
+ return false;
+ i++;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+然后呢就是为啥一些书或者 effective java 中写了 equals 跟 hashCode 要一起重写,这里涉及到当对象作为 HashMap 的 key 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 object 的hashCode 是 native 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.
]]>
- Java
- Dubbo - 线程池
- Dubbo
- 线程池
- ThreadPool
+ java
- Java
- Dubbo
- ThreadPool
- 线程池
- FixedThreadPool
- LimitedThreadPool
- EagerThreadPool
- CachedThreadPool
+ java
- 聊聊 RocketMQ 的 Broker 源码
- /2020/07/19/%E8%81%8A%E8%81%8A-RocketMQ-%E7%9A%84-Broker-%E6%BA%90%E7%A0%81/
- broker 的启动形式有点类似于 NameServer,都是服务类型的,跟 Consumer 差别比较大,
-首先是org.apache.rocketmq.broker.BrokerStartup中的 main 函数,org.apache.rocketmq.broker.BrokerStartup#createBrokerController基本就是读取参数,这里差点把最核心的初始化给漏了,
-final BrokerController controller = new BrokerController(
- brokerConfig,
- nettyServerConfig,
- nettyClientConfig,
- messageStoreConfig);
- // remember all configs to prevent discard
- controller.getConfiguration().registerConfig(properties);
-
- boolean initResult = controller.initialize();
+ 聊聊 Sharding-Jdbc 分库分表下的分页方案
+ /2022/01/09/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E4%B8%8B%E7%9A%84%E5%88%86%E9%A1%B5%E6%96%B9%E6%A1%88/
+ 前面在聊 Sharding-Jdbc 的时候看到了一篇文章,关于一个分页的查询,一直比较直接的想法就是分库分表下的分页是非常不合理的,一般我们的实操方案都是分表加上 ES 搜索做分页,或者通过合表读写分离的方案,因为对于 sharding-jdbc 如果没有带分表键,查询基本都是只能在所有分表都执行一遍,然后再加上分页,基本上是分页越大后续的查询越耗资源,但是仔细的去想这个细节还是这次,就简单说说
首先就是我的分表结构
+CREATE TABLE `student_time_0` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` int(11) NOT NULL,
+ `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
+ `age` tinyint(3) unsigned DEFAULT NULL,
+ `create_time` bigint(20) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=674 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
+有这样的三个表,student_time_0, student_time_1, student_time_2, 以 user_id 作为分表键,根据表数量取模作为分表依据
这里先构造点数据,
+insert into student_time (`name`, `user_id`, `age`, `create_time`) values (?, ?, ?, ?)
+主要是为了保证 create_time 唯一比较好说明问题,
+int i = 0;
+try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(insertSql)) {
+ do {
+ ps.setString(1, localName + new Random().nextInt(100));
+ ps.setLong(2, 10086L + (new Random().nextInt(100)));
+ ps.setInt(3, 18);
+ ps.setLong(4, new Date().getTime());
-前面是以 broker 配置,netty 的服务端和客户端配置,以及消息存储配置在实例化 BrokerController,然后就是初始化了
-public boolean initialize() throws CloneNotSupportedException {
- boolean result = this.topicConfigManager.load();
- result = result && this.consumerOffsetManager.load();
- result = result && this.subscriptionGroupManager.load();
- result = result && this.consumerFilterManager.load();
+ int result = ps.executeUpdate();
+ LOGGER.info("current execute result: {}", result);
+ Thread.sleep(new Random().nextInt(100));
+ i++;
+ } while (i <= 2000);
+三个表的数据分别是 673,678,650,说明符合预期了,各个表数据不一样,接下来比如我们想要做一个这样的分页查询
+select * from student_time ORDER BY create_time ASC limit 1000, 5;
+student_time 对于我们使用的 sharding-jdbc 来说当然是逻辑表,首先从一无所知去想这个查询如果我们自己来处理应该是怎么做,
首先是不是可以每个表都从 333 开始取 5 条数据,类似于下面的查询,然后进行 15 条的合并重排序获取前面的 5 条
+select * from student_time_0 ORDER BY create_time ASC limit 333, 5;
+select * from student_time_1 ORDER BY create_time ASC limit 333, 5;
+select * from student_time_2 ORDER BY create_time ASC limit 333, 5;
+忽略前面 limit 差的 1,这个结果除非三个表的分布是绝对的均匀,否则结果肯定会出现一定的偏差,以为每个表的 333 这个位置对于其他表来说都不一定是一样的,这样对于最后整体的结果,就会出现偏差
因为一直在纠结怎么让这个更直观的表现出来,所以尝试画了个图
![]()
黑色的框代表我从每个表里按排序从 334 到 338 的 5 条数据, 他们在每个表里都是代表了各自正确的排序值,但是对于我们想要的其实是合表后的 1001,1005 这五条,然后我们假设总的排序值位于前 1000 的分布是第 0 个表是 320 条,第 1 个表是 340 条,第 2 个表是 340 条,那么可以明显地看出来我这么查的结果简单合并肯定是不对的。
那么 sharding-jdbc 是如何保证这个结果的呢,其实就是我在每个表里都查分页偏移量和分页大小那么多的数据,在我这个例子里就是对于 0,1,2 三个分表每个都查 1005 条数据,即使我的数据不平衡到最极端的情况,前 1005 条数据都出在某个分表中,也可以正确获得最后的结果,但是明显的问题就是大分页,数据较多,就会导致非常大的问题,即使如 sharding-jdbc 对于合并排序的优化做得比较好,也还是需要传输那么大量的数据,并且查询也耗时,那么有没有解决方案呢,应该说有两个,或者说主要是想讲后者
第一个办法是像这种查询,如果业务上不需要进行跳页,而是只给下一页,那么我们就能把前一次的最大偏移量的 create_time 记录下来,下一页就可以拿着这个偏移量进行查询,这个比较简单易懂,就不多说了
第二个办法是看的58 沈剑的一篇文章,尝试理解讲述一下,
这个办法的第一步跟前面那个错误的方法或者说不准确的方法一样,先是将分页偏移量平均后在三个表里进行查询
+t0
+334 10158 nick95 18 1641548941767
+335 10098 nick11 18 1641548941879
+336 10167 nick51 18 1641548942089
+337 10167 nick3 18 1641548942119
+338 10170 nick57 18 1641548942169
-前面这些就是各个配置的 load 了,然后是个我认为比较重要的部分messageStore 的实例化,
-if (result) {
- try {
- this.messageStore =
- new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener,
- this.brokerConfig);
- if (messageStoreConfig.isEnableDLegerCommitLog()) {
- DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
- ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
- }
- this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore);
- //load plugin
- MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig);
- this.messageStore = MessageStoreFactory.build(context, this.messageStore);
- this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager));
- } catch (IOException e) {
- result = false;
- log.error("Failed to initialize", e);
- }
-}
-result = result && this.messageStore.load();
+t1
+334 10105 nick98 18 1641548939071 最小
+335 10174 nick94 18 1641548939377
+336 10129 nick85 18 1641548939442
+337 10141 nick84 18 1641548939480
+338 10096 nick74 18 1641548939668
-先是实例化,实例化构造函数里的代码比较重要,重点看一下
-public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager,
- final MessageArrivingListener messageArrivingListener, final BrokerConfig brokerConfig) throws IOException {
- this.messageArrivingListener = messageArrivingListener;
- this.brokerConfig = brokerConfig;
- this.messageStoreConfig = messageStoreConfig;
- this.brokerStatsManager = brokerStatsManager;
- this.allocateMappedFileService = new AllocateMappedFileService(this);
- if (messageStoreConfig.isEnableDLegerCommitLog()) {
- this.commitLog = new DLedgerCommitLog(this);
- } else {
- this.commitLog = new CommitLog(this);
- }
- this.consumeQueueTable = new ConcurrentHashMap<>(32);
-
- this.flushConsumeQueueService = new FlushConsumeQueueService();
- this.cleanCommitLogService = new CleanCommitLogService();
- this.cleanConsumeQueueService = new CleanConsumeQueueService();
- this.storeStatsService = new StoreStatsService();
- this.indexService = new IndexService(this);
- if (!messageStoreConfig.isEnableDLegerCommitLog()) {
- this.haService = new HAService(this);
- } else {
- this.haService = null;
- }
- this.reputMessageService = new ReputMessageService();
+t2
+334 10184 nick11 18 1641548945075
+335 10109 nick93 18 1641548945382
+336 10181 nick41 18 1641548945583
+337 10130 nick80 18 1641548945993
+338 10184 nick19 18 1641548946294 最大
+然后要做什么呢,其实目标比较明白,因为前面那种方法其实就是我知道了前一页的偏移量,所以可以直接当做条件来进行查询,那这里我也想着拿到这个条件,所以我将第一遍查出来的最小的 create_time 和最大的 create_time 找出来,然后再去三个表里查询,其实主要是最小值,因为我拿着最小值去查以后我就能知道这个最小值在每个表里处在什么位置,
+t0
+322 10161 nick81 18 1641548939284
+323 10113 nick16 18 1641548939393
+324 10110 nick56 18 1641548939577
+325 10116 nick69 18 1641548939588
+326 10173 nick51 18 1641548939646
- this.scheduleMessageService = new ScheduleMessageService(this);
+t1
+334 10105 nick98 18 1641548939071
+335 10174 nick94 18 1641548939377
+336 10129 nick85 18 1641548939442
+337 10141 nick84 18 1641548939480
+338 10096 nick74 18 1641548939668
- this.transientStorePool = new TransientStorePool(messageStoreConfig);
+t2
+297 10136 nick28 18 1641548939161
+298 10142 nick68 18 1641548939177
+299 10124 nick41 18 1641548939237
+300 10148 nick87 18 1641548939510
+301 10169 nick23 18 1641548939715
+我只贴了前五条数据,为了方便知道偏移量,每个分表都使用了自增主键,我们可以看到前一次查询的最小值分别在其他两个表里的位置分别是 322-1 和 297-1,那么对于总体来说这个时间应该是在 322 - 1 + 333 + 297 - 1 = 951,那这样子我只要对后面的数据最多每个表查 1000 - 951 + 5 = 54 条数据再进行合并排序就可以获得最终正确的结果。
这个就是传说中的二次查询法。
+]]>
+
+ Java
+
+
+ Java
+ Sharding-Jdbc
+
+
+
+ 聊聊 Java 的类加载机制二
+ /2021/06/13/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%BA%8C/
+ 类加载器类加载机制中说来说去其实也逃不开类加载器这个话题,我们就来说下类加载器这个话题,Java 在 jdk1.2 以后开始有了
Java 虚拟机设计团队有意把加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader).
其实在 Java 中类加载器有一个很常用的作用,比如一个类的唯一性,其实是由加载它的类加载器和这个类一起来确定这个类在虚拟机的唯一性,这里也参考下周志明书里的例子
+public class ClassLoaderTest {
- if (messageStoreConfig.isTransientStorePoolEnable()) {
- this.transientStorePool.init();
+ public static void main(String[] args) throws Exception {
+ ClassLoader myLoader = new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ try {
+ String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
+ InputStream is = getClass().getResourceAsStream(fileName);
+ if (is == null) {
+ return super.loadClass(name);
+ }
+ byte[] b = new byte[is.available()];
+ is.read(b);
+ return defineClass(name, b, 0, b.length);
+ } catch (IOException e) {
+ throw new ClassNotFoundException(name);
+ }
+ }
+ };
+ Object object = myLoader.loadClass("com.nicksxs.demo.ClassLoaderTest").newInstance();
+ System.out.println(object.getClass());
+ System.out.println(object instanceof ClassLoaderTest);
+ }
+}
+可以看下结果
![]()
这里说明了当一个是由虚拟机的应用程序类加载器所加载的和另一个由自己写的自定义类加载器加载的,虽然是同一个类,但是 instanceof 的结果就是 false 的
+双亲委派
自 JDK1.2 以来,Java 一直有些三层类加载器、双亲委派的类加载架构
+启动类加载器
首先是启动类加载器,Bootstrap Class Loader,这个类加载器负责加载放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中,也不会被加载)类库加载到虚拟机的内存中,启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把家在请求为派给引导类加载器去处理,那直接使用 null 代替即可,可以看下 java.lang.ClassLoader.getClassLoader()方法的代码片段
+/**
+ * Returns the class loader for the class. Some implementations may use
+ * null to represent the bootstrap class loader. This method will return
+ * null in such implementations if this class was loaded by the bootstrap
+ * class loader.
+ *
+ * <p> If a security manager is present, and the caller's class loader is
+ * not null and the caller's class loader is not the same as or an ancestor of
+ * the class loader for the class whose class loader is requested, then
+ * this method calls the security manager's {@code checkPermission}
+ * method with a {@code RuntimePermission("getClassLoader")}
+ * permission to ensure it's ok to access the class loader for the class.
+ *
+ * <p>If this object
+ * represents a primitive type or void, null is returned.
+ *
+ * @return the class loader that loaded the class or interface
+ * represented by this object.
+ * @throws SecurityException
+ * if a security manager exists and its
+ * {@code checkPermission} method denies
+ * access to the class loader for the class.
+ * @see java.lang.ClassLoader
+ * @see SecurityManager#checkPermission
+ * @see java.lang.RuntimePermission
+ */
+ @CallerSensitive
+ public ClassLoader getClassLoader() {
+ ClassLoader cl = getClassLoader0();
+ if (cl == null)
+ return null;
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
+ }
+ return cl;
+ }
+扩展类加载器
这个类加载器是在类sun.misc.Launcher.ExtClassLoader中以 Java 代码的形式实现的,它负责在家\lib\ext 目录中,或者被 java.ext.dirs系统变量中所指定的路径中的所有类库,它其实目的是为了实现 Java 系统类库的扩展机制
+应用程序类加载器
这个类加载器是由sun.misc.Launcher.AppClassLoader实现,通过 java 代码,并且是 ClassLoader 类中的 getSystemClassLoader()方法的返回值,可以看一下代码
+/**
+ * Returns the system class loader for delegation. This is the default
+ * delegation parent for new <tt>ClassLoader</tt> instances, and is
+ * typically the class loader used to start the application.
+ *
+ * <p> This method is first invoked early in the runtime's startup
+ * sequence, at which point it creates the system class loader and sets it
+ * as the context class loader of the invoking <tt>Thread</tt>.
+ *
+ * <p> The default system class loader is an implementation-dependent
+ * instance of this class.
+ *
+ * <p> If the system property "<tt>java.system.class.loader</tt>" is defined
+ * when this method is first invoked then the value of that property is
+ * taken to be the name of a class that will be returned as the system
+ * class loader. The class is loaded using the default system class loader
+ * and must define a public constructor that takes a single parameter of
+ * type <tt>ClassLoader</tt> which is used as the delegation parent. An
+ * instance is then created using this constructor with the default system
+ * class loader as the parameter. The resulting class loader is defined
+ * to be the system class loader.
+ *
+ * <p> If a security manager is present, and the invoker's class loader is
+ * not <tt>null</tt> and the invoker's class loader is not the same as or
+ * an ancestor of the system class loader, then this method invokes the
+ * security manager's {@link
+ * SecurityManager#checkPermission(java.security.Permission)
+ * <tt>checkPermission</tt>} method with a {@link
+ * RuntimePermission#RuntimePermission(String)
+ * <tt>RuntimePermission("getClassLoader")</tt>} permission to verify
+ * access to the system class loader. If not, a
+ * <tt>SecurityException</tt> will be thrown. </p>
+ *
+ * @return The system <tt>ClassLoader</tt> for delegation, or
+ * <tt>null</tt> if none
+ *
+ * @throws SecurityException
+ * If a security manager exists and its <tt>checkPermission</tt>
+ * method doesn't allow access to the system class loader.
+ *
+ * @throws IllegalStateException
+ * If invoked recursively during the construction of the class
+ * loader specified by the "<tt>java.system.class.loader</tt>"
+ * property.
+ *
+ * @throws Error
+ * If the system property "<tt>java.system.class.loader</tt>"
+ * is defined but the named class could not be loaded, the
+ * provider class does not define the required constructor, or an
+ * exception is thrown by that constructor when it is invoked. The
+ * underlying cause of the error can be retrieved via the
+ * {@link Throwable#getCause()} method.
+ *
+ * @revised 1.4
+ */
+ @CallerSensitive
+ public static ClassLoader getSystemClassLoader() {
+ initSystemClassLoader();
+ if (scl == null) {
+ return null;
+ }
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ checkClassLoaderPermission(scl, Reflection.getCallerClass());
+ }
+ return scl;
+ }
+ private static synchronized void initSystemClassLoader() {
+ if (!sclSet) {
+ if (scl != null)
+ throw new IllegalStateException("recursive invocation");
+ // 主要的第一步是这
+ sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
+ if (l != null) {
+ Throwable oops = null;
+ // 然后是这
+ scl = l.getClassLoader();
+ try {
+ scl = AccessController.doPrivileged(
+ new SystemClassLoaderAction(scl));
+ } catch (PrivilegedActionException pae) {
+ oops = pae.getCause();
+ if (oops instanceof InvocationTargetException) {
+ oops = oops.getCause();
+ }
+ }
+ if (oops != null) {
+ if (oops instanceof Error) {
+ throw (Error) oops;
+ } else {
+ // wrap the exception
+ throw new Error(oops);
+ }
+ }
+ }
+ sclSet = true;
+ }
+ }
+// 接着跟到sun.misc.Launcher#getClassLoader
+public ClassLoader getClassLoader() {
+ return this.loader;
+ }
+// 然后看到这 sun.misc.Launcher#Launcher
+public Launcher() {
+ Launcher.ExtClassLoader var1;
+ try {
+ var1 = Launcher.ExtClassLoader.getExtClassLoader();
+ } catch (IOException var10) {
+ throw new InternalError("Could not create extension class loader", var10);
}
- this.allocateMappedFileService.start();
-
- this.indexService.start();
+ try {
+ // 可以看到 就是 AppClassLoader
+ this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
+ } catch (IOException var9) {
+ throw new InternalError("Could not create application class loader", var9);
+ }
- this.dispatcherList = new LinkedList<>();
- this.dispatcherList.addLast(new CommitLogDispatcherBuildConsumeQueue());
- this.dispatcherList.addLast(new CommitLogDispatcherBuildIndex());
-
- File file = new File(StorePathConfigHelper.getLockFile(messageStoreConfig.getStorePathRootDir()));
- MappedFile.ensureDirOK(file.getParent());
- lockFile = new RandomAccessFile(file, "rw");
- }
+ Thread.currentThread().setContextClassLoader(this.loader);
+ String var2 = System.getProperty("java.security.manager");
+ if (var2 != null) {
+ SecurityManager var3 = null;
+ if (!"".equals(var2) && !"default".equals(var2)) {
+ try {
+ var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
+ } catch (IllegalAccessException var5) {
+ } catch (InstantiationException var6) {
+ } catch (ClassNotFoundException var7) {
+ } catch (ClassCastException var8) {
+ }
+ } else {
+ var3 = new SecurityManager();
+ }
-这里面有很多类,不过先把从构造函数里传进来的忽略下,接下来就是 AllocateMappedFileService 这个service,前面看过文章的可能会根据上面的代码猜到,这也是个 ServiceThread,如果是对RocketMQ 有所了解的可能从名字可以看出这个类是关于 RocketMQ 消息怎么落盘的,当需要创建MappedFile时(在MapedFileQueue.getLastMapedFile方法中),向该线程的requestQueue队列中放入AllocateRequest请求对象,该线程会在后台监听该队列,并在后台创建MapedFile对象,即同时创建了物理文件。然后是创建了 IndexService 服务线程,用来给创建索引;还有是FlushConsumeQueueService是将ConsumeQueue 刷入磁盘;CleanCommitLogService用来清理过期的 CommitLog,默认是 72 小时以上;CleanConsumeQueueService是将小于最新的 CommitLog 偏移量的 ConsumeQueue 清理掉;StoreStatsService是储存统计服务;HAService用于CommitLog 的主备同步;ScheduleMessageService用于定时消息;还有就是这个ReputMessageService非常重要,就是由它实现了将 CommitLog 以 topic+queue 纬度构建 ConsumeQueue,后面TransientStorePool是异步刷盘时的存储buffer,也可以从后面的判断中看出来
-public boolean isTransientStorePoolEnable() {
- return transientStorePoolEnable && FlushDiskType.ASYNC_FLUSH == getFlushDiskType()
- && BrokerRole.SLAVE != getBrokerRole();
- }
+ if (var3 == null) {
+ throw new InternalError("Could not create SecurityManager: " + var2);
+ }
-再然后就是启动两个服务线程,dispatcherList是为CommitLog文件转发请求,差不多这个初始化就这些内容。
-然后回到外层,下面是主备切换的配置,然后是数据统计,接着是存储插件加载,然后是往转发器链表里再加一个过滤器
-if (messageStoreConfig.isEnableDLegerCommitLog()) {
- DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
- ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
+ System.setSecurityManager(var3);
}
- this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore);
- //load plugin
- MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig);
- this.messageStore = MessageStoreFactory.build(context, this.messageStore);
- this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager));
-
-接下来就是org.apache.rocketmq.store.MessageStore#load的过程了,
-
-- 调用ScheduleMessageService.load方法,初始化延迟级别列表。将这些级别(”1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”)的延时存入延迟级别delayLevelTable:ConcurrentHashMap<Integer /* level */, Long/* delay timeMillis */>变量中,例如1s的kv值为1:1000,5s的kv值为2:5000,key值依次类推;每个延迟级别即为一个队列。
-
-2)调用CommitLog.load方法,在此方法中调用MapedFileQueue.load方法,将$HOME /store/commitlog目录下的所有文件加载到MapedFileQueue的List变量中;
-3)调用DefaultMessageStore.loadConsumeQueue方法加载consumequeue文件数据到DefaultMessageStore.consumeQueueTable集合中。
-初始化StoreCheckPoint对象,加载$HOME/store/checkpoint文件,该文件记录三个字段值,分别是物理队列消息时间戳、逻辑队列消息时间戳、索引队列消息时间戳。
-调用IndexService.load方法加载$HOME/store/index目录下的文件。对该目录下的每个文件初始化一个IndexFile对象。然后调用IndexFile对象的load方法将IndexHeader加载到对象的变量中;再根据检查是否存在abort文件,若有存在abort文件,则表示Broker表示上次是异常退出的,则检查checkpoint的indexMsgTimestamp字段值是否小于IndexHeader的endTimestamp值,indexMsgTimestamp值表示最后刷盘的时间,若小于则表示在最后刷盘之后在该文件中还创建了索引,则要删除该Index文件,否则将该IndexFile对象放入indexFileList:ArrayList索引文件集合中。
-然后调用org.apache.rocketmq.store.DefaultMessageStore#recover恢复,前面有根据boolean lastExitOK = !this.isTempFileExist();临时文件是否存在来判断上一次是否正常退出,根据这个状态来选择什么恢复策略
-接下去是初始化 Netty 服务端,初始化发送消息线程池(sendMessageExecutor)、拉取消息线程池(pullMessageExecutor)、管理Broker线程池(adminBrokerExecutor)、客户端管理线程池(clientManageExecutor),注册事件处理器,包括发送消息事件处理器(SendMessageProcessor)、拉取消息事件处理器、查询消息事件处理器(QueryMessageProcessor,包括客户端的心跳事件、注销事件、获取消费者列表事件、更新更新和查询消费进度consumerOffset)、客户端管理事件处理器(ClientManageProcessor)、结束事务处理器(EndTransactionProcessor)、默认事件处理器(AdminBrokerProcessor),然后是定时任务
-BrokerController.this.getBrokerStats().record(); 记录 Broker 状态
-BrokerController.this.consumerOffsetManager.persist(); 持久化consumerOffset
-BrokerController.this.consumerFilterManager.persist();持久化consumerFilter
-BrokerController.this.protectBroker(); 保护 broker,消费慢,不让继续投递
-BrokerController.this.printWaterMark(); 打印水位
-log.info("dispatch behind commit log {} bytes", BrokerController.this.getMessageStore().dispatchBehindBytes()); 检查落后程度
-BrokerController.this.brokerOuterAPI.fetchNameServerAddr(); 定时获取 nameserver
-BrokerController.this.printMasterAndSlaveDiff(); 打印主从不一致
-然后是 tsl,初始化事务消息,初始化 RPCHook
-请把害怕打到公屏上🤦♂️,从线程池名字和调用的方法应该可以看出大部分的用途
-this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
- NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone();
- fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2);
- this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService);
- this.sendMessageExecutor = new BrokerFixedThreadPoolExecutor(
- this.brokerConfig.getSendMessageThreadPoolNums(),
- this.brokerConfig.getSendMessageThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.sendThreadPoolQueue,
- new ThreadFactoryImpl("SendMessageThread_"));
-
- this.pullMessageExecutor = new BrokerFixedThreadPoolExecutor(
- this.brokerConfig.getPullMessageThreadPoolNums(),
- this.brokerConfig.getPullMessageThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.pullThreadPoolQueue,
- new ThreadFactoryImpl("PullMessageThread_"));
-
- this.replyMessageExecutor = new BrokerFixedThreadPoolExecutor(
- this.brokerConfig.getProcessReplyMessageThreadPoolNums(),
- this.brokerConfig.getProcessReplyMessageThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.replyThreadPoolQueue,
- new ThreadFactoryImpl("ProcessReplyMessageThread_"));
-
- this.queryMessageExecutor = new BrokerFixedThreadPoolExecutor(
- this.brokerConfig.getQueryMessageThreadPoolNums(),
- this.brokerConfig.getQueryMessageThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.queryThreadPoolQueue,
- new ThreadFactoryImpl("QueryMessageThread_"));
-
- this.adminBrokerExecutor =
- Executors.newFixedThreadPool(this.brokerConfig.getAdminBrokerThreadPoolNums(), new ThreadFactoryImpl(
- "AdminBrokerThread_"));
-
- this.clientManageExecutor = new ThreadPoolExecutor(
- this.brokerConfig.getClientManageThreadPoolNums(),
- this.brokerConfig.getClientManageThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.clientManagerThreadPoolQueue,
- new ThreadFactoryImpl("ClientManageThread_"));
-
- this.heartbeatExecutor = new BrokerFixedThreadPoolExecutor(
- this.brokerConfig.getHeartbeatThreadPoolNums(),
- this.brokerConfig.getHeartbeatThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.heartbeatThreadPoolQueue,
- new ThreadFactoryImpl("HeartbeatThread_", true));
-
- this.endTransactionExecutor = new BrokerFixedThreadPoolExecutor(
- this.brokerConfig.getEndTransactionThreadPoolNums(),
- this.brokerConfig.getEndTransactionThreadPoolNums(),
- 1000 * 60,
- TimeUnit.MILLISECONDS,
- this.endTransactionThreadPoolQueue,
- new ThreadFactoryImpl("EndTransactionThread_"));
-
- this.consumerManageExecutor =
- Executors.newFixedThreadPool(this.brokerConfig.getConsumerManageThreadPoolNums(), new ThreadFactoryImpl(
- "ConsumerManageThread_"));
-
- this.registerProcessor();
-
- final long initialDelay = UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis();
- final long period = 1000 * 60 * 60 * 24;
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- try {
- BrokerController.this.getBrokerStats().record();
- } catch (Throwable e) {
- log.error("schedule record error.", e);
- }
- }
- }, initialDelay, period, TimeUnit.MILLISECONDS);
-
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- try {
- BrokerController.this.consumerOffsetManager.persist();
- } catch (Throwable e) {
- log.error("schedule persist consumerOffset error.", e);
- }
- }
- }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- try {
- BrokerController.this.consumerFilterManager.persist();
- } catch (Throwable e) {
- log.error("schedule persist consumer filter error.", e);
+ }
+它负责加载用户类路径(ClassPath)上所有的类库,我们可以直接在代码中使用这个类加载器,如果我们的代码中没有自定义的类在加载器,一般情况下这个就是程序中默认的类加载器
+双亲委派模型
![]()
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试家在这个类,而是把这个请求为派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的家在请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起举杯了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要家在这个类,最终都是委派给处于模型最顶层的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双薪委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
可以来看下双亲委派模型的代码实现
+/**
+ * Loads the class with the specified <a href="#name">binary name</a>. The
+ * default implementation of this method searches for classes in the
+ * following order:
+ *
+ * <ol>
+ *
+ * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
+ * has already been loaded. </p></li>
+ *
+ * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
+ * on the parent class loader. If the parent is <tt>null</tt> the class
+ * loader built-in to the virtual machine is used, instead. </p></li>
+ *
+ * <li><p> Invoke the {@link #findClass(String)} method to find the
+ * class. </p></li>
+ *
+ * </ol>
+ *
+ * <p> If the class was found using the above steps, and the
+ * <tt>resolve</tt> flag is true, this method will then invoke the {@link
+ * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
+ *
+ * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
+ * #findClass(String)}, rather than this method. </p>
+ *
+ * <p> Unless overridden, this method synchronizes on the result of
+ * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
+ * during the entire class loading process.
+ *
+ * @param name
+ * The <a href="#name">binary name</a> of the class
+ *
+ * @param resolve
+ * If <tt>true</tt> then resolve the class
+ *
+ * @return The resulting <tt>Class</tt> object
+ *
+ * @throws ClassNotFoundException
+ * If the class could not be found
+ */
+ protected Class<?> loadClass(String name, boolean resolve)
+ throws ClassNotFoundException
+ {
+ synchronized (getClassLoadingLock(name)) {
+ // First, check if the class has already been loaded
+ Class<?> c = findLoadedClass(name);
+ if (c == null) {
+ long t0 = System.nanoTime();
+ try {
+ if (parent != null) {
+ // 委托父类加载
+ c = parent.loadClass(name, false);
+ } else {
+ // 使用启动类加载器
+ c = findBootstrapClassOrNull(name);
}
+ } catch (ClassNotFoundException e) {
+ // ClassNotFoundException thrown if class not found
+ // from the non-null parent class loader
}
- }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- try {
- BrokerController.this.protectBroker();
- } catch (Throwable e) {
- log.error("protectBroker error.", e);
- }
- }
- }, 3, 3, TimeUnit.MINUTES);
+ if (c == null) {
+ // If still not found, then invoke findClass in order
+ // to find the class.
+ long t1 = System.nanoTime();
+ // 调用自己的 findClass() 方法尝试进行加载
+ c = findClass(name);
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- try {
- BrokerController.this.printWaterMark();
- } catch (Throwable e) {
- log.error("printWaterMark error.", e);
- }
+ // this is the defining class loader; record the stats
+ sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
+ sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
+ sun.misc.PerfCounter.getFindClasses().increment();
}
- }, 10, 1, TimeUnit.SECONDS);
-
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
-
- @Override
- public void run() {
- try {
- log.info("dispatch behind commit log {} bytes", BrokerController.this.getMessageStore().dispatchBehindBytes());
- } catch (Throwable e) {
- log.error("schedule dispatchBehindBytes error.", e);
- }
- }
- }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
-
- if (this.brokerConfig.getNamesrvAddr() != null) {
- this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
- log.info("Set user specified name server address: {}", this.brokerConfig.getNamesrvAddr());
- } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
-
- @Override
- public void run() {
- try {
- BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
- } catch (Throwable e) {
- log.error("ScheduledTask fetchNameServerAddr exception", e);
- }
- }
- }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
- }
-
- if (!messageStoreConfig.isEnableDLegerCommitLog()) {
- if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
- if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= 6) {
- this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress());
- this.updateMasterHAServerAddrPeriodically = false;
- } else {
- this.updateMasterHAServerAddrPeriodically = true;
- }
- } else {
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- try {
- BrokerController.this.printMasterAndSlaveDiff();
- } catch (Throwable e) {
- log.error("schedule printMasterAndSlaveDiff error.", e);
- }
- }
- }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
- }
- }
-
- 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();
- ((NettyRemotingServer) fastRemotingServer).loadSslContext();
- }
- });
- } catch (Exception e) {
- log.warn("FileWatchService created error, can't load the certificate dynamically");
- }
- }
- initialTransaction();
- initialAcl();
- initialRpcHooks();
- }
- return result;
-
-
-
-Broker 启动过程
-贴代码
-public void start() throws Exception {
- if (this.messageStore != null) {
- this.messageStore.start();
- }
-
- if (this.remotingServer != null) {
- this.remotingServer.start();
- }
-
- if (this.fastRemotingServer != null) {
- this.fastRemotingServer.start();
- }
-
- if (this.fileWatchService != null) {
- this.fileWatchService.start();
- }
-
- if (this.brokerOuterAPI != null) {
- this.brokerOuterAPI.start();
- }
-
- if (this.pullRequestHoldService != null) {
- this.pullRequestHoldService.start();
- }
-
- if (this.clientHousekeepingService != null) {
- this.clientHousekeepingService.start();
- }
-
- if (this.filterServerManager != null) {
- this.filterServerManager.start();
- }
-
- if (!messageStoreConfig.isEnableDLegerCommitLog()) {
- startProcessorByHa(messageStoreConfig.getBrokerRole());
- handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
- this.registerBrokerAll(true, false, true);
- }
-
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
-
- @Override
- public void run() {
- try {
- BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
- } catch (Throwable e) {
- log.error("registerBrokerAll Exception", e);
- }
- }
- }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
-
- if (this.brokerStatsManager != null) {
- this.brokerStatsManager.start();
- }
-
- if (this.brokerFastFailure != null) {
- this.brokerFastFailure.start();
- }
-
-
- }
-
-首先是启动messageStore,调用 start 方法,这里面又调用了一些代码
-public void start() throws Exception {
-
- lock = lockFile.getChannel().tryLock(0, 1, false);
- if (lock == null || lock.isShared() || !lock.isValid()) {
- throw new RuntimeException("Lock failed,MQ already started");
- }
-
- lockFile.getChannel().write(ByteBuffer.wrap("lock".getBytes()));
- lockFile.getChannel().force(true);
- {
- /**
- * 1. Make sure the fast-forward messages to be truncated during the recovering according to the max physical offset of the commitlog;
- * 2. DLedger committedPos may be missing, so the maxPhysicalPosInLogicQueue maybe bigger that maxOffset returned by DLedgerCommitLog, just let it go;
- * 3. Calculate the reput offset according to the consume queue;
- * 4. Make sure the fall-behind messages to be dispatched before starting the commitlog, especially when the broker role are automatically changed.
- */
- long maxPhysicalPosInLogicQueue = commitLog.getMinOffset();
- for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
- for (ConsumeQueue logic : maps.values()) {
- if (logic.getMaxPhysicOffset() > maxPhysicalPosInLogicQueue) {
- maxPhysicalPosInLogicQueue = logic.getMaxPhysicOffset();
- }
- }
- }
- if (maxPhysicalPosInLogicQueue < 0) {
- maxPhysicalPosInLogicQueue = 0;
- }
- if (maxPhysicalPosInLogicQueue < this.commitLog.getMinOffset()) {
- maxPhysicalPosInLogicQueue = this.commitLog.getMinOffset();
- /**
- * This happens in following conditions:
- * 1. If someone removes all the consumequeue files or the disk get damaged.
- * 2. Launch a new broker, and copy the commitlog from other brokers.
- *
- * All the conditions has the same in common that the maxPhysicalPosInLogicQueue should be 0.
- * If the maxPhysicalPosInLogicQueue is gt 0, there maybe something wrong.
- */
- log.warn("[TooSmallCqOffset] maxPhysicalPosInLogicQueue={} clMinOffset={}", maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset());
}
- log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
- maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
- this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
- this.reputMessageService.start();
-
- /**
- * 1. Finish dispatching the messages fall behind, then to start other services.
- * 2. DLedger committedPos may be missing, so here just require dispatchBehindBytes <= 0
- */
- while (true) {
- if (dispatchBehindBytes() <= 0) {
- break;
- }
- Thread.sleep(1000);
- log.info("Try to finish doing reput the messages fall behind during the starting, reputOffset={} maxOffset={} behind={}", this.reputMessageService.getReputFromOffset(), this.getMaxPhyOffset(), this.dispatchBehindBytes());
+ if (resolve) {
+ resolveClass(c);
}
- this.recoverTopicQueueTable();
- }
-
- if (!messageStoreConfig.isEnableDLegerCommitLog()) {
- this.haService.start();
- this.handleScheduleMessageService(messageStoreConfig.getBrokerRole());
+ return c;
}
-
- this.flushConsumeQueueService.start();
- this.commitLog.start();
- this.storeStatsService.start();
-
- this.createTempFile();
- this.addScheduleTask();
- this.shutdown = false;
- }
-
-
-
-调用DefaultMessageStore.start方法启动DefaultMessageStore对象中的一些服务线程。
-
-- 启动ReputMessageService服务线程
-- 启动FlushConsumeQueueService服务线程;
-- 调用CommitLog.start方法,启动CommitLog对象中的FlushCommitLogService线程服务,若是同步刷盘(SYNC_FLUSH)则是启动GroupCommitService线程服务;若是异步刷盘(ASYNC_FLUSH)则是启动FlushRealTimeService线程服务;
-- 启动StoreStatsService线程服务;
-- 启动定时清理任务
-
-然后是启动ClientHousekeepingService的 netty 服务端和客户端,然后是启动fileWatchService证书服务,接着启动BrokerOuterAPI中的NettyRemotingClient,即建立与NameServer的链接,用于自身Broker与其他模块的RPC功能调用;包括获取NameServer的地址、注册Broker、注销Broker、获取Topic配置、获取消息进度信息、获取订阅关系等RPC功能,然后是PullRequestHoldService服务线程,这个就是实现长轮询的,然后启动管家ClientHousekeepingService服务,负责扫描不活跃的生产者,消费者和 filter,启动FilterServerManager 过滤器服务管理,然后启动定时任务调用org.apache.rocketmq.broker.BrokerController#registerBrokerAll向所有 nameserver 注册 broker,最后是按需开启org.apache.rocketmq.store.stats.BrokerStatsManager和org.apache.rocketmq.broker.latency.BrokerFastFailure,基本上启动过程就完成了
+ }
+破坏双亲委派
关于破坏双亲委派模型,第一次是在 JDK1.2 之后引入了双亲委派模型之前,那么在那之前已经有了类加载器,所以java.lang.ClassLoader 中添加了一个 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。这个跟上面的逻辑其实类似,当父类加载失败,会调用 findClass()来完成加载;第二次是因为这个模型本身还有一些不足之处,比如 SPI 这种,所以有设计了线程下上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 java.lang.Thread#setContextClassLoader() 进行设置,然后第三种是为了追求程序动态性,这里有涉及到了 osgi 等概念,就不展开了
]]>
- MQ
- RocketMQ
- 消息队列
- RocketMQ
- 中间件
- RocketMQ
+ Java
- MQ
- 消息队列
- RocketMQ
- 削峰填谷
- 中间件
- 源码解析
- Broker
+ Java
+ 类加载
+ 加载
+ 验证
+ 准备
+ 解析
+ 初始化
+ 链接
+ 双亲委派
- 聊聊 Sharding-Jdbc 分库分表下的分页方案
- /2022/01/09/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E4%B8%8B%E7%9A%84%E5%88%86%E9%A1%B5%E6%96%B9%E6%A1%88/
- 前面在聊 Sharding-Jdbc 的时候看到了一篇文章,关于一个分页的查询,一直比较直接的想法就是分库分表下的分页是非常不合理的,一般我们的实操方案都是分表加上 ES 搜索做分页,或者通过合表读写分离的方案,因为对于 sharding-jdbc 如果没有带分表键,查询基本都是只能在所有分表都执行一遍,然后再加上分页,基本上是分页越大后续的查询越耗资源,但是仔细的去想这个细节还是这次,就简单说说
首先就是我的分表结构
-CREATE TABLE `student_time_0` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `user_id` int(11) NOT NULL,
- `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
- `age` tinyint(3) unsigned DEFAULT NULL,
- `create_time` bigint(20) DEFAULT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=674 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
-有这样的三个表,student_time_0, student_time_1, student_time_2, 以 user_id 作为分表键,根据表数量取模作为分表依据
这里先构造点数据,
-insert into student_time (`name`, `user_id`, `age`, `create_time`) values (?, ?, ?, ?)
-主要是为了保证 create_time 唯一比较好说明问题,
-int i = 0;
-try (
- Connection conn = dataSource.getConnection();
- PreparedStatement ps = conn.prepareStatement(insertSql)) {
- do {
- ps.setString(1, localName + new Random().nextInt(100));
- ps.setLong(2, 10086L + (new Random().nextInt(100)));
- ps.setInt(3, 18);
- ps.setLong(4, new Date().getTime());
-
-
- int result = ps.executeUpdate();
- LOGGER.info("current execute result: {}", result);
- Thread.sleep(new Random().nextInt(100));
- i++;
- } while (i <= 2000);
-三个表的数据分别是 673,678,650,说明符合预期了,各个表数据不一样,接下来比如我们想要做一个这样的分页查询
-select * from student_time ORDER BY create_time ASC limit 1000, 5;
-student_time 对于我们使用的 sharding-jdbc 来说当然是逻辑表,首先从一无所知去想这个查询如果我们自己来处理应该是怎么做,
首先是不是可以每个表都从 333 开始取 5 条数据,类似于下面的查询,然后进行 15 条的合并重排序获取前面的 5 条
-select * from student_time_0 ORDER BY create_time ASC limit 333, 5;
-select * from student_time_1 ORDER BY create_time ASC limit 333, 5;
-select * from student_time_2 ORDER BY create_time ASC limit 333, 5;
-忽略前面 limit 差的 1,这个结果除非三个表的分布是绝对的均匀,否则结果肯定会出现一定的偏差,以为每个表的 333 这个位置对于其他表来说都不一定是一样的,这样对于最后整体的结果,就会出现偏差
因为一直在纠结怎么让这个更直观的表现出来,所以尝试画了个图
![]()
黑色的框代表我从每个表里按排序从 334 到 338 的 5 条数据, 他们在每个表里都是代表了各自正确的排序值,但是对于我们想要的其实是合表后的 1001,1005 这五条,然后我们假设总的排序值位于前 1000 的分布是第 0 个表是 320 条,第 1 个表是 340 条,第 2 个表是 340 条,那么可以明显地看出来我这么查的结果简单合并肯定是不对的。
那么 sharding-jdbc 是如何保证这个结果的呢,其实就是我在每个表里都查分页偏移量和分页大小那么多的数据,在我这个例子里就是对于 0,1,2 三个分表每个都查 1005 条数据,即使我的数据不平衡到最极端的情况,前 1005 条数据都出在某个分表中,也可以正确获得最后的结果,但是明显的问题就是大分页,数据较多,就会导致非常大的问题,即使如 sharding-jdbc 对于合并排序的优化做得比较好,也还是需要传输那么大量的数据,并且查询也耗时,那么有没有解决方案呢,应该说有两个,或者说主要是想讲后者
第一个办法是像这种查询,如果业务上不需要进行跳页,而是只给下一页,那么我们就能把前一次的最大偏移量的 create_time 记录下来,下一页就可以拿着这个偏移量进行查询,这个比较简单易懂,就不多说了
第二个办法是看的58 沈剑的一篇文章,尝试理解讲述一下,
这个办法的第一步跟前面那个错误的方法或者说不准确的方法一样,先是将分页偏移量平均后在三个表里进行查询
-t0
-334 10158 nick95 18 1641548941767
-335 10098 nick11 18 1641548941879
-336 10167 nick51 18 1641548942089
-337 10167 nick3 18 1641548942119
-338 10170 nick57 18 1641548942169
-
-
-t1
-334 10105 nick98 18 1641548939071 最小
-335 10174 nick94 18 1641548939377
-336 10129 nick85 18 1641548939442
-337 10141 nick84 18 1641548939480
-338 10096 nick74 18 1641548939668
-
-t2
-334 10184 nick11 18 1641548945075
-335 10109 nick93 18 1641548945382
-336 10181 nick41 18 1641548945583
-337 10130 nick80 18 1641548945993
-338 10184 nick19 18 1641548946294 最大
-然后要做什么呢,其实目标比较明白,因为前面那种方法其实就是我知道了前一页的偏移量,所以可以直接当做条件来进行查询,那这里我也想着拿到这个条件,所以我将第一遍查出来的最小的 create_time 和最大的 create_time 找出来,然后再去三个表里查询,其实主要是最小值,因为我拿着最小值去查以后我就能知道这个最小值在每个表里处在什么位置,
-t0
-322 10161 nick81 18 1641548939284
-323 10113 nick16 18 1641548939393
-324 10110 nick56 18 1641548939577
-325 10116 nick69 18 1641548939588
-326 10173 nick51 18 1641548939646
-
-t1
-334 10105 nick98 18 1641548939071
-335 10174 nick94 18 1641548939377
-336 10129 nick85 18 1641548939442
-337 10141 nick84 18 1641548939480
-338 10096 nick74 18 1641548939668
-
-t2
-297 10136 nick28 18 1641548939161
-298 10142 nick68 18 1641548939177
-299 10124 nick41 18 1641548939237
-300 10148 nick87 18 1641548939510
-301 10169 nick23 18 1641548939715
-我只贴了前五条数据,为了方便知道偏移量,每个分表都使用了自增主键,我们可以看到前一次查询的最小值分别在其他两个表里的位置分别是 322-1 和 297-1,那么对于总体来说这个时间应该是在 322 - 1 + 333 + 297 - 1 = 951,那这样子我只要对后面的数据最多每个表查 1000 - 951 + 5 = 54 条数据再进行合并排序就可以获得最终正确的结果。
这个就是传说中的二次查询法。
+ 聊聊 dubbo 的线程池
+ /2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/
+ 之前没注意到这一块,只是比较模糊的印象 dubbo 自己基于 ThreadPoolExecutor 定义了几个线程池,但是没具体看过,主要是觉得就是为了避免使用 jdk 自带的那几个(java.util.concurrent.Executors),防止出现那些问题
看下代码目录主要是这几个
+
+- FixedThreadPool:创建一个复用固定个数线程的线程池。
简单看下代码public Executor getExecutor(URL url) {
+ String name = url.getParameter("threadname", "Dubbo");
+ int threads = url.getParameter("threads", 200);
+ int queues = url.getParameter("queues", 0);
+ return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
+ }
+可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
+- LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
public Executor getExecutor(URL url) {
+ String name = url.getParameter("threadname", "Dubbo");
+ int cores = url.getParameter("corethreads", 0);
+ int threads = url.getParameter("threads", 200);
+ int queues = url.getParameter("queues", 0);
+ return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
+ }
+这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
+- EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
public Executor getExecutor(URL url) {
+ String name = url.getParameter("threadname", "Dubbo");
+ int cores = url.getParameter("corethreads", 0);
+ int threads = url.getParameter("threads", 2147483647);
+ int queues = url.getParameter("queues", 0);
+ int alive = url.getParameter("alive", 60000);
+ TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
+ EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
+ taskQueue.setExecutor(executor);
+ return executor;
+ }
+这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
+- CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
public Executor getExecutor(URL url) {
+ String name = url.getParameter("threadname", "Dubbo");
+ int cores = url.getParameter("corethreads", 0);
+ int threads = url.getParameter("threads", 2147483647);
+ int queues = url.getParameter("queues", 0);
+ int alive = url.getParameter("alive", 60000);
+ return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
+ }
+这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
只是非常简略的介绍下,有兴趣可以自行阅读代码。
+
]]>
Java
+ Dubbo - 线程池
+ Dubbo
+ 线程池
+ ThreadPool
Java
- Sharding-Jdbc
+ Dubbo
+ ThreadPool
+ 线程池
+ FixedThreadPool
+ LimitedThreadPool
+ EagerThreadPool
+ CachedThreadPool
@@ -18408,69 +17794,33 @@ void ReadView::prepare(trx_id_t id) {
- 聊聊 mysql 的 MVCC
- /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/
- 很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢
-其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行
-腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下
-/* Precise data types for system columns and the length of those columns;
-NOTE: the values must run from 0 up in the order given! All codes must
-be less than 256 */
-#define DATA_ROW_ID 0 /* row id: a 48-bit integer */
-#define DATA_ROW_ID_LEN 6 /* stored length for row id */
-
-/** Transaction id: 6 bytes */
-constexpr size_t DATA_TRX_ID = 1;
-
-/** Transaction ID type size in bytes. */
-constexpr size_t DATA_TRX_ID_LEN = 6;
-
-/** Rollback data pointer: 7 bytes */
-constexpr size_t DATA_ROLL_PTR = 2;
-
-/** Rollback data pointer type size in bytes. */
-constexpr size_t DATA_ROLL_PTR_LEN = 7;
-
-一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了
-一个是 DATA_TRX_ID,这个表示这条记录的事务 ID
-还有一个是 DATA_ROLL_PTR 指向回滚段的指针
-指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,
-m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id
-m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id
-m_ids, 这个是 read view 创建时所有的活跃事务 id 数组
-m_creator_trx_id 这个是当前记录的创建事务 id
-判断事务的可见性主要的逻辑是这样,
-
-- 当记录的事务
id 小于最小活跃事务 id,说明是可见的,
-- 如果记录的事务
id 等于当前事务 id,说明是自己的更改,可见
-- 如果记录的事务
id 大于最大的活跃事务 id, 不可见
-- 如果记录的事务
id 介于 m_low_limit_id 和 m_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
具体的代码捞一下看看/** Check whether the changes by id are visible.
- @param[in] id transaction id to check against the view
- @param[in] name table name
- @return whether the view sees the modifications of id. */
- bool changes_visible(trx_id_t id, const table_name_t &name) const
- MY_ATTRIBUTE((warn_unused_result)) {
- ut_ad(id > 0);
-
- if (id < m_up_limit_id || id == m_creator_trx_id) {
- return (true);
- }
-
- check_trx_id_sanity(id, name);
-
- if (id >= m_low_limit_id) {
- return (false);
-
- } else if (m_ids.empty()) {
- return (true);
- }
-
- const ids_t::value_type *p = m_ids.data();
-
- return (!std::binary_search(p, p + m_ids.size(), id));
- }
-剩下来一点是啥呢,就是 Read Committed 和 Repeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据
-
+ 聊一下关于怎么陪伴学习
+ /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还是很害怕,车速还只有十几的时候,还很远的对面来车的时候就觉得很慌了,这个时候如果以常理肯定会说这样子完全不用怕,如果克服恐惧真的这么容易的话,问题就不会那么纠结了,人生是很难完全感同身受的,唯有降低预设的基准让事情从头理清楚,害怕了我们就先休息,有车了我们就停下,先适应完全没车的情况,变得更慢一点,如果这时候着急一点,反而会起到反效果,比如只是说不要怕,接着开,甚至有点厌烦了,那基本这个练车也不太成得了了,而正好是有耐心的一起慢慢练习,还有就是第二件是切身体会,就是当道路本来是两条道,但是封了一条的时候,这时候开车如果是像我这样的新手,如果开车时左右边看着的话,车肯定开不好,因为那样会一直左右调整,反而更容易控制不好左右的距离,蹭到旁边的隔离栏,正确的方式应该是专注于正前方的路,这样才能保证左右边距离尽可能均匀,而不是顾左失右或者顾右失左,所以很多陪伴学习需要注意的是方式和耐心,能够识别到关键点那是最好的,但是有时候更需要的是耐心,纯靠耐心不一定能解决问题,但是可能会找到问题关键点。
+]]>
+
+ 生活
+
+
+ 生活
+
+
+
+ 聊聊 mysql 的 MVCC 续续篇之锁分析
+ /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/
+ 看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档
+
+SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.
+
+纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类
+
+select * from table where ? lock in share mode;
+select * from table where ? for update;
+insert into table values (...);
+update table set ? where ?;
+delete from table where ?;
+
+除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了
![]()
其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了
]]>
Mysql
@@ -18485,26 +17835,94 @@ constexpr size_t DATA_ROLL_PTR_LEN 源码
mvcc
read view
+ gap lock
+ next-key lock
+ 幻读
- 聊聊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();}
-}
+ 聊聊 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
+
+
+
+ 聊聊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() 这条路,将构造方法变成私有
@@ -18594,753 +18012,1302 @@ constexpr size_t DATA_ROLL_PTR_LEN
- 聊聊 mysql 的 MVCC 续续篇之锁分析
- /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/
- 看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
看一段 mysql官方文档
-
-SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.
-
-纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类
-
-select * from table where ? lock in share mode;
-select * from table where ? for update;
-insert into table values (...);
-update table set ? where ?;
-delete from table where ?;
-
-除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段
select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了
![]()
其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了
-]]>
-
- Mysql
- C
- 数据结构
- 源码
- Mysql
-
-
- mysql
- 数据结构
- 源码
- mvcc
- read view
- gap lock
- next-key lock
- 幻读
-
-
-
- 聊聊 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
-
-
-
- 聊聊一次 brew update 引发的血案
- /2020/06/13/%E8%81%8A%E8%81%8A%E4%B8%80%E6%AC%A1-brew-update-%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/
- 熟悉我的人(谁熟悉你啊🙄)知道我以前写过 PHP,虽然现在在工作中没用到了,但是自己的一些小工具还是会用 PHP 来写,但是在 Mac 碰到了一个环境相关的问题,因为我也是个更新狂魔,用了 brew 之后因为 gfw 的原因,如果长时间不更新,有时候要装一个用它装一个软件的话,前置的更新耗时就会让人非常头大,所以我基本会隔天 update 一下,但是这样会带来一个很心烦的问题,就是像这样,因为我是要用一个固定版本的 PHP,如果一直升需要一直配扩展啥的也很麻烦,如果一直升级 PHP 到最新版可能会比较少碰到这个问题
-dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
-这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下
-
-ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。
-
-然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
第一步,我们到 brew的目录下
-cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
-这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
第二步看看它的提交历史
-git log --follow icu4c.rb
-在 git log 的海洋中寻找,寻找它的(64版本)的身影
![]()
第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支
-git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
-其实注意 commit id 旁边的红框,这个是有tag 的,可以直接
-git checkout icu4c-64
-PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本
-brew reinstall ./icu4c.rb
-然后就是第五步,切换版本
-brew switch icu4c 64.2
-最后把分支切回来
-git checkout master
-是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法
-# zsh
-function hiicu64() {
- local last_dir=$(pwd)
+ 聊聊 RocketMQ 的 Broker 源码
+ /2020/07/19/%E8%81%8A%E8%81%8A-RocketMQ-%E7%9A%84-Broker-%E6%BA%90%E7%A0%81/
+ broker 的启动形式有点类似于 NameServer,都是服务类型的,跟 Consumer 差别比较大,
+首先是org.apache.rocketmq.broker.BrokerStartup中的 main 函数,org.apache.rocketmq.broker.BrokerStartup#createBrokerController基本就是读取参数,这里差点把最核心的初始化给漏了,
+final BrokerController controller = new BrokerController(
+ brokerConfig,
+ nettyServerConfig,
+ nettyClientConfig,
+ messageStoreConfig);
+ // remember all configs to prevent discard
+ controller.getConfiguration().registerConfig(properties);
- 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
+ boolean initResult = controller.initialize();
- cd $last_dir
-}
-对应自己的版本改改版本号就可以了,非常好用。
-]]>
-
- Mac
- PHP
- Homebrew
- PHP
- icu4c
-
-
- Mac
- PHP
- Homebrew
- icu4c
- zsh
-
-
-
- 聊聊 SpringBoot 自动装配
- /2021/07/11/%E8%81%8A%E8%81%8ASpringBoot-%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/
- springboot 自动装配调用链
-springboot 相比 spring能更方便开发人员上手,比较重要的一点就是自动装配,大致来看下这个逻辑
-public static void main(String[] args) {
- SpringApplication.run(SpbDemoApplication.class, args);
- }
+前面是以 broker 配置,netty 的服务端和客户端配置,以及消息存储配置在实例化 BrokerController,然后就是初始化了
+public boolean initialize() throws CloneNotSupportedException {
+ boolean result = this.topicConfigManager.load();
- /**
- * Static helper that can be used to run a {@link SpringApplication} from the
- * specified source using default settings.
- * @param primarySource the primary source to load
- * @param args the application arguments (usually passed from a Java main method)
- * @return the running {@link ApplicationContext}
- */
-然后就是上面调用的 run 方法
-public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
- return run(new Class<?>[] { primarySource }, args);
+ result = result && this.consumerOffsetManager.load();
+ result = result && this.subscriptionGroupManager.load();
+ result = result && this.consumerFilterManager.load();
+
+前面这些就是各个配置的 load 了,然后是个我认为比较重要的部分messageStore 的实例化,
+if (result) {
+ try {
+ this.messageStore =
+ new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener,
+ this.brokerConfig);
+ if (messageStoreConfig.isEnableDLegerCommitLog()) {
+ DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
+ ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
+ }
+ this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore);
+ //load plugin
+ MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig);
+ this.messageStore = MessageStoreFactory.build(context, this.messageStore);
+ this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager));
+ } catch (IOException e) {
+ result = false;
+ log.error("Failed to initialize", e);
+ }
}
-/**
- * Static helper that can be used to run a {@link SpringApplication} from the
- * specified sources using default settings and user supplied arguments.
- * @param primarySources the primary sources to load
- * @param args the application arguments (usually passed from a Java main method)
- * @return the running {@link ApplicationContext}
- */
-继续往下看
-public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
- return new SpringApplication(primarySources).run(args);
-}
-
-调用SpringApplication的构造方法
-/**
- * Create a new {@link SpringApplication} instance. The application context will load
- * beans from the specified primary sources (see {@link SpringApplication class-level}
- * documentation for details. The instance can be customized before calling
- * {@link #run(String...)}.
- * @param primarySources the primary bean sources
- * @see #run(Class, String[])
- * @see #SpringApplication(ResourceLoader, Class...)
- * @see #setSources(Set)
- */
+result = result && this.messageStore.load();
-public SpringApplication(Class<?>... primarySources) {
- this(null, primarySources);
-}
+先是实例化,实例化构造函数里的代码比较重要,重点看一下
+public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager,
+ final MessageArrivingListener messageArrivingListener, final BrokerConfig brokerConfig) throws IOException {
+ this.messageArrivingListener = messageArrivingListener;
+ this.brokerConfig = brokerConfig;
+ this.messageStoreConfig = messageStoreConfig;
+ this.brokerStatsManager = brokerStatsManager;
+ this.allocateMappedFileService = new AllocateMappedFileService(this);
+ if (messageStoreConfig.isEnableDLegerCommitLog()) {
+ this.commitLog = new DLedgerCommitLog(this);
+ } else {
+ this.commitLog = new CommitLog(this);
+ }
+ this.consumeQueueTable = new ConcurrentHashMap<>(32);
-/**
- * Create a new {@link SpringApplication} instance. The application context will load
- * beans from the specified primary sources (see {@link SpringApplication class-level}
- * documentation for details. The instance can be customized before calling
- * {@link #run(String...)}.
- * @param resourceLoader the resource loader to use
- * @param primarySources the primary bean sources
- * @see #run(Class, String[])
- * @see #setSources(Set)
- */
-@SuppressWarnings({ "unchecked", "rawtypes" })
-public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
- this.resourceLoader = resourceLoader;
- Assert.notNull(primarySources, "PrimarySources must not be null");
- this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
- this.webApplicationType = WebApplicationType.deduceFromClasspath();
- // 注意看这里的,通过 SpringFactories 获取
- this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
- setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
- setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
- this.mainApplicationClass = deduceMainApplicationClass();
-}
-这里就是重点了
-private List<BootstrapRegistryInitializer> getBootstrapRegistryInitializersFromSpringFactories() {
- ArrayList<BootstrapRegistryInitializer> initializers = new ArrayList<>();
- getSpringFactoriesInstances(Bootstrapper.class).stream()
- .map((bootstrapper) -> ((BootstrapRegistryInitializer) bootstrapper::initialize))
- .forEach(initializers::add);
- initializers.addAll(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
- return initializers;
-}
-private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
- return getSpringFactoriesInstances(type, new Class<?>[] {});
-}
+ this.flushConsumeQueueService = new FlushConsumeQueueService();
+ this.cleanCommitLogService = new CleanCommitLogService();
+ this.cleanConsumeQueueService = new CleanConsumeQueueService();
+ this.storeStatsService = new StoreStatsService();
+ this.indexService = new IndexService(this);
+ if (!messageStoreConfig.isEnableDLegerCommitLog()) {
+ this.haService = new HAService(this);
+ } else {
+ this.haService = null;
+ }
+ this.reputMessageService = new ReputMessageService();
-private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
- ClassLoader classLoader = getClassLoader();
- // Use names and ensure unique to protect against duplicates
- // 去加载所有FACTORIES_RESOURCE_LOCATION路径下面,也就是 META-INF/spring.factories
- Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
- List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
- AnnotationAwareOrderComparator.sort(instances);
- return instances;
-}
+ this.scheduleMessageService = new ScheduleMessageService(this);
-
-/**
- * Load the fully qualified class names of factory implementations of the
- * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
- * class loader.
- * <p>As of Spring Framework 5.3, if a particular implementation class name
- * is discovered more than once for the given factory type, duplicates will
- * be ignored.
- * @param factoryType the interface or abstract class representing the factory
- * @param classLoader the ClassLoader to use for loading resources; can be
- * {@code null} to use the default
- * @throws IllegalArgumentException if an error occurs while loading factory names
- * @see #loadFactories
- */
-public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
- ClassLoader classLoaderToUse = classLoader;
- if (classLoaderToUse == null) {
- classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
- }
- String factoryTypeName = factoryType.getName();
- return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
-}
- private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
- Map<String, List<String>> result = cache.get(classLoader);
- if (result != null) {
- return result;
- }
+ this.transientStorePool = new TransientStorePool(messageStoreConfig);
- result = new HashMap<>();
- try {
- // 获取此 resources,作为 AutoConfiguration 的配置
- Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
- while (urls.hasMoreElements()) {
- URL url = urls.nextElement();
- UrlResource resource = new UrlResource(url);
- Properties properties = PropertiesLoaderUtils.loadProperties(resource);
- for (Map.Entry<?, ?> entry : properties.entrySet()) {
- String factoryTypeName = ((String) entry.getKey()).trim();
- String[] factoryImplementationNames =
- StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
- for (String factoryImplementationName : factoryImplementationNames) {
- result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
- .add(factoryImplementationName.trim());
- }
- }
- }
+ if (messageStoreConfig.isTransientStorePoolEnable()) {
+ this.transientStorePool.init();
+ }
- // Replace all lists with unmodifiable lists containing unique elements
- result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
- .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
- cache.put(classLoader, result);
- }
- catch (IOException ex) {
- throw new IllegalArgumentException("Unable to load factories from location [" +
- FACTORIES_RESOURCE_LOCATION + "]", ex);
- }
- return result;
-}
+ this.allocateMappedFileService.start();
-我们可以看下 spring-boot-autoconfigure 的 META-INF/spring.factories
-# Initializers
-org.springframework.context.ApplicationContextInitializer=\
-org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
-org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
+ this.indexService.start();
-# Application Listeners
-org.springframework.context.ApplicationListener=\
-org.springframework.boot.autoconfigure.BackgroundPreinitializer
+ this.dispatcherList = new LinkedList<>();
+ this.dispatcherList.addLast(new CommitLogDispatcherBuildConsumeQueue());
+ this.dispatcherList.addLast(new CommitLogDispatcherBuildIndex());
-# Environment Post Processors
-org.springframework.boot.env.EnvironmentPostProcessor=\
-org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
+ File file = new File(StorePathConfigHelper.getLockFile(messageStoreConfig.getStorePathRootDir()));
+ MappedFile.ensureDirOK(file.getParent());
+ lockFile = new RandomAccessFile(file, "rw");
+ }
-# Auto Configuration Import Listeners
-org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
-org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
+这里面有很多类,不过先把从构造函数里传进来的忽略下,接下来就是 AllocateMappedFileService 这个service,前面看过文章的可能会根据上面的代码猜到,这也是个 ServiceThread,如果是对RocketMQ 有所了解的可能从名字可以看出这个类是关于 RocketMQ 消息怎么落盘的,当需要创建MappedFile时(在MapedFileQueue.getLastMapedFile方法中),向该线程的requestQueue队列中放入AllocateRequest请求对象,该线程会在后台监听该队列,并在后台创建MapedFile对象,即同时创建了物理文件。然后是创建了 IndexService 服务线程,用来给创建索引;还有是FlushConsumeQueueService是将ConsumeQueue 刷入磁盘;CleanCommitLogService用来清理过期的 CommitLog,默认是 72 小时以上;CleanConsumeQueueService是将小于最新的 CommitLog 偏移量的 ConsumeQueue 清理掉;StoreStatsService是储存统计服务;HAService用于CommitLog 的主备同步;ScheduleMessageService用于定时消息;还有就是这个ReputMessageService非常重要,就是由它实现了将 CommitLog 以 topic+queue 纬度构建 ConsumeQueue,后面TransientStorePool是异步刷盘时的存储buffer,也可以从后面的判断中看出来
+public boolean isTransientStorePoolEnable() {
+ return transientStorePoolEnable && FlushDiskType.ASYNC_FLUSH == getFlushDiskType()
+ && BrokerRole.SLAVE != getBrokerRole();
+ }
-# Auto Configuration Import Filters
-org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
-org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
-org.springframework.boot.autoconfigure.condition.OnClassCondition,\
-org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
+再然后就是启动两个服务线程,dispatcherList是为CommitLog文件转发请求,差不多这个初始化就这些内容。
+然后回到外层,下面是主备切换的配置,然后是数据统计,接着是存储插件加载,然后是往转发器链表里再加一个过滤器
+if (messageStoreConfig.isEnableDLegerCommitLog()) {
+ DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
+ ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
+ }
+ this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore);
+ //load plugin
+ MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig);
+ this.messageStore = MessageStoreFactory.build(context, this.messageStore);
+ this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager));
-# 注意这里,其实就是类似于 dubbo spi 的通过 org.springframework.boot.autoconfigure.EnableAutoConfiguration作为 key
-# 获取下面所有的 AutoConfiguration 配置类
-# Auto Configure
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
-org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
-org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
-org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
-org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
-org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
-org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
-org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
-org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
-org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
-org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
-org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
-org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
-org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
-org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
-org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
-org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
-org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
-org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
-org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
-org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
-org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
-org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
-org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
-org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
-org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
-org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
-org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
-org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
-org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
-org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
-org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
-org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
-org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
-org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
-org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
-org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
-org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
-org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
-org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
-org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
-org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
-org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
-org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
-org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
-org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
-org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
-org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
-org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
-org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
-org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
-org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
-org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\
-org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration,\
-org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
-org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
-org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
-org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
-org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
-org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
-org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
-org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
-org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
-org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
-org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
-org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
-org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\
-org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
-org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
-org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
-org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
-org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
-org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
-org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
-org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
-org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
-org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
-org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
-org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration
+接下来就是org.apache.rocketmq.store.MessageStore#load的过程了,
+
+- 调用ScheduleMessageService.load方法,初始化延迟级别列表。将这些级别(”1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”)的延时存入延迟级别delayLevelTable:ConcurrentHashMap<Integer /* level */, Long/* delay timeMillis */>变量中,例如1s的kv值为1:1000,5s的kv值为2:5000,key值依次类推;每个延迟级别即为一个队列。
+
+2)调用CommitLog.load方法,在此方法中调用MapedFileQueue.load方法,将$HOME /store/commitlog目录下的所有文件加载到MapedFileQueue的List变量中;
+3)调用DefaultMessageStore.loadConsumeQueue方法加载consumequeue文件数据到DefaultMessageStore.consumeQueueTable集合中。
+初始化StoreCheckPoint对象,加载$HOME/store/checkpoint文件,该文件记录三个字段值,分别是物理队列消息时间戳、逻辑队列消息时间戳、索引队列消息时间戳。
+调用IndexService.load方法加载$HOME/store/index目录下的文件。对该目录下的每个文件初始化一个IndexFile对象。然后调用IndexFile对象的load方法将IndexHeader加载到对象的变量中;再根据检查是否存在abort文件,若有存在abort文件,则表示Broker表示上次是异常退出的,则检查checkpoint的indexMsgTimestamp字段值是否小于IndexHeader的endTimestamp值,indexMsgTimestamp值表示最后刷盘的时间,若小于则表示在最后刷盘之后在该文件中还创建了索引,则要删除该Index文件,否则将该IndexFile对象放入indexFileList:ArrayList索引文件集合中。
+然后调用org.apache.rocketmq.store.DefaultMessageStore#recover恢复,前面有根据boolean lastExitOK = !this.isTempFileExist();临时文件是否存在来判断上一次是否正常退出,根据这个状态来选择什么恢复策略
+接下去是初始化 Netty 服务端,初始化发送消息线程池(sendMessageExecutor)、拉取消息线程池(pullMessageExecutor)、管理Broker线程池(adminBrokerExecutor)、客户端管理线程池(clientManageExecutor),注册事件处理器,包括发送消息事件处理器(SendMessageProcessor)、拉取消息事件处理器、查询消息事件处理器(QueryMessageProcessor,包括客户端的心跳事件、注销事件、获取消费者列表事件、更新更新和查询消费进度consumerOffset)、客户端管理事件处理器(ClientManageProcessor)、结束事务处理器(EndTransactionProcessor)、默认事件处理器(AdminBrokerProcessor),然后是定时任务
+BrokerController.this.getBrokerStats().record(); 记录 Broker 状态
+BrokerController.this.consumerOffsetManager.persist(); 持久化consumerOffset
+BrokerController.this.consumerFilterManager.persist();持久化consumerFilter
+BrokerController.this.protectBroker(); 保护 broker,消费慢,不让继续投递
+BrokerController.this.printWaterMark(); 打印水位
+log.info("dispatch behind commit log {} bytes", BrokerController.this.getMessageStore().dispatchBehindBytes()); 检查落后程度
+BrokerController.this.brokerOuterAPI.fetchNameServerAddr(); 定时获取 nameserver
+BrokerController.this.printMasterAndSlaveDiff(); 打印主从不一致
+然后是 tsl,初始化事务消息,初始化 RPCHook
+请把害怕打到公屏上🤦♂️,从线程池名字和调用的方法应该可以看出大部分的用途
+this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
+ NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone();
+ fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2);
+ this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService);
+ this.sendMessageExecutor = new BrokerFixedThreadPoolExecutor(
+ this.brokerConfig.getSendMessageThreadPoolNums(),
+ this.brokerConfig.getSendMessageThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.sendThreadPoolQueue,
+ new ThreadFactoryImpl("SendMessageThread_"));
-# Failure analyzers
-org.springframework.boot.diagnostics.FailureAnalyzer=\
-org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
-org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
-org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
-org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
-org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
-org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
-org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer
+ this.pullMessageExecutor = new BrokerFixedThreadPoolExecutor(
+ this.brokerConfig.getPullMessageThreadPoolNums(),
+ this.brokerConfig.getPullMessageThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.pullThreadPoolQueue,
+ new ThreadFactoryImpl("PullMessageThread_"));
-# Template availability providers
-org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
-org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
-org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
+ this.replyMessageExecutor = new BrokerFixedThreadPoolExecutor(
+ this.brokerConfig.getProcessReplyMessageThreadPoolNums(),
+ this.brokerConfig.getProcessReplyMessageThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.replyThreadPoolQueue,
+ new ThreadFactoryImpl("ProcessReplyMessageThread_"));
-# DataSource initializer detectors
-org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
-org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector
-
+ this.queryMessageExecutor = new BrokerFixedThreadPoolExecutor(
+ this.brokerConfig.getQueryMessageThreadPoolNums(),
+ this.brokerConfig.getQueryMessageThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.queryThreadPoolQueue,
+ new ThreadFactoryImpl("QueryMessageThread_"));
-上面根据 org.springframework.boot.autoconfigure.EnableAutoConfiguration 获取的各个配置类,在通过反射加载就能得到一堆 JavaConfig配置类,然后再根据 ConditionalOnProperty等条件配置加载具体的 bean,大致就是这么个逻辑
-]]>
-
- Java
- SpringBoot
-
-
- Java
- Spring
- SpringBoot
- 自动装配
- AutoConfiguration
-
-
-
- 聊聊传说中的 ThreadLocal
- /2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/
- 说来也惭愧,这个 ThreadLocal 其实一直都是一知半解,而且看了一下之后还发现记错了,所以还是记录下
原先记忆里的都是反过来,一个 ThreadLocal 是里面按照 thread 作为 key,存储线程内容的,真的是半解都米有,完全是错的,这样就得用 concurrentHashMap 这种去存储并且要锁定线程了,然后内容也只能存一个了,想想简直智障
-究竟是啥结构
比如我们在代码中 new 一个 ThreadLocal,
-public static void main(String[] args) {
- ThreadLocal<Man> tl = new ThreadLocal<>();
+ this.adminBrokerExecutor =
+ Executors.newFixedThreadPool(this.brokerConfig.getAdminBrokerThreadPoolNums(), new ThreadFactoryImpl(
+ "AdminBrokerThread_"));
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(2);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(tl.get());
- }).start();
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- tl.set(new Man());
- }).start();
- }
+ this.clientManageExecutor = new ThreadPoolExecutor(
+ this.brokerConfig.getClientManageThreadPoolNums(),
+ this.brokerConfig.getClientManageThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.clientManagerThreadPoolQueue,
+ new ThreadFactoryImpl("ClientManageThread_"));
- static class Man {
- String name = "nick";
- }
-这里构造了两个线程,一个先往里设值,一个后从里取,运行看下结果,
![]()
知道这个用法的话肯定知道是取不到值的,只是具体的原理原来搞错了,我们来看下设值 set 方法
-public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
-}
-写博客这会我才明白我原来咋会错得这么离谱,看到第一行代码 t 就是当前线程,然后第二行就是用这个线程去getMap,然后我是把这个当成从 map 里取值了,其实这里是
-ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
-}
-获取 t 的 threadLocals 成员变量,那这个 threadLocals 又是啥呢
![]()
它其实是线程 Thread 中的一个类型是java.lang.ThreadLocal.ThreadLocalMap的成员变量
这是 ThreadLocal 的一个静态成员变量
-static class ThreadLocalMap {
+ this.heartbeatExecutor = new BrokerFixedThreadPoolExecutor(
+ this.brokerConfig.getHeartbeatThreadPoolNums(),
+ this.brokerConfig.getHeartbeatThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.heartbeatThreadPoolQueue,
+ new ThreadFactoryImpl("HeartbeatThread_", true));
- /**
- * The entries in this hash map extend WeakReference, using
- * its main ref field as the key (which is always a
- * ThreadLocal object). Note that null keys (i.e. entry.get()
- * == null) mean that the key is no longer referenced, so the
- * entry can be expunged from table. Such entries are referred to
- * as "stale entries" in the code that follows.
- */
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
+ this.endTransactionExecutor = new BrokerFixedThreadPoolExecutor(
+ this.brokerConfig.getEndTransactionThreadPoolNums(),
+ this.brokerConfig.getEndTransactionThreadPoolNums(),
+ 1000 * 60,
+ TimeUnit.MILLISECONDS,
+ this.endTransactionThreadPoolQueue,
+ new ThreadFactoryImpl("EndTransactionThread_"));
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- }
-全部代码有点长,只截取了一小部分,然后我们再回头来分析前面说的 set 过程,再 copy 下代码
-public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
-}
-获取到 map 以后呢,如果 map 不为空,就往 map 里 set,这里注意 key 是啥,其实是当前这个 ThreadLocal,这里就比较明白了究竟是啥结构,每个线程都会维护自身的 ThreadLocalMap,它是线程的一个成员变量,当创建 ThreadLocal 的时候,进行设值的时候其实是往这个 map 里以 ThreadLocal 作为 key,往里设 value。
-内存泄漏是什么鬼
这里又要看下前面的 ThreadLocalMap 结构了,类似 HashMap,它有个 Entry 结构,在设置的时候会先包装成一个 Entry
-private void set(ThreadLocal<?> key, Object value) {
+ this.consumerManageExecutor =
+ Executors.newFixedThreadPool(this.brokerConfig.getConsumerManageThreadPoolNums(), new ThreadFactoryImpl(
+ "ConsumerManageThread_"));
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
+ this.registerProcessor();
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
+ final long initialDelay = UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis();
+ final long period = 1000 * 60 * 60 * 24;
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.getBrokerStats().record();
+ } catch (Throwable e) {
+ log.error("schedule record error.", e);
+ }
+ }
+ }, initialDelay, period, TimeUnit.MILLISECONDS);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.consumerOffsetManager.persist();
+ } catch (Throwable e) {
+ log.error("schedule persist consumerOffset error.", e);
+ }
+ }
+ }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
- if (k == key) {
- e.value = value;
- return;
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.consumerFilterManager.persist();
+ } catch (Throwable e) {
+ log.error("schedule persist consumer filter error.", e);
+ }
+ }
+ }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);
+
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.protectBroker();
+ } catch (Throwable e) {
+ log.error("protectBroker error.", e);
+ }
+ }
+ }, 3, 3, TimeUnit.MINUTES);
+
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.printWaterMark();
+ } catch (Throwable e) {
+ log.error("printWaterMark error.", e);
+ }
+ }
+ }, 10, 1, TimeUnit.SECONDS);
+
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ log.info("dispatch behind commit log {} bytes", BrokerController.this.getMessageStore().dispatchBehindBytes());
+ } catch (Throwable e) {
+ log.error("schedule dispatchBehindBytes error.", e);
+ }
+ }
+ }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
+
+ if (this.brokerConfig.getNamesrvAddr() != null) {
+ this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
+ log.info("Set user specified name server address: {}", this.brokerConfig.getNamesrvAddr());
+ } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
+ } catch (Throwable e) {
+ log.error("ScheduledTask fetchNameServerAddr exception", e);
+ }
+ }
+ }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
+ if (!messageStoreConfig.isEnableDLegerCommitLog()) {
+ if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
+ if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= 6) {
+ this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress());
+ this.updateMasterHAServerAddrPeriodically = false;
+ } else {
+ this.updateMasterHAServerAddrPeriodically = true;
+ }
+ } else {
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.printMasterAndSlaveDiff();
+ } catch (Throwable e) {
+ log.error("schedule printMasterAndSlaveDiff error.", e);
+ }
+ }
+ }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ 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();
+ ((NettyRemotingServer) fastRemotingServer).loadSslContext();
+ }
+ });
+ } catch (Exception e) {
+ log.warn("FileWatchService created error, can't load the certificate dynamically");
+ }
}
+ initialTransaction();
+ initialAcl();
+ initialRpcHooks();
}
+ return result;
- tab[i] = new Entry(key, value);
- int sz = ++size;
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
-}
-这里其实比较重要的就是前面的 Entry 的构造方法,Entry 是个 WeakReference 的子类,然后在构造方法里可以看到 key 会被包装成一个弱引用,这里为什么使用弱引用,其实是方便这个 key 被回收,如果前面的 ThreadLocal tl实例被设置成 null 了,如果这里是直接的强引用的话,就只能等到线程整个回收了,但是其实是弱引用也会有问题,主要是因为这个 value,如果在 ThreadLocal tl 被设置成 null 了,那么其实这个 value 就会没法被访问到,所以最好的操作还是在使用完了就 remove 掉
-]]>
-
- Java
-
-
- Java
- ThreadLocal
- 弱引用
- 内存泄漏
- WeakReference
-
-
-
- 聊聊厦门旅游的好与不好
- /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/
- 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。
+
+
+Broker 启动过程
+贴代码
+public void start() throws Exception {
+ if (this.messageStore != null) {
+ this.messageStore.start();
+ }
+
+ if (this.remotingServer != null) {
+ this.remotingServer.start();
+ }
+
+ if (this.fastRemotingServer != null) {
+ this.fastRemotingServer.start();
+ }
+
+ if (this.fileWatchService != null) {
+ this.fileWatchService.start();
+ }
+
+ if (this.brokerOuterAPI != null) {
+ this.brokerOuterAPI.start();
+ }
+
+ if (this.pullRequestHoldService != null) {
+ this.pullRequestHoldService.start();
+ }
+
+ if (this.clientHousekeepingService != null) {
+ this.clientHousekeepingService.start();
+ }
+
+ if (this.filterServerManager != null) {
+ this.filterServerManager.start();
+ }
+
+ if (!messageStoreConfig.isEnableDLegerCommitLog()) {
+ startProcessorByHa(messageStoreConfig.getBrokerRole());
+ handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
+ this.registerBrokerAll(true, false, true);
+ }
+
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
+ } catch (Throwable e) {
+ log.error("registerBrokerAll Exception", e);
+ }
+ }
+ }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
+
+ if (this.brokerStatsManager != null) {
+ this.brokerStatsManager.start();
+ }
+
+ if (this.brokerFastFailure != null) {
+ this.brokerFastFailure.start();
+ }
+
+
+ }
+
+首先是启动messageStore,调用 start 方法,这里面又调用了一些代码
+public void start() throws Exception {
+
+ lock = lockFile.getChannel().tryLock(0, 1, false);
+ if (lock == null || lock.isShared() || !lock.isValid()) {
+ throw new RuntimeException("Lock failed,MQ already started");
+ }
+
+ lockFile.getChannel().write(ByteBuffer.wrap("lock".getBytes()));
+ lockFile.getChannel().force(true);
+ {
+ /**
+ * 1. Make sure the fast-forward messages to be truncated during the recovering according to the max physical offset of the commitlog;
+ * 2. DLedger committedPos may be missing, so the maxPhysicalPosInLogicQueue maybe bigger that maxOffset returned by DLedgerCommitLog, just let it go;
+ * 3. Calculate the reput offset according to the consume queue;
+ * 4. Make sure the fall-behind messages to be dispatched before starting the commitlog, especially when the broker role are automatically changed.
+ */
+ long maxPhysicalPosInLogicQueue = commitLog.getMinOffset();
+ for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
+ for (ConsumeQueue logic : maps.values()) {
+ if (logic.getMaxPhysicOffset() > maxPhysicalPosInLogicQueue) {
+ maxPhysicalPosInLogicQueue = logic.getMaxPhysicOffset();
+ }
+ }
+ }
+ if (maxPhysicalPosInLogicQueue < 0) {
+ maxPhysicalPosInLogicQueue = 0;
+ }
+ if (maxPhysicalPosInLogicQueue < this.commitLog.getMinOffset()) {
+ maxPhysicalPosInLogicQueue = this.commitLog.getMinOffset();
+ /**
+ * This happens in following conditions:
+ * 1. If someone removes all the consumequeue files or the disk get damaged.
+ * 2. Launch a new broker, and copy the commitlog from other brokers.
+ *
+ * All the conditions has the same in common that the maxPhysicalPosInLogicQueue should be 0.
+ * If the maxPhysicalPosInLogicQueue is gt 0, there maybe something wrong.
+ */
+ log.warn("[TooSmallCqOffset] maxPhysicalPosInLogicQueue={} clMinOffset={}", maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset());
+ }
+ log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
+ maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
+ this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
+ this.reputMessageService.start();
+
+ /**
+ * 1. Finish dispatching the messages fall behind, then to start other services.
+ * 2. DLedger committedPos may be missing, so here just require dispatchBehindBytes <= 0
+ */
+ while (true) {
+ if (dispatchBehindBytes() <= 0) {
+ break;
+ }
+ Thread.sleep(1000);
+ log.info("Try to finish doing reput the messages fall behind during the starting, reputOffset={} maxOffset={} behind={}", this.reputMessageService.getReputFromOffset(), this.getMaxPhyOffset(), this.dispatchBehindBytes());
+ }
+ this.recoverTopicQueueTable();
+ }
+
+ if (!messageStoreConfig.isEnableDLegerCommitLog()) {
+ this.haService.start();
+ this.handleScheduleMessageService(messageStoreConfig.getBrokerRole());
+ }
+
+ this.flushConsumeQueueService.start();
+ this.commitLog.start();
+ this.storeStatsService.start();
+
+ this.createTempFile();
+ this.addScheduleTask();
+ this.shutdown = false;
+ }
+
+
+
+调用DefaultMessageStore.start方法启动DefaultMessageStore对象中的一些服务线程。
+
+- 启动ReputMessageService服务线程
+- 启动FlushConsumeQueueService服务线程;
+- 调用CommitLog.start方法,启动CommitLog对象中的FlushCommitLogService线程服务,若是同步刷盘(SYNC_FLUSH)则是启动GroupCommitService线程服务;若是异步刷盘(ASYNC_FLUSH)则是启动FlushRealTimeService线程服务;
+- 启动StoreStatsService线程服务;
+- 启动定时清理任务
+
+然后是启动ClientHousekeepingService的 netty 服务端和客户端,然后是启动fileWatchService证书服务,接着启动BrokerOuterAPI中的NettyRemotingClient,即建立与NameServer的链接,用于自身Broker与其他模块的RPC功能调用;包括获取NameServer的地址、注册Broker、注销Broker、获取Topic配置、获取消息进度信息、获取订阅关系等RPC功能,然后是PullRequestHoldService服务线程,这个就是实现长轮询的,然后启动管家ClientHousekeepingService服务,负责扫描不活跃的生产者,消费者和 filter,启动FilterServerManager 过滤器服务管理,然后启动定时任务调用org.apache.rocketmq.broker.BrokerController#registerBrokerAll向所有 nameserver 注册 broker,最后是按需开启org.apache.rocketmq.store.stats.BrokerStatsManager和org.apache.rocketmq.broker.latency.BrokerFastFailure,基本上启动过程就完成了
]]>
- 生活
+ MQ
+ RocketMQ
+ 消息队列
+ RocketMQ
+ 中间件
+ RocketMQ
- 生活
- 糟心事
- 扶梯
- 踩踏
- 安全
- 电瓶车
+ MQ
+ 消息队列
+ RocketMQ
+ 削峰填谷
+ 中间件
+ 源码解析
+ Broker
- 聊聊我的远程工作体验
- /2022/06/26/%E8%81%8A%E8%81%8A%E6%88%91%E7%9A%84%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C%E4%BD%93%E9%AA%8C/
- 发生疫情之后,因为正好是春节假期,假期结束的时候还不具备回工作地点办公的条件,所以史无前例地开始了远程办公,以前对于远程办公的概念还停留在国外一些有“格局”的企业会允许员工远程办公,当然对于远程办公这个事情本身我个人也并不是全然支持的态度,其中涉及到很多方面,首先远程办公并不代表就是不用去办公地点上班,可以在家里摸鱼,相对能够得到较高报酬的能够远程办公的企业需要在远程办公期间能够有高效的产出,并且也需要像在公司办公地点一样,能随时被联系到,第二点是薪资福利之外的社保公积金,除非薪资相比非远程办公的企业高出比较多,不然没法 cover 企业额外缴纳的社保公积金,听说有部分企业也会远程办公点给员工上社保,但是毕竟能做到这点的很少,在允许远程办公的企业数量这个本来就不大的基数里,大概率是少之又少了。
疫情这个特殊原因开始的远程办公体验也算是开了个之前不太容易开的头,也跟我前面说的第一点有关系,大部分的企业也会担心员工远程办公是否有与在公司办公地点办公一样或者比较接近的办公效率。同时我们在开始远程办公的时候也碰到了因为原先没做过相应准备而导致的许多问题,首先基础设施上就有几个问题,第一个是办公电脑的问题,因为整个公司各个部门的工作性质和内容不同,并不是每个部门都是配笔记本的,或者有些部门并不需要想研发一样带上电脑 on call,所以那么使用台式机或者没有将笔记本带回家的则需要自己准备电脑或者让公司邮寄。第二个是远程网络的问题,像我们公司有研发团队平时也已经准备好了 vpn,但是在这种时候我们没准备好的是 vpn 带宽,毕竟平时只会偶尔有需要连一下 vpn 到公司网络,像这样大量员工都需要连接 vpn 进行工作的话,我们的初步体验就是网络卡的不行,一些远程调试工作没法进行,并且还有一些问题是可能只有我们研发会碰到,比如我们的线上测试服务器网络在办公地点是有网络打通的,但是我们在家就没办法连接,还有就是沟通效率相关,因为这是个全国性的情况,线上会议工具原先都是为特定用户使用,并且视频音频实时传输所需要的带宽质量要求也是比较高的,大规模的远程会议沟通需求让这些做线上会议的服务也算是碰上了类似双十一的大考了,我们是先后使用了 zoom,腾讯会议跟钉钉视频会议,使用体验上来说是 zoom 做得相对比较成熟和稳定,不过后面腾讯会议跟钉钉视频会议也开始赶上来。
前面说的这几个点都是得有远程办公经验的公司才会提前做好相应的准备,比如可以做动态网络扩容,能够在需要大量员工连接公司网络的情况下快速响应提升带宽,另一些则是偏软性的,比如如如何在远程办公的条件下控制我们项目进度,如果保证沟通信息是否能像当面沟通那样准确传达,这方面其实我的经验也是边实操边优化的,最开始我们可能为了高效同步消息,会频繁的使用视频会议沟通,这其实并不能解决沟通效率问题,反而打扰了正常的工作,后续我们在特别是做项目过程中就通过相对简单的每日早会和日报机制,将每天的进度与问题风险点进行同步确认,只与相关直接干系人进行视频电话沟通确认,并且要保持一个思维,即远程办公比较适宜的是相对比较成熟的团队,平常工作和合作都已经有默契或者说规则并且能够遵守,在这个前提下,将目光专注于做的事情而不是管到具体的人有没有全天都在高效工作。同样也希望国内的环境能够有更多的远程火种成长起来,让它成为更好的工作方式,WLB!
+ 聊聊 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
- 聊聊我理解的分布式事务
- /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/
- 前面说了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 的 MVCC
+ /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/
+ 很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢
+其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行
+腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下
+/* Precise data types for system columns and the length of those columns;
+NOTE: the values must run from 0 up in the order given! All codes must
+be less than 256 */
+#define DATA_ROW_ID 0 /* row id: a 48-bit integer */
+#define DATA_ROW_ID_LEN 6 /* stored length for row id */
+
+/** Transaction id: 6 bytes */
+constexpr size_t DATA_TRX_ID = 1;
+
+/** Transaction ID type size in bytes. */
+constexpr size_t DATA_TRX_ID_LEN = 6;
+
+/** Rollback data pointer: 7 bytes */
+constexpr size_t DATA_ROLL_PTR = 2;
+
+/** Rollback data pointer type size in bytes. */
+constexpr size_t DATA_ROLL_PTR_LEN = 7;
+
+一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了
+一个是 DATA_TRX_ID,这个表示这条记录的事务 ID
+还有一个是 DATA_ROLL_PTR 指向回滚段的指针
+指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,
+m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id
+m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id
+m_ids, 这个是 read view 创建时所有的活跃事务 id 数组
+m_creator_trx_id 这个是当前记录的创建事务 id
+判断事务的可见性主要的逻辑是这样,
+
+- 当记录的事务
id 小于最小活跃事务 id,说明是可见的,
+- 如果记录的事务
id 等于当前事务 id,说明是自己的更改,可见
+- 如果记录的事务
id 大于最大的活跃事务 id, 不可见
+- 如果记录的事务
id 介于 m_low_limit_id 和 m_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
具体的代码捞一下看看/** Check whether the changes by id are visible.
+ @param[in] id transaction id to check against the view
+ @param[in] name table name
+ @return whether the view sees the modifications of id. */
+ bool changes_visible(trx_id_t id, const table_name_t &name) const
+ MY_ATTRIBUTE((warn_unused_result)) {
+ ut_ad(id > 0);
+
+ if (id < m_up_limit_id || id == m_creator_trx_id) {
+ return (true);
+ }
+
+ check_trx_id_sanity(id, name);
+
+ if (id >= m_low_limit_id) {
+ return (false);
+
+ } else if (m_ids.empty()) {
+ return (true);
+ }
+
+ const ids_t::value_type *p = m_ids.data();
+
+ return (!std::binary_search(p, p + m_ids.size(), id));
+ }
+剩下来一点是啥呢,就是 Read Committed 和 Repeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据
+
+]]>
+
+ Mysql
+ C
+ 数据结构
+ 源码
+ Mysql
+
+
+ mysql
+ 数据结构
+ 源码
+ mvcc
+ read view
+
+
+
+ 聊聊传说中的 ThreadLocal
+ /2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/
+ 说来也惭愧,这个 ThreadLocal 其实一直都是一知半解,而且看了一下之后还发现记错了,所以还是记录下
原先记忆里的都是反过来,一个 ThreadLocal 是里面按照 thread 作为 key,存储线程内容的,真的是半解都米有,完全是错的,这样就得用 concurrentHashMap 这种去存储并且要锁定线程了,然后内容也只能存一个了,想想简直智障
+究竟是啥结构
比如我们在代码中 new 一个 ThreadLocal,
+public static void main(String[] args) {
+ ThreadLocal<Man> tl = new ThreadLocal<>();
+
+ new Thread(() -> {
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(tl.get());
+ }).start();
+ new Thread(() -> {
+ try {
+ TimeUnit.SECONDS.sleep(1);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ tl.set(new Man());
+ }).start();
+ }
+
+ static class Man {
+ String name = "nick";
+ }
+这里构造了两个线程,一个先往里设值,一个后从里取,运行看下结果,
![]()
知道这个用法的话肯定知道是取不到值的,只是具体的原理原来搞错了,我们来看下设值 set 方法
+public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
+}
+写博客这会我才明白我原来咋会错得这么离谱,看到第一行代码 t 就是当前线程,然后第二行就是用这个线程去getMap,然后我是把这个当成从 map 里取值了,其实这里是
+ThreadLocalMap getMap(Thread t) {
+ return t.threadLocals;
+}
+获取 t 的 threadLocals 成员变量,那这个 threadLocals 又是啥呢
![]()
它其实是线程 Thread 中的一个类型是java.lang.ThreadLocal.ThreadLocalMap的成员变量
这是 ThreadLocal 的一个静态成员变量
+static class ThreadLocalMap {
+
+ /**
+ * The entries in this hash map extend WeakReference, using
+ * its main ref field as the key (which is always a
+ * ThreadLocal object). Note that null keys (i.e. entry.get()
+ * == null) mean that the key is no longer referenced, so the
+ * entry can be expunged from table. Such entries are referred to
+ * as "stale entries" in the code that follows.
+ */
+ static class Entry extends WeakReference<ThreadLocal<?>> {
+ /** The value associated with this ThreadLocal. */
+ Object value;
+
+ Entry(ThreadLocal<?> k, Object v) {
+ super(k);
+ value = v;
+ }
+ }
+ }
+全部代码有点长,只截取了一小部分,然后我们再回头来分析前面说的 set 过程,再 copy 下代码
+public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
+}
+获取到 map 以后呢,如果 map 不为空,就往 map 里 set,这里注意 key 是啥,其实是当前这个 ThreadLocal,这里就比较明白了究竟是啥结构,每个线程都会维护自身的 ThreadLocalMap,它是线程的一个成员变量,当创建 ThreadLocal 的时候,进行设值的时候其实是往这个 map 里以 ThreadLocal 作为 key,往里设 value。
+内存泄漏是什么鬼
这里又要看下前面的 ThreadLocalMap 结构了,类似 HashMap,它有个 Entry 结构,在设置的时候会先包装成一个 Entry
+private void set(ThreadLocal<?> key, Object value) {
+
+ // We don't use a fast path as with get() because it is at
+ // least as common to use set() to create new entries as
+ // it is to replace existing ones, in which case, a fast
+ // path would fail more often than not.
+
+ Entry[] tab = table;
+ int len = tab.length;
+ int i = key.threadLocalHashCode & (len-1);
+
+ for (Entry e = tab[i];
+ e != null;
+ e = tab[i = nextIndex(i, len)]) {
+ ThreadLocal<?> k = e.get();
+
+ if (k == key) {
+ e.value = value;
+ return;
+ }
+
+ if (k == null) {
+ replaceStaleEntry(key, value, i);
+ return;
+ }
+ }
+
+ tab[i] = new Entry(key, value);
+ int sz = ++size;
+ if (!cleanSomeSlots(i, sz) && sz >= threshold)
+ rehash();
+}
+这里其实比较重要的就是前面的 Entry 的构造方法,Entry 是个 WeakReference 的子类,然后在构造方法里可以看到 key 会被包装成一个弱引用,这里为什么使用弱引用,其实是方便这个 key 被回收,如果前面的 ThreadLocal tl实例被设置成 null 了,如果这里是直接的强引用的话,就只能等到线程整个回收了,但是其实是弱引用也会有问题,主要是因为这个 value,如果在 ThreadLocal tl 被设置成 null 了,那么其实这个 value 就会没法被访问到,所以最好的操作还是在使用完了就 remove 掉
+]]>
+
+ Java
+
+
+ Java
+ ThreadLocal
+ 弱引用
+ 内存泄漏
+ WeakReference
+
+
+
+ 聊聊一次 brew update 引发的血案
+ /2020/06/13/%E8%81%8A%E8%81%8A%E4%B8%80%E6%AC%A1-brew-update-%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/
+ 熟悉我的人(谁熟悉你啊🙄)知道我以前写过 PHP,虽然现在在工作中没用到了,但是自己的一些小工具还是会用 PHP 来写,但是在 Mac 碰到了一个环境相关的问题,因为我也是个更新狂魔,用了 brew 之后因为 gfw 的原因,如果长时间不更新,有时候要装一个用它装一个软件的话,前置的更新耗时就会让人非常头大,所以我基本会隔天 update 一下,但是这样会带来一个很心烦的问题,就是像这样,因为我是要用一个固定版本的 PHP,如果一直升需要一直配扩展啥的也很麻烦,如果一直升级 PHP 到最新版可能会比较少碰到这个问题
+dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
+这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下
+
+ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。
+
+然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
第一步,我们到 brew的目录下
+cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
第二步看看它的提交历史
+git log --follow icu4c.rb
+在 git log 的海洋中寻找,寻找它的(64版本)的身影
![]()
第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支
+git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
+其实注意 commit id 旁边的红框,这个是有tag 的,可以直接
+git checkout icu4c-64
+PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本
+brew reinstall ./icu4c.rb
+然后就是第五步,切换版本
+brew switch icu4c 64.2
+最后把分支切回来
+git checkout master
+是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法
+# zsh
+function hiicu64() {
+ local last_dir=$(pwd)
+
+ cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+ git checkout icu4c-4
+ brew reinstall ./icu4c.rb
+ brew switch icu4c 64.2
+ git checkout master
+
+ cd $last_dir
+}
+对应自己的版本改改版本号就可以了,非常好用。
+]]>
+
+ Mac
+ PHP
+ Homebrew
+ PHP
+ icu4c
+
+
+ Mac
+ PHP
+ Homebrew
+ icu4c
+ zsh
+
+
+
+ 聊聊我刚学会的应用诊断方法
+ /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
+
+
+
+ 聊聊我的远程工作体验
+ /2022/06/26/%E8%81%8A%E8%81%8A%E6%88%91%E7%9A%84%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C%E4%BD%93%E9%AA%8C/
+ 发生疫情之后,因为正好是春节假期,假期结束的时候还不具备回工作地点办公的条件,所以史无前例地开始了远程办公,以前对于远程办公的概念还停留在国外一些有“格局”的企业会允许员工远程办公,当然对于远程办公这个事情本身我个人也并不是全然支持的态度,其中涉及到很多方面,首先远程办公并不代表就是不用去办公地点上班,可以在家里摸鱼,相对能够得到较高报酬的能够远程办公的企业需要在远程办公期间能够有高效的产出,并且也需要像在公司办公地点一样,能随时被联系到,第二点是薪资福利之外的社保公积金,除非薪资相比非远程办公的企业高出比较多,不然没法 cover 企业额外缴纳的社保公积金,听说有部分企业也会远程办公点给员工上社保,但是毕竟能做到这点的很少,在允许远程办公的企业数量这个本来就不大的基数里,大概率是少之又少了。
疫情这个特殊原因开始的远程办公体验也算是开了个之前不太容易开的头,也跟我前面说的第一点有关系,大部分的企业也会担心员工远程办公是否有与在公司办公地点办公一样或者比较接近的办公效率。同时我们在开始远程办公的时候也碰到了因为原先没做过相应准备而导致的许多问题,首先基础设施上就有几个问题,第一个是办公电脑的问题,因为整个公司各个部门的工作性质和内容不同,并不是每个部门都是配笔记本的,或者有些部门并不需要想研发一样带上电脑 on call,所以那么使用台式机或者没有将笔记本带回家的则需要自己准备电脑或者让公司邮寄。第二个是远程网络的问题,像我们公司有研发团队平时也已经准备好了 vpn,但是在这种时候我们没准备好的是 vpn 带宽,毕竟平时只会偶尔有需要连一下 vpn 到公司网络,像这样大量员工都需要连接 vpn 进行工作的话,我们的初步体验就是网络卡的不行,一些远程调试工作没法进行,并且还有一些问题是可能只有我们研发会碰到,比如我们的线上测试服务器网络在办公地点是有网络打通的,但是我们在家就没办法连接,还有就是沟通效率相关,因为这是个全国性的情况,线上会议工具原先都是为特定用户使用,并且视频音频实时传输所需要的带宽质量要求也是比较高的,大规模的远程会议沟通需求让这些做线上会议的服务也算是碰上了类似双十一的大考了,我们是先后使用了 zoom,腾讯会议跟钉钉视频会议,使用体验上来说是 zoom 做得相对比较成熟和稳定,不过后面腾讯会议跟钉钉视频会议也开始赶上来。
前面说的这几个点都是得有远程办公经验的公司才会提前做好相应的准备,比如可以做动态网络扩容,能够在需要大量员工连接公司网络的情况下快速响应提升带宽,另一些则是偏软性的,比如如如何在远程办公的条件下控制我们项目进度,如果保证沟通信息是否能像当面沟通那样准确传达,这方面其实我的经验也是边实操边优化的,最开始我们可能为了高效同步消息,会频繁的使用视频会议沟通,这其实并不能解决沟通效率问题,反而打扰了正常的工作,后续我们在特别是做项目过程中就通过相对简单的每日早会和日报机制,将每天的进度与问题风险点进行同步确认,只与相关直接干系人进行视频电话沟通确认,并且要保持一个思维,即远程办公比较适宜的是相对比较成熟的团队,平常工作和合作都已经有默契或者说规则并且能够遵守,在这个前提下,将目光专注于做的事情而不是管到具体的人有没有全天都在高效工作。同样也希望国内的环境能够有更多的远程火种成长起来,让它成为更好的工作方式,WLB!
+]]>
+
+ 生活
+
+
+ 生活
+ 远程办公
+
+
+
+ 聊聊我理解的分布式事务
+ /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/
+ 前面说了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 等
+]]>
+
+ 分布式事务
+ 两阶段提交
+ 三阶段提交
+
+
+ 分布式事务
+ 两阶段提交
+ 三阶段提交
+ 2PC
+ 3PC
+
+
+
+ 聊聊 Linux 下的 top 命令
+ /2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/
+ top 命令在日常的 Linux 使用中,特别是做一些服务器的简单状态查看,排查故障都起了比较大的作用,但是由于这个命令看到的东西比较多,一般只会看部分,或者说像我这样就会比较片面地看一些信息,比如默认是进程维度的,可以在启动命令的时候加-H进入线程模式
+-H :Threads-mode operation
+ Instructs top to display individual threads. Without this command-line option a summation of all threads in each process is shown. Later
+ this can be changed with the `H' interactive command.
+这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样
![]()
执行后是这样
![]()
第二个就是排序了
+SORTING of task window
+
+ For compatibility, this top supports most of the former top sort keys. Since this is primarily a service to former top users, these commands
+ do not appear on any help screen.
+ command sorted-field supported
+ A start time (non-display) No
+ M %MEM Yes
+ N PID Yes
+ P %CPU Yes
+ T TIME+ Yes
+
+ Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
+ mand. That will help ensure that the actual sort environment matches your intent.
+
+ The following interactive commands will only be honored when the current sort field is visible. The sort field might not be visible because:
+ 1) there is insufficient Screen Width
+ 2) the `f' interactive command turned it Off
+
+ < :Move-Sort-Field-Left
+ Moves the sort column to the left unless the current sort field is the first field being displayed.
+
+ > :Move-Sort-Field-Right
+ Moves the sort column to the right unless the current sort field is the last field being displayed.
+查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<和>直接左右移动排序列
+]]>
+
+ Linux
+ 命令
+ 小技巧
+ top
+ top
+ 排序
+
+
+ 排序
+ linux
+ 小技巧
+ top
+
+
+
+ 聊聊最近平淡的生活之《花束般的恋爱》观后感
+ /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 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。
+]]>
+
+ 生活
+
+
+ 生活
+ 看剧
+
+
+
+ 聊聊 SpringBoot 自动装配
+ /2021/07/11/%E8%81%8A%E8%81%8ASpringBoot-%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/
+ springboot 自动装配调用链
+springboot 相比 spring能更方便开发人员上手,比较重要的一点就是自动装配,大致来看下这个逻辑
+public static void main(String[] args) {
+ SpringApplication.run(SpbDemoApplication.class, args);
+ }
+
+ /**
+ * Static helper that can be used to run a {@link SpringApplication} from the
+ * specified source using default settings.
+ * @param primarySource the primary source to load
+ * @param args the application arguments (usually passed from a Java main method)
+ * @return the running {@link ApplicationContext}
+ */
+然后就是上面调用的 run 方法
+public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
+ return run(new Class<?>[] { primarySource }, args);
+}
+
+/**
+ * Static helper that can be used to run a {@link SpringApplication} from the
+ * specified sources using default settings and user supplied arguments.
+ * @param primarySources the primary sources to load
+ * @param args the application arguments (usually passed from a Java main method)
+ * @return the running {@link ApplicationContext}
+ */
+继续往下看
+public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
+ return new SpringApplication(primarySources).run(args);
+}
+
+调用SpringApplication的构造方法
+/**
+ * Create a new {@link SpringApplication} instance. The application context will load
+ * beans from the specified primary sources (see {@link SpringApplication class-level}
+ * documentation for details. The instance can be customized before calling
+ * {@link #run(String...)}.
+ * @param primarySources the primary bean sources
+ * @see #run(Class, String[])
+ * @see #SpringApplication(ResourceLoader, Class...)
+ * @see #setSources(Set)
+ */
+
+public SpringApplication(Class<?>... primarySources) {
+ this(null, primarySources);
+}
+
+/**
+ * Create a new {@link SpringApplication} instance. The application context will load
+ * beans from the specified primary sources (see {@link SpringApplication class-level}
+ * documentation for details. The instance can be customized before calling
+ * {@link #run(String...)}.
+ * @param resourceLoader the resource loader to use
+ * @param primarySources the primary bean sources
+ * @see #run(Class, String[])
+ * @see #setSources(Set)
+ */
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
+ this.resourceLoader = resourceLoader;
+ Assert.notNull(primarySources, "PrimarySources must not be null");
+ this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
+ this.webApplicationType = WebApplicationType.deduceFromClasspath();
+ // 注意看这里的,通过 SpringFactories 获取
+ this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
+ setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
+ setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
+ this.mainApplicationClass = deduceMainApplicationClass();
+}
+这里就是重点了
+private List<BootstrapRegistryInitializer> getBootstrapRegistryInitializersFromSpringFactories() {
+ ArrayList<BootstrapRegistryInitializer> initializers = new ArrayList<>();
+ getSpringFactoriesInstances(Bootstrapper.class).stream()
+ .map((bootstrapper) -> ((BootstrapRegistryInitializer) bootstrapper::initialize))
+ .forEach(initializers::add);
+ initializers.addAll(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
+ return initializers;
+}
+private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
+ return getSpringFactoriesInstances(type, new Class<?>[] {});
+}
+
+private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
+ ClassLoader classLoader = getClassLoader();
+ // Use names and ensure unique to protect against duplicates
+ // 去加载所有FACTORIES_RESOURCE_LOCATION路径下面,也就是 META-INF/spring.factories
+ Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
+ List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
+ AnnotationAwareOrderComparator.sort(instances);
+ return instances;
+}
+
+
+/**
+ * Load the fully qualified class names of factory implementations of the
+ * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
+ * class loader.
+ * <p>As of Spring Framework 5.3, if a particular implementation class name
+ * is discovered more than once for the given factory type, duplicates will
+ * be ignored.
+ * @param factoryType the interface or abstract class representing the factory
+ * @param classLoader the ClassLoader to use for loading resources; can be
+ * {@code null} to use the default
+ * @throws IllegalArgumentException if an error occurs while loading factory names
+ * @see #loadFactories
+ */
+public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
+ ClassLoader classLoaderToUse = classLoader;
+ if (classLoaderToUse == null) {
+ classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
+ }
+ String factoryTypeName = factoryType.getName();
+ return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
+}
+ private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
+ Map<String, List<String>> result = cache.get(classLoader);
+ if (result != null) {
+ return result;
+ }
+
+ result = new HashMap<>();
+ try {
+ // 获取此 resources,作为 AutoConfiguration 的配置
+ Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
+ while (urls.hasMoreElements()) {
+ URL url = urls.nextElement();
+ UrlResource resource = new UrlResource(url);
+ Properties properties = PropertiesLoaderUtils.loadProperties(resource);
+ for (Map.Entry<?, ?> entry : properties.entrySet()) {
+ String factoryTypeName = ((String) entry.getKey()).trim();
+ String[] factoryImplementationNames =
+ StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
+ for (String factoryImplementationName : factoryImplementationNames) {
+ result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
+ .add(factoryImplementationName.trim());
+ }
+ }
+ }
+
+ // Replace all lists with unmodifiable lists containing unique elements
+ result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
+ .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
+ cache.put(classLoader, result);
+ }
+ catch (IOException ex) {
+ throw new IllegalArgumentException("Unable to load factories from location [" +
+ FACTORIES_RESOURCE_LOCATION + "]", ex);
+ }
+ return result;
+}
+
+我们可以看下 spring-boot-autoconfigure 的 META-INF/spring.factories
+# Initializers
+org.springframework.context.ApplicationContextInitializer=\
+org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
+org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
+
+# Application Listeners
+org.springframework.context.ApplicationListener=\
+org.springframework.boot.autoconfigure.BackgroundPreinitializer
+
+# Environment Post Processors
+org.springframework.boot.env.EnvironmentPostProcessor=\
+org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
+
+# Auto Configuration Import Listeners
+org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
+org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
+
+# Auto Configuration Import Filters
+org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
+org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
+org.springframework.boot.autoconfigure.condition.OnClassCondition,\
+org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
+
+# 注意这里,其实就是类似于 dubbo spi 的通过 org.springframework.boot.autoconfigure.EnableAutoConfiguration作为 key
+# 获取下面所有的 AutoConfiguration 配置类
+# Auto Configure
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
+org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
+org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
+org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
+org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
+org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
+org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
+org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
+org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
+org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
+org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
+org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
+org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
+org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
+org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
+org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
+org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
+org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
+org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
+org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
+org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
+org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
+org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
+org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
+org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
+org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
+org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
+org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
+org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
+org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
+org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
+org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
+org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
+org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
+org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
+org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
+org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
+org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
+org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
+org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
+org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
+org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
+org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
+org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
+org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
+org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
+org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
+org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
+org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
+org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
+org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
+org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
+org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\
+org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration,\
+org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
+org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
+org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
+org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
+org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
+org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
+org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
+org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
+org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
+org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
+org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
+org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\
+org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
+org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
+org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
+org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
+org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
+org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
+org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
+org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
+org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
+org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
+org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
+org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration
+
+# Failure analyzers
+org.springframework.boot.diagnostics.FailureAnalyzer=\
+org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
+org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
+org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
+org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
+org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
+org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
+org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer
+
+# Template availability providers
+org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
+org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
+
+# DataSource initializer detectors
+org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
+org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector
+
+
+上面根据 org.springframework.boot.autoconfigure.EnableAutoConfiguration 获取的各个配置类,在通过反射加载就能得到一堆 JavaConfig配置类,然后再根据 ConditionalOnProperty等条件配置加载具体的 bean,大致就是这么个逻辑
]]>
- 分布式事务
- 两阶段提交
- 三阶段提交
+ Java
+ SpringBoot
- 分布式事务
- 两阶段提交
- 三阶段提交
- 2PC
- 3PC
+ Java
+ Spring
+ SpringBoot
+ 自动装配
+ AutoConfiguration
@@ -19358,8 +19325,8 @@ $
生活
糟心事
规则
- 电瓶车
骑车
+ 电瓶车
@@ -19376,55 +19343,53 @@ $
- 聊聊给亲戚朋友的老电脑重装系统那些事儿
- /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 全家桶,别说老电脑了,新机器都不太吃得消。
+ 聊聊最近平淡的生活之看看老剧
+ /2021/11/21/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E7%9C%8B%E8%80%81%E5%89%A7/
+ 最近因为也没什么好看的新剧和综艺所以就看看一些以前看过的老剧,我是个非常念旧的人吧,很多剧都会反反复复地看,一方面之前看过觉得好看的的确是一直记着,还有就是平时工作完了回来就想能放松下,剧情太纠结的,太烧脑的都不喜欢,也就是我常挂在口头的不喜欢看费脑子的剧,跟我不喜欢狼人杀的原因也类似。
+前面其实是看的太阳的后裔,跟 LD 一起看的,之前其实算是看过一点,但是没有看的很完整,并且很多剧情也忘了,只是这个我我可能看得更少一点,因为最开始的时候觉得男主应该是男二,可能对长得这样的男主并且是这样的人设有点失望,感觉不是特别像个特种兵,但是由于本来也比较火,而且 LD 比较喜欢就从这个开始看了,有两个点是比较想说的
+韩剧虽然被吐槽的很多,但是很多剧的质量,情节把控还是优于目前非常多国内剧的,相对来说剧情发展的前后承接不是那么硬凹出来的,而且人设都立得住,这个是非常重要的,很多国内剧怎么说呢,就是当爹的看起来就比儿子没大几岁,三四十岁的人去演一个十岁出头的小姑娘,除非容貌异常,比如刘晓庆这种,不然就会觉得导演在把我们观众当傻子。瞬间就没有想看下去的欲望了。
+再一点就是情节是大众都能接受度比较高的,现在有很多普遍会找一些新奇的视角,比如卖腐,想某某令,两部都叫某某令,这其实是一个点,延伸出去就是跟前面说的一点有点类似,xx 老祖,人看着就二三十,叫 xx 老祖,(喜欢的人轻喷哈)然后名字有一堆,同一个人物一会叫这个名字,一会又叫另一个名字,然后一堆死表情。
+因为今天有个特殊的事情发生,所以简短的写(shui)一篇了
]]>
生活
生活
- 装电脑
- 老电脑
- 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,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。
+ 聊聊那些加塞狗
+ /2021/01/17/%E8%81%8A%E8%81%8A%E9%82%A3%E4%BA%9B%E5%8A%A0%E5%A1%9E%E7%8B%97/
+ 今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~
]]>
生活
+ 开车
生活
- 换车牌
+ 开车
+ 加塞
+ 糟心事
+ 规则
- 聊聊那些加塞狗
- /2021/01/17/%E8%81%8A%E8%81%8A%E9%82%A3%E4%BA%9B%E5%8A%A0%E5%A1%9E%E7%8B%97/
- 今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~
+ 聊聊这次换车牌及其他
+ /2022/02/20/%E8%81%8A%E8%81%8A%E8%BF%99%E6%AC%A1%E6%8D%A2%E8%BD%A6%E7%89%8C%E5%8F%8A%E5%85%B6%E4%BB%96/
+ 去年 8 月份运气比较好,摇到了车牌,本来其实应该很早就开始摇的,前面第一次换工作没注意社保断缴了一个月,也是大意失荆州,后面到了 17 年社保满两年了,好像只摇了一次,还是就没摇过,有点忘了,好像是什么原因导致那次也没摇成功,但是后面暂住证就取消了,需要居住证,居住证又要一年及以上的租房合同,并且那会买车以后也不怎么开,住的地方车位还好,但是公司车位一个月要两三千,甚至还是打车上下班比较实惠,所以也没放在心上,后面摇到房以后,也觉得应该准备起来车子,就开始办了居住证,居住证其实还可以用劳动合同,而且办起来也挺快,大概是三四月份开始摇,到 8 月份的某一天收到短信说摇到了,一开始还挺开心,不过心里抱着也不怎么开,也没怎么大放在心上,不过这里有一点就是我把那个照片直接发出去,上面有着我的身份证号,被 LD 说了一顿,以后也应该小心点,但是后面不知道是哪里看了下,说杭州上牌已经需要国六标准的车了,瞬间感觉是空欢喜了,可是有同事说是可以的,我就又打了官方的电话,结果说可以的,要先转籍,然后再做上牌。
+转籍其实是很方便的,在交警 12123 App 上申请就行了,在转籍以后,需要去实地验车,验车的话,在支付宝-杭州交警生活号里进行预约,找就近的车管所就好,需要准备一些东西,首先是行驶证,机动车登记证书,身份证,居住证,还有车上需要准备的东西是要有三脚架和反光背心,反光背心是最近几个月开始要的,问过之前去验车的只需要三脚架就好了,预约好了的话建议是赶上班时间越早越好,不然过去排队时间要很久,而且人多了以后会很乱,各种插队,而且有很多都是汽车销售,一个销售带着一堆车,我们附近那个进去的小路没一会就堵满车,进去需要先排队,然后扫码,接着交资料,这两个都排着队,如果去晚了就要排很久的队,交完资料才是排队等验车,验车就是打开引擎盖,有人会帮忙拓印发动机车架号,然后验车的会各种检查一下,车里面,还有后备箱,建议车内整理干净点,后备箱不要放杂物,检验完了之后,需要把三脚架跟反光背心放在后备箱盖子上,人在旁边拍个照,然后需要把车牌遮住后再拍个车子的照片,再之后就是去把车牌卸了,这个多吐槽下,那边应该是本来那边师傅帮忙卸车牌,结果他就说是教我们拆,虽然也不算难,但是不排除师傅有在偷懒,完了之后就是把旧车牌交回去,然后需要在手机上(警察叔叔 App)提交各种资料,包括身份证,行驶证,机动车登记证书,提交了之后就等寄车牌过来了。
+这里面缺失的一个环节就是选号了,选号杭州有两个方式,一种就是根据交管局定期发布的选号号段,可以自定义拼 20 个号,在手机上的交警 12123 App 上可以三个一组的形式提交,如果有没被选走的,就可以预选到这个了,但是这种就是也需要有一定策略,最新出的号段能选中的概率大一点,然后数字全是 8,6 这种的肯定会一早就被选走,然后如果跟我一样可以提前选下尾号,因为尾号数字影响限号,我比较有可能周五回家,所以得避开 5,0 的,第二种就是 50 选一跟以前新车选号一样,就不介绍了。第一种选中了以后可以在前面交还旧车牌的时候填上等着寄过来了,因为我是第一种选中的,第二种也可以在手机上选,也在可以在交还车牌的时候现场选。
+总体过程其实是 LD 在各种查资料跟帮我跑来跑去,要不是 LD,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。
]]>
生活
- 开车
生活
- 开车
- 加塞
- 糟心事
- 规则
+ 换车牌
@@ -19442,34 +19407,39 @@ $
- 聊聊最近平淡的生活之看看老剧
- /2021/11/21/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E7%9C%8B%E8%80%81%E5%89%A7/
- 最近因为也没什么好看的新剧和综艺所以就看看一些以前看过的老剧,我是个非常念旧的人吧,很多剧都会反反复复地看,一方面之前看过觉得好看的的确是一直记着,还有就是平时工作完了回来就想能放松下,剧情太纠结的,太烧脑的都不喜欢,也就是我常挂在口头的不喜欢看费脑子的剧,跟我不喜欢狼人杀的原因也类似。
-前面其实是看的太阳的后裔,跟 LD 一起看的,之前其实算是看过一点,但是没有看的很完整,并且很多剧情也忘了,只是这个我我可能看得更少一点,因为最开始的时候觉得男主应该是男二,可能对长得这样的男主并且是这样的人设有点失望,感觉不是特别像个特种兵,但是由于本来也比较火,而且 LD 比较喜欢就从这个开始看了,有两个点是比较想说的
-韩剧虽然被吐槽的很多,但是很多剧的质量,情节把控还是优于目前非常多国内剧的,相对来说剧情发展的前后承接不是那么硬凹出来的,而且人设都立得住,这个是非常重要的,很多国内剧怎么说呢,就是当爹的看起来就比儿子没大几岁,三四十岁的人去演一个十岁出头的小姑娘,除非容貌异常,比如刘晓庆这种,不然就会觉得导演在把我们观众当傻子。瞬间就没有想看下去的欲望了。
-再一点就是情节是大众都能接受度比较高的,现在有很多普遍会找一些新奇的视角,比如卖腐,想某某令,两部都叫某某令,这其实是一个点,延伸出去就是跟前面说的一点有点类似,xx 老祖,人看着就二三十,叫 xx 老祖,(喜欢的人轻喷哈)然后名字有一堆,同一个人物一会叫这个名字,一会又叫另一个名字,然后一堆死表情。
-因为今天有个特殊的事情发生,所以简短的写(shui)一篇了
+ 聊聊如何识别和意识到日常生活中的各类危险
+ /2021/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/
+ 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。
]]>
生活
生活
- 看剧
+ 糟心事
+ 电瓶车
+ 扶梯
+ 踩踏
+ 安全
- 聊聊最近平淡的生活之《花束般的恋爱》观后感
- /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 全家桶
+ 修电脑的
@@ -19606,18 +19576,6 @@ $
dubbo
-
- 记录一次折腾自组 nas 的失败经历-续续篇
- /2023/05/28/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86-%E7%BB%AD%E7%BB%AD%E7%AF%87/
- 之前这个机器已经算是跑起来了,虽然不是很完善也不是最佳实践,不过这篇可能也不算是失败经历了,因为最后成功跑起来了,在没法装最新版的 exsi 的情况下,并且我后面买的华硕 z370 主板点不亮,所以我也有点死心就直接用Windows 下装 vmware workstation 装虚拟机,然后直通硬盘来做 nas,这样可能对于其他人来说是很垃圾的方案,不过因为我很多常用软件的都是在 Windows 环境下的,并且纯黑裙的环境会比较浪费,相比一些同学在群晖里安装 Windows 虚拟机,我觉得还是反过来比较好,毕竟 vmware 做虚拟机应该是比群晖专业点,不过这个方案也有一些问题
第一种方式是直接找网上同学分享的处理好引导的 vmx,这种我碰到了一个问题就是在打开虚拟机安装群晖系统.pat的时候会提示“无法安装此文件,文件可能已损坏”,这其实不是真的文件已损坏,应该是群晖在做校验的时候存在什么条件没有通过,尝试了断网等方式都不成功,所以后来就用了比较釜底抽薪的方案,直接使用大佬开源的 arpl 引导制作工具
第二种方式一开始是躺在我 B 站收藏夹里,有个 up 制作的,做得很细致,也把很多细节也解释了,过程其实不难,就是按步骤一步步执行,但是一开始选择了 918+的系统在我的方案里安装不了,会提示无法安装,经过视频下的评论的知道,尝试使用 920+的系统就顺利安装成功了,这里唯一的区别就是在添加硬盘的时候要选择物理磁盘,然后 vmware 给出的硬盘选项是Physical0, Physical1,记得别选错了,然后在启动后我租了 raid5,4 块 4T 的盘,可以组成一个 10T 多一点的存储空间,打算用来作为比较长读写的区域,更大的盘可能就会作为只读区域,减小写入量。后面还有一些问题待解决,一个是电源,考虑换个稍好一点,因为目前看下来电源风扇的噪音比较大,还有就是主板,最近看中了微星的 z390,不过价格比较贵,打算慢慢蹲蹲看。
-]]>
-
- nas
-
-
- nas
-
-
记录一次折腾自组 nas 的失败经历-续篇
/2023/05/14/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86-%E7%BB%AD%E7%AF%87/
@@ -19631,78 +19589,39 @@ $
- 记录下 Java Stream 的一些高效操作
- /2022/05/15/%E8%AE%B0%E5%BD%95%E4%B8%8B-Java-Lambda-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%AB%98%E6%95%88%E6%93%8D%E4%BD%9C/
- 我们日常在代码里处理一些集合逻辑的时候用到 Stream 其实还挺多的,普通的取值过滤集合一般都是结合 ide 的提示就能搞定了,但是有些不太常用的就在这记录下,争取后面都更新记录下来。
-自定义 distinctByKey 对结果进行去重
stream 中自带的 distinct 只能对元素进行去重
比如下面代码
-public static void main(String[] args) {
- List<Integer> list = new ArrayList<>();
- list.add(1);
- list.add(1);
- list.add(2);
- list = list.stream().distinct().collect(Collectors.toList());
- System.out.println(list);
- }
-结果就是去了重的
-[1, 2]
-但是当我的元素是个复杂对象,我想根据对象里的某个元素进行过滤的时候,就需要用到自定义的 distinctByKey 了,比如下面的想对 userId 进行去重
-public static void main(String[] args) {
- List<StudentRecord> list = new ArrayList<>();
- StudentRecord s1 = new StudentRecord();
- s1.setUserId(11L);
- s1.setCourseId(100L);
- s1.setScore(100);
- list.add(s1);
- StudentRecord s2 = new StudentRecord();
- s2.setUserId(11L);
- s2.setCourseId(101L);
- s2.setScore(100);
- list.add(s2);
- StudentRecord s3 = new StudentRecord();
- s3.setUserId(12L);
- s3.setCourseId(100L);
- s3.setScore(100);
- list.add(s3);
- System.out.println(list.stream().distinct().collect(Collectors.toList()));
- }
- @Data
- static class StudentRecord {
- Long id;
- Long userId;
- Long courseId;
- Integer score;
- }
-结果就是
-[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
-因为对象都不一样,所以就没法去重了,这里就需要用
-public static <T> Predicate<T> distinctByKey(
- Function<? super T, ?> keyExtractor) {
-
- Map<Object, Boolean> seen = new ConcurrentHashMap<>();
- return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
- }
-然后就可以用它来去重了
-System.out.println(list.stream().filter(distinctByKey(StudentRecord::getUserId)).collect(Collectors.toList()));
-看下结果
-[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
-但是说实在的这个功能感觉应该是 stream 默认给实现的
-使用 java.util.stream.Collectors#groupingBy 对 list 进行分组
这个使用场景还是蛮多的,上面的场景里比如我要对 userId 进行分组,就一行代码就解决了
-System.out.println(list.stream().collect(Collectors.groupingBy(StudentRecord::getUserId)));
-结果
{11=[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100)], 12=[StudentRecord(id=null, userId=12, courseId=100, score=100)]}
-很方便的变成了以 userId 作为 key,以相同 userId 的 StudentRecord 的 List 作为 value 的 map 结构
+ 聊聊厦门旅游的好与不好
+ /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 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆
+灵玲马戏团
对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。
+总结
厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。
]]>
- java
+ 生活
+ 旅游
- java
- stream
+ 生活
+ 杭州
+ 旅游
+ 厦门
+ 中山路
+ 局口街
+ 鼓浪屿
+ 曾厝垵
+ 植物园
+ 马戏团
+ 沙茶面
+ 海蛎煎
- 记录一次折腾自组 nas 的失败经历
- /2023/05/07/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86/
- 鉴于现在市面上的成品 nas 对于我来说要不就是太贵了,要不就是便宜的盘位少,性能比较差,很多 nas 主打还有用 docker 什么的,但是性能对于我个人比较特殊的使用方式来说还是不太够用的,比如现在比较性能好的 nas 像绿联新出的 DX4600,用的是N5105,passmark 分数还不如我 15 年买的 pc 上的 i5 4590,当然很多人是考虑功耗,这也是萝卜青菜各有所爱,可能我算下来还是觉得没多大必要
然后就是考虑用什么硬件配置了,这个流派也有很多种,
用蜗牛星际的原版硬件其实对于需求不大的也是挺好的,整套的都解决了,cpu 用 j1900 如果就做 nas 应该也够了,我没选的原因一方面是性能不符合我的要求,另一方面是现在市面上的机器大部分都是战损成色,而且也不太便宜,如果成色比较好的能够 400 以内拿下整机的话感觉还算可以,cpu 换成 j4125 或者 j3455 再加个 100 也能接受,但基本比较少有这种价格,之前看到一个换了 j3455 的只要 360,犹豫了下没下手,其他很多的都是 j1900 的都要 600 左右
然后是各类 E3,E5 和商用服务器类型的,这种的特点是功耗大,其实 cpu 很便宜,E5 2630V3 跟 2630V4 都只要十几块钱,性能过得去,没有选的主要是机箱占地方也比较贵,还有是商用的怕很多系统需要自己找驱动什么的,配件比如 HBA 卡这种,买的不兼容什么的还是挺麻烦的,另外噪音也是个比较大的问题,租的房子比较小,即使放客厅也是靠着卧室的墙边,如果以后换大一点的房子倒是可以考虑
最后就是我目前选的方案,就是普通的民用机器,找盘位多一点的机箱,我原来的 4590 的机器的机箱就不错,但是已经停产了,二手的太重了闲鱼都不出外地,cpu 跟主板其实考虑了很久,因为从 4590 开始核显就能硬解 H264 这种常规的视频了,考虑用intel 四代的 i3 或者 i5 应该纯 nas 来讲是足够用了,但是这样就跟我现在已经有的 4590 有点重叠了,而且也觉得最好是能性能好一些的,就开始看一些稍新一点的,很多用的多的有 i3-8100,跟 i3-10100 这种,但是这些已经被炒的价格比较高,寻寻觅觅了很久看中了 i5-8600,这个价格跟 8500 差不多,性能还好一些,主板就是我标题说的最“失败”的一个点了,主要是因为主板自带的网卡是 Realtek 的,至于更具体的后面会专门介绍,机箱是图便宜买的爱国者半岛铁盒的 F10,内存就买了一根光威的 32g 的,装系统的硬盘是用了以前囤的京东京造的麒麟系列,但是现在对这个系列挺不看好,之前有个盘就掉盘了,维修体验一开始也不好,半个多月维修,电源也是图便宜买的一个先马的平头哥系列,额定 550w 只要 140 左右,整体的机器就攒齐了,但是很多问题也随之出现了
第一个问题是买的二手的板 U 套装,结果寄过来的时候没带挡板,导致一开始装上了又要拆下来;第二个问题是主机贪便宜,主机上固定主板螺丝的螺柱拧不进去,后来店家告诉我让我可以用电源固定的螺丝先拧一下才把螺柱拧进去;第三个问题是因为没什么装机经验导致的,散热装的太累了,因为要兼容各种主板,还有各种螺丝,装的时候也着急;第四个问题是电源的比较便宜,一方面比较不放心安全性,另一方面是我想多装几个硬盘,电源直出的只有四个 sata 口,需要买转接线,从 D 型的 4pin 口子转出来;第五个问题也是电源,主板电源线有点不太够,走背线就比较困难
以上主要是装机的困难,下一篇介绍作为 nas 的各种问题吧
+ 记录一次折腾自组 nas 的失败经历-续续篇
+ /2023/05/28/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86-%E7%BB%AD%E7%BB%AD%E7%AF%87/
+ 之前这个机器已经算是跑起来了,虽然不是很完善也不是最佳实践,不过这篇可能也不算是失败经历了,因为最后成功跑起来了,在没法装最新版的 exsi 的情况下,并且我后面买的华硕 z370 主板点不亮,所以我也有点死心就直接用Windows 下装 vmware workstation 装虚拟机,然后直通硬盘来做 nas,这样可能对于其他人来说是很垃圾的方案,不过因为我很多常用软件的都是在 Windows 环境下的,并且纯黑裙的环境会比较浪费,相比一些同学在群晖里安装 Windows 虚拟机,我觉得还是反过来比较好,毕竟 vmware 做虚拟机应该是比群晖专业点,不过这个方案也有一些问题
第一种方式是直接找网上同学分享的处理好引导的 vmx,这种我碰到了一个问题就是在打开虚拟机安装群晖系统.pat的时候会提示“无法安装此文件,文件可能已损坏”,这其实不是真的文件已损坏,应该是群晖在做校验的时候存在什么条件没有通过,尝试了断网等方式都不成功,所以后来就用了比较釜底抽薪的方案,直接使用大佬开源的 arpl 引导制作工具
第二种方式一开始是躺在我 B 站收藏夹里,有个 up 制作的,做得很细致,也把很多细节也解释了,过程其实不难,就是按步骤一步步执行,但是一开始选择了 918+的系统在我的方案里安装不了,会提示无法安装,经过视频下的评论的知道,尝试使用 920+的系统就顺利安装成功了,这里唯一的区别就是在添加硬盘的时候要选择物理磁盘,然后 vmware 给出的硬盘选项是Physical0, Physical1,记得别选错了,然后在启动后我租了 raid5,4 块 4T 的盘,可以组成一个 10T 多一点的存储空间,打算用来作为比较长读写的区域,更大的盘可能就会作为只读区域,减小写入量。后面还有一些问题待解决,一个是电源,考虑换个稍好一点,因为目前看下来电源风扇的噪音比较大,还有就是主板,最近看中了微星的 z390,不过价格比较贵,打算慢慢蹲蹲看。
]]>
nas
@@ -19712,61 +19631,21 @@ $
- 记录下 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");
- }
-}
-因为我是想写个重命名的小工具,希望通过setUp和tearDown做一些文件初始化和清理工作,但是我把两个case的初始化跟清理工作写到了单个setUp和tearDown中,这样就出现了异常的错误
通过上面的示例代码,可以看到执行结果
-❯ 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方法前后都执行setUp和tearDown
+ 记录一次折腾自组 nas 的失败经历-续续续篇
+ /2023/06/18/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86-%E7%BB%AD%E7%BB%AD%E7%BB%AD%E7%AF%87/
+ 最近在搞 nas 的时候又翻了个很大的错误,因为前面说了正在用的技嘉的 z370m, 这个主板是跟 cpu 一起买的,如果是只是用在 Windows 环境,没什么扩展要求,或者只用 6 个sata盘位,用一个 ssd 做系统盘,是挺不错的,但是如果是像我这样的要搞盘位比较多的nas,发现有一系列的不足点,
1、 前面有讲过,网卡问题,用的是 RTL 瑞昱的网卡,目前知道的最高能支持的 exsi 的版本是 6.7,再高的版本就不支持了,不过其实真的想用的话,6.7 版本也是可以用的,只是我当时是打包的 8.0 的系统,个人也不太喜欢用比较低版本的,所以算是一个不足点
2、 第二点是m.2插槽,只有一个m.2也是很难理解的,一个做系统盘,另一个可以作为简单的数组存储,或者作为群晖的下载缓存,如果只有一个的话特别是现在这个 ssd 的价格这么低了,加一个还是很香的,这个问题跟后面也是有点关系的
3、 PCIE 的插槽,这个主板只支持两个 PCIE 3.0 x1 的,它能支持 1Gbps 的传输速度,后面我买了个 PCIE 3.0 x1 转 m.2 的转接卡,这样其实真的是把速度拖慢了很多,还有一个 PCIE 3.0 X16 的,这是用来装显卡的,只用来扩展做个m.2或者 sata 又感觉很浪费,而且说不定未来会装个显卡
4、 风扇口的问题,风扇接口其实原来一直没概念,我在这次装 nas 之前其实自己没有完整装个台式机过,之前买的目前在使用的主力机两个风扇在换了的时候才知道是用的大 4pin 接口串接的,其中一个有 pwm 的接口也没用,这样就没办法用主板自带调速软件来进行自动或者手动调速,有时候比如晚上不关机,放在房间里风扇声很大还是挺吵的,这个主板只有两个风扇接口,一个是 cpu 的,一个是系统风扇,其实就只有一个可以用了
主要是以上几个点,所以我一直在关注二手主板,之前买了个华硕的 z370 tuf gaming,没想到点不亮,也不知道什么原因,还害我吓得以为 cpu 被我弄坏了,后面慢慢在网上看着发现有一块微星的 z390 TOMAHAWK战斧导弹,各方面接口都很不错,而且还有个千兆网口,在闲鱼蹲了很久终于在前不久蹲到了,买回来赶紧看看能不能点亮,结果一把点亮,还是用的螺丝刀开机的,就心里暗爽感觉捡到了宝,但是装进我的半岛铁盒 F10 机箱就发现跪了,出现的这个问题原来在网上哪里看到过,一直没注意,就是 sata 接口的方向,因为一个是 matx 的板,装进去即使 sata 接口的方向是跟主板平行的也留着足够的空间,但是 atx 板装上之后,大概只有 2 厘米左右的空间并且是两个叠着的端口,一个可能要能硬拗一下,但是两个我试了下,只能识别出一个,这个真让我翻了个大车,而且其实我换这个主板其实还有个大问题,就是我是在 Windows 下的vmware workstation 虚拟机里通过sata 直通,但是这个是认硬盘接口的,也就是顺序是有影响的,换了主板发现也不能支持,后面可能真的想用的话还得重新搞黑裙,要把数据备份出来,幸好换回原来的主板还能用,所以硬件也不能只看性能和接口丰富度,要看适配的其他硬件的合适度以及原先已经有的软件系统
]]>
- php
+ nas
- php
+ nas
- 记录一次折腾自组 nas 的失败经历-续续续篇
- /2023/06/18/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86-%E7%BB%AD%E7%BB%AD%E7%BB%AD%E7%AF%87/
- 最近在搞 nas 的时候又翻了个很大的错误,因为前面说了正在用的技嘉的 z370m, 这个主板是跟 cpu 一起买的,如果是只是用在 Windows 环境,没什么扩展要求,或者只用 6 个sata盘位,用一个 ssd 做系统盘,是挺不错的,但是如果是像我这样的要搞盘位比较多的nas,发现有一系列的不足点,
1、 前面有讲过,网卡问题,用的是 RTL 瑞昱的网卡,目前知道的最高能支持的 exsi 的版本是 6.7,再高的版本就不支持了,不过其实真的想用的话,6.7 版本也是可以用的,只是我当时是打包的 8.0 的系统,个人也不太喜欢用比较低版本的,所以算是一个不足点
2、 第二点是m.2插槽,只有一个m.2也是很难理解的,一个做系统盘,另一个可以作为简单的数组存储,或者作为群晖的下载缓存,如果只有一个的话特别是现在这个 ssd 的价格这么低了,加一个还是很香的,这个问题跟后面也是有点关系的
3、 PCIE 的插槽,这个主板只支持两个 PCIE 3.0 x1 的,它能支持 1Gbps 的传输速度,后面我买了个 PCIE 3.0 x1 转 m.2 的转接卡,这样其实真的是把速度拖慢了很多,还有一个 PCIE 3.0 X16 的,这是用来装显卡的,只用来扩展做个m.2或者 sata 又感觉很浪费,而且说不定未来会装个显卡
4、 风扇口的问题,风扇接口其实原来一直没概念,我在这次装 nas 之前其实自己没有完整装个台式机过,之前买的目前在使用的主力机两个风扇在换了的时候才知道是用的大 4pin 接口串接的,其中一个有 pwm 的接口也没用,这样就没办法用主板自带调速软件来进行自动或者手动调速,有时候比如晚上不关机,放在房间里风扇声很大还是挺吵的,这个主板只有两个风扇接口,一个是 cpu 的,一个是系统风扇,其实就只有一个可以用了
主要是以上几个点,所以我一直在关注二手主板,之前买了个华硕的 z370 tuf gaming,没想到点不亮,也不知道什么原因,还害我吓得以为 cpu 被我弄坏了,后面慢慢在网上看着发现有一块微星的 z390 TOMAHAWK战斧导弹,各方面接口都很不错,而且还有个千兆网口,在闲鱼蹲了很久终于在前不久蹲到了,买回来赶紧看看能不能点亮,结果一把点亮,还是用的螺丝刀开机的,就心里暗爽感觉捡到了宝,但是装进我的半岛铁盒 F10 机箱就发现跪了,出现的这个问题原来在网上哪里看到过,一直没注意,就是 sata 接口的方向,因为一个是 matx 的板,装进去即使 sata 接口的方向是跟主板平行的也留着足够的空间,但是 atx 板装上之后,大概只有 2 厘米左右的空间并且是两个叠着的端口,一个可能要能硬拗一下,但是两个我试了下,只能识别出一个,这个真让我翻了个大车,而且其实我换这个主板其实还有个大问题,就是我是在 Windows 下的vmware workstation 虚拟机里通过sata 直通,但是这个是认硬盘接口的,也就是顺序是有影响的,换了主板发现也不能支持,后面可能真的想用的话还得重新搞黑裙,要把数据备份出来,幸好换回原来的主板还能用,所以硬件也不能只看性能和接口丰富度,要看适配的其他硬件的合适度以及原先已经有的软件系统
+ 记录一次折腾自组 nas 的失败经历
+ /2023/05/07/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%87%AA%E7%BB%84-nas-%E7%9A%84%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%8E%86/
+ 鉴于现在市面上的成品 nas 对于我来说要不就是太贵了,要不就是便宜的盘位少,性能比较差,很多 nas 主打还有用 docker 什么的,但是性能对于我个人比较特殊的使用方式来说还是不太够用的,比如现在比较性能好的 nas 像绿联新出的 DX4600,用的是N5105,passmark 分数还不如我 15 年买的 pc 上的 i5 4590,当然很多人是考虑功耗,这也是萝卜青菜各有所爱,可能我算下来还是觉得没多大必要
然后就是考虑用什么硬件配置了,这个流派也有很多种,
用蜗牛星际的原版硬件其实对于需求不大的也是挺好的,整套的都解决了,cpu 用 j1900 如果就做 nas 应该也够了,我没选的原因一方面是性能不符合我的要求,另一方面是现在市面上的机器大部分都是战损成色,而且也不太便宜,如果成色比较好的能够 400 以内拿下整机的话感觉还算可以,cpu 换成 j4125 或者 j3455 再加个 100 也能接受,但基本比较少有这种价格,之前看到一个换了 j3455 的只要 360,犹豫了下没下手,其他很多的都是 j1900 的都要 600 左右
然后是各类 E3,E5 和商用服务器类型的,这种的特点是功耗大,其实 cpu 很便宜,E5 2630V3 跟 2630V4 都只要十几块钱,性能过得去,没有选的主要是机箱占地方也比较贵,还有是商用的怕很多系统需要自己找驱动什么的,配件比如 HBA 卡这种,买的不兼容什么的还是挺麻烦的,另外噪音也是个比较大的问题,租的房子比较小,即使放客厅也是靠着卧室的墙边,如果以后换大一点的房子倒是可以考虑
最后就是我目前选的方案,就是普通的民用机器,找盘位多一点的机箱,我原来的 4590 的机器的机箱就不错,但是已经停产了,二手的太重了闲鱼都不出外地,cpu 跟主板其实考虑了很久,因为从 4590 开始核显就能硬解 H264 这种常规的视频了,考虑用intel 四代的 i3 或者 i5 应该纯 nas 来讲是足够用了,但是这样就跟我现在已经有的 4590 有点重叠了,而且也觉得最好是能性能好一些的,就开始看一些稍新一点的,很多用的多的有 i3-8100,跟 i3-10100 这种,但是这些已经被炒的价格比较高,寻寻觅觅了很久看中了 i5-8600,这个价格跟 8500 差不多,性能还好一些,主板就是我标题说的最“失败”的一个点了,主要是因为主板自带的网卡是 Realtek 的,至于更具体的后面会专门介绍,机箱是图便宜买的爱国者半岛铁盒的 F10,内存就买了一根光威的 32g 的,装系统的硬盘是用了以前囤的京东京造的麒麟系列,但是现在对这个系列挺不看好,之前有个盘就掉盘了,维修体验一开始也不好,半个多月维修,电源也是图便宜买的一个先马的平头哥系列,额定 550w 只要 140 左右,整体的机器就攒齐了,但是很多问题也随之出现了
第一个问题是买的二手的板 U 套装,结果寄过来的时候没带挡板,导致一开始装上了又要拆下来;第二个问题是主机贪便宜,主机上固定主板螺丝的螺柱拧不进去,后来店家告诉我让我可以用电源固定的螺丝先拧一下才把螺柱拧进去;第三个问题是因为没什么装机经验导致的,散热装的太累了,因为要兼容各种主板,还有各种螺丝,装的时候也着急;第四个问题是电源的比较便宜,一方面比较不放心安全性,另一方面是我想多装几个硬盘,电源直出的只有四个 sata 口,需要买转接线,从 D 型的 4pin 口子转出来;第五个问题也是电源,主板电源线有点不太够,走背线就比较困难
以上主要是装机的困难,下一篇介绍作为 nas 的各种问题吧
]]>
nas
@@ -19824,19 +19703,15 @@ zk3 192.168.2.3
- 解决 网络文件夹目前是以其他用户名和密码进行映射的 问题
- /2023/04/09/%E8%A7%A3%E5%86%B3-%E7%BD%91%E7%BB%9C%E6%96%87%E4%BB%B6%E5%A4%B9%E7%9B%AE%E5%89%8D%E6%98%AF%E4%BB%A5%E5%85%B6%E4%BB%96%E7%94%A8%E6%88%B7%E5%90%8D%E5%92%8C%E5%AF%86%E7%A0%81%E8%BF%9B%E8%A1%8C%E6%98%A0%E5%B0%84%E7%9A%84/
- 之前在使用 smb 协议在 Windows 中共享磁盘使用映射网络驱动器的时候,如果前一次登录过账号密码后面有了改动,或者前一次改错了,
就会出现这样的提示
![]()
应该是 Windows 已经把之前的连接记录下来了,即使是链接不成功的
可以通过在 cmd 或者 powershell 执行 net use 命令查看当前已经连接的
![]()
这样就可以用命令来把这个删除
net use [NETNAME] /delete
比如这边就可以
net use \\xxxxxxxx\f /delete
然后再重新输入账号密码就好了
关于net use的命令使用方式可以参考
-net use [{<DeviceName> | *}] [\\<ComputerName>\<ShareName>[\<volume>]] [{<Password> | *}]] [/user:[<DomainName>\]<UserName] >[/user:[<DottedDomainName>\]<UserName>] [/user: [<UserName@DottedDomainName>] [/savecred] [/smartcard] [{/delete | /persistent:{yes | no}}]
-net use [<DeviceName> [/home[{<Password> | *}] [/delete:{yes | no}]]
-net use [/persistent:{yes | no}]
-
+ 记录下把小米路由器 4A 千兆版刷成 openwrt 的过程
+ /2023/05/21/%E8%AE%B0%E5%BD%95%E4%B8%8B%E6%8A%8A%E5%B0%8F%E7%B1%B3%E8%B7%AF%E7%94%B1%E5%99%A8-4A-%E5%8D%83%E5%85%86%E7%89%88%E5%88%B7%E6%88%90-openwrt-%E7%9A%84%E8%BF%87%E7%A8%8B/
+ 之前在绍兴家里的一条宽带送了个小米路由器 4A,正好原来的小米路由器 3 不知道为啥经常断流不稳定,而且只支持百兆,这边用了 200M 的宽带,感觉也比较浪费,所以就动了这个心思,但是还是有蛮多坑的,首先是看到了一篇文章,写的比较详细,
看到的就是这篇文章
这里使用的是 OpenWRTInvasion 这个项目来破解 ssh,首先这里有个最常见的一个问题,就是文件拉不到,所以有一些可行的方法就是自己起一个http 服务,可以修改脚本代码,直接从这个启动的 http 服务拉取已经下载下的文件,就这个问题我就尝试了很多次,还有就是这个 OpenWRTInvasion 最后一个支持 Windows 的版本就是 0.0.7,后面的版本其实做了很多的优化解决了文件的问题,一开始碰到的问题是本地起了文件服务但是没请求,或者请求了但后续 ssh 没有正常破解,我就换了 Mac 用最新版本的OpenWRTInvasion来尝试进行破解,发现还是不行,结果查了不少资料发现最根本的问题是这个路由器的新版本就不支持这种破解了,因为这个路由器新的版本都是 v2 版本,也就是2.30.x 版本的系统了,原来支持的是 2.28.x 的这些系统,后来幸好是找到了这个版本的系统支持的另一个恩山大神的文章,根据这个文章提供的工具进行破解就成功了,但是破解要多尝试几次,我第一次是失败的,小米路由器 4A 千兆版的版本号也会写作 R4Av2,在搜索一些资料的时候也可以用这个型号去搜,可能也是另一种黑话,路由器以前刷过梅林,padavan,还是第一次刷 openwrt,都已经忘了以前是怎么刷的来着,感觉现在越来越难刷了,特别是 ssh,想给我的 ax6 刷个 openwrt,发现前提是需要先有一个 openwrt 的路由器,简直了,变成先有鸡还是先有蛋的问题了,所以我把这个小米 4A 刷成 openwrt 也有这个考虑,毕竟 4A 配置上不太高,openwrt 各种插件可能还跑不起来,权当做练手和到时候用来开 AX6 的工具了。
]]>
- 技巧
+ 生活
- windows
+ 路由器
@@ -19874,6 +19749,72 @@ net use [/
看书
+
+ 记录下 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");
+ }
+}
+因为我是想写个重命名的小工具,希望通过setUp和tearDown做一些文件初始化和清理工作,但是我把两个case的初始化跟清理工作写到了单个setUp和tearDown中,这样就出现了异常的错误
通过上面的示例代码,可以看到执行结果
+❯ 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方法前后都执行setUp和tearDown
+]]>
+
+ php
+
+
+ php
+
+
+
+ 闲话篇-也算碰到了为老不尊和坏人变老了的典型案例
+ /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,感觉不是老太太拦着点他马上就要冲上来揍我了一样,事后我想想,这种情况我大概只能躺地上装死了,当这个事情发生之前我真的快把前面说的事情(水管阀坏了)给忘了,虽然这是理论上不该我来处理,除非是老头老太太请求我帮忙,这事后面我也从没说起过,本来完全没交集,对他们的是怎么样的人也没概念,总觉得年纪大了可能还比较心宽和蔼点,结果没想到就是一典型的坏人变老了,我说你们这么砸门,我老婆都被吓得不敢开门,结果对面老头老太太的儿子也出来了说,“我们就是敲下门,我母亲是机关单位退休的,所以肯定不会敲门很大声的,你老婆觉得吓到了是你们人生观价值观有问题”,听到这话我差点笑出来,连着两个可笑至极的脑残逻辑,无语他妈给无语开门,无语到家了。对门家我们之前有个印象就是因为我们都是顶楼,这边老小区以前都是把前后阳台包进来的,然后社区就来咨询大家的意见是不是统一把包进来的违建拆掉,还特地上来六楼跟他们说,结果对面的老头就说,“我要去住建局投诉你们”,本来这个事情是违法的,但是社区的意思也是征求各位业主的意见,结果感觉是社区上门强拆了一样,为老不尊,坏人变老了的典范了。
+]]>
+
+ 生活
+
+
+ 生活
+
+
闲聊下乘公交的用户体验
/2021/02/28/%E9%97%B2%E8%81%8A%E4%B8%8B%E4%B9%98%E5%85%AC%E4%BA%A4%E7%9A%84%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C/
@@ -19897,17 +19838,61 @@ net use [/
- 闲话篇-也算碰到了为老不尊和坏人变老了的典型案例
- /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 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。
+]]>
+
+ 生活
+
+
+ 生活
+
+
+
+ 记录下 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
+
+
+
+ 难得的大扫除
+ /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/
+ 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 LD 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。
]]>
生活
生活
+ 大扫除
+
+
+
+ 解决 网络文件夹目前是以其他用户名和密码进行映射的 问题
+ /2023/04/09/%E8%A7%A3%E5%86%B3-%E7%BD%91%E7%BB%9C%E6%96%87%E4%BB%B6%E5%A4%B9%E7%9B%AE%E5%89%8D%E6%98%AF%E4%BB%A5%E5%85%B6%E4%BB%96%E7%94%A8%E6%88%B7%E5%90%8D%E5%92%8C%E5%AF%86%E7%A0%81%E8%BF%9B%E8%A1%8C%E6%98%A0%E5%B0%84%E7%9A%84/
+ 之前在使用 smb 协议在 Windows 中共享磁盘使用映射网络驱动器的时候,如果前一次登录过账号密码后面有了改动,或者前一次改错了,
就会出现这样的提示
![]()
应该是 Windows 已经把之前的连接记录下来了,即使是链接不成功的
可以通过在 cmd 或者 powershell 执行 net use 命令查看当前已经连接的
![]()
这样就可以用命令来把这个删除
net use [NETNAME] /delete
比如这边就可以
net use \\xxxxxxxx\f /delete
然后再重新输入账号密码就好了
关于net use的命令使用方式可以参考
+net use [{<DeviceName> | *}] [\\<ComputerName>\<ShareName>[\<volume>]] [{<Password> | *}]] [/user:[<DomainName>\]<UserName] >[/user:[<DottedDomainName>\]<UserName>] [/user: [<UserName@DottedDomainName>] [/savecred] [/smartcard] [{/delete | /persistent:{yes | no}}]
+net use [<DeviceName> [/home[{<Password> | *}] [/delete:{yes | no}]]
+net use [/persistent:{yes | no}]
+
+]]>
+
+ 技巧
+
+
+ windows
@@ -19997,57 +19982,72 @@ OK (1 t
- 闲话篇-路遇神逻辑骑车带娃爹
- /2022/05/08/%E9%97%B2%E8%AF%9D%E7%AF%87-%E8%B7%AF%E9%81%87%E7%A5%9E%E9%80%BB%E8%BE%91%E9%AA%91%E8%BD%A6%E5%B8%A6%E5%A8%83%E7%88%B9/
- 周末吃完中饭去买菜,没想到碰到这个神(zhi)奇(zhang)大哥带着两个娃,在非机动车道虽然没有像上班高峰车那么多,但是有送外卖,各种叮咚买菜和普通像我这样骑电驴,骑自行车的人,我的情况可能还特殊点,前面说过电驴买了以后本来网上找到过怎么解除限速的,后面看了下,限速 25 虽然慢,但还是对安全很有好处的,我上下班也不赶这个时间,所以就没解除,其他路上的电瓶车包括这位带娃的大哥可能有不少都不符合国标的限速要求或者解除了限速,这些算是铺垫。
-那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。
-现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。
-说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。
-]]>
-
- 生活
-
-
- 生活
-
-
-
- 难得的大扫除
- /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/
- 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 LD 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。
-]]>
-
- 生活
-
-
- 生活
- 大扫除
-
-
-
- 记录下 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
-
-
-
- 记录下把小米路由器 4A 千兆版刷成 openwrt 的过程
- /2023/05/21/%E8%AE%B0%E5%BD%95%E4%B8%8B%E6%8A%8A%E5%B0%8F%E7%B1%B3%E8%B7%AF%E7%94%B1%E5%99%A8-4A-%E5%8D%83%E5%85%86%E7%89%88%E5%88%B7%E6%88%90-openwrt-%E7%9A%84%E8%BF%87%E7%A8%8B/
- 之前在绍兴家里的一条宽带送了个小米路由器 4A,正好原来的小米路由器 3 不知道为啥经常断流不稳定,而且只支持百兆,这边用了 200M 的宽带,感觉也比较浪费,所以就动了这个心思,但是还是有蛮多坑的,首先是看到了一篇文章,写的比较详细,
看到的就是这篇文章
这里使用的是 OpenWRTInvasion 这个项目来破解 ssh,首先这里有个最常见的一个问题,就是文件拉不到,所以有一些可行的方法就是自己起一个http 服务,可以修改脚本代码,直接从这个启动的 http 服务拉取已经下载下的文件,就这个问题我就尝试了很多次,还有就是这个 OpenWRTInvasion 最后一个支持 Windows 的版本就是 0.0.7,后面的版本其实做了很多的优化解决了文件的问题,一开始碰到的问题是本地起了文件服务但是没请求,或者请求了但后续 ssh 没有正常破解,我就换了 Mac 用最新版本的OpenWRTInvasion来尝试进行破解,发现还是不行,结果查了不少资料发现最根本的问题是这个路由器的新版本就不支持这种破解了,因为这个路由器新的版本都是 v2 版本,也就是2.30.x 版本的系统了,原来支持的是 2.28.x 的这些系统,后来幸好是找到了这个版本的系统支持的另一个恩山大神的文章,根据这个文章提供的工具进行破解就成功了,但是破解要多尝试几次,我第一次是失败的,小米路由器 4A 千兆版的版本号也会写作 R4Av2,在搜索一些资料的时候也可以用这个型号去搜,可能也是另一种黑话,路由器以前刷过梅林,padavan,还是第一次刷 openwrt,都已经忘了以前是怎么刷的来着,感觉现在越来越难刷了,特别是 ssh,想给我的 ax6 刷个 openwrt,发现前提是需要先有一个 openwrt 的路由器,简直了,变成先有鸡还是先有蛋的问题了,所以我把这个小米 4A 刷成 openwrt 也有这个考虑,毕竟 4A 配置上不太高,openwrt 各种插件可能还跑不起来,权当做练手和到时候用来开 AX6 的工具了。
+ 记录下 Java Stream 的一些高效操作
+ /2022/05/15/%E8%AE%B0%E5%BD%95%E4%B8%8B-Java-Lambda-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%AB%98%E6%95%88%E6%93%8D%E4%BD%9C/
+ 我们日常在代码里处理一些集合逻辑的时候用到 Stream 其实还挺多的,普通的取值过滤集合一般都是结合 ide 的提示就能搞定了,但是有些不太常用的就在这记录下,争取后面都更新记录下来。
+自定义 distinctByKey 对结果进行去重
stream 中自带的 distinct 只能对元素进行去重
比如下面代码
+public static void main(String[] args) {
+ List<Integer> list = new ArrayList<>();
+ list.add(1);
+ list.add(1);
+ list.add(2);
+ list = list.stream().distinct().collect(Collectors.toList());
+ System.out.println(list);
+ }
+结果就是去了重的
+[1, 2]
+但是当我的元素是个复杂对象,我想根据对象里的某个元素进行过滤的时候,就需要用到自定义的 distinctByKey 了,比如下面的想对 userId 进行去重
+public static void main(String[] args) {
+ List<StudentRecord> list = new ArrayList<>();
+ StudentRecord s1 = new StudentRecord();
+ s1.setUserId(11L);
+ s1.setCourseId(100L);
+ s1.setScore(100);
+ list.add(s1);
+ StudentRecord s2 = new StudentRecord();
+ s2.setUserId(11L);
+ s2.setCourseId(101L);
+ s2.setScore(100);
+ list.add(s2);
+ StudentRecord s3 = new StudentRecord();
+ s3.setUserId(12L);
+ s3.setCourseId(100L);
+ s3.setScore(100);
+ list.add(s3);
+ System.out.println(list.stream().distinct().collect(Collectors.toList()));
+ }
+ @Data
+ static class StudentRecord {
+ Long id;
+ Long userId;
+ Long courseId;
+ Integer score;
+ }
+结果就是
+[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
+因为对象都不一样,所以就没法去重了,这里就需要用
+public static <T> Predicate<T> distinctByKey(
+ Function<? super T, ?> keyExtractor) {
+
+ Map<Object, Boolean> seen = new ConcurrentHashMap<>();
+ return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
+ }
+然后就可以用它来去重了
+System.out.println(list.stream().filter(distinctByKey(StudentRecord::getUserId)).collect(Collectors.toList()));
+看下结果
+[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
+但是说实在的这个功能感觉应该是 stream 默认给实现的
+使用 java.util.stream.Collectors#groupingBy 对 list 进行分组
这个使用场景还是蛮多的,上面的场景里比如我要对 userId 进行分组,就一行代码就解决了
+System.out.println(list.stream().collect(Collectors.groupingBy(StudentRecord::getUserId)));
+结果
{11=[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100)], 12=[StudentRecord(id=null, userId=12, courseId=100, score=100)]}
+很方便的变成了以 userId 作为 key,以相同 userId 的 StudentRecord 的 List 作为 value 的 map 结构
]]>
- 生活
+ java
- 路由器
+ java
+ stream
diff --git a/sitemap.xml b/sitemap.xml
index ee496ef6f7..518d07d76a 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -4,7 +4,7 @@
https://nicksxs.me/2023/08/20/springboot-web-server-%E5%90%AF%E5%8A%A8%E9%80%BB%E8%BE%91/
- 2023-08-20
+ 2023-08-27
monthly
0.6
@@ -623,7 +623,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
@@ -632,7 +632,7 @@
- https://nicksxs.me/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/
+ https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/
2022-06-11
@@ -641,7 +641,7 @@
- https://nicksxs.me/2022/02/13/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%80/
+ https://nicksxs.me/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/
2022-06-11
@@ -668,7 +668,7 @@
- https://nicksxs.me/2021/07/04/Leetcode-42-%E6%8E%A5%E9%9B%A8%E6%B0%B4-Trapping-Rain-Water-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ https://nicksxs.me/2021/05/01/Leetcode-48-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F-Rotate-Image-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
2022-06-11
@@ -677,7 +677,7 @@
- https://nicksxs.me/2021/05/01/Leetcode-48-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F-Rotate-Image-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ https://nicksxs.me/2021/07/04/Leetcode-42-%E6%8E%A5%E9%9B%A8%E6%B0%B4-Trapping-Rain-Water-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
2022-06-11
@@ -713,7 +713,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
@@ -722,7 +722,7 @@
- https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/
+ 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
@@ -731,7 +731,7 @@
- https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
+ https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/
2022-06-11
@@ -821,7 +821,7 @@
- https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/
+ https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/
2022-06-11
@@ -830,7 +830,7 @@
- https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/
+ https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/
2022-06-11
@@ -839,7 +839,7 @@
- https://nicksxs.me/2020/11/22/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/
+ 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
@@ -848,7 +848,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/2020/11/22/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/
2022-06-11
@@ -857,7 +857,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/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/
2022-06-11
@@ -866,7 +866,7 @@
- https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/
+ https://nicksxs.me/2021/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
@@ -875,7 +875,7 @@
- https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/
+ https://nicksxs.me/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
@@ -884,7 +884,7 @@
- https://nicksxs.me/2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/
+ https://nicksxs.me/2021/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
@@ -893,7 +893,7 @@
- https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/
+ https://nicksxs.me/2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/
2022-06-11
@@ -902,7 +902,7 @@
- https://nicksxs.me/2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/
+ https://nicksxs.me/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/
2022-06-11
@@ -911,7 +911,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/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
@@ -920,7 +920,7 @@
- https://nicksxs.me/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/
+ https://nicksxs.me/2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/
2022-06-11
@@ -929,7 +929,7 @@
- https://nicksxs.me/2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/
+ https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/
2022-06-11
@@ -1802,7 +1802,7 @@
- https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/
+ https://nicksxs.me/2019/12/10/Redis-Part-1/
2020-01-12
@@ -1811,7 +1811,7 @@
- https://nicksxs.me/2019/12/10/Redis-Part-1/
+ https://nicksxs.me/2015/03/11/Reverse-Bits/
2020-01-12
@@ -1820,7 +1820,7 @@
- https://nicksxs.me/2015/03/11/Reverse-Bits/
+ https://nicksxs.me/2015/03/13/Reverse-Integer/
2020-01-12
@@ -1829,7 +1829,7 @@
- https://nicksxs.me/2015/03/13/Reverse-Integer/
+ https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/
2020-01-12
@@ -1847,7 +1847,7 @@
- https://nicksxs.me/2015/01/14/Two-Sum/
+ https://nicksxs.me/2016/09/29/binary-watch/
2020-01-12
@@ -1856,7 +1856,7 @@
- https://nicksxs.me/2016/09/29/binary-watch/
+ https://nicksxs.me/2016/08/14/docker-mysql-cluster/
2020-01-12
@@ -1865,7 +1865,7 @@
- https://nicksxs.me/2016/08/14/docker-mysql-cluster/
+ https://nicksxs.me/2015/01/14/Two-Sum/
2020-01-12
@@ -1901,7 +1901,7 @@
- https://nicksxs.me/2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/
+ https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/
2020-01-12
@@ -1910,7 +1910,7 @@
- https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/
+ https://nicksxs.me/2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/
2020-01-12
@@ -1919,7 +1919,7 @@
- https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/
+ https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/
2020-01-12
@@ -1982,7 +1982,7 @@
- https://nicksxs.me/2015/01/04/Path-Sum/
+ https://nicksxs.me/2015/03/11/Number-Of-1-Bits/
2020-01-12
@@ -1991,7 +1991,7 @@
- https://nicksxs.me/2015/03/11/Number-Of-1-Bits/
+ https://nicksxs.me/2015/01/04/Path-Sum/
2020-01-12
@@ -2000,7 +2000,7 @@
- https://nicksxs.me/2015/06/22/invert-binary-tree/
+ https://nicksxs.me/2014/12/23/my-new-post/
2020-01-12
@@ -2009,7 +2009,7 @@
- https://nicksxs.me/2014/12/23/my-new-post/
+ https://nicksxs.me/2015/06/22/invert-binary-tree/
2020-01-12
@@ -2101,7 +2101,7 @@
https://nicksxs.me/
- 2023-08-20
+ 2023-08-27
daily
1.0
@@ -2109,2072 +2109,2072 @@
https://nicksxs.me/tags/%E7%94%9F%E6%B4%BB/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/2020/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/2021/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%8B%96%E6%9B%B4/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Java/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/JVM/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/C/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/leetcode/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/java/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/
- 2023-08-20
+ https://nicksxs.me/tags/Binary-Tree/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Linked-List/
- 2023-08-20
+ https://nicksxs.me/tags/DFS/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Binary-Tree/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/DFS/
- 2023-08-20
+ https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/
- 2023-08-20
+ https://nicksxs.me/tags/Linked-List/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/nas/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/
- 2023-08-20
+ https://nicksxs.me/tags/2019/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/2019/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/
- 2023-08-20
+ https://nicksxs.me/tags/c/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/
- 2023-08-20
+ https://nicksxs.me/tags/2022/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/c/
- 2023-08-20
+ https://nicksxs.me/tags/2023/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%B9%B6%E5%8F%91/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/j-u-c/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/aqs/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/condition/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/await/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/signal/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/lock/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/unlock/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Apollo/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/value/
- 2023-08-20
+ https://nicksxs.me/tags/environment/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/
- 2023-08-20
+ https://nicksxs.me/tags/value/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/environment/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Stream/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Comparator/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%8E%92%E5%BA%8F/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/sort/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/nullsfirst/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Disruptor/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/tags/Filter/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Interceptor/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/AOP/
- 2023-08-20
+ https://nicksxs.me/tags/Disruptor/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Spring/
- 2023-08-20
+ https://nicksxs.me/tags/Dubbo/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Tomcat/
- 2023-08-20
+ https://nicksxs.me/tags/RPC/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Servlet/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Web/
- 2023-08-20
+ https://nicksxs.me/tags/Filter/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Dubbo/
- 2023-08-20
+ https://nicksxs.me/tags/Interceptor/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/RPC/
- 2023-08-20
+ https://nicksxs.me/tags/AOP/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/
- 2023-08-20
+ https://nicksxs.me/tags/Spring/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/G1/
- 2023-08-20
+ https://nicksxs.me/tags/Tomcat/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/GC/
- 2023-08-20
+ https://nicksxs.me/tags/Servlet/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Garbage-First-Collector/
- 2023-08-20
+ https://nicksxs.me/tags/Web/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/2022/
- 2023-08-20
+ https://nicksxs.me/tags/G1/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/2023/
- 2023-08-20
+ https://nicksxs.me/tags/GC/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Print-FooBar-Alternately/
- 2023-08-20
+ https://nicksxs.me/tags/Garbage-First-Collector/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E9%80%92%E5%BD%92/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Preorder-Traversal/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Inorder-Traversal/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%89%8D%E5%BA%8F/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E4%B8%AD%E5%BA%8F/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/DP/
- 2023-08-20
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/tags/Print-FooBar-Alternately/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Shift-2D-Grid/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/stack/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/min-stack/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/leetcode-155/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/3Sum-Closest/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/linked-list/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/tags/First-Bad-Version/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/string/
- 2023-08-20
+ https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Intersection-of-Two-Arrays/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/
- 2023-08-20
+ https://nicksxs.me/tags/First-Bad-Version/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/dp/
- 2023-08-20
+ https://nicksxs.me/tags/string/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/
- 2023-08-20
+ https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Trapping-Rain-Water/
- 2023-08-20
+ https://nicksxs.me/tags/Rotate-Image/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Leetcode-42/
- 2023-08-20
+ https://nicksxs.me/tags/dp/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Rotate-Image/
- 2023-08-20
+ https://nicksxs.me/tags/Trapping-Rain-Water/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/
- 2023-08-20
+ https://nicksxs.me/tags/Leetcode-42/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/headscale/
- 2023-08-20
+ https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/linux/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/grep/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E8%BD%AC%E4%B9%89/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/mfc/
- 2023-08-20
+ https://nicksxs.me/tags/headscale/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Maven/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/C/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Redis/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Distributed-Lock/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/
- 2023-08-20
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/tags/mfc/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/hadoop/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/cluster/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/docker/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/mysql/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Docker/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/namespace/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Dockerfile/
- 2023-08-20
+ https://nicksxs.me/tags/cgroup/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/cgroup/
- 2023-08-20
+ https://nicksxs.me/tags/Dockerfile/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/echo/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/uname/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%8F%91%E8%A1%8C%E7%89%88/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Gogs/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Webhook/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/dnsmasq/
- 2023-08-20
+ https://nicksxs.me/tags/ssh/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Mysql/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Mybatis/
- 2023-08-20
+ https://nicksxs.me/tags/Mysql/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/
- 2023-08-20
+ https://nicksxs.me/tags/Mybatis/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/hexo/
- 2023-08-20
+ https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/ssh/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
- 2023-08-20
+ https://nicksxs.me/tags/hexo/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/
- 2023-08-20
+ https://nicksxs.me/tags/dnsmasq/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/nginx/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/openresty/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/php/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/mq/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/im/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/redis/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%BA%90%E7%A0%81/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%BA%94%E7%94%A8/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Evict/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Rust/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%89%80%E6%9C%89%E6%9D%83/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%96%B0%E8%AF%AD%E8%A8%80/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%88%87%E7%89%87/
- 2023-08-20
+ https://nicksxs.me/tags/spark/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/spark/
- 2023-08-20
+ https://nicksxs.me/tags/python/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/python/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%88%87%E7%89%87/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Spring-Event/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/SpringBoot/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/powershell/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/websocket/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/swoole/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/WordPress/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/gc/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/jvm/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/MQ/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/RocketMQ/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%89%8A%E5%B3%B0%E5%A1%AB%E8%B0%B7/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E7%96%AB%E6%83%85/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%A7%84%E5%88%99/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%A7%84%E5%88%99/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Windows/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/git/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/scp/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E8%BF%90%E5%8A%A8/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%87%8F%E8%82%A5/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E8%B7%91%E6%AD%A5/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%B9%B2%E6%B4%BB/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/tags/wsl/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%BD%B1%E8%AF%84/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%AF%84%E7%94%9F%E8%99%AB/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/
- 2023-08-20
+ https://nicksxs.me/tags/DefaultMQPushConsumer/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/utf8/
- 2023-08-20
+ https://nicksxs.me/tags/NameServer/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/utf8mb4/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/utf8mb4-0900-ai-ci/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/utf8mb4-unicode-ci/
- 2023-08-20
+ https://nicksxs.me/tags/utf8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/utf8mb4-general-ci/
- 2023-08-20
+ https://nicksxs.me/tags/utf8mb4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/DefaultMQPushConsumer/
- 2023-08-20
+ https://nicksxs.me/tags/utf8mb4-0900-ai-ci/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
- 2023-08-20
+ https://nicksxs.me/tags/utf8mb4-unicode-ci/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/NameServer/
- 2023-08-20
+ https://nicksxs.me/tags/utf8mb4-general-ci/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/
- 2023-08-20
+ https://nicksxs.me/tags/wsl/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Druid/
- 2023-08-20
+ https://nicksxs.me/tags/cglib/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/
- 2023-08-20
+ https://nicksxs.me/tags/Druid/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/AutoConfiguration/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/
- 2023-08-20
+ https://nicksxs.me/tags/AutoConfiguration/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%B8%BE%E9%87%8D/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%B0%84%E5%87%BB/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/SPI/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Adaptive/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%B8%BE%E9%87%8D/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%B0%84%E5%87%BB/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/cglib/
- 2023-08-20
+ https://nicksxs.me/tags/SPI/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/
- 2023-08-20
+ https://nicksxs.me/tags/Adaptive/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/Synchronized/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%81%8F%E5%90%91%E9%94%81/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E8%87%AA%E6%97%8B/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%B1%BB%E5%8A%A0%E8%BD%BD/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8A%A0%E8%BD%BD/
- 2023-08-20
+ https://nicksxs.me/tags/JPS/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E9%AA%8C%E8%AF%81/
- 2023-08-20
+ https://nicksxs.me/tags/JStack/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%87%86%E5%A4%87/
- 2023-08-20
+ https://nicksxs.me/tags/JMap/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%A7%A3%E6%9E%90/
- 2023-08-20
+ https://nicksxs.me/tags/Sharding-Jdbc/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%88%9D%E5%A7%8B%E5%8C%96/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%B1%BB%E5%8A%A0%E8%BD%BD/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E9%93%BE%E6%8E%A5/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8A%A0%E8%BD%BD/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE/
- 2023-08-20
+ https://nicksxs.me/tags/%E9%AA%8C%E8%AF%81/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/JPS/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%87%86%E5%A4%87/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/JStack/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%A7%A3%E6%9E%90/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/JMap/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%88%9D%E5%A7%8B%E5%8C%96/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/top/
- 2023-08-20
+ https://nicksxs.me/tags/%E9%93%BE%E6%8E%A5/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Sharding-Jdbc/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/ThreadPool/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/FixedThreadPool/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/LimitedThreadPool/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/EagerThreadPool/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/CachedThreadPool/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/tags/Broker/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/mvcc/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/read-view/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/gap-lock/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/next-key-lock/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Design-Patterns/
- 2023-08-20
+ https://nicksxs.me/tags/is-null/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/
- 2023-08-20
+ https://nicksxs.me/tags/is-not-null/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Singleton/
- 2023-08-20
+ https://nicksxs.me/tags/procedure/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF/
- 2023-08-20
+ https://nicksxs.me/tags/Design-Patterns/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/
- 2023-08-20
+ https://nicksxs.me/tags/Singleton/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/bloom-filter/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%BA%92%E6%96%A5%E9%94%81/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/is-null/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/is-not-null/
- 2023-08-20
+ https://nicksxs.me/tags/bloom-filter/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/procedure/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%BA%92%E6%96%A5%E9%94%81/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Mac/
- 2023-08-20
+ https://nicksxs.me/tags/Broker/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/PHP/
- 2023-08-20
+ https://nicksxs.me/tags/ThreadLocal/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Homebrew/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/icu4c/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/zsh/
- 2023-08-20
+ https://nicksxs.me/tags/WeakReference/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/ThreadLocal/
- 2023-08-20
+ https://nicksxs.me/tags/Mac/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/
- 2023-08-20
+ https://nicksxs.me/tags/PHP/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/
- 2023-08-20
+ https://nicksxs.me/tags/Homebrew/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/WeakReference/
- 2023-08-20
+ https://nicksxs.me/tags/icu4c/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/
- 2023-08-20
+ https://nicksxs.me/tags/zsh/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/
- 2023-08-20
+ https://nicksxs.me/tags/Thread-dump/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/
- 2023-08-20
+ https://nicksxs.me/tags/2PC/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/
- 2023-08-20
+ https://nicksxs.me/tags/3PC/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/
- 2023-08-20
+ https://nicksxs.me/tags/top/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%9C%8B%E5%89%A7/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/Thread-dump/
- 2023-08-20
+ https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/
- 2023-08-20
+ https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
- 2023-08-20
+ https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/2PC/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/3PC/
- 2023-08-20
+ https://nicksxs.me/tags/dubbo/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E7%9C%8B%E5%89%A7/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/
- 2023-08-20
+ https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/
- 2023-08-20
+ https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/
- 2023-08-20
+ https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/dubbo/
- 2023-08-20
+ https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/stream/
- 2023-08-20
+ https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/zookeeper/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/windows/
- 2023-08-20
+ https://nicksxs.me/tags/%E8%B7%AF%E7%94%B1%E5%99%A8/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/tags/%E8%B7%AF%E7%94%B1%E5%99%A8/
- 2023-08-20
+ https://nicksxs.me/tags/windows/
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/tags/stream/
+ 2023-08-27
weekly
0.2
@@ -4183,1106 +4183,1106 @@
https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/
- 2023-08-20
+ 2023-08-27
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/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/JVM/
- 2023-08-20
+ 2023-08-27
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/
- 2023-08-20
+ https://nicksxs.me/categories/leetcode/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/leetcode/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/
- 2023-08-20
+ https://nicksxs.me/categories/Java/leetcode/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/GC/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
- 2023-08-20
+ https://nicksxs.me/categories/Linked-List/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Binary-Tree/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Linked-List/
- 2023-08-20
+ https://nicksxs.me/categories/Binary-Tree/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/nas/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/C/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/
- 2023-08-20
+ https://nicksxs.me/categories/leetcode/java/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/
+ 2023-08-27
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/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/
+ 2023-08-27
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/
- 2023-08-20
+ https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/java/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/java/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E6%9D%91%E4%B8%8A%E6%98%A5%E6%A0%91/
- 2023-08-20
+ https://nicksxs.me/categories/leetcode/java/Linked-List/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Apollo/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/
- 2023-08-20
+ https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E6%9D%91%E4%B8%8A%E6%98%A5%E6%A0%91/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/java/Binary-Tree/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Dubbo/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Filter/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/
- 2023-08-20
+ https://nicksxs.me/categories/leetcode/java/Binary-Tree/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/java/Linked-List/
- 2023-08-20
+ https://nicksxs.me/categories/Filter/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/DP/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Apollo/value/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/stack/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/linked-list/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/leetcode/Lowest-Common-Ancestor-of-a-Binary-Tree/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/
- 2023-08-20
+ https://nicksxs.me/categories/Java/leetcode/Rotate-Image/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Interceptor-AOP/
- 2023-08-20
+ https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/leetcode/Rotate-Image/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/headscale/
- 2023-08-20
+ https://nicksxs.me/categories/Interceptor-AOP/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Linux/
- 2023-08-20
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/categories/headscale/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Maven/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/leetcode/java/DP/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Redis/
- 2023-08-20
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/categories/leetcode/java/stack/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/data-analysis/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/java/stack/
- 2023-08-20
+ https://nicksxs.me/categories/leetcode/java/linked-list/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/docker/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Docker/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/leetcode/java/linked-list/
- 2023-08-20
+ https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/
- 2023-08-20
+ https://nicksxs.me/categories/ssh/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/dns/
- 2023-08-20
+ https://nicksxs.me/categories/Java/Mybatis/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/leetcode/java/string/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/Mybatis/
- 2023-08-20
+ https://nicksxs.me/categories/Spring/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/hexo/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Spring/
- 2023-08-20
+ https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/ssh/
- 2023-08-20
+ https://nicksxs.me/categories/dns/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/
- 2023-08-20
+ https://nicksxs.me/categories/Redis/Distributed-Lock/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/nginx/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/php/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/categories/Redis/Distributed-Lock/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/redis/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Spring/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/SpringBoot/
- 2023-08-20
+ 2023-08-27
+ weekly
+ 0.2
+
+
+
+ https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/gc/
- 2023-08-20
+ https://nicksxs.me/categories/Mysql/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/Mybatis/Mysql/
- 2023-08-20
+ https://nicksxs.me/categories/Java/gc/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/
- 2023-08-20
+ https://nicksxs.me/categories/Java/Mybatis/Mysql/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/
- 2023-08-20
+ https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mysql/
- 2023-08-20
+ https://nicksxs.me/categories/Spring/Servlet/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/hexo/%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/MQ/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/
- 2023-08-20
+ https://nicksxs.me/categories/Mybatis/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Spring/Servlet/
- 2023-08-20
+ https://nicksxs.me/categories/hexo/%E6%8A%80%E5%B7%A7/
+ 2023-08-27
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/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Windows/
- 2023-08-20
+ https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/git/
- 2023-08-20
+ https://nicksxs.me/categories/Windows/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/shell/
- 2023-08-20
+ https://nicksxs.me/categories/git/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/
- 2023-08-20
+ https://nicksxs.me/categories/shell/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mybatis/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/wsl/
- 2023-08-20
+ https://nicksxs.me/categories/C/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/
- 2023-08-20
+ https://nicksxs.me/categories/wsl/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/C/
- 2023-08-20
+ https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Dubbo/RPC/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/
- 2023-08-20
+ https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo-RPC/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/
- 2023-08-20
+ https://nicksxs.me/categories/Thread-dump/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Thread-dump/
- 2023-08-20
+ https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/
- 2023-08-20
+ https://nicksxs.me/categories/Java/Design-Patterns/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/
- 2023-08-20
+ https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Java/Design-Patterns/
- 2023-08-20
+ https://nicksxs.me/categories/Mac/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/
- 2023-08-20
+ https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mac/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/SpringBoot/
- 2023-08-20
+ https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/
- 2023-08-20
+ https://nicksxs.me/categories/Rust/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/
- 2023-08-20
+ https://nicksxs.me/categories/SpringBoot/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Rust/
- 2023-08-20
+ https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/gc/jvm/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/
- 2023-08-20
+ https://nicksxs.me/categories/Spring/Servlet/Interceptor/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/MQ/RocketMQ/
- 2023-08-20
+ 2023-08-27
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/
- 2023-08-20
+ https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Spring/Servlet/Interceptor/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/
+ 2023-08-27
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/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Windows/%E5%B0%8F%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ 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-08-27
weekly
0.2
- https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ https://nicksxs.me/categories/Windows/%E5%B0%8F%E6%8A%80%E5%B7%A7/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/
- 2023-08-20
+ https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/
- 2023-08-20
+ https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/
+ 2023-08-27
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/
- 2023-08-20
- weekly
- 0.2
-
-
-
- https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/C/Redis/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/
- 2023-08-20
+ https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
- 2023-08-20
+ https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Java/Singleton/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/
- 2023-08-20
+ https://nicksxs.me/categories/Mac/PHP/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mac/PHP/
- 2023-08-20
+ https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Spring/Mybatis/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/
- 2023-08-20
+ https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E7%BE%8E%E5%9B%BD/
- 2023-08-20
+ https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E5%8F%A3%E7%BD%A9/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E5%8F%A3%E7%BD%A9/
- 2023-08-20
+ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E7%BE%8E%E5%9B%BD/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/%E5%B9%B2%E6%B4%BB/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/MQ/RocketMQ/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/
- 2023-08-20
+ https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/
- 2023-08-20
+ https://nicksxs.me/categories/C/Mysql/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/
- 2023-08-20
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/C/Mysql/
- 2023-08-20
+ https://nicksxs.me/categories/Mac/Homebrew/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/Mac/Homebrew/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
+ 2023-08-27
weekly
0.2
- https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/
- 2023-08-20
+ https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/RocketMQ/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo/SPI/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ThreadPool/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/PHP/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/Dubbo/SPI/Adaptive/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/%E5%87%BB%E7%A9%BF/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/PHP/icu4c/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/RocketMQ/
- 2023-08-20
+ 2023-08-27
weekly
0.2
https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/%E5%87%BB%E7%A9%BF/%E9%9B%AA%E5%B4%A9/
- 2023-08-20
+ 2023-08-27
weekly
0.2
diff --git a/tags/Dubbo/index.html b/tags/Dubbo/index.html
index 71645c5548..6b7918d378 100644
--- a/tags/Dubbo/index.html
+++ b/tags/Dubbo/index.html
@@ -1 +1 @@
-标签: dubbo | Nicksxs's Blog
0%js>
\ No newline at end of file
+标签: dubbo | Nicksxs's Blog
0%
\ No newline at end of file
diff --git a/tags/Windows/index.html b/tags/Windows/index.html
index e4a24e8db2..ce2a90b682 100644
--- a/tags/Windows/index.html
+++ b/tags/Windows/index.html
@@ -1 +1 @@
-标签: windows | Nicksxs's Blog
0%>
\ No newline at end of file
+标签: windows | Nicksxs's Blog
0%
\ No newline at end of file
diff --git a/tags/php/index.html b/tags/php/index.html
index b04493f782..b106a60688 100644
--- a/tags/php/index.html
+++ b/tags/php/index.html
@@ -1 +1 @@
-标签: PHP | Nicksxs's Blog
0%YVd/rzeR6SLLcDFYEidcybldM= src=https://cdnjs.cloudflare.com/ajax/libs/algoliasearch/4.14.3/algoliasearch-lite.umd.js>
\ No newline at end of file
+标签: PHP | Nicksxs's Blog
0%
\ No newline at end of file