From ec19b6efb0f57140551c6e73b153e7dc3904956c Mon Sep 17 00:00:00 2001 From: nicksxs Date: Mon, 22 Aug 2022 17:29:19 +0800 Subject: [PATCH] Site updated: 2022-08-22 17:29:17 --- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../index.html | 2 +- .../聊聊-Linux-下的-top-命令/index.html | 4 +- 2021/07/18/2021-年中总结/index.html | 2 +- .../index.html | 4 +- atom.xml | 2 +- baidusitemap.xml | 28 +- index.html | 4 +- leancloud_counter_security_urls.json | 2 +- page/18/index.html | 2 +- page/20/index.html | 2 +- page/21/index.html | 2 +- page/22/index.html | 2 +- search.xml | 6218 ++++++++--------- sitemap.xml | 534 +- tags/DP/index.html | 2 +- tags/JVM/index.html | 2 +- tags/linked-list/index.html | 2 +- tags/mysql/index.html | 2 +- tags/php/index.html | 2 +- 22 files changed, 3412 insertions(+), 3412 deletions(-) diff --git a/2020/09/06/mybatis-的-和-是有啥区别/index.html b/2020/09/06/mybatis-的-和-是有啥区别/index.html index 5ca2a1c14b..90614bd01d 100644 --- a/2020/09/06/mybatis-的-和-是有啥区别/index.html +++ b/2020/09/06/mybatis-的-和-是有啥区别/index.html @@ -1,4 +1,4 @@ -mybatis 的 $ 和 # 是有啥区别 | Nicksxs's Blog

Nicksxs's Blog

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

0%

mybatis 的 $ 和 # 是有啥区别

这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
在这里做了替换,将#{}替换成了?

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

// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
+mybatis 的 $ 和 # 是有啥区别 | Nicksxs's Blog

Nicksxs's Blog

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

0%

mybatis 的 $ 和 # 是有啥区别

这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
在这里做了替换,将#{}替换成了?

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

// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
 public SqlSource parseScriptNode() {
     MixedSqlNode rootSqlNode = parseDynamicTags(context);
     SqlSource sqlSource;
diff --git a/2020/10/03/mybatis-的缓存是怎么回事/index.html b/2020/10/03/mybatis-的缓存是怎么回事/index.html
index 2c1f38ad28..c712ee591a 100644
--- a/2020/10/03/mybatis-的缓存是怎么回事/index.html
+++ b/2020/10/03/mybatis-的缓存是怎么回事/index.html
@@ -1,4 +1,4 @@
-mybatis 的缓存是怎么回事 | Nicksxs's Blog

Nicksxs's Blog

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

0%

mybatis 的缓存是怎么回事

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 {
+mybatis 的缓存是怎么回事 | Nicksxs's Blog

Nicksxs's Blog

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

0%

mybatis 的缓存是怎么回事

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;
 
diff --git a/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html b/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html
index 7708e06e82..107479fd23 100644
--- a/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html
+++ b/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html
@@ -1,4 +1,4 @@
-Leetcode 234 回文链表(Palindrome Linked List) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

0%

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

题目介绍

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

例一 Example 1:

Input: 1->2
Output: false

例二 Example 2:

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

挑战下自己

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

简要分析

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

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

Nicksxs's Blog

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

0%

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

题目介绍

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

例一 Example 1:

Input: 1->2
Output: false

例二 Example 2:

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

挑战下自己

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

简要分析

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

/**
  * Definition for singly-linked list.
  * public class ListNode {
  *     int val;
diff --git a/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html b/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html
index 3068ba6af5..6474bf9112 100644
--- a/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html
+++ b/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html
@@ -1,4 +1,4 @@
-Leetcode 160 相交链表(intersection-of-two-linked-lists) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

0%

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

题目介绍

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

For example, the following two linked lists:

begin to intersect at node c1.

Example 1:

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

Nicksxs's Blog

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

0%

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

题目介绍

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

For example, the following two linked lists:

begin to intersect at node c1.

Example 1:

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

分析题解

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

代码

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
         if (headA == null || headB == null) {
diff --git a/2021/03/28/聊聊-Linux-下的-top-命令/index.html b/2021/03/28/聊聊-Linux-下的-top-命令/index.html
index d35f3b13f9..74b9310a4e 100644
--- a/2021/03/28/聊聊-Linux-下的-top-命令/index.html
+++ b/2021/03/28/聊聊-Linux-下的-top-命令/index.html
@@ -1,4 +1,4 @@
-聊聊 Linux 下的 top 命令 | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊聊 Linux 下的 top 命令

top 命令在日常的 Linux 使用中,特别是做一些服务器的简单状态查看,排查故障都起了比较大的作用,但是由于这个命令看到的东西比较多,一般只会看部分,或者说像我这样就会比较片面地看一些信息,比如默认是进程维度的,可以在启动命令的时候加-H进入线程模式

-H  :Threads-mode operation
+聊聊 Linux 下的 top 命令 | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊聊 Linux 下的 top 命令

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
 
@@ -22,4 +22,4 @@
                  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 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<>直接左右移动排序列

\ No newline at end of file + 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 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<>直接左右移动排序列

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

Nicksxs's Blog

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

0%

2021 年中总结

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

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

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

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

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

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

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

Nicksxs's Blog

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

0%

2021 年中总结

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

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

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

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

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

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

\ No newline at end of file diff --git a/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/index.html b/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/index.html index 945e0bd65a..ef67c0387b 100644 --- a/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/index.html +++ b/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/index.html @@ -1,6 +1,6 @@ -聊一下 SpringBoot 设置非 web 应用的方法 | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊一下 SpringBoot 设置非 web 应用的方法

寻找原因

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

解决方案

老版本 设置 spring 不带 web 功能

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

SpringApplication app = new SpringApplication(BootStrap.class);
+聊一下 SpringBoot 设置非 web 应用的方法 | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊一下 SpringBoot 设置非 web 应用的方法

寻找原因

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

解决方案

老版本 设置 spring 不带 web 功能

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

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

新版本

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

spring.main.web-application-type=none

或者

SpringApplication app = new SpringApplication(KnowledgePlatformApplication.class);
+app.run(args);

新版本

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

spring.main.web-application-type=none

或者

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

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

public enum WebApplicationType {
 
 	/**
diff --git a/atom.xml b/atom.xml
index d36755fae5..310116235f 100644
--- a/atom.xml
+++ b/atom.xml
@@ -112,7 +112,7 @@
     
     https://nicksxs.me/2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/
     2022-07-31T11:16:33.000Z
-    2022-07-31T11:16:33.987Z
+    2022-08-22T09:27:44.983Z
     
     
       
diff --git a/baidusitemap.xml b/baidusitemap.xml
index c993e90992..fa6da27973 100644
--- a/baidusitemap.xml
+++ b/baidusitemap.xml
@@ -1,5 +1,9 @@
 
 
+  
+    https://nicksxs.me/2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/
+    2022-08-22
+  
   
     https://nicksxs.me/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/
     2022-08-22
@@ -20,10 +24,6 @@
     https://nicksxs.me/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/
     2022-08-06
   
-  
-    https://nicksxs.me/2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/
-    2022-07-31
-  
   
     https://nicksxs.me/2022/07/17/%E3%80%8A%E9%95%BF%E5%AE%89%E7%9A%84%E8%8D%94%E6%9E%9D%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F/
     2022-07-17
@@ -85,15 +85,15 @@
     2022-06-11
   
   
-    https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/
+    https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/
     2022-06-11
   
   
-    https://nicksxs.me/2021/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/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/
     2022-06-11
   
   
-    https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/
+    https://nicksxs.me/2021/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
   
   
@@ -141,15 +141,15 @@
     2022-06-11
   
   
-    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/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/
     2022-06-11
   
   
-    https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/
+    https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
     2022-06-11
   
   
-    https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
+    https://nicksxs.me/2021/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
   
   
@@ -621,11 +621,11 @@
     2020-01-12
   
   
-    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
   
   
-    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/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
   
   
@@ -673,11 +673,11 @@
     2020-01-12
   
   
-    https://nicksxs.me/2016/07/13/swoole-websocket-test/
+    https://nicksxs.me/2016/10/12/summary-ranges-228/
     2020-01-12
   
   
-    https://nicksxs.me/2016/10/12/summary-ranges-228/
+    https://nicksxs.me/2016/07/13/swoole-websocket-test/
     2020-01-12
   
   
diff --git a/index.html b/index.html
index c008f2f04f..b30c3c3cfd 100644
--- a/index.html
+++ b/index.html
@@ -69,9 +69,9 @@
             }
         }
         return closestSum;
-    }

结果

寻找原因

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

解决方案

老版本 设置 spring 不带 web 功能

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

SpringApplication app = new SpringApplication(BootStrap.class);
+    }

结果

寻找原因

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

解决方案

老版本 设置 spring 不带 web 功能

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

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

新版本

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

spring.main.web-application-type=none

或者

SpringApplication app = new SpringApplication(KnowledgePlatformApplication.class);
+app.run(args);

新版本

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

spring.main.web-application-type=none

或者

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

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

public enum WebApplicationType {
 
 	/**
diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json
index 4a8a3b92a7..bd4ca83e58 100644
--- a/leancloud_counter_security_urls.json
+++ b/leancloud_counter_security_urls.json
@@ -1 +1 @@
-[{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"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":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"Disruptor 系列二","url":"/2022/02/27/Disruptor-系列二/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"JVM源码分析之G1垃圾收集器分析一","url":"/2019/12/07/JVM-G1-Part-1/"},{"title":"Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析","url":"/2021/10/07/Leetcode-021-合并两个有序链表-Merge-Two-Sorted-Lists-题解分析/"},{"title":"Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"title":"Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"title":"Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"title":"Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析","url":"/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/"},{"title":"Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"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 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"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 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 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"title":"Leetcode 3 Longest Substring Without Repeating Characters 题解分析","url":"/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-题解分析/"},{"title":"Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析","url":"/2022/03/07/Leetcode-349-两个数组的交集-Intersection-of-Two-Arrays-Easy-题解分析/"},{"title":"Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"title":"Leetcode 42 接雨水 (Trapping Rain Water) 题解分析","url":"/2021/07/04/Leetcode-42-接雨水-Trapping-Rain-Water-题解分析/"},{"title":"Leetcode 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析","url":"/2022/06/19/Leetcode-698-划分为k个相等的子集-Partition-to-K-Equal-Sum-Subsets-Medium-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"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":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"title":"minimum-size-subarray-sum-209","url":"/2016/10/11/minimum-size-subarray-sum-209/"},{"title":"C++ 指针使用中的一个小问题","url":"/2014/12/23/my-new-post/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/mybatis-的-和-是有啥区别/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"nginx 日志小记","url":"/2022/04/17/nginx-日志小记/"},{"title":"openresty","url":"/2019/06/18/openresty/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"pcre-intro-and-a-simple-package","url":"/2015/01/16/pcre-intro-and-a-simple-package/"},{"title":"php-abstract-class-and-interface","url":"/2016/11/10/php-abstract-class-and-interface/"},{"title":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"redis数据结构介绍三-第三部分 整数集合","url":"/2020/01/10/redis数据结构介绍三/"},{"title":"redis数据结构介绍-第一部分 SDS,链表,字典","url":"/2019/12/26/redis数据结构介绍/"},{"title":"redis数据结构介绍二-第二部分 跳表","url":"/2020/01/04/redis数据结构介绍二/"},{"title":"redis数据结构介绍五-第五部分 对象","url":"/2020/01/20/redis数据结构介绍五/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"redis数据结构介绍四-第四部分 压缩表","url":"/2020/01/19/redis数据结构介绍四/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/redis系列介绍七/"},{"title":"redis系列介绍八-淘汰策略","url":"/2020/04/18/redis系列介绍八/"},{"title":"redis过期策略复习","url":"/2021/07/25/redis过期策略复习/"},{"title":"rust学习笔记-所有权三之切片","url":"/2021/05/16/rust学习笔记-所有权三之切片/"},{"title":"rust学习笔记-所有权二","url":"/2021/04/18/rust学习笔记-所有权二/"},{"title":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"title":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"Leetcode 155 最小栈(Min Stack) 题解分析","url":"/2020/12/06/Leetcode-155-最小栈-Min-Stack-题解分析/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"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 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"}]
\ No newline at end of file
+[{"title":"村上春树《1Q84》读后感","url":"/2019/12/18/1Q84读后感/"},{"title":"2020 年终总结","url":"/2021/03/31/2020-年终总结/"},{"title":"2020年中总结","url":"/2020/07/11/2020年中总结/"},{"title":"2021 年中总结","url":"/2021/07/18/2021-年中总结/"},{"title":"2021 年终总结","url":"/2022/01/22/2021-年终总结/"},{"title":"34_Search_for_a_Range","url":"/2016/08/14/34-Search-for-a-Range/"},{"title":"AQS篇二 之 Condition 浅析笔记","url":"/2021/02/21/AQS-之-Condition-浅析笔记/"},{"title":"AQS篇一","url":"/2021/02/14/AQS篇一/"},{"title":"AbstractQueuedSynchronizer","url":"/2019/09/23/AbstractQueuedSynchronizer/"},{"title":"add-two-number","url":"/2015/04/14/Add-Two-Number/"},{"title":"Apollo 的 value 注解是怎么自动更新的","url":"/2020/11/01/Apollo-的-value-注解是怎么自动更新的/"},{"title":"Clone Graph Part I","url":"/2014/12/30/Clone-Graph-Part-I/"},{"title":"Comparator使用小记","url":"/2020/04/05/Comparator使用小记/"},{"title":"Disruptor 系列一","url":"/2022/02/13/Disruptor-系列一/"},{"title":"Disruptor 系列二","url":"/2022/02/27/Disruptor-系列二/"},{"title":"Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?","url":"/2020/08/22/Filter-Intercepter-Aop-啥-啥-啥-这些都是啥/"},{"title":"G1收集器概述","url":"/2020/02/09/G1收集器概述/"},{"title":"JVM源码分析之G1垃圾收集器分析一","url":"/2019/12/07/JVM-G1-Part-1/"},{"title":"Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析","url":"/2021/10/07/Leetcode-021-合并两个有序链表-Merge-Two-Sorted-Lists-题解分析/"},{"title":"Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析","url":"/2021/10/31/Leetcode-028-实现-strStr-Implement-strStr-题解分析/"},{"title":"Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析","url":"/2021/11/28/Leetcode-053-最大子序和-Maximum-Subarray-题解分析/"},{"title":"Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析","url":"/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/"},{"title":"Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析","url":"/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/"},{"title":"Leetcode 1115 交替打印 FooBar ( Print FooBar Alternately *Medium* ) 题解分析","url":"/2022/05/01/Leetcode-1115-交替打印-FooBar-Print-FooBar-Alternately-Medium-题解分析/"},{"title":"Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析","url":"/2021/03/14/Leetcode-121-买卖股票的最佳时机-Best-Time-to-Buy-and-Sell-Stock-题解分析/"},{"title":"Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析","url":"/2021/01/24/Leetcode-124-二叉树中的最大路径和-Binary-Tree-Maximum-Path-Sum-题解分析/"},{"title":"Leetcode 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 2 Add Two Numbers 题解分析","url":"/2020/10/11/Leetcode-2-Add-Two-Numbers-题解分析/"},{"title":"2019年终总结","url":"/2020/02/01/2019年终总结/"},{"title":"Leetcode 234 回文链表(Palindrome Linked List) 题解分析","url":"/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/"},{"title":"Leetcode 236 二叉树的最近公共祖先(Lowest Common Ancestor of a Binary Tree) 题解分析","url":"/2021/05/23/Leetcode-236-二叉树的最近公共祖先-Lowest-Common-Ancestor-of-a-Binary-Tree-题解分析/"},{"title":"Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析","url":"/2022/08/14/Leetcode-278-第一个错误的版本-First-Bad-Version-Easy-题解分析/"},{"title":"Leetcode 3 Longest Substring Without Repeating Characters 题解分析","url":"/2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-题解分析/"},{"title":"Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析","url":"/2022/07/22/Leetcode-1260-二维网格迁移-Shift-2D-Grid-Easy-题解分析/"},{"title":"Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析","url":"/2022/07/02/Leetcode-20-有效的括号-Valid-Parentheses-Easy-题解分析/"},{"title":"Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析","url":"/2022/03/07/Leetcode-349-两个数组的交集-Intersection-of-Two-Arrays-Easy-题解分析/"},{"title":"Leetcode 48 旋转图像(Rotate Image) 题解分析","url":"/2021/05/01/Leetcode-48-旋转图像-Rotate-Image-题解分析/"},{"title":"Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析","url":"/2022/03/27/Leetcode-4-寻找两个正序数组的中位数-Median-of-Two-Sorted-Arrays-Hard-题解分析/"},{"title":"Leetcode 83 删除排序链表中的重复元素 ( Remove Duplicates from Sorted List *Easy* ) 题解分析","url":"/2022/03/13/Leetcode-83-删除排序链表中的重复元素-Remove-Duplicates-from-Sorted-List-Easy-题解分析/"},{"title":"leetcode no.3","url":"/2015/04/15/Leetcode-No-3/"},{"title":"Linux 下 grep 命令的一点小技巧","url":"/2020/08/06/Linux-下-grep-命令的一点小技巧/"},{"title":"MFC 模态对话框","url":"/2014/12/24/MFC 模态对话框/"},{"title":"Maven实用小技巧","url":"/2020/02/16/Maven实用小技巧/"},{"title":"Number of 1 Bits","url":"/2015/03/11/Number-Of-1-Bits/"},{"title":"Path Sum","url":"/2015/01/04/Path-Sum/"},{"title":"Redis_分布式锁","url":"/2019/12/10/Redis-Part-1/"},{"title":"Reverse Bits","url":"/2015/03/11/Reverse-Bits/"},{"title":"Reverse Integer","url":"/2015/03/13/Reverse-Integer/"},{"title":"two sum","url":"/2015/01/14/Two-Sum/"},{"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":"docker比一般多一点的初学者介绍二","url":"/2020/03/15/docker比一般多一点的初学者介绍二/"},{"title":"docker使用中发现的echo命令的一个小技巧及其他","url":"/2020/03/29/echo命令的一个小技巧/"},{"title":"dubbo 客户端配置的一个重要知识点","url":"/2022/06/11/dubbo-客户端配置的一个重要知识点/"},{"title":"gogs使用webhook部署react单页应用","url":"/2020/02/22/gogs使用webhook部署react单页应用/"},{"title":"invert-binary-tree","url":"/2015/06/22/invert-binary-tree/"},{"title":"Dubbo 使用的几个记忆点","url":"/2022/04/02/Dubbo-使用的几个记忆点/"},{"title":"minimum-size-subarray-sum-209","url":"/2016/10/11/minimum-size-subarray-sum-209/"},{"title":"C++ 指针使用中的一个小问题","url":"/2014/12/23/my-new-post/"},{"title":"mybatis 的缓存是怎么回事","url":"/2020/10/03/mybatis-的缓存是怎么回事/"},{"title":"mybatis 的 $ 和 # 是有啥区别","url":"/2020/09/06/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":"php-abstract-class-and-interface","url":"/2016/11/10/php-abstract-class-and-interface/"},{"title":"rabbitmq-tips","url":"/2017/04/25/rabbitmq-tips/"},{"title":"redis 的 rdb 和 COW 介绍","url":"/2021/08/15/redis-的-rdb-和-COW-介绍/"},{"title":"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/19/redis数据结构介绍四/"},{"title":"mybatis 的 foreach 使用的注意点","url":"/2022/07/09/mybatis-的-foreach-使用的注意点/"},{"title":"redis淘汰策略复习","url":"/2021/08/01/redis淘汰策略复习/"},{"title":"redis系列介绍七-过期策略","url":"/2020/04/12/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":"rust学习笔记-所有权一","url":"/2021/04/18/rust学习笔记/"},{"title":"spark-little-tips","url":"/2017/03/28/spark-little-tips/"},{"title":"summary-ranges-228","url":"/2016/10/12/summary-ranges-228/"},{"title":"spring event 介绍","url":"/2022/01/30/spring-event-介绍/"},{"title":"swoole-websocket-test","url":"/2016/07/13/swoole-websocket-test/"},{"title":"wordpress 忘记密码的一种解决方法","url":"/2021/12/05/wordpress-忘记密码的一种解决方法/"},{"title":"redis数据结构介绍六 快表","url":"/2020/01/22/redis数据结构介绍六/"},{"title":"《长安的荔枝》读后感","url":"/2022/07/17/《长安的荔枝》读后感/"},{"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":"介绍一下 RocketMQ","url":"/2020/06/21/介绍一下-RocketMQ/"},{"title":"介绍下最近比较实用的端口转发","url":"/2021/11/14/介绍下最近比较实用的端口转发/"},{"title":"从丁仲礼被美国制裁聊点啥","url":"/2020/12/20/从丁仲礼被美国制裁聊点啥/"},{"title":"从清华美院学姐聊聊我们身边的恶人","url":"/2020/11/29/从清华美院学姐聊聊我们身边的恶人/"},{"title":"关于公共交通再吐个槽","url":"/2021/03/21/关于公共交通再吐个槽/"},{"title":"上次的其他 外行聊国足","url":"/2022/03/06/上次的其他-外行聊国足/"},{"title":"关于读书打卡与分享","url":"/2021/02/07/关于读书打卡与分享/"},{"title":"分享记录一下一个 scp 操作方法","url":"/2022/02/06/分享记录一下一个-scp-操作方法/"},{"title":"周末我在老丈人家打了天小工","url":"/2020/08/16/周末我在老丈人家打了天小工/"},{"title":"在老丈人家的小工记三","url":"/2020/09/13/在老丈人家的小工记三/"},{"title":"在老丈人家的小工记五","url":"/2020/10/18/在老丈人家的小工记五/"},{"title":"在老丈人家的小工记四","url":"/2020/09/26/在老丈人家的小工记四/"},{"title":"寄生虫观后感","url":"/2020/03/01/寄生虫观后感/"},{"title":"屯菜惊魂记","url":"/2022/04/24/屯菜惊魂记/"},{"title":"《垃圾回收算法手册读书》笔记之整理算法","url":"/2021/03/07/《垃圾回收算法手册读书》笔记之整理算法/"},{"title":"搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答","url":"/2022/01/16/搬运两个-StackOverflow-上的-Mysql-编码相关的问题解答/"},{"title":"是何原因竟让两人深夜奔袭十公里","url":"/2022/06/05/是何原因竟让两人深夜奔袭十公里/"},{"title":"看完了扫黑风暴,聊聊感想","url":"/2021/10/24/看完了扫黑风暴-聊聊感想/"},{"title":"我是如何走上跑步这条不归路的","url":"/2020/07/26/我是如何走上跑步这条不归路的/"},{"title":"聊一下 RocketMQ 的 DefaultMQPushConsumer 源码","url":"/2020/06/26/聊一下-RocketMQ-的-Consumer/"},{"title":"聊一下 RocketMQ 的 NameServer 源码","url":"/2020/07/05/聊一下-RocketMQ-的-NameServer-源码/"},{"title":"聊一下 RocketMQ 的消息存储之 MMAP","url":"/2021/09/04/聊一下-RocketMQ-的消息存储/"},{"title":"聊一下 RocketMQ 的消息存储三","url":"/2021/10/03/聊一下-RocketMQ-的消息存储三/"},{"title":"聊一下 RocketMQ 的消息存储二","url":"/2021/09/12/聊一下-RocketMQ-的消息存储二/"},{"title":"聊一下 RocketMQ 的消息存储四","url":"/2021/10/17/聊一下-RocketMQ-的消息存储四/"},{"title":"给小电驴上牌","url":"/2022/03/20/给小电驴上牌/"},{"title":"聊一下 RocketMQ 的顺序消息","url":"/2021/08/29/聊一下-RocketMQ-的顺序消息/"},{"title":"聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点","url":"/2021/09/19/聊一下-SpringBoot-中使用的-cglib-作为动态代理中的一个注意点/"},{"title":"聊一下 SpringBoot 中动态切换数据源的方法","url":"/2021/09/26/聊一下-SpringBoot-中动态切换数据源的方法/"},{"title":"聊在东京奥运会闭幕式这天-二","url":"/2021/08/19/聊在东京奥运会闭幕式这天-二/"},{"title":"聊一下 SpringBoot 设置非 web 应用的方法","url":"/2022/07/31/聊一下-SpringBoot-设置非-web-应用的方法/"},{"title":"聊在东京奥运会闭幕式这天","url":"/2021/08/08/聊在东京奥运会闭幕式这天/"},{"title":"聊聊 Dubbo 的 SPI","url":"/2020/05/31/聊聊-Dubbo-的-SPI/"},{"title":"聊聊 Dubbo 的 SPI 续之自适应拓展","url":"/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/"},{"title":"聊聊 Dubbo 的容错机制","url":"/2020/11/22/聊聊-Dubbo-的容错机制/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字","url":"/2021/06/20/聊聊-Java-中绕不开的-Synchronized-关键字/"},{"title":"一个 nginx 的简单记忆点","url":"/2022/08/21/一个-nginx-的简单记忆点/"},{"title":"聊聊 Java 的类加载机制一","url":"/2020/11/08/聊聊-Java-的类加载机制/"},{"title":"聊聊 Java 的类加载机制二","url":"/2021/06/13/聊聊-Java-的类加载机制二/"},{"title":"聊聊 Java 自带的那些*逆天*工具","url":"/2020/08/02/聊聊-Java-自带的那些逆天工具/"},{"title":"聊聊 RocketMQ 的 Broker 源码","url":"/2020/07/19/聊聊-RocketMQ-的-Broker-源码/"},{"title":"聊聊 Linux 下的 top 命令","url":"/2021/03/28/聊聊-Linux-下的-top-命令/"},{"title":"聊聊 Sharding-Jdbc 的简单使用","url":"/2021/12/12/聊聊-Sharding-Jdbc-的简单使用/"},{"title":"聊聊 Sharding-Jdbc 分库分表下的分页方案","url":"/2022/01/09/聊聊-Sharding-Jdbc-分库分表下的分页方案/"},{"title":"聊聊 Sharding-Jdbc 的简单原理初篇","url":"/2021/12/26/聊聊-Sharding-Jdbc-的简单原理初篇/"},{"title":"聊聊 mysql 的 MVCC 续篇","url":"/2020/05/02/聊聊-mysql-的-MVCC-续篇/"},{"title":"聊聊 dubbo 的线程池","url":"/2021/04/04/聊聊-dubbo-的线程池/"},{"title":"分享记录一下一个 git 操作方法","url":"/2022/02/06/分享记录一下一个-git-操作方法/"},{"title":"聊聊 mysql 的 MVCC","url":"/2020/04/26/聊聊-mysql-的-MVCC/"},{"title":"聊聊 mysql 索引的一些细节","url":"/2020/12/27/聊聊-mysql-索引的一些细节/"},{"title":"聊聊 redis 缓存的应用问题","url":"/2021/01/31/聊聊-redis-缓存的应用问题/"},{"title":"聊聊Java中的单例模式","url":"/2019/12/21/聊聊Java中的单例模式/"},{"title":"聊聊 SpringBoot 自动装配","url":"/2021/07/11/聊聊SpringBoot-自动装配/"},{"title":"聊聊一次 brew update 引发的血案","url":"/2020/06/13/聊聊一次-brew-update-引发的血案/"},{"title":"聊聊传说中的 ThreadLocal","url":"/2021/05/30/聊聊传说中的-ThreadLocal/"},{"title":"聊聊厦门旅游的好与不好","url":"/2021/04/11/聊聊厦门旅游的好与不好/"},{"title":"聊聊如何识别和意识到日常生活中的各类危险","url":"/2021/06/06/聊聊如何识别和意识到日常生活中的各类危险/"},{"title":"聊聊我刚学会的应用诊断方法","url":"/2020/05/22/聊聊我刚学会的应用诊断方法/"},{"title":"聊聊我理解的分布式事务","url":"/2020/05/17/聊聊我理解的分布式事务/"},{"title":"聊聊我的远程工作体验","url":"/2022/06/26/聊聊我的远程工作体验/"},{"title":"聊聊最近平淡的生活之又聊通勤","url":"/2021/11/07/聊聊最近平淡的生活/"},{"title":"聊聊最近平淡的生活之《花束般的恋爱》观后感","url":"/2021/12/31/聊聊最近平淡的生活之《花束般的恋爱》观后感/"},{"title":"聊聊最近平淡的生活之看《神探狄仁杰》","url":"/2021/12/19/聊聊最近平淡的生活之看《神探狄仁杰》/"},{"title":"聊聊最近平淡的生活之看看老剧","url":"/2021/11/21/聊聊最近平淡的生活之看看老剧/"},{"title":"聊聊给亲戚朋友的老电脑重装系统那些事儿","url":"/2021/05/09/聊聊给亲戚朋友的老电脑重装系统那些事儿/"},{"title":"聊聊这次换车牌及其他","url":"/2022/02/20/聊聊这次换车牌及其他/"},{"title":"聊聊那些加塞狗","url":"/2021/01/17/聊聊那些加塞狗/"},{"title":"聊聊部分公交车的设计bug","url":"/2021/12/05/聊聊部分公交车的设计bug/"},{"title":"记录下 zookeeper 集群迁移和易错点","url":"/2022/05/29/记录下-zookeeper-集群迁移/"},{"title":"这周末我又在老丈人家打了天小工","url":"/2020/08/30/这周末我又在老丈人家打了天小工/"},{"title":"记录下 Java Stream 的一些高效操作","url":"/2022/05/15/记录下-Java-Lambda-的一些高效操作/"},{"title":"重看了下《蛮荒记》说说感受","url":"/2021/10/10/重看了下《蛮荒记》说说感受/"},{"title":"闲聊下乘公交的用户体验","url":"/2021/02/28/闲聊下乘公交的用户体验/"},{"title":"闲话篇-也算碰到了为老不尊和坏人变老了的典型案例","url":"/2022/05/22/闲话篇-也算碰到了为老不尊和坏人变老了的典型案例/"},{"title":"闲话篇-路遇神逻辑骑车带娃爹","url":"/2022/05/08/闲话篇-路遇神逻辑骑车带娃爹/"},{"title":"难得的大扫除","url":"/2022/04/10/难得的大扫除/"},{"title":"聊聊 Java 的 equals 和 hashCode 方法","url":"/2021/01/03/聊聊-Java-的-equals-和-hashCode-方法/"},{"title":"聊聊 Java 中绕不开的 Synchronized 关键字-二","url":"/2021/06/27/聊聊-Java-中绕不开的-Synchronized-关键字-二/"},{"title":"聊聊 mysql 的 MVCC 续续篇之锁分析","url":"/2020/05/10/聊聊-mysql-的-MVCC-续续篇之加锁分析/"}]
\ No newline at end of file
diff --git a/page/18/index.html b/page/18/index.html
index 2b34d77d2d..b4aea71df6 100644
--- a/page/18/index.html
+++ b/page/18/index.html
@@ -21,7 +21,7 @@
     int res = Math.max(left + right + root.val, currentSum);
     ans = Math.max(res, ans);
     return currentSum;
-}

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

结果图

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

今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~

题目介绍

写一个程序找出两个单向链表的交叉起始点,可能是我英语不好,图里画的其实还有一点是交叉以后所有节点都是相同的
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
+}

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

结果图

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

今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~

题目介绍

写一个程序找出两个单向链表的交叉起始点,可能是我英语不好,图里画的其实还有一点是交叉以后所有节点都是相同的
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) {
diff --git a/page/20/index.html b/page/20/index.html
index 001b55e9d8..e95b83ef72 100644
--- a/page/20/index.html
+++ b/page/20/index.html
@@ -93,7 +93,7 @@ public Result invoke(final Invocation invocation) throws RpcException {
                 + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                 + Version.getVersion() + ". Last error is: "
                 + le.getMessage(), le.getCause() != null ? le.getCause() : le);
-    }

Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。

Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。

题目介绍

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 出来跟链表从头开始比较,全对上了就是回文了

/**
+    }

Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。

Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。

题目介绍

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;
diff --git a/page/21/index.html b/page/21/index.html
index 85b044b783..22ca1823ea 100644
--- a/page/21/index.html
+++ b/page/21/index.html
@@ -52,7 +52,7 @@ Output: [8,9,9,9,0,0,0,1]
 //        tail = null;
         return root;
-    }

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

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 {
+    }

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

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;
 
diff --git a/page/22/index.html b/page/22/index.html
index 29fa3c38eb..2a96d63653 100644
--- a/page/22/index.html
+++ b/page/22/index.html
@@ -38,7 +38,7 @@ Output: 0

注释应该写的比较清楚了。

小工记三

前面这两周周末也都去老丈人家帮忙了,上上周周六先是去了那个在装修的旧房子那,把三楼收拾了下,因为要搬进来住,来不及等二楼装修好,就要把三楼里的东西都整理干净,这个活感觉是比较 easy,原来是就准备把三楼当放东西仓储的地方了,我们乡下大部分三层楼都是这么用的,这次也是没办法,之前搬进来的木头什么的都搬出去,主要是这上面灰尘太多,后面清理鼻孔的时候都是黑色的了,把东西都搬出去以后主要是地还是很脏,就扫了地拖了地,因为是水泥地,灰尘又太多了,拖起来都是会灰尘扬起来,整个脱完了的确干净很多,然而这会就出了个大乌龙,我们清理的是三楼的西边一间,结果老丈人上来说要住东边那间的🤦‍♂️,不过其实西边的也得清理,因为还是要放被子什么的,不算是白费功夫,接着清理东边那间,之前这个房子做过群租房,里面有个高低铺的床,当时觉得可以用在放被子什么的就没扔,只是拆掉了放旁边,我们就把它擦干净了又装好,发现螺丝🔩少了几个,亘古不变的真理,拆了以后装要不就多几个要不就少几个,不是很牢靠,不过用来放放被子省得放地上总还是可以的,对了前面还做了个事情就是铺地毯,其实也不是地毯,就是类似于墙布雨篷布那种,别人不用了送给我们的,三楼水泥地也不会铺瓷砖地板了就放一下,干净好看点,不过大小不合适要裁一下,那把剪刀是真的太难用了,我手都要抽筋了,它就是刀口只有一小个点是能剪下来的,其他都是钝的,后来还是用刀片直接裁,铺好以后,真的感觉也不太一样了,焕然一新的感觉
差不多中午了就去吃饭了,之前两次是去了一家小饭店,还是还比较干净,但是店里菜不好吃,还死贵,这次去了一家小快餐店,口味好,便宜,味道是真的不错,带鱼跟黄鱼都好吃,一点都不腥,我对这类比较腥的鱼真的是很挑剔的,基本上除了家里做的很少吃外面的,那天抱着试试的态度吃了下,真的还不错,后来丈母娘说好像这家老板是给别人结婚喜事酒席当厨师的,怪不得做的好吃,其实本来是有一点小抗拒,怕不干净什么的,后来发现菜很好吃,而且可能是老丈人跟干活的师傅去吃的比较多,老板很客气,我们吃完饭,还给我们买了葡萄吃,不过这家店有一个槽点,就是饭比较不好吃,有时候会夹生,不过后面聊起来其实是这种小菜馆饭点的通病,烧的太早太多容易多出来浪费,烧的迟了不够吃,而且大的电饭锅比较不容易烧好。
下午前面还是在处理三楼的,窗户上各种钉子,实在是太多了,我后面在走廊上排了一排🤦‍♂️,有些是直接断了,有些是就撬了出来,感觉我在杭州租房也没有这样子各种钉钉子,挂下衣服什么的也不用这么多吧,比较不能理解,搞得到处都是钉子。那天我爸也去帮忙了,主要是在卫生间里做白缝,其实也是个技术活,印象中好像我小时候自己家里也做过这个事情,但是比较模糊了,后面我们三楼搞完了就去帮我爸了,前面是我老婆二爹在那先刷上白缝,这里叫白缝,有些考究的也叫美缝,就是瓷砖铺完之后的缝,如果不去弄的话,里面水泥的颜色就露出来了,而且容易渗水,所以就要用白水泥加胶水搅拌之后糊在缝上,但是也不是直接糊,先要把缝抠一抠,因为铺瓷砖的还不会仔细到每个缝里的水泥都是一样满,而且也需要一些空间糊上去,不然就太表面的一层很容易被水直接冲掉了,然后这次其实也不是用的白水泥,而是直接现成买来就已经配好的用来填缝的,兑水搅拌均匀就好了,后面就主要是我跟我爸在搞,那个时候真的觉得我实在是太胖了,蹲下去真的没一会就受不了了,膝盖什么的太难受了,后面我跪着刷,然后膝盖又疼,也是比较不容易,不过我爸动作很快,我中间跪累了休息一会,我爸就能搞一大片,后面其实我也没做多少(谦虚一下),总体来讲这次不是很累,就是蹲着跪着腿有点受不了,是应该好好减肥了。

这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
在这里做了替换,将#{}替换成了?

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

// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
+}

注释应该写的比较清楚了。

小工记三

前面这两周周末也都去老丈人家帮忙了,上上周周六先是去了那个在装修的旧房子那,把三楼收拾了下,因为要搬进来住,来不及等二楼装修好,就要把三楼里的东西都整理干净,这个活感觉是比较 easy,原来是就准备把三楼当放东西仓储的地方了,我们乡下大部分三层楼都是这么用的,这次也是没办法,之前搬进来的木头什么的都搬出去,主要是这上面灰尘太多,后面清理鼻孔的时候都是黑色的了,把东西都搬出去以后主要是地还是很脏,就扫了地拖了地,因为是水泥地,灰尘又太多了,拖起来都是会灰尘扬起来,整个脱完了的确干净很多,然而这会就出了个大乌龙,我们清理的是三楼的西边一间,结果老丈人上来说要住东边那间的🤦‍♂️,不过其实西边的也得清理,因为还是要放被子什么的,不算是白费功夫,接着清理东边那间,之前这个房子做过群租房,里面有个高低铺的床,当时觉得可以用在放被子什么的就没扔,只是拆掉了放旁边,我们就把它擦干净了又装好,发现螺丝🔩少了几个,亘古不变的真理,拆了以后装要不就多几个要不就少几个,不是很牢靠,不过用来放放被子省得放地上总还是可以的,对了前面还做了个事情就是铺地毯,其实也不是地毯,就是类似于墙布雨篷布那种,别人不用了送给我们的,三楼水泥地也不会铺瓷砖地板了就放一下,干净好看点,不过大小不合适要裁一下,那把剪刀是真的太难用了,我手都要抽筋了,它就是刀口只有一小个点是能剪下来的,其他都是钝的,后来还是用刀片直接裁,铺好以后,真的感觉也不太一样了,焕然一新的感觉
差不多中午了就去吃饭了,之前两次是去了一家小饭店,还是还比较干净,但是店里菜不好吃,还死贵,这次去了一家小快餐店,口味好,便宜,味道是真的不错,带鱼跟黄鱼都好吃,一点都不腥,我对这类比较腥的鱼真的是很挑剔的,基本上除了家里做的很少吃外面的,那天抱着试试的态度吃了下,真的还不错,后来丈母娘说好像这家老板是给别人结婚喜事酒席当厨师的,怪不得做的好吃,其实本来是有一点小抗拒,怕不干净什么的,后来发现菜很好吃,而且可能是老丈人跟干活的师傅去吃的比较多,老板很客气,我们吃完饭,还给我们买了葡萄吃,不过这家店有一个槽点,就是饭比较不好吃,有时候会夹生,不过后面聊起来其实是这种小菜馆饭点的通病,烧的太早太多容易多出来浪费,烧的迟了不够吃,而且大的电饭锅比较不容易烧好。
下午前面还是在处理三楼的,窗户上各种钉子,实在是太多了,我后面在走廊上排了一排🤦‍♂️,有些是直接断了,有些是就撬了出来,感觉我在杭州租房也没有这样子各种钉钉子,挂下衣服什么的也不用这么多吧,比较不能理解,搞得到处都是钉子。那天我爸也去帮忙了,主要是在卫生间里做白缝,其实也是个技术活,印象中好像我小时候自己家里也做过这个事情,但是比较模糊了,后面我们三楼搞完了就去帮我爸了,前面是我老婆二爹在那先刷上白缝,这里叫白缝,有些考究的也叫美缝,就是瓷砖铺完之后的缝,如果不去弄的话,里面水泥的颜色就露出来了,而且容易渗水,所以就要用白水泥加胶水搅拌之后糊在缝上,但是也不是直接糊,先要把缝抠一抠,因为铺瓷砖的还不会仔细到每个缝里的水泥都是一样满,而且也需要一些空间糊上去,不然就太表面的一层很容易被水直接冲掉了,然后这次其实也不是用的白水泥,而是直接现成买来就已经配好的用来填缝的,兑水搅拌均匀就好了,后面就主要是我跟我爸在搞,那个时候真的觉得我实在是太胖了,蹲下去真的没一会就受不了了,膝盖什么的太难受了,后面我跪着刷,然后膝盖又疼,也是比较不容易,不过我爸动作很快,我中间跪累了休息一会,我爸就能搞一大片,后面其实我也没做多少(谦虚一下),总体来讲这次不是很累,就是蹲着跪着腿有点受不了,是应该好好减肥了。

这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
在这里做了替换,将#{}替换成了?

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

// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
 public SqlSource parseScriptNode() {
     MixedSqlNode rootSqlNode = parseDynamicTags(context);
     SqlSource sqlSource;
diff --git a/search.xml b/search.xml
index 7cb23d068c..4a96ce6478 100644
--- a/search.xml
+++ b/search.xml
@@ -1,32 +1,23 @@
 
 
   
-    2019年终总结
-    /2020/02/01/2019%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/
-    今天是农历初八了,年前一个月的时候就准备做下今年的年终总结,可是写了一点觉得太情绪化了,希望后面写个平淡点的,正好最近技术方面还没有看到一个完整成文的内容,就来写一下这一年的总结,尽量少写一点太情绪化的东西。

-

跳槽

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

-

面试

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

-

技术方向

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

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

生活

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

-

期待

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

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

+

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

+

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

+

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

+]]>
+ + 生活 + 年中总结 + 2020 + + + 生活 + 2020 + 年中总结 + +
+ + 2021 年中总结 + /2021/07/18/2021-%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + 又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

+

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

+

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

+

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

+

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

+

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

+]]>
+ + 生活 + 年中总结 + 2021 + + + 生活 + 2021 + 年中总结 + 技术 + 读书 + +
2021 年终总结 /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ @@ -615,112 +648,297 @@ public: - 村上春树《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/07/18/2021-%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

-

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

-

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

-

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

-

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

-

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

-]]>
- - 生活 - 年中总结 - 2021 - - - 生活 - 2021 - 技术 - 读书 - 年中总结 - -
- - AbstractQueuedSynchronizer - /2019/09/23/AbstractQueuedSynchronizer/ - 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
首先 AQS 只要有这些属性

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

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

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

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

+

第一个线程

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

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

第二个线程

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

+
/**
+     * Acquires in exclusive mode, ignoring interrupts.  Implemented
+     * by invoking at least once {@link #tryAcquire},
+     * returning on success.  Otherwise the thread is queued, possibly
+     * repeatedly blocking and unblocking, invoking {@link
+     * #tryAcquire} until success.  This method can be used
+     * to implement method {@link Lock#lock}.
+     *
+     * @param arg the acquire argument.  This value is conveyed to
+     *        {@link #tryAcquire} but is otherwise uninterpreted and
+     *        can represent anything you like.
      */
-    // 同样的不分析,略过吧
-    static final int PROPAGATE = -3;
-    // =====================================================
-
+    public final void acquire(int arg) {
+        // 前面第一种情况是tryAcquire直接成功了,这个if判断第一个条件就是false,就不往下执行了
+        // 如果是第二个线程,第一个条件获取锁不成功,条件判断!tryAcquire(arg) == true,就会走
+        // acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
+        if (!tryAcquire(arg) &&
+            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
+            selfInterrupt();
+    }
- // 取值为上面的1、-1、-2、-3,或者0(以后会讲到) - // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待, - // ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。 - volatile int waitStatus; - // 前驱节点的引用 - volatile Node prev; - // 后继节点的引用 - volatile Node next; - // 这个就是线程本尊 - volatile Thread thread; +

然后来看下addWaiter的逻辑

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

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

-]]>
- - java - - - java - aqs +

然后就是enq的逻辑了

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

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

+

解锁

通过代码来看下

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

-

第一个线程

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

-
/**
-         * Fair version of tryAcquire.  Don't grant access unless
-         * recursive call or no waiters or is first.
-         */
-        protected final boolean tryAcquire(int acquires) {
-            final Thread current = Thread.currentThread();
-            int c = getState();
-            // 这里如果state还是0说明锁还空着
-            if (c == 0) {
-                // 因为是公平锁版本的,先去看下是否阻塞队列里有排着队的
-                if (!hasQueuedPredecessors() &&
-                    compareAndSetState(0, acquires)) {
-                    // 没有排队的,并且state使用cas设置成功的就标记当前占有锁的线程是我
-                    setExclusiveOwnerThread(current);
-                    // 然后其实就返回了,包括阻塞队列的head和tail节点和waitStatus都没有设置
-                    return true;
-                }
-            }
-            else if (current == getExclusiveOwnerThread()) {
-                int nextc = c + acquires;
-                if (nextc < 0)
-                    throw new Error("Maximum lock count exceeded");
-                setState(nextc);
-                return true;
-            }
-            // 这里就是第二个线程会返回false
-            return false;
-        }
-    }
+ Comparator使用小记 + /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/ + 在Java8的stream之前,将对象进行排序的时候,可能需要对象实现Comparable接口,或者自己实现一个Comparator,

