diff --git a/2017/04/25/rabbitmq-tips/index.html b/2017/04/25/rabbitmq-tips/index.html index a6f288b130..22e4452401 100644 --- a/2017/04/25/rabbitmq-tips/index.html +++ b/2017/04/25/rabbitmq-tips/index.html @@ -1,4 +1,4 @@ -rabbitmq-tips | Nicksxs's Blog

Nicksxs's Blog

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

0%

rabbitmq-tips

rabbitmq 介绍

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

简单的使用经验

通俗的理解

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

集群经验

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

Cluster status of node rabbit@rabbit1 ...
+rabbitmq-tips | Nicksxs's Blog

Nicksxs's Blog

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

0%

rabbitmq-tips

rabbitmq 介绍

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

简单的使用经验

通俗的理解

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

集群经验

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

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

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

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

其他可以参考官方文档

一些坑

消息丢失

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

集群搭建

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

\ No newline at end of file +Starting node rabbit@rabbit2 ...done.

其他可以参考官方文档

一些坑

消息丢失

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

集群搭建

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

\ No newline at end of file diff --git a/2020/05/31/聊聊-Dubbo-的-SPI/index.html b/2020/05/31/聊聊-Dubbo-的-SPI/index.html index 9f60a9fff8..1478a4e725 100644 --- a/2020/05/31/聊聊-Dubbo-的-SPI/index.html +++ b/2020/05/31/聊聊-Dubbo-的-SPI/index.html @@ -1,4 +1,4 @@ -聊聊 Dubbo 的 SPI | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊聊 Dubbo 的 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 就能加载对应的实现,

/**
+聊聊 Dubbo 的 SPI | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊聊 Dubbo 的 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
diff --git a/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/index.html b/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/index.html
index 2351ac7182..969df0f8da 100644
--- a/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/index.html
+++ b/2020/06/06/聊聊-Dubbo-的-SPI-续之自适应拓展/index.html
@@ -1,4 +1,4 @@
-聊聊 Dubbo 的 SPI 续之自适应拓展 | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊聊 Dubbo 的 SPI 续之自适应拓展

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 T getAdaptiveExtension() {
+聊聊 Dubbo 的 SPI 续之自适应拓展 | Nicksxs's Blog

Nicksxs's Blog

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

0%

聊聊 Dubbo 的 SPI 续之自适应拓展

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 T getAdaptiveExtension() {
         Object instance = cachedAdaptiveInstance.get();
         if (instance == null) {
             if (createAdaptiveInstanceError == null) {
diff --git a/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html b/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html
index 6d1034897b..7c63050292 100644
--- a/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html
+++ b/2020/10/25/Leetcode-104-二叉树的最大深度-Maximum-Depth-of-Binary-Tree-题解分析/index.html
@@ -1,4 +1,4 @@
-Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

0%

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

题目介绍

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

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

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

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

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

Nicksxs's Blog

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

0%

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

题目介绍

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

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

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

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

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

分析

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

\ No newline at end of file +}

分析

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

\ No newline at end of file diff --git a/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html b/2020/11/15/Leetcode-234-回文联表-Palindrome-Linked-List-题解分析/index.html index a3d29afa06..3d6763df55 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;
@@ -31,4 +31,4 @@
         }
         return true;
     }
-}
\ No newline at end of file +}
\ No newline at end of file diff --git a/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html b/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html index 9d9adb1dae..59b09e84d2 100644 --- a/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html +++ b/2020/12/13/Leetcode-105-从前序与中序遍历序列构造二叉树-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-题解分析/index.html @@ -1,4 +1,4 @@ -Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析 | Nicksxs's Blog

Nicksxs's Blog

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

0%

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

题目介绍

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

注意

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

例子:

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

Nicksxs's Blog

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

0%

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

题目介绍

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

注意

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

例子:

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

返回的二叉树

  3
  / \
 9  20
@@ -32,4 +32,4 @@ inorder = [9,3,15,20,7]
\ No newline at end of file +}
\ No newline at end of file diff --git a/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html b/2021/01/10/Leetcode-160-相交链表-intersection-of-two-linked-lists-题解分析/index.html index 646c3925a9..4e637a77ce 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) {
@@ -42,4 +42,4 @@ Input Explanation: The intersected node's value is 8 (note that this must no
             }
         }
         return null;
-    }

总结

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

\ No newline at end of file + }

总结

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

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

Nicksxs's Blog

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

0%

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

题目介绍

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

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

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

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

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

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

简要分析

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

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

代码

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

Nicksxs's Blog

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

0%

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

题目介绍

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

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

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

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

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

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

简要分析

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

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

代码

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

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

结果图

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

\ No newline at end of file +}

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

结果图

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

\ No newline at end of file diff --git a/baidusitemap.xml b/baidusitemap.xml index d78858cd61..526fbe8d8d 100644 --- a/baidusitemap.xml +++ b/baidusitemap.xml @@ -89,19 +89,19 @@ 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/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ 2022-06-11 - https://nicksxs.me/2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ + 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 @@ -189,11 +189,11 @@ 2022-06-11 - https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ + https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ 2022-06-11 - https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ + https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ 2022-06-11 @@ -604,10 +604,6 @@ https://nicksxs.me/2016/08/14/docker-mysql-cluster/ 2020-01-12 - - https://nicksxs.me/2016/09/29/binary-watch/ - 2020-01-12 - https://nicksxs.me/2016/10/11/minimum-size-subarray-sum-209/ 2020-01-12 @@ -617,15 +613,15 @@ 2020-01-12 - https://nicksxs.me/2019/06/18/openresty/ + https://nicksxs.me/2016/09/29/binary-watch/ 2020-01-12 - https://nicksxs.me/2016/11/10/php-abstract-class-and-interface/ + https://nicksxs.me/2019/06/18/openresty/ 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/2016/11/10/php-abstract-class-and-interface/ 2020-01-12 @@ -637,7 +633,7 @@ 2020-01-12 - https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ + 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 @@ -645,11 +641,11 @@ 2020-01-12 - https://nicksxs.me/2015/04/15/Leetcode-No-3/ + https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ 2020-01-12 - https://nicksxs.me/2015/01/04/Path-Sum/ + https://nicksxs.me/2015/04/15/Leetcode-No-3/ 2020-01-12 @@ -657,7 +653,7 @@ 2020-01-12 - https://nicksxs.me/2015/06/22/invert-binary-tree/ + https://nicksxs.me/2015/01/04/Path-Sum/ 2020-01-12 @@ -684,6 +680,10 @@ https://nicksxs.me/2016/07/13/swoole-websocket-test/ 2020-01-12 + + https://nicksxs.me/2015/06/22/invert-binary-tree/ + 2020-01-12 + https://nicksxs.me/2019/12/21/%E8%81%8A%E8%81%8AJava%E4%B8%AD%E7%9A%84%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/ 2019-12-22 diff --git a/categories/Java/GC/index.html b/categories/Java/GC/index.html index 0d3a4f8a70..c19c169f16 100644 --- a/categories/Java/GC/index.html +++ b/categories/Java/GC/index.html @@ -1 +1 @@ -分类: gc | Nicksxs's Blog

Nicksxs's Blog

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

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

Nicksxs's Blog

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

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

Nicksxs's Blog

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

0%
js/third-party/fancybox.js"> \ No newline at end of file +分类: java | Nicksxs's Blog

Nicksxs's Blog

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

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

Nicksxs's Blog

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

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

Nicksxs's Blog

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

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

Nicksxs's Blog

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

0%

linked list 分类

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

Nicksxs's Blog

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

0%

linked list 分类

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

Nicksxs's Blog

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

0%

linked list 分类

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

Nicksxs's Blog

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

0%

linked list 分类

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

Nicksxs's Blog

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

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

Nicksxs's Blog

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

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

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

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 T getAdaptiveExtension() {
+}

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

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 T getAdaptiveExtension() {
         Object instance = cachedAdaptiveInstance.get();
         if (instance == null) {
             if (createAdaptiveInstanceError == null) {
@@ -123,7 +123,7 @@
         return extension.refer(arg0, arg1);
     }
 }
-

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 就能加载对应的实现,

/**
+

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
diff --git a/search.xml b/search.xml
index 4c8295b756..c94061a8c5 100644
--- a/search.xml
+++ b/search.xml
@@ -1,5 +1,25 @@
 
 
+  
+    村上春树《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 更新
去到戎野老师家之后,天吾知道了关于深绘里的一些事情,深绘里的父亲与戎野老师应该是老友,深绘里的父亲在当初成立了一个叫”先驱”的公社,一个独立运行的社会组织,以运营农场作为物资来源,追求更为松散的共同体,即不过分激进地公有制,进行松散的共同生活,承认私有财产,简而言之就是这样一个能稳定存活下来的独立社会组织,但是随着稳定运行,内部的激进派和稳健派开始出现分歧,不可磨合,后来两派就分裂了,深绘里的父亲,深田保留在了稳健派,但是此时其实深田保内心是矛盾的,以为一开始其实是他倡导的独立革命才组织起了这群人,然而现在他又认清了现实社会已经不太相信能通过革命来独立的可能性,后来激进派便开始越加封闭,而且进行军事训练和思想教育,而后这个先驱的激进派别便有了新的名字”黎明”,深绘里也是在此时从先驱逃离来投靠戎野老师
暂时先写到这,未完待续~

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

-

跳槽

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

-

面试

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

-

技术方向

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

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

生活

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

-

期待

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

-]]>
- - 生活 - 年终总结 - 2019 - - - 生活 - 年终总结 - 2019 - -
- - 村上春树《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 更新
去到戎野老师家之后,天吾知道了关于深绘里的一些事情,深绘里的父亲与戎野老师应该是老友,深绘里的父亲在当初成立了一个叫”先驱”的公社,一个独立运行的社会组织,以运营农场作为物资来源,追求更为松散的共同体,即不过分激进地公有制,进行松散的共同生活,承认私有财产,简而言之就是这样一个能稳定存活下来的独立社会组织,但是随着稳定运行,内部的激进派和稳健派开始出现分歧,不可磨合,后来两派就分裂了,深绘里的父亲,深田保留在了稳健派,但是此时其实深田保内心是矛盾的,以为一开始其实是他倡导的独立革命才组织起了这群人,然而现在他又认清了现实社会已经不太相信能通过革命来独立的可能性,后来激进派便开始越加封闭,而且进行军事训练和思想教育,而后这个先驱的激进派别便有了新的名字”黎明”,深绘里也是在此时从先驱逃离来投靠戎野老师
暂时先写到这,未完待续~

-]]>
- - 生活 - 读后感 - 村上春树 - - - 读后感 - -
2020年中总结 /2020/07/11/2020%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ @@ -93,6 +64,23 @@ 年中总结 + + 2021 年终总结 + /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 又是一年年终总结,本着极度讨厌实时需求的理念,我还是 T+N 发布这个年终总结

+

工作篇

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

+]]>
+ + 生活 + 年终总结 + + + 生活 + 年终总结 + 2021 + 拖更 + +
34_Search_for_a_Range /2016/08/14/34-Search-for-a-Range/ @@ -637,354 +625,88 @@ public: - 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 - 年中总结 - 技术 - 读书 - -
- - add-two-number - /2015/04/14/Add-Two-Number/ - problem

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

-

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

-

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

- -

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

-

正确代码

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

第二个线程

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

+
/**
+     * 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();
+    }
-

失败的代码

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

-
@Override
-  protected void processField(Object bean, String beanName, Field field) {
-    // register @Value on field
-    Value value = field.getAnnotation(Value.class);
-    if (value == null) {
-      return;
-    }
-    Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
-
-    if (keys.isEmpty()) {
-      return;
-    }
-
-    for (String key : keys) {
-      SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
-      springValueRegistry.register(beanFactory, key, springValue);
-      logger.debug("Monitoring {}", springValue);
-    }
-  }
-

然后我们看下这个springValueRegistry是啥玩意

-
public class SpringValueRegistry {
-  private static final long CLEAN_INTERVAL_IN_SECONDS = 5;
-  private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
-  private final AtomicBoolean initialized = new AtomicBoolean(false);
-  private final Object LOCK = new Object();
-
-  public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
-    if (!registry.containsKey(beanFactory)) {
-      synchronized (LOCK) {
-        if (!registry.containsKey(beanFactory)) {
-          registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
-        }
-      }
-    }
-
-    registry.get(beanFactory).put(key, springValue);
-
-    // lazy initialize
-    if (initialized.compareAndSet(false, true)) {
-      initialize();
-    }
-  }
-

这类其实就是个 map 来存放 springvalue,然后有com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener来监听更新操作,当有变更时

-
@Override
- public void onChange(ConfigChangeEvent changeEvent) {
-   Set<String> keys = changeEvent.changedKeys();
-   if (CollectionUtils.isEmpty(keys)) {
-     return;
-   }
-   for (String key : keys) {
-     // 1. check whether the changed key is relevant
-     Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
-     if (targetValues == null || targetValues.isEmpty()) {
-       continue;
-     }
-
-     // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
-     // 这里其实有一点比较绕,是因为 Apollo 里的 namespace 划分,会出现 key 相同,但是 namespace 不同的情况,所以会有个优先级存在,所以需要去校验 environment 里面的是否已经更新,如果未更新则表示不需要更新
-     if (!shouldTriggerAutoUpdate(changeEvent, key)) {
-       continue;
-     }
-
-     // 3. update the value
-     for (SpringValue val : targetValues) {
-       updateSpringValue(val);
-     }
-   }
- }
-

其实原理很简单,就是得了解知道下

-]]>
- - Java - Apollo - value - - - Java - Apollo - value - 注解 - environment - -
- - Clone Graph Part I - /2014/12/30/Clone-Graph-Part-I/ - problem
Clone a graph. Input is a Node pointer. Return the Node pointer of the cloned graph.
-
-A graph is defined below:
-struct Node {
-vector neighbors;
-}
- - -

code

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

anlysis

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

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

-

第一个线程

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

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

第二个线程

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

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

然后来看下addWaiter的逻辑

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

然后来看下addWaiter的逻辑

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

然后就是enq的逻辑了

/**
@@ -1130,6 +852,262 @@ Node *clone(Node *graph) {
         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 + +
+ + add-two-number + /2015/04/14/Add-Two-Number/ + problem

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

+

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

+

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

+ +

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

+

正确代码

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

失败的代码

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

+
@Override
+  protected void processField(Object bean, String beanName, Field field) {
+    // register @Value on field
+    Value value = field.getAnnotation(Value.class);
+    if (value == null) {
+      return;
+    }
+    Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
+
+    if (keys.isEmpty()) {
+      return;
+    }
+
+    for (String key : keys) {
+      SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
+      springValueRegistry.register(beanFactory, key, springValue);
+      logger.debug("Monitoring {}", springValue);
+    }
+  }
+

然后我们看下这个springValueRegistry是啥玩意

+
public class SpringValueRegistry {
+  private static final long CLEAN_INTERVAL_IN_SECONDS = 5;
+  private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
+  private final AtomicBoolean initialized = new AtomicBoolean(false);
+  private final Object LOCK = new Object();
+
+  public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
+    if (!registry.containsKey(beanFactory)) {
+      synchronized (LOCK) {
+        if (!registry.containsKey(beanFactory)) {
+          registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
+        }
+      }
+    }
+
+    registry.get(beanFactory).put(key, springValue);
+
+    // lazy initialize
+    if (initialized.compareAndSet(false, true)) {
+      initialize();
+    }
+  }
+

这类其实就是个 map 来存放 springvalue,然后有com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener来监听更新操作,当有变更时

+
@Override
+ public void onChange(ConfigChangeEvent changeEvent) {
+   Set<String> keys = changeEvent.changedKeys();
+   if (CollectionUtils.isEmpty(keys)) {
+     return;
+   }
+   for (String key : keys) {
+     // 1. check whether the changed key is relevant
+     Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
+     if (targetValues == null || targetValues.isEmpty()) {
+       continue;
+     }
+
+     // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
+     // 这里其实有一点比较绕,是因为 Apollo 里的 namespace 划分,会出现 key 相同,但是 namespace 不同的情况,所以会有个优先级存在,所以需要去校验 environment 里面的是否已经更新,如果未更新则表示不需要更新
+     if (!shouldTriggerAutoUpdate(changeEvent, key)) {
+       continue;
+     }
+
+     // 3. update the value
+     for (SpringValue val : targetValues) {
+       updateSpringValue(val);
+     }
+   }
+ }
+

其实原理很简单,就是得了解知道下

+]]>
+ + Java + Apollo + value + + + Java + Apollo + value + 注解 + environment + +
Comparator使用小记 /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/ @@ -1232,86 +1210,206 @@ Node *clone(Node *graph) { - AbstractQueuedSynchronizer - /2019/09/23/AbstractQueuedSynchronizer/ - 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
首先 AQS 只要有这些属性

-
// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
-private transient volatile Node head;
+    Clone Graph Part I
+    /2014/12/30/Clone-Graph-Part-I/
+    problem
Clone a graph. Input is a Node pointer. Return the Node pointer of the cloned graph.
 
-// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
-private transient volatile Node tail;
+A graph is defined below:
+struct Node {
+vector neighbors;
+}
-// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁 -// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1 -private volatile int state; + +

code

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

anlysis

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

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

+
public class LongEvent {
+    private long value;
 
-// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
-// 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;
+    public void set(long value) {
+        this.value = value;
+    }
 
-    // ======== 下面的几个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;
-    // =====================================================
+    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> {
 
-    // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
-    // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
-    //    ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
-    volatile int waitStatus;
-    // 前驱节点的引用
-    volatile Node prev;
-    // 后继节点的引用
-    volatile Node next;
-    // 这个就是线程本尊
-    volatile Thread thread;
+    // 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;
 
-}
-

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

+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 - java - aqs + Java + Disruptor
- 2021 年终总结 - /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 又是一年年终总结,本着极度讨厌实时需求的理念,我还是 T+N 发布这个年终总结

-

工作篇

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

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

譬如我在用的这个笔记本

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

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

+
class LhsPadding
+{
+    protected long p1, p2, p3, p4, p5, p6, p7;
+}
+
+class Value extends LhsPadding
+{
+    protected volatile long value;
+}
+
+class RhsPadding extends Value
+{
+    protected long p9, p10, p11, p12, p13, p14, p15;
+}
+
+/**
+ * <p>Concurrent sequence class used for tracking the progress of
+ * the ring buffer and event processors.  Support a number
+ * of concurrent operations including CAS and order writes.
+ *
+ * <p>Also attempts to be more efficient with regards to false
+ * sharing by adding padding around the volatile field.
+ */
+public class Sequence extends RhsPadding
+{
+

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

]]>
- 生活 - 年终总结 + Java - 生活 - 年终总结 - 2021 - 拖更 + Java + Disruptor + +
+ + Dubbo 使用的几个记忆点 + /2022/04/02/Dubbo-%E4%BD%BF%E7%94%A8%E7%9A%84%E5%87%A0%E4%B8%AA%E8%AE%B0%E5%BF%86%E7%82%B9/ + 因为后台使用的 dubbo 作为 rpc 框架,并且会有一些日常使用情景有一些小的技巧,在这里做下记录作笔记用

+

dubbo 只拉取不注册

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

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

+

dubbo 只注册不拉取

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

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

+

权重配置

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

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

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

譬如我在用的这个笔记本

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

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

-
class LhsPadding
-{
-    protected long p1, p2, p3, p4, p5, p6, p7;
-}
-
-class Value extends LhsPadding
-{
-    protected volatile long value;
-}
-
-class RhsPadding extends Value
-{
-    protected long p9, p10, p11, p12, p13, p14, p15;
-}
+    Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析
+    /2021/10/07/Leetcode-021-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8-Merge-Two-Sorted-Lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

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

+

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

+

示例 1

+
+

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

+
+

示例 2

+

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

+
+

示例 3

+

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

+
+

简要分析

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

+

题解代码

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
+        // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
+        if (l1 == null) {
+            return l2;
+        }
+        if (l2 == null) {
+            return l1;
+        }
+        // new 一个合并后的头结点
+        ListNode merged = new ListNode();
+        // 这个是当前节点
+        ListNode current = merged;
+        // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
+        // 因为内部前两个if就是跳出条件
+        while (true) {
+            if (l1 == null) {
+                // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
+                // 所以不能是直接current = l2,这样就是把后面的直接丢了
+                current.val = l2.val;
+                current.next = l2.next;
+                break;
+            }
+            if (l2 == null) {
+                current.val = l1.val;
+                current.next = l1.next;
+                break;
+            }
+            // 这里是两个链表都不为空的时候,就比较下大小
+            if (l1.val < l2.val) {
+                current.val = l1.val;
+                l1 = l1.next;
+            } else {
+                current.val = l2.val;
+                l2 = l2.next;
+            }
+            // 这里是new个新的,其实也可以放在循环头上
+            current.next = new ListNode();
+            current = current.next;
+        }
+        current = null;
+        // 返回这个头结点
+        return merged;
+    }
-/** - * <p>Concurrent sequence class used for tracking the progress of - * the ring buffer and event processors. Support a number - * of concurrent operations including CAS and order writes. - * - * <p>Also attempts to be more efficient with regards to false - * sharing by adding padding around the volatile field. - */ -public class Sequence extends RhsPadding -{
-

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

+

结果

]]>
Java + leetcode - Java - Disruptor + leetcode + java + 题解
@@ -2668,10 +2799,10 @@ Output: 0 @@ -2731,9 +2862,9 @@ inorder = [9,3,15,20,7] - 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 或者很低

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

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

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

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

示例

Example 1:

+

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

+
+

Example 2:

+

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

+
+

题解

简析

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

+

code

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

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

-

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

-

示例 1

-
-

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

-
-

示例 2

-

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

-
-

示例 3

-

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

-
-

简要分析

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

-

题解代码

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
-        // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
-        if (l1 == null) {
-            return l2;
-        }
-        if (l2 == null) {
-            return l1;
+    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];
         }
-        // new 一个合并后的头结点
-        ListNode merged = new ListNode();
-        // 这个是当前节点
-        ListNode current = merged;
-        // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
-        // 因为内部前两个if就是跳出条件
-        while (true) {
-            if (l1 == null) {
-                // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
-                // 所以不能是直接current = l2,这样就是把后面的直接丢了
-                current.val = l2.val;
-                current.next = l2.next;
-                break;
-            }
-            if (l2 == null) {
-                current.val = l1.val;
-                current.next = l1.next;
-                break;
-            }
-            // 这里是两个链表都不为空的时候,就比较下大小
-            if (l1.val < l2.val) {
-                current.val = l1.val;
-                l1 = l1.next;
-            } else {
-                current.val = l2.val;
-                l2 = l2.next;
-            }
-            // 这里是new个新的,其实也可以放在循环头上
-            current.next = new ListNode();
-            current = current.next;
+        if (prices[i] > maxOut) {
+            // 表示一个可卖出点,即比买入值高时
+            maxOut = prices[i];
+            // 需要设置一个历史值
+            maxSofar = Math.max(maxSofar, maxOut - maxIn);
         }
-        current = null;
-        // 返回这个头结点
-        return merged;
-    }
+ } + return maxSofar; +}
-

结果

+

总结下

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

]]>
Java leetcode + java + DP + DP leetcode java 题解 + DP
@@ -2883,58 +3040,90 @@ inorder = [9,3,15,20,7] - Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析 - /2021/03/14/Leetcode-121-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-Best-Time-to-Buy-and-Sell-Stock-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

-

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

-

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

-

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

-

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

-

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

-

简单分析

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

-
int maxSofar = 0;
-public int maxProfit(int[] prices) {
-    if (prices.length <= 1) {
-        return 0;
-    }
-    int maxIn = prices[0];
-    int maxOut = prices[0];
-    for (int i = 1; i < prices.length; i++) {
-        if (maxIn > prices[i]) {
-            // 当循环当前值小于之前的买入值时就当成买入值,同时卖出也要更新
-            maxIn = prices[i];
-            maxOut = prices[i];
-        }
-        if (prices[i] > maxOut) {
-            // 表示一个可卖出点,即比买入值高时
-            maxOut = prices[i];
-            // 需要设置一个历史值
-            maxSofar = Math.max(maxSofar, maxOut - maxIn);
+    Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析
+    /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

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

+

In one shift operation:

+

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

+

示例

Example 1:

+
+

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

+
+

Example 2:

+
+

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

+
+

Example 3:

+

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

+
+

提示

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

解析

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

+

代码

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

总结下

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

+

结果数据


比较慢

]]>
Java leetcode - java - DP - DP leetcode java 题解 - DP + Shift 2D Grid
@@ -3025,92 +3214,72 @@ minStack.getMin(); // return -2
-

事件生产

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

事件处理器

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

主方法代码

-
package disruptor;
-
-import com.lmax.disruptor.RingBuffer;
-import com.lmax.disruptor.dsl.Disruptor;
-import com.lmax.disruptor.util.DaemonThreadFactory;
-
-import java.nio.ByteBuffer;
-
-public class LongEventMain
-{
-    public static void main(String[] args) throws Exception
-    {
-        // 这个需要是 2 的幂次,这样在定位的时候只需要位移操作,也能减少各种计算操作
-        int bufferSize = 1024; 
-
-        Disruptor<LongEvent> disruptor = 
-                new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
-
-        // 类似于注册处理器
-        disruptor.handleEventsWith(new LongEventHandler());
-        // 或者直接用 lambda
-        disruptor.handleEventsWith((event, sequence, endOfBatch) ->
-                System.out.println("Event: " + event));
-        // 启动我们的 disruptor
-        disruptor.start(); 
-
-
-        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); 
-        ByteBuffer bb = ByteBuffer.allocate(8);
-        for (long l = 0; true; l++)
-        {
-            bb.putLong(0, l);
-            // 生产事件
-            ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
-            Thread.sleep(1000);
+    Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析
+    /2022/08/06/Leetcode-16-%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C-3Sum-Closest-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+    题目介绍

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

+

Return the sum of the three integers.

+

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

+

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

+

示例

Example 1:

+

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

+
+

Example 2:

+

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

+
+

Constraints:

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

简单解析

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

+

code

public int threeSumClosest(int[] nums, int target) {
+        Arrays.sort(nums);
+        // 当前最近的和
+        int closestSum = nums[0] + nums[1] + nums[nums.length - 1];
+        for (int i = 0; i < nums.length - 2; i++) {
+            if (i == 0 || nums[i] != nums[i - 1]) {
+                // 左指针
+                int left = i + 1;
+                // 右指针
+                int right = nums.length - 1;
+                // 判断是否遍历完了
+                while (left < right) {
+                    // 当前的和
+                    int sum = nums[i] + nums[left] + nums[right];
+                    // 小优化,相等就略过了
+                    while (left < right && nums[left] == nums[left + 1]) {
+                        left++;
+                    }
+                    while (left < right && nums[right] == nums[right - 1]) {
+                        right--;
+                    }
+                    // 这里判断,其实也还是希望趋近目标值
+                    if (sum < target) {
+                        left++;
+                    } else {
+                        right--;
+                    }
+                    // 判断是否需要替换
+                    if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
+                        closestSum = sum;
+                    }
+                }
+            }
         }
-    }
-}
-

运行下可以看到运行结果

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

+ return closestSum; + }
+ +

结果

]]>
Java + leetcode - Java - Disruptor + leetcode + java + 题解 + 3Sum Closest
@@ -3178,8 +3347,8 @@ Input Explanation: The intersected node's value is 8 (note that this must no leetcode java - Linked List 题解 + Linked List @@ -3244,133 +3413,82 @@ Output: [8,9,9,9,0,0,0,1] - Leetcode 16 最接近的三数之和 ( 3Sum Closest *Medium* ) 题解分析 - /2022/08/06/Leetcode-16-%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C-3Sum-Closest-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

-

Return the sum of the three integers.

-

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

-

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

-

示例

Example 1:

-

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

-
-

Example 2:

-

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

-
-

Constraints:

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

简单解析

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

-

code

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

结果

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

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

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

-
    -
  • thread A will call foo(), while
  • -
  • thread B will call bar().
    Modify the given program to output "foobar" n times.
  • -
+ Leetcode 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: n = 1
Output: “foobar”
Explanation: There are two threads being fired asynchronously. One of them calls foo(), while the other calls bar().
“foobar” is being output 1 time.

+

Input: s = “()”
Output: true

Example 2:

-

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

+

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

-

题解

简析

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

-

code

class FooBar {
-    
-    private final Semaphore foo = new Semaphore(1);
-    private final Semaphore bar = new Semaphore(0);
-    private int n;
-
-    public FooBar(int n) {
-        this.n = n;
-    }
+

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) {
 
-    public void foo(Runnable printFoo) throws InterruptedException {
-        
-        for (int i = 0; i < n; i++) {
-            foo.acquire();
-        	// printFoo.run() outputs "foo". Do not change or remove this line.
-        	printFoo.run();
-            bar.release();
+        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;
+            }
 
-    public void bar(Runnable printBar) throws InterruptedException {
-        
-        for (int i = 0; i < n; i++) {
-            bar.acquire();
-            // printBar.run() outputs "bar". Do not change or remove this line.
-        	printBar.run();
-            foo.release();
         }
+        return stk.size() == 0;
     }
-}
+}
+ ]]>
Java @@ -3379,8 +3497,6 @@ Output: [8,9,9,9,0,0,0,1]
@@ -3435,8 +3551,8 @@ Output: [8,9,9,9,0,0,0,1] @@ -3487,6 +3603,75 @@ Output: [8,9,9,9,0,0,0,1] + + Leetcode 3 Longest Substring Without Repeating Characters 题解分析 + /2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 又做了个题,看记录是以前用 C++写过的,现在捋一捋思路,用 Java 再写了一下,思路还是比较清晰的,但是边界细节处理得比较差

+

简要介绍

Given a string s, find the length of the longest substring without repeating characters.

+

样例

Example 1:

Input: s = "abcabcbb"
+Output: 3
+Explanation: The answer is "abc", with the length of 3.
+ +

Example 2:

Input: s = "bbbbb"
+Output: 1
+Explanation: The answer is "b", with the length of 1.
+

Example 3:

Input: s = "pwwkew"
+Output: 3
+Explanation: The answer is "wke", with the length of 3.
+Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
+

Example 4:

Input: s = ""
+Output: 0
+ +

就是一个最长不重复的字符串长度,因为也是中等难度的题,不太需要特别复杂的思考,最基本的就是O(N*N)两重循环,不过显然不太好,万一超时间,还有一种就是线性复杂度的了,这个就是需要搞定一个思路,比如字符串时 abcdefgaqwrty,比如遍历到第二个a的时候其实不用再从头去遍历了,只要把前面那个a给排除掉,继续往下算就好了

+
class Solution {
+    Map<String, Integer> counter = new HashMap<>();
+    public int lengthOfLongestSubstring(String s) {
+        int length = s.length();
+        // 当前的长度
+        int subStringLength = 0;
+        // 最长的长度
+        int maxSubStringLength = 0;
+        // 考虑到重复的位置已经被跳过的情况,即已经在当前长度的字符串范围之前的重复字符不需要回溯
+        int lastDuplicatePos = -1;
+        for (int i = 0; i < length; i++) {
+            // 使用 map 存储字符和上一次出现的位置,如果存在并且大于上一次重复位置
+            if (counter.get(String.valueOf(s.charAt(i))) != null && counter.get(String.valueOf(s.charAt(i))) > lastDuplicatePos) {
+                // 记录重复位置
+                lastDuplicatePos = counter.get(String.valueOf(s.charAt(i)));
+                // 重置不重复子串的长度,减去重复起点
+                subStringLength = i - counter.get(String.valueOf(s.charAt(i))) - 1;
+                // 替换当前位置
+                counter.replace(String.valueOf(s.charAt(i)), i);
+            } else {
+                // 如果不存在就直接 put
+                counter.put(String.valueOf(s.charAt(i)), i);
+            }
+            // 长度累加
+            subStringLength++;
+            if (subStringLength > maxSubStringLength) {
+                // 简单替换
+                maxSubStringLength = subStringLength;
+            }
+        }
+        return maxSubStringLength;
+    }
+}
+

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

+]]>
+ + Java + leetcode + java + 字符串 - online + string + + + leetcode + java + 题解 + string + +
Leetcode 278 第一个错误的版本 ( First Bad Version *Easy* ) 题解分析 /2022/08/14/Leetcode-278-%E7%AC%AC%E4%B8%80%E4%B8%AA%E9%94%99%E8%AF%AF%E7%9A%84%E7%89%88%E6%9C%AC-First-Bad-Version-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ @@ -3535,153 +3720,32 @@ Output: [8,9,9,9,0,0,0,1] - Leetcode 1260 二维网格迁移 ( Shift 2D Grid *Easy* ) 题解分析 - /2022/07/22/Leetcode-1260-%E4%BA%8C%E7%BB%B4%E7%BD%91%E6%A0%BC%E8%BF%81%E7%A7%BB-Shift-2D-Grid-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

-

In one shift operation:

-

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

-

示例

Example 1:

-
-