+

比如这样子

+

我的对象是Entity

+
public class Entity {
 
-

第二个线程

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

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

然后来看下addWaiter的逻辑

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

然后就是enq的逻辑了

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

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

-

解锁

通过代码来看下

-
/**
-     * Attempts to release this lock.
-     *
-     * <p>If the current thread is the holder of this lock then the hold
-     * count is decremented.  If the hold count is now zero then the lock
-     * is released.  If the current thread is not the holder of this
-     * lock then {@link IllegalMonitorStateException} is thrown.
-     *
-     * @throws IllegalMonitorStateException if the current thread does not
-     *         hold this lock
-     */
-    public void unlock() {
-        // 释放锁
-        sync.release(1);
+    public void setId(Long id) {
+        this.id = id;
     }
-/**
-     * Releases in exclusive mode.  Implemented by unblocking one or
-     * more threads if {@link #tryRelease} returns true.
-     * This method can be used to implement method {@link Lock#unlock}.
-     *
-     * @param arg the release argument.  This value is conveyed to
-     *        {@link #tryRelease} but is otherwise uninterpreted and
-     *        can represent anything you like.
-     * @return the value returned from {@link #tryRelease}
-     */
-    public final boolean release(int arg) {
-        // 尝试去释放
-        if (tryRelease(arg)) {
-            Node h = head;
-            if (h != null && h.waitStatus != 0)
-                unparkSuccessor(h);
-            return true;
-        }
-        return false;
+
+    public Long getSortValue() {
+        return sortValue;
     }
-protected final boolean tryRelease(int releases) {
-            int c = getState() - releases;
-            if (Thread.currentThread() != getExclusiveOwnerThread())
-                throw new IllegalMonitorStateException();
-            boolean free = false;
-    		// 判断是否完全释放锁,因为可重入
-            if (c == 0) {
-                free = true;
-                setExclusiveOwnerThread(null);
-            }
-            setState(c);
-            return free;
-        }
-// 这段代码和上面的一致,只是为了顺序性,又拷下来看下
 
-public final boolean release(int arg) {
-        // 尝试去释放,如果是完全释放,返回的就是true,否则是false
-        if (tryRelease(arg)) {
-            Node h = head;
-            // 这里判断头结点是否为空以及waitStatus的状态,前面说了head节点其实是
-            // 在第二个线程进来的时候初始化的,如果是空的话说明没后续节点,并且waitStatus
-            // 也表示了后续的等待状态
-            if (h != null && h.waitStatus != 0)
-                unparkSuccessor(h);
-            return true;
+    public void setSortValue(Long sortValue) {
+        this.sortValue = sortValue;
+    }
+}
+ +

Comparator

+
public class MyComparator implements Comparator {
+    @Override
+    public int compare(Object o1, Object o2) {
+        Entity e1 = (Entity) o1;
+        Entity e2 = (Entity) o2;
+        if (e1.getSortValue() < e2.getSortValue()) {
+            return -1;
+        } else if (e1.getSortValue().equals(e2.getSortValue())) {
+            return 0;
+        } else {
+            return 1;
         }
-        return false;
     }
+}
-/** - * Wakes up node's successor, if one exists. - * - * @param node the node - */ -// 唤醒后继节点 - private void unparkSuccessor(Node node) { - /* - * If status is negative (i.e., possibly needing signal) try - * to clear in anticipation of signalling. It is OK if this - * fails or if status is changed by waiting thread. - */ - int ws = node.waitStatus; - if (ws < 0) - compareAndSetWaitStatus(node, ws, 0); +

比较代码

+
private static MyComparator myComparator = new MyComparator();
 
-        /*
-         * Thread to unpark is held in successor, which is normally
-         * just the next node.  But if cancelled or apparently null,
-         * traverse backwards from tail to find the actual
-         * non-cancelled successor.
-         */
-        Node s = node.next;
-        // 如果后继节点是空或者当前节点取消等待了
-        if (s == null || s.waitStatus > 0) {
-            s = null;
-            // 从后往前找,找到非取消的节点,注意这里不是找到就退出,而是一直找到头
-            // 所以不必担心中间有取消的
-            for (Node t = tail; t != null && t != node; t = t.prev)
-                if (t.waitStatus <= 0)
-                    s = t;
-        }
-        if (s != null)
-            // 将其唤醒
-            LockSupport.unpark(s.thread);
-    }
+ public static void main(String[] args) { + List<Entity> list = new ArrayList<Entity>(); + Entity e1 = new Entity(); + e1.setId(1L); + e1.setSortValue(1L); + list.add(e1); + Entity e2 = new Entity(); + e2.setId(2L); + e2.setSortValue(null); + list.add(e2); + Collections.sort(list, myComparator);
+

看到这里的e2的排序值是null,在Comparator中如果要正常运行的话,就得判空之类的,这里有两点需要,一个是不想写这个MyComparator,然后也没那么好排除掉list里排序值,那么有什么办法能解决这种问题呢,应该说java的这方面真的是很强大

+

+

看一下nullsFirst的实现

+
final static class NullComparator<T> implements Comparator<T>, Serializable {
+        private static final long serialVersionUID = -7569533591570686392L;
+        private final boolean nullFirst;
+        // if null, non-null Ts are considered equal
+        private final Comparator<T> real;
 
+        @SuppressWarnings("unchecked")
+        NullComparator(boolean nullFirst, Comparator<? super T> real) {
+            this.nullFirst = nullFirst;
+            this.real = (Comparator<T>) real;
+        }
 
+        @Override
+        public int compare(T a, T b) {
+            if (a == null) {
+                return (b == null) ? 0 : (nullFirst ? -1 : 1);
+            } else if (b == null) {
+                return nullFirst ? 1: -1;
+            } else {
+                return (real == null) ? 0 : real.compare(a, b);
+            }
+        }
+

核心代码就是下面这段,其实就是帮我们把前面要做的事情做掉了,是不是挺方便的,小记一下哈

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

譬如我在用的这个笔记本

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

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

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

+
public class LongEvent {
+    private long value;
 
-class Value extends LhsPadding
-{
-    protected volatile long value;
-}
+    public void set(long value) {
+        this.value = value;
+    }
 
-class RhsPadding extends Value
-{
+    public long getValue() {
+        return value;
+    }
+
+    public void setValue(long value) {
+        this.value = value;
+    }
+}
+

事件生产

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

事件处理器

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

主方法代码

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

运行下可以看到运行结果

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

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

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

事件生产

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

事件处理器

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

主方法代码

-
package disruptor;
-
-import com.lmax.disruptor.RingBuffer;
-import com.lmax.disruptor.dsl.Disruptor;
-import com.lmax.disruptor.util.DaemonThreadFactory;
-
-import java.nio.ByteBuffer;
-
-public class LongEventMain
-{
-    public static void main(String[] args) throws Exception
-    {
-        // 这个需要是 2 的幂次,这样在定位的时候只需要位移操作,也能减少各种计算操作
-        int bufferSize = 1024; 
-
-        Disruptor<LongEvent> disruptor = 
-                new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
-
-        // 类似于注册处理器
-        disruptor.handleEventsWith(new LongEventHandler());
-        // 或者直接用 lambda
-        disruptor.handleEventsWith((event, sequence, endOfBatch) ->
-                System.out.println("Event: " + event));
-        // 启动我们的 disruptor
-        disruptor.start(); 
-
-
-        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); 
-        ByteBuffer bb = ByteBuffer.allocate(8);
-        for (long l = 0; true; l++)
-        {
-            bb.putLong(0, l);
-            // 生产事件
-            ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
-            Thread.sleep(1000);
+    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 - Java - Disruptor + leetcode + java + 题解 + DP
@@ -2926,133 +3047,90 @@ inorder = [9,3,15,20,7] - Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析 - /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

-

In one shift operation:

-

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

-

示例

Example 1:

-
-

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

-
-

Example 2:

-
-

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

-
-

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 155 最小栈(Min Stack) 题解分析
+    /2020/12/06/Leetcode-155-%E6%9C%80%E5%B0%8F%E6%A0%88-Min-Stack-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

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

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

示例

Example 1:

+
Input
+["MinStack","push","push","push","getMin","pop","top","getMin"]
+[[],[-2],[0],[-3],[],[],[],[]]
 
-                line.add(grid[passLine][last]);
+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() {
+
+        }
+
+        public void push(int x) {
+            // 放入主栈
+            s1.push(x);
+            // 当 s2 是空或者当前值是小于"等于" s2 栈顶时,压入辅助最小值的栈
+            // 注意这里的"等于"非常必要,因为当最小值有多个的情况下,也需要压入栈,否则在 pop 的时候就会不对等
+            if (s2.isEmpty() || x <= s2.peek()) {
+                s2.push(x);
             }
-            matrix.add(line);
         }
-        return matrix;
-    }
-

结果数据


比较慢

-]]>
- - Java - leetcode - - - leetcode - java - 题解 - Shift 2D Grid - - - - 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];
+        public void pop() {
+            // 首先就是主栈要 pop,然后就是第二个了,跟上面的"等于"很有关系,
+            // 因为如果有两个最小值,如果前面等于的情况没有压栈,那这边相等的时候 pop 就会少一个了,可能就导致最小值不对了
+            int x = s1.pop();
+            if (x == s2.peek())  {
+                s2.pop();
+            }
         }
-        if (prices[i] > maxOut) {
-            // 表示一个可卖出点,即比买入值高时
-            maxOut = prices[i];
-            // 需要设置一个历史值
-            maxSofar = Math.max(maxSofar, maxOut - maxIn);
+
+        public int top() {
+            // 栈顶的元素
+            return s1.peek();
         }
-    }
-    return maxSofar;
-}
-

总结下

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

+ public int getMin() { + // 辅助最小栈的栈顶 + return s2.peek(); + } + }
+ ]]>
Java leetcode java - DP - DP + stack + stack leetcode java 题解 - DP + stack + min stack + 最小栈 + leetcode 155
@@ -3182,9 +3260,9 @@ Input Explanation: The intersected node's value is 8 (note that this must no Java leetcode - Linked List java Linked List + Linked List leetcode @@ -3255,90 +3333,32 @@ Output: [8,9,9,9,0,0,0,1] - Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析 - /2022/07/02/Leetcode-20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7-Valid-Parentheses-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

-

An input string is valid if:

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

示例

Example 1:

-

Input: s = “()”
Output: true

-
-

Example 2:

-

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

-
-

Example 3:

-

Input: s = “(]”
Output: false

-
-

Constraints:

    -
  • 1 <= s.length <= 10^4
  • -
  • s consists of parentheses only '()[]{}'.
  • + 2019年终总结 + /2020/02/01/2019%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 今天是农历初八了,年前一个月的时候就准备做下今年的年终总结,可是写了一点觉得太情绪化了,希望后面写个平淡点的,正好最近技术方面还没有看到一个完整成文的内容,就来写一下这一年的总结,尽量少写一点太情绪化的东西。

    +

    跳槽

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

    +

    面试

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

    +

    技术方向

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

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

    解析

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

    -

    代码

    class Solution {
    -    public boolean isValid(String s) {
    -
    -        if (s.length() % 2 != 0) {
    -            return false;
    -        }
    -        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;
    -            }
    -
    -        }
    -        return stk.size() == 0;
    -    }
    -}
    - +

    生活

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

    +

    期待

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

    ]]>
    - Java - leetcode + 生活 + 年终总结 + 2019 - leetcode - java + 生活 + 年终总结 + 2019 @@ -3386,9 +3406,9 @@ Output: [8,9,9,9,0,0,0,1] - 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/ + Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析 + /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

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

    +

    In one shift operation:

    +

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

    +

    示例

    Example 1:

    +
    +

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

    +
    +

    Example 2:

    +
    +

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

    +
    +

    Example 3:

    +

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

    +
    +

    提示

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

    解析

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

    +

    代码

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

    结果数据


    比较慢

    +]]>
    + + Java + leetcode + + + leetcode + java + 题解 + Shift 2D Grid + +
    + + Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析 + /2022/07/02/Leetcode-20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7-Valid-Parentheses-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

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

    +

    An input string is valid if:

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

    示例

    Example 1:

    +

    Input: s = “()”
    Output: true

    +
    +

    Example 2:

    +

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

    +
    +

    Example 3:

    +

    Input: s = “(]”
    Output: false

    +
    +

    Constraints:

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

    解析

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

    +

    代码

    class Solution {
    +    public boolean isValid(String s) {
    +
    +        if (s.length() % 2 != 0) {
    +            return false;
    +        }
    +        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;
    +            }
    +
    +        }
    +        return stk.size() == 0;
    +    }
    +}
    + +]]>
    + + Java + leetcode + + + leetcode + java + +
    + + 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 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

     

    示例

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

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

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

-

ziplist

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

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

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

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

压缩

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

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

插入过程

简单说下插入元素的过程

-
/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
-void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
-                   int where) {
-    if (where == QUICKLIST_HEAD) {
-        quicklistPushHead(quicklist, value, sz);
-    } else if (where == QUICKLIST_TAIL) {
-        quicklistPushTail(quicklist, value, sz);
-    }
-}
-
-/* Add new entry to head node of quicklist.
- *
- * Returns 0 if used existing head.
- * Returns 1 if new head created. */
-int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
-    quicklistNode *orig_head = quicklist->head;
-    if (likely(
-            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
-        quicklist->head->zl =
-            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
-        quicklistNodeUpdateSz(quicklist->head);
-    } else {
-        quicklistNode *node = quicklistCreateNode();
-        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
-
-        quicklistNodeUpdateSz(node);
-        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
-    }
-    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 @@ -6167,6 +5821,72 @@ REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist, 源码
+ + 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 + +
redis淘汰策略复习 /2021/08/01/redis%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ @@ -6652,6 +6372,44 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;源码 + + redis过期策略复习 + /2021/07/25/redis%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ + redis过期策略复习

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

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

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

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

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

+]]>
+ + redis + + + redis + 应用 + 过期策略 + +
redis系列介绍八-淘汰策略 /2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/ @@ -7181,108 +6939,6 @@ uint8_t LFULogIncr(uint8_t counter) { 源码 - - redis过期策略复习 - /2021/07/25/redis%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/ - redis过期策略复习

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

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

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

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

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

-]]>
- - redis - - - redis - 应用 - 过期策略 - -
- - rust学习笔记-所有权三之切片 - /2021/05/16/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%B8%89%E4%B9%8B%E5%88%87%E7%89%87/ - 除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice)。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。
例如代码

-
fn main() {
-    let mut s = String::from("hello world");
-
-    let word = first_word(&s);
-
-    s.clear();
-
-    // 这时候虽然 word 还是 5,但是 s 已经被清除了,所以就没存在的意义
-}
-

这里其实我们就需要关注 s 的存在性,代码的逻辑合理性就需要额外去维护,此时我们就可以用切片

-
let s = String::from("hello world")
-
-let hello = &s[0..5];
-let world = &s[6..11];
-

其实跟 Python 的list 之类的语法有点类似,当然里面还有些语法糖,比如可以直接用省略后面的数字表示直接引用到结尾

-
let hello = &s[0..];
-

甚至再进一步

-
let hello = &s[..];
-

使用了切片之后

-
fn first_word(s: &String) -> &str {
-    let bytes = s.as_bytes();
-
-    for (i, &item) in bytes.iter().enumerate() {
-        if item == b' ' {
-            return &s[0..i];
-        }
-    }
-
-    &s[..]
-}
-fn main() {
-    let mut s = String::from("hello world");
-
-    let word = first_word(&s);
-
-    s.clear(); // error!
-
-    println!("the first word is: {}", word);
-}
-

那再执行 main 函数的时候就会抛错,因为 word 还是个切片,需要保证 s 的有效性,并且其实我们可以将函数申明成

-
fn first_word(s: &str) -> &str {
-

这样就既能处理&String 的情况,就是当成完整字符串的切片,也能处理普通的切片。
其他类型的切片

-
let a = [1, 2, 3, 4, 5];
-let slice = &a[1..3];
-

简单记录下,具体可以去看看这本书

-]]>
- - 语言 - Rust - - - Rust - 所有权 - 内存分布 - 新语言 - 可变引用 - 不可变引用 - 切片 - -
rust学习笔记-所有权二 /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ @@ -7371,46 +7027,110 @@ hz 10 + + 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 + 所有权 + 内存分布 + 新语言 + +
+ + 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:
@@ -7431,6 +7151,35 @@ for _ in data:
         python
       
   
+  
+    summary-ranges-228
+    /2016/10/12/summary-ranges-228/
+    problem

Given a sorted integer array without duplicates, return the summary of its ranges.

+

For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].

+

题解

每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考

+

Code

class Solution {
+public:
+    vector<string> summaryRanges(vector<int>& nums) {
+        int i = 0, j = 1, n;
+        vector<string> res;
+        n = nums.size();
+        while(i < n){
+            j = 1;
+            while(j < n && nums[i+j] - nums[i] == j) j++;
+            res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
+            i += j;
+        }
+        return res;
+    }
+};
]]>
+ + leetcode + + + leetcode + c++ + +
spring event 介绍 /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ @@ -7673,89 +7422,220 @@ user3: - summary-ranges-228 - /2016/10/12/summary-ranges-228/ - problem

Given a sorted integer array without duplicates, return the summary of its ranges.

-

For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].

-

题解

每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考

-

Code

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

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

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

+

ziplist

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

+
size_t offset = (-fill) - 1;
+if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
+    if (sz <= optimization_level[offset]) {
+        return 1;
+    } else {
+        return 0;
     }
-};
]]>
- - leetcode - - - leetcode - c++ - - - - 《垃圾回收算法手册读书》笔记之整理算法 - /2021/03/07/%E3%80%8A%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%89%8B%E5%86%8C%E8%AF%BB%E4%B9%A6%E3%80%8B%E7%AC%94%E8%AE%B0%E4%B9%8B%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95/ - 最近看了下这本垃圾回收算法手册,看到了第三章的标记-整理回收算法,做个简单的读书笔记

-

双指针整理算法

对于一块待整理区域,通过两个指针,free 在区域的起始端,scan 指针在区域的末端,free 指针从前往后知道找到空闲区域,scan 从后往前一直找到存活对象,当 free 指针未与 scan 指针交叉时,会给 scan 位置的对象特定位置标记上 free 的地址,即将要转移的地址,不过这里有个限制,这种整理算法一般会用于对象大小统一的情况,否则 free 指针扫描时还需要匹配scan 指针扫描到的存活对象的大小。

-

Lisp 2 整理算法

需要三次完整遍历堆区域
第一遍是遍历后将计算出所有对象的最终地址(转发地址)
第二遍是使用转发地址更新赋值器线程根以及被标记对象中的引用,该操作将确保它们指向对象的新位置
第三次遍历是relocate最终将存活对象移动到其新的目标位置

-

引线整理算法

这个真的长见识了,

可以看到,原来是 A,B,C 对象引用了 N,这里会在第一次遍历的时候把这种引用反过来,让 N 的对象头部保存下 A 的地址,表示这类引用,然后在遍历到 B 的时候在链起来,到最后就会把所有引用了 N 对象的所有对象通过引线链起来,在第二次遍历的时候就把更新A,B,C 对象引用的 N 地址,并且移动 N 对象

-

单次遍历算法

这个一直提到过位图的实现方式,

可以看到在第一步会先通过位图标记,标记的方式是位图的每一位对应的堆内存的一个字(这里可能指的是 byte 吧),然后将一个存活对象的内存区域的第一个字跟最后一个字标记,这里如果在通过普通的方式就还需要一个地方在存转发地址,但是因为具体的位置可以通过位图算出来,也就不需要额外记录了

-]]>
- - Java - gc - jvm - - - java - gc - 标记整理 - 垃圾回收 - jvm - -
- - 一个 nginx 的简单记忆点 - /2022/08/21/%E4%B8%80%E4%B8%AA-nginx-%E7%9A%84%E7%AE%80%E5%8D%95%E8%AE%B0%E5%BF%86%E7%82%B9/ - 上周在处理一个 nginx 配置的时候,发现了一个之前不理解的小点,说一个场景,就是我们一般的处理方式就是一个 ip 端口只能配置一个域名的服务,比如 https://nicksxs.me 对应配置到 127.0.0.1:443,如果我想要把 https://nicksxs.com 也解析到这个服务器,并转发到不同的下游,这里就需要借助所谓的 SNI 的功能

-

Server Name Indication

A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. SNI is currently supported by most modern browsers, though may not be used by some old or special clients.
来源
机翻一下:在单个 IP 地址上运行多个 HTTPS 服务器的更通用的解决方案是 TLS 服务器名称指示扩展(SNI,RFC 6066),它允许浏览器在 SSL 握手期间传递请求的服务器名称,因此,服务器将知道哪个 它应该用于连接的证书。 目前大多数现代浏览器都支持 SNI,但某些旧的或特殊的客户端可能不使用 SNI。

-

首先我们需要确认 sni 已被支持

在实际的配置中就可以这样

-
stream {
-  map $ssl_preread_server_name $stream_map {
-    nicksxs.me nme;
-    nicksxs.com ncom;
-  }
+} else {
+    return 0;
+}
 
-  upstream nme {
-    server 127.0.0.1:8000;
-  }
-  upstream ncom {
-    server 127.0.0.1:8001;
-  }
+/* Optimization levels for size-based filling */
+static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
 
-  server {
-    listen 443 reuseport;
-    proxy_pass $stream_map;
-    ssl_preread on;
-  }
-}
-

类似这样,但是这个理解是非常肤浅和不完善的,只是简单记忆下,后续再进行补充完整

-

还有一点就是我们在配置的时候经常配置就是 server_name,但是会看到直接在使用 ssl_server_name,
其实在listen 标识了 ssl, 对应的 ssl_server_name 就等于 server_name,不需要额外处理了。