Input: 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
  • + 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等,跟消息队列主题关系不大🤦‍♂️
      这么看还是学了点东西的,希望新一年再接再厉。
    -

    解析

    这个题主要是矩阵或者说数组的操作,并且题目要返回的是个 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 3 Longest Substring Without Repeating Characters 题解分析 - /2020/09/20/Leetcode-3-Longest-Substring-Without-Repeating-Characters-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 又做了个题,看记录是以前用 C++写过的,现在捋一捋思路,用 Java 再写了一下,思路还是比较清晰的,但是边界细节处理得比较差

    -

    简要介绍

    Given a string s, find the length of the longest substring without repeating characters.

    -

    样例

    Example 1:

    Input: s = "abcabcbb"
    -Output: 3
    -Explanation: The answer is "abc", with the length of 3.
    - -

    Example 2:

    Input: s = "bbbbb"
    -Output: 1
    -Explanation: The answer is "b", with the length of 1.
    -

    Example 3:

    Input: s = "pwwkew"
    -Output: 3
    -Explanation: The answer is "wke", with the length of 3.
    -Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
    -

    Example 4:

    Input: s = ""
    -Output: 0
    - -

    就是一个最长不重复的字符串长度,因为也是中等难度的题,不太需要特别复杂的思考,最基本的就是O(N*N)两重循环,不过显然不太好,万一超时间,还有一种就是线性复杂度的了,这个就是需要搞定一个思路,比如字符串时 abcdefgaqwrty,比如遍历到第二个a的时候其实不用再从头去遍历了,只要把前面那个a给排除掉,继续往下算就好了

    -
    class Solution {
    -    Map<String, Integer> counter = new HashMap<>();
    -    public int lengthOfLongestSubstring(String s) {
    -        int length = s.length();
    -        // 当前的长度
    -        int subStringLength = 0;
    -        // 最长的长度
    -        int maxSubStringLength = 0;
    -        // 考虑到重复的位置已经被跳过的情况,即已经在当前长度的字符串范围之前的重复字符不需要回溯
    -        int lastDuplicatePos = -1;
    -        for (int i = 0; i < length; i++) {
    -            // 使用 map 存储字符和上一次出现的位置,如果存在并且大于上一次重复位置
    -            if (counter.get(String.valueOf(s.charAt(i))) != null && counter.get(String.valueOf(s.charAt(i))) > lastDuplicatePos) {
    -                // 记录重复位置
    -                lastDuplicatePos = counter.get(String.valueOf(s.charAt(i)));
    -                // 重置不重复子串的长度,减去重复起点
    -                subStringLength = i - counter.get(String.valueOf(s.charAt(i))) - 1;
    -                // 替换当前位置
    -                counter.replace(String.valueOf(s.charAt(i)), i);
    -            } else {
    -                // 如果不存在就直接 put
    -                counter.put(String.valueOf(s.charAt(i)), i);
    -            }
    -            // 长度累加
    -            subStringLength++;
    -            if (subStringLength > maxSubStringLength) {
    -                // 简单替换
    -                maxSubStringLength = subStringLength;
    -            }
    -        }
    -        return maxSubStringLength;
    -    }
    -}
    -

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

    +

    生活

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

    +

    期待

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

    ]]>
    - Java - leetcode - java - 字符串 - online - string + 生活 + 年终总结 + 2019 - leetcode - java - 题解 - string + 生活 + 年终总结 + 2019
    @@ -3758,6 +3822,72 @@ maxR[n -Leetcode 42 + + Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析 + /2022/03/07/Leetcode-349-%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86-Intersection-of-Two-Arrays-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

    给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

    +

     

    +

    示例

    +

    示例 1:

    +

    输入:nums1 = [1,2,2,1], nums2 = [2,2]
    输出:[2]

    +
    +
    +

    示例 2:

    +

    输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
    输出:[9,4]
    解释:[4,9] 也是可通过的 + 

    +
    +

    提示:

      +
    • 1 <= nums1.length, nums2.length <= 1000
    • +
    • 0 <= nums1[i], nums2[i] <= 1000
    • +
    +

    分析与题解

    两个数组的交集,最简单就是两层循环了把两个都存在的找出来,不过还有个要去重的问题,稍微思考下可以使用集合 set 来处理,先把一个数组全丢进去,再对比另外一个,如果出现在第一个集合里就丢进一个新的集合,最后转换成数组,这次我稍微取了个巧,因为看到了提示里的条件,两个数组中的元素都是不大于 1000 的,所以就搞了个 1000 长度的数组,如果在第一个数组出现,就在对应的下标设置成 1,如果在第二个数组也出现了就加 1,

    +

    code

    public int[] intersection(int[] nums1, int[] nums2) {
    +    // 大小是 1000 的数组,如果没有提示的条件就没法这么做
    +    // define a array which size is 1000, and can not be done like this without the condition in notice
    +        int[] inter = new int[1000];
    +        int[] outer;
    +        int m = 0;
    +        for (int j : nums1) {
    +            //  这里得是设置成 1,因为有可能 nums1 就出现了重复元素,如果直接++会造成结果重复
    +            // need to be set 1, cause element in nums1 can be duplicated
    +            inter[j] = 1;
    +        }
    +        for (int j : nums2) {
    +            if (inter[j] > 0) {
    +                // 这里可以直接+1,因为后面判断只需要判断大于 1
    +                // just plus 1, cause we can judge with condition that larger than  1
    +                inter[j] += 1;
    +            }
    +        }
    +        for (int i = 0; i < inter.length; i++) {
    +            // 统计下元素数量
    +            // count distinct elements
    +            if (inter[i] > 1) {
    +                m++;
    +            }
    +        }
    +        // initial a array of size m
    +        outer = new int[m];
    +        m = 0;
    +        for (int i = 0; i < inter.length; i++) {
    +            if (inter[i] > 1) {
    +                // add to outer
    +                outer[m++] = i;
    +            }
    +        }
    +        return outer;
    +    }
    ]]>
    + + Java + leetcode + + + leetcode + java + 题解 + Intersection of Two Arrays + +
    Leetcode 48 旋转图像(Rotate Image) 题解分析 /2021/05/01/Leetcode-48-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F-Rotate-Image-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ @@ -3816,6 +3946,29 @@ maxR[n -矩阵 + + 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 + 年中总结 + 技术 + 读书 + +
    Leetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析 /2022/03/27/Leetcode-4-%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0-Median-of-Two-Sorted-Arrays-Hard-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ @@ -3887,265 +4040,6 @@ maxR[n -Median of Two Sorted Arrays - - Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析 - /2022/06/19/Leetcode-698-%E5%88%92%E5%88%86%E4%B8%BAk%E4%B8%AA%E7%9B%B8%E7%AD%89%E7%9A%84%E5%AD%90%E9%9B%86-Partition-to-K-Equal-Sum-Subsets-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

    Given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.

    -

    示例

    Example 1:

    -
    -

    Input: nums = [4,3,2,3,5,2,1], k = 4
    Output: true
    Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.

    -
    -

    Example 2:

    -
    -

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

    -
    -

    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 - -
    - - 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 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析 - /2022/08/23/Leetcode-885-%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5-III-Spiral-Matrix-III-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

    You start at the cell (rStart, cStart) of an rows x cols grid facing east. The northwest corner is at the first row and column in the grid, and the southeast corner is at the last row and column.

    -

    You will walk in a clockwise spiral shape to visit every position in this grid. Whenever you move outside the grid’s boundary, we continue our walk outside the grid (but may return to the grid boundary later.). Eventually, we reach all rows * cols spaces of the grid.

    -

    Return an array of coordinates representing the positions of the grid in the order you visited them.

    -

    Example 1:

    -

    Input: rows = 1, cols = 4, rStart = 0, cStart = 0
    Output: [[0,0],[0,1],[0,2],[0,3]]

    -
    -

    Example 2:

    -

    Input: rows = 5, cols = 6, rStart = 1, cStart = 4
    Output: [[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]]

    -
    -

    Constraints:

    -
      -
    • 1 <= rows, cols <= 100
    • -
    • 0 <= rStart < rows
    • -
    • 0 <= cStart < cols
    • -
    -

    简析

    这个题主要是要相同螺旋矩阵的转变方向的边界判断,已经相同步长会行进两次这个规律,写代码倒不复杂

    -

    代码

    public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
    -        int size = rows * cols;
    -        int x = rStart, y = cStart;
    -        // 返回的二维矩阵
    -        int[][] matrix = new int[size][2];
    -        // 传入的参数就是入口第一个
    -        matrix[0][0] = rStart;
    -        matrix[0][1] = cStart;
    -        // 作为数量
    -        int z = 1;
    -        // 步进,1,1,2,2,3,3,4 ... 螺旋矩阵的增长
    -        int a = 1;
    -        // 方向 1 表示右,2 表示下,3 表示左,4 表示上
    -        int dir = 1;
    -        while (z < size) {
    -            for (int i = 0; i < 2; i++) {
    -                for (int j= 0; j < a; j++) {
    -                    // 处理方向
    -                    if (dir % 4 == 1) {
    -                        y++;
    -                    } else if (dir % 4 == 2) {
    -                        x++;
    -                    } else if (dir % 4 == 3) {
    -                        y--;
    -                    } else {
    -                        x--;
    -                    }
    -                    // 如果在实际矩阵内
    -                    if (x < rows && y < cols && x >= 0 && y >= 0) {
    -                        matrix[z][0] = x;
    -                        matrix[z][1] = y;
    -                        z++;
    -                    }
    -                }
    -                // 转变方向
    -                dir++;
    -            }
    -            // 步进++
    -            a++;
    -        }
    -        return matrix;
    -    }
    - -

    结果

    -]]>
    - - Java - leetcode - - - leetcode - java - 题解 - -
    leetcode no.3 /2015/04/15/Leetcode-No-3/ @@ -4229,124 +4123,6 @@ maxR[n -mfc - - Leetcode 349 两个数组的交集 ( Intersection of Two Arrays *Easy* ) 题解分析 - /2022/03/07/Leetcode-349-%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86-Intersection-of-Two-Arrays-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

    给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

    -

     

    -

    示例

    -

    示例 1:

    -

    输入:nums1 = [1,2,2,1], nums2 = [2,2]
    输出:[2]

    -
    -
    -

    示例 2:

    -

    输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
    输出:[9,4]
    解释:[4,9] 也是可通过的 - 

    -
    -

    提示:

      -
    • 1 <= nums1.length, nums2.length <= 1000
    • -
    • 0 <= nums1[i], nums2[i] <= 1000
    • -
    -

    分析与题解

    两个数组的交集,最简单就是两层循环了把两个都存在的找出来,不过还有个要去重的问题,稍微思考下可以使用集合 set 来处理,先把一个数组全丢进去,再对比另外一个,如果出现在第一个集合里就丢进一个新的集合,最后转换成数组,这次我稍微取了个巧,因为看到了提示里的条件,两个数组中的元素都是不大于 1000 的,所以就搞了个 1000 长度的数组,如果在第一个数组出现,就在对应的下标设置成 1,如果在第二个数组也出现了就加 1,

    -

    code

    public int[] intersection(int[] nums1, int[] nums2) {
    -    // 大小是 1000 的数组,如果没有提示的条件就没法这么做
    -    // define a array which size is 1000, and can not be done like this without the condition in notice
    -        int[] inter = new int[1000];
    -        int[] outer;
    -        int m = 0;
    -        for (int j : nums1) {
    -            //  这里得是设置成 1,因为有可能 nums1 就出现了重复元素,如果直接++会造成结果重复
    -            // need to be set 1, cause element in nums1 can be duplicated
    -            inter[j] = 1;
    -        }
    -        for (int j : nums2) {
    -            if (inter[j] > 0) {
    -                // 这里可以直接+1,因为后面判断只需要判断大于 1
    -                // just plus 1, cause we can judge with condition that larger than  1
    -                inter[j] += 1;
    -            }
    -        }
    -        for (int i = 0; i < inter.length; i++) {
    -            // 统计下元素数量
    -            // count distinct elements
    -            if (inter[i] > 1) {
    -                m++;
    -            }
    -        }
    -        // initial a array of size m
    -        outer = new int[m];
    -        m = 0;
    -        for (int i = 0; i < inter.length; i++) {
    -            if (inter[i] > 1) {
    -                // add to outer
    -                outer[m++] = i;
    -            }
    -        }
    -        return outer;
    -    }
    ]]>
    - - Java - leetcode - - - leetcode - java - 题解 - Intersection of Two Arrays - -
    - - Path Sum - /2015/01/04/Path-Sum/ - problem

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

    - -

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

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

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

    -

    Analysis

    using simple deep first search

    -

    code

    /*
    -  Definition for binary tree
    -  struct TreeNode {
    -      int val;
    -      TreeNode *left;
    -      TreeNode *right;
    -      TreeNode(int x) : val(x), left(NULL), right(NULL)}
    -  };
    - */
    -class Solution {
    -public:
    -    bool deep_first_search(TreeNode *node, int sum, int curSum)
    -    {
    -        if (node == NULL)
    -            return false;
    -        
    -        if (node->left == NULL && node->right == NULL)
    -            return curSum + node->val == sum;
    -               
    -        return deep_first_search(node->left, sum, curSum + node->val) || deep_first_search(node->right, sum, curSum + node->val);
    -    }
    -    
    -    bool hasPathSum(TreeNode *root, int sum) {
    -        // Start typing your C/C++ solution below
    -        // DO NOT write int main() function
    -        return deep_first_search(root, sum, 0);
    -    }
    -};
    -]]>
    - - leetcode - - - leetcode - c++ - -
    Maven实用小技巧 /2020/02/16/Maven%E5%AE%9E%E7%94%A8%E5%B0%8F%E6%8A%80%E5%B7%A7/ @@ -4426,24 +4202,27 @@ OS name: "mac os x", version: "10.14.6", arch: "x86_64& - Reverse Bits - /2015/03/11/Reverse-Bits/ - Reverse Bits

    Reverse bits of a given 32 bits unsigned integer.

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

    - -

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

    + Number of 1 Bits + /2015/03/11/Number-Of-1-Bits/ + Number of 1 Bits

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

    + +

    分析

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


    -

    code

    class Solution {
    -public:
    -    uint32_t reverseBits(uint32_t n) {
    -        n = ((n >> 1) & 0x55555555) | ((n & 0x55555555) << 1);
    -        n = ((n >> 2) & 0x33333333) | ((n & 0x33333333) << 2);
    -        n = ((n >> 4) & 0x0f0f0f0f) | ((n & 0x0f0f0f0f) << 4);
    -        n = ((n >> 8) & 0x00ff00ff) | ((n & 0x00ff00ff) << 8);
    -        n = ((n >> 16) & 0x0000ffff) | ((n & 0x0000ffff) << 16);
    -        return n;
    -    }
    -};
    -]]>
    +

    code

    int hammingWeight(uint32_t n) {
    +        const uint32_t m1  = 0x55555555; //binary: 0101...  
    +        const uint32_t m2  = 0x33333333; //binary: 00110011..  
    +        const uint32_t m4  = 0x0f0f0f0f; //binary:  4 zeros,  4 ones ...  
    +        const uint32_t m8  = 0x00ff00ff; //binary:  8 zeros,  8 ones ...  
    +        const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...  
    +        
    +        n = (n & m1 ) + ((n >>  1) & m1 ); //put count of each  2 bits into those  2 bits   
    +        n = (n & m2 ) + ((n >>  2) & m2 ); //put count of each  4 bits into those  4 bits   
    +        n = (n & m4 ) + ((n >>  4) & m4 ); //put count of each  8 bits into those  8 bits   
    +        n = (n & m8 ) + ((n >>  8) & m8 ); //put count of each 16 bits into those 16 bits   
    +        n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits   
    +        return n; 
    +
    +}
    ]]>
    leetcode @@ -4453,40 +4232,48 @@ public:
    - Reverse Integer - /2015/03/13/Reverse-Integer/ - Reverse Integer

    Reverse digits of an integer.

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

    + Path Sum + /2015/01/04/Path-Sum/ + problem

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

    -

    spoilers

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

    -

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

    -

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

    -

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

    -
    -

    code

    class Solution {
    +

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

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

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

    +

    Analysis

    using simple deep first search

    +

    code

    /*
    +  Definition for binary tree
    +  struct TreeNode {
    +      int val;
    +      TreeNode *left;
    +      TreeNode *right;
    +      TreeNode(int x) : val(x), left(NULL), right(NULL)}
    +  };
    + */
    +class Solution {
     public:
    -    int reverse(int x) {
    -
    -        int max = 1 << 31 - 1;
    -        int ret = 0;
    -        max = (max - 1) * 2 + 1;
    -        int min = 1 << 31;
    -        if(x < 0)
    -            while(x != 0){
    -                if(ret < (min - x % 10) / 10)
    -                    return 0;
    -                ret = ret * 10 + x % 10;
    -                x = x / 10;
    -            }
    -        else
    -            while(x != 0){
    -               if(ret > (max -x % 10) / 10)
    -                    return 0;
    -                ret = ret * 10 + x % 10;
    -                x = x / 10;
    -            }
    -        return ret;
    +    bool deep_first_search(TreeNode *node, int sum, int curSum)
    +    {
    +        if (node == NULL)
    +            return false;
    +        
    +        if (node->left == NULL && node->right == NULL)
    +            return curSum + node->val == sum;
    +               
    +        return deep_first_search(node->left, sum, curSum + node->val) || deep_first_search(node->right, sum, curSum + node->val);
         }
    -};
    + + bool hasPathSum(TreeNode *root, int sum) { + // Start typing your C/C++ solution below + // DO NOT write int main() function + return deep_first_search(root, sum, 0); + } +};
    ]]>
    leetcode @@ -4497,90 +4284,128 @@ public:
    - two sum - /2015/01/14/Two-Sum/ - problem

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

    -

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

    - -

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

    -

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

    -

    code

    struct Node
    -{
    -    int num, pos;
    -};
    -bool cmp(Node a, Node b)
    -{
    -    return a.num < b.num;
    -}
    -class Solution {
    -public:
    -    vector<int> twoSum(vector<int> &numbers, int target) {
    -        // Start typing your C/C++ solution below
    -        // DO NOT write int main() function
    -        vector<int> result;
    -        vector<Node> array;
    -        for (int i = 0; i < numbers.size(); i++)
    -        {
    -            Node temp;
    -            temp.num = numbers[i];
    -            temp.pos = i;
    -            array.push_back(temp);
    -        }
    -
    -        sort(array.begin(), array.end(), cmp);
    -        for (int i = 0, j = array.size() - 1; i != j;)
    -        {
    -            int sum = array[i].num + array[j].num;
    -            if (sum == target)
    -            {
    -                if (array[i].pos < array[j].pos)
    -                {
    -                    result.push_back(array[i].pos + 1);
    -                    result.push_back(array[j].pos + 1);
    -                } else
    -                {
    -                    result.push_back(array[j].pos + 1);
    -                    result.push_back(array[i].pos + 1);
    -                }
    -                break;
    -            } else if (sum < target)
    -            {
    -                i++;
    -            } else if (sum > target)
    -            {
    -                j--;
    -            }
    -        }
    -        return result;
    -    }
    -};
    + Leetcode 698 划分为k个相等的子集 ( Partition to K Equal Sum Subsets *Medium* ) 题解分析 + /2022/06/19/Leetcode-698-%E5%88%92%E5%88%86%E4%B8%BAk%E4%B8%AA%E7%9B%B8%E7%AD%89%E7%9A%84%E5%AD%90%E9%9B%86-Partition-to-K-Equal-Sum-Subsets-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

    Given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.

    +

    示例

    Example 1:

    +
    +

    Input: nums = [4,3,2,3,5,2,1], k = 4
    Output: true
    Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.

    +
    +

    Example 2:

    +
    +

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

    +
    +

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

    Analysis

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

    + 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 - c++ + java
    - ambari-summary - /2017/05/09/ambari-summary/ - 初识ambari

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

    -

    简单过程

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

    Reverse bits of a given 32 bits unsigned integer.

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

    + +

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

    +
    +

    code

    class Solution {
    +public:
    +    uint32_t reverseBits(uint32_t n) {
    +        n = ((n >> 1) & 0x55555555) | ((n & 0x55555555) << 1);
    +        n = ((n >> 2) & 0x33333333) | ((n & 0x33333333) << 2);
    +        n = ((n >> 4) & 0x0f0f0f0f) | ((n & 0x0f0f0f0f) << 4);
    +        n = ((n >> 8) & 0x00ff00ff) | ((n & 0x00ff00ff) << 8);
    +        n = ((n >> 16) & 0x0000ffff) | ((n & 0x0000ffff) << 16);
    +        return n;
    +    }
    +};
    ]]>
    - data analysis + leetcode - hadoop - cluster + leetcode + c++
    @@ -4636,62 +4461,41 @@ public: - docker-mysql-cluster - /2016/08/14/docker-mysql-cluster/ - docker-mysql-cluster

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

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

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

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

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

    主从配置

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

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

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

    -]]>
    - - docker - - - docker - mysql - -
    - - binary-watch - /2016/09/29/binary-watch/ - problem

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

    -

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

    -

    -

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

    -

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

    -

    Example:

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

    Note:

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

    题解

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

    -

    Code

    class Solution {
    +    Reverse Integer
    +    /2015/03/13/Reverse-Integer/
    +    Reverse Integer

    Reverse digits of an integer.

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

    + +

    spoilers

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

    +

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

    +

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

    +

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

    +
    +

    code

    class Solution {
     public:
    -    vector<string> readBinaryWatch(int num) {
    -        vector<string> res;
    -        for (int h = 0; h < 12; ++h) {
    -            for (int m = 0; m < 60; ++m) {
    -                if (bitset<10>((h << 6) + m).count() == num) {
    -                    res.push_back(to_string(h) + (m < 10 ? ":0" : ":") + to_string(m));
    -                }
    +    int reverse(int x) {
    +
    +        int max = 1 << 31 - 1;
    +        int ret = 0;
    +        max = (max - 1) * 2 + 1;
    +        int min = 1 << 31;
    +        if(x < 0)
    +            while(x != 0){
    +                if(ret < (min - x % 10) / 10)
    +                    return 0;
    +                ret = ret * 10 + x % 10;
    +                x = x / 10;
                 }
    -        }
    -        return res;
    +        else
    +            while(x != 0){
    +               if(ret > (max -x % 10) / 10)
    +                    return 0;
    +                ret = ret * 10 + x % 10;
    +                x = x / 10;
    +            }
    +        return ret;
         }
    -};
    ]]>
    +};
    +]]>
    leetcode @@ -4701,42 +4505,156 @@ public:
    - docker比一般多一点的初学者介绍 - /2020/03/08/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D/ - 因为最近想搭一个phabricator用来做看板和任务管理,一开始了解这个是Easy大大有在微博推荐过,后来苏洋也在群里和博客里说到了,看上去还不错的样子,因为主角是docker所以就不介绍太多,后面有机会写一下。

    -

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

    -

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

    -

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

    -

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

    -

    安装

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

    -
    sudo docker info
    - -

    输出以下信息

    -

    简单运行

    然后再来运行个 hello world 呗,

    -
    sudo docker run hello-world
    - -

    输出了这些

    -

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

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

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

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

    要有个虚拟终端,

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

    镜像

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

    -

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

    -

    -

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

    -

    在容器里看下进程,

    -

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

    -

    查看宿主机上的容器

    sudo docker ps -a
    - -

    -

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

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

    + two sum + /2015/01/14/Two-Sum/ + problem

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

    +

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

    + +

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

    +

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

    +

    code

    struct Node
    +{
    +    int num, pos;
    +};
    +bool cmp(Node a, Node b)
    +{
    +    return a.num < b.num;
    +}
    +class Solution {
    +public:
    +    vector<int> twoSum(vector<int> &numbers, int target) {
    +        // Start typing your C/C++ solution below
    +        // DO NOT write int main() function
    +        vector<int> result;
    +        vector<Node> array;
    +        for (int i = 0; i < numbers.size(); i++)
    +        {
    +            Node temp;
    +            temp.num = numbers[i];
    +            temp.pos = i;
    +            array.push_back(temp);
    +        }
    +
    +        sort(array.begin(), array.end(), cmp);
    +        for (int i = 0, j = array.size() - 1; i != j;)
    +        {
    +            int sum = array[i].num + array[j].num;
    +            if (sum == target)
    +            {
    +                if (array[i].pos < array[j].pos)
    +                {
    +                    result.push_back(array[i].pos + 1);
    +                    result.push_back(array[j].pos + 1);
    +                } else
    +                {
    +                    result.push_back(array[j].pos + 1);
    +                    result.push_back(array[i].pos + 1);
    +                }
    +                break;
    +            } else if (sum < target)
    +            {
    +                i++;
    +            } else if (sum > target)
    +            {
    +                j--;
    +            }
    +        }
    +        return result;
    +    }
    +};
    + +

    Analysis

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

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

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

    +

    简单过程

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

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

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

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

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

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

    主从配置

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

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

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

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

    +

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

    +

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

    +

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

    +

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

    +

    安装

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

    +
    sudo docker info
    + +

    输出以下信息

    +

    简单运行

    然后再来运行个 hello world 呗,

    +
    sudo docker run hello-world
    + +

    输出了这些

    +

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

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

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

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

    要有个虚拟终端,

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

    镜像

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

    +

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

    +

    +

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

    +

    在容器里看下进程,

    +

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

    +

    查看宿主机上的容器

    sudo docker ps -a
    + +

    +

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

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

    sudo docker exec -it [containerID] bash

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

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

    有个别应用用的是这个

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

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

    +
    @SPI("curator")
    +public interface ZookeeperTransporter {
    +
    +    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    +    ZookeeperClient connect(URL url);
    +
    +}
    +

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

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


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

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

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

    +]]>
    + + Java + Dubbo + + + Java + Dubbo + +
    docker使用中发现的echo命令的一个小技巧及其他 /2020/03/29/echo%E5%91%BD%E4%BB%A4%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E6%8A%80%E5%B7%A7/ @@ -4855,36 +4828,6 @@ EXPOSE 80 - - Number of 1 Bits - /2015/03/11/Number-Of-1-Bits/ - Number of 1 Bits

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

    - -

    分析

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

    -
    -

    code

    int hammingWeight(uint32_t n) {
    -        const uint32_t m1  = 0x55555555; //binary: 0101...  
    -        const uint32_t m2  = 0x33333333; //binary: 00110011..  
    -        const uint32_t m4  = 0x0f0f0f0f; //binary:  4 zeros,  4 ones ...  
    -        const uint32_t m8  = 0x00ff00ff; //binary:  8 zeros,  8 ones ...  
    -        const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...  
    -        
    -        n = (n & m1 ) + ((n >>  1) & m1 ); //put count of each  2 bits into those  2 bits   
    -        n = (n & m2 ) + ((n >>  2) & m2 ); //put count of each  4 bits into those  4 bits   
    -        n = (n & m4 ) + ((n >>  4) & m4 ); //put count of each  8 bits into those  8 bits   
    -        n = (n & m8 ) + ((n >>  8) & m8 ); //put count of each 16 bits into those 16 bits   
    -        n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits   
    -        return n; 
    -
    -}
    ]]>
    - - leetcode - - - leetcode - c++ - -
    gogs使用webhook部署react单页应用 /2020/02/22/gogs%E4%BD%BF%E7%94%A8webhook%E9%83%A8%E7%BD%B2react%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8/ @@ -4985,52 +4928,77 @@ myusername ALL = this original tweet by Max Howell:

    -
    -

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

    -
    -
    /**
    - * Definition for a binary tree node.
    - * struct TreeNode {
    - *     int val;
    - *     TreeNode *left;
    - *     TreeNode *right;
    - *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    - * };
    - */
    -class Solution {
    -public:
    -    TreeNode* invertTree(TreeNode* root) {
    -        if(root == NULL) return root;
    -        TreeNode* temp;
    -        temp = invertTree(root->left);
    -        root->left = invertTree(root->right);
    -        root->right = temp;
    -        return root;
    -    }
    -};
    ]]> + Leetcode 885 螺旋矩阵 III ( Spiral Matrix III *Medium* ) 题解分析 + /2022/08/23/Leetcode-885-%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5-III-Spiral-Matrix-III-Medium-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ + 题目介绍

    You start at the cell (rStart, cStart) of an rows x cols grid facing east. The northwest corner is at the first row and column in the grid, and the southeast corner is at the last row and column.

    +

    You will walk in a clockwise spiral shape to visit every position in this grid. Whenever you move outside the grid’s boundary, we continue our walk outside the grid (but may return to the grid boundary later.). Eventually, we reach all rows * cols spaces of the grid.

    +

    Return an array of coordinates representing the positions of the grid in the order you visited them.

    +

    Example 1:

    +

    Input: rows = 1, cols = 4, rStart = 0, cStart = 0
    Output: [[0,0],[0,1],[0,2],[0,3]]

    +
    +

    Example 2:

    +

    Input: rows = 5, cols = 6, rStart = 1, cStart = 4
    Output: [[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]]

    +
    +

    Constraints:

    +
      +
    • 1 <= rows, cols <= 100
    • +
    • 0 <= rStart < rows
    • +
    • 0 <= cStart < cols
    • +
    +

    简析

    这个题主要是要相同螺旋矩阵的转变方向的边界判断,已经相同步长会行进两次这个规律,写代码倒不复杂

    +

    代码

    public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
    +        int size = rows * cols;
    +        int x = rStart, y = cStart;
    +        // 返回的二维矩阵
    +        int[][] matrix = new int[size][2];
    +        // 传入的参数就是入口第一个
    +        matrix[0][0] = rStart;
    +        matrix[0][1] = cStart;
    +        // 作为数量
    +        int z = 1;
    +        // 步进,1,1,2,2,3,3,4 ... 螺旋矩阵的增长
    +        int a = 1;
    +        // 方向 1 表示右,2 表示下,3 表示左,4 表示上
    +        int dir = 1;
    +        while (z < size) {
    +            for (int i = 0; i < 2; i++) {
    +                for (int j= 0; j < a; j++) {
    +                    // 处理方向
    +                    if (dir % 4 == 1) {
    +                        y++;
    +                    } else if (dir % 4 == 2) {
    +                        x++;
    +                    } else if (dir % 4 == 3) {
    +                        y--;
    +                    } else {
    +                        x--;
    +                    }
    +                    // 如果在实际矩阵内
    +                    if (x < rows && y < cols && x >= 0 && y >= 0) {
    +                        matrix[z][0] = x;
    +                        matrix[z][1] = y;
    +                        z++;
    +                    }
    +                }
    +                // 转变方向
    +                dir++;
    +            }
    +            // 步进++
    +            a++;
    +        }
    +        return matrix;
    +    }
    + +

    结果

    +]]>
    + Java leetcode leetcode - c++ + java + 题解
    @@ -5084,6 +5052,45 @@ public: 博客,文章 + + Redis_分布式锁 + /2019/12/10/Redis-Part-1/ + 今天看了一下 redis 分布式锁 redlock 的实现,简单记录下,

    +

    加锁

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

    +

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

    +

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

    +
    SET resource_name my_random_value NX PX 30000
    +

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

    +

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

    +

    解锁

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

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

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

    +

    多节点

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

    +
      +
    • 获取当前时间(毫秒数)。
    • +
    • 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
    • +
    • 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
    • +
    • 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
    • +
    • 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。
      释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。这里为什么要向所有的节点发送释放锁的操作呢,这里是因为有部分的节点的失败原因可能是加锁时阻塞,加锁成功的结果没有及时返回,所以为了防止这种情况还是需要向所有发起这个释放锁的操作。
      初步记录就先到这。
    • +
    +]]>
    + + Redis + Distributed Lock + C + Redis + + + C + Redis + Distributed Lock + 分布式锁 + +
    mybatis 的 $ 和 # 是有啥区别 /2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/ @@ -5179,45 +5186,6 @@ public class DynamicSqlSource implements SqlSource { Sql注入 - - Redis_分布式锁 - /2019/12/10/Redis-Part-1/ - 今天看了一下 redis 分布式锁 redlock 的实现,简单记录下,

    -

    加锁

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

    -

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

    -

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

    -
    SET resource_name my_random_value NX PX 30000
    -

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

    -

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

    -

    解锁

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

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

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

    -

    多节点

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

    -
      -
    • 获取当前时间(毫秒数)。
    • -
    • 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
    • -
    • 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
    • -
    • 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
    • -
    • 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。
      释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。这里为什么要向所有的节点发送释放锁的操作呢,这里是因为有部分的节点的失败原因可能是加锁时阻塞,加锁成功的结果没有及时返回,所以为了防止这种情况还是需要向所有发起这个释放锁的操作。
      初步记录就先到这。
    • -
    -]]>
    - - Redis - Distributed Lock - C - Redis - - - C - Redis - Distributed Lock - 分布式锁 - -
    mybatis 的缓存是怎么回事 /2020/10/03/mybatis-%E7%9A%84%E7%BC%93%E5%AD%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/ @@ -5462,6 +5430,44 @@ public class DynamicSqlSource implements SqlSource { 缓存 + + binary-watch + /2016/09/29/binary-watch/ + problem

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

    +

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

    +

    +

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

    +

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

    +

    Example:

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

    Note:

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

    题解

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

    +

    Code

    class Solution {
    +public:
    +    vector<string> readBinaryWatch(int num) {
    +        vector<string> res;
    +        for (int h = 0; h < 12; ++h) {
    +            for (int m = 0; m < 60; ++m) {
    +                if (bitset<10>((h << 6) + m).count() == num) {
    +                    res.push_back(to_string(h) + (m < 10 ? ":0" : ":") + to_string(m));
    +                }
    +            }
    +        }
    +        return res;
    +    }
    +};
    ]]>
    + + leetcode + + + leetcode + c++ + +
    openresty /2019/06/18/openresty/ @@ -5577,55 +5583,48 @@ int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, in - php-abstract-class-and-interface - /2016/11/10/php-abstract-class-and-interface/ - PHP抽象类和接口
      -
    • 抽象类与接口
    • -
    • 抽象类内可以包含非抽象函数,即可实现函数
    • -
    • 抽象类内必须包含至少一个抽象方法,抽象类和接口均不能实例化
    • -
    • 抽象类可以设置访问级别,接口默认都是public
    • -
    • 类可以实现多个接口但不能继承多个抽象类
    • -
    • 类必须实现抽象类和接口里的抽象方法,不一定要实现抽象类的非抽象方法
    • -
    • 接口内不能定义变量,但是可以定义常量
    • -
    -

    示例代码

    <?php
    -interface int1{
    -    const INTER1 = 111;
    -    function inter1();
    -}
    -interface int2{
    -    const INTER1 = 222;
    -    function inter2();
    -}
    -abstract class abst1{
    -    public function abstr1(){
    -        echo 1111;
    -    }
    -    abstract function abstra1(){
    -        echo 'ahahahha';
    -    }
    -}
    -abstract class abst2{
    -    public function abstr2(){
    -        echo 1111;
    -    }
    -    abstract function abstra2();
    -}
    -class normal1 extends abst1{
    -    protected function abstr2(){
    -        echo 222;
    -    }
    -}
    + nginx 日志小记 + /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ + nginx 默认的日志有特定的格式,我们也可以自定义,

    +

    默认的格式是预定义的 combined

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

    result

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

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

    +
    Syntax:	access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
    +        access_log off;
    +Default: access_log logs/access.log combined;
    +Context: http, server, location, if in location, limit_except
    -Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
    -]]>
    +

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

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

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

    +

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

    +

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

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

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

    +

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

    +]]>
    - php + nginx - php + nginx + 日志
    @@ -5654,11 +5653,119 @@ Starting node rabbit@rabbit2 Linux中的CopyOnWrite

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

    +

    CopyOnWrite的好处:

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

    +

    Redis中的CopyOnWrite

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

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

    示例代码

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

    result

    PHP Fatal error:  Abstract function abst1::abstra1() cannot contain body in new.php on line 17
    +
    +Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
    +]]>
    + + php + + + php + +
    + + redis数据结构介绍三-第三部分 整数集合 + /2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/ + redis中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

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

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

    +
    +

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

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

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

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

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

    -

    Linux中的CopyOnWrite

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

    -

    CopyOnWrite的好处:

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

    -

    Redis中的CopyOnWrite

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

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

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

    主体结构就是这个 redisObject,

    +
      +
    • type: 字段表示对象的类型,它对应的就是 redis 的对外暴露的,或者说用户可以使用的五种类型,OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
    • +
    • encoding: 字段表示这个对象在 redis 内部的编码方式,由OBJ_ENCODING_开头的 11 种
    • +
    • lru: 做LRU替换算法用,占24个bit
    • +
    • refcount: 引用计数。它允许robj对象在某些情况下被共享。
    • +
    • ptr: 指向底层实现数据结构的指针
      当 type 是 OBJ_STRING 时,表示类型是个 string,它的编码方式 encoding 可能有 OBJ_ENCODING_RAW,OBJ_ENCODING_INT,OBJ_ENCODING_EMBSTR 三种
      当 type 是 OBJ_LIST 时,表示类型是 list,它的编码方式 encoding 是 OBJ_ENCODING_QUICKLIST,对于早一些的版本,2.2这种可能还会使用 OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_LINKEDLIST
      当 type 是 OBJ_SET 时,是个集合,但是得看具体元素的类型,有可能使用整数集合,OBJ_ENCODING_INTSET, 如果元素不全是整型或者数量超过一定限制,那么编码就是 OBJ_ENCODING_HT hash table 了
      当 type 是 OBJ_ZSET 时,是个有序集合,它底层有可能使用的是 OBJ_ENCODING_ZIPLIST 或者 OBJ_ENCODING_SKIPLIST
      当 type 是 OBJ_HASH 时,一开始也是 OBJ_ENCODING_ZIPLIST,然后当数据量大于 hash_max_ziplist_entries 时会转成 OBJ_ENCODING_HT
    • +
    ]]>
    - redis + Redis + 数据结构 + C + 源码 + Redis redis + 数据结构 + 源码
    - redis数据结构介绍-第一部分 SDS,链表,字典 - /2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ - redis是现在服务端很常用的缓存中间件,其实原来还有memcache之类的竞品,但是现在貌似 redis 快一统江湖,这里当然不是在吹,只是个人角度的一个感觉,不权威只是主观感觉。
    redis 主要有五种数据结构,StringsListsSetsHashesSorted Sets,这五种数据结构先简单介绍下,Strings类型的其实就是我们最常用的 key-value,实际开发中也会用的最多;Lists是列表,这个有些会用来做队列,因为 redis 目前常用的版本支持丰富的列表操作;还有是Sets集合,这个主要的特点就是集合中元素不重复,可以用在有这类需求的场景里;Hashes是叫散列,类似于 Python 中的字典结构;还有就是Sorted Sets这个是个有序集合;一眼看这些其实没啥特别的,除了最后这个有序集合,不过去了解背后的实现方式还是比较有意思的。

    -

    SDS 简单动态字符串

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

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

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

    -

    链表

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

    -
    typedef struct listNode {
    -    // 前置节点
    -    struct listNode *prev;
    -    // 后置节点
    -    struct listNode *next;
    -    // 值
    -    void *value;
    -} listNode;
    -
    -typedef struct list {
    -    // 链表表头
    -    listNode *head;
    -    // 当前节点,也可以说是最后节点
    -    listNode *tail;
    -    // 节点复制函数
    -    void *(*dup)(void *ptr);
    -    // 节点值释放函数
    -    void (*free)(void *ptr);
    -    // 节点值比较函数
    -    int (*match)(void *ptr, void *key);
    -    // 链表包含的节点数量
    -    unsigned int len;
    -} list;
    -

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

    -

    字典

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

    -
    typedef struct dictEntry {
    -    void *key;
    -    void *val;
    -    struct dictEntry *next;
    -} dictEntry;
    -
    -typedef struct dictType {
    -    unsigned int (*hashFunction)(const void *key);
    -    void *(*keyDup)(void *privdata, const void *key);
    -    void *(*valDup)(void *privdata, const void *obj);
    -    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    -    void (*keyDestructor)(void *privdata, void *key);
    -    void (*valDestructor)(void *privdata, void *obj);
    -} dictType;
    -
    -/* This is our hash table structure. Every dictionary has two of this as we
    - * implement incremental rehashing, for the old to the new table. */
    -typedef struct dictht {
    -    dictEntry **table;
    -    unsigned long size;
    -    unsigned long sizemask;
    -    unsigned long used;
    -} dictht;
    -
    -typedef struct dict {
    -    dictType *type;
    -    void *privdata;
    -    dictht ht[2];
    -    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    -    int iterators; /* number of iterators currently running */
    -} dict;
    -

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

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

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

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

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

    -
    -

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

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

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

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

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

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

    主体结构就是这个 redisObject,

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

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

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

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

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

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

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

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

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

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

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

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

    -]]>
    - - redis - - - redis - 淘汰策略 - 应用 - Evict - -
    redis数据结构介绍四-第四部分 压缩表 /2020/01/19/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%9B%9B/ @@ -6292,6 +6153,110 @@ REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
    [0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
     |**zlbytes***|  |***zltail***|  |*zllen*|  |entry1 entry2|  |zlend|

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

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

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

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

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

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

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

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

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

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

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

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

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

    ]]>
    Redis @@ -6735,44 +6700,6 @@ 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/ @@ -7303,52 +7230,154 @@ uint8_t LFULogIncr(uint8_t counter) { - rust学习笔记-所有权二 - /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ - 这里需要说道函数和返回值了
    可以看书上的这个例子

    对于这种情况,当进入函数内部时,会把传入的变量的所有权转移进函数内部,如果最后还是要返回该变量,但是如果此时还要返回别的计算结果,就可能需要笨拙地使用元组

    -

    引用

    此时我们就可以用引用来解决这个问题

    -
    fn main() {
    -    let s1 = String::from("hello");
    -    let len = calculate_length(&s1);
    +    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
    - println!("The length of '{}' is {}", s1, len); -} -fn calculate_length(s: &String) -> usize { - s.len() -}
    -

    这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值

    由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃

    -

    可变引用

    而当我们尝试对引用的字符串进行修改时

    -
    fn main() {
    -    let s1 = String::from("hello");
    -    change(&s1);
    -}
    -fn change(s: &String) {
    -    s.push_str(", world");
    -}
    -

    就会有以下报错,

    其实也很容易发现,毕竟没有 mut 指出这是可变引用,同时需要将 s1 改成 mut 可变的

    +

    可以配置这个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 s1 = String::from("hello");
    -    change(&mut s1);
    -}
    +    let mut s = String::from("hello world");
     
    +    let word = first_word(&s);
     
    -fn change(s: &mut String) {
    -    s.push_str(", world");
    -}
    -

    再看一个例子

    -
    fn main() {
    -    let mut s1 = String::from("hello");
    -    let r1 = &mut s1;
    -    let r2 = &mut s1;
    -}
    -

    这个例子在书里是会报错的,因为同时存在一个以上的可变引用,但是在我运行的版本里前面这段没有报错,只有当我真的要去更改的时候

    -
    fn main() {
    -    let mut s1 = String::from("hello");
    -    let mut r1 = &mut s1;
    -    let mut r2 = &mut s1;
    -    change(&mut r1);
    -    change(&mut r2);
    -}
    +    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/ + 这里需要说道函数和返回值了
    可以看书上的这个例子

    对于这种情况,当进入函数内部时,会把传入的变量的所有权转移进函数内部,如果最后还是要返回该变量,但是如果此时还要返回别的计算结果,就可能需要笨拙地使用元组

    +

    引用

    此时我们就可以用引用来解决这个问题

    +
    fn main() {
    +    let s1 = String::from("hello");
    +    let len = calculate_length(&s1);
    +
    +    println!("The length of '{}' is {}", s1, len);
    +}
    +fn calculate_length(s: &String) -> usize {
    +    s.len()
    +}
    +

    这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值

    由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃

    +

    可变引用

    而当我们尝试对引用的字符串进行修改时

    +
    fn main() {
    +    let s1 = String::from("hello");
    +    change(&s1);
    +}
    +fn change(s: &String) {
    +    s.push_str(", world");
    +}
    +

    就会有以下报错,

    其实也很容易发现,毕竟没有 mut 指出这是可变引用,同时需要将 s1 改成 mut 可变的

    +
    fn main() {
    +    let mut s1 = String::from("hello");
    +    change(&mut s1);
    +}
    +
    +
    +fn change(s: &mut String) {
    +    s.push_str(", world");
    +}
    +

    再看一个例子

    +
    fn main() {
    +    let mut s1 = String::from("hello");
    +    let r1 = &mut s1;
    +    let r2 = &mut s1;
    +}
    +

    这个例子在书里是会报错的,因为同时存在一个以上的可变引用,但是在我运行的版本里前面这段没有报错,只有当我真的要去更改的时候

    +
    fn main() {
    +    let mut s1 = String::from("hello");
    +    let mut r1 = &mut s1;
    +    let mut r2 = &mut s1;
    +    change(&mut r1);
    +    change(&mut r2);
    +}
     
     
     fn change(s: &mut String) {
    @@ -7426,70 +7455,6 @@ uint8_t LFULogIncr(uint8_t counter) {
             新语言
           
       
    -  
    -    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 - 所有权 - 内存分布 - 新语言 - 可变引用 - 不可变引用 - 切片 - -
    spark-little-tips /2017/03/28/spark-little-tips/ @@ -7514,35 +7479,6 @@ 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/ @@ -7643,6 +7579,35 @@ public: Spring Event + + summary-ranges-228 + /2016/10/12/summary-ranges-228/ + problem

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

    +

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

    +

    题解

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

    +

    Code

    class Solution {
    +public:
    +    vector<string> summaryRanges(vector<int>& nums) {
    +        int i = 0, j = 1, n;
    +        vector<string> res;
    +        n = nums.size();
    +        while(i < n){
    +            j = 1;
    +            while(j < n && nums[i+j] - nums[i] == j) j++;
    +            res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
    +            i += j;
    +        }
    +        return res;
    +    }
    +};
    ]]>
    + + leetcode + + + leetcode + c++ + +
    swoole-websocket-test /2016/07/13/swoole-websocket-test/ @@ -7768,6 +7733,22 @@ user3: swoole + + wordpress 忘记密码的一种解决方法 + /2021/12/05/wordpress-%E5%BF%98%E8%AE%B0%E5%AF%86%E7%A0%81%E7%9A%84%E4%B8%80%E7%A7%8D%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/ + 前阵子搭了个 WordPress,但是没怎么用,前两天发现忘了登录密码了,最近不知道是什么情况,chrome 的记住密码跟历史记录感觉有点问题,历史记录丢了不少东西,可能是时间太久了,但是理论上应该有 LRU 这种策略的,有些还比较常用,还有记住密码,因为个人域名都是用子域名分配给各个服务,有些记住了,有些又没记住密码,略蛋疼,所以就找了下这个方案。
    当然这个方案不是最优的,有很多限制,首先就是要能够登陆 WordPress 的数据库,不然这个方法是没用的。
    首先不管用什么方式(别违法)先登陆数据库,选择 WordPress 的数据库,可以看到里面有几个表,我们的目标就是 wp_users 表,用 select 查询看下可以看到有用户的数据,如果是像我这样搭着玩的没有创建其他用户的话应该就只有一个用户,那我们的表里的用户数据就只会有一条,当然多条的话可以通过用户名来找

    然后可能我这个版本是这样,没有装额外的插件,密码只是经过了 MD5 的单向哈希,所以我们可以设定一个新密码,然后用 MD5 编码后直接更新进去

    +
    UPDATE wp_users SET user_pass = MD5('123456') WHERE ID = 1;
    + +

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

    +]]>
    + + 小技巧 + + + WordPress + 小技巧 + +
    《垃圾回收算法手册读书》笔记之整理算法 /2021/03/07/%E3%80%8A%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%89%8B%E5%86%8C%E8%AF%BB%E4%B9%A6%E3%80%8B%E7%AC%94%E8%AE%B0%E4%B9%8B%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95/ @@ -7791,119 +7772,65 @@ user3: - wordpress 忘记密码的一种解决方法 - /2021/12/05/wordpress-%E5%BF%98%E8%AE%B0%E5%AF%86%E7%A0%81%E7%9A%84%E4%B8%80%E7%A7%8D%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/ - 前阵子搭了个 WordPress,但是没怎么用,前两天发现忘了登录密码了,最近不知道是什么情况,chrome 的记住密码跟历史记录感觉有点问题,历史记录丢了不少东西,可能是时间太久了,但是理论上应该有 LRU 这种策略的,有些还比较常用,还有记住密码,因为个人域名都是用子域名分配给各个服务,有些记住了,有些又没记住密码,略蛋疼,所以就找了下这个方案。
    当然这个方案不是最优的,有很多限制,首先就是要能够登陆 WordPress 的数据库,不然这个方法是没用的。
    首先不管用什么方式(别违法)先登陆数据库,选择 WordPress 的数据库,可以看到里面有几个表,我们的目标就是 wp_users 表,用 select 查询看下可以看到有用户的数据,如果是像我这样搭着玩的没有创建其他用户的话应该就只有一个用户,那我们的表里的用户数据就只会有一条,当然多条的话可以通过用户名来找

    然后可能我这个版本是这样,没有装额外的插件,密码只是经过了 MD5 的单向哈希,所以我们可以设定一个新密码,然后用 MD5 编码后直接更新进去

    -
    UPDATE wp_users SET user_pass = MD5('123456') WHERE ID = 1;
    - -

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

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

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

    有个别应用用的是这个

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

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

    -
    @SPI("curator")
    -public interface ZookeeperTransporter {
    -
    -    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    -    ZookeeperClient connect(URL url);
    -
    -}
    -

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

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


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

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

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

    -]]>
    + invert-binary-tree + /2015/06/22/invert-binary-tree/ + Invert a binary tree

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

    to

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

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

    +
    +

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

    +
    +
    /**
    + * Definition for a binary tree node.
    + * struct TreeNode {
    + *     int val;
    + *     TreeNode *left;
    + *     TreeNode *right;
    + *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    + * };
    + */
    +class Solution {
    +public:
    +    TreeNode* invertTree(TreeNode* root) {
    +        if(root == NULL) return root;
    +        TreeNode* temp;
    +        temp = invertTree(root->left);
    +        root->left = invertTree(root->right);
    +        root->right = temp;
    +        return root;
    +    }
    +};
    ]]>
    - Java - Dubbo + leetcode - Java - Dubbo + leetcode + c++
    - nginx 日志小记 - /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ - nginx 默认的日志有特定的格式,我们也可以自定义,

    -

    默认的格式是预定义的 combined

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

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

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

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

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

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

    -

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

    -

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

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

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

    -

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

    + 上次的其他 外行聊国足 + /2022/03/06/%E4%B8%8A%E6%AC%A1%E7%9A%84%E5%85%B6%E4%BB%96-%E5%A4%96%E8%A1%8C%E8%81%8A%E5%9B%BD%E8%B6%B3/ + 上次本来想在换车牌后面聊下这个话题,为啥要聊这个话题呢,也很简单,在地铁上看到一对猜测是情侣或者比较关系好的男女同学在聊,因为是这位男同学是大学学的工科,然后自己爱好设计绘画相关的,可能还以此赚了点钱,在地铁上讨论男的要不要好好努力把大学课程完成好,大致的观点是没必要,本来就不适合,这一段我就不说了,恋爱人的嘴,信你个鬼。
    后面男的说在家里又跟他爹吵了关于男足的,估计是那次输了越南,实话说我不是个足球迷,对各方面技术相关也不熟,只是对包括这个人的解释和网上一些观点的看法,纯主观,这次地铁上这位说的大概意思是足球这个训练什么的很难的,要想赢越南也很难的,不是我们能嘴炮的;在网上看到一个赞同数很多的一个回答,说什么中国是个体育弱国,但是由于有一些乒乓球,跳水等小众项目比较厉害,让民众给误解了,首先我先来反驳下这个偷换概念的观点,第一所谓的体育弱国,跟我们觉得足球不应该这么差没半毛钱关系,因为体育弱国,我们的足球本来就不是顶尖的,也并不是去跟顶尖的球队去争,以足球为例,跟巴西,阿根廷,英国,德国,西班牙,意大利,法国这些足球强国,去比较,我相信没有一个足球迷会这么去做对比,因为我们足球历史最高排名是 1998 年的 37 名,最差是 100 名,把能数出来的强队都数完,估计都还不会到 37,所以根本没有跟强队去做对比,第二体育弱国,我们的体育投入是在逐年降低吗,我们是因战乱没法好好训练踢球?还是这帮傻逼就不争气,前面也说了我们足球世界排名最高 37,最低 100,那么前阵子我们输的越南是第几,目前我们的排名 77 名,越南 92 名,看明白了么,轮排名我们都不至于输越南,然后就是这个排名,这也是我想回应那位地铁上的兄弟,我觉得除了造核弹这种高精尖技术,绝大部分包含足球这类运动,遵循类二八原则,比如满分是 100 分,从 80 提到 90 分或者 90 分提到 100 分非常难,30 分提到 40 分,50 分提到 60 分我觉得都是可以凭后天努力达成的,基本不受天赋限制,这里可以以篮球来类比下,相对足球的确篮球没有那么火,或者行业市值没法比,但是也算是相对大众了,中国在篮球方面相对比较好一点,在 08 年奥运会冲进过八强,那也不是唯一的巅峰,但是我说这个其实是想说明两方面的事情,第一,像篮球一样,状态是有起起伏伏,排名也会变动,但是我觉得至少能维持一个相对稳定的总体排名和持平或者上升的趋势,这恰恰是我们这种所谓的“体育弱国”应该走的路线,第二就是去支持我的类二八原则的,可以看到我们的篮球这两年也很垃圾,排名跌到 29 了,那问题我觉得跟足球是一样的,就是不能脚踏实地,如斯科拉说的,中国篮球太缺少竞争,打得好不好都是这些人打,打输了还是照样拿钱,相对足球,篮球的技术我还是懂一些的,对比 08 年的中国男篮,的确像姚明跟王治郅这样的天赋型+努力型球员少了以后竞争力下降在所难免,但是去对比下基本功,传球,投篮,罚球稳定性,也完全不是一个水平的,这些就是我说的,可以通过努力训练拿 80 分的,只要拿到 80 分,甚至只要拿到 60 分,我觉得应该就还算对得起球迷了,就像 NBA 里球队也会有核心球员的更替,战绩起起伏伏,但是基本功这东西,防守积极性,我觉得不随核心球员的变化而变化,就像姚明这样的天赋,其实他应该还有一些先天缺陷,大脚趾较长等,但是他从 CBA 到 NBA,在 NBA 适应并且打成顶尖中锋,离不开刻苦训练,任何的成功都不是纯天赋的,必须要付出足够的努力。
    说回足球,如果像前面那么洗地(体育弱国),那能给我维持住一个稳定的排名我也能接受,问题是我们的经济物质资源比 2000 年前应该有了质的变化,身体素质也越来越好,即使是体育弱国,这么继续走下坡路,半死不活的,不觉得是打了自己的脸么。足球也需要基本功,基本的体能,力量这些,看看现在这些国足运动员的体型,对比下女足,说实话,如果男足这些运动员都练得不错的体脂率,耐力等,成绩即使不好,也不会比现在更差。
    纯主观吐槽,勿喷。

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

    + 分享记录一下一个 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
    - 一个 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,不需要额外处理了。

    + 分享记录一下一个 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/ + scp 是个在服务器之间拷贝文件的一个常用命令,有时候有个场景是比如我们需要拷贝一些带有共同前缀的文件,但是有一个问题是比如我们有使用 zsh 的话,会出现一个报错,

    +
    zsh: no matches found: root@100.100.100.100://root/prefix*
    +

    这里就比较奇怪了,这个前缀的文件肯定是有的,这里其实是由于 zsh 会对 * 进行展开,这个可以在例如 ls 命令在使用中就可以发现 zsh 有这个特性
    需要使用双引号或单引号将路径包起来或者在*之前加反斜杠\来阻止对*展开和转义

    +
    scp root@100.100.100.100://root/prefix* .
    +

    通过使用双引号"进行转义

    +
    scp root@100.100.100.100:"//root/prefix*" .
    +

    或者可以将 shell 从 zsh 切换成 bash

    ]]>
    - nginx + shell + 小技巧 - nginx + scp
    @@ -8189,36 +8106,23 @@ access_log /path/to/access.log combined - 《长安的荔枝》读后感 - /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/ - 断断续续地看完了马伯庸老师的《长安的荔枝》,一开始是看这本书在排行榜排得很高,又是马伯庸的,之前看过他的《古董局中局》,还是很有意思的,而且正好是比较短的,不过前后也拖了蛮久才看完,看完后读了下马老师自己写的后记,就特别有感触。
    整个故事是围绕一个上林署监事李善德被委任一项给贵妃送荔枝的差事展开,“长安回望绣成堆,山顶千门次第开,一骑红尘妃子笑,无人知是荔枝来”,以前没细究过这个送荔枝的过程,但是以以前的运输速度和保鲜条件,感觉也不是太现实,所以主人公一开始就以为只是像以往一样是送荔枝干这种,能比较方便运输,不容易变质的,结果发现其实是同僚在坑他,这次是要在贵妃生辰的时候给贵妃送来新鲜的岭南荔枝,用比较时兴的词来说,这就是个送命题啊,鲜荔枝一日色变,两日香变,三日味变,同僚的还有杜甫跟韩承,都觉得老李可以直接写休书了,保全家人,不然就是全家送命,李善德也觉得基本算是判刑了,而且其实是这事被转了几次,最后到老李所在的上林署,主管为了骗他接下这个活还特意在文书上把荔枝鲜的“鲜”字贴住,那会叫做“贴黄”,变成了荔枝“煎”,所以说官场险恶,大家都想把这烫手山芋丢出去,结果丢到了我们老实的老李头上,但是从接到这个通知到贵妃的生辰六月初一还有挺长的时间,其实这个活虽然送命,但是在前期这个“荔枝使”也基本就是类似带着尚方宝剑,御赐黄马褂的职位,随便申请经费,不必像常规的部门费用需要定预算,申请后再层层审批,而是特事特批特办的耍赖做法,所以在这段时间是能够潇洒挥霍一下的。其实可以好好地捞一波给妻女,然后写下和离,在自己死后能让她们过的好一些,但最后还是在杜甫的一番劝导下做出了尝试一番的决定,因为也没其他办法,既是退无可退,何不向前拼死一搏,其实说到这,我觉得看这本书感觉有所收获的第一点,有时候总觉得事情没戏了,想躺平放弃了,但是这样其实这个结果是不会变好的,尝试努力,拼尽全力搏一搏,说不定会有所改观,至少不会变更坏了。

    + 在老丈人家的小工记四 + /2020/09/26/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E5%9B%9B/ + 小工记四

    第四周去的时候让我们去了现在在住的房子里,去三楼整理东西了,蛮多的东西需要收拾整理,有些需要丢一下,以前往往是把不太要用的东西就放三楼了,但是后面就不会再去收拾整理,LD 跟丈母娘负责收拾,我不太知道哪些还要的,哪些不要了,而且本来也不擅长这种收拾🤦‍♂️,然后就变成她们收拾出来废纸箱,我负责拆掉,压平,这时候终于觉得体重还算是有点作用,总体来说这个事情我其实也不擅长,不擅长的主要是捆起来,可能我总是小题大做,因为纸箱大小不一,如果不做一下分类,然后把大的折小一些的话,直接绑起来,容易拎起来就散掉了,而且一些鞋盒子这种小件的纸盒会比较薄,冰箱这种大件的比较厚,厚的比较不容易变形,需要大力踩踏,而且扎的时候需要用体重压住捆实了之后那样子才是真的捆实的,不然待会又是松松垮垮容易滑出来散架,因为压住了捆好后,下来了之后箱子就会弹开了把绳子崩紧实,感觉又是掌握到生活小技巧了😃,我这里其实比较单调无聊,然后 LD 那可以说非常厉害了,一共理出来 11 把旧电扇,还有好多没用过全新的不锈钢脸盆大大小小的,感觉比店里在卖的还多,还有是有比较多小时候的东西,特别多小时候的衣服,其实这种对我来说最难了,可能需要读一下断舍离,蛮多东西都舍不得扔,但是其实是没啥用了,然后还占地方,这天应该算是比较轻松的一天了,上午主要是把收拾出来要的和不要的搬下楼,然后下午要去把纸板给卖掉。中午还是去小快餐店吃的,在住的家里理东西还有个好处就是中午吃完饭可以小憩一下,因为我个人是非常依赖午休的,不然下午完全没精神,而且心态也会比较烦躁,一方面是客观的的确比较疲惫,另一方面应该主观心理作用也有点影响,就像上班的时候也是觉得不午睡就会很难受,心理作用也有一点,不过总之能睡还是睡一会,真的没办法就心态好点,吃完午饭之后我们就推着小平板车去收废品的地方卖掉了上午我收拾捆起来的纸板,好像卖了一百多,都是直接过地磅了,不用一捆一捆地称,不过有个小插曲,那里另外一个大爷在倒他的三轮车的时候撞了我一下,还好车速慢,屁股上肉垫后,接下来就比较麻烦了,是LD 她们两姐妹从小到大的书,也要去卖掉,小平板车就载不下了,而且着实也不太好推,轮子不太平,导致推着很累,书有好多箱,本来是想去亲戚家借电动三轮车,因为不会开摩托的那种,摩托的那种 LD 邻居家就有,可是到了那发现那个也是很大,而且刹车是用脚踩的那种,俺爹不太放心,就说第二天周日他有空会帮忙去载了卖掉的,然后比较搞笑的来了,丈母娘看错了时间,以为已经快五点了,就让我们顺便在车里带点东西去在修的房子,放到那边三楼去,到了那还跟老丈人说已经这么迟了要赶紧去菜场买菜了,结果我们回来以后才发现看错了一个小时🤦‍♂️。
    前面可以没提,前三周去的我们一般就周六去一天,然后周日因为要早点回杭州,而且可能想让我们周日能休息下,但是这周就因为周日的时候我爸要去帮忙载书,然后 LD 姐姐也会过来收拾东西,我们周日就又去整理收拾了,周日由于俺爹去的很早,我过去的时候书已经木有了,主要是去收拾东西了,把一些有用没用的继续整理,基本上三楼的就处理完毕了,舒了一大口气,毕竟让丈母娘一个人收拾实在是太累了,但是要扔掉的衣服比较棘手,附近知道的青蛙回收桶被推倒了,其他地方也不知道哪里有,我们就先载了一些东西去在修的房子那,然后去找青蛙桶,结果一个小区可以进,但是已经满了,另一个不让进,后来只能让 LD 姐姐带去她们小区扔了,塞了满满一车。因为要赶回杭州的车就没有等我爸一起回来,他还在那帮忙搞卫生间的墙缝。
    虽然这两天不太热,活也不算很吃力,不过我这个体重和易出汗的体质,还是让短袖不知道湿透了多少次,灌了好多水和冰红茶(下午能提提神),回来周一早上称体重也比较喜人,差一点就达到阶段目标,可以想想去哪里吃之前想好的烤肉跟火锅了(估计吃完立马回到解放前)。

    ]]>
    - 读后感 生活 + 运动 + 跑步 + 干活 生活 - 读后感 - -
    - - 分享记录一下一个 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 + 小技巧 + 运动 + 减肥 + 跑步 + 干活
    @@ -8243,179 +8147,172 @@ access_log /path/to/access.log combined - 我是如何走上跑步这条不归路的 - /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 精神😄)。

    -

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

    -

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

    -]]>
    - - 生活 - 运动 - 跑步 - - - 生活 - 运动 - 减肥 - 跑步 - -
    - - 分享记录一下一个 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/ - scp 是个在服务器之间拷贝文件的一个常用命令,有时候有个场景是比如我们需要拷贝一些带有共同前缀的文件,但是有一个问题是比如我们有使用 zsh 的话,会出现一个报错,

    -
    zsh: no matches found: root@100.100.100.100://root/prefix*
    -

    这里就比较奇怪了,这个前缀的文件肯定是有的,这里其实是由于 zsh 会对 * 进行展开,这个可以在例如 ls 命令在使用中就可以发现 zsh 有这个特性
    需要使用双引号或单引号将路径包起来或者在*之前加反斜杠\来阻止对*展开和转义

    -
    scp root@100.100.100.100://root/prefix* .
    -

    通过使用双引号"进行转义

    -
    scp root@100.100.100.100:"//root/prefix*" .
    -

    或者可以将 shell 从 zsh 切换成 bash

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

    +

    SDS 简单动态字符串

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

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

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

    +

    链表

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

    +
    typedef struct listNode {
    +    // 前置节点
    +    struct listNode *prev;
    +    // 后置节点
    +    struct listNode *next;
    +    // 值
    +    void *value;
    +} listNode;
    +
    +typedef struct list {
    +    // 链表表头
    +    listNode *head;
    +    // 当前节点,也可以说是最后节点
    +    listNode *tail;
    +    // 节点复制函数
    +    void *(*dup)(void *ptr);
    +    // 节点值释放函数
    +    void (*free)(void *ptr);
    +    // 节点值比较函数
    +    int (*match)(void *ptr, void *key);
    +    // 链表包含的节点数量
    +    unsigned int len;
    +} list;
    +

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

    +

    字典

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

    +
    typedef struct dictEntry {
    +    void *key;
    +    void *val;
    +    struct dictEntry *next;
    +} dictEntry;
    +
    +typedef struct dictType {
    +    unsigned int (*hashFunction)(const void *key);
    +    void *(*keyDup)(void *privdata, const void *key);
    +    void *(*valDup)(void *privdata, const void *obj);
    +    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    +    void (*keyDestructor)(void *privdata, void *key);
    +    void (*valDestructor)(void *privdata, void *obj);
    +} dictType;
    +
    +/* This is our hash table structure. Every dictionary has two of this as we
    + * implement incremental rehashing, for the old to the new table. */
    +typedef struct dictht {
    +    dictEntry **table;
    +    unsigned long size;
    +    unsigned long sizemask;
    +    unsigned long used;
    +} dictht;
    +
    +typedef struct dict {
    +    dictType *type;
    +    void *privdata;
    +    dictht ht[2];
    +    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    +    int iterators; /* number of iterators currently running */
    +} dict;
    +

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

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

    +]]>
    + + Redis + 数据结构 + C + 源码 + Redis - scp + redis + 数据结构 + 源码
    - 在老丈人家的小工记四 - /2020/09/26/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E5%9B%9B/ - 小工记四

    第四周去的时候让我们去了现在在住的房子里,去三楼整理东西了,蛮多的东西需要收拾整理,有些需要丢一下,以前往往是把不太要用的东西就放三楼了,但是后面就不会再去收拾整理,LD 跟丈母娘负责收拾,我不太知道哪些还要的,哪些不要了,而且本来也不擅长这种收拾🤦‍♂️,然后就变成她们收拾出来废纸箱,我负责拆掉,压平,这时候终于觉得体重还算是有点作用,总体来说这个事情我其实也不擅长,不擅长的主要是捆起来,可能我总是小题大做,因为纸箱大小不一,如果不做一下分类,然后把大的折小一些的话,直接绑起来,容易拎起来就散掉了,而且一些鞋盒子这种小件的纸盒会比较薄,冰箱这种大件的比较厚,厚的比较不容易变形,需要大力踩踏,而且扎的时候需要用体重压住捆实了之后那样子才是真的捆实的,不然待会又是松松垮垮容易滑出来散架,因为压住了捆好后,下来了之后箱子就会弹开了把绳子崩紧实,感觉又是掌握到生活小技巧了😃,我这里其实比较单调无聊,然后 LD 那可以说非常厉害了,一共理出来 11 把旧电扇,还有好多没用过全新的不锈钢脸盆大大小小的,感觉比店里在卖的还多,还有是有比较多小时候的东西,特别多小时候的衣服,其实这种对我来说最难了,可能需要读一下断舍离,蛮多东西都舍不得扔,但是其实是没啥用了,然后还占地方,这天应该算是比较轻松的一天了,上午主要是把收拾出来要的和不要的搬下楼,然后下午要去把纸板给卖掉。中午还是去小快餐店吃的,在住的家里理东西还有个好处就是中午吃完饭可以小憩一下,因为我个人是非常依赖午休的,不然下午完全没精神,而且心态也会比较烦躁,一方面是客观的的确比较疲惫,另一方面应该主观心理作用也有点影响,就像上班的时候也是觉得不午睡就会很难受,心理作用也有一点,不过总之能睡还是睡一会,真的没办法就心态好点,吃完午饭之后我们就推着小平板车去收废品的地方卖掉了上午我收拾捆起来的纸板,好像卖了一百多,都是直接过地磅了,不用一捆一捆地称,不过有个小插曲,那里另外一个大爷在倒他的三轮车的时候撞了我一下,还好车速慢,屁股上肉垫后,接下来就比较麻烦了,是LD 她们两姐妹从小到大的书,也要去卖掉,小平板车就载不下了,而且着实也不太好推,轮子不太平,导致推着很累,书有好多箱,本来是想去亲戚家借电动三轮车,因为不会开摩托的那种,摩托的那种 LD 邻居家就有,可是到了那发现那个也是很大,而且刹车是用脚踩的那种,俺爹不太放心,就说第二天周日他有空会帮忙去载了卖掉的,然后比较搞笑的来了,丈母娘看错了时间,以为已经快五点了,就让我们顺便在车里带点东西去在修的房子,放到那边三楼去,到了那还跟老丈人说已经这么迟了要赶紧去菜场买菜了,结果我们回来以后才发现看错了一个小时🤦‍♂️。
    前面可以没提,前三周去的我们一般就周六去一天,然后周日因为要早点回杭州,而且可能想让我们周日能休息下,但是这周就因为周日的时候我爸要去帮忙载书,然后 LD 姐姐也会过来收拾东西,我们周日就又去整理收拾了,周日由于俺爹去的很早,我过去的时候书已经木有了,主要是去收拾东西了,把一些有用没用的继续整理,基本上三楼的就处理完毕了,舒了一大口气,毕竟让丈母娘一个人收拾实在是太累了,但是要扔掉的衣服比较棘手,附近知道的青蛙回收桶被推倒了,其他地方也不知道哪里有,我们就先载了一些东西去在修的房子那,然后去找青蛙桶,结果一个小区可以进,但是已经满了,另一个不让进,后来只能让 LD 姐姐带去她们小区扔了,塞了满满一车。因为要赶回杭州的车就没有等我爸一起回来,他还在那帮忙搞卫生间的墙缝。
    虽然这两天不太热,活也不算很吃力,不过我这个体重和易出汗的体质,还是让短袖不知道湿透了多少次,灌了好多水和冰红茶(下午能提提神),回来周一早上称体重也比较喜人,差一点就达到阶段目标,可以想想去哪里吃之前想好的烤肉跟火锅了(估计吃完立马回到解放前)。

    + 我是如何走上跑步这条不归路的 + /2020/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 精神😄)。

    +

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

    +

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

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

    -

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

    -

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

    -

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

    + 《长安的荔枝》读后感 + /2022/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/ + 断断续续地看完了马伯庸老师的《长安的荔枝》,一开始是看这本书在排行榜排得很高,又是马伯庸的,之前看过他的《古董局中局》,还是很有意思的,而且正好是比较短的,不过前后也拖了蛮久才看完,看完后读了下马老师自己写的后记,就特别有感触。
    整个故事是围绕一个上林署监事李善德被委任一项给贵妃送荔枝的差事展开,“长安回望绣成堆,山顶千门次第开,一骑红尘妃子笑,无人知是荔枝来”,以前没细究过这个送荔枝的过程,但是以以前的运输速度和保鲜条件,感觉也不是太现实,所以主人公一开始就以为只是像以往一样是送荔枝干这种,能比较方便运输,不容易变质的,结果发现其实是同僚在坑他,这次是要在贵妃生辰的时候给贵妃送来新鲜的岭南荔枝,用比较时兴的词来说,这就是个送命题啊,鲜荔枝一日色变,两日香变,三日味变,同僚的还有杜甫跟韩承,都觉得老李可以直接写休书了,保全家人,不然就是全家送命,李善德也觉得基本算是判刑了,而且其实是这事被转了几次,最后到老李所在的上林署,主管为了骗他接下这个活还特意在文书上把荔枝鲜的“鲜”字贴住,那会叫做“贴黄”,变成了荔枝“煎”,所以说官场险恶,大家都想把这烫手山芋丢出去,结果丢到了我们老实的老李头上,但是从接到这个通知到贵妃的生辰六月初一还有挺长的时间,其实这个活虽然送命,但是在前期这个“荔枝使”也基本就是类似带着尚方宝剑,御赐黄马褂的职位,随便申请经费,不必像常规的部门费用需要定预算,申请后再层层审批,而是特事特批特办的耍赖做法,所以在这段时间是能够潇洒挥霍一下的。其实可以好好地捞一波给妻女,然后写下和离,在自己死后能让她们过的好一些,但最后还是在杜甫的一番劝导下做出了尝试一番的决定,因为也没其他办法,既是退无可退,何不向前拼死一搏,其实说到这,我觉得看这本书感觉有所收获的第一点,有时候总觉得事情没戏了,想躺平放弃了,但是这样其实这个结果是不会变好的,尝试努力,拼尽全力搏一搏,说不定会有所改观,至少不会变更坏了。

    ]]>
    + 读后感 生活 生活 - 影评 + 读后感
    - 搬运两个 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 字符是什么?

    + 一个 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,不需要额外处理了。

    ]]>
    - Mysql + nginx - mysql - 字符集 - 编码 - utf8 - utf8mb4 - utf8mb4_0900_ai_ci - utf8mb4_unicode_ci - utf8mb4_general_ci + nginx
    - 是何原因竟让两人深夜奔袭十公里 - /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

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

    ]]>
    生活 生活 + 囤物资
    @@ -9212,174 +9109,26 @@ access_log /path/to/access.log combined - 聊一下 RocketMQ 的消息存储三 - /2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ - ConsumeQueue 其实是定位到一个 topic 下的消息在 CommitLog 下的偏移量,它也是固定大小的

    -
    // ConsumeQueue file size,default is 30W
    -private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
    +    聊一下 RocketMQ 的 NameServer 源码
    +    /2020/07/05/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-NameServer-%E6%BA%90%E7%A0%81/
    +    前面介绍了,nameserver相当于dubbo的注册中心,用与管理broker,broker会在启动的时候注册到nameserver,并且会发送心跳给namaserver,nameserver负责保存活跃的broker,包括master和slave,同时保存topic和topic下的队列,以及filter列表,然后为producer和consumer的请求提供服务。

    +

    启动过程

    public static void main(String[] args) {
    +        main0(args);
    +    }
     
    -public static final int CQ_STORE_UNIT_SIZE = 20;
    + public static NamesrvController main0(String[] args) { -

    所以文件大小是5.7M 左右

    -

    5udpag

    -

    ConsumeQueue 的构建是通过org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService运行后的 doReput 方法,而启动是的 reputFromOffset 则是通过org.apache.rocketmq.store.DefaultMessageStore#start中下面代码设置并启动

    -
    log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
    -                maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
    -            this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
    -            this.reputMessageService.start();
    - -

    看一下 doReput 的逻辑

    -
    private void doReput() {
    -            if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
    -                log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
    -                    this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
    -                this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
    -            }
    -            for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
    -
    -                if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
    -                    && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
    -                    break;
    -                }
    -
    -              // 根据偏移量获取消息
    -                SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
    -                if (result != null) {
    -                    try {
    -                        this.reputFromOffset = result.getStartOffset();
    -
    -                        for (int readSize = 0; readSize < result.getSize() && doNext; ) {
    -                          // 消息校验和转换
    -                            DispatchRequest dispatchRequest =
    -                                DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
    -                            int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
    -
    -                            if (dispatchRequest.isSuccess()) {
    -                                if (size > 0) {
    -                                  // 进行分发处理,包括 ConsumeQueue 和 IndexFile
    -                                    DefaultMessageStore.this.doDispatch(dispatchRequest);
    -
    -                                    if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
    -                                        && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
    -                                        DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
    -                                            dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
    -                                            dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
    -                                            dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
    -                                    }
    -
    -                                    this.reputFromOffset += size;
    -                                    readSize += size;
    -                                    if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
    -                                        DefaultMessageStore.this.storeStatsService
    -                                            .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
    -                                        DefaultMessageStore.this.storeStatsService
    -                                            .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
    -                                            .addAndGet(dispatchRequest.getMsgSize());
    -                                    }
    -                                } else if (size == 0) {
    -                                    this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
    -                                    readSize = result.getSize();
    -                                }
    -                            } else if (!dispatchRequest.isSuccess()) {
    -
    -                                if (size > 0) {
    -                                    log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
    -                                    this.reputFromOffset += size;
    -                                } else {
    -                                    doNext = false;
    -                                    // If user open the dledger pattern or the broker is master node,
    -                                    // it will not ignore the exception and fix the reputFromOffset variable
    -                                    if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
    -                                        DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
    -                                        log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
    -                                            this.reputFromOffset);
    -                                        this.reputFromOffset += result.getSize() - readSize;
    -                                    }
    -                                }
    -                            }
    -                        }
    -                    } finally {
    -                        result.release();
    -                    }
    -                } else {
    -                    doNext = false;
    -                }
    -            }
    -        }
    - -

    分发的逻辑看到这

    -
        class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
    -
    -        @Override
    -        public void dispatch(DispatchRequest request) {
    -            final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
    -            switch (tranType) {
    -                case MessageSysFlag.TRANSACTION_NOT_TYPE:
    -                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
    -                    DefaultMessageStore.this.putMessagePositionInfo(request);
    -                    break;
    -                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    -                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
    -                    break;
    -            }
    -        }
    -    }
    -public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    -        ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    -        cq.putMessagePositionInfoWrapper(dispatchRequest);
    -    }
    - -

    真正存储的是在这

    -
    private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
    -    final long cqOffset) {
    -
    -    if (offset + size <= this.maxPhysicOffset) {
    -        log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
    -        return true;
    -    }
    -
    -    this.byteBufferIndex.flip();
    -    this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
    -    this.byteBufferIndex.putLong(offset);
    -    this.byteBufferIndex.putInt(size);
    -    this.byteBufferIndex.putLong(tagsCode);
    - -

    这里也可以看到 ConsumeQueue 的存储格式,

    -

    AA6Tve

    -

    偏移量,消息大小,跟 tag 的 hashCode

    -]]>
    - - MQ - RocketMQ - 消息队列 - - - MQ - 消息队列 - RocketMQ - - - - 聊一下 RocketMQ 的 NameServer 源码 - /2020/07/05/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84-NameServer-%E6%BA%90%E7%A0%81/ - 前面介绍了,nameserver相当于dubbo的注册中心,用与管理broker,broker会在启动的时候注册到nameserver,并且会发送心跳给namaserver,nameserver负责保存活跃的broker,包括master和slave,同时保存topic和topic下的队列,以及filter列表,然后为producer和consumer的请求提供服务。

    -

    启动过程

    public static void main(String[] args) {
    -        main0(args);
    -    }
    -
    -    public static NamesrvController main0(String[] args) {
    -
    -        try {
    -            NamesrvController controller = createNamesrvController(args);
    -            start(controller);
    -            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
    -            log.info(tip);
    -            System.out.printf("%s%n", tip);
    -            return controller;
    -        } catch (Throwable e) {
    -            e.printStackTrace();
    -            System.exit(-1);
    -        }
    +        try {
    +            NamesrvController controller = createNamesrvController(args);
    +            start(controller);
    +            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
    +            log.info(tip);
    +            System.out.printf("%s%n", tip);
    +            return controller;
    +        } catch (Throwable e) {
    +            e.printStackTrace();
    +            System.exit(-1);
    +        }
     
             return null;
         }
    @@ -9987,40 +9736,261 @@ access_log /path/to/access.log combined
    - 聊一下 RocketMQ 的消息存储二 - /2021/09/12/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%BA%8C/ - CommitLog 结构

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

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

    +

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

    +

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

    +

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

    +]]>
    + + 生活 + + + 生活 + 影评 + +
    + + 聊一下 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);
    -

    kLahwW

    -

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

    -

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

    -
    public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    -        // Set the storage time
    -        msg.setStoreTimestamp(System.currentTimeMillis());
    -        // Set the message body BODY CRC (consider the most appropriate setting
    -        // on the client)
    -        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
    -        // Back to Results
    -        AppendMessageResult result = null;
     
    -        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
     
    -        String topic = msg.getTopic();
    -        int queueId = msg.getQueueId();
    +

    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);
    - final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); - if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE - || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { - // Delay Delivery - if (msg.getDelayTimeLevel() > 0) { - if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) { - msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()); - } +

    I68mFx

    +
    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
    +    perror("kernel lease set signal");
    +    return -1;
    +}
    +/* l_type can be F_RDLCK F_WRLCK */
    +if(fcntl(fd, F_SETLEASE, l_type)){
    +    perror("kernel lease set type");
    +    return -1;
    +}
    +]]> + + MQ + RocketMQ + 消息队列 + + + MQ + 消息队列 + RocketMQ + + + + 聊一下 RocketMQ 的消息存储三 + /2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + ConsumeQueue 其实是定位到一个 topic 下的消息在 CommitLog 下的偏移量,它也是固定大小的

    +
    // ConsumeQueue file size,default is 30W
    +private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
     
    -                topic = ScheduleMessageService.SCHEDULE_TOPIC;
    -                queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
    +public static final int CQ_STORE_UNIT_SIZE = 20;
    + +

    所以文件大小是5.7M 左右

    +

    5udpag

    +

    ConsumeQueue 的构建是通过org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService运行后的 doReput 方法,而启动是的 reputFromOffset 则是通过org.apache.rocketmq.store.DefaultMessageStore#start中下面代码设置并启动

    +
    log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
    +                maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
    +            this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
    +            this.reputMessageService.start();
    + +

    看一下 doReput 的逻辑

    +
    private void doReput() {
    +            if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
    +                log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
    +                    this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
    +                this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
    +            }
    +            for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
    +
    +                if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
    +                    && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
    +                    break;
    +                }
    +
    +              // 根据偏移量获取消息
    +                SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
    +                if (result != null) {
    +                    try {
    +                        this.reputFromOffset = result.getStartOffset();
    +
    +                        for (int readSize = 0; readSize < result.getSize() && doNext; ) {
    +                          // 消息校验和转换
    +                            DispatchRequest dispatchRequest =
    +                                DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
    +                            int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
    +
    +                            if (dispatchRequest.isSuccess()) {
    +                                if (size > 0) {
    +                                  // 进行分发处理,包括 ConsumeQueue 和 IndexFile
    +                                    DefaultMessageStore.this.doDispatch(dispatchRequest);
    +
    +                                    if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
    +                                        && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
    +                                        DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
    +                                            dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
    +                                            dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
    +                                            dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
    +                                    }
    +
    +                                    this.reputFromOffset += size;
    +                                    readSize += size;
    +                                    if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
    +                                        DefaultMessageStore.this.storeStatsService
    +                                            .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
    +                                        DefaultMessageStore.this.storeStatsService
    +                                            .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
    +                                            .addAndGet(dispatchRequest.getMsgSize());
    +                                    }
    +                                } else if (size == 0) {
    +                                    this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
    +                                    readSize = result.getSize();
    +                                }
    +                            } else if (!dispatchRequest.isSuccess()) {
    +
    +                                if (size > 0) {
    +                                    log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
    +                                    this.reputFromOffset += size;
    +                                } else {
    +                                    doNext = false;
    +                                    // If user open the dledger pattern or the broker is master node,
    +                                    // it will not ignore the exception and fix the reputFromOffset variable
    +                                    if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
    +                                        DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
    +                                        log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
    +                                            this.reputFromOffset);
    +                                        this.reputFromOffset += result.getSize() - readSize;
    +                                    }
    +                                }
    +                            }
    +                        }
    +                    } finally {
    +                        result.release();
    +                    }
    +                } else {
    +                    doNext = false;
    +                }
    +            }
    +        }
    + +

    分发的逻辑看到这

    +
        class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
    +
    +        @Override
    +        public void dispatch(DispatchRequest request) {
    +            final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
    +            switch (tranType) {
    +                case MessageSysFlag.TRANSACTION_NOT_TYPE:
    +                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
    +                    DefaultMessageStore.this.putMessagePositionInfo(request);
    +                    break;
    +                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    +                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
    +                    break;
    +            }
    +        }
    +    }
    +public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    +        ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    +        cq.putMessagePositionInfoWrapper(dispatchRequest);
    +    }
    + +

    真正存储的是在这

    +
    private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
    +    final long cqOffset) {
    +
    +    if (offset + size <= this.maxPhysicOffset) {
    +        log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
    +        return true;
    +    }
    +
    +    this.byteBufferIndex.flip();
    +    this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
    +    this.byteBufferIndex.putLong(offset);
    +    this.byteBufferIndex.putInt(size);
    +    this.byteBufferIndex.putLong(tagsCode);
    + +

    这里也可以看到 ConsumeQueue 的存储格式,

    +

    AA6Tve

    +

    偏移量,消息大小,跟 tag 的 hashCode

    +]]>
    + + MQ + RocketMQ + 消息队列 + + + MQ + 消息队列 + RocketMQ + +
    + + 聊一下 RocketMQ 的消息存储二 + /2021/09/12/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%BA%8C/ + CommitLog 结构

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

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

    kLahwW

    +

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

    +

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

    +
    public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    +        // Set the storage time
    +        msg.setStoreTimestamp(System.currentTimeMillis());
    +        // Set the message body BODY CRC (consider the most appropriate setting
    +        // on the client)
    +        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
    +        // Back to Results
    +        AppendMessageResult result = null;
    +
    +        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
    +
    +        String topic = msg.getTopic();
    +        int queueId = msg.getQueueId();
    +
    +        final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    +        if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
    +            || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
    +            // Delay Delivery
    +            if (msg.getDelayTimeLevel() > 0) {
    +                if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
    +                    msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
    +                }
    +
    +                topic = ScheduleMessageService.SCHEDULE_TOPIC;
    +                queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
     
                     // Backup real topic, queueId
                     MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
    @@ -10368,53 +10338,208 @@ access_log /path/to/access.log combined 
       
       
    -    聊一下 RocketMQ 的顺序消息
    -    /2021/08/29/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E9%A1%BA%E5%BA%8F%E6%B6%88%E6%81%AF/
    -    rocketmq 里有一种比较特殊的用法,就是顺序消息,比如订单的生命周期里,在创建,支付,签收等状态轮转中,会发出来对应的消息,这里面就比较需要去保证他们的顺序,当然在处理的业务代码也可以做对应的处理,结合消息重投,但是如果这里消息就能保证顺序性了,那么业务代码就能更好的关注业务代码的处理。

    -

    首先有一种情况是全局的有序,比如对于一个 topic 里就得发送线程保证只有一个,内部的 queue 也只有一个,消费线程也只有一个,这样就能比较容易的保证全局顺序性了,但是这里的问题就是完全限制了性能,不是很现实,在真实场景里很多都是比如对于同一个订单,需要去保证状态的轮转是按照预期的顺序来,而不必要全局的有序性。

    -

    对于这类的有序性,需要在发送和接收方都有对应的处理,在发送消息中,需要去指定 selector,即MessageQueueSelector,能够以固定的方式是分配到对应的 MessageQueue

    -

    比如像 RocketMQ 中的示例

    -
    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
    -                @Override
    -                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
    -                    Long id = (Long) arg;  //message queue is selected by #salesOrderID
    -                    long index = id % mqs.size();
    -                    return mqs.get((int) index);
    -                }
    -            }, orderList.get(i).getOrderId());
    + 聊一下 RocketMQ 的消息存储四 + /2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/ + IndexFile 结构 hash 结构能够通过 key 寻找到对应在 CommitLog 中的位置

    +

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

    +
    class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
     
    -

    而在消费侧有几个点比较重要,首先我们要保证一个 MessageQueue只被一个消费者消费,消费队列存在broker端,要保证 MessageQueue 只被一个消费者消费,那么消费者在进行消息拉取消费时就必须向mq服务器申请队列锁,消费者申请队列锁的代码存在于RebalanceService消息队列负载的实现代码中。

    -
    List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
    -        for (MessageQueue mq : mqSet) {
    -            if (!this.processQueueTable.containsKey(mq)) {
    -              // 判断是否顺序,如果是顺序消费的,则需要加锁
    -                if (isOrder && !this.lock(mq)) {
    -                    log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
    -                    continue;
    -                }
    +    @Override
    +    public void dispatch(DispatchRequest request) {
    +        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    +            DefaultMessageStore.this.indexService.buildIndex(request);
    +        }
    +    }
    +}
    +public void buildIndex(DispatchRequest req) {
    +        IndexFile indexFile = retryGetAndCreateIndexFile();
    +        if (indexFile != null) {
    +            long endPhyOffset = indexFile.getEndPhyOffset();
    +            DispatchRequest msg = req;
    +            String topic = msg.getTopic();
    +            String keys = msg.getKeys();
    +            if (msg.getCommitLogOffset() < endPhyOffset) {
    +                return;
    +            }
     
    -                this.removeDirtyOffset(mq);
    -                ProcessQueue pq = new ProcessQueue();
    -                long nextOffset = this.computePullFromWhere(mq);
    -                if (nextOffset >= 0) {
    -                    ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
    -                    if (pre != null) {
    -                        log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
    -                    } else {
    -                        log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
    -                        PullRequest pullRequest = new PullRequest();
    -                        pullRequest.setConsumerGroup(consumerGroup);
    -                        pullRequest.setNextOffset(nextOffset);
    -                        pullRequest.setMessageQueue(mq);
    -                        pullRequest.setProcessQueue(pq);
    -                        pullRequestList.add(pullRequest);
    -                        changed = true;
    -                    }
    -                } else {
    -                    log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
    +            final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    +            switch (tranType) {
    +                case MessageSysFlag.TRANSACTION_NOT_TYPE:
    +                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    +                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
    +                    break;
    +                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
    +                    return;
    +            }
    +
    +            if (req.getUniqKey() != null) {
    +                indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
    +                if (indexFile == null) {
    +                    log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
    +                    return;
                     }
                 }
    -        }
    + + if (keys != null && keys.length() > 0) { + String[] keyset = keys.split(MessageConst.KEY_SEPARATOR); + for (int i = 0; i < keyset.length; i++) { + String key = keyset[i]; + if (key.length() > 0) { + indexFile = putKey(indexFile, msg, buildKey(topic, key)); + if (indexFile == null) { + log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); + return; + } + } + } + } + } else { + log.error("build index error, stop building index"); + } + }
    + +

    配置的数量

    +
    private boolean messageIndexEnable = true;
    +private int maxHashSlotNum = 5000000;
    +private int maxIndexNum = 5000000 * 4;
    + +

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

    +
    public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    +        if (this.indexHeader.getIndexCount() < this.indexNum) {
    +          // 获取 key 的 hash
    +            int keyHash = indexKeyHashMethod(key);
    +          // 计算属于哪个 slot
    +            int slotPos = keyHash % this.hashSlotNum;
    +          // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
    +            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    +
    +            FileLock fileLock = null;
    +
    +            try {
    +
    +                // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
    +                // false);
    +                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    +                if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
    +                    slotValue = invalidIndex;
    +                }
    +
    +                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
    +
    +                timeDiff = timeDiff / 1000;
    +
    +                if (this.indexHeader.getBeginTimestamp() <= 0) {
    +                    timeDiff = 0;
    +                } else if (timeDiff > Integer.MAX_VALUE) {
    +                    timeDiff = Integer.MAX_VALUE;
    +                } else if (timeDiff < 0) {
    +                    timeDiff = 0;
    +                }
    +
    +              // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
    +                int absIndexPos =
    +                    IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
    +                        + this.indexHeader.getIndexCount() * indexSize;
    +							
    +                this.mappedByteBuffer.putInt(absIndexPos, keyHash);
    +                this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
    +                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
    +                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
    +
    +              // 存放的是数量位移,不是绝对位置
    +                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    +
    +                if (this.indexHeader.getIndexCount() <= 1) {
    +                    this.indexHeader.setBeginPhyOffset(phyOffset);
    +                    this.indexHeader.setBeginTimestamp(storeTimestamp);
    +                }
    +
    +                this.indexHeader.incHashSlotCount();
    +                this.indexHeader.incIndexCount();
    +                this.indexHeader.setEndPhyOffset(phyOffset);
    +                this.indexHeader.setEndTimestamp(storeTimestamp);
    +
    +                return true;
    +            } catch (Exception e) {
    +                log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
    +            } finally {
    +                if (fileLock != null) {
    +                    try {
    +                        fileLock.release();
    +                    } catch (IOException e) {
    +                        log.error("Failed to release the lock", e);
    +                    }
    +                }
    +            }
    +        } else {
    +            log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
    +                + "; index max num = " + this.indexNum);
    +        }
    +
    +        return false;
    +    }
    + +

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

    +]]>
    + + MQ + RocketMQ + 消息队列 + + + MQ + 消息队列 + RocketMQ + +
    + + 聊一下 RocketMQ 的顺序消息 + /2021/08/29/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E9%A1%BA%E5%BA%8F%E6%B6%88%E6%81%AF/ + rocketmq 里有一种比较特殊的用法,就是顺序消息,比如订单的生命周期里,在创建,支付,签收等状态轮转中,会发出来对应的消息,这里面就比较需要去保证他们的顺序,当然在处理的业务代码也可以做对应的处理,结合消息重投,但是如果这里消息就能保证顺序性了,那么业务代码就能更好的关注业务代码的处理。

    +

    首先有一种情况是全局的有序,比如对于一个 topic 里就得发送线程保证只有一个,内部的 queue 也只有一个,消费线程也只有一个,这样就能比较容易的保证全局顺序性了,但是这里的问题就是完全限制了性能,不是很现实,在真实场景里很多都是比如对于同一个订单,需要去保证状态的轮转是按照预期的顺序来,而不必要全局的有序性。

    +

    对于这类的有序性,需要在发送和接收方都有对应的处理,在发送消息中,需要去指定 selector,即MessageQueueSelector,能够以固定的方式是分配到对应的 MessageQueue

    +

    比如像 RocketMQ 中的示例

    +
    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
    +                @Override
    +                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
    +                    Long id = (Long) arg;  //message queue is selected by #salesOrderID
    +                    long index = id % mqs.size();
    +                    return mqs.get((int) index);
    +                }
    +            }, orderList.get(i).getOrderId());
    + +

    而在消费侧有几个点比较重要,首先我们要保证一个 MessageQueue只被一个消费者消费,消费队列存在broker端,要保证 MessageQueue 只被一个消费者消费,那么消费者在进行消息拉取消费时就必须向mq服务器申请队列锁,消费者申请队列锁的代码存在于RebalanceService消息队列负载的实现代码中。

    +
    List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
    +        for (MessageQueue mq : mqSet) {
    +            if (!this.processQueueTable.containsKey(mq)) {
    +              // 判断是否顺序,如果是顺序消费的,则需要加锁
    +                if (isOrder && !this.lock(mq)) {
    +                    log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
    +                    continue;
    +                }
    +
    +                this.removeDirtyOffset(mq);
    +                ProcessQueue pq = new ProcessQueue();
    +                long nextOffset = this.computePullFromWhere(mq);
    +                if (nextOffset >= 0) {
    +                    ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
    +                    if (pre != null) {
    +                        log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
    +                    } else {
    +                        log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
    +                        PullRequest pullRequest = new PullRequest();
    +                        pullRequest.setConsumerGroup(consumerGroup);
    +                        pullRequest.setNextOffset(nextOffset);
    +                        pullRequest.setMessageQueue(mq);
    +                        pullRequest.setProcessQueue(pq);
    +                        pullRequestList.add(pullRequest);
    +                        changed = true;
    +                    }
    +                } else {
    +                    log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
    +                }
    +            }
    +        }

    在申请到锁之后会创建 pullRequest 进行消息拉取,消息拉取部分的代码实现在PullMessageService中,

    @Override
    @@ -10627,415 +10752,133 @@ access_log /path/to/access.log combined 
       
       
    -    聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点
    -    /2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/
    -    这个话题是由一次组内同学分享引出来的,首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现,这里也稍微讲一下,在一个 1.x 的老项目里,可以看到AopAutoConfiguration 是这样的

    -
    @Configuration
    -@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
    -@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    -public class AopAutoConfiguration {
    -
    -	@Configuration
    -	@EnableAspectJAutoProxy(proxyTargetClass = false)
    -	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
    -	public static class JdkDynamicAutoProxyConfiguration {
    -	}
    -
    -	@Configuration
    -	@EnableAspectJAutoProxy(proxyTargetClass = true)
    -	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
    -	public static class CglibAutoProxyConfiguration {
    -	}
    -
    -}
    - -

    而在 2.x 中变成了这样

    -
    @Configuration(proxyBeanMethods = false)
    -@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    -public class AopAutoConfiguration {
    -
    -	@Configuration(proxyBeanMethods = false)
    -	@ConditionalOnClass(Advice.class)
    -	static class AspectJAutoProxyingConfiguration {
    -
    -		@Configuration(proxyBeanMethods = false)
    -		@EnableAspectJAutoProxy(proxyTargetClass = false)
    -		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
    -		static class JdkDynamicAutoProxyConfiguration {
    -
    -		}
    -
    -		@Configuration(proxyBeanMethods = false)
    -		@EnableAspectJAutoProxy(proxyTargetClass = true)
    -		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
    -				matchIfMissing = true)
    -		static class CglibAutoProxyConfiguration {
    -
    -		}
    -
    -	}
    - -

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

    -

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

    -
    @Mapper
    -public interface StudentMapper {
    -		// 就是插入一条数据
    -    @Insert("insert into student(name, age)" + "values ('nick', '18')")
    -    public Long insert();
    -}
    -
    -@Component
    -public class StudentManager {
    -
    -    @Resource
    -    private StudentMapper studentMapper;
    -    
    -    public Long createStudent() {
    -        return studentMapper.insert();
    -    }
    -}
    -
    -@Component
    -public class StudentServiceImpl implements StudentService {
    -
    -    @Resource
    -    private StudentManager studentManager;
    -
    -    // 自己引用
    -    @Resource
    -    private StudentServiceImpl studentService;
    -
    -    @Override
    -    @Transactional
    -    public Long createStudent() {
    -        Long id = studentManager.createStudent();
    -        Long id2 = studentService.createStudent2();
    -        return 1L;
    -    }
    -
    -    @Transactional
    -    private Long createStudent2() {
    -//        Integer t = Integer.valueOf("aaa");
    -        return studentManager.createStudent();
    -    }
    -}
    - -

    第一个公有方法 createStudent 首先调用了 manager 层的创建方法,然后再通过引入的 studentService 调用了createStudent2,我们先跑一下看看会出现啥情况,果不其然报错了,正是这个报错让我纠结了很久

    -

    EdR7oB

    -

    报了个空指针,而且是在 createStudent2 已经被调用到了,在它的内部,报的 studentManager 是 null,首先 cglib 作为动态代理它是通过继承的方式来实现的,相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类,具体的代理切面逻辑在子类实现,然后在调用目标对象的目标方法,但是继承的方式对于 final 和私有方法其实是没法进行代理的,因为没法继承,所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了,也就是不会进入这个方法内部,后面才发现犯了个特别二的错误,继承的方式去调用父类的私有方法,对于 Java 来说是可以调用到的,父类的私有方法并不由子类的InstanceKlass维护,只能通过子类的InstanceKlass找到Java类对应的_super,这样间接地访问。也就是说子类其实是可以访问的,那为啥访问了会报空指针呢,这里报的是studentManager 是空的,可以往依赖注入方面去想,如果忽略依赖注入,我这个studentManager 的确是 null,那是不是就没有被依赖注入呢,但是为啥前面那个可以呢

    -

    这个问题着实查了很久,不废话来看代码

    -
    @Override
    -		protected Object invokeJoinpoint() throws Throwable {
    -			if (this.methodProxy != null) {
    -        // 这里的 target 就是被代理的 bean
    -				return this.methodProxy.invoke(this.target, this.arguments);
    -			}
    -			else {
    -				return super.invokeJoinpoint();
    -			}
    -		}
    - - - -

    这个是org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation的代码,其实它在这里不是直接调用 super 也就是父类的方法,而是通过 methodProxy 调用 target 目标对象的方法,也就是原始的 studentService bean 的方法,这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用,否则就会出现上面的问题,因为 cglib 其实是通过继承来实现,通过将调用转移到子类上加入代理逻辑,我们在简单使用的时候会直接 invokeSuper() 调用父类的方法,但是在这里 spring 的场景里需要去支持 spring 的功能逻辑,所以上面的问题就可以开始来解释了,因为 createStudent 是公共方法,cglib 可以对其进行继承代理,但是在执行逻辑的时候其实是通过调用目标对象,也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent,而对于下面的 createStudent2 方法因为是私有方法,不会走代理逻辑,也就不会有调用回目标对象的逻辑,只是通过继承关系,在子类中没有这个方法,所以会通过子类的InstanceKlass找到这个类对应的_super,然后调用父类的这个私有方法,这里要搞清楚一个点,从这个代理类直接找到其父类然后调用这个私有方法,这个类是由 cglib 生成的,不是被 spring 管理起来经过依赖注入的 bean,所以是没有 studentManager 这个依赖的,也就出现了前面的问题

    -

    而在前面提到的cglib通过methodProxy调用到目标对象,目标对象是在什么时候设置的呢,其实是在bean的生命周期中,org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中,会调用实现了这个接口的方法,

    -
    @Override
    -public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    -	if (bean != null) {
    -		Object cacheKey = getCacheKey(bean.getClass(), beanName);
    -		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    -			return wrapIfNecessary(bean, beanName, cacheKey);
    -		}
    -	}
    -	return bean;
    -}
    - -

    具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里

    -
    protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
    -		if (StringUtils.hasLength(beanName)) {
    -			return (FactoryBean.class.isAssignableFrom(beanClass) ?
    -					BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
    -		}
    -		else {
    -			return beanClass;
    -		}
    -	}
    -
    -	/**
    -	 * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
    -	 * @param bean the raw bean instance
    -	 * @param beanName the name of the bean
    -	 * @param cacheKey the cache key for metadata access
    -	 * @return a proxy wrapping the bean, or the raw bean instance as-is
    -	 */
    -	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    -		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
    -			return bean;
    -		}
    -		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
    -			return bean;
    -		}
    -		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
    -			this.advisedBeans.put(cacheKey, Boolean.FALSE);
    -			return bean;
    -		}
    -
    -		// Create proxy if we have advice.
    -		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    -		if (specificInterceptors != DO_NOT_PROXY) {
    -			this.advisedBeans.put(cacheKey, Boolean.TRUE);
    -			Object proxy = createProxy(
    -					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
    -			this.proxyTypes.put(cacheKey, proxy.getClass());
    -			return proxy;
    -		}
    -
    -		this.advisedBeans.put(cacheKey, Boolean.FALSE);
    -		return bean;
    -	}
    - -

    然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类

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

    + 搬运两个 StackOverflow 上的 Mysql 编码相关的问题解答 + /2022/01/16/%E6%90%AC%E8%BF%90%E4%B8%A4%E4%B8%AA-StackOverflow-%E4%B8%8A%E7%9A%84-Mysql-%E7%BC%96%E7%A0%81%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94/ + Mysql 字符编码和排序规则

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

    +

    原文

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

    +

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

    +

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

    +

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

    +

    Key differences

    +

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

    +

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

    +

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

    +

    Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci

    +

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

    +

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

    +

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

    +

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

    +

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

    +

    What should you use?

    +

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

    +

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

    +

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

    +

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

    +

    What the parts mean

    +

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

    +

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

    +

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

    +

    翻译

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

    +

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

    +

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

    +

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

    +

    主要区别

    +

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

    +

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

    +

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

    +

    utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势

    +

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

    +

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

    +

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

    +

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

    +

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

    +

    你应该用什么?

    +

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

    +

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

    +

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

    +

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

    +

    其余各个部分是什么意思

    +

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

    +

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

    +

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

    +

    utf8 和 utf8mb4 编码有什么区别

    原文

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

    +

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

    +

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

    +
    +

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

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

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

    +

    译文

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

    +

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

    +

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

    +
    +

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

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

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

    +

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

    ]]>
    - 生活 + Mysql - 生活 - 囤物资 + mysql + 字符集 + 编码 + utf8 + utf8mb4 + utf8mb4_0900_ai_ci + utf8mb4_unicode_ci + utf8mb4_general_ci
    - 聊一下 RocketMQ 的消息存储四 - /2021/10/17/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E5%9B%9B/ - IndexFile 结构 hash 结构能够通过 key 寻找到对应在 CommitLog 中的位置

    -

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

    -
    class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
    +    聊一下 SpringBoot 中动态切换数据源的方法
    +    /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
    -    public void dispatch(DispatchRequest request) {
    -        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    -            DefaultMessageStore.this.indexService.buildIndex(request);
    +    protected Object determineCurrentLookupKey() {
    +        if (DatabaseContextHolder.getDatabaseType() != null) {
    +            return DatabaseContextHolder.getDatabaseType().getName();
             }
    +        return DatabaseType.MASTER1.getName();
         }
    -}
    -public void buildIndex(DispatchRequest req) {
    -        IndexFile indexFile = retryGetAndCreateIndexFile();
    -        if (indexFile != null) {
    -            long endPhyOffset = indexFile.getEndPhyOffset();
    -            DispatchRequest msg = req;
    -            String topic = msg.getTopic();
    -            String keys = msg.getKeys();
    -            if (msg.getCommitLogOffset() < endPhyOffset) {
    -                return;
    -            }
    -
    -            final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    -            switch (tranType) {
    -                case MessageSysFlag.TRANSACTION_NOT_TYPE:
    -                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    -                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
    -                    break;
    -                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
    -                    return;
    -            }
    +}
    - if (req.getUniqKey() != null) { - indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey())); - if (indexFile == null) { - log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); - return; - } - } - - if (keys != null && keys.length() > 0) { - String[] keyset = keys.split(MessageConst.KEY_SEPARATOR); - for (int i = 0; i < keyset.length; i++) { - String key = keyset[i]; - if (key.length() > 0) { - indexFile = putKey(indexFile, msg, buildKey(topic, key)); - if (indexFile == null) { - log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); - return; - } - } - } - } - } else { - log.error("build index error, stop building index"); - } - }
    - -

    配置的数量

    -
    private boolean messageIndexEnable = true;
    -private int maxHashSlotNum = 5000000;
    -private int maxIndexNum = 5000000 * 4;
    - -

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

    -
    public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    -        if (this.indexHeader.getIndexCount() < this.indexNum) {
    -          // 获取 key 的 hash
    -            int keyHash = indexKeyHashMethod(key);
    -          // 计算属于哪个 slot
    -            int slotPos = keyHash % this.hashSlotNum;
    -          // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
    -            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    -
    -            FileLock fileLock = null;
    -
    -            try {
    -
    -                // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
    -                // false);
    -                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    -                if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
    -                    slotValue = invalidIndex;
    -                }
    -
    -                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
    -
    -                timeDiff = timeDiff / 1000;
    -
    -                if (this.indexHeader.getBeginTimestamp() <= 0) {
    -                    timeDiff = 0;
    -                } else if (timeDiff > Integer.MAX_VALUE) {
    -                    timeDiff = Integer.MAX_VALUE;
    -                } else if (timeDiff < 0) {
    -                    timeDiff = 0;
    -                }
    -
    -              // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
    -                int absIndexPos =
    -                    IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
    -                        + this.indexHeader.getIndexCount() * indexSize;
    -							
    -                this.mappedByteBuffer.putInt(absIndexPos, keyHash);
    -                this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
    -                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
    -                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
    -
    -              // 存放的是数量位移,不是绝对位置
    -                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    -
    -                if (this.indexHeader.getIndexCount() <= 1) {
    -                    this.indexHeader.setBeginPhyOffset(phyOffset);
    -                    this.indexHeader.setBeginTimestamp(storeTimestamp);
    -                }
    -
    -                this.indexHeader.incHashSlotCount();
    -                this.indexHeader.incIndexCount();
    -                this.indexHeader.setEndPhyOffset(phyOffset);
    -                this.indexHeader.setEndTimestamp(storeTimestamp);
    -
    -                return true;
    -            } catch (Exception e) {
    -                log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
    -            } finally {
    -                if (fileLock != null) {
    -                    try {
    -                        fileLock.release();
    -                    } catch (IOException e) {
    -                        log.error("Failed to release the lock", e);
    -                    }
    -                }
    -            }
    -        } else {
    -            log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
    -                + "; index max num = " + this.indexNum);
    -        }
    -
    -        return false;
    -    }
    - -

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

    -]]>
    - - MQ - RocketMQ - 消息队列 - - - MQ - 消息队列 - RocketMQ - -
    - - 聊在东京奥运会闭幕式这天-二 - /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 中动态切换数据源的方法 - /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"/>
    +

    而如何使用这个 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"
    @@ -11188,40 +11031,662 @@ access_log /path/to/access.log combined }
    -

    看一下运行结果

    -

    -

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

    +

    看一下运行结果

    +

    +

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

    +]]> + + Java + SpringBoot + + + Java + Spring + SpringBoot + Druid + 数据源动态切换 + + + + 聊在东京奥运会闭幕式这天-二 + /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 岁可能其他项目已经是年龄很大了,不过前面说的举重的吕小军军神也是年纪蛮大了,但是非常强,而且在油管上简直就是个神,相对来说射击是关注比较少,杨倩的也只是看了后面拿到冠军这个结果,有些因为时间或者电视上没放,但是成绩还是不错的,没多少喷点。

    +

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

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

    +

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

    +

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

    +

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

    +]]>
    + + 生活 + 运动 + + + 生活 + 运动 + 东京奥运会 + 乒乓球 + 跳水 + +
    + + 聊聊 Dubbo 的 SPI 续之自适应拓展 + /2020/06/06/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84-SPI-%E7%BB%AD%E4%B9%8B%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ + 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 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);
    +            }
    +        }
    +
    +        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);
    +        }
    +    }
    +
    +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 + 自适应拓展 + +
    + + 聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点 + /2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ + 这个话题是由一次组内同学分享引出来的,首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现,这里也稍微讲一下,在一个 1.x 的老项目里,可以看到AopAutoConfiguration 是这样的

    +
    @Configuration
    +@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
    +@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    +public class AopAutoConfiguration {
    +
    +	@Configuration
    +	@EnableAspectJAutoProxy(proxyTargetClass = false)
    +	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
    +	public static class JdkDynamicAutoProxyConfiguration {
    +	}
    +
    +	@Configuration
    +	@EnableAspectJAutoProxy(proxyTargetClass = true)
    +	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
    +	public static class CglibAutoProxyConfiguration {
    +	}
    +
    +}
    + +

    而在 2.x 中变成了这样

    +
    @Configuration(proxyBeanMethods = false)
    +@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    +public class AopAutoConfiguration {
    +
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnClass(Advice.class)
    +	static class AspectJAutoProxyingConfiguration {
    +
    +		@Configuration(proxyBeanMethods = false)
    +		@EnableAspectJAutoProxy(proxyTargetClass = false)
    +		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
    +		static class JdkDynamicAutoProxyConfiguration {
    +
    +		}
    +
    +		@Configuration(proxyBeanMethods = false)
    +		@EnableAspectJAutoProxy(proxyTargetClass = true)
    +		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
    +				matchIfMissing = true)
    +		static class CglibAutoProxyConfiguration {
    +
    +		}
    +
    +	}
    + +

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

    +

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

    +
    @Mapper
    +public interface StudentMapper {
    +		// 就是插入一条数据
    +    @Insert("insert into student(name, age)" + "values ('nick', '18')")
    +    public Long insert();
    +}
    +
    +@Component
    +public class StudentManager {
    +
    +    @Resource
    +    private StudentMapper studentMapper;
    +    
    +    public Long createStudent() {
    +        return studentMapper.insert();
    +    }
    +}
    +
    +@Component
    +public class StudentServiceImpl implements StudentService {
    +
    +    @Resource
    +    private StudentManager studentManager;
    +
    +    // 自己引用
    +    @Resource
    +    private StudentServiceImpl studentService;
    +
    +    @Override
    +    @Transactional
    +    public Long createStudent() {
    +        Long id = studentManager.createStudent();
    +        Long id2 = studentService.createStudent2();
    +        return 1L;
    +    }
    +
    +    @Transactional
    +    private Long createStudent2() {
    +//        Integer t = Integer.valueOf("aaa");
    +        return studentManager.createStudent();
    +    }
    +}
    + +

    第一个公有方法 createStudent 首先调用了 manager 层的创建方法,然后再通过引入的 studentService 调用了createStudent2,我们先跑一下看看会出现啥情况,果不其然报错了,正是这个报错让我纠结了很久

    +

    EdR7oB

    +

    报了个空指针,而且是在 createStudent2 已经被调用到了,在它的内部,报的 studentManager 是 null,首先 cglib 作为动态代理它是通过继承的方式来实现的,相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类,具体的代理切面逻辑在子类实现,然后在调用目标对象的目标方法,但是继承的方式对于 final 和私有方法其实是没法进行代理的,因为没法继承,所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了,也就是不会进入这个方法内部,后面才发现犯了个特别二的错误,继承的方式去调用父类的私有方法,对于 Java 来说是可以调用到的,父类的私有方法并不由子类的InstanceKlass维护,只能通过子类的InstanceKlass找到Java类对应的_super,这样间接地访问。也就是说子类其实是可以访问的,那为啥访问了会报空指针呢,这里报的是studentManager 是空的,可以往依赖注入方面去想,如果忽略依赖注入,我这个studentManager 的确是 null,那是不是就没有被依赖注入呢,但是为啥前面那个可以呢

    +

    这个问题着实查了很久,不废话来看代码

    +
    @Override
    +		protected Object invokeJoinpoint() throws Throwable {
    +			if (this.methodProxy != null) {
    +        // 这里的 target 就是被代理的 bean
    +				return this.methodProxy.invoke(this.target, this.arguments);
    +			}
    +			else {
    +				return super.invokeJoinpoint();
    +			}
    +		}
    + + + +

    这个是org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation的代码,其实它在这里不是直接调用 super 也就是父类的方法,而是通过 methodProxy 调用 target 目标对象的方法,也就是原始的 studentService bean 的方法,这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用,否则就会出现上面的问题,因为 cglib 其实是通过继承来实现,通过将调用转移到子类上加入代理逻辑,我们在简单使用的时候会直接 invokeSuper() 调用父类的方法,但是在这里 spring 的场景里需要去支持 spring 的功能逻辑,所以上面的问题就可以开始来解释了,因为 createStudent 是公共方法,cglib 可以对其进行继承代理,但是在执行逻辑的时候其实是通过调用目标对象,也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent,而对于下面的 createStudent2 方法因为是私有方法,不会走代理逻辑,也就不会有调用回目标对象的逻辑,只是通过继承关系,在子类中没有这个方法,所以会通过子类的InstanceKlass找到这个类对应的_super,然后调用父类的这个私有方法,这里要搞清楚一个点,从这个代理类直接找到其父类然后调用这个私有方法,这个类是由 cglib 生成的,不是被 spring 管理起来经过依赖注入的 bean,所以是没有 studentManager 这个依赖的,也就出现了前面的问题

    +

    而在前面提到的cglib通过methodProxy调用到目标对象,目标对象是在什么时候设置的呢,其实是在bean的生命周期中,org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中,会调用实现了这个接口的方法,

    +
    @Override
    +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    +	if (bean != null) {
    +		Object cacheKey = getCacheKey(bean.getClass(), beanName);
    +		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    +			return wrapIfNecessary(bean, beanName, cacheKey);
    +		}
    +	}
    +	return bean;
    +}
    + +

    具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里

    +
    protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
    +		if (StringUtils.hasLength(beanName)) {
    +			return (FactoryBean.class.isAssignableFrom(beanClass) ?
    +					BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
    +		}
    +		else {
    +			return beanClass;
    +		}
    +	}
    +
    +	/**
    +	 * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
    +	 * @param bean the raw bean instance
    +	 * @param beanName the name of the bean
    +	 * @param cacheKey the cache key for metadata access
    +	 * @return a proxy wrapping the bean, or the raw bean instance as-is
    +	 */
    +	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    +		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
    +			return bean;
    +		}
    +		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
    +			return bean;
    +		}
    +		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
    +			this.advisedBeans.put(cacheKey, Boolean.FALSE);
    +			return bean;
    +		}
    +
    +		// Create proxy if we have advice.
    +		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    +		if (specificInterceptors != DO_NOT_PROXY) {
    +			this.advisedBeans.put(cacheKey, Boolean.TRUE);
    +			Object proxy = createProxy(
    +					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
    +			this.proxyTypes.put(cacheKey, proxy.getClass());
    +			return proxy;
    +		}
    +
    +		this.advisedBeans.put(cacheKey, Boolean.FALSE);
    +		return bean;
    +	}
    + +

    然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类

    +]]>
    + + Java + SpringBoot + + + Java + Spring + SpringBoot + cglib + 事务 + +
    + + 聊聊 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));
    +                }
    +            }
    +            return instance;
    +        } catch (Throwable t) {
    +            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
    +                    type + ")  could not be instantiated: " + t.getMessage(), t);
    +        }
    +    }
    +

    然后就是创建扩展了,这里如果 wrapperClasses 就会遍历生成wrapper实例,并做 setter 依赖注入,但是这里cachedWrapperClasses的来源还是有点搞不清楚,得再看下 com.alibaba.dubbo.common.extension.ExtensionLoader#loadFile的具体逻辑
    又看了遍新的代码,这个函数被抽出来了

    +
    /**
    +    * test if clazz is a wrapper class
    +    * <p>
    +    * which has Constructor with given class type as its only argument
    +    */
    +   private boolean isWrapperClass(Class<?> clazz) {
    +       try {
    +           clazz.getConstructor(type);
    +           return true;
    +       } catch (NoSuchMethodException e) {
    +           return false;
    +       }
    +   }
    +

    是否是 wrapperClass 其实就看构造函数的。

    +]]>
    + + Java + Dubbo + RPC + SPI + Dubbo + SPI + + + Java + Dubbo + RPC + SPI + +
    + + 聊聊 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 - SpringBoot Java - Spring - SpringBoot - Druid - 数据源动态切换 - -
    - - 聊在东京奥运会闭幕式这天 - /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 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

    -

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

    -]]>
    - - 生活 - 运动 - - - 生活 - 运动 - 东京奥运会 - 乒乓球 - 跳水 + Synchronized + 偏向锁 + 轻量级锁 + 重量级锁 + 自旋
    @@ -11240,435 +11705,281 @@ access_log /path/to/access.log combined <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()
    -                            + ", but there have been failed providers " + providers
    -                            + " (" + providers.size() + "/" + copyInvokers.size()
    -                            + ") from the registry " + directory.getUrl().getAddress()
    -                            + " on the consumer " + NetUtils.getLocalHost()
    -                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
    -                            + le.getMessage(), le);
    -                }
    -                return result;
    -            } catch (RpcException e) {
    -                if (e.isBiz()) { // biz exception.
    -                    throw e;
    -                }
    -                le = e;
    -            } catch (Throwable e) {
    -                le = new RpcException(e.getMessage(), e);
    -            } finally {
    -                providers.add(invoker.getUrl().getAddress());
    -            }
    -        }
    -        throw new RpcException(le.getCode(), "Failed to invoke the method "
    -                + methodName + " in the service " + getInterface().getName()
    -                + ". Tried " + len + " times of the providers " + providers
    -                + " (" + providers.size() + "/" + copyInvokers.size()
    -                + ") from the registry " + directory.getUrl().getAddress()
    -                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
    -                + Version.getVersion() + ". Last error is: "
    -                + le.getMessage(), le.getCause() != null ? le.getCause() : le);
    -    }
    - - -

    Failfast Cluster

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

    -

    Failsafe Cluster

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

    -

    Failback Cluster

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

    -

    Forking Cluster

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

    -

    Broadcast Cluster

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

    -]]> - - Java - Dubbo - RPC - Dubbo - 容错机制 - - - Java - Dubbo - RPC - 容错机制 - -
    - - 聊聊 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 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);
    -            }
    -        }
    -
    -        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);
    -        }
    -    }
    -
    -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);
    +    public final static String NAME = "failover";
     
    -        return extension.export(arg0);
    +    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    +        return new FailoverClusterInvoker<T>(directory);
         }
     
    -    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());
    +}
    +

    这个代码就非常简单,重点需要看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()
    +                            + ", but there have been failed providers " + providers
    +                            + " (" + providers.size() + "/" + copyInvokers.size()
    +                            + ") from the registry " + directory.getUrl().getAddress()
    +                            + " on the consumer " + NetUtils.getLocalHost()
    +                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
    +                            + le.getMessage(), le);
    +                }
    +                return result;
    +            } catch (RpcException e) {
    +                if (e.isBiz()) { // biz exception.
    +                    throw e;
    +                }
    +                le = e;
    +            } catch (Throwable e) {
    +                le = new RpcException(e.getMessage(), e);
    +            } finally {
    +                providers.add(invoker.getUrl().getAddress());
    +            }
    +        }
    +        throw new RpcException(le.getCode(), "Failed to invoke the method "
    +                + methodName + " in the service " + getInterface().getName()
    +                + ". Tried " + len + " times of the providers " + providers
    +                + " (" + providers.size() + "/" + copyInvokers.size()
    +                + ") from the registry " + directory.getUrl().getAddress()
    +                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
    +                + Version.getVersion() + ". Last error is: "
    +                + le.getMessage(), le.getCause() != null ? le.getCause() : le);
    +    }
    - 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); - } -} -
    +

    Failfast Cluster

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

    +

    Failsafe Cluster

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

    +

    Failback Cluster

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

    +

    Forking Cluster

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

    +

    Broadcast Cluster

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

    ]]>
    Java + Dubbo - RPC Dubbo - RPC - Dubbo - SPI - SPI - Adaptive + 容错机制 Java Dubbo RPC - SPI - Adaptive - 自适应拓展 + 容错机制
    - 聊聊 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)
    + 聊聊 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 {
     
    -
    enum { locked_value             = 0,
    -         unlocked_value           = 1,
    -         monitor_value            = 2,
    -         marked_value             = 3,
    -         biased_lock_pattern      = 5
    -};
    -
    + public static void main(String[] args) { + SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); + synchronizedDemo.lockMethod(); + } -

    我们可以用 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 synchronized void lockMethod() { + System.out.println("here i'm locked"); + } -

    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());
    -		}
    -} 
    + public void lockSynchronizedDemo() { + synchronized (this) { + System.out.println("here lock class"); + } + } +}
    + +

    然后来查看反编译结果,其实代码(日光)之下并无新事,即使是完全不懂的也可以通过一些词义看出一些意义

    +
      Last modified 2021620; size 729 bytes
    +  MD5 checksum dd9c529863bd7ff839a95481db578ad9
    +  Compiled from "SynchronizedDemo.java"
    +public class SynchronizedDemo
    +  minor version: 0
    +  major version: 53
    +  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    +  this_class: #2                          // SynchronizedDemo
    +  super_class: #9                         // java/lang/Object
    +  interfaces: 0, fields: 0, methods: 4, attributes: 1
    +Constant pool:
    +   #1 = Methodref          #9.#22         // java/lang/Object."<init>":()V
    +   #2 = Class              #23            // SynchronizedDemo
    +   #3 = Methodref          #2.#22         // SynchronizedDemo."<init>":()V
    +   #4 = Methodref          #2.#24         // SynchronizedDemo.lockMethod:()V
    +   #5 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
    +   #6 = String             #27            // here i\'m locked
    +   #7 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
    +   #8 = String             #30            // here lock class
    +   #9 = Class              #31            // java/lang/Object
    +  #10 = Utf8               <init>
    +  #11 = Utf8               ()V
    +  #12 = Utf8               Code
    +  #13 = Utf8               LineNumberTable
    +  #14 = Utf8               main
    +  #15 = Utf8               ([Ljava/lang/String;)V
    +  #16 = Utf8               lockMethod
    +  #17 = Utf8               lockSynchronizedDemo
    +  #18 = Utf8               StackMapTable
    +  #19 = Class              #32            // java/lang/Throwable
    +  #20 = Utf8               SourceFile
    +  #21 = Utf8               SynchronizedDemo.java
    +  #22 = NameAndType        #10:#11        // "<init>":()V
    +  #23 = Utf8               SynchronizedDemo
    +  #24 = NameAndType        #16:#11        // lockMethod:()V
    +  #25 = Class              #33            // java/lang/System
    +  #26 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
    +  #27 = Utf8               here i\'m locked
    +  #28 = Class              #36            // java/io/PrintStream
    +  #29 = NameAndType        #37:#38        // println:(Ljava/lang/String;)V
    +  #30 = Utf8               here lock class
    +  #31 = Utf8               java/lang/Object
    +  #32 = Utf8               java/lang/Throwable
    +  #33 = Utf8               java/lang/System
    +  #34 = Utf8               out
    +  #35 = Utf8               Ljava/io/PrintStream;
    +  #36 = Utf8               java/io/PrintStream
    +  #37 = Utf8               println
    +  #38 = Utf8               (Ljava/lang/String;)V
    +{
    +  public SynchronizedDemo();
    +    descriptor: ()V
    +    flags: (0x0001) ACC_PUBLIC
    +    Code:
    +      stack=1, locals=1, args_size=1
    +         0: aload_0
    +         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
    +         4: return
    +      LineNumberTable:
    +        line 5: 0
     
    -

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

    -

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

    -
    public class ObjectHeaderDemo {
    -    public static void main(String[] args) throws InterruptedException {
    -        TimeUnit.SECONDS.sleep(5);
    -        L l = new L();
    -        System.out.println(ClassLayout.parseInstance(l).toPrintable());
    -        synchronized (l) {
    -            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
    -        }
    -        synchronized (l) {
    -            System.out.println("2\n" + ClassLayout.parseInstance(l).toPrintable());
    -        }
    -		}
    -}
    + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=2, args_size=1 + 0: new #2 // class SynchronizedDemo + 3: dup + 4: invokespecial #3 // Method "<init>":()V + 7: astore_1 + 8: aload_1 + 9: invokevirtual #4 // Method lockMethod:()V + 12: return + LineNumberTable: + line 8: 0 + line 9: 8 + line 10: 12 -

    看下运行结果

    -

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

    -

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

    -

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

    -
    public class ObjectHeaderDemo {
    -    public static void main(String[] args) throws InterruptedException {
    -        TimeUnit.SECONDS.sleep(5);
    -        L l = new L();
    -        System.out.println(ClassLayout.parseInstance(l).toPrintable());
    -        synchronized (l) {
    -            System.out.println("1\n" + ClassLayout.parseInstance(l).toPrintable());
    -        }
    -				Thread thread1 = new Thread() {
    -            @Override
    -            public void run() {
    -                try {
    -                    TimeUnit.SECONDS.sleep(5L);
    -                } catch (InterruptedException e) {
    -                    e.printStackTrace();
    -                }
    -                synchronized (l) {
    -                    System.out.println("thread1 获取锁成功");
    -                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
    -                    try {
    -                        TimeUnit.SECONDS.sleep(5L);
    -                    } catch (InterruptedException e) {
    -                        e.printStackTrace();
    -                    }
    -                }
    -            }
    -        };
    -				thread1.start();
    -		}
    -}
    + public synchronized void lockMethod(); + descriptor: ()V + flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED + Code: + stack=2, locals=1, args_size=1 + 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; + 3: ldc #6 // String here i\'m locked + 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V + 8: return + LineNumberTable: + line 13: 0 + line 14: 8 -

    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();
    -    }
    +  public void lockSynchronizedDemo();
    +    descriptor: ()V
    +    flags: (0x0001) ACC_PUBLIC
    +    Code:
    +      stack=2, locals=3, args_size=1
    +         0: aload_0
    +         1: dup
    +         2: astore_1
    +         3: monitorenter
    +         4: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
    +         7: ldc           #8                  // String here lock class
    +         9: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    +        12: aload_1
    +        13: monitorexit
    +        14: goto          22
    +        17: astore_2
    +        18: aload_1
    +        19: monitorexit
    +        20: aload_2
    +        21: athrow
    +        22: return
    +      Exception table:
    +         from    to  target type
    +             4    14    17   any
    +            17    20    17   any
    +      LineNumberTable:
    +        line 17: 0
    +        line 18: 4
    +        line 19: 12
    +        line 20: 22
    +      StackMapTable: number_of_entries = 2
    +        frame_type = 255 /* full_frame */
    +          offset_delta = 17
    +          locals = [ class SynchronizedDemo, class java/lang/Object ]
    +          stack = [ class java/lang/Throwable ]
    +        frame_type = 250 /* chop */
    +          offset_delta = 4
     }
    +SourceFile: "SynchronizedDemo.java"
    -class L { - private boolean myboolean = true; -}
    - -

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

    -

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

    +

    其中lockMethod中可以看到是通过 ACC_SYNCHRONIZED flag 来标记是被 synchronized 修饰,前面的 ACC 应该是 access 的意思,并且通过 ACC_PUBLIC 也可以看出来他们是同一类访问权限关键字来控制的,而修饰类则是通过3: monitorenter13: monitorexit来控制并发,这个是原来就知道,后来看了下才知道修饰方法是不一样的,但是在前期都比较诟病是 synchronized 的性能,像 monitor 也是通过操作系统的mutex lock互斥锁来实现的,相对是比较重的锁,于是在 JDK 1.6 之后对 synchronized 做了一系列优化,包括偏向锁,轻量级锁,并且包括像 ConcurrentHashMap 这类并发集合都有在使用 synchronized 关键字配合 cas 来做并发保护,

    +

    jdk 对于 synchronized 的优化主要在于多重状态锁的升级,最初会使用偏向锁,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
    而当出现线程尝试进入同步块时发现已有偏向锁,并且是其他线程时,会将锁升级成轻量级锁,并且自旋尝试获取锁,如果自旋成功则表示获取轻量级锁成功,否则将会升级成重量级锁进行阻塞,当然这里具体的还很复杂,说的比较浅薄主体还是想将原先的阻塞互斥锁进行轻量化,区分特殊情况进行加锁。

    ]]>
    Java @@ -11707,153 +12018,30 @@ public Result invoke(final Invocation invocation) throws RpcException { i++; } return true; - } - } - return false; - }
    -

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

    -]]>
    - - java - - - java - -
    - - 聊聊 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));
    -                }
    -            }
    -            return instance;
    -        } catch (Throwable t) {
    -            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
    -                    type + ")  could not be instantiated: " + t.getMessage(), t);
    +            }
             }
    -    }
    -

    然后就是创建扩展了,这里如果 wrapperClasses 就会遍历生成wrapper实例,并做 setter 依赖注入,但是这里cachedWrapperClasses的来源还是有点搞不清楚,得再看下 com.alibaba.dubbo.common.extension.ExtensionLoader#loadFile的具体逻辑
    又看了遍新的代码,这个函数被抽出来了

    -
    /**
    -    * test if clazz is a wrapper class
    -    * <p>
    -    * which has Constructor with given class type as its only argument
    -    */
    -   private boolean isWrapperClass(Class<?> clazz) {
    -       try {
    -           clazz.getConstructor(type);
    -           return true;
    -       } catch (NoSuchMethodException e) {
    -           return false;
    -       }
    -   }
    -

    是否是 wrapperClass 其实就看构造函数的。

    + return false; + }
    +

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

    ]]> - Java - Dubbo - RPC - Dubbo - SPI - SPI + java - Java - Dubbo - RPC - SPI + java - 聊聊 Java 的类加载机制一 - /2020/11/08/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/ - 一说到这个主题,想到的应该是双亲委派模型,不过讲的包括但不限于这个,主要内容是参考深入理解 Java 虚拟机书中的介绍,
    一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。

    -
      -
    • 加载

    • -
    -
      -
    1. 通过一个类的全限定名来获取定义此类的二进制字节流
    2. -
    3. 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
    4. -
    5. 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
    6. -
    -
      -
    • 验证

    • -
    -
      -
    1. 文件格式验证
    2. -
    3. 元数据验证
    4. -
    5. 字节码验证
    6. -
    7. 符号引用验证
    8. -
    -
      -
    • 准备

      准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

      -
    • -
    • 解析

      解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程

      -
    • -
    -

    以上验证准备解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

    -
      -
    • 初始化

      类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
    • -
    -

    最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

    + 是何原因竟让两人深夜奔袭十公里 + /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

    ]]>
    - Java - 类加载 + 生活 + + 生活 +
    聊聊 Java 的类加载机制二 @@ -13264,277 +13452,142 @@ 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 {
    -        // Configure actual data sources
    -        Map<String, DataSource> dataSourceMap = new HashMap<>();
    -
    -        
    -        // Configure the first data source
    -        // 使用了默认的Hikari连接池的 DataSource
    -        HikariDataSource dataSource1 = new HikariDataSource();
    -        dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
    -        dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/sharding");
    -        dataSource1.setUsername("username");
    -        dataSource1.setPassword("password");
    -        dataSourceMap.put("ds0", dataSource1);
    -
    -        // Configure student table rule
    -        // 这里是配置分表逻辑,逻辑表是 student,对应真实的表是 student_0 到 student_1, 这个配置方式就是有多少表可以用 student_$->{0..n}
    -        ShardingTableRuleConfiguration studentTableRuleConfig = new ShardingTableRuleConfiguration("student", "ds0.student_$->{0..1}");
    -
    -        // 设置分表字段
    -        studentTableRuleConfig.setTableShardingStrategy(new StandardShardingStrategyConfiguration("user_id", "tableShardingAlgorithm"));
    -
    -
    -        // Configure sharding rule
    -        // 配置 studentTableRuleConfig
    -        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
    -        shardingRuleConfig.getTables().add(studentTableRuleConfig);
    -
    -        // Configure table sharding algorithm
    -        Properties tableShardingAlgorithmrProps = new Properties();
    -        // 算法表达式就是根据 user_id 对 2 进行取模
    -        tableShardingAlgorithmrProps.setProperty("algorithm-expression", "student_${user_id % 2}");
    -        shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps));
    +    聊一下 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,
     
    -        // 然后创建这个 DataSource
    -        return ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), new Properties());
    +	/**
    +	 * The application should run as a servlet-based web application and should start an
    +	 * embedded servlet web server.
    +	 */
    +	SERVLET,
     
    -    }
    -}
    -

    然后我们就可以在使用这个 DataSource 了,先看下这两个表的数据

    -
    @Override
    -    public void run(String... args) {
    -        LOGGER.info("run here");
    -        String sql = "SELECT * FROM student WHERE user_id=? ";
    -        try (
    -                Connection conn = dataSource.getConnection();
    -                PreparedStatement ps = conn.prepareStatement(sql)) {
    -            // 参数就是 user_id,然后也是分表键,对 2 取模就是 1,应该是去 student_1 取数据
    -            ps.setInt(1, 1001);
    +	/**
    +	 * The application should run as a reactive web application and should start an
    +	 * embedded reactive web server.
    +	 */
    +	REACTIVE
     
    -            ResultSet resultSet = ps.executeQuery();
    -            while (resultSet.next()) {
    -                final int id = resultSet.getInt("id");
    -                final String name = resultSet.getString("name");
    -                final int userId = resultSet.getInt("user_id");
    -                final int age = resultSet.getInt("age");
    -                System.out.println("奇数表 id:" + id + " 姓名:" + name
    -                        + " 用户 id:" + userId + " 年龄:" + age );
    -                System.out.println("=============================");
    -            }
    -            // 参数就是 user_id,然后也是分表键,对 2 取模就是 0,应该是去 student_0 取数据
    -            ps.setInt(1, 1000);
    -            resultSet = ps.executeQuery();
    -            while (resultSet.next()) {
    -                final int id = resultSet.getInt("id");
    -                final String name = resultSet.getString("name");
    -                final int userId = resultSet.getInt("user_id");
    -                final int age = resultSet.getInt("age");
    -                System.out.println("偶数表 id:" + id + " 姓名:" + name
    -                        + " 用户 id:" + userId + " 年龄:" + age );
    -                System.out.println("=============================");
    -            }
    -        } catch (SQLException e) {
    -            e.printStackTrace();
    -        }
    -    }
    -

    看下查询结果

    +}
    +

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

    ]]>
    Java + SpringBoot Java - Sharding-Jdbc + Spring + SpringBoot + 自动装配 + AutoConfiguration
    - 聊聊 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();
    -    }
    +    聊聊 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());
     
    -    public synchronized void lockMethod() {
    -        System.out.println("here i'm locked");
    -    }
     
    -    public void lockSynchronizedDemo() {
    -        synchronized (this) {
    -            System.out.println("here lock class");
    -        }
    -    }
    -}
    + 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
     
    -

    然后来查看反编译结果,其实代码(日光)之下并无新事,即使是完全不懂的也可以通过一些词义看出一些意义

    -
      Last modified 2021620; size 729 bytes
    -  MD5 checksum dd9c529863bd7ff839a95481db578ad9
    -  Compiled from "SynchronizedDemo.java"
    -public class SynchronizedDemo
    -  minor version: 0
    -  major version: 53
    -  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    -  this_class: #2                          // SynchronizedDemo
    -  super_class: #9                         // java/lang/Object
    -  interfaces: 0, fields: 0, methods: 4, attributes: 1
    -Constant pool:
    -   #1 = Methodref          #9.#22         // java/lang/Object."<init>":()V
    -   #2 = Class              #23            // SynchronizedDemo
    -   #3 = Methodref          #2.#22         // SynchronizedDemo."<init>":()V
    -   #4 = Methodref          #2.#24         // SynchronizedDemo.lockMethod:()V
    -   #5 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
    -   #6 = String             #27            // here i\'m locked
    -   #7 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
    -   #8 = String             #30            // here lock class
    -   #9 = Class              #31            // java/lang/Object
    -  #10 = Utf8               <init>
    -  #11 = Utf8               ()V
    -  #12 = Utf8               Code
    -  #13 = Utf8               LineNumberTable
    -  #14 = Utf8               main
    -  #15 = Utf8               ([Ljava/lang/String;)V
    -  #16 = Utf8               lockMethod
    -  #17 = Utf8               lockSynchronizedDemo
    -  #18 = Utf8               StackMapTable
    -  #19 = Class              #32            // java/lang/Throwable
    -  #20 = Utf8               SourceFile
    -  #21 = Utf8               SynchronizedDemo.java
    -  #22 = NameAndType        #10:#11        // "<init>":()V
    -  #23 = Utf8               SynchronizedDemo
    -  #24 = NameAndType        #16:#11        // lockMethod:()V
    -  #25 = Class              #33            // java/lang/System
    -  #26 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
    -  #27 = Utf8               here i\'m locked
    -  #28 = Class              #36            // java/io/PrintStream
    -  #29 = NameAndType        #37:#38        // println:(Ljava/lang/String;)V
    -  #30 = Utf8               here lock class
    -  #31 = Utf8               java/lang/Object
    -  #32 = Utf8               java/lang/Throwable
    -  #33 = Utf8               java/lang/System
    -  #34 = Utf8               out
    -  #35 = Utf8               Ljava/io/PrintStream;
    -  #36 = Utf8               java/io/PrintStream
    -  #37 = Utf8               println
    -  #38 = Utf8               (Ljava/lang/String;)V
    -{
    -  public SynchronizedDemo();
    -    descriptor: ()V
    -    flags: (0x0001) ACC_PUBLIC
    -    Code:
    -      stack=1, locals=1, args_size=1
    -         0: aload_0
    -         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
    -         4: return
    -      LineNumberTable:
    -        line 5: 0
     
    -  public static void main(java.lang.String[]);
    -    descriptor: ([Ljava/lang/String;)V
    -    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    -    Code:
    -      stack=2, locals=2, args_size=1
    -         0: new           #2                  // class SynchronizedDemo
    -         3: dup
    -         4: invokespecial #3                  // Method "<init>":()V
    -         7: astore_1
    -         8: aload_1
    -         9: invokevirtual #4                  // Method lockMethod:()V
    -        12: return
    -      LineNumberTable:
    -        line 8: 0
    -        line 9: 8
    -        line 10: 12
    +t1
    +334 10105 nick98  18  1641548939071   最小
    +335 10174 nick94  18  1641548939377
    +336 10129 nick85  18  1641548939442
    +337 10141 nick84  18  1641548939480
    +338 10096 nick74  18  1641548939668
     
    -  public synchronized void lockMethod();
    -    descriptor: ()V
    -    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    -    Code:
    -      stack=2, locals=1, args_size=1
    -         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
    -         3: ldc           #6                  // String here i\'m locked
    -         5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    -         8: return
    -      LineNumberTable:
    -        line 13: 0
    -        line 14: 8
    +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
     
    -  public void lockSynchronizedDemo();
    -    descriptor: ()V
    -    flags: (0x0001) ACC_PUBLIC
    -    Code:
    -      stack=2, locals=3, args_size=1
    -         0: aload_0
    -         1: dup
    -         2: astore_1
    -         3: monitorenter
    -         4: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
    -         7: ldc           #8                  // String here lock class
    -         9: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    -        12: aload_1
    -        13: monitorexit
    -        14: goto          22
    -        17: astore_2
    -        18: aload_1
    -        19: monitorexit
    -        20: aload_2
    -        21: athrow
    -        22: return
    -      Exception table:
    -         from    to  target type
    -             4    14    17   any
    -            17    20    17   any
    -      LineNumberTable:
    -        line 17: 0
    -        line 18: 4
    -        line 19: 12
    -        line 20: 22
    -      StackMapTable: number_of_entries = 2
    -        frame_type = 255 /* full_frame */
    -          offset_delta = 17
    -          locals = [ class SynchronizedDemo, class java/lang/Object ]
    -          stack = [ class java/lang/Throwable ]
    -        frame_type = 250 /* chop */
    -          offset_delta = 4
    -}
    -SourceFile: "SynchronizedDemo.java"
    +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 -

    其中lockMethod中可以看到是通过 ACC_SYNCHRONIZED flag 来标记是被 synchronized 修饰,前面的 ACC 应该是 access 的意思,并且通过 ACC_PUBLIC 也可以看出来他们是同一类访问权限关键字来控制的,而修饰类则是通过3: monitorenter13: monitorexit来控制并发,这个是原来就知道,后来看了下才知道修饰方法是不一样的,但是在前期都比较诟病是 synchronized 的性能,像 monitor 也是通过操作系统的mutex lock互斥锁来实现的,相对是比较重的锁,于是在 JDK 1.6 之后对 synchronized 做了一系列优化,包括偏向锁,轻量级锁,并且包括像 ConcurrentHashMap 这类并发集合都有在使用 synchronized 关键字配合 cas 来做并发保护,

    -

    jdk 对于 synchronized 的优化主要在于多重状态锁的升级,最初会使用偏向锁,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
    而当出现线程尝试进入同步块时发现已有偏向锁,并且是其他线程时,会将锁升级成轻量级锁,并且自旋尝试获取锁,如果自旋成功则表示获取轻量级锁成功,否则将会升级成重量级锁进行阻塞,当然这里具体的还很复杂,说的比较浅薄主体还是想将原先的阻塞互斥锁进行轻量化,区分特殊情况进行加锁。

    +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 - Synchronized - 偏向锁 - 轻量级锁 - 重量级锁 - 自旋 + Sharding-Jdbc
    @@ -13652,91 +13705,8 @@ void ReadView::prepare(trx_id_t id) {
  • insert into table values (...);
  • update table set ? where ?;
  • delete from table where ?;
  • -
-

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

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

-]]>
- - Mysql - C - 数据结构 - 源码 - Mysql - - - mysql - 数据结构 - 源码 - mvcc - read view - gap lock - next-key lock - 幻读 - -
- - 聊聊 mysql 的 MVCC - /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/ - 很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢

-

其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行

-

腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下

-
/* Precise data types for system columns and the length of those columns;
-NOTE: the values must run from 0 up in the order given! All codes must
-be less than 256 */
-#define DATA_ROW_ID 0     /* row id: a 48-bit integer */
-#define DATA_ROW_ID_LEN 6 /* stored length for row id */
-
-/** Transaction id: 6 bytes */
-constexpr size_t DATA_TRX_ID = 1;
-
-/** Transaction ID type size in bytes. */
-constexpr size_t DATA_TRX_ID_LEN = 6;
-
-/** Rollback data pointer: 7 bytes */
-constexpr size_t DATA_ROLL_PTR = 2;
-
-/** Rollback data pointer type size in bytes. */
-constexpr size_t DATA_ROLL_PTR_LEN = 7;
- -

一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了

-

一个是 DATA_TRX_ID,这个表示这条记录的事务 ID

-

还有一个是 DATA_ROLL_PTR 指向回滚段的指针

-

指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,

-

m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id

-

m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id

-

m_ids, 这个是 read view 创建时所有的活跃事务 id 数组

-

m_creator_trx_id 这个是当前记录的创建事务 id

-

判断事务的可见性主要的逻辑是这样,

-
    -
  1. 当记录的事务 id 小于最小活跃事务 id,说明是可见的,
  2. -
  3. 如果记录的事务 id 等于当前事务 id,说明是自己的更改,可见
  4. -
  5. 如果记录的事务 id 大于最大的活跃事务 id, 不可见
  6. -
  7. 如果记录的事务 id 介于 m_low_limit_idm_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
    具体的代码捞一下看看
    /** Check whether the changes by id are visible.
    -  @param[in]	id	transaction id to check against the view
    -  @param[in]	name	table name
    -  @return whether the view sees the modifications of id. */
    -  bool changes_visible(trx_id_t id, const table_name_t &name) const
    -      MY_ATTRIBUTE((warn_unused_result)) {
    -    ut_ad(id > 0);
    -
    -    if (id < m_up_limit_id || id == m_creator_trx_id) {
    -      return (true);
    -    }
    -
    -    check_trx_id_sanity(id, name);
    -
    -    if (id >= m_low_limit_id) {
    -      return (false);
    -
    -    } else if (m_ids.empty()) {
    -      return (true);
    -    }
    -
    -    const ids_t::value_type *p = m_ids.data();
    -
    -    return (!std::binary_search(p, p + m_ids.size(), id));
    -  }
    -剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据
  8. -