+/* 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节点的操作类似链表的插入。

]]>
- nginx + Redis + 数据结构 + C + 源码 + Redis - nginx + redis + 数据结构 + 源码
@@ -7773,33 +7653,205 @@ public: - 介绍一下 RocketMQ - /2020/06/21/%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B-RocketMQ/ - 说起消息队列一般Web后端做过一段时间开发的肯定会用过,在前司的时候用的是改良版的 NSQ,有点像 NOSQL 的简写版🙄,其实是个go 语言写的消息队列,nsq 看代码提交感觉最近更新的不是很勤,不过因为前司有专门的中间件团队,所以还是挺好用的,而且中间件团队的大牛也很厉害,一次都没碰到过丢消息之类的错误,然后现在公司用的是 RocketMQ,本着总还是要了解下的,并且消息队列也是服务端开发中一个很重要的中间件,因为不太有不需要用消息队列的后端团队了吧,原来对 nsq 也不是特别了解原理,就打算了解下 RocketMQ。

-

还是像我这样的小白专属,消息队列用来干啥,很多都是标准答案,用来削峰填谷的,这个完全对,只是我想结合场景说给像我这样的小白同学听,想想一个电商的下单功能,除了 AT 两家之外应该大部分都是接入的支付,那么下单支付完成后一般都是等支付回调,告诉你支付完成了(也有可能是失败了,或者超时了咱们主动去查),然后这个回调里我们自己的业务代码干点啥,首先比如是把订单状态改掉了,然后会有各类的操作,比如把优惠券核销了,把其他金钱相关的也核销了,把购物车里对应的商品给删了,还有更次要的,比如发个客服消息,让用户确认下地址的,给用户加积分的等等等等,想象下如果这些都是回调里一股脑儿做掉了,那可能你的代码健壮性跟相关服务的稳定性还有性能要达到一个非常高的水平才能让业务不出现异常,并且万一流量打起来了,这些重要的不重要的操作都会阻塞着,所以需要用一个消息队列,在接到回调后只处理极少的几个核心操作,完了就把这个消息丢进消息队列里,让各个业务方去消费这个消息,把客服消息发一下,给用户加个积分等等,这样子主要的业务流程需要处理的事情就少了,速度也加快了,这个例子呢不能严格算是削峰填谷的例子,不过也算是消息队列的比较典型的使用场景了,要说真实的削峰填谷的话其实可以这么理解,假如短时间内有 1w 个请求进来,系统能支持的 QPS 才 1000,那么正常情况下服务就挂了,或者被限流了,为了让服务正常,那么可以把这些请求先放进消息队列里,我服务端以拉的模式按我的处理能力来消费,这样就没啥问题了

-

扯了这么多来聊聊 RocketMQ 长啥样

-

6073827-a998e005dd13967c

-

总共有四大部分:NameServer,Broker,Producer,Consumer。

-

NameServer

NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。

-

NameServer压力不会太大,正常情况主要负责维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多,会导致一次心跳中,光Topic的数据就非常大,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败。

-

Broker

Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。

-
    -
  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • -
  • Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • -
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • -
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  • -
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。
  • -
-
Broker的特点

1.负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。

-

2.动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。

+ Leetcode 42 接雨水 (Trapping Rain Water) 题解分析 + /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/ + 题目介绍

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

+

示例


输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

+

简单分析

其实最开始的想法是从左到右扫区间,就是示例中的第一个水槽跟第二个水槽都可以用这个办法解决

前面这种是属于右侧比左侧高的情况,对于左侧高右侧低的就不行了,(写这篇的时候想起来可以再反着扫一遍可能可以)

所以这个方案不好,贴一下这个方案的代码

+
public int trap(int[] height) {
+    int lastLeft = -1;
+    int sum = 0;
+    int tempSum = 0;
+    boolean startFlag = true;
+    for (int j : height) {
+        if (startFlag && j <= 0) {
+            startFlag = false;
+            continue;
+        }
+        if (j >= lastLeft) {
+            sum += tempSum;
+            tempSum = 0;
+            lastLeft = j;
+        } else {
+            tempSum += lastLeft - j;
+        }
+    }
+    return sum;
+}
+

后面结合网上的解法,其实可以反过来,对于每个格子找左右侧的最大值,取小的那个和当前格子的差值就是这一个的储水量了

理解了这种想法,代码其实就不难了

+

代码

int n = height.length;
+if (n <= 2) {
+    return 0;
+}
+// 思路转变下,其实可以对于每一格算储水量,算法就是找到这一格左边的最高点跟这一格右边的最高点,
+// 比较两侧的最高点,取小的那个,然后再跟当前格子的高度对比,差值就是当前格的储水量
+int maxL[] = new int[n];
+int maxR[] = new int[n];
+int max = height[0];
+maxL[0] = 0;
+// 计算左侧的最高点
+for (int i = 1; i < n - 1; i++) {
+    maxL[i] = max;
+    if (max < height[i]) {
+        max = height[i];
+    }
+}
+max = height[n - 1];
+maxR[n - 1] = 0;
+int tempSum, sum = 0;
+// 计算右侧的最高点,并且同步算出来储水量,节省一个循环
+for (int i = n - 2; i > 0; i--) {
+    maxR[i] = max;
+    if (height[i] > max) {
+        max = height[i];
+    }
+    tempSum = Math.min(maxL[i], maxR[i]) - height[i];
+    if (tempSum > 0) {
+        sum += tempSum;
+    }
+}
+return sum;
+]]>
+ + Java + leetcode + + + leetcode + java + dp + 代码题解 + Trapping Rain Water + 接雨水 + Leetcode 42 + +
+ + 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:

-

Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。
Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后想NameServer注册,Producer、Consumer通过NameServer发现新Broker,立即跟该Broker直连,收发消息。

+

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.

-

3.高可用&高可靠

+

Example 2:

-

高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。
高可靠:所有发往broker的消息,有同步刷盘和异步刷盘机制;同步刷盘时,消息写入物理文件才会返回成功,异步刷盘时,只有机器宕机,才会产生消息丢失,broker挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电

+

Input: nums = [1,2,3,4], k = 3
Output: false

-

Producer

Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
RocketMQ提供三种发送方式:

+

Constraints:

+
    +
  • 1 <= k <= nums.length <= 16
  • +
  • 1 <= nums[i] <= 10^4
  • +
  • The frequency of each element is in the range [1, 4].
  • +
+

解析

看到这个题一开始以为挺简单,但是仔细想想问题还是挺多的,首先是分成 k 组,但是数量不限,应该需要用到回溯的方式,同时对于时间和空间复杂度也有要求,一开始这个代码是超时的,我也试了下 leetcode 上 discussion 里 vote 最高的提交也是超时的,不过看 discussion 里的帖子,貌似是后面加了一些条件,可以帮忙提高执行效率,第三条提示不太清楚意图,具体可以看下代码

+

代码

public boolean canPartitionKSubsets(int[] nums, int k) {
+    if (k == 1) {
+        return true;
+    }
+    int sum = 0, n;
+    n = nums.length;
+    for (int num : nums) {
+        sum += num;
+    }
+    if (sum % k != 0) {
+        return false;
+    }
+
+    int avg = sum / k;
+    // 排序
+    Arrays.sort(nums);
+    // 做个前置判断,如果最大值超过分组平均值了就可以返回 false 了
+    if (nums[n - 1] > avg) {
+        return false;
+    }
+    // 这里取了个巧,先将数组中元素就等于分组平均值的直接排除了
+    int calculated = 0;
+    for (int i = n - 1; i > 0; i--) {
+        if (nums[i] == avg) {
+            k--;
+            calculated++;
+        }
+    }
+
+    int[] bucket = new int[k];
+    // 初始化 bucket
+    for (int i = 0; i < k; i++) {
+        bucket[i] = avg;
+    }
+
+    // 提前做下边界判断
+    if (nums[n - 1] > avg) {
+        return false;
+    }
+
+    return backTraversal(nums, bucket, k, n - 1 - calculated);
+}
+
+private boolean backTraversal(int[] nums, int[] bucket, int k, int cur) {
+    if (cur < 0) {
+        return true;
+    }
+    for (int i = 0; i < k; i++) {
+        if (bucket[i] == nums[cur] || bucket[i] >= nums[cur] + nums[0]) {
+            // 判断如果当前 bucket[i] 剩余的数字等于nums[cur], 即当前bucket已经满了
+            // 或者如果当前 bucket[i] 剩余的数字大于等于 nums[cur] + nums[0] ,
+            // 因为nums 在经过排序后 nums[0]是最小值,如果加上 nums[0] 都已经超过bucket[i] 了,
+            // 那当前bucket[i] 肯定是没法由包含 nums[cur] 的组合组成一个满足和为前面 s/k 的组合了
+            // 这里判断的是 nums[cur] ,如果第一次 k 次循环都不符合其实就返回 false 了
+
+            // 而如果符合,就将 bucket[i] 减去 nums[cur] 再次进入递归,
+            // 这里进入递归有个收敛参数就是 cur - 1,因为其实判断 cur 递减作为一个结束条件
+            bucket[i] -= nums[cur];
+            // 符合条件,这里对应着入口,当 cur 被减到 0 了,就表示都符合了因为是根据所有值的和 s 和 k 组除出来的平均值,当所有数都通过前面的 if 判断符合了,并且每个数字都使用了,
+            // 即说明已经符合要求了
+            if (backTraversal(nums, bucket, k, cur - 1)) return true;
+            // 这边是个回退机制,如果前面 nums[cur]没办法组合成和为平均值的话就减掉进入下一个循环
+            bucket[i] += nums[cur];
+        }
+    }
+    return false;
+}
+ +

最后贴个图

+]]>
+ + Java + leetcode + + + leetcode + java + +
+ + 介绍一下 RocketMQ + /2020/06/21/%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B-RocketMQ/ + 说起消息队列一般Web后端做过一段时间开发的肯定会用过,在前司的时候用的是改良版的 NSQ,有点像 NOSQL 的简写版🙄,其实是个go 语言写的消息队列,nsq 看代码提交感觉最近更新的不是很勤,不过因为前司有专门的中间件团队,所以还是挺好用的,而且中间件团队的大牛也很厉害,一次都没碰到过丢消息之类的错误,然后现在公司用的是 RocketMQ,本着总还是要了解下的,并且消息队列也是服务端开发中一个很重要的中间件,因为不太有不需要用消息队列的后端团队了吧,原来对 nsq 也不是特别了解原理,就打算了解下 RocketMQ。

+

还是像我这样的小白专属,消息队列用来干啥,很多都是标准答案,用来削峰填谷的,这个完全对,只是我想结合场景说给像我这样的小白同学听,想想一个电商的下单功能,除了 AT 两家之外应该大部分都是接入的支付,那么下单支付完成后一般都是等支付回调,告诉你支付完成了(也有可能是失败了,或者超时了咱们主动去查),然后这个回调里我们自己的业务代码干点啥,首先比如是把订单状态改掉了,然后会有各类的操作,比如把优惠券核销了,把其他金钱相关的也核销了,把购物车里对应的商品给删了,还有更次要的,比如发个客服消息,让用户确认下地址的,给用户加积分的等等等等,想象下如果这些都是回调里一股脑儿做掉了,那可能你的代码健壮性跟相关服务的稳定性还有性能要达到一个非常高的水平才能让业务不出现异常,并且万一流量打起来了,这些重要的不重要的操作都会阻塞着,所以需要用一个消息队列,在接到回调后只处理极少的几个核心操作,完了就把这个消息丢进消息队列里,让各个业务方去消费这个消息,把客服消息发一下,给用户加个积分等等,这样子主要的业务流程需要处理的事情就少了,速度也加快了,这个例子呢不能严格算是削峰填谷的例子,不过也算是消息队列的比较典型的使用场景了,要说真实的削峰填谷的话其实可以这么理解,假如短时间内有 1w 个请求进来,系统能支持的 QPS 才 1000,那么正常情况下服务就挂了,或者被限流了,为了让服务正常,那么可以把这些请求先放进消息队列里,我服务端以拉的模式按我的处理能力来消费,这样就没啥问题了

+

扯了这么多来聊聊 RocketMQ 长啥样

+

6073827-a998e005dd13967c

+

总共有四大部分:NameServer,Broker,Producer,Consumer。

+

NameServer

NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。

+

NameServer压力不会太大,正常情况主要负责维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多,会导致一次心跳中,光Topic的数据就非常大,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败。

+

Broker

Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。

+
    +
  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • +
  • Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • +
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • +
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  • +
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。
  • +
+
Broker的特点

1.负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。

+

2.动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。

+
+

Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。
Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后想NameServer注册,Producer、Consumer通过NameServer发现新Broker,立即跟该Broker直连,收发消息。

+
+

3.高可用&高可靠

+
+

高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。
高可靠:所有发往broker的消息,有同步刷盘和异步刷盘机制;同步刷盘时,消息写入物理文件才会返回成功,异步刷盘时,只有机器宕机,才会产生消息丢失,broker挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电

+
+

Producer

Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
RocketMQ提供三种发送方式:

同步:在广泛的场景中使用可靠的同步传输,如重要的通知信息、短信通知、短信营销系统等。
异步:异步发送通常用于响应时间敏感的业务场景,发送出去即刻返回,利用回调做后续处理。
一次性:一次性发送用于需要中等可靠性的情况,如日志收集,发送出去即完成,不用等待发送结果,回调等等。

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

+]]>
+ + 生活 + 运动 + + + 生活 + +
关于读书打卡与分享 /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/ @@ -7947,25 +8012,6 @@ public: 足球 - - 分享记录一下一个 git 操作方法 - /2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-git-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/ - 前阵子一个同事因为发现某个分支上的代码好像有缺失导致无法正常运行,然后就对比了下把缺失的代码从另一个分支上拷了过来,可能有所欠考虑,不过主要是说下操作过程和最后的处理方法,这位同学的操作是改一些代码commit 一下,这样的 commit 了大概五六次,并且已经 push 到了远端,然后就在想要怎么去处理,在本地可以 reset,已经到远端了,一个很不优雅的操作就是本地 reset 了用 force push,这个当然是不可取的,然后就是 revert 了,但是又已经 commit 了好几次了,网上看了下,好像处理方法还挺成熟的,git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。可以理解为就是提交一个反向的操作,这里其实我们可以用range revert来进行 git revert, 用法就是

-
git revert OLDER_COMMIT^..NEWER_COMMIT
-

这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明

然后就可以用 -n 参数,表示不自动提交

-
git revert -n OLDER_COMMIT^..NEWER_COMMIT
-git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"
- - -]]>
- - git - 小技巧 - - - git - -
分享记录一下一个 scp 操作方法 /2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-scp-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/ @@ -8024,19 +8070,6 @@ public: 干活 - - 上次的其他 外行聊国足 - /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 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
纯主观吐槽,勿喷。

-]]>
- - 生活 - 运动 - - - 生活 - -
在老丈人家的小工记五 /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/ @@ -8112,149 +8145,162 @@ public: - 我是如何走上跑步这条不归路的 - /2020/07/26/%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E8%B5%B0%E4%B8%8A%E8%B7%91%E6%AD%A5%E8%BF%99%E6%9D%A1%E4%B8%8D%E5%BD%92%E8%B7%AF%E7%9A%84/ - 这周因为没有准备技术方面的内容加之之前有想分享下我和跑步的一些事情,我从小学开始就是个体育渣,因为体重大非常胖,小学的时候要做仰卧起坐,基本是一个都起不来,然后那时候跑步也是要我命那种,跟另外一个比较胖的同学在跑步队尾苟延残喘,只有立定跳远还行。

-

时光飞逝,我在初中高中的时候因为爱打篮球,以为自己体质已经有了质的变化,所以在体育课跑步的时候妄图跟一位体育非常好的同学一起跑,结果跟的快断气了,最终还是确认了自己是个体育渣,特别是到了大学的第一次体测跑一千米,跑完直接吐了,一则是大学太宅不运动,二则的确是底子不好。那么怎么会去跑步了呢,其实也没什么特殊的原因,就是工作以后因为运动得更少,体质差,而且越来越胖,所以就想运动下,加之跑步也是我觉得成本最低的运动了,刚好那时候17 年租的地方附近小区周围的路车不太多,一圈刚好一公里多,就觉得开始跑跑看,其实想想以前觉得一千米是非常远的,学校塑胶跑道才 400 米,一千米要两圈半,太难了,但是后来在这个小区周围跑的时候好像跑了一圈以后还能再跑一点,最后跑了两圈,可把自己牛坏了,我都能跑两千米了,哈哈,这是个什么概念呢,大学里最让我绝望的两项体育相关的内容就是一千米和十二分钟跑,一千米把我跑吐了,十二分钟跑及格五圈半也能让我跑完花一周时间恢复以及提前一周心理压力爆炸,虽然我那时候跑的不快,但是已经能跑两千米了,瞬间让自己自信心爆炸,并且跑完步出完汗的感觉是非常棒的,毕竟吃奶茶鸡排都能心安理得了,谁叫我跑步了呢😄,其实现在回去看,那时候跑得还算快的,因为还比较瘦,现在要跑得那么快心跳就太快了,关于心跳什么的后面说,开始建立起自信心之后,对跑步这件事就开始不那么排斥跟害怕了,毕竟我能跑两千米了,然后试试三公里,哇,也可以了呢,三公里是什么概念呢,我大学里跑过最多的一次是十二分钟跑五圈半还是六圈,也就是两公里多,不到三公里,几乎是生涯最长了,一时间产生了一些我可能是个被埋没的运动天才的错觉,其实细想下也能明白,只是速度足够慢了就能跑多一点,毕竟提测一千米是要跑进四分钟才及格,自己跑的时候一千米跑六分多钟已经算不慢了(对我自己来说),但是即使是这样还是对把跑步坚持下去这件事有了很大的正面激励作用,并且由于那时候上下班骑车,整个体重控制的比较理想,导致一时间误会跑步就能非常快的减肥(其实这是我跑步历程中比较大的误区之一),因为会在跑步前后称下体重,如果跑个五公里(后面可以跑五公里了),可能体重就能降 0.5 千克,但实际上这只是这五公里跑步身体流失的水分,喝杯水就回来了,那时候能控制体重主要是骑车跟跑步一起的作用,并且工作压力相对来讲比较小,没有过劳肥。

-

后面其实跑步慢慢变得一个比较习惯的运动了,从三公里,到五公里,到七公里,再到十公里,十公里差不多对我来说是个坎,一直还不能比较轻松的跑十公里,可能近一两年好了一些(原谅我只是跟自己比较,跟那些大神比差得不知道多远),其实对我来说每次都是个突破,因为其实与他人比较没有特别大意义,比较顶尖的差得太远,比较普通的也不行,都会打击自信心,比较比我差的就更没意义了,所以其实能挑战自己,能把自己的上限提高才是最有意义的,这也是我看着朋友圈里的一些大神的速度除了佩服赞叹之外没什么其他的惭愧或者说嫌弃自己的感觉(阿 Q 精神😄)。

-

一直感性地觉得,跑步能解压,跑完浑身汗,有种把身体的负能量都排出去的感觉,也把吃太多的罪恶感排解掉了🤦‍♂️,之前朋友有看一本书,书名差不多叫越跑越接近自己,这个也是我觉得挺准确的一句话,当跑到接近极限了,还想再继续再跑一点,再跑一点就能突破自己上一次的最远记录了,再跑一点就能又一次突破自己了,成人以后,毕业以后,进入社会以后,世事总是难以件件顺遂,磕磕绊绊的往前走,总觉得要崩溃了,但是还是得坚持,再熬一下,再拼一下,可能还是失败,但人生呢,运气好的人和事总是小概率的,唯有面对挫折,还是日拱一卒,来日方长,我们再坚持下,没准下一次,没准再跑一会,就能突破自己,达到新的境界。

-

另外个人后期对跑步的一些知识和理解也变得深入一些,比如伤膝盖,其实跑步的确伤膝盖,需要做一些准备和防护,最好的是适合自己的跑鞋和比较好的路(最好的是塑胶跑道了),也要注意热身跟跑后的拉伸(虽然我做的很差),还有就是注意心率,每个人有自己的适宜心率,我这就不冒充科普达人了,可以自行搜索关键字,先说到这吧~

-]]>
- - 生活 - 运动 - 跑步 - - - 生活 - 运动 - 减肥 - 跑步 - -
- - 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 或者很低

+ 《垃圾回收算法手册读书》笔记之整理算法 + /2021/03/07/%E3%80%8A%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%89%8B%E5%86%8C%E8%AF%BB%E4%B9%A6%E3%80%8B%E7%AC%94%E8%AE%B0%E4%B9%8B%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95/ + 最近看了下这本垃圾回收算法手册,看到了第三章的标记-整理回收算法,做个简单的读书笔记

+

双指针整理算法

对于一块待整理区域,通过两个指针,free 在区域的起始端,scan 指针在区域的末端,free 指针从前往后知道找到空闲区域,scan 从后往前一直找到存活对象,当 free 指针未与 scan 指针交叉时,会给 scan 位置的对象特定位置标记上 free 的地址,即将要转移的地址,不过这里有个限制,这种整理算法一般会用于对象大小统一的情况,否则 free 指针扫描时还需要匹配scan 指针扫描到的存活对象的大小。

+

Lisp 2 整理算法

需要三次完整遍历堆区域
第一遍是遍历后将计算出所有对象的最终地址(转发地址)
第二遍是使用转发地址更新赋值器线程根以及被标记对象中的引用,该操作将确保它们指向对象的新位置
第三次遍历是relocate最终将存活对象移动到其新的目标位置

+

引线整理算法

这个真的长见识了,

可以看到,原来是 A,B,C 对象引用了 N,这里会在第一次遍历的时候把这种引用反过来,让 N 的对象头部保存下 A 的地址,表示这类引用,然后在遍历到 B 的时候在链起来,到最后就会把所有引用了 N 对象的所有对象通过引线链起来,在第二次遍历的时候就把更新A,B,C 对象引用的 N 地址,并且移动 N 对象

+

单次遍历算法

这个一直提到过位图的实现方式,

可以看到在第一步会先通过位图标记,标记的方式是位图的每一位对应的堆内存的一个字(这里可能指的是 byte 吧),然后将一个存活对象的内存区域的第一个字跟最后一个字标记,这里如果在通过普通的方式就还需要一个地方在存转发地址,但是因为具体的位置可以通过位图算出来,也就不需要额外记录了

]]>
Java - Dubbo - - - Java - Dubbo - RPC - 负载均衡 - -
- - 看完了扫黑风暴,聊聊感想 - /2021/10/24/%E7%9C%8B%E5%AE%8C%E4%BA%86%E6%89%AB%E9%BB%91%E9%A3%8E%E6%9A%B4-%E8%81%8A%E8%81%8A%E6%84%9F%E6%83%B3/ - 一直在想这篇怎么写,看了这部剧其实对我的一些观念是有影响的,应该是在 9 月份就看完了,到现在可能才会稍微平静一点,一开始是没有想看这部剧,因为同期有一部差不多同名的电影,被投诉了对湖南埋尸案家属伤害很大,我以为就是投诉的这部电视剧,后来同事跟我说不是,所以就想着看一下,但是没有马上看,因为一直不喜欢追这种比较纠结的剧,当时看人民的名义,就是后面等不了了直接看了小说,所以差不多是等到更完了才看的。

-

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

-

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

-

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

-]]>
- - 生活 + gc + jvm - 生活 - 影评 + java + gc + 标记整理 + 垃圾回收 + jvm
- Leetcode 155 最小栈(Min Stack) 题解分析 - /2020/12/06/Leetcode-155-%E6%9C%80%E5%B0%8F%E6%A0%88-Min-Stack-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

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

示例

Example 1:

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

简要分析

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

-

代码

class MinStack {
-        // 这个作为主栈
-        Stack<Integer> s1 = new Stack<>();
-        // 这个作为辅助栈,放最小值的栈
-        Stack<Integer> s2 = new Stack<>();
-        /** initialize your data structure here. */
-        public MinStack() {
-
-        }
-
-        public void push(int x) {
-            // 放入主栈
-            s1.push(x);
-            // 当 s2 是空或者当前值是小于"等于" s2 栈顶时,压入辅助最小值的栈
-            // 注意这里的"等于"非常必要,因为当最小值有多个的情况下,也需要压入栈,否则在 pop 的时候就会不对等
-            if (s2.isEmpty() || x <= s2.peek()) {
-                s2.push(x);
-            }
-        }
-
-        public void pop() {
-            // 首先就是主栈要 pop,然后就是第二个了,跟上面的"等于"很有关系,
-            // 因为如果有两个最小值,如果前面等于的情况没有压栈,那这边相等的时候 pop 就会少一个了,可能就导致最小值不对了
-            int x = s1.pop();
-            if (x == s2.peek())  {
-                s2.pop();
-            }
-        }
-
-        public int top() {
-            // 栈顶的元素
-            return s1.peek();
-        }
-
-        public int getMin() {
-            // 辅助最小栈的栈顶
-            return s2.peek();
-        }
-    }
- + 搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答 + /2022/01/16/%E6%90%AC%E8%BF%90%E4%B8%A4%E4%B8%AA-StackOverflow-%E4%B8%8A%E7%9A%84-Mysql-%E7%BC%96%E7%A0%81%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94/ + Mysql 字符编码和排序规则

这个一直是属于一知半解的状态,知道 utf8 跟 utf8mb4 的区别主要是能不能支持 emoji,但是具体后面配置的排序规则是用来干嘛,或者有什么区别,应该使用哪个,所以在 stackoverflow 上找了下,有两个比较不错的解答,就搬过来并且配合机翻做了点修改

+

原文

For those people still arriving at this question in 2020 or later, there are newer options that may be better than both of these. For example, utf8mb4_0900_ai_ci.

+

All these collations are for the UTF-8 character encoding. The differences are in how text is sorted and compared.

+

_unicode_ci and _general_ci are two different sets of rules for sorting and comparing text according to the way we expect. Newer versions of MySQL introduce new sets of rules, too, such as _0900_ai_ci for equivalent rules based on Unicode 9.0 - and with no equivalent _general_ci variant. People reading this now should probably use one of these newer collations instead of either _unicode_ci or _general_ci. The description of those older collations below is provided for interest only.

+

MySQL is currently transitioning away from an older, flawed UTF-8 implementation. For now, you need to use utf8mb4 instead of utf8 for the character encoding part, to ensure you are getting the fixed version. The flawed version remains for backward compatibility, though it is being deprecated.

+

Key differences

+

utf8mb4_unicode_ci is based on the official Unicode rules for universal sorting and comparison, which sorts accurately in a wide range of languages.

+

utf8mb4_general_ci is a simplified set of sorting rules which aims to do as well as it can while taking many short-cuts designed to improve speed. It does not follow the Unicode rules and will result in undesirable sorting or comparison in some situations, such as when using particular languages or characters.

+

On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.

+

Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci

+

utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.

+

As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:

+

For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).

+

Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.

+

In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.

+

What should you use?

+

There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.

+

In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.

+

There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.

+

One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.

+

What the parts mean

+

Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.

+

Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.

+

And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.

+

翻译

对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci

+

所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。

+

_unicode_ci_general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci_general_ci。下面对那些较旧的排序规则的描述仅供参考。

+

MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。

+

主要区别

+

utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。

+

utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。

+

在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。

+

utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势

+

utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。

+

就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:

+

例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。

+

一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。

+

在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。

+

你应该用什么?

+

几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。

+

过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。

+

有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。

+

我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。

+

其余各个部分是什么意思

+

首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。

+

接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。

+

最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。

+

utf8 和 utf8mb4 编码有什么区别

原文

UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.

+

So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.

+

This is what (a previous version of the same page at)the MySQL documentationhas to say about it:

+
+

The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:

+
    +
  • For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
  • +
  • For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
  • +
+
+

So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.

+

译文

UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。

+

所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较

+

这就是(同一页面的先前版本)MySQL 文档 不得不说的:

+
+

名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:

+
    +
  • 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
  • +
  • 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
  • +
+
+

因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅

+

实际使用中最常见的非 BMP Unicode 字符是什么?

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

+]]>
+ + 生活 + + + 生活 + +
+ + 看完了扫黑风暴,聊聊感想 + /2021/10/24/%E7%9C%8B%E5%AE%8C%E4%BA%86%E6%89%AB%E9%BB%91%E9%A3%8E%E6%9A%B4-%E8%81%8A%E8%81%8A%E6%84%9F%E6%83%B3/ + 一直在想这篇怎么写,看了这部剧其实对我的一些观念是有影响的,应该是在 9 月份就看完了,到现在可能才会稍微平静一点,一开始是没有想看这部剧,因为同期有一部差不多同名的电影,被投诉了对湖南埋尸案家属伤害很大,我以为就是投诉的这部电视剧,后来同事跟我说不是,所以就想着看一下,但是没有马上看,因为一直不喜欢追这种比较纠结的剧,当时看人民的名义,就是后面等不了了直接看了小说,所以差不多是等到更完了才看的。

+

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

+

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

+

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

+]]>
+ + 生活 + + + 生活 + 影评 + +
+ + 我是如何走上跑步这条不归路的 + /2020/07/26/%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E8%B5%B0%E4%B8%8A%E8%B7%91%E6%AD%A5%E8%BF%99%E6%9D%A1%E4%B8%8D%E5%BD%92%E8%B7%AF%E7%9A%84/ + 这周因为没有准备技术方面的内容加之之前有想分享下我和跑步的一些事情,我从小学开始就是个体育渣,因为体重大非常胖,小学的时候要做仰卧起坐,基本是一个都起不来,然后那时候跑步也是要我命那种,跟另外一个比较胖的同学在跑步队尾苟延残喘,只有立定跳远还行。

+

时光飞逝,我在初中高中的时候因为爱打篮球,以为自己体质已经有了质的变化,所以在体育课跑步的时候妄图跟一位体育非常好的同学一起跑,结果跟的快断气了,最终还是确认了自己是个体育渣,特别是到了大学的第一次体测跑一千米,跑完直接吐了,一则是大学太宅不运动,二则的确是底子不好。那么怎么会去跑步了呢,其实也没什么特殊的原因,就是工作以后因为运动得更少,体质差,而且越来越胖,所以就想运动下,加之跑步也是我觉得成本最低的运动了,刚好那时候17 年租的地方附近小区周围的路车不太多,一圈刚好一公里多,就觉得开始跑跑看,其实想想以前觉得一千米是非常远的,学校塑胶跑道才 400 米,一千米要两圈半,太难了,但是后来在这个小区周围跑的时候好像跑了一圈以后还能再跑一点,最后跑了两圈,可把自己牛坏了,我都能跑两千米了,哈哈,这是个什么概念呢,大学里最让我绝望的两项体育相关的内容就是一千米和十二分钟跑,一千米把我跑吐了,十二分钟跑及格五圈半也能让我跑完花一周时间恢复以及提前一周心理压力爆炸,虽然我那时候跑的不快,但是已经能跑两千米了,瞬间让自己自信心爆炸,并且跑完步出完汗的感觉是非常棒的,毕竟吃奶茶鸡排都能心安理得了,谁叫我跑步了呢😄,其实现在回去看,那时候跑得还算快的,因为还比较瘦,现在要跑得那么快心跳就太快了,关于心跳什么的后面说,开始建立起自信心之后,对跑步这件事就开始不那么排斥跟害怕了,毕竟我能跑两千米了,然后试试三公里,哇,也可以了呢,三公里是什么概念呢,我大学里跑过最多的一次是十二分钟跑五圈半还是六圈,也就是两公里多,不到三公里,几乎是生涯最长了,一时间产生了一些我可能是个被埋没的运动天才的错觉,其实细想下也能明白,只是速度足够慢了就能跑多一点,毕竟提测一千米是要跑进四分钟才及格,自己跑的时候一千米跑六分多钟已经算不慢了(对我自己来说),但是即使是这样还是对把跑步坚持下去这件事有了很大的正面激励作用,并且由于那时候上下班骑车,整个体重控制的比较理想,导致一时间误会跑步就能非常快的减肥(其实这是我跑步历程中比较大的误区之一),因为会在跑步前后称下体重,如果跑个五公里(后面可以跑五公里了),可能体重就能降 0.5 千克,但实际上这只是这五公里跑步身体流失的水分,喝杯水就回来了,那时候能控制体重主要是骑车跟跑步一起的作用,并且工作压力相对来讲比较小,没有过劳肥。

+

后面其实跑步慢慢变得一个比较习惯的运动了,从三公里,到五公里,到七公里,再到十公里,十公里差不多对我来说是个坎,一直还不能比较轻松的跑十公里,可能近一两年好了一些(原谅我只是跟自己比较,跟那些大神比差得不知道多远),其实对我来说每次都是个突破,因为其实与他人比较没有特别大意义,比较顶尖的差得太远,比较普通的也不行,都会打击自信心,比较比我差的就更没意义了,所以其实能挑战自己,能把自己的上限提高才是最有意义的,这也是我看着朋友圈里的一些大神的速度除了佩服赞叹之外没什么其他的惭愧或者说嫌弃自己的感觉(阿 Q 精神😄)。

+

一直感性地觉得,跑步能解压,跑完浑身汗,有种把身体的负能量都排出去的感觉,也把吃太多的罪恶感排解掉了🤦‍♂️,之前朋友有看一本书,书名差不多叫越跑越接近自己,这个也是我觉得挺准确的一句话,当跑到接近极限了,还想再继续再跑一点,再跑一点就能突破自己上一次的最远记录了,再跑一点就能又一次突破自己了,成人以后,毕业以后,进入社会以后,世事总是难以件件顺遂,磕磕绊绊的往前走,总觉得要崩溃了,但是还是得坚持,再熬一下,再拼一下,可能还是失败,但人生呢,运气好的人和事总是小概率的,唯有面对挫折,还是日拱一卒,来日方长,我们再坚持下,没准下一次,没准再跑一会,就能突破自己,达到新的境界。