+ +

除了第一条是 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 @@ -13751,6 +13721,38 @@ constexpr size_t DATA_ROLL_PTR_LEN 源码 mvcc read view + gap lock + next-key lock + 幻读 + +
+ + 聊聊 Sharding-Jdbc 的简单原理初篇 + /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
就是我在执行 query 的时候犯过一个比较难发现的错误,

+
ResultSet resultSet = ps.executeQuery(sql);
+

实际上应该是

+
ResultSet resultSet = ps.executeQuery();
+

而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里

+
@Override
+public final ResultSet executeQuery(final String sql) throws SQLException {
+    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
+}
+

这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

在前一篇的 demo 中的

+
Connection conn = dataSource.getConnection();
+

其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement

+
PreparedStatement ps = conn.prepareStatement(sql)
+

就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行

+
ResultSet resultSet = ps.executeQuery();
+

然后获得结果
org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

+

其实像 mybatis 也是基于这样去实现的

+]]>
+ + Java + + + Java + Sharding-Jdbc
@@ -13818,55 +13820,6 @@ $ procedure - - 聊一下 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 - -
聊聊 redis 缓存的应用问题 /2021/01/31/%E8%81%8A%E8%81%8A-redis-%E7%BC%93%E5%AD%98%E7%9A%84%E5%BA%94%E7%94%A8%E9%97%AE%E9%A2%98/ @@ -13898,6 +13851,81 @@ app.setWebAp 互斥锁 + + 聊聊Java中的单例模式 + /2019/12/21/%E8%81%8A%E8%81%8AJava%E4%B8%AD%E7%9A%84%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/ + 这是个 Java 面试的高频问题,我也遇到过,以往都是觉得这类题没意思,网上一搜一大堆,也不愿意记,其实说回来,主要还是没静下心来好好去理解,今天无意中看到一个课程,基本帮我把一些疑惑的点讲清楚了,首先单例是啥意思,这个其实是有范围一说,比如我起了个Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的

+

饿汉模式

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

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

+

饱汉模式

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

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

+

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

+

嵌套类

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

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

+

枚举单例

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

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

+]]>
+ + Java + Design Patterns + Singleton + + + 设计模式 + Design Patterns + 单例 + Singleton + +
聊聊 SpringBoot 自动装配 /2021/07/11/%E8%81%8A%E8%81%8ASpringBoot-%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ @@ -14265,115 +14293,40 @@ app.setWebAp
git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7

其实注意 commit id 旁边的红框,这个是有tag 的,可以直接

git checkout icu4c-64
-

PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

-
brew reinstall ./icu4c.rb
-

然后就是第五步,切换版本

-
brew switch icu4c 64.2
-

最后把分支切回来

-
git checkout master
-

是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

-
# zsh
-function hiicu64() {
-  local last_dir=$(pwd)
-
-  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
-  git checkout icu4c-4
-  brew reinstall ./icu4c.rb
-  brew switch icu4c 64.2
-  git checkout master
-
-  cd $last_dir
-}
-

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

-]]> - - Mac - PHP - Homebrew - PHP - icu4c - - - Mac - PHP - Homebrew - icu4c - zsh - -
- - 聊聊Java中的单例模式 - /2019/12/21/%E8%81%8A%E8%81%8AJava%E4%B8%AD%E7%9A%84%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/ - 这是个 Java 面试的高频问题,我也遇到过,以往都是觉得这类题没意思,网上一搜一大堆,也不愿意记,其实说回来,主要还是没静下心来好好去理解,今天无意中看到一个课程,基本帮我把一些疑惑的点讲清楚了,首先单例是啥意思,这个其实是有范围一说,比如我起了个Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的