+

另外个人后期对跑步的一些知识和理解也变得深入一些,比如伤膝盖,其实跑步的确伤膝盖,需要做一些准备和防护,最好的是适合自己的跑鞋和比较好的路(最好的是塑胶跑道了),也要注意热身跟跑后的拉伸(虽然我做的很差),还有就是注意心率,每个人有自己的适宜心率,我这就不冒充科普达人了,可以自行搜索关键字,先说到这吧~

+]]>
+ + 生活 + 运动 + 跑步 + + + 生活 + 运动 + 减肥 + 跑步
@@ -9050,94 +9096,6 @@ minStack.getMin(); // return -2 聊一下 RocketMQ 的 NameServer 源码 /2020/07/05/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-NameServer-%E6%BA%90%E7%A0%81/ @@ -9766,15 +9724,60 @@ minStack.getMin(); // return -2 @@ -10461,6 +10464,20 @@ minStack.getMin(); // return -2 - 聊聊 Dubbo 的 SPI - /2020/05/31/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI/ - SPI全称是Service Provider Interface,咋眼看跟api是不是有点相似,api是application interface,这两个其实在某些方面有类似的地方,也有蛮大的区别,比如我们基于 dubbo 的微服务,一般我们可以提供服务,然后非泛化调用的话,我们可以把 api 包提供给应用调用方,他们根据接口签名传对应参数并配置好对应的服务发现如 zk 等就可以调用我们的服务了,然后 spi 会有点类似但是是反过来的关系,相当于是一种规范,比如我约定完成这个功能需要两个有两个接口,一个是连接的,一个是断开的,其实就可以用 jdbc 的驱动举例,比较老套了,然后各个厂家去做具体的实现吧,到时候根据我接口的全限定名的文件来加载实际的实现类,然后运行的时候调用对应实现类的方法就完了

-

3sKdpg

-

看上面的图,java.sql.Driver就是 spi,对应在classpath 的 META-INF/services 目录下的这个文件,里边的内容就是具体的实现类

-

1590735097909

-

简单介绍了 Java的 SPI,再来说说 dubbo 的,dubbo 中为啥要用 SPI 呢,主要是为了框架的可扩展性和性能方面的考虑,比如协议层 dubbo 默认使用 dubbo 协议,同时也支持很多其他协议,也支持用户自己实现协议,那么跟 Java 的 SPI 会有什么区别呢,我们也来看个文件

-

bqxWMp

-

是不是看着很想,又有点不一样,在 Java 的 SPI 配置文件里每一行只有一个实现类的全限定名,在 Dubbo的 SPI配置文件中是 key=value 的形式,我们只需要对应的 key 就能加载对应的实现,

-
/**
-     * 返回指定名字的扩展。如果指定名字的扩展不存在,则抛异常 {@link IllegalStateException}.
-     *
-     * @param name
-     * @return
-     */
-	@SuppressWarnings("unchecked")
-	public T getExtension(String name) {
-		if (name == null || name.length() == 0)
-		    throw new IllegalArgumentException("Extension name == null");
-		if ("true".equals(name)) {
-		    return getDefaultExtension();
-		}
-		Holder<Object> holder = cachedInstances.get(name);
-		if (holder == null) {
-		    cachedInstances.putIfAbsent(name, new Holder<Object>());
-		    holder = cachedInstances.get(name);
-		}
-		Object instance = holder.get();
-		if (instance == null) {
-		    synchronized (holder) {
-	            instance = holder.get();
-	            if (instance == null) {
-	                instance = createExtension(name);
-	                holder.set(instance);
-	            }
-	        }
-		}
-		return (T) instance;
-	}
-

这里其实就可以看出来第二个不同点了,就是这个cachedInstances,第一个是不用像 Java 原生的 SPI 那样去遍历加载对应的服务类,只需要通过 key 去寻找,并且寻找的时候会先从缓存的对象里去取,还有就是注意下这里的 DCL(double check lock)

-
@SuppressWarnings("unchecked")
-    private T createExtension(String name) {
-        Class<?> clazz = getExtensionClasses().get(name);
-        if (clazz == null) {
-            throw findException(name);
-        }
-        try {
-            T instance = (T) EXTENSION_INSTANCES.get(clazz);
-            if (instance == null) {
-                EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
-                instance = (T) EXTENSION_INSTANCES.get(clazz);
-            }
-            injectExtension(instance);
-            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
+    聊在东京奥运会闭幕式这天-二
+    /2021/08/19/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9-%E4%BA%8C/
+    前面主要还是说了乒乓球的,因为整体还是乒乓球的比赛赛程比较长,比较激烈,扣人心弦,记得那会在公司没法看视频直播,就偶尔看看奥运会官网的比分,还几场马龙樊振东,陈梦被赢了一局就吓尿了,已经被混双那场留下了阴影,其实后面去看看16 年的比赛什么的,中国队虽然拿了这么多冠军,但是自改成 11 分制以来,其实都没办法那么完全彻底地碾压,而且像张继科,樊振东,陈梦都多少有些慢热,现在看来是马龙比较全面,不过看过了马龙,刘国梁,许昕等的一些过往经历,都是起起伏伏,即使是张怡宁这样的大魔王,也经历过逢王楠不赢的阶段,心态无法调整好。

+

其实最开始是举重项目,侯志慧是女子 49 公斤级的冠军,这场比赛是全场都看,其实看中国队的举重比赛跟跳水有点像,每一轮都需要到最后才能等到中国队,跳水其实每轮都有,举重会按照自己报的试举重量进行排名,重量大的会在后面举,抓举和挺举各三次试举机会,有时候会看着比较焦虑,一直等不来,怕一上来就没试举成功,而且中国队一般试举重量就是很大的,容易一次试举不成功就马上下一次,连着举其实压力会非常大,说实话真的是外行看热闹,每次都是多懂一点点,这次由于实在是比较无聊,所以看的会比较专心点,对于对应的规则知识点也会多了解一点,同时对于举重,没想到我们国家的这些运动员有这么强,最后八块金牌拿了七块,有一块拿到银牌也是有点因为教练的策略问题,这里其实也稍微知道一点,因为报上去的试举重量是谁小谁先举,并且我们国家都是实力非常强的,所以都会报大一些,并且如果这个项目有实力相近的选手,会比竞对多报一公斤,这样子如果前面竞争对手没举成功,我们把握就很大了,最坏的情况即使对手试举成功了,我们还有机会搏一把,比如谌利军这样的,只是说说感想,举重运动员真的是个比较单纯的群体,而且训练是非常痛苦枯燥的,非常容易受伤,像挺举就有点会压迫呼吸通道,看到好几个都是脸憋得通红,甚至直接因为压迫气道而没法完成后面的挺举,像之前 16 年的举重比赛,有个运动员没成功夺冠就非常愧疚地哭着说对不起祖国,没有获得冠军,这是怎么样的一种歉疚,怎么样的一种纯粹的感情呢,相对应地来说,我又要举男足,男篮的例子了,很多人在那嘲笑我这样对男足男篮愤愤不平的人,说可能我这样的人都没交个税(从缴纳个税的数量比例来算有可能),只是这里有两个打脸的事情,我足额缴纳个税,接近 20%的薪资都缴了个税,并且我买的所有东西都缴了增值税,如果让我这样缴纳了个税,缴纳了增值税的有个人的投票权,我一定会投票不让男足男篮使用我缴纳我的税金,用我们的缴纳的税,打出这么烂的表现,想乒乓球混双,拿个亚军都会被喷,那可是世界第二了,而且是就输了那么一场,足球篮球呢,我觉得是一方面成绩差,因为比赛真的有状态跟心态的影响,偶尔有一场失误非常正常,NBA 被黑八的有这么多强队,但是如果像男足男篮,成绩是越来越差,用范志毅的话来说就是脸都不要了,还有就是精气神,要在比赛中打出胜负欲,保持这种争胜心,才有机会再进步,前火箭队主教练鲁迪·汤姆贾诺维奇的话,“永远不要低估冠军的决心”,即使我现在打不过你,我会在下一次,下下次打败你,竞技体育永远要有这种精神,可以接受一时的失败,但是要保持永远争胜的心。

+

第一块金牌是杨倩拿下的,中国队拿奥运会首金也是有政治任务的,而恰恰杨倩这个金牌也有点碰巧是对手最后一枪失误了,当然竞技体育,特别是射击,真的是容不得一点点失误,像前面几届的美国神通埃蒙斯,失之毫厘差之千里,但是这个具体评价就比较少,唯一一点让我比较出戏的就是杨倩真的非常像王刚的徒弟漆二娃,哈哈,微博上也有挺多人觉得像,射击还是个比较可以接受年纪稍大的运动员,需要经验和稳定性,相对来说爆发力体力稍好一点,像庞伟这样的,混合团体10米气手枪金牌,36 岁可能其他项目已经是年龄很大了,不过前面说的举重的吕小军军神也是年纪蛮大了,但是非常强,而且在油管上简直就是个神,相对来说射击是关注比较少,杨倩的也只是看了后面拿到冠军这个结果,有些因为时间或者电视上没放,但是成绩还是不错的,没多少喷点。

+

第二篇先到这,纯主观,轻喷。

+]]>
+ + 生活 + 运动 + + + 生活 + 运动 + 东京奥运会 + 举重 + 射击 + + + + 聊一下 SpringBoot 设置非 web 应用的方法 + /2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/ + 寻找原因

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

+

解决方案

老版本 设置 spring 不带 web 功能

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

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

新版本

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

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

或者

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

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

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

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

+]]>
+ + Java + SpringBoot + + + Java + Spring + SpringBoot + 自动装配 + AutoConfiguration + +
+ + 聊在东京奥运会闭幕式这天 + /2021/08/08/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9/ + 这届奥运会有可能是我除了 08 年之外关注度最高的一届奥运会,原因可能是因为最近也没什么电影综艺啥的比较好看,前面看跑男倒还行,不是说多好,也就图一乐,最开始看向往的生活觉得也挺不错的,后面变成了统一来了就看黄磊做饭,然后夸黄磊做饭好吃,然后无聊的说这种生活多么多么美好,单调无聊,差不多弃了,这里面还包括大华不在了,大华其实个人还是有点呱噪的,但是挺能搞气氛,并且也有才华,彭彭跟子枫人是不讨厌,但是撑不起来,所以也导致了前面说的结果,都变成了黄磊彩虹屁现场,虽然偶尔怀疑他是否做得好吃,但是整体还是承认的,可对于一个这么多季了的综艺来说,这样也有点单调了。

+

还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。

+

这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

+

乒乓球的说的有点多,就分篇说了,第一篇先到这。

+]]>
+ + 生活 + 运动 + + + 生活 + 运动 + 东京奥运会 + 乒乓球 + 跳水 + +
+ + 聊聊 Dubbo 的 SPI + /2020/05/31/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI/ + SPI全称是Service Provider Interface,咋眼看跟api是不是有点相似,api是application interface,这两个其实在某些方面有类似的地方,也有蛮大的区别,比如我们基于 dubbo 的微服务,一般我们可以提供服务,然后非泛化调用的话,我们可以把 api 包提供给应用调用方,他们根据接口签名传对应参数并配置好对应的服务发现如 zk 等就可以调用我们的服务了,然后 spi 会有点类似但是是反过来的关系,相当于是一种规范,比如我约定完成这个功能需要两个有两个接口,一个是连接的,一个是断开的,其实就可以用 jdbc 的驱动举例,比较老套了,然后各个厂家去做具体的实现吧,到时候根据我接口的全限定名的文件来加载实际的实现类,然后运行的时候调用对应实现类的方法就完了

+

3sKdpg

+

看上面的图,java.sql.Driver就是 spi,对应在classpath 的 META-INF/services 目录下的这个文件,里边的内容就是具体的实现类

+

1590735097909

+

简单介绍了 Java的 SPI,再来说说 dubbo 的,dubbo 中为啥要用 SPI 呢,主要是为了框架的可扩展性和性能方面的考虑,比如协议层 dubbo 默认使用 dubbo 协议,同时也支持很多其他协议,也支持用户自己实现协议,那么跟 Java 的 SPI 会有什么区别呢,我们也来看个文件

+

bqxWMp

+

是不是看着很想,又有点不一样,在 Java 的 SPI 配置文件里每一行只有一个实现类的全限定名,在 Dubbo的 SPI配置文件中是 key=value 的形式,我们只需要对应的 key 就能加载对应的实现,

+
/**
+     * 返回指定名字的扩展。如果指定名字的扩展不存在,则抛异常 {@link IllegalStateException}.
+     *
+     * @param name
+     * @return
+     */
+	@SuppressWarnings("unchecked")
+	public T getExtension(String name) {
+		if (name == null || name.length() == 0)
+		    throw new IllegalArgumentException("Extension name == null");
+		if ("true".equals(name)) {
+		    return getDefaultExtension();
+		}
+		Holder<Object> holder = cachedInstances.get(name);
+		if (holder == null) {
+		    cachedInstances.putIfAbsent(name, new Holder<Object>());
+		    holder = cachedInstances.get(name);
+		}
+		Object instance = holder.get();
+		if (instance == null) {
+		    synchronized (holder) {
+	            instance = holder.get();
+	            if (instance == null) {
+	                instance = createExtension(name);
+	                holder.set(instance);
+	            }
+	        }
+		}
+		return (T) instance;
+	}
+

这里其实就可以看出来第二个不同点了,就是这个cachedInstances,第一个是不用像 Java 原生的 SPI 那样去遍历加载对应的服务类,只需要通过 key 去寻找,并且寻找的时候会先从缓存的对象里去取,还有就是注意下这里的 DCL(double check lock)