-

饿汉模式

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

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

-

饱汉模式

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

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

-

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

-

嵌套类

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

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

-

枚举单例

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

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

+

PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

+
brew reinstall ./icu4c.rb
+

然后就是第五步,切换版本

+
brew switch icu4c 64.2
+

最后把分支切回来

+
git checkout master
+

是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

+
# zsh
+function hiicu64() {
+  local last_dir=$(pwd)
+
+  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+  git checkout icu4c-4
+  brew reinstall ./icu4c.rb
+  brew switch icu4c 64.2
+  git checkout master
+
+  cd $last_dir
+}
+

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

]]>
- Java - Design Patterns - Singleton + Mac + PHP + Homebrew + PHP + icu4c - 设计模式 - Design Patterns - 单例 - Singleton + Mac + PHP + Homebrew + icu4c + zsh
@@ -14495,23 +14448,6 @@ app.setWebAp WeakReference - - 聊聊如何识别和意识到日常生活中的各类危险 - /2021/06/06/%E8%81%8A%E8%81%8A%E5%A6%82%E4%BD%95%E8%AF%86%E5%88%AB%E5%92%8C%E6%84%8F%E8%AF%86%E5%88%B0%E6%97%A5%E5%B8%B8%E7%94%9F%E6%B4%BB%E4%B8%AD%E7%9A%84%E5%90%84%E7%B1%BB%E5%8D%B1%E9%99%A9/ - 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。

-]]>
- - 生活 - - - 生活 - 糟心事 - 扶梯 - 踩踏 - 安全 - 电瓶车 - -
聊聊厦门旅游的好与不好 /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ @@ -14542,6 +14478,23 @@ app.setWebAp 海蛎煎 + + 聊聊如何识别和意识到日常生活中的各类危险 + /2021/06/06/%E8%81%8A%E8%81%8A%E5%A6%82%E4%BD%95%E8%AF%86%E5%88%AB%E5%92%8C%E6%84%8F%E8%AF%86%E5%88%B0%E6%97%A5%E5%B8%B8%E7%94%9F%E6%B4%BB%E4%B8%AD%E7%9A%84%E5%90%84%E7%B1%BB%E5%8D%B1%E9%99%A9/ + 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。

+]]>
+ + 生活 + + + 生活 + 糟心事 + 扶梯 + 踩踏 + 安全 + 电瓶车 + +
聊聊我刚学会的应用诊断方法 /2020/05/22/%E8%81%8A%E8%81%8A%E6%88%91%E5%88%9A%E5%AD%A6%E4%BC%9A%E7%9A%84%E5%BA%94%E7%94%A8%E8%AF%8A%E6%96%AD%E6%96%B9%E6%B3%95/ @@ -14609,201 +14562,37 @@ app.setWebAp 2PC 3PC - - - 聊聊最近平淡的生活之又聊通勤 - /2021/11/07/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB/ - 一直以来过着特别平淡普通的生活,不过大多数人应该都这样吧,也许有些人可以把平凡的生活过得精彩,最简单的说明就是朋友圈吧,看我一年的盆友圈虽然在发,不过大概 90%的都是发发跑步的打卡,偶尔会有稀稀拉拉的点赞,天天上班,也不喜欢发什么状态,觉得没什么人关注,索性不发。

-

只是这么平淡的生活就有一些自己比较心烦纠结的,之前有提到过的交通,最近似乎又发现了一点,就真相总是让人跌破眼镜,以前觉得我可能是胆子比较小,所以会觉得怎么路上这些电瓶都是这么肆无忌惮的往我冲过来,后面慢慢有一种借用电视剧读心神探的概念,安全距离,觉得大部分人跟我一样,骑电瓶车什么的总还是有个安全距离,只是可能这个安全距离对于不同的人不一样,那些骑电瓶车的潜意识里的安全距离是非常短,所以经常会骑车离着你非常近才会刹车,但是这个安全距离理论最近又被推翻了,因为经历过几次电瓶车就是已经跟你有身体接触了,但是没到把人撞倒的程度,似乎这些骑电瓶车的觉得步行的行人在人行道上是空气,蹭一下也无所谓,反正不能挡我的路,总感觉要不是我在前面骑自行车太慢挡着电瓶车,不然他们都能起飞去干掉 F35 解放湾湾了;

-

另一个问题应该是说我们交通规则普及的太少,虽然我们没有路权这个名词概念,但是其实是有这个优先级的,包括像杭州是以公交车在人行道礼让行人闻名的,其实这个文明的行为只限于人行道在直行路中间的,大部分在十字路口,右转的公交车很少会让直行人行道的,前提是直行的绿灯的时候,特别是像公交车这样,车身特别长,右转的时候会有比较大的死角,如果是公交车先转,行人或者自行车很容易被卷进去,非常危险的,私家车就更不用说了,反正右转即使人行道上人非常多要转的也是一秒都不等,所以我自己在开车的时候是尽量在右转的时候等人行道上的行人或者骑车的走完,因为总会觉得我是不是有点双标,骑车走路的时候希望开车的能按规则让我,自己开车的时候又想赶紧开走,所以在开车的时候尽量做到让行车和骑车的。

-

还有个其实是写着写着想起来的,比如我骑车左转的时候,因为我是左转到对角那就到了,跟那些左转后要再直行的不一样,我们应该在学车的时候也学过,超车要从左边超,但是往往那些骑电瓶车的在左转的时候会从我右边超过来再往左边撇过去,如果留的空间大还好,有些电瓶车就是如果车头超过了就不管他的车屁股,如果我不减速,自行车就被刮倒了,可能的确是别人就不是人,只要不把你撞倒就无所谓,反正为了你自己不被撞倒你肯定会让的。

-]]>
- - 生活 - - - 生活 - 糟心事 - 规则 - 电瓶车 - 骑车 - -
- - 聊聊 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 - -
- - 聊聊最近平淡的生活之看《神探狄仁杰》 - /2021/12/19/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E3%80%8A%E7%A5%9E%E6%8E%A2%E7%8B%84%E4%BB%81%E6%9D%B0%E3%80%8B/ - 其实最近看的不止这一部,前面看了《继承者们》,《少年包青天》这些,就一起聊下,其中看《继承者们》算是个人比较喜欢,以前就有这种看剧的习惯,这个跟《一生一世》里任嘉伦说自己看《寻秦记》看了几十遍一样,我看喜欢的剧也基本上会看不止五遍,继承者们是有帅哥美女看,而且印象中剧情也挺甜的,一般情况下最好是已经有点遗忘剧情了,因为我个人觉得看剧分两种,无聊了又心情不太好,可以看些这类轻松又看过的剧,可以不完全专心地看剧,另外有心情专心看的时候,可以看一些需要思考,一些探案类的或者烧脑类。
最近看了《神探狄仁杰》,因为跟前面看的《少年包青天》都是这类古装探案剧,正好有些感想,《少年包青天》算是儿时阴影,本来是不太会去看的,正好有一次有机会跟 LD 一起看了会就也觉得比较有意思就看了下去,不得不说,以前的这些剧还是很不错的,包括剧情和演员,第一部一共是 40 集,看的过程中也发现了大概是五个案子,平均八集一个案子,整体节奏还是比较慢的,但是基本每个案子其实都是构思得很巧妙,很久以前看过但是现在基本不太记得剧情了,每个案子在前面几集的时候基本都猜不到犯案逻辑,但是在看了狄仁杰之后,发现两部剧也有比较大的差别,少年包青天相对来说逻辑性会更强一些,个人主观觉得推理的严谨性更高,可能剧本打磨上更将就一下,而狄仁杰因为要提现他的个人强项,不比少年包青天中有公孙策一时瑜亮的情节,狄仁杰中分工明确,李元芳是个武力担当,曾泰是捧哏的,相对来说是狄仁杰在案子里从始至终地推进案情,有些甚至有些玄乎,会有一些跳脱跟不合理,有些像是狄仁杰的奇遇,不过这些想法是私人的观点,并不是想要评孰优孰劣;第二个感受是不知道是不是年代关系,特别是少年包青天,每个案件的大 boss 基本都不是个完全的坏人,甚至都是比较情有可原的可怜人,因为一些特殊原因,而好几个都是包拯身边的人,这一点其实跟狄仁杰里第一个使团惊魂案件比较类似,虎敬晖也是个人物形象比较丰满的角色,不是个标签化的淡薄形象,跟金木兰的感情和反叛行动在最后都说明的缘由,而且也有随着跟狄仁杰一起办案被其影响感化,最终为了救狄仁杰而被金木兰所杀,只是这样金木兰这个角色就会有些偏执和符号化,当然剧本肯定不是能面面俱到,这样的剧本已经比现在很多流量剧的好很多了。还想到了前阵子看的《指环王》中的白袍萨鲁曼在剧中也是个比较单薄的角色,这样的电影彪炳影史也没办法把个个人物都设计得完整有血有肉,或者说这本来也是应该有侧重点,当然其实我也不觉得指环王就是绝对的最好的,因为相对来说故事情节的复杂性等真的不如西游记,只是在 86 版之后的各种乱七八糟的翻牌和乱拍已经让这个真正的王者神话故事有点力不从心,这里边有部西游记后传是个人还比较喜欢的,虽然武打动作比较鬼畜,但是剧情基本是无敌的,在西游的架构上衍生出来这么完整丰富的故事,人物角色也都有各自的出彩点。
说回狄仁杰,在这之前也看过徐克拍的几部狄仁杰的电影版,第一部刘德华拍得相对完成度更高,故事性也可圈可点,后面几部就是剧情拉胯,靠特效拉回来一点分,虽说这个也是所谓的狄仁杰宇宙的构思在里面但是现在来看基本是跟西游那些差不多,完全没有整体性可言,打一枪换一个地方,演员也没有延续性,剧情也是前后跳脱,没什么关联跟承上启下,导致质量层次不一,更不用谈什么狄仁杰宇宙了,不过这个事情也是难说,原因很多,现在资本都是更加趋利的,一些需要更长久时间才能有回报的投资是很难获得资本青睐,所以只能将重心投给选择一些流量明星,而本来应该将资源投给剧本打磨的基本就没了,再深入说也没意义了,社会现状就是这样。
还有一点感想是,以前的剧里的拍摄环境还是比较惨的,看着一些房子,甚至皇宫都是比较破旧的,地面还是石板这种,想想以前的演员的环境再想想现在的,比如成龙说的,以前他拍剧就是啪摔了,问这条有没有过,过了就直接送医院,而不是现在可能手蹭破点皮就大叫,甚至还有饭圈这些破事。

-]]>
- - 生活 - - - 生活 - 看剧 - -
- - 聊聊最近平淡的生活之看看老剧 - /2021/11/21/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E7%9C%8B%E8%80%81%E5%89%A7/ - 最近因为也没什么好看的新剧和综艺所以就看看一些以前看过的老剧,我是个非常念旧的人吧,很多剧都会反反复复地看,一方面之前看过觉得好看的的确是一直记着,还有就是平时工作完了回来就想能放松下,剧情太纠结的,太烧脑的都不喜欢,也就是我常挂在口头的不喜欢看费脑子的剧,跟我不喜欢狼人杀的原因也类似。

-

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

-

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

-

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

-

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

-]]>
- - 生活 - - - 生活 - 看剧 - -
- - 聊一下 RocketMQ 的消息存储之 MMAP - /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ - 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

-
read(file, tmp_buf, len);
-write(socket, tmp_buf, len);
- - - -

vms95Z

-

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

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

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

-

mmap

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

-
-

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

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

I68mFx

-
-

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

-

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

-

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

-

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

-

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

-

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

-

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

-
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
-    perror("kernel lease set signal");
-    return -1;
-}
-/* l_type can be F_RDLCK F_WRLCK */
-if(fcntl(fd, F_SETLEASE, l_type)){
-    perror("kernel lease set type");
-    return -1;
-}
+
+ + 聊聊我的远程工作体验 + /2022/06/26/%E8%81%8A%E8%81%8A%E6%88%91%E7%9A%84%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C%E4%BD%93%E9%AA%8C/ + 发生疫情之后,因为正好是春节假期,假期结束的时候还不具备回工作地点办公的条件,所以史无前例地开始了远程办公,以前对于远程办公的概念还停留在国外一些有“格局”的企业会允许员工远程办公,当然对于远程办公这个事情本身我个人也并不是全然支持的态度,其中涉及到很多方面,首先远程办公并不代表就是不用去办公地点上班,可以在家里摸鱼,相对能够得到较高报酬的能够远程办公的企业需要在远程办公期间能够有高效的产出,并且也需要像在公司办公地点一样,能随时被联系到,第二点是薪资福利之外的社保公积金,除非薪资相比非远程办公的企业高出比较多,不然没法 cover 企业额外缴纳的社保公积金,听说有部分企业也会远程办公点给员工上社保,但是毕竟能做到这点的很少,在允许远程办公的企业数量这个本来就不大的基数里,大概率是少之又少了。
疫情这个特殊原因开始的远程办公体验也算是开了个之前不太容易开的头,也跟我前面说的第一点有关系,大部分的企业也会担心员工远程办公是否有与在公司办公地点办公一样或者比较接近的办公效率。同时我们在开始远程办公的时候也碰到了因为原先没做过相应准备而导致的许多问题,首先基础设施上就有几个问题,第一个是办公电脑的问题,因为整个公司各个部门的工作性质和内容不同,并不是每个部门都是配笔记本的,或者有些部门并不需要想研发一样带上电脑 on call,所以那么使用台式机或者没有将笔记本带回家的则需要自己准备电脑或者让公司邮寄。第二个是远程网络的问题,像我们公司有研发团队平时也已经准备好了 vpn,但是在这种时候我们没准备好的是 vpn 带宽,毕竟平时只会偶尔有需要连一下 vpn 到公司网络,像这样大量员工都需要连接 vpn 进行工作的话,我们的初步体验就是网络卡的不行,一些远程调试工作没法进行,并且还有一些问题是可能只有我们研发会碰到,比如我们的线上测试服务器网络在办公地点是有网络打通的,但是我们在家就没办法连接,还有就是沟通效率相关,因为这是个全国性的情况,线上会议工具原先都是为特定用户使用,并且视频音频实时传输所需要的带宽质量要求也是比较高的,大规模的远程会议沟通需求让这些做线上会议的服务也算是碰上了类似双十一的大考了,我们是先后使用了 zoom,腾讯会议跟钉钉视频会议,使用体验上来说是 zoom 做得相对比较成熟和稳定,不过后面腾讯会议跟钉钉视频会议也开始赶上来。
前面说的这几个点都是得有远程办公经验的公司才会提前做好相应的准备,比如可以做动态网络扩容,能够在需要大量员工连接公司网络的情况下快速响应提升带宽,另一些则是偏软性的,比如如如何在远程办公的条件下控制我们项目进度,如果保证沟通信息是否能像当面沟通那样准确传达,这方面其实我的经验也是边实操边优化的,最开始我们可能为了高效同步消息,会频繁的使用视频会议沟通,这其实并不能解决沟通效率问题,反而打扰了正常的工作,后续我们在特别是做项目过程中就通过相对简单的每日早会和日报机制,将每天的进度与问题风险点进行同步确认,只与相关直接干系人进行视频电话沟通确认,并且要保持一个思维,即远程办公比较适宜的是相对比较成熟的团队,平常工作和合作都已经有默契或者说规则并且能够遵守,在这个前提下,将目光专注于做的事情而不是管到具体的人有没有全天都在高效工作。同样也希望国内的环境能够有更多的远程火种成长起来,让它成为更好的工作方式,WLB!

]]>
- MQ - RocketMQ - 消息队列 + 生活 - MQ - 消息队列 - RocketMQ + 生活 + 远程办公 + +
+ + 聊聊最近平淡的生活之又聊通勤 + /2021/11/07/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB/ + 一直以来过着特别平淡普通的生活,不过大多数人应该都这样吧,也许有些人可以把平凡的生活过得精彩,最简单的说明就是朋友圈吧,看我一年的盆友圈虽然在发,不过大概 90%的都是发发跑步的打卡,偶尔会有稀稀拉拉的点赞,天天上班,也不喜欢发什么状态,觉得没什么人关注,索性不发。

+

只是这么平淡的生活就有一些自己比较心烦纠结的,之前有提到过的交通,最近似乎又发现了一点,就真相总是让人跌破眼镜,以前觉得我可能是胆子比较小,所以会觉得怎么路上这些电瓶都是这么肆无忌惮的往我冲过来,后面慢慢有一种借用电视剧读心神探的概念,安全距离,觉得大部分人跟我一样,骑电瓶车什么的总还是有个安全距离,只是可能这个安全距离对于不同的人不一样,那些骑电瓶车的潜意识里的安全距离是非常短,所以经常会骑车离着你非常近才会刹车,但是这个安全距离理论最近又被推翻了,因为经历过几次电瓶车就是已经跟你有身体接触了,但是没到把人撞倒的程度,似乎这些骑电瓶车的觉得步行的行人在人行道上是空气,蹭一下也无所谓,反正不能挡我的路,总感觉要不是我在前面骑自行车太慢挡着电瓶车,不然他们都能起飞去干掉 F35 解放湾湾了;

+

另一个问题应该是说我们交通规则普及的太少,虽然我们没有路权这个名词概念,但是其实是有这个优先级的,包括像杭州是以公交车在人行道礼让行人闻名的,其实这个文明的行为只限于人行道在直行路中间的,大部分在十字路口,右转的公交车很少会让直行人行道的,前提是直行的绿灯的时候,特别是像公交车这样,车身特别长,右转的时候会有比较大的死角,如果是公交车先转,行人或者自行车很容易被卷进去,非常危险的,私家车就更不用说了,反正右转即使人行道上人非常多要转的也是一秒都不等,所以我自己在开车的时候是尽量在右转的时候等人行道上的行人或者骑车的走完,因为总会觉得我是不是有点双标,骑车走路的时候希望开车的能按规则让我,自己开车的时候又想赶紧开走,所以在开车的时候尽量做到让行车和骑车的。