+
@SuppressWarnings("unchecked")
+    private T createExtension(String name) {
+        Class<?> clazz = getExtensionClasses().get(name);
+        if (clazz == null) {
+            throw findException(name);
+        }
+        try {
+            T instance = (T) EXTENSION_INSTANCES.get(clazz);
+            if (instance == null) {
+                EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
+                instance = (T) EXTENSION_INSTANCES.get(clazz);
+            }
+            injectExtension(instance);
+            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
             if (wrapperClasses != null && wrapperClasses.size() > 0) {
                 for (Class<?> wrapperClass : wrapperClasses) {
                     instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
@@ -11237,88 +11300,236 @@ app.setWebAp
       
   
   
-    聊聊 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/
-    之前看了 dubbo 的一些代码,在学习过程中,主要关注那些比较“高级”的内容,SPI,自适应扩展等,却忘了一些作为一个 rpc 框架最核心需要的部分,比如如何通信,序列化,网络,容错机制等等,因为其实这个最核心的就是远程调用,自适应扩展其实就是让代码可扩展性,可读性,更优雅等,写的搓一点其实也问题不大,但是一个合适的通信协议,序列化方法,如何容错等却是真正保证是一个 rpc 框架最重要的要素。
首先来看这张图
cluster
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
各节点关系:

-
    -
  • 这里的 InvokerProvider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
  • -
  • Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
  • -
  • ClusterDirectory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
  • -
  • Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
  • -
  • LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
  • -
-

集群容错模式

Failover Cluster

失败自动切换,当出现失败,重试其它服务器 1。通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)。

-

重试次数配置如下:

-

<dubbo:service retries=”2” />
这里重点看下 Failover Cluster集群模式的实现

-
public class FailoverCluster implements Cluster {
+    聊聊 Dubbo 的 SPI 续之自适应拓展
+    /2020/06/06/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI-%E7%BB%AD%E4%B9%8B%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/
+    Adaptive

这个应该是 Dubbo SPI 里最玄妙的东西了,一开始没懂,自适应扩展点加载,
dubbo://123.123.123.123:1234/com.nicksxs.demo.service.HelloWorldService?anyhost=true&application=demo&default.loadbalance=random&default.service.filter=LoggerFilter&dubbo=2.5.3&interface=com.nicksxs.demo.service.HelloWorldService&logger=slf4j&methods=method1,method2,method3,method4&pid=4292&retries=0&side=provider&threadpool=fixed&threads=200&timeout=2000&timestamp=1590647155886
那我从比较能理解的角度或者说思路去讲讲我的理解,因为直接将原理如果脱离了使用,对于我这样的理解能力比较差的可能会比较吃力,从使用场景开始讲可能会比较舒服了,这里可以看到参数里有蛮多的,举个例子,比如这个 threadpool = fixed,说明线程池使用的是 fixed 对应的实现,也就是下图的这个

这样子似乎没啥问题了,反正就是用dubbo 的 spi 加载嘛,好像没啥问题,其实问题还是存在的,或者说不太优雅,比如要先判断我这个 fixed 对应的实现类是哪个,这里可能就有个 if-else 判断了,但是 dubbo 的开发人员似乎不太想这么做这个事情,

+

譬如我们在引用一个服务时,在ReferenceConfig 中的

+
private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
- public final static String NAME = "failover"; +

就获取了自适应拓展,

+
public T getAdaptiveExtension() {
+        Object instance = cachedAdaptiveInstance.get();
+        if (instance == null) {
+            if (createAdaptiveInstanceError == null) {
+                synchronized (cachedAdaptiveInstance) {
+                    instance = cachedAdaptiveInstance.get();
+                    if (instance == null) {
+                        try {
+                            instance = createAdaptiveExtension();
+                            cachedAdaptiveInstance.set(instance);
+                        } catch (Throwable t) {
+                            createAdaptiveInstanceError = t;
+                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
+                        }
+                    }
+                }
+            } else {
+                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
+            }
+        }
 
-    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
-        return new FailoverClusterInvoker<T>(directory);
+        return (T) instance;
+    }
+ +

这里也使用了 DCL,来锁cachedAdaptiveInstance,当缓存中没有时就去创建自适应拓展

+
private T createAdaptiveExtension() {
+        try {
+          // 获取自适应拓展类然后实例化
+            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
+        } catch (Exception e) {
+            throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
+        }
     }
 
-}
-

这个代码就非常简单,重点需要看FailoverClusterInvoker里的代码,FailoverClusterInvoker继承了AbstractClusterInvoker类,其中invoke 方法是在抽象类里实现的

-
@Override
-public Result invoke(final Invocation invocation) throws RpcException {
-    checkWhetherDestroyed();
-    // binding attachments into invocation.
-    // 绑定 attachments 到 invocation 中.
-    Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
-    if (contextAttachments != null && contextAttachments.size() != 0) {
-        ((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
-    }
-    // 列举 Invoker
-    List<Invoker<T>> invokers = list(invocation);
-    // 加载 LoadBalance 负载均衡器
-    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
-    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
-    // 调用 实际的 doInvoke 进行后续操作
-    return doInvoke(invocation, invokers, loadbalance);
-}
-// 这是个抽象方法,实际是由子类实现的
- protected abstract Result doInvoke(Invocation invocation, List<Invoker<T>> invokers,
-                                       LoadBalance loadbalance) throws RpcException;
-

然后重点就是FailoverClusterInvoker中的doInvoke方法了,其实它里面也就这么一个方法

-
@Override
-    @SuppressWarnings({"unchecked", "rawtypes"})
-    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
-        List<Invoker<T>> copyInvokers = invokers;
-        checkInvokers(copyInvokers, invocation);
-        String methodName = RpcUtils.getMethodName(invocation);
-        // 获取重试次数,这里默认是 2 次,还有可以注意下后面的+1
-        int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
-        if (len <= 0) {
-            len = 1;
-        }
-        // retry loop.
-        RpcException le = null; // last exception.
-        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
-        Set<String> providers = new HashSet<String>(len);
-        // 循环调用,失败重试
-        for (int i = 0; i < len; i++) {
-            //Reselect before retry to avoid a change of candidate `invokers`.
-            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
-            if (i > 0) {
-                checkWhetherDestroyed();
-                // 在进行重试前重新列举 Invoker,这样做的好处是,如果某个服务挂了,
-                // 通过调用 list 可得到最新可用的 Invoker 列表
-                copyInvokers = list(invocation);
-                // check again
-                // 对 copyinvokers 进行判空检查
-                checkInvokers(copyInvokers, invocation);
-            }
-            // 通过负载均衡来选择 invoker
-            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
-            // 将其添加到 invoker 到 invoked 列表中
-            invoked.add(invoker);
-            // 设置上下文
-            RpcContext.getContext().setInvokers((List) invoked);
-            try {
-                // 正式调用
-                Result result = invoker.invoke(invocation);
-                if (le != null && logger.isWarnEnabled()) {
+private Class<?> getAdaptiveExtensionClass() {
+  			// 这里会获取拓展类,如果没有自适应的拓展类,那么就需要调用createAdaptiveExtensionClass
+        getExtensionClasses();
+        if (cachedAdaptiveClass != null) {
+            return cachedAdaptiveClass;
+        }
+        return cachedAdaptiveClass = createAdaptiveExtensionClass();
+    }
+private Class<?> createAdaptiveExtensionClass() {
+  			// 这里去生成了自适应拓展的代码,具体生成逻辑比较复杂先不展开讲
+        String code = createAdaptiveExtensionClassCode();
+        ClassLoader classLoader = findClassLoader();
+        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
+        return compiler.compile(code, classLoader);
+    }
+ +

生成的代码像这样

+
package com.alibaba.dubbo.rpc;
+
+import com.alibaba.dubbo.common.extension.ExtensionLoader;
+
+
+public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
+    public void destroy() {
+        throw new UnsupportedOperationException(
+            "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
+    }
+
+    public int getDefaultPort() {
+        throw new UnsupportedOperationException(
+            "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
+    }
+
+    public com.alibaba.dubbo.rpc.Exporter export(
+        com.alibaba.dubbo.rpc.Invoker arg0)
+        throws com.alibaba.dubbo.rpc.RpcException {
+        if (arg0 == null) {
+            throw new IllegalArgumentException(
+                "com.alibaba.dubbo.rpc.Invoker argument == null");
+        }
+
+        if (arg0.getUrl() == null) {
+            throw new IllegalArgumentException(
+                "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
+        }
+
+        com.alibaba.dubbo.common.URL url = arg0.getUrl();
+        String extName = ((url.getProtocol() == null) ? "dubbo"
+                                                      : url.getProtocol());
+
+        if (extName == null) {
+            throw new IllegalStateException(
+                "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
+                url.toString() + ") use keys([protocol])");
+        }
+
+        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
+                                                                                                   .getExtension(extName);
+
+        return extension.export(arg0);
+    }
+
+    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,
+        com.alibaba.dubbo.common.URL arg1)
+        throws com.alibaba.dubbo.rpc.RpcException {
+        if (arg1 == null) {
+            throw new IllegalArgumentException("url == null");
+        }
+
+        com.alibaba.dubbo.common.URL url = arg1;
+      // 其实前面所说的逻辑就在这里呈现了
+        String extName = ((url.getProtocol() == null) ? "dubbo"
+                                                      : url.getProtocol());
+
+        if (extName == null) {
+            throw new IllegalStateException(
+                "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
+                url.toString() + ") use keys([protocol])");
+        }
+				// 在这就是实际的通过dubbo 的 spi 去加载实际对应的扩展
+        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
+                                                                                                   .getExtension(extName);
+
+        return extension.refer(arg0, arg1);
+    }
+}
+
+]]>
+ + Java + Dubbo + RPC + SPI + Dubbo + SPI + Adaptive + + + Java + Dubbo + RPC + SPI + Adaptive + 自适应拓展 + + + + 聊聊 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/ + 之前看了 dubbo 的一些代码,在学习过程中,主要关注那些比较“高级”的内容,SPI,自适应扩展等,却忘了一些作为一个 rpc 框架最核心需要的部分,比如如何通信,序列化,网络,容错机制等等,因为其实这个最核心的就是远程调用,自适应扩展其实就是让代码可扩展性,可读性,更优雅等,写的搓一点其实也问题不大,但是一个合适的通信协议,序列化方法,如何容错等却是真正保证是一个 rpc 框架最重要的要素。
首先来看这张图
cluster
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
各节点关系:

+
    +
  • 这里的 InvokerProvider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
  • +
  • Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
  • +
  • ClusterDirectory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
  • +
  • Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
  • +
  • LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
  • +
+

集群容错模式

Failover Cluster

失败自动切换,当出现失败,重试其它服务器 1。通常用于读操作,但重试会带来更长延迟。可通过 retries=”2” 来设置重试次数(不含第一次)。

+

重试次数配置如下:

+

<dubbo:service retries=”2” />
这里重点看下 Failover Cluster集群模式的实现

+
public class FailoverCluster implements Cluster {
+
+    public final static String NAME = "failover";
+
+    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
+        return new FailoverClusterInvoker<T>(directory);
+    }
+
+}
+

这个代码就非常简单,重点需要看FailoverClusterInvoker里的代码,FailoverClusterInvoker继承了AbstractClusterInvoker类,其中invoke 方法是在抽象类里实现的

+
@Override
+public Result invoke(final Invocation invocation) throws RpcException {
+    checkWhetherDestroyed();
+    // binding attachments into invocation.
+    // 绑定 attachments 到 invocation 中.
+    Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
+    if (contextAttachments != null && contextAttachments.size() != 0) {
+        ((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
+    }
+    // 列举 Invoker
+    List<Invoker<T>> invokers = list(invocation);
+    // 加载 LoadBalance 负载均衡器
+    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
+    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
+    // 调用 实际的 doInvoke 进行后续操作
+    return doInvoke(invocation, invokers, loadbalance);
+}
+// 这是个抽象方法,实际是由子类实现的
+ protected abstract Result doInvoke(Invocation invocation, List<Invoker<T>> invokers,
+                                       LoadBalance loadbalance) throws RpcException;
+

然后重点就是FailoverClusterInvoker中的doInvoke方法了,其实它里面也就这么一个方法

+
@Override
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
+        List<Invoker<T>> copyInvokers = invokers;
+        checkInvokers(copyInvokers, invocation);
+        String methodName = RpcUtils.getMethodName(invocation);
+        // 获取重试次数,这里默认是 2 次,还有可以注意下后面的+1
+        int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
+        if (len <= 0) {
+            len = 1;
+        }
+        // retry loop.
+        RpcException le = null; // last exception.
+        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
+        Set<String> providers = new HashSet<String>(len);
+        // 循环调用,失败重试
+        for (int i = 0; i < len; i++) {
+            //Reselect before retry to avoid a change of candidate `invokers`.
+            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
+            if (i > 0) {
+                checkWhetherDestroyed();
+                // 在进行重试前重新列举 Invoker,这样做的好处是,如果某个服务挂了,
+                // 通过调用 list 可得到最新可用的 Invoker 列表
+                copyInvokers = list(invocation);
+                // check again
+                // 对 copyinvokers 进行判空检查
+                checkInvokers(copyInvokers, invocation);
+            }
+            // 通过负载均衡来选择 invoker
+            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
+            // 将其添加到 invoker 到 invoked 列表中
+            invoked.add(invoker);
+            // 设置上下文
+            RpcContext.getContext().setInvokers((List) invoked);
+            try {
+                // 正式调用
+                Result result = invoker.invoke(invocation);
+                if (le != null && logger.isWarnEnabled()) {
                     logger.warn("Although retry the method " + methodName
                             + " in the service " + getInterface().getName()
                             + " was successful by the provider " + invoker.getUrl().getAddress()
@@ -11372,194 +11583,19 @@ 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 中绕不开的 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 {
 
-

我们可以用 java jol库来查看对象头,通过一段简单的代码来看下

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-		}
-}
+ public static void main(String[] args) { + SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); + synchronizedDemo.lockMethod(); + } -

Untitled

-

然后可以看到打印输出,当然这里因为对齐方式,我们看到的其实顺序是反过来的,按最后三位去看,我们这是 001,好像偏向锁都没开,这里使用的是 jdk1.8,默认开始偏向锁的,其实这里有涉及到了一个配置,jdk1.8 中偏向锁会延迟 4 秒开启,可以通过添加启动参数 -XX:+PrintFlagsFinal,看到

-

偏向锁延迟

-

因为在初始化的时候防止线程竞争有大量的偏向锁撤销升级,所以会延迟 4s 开启

-

我们再来延迟 5s 看看

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-				TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-		}
-} 
- -

https://img.nicksxs.com/uPic/2LBKpX.jpg

-

可以看到偏向锁设置已经开启了,我们来是一下加个偏向锁

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-        synchronized (l) {
-            System.out.println("2\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-		}
-}
- -

看下运行结果

-

https://img.nicksxs.com/uPic/V2l78m.png

-

可以看到是加上了 101 = 5 也就是偏向锁,后面是线程 id

-

当我再使用一个线程来竞争这个锁的时候

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-				Thread thread1 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread1 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                    try {
-                        TimeUnit.SECONDS.sleep(5L);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        };
-				thread1.start();
-		}
-}
- -

https://img.nicksxs.com/uPic/bRMvlR.png

-

可以看到变成了轻量级锁,在线程没有争抢,只是进行了切换,就会使用轻量级锁,当两个线程在竞争了,就又会升级成重量级锁

-
public class ObjectHeaderDemo {
-    public static void main(String[] args) throws InterruptedException {
-        TimeUnit.SECONDS.sleep(5);
-        L l = new L();
-        System.out.println(ClassLayout.parseInstance(l).toPrintable());
-        synchronized (l) {
-            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
-        }
-        Thread thread1 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread1 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                    try {
-                        TimeUnit.SECONDS.sleep(5L);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        };
-        Thread thread2 = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    TimeUnit.SECONDS.sleep(5L);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                synchronized (l) {
-                    System.out.println("thread2 获取锁成功");
-                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
-                }
-            }
-        };
-        thread1.start();
-        thread2.start();
-    }
-}
-
-class L {
-    private boolean myboolean = true;
-}
- -

https://img.nicksxs.com/uPic/LMzMtR.png

-

可以看到变成了重量级锁。

-]]> - - Java - - - Java - Synchronized - 偏向锁 - 轻量级锁 - 重量级锁 - 自旋 - - - - 聊聊 Java 中绕不开的 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 synchronized void lockMethod() {
+        System.out.println("here i'm locked");
+    }
 
     public void lockSynchronizedDemo() {
         synchronized (this) {
@@ -11716,41 +11752,38 @@ public Result invoke(final Invocation invocation) throws RpcException {
       
   
   
-    聊聊 Java 的 equals 和 hashCode 方法
-    /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/
-    Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
简单看下代码
java.lang.Object#equals

-
public boolean equals(Object obj) {
-        return (this == obj);
-    }
-

对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了
java.lang.String#equals

-
public boolean equals(Object anObject) {
-        if (this == anObject) {
-            return true;
-        }
-        if (anObject instanceof String) {
-            String anotherString = (String)anObject;
-            int n = value.length;
-            if (n == anotherString.value.length) {
-                char v1[] = value;
-                char v2[] = anotherString.value;
-                int i = 0;
-                while (n-- != 0) {
-                    if (v1[i] != v2[i])
-                        return false;
-                    i++;
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-

然后呢就是为啥一些书或者 effective java 中写了 equalshashCode 要一起重写,这里涉及到当对象作为 HashMapkey 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 objecthashCodenative 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.

+ 一个 nginx 的简单记忆点 + /2022/08/21/%E4%B8%80%E4%B8%AA-nginx-%E7%9A%84%E7%AE%80%E5%8D%95%E8%AE%B0%E5%BF%86%E7%82%B9/ + 上周在处理一个 nginx 配置的时候,发现了一个之前不理解的小点,说一个场景,就是我们一般的处理方式就是一个 ip 端口只能配置一个域名的服务,比如 https://nicksxs.me 对应配置到 127.0.0.1:443,如果我想要把 https://nicksxs.com 也解析到这个服务器,并转发到不同的下游,这里就需要借助所谓的 SNI 的功能

+

Server Name Indication

A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. SNI is currently supported by most modern browsers, though may not be used by some old or special clients.
来源
机翻一下:在单个 IP 地址上运行多个 HTTPS 服务器的更通用的解决方案是 TLS 服务器名称指示扩展(SNI,RFC 6066),它允许浏览器在 SSL 握手期间传递请求的服务器名称,因此,服务器将知道哪个 它应该用于连接的证书。 目前大多数现代浏览器都支持 SNI,但某些旧的或特殊的客户端可能不使用 SNI。

+

首先我们需要确认 sni 已被支持

在实际的配置中就可以这样

+
stream {
+  map $ssl_preread_server_name $stream_map {
+    nicksxs.me nme;
+    nicksxs.com ncom;
+  }
+
+  upstream nme {
+    server 127.0.0.1:8000;
+  }
+  upstream ncom {
+    server 127.0.0.1:8001;
+  }
+
+  server {
+    listen 443 reuseport;
+    proxy_pass $stream_map;
+    ssl_preread on;
+  }
+}
+

类似这样,但是这个理解是非常肤浅和不完善的,只是简单记忆下,后续再进行补充完整

+

还有一点就是我们在配置的时候经常配置就是 server_name,但是会看到直接在使用 ssl_server_name,
其实在listen 标识了 ssl, 对应的 ssl_server_name 就等于 server_name,不需要额外处理了。

]]>
- java + nginx - java + nginx
@@ -12626,54 +12659,6 @@ JNI global references: -H :Threads-mode operation - Instructs top to display individual threads. Without this command-line option a summation of all threads in each process is shown. Later - this can be changed with the `H' interactive command.
-

这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样

执行后是这样

第二个就是排序了

-
SORTING of task window
-
-          For  compatibility,  this top supports most of the former top sort keys.  Since this is primarily a service to former top users, these commands
-          do not appear on any help screen.
-                command   sorted-field                  supported
-                A         start time (non-display)      No
-                M         %MEM                          Yes
-                N         PID                           Yes
-                P         %CPU                          Yes
-                T         TIME+                         Yes
-
-          Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
-          mand.  That will help ensure that the actual sort environment matches your intent.
-
-          The following interactive commands will only be honored when the current sort field is visible.  The sort field might not be visible because:
-                1) there is insufficient Screen Width
-                2) the `f' interactive command turned it Off
-
-             <  :Move-Sort-Field-Left
-                 Moves the sort column to the left unless the current sort field is the first field being displayed.
-
-             >  :Move-Sort-Field-Right
-                 Moves the sort column to the right unless the current sort field is the last field being displayed.
-

查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<>直接左右移动排序列

-]]>
- - Linux - 命令 - 小技巧 - top - top - 排序 - - - linux - 排序 - 小技巧 - top - -
聊聊 RocketMQ 的 Broker 源码 /2020/07/19/%E8%81%8A%E8%81%8A-RocketMQ-%E7%9A%84-Broker-%E6%BA%90%E7%A0%81/ @@ -13200,25 +13185,73 @@ result = result - 聊聊 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 {
+    聊聊 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 + + + + 聊聊 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<>();
 
@@ -13305,121 +13338,122 @@ result = result 
   
   
-    聊一下 RocketMQ 的消息存储之 MMAP
-    /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/
-    这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

-
read(file, tmp_buf, len);
-write(socket, tmp_buf, len);
+ 聊聊 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
 
-

vms95Z

-

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

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

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

-

mmap

mmap基于 OS 的 mmap 的内存映射技术,通过MMU 映射文件,将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作,使随机读写文件和读写内存相似的速度。

-
-

mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

-
-
tmp_buf = mmap(file, len);
-write(socket, tmp_buf, len);
-

I68mFx

-
-

第一步:mmap系统调用使得文件内容被DMA引擎复制到内核缓冲区。然后该缓冲区与用户进程共享,在内核和用户内存空间之间不进行任何拷贝。

-

第二步:写系统调用使得内核将数据从原来的内核缓冲区复制到与套接字相关的内核缓冲区。

-

第三步:第三次拷贝发生在DMA引擎将数据从内核套接字缓冲区传递给协议引擎时。

-

通过使用mmap而不是read,我们将内核需要拷贝的数据量减少了一半。当大量的数据被传输时,这将有很好的效果。然而,这种改进并不是没有代价的;在使用mmap+write方法时,有一些隐藏的陷阱。例如当你对一个文件进行内存映射,然后在另一个进程截断同一文件时调用写。你的写系统调用将被总线错误信号SIGBUS打断,因为你执行了一个错误的内存访问。该信号的默认行为是杀死进程并dumpcore–这对网络服务器来说不是最理想的操作。

-

有两种方法可以解决这个问题。

-

第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用返回。通过这样做,写系统调用会返回它在被打断之前所写的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,一个治标不治本的解决方案。因为SIGBUS预示着进程出了严重的问题,所以不鼓励使用这种解决方案。

-

第二个解决方案涉及内核的文件租赁(在Windows中称为 “机会锁”)。这是解决这个问题的正确方法。通过在文件描述符上使用租赁,你与内核在一个特定的文件上达成租约。然后你可以向内核请求一个读/写租约。当另一个进程试图截断你正在传输的文件时,内核会向你发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉你内核即将终止你对该文件的写或读租约。在你的程序访问一个无效的地址和被SIGBUS信号杀死之前,你的写调用会被打断了。写入调用的返回值是中断前写入的字节数,errno将被设置为成功。下面是一些示例代码,显示了如何从内核中获得租约。

-
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
-    perror("kernel lease set signal");
-    return -1;
-}
-/* l_type can be F_RDLCK F_WRLCK */
-if(fcntl(fd, F_SETLEASE, l_type)){
-    perror("kernel lease set type");
-    return -1;
-}
+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 条数据再进行合并排序就可以获得最终正确的结果。
这个就是传说中的二次查询法。

]]>
- MQ - RocketMQ - 消息队列 + Java - MQ - 消息队列 - RocketMQ + Java + Sharding-Jdbc
- 聊聊 dubbo 的线程池 - /2021/04/04/%E8%81%8A%E8%81%8A-dubbo-%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 之前没注意到这一块,只是比较模糊的印象 dubbo 自己基于 ThreadPoolExecutor 定义了几个线程池,但是没具体看过,主要是觉得就是为了避免使用 jdk 自带的那几个(java.util.concurrent.Executors),防止出现那些问题
看下代码目录主要是这几个

-
    -
  • FixedThreadPool:创建一个复用固定个数线程的线程池。
    简单看下代码
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int threads = url.getParameter("threads", 200);
    -        int queues = url.getParameter("queues", 0);
    -        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -    }
    -可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
  • -
  • LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int cores = url.getParameter("corethreads", 0);
    -        int threads = url.getParameter("threads", 200);
    -        int queues = url.getParameter("queues", 0);
    -        return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -    }
    -这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
  • -
  • EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int cores = url.getParameter("corethreads", 0);
    -        int threads = url.getParameter("threads", 2147483647);
    -        int queues = url.getParameter("queues", 0);
    -        int alive = url.getParameter("alive", 60000);
    -        TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
    -        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -        taskQueue.setExecutor(executor);
    -        return executor;
    -    }
    -这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
  • -
  • CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
    public Executor getExecutor(URL url) {
    -        String name = url.getParameter("threadname", "Dubbo");
    -        int cores = url.getParameter("corethreads", 0);
    -        int threads = url.getParameter("threads", 2147483647);
    -        int queues = url.getParameter("queues", 0);
    -        int alive = url.getParameter("alive", 60000);
    -        return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    -    }
    -这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
    只是非常简略的介绍下,有兴趣可以自行阅读代码。
  • -
+ 聊聊 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 - Dubbo - 线程池 - Dubbo - 线程池 - ThreadPool Java - Dubbo - ThreadPool - 线程池 - FixedThreadPool - LimitedThreadPool - EagerThreadPool - CachedThreadPool + Sharding-Jdbc
@@ -13463,38 +13497,83 @@ void ReadView::prepare(trx_id_t id) { - 聊聊 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
但是对于以下几类

+ 聊聊 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),防止出现那些问题
看下代码目录主要是这几个

    -
  • 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 ?;
  • +
  • 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,保活时间是一分钟
    只是非常简略的介绍下,有兴趣可以自行阅读代码。
-

除了第一条是 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 + Java + Dubbo - 线程池 + Dubbo + 线程池 + ThreadPool - mysql - 数据结构 - 源码 - mvcc - read view - gap lock - next-key lock - 幻读 + Java + Dubbo + ThreadPool + 线程池 + FixedThreadPool + LimitedThreadPool + EagerThreadPool + CachedThreadPool + +
+ + 分享记录一下一个 git 操作方法 + /2022/02/06/%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B%E4%B8%80%E4%B8%AA-git-%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95/ + 前阵子一个同事因为发现某个分支上的代码好像有缺失导致无法正常运行,然后就对比了下把缺失的代码从另一个分支上拷了过来,可能有所欠考虑,不过主要是说下操作过程和最后的处理方法,这位同学的操作是改一些代码commit 一下,这样的 commit 了大概五六次,并且已经 push 到了远端,然后就在想要怎么去处理,在本地可以 reset,已经到远端了,一个很不优雅的操作就是本地 reset 了用 force push,这个当然是不可取的,然后就是 revert 了,但是又已经 commit 了好几次了,网上看了下,好像处理方法还挺成熟的,git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。可以理解为就是提交一个反向的操作,这里其实我们可以用range revert来进行 git revert, 用法就是

+
git revert OLDER_COMMIT^..NEWER_COMMIT
+

这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明

然后就可以用 -n 参数,表示不自动提交

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

-]]>
- - Java - - - Java - Sharding-Jdbc - -
聊聊我的远程工作体验 /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/ @@ -14507,6 +14496,20 @@ t2 骑车 + + 聊聊最近平淡的生活之《花束般的恋爱》观后感 + /2021/12/31/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E3%80%8A%E8%8A%B1%E6%9D%9F%E8%88%AC%E7%9A%84%E6%81%8B%E7%88%B1%E3%80%8B%E8%A7%82%E5%90%8E%E6%84%9F/ + 周末在领导的提议下看了豆瓣的年度榜单,本来感觉没啥心情看的,看到主演有有村架纯就觉得可以看一下,颜值即正义嘛,男主小麦跟女主小娟(后面简称小麦跟小娟)是两个在一次非常偶然的没赶上地铁末班车事件中相识,这里得说下日本这种通宵营业的店好像挺不错的,看着也挺正常,国内估计只有酒吧之类的可以。晚上去的地方是有点暗暗的,好像也有点类似酒吧,旁边有类似于 dj 那种,然后同桌的还有除了男女主的另外一对男女,也是因为没赶上地铁末班车的,但也是陌生人,然后小麦突然看到了有个非常有名的电影人,小娟竟然也认识,然后旁边那对完全不认识,还在那吹自己看过很多电影,比如《肖申克的救赎》,于是男女主都特别鄙夷地看着他们,然后他们又去了另一个有点像泡澡的地方席地而坐,他们发现了自己的鞋子都是一样的,然后在女的去上厕所的时候,小麦暗恋的学姐也来了,然后小麦就去跟学姐他们一起坐了,小娟回来后有点不开心就说去朋友家睡,幸好小麦看出来了(他竟然看出来了,本来以为应该是没填过恋爱很木讷的),就追出去,然后就去了小麦家,到了家小娟发现小麦家的书柜上的书简直就跟她自己家的一模一样,小麦还给小娟吹了头发,一起吃烤饭团,看电影,第二天送小娟上了公交,还约好了一起看木乃伊展,然而并没有交换联系方式,但是他们还是约上了一起看了木乃伊展,在餐馆就出现了片头那一幕的来源,因为餐馆他们想一起听歌,就用有线耳机一人一个耳朵听,但是旁边就有个大叔说“你们是不是不爱音乐,左右耳朵是不一样的,只有一起听才是真正的音乐”这样的话,然后的剧情有点跳,因为是指他们一直在这家餐馆吃饭,中间有他们一起出去玩情节穿插着,也是在这他们确立了关系,可以说主体就是体现了他们非常的合拍和默契,就像一些影评说的,这部电影是说如何跟百分百合拍的人分手,然后就是正常的恋爱开始啪啪啪,一直腻在床上,也没去就业说明会,后面也有讲了一点小麦带着小娟去认识他的朋友,也把小娟介绍给了他们认识,这里算是个小伏笔,后面他们分手也有这里的人的一些关系,接下去的剧情说实话我是不太喜欢的,如果一部八分的电影只是说恋爱被现实打败的话,我觉得在我这是不合格的,但是事实也是这样,小麦其实是有家里的资助,所以后面还是按自己的喜好给一些机构画点插画,小娟则要出去工作,因为小娟家庭观念也是要让她出去有正经工作,用脚指头想也能知道肯定不顺利,然后就是暂时在一家蛋糕店工作,小麦就每天去接小娟,日子过得甜甜蜜蜜,后面小娟在自己的努力下考了个什么资格证,去了一家医院还是什么做前台行政,这中间当然就有父母来见面吃饭了,他们在开始恋爱不久就同居合租了,然后小娟父母就是来说要让她有个正经工作,对男的说的话就是人生就是责任这类的话,而小麦爸爸算是个导火索,因为小麦家里是做烟花生意的,他爸让他就做烟花生意,因为要回老家,并且小麦也不想做,所以就拒绝了,然后他爸就说不给他每个月五万的资助,这也导致了小麦需要去找工作,这个过程也是很辛苦,本来想要年前找好工作,然后事与愿违,后面有一次小娟被同事吐槽怎么从来不去团建,于是她就去了(我以为会拒绝),正在团建的时候小麦给她电话,说找到工作了,是一个创业物流公司这种,这里剧情就是我觉得比较俗套的,小麦各种被虐,累成狗,但是就像小娟爸爸说的话,人生就是责任,所以一直在坚持,但是这样也导致了跟小娟的交流也越来越少,他们原来最爱的漫画,爱玩的游戏,也只剩小娟一个人看,一个人玩,而正是这个时候,小娟说她辞掉了工作,去做一个不是太靠谱的漫画改造的密室逃脱,然后这里其实有一点后面争议很大的,就是这个工作其实是前面小麦介绍给小娟的那些朋友中一个的女朋友介绍的,而在有个剧情就是小娟有一次在这个密室逃脱的老板怀里醒过来,是在 KTV 那样的场景里,这就有很多人觉得小娟是不是出轨了,我觉得其实不那么重要,因为这个离职的事情已经让一切矛盾都摆在眼前,小麦其实是接受这种需要承担责任的生活,也想着要跟小娟结婚,但是小娟似乎还是想要过着那样理想的生活,做自己想做的事情,看自己爱看的漫画,也要小麦能像以前那样一直那么默契的有着相同的爱好,这里的触发点其实还有个是那个小麦的朋友(也就是他女朋友介绍小娟那个不靠谱工作的)的葬礼上,小麦在参加完葬礼后有挺多想倾诉的,而小娟只是想睡了,这个让小麦第二天起来都不想理小娟,只是这里我不太理解,难道这点闹情绪都不能接受吗,所谓的合拍也只是毫无限制的情况下的合拍吧,真正的生活怎么可能如此理想呢,即使没有物质生活的压力,也会有其他的各种压力和限制,在这之后其实小麦想说的是小娟是不是没有想跟自己继续在一起的想法了,而小娟觉得都不说话了,还怎么结婚呢,后面其实导演搞了个小 trick,突然放了异常婚礼,但是不是男女主的,我并不觉得这个桥段很好,在婚礼里男女主都觉得自己想要跟对方说分手了,但是当他们去了最开始一直去的餐馆的时候,一个算是一个现实映照的就是他们一直坐的位子被占了,可能也是导演想通过这个来说明他们已经回不去了,在餐馆交谈的时候,小麦其实是说他们结婚吧,并没有想前面婚礼上预设地要分手,但是小娟放弃了,不想结婚,因为不想过那样的生活了,而小麦觉得可能生活就是那样,不可能一直保持刚恋爱时候的那种感觉,生活就是责任,人生就意味着责任。

+

我的一些观点也在前面说了,恋爱到婚姻,即使物质没问题,经济没问题,也会有各种各样的问题,需要一起去解决,因为结婚就意味着需要相互扶持,而不是各取所需,可能我的要求比较高,后面男女主在分手后还一起住了一段时间,我原来还在想会不会通过这个方式让他们继续去磨合同步,只是我失望了,最后给个打分可能是 5 到 6 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。

+]]>
+ + 生活 + + + 生活 + 看剧 + +
聊聊最近平淡的生活之看《神探狄仁杰》 /2021/12/19/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E3%80%8A%E7%A5%9E%E6%8E%A2%E7%8B%84%E4%BB%81%E6%9D%B0%E3%80%8B/ @@ -14557,211 +14560,19 @@ t2 - 聊一下 SpringBoot 中动态切换数据源的方法 - /2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ - 其实这个表示有点不太对,应该是 Druid 动态切换数据源的方法,只是应用在了 springboot 框架中,准备代码准备了半天,之前在一次数据库迁移中使用了,发现 Druid 还是很强大的,用来做动态数据源切换很方便。

-

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

-

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

-
public class DynamicDataSource extends AbstractRoutingDataSource {
-
-    @Override
-    protected Object determineCurrentLookupKey() {
-        if (DatabaseContextHolder.getDatabaseType() != null) {
-            return DatabaseContextHolder.getDatabaseType().getName();
-        }
-        return DatabaseType.MASTER1.getName();
-    }
-}
- -

而如何使用这个 lookupKey 呢,就涉及到我们的 DataSource 配置了,原来就是我们可以直接通过spring 的 jdbc 配置数据源,像这样

-

-

现在我们要使用 Druid 作为数据源了,然后配置 DynamicDataSource 的参数,通过 key 来选择对应的 DataSource,也就是下面配的 master1 和 master2