+

还有个其实是写着写着想起来的,比如我骑车左转的时候,因为我是左转到对角那就到了,跟那些左转后要再直行的不一样,我们应该在学车的时候也学过,超车要从左边超,但是往往那些骑电瓶车的在左转的时候会从我右边超过来再往左边撇过去,如果留的空间大还好,有些电瓶车就是如果车头超过了就不管他的车屁股,如果我不减速,自行车就被刮倒了,可能的确是别人就不是人,只要不把你撞倒就无所谓,反正为了你自己不被撞倒你肯定会让的。

+]]>
+ + 生活 + + + 生活 + 糟心事 + 规则 + 电瓶车 + 骑车
@@ -14811,6 +14600,36 @@ 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/ + 其实最近看的不止这一部,前面看了《继承者们》,《少年包青天》这些,就一起聊下,其中看《继承者们》算是个人比较喜欢,以前就有这种看剧的习惯,这个跟《一生一世》里任嘉伦说自己看《寻秦记》看了几十遍一样,我看喜欢的剧也基本上会看不止五遍,继承者们是有帅哥美女看,而且印象中剧情也挺甜的,一般情况下最好是已经有点遗忘剧情了,因为我个人觉得看剧分两种,无聊了又心情不太好,可以看些这类轻松又看过的剧,可以不完全专心地看剧,另外有心情专心看的时候,可以看一些需要思考,一些探案类的或者烧脑类。
最近看了《神探狄仁杰》,因为跟前面看的《少年包青天》都是这类古装探案剧,正好有些感想,《少年包青天》算是儿时阴影,本来是不太会去看的,正好有一次有机会跟 LD 一起看了会就也觉得比较有意思就看了下去,不得不说,以前的这些剧还是很不错的,包括剧情和演员,第一部一共是 40 集,看的过程中也发现了大概是五个案子,平均八集一个案子,整体节奏还是比较慢的,但是基本每个案子其实都是构思得很巧妙,很久以前看过但是现在基本不太记得剧情了,每个案子在前面几集的时候基本都猜不到犯案逻辑,但是在看了狄仁杰之后,发现两部剧也有比较大的差别,少年包青天相对来说逻辑性会更强一些,个人主观觉得推理的严谨性更高,可能剧本打磨上更将就一下,而狄仁杰因为要提现他的个人强项,不比少年包青天中有公孙策一时瑜亮的情节,狄仁杰中分工明确,李元芳是个武力担当,曾泰是捧哏的,相对来说是狄仁杰在案子里从始至终地推进案情,有些甚至有些玄乎,会有一些跳脱跟不合理,有些像是狄仁杰的奇遇,不过这些想法是私人的观点,并不是想要评孰优孰劣;第二个感受是不知道是不是年代关系,特别是少年包青天,每个案件的大 boss 基本都不是个完全的坏人,甚至都是比较情有可原的可怜人,因为一些特殊原因,而好几个都是包拯身边的人,这一点其实跟狄仁杰里第一个使团惊魂案件比较类似,虎敬晖也是个人物形象比较丰满的角色,不是个标签化的淡薄形象,跟金木兰的感情和反叛行动在最后都说明的缘由,而且也有随着跟狄仁杰一起办案被其影响感化,最终为了救狄仁杰而被金木兰所杀,只是这样金木兰这个角色就会有些偏执和符号化,当然剧本肯定不是能面面俱到,这样的剧本已经比现在很多流量剧的好很多了。还想到了前阵子看的《指环王》中的白袍萨鲁曼在剧中也是个比较单薄的角色,这样的电影彪炳影史也没办法把个个人物都设计得完整有血有肉,或者说这本来也是应该有侧重点,当然其实我也不觉得指环王就是绝对的最好的,因为相对来说故事情节的复杂性等真的不如西游记,只是在 86 版之后的各种乱七八糟的翻牌和乱拍已经让这个真正的王者神话故事有点力不从心,这里边有部西游记后传是个人还比较喜欢的,虽然武打动作比较鬼畜,但是剧情基本是无敌的,在西游的架构上衍生出来这么完整丰富的故事,人物角色也都有各自的出彩点。
说回狄仁杰,在这之前也看过徐克拍的几部狄仁杰的电影版,第一部刘德华拍得相对完成度更高,故事性也可圈可点,后面几部就是剧情拉胯,靠特效拉回来一点分,虽说这个也是所谓的狄仁杰宇宙的构思在里面但是现在来看基本是跟西游那些差不多,完全没有整体性可言,打一枪换一个地方,演员也没有延续性,剧情也是前后跳脱,没什么关联跟承上启下,导致质量层次不一,更不用谈什么狄仁杰宇宙了,不过这个事情也是难说,原因很多,现在资本都是更加趋利的,一些需要更长久时间才能有回报的投资是很难获得资本青睐,所以只能将重心投给选择一些流量明星,而本来应该将资源投给剧本打磨的基本就没了,再深入说也没意义了,社会现状就是这样。
还有一点感想是,以前的剧里的拍摄环境还是比较惨的,看着一些房子,甚至皇宫都是比较破旧的,地面还是石板这种,想想以前的演员的环境再想想现在的,比如成龙说的,以前他拍剧就是啪摔了,问这条有没有过,过了就直接送医院,而不是现在可能手蹭破点皮就大叫,甚至还有饭圈这些破事。

+]]>
+ + 生活 + + + 生活 + 看剧 + +
+ + 聊聊最近平淡的生活之看看老剧 + /2021/11/21/%E8%81%8A%E8%81%8A%E6%9C%80%E8%BF%91%E5%B9%B3%E6%B7%A1%E7%9A%84%E7%94%9F%E6%B4%BB%E4%B9%8B%E7%9C%8B%E7%9C%8B%E8%80%81%E5%89%A7/ + 最近因为也没什么好看的新剧和综艺所以就看看一些以前看过的老剧,我是个非常念旧的人吧,很多剧都会反反复复地看,一方面之前看过觉得好看的的确是一直记着,还有就是平时工作完了回来就想能放松下,剧情太纠结的,太烧脑的都不喜欢,也就是我常挂在口头的不喜欢看费脑子的剧,跟我不喜欢狼人杀的原因也类似。

+

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

+

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

+

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

+

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

]]>
生活 @@ -14840,32 +14659,19 @@ t2
- 聊聊 Sharding-Jdbc 的简单原理初篇 - /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ - 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
就是我在执行 query 的时候犯过一个比较难发现的错误,

-
ResultSet resultSet = ps.executeQuery(sql);
-

实际上应该是

-
ResultSet resultSet = ps.executeQuery();
-

而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里

-
@Override
-public final ResultSet executeQuery(final String sql) throws SQLException {
-    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
-}
-

这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

在前一篇的 demo 中的

-
Connection conn = dataSource.getConnection();
-

其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement

-
PreparedStatement ps = conn.prepareStatement(sql)
-

就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行

-
ResultSet resultSet = ps.executeQuery();
-

然后获得结果
org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

-

其实像 mybatis 也是基于这样去实现的

+ 聊聊这次换车牌及其他 + /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 + 生活 - Java - Sharding-Jdbc + 生活 + 换车牌
@@ -14900,16 +14706,72 @@ t2 - 聊聊我的远程工作体验 - /2022/06/26/%E8%81%8A%E8%81%8A%E6%88%91%E7%9A%84%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C%E4%BD%93%E9%AA%8C/ - 发生疫情之后,因为正好是春节假期,假期结束的时候还不具备回工作地点办公的条件,所以史无前例地开始了远程办公,以前对于远程办公的概念还停留在国外一些有“格局”的企业会允许员工远程办公,当然对于远程办公这个事情本身我个人也并不是全然支持的态度,其中涉及到很多方面,首先远程办公并不代表就是不用去办公地点上班,可以在家里摸鱼,相对能够得到较高报酬的能够远程办公的企业需要在远程办公期间能够有高效的产出,并且也需要像在公司办公地点一样,能随时被联系到,第二点是薪资福利之外的社保公积金,除非薪资相比非远程办公的企业高出比较多,不然没法 cover 企业额外缴纳的社保公积金,听说有部分企业也会远程办公点给员工上社保,但是毕竟能做到这点的很少,在允许远程办公的企业数量这个本来就不大的基数里,大概率是少之又少了。
疫情这个特殊原因开始的远程办公体验也算是开了个之前不太容易开的头,也跟我前面说的第一点有关系,大部分的企业也会担心员工远程办公是否有与在公司办公地点办公一样或者比较接近的办公效率。同时我们在开始远程办公的时候也碰到了因为原先没做过相应准备而导致的许多问题,首先基础设施上就有几个问题,第一个是办公电脑的问题,因为整个公司各个部门的工作性质和内容不同,并不是每个部门都是配笔记本的,或者有些部门并不需要想研发一样带上电脑 on call,所以那么使用台式机或者没有将笔记本带回家的则需要自己准备电脑或者让公司邮寄。第二个是远程网络的问题,像我们公司有研发团队平时也已经准备好了 vpn,但是在这种时候我们没准备好的是 vpn 带宽,毕竟平时只会偶尔有需要连一下 vpn 到公司网络,像这样大量员工都需要连接 vpn 进行工作的话,我们的初步体验就是网络卡的不行,一些远程调试工作没法进行,并且还有一些问题是可能只有我们研发会碰到,比如我们的线上测试服务器网络在办公地点是有网络打通的,但是我们在家就没办法连接,还有就是沟通效率相关,因为这是个全国性的情况,线上会议工具原先都是为特定用户使用,并且视频音频实时传输所需要的带宽质量要求也是比较高的,大规模的远程会议沟通需求让这些做线上会议的服务也算是碰上了类似双十一的大考了,我们是先后使用了 zoom,腾讯会议跟钉钉视频会议,使用体验上来说是 zoom 做得相对比较成熟和稳定,不过后面腾讯会议跟钉钉视频会议也开始赶上来。
前面说的这几个点都是得有远程办公经验的公司才会提前做好相应的准备,比如可以做动态网络扩容,能够在需要大量员工连接公司网络的情况下快速响应提升带宽,另一些则是偏软性的,比如如如何在远程办公的条件下控制我们项目进度,如果保证沟通信息是否能像当面沟通那样准确传达,这方面其实我的经验也是边实操边优化的,最开始我们可能为了高效同步消息,会频繁的使用视频会议沟通,这其实并不能解决沟通效率问题,反而打扰了正常的工作,后续我们在特别是做项目过程中就通过相对简单的每日早会和日报机制,将每天的进度与问题风险点进行同步确认,只与相关直接干系人进行视频电话沟通确认,并且要保持一个思维,即远程办公比较适宜的是相对比较成熟的团队,平常工作和合作都已经有默契或者说规则并且能够遵守,在这个前提下,将目光专注于做的事情而不是管到具体的人有没有全天都在高效工作。同样也希望国内的环境能够有更多的远程火种成长起来,让它成为更好的工作方式,WLB!

+ 记录下 Java Stream 的一些高效操作 + /2022/05/15/%E8%AE%B0%E5%BD%95%E4%B8%8B-Java-Lambda-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%AB%98%E6%95%88%E6%93%8D%E4%BD%9C/ + 我们日常在代码里处理一些集合逻辑的时候用到 Stream 其实还挺多的,普通的取值过滤集合一般都是结合 ide 的提示就能搞定了,但是有些不太常用的就在这记录下,争取后面都更新记录下来。

+

自定义 distinctByKey 对结果进行去重

stream 中自带的 distinct 只能对元素进行去重
比如下面代码

+
public static void main(String[] args) {
+        List<Integer> list = new ArrayList<>();
+        list.add(1);
+        list.add(1);
+        list.add(2);
+        list = list.stream().distinct().collect(Collectors.toList());
+        System.out.println(list);
+    }
+

结果就是去了重的

+
[1, 2]
+

但是当我的元素是个复杂对象,我想根据对象里的某个元素进行过滤的时候,就需要用到自定义的 distinctByKey 了,比如下面的想对 userId 进行去重

+
public static void main(String[] args) {
+        List<StudentRecord> list = new ArrayList<>();
+        StudentRecord s1 = new StudentRecord();
+        s1.setUserId(11L);
+        s1.setCourseId(100L);
+        s1.setScore(100);
+        list.add(s1);
+        StudentRecord s2 = new StudentRecord();
+        s2.setUserId(11L);
+        s2.setCourseId(101L);
+        s2.setScore(100);
+        list.add(s2);
+        StudentRecord s3 = new StudentRecord();
+        s3.setUserId(12L);
+        s3.setCourseId(100L);
+        s3.setScore(100);
+        list.add(s3);
+        System.out.println(list.stream().distinct().collect(Collectors.toList()));
+    }
+    @Data
+    static class StudentRecord {
+        Long id;
+        Long userId;
+        Long courseId;
+        Integer score;
+    }
+

结果就是

+
[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
+

因为对象都不一样,所以就没法去重了,这里就需要用

+
public static <T> Predicate<T> distinctByKey(
+            Function<? super T, ?> keyExtractor) {
+
+        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
+        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
+    }
+

然后就可以用它来去重了

+
System.out.println(list.stream().filter(distinctByKey(StudentRecord::getUserId)).collect(Collectors.toList()));
+

看下结果

+
[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
+

但是说实在的这个功能感觉应该是 stream 默认给实现的

+

使用 java.util.stream.Collectors#groupingBy 对 list 进行分组

这个使用场景还是蛮多的,上面的场景里比如我要对 userId 进行分组,就一行代码就解决了

+
System.out.println(list.stream().collect(Collectors.groupingBy(StudentRecord::getUserId)));
+

结果

{11=[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100)], 12=[StudentRecord(id=null, userId=12, courseId=100, score=100)]}
+

很方便的变成了以 userId 作为 key,以相同 userIdStudentRecordList 作为 valuemap 结构

]]>
- 生活 + java - 生活 - 远程办公 + java + stream
@@ -14990,130 +14852,45 @@ server.6=192.168< 生活 - - 生活 - 看书 - - - - 闲聊下乘公交的用户体验 - /2021/02/28/%E9%97%B2%E8%81%8A%E4%B8%8B%E4%B9%98%E5%85%AC%E4%BA%A4%E7%9A%84%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C/ - 新年开工开车来杭州,因为没有车位加限行今天来就没开车来了,从东站做公交回住的地方,这班神奇的车我之前也吐槽过了,有神奇的乘客和神奇的司机,因为基本上这班车是从我毕业就开始乘了,所以也算是比较熟悉了,以前总体感觉不太好的是乘坐时间太长了,不过这个也不能怪车,是我自己住得远(离东站),后来住到了现在的地方,也算是直达,并且 LD 比较喜欢直达的,不爱更快却要换乘的地铁,所以坐的频率比较高,也说过前面那些比较气人的乘客,自己不好好戴口罩,反而联合一起上车的乘客诽谤司机,说他要吃人了要打人了,也正是这个司机比较有意思,上车就让戴好口罩,还给大家讲,哪里哪里又有疫情了,我觉得其实这个司机还是不错的,特殊时期,对于这种公共交通,这样的确是比较负责任的做法,只是说话方式,语气这个因人而异,他也不是来伺候人的,而且这么一大车人,说了一遍不行,再说一遍,三遍以上了,嗓门大一点也属于正常的人的行为。
还是说回今天要说的,今天这位司机我看着跟前面说的那位有点像,因为上车的时候比较暗没看清脸,主要原因是这位司机开车比较猛,比较急,然后车上因为这个时间点,比较多大学开学来的学生,拎着个行李箱,一开始是前面已经都站满了人,后面还有很多空位,因为后面没地方放行李箱,就因为这样前面站着的有几个就在说司机开慢点,结果司机貌似也没听进去,还是我行我素,过了会又有人说司机开稳一点,就在这个人说完没一会,停在红绿灯路口的车里,就有人问有没有垃圾桶,接着又让司机开门,说晕车太严重了,要下车,司机开了门,我望出去两个妹子下了车,好像在路边草丛吐了,前面开门下车的时候就有人说她们第一次来杭州,可能有点责怪司机开的不稳,也影响了杭州交通给新来杭州的人的感受,说完了事情经过,其实我有蛮多感触,对于杭州公交司机,我大概是大一来了没多久,陪室友去文三路买电脑就晕车,下车的时候在公交车站吐了,可能是从大学开始缺乏锻炼,又饮食不规律,更加容易晕车,大部分晕车我觉得都是我自己的原因,有时候是上车前吃太多了,或者早上起太早,没睡好,没吃东西,反正自己也是挺多原因的,说到司机的原因的话,我觉得可能这班车还算好的,最让我难受的还是上下班高峰的时候,因为经过的那条路是比较重要的主干道,路比较老比较窄,并且还有很多人行道,所以经常一脚油门连带着一脚刹车,真的很难受,这种算是我觉得真的是公交体验比较差的一点,但是这一点呢也不能完全怪公交司机,杭州的路政规划是很垃圾,没看错,是垃圾,所以总体结论是公交还行,主要是路政规划就是垃圾,包括这条主干道这么多人行道,并且两边都是老小区,老年人在上班高峰可能要买菜送娃或者其他事情,在通畅的情况下可能只需要六分钟的路程,有时候因为各种原因,半小时都开不完,扯开去一点,杭州的路,核心的高速说封就封,本来是高架可以直接通到城西,结果没造,到了路本已经很拥挤的时候开始来造隧道,各种破坏,隧道接高架的地方,无尽的加塞,对于我这样的小白司机来说真的是太恶心了,所以我一直想说的就是杭州这个地方房价领先基础设施十年,地铁,高架,高速通通不行,地面道路就更不行了。
总结下,其实杭州的真正的公交体验差,应该还是路造成的,对于前面的那两位妹子来说,有可能是她们来自于公交司机都是开的特别稳,并且路况也很好的地方,也或者是我被虐习惯了🤦‍♂️

-]]>
- - 生活 - 公交 - - - 生活 - 开车 - 加塞 - 糟心事 - 规则 - 公交 - 路政规划 - 基础设施 - 杭州 - 高速 - -
- - 记录下 Java Stream 的一些高效操作 - /2022/05/15/%E8%AE%B0%E5%BD%95%E4%B8%8B-Java-Lambda-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%AB%98%E6%95%88%E6%93%8D%E4%BD%9C/ - 我们日常在代码里处理一些集合逻辑的时候用到 Stream 其实还挺多的,普通的取值过滤集合一般都是结合 ide 的提示就能搞定了,但是有些不太常用的就在这记录下,争取后面都更新记录下来。

-

自定义 distinctByKey 对结果进行去重

stream 中自带的 distinct 只能对元素进行去重
比如下面代码

-
public static void main(String[] args) {
-        List<Integer> list = new ArrayList<>();
-        list.add(1);
-        list.add(1);
-        list.add(2);
-        list = list.stream().distinct().collect(Collectors.toList());
-        System.out.println(list);
-    }
-

结果就是去了重的

-
[1, 2]
-

但是当我的元素是个复杂对象,我想根据对象里的某个元素进行过滤的时候,就需要用到自定义的 distinctByKey 了,比如下面的想对 userId 进行去重

-
public static void main(String[] args) {
-        List<StudentRecord> list = new ArrayList<>();
-        StudentRecord s1 = new StudentRecord();
-        s1.setUserId(11L);
-        s1.setCourseId(100L);
-        s1.setScore(100);
-        list.add(s1);
-        StudentRecord s2 = new StudentRecord();
-        s2.setUserId(11L);
-        s2.setCourseId(101L);
-        s2.setScore(100);
-        list.add(s2);
-        StudentRecord s3 = new StudentRecord();
-        s3.setUserId(12L);
-        s3.setCourseId(100L);
-        s3.setScore(100);
-        list.add(s3);
-        System.out.println(list.stream().distinct().collect(Collectors.toList()));
-    }
-    @Data
-    static class StudentRecord {
-        Long id;
-        Long userId;
-        Long courseId;
-        Integer score;
-    }
-

结果就是

-
[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
-

因为对象都不一样,所以就没法去重了,这里就需要用

-
public static <T> Predicate<T> distinctByKey(
-            Function<? super T, ?> keyExtractor) {
-
-        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
-        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
-    }
-

然后就可以用它来去重了

-
System.out.println(list.stream().filter(distinctByKey(StudentRecord::getUserId)).collect(Collectors.toList()));
-

看下结果

-
[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=12, courseId=100, score=100)]
-

但是说实在的这个功能感觉应该是 stream 默认给实现的

-

使用 java.util.stream.Collectors#groupingBy 对 list 进行分组

这个使用场景还是蛮多的,上面的场景里比如我要对 userId 进行分组,就一行代码就解决了

-
System.out.println(list.stream().collect(Collectors.groupingBy(StudentRecord::getUserId)));
-

结果

{11=[StudentRecord(id=null, userId=11, courseId=100, score=100), StudentRecord(id=null, userId=11, courseId=101, score=100)], 12=[StudentRecord(id=null, userId=12, courseId=100, score=100)]}
-

很方便的变成了以 userId 作为 key,以相同 userIdStudentRecordList 作为 valuemap 结构

-]]>
- - java - - - java - stream + + 生活 + 看书
- 闲话篇-也算碰到了为老不尊和坏人变老了的典型案例 - /2022/05/22/%E9%97%B2%E8%AF%9D%E7%AF%87-%E4%B9%9F%E7%AE%97%E7%A2%B0%E5%88%B0%E4%BA%86%E4%B8%BA%E8%80%81%E4%B8%8D%E5%B0%8A%E5%92%8C%E5%9D%8F%E4%BA%BA%E5%8F%98%E8%80%81%E4%BA%86%E7%9A%84%E5%85%B8%E5%9E%8B%E6%A1%88%E4%BE%8B/ - 在目前的房子也差不多租了四五年了,楼下邻居换了两拨了,我们这栋楼装修了不知道多少次,因为是学区的原因,房子交易的频率还是比较高的,不过比较神奇的我们对门的没换过,而且一直也没什么交集(除了后面说的水管爆裂),就进出的时候偶尔看到应该是住着一对老夫妻,感觉年纪也有个七八十了。

-

对对面这户人家的印象,就是对面的老头子经常是我出门上班去了他回来,看着他颤颤巍巍地走楼梯,我看到了都靠边走,而且有几次还听见好像是他儿子在说他,”年假这么大了,还是少出去吧”,说实话除了这次的事情,之前就有一次水管阀门爆裂了,算是有点交集,那次大概是去年冬天,天气已经很冷了,我们周日下午回来看到楼梯有点湿,但是没什么特别的异常就没怎么注意,到晚上洗完澡,楼下的邻居就来敲门,说我们门外的水表那一直在流水,出门一看真的是懵了,外面水表那在哗哗哗地流水,导致楼梯那就跟水帘洞一样,仔细看看是对面家的水表阀门那在漏水,我只能先用塑料袋包一下,然后大冬天(刚洗完澡)穿着凉拖跑下去找物业保安,走到一楼的时候发现水一直流到一楼了,楼梯上都是水流下来,五楼那是最惨的,感觉门框周边都浸透了,五楼的也是态度比较差的让我一定要把水弄好了,但是前面也说了谁是从对门那户的水表阀那出来的,理论上应该让对面的处理,结果我敲门敲了半天对面都没反应,想着我放着不管也不太好,就去找了物业保安,保安上来看了只能先把总阀关了,我也打电话给维修自来水管的,自来水公司的人过了会也是真的来修了,我那会是挺怕不来修,自来水公司的师傅到了以后拿开一看是对面那户的有个阀门估计是自己换上去的,跟我们这的完全不一样,看上去就比较劣质,师傅也挺气的,大晚上被叫过来,我又尝试着去敲门也还是没人应,也没办法,对面老人家我敲太响到时候出来说我吓到他们啥的,第二天去说也没现场了。

-

前面的这件事是个重要铺垫,前几天 LD 下班后把厨余垃圾套好袋子放在门口,打算等我下班了因为要去做核酸(hz 48 小时核酸)顺便带下去,结果到了七点多,说对面的老太太在那疯狂砸门了,LD 被吓到了不敢开门,老太太在外面一边砸门一边骂,“你们年轻人怎么素质这么差”(他们家也经常在门口放垃圾,我们刚来住的时候在楼梯转角他们就放这个废弃的洗衣机,每次走楼梯带点东西都要小心翼翼地走,不然都过不去,然后我赶紧赶回去,结果她听到我回家了,还特意开门继续骂,“你们年轻人怎么素质这么差,垃圾放在这里”,我说我们刚才放在这,打算待会做核酸的时候去扔掉,结果他们家老头,都已经没了牙齿,在那瞪大眼睛说,“你们早上就放在这了的,”我说是LD 刚才下班了放的,争论了一会,我说这个事情我们门口放了垃圾,这会我就去扔掉了,但是你们家老太太这么砸门总不太好,像之前门口水管爆掉了,我敲了门没人应,我也没要砸门一定把你们叫醒,结果老头老太说我们的水管从来没换过,不可能破的(其实到这,再往后说就没意思了,跟这么不要脸的人说多了也只是瞎扯),一会又回到这个垃圾的问题,那个老头说“你们昨天就放在这里了的”,睁着眼说瞎话可真是 666,感觉不是老太太拦着点他马上就要冲上来揍我了一样,事后我想想,这种情况我大概只能躺地上装死了,当这个事情发生之前我真的快把前面说的事情(水管阀坏了)给忘了,虽然这是理论上不该我来处理,除非是老头老太太请求我帮忙,这事后面我也从没说起过,本来完全没交集,对他们的是怎么样的人也没概念,总觉得年纪大了可能还比较心宽和蔼点,结果没想到就是一典型的坏人变老了,我说你们这么砸门,我老婆都被吓得不敢开门,结果对面老头老太太的儿子也出来了说,“我们就是敲下门,我母亲是机关单位退休的,所以肯定不会敲门很大声的,你老婆觉得吓到了是你们人生观价值观有问题”,听到这话我差点笑出来,连着两个可笑至极的脑残逻辑,无语他妈给无语开门,无语到家了。对门家我们之前有个印象就是因为我们都是顶楼,这边老小区以前都是把前后阳台包进来的,然后社区就来咨询大家的意见是不是统一把包进来的违建拆掉,还特地上来六楼跟他们说,结果对面的老头就说,“我要去住建局投诉你们”,本来这个事情是违法的,但是社区的意思也是征求各位业主的意见,结果感觉是社区上门强拆了一样,为老不尊,坏人变老了的典范了。

+ 闲聊下乘公交的用户体验 + /2021/02/28/%E9%97%B2%E8%81%8A%E4%B8%8B%E4%B9%98%E5%85%AC%E4%BA%A4%E7%9A%84%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C/ + 新年开工开车来杭州,因为没有车位加限行今天来就没开车来了,从东站做公交回住的地方,这班神奇的车我之前也吐槽过了,有神奇的乘客和神奇的司机,因为基本上这班车是从我毕业就开始乘了,所以也算是比较熟悉了,以前总体感觉不太好的是乘坐时间太长了,不过这个也不能怪车,是我自己住得远(离东站),后来住到了现在的地方,也算是直达,并且 LD 比较喜欢直达的,不爱更快却要换乘的地铁,所以坐的频率比较高,也说过前面那些比较气人的乘客,自己不好好戴口罩,反而联合一起上车的乘客诽谤司机,说他要吃人了要打人了,也正是这个司机比较有意思,上车就让戴好口罩,还给大家讲,哪里哪里又有疫情了,我觉得其实这个司机还是不错的,特殊时期,对于这种公共交通,这样的确是比较负责任的做法,只是说话方式,语气这个因人而异,他也不是来伺候人的,而且这么一大车人,说了一遍不行,再说一遍,三遍以上了,嗓门大一点也属于正常的人的行为。
还是说回今天要说的,今天这位司机我看着跟前面说的那位有点像,因为上车的时候比较暗没看清脸,主要原因是这位司机开车比较猛,比较急,然后车上因为这个时间点,比较多大学开学来的学生,拎着个行李箱,一开始是前面已经都站满了人,后面还有很多空位,因为后面没地方放行李箱,就因为这样前面站着的有几个就在说司机开慢点,结果司机貌似也没听进去,还是我行我素,过了会又有人说司机开稳一点,就在这个人说完没一会,停在红绿灯路口的车里,就有人问有没有垃圾桶,接着又让司机开门,说晕车太严重了,要下车,司机开了门,我望出去两个妹子下了车,好像在路边草丛吐了,前面开门下车的时候就有人说她们第一次来杭州,可能有点责怪司机开的不稳,也影响了杭州交通给新来杭州的人的感受,说完了事情经过,其实我有蛮多感触,对于杭州公交司机,我大概是大一来了没多久,陪室友去文三路买电脑就晕车,下车的时候在公交车站吐了,可能是从大学开始缺乏锻炼,又饮食不规律,更加容易晕车,大部分晕车我觉得都是我自己的原因,有时候是上车前吃太多了,或者早上起太早,没睡好,没吃东西,反正自己也是挺多原因的,说到司机的原因的话,我觉得可能这班车还算好的,最让我难受的还是上下班高峰的时候,因为经过的那条路是比较重要的主干道,路比较老比较窄,并且还有很多人行道,所以经常一脚油门连带着一脚刹车,真的很难受,这种算是我觉得真的是公交体验比较差的一点,但是这一点呢也不能完全怪公交司机,杭州的路政规划是很垃圾,没看错,是垃圾,所以总体结论是公交还行,主要是路政规划就是垃圾,包括这条主干道这么多人行道,并且两边都是老小区,老年人在上班高峰可能要买菜送娃或者其他事情,在通畅的情况下可能只需要六分钟的路程,有时候因为各种原因,半小时都开不完,扯开去一点,杭州的路,核心的高速说封就封,本来是高架可以直接通到城西,结果没造,到了路本已经很拥挤的时候开始来造隧道,各种破坏,隧道接高架的地方,无尽的加塞,对于我这样的小白司机来说真的是太恶心了,所以我一直想说的就是杭州这个地方房价领先基础设施十年,地铁,高架,高速通通不行,地面道路就更不行了。
总结下,其实杭州的真正的公交体验差,应该还是路造成的,对于前面的那两位妹子来说,有可能是她们来自于公交司机都是开的特别稳,并且路况也很好的地方,也或者是我被虐习惯了🤦‍♂️

]]>
生活 + 公交 生活 + 开车 + 加塞 + 糟心事 + 规则 + 公交 + 路政规划 + 基础设施 + 杭州 + 高速
- 聊聊这次换车牌及其他 - /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/22/%E9%97%B2%E8%AF%9D%E7%AF%87-%E4%B9%9F%E7%AE%97%E7%A2%B0%E5%88%B0%E4%BA%86%E4%B8%BA%E8%80%81%E4%B8%8D%E5%B0%8A%E5%92%8C%E5%9D%8F%E4%BA%BA%E5%8F%98%E8%80%81%E4%BA%86%E7%9A%84%E5%85%B8%E5%9E%8B%E6%A1%88%E4%BE%8B/ + 在目前的房子也差不多租了四五年了,楼下邻居换了两拨了,我们这栋楼装修了不知道多少次,因为是学区的原因,房子交易的频率还是比较高的,不过比较神奇的我们对门的没换过,而且一直也没什么交集(除了后面说的水管爆裂),就进出的时候偶尔看到应该是住着一对老夫妻,感觉年纪也有个七八十了。

+

对对面这户人家的印象,就是对面的老头子经常是我出门上班去了他回来,看着他颤颤巍巍地走楼梯,我看到了都靠边走,而且有几次还听见好像是他儿子在说他,”年假这么大了,还是少出去吧”,说实话除了这次的事情,之前就有一次水管阀门爆裂了,算是有点交集,那次大概是去年冬天,天气已经很冷了,我们周日下午回来看到楼梯有点湿,但是没什么特别的异常就没怎么注意,到晚上洗完澡,楼下的邻居就来敲门,说我们门外的水表那一直在流水,出门一看真的是懵了,外面水表那在哗哗哗地流水,导致楼梯那就跟水帘洞一样,仔细看看是对面家的水表阀门那在漏水,我只能先用塑料袋包一下,然后大冬天(刚洗完澡)穿着凉拖跑下去找物业保安,走到一楼的时候发现水一直流到一楼了,楼梯上都是水流下来,五楼那是最惨的,感觉门框周边都浸透了,五楼的也是态度比较差的让我一定要把水弄好了,但是前面也说了谁是从对门那户的水表阀那出来的,理论上应该让对面的处理,结果我敲门敲了半天对面都没反应,想着我放着不管也不太好,就去找了物业保安,保安上来看了只能先把总阀关了,我也打电话给维修自来水管的,自来水公司的人过了会也是真的来修了,我那会是挺怕不来修,自来水公司的师傅到了以后拿开一看是对面那户的有个阀门估计是自己换上去的,跟我们这的完全不一样,看上去就比较劣质,师傅也挺气的,大晚上被叫过来,我又尝试着去敲门也还是没人应,也没办法,对面老人家我敲太响到时候出来说我吓到他们啥的,第二天去说也没现场了。

+