-
<bean id="master1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
-          destroy-method="close"
-          p:driverClassName="com.mysql.cj.jdbc.Driver"
-          p:url="${master1.demo.datasource.url}"
-          p:username="${master1.demo.datasource.username}"
-          p:password="${master1.demo.datasource.password}"
-          p:initialSize="5"
-          p:minIdle="1"
-          p:maxActive="10"
-          p:maxWait="60000"
-          p:timeBetweenEvictionRunsMillis="60000"
-          p:minEvictableIdleTimeMillis="300000"
-          p:validationQuery="SELECT 'x'"
-          p:testWhileIdle="true"
-          p:testOnBorrow="false"
-          p:testOnReturn="false"
-          p:poolPreparedStatements="false"
-          p:maxPoolPreparedStatementPerConnectionSize="20"
-          p:connectionProperties="config.decrypt=true"
-          p:filters="stat,config"/>
-
-    <bean id="master2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
-          destroy-method="close"
-          p:driverClassName="com.mysql.cj.jdbc.Driver"
-          p:url="${master2.demo.datasource.url}"
-          p:username="${master2.demo.datasource.username}"
-          p:password="${master2.demo.datasource.password}"
-          p:initialSize="5"
-          p:minIdle="1"
-          p:maxActive="10"
-          p:maxWait="60000"
-          p:timeBetweenEvictionRunsMillis="60000"
-          p:minEvictableIdleTimeMillis="300000"
-          p:validationQuery="SELECT 'x'"
-          p:testWhileIdle="true"
-          p:testOnBorrow="false"
-          p:testOnReturn="false"
-          p:poolPreparedStatements="false"
-          p:maxPoolPreparedStatementPerConnectionSize="20"
-          p:connectionProperties="config.decrypt=true"
-          p:filters="stat,config"/>
-
-    <bean id="dataSource" class="com.nicksxs.springdemo.config.DynamicDataSource">
-        <property name="targetDataSources">
-            <map key-type="java.lang.String">
-                <!-- master -->
-                <entry key="master1" value-ref="master1"/>
-                <!-- slave -->
-                <entry key="master2" value-ref="master2"/>
-            </map>
-        </property>
-        <property name="defaultTargetDataSource" ref="master1"/>
-    </bean>
- -

现在就要回到头上,介绍下这个DatabaseContextHolder,这里使用了 ThreadLocal 存放这个 DatabaseType,为啥要用这个是因为前面说的我们想要让接口层面去配置不同的数据源,要把持相互隔离不受影响,就使用了 ThreadLocal,关于它也可以看我前面写的一篇文章聊聊传说中的 ThreadLocal,而 DatabaseType 就是个简单的枚举

-
public class DatabaseContextHolder {
-    public static final ThreadLocal<DatabaseType> databaseTypeThreadLocal = new ThreadLocal<>();
-
-    public static DatabaseType getDatabaseType() {
-        return databaseTypeThreadLocal.get();
-    }
-
-    public static void putDatabaseType(DatabaseType databaseType) {
-        databaseTypeThreadLocal.set(databaseType);
-    }
-
-    public static void clearDatabaseType() {
-        databaseTypeThreadLocal.remove();
-    }
-}
-public enum DatabaseType {
-    MASTER1("master1", "1"),
-    MASTER2("master2", "2");
-
-    private final String name;
-    private final String value;
-
-    DatabaseType(String name, String value) {
-        this.name = name;
-        this.value = value;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public String getValue() {
-        return value;
-    }
-
-    public static DatabaseType getDatabaseType(String name) {
-        if (MASTER2.name.equals(name)) {
-            return MASTER2;
-        }
-        return MASTER1;
-    }
-}
- -

这边可以看到就是通过动态地通过putDatabaseType设置lookupKey来进行数据源切换,要通过接口注解配置来进行设置的话,我们就需要一个注解

-
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface DataSource {
-    String value();
-}
- -

这个注解可以配置在我的接口方法上,比如这样

-
public interface StudentService {
-
-    @DataSource("master1")
-    public Student queryOne();
-
-    @DataSource("master2")
-    public Student queryAnother();
-
-}
- -

通过切面来进行数据源的设置

-
@Aspect
-@Component
-@Order(-1)
-public class DataSourceAspect {
-
-    @Pointcut("execution(* com.nicksxs.springdemo.service..*.*(..))")
-    public void pointCut() {
-
-    }
-
-
-    @Before("pointCut()")
-    public void before(JoinPoint point)
-    {
-        Object target = point.getTarget();
-        System.out.println(target.toString());
-        String method = point.getSignature().getName();
-        System.out.println(method);
-        Class<?>[] classz = target.getClass().getInterfaces();
-        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
-                .getMethod().getParameterTypes();
-        try {
-            Method m = classz[0].getMethod(method, parameterTypes);
-            System.out.println("method"+ m.getName());
-            if (m.isAnnotationPresent(DataSource.class)) {
-                DataSource data = m.getAnnotation(DataSource.class);
-                System.out.println("dataSource:"+data.value());
-                DatabaseContextHolder.putDatabaseType(DatabaseType.getDatabaseType(data.value()));
-            }
-
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    @After("pointCut()")
-    public void after() {
-				DatabaseContextHolder.clearDatabaseType();
-    }
-}
- -

通过接口判断是否带有注解跟是注解的值,DatabaseType 的配置不太好,不过先忽略了,然后在切点后进行清理

-

这是我 master1 的数据,

-

-

master2 的数据

-

-

然后跑一下简单的 demo,

-
@Override
-public void run(String...args) {
-	LOGGER.info("run here");
-	System.out.println(studentService.queryOne());
-	System.out.println(studentService.queryAnother());
-
-}
- -

看一下运行结果

-

-

其实这个方法应用场景不止可以用来迁移数据库,还能实现精细化的读写数据源分离之类的,算是做个简单记录和分享。

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

]]>
- Java - SpringBoot + 生活 - Java - Spring - SpringBoot - Druid - 数据源动态切换 + 生活 + 换车牌
@@ -14795,6 +14606,73 @@ t2 杭州 + + 记录下 zookeeper 集群迁移和易错点 + /2022/05/29/%E8%AE%B0%E5%BD%95%E4%B8%8B-zookeeper-%E9%9B%86%E7%BE%A4%E8%BF%81%E7%A7%BB/ + 前阵子做了zk 的集群升级迁移,大概情况是原来是一个三节点的 zk 集群(最小可用
大概是

+
zk1 192.168.2.1
+zk2 192.168.2.2
+zk3 192.168.2.3
+

在 zoo.cfg 中的配置就是如下

+
server.1=192.168.2.1:2888:3888
+server.2=192.168.2.2:2888:3888
+server.3=192.168.2.3:2888:3888
+

加节点

需要将集群迁移到 192.168.2.4(简称 zk4),192.168.2.5(简称 zk5),192.168.2.6(简称 zk6) 这三台机器上,目前新的这三台机器上是没有 zk 部署的, 我们想要的是数据不丢失,那主要考虑的就是滚动升级,这里我其实犯了几个错误,也特别说明下
首先我们想要新的三台机器加进去,所以我在zk4,zk5,zk6 的配置是这样

+
server.1=192.168.2.1:2888:3888
+server.2=192.168.2.2:2888:3888
+server.3=192.168.2.3:2888:3888
+server.4=192.168.2.4:2888:3888
+server.5=192.168.2.5:2888:3888
+server.6=192.168.2.6:2888:3888
+

这样起来发现状态是该节点没起来,
PS:查看当前节点状态可以通过 ./zkServer.sh status 来查看
第一个问题是我需要一个myid文件,标识我是哪个节点,里面的内容就写 456 这样就行了,并且这个文件的路径应该在配置文件中指定的dataDir=数据目录下
第二个问题是困扰我比较久的,我在按上面的配置启动节点后,发现这几个节点都是没起来的,并且有 FastLeaderElection@xxx - Notification time out: 60000 这个报错,一开始以为是网络不通,端口没开这些原因,检查了下都是通的,结果原因其实跟我之前的一个考虑是相关的,当有六个节点的时候,理论上需要有半数以上的节点可用,集群才会是健康的,但是按我这个方式起来,其实我配置了六个节点,但是其中三个都是不可用的(包括自身节点),那么它自然是没办法正常工作,所以这里其实也需要滚动添加,类似于这样
我的 zk4 的配置应该是这样

+
server.1=192.168.2.1:2888:3888
+server.2=192.168.2.2:2888:3888
+server.3=192.168.2.3:2888:3888
+server.4=192.168.2.4:2888:3888
+

然后 zk5 的配置

+
server.1=192.168.2.1:2888:3888
+server.2=192.168.2.2:2888:3888
+server.3=192.168.2.3:2888:3888
+server.4=192.168.2.4:2888:3888
+server.5=192.168.2.5:2888:3888
+

接着 zk6 的配置就可以是全部了

+
server.1=192.168.2.1:2888:3888
+server.2=192.168.2.2:2888:3888
+server.3=192.168.2.3:2888:3888
+server.4=192.168.2.4:2888:3888
+server.5=192.168.2.5:2888:3888
+server.6=192.168.2.6:2888:3888
+

然后为了集群完全更新,就继续在 zk4zk5 加上其他节点,这样我的 6 节点集群就起来了

+

下节点

这里我踩了另外一个坑,或者说没搞清楚两种方式的差别,

+

第一种

首先说说我没采用的第一种方式,(也是比较合理的)其实上面这个集群有个明显的问题,老集群其实还是各自认了一个三节点的集群,其中 zk3 是主节点,对于 zk1,zk2,zk3 来说它们能看到的就只有这三个节点,对于后三个 zk4,zk5,zk6 节点来说他们能连上其余五个节点,可以认为这是个六节点的集群,那么比较合理的操作应该是在老的三节点上把后面三个也都加进来,即每个节点的配置里 server 都有 6 个,然后我再对老的节点进行下线,这里下线需要注意的比较理想的是下一个节点就要修改配置,挪掉下线的节点后进行一遍重启,比如我知道了集群中的 leader 是在 zk3 上面,那么我先将 zk1 和 zk2 下掉,那么在我将 zk1 下线的之后,我将其他的五个节点都删除 zk1 的配置,然后重启,这样其实不是必须,但相对会可靠些,理论上我也可以在下掉 zk1 和 zk2 之后再修改配置重启其余节点。而当只剩下 zk3,zk4,zk5,zk6 四个节点的集群后,并且每个节点里的配置也只有这四个 server,我再下线 zk3 这个 leader 的时候,就会进行选举,再选出新的 leader,因为刚好是三节点,同样保证了最小可用。

+

第二种

这也是我踩坑的一种方式,就是我没有修改原来三节点的配置,并且我一开始以为可以通过下线 zk1,zk2,zk3(进行选举)的方式完成下线,然后再进行重启,但是这种方式就是我上面说的,原来的三节点里我下掉 zk1 还是能够正常运行,但是我下线 zk2 的时候,这个集群就等于是挂了,小于最小可用了,这样三节点都挂了,而且对于新加入的三个节点来说,又回到了最初起不来一样状态,六节点里只有三节点在线,导致整个集群都挂了,所以对于我这样的操作来说,我需要滚动修改启动,在下线 zk1 的时候就需要把 zk4,zk5,zk6 中的 zk1 移除后重启,当然这样唯一的好处就是可以少重启几个,同样继续下线 zk2 的时候,把 zk2 移除掉再重启,其实在移除 zk1 后修改重启后,在下线 zk2 的时候,集群就会重新选举了,因为 zk2 下线的时候,zk3 还是会一起下线。这个是我们需要特别注意的

+]]>
+ + java + + + zookeeper + +
+ + 这周末我又在老丈人家打了天小工 + /2020/08/30/%E8%BF%99%E5%91%A8%E6%9C%AB%E6%88%91%E5%8F%88%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/ + 因为活实在比较多,也不太好叫大工(活比较杂散),相比上一次我跟 LD 俩人晚起了一点,我真的是只要有事,早上就醒的很早,准备八点出发的,六点就醒了,然后想继续睡就一直做梦🤦‍♂️,差不多八点半多到的丈人家,他们应该已经干了有一会了,我们到了以后就分配给我撬地板的活,上次说的那个敲掉柜子的房间里,还铺着质地还不错的木地板,但是也不想要了,得撬掉重新铺。
拿着撬棍和榔头就上楼去干了,浙江这几天的天气,最高温度一般 38、9,楼上那个房间也没风扇,有了也不能用,都是灰尘,撬了两下,我感觉我体内的水就像真气爆发一样变成汗炸了出来,眼睛全被汗糊住了,可能大部分人不太了解地板是怎么铺的,一般是在地面先铺一层混凝土,混凝土中间嵌进去规则的长条木条,然后真正的地板一块块的都是钉在那个木条上,用那种气枪钉和普通的钉子,并且块跟块之前还有一个木头的槽结构相互耦合,然后边缘的一圈在用较薄的木板将整个木地板封边(这些词都是我现造的),边缘的用的钉子会更多,所以那几下真的很用力,而且撬地板,得蹲下起来,如此反复,对于我这个体重快超过身高的中年人来说的确是非常大的挑战,接下来继续撬了几个,已经有种要虚脱晕倒的感觉了,及时去喝水擦了汗,又歇了一会,为啥一上来就这么拼呢,主要是因为那个房间丈人在干活的时候是直接看得到的🤦‍♂️,后来被 LD 一顿教育,本来就是去帮忙的,又不是专业做这个的,急啥。
喝了水之后,又稍稍歇了一会,就开始继续撬了,本来觉得这个地板撬着好像还行,房间不大,没多久就撬完了,撬完之后喝了点饮料(补充点糖分,早餐吃得少,有点低血糖),然后看到 LD 在撬下面的木条了,这个动作开始了那天最大的经验值收集行动,前面说了这个木条一般是跟混凝土一块铺上去的,但是谁也没想到,这个混凝土铺上去的时候竟然处理的这么随意,根本没考虑跟下面的贴合,所以撬木条的时候直接把木条跟木条中间大块大块的混凝土一块撬起来了,想想那重量,于是我这靠蛮力干活的,就用力把木条带着混凝土一块撬了起来,还沾沾自喜,但是发现结果是撬起来一块之后,体力值瞬间归零,上一篇我也提到了,其实干这类活也是很有技巧性的,但是上次的是我没学会,可能需要花时间学的,但是这次是LD 用她的纤细胳膊教会我的,我在撬的时候,屏住一口气,双手用力,起,大概是吃好几口奶的力气都用出来了,但是 LD 在我休息的时候,慢慢悠悠的,先把撬棍挤到木条或者混凝土跟下层的缝里,然后往下垫一小块混凝土碎石,然后轻轻松松的扳两下,就撬开了,亏我高中的时候引以为傲的物理成绩,作为物理课代表,这么浅显易懂的杠杆原理都完全不会用到生活里,后面在用这个技巧撬的过程中,真的觉得自己蠢到家了,当然在明白了用点杠杆原理之后,撬地板的活就变得慢慢悠悠,悠哉悠哉的了(其实还是很热的,披着毛巾擦眼睛)。
上午的活差不多完了,后面就是把撬出来的混凝土和地板条丢下去,地上铺着不用了的被子,然后就是午饭和午休环节了,午饭换了一家快餐,味道非常可以,下午的活就比较单调了,帮忙清理了上去扔下来的混凝土碎块跟木条,然后稍微打扫了下,老丈人就让我们回家了,接着上次说的,还是觉得比跑步啥的消耗大太多了,那汗流的,一口就能喝完一瓶 500 毫升左右的矿泉水。

+]]>
+ + 生活 + 运动 + 跑步 + 干活 + + + 生活 + 运动 + 减肥 + 跑步 + 干活 + +
记录下 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/ @@ -14864,73 +14742,6 @@ t2 stream - - 记录下 zookeeper 集群迁移和易错点 - /2022/05/29/%E8%AE%B0%E5%BD%95%E4%B8%8B-zookeeper-%E9%9B%86%E7%BE%A4%E8%BF%81%E7%A7%BB/ - 前阵子做了zk 的集群升级迁移,大概情况是原来是一个三节点的 zk 集群(最小可用
大概是

-
zk1 192.168.2.1
-zk2 192.168.2.2
-zk3 192.168.2.3
-

在 zoo.cfg 中的配置就是如下

-
server.1=192.168.2.1:2888:3888
-server.2=192.168.2.2:2888:3888
-server.3=192.168.2.3:2888:3888
-

加节点

需要将集群迁移到 192.168.2.4(简称 zk4),192.168.2.5(简称 zk5),192.168.2.6(简称 zk6) 这三台机器上,目前新的这三台机器上是没有 zk 部署的, 我们想要的是数据不丢失,那主要考虑的就是滚动升级,这里我其实犯了几个错误,也特别说明下
首先我们想要新的三台机器加进去,所以我在zk4,zk5,zk6 的配置是这样

-
server.1=192.168.2.1:2888:3888
-server.2=192.168.2.2:2888:3888
-server.3=192.168.2.3:2888:3888
-server.4=192.168.2.4:2888:3888
-server.5=192.168.2.5:2888:3888
-server.6=192.168.2.6:2888:3888
-

这样起来发现状态是该节点没起来,
PS:查看当前节点状态可以通过 ./zkServer.sh status 来查看
第一个问题是我需要一个myid文件,标识我是哪个节点,里面的内容就写 456 这样就行了,并且这个文件的路径应该在配置文件中指定的dataDir=数据目录下
第二个问题是困扰我比较久的,我在按上面的配置启动节点后,发现这几个节点都是没起来的,并且有 FastLeaderElection@xxx - Notification time out: 60000 这个报错,一开始以为是网络不通,端口没开这些原因,检查了下都是通的,结果原因其实跟我之前的一个考虑是相关的,当有六个节点的时候,理论上需要有半数以上的节点可用,集群才会是健康的,但是按我这个方式起来,其实我配置了六个节点,但是其中三个都是不可用的(包括自身节点),那么它自然是没办法正常工作,所以这里其实也需要滚动添加,类似于这样
我的 zk4 的配置应该是这样

-
server.1=192.168.2.1:2888:3888
-server.2=192.168.2.2:2888:3888
-server.3=192.168.2.3:2888:3888
-server.4=192.168.2.4:2888:3888
-

然后 zk5 的配置

-
server.1=192.168.2.1:2888:3888
-server.2=192.168.2.2:2888:3888
-server.3=192.168.2.3:2888:3888
-server.4=192.168.2.4:2888:3888
-server.5=192.168.2.5:2888:3888
-

接着 zk6 的配置就可以是全部了

-
server.1=192.168.2.1:2888:3888
-server.2=192.168.2.2:2888:3888
-server.3=192.168.2.3:2888:3888
-server.4=192.168.2.4:2888:3888
-server.5=192.168.2.5:2888:3888
-server.6=192.168.2.6:2888:3888
-

然后为了集群完全更新,就继续在 zk4zk5 加上其他节点,这样我的 6 节点集群就起来了

-

下节点

这里我踩了另外一个坑,或者说没搞清楚两种方式的差别,

-

第一种

首先说说我没采用的第一种方式,(也是比较合理的)其实上面这个集群有个明显的问题,老集群其实还是各自认了一个三节点的集群,其中 zk3 是主节点,对于 zk1,zk2,zk3 来说它们能看到的就只有这三个节点,对于后三个 zk4,zk5,zk6 节点来说他们能连上其余五个节点,可以认为这是个六节点的集群,那么比较合理的操作应该是在老的三节点上把后面三个也都加进来,即每个节点的配置里 server 都有 6 个,然后我再对老的节点进行下线,这里下线需要注意的比较理想的是下一个节点就要修改配置,挪掉下线的节点后进行一遍重启,比如我知道了集群中的 leader 是在 zk3 上面,那么我先将 zk1 和 zk2 下掉,那么在我将 zk1 下线的之后,我将其他的五个节点都删除 zk1 的配置,然后重启,这样其实不是必须,但相对会可靠些,理论上我也可以在下掉 zk1 和 zk2 之后再修改配置重启其余节点。而当只剩下 zk3,zk4,zk5,zk6 四个节点的集群后,并且每个节点里的配置也只有这四个 server,我再下线 zk3 这个 leader 的时候,就会进行选举,再选出新的 leader,因为刚好是三节点,同样保证了最小可用。

-

第二种

这也是我踩坑的一种方式,就是我没有修改原来三节点的配置,并且我一开始以为可以通过下线 zk1,zk2,zk3(进行选举)的方式完成下线,然后再进行重启,但是这种方式就是我上面说的,原来的三节点里我下掉 zk1 还是能够正常运行,但是我下线 zk2 的时候,这个集群就等于是挂了,小于最小可用了,这样三节点都挂了,而且对于新加入的三个节点来说,又回到了最初起不来一样状态,六节点里只有三节点在线,导致整个集群都挂了,所以对于我这样的操作来说,我需要滚动修改启动,在下线 zk1 的时候就需要把 zk4,zk5,zk6 中的 zk1 移除后重启,当然这样唯一的好处就是可以少重启几个,同样继续下线 zk2 的时候,把 zk2 移除掉再重启,其实在移除 zk1 后修改重启后,在下线 zk2 的时候,集群就会重新选举了,因为 zk2 下线的时候,zk3 还是会一起下线。这个是我们需要特别注意的

-]]>
- - java - - - zookeeper - -
- - 这周末我又在老丈人家打了天小工 - /2020/08/30/%E8%BF%99%E5%91%A8%E6%9C%AB%E6%88%91%E5%8F%88%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E6%89%93%E4%BA%86%E5%A4%A9%E5%B0%8F%E5%B7%A5/ - 因为活实在比较多,也不太好叫大工(活比较杂散),相比上一次我跟 LD 俩人晚起了一点,我真的是只要有事,早上就醒的很早,准备八点出发的,六点就醒了,然后想继续睡就一直做梦🤦‍♂️,差不多八点半多到的丈人家,他们应该已经干了有一会了,我们到了以后就分配给我撬地板的活,上次说的那个敲掉柜子的房间里,还铺着质地还不错的木地板,但是也不想要了,得撬掉重新铺。
拿着撬棍和榔头就上楼去干了,浙江这几天的天气,最高温度一般 38、9,楼上那个房间也没风扇,有了也不能用,都是灰尘,撬了两下,我感觉我体内的水就像真气爆发一样变成汗炸了出来,眼睛全被汗糊住了,可能大部分人不太了解地板是怎么铺的,一般是在地面先铺一层混凝土,混凝土中间嵌进去规则的长条木条,然后真正的地板一块块的都是钉在那个木条上,用那种气枪钉和普通的钉子,并且块跟块之前还有一个木头的槽结构相互耦合,然后边缘的一圈在用较薄的木板将整个木地板封边(这些词都是我现造的),边缘的用的钉子会更多,所以那几下真的很用力,而且撬地板,得蹲下起来,如此反复,对于我这个体重快超过身高的中年人来说的确是非常大的挑战,接下来继续撬了几个,已经有种要虚脱晕倒的感觉了,及时去喝水擦了汗,又歇了一会,为啥一上来就这么拼呢,主要是因为那个房间丈人在干活的时候是直接看得到的🤦‍♂️,后来被 LD 一顿教育,本来就是去帮忙的,又不是专业做这个的,急啥。
喝了水之后,又稍稍歇了一会,就开始继续撬了,本来觉得这个地板撬着好像还行,房间不大,没多久就撬完了,撬完之后喝了点饮料(补充点糖分,早餐吃得少,有点低血糖),然后看到 LD 在撬下面的木条了,这个动作开始了那天最大的经验值收集行动,前面说了这个木条一般是跟混凝土一块铺上去的,但是谁也没想到,这个混凝土铺上去的时候竟然处理的这么随意,根本没考虑跟下面的贴合,所以撬木条的时候直接把木条跟木条中间大块大块的混凝土一块撬起来了,想想那重量,于是我这靠蛮力干活的,就用力把木条带着混凝土一块撬了起来,还沾沾自喜,但是发现结果是撬起来一块之后,体力值瞬间归零,上一篇我也提到了,其实干这类活也是很有技巧性的,但是上次的是我没学会,可能需要花时间学的,但是这次是LD 用她的纤细胳膊教会我的,我在撬的时候,屏住一口气,双手用力,起,大概是吃好几口奶的力气都用出来了,但是 LD 在我休息的时候,慢慢悠悠的,先把撬棍挤到木条或者混凝土跟下层的缝里,然后往下垫一小块混凝土碎石,然后轻轻松松的扳两下,就撬开了,亏我高中的时候引以为傲的物理成绩,作为物理课代表,这么浅显易懂的杠杆原理都完全不会用到生活里,后面在用这个技巧撬的过程中,真的觉得自己蠢到家了,当然在明白了用点杠杆原理之后,撬地板的活就变得慢慢悠悠,悠哉悠哉的了(其实还是很热的,披着毛巾擦眼睛)。
上午的活差不多完了,后面就是把撬出来的混凝土和地板条丢下去,地上铺着不用了的被子,然后就是午饭和午休环节了,午饭换了一家快餐,味道非常可以,下午的活就比较单调了,帮忙清理了上去扔下来的混凝土碎块跟木条,然后稍微打扫了下,老丈人就让我们回家了,接着上次说的,还是觉得比跑步啥的消耗大太多了,那汗流的,一口就能喝完一瓶 500 毫升左右的矿泉水。

-]]>
- - 生活 - 运动 - 跑步 - 干活 - - - 生活 - 运动 - 减肥 - 跑步 - 干活 - -
重看了下《蛮荒记》说说感受 /2021/10/10/%E9%87%8D%E7%9C%8B%E4%BA%86%E4%B8%8B%E3%80%8A%E8%9B%AE%E8%8D%92%E8%AE%B0%E3%80%8B%E8%AF%B4%E8%AF%B4%E6%84%9F%E5%8F%97/ @@ -14984,90 +14795,279 @@ server.6=192.168< - 聊聊这次换车牌及其他 - /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,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。

+ 闲话篇-路遇神逻辑骑车带娃爹 + /2022/05/08/%E9%97%B2%E8%AF%9D%E7%AF%87-%E8%B7%AF%E9%81%87%E7%A5%9E%E9%80%BB%E8%BE%91%E9%AA%91%E8%BD%A6%E5%B8%A6%E5%A8%83%E7%88%B9/ + 周末吃完中饭去买菜,没想到碰到这个神(zhi)奇(zhang)大哥带着两个娃,在非机动车道虽然没有像上班高峰车那么多,但是有送外卖,各种叮咚买菜和普通像我这样骑电驴,骑自行车的人,我的情况可能还特殊点,前面说过电驴买了以后本来网上找到过怎么解除限速的,后面看了下,限速 25 虽然慢,但还是对安全很有好处的,我上下班也不赶这个时间,所以就没解除,其他路上的电瓶车包括这位带娃的大哥可能有不少都不符合国标的限速要求或者解除了限速,这些算是铺垫。

+

那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。

+

现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。

+

说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。

]]>
生活 生活 - 换车牌
- 闲话篇-路遇神逻辑骑车带娃爹 - /2022/05/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 吹牛说拖地我是专业的,从小拖到大,有些地板缝边上的污渍,我又是用力来回拖,再用脚踩着拖,还是能把一些原来以为拖不掉的污渍给拖干净了,但是后来的厨房就比较难,用洗洁精来回拖感觉一点都起不来,可能是污渍积了太久了,一开始都想要放弃了,就打算拖干就好了,后来突然看到旁边有个洗衣服的板刷,结果竟然能刷起来,这样就停不下来了,说累是真的非常累,感觉刷一块瓷砖就要休息一会,但是整体刷完之后就是焕然一新的赶脚,简直太有成就感了。

]]>
生活 生活 + 大扫除
- 聊聊 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 的 equals 和 hashCode 方法 + /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/ + Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
简单看下代码
java.lang.Object#equals

+
public boolean equals(Object obj) {
+        return (this == obj);
+    }
+

对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了
java.lang.String#equals

+
public boolean equals(Object anObject) {
+        if (this == anObject) {
+            return true;
+        }
+        if (anObject instanceof String) {
+            String anotherString = (String)anObject;
+            int n = value.length;
+            if (n == anotherString.value.length) {
+                char v1[] = value;
+                char v2[] = anotherString.value;
+                int i = 0;
+                while (n-- != 0) {
+                    if (v1[i] != v2[i])
+                        return false;
+                    i++;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+

然后呢就是为啥一些书或者 effective java 中写了 equalshashCode 要一起重写,这里涉及到当对象作为 HashMapkey 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 objecthashCodenative 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.

]]>
- Java + java - Java - Sharding-Jdbc + java
- 聊聊最近平淡的生活之《花束般的恋爱》观后感 - /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 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。

+ 聊聊 Java 中绕不开的 Synchronized 关键字-二 + /2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ + Java并发

synchronized 的一些学习记录

+

jdk1.6 以后对 synchronized 进行了一些优化,包括偏向锁,轻量级锁,重量级锁等

+

这些锁的加锁方式大多跟对象头有关,我们可以查看 jdk 代码

+

首先对象头的位置注释

+
// Bit-format of an object header (most significant first, big endian layout below):
+//
+//  32 bits:
+//  --------
+//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
+//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
+//             size:32 ------------------------------------------>| (CMS free block)
+//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
+//
+//  64 bits:
+//  --------
+//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
+//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
+//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
+//  size:64 ----------------------------------------------------->| (CMS free block)
+//
+//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
+//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
+//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
+//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
+ +
enum { locked_value             = 0,
+         unlocked_value           = 1,
+         monitor_value            = 2,
+         marked_value             = 3,
+         biased_lock_pattern      = 5
+};
+
+ +

我们可以用 java jol库来查看对象头,通过一段简单的代码来看下

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+		}
+}
+ +

Untitled

+

然后可以看到打印输出,当然这里因为对齐方式,我们看到的其实顺序是反过来的,按最后三位去看,我们这是 001,好像偏向锁都没开,这里使用的是 jdk1.8,默认开始偏向锁的,其实这里有涉及到了一个配置,jdk1.8 中偏向锁会延迟 4 秒开启,可以通过添加启动参数 -XX:+PrintFlagsFinal,看到

+

偏向锁延迟

+

因为在初始化的时候防止线程竞争有大量的偏向锁撤销升级,所以会延迟 4s 开启

+

我们再来延迟 5s 看看

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+				TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+		}
+} 
+ +

https://img.nicksxs.com/uPic/2LBKpX.jpg

+

可以看到偏向锁设置已经开启了,我们来是一下加个偏向锁

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+        synchronized (l) {
+            System.out.println("2\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+		}
+}
+ +

看下运行结果

+

https://img.nicksxs.com/uPic/V2l78m.png

+

可以看到是加上了 101 = 5 也就是偏向锁,后面是线程 id

+

当我再使用一个线程来竞争这个锁的时候

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+				Thread thread1 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread1 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                    try {
+                        TimeUnit.SECONDS.sleep(5L);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+				thread1.start();
+		}
+}
+ +

https://img.nicksxs.com/uPic/bRMvlR.png

+

可以看到变成了轻量级锁,在线程没有争抢,只是进行了切换,就会使用轻量级锁,当两个线程在竞争了,就又会升级成重量级锁

+
public class ObjectHeaderDemo {
+    public static void main(String[] args) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(5);
+        L l = new L();
+        System.out.println(ClassLayout.parseInstance(l).toPrintable());
+        synchronized (l) {
+            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
+        }
+        Thread thread1 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread1 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                    try {
+                        TimeUnit.SECONDS.sleep(5L);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        };
+        Thread thread2 = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    TimeUnit.SECONDS.sleep(5L);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                synchronized (l) {
+                    System.out.println("thread2 获取锁成功");
+                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
+                }
+            }
+        };
+        thread1.start();
+        thread2.start();
+    }
+}
+
+class L {
+    private boolean myboolean = true;
+}
+ +

https://img.nicksxs.com/uPic/LMzMtR.png

+

可以看到变成了重量级锁。

]]>
- 生活 + Java - 生活 - 看剧 + Java + Synchronized + 偏向锁 + 轻量级锁 + 重量级锁 + 自旋
- 难得的大扫除 - /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/ - 因为房东要来续签合同,记得之前她说要来看看,后来一直都没来成,一方面我们没打扫过也不想被看到,小房子东西从搬进来以后越来越多,虽然不是脏乱差,但也觉得有点不满意干净状态,这里不得不感叹房东家的有钱程度,买了房子自己都没进房子看过,买来只是为了个学籍,去年前房东把房子卖给新房东后,我们还是比较担心会要换房子了,这里其实是个我们在乎的优点略大于缺点的小房子,面积比较小,但是交通便利以及上下班通勤,周边配套也还不错,有个比较大的菜市场,虽然不常去,因为不太会挑不会还价,还是主要去附近一公里左右的超市,可以安静地挑菜,但是说实在的菜场的菜还是比超市新鲜一些。
大扫除说实在的住在这边以后就没有一次真正意义上的大扫除,因为平时也有在正常打扫,只有偶尔的厨房煤气灶和厕所专门清理下,平时扫地拖地都有做,但是因为说实在的这房子也比较老了,地板什么的都有明显的老化,表面上的油漆都已经被磨损掉了,一些污渍很难拖干净,而且包括厨房和厕所的瓷砖都是纹路特别多,加上磨损,基本是污渍很多,特别是厨房的,又有油渍,我们搬进来的时候厨房的地就已经不太干净了,还有一点就是虽然不是在乡下的房子,但是旁边有两条主干道,一般只要开着窗没几天就灰尘积起来了,公司的电脑在家两天不到就一层灰,而且有些灰在地上时间久一点就会变成那种棉絮状的,看起来就会觉得更脏,并且地板我们平时就是扫一下,然后拖一下没明显的脏东西跟大灰尘就好了,有一些脏的就很难拖干净。
这次的算是整体的大扫除,把柜子,桌子,茶几台,窗边的灰尘都要擦掉,有一些角落还是有蛮多灰尘,当然特别难受的就是电脑那些接口,线缆上的,都杂糅在一块,如果要全都解开了理顺了还是比较麻烦,并且得断电,所以还是尽力清理,但没有全弄开了(我承认我是在偷懒,这里得说下清理了键盘,键盘之前都是放着用,也没盖住,按键缝里就很容易积灰也很难清理,这次索性直接把键全拔了,但是里面的清理也还是挺麻烦,因为不是平板一块,而且还有小孔,有些缝隙也比较难擦进去,只能慢慢地用牙线棒裹着抹布还有棉签擦一下,然后把键帽用洗手液什么的都擦一下洗洗干净,最后晾干了装好感觉就是一把新键盘了,后面主要是拖地了,这次最神奇的就是这个拖地,本来我就跟 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 + C + 数据结构 + 源码 + Mysql - 生活 - 大扫除 + mysql + 数据结构 + 源码 + mvcc + read view + gap lock + next-key lock + 幻读
diff --git a/sitemap.xml b/sitemap.xml index c98c2f3442..961467e9cd 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1,6 +1,15 @@ + + https://nicksxs.me/2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/ + + 2022-08-22 + + monthly + 0.6 + + https://nicksxs.me/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/ @@ -46,15 +55,6 @@ 0.6 - - https://nicksxs.me/2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/ - - 2022-07-31 - - monthly - 0.6 - - https://nicksxs.me/2022/07/17/%E3%80%8A%E9%95%BF%E5%AE%89%E7%9A%84%E8%8D%94%E6%9E%9D%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F/ @@ -191,7 +191,7 @@ - https://nicksxs.me/2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ 2022-06-11 @@ -200,7 +200,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/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ 2022-06-11 @@ -209,7 +209,7 @@ - https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + https://nicksxs.me/2021/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 @@ -317,7 +317,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/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-06-11 @@ -326,7 +326,7 @@ - https://nicksxs.me/2020/08/06/Linux-%E4%B8%8B-grep-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E7%82%B9%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ 2022-06-11 @@ -335,7 +335,7 @@ - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 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 @@ -1424,7 +1424,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 @@ -1433,7 +1433,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/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 @@ -1568,7 +1568,7 @@ - https://nicksxs.me/2016/07/13/swoole-websocket-test/ + https://nicksxs.me/2016/10/12/summary-ranges-228/ 2020-01-12 @@ -1577,7 +1577,7 @@ - https://nicksxs.me/2016/10/12/summary-ranges-228/ + https://nicksxs.me/2016/07/13/swoole-websocket-test/ 2020-01-12 @@ -1729,14 +1729,28 @@ - https://nicksxs.me/tags/Linked-List/ + https://nicksxs.me/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/2019/ + https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ 2022-08-22 weekly 0.2 @@ -1806,56 +1820,63 @@ - https://nicksxs.me/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/ + https://nicksxs.me/tags/Apollo/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ + https://nicksxs.me/tags/value/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ + https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + https://nicksxs.me/tags/environment/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Apollo/ + https://nicksxs.me/tags/Stream/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/value/ + https://nicksxs.me/tags/Comparator/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ + https://nicksxs.me/tags/%E6%8E%92%E5%BA%8F/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/environment/ + https://nicksxs.me/tags/sort/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/tags/nullsfirst/ 2022-08-22 weekly 0.2 @@ -1868,6 +1889,13 @@ 0.2 + + https://nicksxs.me/tags/Linked-List/ + 2022-08-22 + weekly + 0.2 + + https://nicksxs.me/tags/Filter/ 2022-08-22 @@ -1981,112 +2009,112 @@ - https://nicksxs.me/tags/Shift-2D-Grid/ + https://nicksxs.me/tags/DP/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/DP/ + https://nicksxs.me/tags/stack/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/3Sum-Closest/ + https://nicksxs.me/tags/min-stack/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/linked-list/ + https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ + https://nicksxs.me/tags/leetcode-155/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/First-Bad-Version/ + https://nicksxs.me/tags/3Sum-Closest/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/string/ + https://nicksxs.me/tags/linked-list/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Intersection-of-Two-Arrays/ + https://nicksxs.me/tags/2019/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ + https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/dp/ + https://nicksxs.me/tags/First-Bad-Version/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ + https://nicksxs.me/tags/string/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Trapping-Rain-Water/ + https://nicksxs.me/tags/Shift-2D-Grid/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ + https://nicksxs.me/tags/Intersection-of-Two-Arrays/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Leetcode-42/ + https://nicksxs.me/tags/Rotate-Image/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Rotate-Image/ + https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/ + https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ 2022-08-22 weekly 0.2 @@ -2218,13 +2246,6 @@ 0.2 - - https://nicksxs.me/tags/Dubbo/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/tags/echo/ 2022-08-22 @@ -2247,56 +2268,42 @@ - https://nicksxs.me/tags/Gogs/ - 2022-08-22 - weekly - 0.2 - - - - https://nicksxs.me/tags/Webhook/ - 2022-08-22 - weekly - 0.2 - - - - https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/ + https://nicksxs.me/tags/Dubbo/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Stream/ + https://nicksxs.me/tags/Gogs/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Comparator/ + https://nicksxs.me/tags/Webhook/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%8E%92%E5%BA%8F/ + https://nicksxs.me/tags/RPC/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/sort/ + https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/nullsfirst/ + https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/ 2022-08-22 weekly 0.2 @@ -2317,14 +2324,14 @@ - https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ 2022-08-22 weekly 0.2 @@ -2520,28 +2527,35 @@ - https://nicksxs.me/tags/gc/ + https://nicksxs.me/tags/dp/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ + https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ + https://nicksxs.me/tags/Trapping-Rain-Water/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/jvm/ + https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/tags/Leetcode-42/ 2022-08-22 weekly 0.2 @@ -2722,13 +2736,6 @@ 0.2 - - https://nicksxs.me/tags/git/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/tags/scp/ 2022-08-22 @@ -2786,168 +2793,168 @@ - https://nicksxs.me/tags/RPC/ + https://nicksxs.me/tags/gc/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ + https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/stack/ + https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/min-stack/ + https://nicksxs.me/tags/jvm/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ + https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/leetcode-155/ + https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/DefaultMQPushConsumer/ + https://nicksxs.me/tags/utf8/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/ + https://nicksxs.me/tags/utf8mb4/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ + https://nicksxs.me/tags/utf8mb4-0900-ai-ci/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/ + https://nicksxs.me/tags/utf8mb4-unicode-ci/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/utf8/ + https://nicksxs.me/tags/utf8mb4-general-ci/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/utf8mb4/ + https://nicksxs.me/tags/DefaultMQPushConsumer/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-0900-ai-ci/ + https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-unicode-ci/ + https://nicksxs.me/tags/NameServer/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-general-ci/ + https://nicksxs.me/tags/SpringBoot/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/NameServer/ + https://nicksxs.me/tags/cglib/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/SpringBoot/ + https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/cglib/ + https://nicksxs.me/tags/Druid/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ + https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ + https://nicksxs.me/tags/%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/AutoConfiguration/ + https://nicksxs.me/tags/%E4%B8%BE%E9%87%8D/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A/ + https://nicksxs.me/tags/%E5%B0%84%E5%87%BB/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%BE%E9%87%8D/ + https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E5%B0%84%E5%87%BB/ + https://nicksxs.me/tags/AutoConfiguration/ 2022-08-22 weekly 0.2 @@ -3108,14 +3115,14 @@ - https://nicksxs.me/tags/top/ + https://nicksxs.me/tags/Broker/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/Broker/ + https://nicksxs.me/tags/top/ 2022-08-22 weekly 0.2 @@ -3129,98 +3136,105 @@ - https://nicksxs.me/tags/ThreadPool/ + https://nicksxs.me/tags/mvcc/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + https://nicksxs.me/tags/read-view/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/FixedThreadPool/ + https://nicksxs.me/tags/gap-lock/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/LimitedThreadPool/ + https://nicksxs.me/tags/next-key-lock/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/EagerThreadPool/ + https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/CachedThreadPool/ + https://nicksxs.me/tags/ThreadPool/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/mvcc/ + https://nicksxs.me/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/read-view/ + https://nicksxs.me/tags/FixedThreadPool/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/gap-lock/ + https://nicksxs.me/tags/LimitedThreadPool/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/next-key-lock/ + https://nicksxs.me/tags/EagerThreadPool/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/ + https://nicksxs.me/tags/CachedThreadPool/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/ + https://nicksxs.me/tags/git/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/is-null/ + https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/is-not-null/ + https://nicksxs.me/tags/is-null/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/tags/is-not-null/ 2022-08-22 weekly 0.2 @@ -3556,14 +3570,14 @@ - https://nicksxs.me/tags/Druid/ + https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ 2022-08-22 weekly 0.2 - https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ + https://nicksxs.me/tags/zookeeper/ 2022-08-22 weekly 0.2 @@ -3576,13 +3590,6 @@ 0.2 - - https://nicksxs.me/tags/zookeeper/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ 2022-08-22 @@ -3597,13 +3604,6 @@ 0.2 - - https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ 2022-08-22 @@ -3634,6 +3634,13 @@ 0.2 + + https://nicksxs.me/categories/Java/leetcode/ + 2022-08-22 + weekly + 0.2 + + https://nicksxs.me/categories/Java/JVM/ 2022-08-22 @@ -3677,63 +3684,77 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ + https://nicksxs.me/categories/C/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Linked-List/ + https://nicksxs.me/categories/leetcode/java/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/ + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/leetcode/java/Binary-Tree/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/ + https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E6%9D%91%E4%B8%8A%E6%98%A5%E6%A0%91/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/ 2022-08-22 weekly 0.2 @@ -3754,28 +3775,28 @@ - https://nicksxs.me/categories/Filter/ + https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/ + https://nicksxs.me/categories/Filter/ 2022-08-22 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/ + https://nicksxs.me/categories/Linked-List/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/ + https://nicksxs.me/categories/Java/Apollo/value/ 2022-08-22 weekly 0.2 @@ -3789,14 +3810,35 @@ - https://nicksxs.me/categories/linked-list/ + https://nicksxs.me/categories/Interceptor-AOP/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/Apollo/value/ + https://nicksxs.me/categories/stack/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/categories/leetcode/java/Linked-List/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ + 2022-08-22 + weekly + 0.2 + + + + https://nicksxs.me/categories/linked-list/ 2022-08-22 weekly 0.2 @@ -3810,21 +3852,21 @@ - https://nicksxs.me/categories/leetcode/java/Linked-List/ + https://nicksxs.me/categories/leetcode/java/DP/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ + https://nicksxs.me/categories/Spring/ 2022-08-22 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/ + https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ 2022-08-22 weekly 0.2 @@ -3838,28 +3880,28 @@ - https://nicksxs.me/categories/Linux/ + https://nicksxs.me/categories/leetcode/java/stack/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Interceptor-AOP/ + https://nicksxs.me/categories/Linux/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ + https://nicksxs.me/categories/Java/Maven/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/Maven/ + https://nicksxs.me/categories/leetcode/java/linked-list/ 2022-08-22 weekly 0.2 @@ -3894,35 +3936,35 @@ - https://nicksxs.me/categories/Java/Dubbo/ + https://nicksxs.me/categories/Spring/Servlet/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/DP/ + https://nicksxs.me/categories/Java/Dubbo/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ + https://nicksxs.me/categories/leetcode/java/string/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/linked-list/ + https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ 2022-08-22 weekly 0.2 @@ -3950,84 +3992,84 @@ - https://nicksxs.me/categories/redis/ + https://nicksxs.me/categories/Redis/Distributed-Lock/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/string/ + https://nicksxs.me/categories/redis/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Spring/ + https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ + https://nicksxs.me/categories/Spring/Servlet/Interceptor/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/Spring/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/Java/Spring/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Redis/Distributed-Lock/ + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/gc/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ + https://nicksxs.me/categories/MQ/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/MQ/ + https://nicksxs.me/categories/ssh/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/ssh/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ 2022-08-22 weekly 0.2 @@ -4048,21 +4090,21 @@ - https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ + https://nicksxs.me/categories/Mybatis/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/git/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ 2022-08-22 weekly 0.2 @@ -4076,7 +4118,7 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ + https://nicksxs.me/categories/Mysql/ 2022-08-22 weekly 0.2 @@ -4090,42 +4132,42 @@ - https://nicksxs.me/categories/Mysql/ + https://nicksxs.me/categories/Java/gc/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/stack/ + https://nicksxs.me/categories/C/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/SpringBoot/ + https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Mybatis/ + https://nicksxs.me/categories/Java/SpringBoot/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/Mybatis/Mysql/ + https://nicksxs.me/categories/Java/Dubbo/RPC/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/RPC/ + https://nicksxs.me/categories/Java/Mybatis/Mysql/ 2022-08-22 weekly 0.2 @@ -4146,7 +4188,7 @@ - https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/ 2022-08-22 weekly 0.2 @@ -4160,112 +4202,112 @@ - https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ + https://nicksxs.me/categories/git/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/Design-Patterns/ + https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Mac/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/SpringBoot/ + https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ + https://nicksxs.me/categories/Java/Design-Patterns/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ + https://nicksxs.me/categories/Mac/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ + https://nicksxs.me/categories/SpringBoot/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Spring/Servlet/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ + https://nicksxs.me/categories/Rust/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Rust/ + https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Java/gc/jvm/ + https://nicksxs.me/categories/MQ/RocketMQ/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/MQ/RocketMQ/ + https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/ 2022-08-22 weekly 0.2 @@ -4279,14 +4321,14 @@ - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ 2022-08-22 weekly 0.2 @@ -4300,14 +4342,14 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ + https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ 2022-08-22 weekly 0.2 @@ -4321,21 +4363,14 @@ - https://nicksxs.me/categories/leetcode/java/stack/ - 2022-08-22 - weekly - 0.2 - - - - https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/categories/Java/gc/jvm/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/categories/C/Redis/ 2022-08-22 weekly 0.2 @@ -4356,28 +4391,28 @@ - https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ + https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ + https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2022-08-22 weekly 0.2 @@ -4412,7 +4447,7 @@ - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/ + https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/ 2022-08-22 weekly 0.2 @@ -4426,21 +4461,14 @@ - https://nicksxs.me/categories/Spring/Servlet/Interceptor/ - 2022-08-22 - weekly - 0.2 - - - - https://nicksxs.me/categories/C/Redis/ + https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/ 2022-08-22 weekly 0.2 @@ -4461,14 +4489,14 @@ - 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/ + https://nicksxs.me/categories/Spring/Mybatis/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/%E5%B9%B2%E6%B4%BB/ 2022-08-22 weekly 0.2 @@ -4481,13 +4509,6 @@ 0.2 - - https://nicksxs.me/categories/Spring/Mybatis/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/categories/Dubbo/SPI/ 2022-08-22 @@ -4503,14 +4524,14 @@ - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/ + https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/ 2022-08-22 weekly 0.2 - https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/ + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/ 2022-08-22 weekly 0.2 @@ -4551,13 +4572,6 @@ 0.2 - - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ 2022-08-22 @@ -4565,13 +4579,6 @@ 0.2 - - https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/RocketMQ/ 2022-08-22 @@ -4580,7 +4587,7 @@ - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/ + https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ 2022-08-22 weekly 0.2 @@ -4614,13 +4621,6 @@ 0.2 - - https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ - 2022-08-22 - weekly - 0.2 - - https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/%E5%87%BB%E7%A9%BF/ 2022-08-22 diff --git a/tags/DP/index.html b/tags/DP/index.html index 44949a2918..dc7c2bb5dc 100644 --- a/tags/DP/index.html +++ b/tags/DP/index.html @@ -1 +1 @@ -标签: dp | Nicksxs's Blog

Nicksxs's Blog

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

0%
squs":"disqus"}} \ No newline at end of file +标签: dp | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file diff --git a/tags/JVM/index.html b/tags/JVM/index.html index 12487c0ff8..2c42e09133 100644 --- a/tags/JVM/index.html +++ b/tags/JVM/index.html @@ -1 +1 @@ -标签: jvm | Nicksxs's Blog

Nicksxs's Blog

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

0%
rity":true,"url":"https://nicksxs.me/tags/JVM/"} \ No newline at end of file +标签: jvm | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file diff --git a/tags/linked-list/index.html b/tags/linked-list/index.html index 6a350e72ab..c0f80771b5 100644 --- a/tags/linked-list/index.html +++ b/tags/linked-list/index.html @@ -1 +1 @@ -标签: linked list | Nicksxs's Blog

Nicksxs's Blog

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

0%

linked list 标签

2020
n-analytics.js"> \ No newline at end of file +标签: linked list | Nicksxs's Blog

Nicksxs's Blog

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

0%

linked list 标签

2020
\ No newline at end of file diff --git a/tags/mysql/index.html b/tags/mysql/index.html index cb460e005c..2260ce6dea 100644 --- a/tags/mysql/index.html +++ b/tags/mysql/index.html @@ -1 +1 @@ -标签: Mysql | Nicksxs's Blog

Nicksxs's Blog

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

0%
.js" integrity="sha256-vXZMYLEqsROAXkEw93GGIvaB2ab+QW6w3+1ahD9nXXA=" crossorigin="anonymous"> \ No newline at end of file +标签: Mysql | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file diff --git a/tags/php/index.html b/tags/php/index.html index 1c981edf8a..99ed8d511d 100644 --- a/tags/php/index.html +++ b/tags/php/index.html @@ -1 +1 @@ -标签: PHP | Nicksxs's Blog

Nicksxs's Blog

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

0%
,"home":false,"archive":false,"delay":true,"timeout":3000,"priority":true,"url":"https://nicksxs.me/tags/php/"} \ No newline at end of file +标签: PHP | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file