前面的这件事是个重要铺垫,前几天 LD 下班后把厨余垃圾套好袋子放在门口,打算等我下班了因为要去做核酸(hz 48 小时核酸)顺便带下去,结果到了七点多,说对面的老太太在那疯狂砸门了,LD 被吓到了不敢开门,老太太在外面一边砸门一边骂,“你们年轻人怎么素质这么差”(他们家也经常在门口放垃圾,我们刚来住的时候在楼梯转角他们就放这个废弃的洗衣机,每次走楼梯带点东西都要小心翼翼地走,不然都过不去,然后我赶紧赶回去,结果她听到我回家了,还特意开门继续骂,“你们年轻人怎么素质这么差,垃圾放在这里”,我说我们刚才放在这,打算待会做核酸的时候去扔掉,结果他们家老头,都已经没了牙齿,在那瞪大眼睛说,“你们早上就放在这了的,”我说是LD 刚才下班了放的,争论了一会,我说这个事情我们门口放了垃圾,这会我就去扔掉了,但是你们家老太太这么砸门总不太好,像之前门口水管爆掉了,我敲了门没人应,我也没要砸门一定把你们叫醒,结果老头老太说我们的水管从来没换过,不可能破的(其实到这,再往后说就没意思了,跟这么不要脸的人说多了也只是瞎扯),一会又回到这个垃圾的问题,那个老头说“你们昨天就放在这里了的”,睁着眼说瞎话可真是 666,感觉不是老太太拦着点他马上就要冲上来揍我了一样,事后我想想,这种情况我大概只能躺地上装死了,当这个事情发生之前我真的快把前面说的事情(水管阀坏了)给忘了,虽然这是理论上不该我来处理,除非是老头老太太请求我帮忙,这事后面我也从没说起过,本来完全没交集,对他们的是怎么样的人也没概念,总觉得年纪大了可能还比较心宽和蔼点,结果没想到就是一典型的坏人变老了,我说你们这么砸门,我老婆都被吓得不敢开门,结果对面老头老太太的儿子也出来了说,“我们就是敲下门,我母亲是机关单位退休的,所以肯定不会敲门很大声的,你老婆觉得吓到了是你们人生观价值观有问题”,听到这话我差点笑出来,连着两个可笑至极的脑残逻辑,无语他妈给无语开门,无语到家了。对门家我们之前有个印象就是因为我们都是顶楼,这边老小区以前都是把前后阳台包进来的,然后社区就来咨询大家的意见是不是统一把包进来的违建拆掉,还特地上来六楼跟他们说,结果对面的老头就说,“我要去住建局投诉你们”,本来这个事情是违法的,但是社区的意思也是征求各位业主的意见,结果感觉是社区上门强拆了一样,为老不尊,坏人变老了的典范了。

]]>
生活 生活 - 换车牌
@@ -15131,6 +14908,44 @@ server.6=192.168< 生活 + + 聊聊 Java 的类加载机制一 + /2020/11/08/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/ + 一说到这个主题,想到的应该是双亲委派模型,不过讲的包括但不限于这个,主要内容是参考深入理解 Java 虚拟机书中的介绍,
一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。

+
    +
  • 加载

  • +
+
    +
  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. +
  3. 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  4. +
  5. 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  6. +
+
    +
  • 验证

  • +
+
    +
  1. 文件格式验证
  2. +
  3. 元数据验证
  4. +
  5. 字节码验证
  6. +
  7. 符号引用验证
  8. +
+
    +
  • 准备

    准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

    +
  • +
  • 解析

    解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程

    +
  • +
+

以上验证准备解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

+
    +
  • 初始化

    类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
  • +
+

最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

+]]>
+ + Java + 类加载 + +
难得的大扫除 /2022/04/10/%E9%9A%BE%E5%BE%97%E7%9A%84%E5%A4%A7%E6%89%AB%E9%99%A4/ @@ -15144,4 +14959,189 @@ server.6=192.168< 大扫除 + + 聊聊 Sharding-Jdbc 的简单使用 + /2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ + 我们在日常工作中还是使用比较多的分库分表组件的,其中比较优秀的就有 Sharding-Jdbc,一开始由当当开源,后来捐献给了 Apache,说一下简单使用,因为原来经常的使用都是基于 xml 跟 properties 组合起来使用,这里主要试下用 Java Config 来配置
首先是通过 Spring Initializr 创建个带 jdbc 的 Spring Boot 项目,然后引入主要的依赖

+
<dependency>
+    <groupId>org.apache.shardingsphere</groupId>
+    <artifactId>shardingsphere-jdbc-core</artifactId>
+    <version>5.0.0-beta</version>
+</dependency>
+

因为前面有聊过 Spring Boot 的自动加载,在这里 spring 就会自己去找 DataSource 的配置,所以要在入口把它干掉

+
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
+public class ShardingJdbcDemoApplication implements CommandLineRunner {
+

然后因为想在入口跑代码,就实现了下 org.springframework.boot.CommandLineRunner 主要是后面的 Java Config 代码

+

+// 注意这里的注解,可以让 Spring 自动帮忙加载,也就是 Java Config 的核心
+@Configuration
+public class MysqlConfig {
+
+    @Bean
+    public DataSource dataSource() throws SQLException {
+        // Configure actual data sources
+        Map<String, DataSource> dataSourceMap = new HashMap<>();
+
+        
+        // Configure the first data source
+        // 使用了默认的Hikari连接池的 DataSource
+        HikariDataSource dataSource1 = new HikariDataSource();
+        dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
+        dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/sharding");
+        dataSource1.setUsername("username");
+        dataSource1.setPassword("password");
+        dataSourceMap.put("ds0", dataSource1);
+
+        // Configure student table rule
+        // 这里是配置分表逻辑,逻辑表是 student,对应真实的表是 student_0 到 student_1, 这个配置方式就是有多少表可以用 student_$->{0..n}
+        ShardingTableRuleConfiguration studentTableRuleConfig = new ShardingTableRuleConfiguration("student", "ds0.student_$->{0..1}");
+
+        // 设置分表字段
+        studentTableRuleConfig.setTableShardingStrategy(new StandardShardingStrategyConfiguration("user_id", "tableShardingAlgorithm"));
+
+
+        // Configure sharding rule
+        // 配置 studentTableRuleConfig
+        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
+        shardingRuleConfig.getTables().add(studentTableRuleConfig);
+
+        // Configure table sharding algorithm
+        Properties tableShardingAlgorithmrProps = new Properties();
+        // 算法表达式就是根据 user_id 对 2 进行取模
+        tableShardingAlgorithmrProps.setProperty("algorithm-expression", "student_${user_id % 2}");
+        shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps));
+
+
+        // 然后创建这个 DataSource
+        return ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), new Properties());
+
+    }
+}
+

然后我们就可以在使用这个 DataSource 了,先看下这两个表的数据

+
@Override
+    public void run(String... args) {
+        LOGGER.info("run here");
+        String sql = "SELECT * FROM student WHERE user_id=? ";
+        try (
+                Connection conn = dataSource.getConnection();
+                PreparedStatement ps = conn.prepareStatement(sql)) {
+            // 参数就是 user_id,然后也是分表键,对 2 取模就是 1,应该是去 student_1 取数据
+            ps.setInt(1, 1001);
+
+            ResultSet resultSet = ps.executeQuery();
+            while (resultSet.next()) {
+                final int id = resultSet.getInt("id");
+                final String name = resultSet.getString("name");
+                final int userId = resultSet.getInt("user_id");
+                final int age = resultSet.getInt("age");
+                System.out.println("奇数表 id:" + id + " 姓名:" + name
+                        + " 用户 id:" + userId + " 年龄:" + age );
+                System.out.println("=============================");
+            }
+            // 参数就是 user_id,然后也是分表键,对 2 取模就是 0,应该是去 student_0 取数据
+            ps.setInt(1, 1000);
+            resultSet = ps.executeQuery();
+            while (resultSet.next()) {
+                final int id = resultSet.getInt("id");
+                final String name = resultSet.getString("name");
+                final int userId = resultSet.getInt("user_id");
+                final int age = resultSet.getInt("age");
+                System.out.println("偶数表 id:" + id + " 姓名:" + name
+                        + " 用户 id:" + userId + " 年龄:" + age );
+                System.out.println("=============================");
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+

看下查询结果

+]]>
+ + Java + + + Java + Sharding-Jdbc + +
+ + 聊聊 mysql 的 MVCC + /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/ + 很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢

+

其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行

+

腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下

+
/* Precise data types for system columns and the length of those columns;
+NOTE: the values must run from 0 up in the order given! All codes must
+be less than 256 */
+#define DATA_ROW_ID 0     /* row id: a 48-bit integer */
+#define DATA_ROW_ID_LEN 6 /* stored length for row id */
+
+/** Transaction id: 6 bytes */
+constexpr size_t DATA_TRX_ID = 1;
+
+/** Transaction ID type size in bytes. */
+constexpr size_t DATA_TRX_ID_LEN = 6;
+
+/** Rollback data pointer: 7 bytes */
+constexpr size_t DATA_ROLL_PTR = 2;
+
+/** Rollback data pointer type size in bytes. */
+constexpr size_t DATA_ROLL_PTR_LEN = 7;
+ +

一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了

+

一个是 DATA_TRX_ID,这个表示这条记录的事务 ID

+

还有一个是 DATA_ROLL_PTR 指向回滚段的指针

+

指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,

+

m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id

+

m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id

+

m_ids, 这个是 read view 创建时所有的活跃事务 id 数组

+

m_creator_trx_id 这个是当前记录的创建事务 id

+

判断事务的可见性主要的逻辑是这样,

+
    +
  1. 当记录的事务 id 小于最小活跃事务 id,说明是可见的,
  2. +
  3. 如果记录的事务 id 等于当前事务 id,说明是自己的更改,可见
  4. +
  5. 如果记录的事务 id 大于最大的活跃事务 id, 不可见
  6. +
  7. 如果记录的事务 id 介于 m_low_limit_idm_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
    具体的代码捞一下看看
    /** Check whether the changes by id are visible.
    +  @param[in]	id	transaction id to check against the view
    +  @param[in]	name	table name
    +  @return whether the view sees the modifications of id. */
    +  bool changes_visible(trx_id_t id, const table_name_t &name) const
    +      MY_ATTRIBUTE((warn_unused_result)) {
    +    ut_ad(id > 0);
    +
    +    if (id < m_up_limit_id || id == m_creator_trx_id) {
    +      return (true);
    +    }
    +
    +    check_trx_id_sanity(id, name);
    +
    +    if (id >= m_low_limit_id) {
    +      return (false);
    +
    +    } else if (m_ids.empty()) {
    +      return (true);
    +    }
    +
    +    const ids_t::value_type *p = m_ids.data();
    +
    +    return (!std::binary_search(p, p + m_ids.size(), id));
    +  }
    +剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据
  8. +
+]]>
+ + Mysql + C + 数据结构 + 源码 + Mysql + + + mysql + 数据结构 + 源码 + mvcc + read view + +
diff --git a/sitemap.xml b/sitemap.xml index e197409804..6b4380f981 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -200,7 +200,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 @@ -209,7 +209,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 @@ -218,7 +218,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/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ 2022-06-11 @@ -227,7 +227,7 @@ - https://nicksxs.me/2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ + 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 @@ -425,7 +425,7 @@ - https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ + https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ 2022-06-11 @@ -434,7 +434,7 @@ - https://nicksxs.me/2021/09/26/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E6%96%B9%E6%B3%95/ + https://nicksxs.me/2021/09/19/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84-cglib-%E4%BD%9C%E4%B8%BA%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/ 2022-06-11 @@ -1388,7 +1388,7 @@ - https://nicksxs.me/2016/09/29/binary-watch/ + https://nicksxs.me/2016/10/11/minimum-size-subarray-sum-209/ 2020-01-12 @@ -1397,7 +1397,7 @@ - https://nicksxs.me/2016/10/11/minimum-size-subarray-sum-209/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 @@ -1406,7 +1406,7 @@ - https://nicksxs.me/2019/12/10/Redis-Part-1/ + https://nicksxs.me/2016/09/29/binary-watch/ 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 @@ -1442,7 +1442,7 @@ - https://nicksxs.me/2020/01/10/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%B8%89/ + https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ 2020-01-12 @@ -1451,7 +1451,7 @@ - https://nicksxs.me/2020/01/04/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BA%8C/ + https://nicksxs.me/2019/12/26/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D/ 2020-01-12 @@ -1487,7 +1487,7 @@ - https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ + https://nicksxs.me/2019/09/23/AbstractQueuedSynchronizer/ 2020-01-12 @@ -1496,7 +1496,7 @@ - https://nicksxs.me/2019/09/23/AbstractQueuedSynchronizer/ + https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ 2020-01-12 @@ -1514,7 +1514,7 @@ - https://nicksxs.me/2015/01/04/Path-Sum/ + https://nicksxs.me/2015/03/11/Number-Of-1-Bits/ 2020-01-12 @@ -1523,7 +1523,7 @@ - https://nicksxs.me/2015/03/11/Number-Of-1-Bits/ + https://nicksxs.me/2015/01/04/Path-Sum/ 2020-01-12 @@ -1532,7 +1532,7 @@ - https://nicksxs.me/2015/06/22/invert-binary-tree/ + https://nicksxs.me/2014/12/23/my-new-post/ 2020-01-12 @@ -1541,7 +1541,7 @@ - https://nicksxs.me/2014/12/23/my-new-post/ + https://nicksxs.me/2015/01/16/pcre-intro-and-a-simple-package/ 2020-01-12 @@ -1550,7 +1550,7 @@ - https://nicksxs.me/2015/01/16/pcre-intro-and-a-simple-package/ + https://nicksxs.me/2017/04/25/rabbitmq-tips/ 2020-01-12 @@ -1559,7 +1559,7 @@ - https://nicksxs.me/2017/04/25/rabbitmq-tips/ + https://nicksxs.me/2017/03/28/spark-little-tips/ 2020-01-12 @@ -1568,7 +1568,7 @@ - https://nicksxs.me/2017/03/28/spark-little-tips/ + 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 @@ -1586,7 +1586,7 @@ - https://nicksxs.me/2016/07/13/swoole-websocket-test/ + https://nicksxs.me/2015/06/22/invert-binary-tree/ 2020-01-12 @@ -1633,7 +1633,7 @@ https://nicksxs.me/ - 2022-08-25 + 2022-08-26 daily 1.0 @@ -1641,1981 +1641,1981 @@ https://nicksxs.me/tags/%E7%94%9F%E6%B4%BB/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/2020/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/2021/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%8B%96%E6%9B%B4/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Java/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/JVM/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/C/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/leetcode/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/java/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/Linked-List/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/ - 2022-08-25 + https://nicksxs.me/tags/Binary-Tree/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Binary-Tree/ - 2022-08-25 + https://nicksxs.me/tags/DFS/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/DFS/ - 2022-08-25 + https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ - 2022-08-25 + https://nicksxs.me/tags/%E9%A2%98%E8%A7%A3/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/2019/ - 2022-08-25 + https://nicksxs.me/tags/Linked-List/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/c/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B6%E5%8F%91/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/j-u-c/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/aqs/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/condition/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/await/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/signal/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/lock/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/unlock/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ - 2022-08-25 + https://nicksxs.me/tags/Apollo/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ - 2022-08-25 + https://nicksxs.me/tags/value/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Apollo/ - 2022-08-25 + https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/value/ - 2022-08-25 + https://nicksxs.me/tags/environment/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ - 2022-08-25 + https://nicksxs.me/tags/Stream/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/environment/ - 2022-08-25 + https://nicksxs.me/tags/Comparator/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Stream/ - 2022-08-25 + https://nicksxs.me/tags/%E6%8E%92%E5%BA%8F/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Comparator/ - 2022-08-25 + https://nicksxs.me/tags/sort/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%8E%92%E5%BA%8F/ - 2022-08-25 + https://nicksxs.me/tags/nullsfirst/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/sort/ - 2022-08-25 + https://nicksxs.me/tags/Disruptor/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/nullsfirst/ - 2022-08-25 + https://nicksxs.me/tags/Dubbo/ + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/RPC/ + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Filter/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Interceptor/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/AOP/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Spring/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Tomcat/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Servlet/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Web/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/G1/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/GC/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Garbage-First-Collector/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/Disruptor/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E9%80%92%E5%BD%92/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Preorder-Traversal/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Inorder-Traversal/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%89%8D%E5%BA%8F/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%AD%E5%BA%8F/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/Dubbo/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/RPC/ - 2022-08-25 + https://nicksxs.me/tags/Print-FooBar-Alternately/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/ - 2022-08-25 + https://nicksxs.me/tags/DP/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/DP/ - 2022-08-25 + https://nicksxs.me/tags/Shift-2D-Grid/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/stack/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/min-stack/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%9C%80%E5%B0%8F%E6%A0%88/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/leetcode-155/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/linked-list/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/3Sum-Closest/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Print-FooBar-Alternately/ - 2022-08-25 + https://nicksxs.me/tags/linked-list/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/First-Bad-Version/ - 2022-08-25 + https://nicksxs.me/tags/string/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Shift-2D-Grid/ - 2022-08-25 + https://nicksxs.me/tags/First-Bad-Version/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/string/ - 2022-08-25 + https://nicksxs.me/tags/2019/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/dp/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%BB%A3%E7%A0%81%E9%A2%98%E8%A7%A3/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Trapping-Rain-Water/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%8E%A5%E9%9B%A8%E6%B0%B4/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Leetcode-42/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/Intersection-of-Two-Arrays/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Rotate-Image/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%9F%A9%E9%98%B5/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E6%8A%80%E6%9C%AF/ + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%AF%BB%E4%B9%A6/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/linux/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/grep/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%BD%AC%E4%B9%89/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/mfc/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Intersection-of-Two-Arrays/ - 2022-08-25 + https://nicksxs.me/tags/Maven/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Maven/ - 2022-08-25 + https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/hadoop/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/cluster/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/Remove-Duplicates-from-Sorted-List/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/docker/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/mysql/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Docker/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/namespace/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/cgroup/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Dockerfile/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/echo/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/uname/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8F%91%E8%A1%8C%E7%89%88/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Gogs/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Webhook/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8D%9A%E5%AE%A2%EF%BC%8C%E6%96%87%E7%AB%A0/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Mysql/ - 2022-08-25 + https://nicksxs.me/tags/C/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Mybatis/ - 2022-08-25 + https://nicksxs.me/tags/Redis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ - 2022-08-25 + https://nicksxs.me/tags/Distributed-Lock/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/C/ - 2022-08-25 + https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Redis/ - 2022-08-25 + https://nicksxs.me/tags/Mysql/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Distributed-Lock/ - 2022-08-25 + https://nicksxs.me/tags/Mybatis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/ - 2022-08-25 + https://nicksxs.me/tags/Sql%E6%B3%A8%E5%85%A5/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%BC%93%E5%AD%98/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/openresty/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/nginx/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/php/ - 2022-08-25 + https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/mq/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/php/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/im/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/redis/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%BA%90%E7%A0%81/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%BA%94%E7%94%A8/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Evict/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Rust/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%89%80%E6%9C%89%E6%9D%83/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%96%B0%E8%AF%AD%E8%A8%80/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%88%87%E7%89%87/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/spark/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/python/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Spring-Event/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/websocket/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/swoole/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/gc/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ - 2022-08-25 + https://nicksxs.me/tags/WordPress/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ - 2022-08-25 + https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/jvm/ - 2022-08-25 + https://nicksxs.me/tags/gc/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/WordPress/ - 2022-08-25 + https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-08-25 + https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ - 2022-08-25 + https://nicksxs.me/tags/jvm/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/MQ/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/RocketMQ/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%89%8A%E5%B3%B0%E5%A1%AB%E8%B0%B7/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/ssh/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E6%89%93%E5%8D%A1/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%B6%B3%E7%90%83/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%BF%90%E5%8A%A8/ - 2022-08-25 + https://nicksxs.me/tags/git/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%87%8F%E8%82%A5/ - 2022-08-25 + https://nicksxs.me/tags/scp/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%91%E6%AD%A5/ - 2022-08-25 + https://nicksxs.me/tags/%E8%BF%90%E5%8A%A8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B2%E6%B4%BB/ - 2022-08-25 + https://nicksxs.me/tags/%E5%87%8F%E8%82%A5/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/git/ - 2022-08-25 + https://nicksxs.me/tags/%E8%B7%91%E6%AD%A5/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%BD%B1%E8%AF%84/ - 2022-08-25 + https://nicksxs.me/tags/%E5%B9%B2%E6%B4%BB/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%AF%84%E7%94%9F%E8%99%AB/ - 2022-08-25 + https://nicksxs.me/tags/%E5%BD%B1%E8%AF%84/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/scp/ - 2022-08-25 + https://nicksxs.me/tags/%E5%AF%84%E7%94%9F%E8%99%AB/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ - 2022-08-25 + https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/ - 2022-08-25 + https://nicksxs.me/tags/DefaultMQPushConsumer/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/utf8/ - 2022-08-25 + https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/utf8mb4/ - 2022-08-25 + https://nicksxs.me/tags/NameServer/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-0900-ai-ci/ - 2022-08-25 + https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-unicode-ci/ - 2022-08-25 + https://nicksxs.me/tags/%E7%BC%96%E7%A0%81/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/utf8mb4-general-ci/ - 2022-08-25 + https://nicksxs.me/tags/utf8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/DefaultMQPushConsumer/ - 2022-08-25 + https://nicksxs.me/tags/utf8mb4/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/ - 2022-08-25 + https://nicksxs.me/tags/utf8mb4-0900-ai-ci/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/NameServer/ - 2022-08-25 + https://nicksxs.me/tags/utf8mb4-unicode-ci/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/SpringBoot/ - 2022-08-25 + https://nicksxs.me/tags/utf8mb4-general-ci/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/cglib/ - 2022-08-25 + https://nicksxs.me/tags/SpringBoot/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ - 2022-08-25 + https://nicksxs.me/tags/Druid/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%9B%A4%E7%89%A9%E8%B5%84/ - 2022-08-25 + 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-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%BE%E9%87%8D/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B0%84%E5%87%BB/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Druid/ - 2022-08-25 + https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ - 2022-08-25 + https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E4%B9%92%E4%B9%93%E7%90%83/ - 2022-08-25 + https://nicksxs.me/tags/SPI/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%B3%E6%B0%B4/ - 2022-08-25 + https://nicksxs.me/tags/Adaptive/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ - 2022-08-25 + https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/SPI/ - 2022-08-25 + https://nicksxs.me/tags/cglib/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Adaptive/ - 2022-08-25 + https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/ - 2022-08-25 + https://nicksxs.me/tags/Synchronized/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Synchronized/ - 2022-08-25 + https://nicksxs.me/tags/%E5%81%8F%E5%90%91%E9%94%81/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%81%8F%E5%90%91%E9%94%81/ - 2022-08-25 + https://nicksxs.me/tags/%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81/ - 2022-08-25 + https://nicksxs.me/tags/%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81/ - 2022-08-25 + https://nicksxs.me/tags/%E8%87%AA%E6%97%8B/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%87%AA%E6%97%8B/ - 2022-08-25 + https://nicksxs.me/tags/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8A%A0%E8%BD%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E9%AA%8C%E8%AF%81/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%87%86%E5%A4%87/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%A7%A3%E6%9E%90/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%88%9D%E5%A7%8B%E5%8C%96/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E9%93%BE%E6%8E%A5/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/JPS/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/JStack/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/JMap/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/top/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Broker/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/AutoConfiguration/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Sharding-Jdbc/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/ThreadPool/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/FixedThreadPool/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/LimitedThreadPool/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/EagerThreadPool/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/CachedThreadPool/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/mvcc/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/read-view/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/gap-lock/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/next-key-lock/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B9%BB%E8%AF%BB/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%B4%A2%E5%BC%95/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/is-null/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/is-not-null/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/procedure/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/AutoConfiguration/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/bloom-filter/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%BA%92%E6%96%A5%E9%94%81/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Mac/ - 2022-08-25 + https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/PHP/ - 2022-08-25 + https://nicksxs.me/tags/Design-Patterns/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Homebrew/ - 2022-08-25 + https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/icu4c/ - 2022-08-25 + https://nicksxs.me/tags/Singleton/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/zsh/ - 2022-08-25 + https://nicksxs.me/tags/Mac/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ - 2022-08-25 + https://nicksxs.me/tags/PHP/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Design-Patterns/ - 2022-08-25 + https://nicksxs.me/tags/Homebrew/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%8D%95%E4%BE%8B/ - 2022-08-25 + https://nicksxs.me/tags/icu4c/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/Singleton/ - 2022-08-25 + https://nicksxs.me/tags/zsh/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/ThreadLocal/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/WeakReference/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ - 2022-08-25 + https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ - 2022-08-25 + https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ - 2022-08-25 + https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ - 2022-08-25 + https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ - 2022-08-25 + https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ - 2022-08-25 + https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ - 2022-08-25 + https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ - 2022-08-25 + https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ - 2022-08-25 + https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ - 2022-08-25 + https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ - 2022-08-25 + https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ - 2022-08-25 + https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ - 2022-08-25 + https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ - 2022-08-25 + https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/Thread-dump/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/2PC/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/3PC/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/tags/%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E7%9C%8B%E5%89%A7/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%A3%85%E7%94%B5%E8%84%91/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E8%80%81%E7%94%B5%E8%84%91/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/360-%E5%85%A8%E5%AE%B6%E6%A1%B6/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E4%BF%AE%E7%94%B5%E8%84%91%E7%9A%84/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/tags/%E8%BF%9C%E7%A8%8B%E5%8A%9E%E5%85%AC/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/zookeeper/ - 2022-08-25 + https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ - 2022-08-25 + https://nicksxs.me/tags/stream/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ - 2022-08-25 + https://nicksxs.me/tags/zookeeper/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/stream/ - 2022-08-25 + https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/tags/%E6%8D%A2%E8%BD%A6%E7%89%8C/ - 2022-08-25 + https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ + 2022-08-26 weekly 0.2 https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ - 2022-08-25 + 2022-08-26 weekly 0.2 @@ -3624,1036 +3624,1036 @@ https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Java/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Java/JVM/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/ - 2022-08-25 + https://nicksxs.me/categories/Java/leetcode/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/ - 2022-08-25 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Java/GC/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Linked-List/ - 2022-08-25 + https://nicksxs.me/categories/Binary-Tree/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Binary-Tree/ - 2022-08-25 + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ - 2022-08-25 + https://nicksxs.me/categories/Linked-List/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ - 2022-08-25 + https://nicksxs.me/categories/C/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/C/ - 2022-08-25 + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/ - 2022-08-25 + https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Apollo/ - 2022-08-25 + https://nicksxs.me/categories/java/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ - 2022-08-25 + 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-26 weekly 0.2 - https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ - 2022-08-25 + https://nicksxs.me/categories/Java/Apollo/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/java/ - 2022-08-25 + https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Filter/ - 2022-08-25 + https://nicksxs.me/categories/Java/Dubbo/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E6%9D%91%E4%B8%8A%E6%98%A5%E6%A0%91/ - 2022-08-25 + 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-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ - 2022-08-25 + https://nicksxs.me/categories/Filter/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/Binary-Tree/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2021/ - 2022-08-25 + https://nicksxs.me/categories/DP/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/DP/ - 2022-08-25 + https://nicksxs.me/categories/stack/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/ - 2022-08-25 + https://nicksxs.me/categories/Java/Apollo/value/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/stack/ - 2022-08-25 + https://nicksxs.me/categories/linked-list/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Linked-List/ - 2022-08-25 + https://nicksxs.me/categories/Java/leetcode/Lowest-Common-Ancestor-of-a-Binary-Tree/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Apollo/value/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/Linked-List/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/linked-list/ - 2022-08-25 + 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-26 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/Lowest-Common-Ancestor-of-a-Binary-Tree/ - 2022-08-25 + https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Java/leetcode/Rotate-Image/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ - 2022-08-25 + 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-26 weekly 0.2 https://nicksxs.me/categories/Linux/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/categories/Java/Maven/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Interceptor-AOP/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Maven/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/data-analysis/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/docker/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Docker/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/DP/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ - 2022-08-25 + https://nicksxs.me/categories/Docker/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Mybatis/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/stack/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Redis/ - 2022-08-25 + https://nicksxs.me/categories/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/DP/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/linked-list/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/nginx/ - 2022-08-25 + https://nicksxs.me/categories/Redis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/php/ - 2022-08-25 + https://nicksxs.me/categories/Java/Mybatis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/stack/ - 2022-08-25 + https://nicksxs.me/categories/nginx/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/redis/ - 2022-08-25 + https://nicksxs.me/categories/php/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/linked-list/ - 2022-08-25 + https://nicksxs.me/categories/redis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ - 2022-08-25 + https://nicksxs.me/categories/leetcode/java/string/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/string/ - 2022-08-25 + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Spring/ - 2022-08-25 + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ - 2022-08-25 + https://nicksxs.me/categories/Spring/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/gc/ - 2022-08-25 + https://nicksxs.me/categories/Java/Spring/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ - 2022-08-25 + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-08-25 + https://nicksxs.me/categories/Java/gc/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Spring/ - 2022-08-25 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/MQ/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/ssh/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/ - 2022-08-25 + https://nicksxs.me/categories/git/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ - 2022-08-25 + https://nicksxs.me/categories/shell/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/git/ - 2022-08-25 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ - 2022-08-25 + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%94%9F%E6%B4%BB/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/shell/ - 2022-08-25 + https://nicksxs.me/categories/Redis/Distributed-Lock/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mysql/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Redis/Distributed-Lock/ - 2022-08-25 + https://nicksxs.me/categories/Mybatis/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Java/SpringBoot/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Mybatis/ - 2022-08-25 + https://nicksxs.me/categories/Java/Dubbo/RPC/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Dubbo-RPC/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/categories/Java/Dubbo/RPC/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ - 2022-08-25 + https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Java/Mybatis/Mysql/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Thread-dump/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ - 2022-08-25 + https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 2022-08-25 + https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ - 2022-08-25 + https://nicksxs.me/categories/Java/Design-Patterns/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mac/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/SpringBoot/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Design-Patterns/ - 2022-08-25 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ - 2022-08-25 + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BC%80%E8%BD%A6/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Rust/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/gc/jvm/ - 2022-08-25 + https://nicksxs.me/categories/Spring/Servlet/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/grep/ - 2022-08-25 + https://nicksxs.me/categories/Java/%E7%B1%BB%E5%8A%A0%E8%BD%BD/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Spring/Servlet/ - 2022-08-25 + https://nicksxs.me/categories/Java/gc/jvm/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/MQ/RocketMQ/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/ssh/%E6%8A%80%E5%B7%A7/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ - 2022-08-25 + https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/git/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-08-25 + https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ - 2022-08-25 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ - 2022-08-25 + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ - 2022-08-25 + https://nicksxs.me/categories/C/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/C/ - 2022-08-25 + https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Dubbo/ - 2022-08-25 + https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Dubbo/RPC/SPI/ - 2022-08-25 + https://nicksxs.me/categories/Dubbo/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ - 2022-08-25 + https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ - 2022-08-25 + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/top/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ - 2022-08-25 + https://nicksxs.me/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mysql/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mysql/%E7%B4%A2%E5%BC%95/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Redis/%E7%BC%93%E5%AD%98/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Mac/PHP/ - 2022-08-25 + https://nicksxs.me/categories/Java/Singleton/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Java/Singleton/ - 2022-08-25 + https://nicksxs.me/categories/Mac/PHP/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ - 2022-08-25 + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/ - 2022-08-25 + https://nicksxs.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Spring/Servlet/Interceptor/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E7%BE%8E%E5%9B%BD/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E5%8F%A3%E7%BD%A9/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/%E5%B9%B2%E6%B4%BB/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Docker/%E5%8F%91%E8%A1%8C%E7%89%88%E6%9C%AC/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/MQ/RocketMQ/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ - 2022-08-25 + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Spring/Mybatis/ - 2022-08-25 + https://nicksxs.me/categories/C/Redis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/C/Redis/ - 2022-08-25 + https://nicksxs.me/categories/Spring/Mybatis/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ - 2022-08-25 + https://nicksxs.me/categories/Dubbo/SPI/ + 2022-08-26 weekly 0.2 - https://nicksxs.me/categories/Dubbo/SPI/ - 2022-08-25 + https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E5%B7%A5%E5%85%B7/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mysql/%E6%BA%90%E7%A0%81/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/C/Mysql/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Mac/Homebrew/ - 2022-08-25 + 2022-08-26 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-25 + 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-26 weekly 0.2 - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/ - 2022-08-25 + 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-26 weekly 0.2 https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/RocketMQ/ - 2022-08-25 - weekly - 0.2 - - - - https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/top/%E6%8E%92%E5%BA%8F/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ThreadPool/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/PHP/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/ - 2022-08-25 + 2022-08-26 + weekly + 0.2 + + + + https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ + 2022-08-26 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-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/PHP/icu4c/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/RocketMQ/ - 2022-08-25 + 2022-08-26 weekly 0.2 https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/%E5%87%BB%E7%A9%BF/%E9%9B%AA%E5%B4%A9/ - 2022-08-25 + 2022-08-26 weekly 0.2 diff --git a/tags/DP/index.html b/tags/DP/index.html index 4e071133a1..aadf3534af 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 02628d3914..595ceba23c 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/java/page/3/index.html b/tags/java/page/3/index.html index 0a551d8775..441cad4f2d 100644 --- a/tags/java/page/3/index.html +++ b/tags/java/page/3/index.html @@ -1 +1 @@ -标签: java | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file +标签: Java | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file diff --git a/tags/java/page/5/index.html b/tags/java/page/5/index.html index d2160d26ba..afbd59c989 100644 --- a/tags/java/page/5/index.html +++ b/tags/java/page/5/index.html @@ -1 +1 @@ -标签: Java | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file +标签: java | Nicksxs's Blog

Nicksxs's Blog

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

0%
\ No newline at end of file diff --git a/tags/linked-list/index.html b/tags/linked-list/index.html index 6b735af962..dc16c8dd91 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 3be2d2f6a5..05384ccc13 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 5c48b5d5d3..8dd4cbec06 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