diff --git a/2019/06/18/openresty/index.html b/2019/06/18/openresty/index.html index 4994347194..53cd9e3b01 100644 --- a/2019/06/18/openresty/index.html +++ b/2019/06/18/openresty/index.html @@ -1,4 +1,4 @@ -openresty | Nicksxs's Blog

Nicksxs's Blog

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

openresty

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

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

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

http {
+openresty | Nicksxs's Blog

Nicksxs's Blog

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

openresty

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

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

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

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

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

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

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

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

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

  • 0%
    \ No newline at end of file +end

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

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

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

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

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

  • 0%
    \ No newline at end of file diff --git a/2019/12/26/redis数据结构介绍/index.html b/2019/12/26/redis数据结构介绍/index.html index 92b03616bb..a340491ca7 100644 --- a/2019/12/26/redis数据结构介绍/index.html +++ b/2019/12/26/redis数据结构介绍/index.html @@ -1,4 +1,4 @@ -redis数据结构介绍-第一部分 SDS,链表,字典 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍-第一部分 SDS,链表,字典

    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 {
    +redis数据结构介绍-第一部分 SDS,链表,字典 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍-第一部分 SDS,链表,字典

    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;
         // 字符串空余字符数
    diff --git a/2020/01/04/redis数据结构介绍二/index.html b/2020/01/04/redis数据结构介绍二/index.html
    index df12abab00..d9cb83d539 100644
    --- a/2020/01/04/redis数据结构介绍二/index.html
    +++ b/2020/01/04/redis数据结构介绍二/index.html
    @@ -1,4 +1,4 @@
    -redis数据结构介绍二-第二部分 跳表 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍二-第二部分 跳表

    跳表 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 */
    +redis数据结构介绍二-第二部分 跳表 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍二-第二部分 跳表

    跳表 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;
    diff --git a/2020/01/10/redis数据结构介绍三/index.html b/2020/01/10/redis数据结构介绍三/index.html
    index 85c50baaf2..b6b289f91a 100644
    --- a/2020/01/10/redis数据结构介绍三/index.html
    +++ b/2020/01/10/redis数据结构介绍三/index.html
    @@ -1,4 +1,4 @@
    -redis数据结构介绍三-第三部分 整数集合 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍三-第三部分 整数集合

    redis中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

    typedef struct intset {
    +redis数据结构介绍三-第三部分 整数集合 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍三-第三部分 整数集合

    redis中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

    typedef struct intset {
         // 编码方式
         uint32_t encoding;
         // 集合包含的元素数量
    diff --git a/2020/01/19/redis数据结构介绍四/index.html b/2020/01/19/redis数据结构介绍四/index.html
    index 947a414199..68db140506 100644
    --- a/2020/01/19/redis数据结构介绍四/index.html
    +++ b/2020/01/19/redis数据结构介绍四/index.html
    @@ -1,4 +1,4 @@
    -redis数据结构介绍四-第四部分 压缩表 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍四-第四部分 压缩表

    在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

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

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

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

    <prevlen> <encoding> <entry-data>

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

    * |00pppppp| - 1 byte
    +redis数据结构介绍四-第四部分 压缩表 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍四-第四部分 压缩表

    在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

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

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

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

    <prevlen> <encoding> <entry-data>

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

    * |00pppppp| - 1 byte
     *      String value with length less than or equal to 63 bytes (6 bits).
     *      "pppppp" represents the unsigned 6 bit length.
     * |01pppppp|qqqqqqqq| - 2 bytes
    diff --git a/2020/01/20/redis数据结构介绍五/index.html b/2020/01/20/redis数据结构介绍五/index.html
    index 913918d8c8..c79c8a2eb4 100644
    --- a/2020/01/20/redis数据结构介绍五/index.html
    +++ b/2020/01/20/redis数据结构介绍五/index.html
    @@ -1,4 +1,4 @@
    -redis数据结构介绍五-第五部分 对象 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍五-第五部分 对象

    前面说了这么些数据结构,其实大家对于 redis 最初的印象应该就是个 key-value 的缓存,类似于 memcache,redis 其实也是个 key-value,key 还是一样的字符串,或者说就是用 redis 自己的动态字符串实现,但是 value 其实就是前面说的那些数据结构,差不多快说完了,还有个 quicklist 后面还有一篇,这里先介绍下 redis 对于这些不同类型的 value 是怎么实现的,首先看下 redisObject 的源码头文件

    /* The actual Redis Object */
    +redis数据结构介绍五-第五部分 对象 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍五-第五部分 对象

    前面说了这么些数据结构,其实大家对于 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. */
    diff --git a/2020/01/22/redis数据结构介绍六/index.html b/2020/01/22/redis数据结构介绍六/index.html
    index 34548ef684..5a57059268 100644
    --- a/2020/01/22/redis数据结构介绍六/index.html
    +++ b/2020/01/22/redis数据结构介绍六/index.html
    @@ -1,4 +1,4 @@
    -redis数据结构介绍六 快表 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍六 快表

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

    /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
    +redis数据结构介绍六 快表 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis数据结构介绍六 快表

    这应该是 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.
    diff --git a/2020/04/12/redis系列介绍七/index.html b/2020/04/12/redis系列介绍七/index.html
    index 8d90dcfe3b..3d78ec3778 100644
    --- a/2020/04/12/redis系列介绍七/index.html
    +++ b/2020/04/12/redis系列介绍七/index.html
    @@ -1,4 +1,4 @@
    -redis系列介绍七-过期策略 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis系列介绍七-过期策略

    这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
    redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,

    redis过期策略

    redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
    懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
    主要代码是

    /* This function is called when we are going to perform some operation
    +redis系列介绍七-过期策略 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis系列介绍七-过期策略

    这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
    redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,

    redis过期策略

    redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
    懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
    主要代码是

    /* This function is called when we are going to perform some operation
      * in a given key, but such key may be already logically expired even if
      * it still exists in the database. The main way this function is called
      * is via lookupKey*() family of functions.
    diff --git a/2020/04/18/redis系列介绍八/index.html b/2020/04/18/redis系列介绍八/index.html
    index 9af6978d25..dd117a6a0a 100644
    --- a/2020/04/18/redis系列介绍八/index.html
    +++ b/2020/04/18/redis系列介绍八/index.html
    @@ -1,4 +1,4 @@
    -redis系列介绍八-淘汰策略 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis系列介绍八-淘汰策略

    LRU

    说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
    ,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
    当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
    redis 针对这个策略的改进做了一个实验,这里借用下图

    首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

    typedef struct redisObject {
    +redis系列介绍八-淘汰策略 | Nicksxs's Blog

    Nicksxs's Blog

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

    redis系列介绍八-淘汰策略

    LRU

    说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
    ,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
    当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
    redis 针对这个策略的改进做了一个实验,这里借用下图

    首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

    typedef struct redisObject {
         unsigned type:4;
         unsigned encoding:4;
         unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
    diff --git a/2020/11/01/Apollo-的-value-注解是怎么自动更新的/index.html b/2020/11/01/Apollo-的-value-注解是怎么自动更新的/index.html
    index 3ece1250a6..ec16cbdce2 100644
    --- a/2020/11/01/Apollo-的-value-注解是怎么自动更新的/index.html
    +++ b/2020/11/01/Apollo-的-value-注解是怎么自动更新的/index.html
    @@ -1,4 +1,4 @@
    -Apollo 的 value 注解是怎么自动更新的 | Nicksxs's Blog

    Nicksxs's Blog

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

    Apollo 的 value 注解是怎么自动更新的

    在前司和目前公司,用的配置中心都是使用的 Apollo,经过了业界验证,比较强大的配置管理系统,特别是在0.10 后开始支持对使用 value 注解的配置值进行自动更新,今天刚好有个同学问到我,就顺便写篇文章记录下,其实也是借助于 spring 强大的 bean 生命周期管理,可以实现BeanPostProcessor接口,使用postProcessBeforeInitialization方法,来对bean 内部的属性和方法进行判断,是否有 value 注解,如果有就是将它注册到一个 map 中,可以看到这个方法com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField

    @Override
    +Apollo 的 value 注解是怎么自动更新的 | Nicksxs's Blog

    Nicksxs's Blog

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

    Apollo 的 value 注解是怎么自动更新的

    在前司和目前公司,用的配置中心都是使用的 Apollo,经过了业界验证,比较强大的配置管理系统,特别是在0.10 后开始支持对使用 value 注解的配置值进行自动更新,今天刚好有个同学问到我,就顺便写篇文章记录下,其实也是借助于 spring 强大的 bean 生命周期管理,可以实现BeanPostProcessor接口,使用postProcessBeforeInitialization方法,来对bean 内部的属性和方法进行判断,是否有 value 注解,如果有就是将它注册到一个 map 中,可以看到这个方法com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField

    @Override
       protected void processField(Object bean, String beanName, Field field) {
         // register @Value on field
         Value value = field.getAnnotation(Value.class);
    @@ -61,4 +61,4 @@
            updateSpringValue(val);
          }
        }
    - }

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

    0%
    \ No newline at end of file + }

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

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

    LRU

    说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
    ,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
    当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
    redis 针对这个策略的改进做了一个实验,这里借用下图

    首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

    typedef struct redisObject {
    +  }
    剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据

    LRU

    说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
    ,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
    当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
    redis 针对这个策略的改进做了一个实验,这里借用下图

    首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

    typedef struct redisObject {
         unsigned type:4;
         unsigned encoding:4;
         unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
    @@ -529,7 +529,7 @@ uint8_t LFULogIncr(uint8_t counter) {
     | 10     | 10         | 18         | 142        | 255        | 255        |
     +--------+------------+------------+------------+------------+------------+
     | 100    | 8          | 11         | 49         | 143        | 255        |
    -+--------+------------+------------+------------+------------+------------+

    简而言之就是 lfu_log_factor 越大变化的越慢

    总结

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

    这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
    redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,

    redis过期策略

    redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
    懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
    主要代码是

    /* This function is called when we are going to perform some operation
    ++--------+------------+------------+------------+------------+------------+

    简而言之就是 lfu_log_factor 越大变化的越慢

    总结

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

    这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
    redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,

    redis过期策略

    redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
    懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
    主要代码是

    /* This function is called when we are going to perform some operation
      * in a given key, but such key may be already logically expired even if
      * it still exists in the database. The main way this function is called
      * is via lookupKey*() family of functions.
    diff --git a/page/33/index.html b/page/33/index.html
    index fd912a4f96..3b71a371a9 100644
    --- a/page/33/index.html
    +++ b/page/33/index.html
    @@ -158,7 +158,7 @@ OS name: "mac os x", version: "10.14.6", arch: "x86_64", family: "mac"

    hotspot/share/gc/g1/heapRegionType.hpp

    当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

    由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

    需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。

    注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。

    在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。

    今天是农历初八了,年前一个月的时候就准备做下今年的年终总结,可是写了一点觉得太情绪化了,希望后面写个平淡点的,正好最近技术方面还没有看到一个完整成文的内容,就来写一下这一年的总结,尽量少写一点太情绪化的东西。

    跳槽

    年初换了个公司,也算换了个环境,跟前公司不太一样,做的事情方向也不同,可能是侧重点不同,一开始有些不适应,主要是压力上,会觉得压力比较大,但是总体来说与人相处的部分还是不错的,做的技术方向还是Java,这里也感谢前东家让我有机会转了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公里的目标,体重也稍有下降,比较满意的结果。

    期待

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

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

    /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
    + } Tag;

    hotspot/share/gc/g1/heapRegionType.hpp

    当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

    由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

    需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。

    注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。

    在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。

    今天是农历初八了,年前一个月的时候就准备做下今年的年终总结,可是写了一点觉得太情绪化了,希望后面写个平淡点的,正好最近技术方面还没有看到一个完整成文的内容,就来写一下这一年的总结,尽量少写一点太情绪化的东西。

    跳槽

    年初换了个公司,也算换了个环境,跟前公司不太一样,做的事情方向也不同,可能是侧重点不同,一开始有些不适应,主要是压力上,会觉得压力比较大,但是总体来说与人相处的部分还是不错的,做的技术方向还是Java,这里也感谢前东家让我有机会转了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公里的目标,体重也稍有下降,比较满意的结果。

    期待

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

    这应该是 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.
    diff --git a/page/34/index.html b/page/34/index.html
    index 55063fe33c..2309559d96 100644
    --- a/page/34/index.html
    +++ b/page/34/index.html
    @@ -1,4 +1,4 @@
    -Nicksxs's Blog - What hurts more, the pain of hard work or the pain of regret?

    Nicksxs's Blog

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

    前面说了这么些数据结构,其实大家对于 redis 最初的印象应该就是个 key-value 的缓存,类似于 memcache,redis 其实也是个 key-value,key 还是一样的字符串,或者说就是用 redis 自己的动态字符串实现,但是 value 其实就是前面说的那些数据结构,差不多快说完了,还有个 quicklist 后面还有一篇,这里先介绍下 redis 对于这些不同类型的 value 是怎么实现的,首先看下 redisObject 的源码头文件

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

    Nicksxs's Blog

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

    前面说了这么些数据结构,其实大家对于 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. */
    @@ -33,7 +33,7 @@ typedef struct redisObject {
                                 * 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 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

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

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

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

    <prevlen> <encoding> <entry-data>

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

    * |00pppppp| - 1 byte
    +} 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 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

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

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

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

    <prevlen> <encoding> <entry-data>

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

    * |00pppppp| - 1 byte
     *      String value with length less than or equal to 63 bytes (6 bits).
     *      "pppppp" represents the unsigned 6 bit length.
     * |01pppppp|qqqqqqqq| - 2 bytes
    @@ -60,7 +60,7 @@ typedef struct redisObject {
     *      1 to 13 because 0000 and 1111 can not be used, so 1 should be
     *      subtracted from the encoded 4 bit value to obtain the right value.
     * |11111111| - End of ziplist special entry.

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

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

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

    redis中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

    typedef struct intset {
    +|**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中对于 set 其实有两种处理,对于元素均为整型,并且元素数目较少时,使用 intset 作为底层数据结构,否则使用 dict 作为底层数据结构,先看一下代码先

    typedef struct intset {
         // 编码方式
         uint32_t encoding;
         // 集合包含的元素数量
    @@ -76,7 +76,7 @@ typedef struct redisObject {
     #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

    跳表 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 */
    +};

    在初始化这个 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

    跳表 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;
    @@ -102,7 +102,7 @@ int zslRandomLevel(void) {
         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是现在服务端很常用的缓存中间件,其实原来还有memcache之类的竞品,但是现在貌似 redis 快一统江湖,这里当然不是在吹,只是个人角度的一个感觉,不权威只是主观感觉。
    redis 主要有五种数据结构,StringsListsSetsHashesSorted Sets,这五种数据结构先简单介绍下,Strings类型的其实就是我们最常用的 key-value,实际开发中也会用的最多;Lists是列表,这个有些会用来做队列,因为 redis 目前常用的版本支持丰富的列表操作;还有是Sets集合,这个主要的特点就是集合中元素不重复,可以用在有这类需求的场景里;Hashes是叫散列,类似于 Python 中的字典结构;还有就是Sorted Sets这个是个有序集合;一眼看这些其实没啥特别的,除了最后这个有序集合,不过去了解背后的实现方式还是比较有意思的。

    SDS 简单动态字符串

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

    struct sdshdr {
    +}

    当随机值跟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是现在服务端很常用的缓存中间件,其实原来还有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;
         // 字符串空余字符数
    diff --git a/search.xml b/search.xml
    index 4bb784338a..405b42534e 100644
    --- a/search.xml
    +++ b/search.xml
    @@ -21,32 +21,22 @@
           
       
       
    -    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公里的目标,体重也稍有下降,比较满意的结果。

    -

    期待

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

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

    +

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

    +

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

    +

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

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

    -

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

    -

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

    -

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

    + 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公里的目标,体重也稍有下降,比较满意的结果。

    +

    期待

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

    ]]>
    生活 - 年中总结 - 2020 + 年终总结 + 2019 生活 - 2020 - 年中总结 + 年终总结 + 2019
    @@ -117,10 +117,9 @@ - 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。
    整体的职业规划貌似陷入了比较大的困惑期,在目前公司发展前景不是很大,但是出去貌似也没有比较适合我的机会,总的来说还是杭州比较卷,个人觉得有自己的时间是非常重要的,而且这个不光是用来自我提升的,还是让自己有足够的时间做缓冲,有足够的时间锻炼减肥,时间少的情况下,不光会在仅有的时间里暴饮暴食,还没空锻炼,身体是革命的本钱,现在其实能特别明显地感觉到身体状态下滑,容易疲劳,焦虑。所以是否也许有可能以后要往外企这类的方向去发展。
    工作上其实还是有个不大不小的缺点,就是容易激动,容易焦虑,前一点可能有稍稍地改观,因为工作中的很多现状其实是我个人难以改变的,即使觉得不合理,但是结构在那里,还不如自己放宽心,尽量做好事情就行。第二点的话还是做得比较差,一直以来抗压能力都比较差,跟成长环境,家庭环境都有比较大的关系,而且说实在的特别是父母,基本也没有在这方面给我正向的帮助,比较擅长给我施压,从小就是通过压力让我好好读书,当个乖学生,考个好学校,并没有能真正地理解我的压力,教我或者帮助我解压,只会在那说着不着边际的空话,甚至经常反过来对我施压。还是希望能慢慢解开,这点可能对我身体也有影响,也许需要看一些心理疏导相关的书籍。工作篇暂时到这,后续还有其他篇,未完待续哈哈😀

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

    ]]>
    生活 @@ -129,8 +128,8 @@ 生活 年终总结 - 2021 - 拖更 + 2022 + 2023
    @@ -179,6 +178,23 @@ public: c++ + + 2021 年终总结 + /2022/01/22/2021-%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + 又是一年年终总结,本着极度讨厌实时需求的理念,我还是 T+N 发布这个年终总结

    +

    工作篇

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

    +]]>
    + + 生活 + 年终总结 + + + 生活 + 年终总结 + 2021 + 拖更 + +
    AQS篇二 之 Condition 浅析笔记 /2021/02/21/AQS-%E4%B9%8B-Condition-%E6%B5%85%E6%9E%90%E7%AC%94%E8%AE%B0/ @@ -904,6 +920,113 @@ public: 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 + +
    + + Apollo 如何获取当前环境 + /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/ + 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境

    +
    public enum Env{
    +  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
    +
    +  public static Env fromString(String env) {
    +    Env environment = EnvUtils.transformEnv(env);
    +    Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
    +    return environment;
    +  }
    +}
    +

    而这些解释

    +
    /**
    + * Here is the brief description for all the predefined environments:
    + * <ul>
    + *   <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
    + *   <li>DEV: Development environment</li>
    + *   <li>FWS: Feature Web Service Test environment</li>
    + *   <li>FAT: Feature Acceptance Test environment</li>
    + *   <li>UAT: User Acceptance Test environment</li>
    + *   <li>LPT: Load and Performance Test environment</li>
    + *   <li>PRO: Production environment</li>
    + *   <li>TOOLS: Tooling environment, a special area in production environment which allows
    + * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
    + * </ul>
    + */
    +

    那如果要在运行时知道 apollo 当前使用的环境可以用这个

    +
    Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
    +

    简单记录下。

    +]]>
    + + Java + + + Java + Apollo + environment + +
    add-two-number /2015/04/14/Add-Two-Number/ @@ -1434,50 +1557,9 @@ public: Java Apollo + environment value 注解 - environment - - - - Apollo 如何获取当前环境 - /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/ - 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境

    -
    public enum Env{
    -  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
    -
    -  public static Env fromString(String env) {
    -    Env environment = EnvUtils.transformEnv(env);
    -    Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
    -    return environment;
    -  }
    -}
    -

    而这些解释

    -
    /**
    - * Here is the brief description for all the predefined environments:
    - * <ul>
    - *   <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
    - *   <li>DEV: Development environment</li>
    - *   <li>FWS: Feature Web Service Test environment</li>
    - *   <li>FAT: Feature Acceptance Test environment</li>
    - *   <li>UAT: User Acceptance Test environment</li>
    - *   <li>LPT: Load and Performance Test environment</li>
    - *   <li>PRO: Production environment</li>
    - *   <li>TOOLS: Tooling environment, a special area in production environment which allows
    - * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
    - * </ul>
    - */
    -

    那如果要在运行时知道 apollo 当前使用的环境可以用这个

    -
    Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
    -

    简单记录下。

    -]]>
    - - Java - - - Java - Apollo - environment
    @@ -1534,82 +1616,16 @@ Node *clone(Node *graph) { - AbstractQueuedSynchronizer - /2019/09/23/AbstractQueuedSynchronizer/ - 最近看了大神的 AQS 的文章,之前总是断断续续地看一点,每次都知难而退,下次看又从头开始,昨天总算硬着头皮看完了第一部分
    首先 AQS 只要有这些属性

    -
    // 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
    -private transient volatile Node head;
    +    Comparator使用小记
    +    /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/
    +    在Java8的stream之前,将对象进行排序的时候,可能需要对象实现Comparable接口,或者自己实现一个Comparator,

    +

    比如这样子

    +

    我的对象是Entity

    +
    public class Entity {
     
    -// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
    -private transient volatile Node tail;
    +    private Long id;
     
    -// 这个是最重要的,代表当前锁的状态,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 - - - - Comparator使用小记 - /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/ - 在Java8的stream之前,将对象进行排序的时候,可能需要对象实现Comparable接口,或者自己实现一个Comparator,

    -

    比如这样子

    -

    我的对象是Entity

    -
    public class Entity {
    -
    -    private Long id;
    -
    -    private Long sortValue;
    +    private Long sortValue;
     
         public Long getId() {
             return id;
    @@ -1780,45 +1796,6 @@ Node *clone(Node *graph) {
         }
     }

    运行下可以看到运行结果

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

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

    譬如我在用的这个笔记本

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

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

    -
    class LhsPadding
    -{
    -    protected long p1, p2, p3, p4, p5, p6, p7;
    -}
    -
    -class Value extends LhsPadding
    -{
    -    protected volatile long value;
    -}
    -
    -class RhsPadding extends Value
    -{
    -    protected long p9, p10, p11, p12, p13, p14, p15;
    -}
    -
    -/**
    - * <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 @@ -1851,142 +1828,187 @@ Node *clone(Node *graph) {
    - G1收集器概述 - /2020/02/09/G1%E6%94%B6%E9%9B%86%E5%99%A8%E6%A6%82%E8%BF%B0/ - G1: The Garbage-First Collector, 垃圾回收优先的垃圾回收器,目标是用户多核 cpu 和大内存的机器,最大的特点就是可预测的停顿时间,官方给出的介绍是提供一个用户在大的堆内存情况下一个低延迟表现的解决方案,通常是 6GB 及以上的堆大小,有低于 0.5 秒稳定的可预测的停顿时间。

    -

    这里主要介绍这个比较新的垃圾回收器,在 G1 之前的垃圾回收器都是基于如下图的内存结构分布,有新生代,老年代和永久代(jdk8 之前),然后G1 往前的那些垃圾回收器都有个分代,比如 serial,parallel 等,一般有个应用的组合,最初的 serial 和 serial old,因为新生代和老年代的收集方式不太一样,新生代主要是标记复制,所以有 eden 跟两个 survival区,老年代一般用标记整理方式,而 G1 对这个不太一样。

    看一下 G1 的内存分布

    可以看到这有很大的不同,G1 通过将内存分成大小相等的 region,每个region是存在于一个连续的虚拟内存范围,对于某个 region 来说其角色是类似于原来的收集器的Eden、Survivor、Old Generation,这个具体在代码层面

    -
    // We encode the value of the heap region type so the generation can be
    - // determined quickly. The tag is split into two parts:
    - //
    - //   major type (young, old, humongous, archive)           : top N-1 bits
    - //   minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
    - //
    - // If there's need to increase the number of minor types in the
    - // future, we'll have to increase the size of the latter and hence
    - // decrease the size of the former.
    - //
    - // 00000 0 [ 0] Free
    - //
    - // 00001 0 [ 2] Young Mask
    - // 00001 0 [ 2] Eden
    - // 00001 1 [ 3] Survivor
    - //
    - // 00010 0 [ 4] Humongous Mask
    - // 00100 0 [ 8] Pinned Mask
    - // 00110 0 [12] Starts Humongous
    - // 00110 1 [13] Continues Humongous
    - //
    - // 01000 0 [16] Old Mask
    - //
    - // 10000 0 [32] Archive Mask
    - // 11100 0 [56] Open Archive
    - // 11100 1 [57] Closed Archive
    - //
    - typedef enum {
    -   FreeTag               = 0,
    +    Disruptor 系列三
    +    /2022/09/25/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%89/
    +    原来一直有点被误导,
    gatingSequences用来标识每个 processer 的操作位点,但是怎么记录更新有点搞不清楚
    其实问题在于 gatingSequences 是个 Sequence 数组,首先要看下怎么加进去的,
    可以看到是在 com.lmax.disruptor.RingBuffer#addGatingSequences 这个方法里添加
    首先是 com.lmax.disruptor.dsl.Disruptor#handleEventsWith(com.lmax.disruptor.EventHandler<? super T>...)
    然后执行 com.lmax.disruptor.dsl.Disruptor#createEventProcessors(com.lmax.disruptor.Sequence[], com.lmax.disruptor.EventHandler<? super T>[])

    +
    EventHandlerGroup<T> createEventProcessors(
    +        final Sequence[] barrierSequences,
    +        final EventHandler<? super T>[] eventHandlers)
    +    {
    +        checkNotStarted();
     
    -   YoungMask             = 2,
    -   EdenTag               = YoungMask,
    -   SurvTag               = YoungMask + 1,
    +        final Sequence[] processorSequences = new Sequence[eventHandlers.length];
    +        final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);
     
    -   HumongousMask         = 4,
    -   PinnedMask            = 8,
    -   StartsHumongousTag    = HumongousMask | PinnedMask,
    -   ContinuesHumongousTag = HumongousMask | PinnedMask + 1,
    +        for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
    +        {
    +            final EventHandler<? super T> eventHandler = eventHandlers[i];
     
    -   OldMask               = 16,
    -   OldTag                = OldMask,
    +            // 这里将 handler 包装成一个 BatchEventProcessor
    +            final BatchEventProcessor<T> batchEventProcessor =
    +                new BatchEventProcessor<>(ringBuffer, barrier, eventHandler);
     
    -   // Archive regions are regions with immutable content (i.e. not reclaimed, and
    -   // not allocated into during regular operation). They differ in the kind of references
    -   // allowed for the contained objects:
    -   // - Closed archive regions form a separate self-contained (closed) object graph
    -   // within the set of all of these regions. No references outside of closed
    -   // archive regions are allowed.
    -   // - Open archive regions have no restrictions on the references of their objects.
    -   // Objects within these regions are allowed to have references to objects
    -   // contained in any other kind of regions.
    -   ArchiveMask           = 32,
    -   OpenArchiveTag        = ArchiveMask | PinnedMask | OldMask,
    -   ClosedArchiveTag      = ArchiveMask | PinnedMask | OldMask + 1
    - } Tag;
    + if (exceptionHandler != null) + { + batchEventProcessor.setExceptionHandler(exceptionHandler); + } -

    hotspot/share/gc/g1/heapRegionType.hpp

    -

    当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

    -

    由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

    -

    需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。

    -

    注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。

    -

    在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。

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

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

    -
    
    -package javax.servlet;
    +            consumerRepository.add(batchEventProcessor, eventHandler, barrier);
    +            processorSequences[i] = batchEventProcessor.getSequence();
    +        }
     
    -import java.io.IOException;
    +        updateGatingSequencesForNextInChain(barrierSequences, processorSequences);
     
    -/**
    - * Defines methods that all servlets must implement.
    - *
    - * <p>
    - * A servlet is a small Java program that runs within a Web server. Servlets
    - * receive and respond to requests from Web clients, usually across HTTP, the
    - * HyperText Transfer Protocol.
    - *
    - * <p>
    - * To implement this interface, you can write a generic servlet that extends
    - * <code>javax.servlet.GenericServlet</code> or an HTTP servlet that extends
    - * <code>javax.servlet.http.HttpServlet</code>.
    - *
    - * <p>
    - * This interface defines methods to initialize a servlet, to service requests,
    - * and to remove a servlet from the server. These are known as life-cycle
    - * methods and are called in the following sequence:
    - * <ol>
    - * <li>The servlet is constructed, then initialized with the <code>init</code>
    - * method.
    - * <li>Any calls from clients to the <code>service</code> method are handled.
    - * <li>The servlet is taken out of service, then destroyed with the
    - * <code>destroy</code> method, then garbage collected and finalized.
    - * </ol>
    - *
    - * <p>
    - * In addition to the life-cycle methods, this interface provides the
    - * <code>getServletConfig</code> method, which the servlet can use to get any
    - * startup information, and the <code>getServletInfo</code> method, which allows
    +        return new EventHandlerGroup<>(this, consumerRepository, processorSequences);
    +    }
    + +

    BatchEventProcessor 在类内有个定义 sequence

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

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

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

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

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

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

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

    然后是

    +
    private void processEvents()
    +    {
    +        T event = null;
    +        long nextSequence = sequence.get() + 1L;
    +
    +        while (true)
    +        {
    +            try
    +            {
    +                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
    +                if (batchStartAware != null)
    +                {
    +                    batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
    +                }
    +
    +                while (nextSequence <= availableSequence)
    +                {
    +                    event = dataProvider.get(nextSequence);
    +                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
    +                    nextSequence++;
    +                }
    +                // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
    +                sequence.set(availableSequence);
    +            }
    +            catch (final TimeoutException e)
    +            {
    +                notifyTimeout(sequence.get());
    +            }
    +            catch (final AlertException ex)
    +            {
    +                if (running.get() != RUNNING)
    +                {
    +                    break;
    +                }
    +            }
    +            catch (final Throwable ex)
    +            {
    +                handleEventException(ex, nextSequence, event);
    +                // 如果是异常就只是 nextSequence
    +                sequence.set(nextSequence);
    +                nextSequence++;
    +            }
    +        }
    +    }
    +]]>
    + + Java + + + Java + Disruptor + +
    + + Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥? + /2020/08/22/Filter-Intercepter-Aop-%E5%95%A5-%E5%95%A5-%E5%95%A5-%E8%BF%99%E4%BA%9B%E9%83%BD%E6%98%AF%E5%95%A5/ + 本来是想取个像现在那些公众号转了又转的文章标题,”面试官再问你xxxxx,就把这篇文章甩给他看”这种标题,但是觉得实在太 low 了,还是用一部我比较喜欢的电影里的一句台词,《人在囧途》里王宝强对着那张老板给他的欠条,看不懂字时候说的那句,这些都是些啥(第四声)
    当我刚开始面 Java 的时候,其实我真的没注意这方面的东西,实话说就是不知道这些是啥,开发中用过 Interceptor和 Aop,了解 aop 的实现原理,但是不知道 Java web 中的 Filter 是怎么回事,知道 dubbo 的 filter,就这样,所以被问到了的确是回答不出来,可能就觉得这个渣渣,这么简单的都不会,所以还是花点时间来看看这个是个啥,为了避免我口吐芬芳,还是耐下性子来简单说下这几个东西
    首先是 servlet,怎么去解释这个呢,因为之前是 PHPer,所以比较喜欢用它来举例子,在普通的 PHP 的 web 应用中一般有几部分组成,接受 HTTP 请求的是前置的 nginx 或者 apache,但是这俩玩意都是只能处理静态的请求,远古时代 PHP 和 HTML 混编是通过 apache 的 php module,跟后来 nginx 使用 php-fpm 其实道理类似,就是把请求中需要 PHP 处理的转发给 PHP,在 Java 中呢,是有个比较牛叉的叫 Tomcat 的,它可以把请求转成 servlet,而 servlet 其实就是一种实现了特定接口的 Java 代码,

    +
    
    +package javax.servlet;
    +
    +import java.io.IOException;
    +
    +/**
    + * Defines methods that all servlets must implement.
    + *
    + * <p>
    + * A servlet is a small Java program that runs within a Web server. Servlets
    + * receive and respond to requests from Web clients, usually across HTTP, the
    + * HyperText Transfer Protocol.
    + *
    + * <p>
    + * To implement this interface, you can write a generic servlet that extends
    + * <code>javax.servlet.GenericServlet</code> or an HTTP servlet that extends
    + * <code>javax.servlet.http.HttpServlet</code>.
    + *
    + * <p>
    + * This interface defines methods to initialize a servlet, to service requests,
    + * and to remove a servlet from the server. These are known as life-cycle
    + * methods and are called in the following sequence:
    + * <ol>
    + * <li>The servlet is constructed, then initialized with the <code>init</code>
    + * method.
    + * <li>Any calls from clients to the <code>service</code> method are handled.
    + * <li>The servlet is taken out of service, then destroyed with the
    + * <code>destroy</code> method, then garbage collected and finalized.
    + * </ol>
    + *
    + * <p>
    + * In addition to the life-cycle methods, this interface provides the
    + * <code>getServletConfig</code> method, which the servlet can use to get any
    + * startup information, and the <code>getServletInfo</code> method, which allows
      * the servlet to return basic information about itself, such as author,
      * version, and copyright.
      *
    @@ -2417,80 +2439,202 @@ Node *clone(Node *graph) {
           
       
       
    -    Leetcode 021 合并两个有序链表 ( Merge Two Sorted Lists ) 题解分析
    -    /2021/10/07/Leetcode-021-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8-Merge-Two-Sorted-Lists-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
    -    题目介绍

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

    -

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

    -

    示例 1

    -
    -

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

    -
    -

    示例 2

    -

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

    -
    -

    示例 3

    -

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

    -
    -

    简要分析

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

    -

    题解代码

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    -        // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
    -        if (l1 == null) {
    -            return l2;
    -        }
    -        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;
    -    }
    + G1收集器概述 + /2020/02/09/G1%E6%94%B6%E9%9B%86%E5%99%A8%E6%A6%82%E8%BF%B0/ + G1: The Garbage-First Collector, 垃圾回收优先的垃圾回收器,目标是用户多核 cpu 和大内存的机器,最大的特点就是可预测的停顿时间,官方给出的介绍是提供一个用户在大的堆内存情况下一个低延迟表现的解决方案,通常是 6GB 及以上的堆大小,有低于 0.5 秒稳定的可预测的停顿时间。

    +

    这里主要介绍这个比较新的垃圾回收器,在 G1 之前的垃圾回收器都是基于如下图的内存结构分布,有新生代,老年代和永久代(jdk8 之前),然后G1 往前的那些垃圾回收器都有个分代,比如 serial,parallel 等,一般有个应用的组合,最初的 serial 和 serial old,因为新生代和老年代的收集方式不太一样,新生代主要是标记复制,所以有 eden 跟两个 survival区,老年代一般用标记整理方式,而 G1 对这个不太一样。

    看一下 G1 的内存分布

    可以看到这有很大的不同,G1 通过将内存分成大小相等的 region,每个region是存在于一个连续的虚拟内存范围,对于某个 region 来说其角色是类似于原来的收集器的Eden、Survivor、Old Generation,这个具体在代码层面

    +
    // We encode the value of the heap region type so the generation can be
    + // determined quickly. The tag is split into two parts:
    + //
    + //   major type (young, old, humongous, archive)           : top N-1 bits
    + //   minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
    + //
    + // If there's need to increase the number of minor types in the
    + // future, we'll have to increase the size of the latter and hence
    + // decrease the size of the former.
    + //
    + // 00000 0 [ 0] Free
    + //
    + // 00001 0 [ 2] Young Mask
    + // 00001 0 [ 2] Eden
    + // 00001 1 [ 3] Survivor
    + //
    + // 00010 0 [ 4] Humongous Mask
    + // 00100 0 [ 8] Pinned Mask
    + // 00110 0 [12] Starts Humongous
    + // 00110 1 [13] Continues Humongous
    + //
    + // 01000 0 [16] Old Mask
    + //
    + // 10000 0 [32] Archive Mask
    + // 11100 0 [56] Open Archive
    + // 11100 1 [57] Closed Archive
    + //
    + typedef enum {
    +   FreeTag               = 0,
     
    -

    结果

    -]]> - - Java - leetcode - - - leetcode - java - 题解 - - - - JVM源码分析之G1垃圾收集器分析一 - /2019/12/07/JVM-G1-Part-1/ + YoungMask = 2, + EdenTag = YoungMask, + SurvTag = YoungMask + 1, + + HumongousMask = 4, + PinnedMask = 8, + StartsHumongousTag = HumongousMask | PinnedMask, + ContinuesHumongousTag = HumongousMask | PinnedMask + 1, + + OldMask = 16, + OldTag = OldMask, + + // Archive regions are regions with immutable content (i.e. not reclaimed, and + // not allocated into during regular operation). They differ in the kind of references + // allowed for the contained objects: + // - Closed archive regions form a separate self-contained (closed) object graph + // within the set of all of these regions. No references outside of closed + // archive regions are allowed. + // - Open archive regions have no restrictions on the references of their objects. + // Objects within these regions are allowed to have references to objects + // contained in any other kind of regions. + ArchiveMask = 32, + OpenArchiveTag = ArchiveMask | PinnedMask | OldMask, + ClosedArchiveTag = ArchiveMask | PinnedMask | OldMask + 1 + } Tag;
    + +

    hotspot/share/gc/g1/heapRegionType.hpp

    +

    当执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1执行并发全局标记阶段,以确定整个堆中对象的存活性。标记阶段完成后,G1知道哪些region是基本空的。它首先收集这些region,通常会产生大量的可用空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

    +

    由G1标识为可回收的区域是使用撤离的方式(Evacuation)。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每次垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这是优于前面两种方法的。 CMS(并发标记扫描)垃圾收集器不进行压缩。 ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

    +

    需要重点注意的是,G1不是实时收集器。它很有可能达到设定的暂停时间目标,但并非绝对确定。 G1根据先前收集的数据,估算在用户指定的目标时间内可以收集多少个区域。因此,收集器具有收集区域成本的合理准确的模型,并且收集器使用此模型来确定要收集哪些和多少个区域,同时保持在暂停时间目标之内。

    +

    注意:G1同时具有并发(与应用程序线程一起运行,例如优化,标记,清理)和并行(多线程,例如stw)阶段。Full GC仍然是单线程的,但是如果正确调优,您的应用程序应该可以避免Full GC。

    +

    在前面那篇中在代码层面简单的了解了这个可预测时间的过程,这也是 G1 的一大特点。

    +]]>
    + + Java + JVM + GC + C++ + + + Java + JVM + C++ + G1 + GC + Garbage-First Collector + +
    + + 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 + + + Java + Disruptor + +
    + + 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;
    +    }
    + +

    结果

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

    HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
                                                    uint gc_count_before,
    @@ -3094,45 +3238,6 @@ Node *clone(Node *graph) {
             C++
           
       
    -  
    -    Leetcode 053 最大子序和 ( Maximum Subarray ) 题解分析
    -    /2021/11/28/Leetcode-053-%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C-Maximum-Subarray-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
    -    题目介绍

    Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

    -

    A subarray is a contiguous part of an array.

    -

    示例

    Example 1:

    -
    -

    Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
    Output: 6
    Explanation: [4,-1,2,1] has the largest sum = 6.

    -
    -

    Example 2:

    -
    -

    Input: nums = [1]
    Output: 1

    -
    -

    Example 3:

    -
    -

    Input: nums = [5,4,-1,7,8]
    Output: 23

    -
    -

    说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。

    -

    代码

    public int maxSubArray(int[] nums) {
    -        int max = nums[0];
    -        int sum = nums[0];
    -        for (int i = 1; i < nums.length; i++) {
    -            // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
    -            sum = Math.max(nums[i], sum + nums[i]);
    -            // max 是用来承载最大值的
    -            max = Math.max(max, sum);
    -        }
    -        return max;
    -    }
    ]]>
    - - Java - leetcode - - - leetcode - java - 题解 - -
    Leetcode 028 实现 strStr() ( Implement strStr() ) 题解分析 /2021/10/31/Leetcode-028-%E5%AE%9E%E7%8E%B0-strStr-Implement-strStr-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ @@ -3209,569 +3314,187 @@ Output: 0 - Leetcode 104 二叉树的最大深度(Maximum Depth of Binary Tree) 题解分析 - /2020/10/25/Leetcode-104-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6-Maximum-Depth-of-Binary-Tree-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

    -

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

    -

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

    -

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

    -
      3
    - / \
    -9  20
    -  /  \
    - 15   7
    -

    返回它的最大深度 3 。

    -

    代码

    // 主体是个递归的应用
    -public int maxDepth(TreeNode root) {
    -    // 节点的退出条件之一
    -    if (root == null) {
    -        return 0;
    -    }
    -    int left = 0;
    -    int right = 0;
    -    // 存在左子树,就递归左子树
    -    if (root.left != null) {
    -        left = maxDepth(root.left);
    -    }
    -    // 存在右子树,就递归右子树
    -    if (root.right != null) {
    -        right = maxDepth(root.right);
    -    }
    -    // 前面返回后,左右取大者
    -    return Math.max(left + 1, right + 1);
    -}
    -

    分析

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

    -]]>
    - - Java - leetcode - Binary Tree - java - Binary Tree - DFS - - - leetcode - java - Binary Tree - DFS - 二叉树 - 题解 - -
    - - Leetcode 105 从前序与中序遍历序列构造二叉树(Construct Binary Tree from Preorder and Inorder Traversal) 题解分析 - /2020/12/13/Leetcode-105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

    -

    注意

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

    -

    例子:

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

    返回的二叉树

    -
      3
    - / \
    -9  20
    -  /  \
    - 15   7
    + Headscale初体验以及踩坑记 + /2023/01/22/Headscale%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BB%A5%E5%8F%8A%E8%B8%A9%E5%9D%91%E8%AE%B0/ + 最近或者说很久以前就想着能够把几个散装服务器以及家里的网络连起来,譬如一些remote desktop的访问,之前搞了下frp,因为家里电脑没怎么注意安全性就被搞了一下,所以还是想用相对更安全的方式,比如限定ip和端口进行访问,但是感觉ip也不固定就比较难搞,后来看到了 TailscaleHeadscale 的方式,就想着试试看,没想到一开始就踩了几个比较莫名其妙的坑。
    可以按官方文档去搭建,也可以在网上找一些其他人搭建的教程。我碰到的主要是关于配置文件的问题

    +

    第一个问题

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

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

    +
    ---
    +# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
    +#
    +# - `/etc/headscale`
    +# - `~/.headscale`
    +# - current working directory
     
    +# The url clients will connect to.
    +# Typically this will be a domain like:
    +#
    +# https://myheadscale.example.com:443
    +#
    +server_url: http://127.0.0.1:8080
     
    -

    简要分析

    看到这个题可以想到一个比较常规的解法就是递归拆树,前序就是根左右,中序就是左根右,然后就是通过前序已经确定的根在中序中找到,然后去划分左右子树,这个例子里是 3,找到中序中的位置,那么就可以确定,9 是左子树,15,20,7是右子树,然后对应的可以根据左右子树的元素数量在前序中划分左右子树,再继续递归就行

    -
    class Solution {
    -    public TreeNode buildTree(int[] preorder, int[] inorder) {
    -      // 获取下数组长度
    -        int n = preorder.length;
    -        // 排除一下异常和边界
    -        if (n != inorder.length) {
    -            return null;
    -        }
    -        if (n == 0) {
    -            return null;
    -        }
    -        if (n == 1) {
    -            return new TreeNode(preorder[0]);
    -        }
    -        // 获得根节点
    -        TreeNode node = new TreeNode(preorder[0]);
    -        int pos = 0;
    -        // 找到中序中的位置
    -        for (int i = 0; i < inorder.length; i++) {
    -            if (node.val == inorder[i]) {
    -                pos = i;
    -                break;
    -            }
    -        }
    -        // 划分左右再进行递归,注意下`Arrays.copyOfRange`的用法
    -        node.left = buildTree(Arrays.copyOfRange(preorder, 1, pos + 1), Arrays.copyOfRange(inorder, 0, pos));
    -        node.right = buildTree(Arrays.copyOfRange(preorder, pos + 1, n), Arrays.copyOfRange(inorder, pos + 1, n));
    -        return node;
    -    }
    -}
    ]]> - - Java - leetcode - Binary Tree - java - Binary Tree - DFS - - - leetcode - java - Binary Tree - 二叉树 - 题解 - 递归 - Preorder Traversal - Inorder Traversal - 前序 - 中序 - - - - Disruptor 系列三 - /2022/09/25/Disruptor-%E7%B3%BB%E5%88%97%E4%B8%89/ - 原来一直有点被误导,
    gatingSequences用来标识每个 processer 的操作位点,但是怎么记录更新有点搞不清楚
    其实问题在于 gatingSequences 是个 Sequence 数组,首先要看下怎么加进去的,
    可以看到是在 com.lmax.disruptor.RingBuffer#addGatingSequences 这个方法里添加
    首先是 com.lmax.disruptor.dsl.Disruptor#handleEventsWith(com.lmax.disruptor.EventHandler<? super T>...)
    然后执行 com.lmax.disruptor.dsl.Disruptor#createEventProcessors(com.lmax.disruptor.Sequence[], com.lmax.disruptor.EventHandler<? super T>[])

    -
    EventHandlerGroup<T> createEventProcessors(
    -        final Sequence[] barrierSequences,
    -        final EventHandler<? super T>[] eventHandlers)
    -    {
    -        checkNotStarted();
    +# Address to listen to / bind to on the server
    +#
    +# For production:
    +# listen_addr: 0.0.0.0:8080
    +listen_addr: 127.0.0.1:8080
     
    -        final Sequence[] processorSequences = new Sequence[eventHandlers.length];
    -        final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);
    +# Address to listen to /metrics, you may want
    +# to keep this endpoint private to your internal
    +# network
    +#
    +metrics_listen_addr: 127.0.0.1:9090
     
    -        for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
    -        {
    -            final EventHandler<? super T> eventHandler = eventHandlers[i];
    +# Address to listen for gRPC.
    +# gRPC is used for controlling a headscale server
    +# remotely with the CLI
    +# Note: Remote access _only_ works if you have
    +# valid certificates.
    +#
    +# For production:
    +# grpc_listen_addr: 0.0.0.0:50443
    +grpc_listen_addr: 127.0.0.1:50443
     
    -            // 这里将 handler 包装成一个 BatchEventProcessor
    -            final BatchEventProcessor<T> batchEventProcessor =
    -                new BatchEventProcessor<>(ringBuffer, barrier, eventHandler);
    +# Allow the gRPC admin interface to run in INSECURE
    +# mode. This is not recommended as the traffic will
    +# be unencrypted. Only enable if you know what you
    +# are doing.
    +grpc_allow_insecure: false
     
    -            if (exceptionHandler != null)
    -            {
    -                batchEventProcessor.setExceptionHandler(exceptionHandler);
    -            }
    +# Private key used to encrypt the traffic between headscale
    +# and Tailscale clients.
    +# The private key file will be autogenerated if it's missing.
    +#
    +# For production:
    +# /var/lib/headscale/private.key
    +private_key_path: ./private.key
     
    -            consumerRepository.add(batchEventProcessor, eventHandler, barrier);
    -            processorSequences[i] = batchEventProcessor.getSequence();
    -        }
    +# The Noise section includes specific configuration for the
    +# TS2021 Noise protocol
    +noise:
    +  # The Noise private key is used to encrypt the
    +  # traffic between headscale and Tailscale clients when
    +  # using the new Noise-based protocol. It must be different
    +  # from the legacy private key.
    +  #
    +  # For production:
    +  # private_key_path: /var/lib/headscale/noise_private.key
    +  private_key_path: ./noise_private.key
     
    -        updateGatingSequencesForNextInChain(barrierSequences, processorSequences);
    +# List of IP prefixes to allocate tailaddresses from.
    +# Each prefix consists of either an IPv4 or IPv6 address,
    +# and the associated prefix length, delimited by a slash.
    +# While this looks like it can take arbitrary values, it
    +# needs to be within IP ranges supported by the Tailscale
    +# client.
    +# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
    +# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
    +ip_prefixes:
    +  - fd7a:115c:a1e0::/48
    +  - 100.64.0.0/10
     
    -        return new EventHandlerGroup<>(this, consumerRepository, processorSequences);
    -    }
    +# DERP is a relay system that Tailscale uses when a direct +# connection cannot be established. +# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp +# +# headscale needs a list of DERP servers that can be presented +# to the clients. +derp: + server: + # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config + # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place + enabled: false -

    BatchEventProcessor 在类内有个定义 sequence

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

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

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

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

    -
    private void updateGatingSequencesForNextInChain(final Sequence[] barrierSequences, final Sequence[] processorSequences)
    -    {
    -        if (processorSequences.length > 0)
    -        {
    -            // 然后在这里添加
    -            ringBuffer.addGatingSequences(processorSequences);
    -            for (final Sequence barrierSequence : barrierSequences)
    -            {
    -                ringBuffer.removeGatingSequence(barrierSequence);
    -            }
    -            consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
    -        }
    -    }
    + # Region ID to use for the embedded DERP server. + # The local DERP prevails if the region ID collides with other region ID coming from + # the regular DERP config. + region_id: 999 -

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

    -
    public void run()
    -    {
    -        if (running.compareAndSet(IDLE, RUNNING))
    -        {
    -            sequenceBarrier.clearAlert();
    +    # Region code and name are displayed in the Tailscale UI to identify a DERP region
    +    region_code: "headscale"
    +    region_name: "Headscale Embedded DERP"
     
    -            notifyStart();
    -            try
    -            {
    -                if (running.get() == RUNNING)
    -                {
    -                    processEvents();
    -                }
    -            }
    -            finally
    -            {
    -                notifyShutdown();
    -                running.set(IDLE);
    -            }
    -        }
    -        else
    -        {
    -            // This is a little bit of guess work.  The running state could of changed to HALTED by
    -            // this point.  However, Java does not have compareAndExchange which is the only way
    -            // to get it exactly correct.
    -            if (running.get() == RUNNING)
    -            {
    -                throw new IllegalStateException("Thread is already running");
    -            }
    -            else
    -            {
    -                earlyExit();
    -            }
    -        }
    -    }
    -

    然后是

    -
    private void processEvents()
    -    {
    -        T event = null;
    -        long nextSequence = sequence.get() + 1L;
    +    # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
    +    # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
    +    #
    +    # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
    +    stun_listen_addr: "0.0.0.0:3478"
     
    -        while (true)
    -        {
    -            try
    -            {
    -                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
    -                if (batchStartAware != null)
    -                {
    -                    batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
    -                }
    +  # List of externally available DERP maps encoded in JSON
    +  urls:
    +    - https://controlplane.tailscale.com/derpmap/default
     
    -                while (nextSequence <= availableSequence)
    -                {
    -                    event = dataProvider.get(nextSequence);
    -                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
    -                    nextSequence++;
    -                }
    -                // 如果正常处理完,那就是会更新为 availableSequence,因为都处理好了
    -                sequence.set(availableSequence);
    -            }
    -            catch (final TimeoutException e)
    -            {
    -                notifyTimeout(sequence.get());
    -            }
    -            catch (final AlertException ex)
    -            {
    -                if (running.get() != RUNNING)
    -                {
    -                    break;
    -                }
    -            }
    -            catch (final Throwable ex)
    -            {
    -                handleEventException(ex, nextSequence, event);
    -                // 如果是异常就只是 nextSequence
    -                sequence.set(nextSequence);
    -                nextSequence++;
    -            }
    -        }
    -    }
    -]]>
    - - Java - - - Java - Disruptor - -
    - - Leetcode 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");
    -    }
    -  }
    +  # Locally available DERP map files encoded in YAML
    +  #
    +  # This option is mostly interesting for people hosting
    +  # their own DERP servers:
    +  # https://tailscale.com/kb/1118/custom-derp-servers/
    +  #
    +  # paths:
    +  #   - /etc/headscale/derp-example.yaml
    +  paths: []
     
    -  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;
    +  # If enabled, a worker will be set up to periodically
    +  # refresh the given sources and update the derpmap
    +  # will be set up.
    +  auto_update_enabled: true
     
    -    public FooBar(int n) {
    -        this.n = n;
    -    }
    +  # How often should we check for DERP updates?
    +  update_frequency: 24h
     
    -    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();
    -        }
    -    }
    +# Disables the automatic check for headscale updates on startup
    +disable_check_updates: false
     
    -    public void bar(Runnable printBar) throws InterruptedException {
    -        
    -        for (int i = 0; i < n; i++) {
    -            bar.acquire();
    -            // printBar.run() outputs "bar". Do not change or remove this line.
    -        	printBar.run();
    -            foo.release();
    -        }
    -    }
    -}
    -]]>
    - - Java - leetcode - - - leetcode - java - 题解 - Print FooBar Alternately - -
    - - Leetcode 121 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 题解分析 - /2021/03/14/Leetcode-121-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-Best-Time-to-Buy-and-Sell-Stock-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

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

    -

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

    -

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

    -

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

    -

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

    -

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

    -

    简单分析

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

    -
    int maxSofar = 0;
    -public int maxProfit(int[] prices) {
    -    if (prices.length <= 1) {
    -        return 0;
    -    }
    -    int maxIn = prices[0];
    -    int maxOut = prices[0];
    -    for (int i = 1; i < prices.length; i++) {
    -        if (maxIn > prices[i]) {
    -            // 当循环当前值小于之前的买入值时就当成买入值,同时卖出也要更新
    -            maxIn = prices[i];
    -            maxOut = prices[i];
    -        }
    -        if (prices[i] > maxOut) {
    -            // 表示一个可卖出点,即比买入值高时
    -            maxOut = prices[i];
    -            // 需要设置一个历史值
    -            maxSofar = Math.max(maxSofar, maxOut - maxIn);
    -        }
    -    }
    -    return maxSofar;
    -}
    +# Time before an inactive ephemeral node is deleted? +ephemeral_node_inactivity_timeout: 30m -

    总结下

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

    -]]>
    - - Java - leetcode - java - DP - DP - - - leetcode - java - 题解 - DP - -
    - - Headscale初体验以及踩坑记 - /2023/01/22/Headscale%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BB%A5%E5%8F%8A%E8%B8%A9%E5%9D%91%E8%AE%B0/ - 最近或者说很久以前就想着能够把几个散装服务器以及家里的网络连起来,譬如一些remote desktop的访问,之前搞了下frp,因为家里电脑没怎么注意安全性就被搞了一下,所以还是想用相对更安全的方式,比如限定ip和端口进行访问,但是感觉ip也不固定就比较难搞,后来看到了 TailscaleHeadscale 的方式,就想着试试看,没想到一开始就踩了几个比较莫名其妙的坑。
    可以按官方文档去搭建,也可以在网上找一些其他人搭建的教程。我碰到的主要是关于配置文件的问题

    -

    第一个问题

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

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

    -
    ---
    -# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
    -#
    -# - `/etc/headscale`
    -# - `~/.headscale`
    -# - current working directory
    +# Period to check for node updates within the tailnet. A value too low will severely affect
    +# CPU consumption of Headscale. A value too high (over 60s) will cause problems
    +# for the nodes, as they won't get updates or keep alive messages frequently enough.
    +# In case of doubts, do not touch the default 10s.
    +node_update_check_interval: 10s
     
    -# The url clients will connect to.
    -# Typically this will be a domain like:
    -#
    -# https://myheadscale.example.com:443
    -#
    -server_url: http://127.0.0.1:8080
    +# SQLite config
    +db_type: sqlite3
     
    -# Address to listen to / bind to on the server
    -#
     # For production:
    -# listen_addr: 0.0.0.0:8080
    -listen_addr: 127.0.0.1:8080
    +# db_path: /var/lib/headscale/db.sqlite
    +db_path: ./db.sqlite
     
    -# Address to listen to /metrics, you may want
    -# to keep this endpoint private to your internal
    -# network
    -#
    -metrics_listen_addr: 127.0.0.1:9090
    +# # Postgres config
    +# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
    +# db_type: postgres
    +# db_host: localhost
    +# db_port: 5432
    +# db_name: headscale
    +# db_user: foo
    +# db_pass: bar
     
    -# Address to listen for gRPC.
    -# gRPC is used for controlling a headscale server
    -# remotely with the CLI
    -# Note: Remote access _only_ works if you have
    -# valid certificates.
    +# If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
    +# in the 'db_ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
    +# db_ssl: false
    +
    +### TLS configuration
     #
    -# For production:
    -# grpc_listen_addr: 0.0.0.0:50443
    -grpc_listen_addr: 127.0.0.1:50443
    +## Let's encrypt / ACME
    +#
    +# headscale supports automatically requesting and setting up
    +# TLS for a domain with Let's Encrypt.
    +#
    +# URL to ACME directory
    +acme_url: https://acme-v02.api.letsencrypt.org/directory
     
    -# Allow the gRPC admin interface to run in INSECURE
    -# mode. This is not recommended as the traffic will
    -# be unencrypted. Only enable if you know what you
    -# are doing.
    -grpc_allow_insecure: false
    +# Email to register with ACME provider
    +acme_email: ""
     
    -# Private key used to encrypt the traffic between headscale
    -# and Tailscale clients.
    -# The private key file will be autogenerated if it's missing.
    -#
    +# Domain name to request a TLS certificate for:
    +tls_letsencrypt_hostname: ""
    +
    +# Path to store certificates and metadata needed by
    +# letsencrypt
     # For production:
    -# /var/lib/headscale/private.key
    -private_key_path: ./private.key
    -
    -# The Noise section includes specific configuration for the
    -# TS2021 Noise protocol
    -noise:
    -  # The Noise private key is used to encrypt the
    -  # traffic between headscale and Tailscale clients when
    -  # using the new Noise-based protocol. It must be different
    -  # from the legacy private key.
    -  #
    -  # For production:
    -  # private_key_path: /var/lib/headscale/noise_private.key
    -  private_key_path: ./noise_private.key
    -
    -# List of IP prefixes to allocate tailaddresses from.
    -# Each prefix consists of either an IPv4 or IPv6 address,
    -# and the associated prefix length, delimited by a slash.
    -# While this looks like it can take arbitrary values, it
    -# needs to be within IP ranges supported by the Tailscale
    -# client.
    -# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
    -# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
    -ip_prefixes:
    -  - fd7a:115c:a1e0::/48
    -  - 100.64.0.0/10
    -
    -# DERP is a relay system that Tailscale uses when a direct
    -# connection cannot be established.
    -# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
    -#
    -# headscale needs a list of DERP servers that can be presented
    -# to the clients.
    -derp:
    -  server:
    -    # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
    -    # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
    -    enabled: false
    -
    -    # Region ID to use for the embedded DERP server.
    -    # The local DERP prevails if the region ID collides with other region ID coming from
    -    # the regular DERP config.
    -    region_id: 999
    -
    -    # Region code and name are displayed in the Tailscale UI to identify a DERP region
    -    region_code: "headscale"
    -    region_name: "Headscale Embedded DERP"
    -
    -    # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
    -    # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
    -    #
    -    # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
    -    stun_listen_addr: "0.0.0.0:3478"
    -
    -  # List of externally available DERP maps encoded in JSON
    -  urls:
    -    - https://controlplane.tailscale.com/derpmap/default
    -
    -  # Locally available DERP map files encoded in YAML
    -  #
    -  # This option is mostly interesting for people hosting
    -  # their own DERP servers:
    -  # https://tailscale.com/kb/1118/custom-derp-servers/
    -  #
    -  # paths:
    -  #   - /etc/headscale/derp-example.yaml
    -  paths: []
    -
    -  # If enabled, a worker will be set up to periodically
    -  # refresh the given sources and update the derpmap
    -  # will be set up.
    -  auto_update_enabled: true
    -
    -  # How often should we check for DERP updates?
    -  update_frequency: 24h
    -
    -# Disables the automatic check for headscale updates on startup
    -disable_check_updates: false
    -
    -# Time before an inactive ephemeral node is deleted?
    -ephemeral_node_inactivity_timeout: 30m
    -
    -# Period to check for node updates within the tailnet. A value too low will severely affect
    -# CPU consumption of Headscale. A value too high (over 60s) will cause problems
    -# for the nodes, as they won't get updates or keep alive messages frequently enough.
    -# In case of doubts, do not touch the default 10s.
    -node_update_check_interval: 10s
    -
    -# SQLite config
    -db_type: sqlite3
    -
    -# For production:
    -# db_path: /var/lib/headscale/db.sqlite
    -db_path: ./db.sqlite
    -
    -# # Postgres config
    -# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
    -# db_type: postgres
    -# db_host: localhost
    -# db_port: 5432
    -# db_name: headscale
    -# db_user: foo
    -# db_pass: bar
    -
    -# If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
    -# in the 'db_ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
    -# db_ssl: false
    -
    -### TLS configuration
    -#
    -## Let's encrypt / ACME
    -#
    -# headscale supports automatically requesting and setting up
    -# TLS for a domain with Let's Encrypt.
    -#
    -# URL to ACME directory
    -acme_url: https://acme-v02.api.letsencrypt.org/directory
    -
    -# Email to register with ACME provider
    -acme_email: ""
    -
    -# Domain name to request a TLS certificate for:
    -tls_letsencrypt_hostname: ""
    -
    -# Path to store certificates and metadata needed by
    -# letsencrypt
    -# For production:
    -# tls_letsencrypt_cache_dir: /var/lib/headscale/cache
    -tls_letsencrypt_cache_dir: ./cache
    +# tls_letsencrypt_cache_dir: /var/lib/headscale/cache
    +tls_letsencrypt_cache_dir: ./cache
     
     # Type of ACME challenge to use, currently supported types:
     # HTTP-01 or TLS-ALPN-01
    @@ -3949,49 +3672,97 @@ inorder = [9,3,15,20,7]
    + +

    模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
    对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
    要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
    在主对话框类得到值。

    ]]>
    - leetcode + C++ - leetcode - c++ - -
    - - Number of 1 Bits - /2015/03/11/Number-Of-1-Bits/ - Number of 1 Bits

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

    - -

    分析

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

    -
    -

    code

    int hammingWeight(uint32_t n) {
    -        const uint32_t m1  = 0x55555555; //binary: 0101...  
    -        const uint32_t m2  = 0x33333333; //binary: 00110011..  
    -        const uint32_t m4  = 0x0f0f0f0f; //binary:  4 zeros,  4 ones ...  
    -        const uint32_t m8  = 0x00ff00ff; //binary:  8 zeros,  8 ones ...  
    -        const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...  
    -        
    -        n = (n & m1 ) + ((n >>  1) & m1 ); //put count of each  2 bits into those  2 bits   
    -        n = (n & m2 ) + ((n >>  2) & m2 ); //put count of each  4 bits into those  4 bits   
    -        n = (n & m4 ) + ((n >>  4) & m4 ); //put count of each  8 bits into those  8 bits   
    -        n = (n & m8 ) + ((n >>  8) & m8 ); //put count of each 16 bits into those 16 bits   
    -        n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits   
    -        return n; 
    -
    -}
    ]]>
    - - leetcode - - - leetcode c++ + mfc
    @@ -5400,6 +5443,36 @@ OS name: "mac os x", version: "10.14.6", arch: "x86_64& Maven + + Number of 1 Bits + /2015/03/11/Number-Of-1-Bits/ + Number of 1 Bits

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

    + +

    分析

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

    +
    +

    code

    int hammingWeight(uint32_t n) {
    +        const uint32_t m1  = 0x55555555; //binary: 0101...  
    +        const uint32_t m2  = 0x33333333; //binary: 00110011..  
    +        const uint32_t m4  = 0x0f0f0f0f; //binary:  4 zeros,  4 ones ...  
    +        const uint32_t m8  = 0x00ff00ff; //binary:  8 zeros,  8 ones ...  
    +        const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...  
    +        
    +        n = (n & m1 ) + ((n >>  1) & m1 ); //put count of each  2 bits into those  2 bits   
    +        n = (n & m2 ) + ((n >>  2) & m2 ); //put count of each  4 bits into those  4 bits   
    +        n = (n & m4 ) + ((n >>  4) & m4 ); //put count of each  8 bits into those  8 bits   
    +        n = (n & m8 ) + ((n >>  8) & m8 ); //put count of each 16 bits into those 16 bits   
    +        n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits   
    +        return n; 
    +
    +}
    ]]>
    + + leetcode + + + leetcode + c++ + +
    Redis_分布式锁 /2019/12/10/Redis-Part-1/ @@ -5511,69 +5584,65 @@ public: - MFC 模态对话框 - /2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ - void CTestDialog::OnBnClickedOk() -{ - CString m_SrcTest; - int nIndex = m_CbTest.GetCurSel(); - m_CbTest.GetLBText(nIndex, m_SrcTest); - OnOK(); -}
    - -

    模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
    对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
    要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
    在主对话框类得到值。

    -]]>
    - - C++ - - - c++ - mfc - -
    - - 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.

    + 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.

    -

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

    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:
    -    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) {
    +    vector<int> twoSum(vector<int> &numbers, int target) {
             // Start typing your C/C++ solution below
             // DO NOT write int main() function
    -        return deep_first_search(root, sum, 0);
    +        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 @@ -5648,6 +5717,76 @@ master_port=3306; //如果是同一宿主机 mysql
    + + 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++ + +
    + + 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比一般多一点的初学者介绍 /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/ @@ -5705,130 +5844,65 @@ Run a command 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
    • -
    + docker比一般多一点的初学者介绍三 + /2020/03/21/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%B8%89/ + 运行第一个 Dockerfile

    上一篇的 Dockerfile 我们停留在构建阶段,现在来把它跑起来

    +
    docker run -d -p 80 --name static_web nicksxs/static_web \
    +nginx -g "daemon off;"
    +

    这里的-d表示以分离模型运行docker (detached),然后-p 是表示将容器的 80 端口开放给宿主机,然后容器名就叫 static_web,使用了我们上次构建的 static_web 镜像,后面的是让 nginx 在前台运行

    可以看到返回了个容器 id,但是具体情况没出现,也没连上去,那我们想看看怎么访问在 Dockerfile 里写的静态页面,我们来看下docker 进程

    发现为我们随机分配了一个宿主机的端口,32768,去服务器的防火墙把这个外网端口开一下,看看是不是符合我们的预期呢

    好像不太对额,应该是 ubuntu 安装的 nginx 的默认工作目录不对,我们来进容器看看,再熟悉下命令docker exec -it 4792455ca2ed /bin/bash
    记得容器 id 换成自己的,进入容器后得找找 nginx 的配置文件,通常在/etc/nginx,/usr/local/etc等目录下,然后找到我们的目录是在这

    所以把刚才的内容复制过去再试试

    目标达成,give me five✌️

    +

    第二个 Dockerfile

    然后就想来动态一点的,毕竟写过 PHP,就来试试 PHP
    再建一个目录叫 dynamic_web,里面创建 src 目录,放一个 index.php
    内容是

    +
    <?php
    +echo "Hello World!";
    +

    然后在 dynamic_web 目录下创建 Dockerfile,

    +
    FROM trafex/alpine-nginx-php7:latest
    +COPY src/ /var/www/html
    +EXPOSE 80
    +

    Dockerfile 虽然只有三行,不过要着重说明下,这个底包其实不是docker 官方的,有两点考虑,一点是官方的基本都是 php apache 的镜像,还有就是 alpine这个,截取一段中文介绍

    +
    +

    Alpine 操作系统是一个面向安全的轻型 Linux 发行版。它不同于通常 Linux 发行版,Alpine 采用了 musl libc 和 busybox 以减小系统的体积和运行时资源消耗,但功能上比 busybox 又完善的多,因此得到开源社区越来越多的青睐。在保持瘦身的同时,Alpine 还提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过 apk 命令直接查询和安装各种软件。
    Alpine 由非商业组织维护的,支持广泛场景的 Linux发行版,它特别为资深/重度Linux用户而优化,关注安全,性能和资源效能。Alpine 镜像可以适用于更多常用场景,并且是一个优秀的可以适用于生产的基础系统/环境。

    +
    +
    +

    Alpine Docker 镜像也继承了 Alpine Linux 发行版的这些优势。相比于其他 Docker 镜像,它的容量非常小,仅仅只有 5 MB 左右(对比 Ubuntu 系列镜像接近 200 MB),且拥有非常友好的包管理机制。官方镜像来自 docker-alpine 项目。

    +
    +
    +

    目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 做为基础镜像环境。这样会带来多个好处。包括镜像下载速度加快,镜像安全性提高,主机之间的切换更方便,占用更少磁盘空间等。

    +
    +

    一方面在没有镜像的情况下,拉取 docker 镜像还是比较费力的,第二个就是也能节省硬盘空间,所以目前有大部分的 docker 镜像都将 alpine 作为基础镜像了
    然后再来构建下

    这里还有个点,就是上面的那个镜像我们也是 EXPOSE 80端口,然后外部宿主机会随机映射一个端口,为了偷个懒,我们就直接指定外部端口了
    docker run -d -p 80:80 dynamic_web打开浏览器发现访问不了,咋回事呢
    因为我们没看trafex/alpine-nginx-php7:latest这个镜像说明,它内部的服务是 8080 端口的,所以我们映射的暴露端口应该是 8080,再用 docker run -d -p 80:8080 dynamic_web这个启动,

    ]]>
    - data analysis + Docker + 介绍 - hadoop - cluster + Docker + namespace + Dockerfile
    - docker比一般多一点的初学者介绍二 - /2020/03/15/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%BA%8C/ - 限制下 docker 的 cpu 使用率

    这里我们开始玩一点有意思的,我们在容器里装下 vim 和 gcc,然后写这样一段 c 代码

    -
    #include <stdio.h>
    -int main(void)
    -{
    -    int i = 0;
    -    for(;;) i++;
    -    return 0;
    -}
    -

    就是一个最简单的死循环,然后在容器里跑起来

    -
    $ gcc 1.c 
    -$ ./a.out
    -

    然后我们来看下系统资源占用(CPU)
    Xs562iawhHyMxeO
    上图是在容器里的,可以看到 cpu 已经 100%了
    然后看看容器外面的
    ecqH8XJ4k7rKhzu
    可以看到一个核的 cpu 也被占满了,因为是个双核的机器,并且代码是单线程的
    然后呢我们要做点啥
    因为已经在这个 ubuntu 容器中装了 vim 和 gcc,考虑到国内的网络,所以我们先把这个容器 commit 一下,

    -
    docker commit -a "nick" -m "my ubuntu" f63c5607df06 my_ubuntu:v1
    -

    然后再运行起来

    -
    docker run -it --cpus=0.1 my_ubuntu:v1 bash
    -


    我们的代码跟可执行文件都还在,要的就是这个效果,然后再运行一下

    结果是这个样子的,有点神奇是不,关键就在于 run 的时候的--cpus=0.1这个参数,它其实就是基于我前一篇说的 cgroup 技术,能将进程之间的cpu,内存等资源进行隔离

    -

    开始第一个 Dockerfile

    上一面为了复用那个我装了 vim 跟 gcc 的容器,我把它提交到了本地,使用了docker commit命令,有点类似于 git 的 commit,但是这个不是个很好的操作方式,需要手动介入,这里更推荐使用 Dockerfile 来构建镜像

    -
    From ubuntu:latest
    -MAINTAINER Nicksxs "nicksxs@hotmail.com"
    -RUN  sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
    -RUN apt-get clean
    -RUN apt-get update && apt install -y nginx
    -RUN echo 'Hi, i am in container' \
    -    > /usr/share/nginx/html/index.html
    -EXPOSE 80
    -

    先解释下这是在干嘛,首先是这个From ubuntu:latest基于的 ubuntu 的最新版本的镜像,然后第二行是维护人的信息,第三四行么作为墙内人你懂的,把 ubuntu 的源换成阿里云的,不然就有的等了,然后就是装下 nginx,往默认的 nginx 的入口 html 文件里输入一行欢迎语,然后暴露 80 端口
    然后我们使用sudo docker build -t="nicksxs/static_web" .命令来基于这个 Dockerfile 构建我们自己的镜像,过程中是这样的


    可以看到图中,我的 Dockerfile 是 7 行,里面就执行了 7 步,并且每一步都有一个类似于容器 id 的层 id 出来,这里就是一个比较重要的东西,docker 在构建的时候其实是有这个层的概念,Dockerfile 里的每一行都会往上加一层,这里有还注意下命令后面的.,代表当前目录下会自行去寻找 Dockerfile 进行构建,构建完了之后我们再看下我们的本地镜像

    我们自己的镜像出现啦
    然后有个问题,如果这个构建中途报了错咋办呢,来试试看,我们把 nginx 改成随便的一个错误名,nginxx(不知道会不会运气好真的有这玩意),再来 build 一把

    找不到 nginxx 包,是不是这个镜像就完全不能用呢,当然也不是,因为前面说到了,docker 是基于层去构建的,可以看到前面的 4 个 step 都没报错,那我们基于最后的成功步骤创建下容器看看
    也就是sudo docker run -t -i bd26f991b6c8 /bin/bash
    答案是可以的,只是没装成功 nginx

    还有一点注意到没,前面的几个 step 都有一句 Using cache,说明 docker 在构建镜像的时候是有缓存的,这也更能说明 docker 是基于层去构建镜像,同样的底包,同样的步骤,这些层是可以被复用的,这就是 docker 的构建缓存,当然我们也可以在 build 的时候加上--no-cache去把构建缓存禁用掉。

    + docker比一般多一点的初学者介绍四 + /2022/12/25/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%E5%9B%9B/ + 这次单独介绍下docker体系里非常重要的cgroup,docker对资源的限制也是基于cgroup构建的,
    简单尝试
    新建一个shell脚本

    +
    #!/bin/bash
    +while true;do
    +    echo "1"
    +done
    + +

    直接执行的话就是单核100%的cpu

    +

    首先在cgroup下面建个目录

    +
    mkdir -p /sys/fs/cgroup/cpu/sxs_test/
    +

    查看目录下的文件

    其中cpuacct开头的表示cpu相关的统计信息,
    我们要配置cpu的额度,是在cpu.cfs_quota_us中

    +
    echo 2000 > /sys/fs/cgroup/cpu/sxs_test/cpu.cfs_quota_us  
    +

    这样表示可以使用2%的cpu,总的配额是在cpu.cfs_period_us中

    +

    然后将当前进程输入到cgroup.procs,

    +
    echo $$ > /sys/fs/cgroup/cpu/sxs_test/cgroup.procs
    +

    这样就会自动继承当前进程产生的新进程
    再次执行就可以看到cpu被限制了

    ]]>
    Docker - 介绍 Docker - namespace - cgroup
    @@ -5886,42 +5960,6 @@ EXPOSE 80 - - docker比一般多一点的初学者介绍三 - /2020/03/21/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%B8%89/ - 运行第一个 Dockerfile

    上一篇的 Dockerfile 我们停留在构建阶段,现在来把它跑起来

    -
    docker run -d -p 80 --name static_web nicksxs/static_web \
    -nginx -g "daemon off;"
    -

    这里的-d表示以分离模型运行docker (detached),然后-p 是表示将容器的 80 端口开放给宿主机,然后容器名就叫 static_web,使用了我们上次构建的 static_web 镜像,后面的是让 nginx 在前台运行

    可以看到返回了个容器 id,但是具体情况没出现,也没连上去,那我们想看看怎么访问在 Dockerfile 里写的静态页面,我们来看下docker 进程

    发现为我们随机分配了一个宿主机的端口,32768,去服务器的防火墙把这个外网端口开一下,看看是不是符合我们的预期呢

    好像不太对额,应该是 ubuntu 安装的 nginx 的默认工作目录不对,我们来进容器看看,再熟悉下命令docker exec -it 4792455ca2ed /bin/bash
    记得容器 id 换成自己的,进入容器后得找找 nginx 的配置文件,通常在/etc/nginx,/usr/local/etc等目录下,然后找到我们的目录是在这

    所以把刚才的内容复制过去再试试

    目标达成,give me five✌️

    -

    第二个 Dockerfile

    然后就想来动态一点的,毕竟写过 PHP,就来试试 PHP
    再建一个目录叫 dynamic_web,里面创建 src 目录,放一个 index.php
    内容是

    -
    <?php
    -echo "Hello World!";
    -

    然后在 dynamic_web 目录下创建 Dockerfile,

    -
    FROM trafex/alpine-nginx-php7:latest
    -COPY src/ /var/www/html
    -EXPOSE 80
    -

    Dockerfile 虽然只有三行,不过要着重说明下,这个底包其实不是docker 官方的,有两点考虑,一点是官方的基本都是 php apache 的镜像,还有就是 alpine这个,截取一段中文介绍

    -
    -

    Alpine 操作系统是一个面向安全的轻型 Linux 发行版。它不同于通常 Linux 发行版,Alpine 采用了 musl libc 和 busybox 以减小系统的体积和运行时资源消耗,但功能上比 busybox 又完善的多,因此得到开源社区越来越多的青睐。在保持瘦身的同时,Alpine 还提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过 apk 命令直接查询和安装各种软件。
    Alpine 由非商业组织维护的,支持广泛场景的 Linux发行版,它特别为资深/重度Linux用户而优化,关注安全,性能和资源效能。Alpine 镜像可以适用于更多常用场景,并且是一个优秀的可以适用于生产的基础系统/环境。

    -
    -
    -

    Alpine Docker 镜像也继承了 Alpine Linux 发行版的这些优势。相比于其他 Docker 镜像,它的容量非常小,仅仅只有 5 MB 左右(对比 Ubuntu 系列镜像接近 200 MB),且拥有非常友好的包管理机制。官方镜像来自 docker-alpine 项目。

    -
    -
    -

    目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 做为基础镜像环境。这样会带来多个好处。包括镜像下载速度加快,镜像安全性提高,主机之间的切换更方便,占用更少磁盘空间等。

    -
    -

    一方面在没有镜像的情况下,拉取 docker 镜像还是比较费力的,第二个就是也能节省硬盘空间,所以目前有大部分的 docker 镜像都将 alpine 作为基础镜像了
    然后再来构建下

    这里还有个点,就是上面的那个镜像我们也是 EXPOSE 80端口,然后外部宿主机会随机映射一个端口,为了偷个懒,我们就直接指定外部端口了
    docker run -d -p 80:80 dynamic_web打开浏览器发现访问不了,咋回事呢
    因为我们没看trafex/alpine-nginx-php7:latest这个镜像说明,它内部的服务是 8080 端口的,所以我们映射的暴露端口应该是 8080,再用 docker run -d -p 80:8080 dynamic_web这个启动,

    -]]>
    - - Docker - 介绍 - - - Docker - namespace - Dockerfile - -
    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/ @@ -5945,32 +5983,6 @@ EXPOSE 80 - - docker比一般多一点的初学者介绍四 - /2022/12/25/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%E5%9B%9B/ - 这次单独介绍下docker体系里非常重要的cgroup,docker对资源的限制也是基于cgroup构建的,
    简单尝试
    新建一个shell脚本

    -
    #!/bin/bash
    -while true;do
    -    echo "1"
    -done
    - -

    直接执行的话就是单核100%的cpu

    -

    首先在cgroup下面建个目录

    -
    mkdir -p /sys/fs/cgroup/cpu/sxs_test/
    -

    查看目录下的文件

    其中cpuacct开头的表示cpu相关的统计信息,
    我们要配置cpu的额度,是在cpu.cfs_quota_us中

    -
    echo 2000 > /sys/fs/cgroup/cpu/sxs_test/cpu.cfs_quota_us  
    -

    这样表示可以使用2%的cpu,总的配额是在cpu.cfs_period_us中

    -

    然后将当前进程输入到cgroup.procs,

    -
    echo $$ > /sys/fs/cgroup/cpu/sxs_test/cgroup.procs
    -

    这样就会自动继承当前进程产生的新进程
    再次执行就可以看到cpu被限制了

    -]]>
    - - Docker - - - Docker - -
    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/ @@ -6070,6 +6082,44 @@ myusername ALL = problem

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

    +

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

    +

    题解

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

    +

    Code

    class Solution {
    +public:
    +    int minSubArrayLen(int s, vector<int>& nums) {
    +        int len = nums.size();
    +        if(len == 0) return 0;
    +        int minlen = INT_MAX;
    +        int sum = 0;
    +        
    +        int left = 0;
    +        int right = -1;
    +        while(right < len)
    +        {
    +            while(sum < s && right < len)
    +                sum += nums[++right];
    +            if(sum >= s)
    +            {
    +                minlen = minlen < right - left + 1 ? minlen : right - left + 1;
    +                sum -= nums[left++];
    +            }
    +        }
    +        return minlen > len ? 0 : minlen;
    +    }
    +};
    +]]> + + leetcode + + + leetcode + c++ + +
    C++ 指针使用中的一个小问题 /2014/12/23/my-new-post/ @@ -6133,41 +6183,69 @@ public: - minimum-size-subarray-sum-209 - /2016/10/11/minimum-size-subarray-sum-209/ - problem

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

    -

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

    -

    题解

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

    -

    Code

    class Solution {
    -public:
    -    int minSubArrayLen(int s, vector<int>& nums) {
    -        int len = nums.size();
    -        if(len == 0) return 0;
    -        int minlen = INT_MAX;
    -        int sum = 0;
    -        
    -        int left = 0;
    -        int right = -1;
    -        while(right < len)
    -        {
    -            while(sum < s && right < len)
    -                sum += nums[++right];
    -            if(sum >= s)
    -            {
    -                minlen = minlen < right - left + 1 ? minlen : right - left + 1;
    -                sum -= nums[left++];
    -            }
    -        }
    -        return minlen > len ? 0 : minlen;
    -    }
    -};
    -]]>
    + mybatis 的 foreach 使用的注意点 + /2022/07/09/mybatis-%E7%9A%84-foreach-%E4%BD%BF%E7%94%A8%E7%9A%84%E6%B3%A8%E6%84%8F%E7%82%B9/ + mybatis 在作为轻量级 orm 框架,如果要使用类似于 in 查询的语句,除了直接替换字符串,还可以使用 foreach 标签
    在mybatis的 dtd 文件中可以看到可以配置这些字段,

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

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

    +

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

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

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

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

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

    +
    @Override
    +public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    +  try {
    +    MappedStatement ms = configuration.getMappedStatement(statement);
    +    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    +  } catch (Exception e) {
    +    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    +  } finally {
    +    ErrorContext.instance().reset();
    +  }
    +}
    +// 就是在这帮我们做了转换
    +  private Object wrapCollection(final Object object) {
    +  if (object instanceof Collection) {
    +    StrictMap<Object> map = new StrictMap<Object>();
    +    map.put("collection", object);
    +    if (object instanceof List) {
    +      // 如果类型是list 就会转成以 list 为 key 的 map
    +      map.put("list", object);
    +    }
    +    return map;
    +  } else if (object != null && object.getClass().isArray()) {
    +    StrictMap<Object> map = new StrictMap<Object>();
    +    map.put("array", object);
    +    return map;
    +  }
    +  return object;
    +  }
    ]]>
    - leetcode + Java + Mybatis + Mysql - leetcode - c++ + Java + Mysql + Mybatis
    @@ -6265,124 +6343,6 @@ public class DynamicSqlSource implements SqlSource { Sql注入 - - mybatis 的 foreach 使用的注意点 - /2022/07/09/mybatis-%E7%9A%84-foreach-%E4%BD%BF%E7%94%A8%E7%9A%84%E6%B3%A8%E6%84%8F%E7%82%B9/ - mybatis 在作为轻量级 orm 框架,如果要使用类似于 in 查询的语句,除了直接替换字符串,还可以使用 foreach 标签
    在mybatis的 dtd 文件中可以看到可以配置这些字段,

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

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

    -

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

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

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

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

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

    -
    @Override
    -public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    -  try {
    -    MappedStatement ms = configuration.getMappedStatement(statement);
    -    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    -  } catch (Exception e) {
    -    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    -  } finally {
    -    ErrorContext.instance().reset();
    -  }
    -}
    -// 就是在这帮我们做了转换
    -  private Object wrapCollection(final Object object) {
    -  if (object instanceof Collection) {
    -    StrictMap<Object> map = new StrictMap<Object>();
    -    map.put("collection", object);
    -    if (object instanceof List) {
    -      // 如果类型是list 就会转成以 list 为 key 的 map
    -      map.put("list", object);
    -    }
    -    return map;
    -  } else if (object != null && object.getClass().isArray()) {
    -    StrictMap<Object> map = new StrictMap<Object>();
    -    map.put("array", object);
    -    return map;
    -  }
    -  return object;
    -  }
    ]]>
    - - Java - Mybatis - Mysql - - - Java - Mysql - Mybatis - -
    - - Leetcode 747 至少是其他数字两倍的最大数 ( Largest Number At Least Twice of Others *Easy* ) 题解分析 - /2022/10/02/Leetcode-747-%E8%87%B3%E5%B0%91%E6%98%AF%E5%85%B6%E4%BB%96%E6%95%B0%E5%AD%97%E4%B8%A4%E5%80%8D%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0-Largest-Number-At-Least-Twice-of-Others-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ - 题目介绍

    You are given an integer array nums where the largest integer is unique.

    -

    Determine whether the largest element in the array is at least twice as much as every other number in the array. If it is, return the index of the largest element, or return -1 otherwise.
    确认在数组中的最大数是否是其余任意数的两倍大及以上,如果是返回索引,如果不是返回-1

    -

    示例

    Example 1:

    -

    Input: nums = [3,6,1,0]
    Output: 1
    Explanation: 6 is the largest integer.
    For every other number in the array x, 6 is at least twice as big as x.
    The index of value 6 is 1, so we return 1.

    -
    -

    Example 2:

    -

    Input: nums = [1,2,3,4]
    Output: -1
    Explanation: 4 is less than twice the value of 3, so we return -1.

    -
    -

    提示:

      -
    • 2 <= nums.length <= 50
    • -
    • 0 <= nums[i] <= 100
    • -
    • The largest element in nums is unique.
    • -
    -

    简要解析

    这个题easy是题意也比较简单,找最大值,并且最大值是其他任意值的两倍及以上,其实就是找最大值跟次大值,比较下就好了

    -

    代码

    public int dominantIndex(int[] nums) {
    -    int largest = Integer.MIN_VALUE;
    -    int second = Integer.MIN_VALUE;
    -    int largestIndex = -1;
    -    for (int i = 0; i < nums.length; i++) {
    -        // 如果有最大的就更新,同时更新最大值和第二大的
    -        if (nums[i] > largest) {
    -            second = largest;
    -            largest = nums[i];
    -            largestIndex = i;
    -        } else if (nums[i] > second) {
    -            // 没有超过最大的,但是比第二大的更大就更新第二大的
    -            second = nums[i];
    -        }
    -    }
    -
    -    // 判断下是否符合题目要求,要是所有值的两倍及以上
    -    if (largest >= 2 * second) {
    -        return largestIndex;
    -    } else {
    -        return -1;
    -    }
    -}
    -

    通过图

    第一次错了是把第二大的情况只考虑第一种,也有可能最大值完全没经过替换就变成最大值了

    -]]>
    - - Java - leetcode - - - leetcode - java - 题解 - -
    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/ @@ -6628,71 +6588,44 @@ separator CDATA #IMPLIED - mybatis系列-mybatis是如何初始化mapper的 - /2022/12/04/mybatis%E6%98%AF%E5%A6%82%E4%BD%95%E5%88%9D%E5%A7%8B%E5%8C%96mapper%E7%9A%84/ - 前一篇讲了mybatis的初始化使用,如果我第一次看到这个使用入门文档,比较会产生疑惑的是配置了mapper,怎么就能通过selectOne跟语句id就能执行sql了,那么第一个问题,就是mapper是怎么被解析的,存在哪里,怎么被拿出来的

    -

    添加解析mapper

    org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
    -public SqlSessionFactory build(InputStream inputStream) {
    -  return build(inputStream, null, null);
    -}
    + mybatis系列-dataSource解析 + /2023/01/08/mybatis%E7%B3%BB%E5%88%97-dataSource%E8%A7%A3%E6%9E%90/ + DataSource 作为数据库查询的最重要的数据源,在 mybatis 中也展开来说下
    首先是解析的过程

    +
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    -

    通过读取mybatis-config.xml来构建SqlSessionFactory,

    -
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    -  try {
    -    // 创建下xml的解析器
    -    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    -    // 进行解析,后再构建
    -    return build(parser.parse());
    -  } catch (Exception e) {
    -    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    -  } finally {
    -    ErrorContext.instance().reset();
    +

    在构建 SqlSessionFactory 也就是 DefaultSqlSessionFactory 的时候,

    +
    public SqlSessionFactory build(InputStream inputStream) {
    +    return build(inputStream, null, null);
    +  }
    +public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
         try {
    -       if (inputStream != null) {
    -         inputStream.close();
    -       }
    -    } catch (IOException e) {
    -      // Intentionally ignore. Prefer previous error.
    +      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    +      return build(parser.parse());
    +    } catch (Exception e) {
    +      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    +    } finally {
    +      ErrorContext.instance().reset();
    +      try {
    +      	if (inputStream != null) {
    +      	  inputStream.close();
    +      	}
    +      } catch (IOException e) {
    +        // Intentionally ignore. Prefer previous error.
    +      }
         }
    -  }
    - -

    创建XMLConfigBuilder

    -
    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    -    // --------> 创建 XPathParser
    -  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    -}
    -
    -public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
    -    commonConstructor(validation, variables, entityResolver);
    -    this.document = createDocument(new InputSource(inputStream));
    +  }
    +

    前面也说过,就是解析 mybatis-config.xmlConfiguration

    +
    public Configuration parse() {
    +  if (parsed) {
    +    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
       }
    -
    -private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    -  super(new Configuration());
    -  ErrorContext.instance().resource("SQL Mapper Configuration");
    -  this.configuration.setVariables(props);
    -  this.parsed = false;
    -  this.environment = environment;
    -  this.parser = parser;
    -}
    - -

    这里主要是创建了Builder包含了Parser
    然后调用parse方法

    -
    public Configuration parse() {
    -  if (parsed) {
    -    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    -  }
    -  // 标记下是否已解析,但是这里是否有线程安全问题
       parsed = true;
    -  // --------> 解析配置
       parseConfiguration(parser.evalNode("/configuration"));
       return configuration;
    -}
    - -

    实际的解析区分了各类标签

    -
    private void parseConfiguration(XNode root) {
    +}
    +private void parseConfiguration(XNode root) {
       try {
         // issue #117 read properties first
    -    // 解析properties,这个不是spring自带的,需要额外配置,并且在config文件里应该放在最前
         propertiesElement(root.evalNode("properties"));
         Properties settings = settingsAsProperties(root.evalNode("settings"));
         loadCustomVfs(settings);
    @@ -6704,276 +6637,236 @@ separator CDATA #IMPLIED
         reflectorFactoryElement(root.evalNode("reflectorFactory"));
         settingsElement(settings);
         // read it after objectFactory and objectWrapperFactory issue #631
    +    // -------------> 是在这里解析了DataSource
         environmentsElement(root.evalNode("environments"));
         databaseIdProviderElement(root.evalNode("databaseIdProvider"));
         typeHandlerElement(root.evalNode("typeHandlers"));
    -    // ----------> 我们需要关注的是mapper的处理
         mapperElement(root.evalNode("mappers"));
       } catch (Exception e) {
         throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
       }
    -}
    - -

    然后就是调用到mapperElement方法了

    -
    private void mapperElement(XNode parent) throws Exception {
    -  if (parent != null) {
    -    for (XNode child : parent.getChildren()) {
    -      if ("package".equals(child.getName())) {
    -        String mapperPackage = child.getStringAttribute("name");
    -        configuration.addMappers(mapperPackage);
    -      } else {
    -        String resource = child.getStringAttribute("resource");
    -        String url = child.getStringAttribute("url");
    -        String mapperClass = child.getStringAttribute("class");
    -        if (resource != null && url == null && mapperClass == null) {
    -          ErrorContext.instance().resource(resource);
    -          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
    -            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    -            // --------> 我们这没有指定package,所以是走到这
    -            mapperParser.parse();
    -          }
    -        } else if (resource == null && url != null && mapperClass == null) {
    -          ErrorContext.instance().resource(url);
    -          try(InputStream inputStream = Resources.getUrlAsStream(url)){
    -            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
    -            mapperParser.parse();
    -          }
    -        } else if (resource == null && url == null && mapperClass != null) {
    -          Class<?> mapperInterface = Resources.classForName(mapperClass);
    -          configuration.addMapper(mapperInterface);
    -        } else {
    -          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
    -        }
    +}
    +

    环境解析了这一块的内容

    +
    <environments default="development">
    +        <environment id="development">
    +            <transactionManager type="JDBC"/>
    +            <dataSource type="POOLED">
    +                <property name="driver" value="${driver}"/>
    +                <property name="url" value="${url}"/>
    +                <property name="username" value="${username}"/>
    +                <property name="password" value="${password}"/>
    +            </dataSource>
    +        </environment>
    +    </environments>
    +

    解析也是自上而下的,

    +
    private void environmentsElement(XNode context) throws Exception {
    +  if (context != null) {
    +    if (environment == null) {
    +      environment = context.getStringAttribute("default");
    +    }
    +    for (XNode child : context.getChildren()) {
    +      String id = child.getStringAttribute("id");
    +      if (isSpecifiedEnvironment(id)) {
    +        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
    +        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
    +        DataSource dataSource = dsFactory.getDataSource();
    +        Environment.Builder environmentBuilder = new Environment.Builder(id)
    +            .transactionFactory(txFactory)
    +            .dataSource(dataSource);
    +        configuration.setEnvironment(environmentBuilder.build());
    +        break;
           }
         }
       }
    -}
    - -

    核心就在这个parse()方法

    -
    public void parse() {
    -  if (!configuration.isResourceLoaded(resource)) {
    -    // -------> 然后就是走到这里,配置xml的mapper节点的内容
    -    configurationElement(parser.evalNode("/mapper"));
    -    configuration.addLoadedResource(resource);
    -    bindMapperForNamespace();
    -  }
    -
    -  parsePendingResultMaps();
    -  parsePendingCacheRefs();
    -  parsePendingStatements();
    -}
    - -

    具体的处理逻辑

    -
    private void configurationElement(XNode context) {
    -  try {
    -    String namespace = context.getStringAttribute("namespace");
    -    if (namespace == null || namespace.isEmpty()) {
    -      throw new BuilderException("Mapper's namespace cannot be empty");
    -    }
    -    builderAssistant.setCurrentNamespace(namespace);
    -    cacheRefElement(context.evalNode("cache-ref"));
    -    cacheElement(context.evalNode("cache"));
    -    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    -    resultMapElements(context.evalNodes("/mapper/resultMap"));
    -    sqlElement(context.evalNodes("/mapper/sql"));
    -    // ------->  走到这,从上下文构建statement
    -    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    -  } catch (Exception e) {
    -    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    +}
    +

    前面第一步是解析事务管理器元素

    +
    private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    +  if (context != null) {
    +    String type = context.getStringAttribute("type");
    +    Properties props = context.getChildrenAsProperties();
    +    TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    +    factory.setProperties(props);
    +    return factory;
       }
    -}
    - -

    具体代码在这,从上下文构建statement,只不过区分了下databaseId

    -
    private void buildStatementFromContext(List<XNode> list) {
    -  if (configuration.getDatabaseId() != null) {
    -    buildStatementFromContext(list, configuration.getDatabaseId());
    +  throw new BuilderException("Environment declaration requires a TransactionFactory.");
    +}
    +

    而这里的 resolveClass 其实就使用了上一篇的 typeAliases 系统,这里是使用了 JdbcTransactionFactory 作为事务管理器,
    后面的就是 DataSourceFactory 的创建也是 DataSource 的创建

    +
    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    +  if (context != null) {
    +    String type = context.getStringAttribute("type");
    +    Properties props = context.getChildrenAsProperties();
    +    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    +    factory.setProperties(props);
    +    return factory;
       }
    -  // -----> 判断databaseId
    -  buildStatementFromContext(list, null);
    -}
    + throw new BuilderException("Environment declaration requires a DataSourceFactory."); +}
    +

    因为在config文件中设置了Pooled,所以对应创建的就是 PooledDataSourceFactory
    但是这里其实有个比较需要注意的,mybatis 这里的其实是继承了 UnpooledDataSourceFactory
    将基础方法都放在了 UnpooledDataSourceFactory

    +
    public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
     
    -

    判断下databaseId

    -
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    -  for (XNode context : list) {
    -    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    -    try {
    -      // -------> 解析statement节点
    -      statementParser.parseStatementNode();
    -    } catch (IncompleteElementException e) {
    -      configuration.addIncompleteStatement(statementParser);
    -    }
    +  public PooledDataSourceFactory() {
    +    this.dataSource = new PooledDataSource();
       }
    -}
    -

    接下来就是真正处理的xml语句内容的,各个节点的信息内容

    -
    public void parseStatementNode() {
    -  String id = context.getStringAttribute("id");
    -  String databaseId = context.getStringAttribute("databaseId");
    +}
    +

    这里只保留了在构造方法里创建 DataSource
    而这个 PooledDataSource 虽然没有直接继承 UnpooledDataSource,但其实
    在构造方法里也是

    +
    public PooledDataSource() {
    +  dataSource = new UnpooledDataSource();
    +}
    +

    至于为什么这么做呢应该也是考虑到能比较多的复用代码,因为 Pooled 其实跟 Unpooled 最重要的差别就在于是不是每次都重开连接
    使用连接池能够让应用在有大量查询的时候不用反复创建连接,省去了建联的网络等开销,Unpooled就是完成与数据库的连接,并可以获取该连接
    主要的代码

    +
    @Override
    +public Connection getConnection() throws SQLException {
    +  return doGetConnection(username, password);
    +}
     
    -  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    -    return;
    +@Override
    +public Connection getConnection(String username, String password) throws SQLException {
    +  return doGetConnection(username, password);
    +}
    +private Connection doGetConnection(String username, String password) throws SQLException {
    +  Properties props = new Properties();
    +  if (driverProperties != null) {
    +    props.putAll(driverProperties);
       }
    -
    -  String nodeName = context.getNode().getNodeName();
    -  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    -  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    -  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    -  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    -  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    -
    -  // Include Fragments before parsing
    -  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    -  includeParser.applyIncludes(context.getNode());
    -
    -  String parameterType = context.getStringAttribute("parameterType");
    -  Class<?> parameterTypeClass = resolveClass(parameterType);
    -
    -  String lang = context.getStringAttribute("lang");
    -  LanguageDriver langDriver = getLanguageDriver(lang);
    -
    -  // Parse selectKey after includes and remove them.
    -  processSelectKeyNodes(id, parameterTypeClass, langDriver);
    -
    -  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    -  KeyGenerator keyGenerator;
    -  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    -  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    -  if (configuration.hasKeyGenerator(keyStatementId)) {
    -    keyGenerator = configuration.getKeyGenerator(keyStatementId);
    -  } else {
    -    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
    -        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
    -        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    +  if (username != null) {
    +    props.setProperty("user", username);
       }
    -
    -  // 语句的主要参数解析
    -  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    -  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    -  Integer fetchSize = context.getIntAttribute("fetchSize");
    -  Integer timeout = context.getIntAttribute("timeout");
    -  String parameterMap = context.getStringAttribute("parameterMap");
    -  String resultType = context.getStringAttribute("resultType");
    -  Class<?> resultTypeClass = resolveClass(resultType);
    -  String resultMap = context.getStringAttribute("resultMap");
    -  String resultSetType = context.getStringAttribute("resultSetType");
    -  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    -  if (resultSetTypeEnum == null) {
    -    resultSetTypeEnum = configuration.getDefaultResultSetType();
    +  if (password != null) {
    +    props.setProperty("password", password);
       }
    -  String keyProperty = context.getStringAttribute("keyProperty");
    -  String keyColumn = context.getStringAttribute("keyColumn");
    -  String resultSets = context.getStringAttribute("resultSets");
    -
    -  // --------> 添加映射的statement
    -  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
    -      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    -      resultSetTypeEnum, flushCache, useCache, resultOrdered,
    -      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    -}
    + return doGetConnection(props); +} +private Connection doGetConnection(Properties properties) throws SQLException { + initializeDriver(); + Connection connection = DriverManager.getConnection(url, properties); + configureConnection(connection); + return connection; +}
    +

    而对于Pooled就会处理池化的逻辑

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

    添加的逻辑具体可以看下

    -
    public MappedStatement addMappedStatement(
    -    String id,
    -    SqlSource sqlSource,
    -    StatementType statementType,
    -    SqlCommandType sqlCommandType,
    -    Integer fetchSize,
    -    Integer timeout,
    -    String parameterMap,
    -    Class<?> parameterType,
    -    String resultMap,
    -    Class<?> resultType,
    -    ResultSetType resultSetType,
    -    boolean flushCache,
    -    boolean useCache,
    -    boolean resultOrdered,
    -    KeyGenerator keyGenerator,
    -    String keyProperty,
    -    String keyColumn,
    -    String databaseId,
    -    LanguageDriver lang,
    -    String resultSets) {
    -
    -  if (unresolvedCacheRef) {
    -    throw new IncompleteElementException("Cache-ref not yet resolved");
    -  }
    -
    -  id = applyCurrentNamespace(id, false);
    -  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    -
    -  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
    -      .resource(resource)
    -      .fetchSize(fetchSize)
    -      .timeout(timeout)
    -      .statementType(statementType)
    -      .keyGenerator(keyGenerator)
    -      .keyProperty(keyProperty)
    -      .keyColumn(keyColumn)
    -      .databaseId(databaseId)
    -      .lang(lang)
    -      .resultOrdered(resultOrdered)
    -      .resultSets(resultSets)
    -      .resultMaps(getStatementResultMaps(resultMap, resultType, id))
    -      .resultSetType(resultSetType)
    -      .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
    -      .useCache(valueOrDefault(useCache, isSelect))
    -      .cache(currentCache);
    -
    -  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    -  if (statementParameterMap != null) {
    -    statementBuilder.parameterMap(statementParameterMap);
    -  }
    -
    -  MappedStatement statement = statementBuilder.build();
    -  // ------>  正好是这里在configuration中添加了映射好的statement
    -  configuration.addMappedStatement(statement);
    -  return statement;
    -}
    - -

    而里面就是往map里添加

    -
    public void addMappedStatement(MappedStatement ms) {
    -  mappedStatements.put(ms.getId(), ms);
    -}
    - -

    获取mapper

    StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
    - -

    就是调用了 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)

    -
    public <T> T selectOne(String statement, Object parameter) {
    -  // Popular vote was to return null on 0 results and throw exception on too many.
    -  List<T> list = this.selectList(statement, parameter);
    -  if (list.size() == 1) {
    -    return list.get(0);
    -  } else if (list.size() > 1) {
    -    throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    -  } else {
    -    return null;
    -  }
    -}
    - -

    调用实际的实现方法

    -
    public <E> List<E> selectList(String statement, Object parameter) {
    -  return this.selectList(statement, parameter, RowBounds.DEFAULT);
    -}
    - -

    这里还有一层

    -
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    -  return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
    -}
    + } + if (conn == null) { + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } -

    根本的就是从configuration里获取了mappedStatement

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

    它的入口不是个get方法,而是pop,从含义来来讲就不一样
    org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection()

    +
    @Override
    +public Connection getConnection() throws SQLException {
    +  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
    +}
    +

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

    ]]>
    Java @@ -6986,44 +6879,71 @@ separator CDATA #IMPLIED
    - mybatis系列-dataSource解析 - /2023/01/08/mybatis%E7%B3%BB%E5%88%97-dataSource%E8%A7%A3%E6%9E%90/ - DataSource 作为数据库查询的最重要的数据源,在 mybatis 中也展开来说下
    首先是解析的过程

    -
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    + mybatis系列-mybatis是如何初始化mapper的 + /2022/12/04/mybatis%E6%98%AF%E5%A6%82%E4%BD%95%E5%88%9D%E5%A7%8B%E5%8C%96mapper%E7%9A%84/ + 前一篇讲了mybatis的初始化使用,如果我第一次看到这个使用入门文档,比较会产生疑惑的是配置了mapper,怎么就能通过selectOne跟语句id就能执行sql了,那么第一个问题,就是mapper是怎么被解析的,存在哪里,怎么被拿出来的

    +

    添加解析mapper

    org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
    +public SqlSessionFactory build(InputStream inputStream) {
    +  return build(inputStream, null, null);
    +}
    -

    在构建 SqlSessionFactory 也就是 DefaultSqlSessionFactory 的时候,

    -
    public SqlSessionFactory build(InputStream inputStream) {
    -    return build(inputStream, null, null);
    -  }
    -public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    +

    通过读取mybatis-config.xml来构建SqlSessionFactory,

    +
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    +  try {
    +    // 创建下xml的解析器
    +    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    +    // 进行解析,后再构建
    +    return build(parser.parse());
    +  } catch (Exception e) {
    +    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    +  } finally {
    +    ErrorContext.instance().reset();
         try {
    -      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    -      return build(parser.parse());
    -    } catch (Exception e) {
    -      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    -    } finally {
    -      ErrorContext.instance().reset();
    -      try {
    -      	if (inputStream != null) {
    -      	  inputStream.close();
    -      	}
    -      } catch (IOException e) {
    -        // Intentionally ignore. Prefer previous error.
    -      }
    -    }
    -  }
    -

    前面也说过,就是解析 mybatis-config.xmlConfiguration

    + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + // Intentionally ignore. Prefer previous error. + } + }
    + +

    创建XMLConfigBuilder

    +
    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    +    // --------> 创建 XPathParser
    +  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    +}
    +
    +public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
    +    commonConstructor(validation, variables, entityResolver);
    +    this.document = createDocument(new InputSource(inputStream));
    +  }
    +
    +private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    +  super(new Configuration());
    +  ErrorContext.instance().resource("SQL Mapper Configuration");
    +  this.configuration.setVariables(props);
    +  this.parsed = false;
    +  this.environment = environment;
    +  this.parser = parser;
    +}
    + +

    这里主要是创建了Builder包含了Parser
    然后调用parse方法

    public Configuration parse() {
       if (parsed) {
         throw new BuilderException("Each XMLConfigBuilder can only be used once.");
       }
    +  // 标记下是否已解析,但是这里是否有线程安全问题
       parsed = true;
    +  // --------> 解析配置
       parseConfiguration(parser.evalNode("/configuration"));
       return configuration;
    -}
    -private void parseConfiguration(XNode root) {
    +}
    + +

    实际的解析区分了各类标签

    +
    private void parseConfiguration(XNode root) {
       try {
         // issue #117 read properties first
    +    // 解析properties,这个不是spring自带的,需要额外配置,并且在config文件里应该放在最前
         propertiesElement(root.evalNode("properties"));
         Properties settings = settingsAsProperties(root.evalNode("settings"));
         loadCustomVfs(settings);
    @@ -7035,236 +6955,276 @@ separator CDATA #IMPLIED
         reflectorFactoryElement(root.evalNode("reflectorFactory"));
         settingsElement(settings);
         // read it after objectFactory and objectWrapperFactory issue #631
    -    // -------------> 是在这里解析了DataSource
         environmentsElement(root.evalNode("environments"));
         databaseIdProviderElement(root.evalNode("databaseIdProvider"));
         typeHandlerElement(root.evalNode("typeHandlers"));
    +    // ----------> 我们需要关注的是mapper的处理
         mapperElement(root.evalNode("mappers"));
       } catch (Exception e) {
         throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
       }
    -}
    -

    环境解析了这一块的内容

    -
    <environments default="development">
    -        <environment id="development">
    -            <transactionManager type="JDBC"/>
    -            <dataSource type="POOLED">
    -                <property name="driver" value="${driver}"/>
    -                <property name="url" value="${url}"/>
    -                <property name="username" value="${username}"/>
    -                <property name="password" value="${password}"/>
    -            </dataSource>
    -        </environment>
    -    </environments>
    -

    解析也是自上而下的,

    -
    private void environmentsElement(XNode context) throws Exception {
    -  if (context != null) {
    -    if (environment == null) {
    -      environment = context.getStringAttribute("default");
    -    }
    -    for (XNode child : context.getChildren()) {
    -      String id = child.getStringAttribute("id");
    -      if (isSpecifiedEnvironment(id)) {
    -        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
    -        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
    -        DataSource dataSource = dsFactory.getDataSource();
    -        Environment.Builder environmentBuilder = new Environment.Builder(id)
    -            .transactionFactory(txFactory)
    -            .dataSource(dataSource);
    -        configuration.setEnvironment(environmentBuilder.build());
    -        break;
    +}
    + +

    然后就是调用到mapperElement方法了

    +
    private void mapperElement(XNode parent) throws Exception {
    +  if (parent != null) {
    +    for (XNode child : parent.getChildren()) {
    +      if ("package".equals(child.getName())) {
    +        String mapperPackage = child.getStringAttribute("name");
    +        configuration.addMappers(mapperPackage);
    +      } else {
    +        String resource = child.getStringAttribute("resource");
    +        String url = child.getStringAttribute("url");
    +        String mapperClass = child.getStringAttribute("class");
    +        if (resource != null && url == null && mapperClass == null) {
    +          ErrorContext.instance().resource(resource);
    +          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
    +            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    +            // --------> 我们这没有指定package,所以是走到这
    +            mapperParser.parse();
    +          }
    +        } else if (resource == null && url != null && mapperClass == null) {
    +          ErrorContext.instance().resource(url);
    +          try(InputStream inputStream = Resources.getUrlAsStream(url)){
    +            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
    +            mapperParser.parse();
    +          }
    +        } else if (resource == null && url == null && mapperClass != null) {
    +          Class<?> mapperInterface = Resources.classForName(mapperClass);
    +          configuration.addMapper(mapperInterface);
    +        } else {
    +          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
    +        }
           }
         }
       }
    -}
    -

    前面第一步是解析事务管理器元素

    -
    private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    -  if (context != null) {
    -    String type = context.getStringAttribute("type");
    -    Properties props = context.getChildrenAsProperties();
    -    TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    -    factory.setProperties(props);
    -    return factory;
    -  }
    -  throw new BuilderException("Environment declaration requires a TransactionFactory.");
    -}
    -

    而这里的 resolveClass 其实就使用了上一篇的 typeAliases 系统,这里是使用了 JdbcTransactionFactory 作为事务管理器,
    后面的就是 DataSourceFactory 的创建也是 DataSource 的创建

    -
    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    -  if (context != null) {
    -    String type = context.getStringAttribute("type");
    -    Properties props = context.getChildrenAsProperties();
    -    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    -    factory.setProperties(props);
    -    return factory;
    +}
    + +

    核心就在这个parse()方法

    +
    public void parse() {
    +  if (!configuration.isResourceLoaded(resource)) {
    +    // -------> 然后就是走到这里,配置xml的mapper节点的内容
    +    configurationElement(parser.evalNode("/mapper"));
    +    configuration.addLoadedResource(resource);
    +    bindMapperForNamespace();
       }
    -  throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    -}
    -

    因为在config文件中设置了Pooled,所以对应创建的就是 PooledDataSourceFactory
    但是这里其实有个比较需要注意的,mybatis 这里的其实是继承了 UnpooledDataSourceFactory
    将基础方法都放在了 UnpooledDataSourceFactory

    -
    public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
     
    -  public PooledDataSourceFactory() {
    -    this.dataSource = new PooledDataSource();
    +  parsePendingResultMaps();
    +  parsePendingCacheRefs();
    +  parsePendingStatements();
    +}
    + +

    具体的处理逻辑

    +
    private void configurationElement(XNode context) {
    +  try {
    +    String namespace = context.getStringAttribute("namespace");
    +    if (namespace == null || namespace.isEmpty()) {
    +      throw new BuilderException("Mapper's namespace cannot be empty");
    +    }
    +    builderAssistant.setCurrentNamespace(namespace);
    +    cacheRefElement(context.evalNode("cache-ref"));
    +    cacheElement(context.evalNode("cache"));
    +    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    +    resultMapElements(context.evalNodes("/mapper/resultMap"));
    +    sqlElement(context.evalNodes("/mapper/sql"));
    +    // ------->  走到这,从上下文构建statement
    +    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    +  } catch (Exception e) {
    +    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
       }
    +}
    +

    具体代码在这,从上下文构建statement,只不过区分了下databaseId

    +
    private void buildStatementFromContext(List<XNode> list) {
    +  if (configuration.getDatabaseId() != null) {
    +    buildStatementFromContext(list, configuration.getDatabaseId());
    +  }
    +  // -----> 判断databaseId
    +  buildStatementFromContext(list, null);
     }
    -

    这里只保留了在构造方法里创建 DataSource
    而这个 PooledDataSource 虽然没有直接继承 UnpooledDataSource,但其实
    在构造方法里也是

    -
    public PooledDataSource() {
    -  dataSource = new UnpooledDataSource();
    -}
    -

    至于为什么这么做呢应该也是考虑到能比较多的复用代码,因为 Pooled 其实跟 Unpooled 最重要的差别就在于是不是每次都重开连接
    使用连接池能够让应用在有大量查询的时候不用反复创建连接,省去了建联的网络等开销,Unpooled就是完成与数据库的连接,并可以获取该连接
    主要的代码

    -
    @Override
    -public Connection getConnection() throws SQLException {
    -  return doGetConnection(username, password);
    -}
     
    -@Override
    -public Connection getConnection(String username, String password) throws SQLException {
    -  return doGetConnection(username, password);
    -}
    -private Connection doGetConnection(String username, String password) throws SQLException {
    -  Properties props = new Properties();
    -  if (driverProperties != null) {
    -    props.putAll(driverProperties);
    +

    判断下databaseId

    +
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    +  for (XNode context : list) {
    +    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    +    try {
    +      // -------> 解析statement节点
    +      statementParser.parseStatementNode();
    +    } catch (IncompleteElementException e) {
    +      configuration.addIncompleteStatement(statementParser);
    +    }
       }
    -  if (username != null) {
    -    props.setProperty("user", username);
    +}
    + +

    接下来就是真正处理的xml语句内容的,各个节点的信息内容

    +
    public void parseStatementNode() {
    +  String id = context.getStringAttribute("id");
    +  String databaseId = context.getStringAttribute("databaseId");
    +
    +  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    +    return;
       }
    -  if (password != null) {
    -    props.setProperty("password", password);
    +
    +  String nodeName = context.getNode().getNodeName();
    +  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    +  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    +  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    +  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    +  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    +
    +  // Include Fragments before parsing
    +  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    +  includeParser.applyIncludes(context.getNode());
    +
    +  String parameterType = context.getStringAttribute("parameterType");
    +  Class<?> parameterTypeClass = resolveClass(parameterType);
    +
    +  String lang = context.getStringAttribute("lang");
    +  LanguageDriver langDriver = getLanguageDriver(lang);
    +
    +  // Parse selectKey after includes and remove them.
    +  processSelectKeyNodes(id, parameterTypeClass, langDriver);
    +
    +  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    +  KeyGenerator keyGenerator;
    +  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    +  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    +  if (configuration.hasKeyGenerator(keyStatementId)) {
    +    keyGenerator = configuration.getKeyGenerator(keyStatementId);
    +  } else {
    +    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
    +        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
    +        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
       }
    -  return doGetConnection(props);
    -}
    -private Connection doGetConnection(Properties properties) throws SQLException {
    -  initializeDriver();
    -  Connection connection = DriverManager.getConnection(url, properties);
    -  configureConnection(connection);
    -  return connection;
    -}
    -

    而对于Pooled就会处理池化的逻辑

    -
    private PooledConnection popConnection(String username, String password) throws SQLException {
    -    boolean countedWait = false;
    -    PooledConnection conn = null;
    -    long t = System.currentTimeMillis();
    -    int localBadConnectionCount = 0;
     
    -    while (conn == null) {
    -      lock.lock();
    -      try {
    -        if (!state.idleConnections.isEmpty()) {
    -          // Pool has available connection
    -          conn = state.idleConnections.remove(0);
    -          if (log.isDebugEnabled()) {
    -            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
    -          }
    -        } else {
    -          // Pool does not have available connection
    -          if (state.activeConnections.size() < poolMaximumActiveConnections) {
    -            // Can create new connection
    -            conn = new PooledConnection(dataSource.getConnection(), this);
    -            if (log.isDebugEnabled()) {
    -              log.debug("Created connection " + conn.getRealHashCode() + ".");
    -            }
    -          } else {
    -            // Cannot create new connection
    -            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
    -            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
    -            if (longestCheckoutTime > poolMaximumCheckoutTime) {
    -              // Can claim overdue connection
    -              state.claimedOverdueConnectionCount++;
    -              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
    -              state.accumulatedCheckoutTime += longestCheckoutTime;
    -              state.activeConnections.remove(oldestActiveConnection);
    -              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
    -                try {
    -                  oldestActiveConnection.getRealConnection().rollback();
    -                } catch (SQLException e) {
    -                  /*
    -                     Just log a message for debug and continue to execute the following
    -                     statement like nothing happened.
    -                     Wrap the bad connection with a new PooledConnection, this will help
    -                     to not interrupt current executing thread and give current thread a
    -                     chance to join the next competition for another valid/good database
    -                     connection. At the end of this loop, bad {@link @conn} will be set as null.
    -                   */
    -                  log.debug("Bad connection. Could not roll back");
    -                }
    -              }
    -              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
    -              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
    -              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
    -              oldestActiveConnection.invalidate();
    -              if (log.isDebugEnabled()) {
    -                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
    -              }
    -            } else {
    -              // Must wait
    -              try {
    -                if (!countedWait) {
    -                  state.hadToWaitCount++;
    -                  countedWait = true;
    -                }
    -                if (log.isDebugEnabled()) {
    -                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
    -                }
    -                long wt = System.currentTimeMillis();
    -                condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
    -                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
    -              } catch (InterruptedException e) {
    -                // set interrupt flag
    -                Thread.currentThread().interrupt();
    -                break;
    -              }
    -            }
    -          }
    -        }
    -        if (conn != null) {
    -          // ping to server and check the connection is valid or not
    -          if (conn.isValid()) {
    -            if (!conn.getRealConnection().getAutoCommit()) {
    -              conn.getRealConnection().rollback();
    -            }
    -            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
    -            conn.setCheckoutTimestamp(System.currentTimeMillis());
    -            conn.setLastUsedTimestamp(System.currentTimeMillis());
    -            state.activeConnections.add(conn);
    -            state.requestCount++;
    -            state.accumulatedRequestTime += System.currentTimeMillis() - t;
    -          } else {
    -            if (log.isDebugEnabled()) {
    -              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
    -            }
    -            state.badConnectionCount++;
    -            localBadConnectionCount++;
    -            conn = null;
    -            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
    -              if (log.isDebugEnabled()) {
    -                log.debug("PooledDataSource: Could not get a good connection to the database.");
    -              }
    -              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
    -            }
    -          }
    -        }
    -      } finally {
    -        lock.unlock();
    -      }
    +  // 语句的主要参数解析
    +  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    +  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    +  Integer fetchSize = context.getIntAttribute("fetchSize");
    +  Integer timeout = context.getIntAttribute("timeout");
    +  String parameterMap = context.getStringAttribute("parameterMap");
    +  String resultType = context.getStringAttribute("resultType");
    +  Class<?> resultTypeClass = resolveClass(resultType);
    +  String resultMap = context.getStringAttribute("resultMap");
    +  String resultSetType = context.getStringAttribute("resultSetType");
    +  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    +  if (resultSetTypeEnum == null) {
    +    resultSetTypeEnum = configuration.getDefaultResultSetType();
    +  }
    +  String keyProperty = context.getStringAttribute("keyProperty");
    +  String keyColumn = context.getStringAttribute("keyColumn");
    +  String resultSets = context.getStringAttribute("resultSets");
     
    -    }
    +  // --------> 添加映射的statement
    +  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
    +      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    +      resultSetTypeEnum, flushCache, useCache, resultOrdered,
    +      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    +}
    - if (conn == null) { - if (log.isDebugEnabled()) { - log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); - } - throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); - } - return conn; - }
    -

    它的入口不是个get方法,而是pop,从含义来来讲就不一样
    org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection()

    -
    @Override
    -public Connection getConnection() throws SQLException {
    -  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
    -}
    -

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

    +

    添加的逻辑具体可以看下

    +
    public MappedStatement addMappedStatement(
    +    String id,
    +    SqlSource sqlSource,
    +    StatementType statementType,
    +    SqlCommandType sqlCommandType,
    +    Integer fetchSize,
    +    Integer timeout,
    +    String parameterMap,
    +    Class<?> parameterType,
    +    String resultMap,
    +    Class<?> resultType,
    +    ResultSetType resultSetType,
    +    boolean flushCache,
    +    boolean useCache,
    +    boolean resultOrdered,
    +    KeyGenerator keyGenerator,
    +    String keyProperty,
    +    String keyColumn,
    +    String databaseId,
    +    LanguageDriver lang,
    +    String resultSets) {
    +
    +  if (unresolvedCacheRef) {
    +    throw new IncompleteElementException("Cache-ref not yet resolved");
    +  }
    +
    +  id = applyCurrentNamespace(id, false);
    +  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    +
    +  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
    +      .resource(resource)
    +      .fetchSize(fetchSize)
    +      .timeout(timeout)
    +      .statementType(statementType)
    +      .keyGenerator(keyGenerator)
    +      .keyProperty(keyProperty)
    +      .keyColumn(keyColumn)
    +      .databaseId(databaseId)
    +      .lang(lang)
    +      .resultOrdered(resultOrdered)
    +      .resultSets(resultSets)
    +      .resultMaps(getStatementResultMaps(resultMap, resultType, id))
    +      .resultSetType(resultSetType)
    +      .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
    +      .useCache(valueOrDefault(useCache, isSelect))
    +      .cache(currentCache);
    +
    +  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    +  if (statementParameterMap != null) {
    +    statementBuilder.parameterMap(statementParameterMap);
    +  }
    +
    +  MappedStatement statement = statementBuilder.build();
    +  // ------>  正好是这里在configuration中添加了映射好的statement
    +  configuration.addMappedStatement(statement);
    +  return statement;
    +}
    + +

    而里面就是往map里添加

    +
    public void addMappedStatement(MappedStatement ms) {
    +  mappedStatements.put(ms.getId(), ms);
    +}
    + +

    获取mapper

    StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
    + +

    就是调用了 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)

    +
    public <T> T selectOne(String statement, Object parameter) {
    +  // Popular vote was to return null on 0 results and throw exception on too many.
    +  List<T> list = this.selectList(statement, parameter);
    +  if (list.size() == 1) {
    +    return list.get(0);
    +  } else if (list.size() > 1) {
    +    throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    +  } else {
    +    return null;
    +  }
    +}
    + +

    调用实际的实现方法

    +
    public <E> List<E> selectList(String statement, Object parameter) {
    +  return this.selectList(statement, parameter, RowBounds.DEFAULT);
    +}
    + +

    这里还有一层

    +
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    +  return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
    +}
    + + +

    根本的就是从configuration里获取了mappedStatement

    +
    private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    +  try {
    +    // 这里进行了获取
    +    MappedStatement ms = configuration.getMappedStatement(statement);
    +    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    +  } catch (Exception e) {
    +    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    +  } finally {
    +    ErrorContext.instance().reset();
    +  }
    +}
    ]]>
    Java @@ -7277,201 +7237,387 @@ separator CDATA #IMPLIED
    - mybatis系列-第一条sql的更多细节 - /2022/12/18/mybatis%E7%B3%BB%E5%88%97-%E7%AC%AC%E4%B8%80%E6%9D%A1sql%E7%9A%84%E6%9B%B4%E5%A4%9A%E7%BB%86%E8%8A%82/ - 执行细节
    首先设置了默认的languageDriver
    org/mybatis/mybatis/3.5.11/mybatis-3.5.11-sources.jar!/org/apache/ibatis/session/Configuration.java:215
    configuration的构造方法里

    -
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    + mybatis系列-typeAliases系统 + /2023/01/01/mybatis%E7%B3%BB%E5%88%97-typeAliases%E7%B3%BB%E7%BB%9F/ + 其实前面已经聊到过这个概念,在mybatis的配置中,以及一些初始化逻辑都是用了typeAliases,

    +
    <typeAliases>
    +  <typeAlias alias="Author" type="domain.blog.Author"/>
    +  <typeAlias alias="Blog" type="domain.blog.Blog"/>
    +  <typeAlias alias="Comment" type="domain.blog.Comment"/>
    +  <typeAlias alias="Post" type="domain.blog.Post"/>
    +  <typeAlias alias="Section" type="domain.blog.Section"/>
    +  <typeAlias alias="Tag" type="domain.blog.Tag"/>
    +</typeAliases>
    +

    可以在这里注册类型别名,然后在mybatis中配置使用时,可以简化这些类型的使用,其底层逻辑主要是一个map,

    +
    public class TypeAliasRegistry {
     
    -

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

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

    createSqlSource 就会调用

    -
    @Override
    -public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    -  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    -  return builder.parseScriptNode();
    -}
    + private final Map<String, Class<?>> typeAliases = new HashMap<>();
    +

    以string作为key,class对象作为value,比如我们在一开始使用的配置文件

    +
    <dataSource type="POOLED">
    +    <property name="driver" value="${driver}"/>
    +    <property name="url" value="${url}"/>
    +    <property name="username" value="${username}"/>
    +    <property name="password" value="${password}"/>
    +</dataSource>
    +

    这里使用的dataSource是POOLED,那它肯定是个别名或者需要对应处理
    而这个别名就是在Configuration的构造方法里初始化

    +
    public Configuration() {
    +    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    +    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
     
    -

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

    -
    public SqlSource parseScriptNode() {
    -  MixedSqlNode rootSqlNode = parseDynamicTags(context);
    -  SqlSource sqlSource;
    -  if (isDynamic) {
    -    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    -  } else {
    -    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    -  }
    -  return sqlSource;
    -}
    + typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); + typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); + typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); -

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

    -
    protected MixedSqlNode parseDynamicTags(XNode node) {
    -    List<SqlNode> contents = new ArrayList<>();
    -    NodeList children = node.getNode().getChildNodes();
    -    for (int i = 0; i < children.getLength(); i++) {
    -      XNode child = node.newXNode(children.item(i));
    -      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
    -        String data = child.getStringBody("");
    -        TextSqlNode textSqlNode = new TextSqlNode(data);
    -        // ---------> 主要是这边的逻辑
    -        if (textSqlNode.isDynamic()) {
    -          contents.add(textSqlNode);
    -          isDynamic = true;
    -        } else {
    -          contents.add(new StaticTextSqlNode(data));
    -        }
    -      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
    -        String nodeName = child.getNode().getNodeName();
    -        NodeHandler handler = nodeHandlerMap.get(nodeName);
    -        if (handler == null) {
    -          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
    -        }
    -        handler.handleNode(child, contents);
    -        isDynamic = true;
    -      }
    -    }
    -    return new MixedSqlNode(contents);
    -  }
    + typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); + typeAliasRegistry.registerAlias("FIFO", FifoCache.class); + typeAliasRegistry.registerAlias("LRU", LruCache.class); + typeAliasRegistry.registerAlias("SOFT", SoftCache.class); + typeAliasRegistry.registerAlias("WEAK", WeakCache.class); -

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

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

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

    -
    private GenericTokenParser createParser(TokenHandler handler) {
    -  return new GenericTokenParser("${", "}", handler);
    -}
    + typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); + typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); -

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

    -
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    + typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); + typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); + typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); + typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); + typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); + typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); + typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); -

    如果不是的话就创建RawSqlSource

    -
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    -```java
    +    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    +    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
     
    -但是这不是一个真实可用的 `sqlSource` ,
    -实际创建的时候会走到这
    -```java
    -public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    -    this(configuration, getSql(configuration, rootSqlNode), parameterType);
    +    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    +    languageRegistry.register(RawLanguageDriver.class);
    +  }
    +

    正是通过typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);这一行,注册了
    POOLED对应的别名类型是PooledDataSourceFactory.class
    具体的注册方法是在

    +
    public void registerAlias(String alias, Class<?> value) {
    +  if (alias == null) {
    +    throw new TypeException("The parameter alias cannot be null");
       }
    -
    -  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    -    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    -    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    -    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
    -  }
    - -

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

    -
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    -  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    -  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    -  String sql;
    -  if (configuration.isShrinkWhitespacesInSql()) {
    -    sql = parser.parse(removeExtraWhitespaces(originalSql));
    -  } else {
    -    sql = parser.parse(originalSql);
    +  // issue #748
    +  // 转换成小写,
    +  String key = alias.toLowerCase(Locale.ENGLISH);
    +  // 判断是否已经注册过了
    +  if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
    +    throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
       }
    -  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    -}
    - -

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

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

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

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

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

    -
    public BoundSql getBoundSql(Object parameterObject) {
    -    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    -    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    -    if (parameterMappings == null || parameterMappings.isEmpty()) {
    -      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    -    }
    -
    -    // check for nested result maps in parameter mappings (issue #30)
    -    for (ParameterMapping pm : boundSql.getParameterMappings()) {
    -      String rmId = pm.getResultMapId();
    -      if (rmId != null) {
    -        ResultMap rm = configuration.getResultMap(rmId);
    -        if (rm != null) {
    -          hasNestedResultMaps |= rm.hasNestedResultMaps();
    -        }
    +  // 放进map里
    +  typeAliases.put(key, value);
    +}
    +

    而获取的逻辑在这

    +
    public <T> Class<T> resolveAlias(String string) {
    +    try {
    +      if (string == null) {
    +        return null;
    +      }
    +      // issue #748
    +      // 同样的转成小写
    +      String key = string.toLowerCase(Locale.ENGLISH);
    +      Class<T> value;
    +      if (typeAliases.containsKey(key)) {
    +        value = (Class<T>) typeAliases.get(key);
    +      } else {
    +        // 这里还有从路径下处理的逻辑
    +        value = (Class<T>) Resources.classForName(string);
           }
    +      return value;
    +    } catch (ClassNotFoundException e) {
    +      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
         }
    -
    -    return boundSql;
       }
    +

    逻辑比较简单,但是在mybatis中也是不可或缺的一块概念

    +]]> + + Java + Mybatis + + + Java + Mysql + Mybatis + + + + docker比一般多一点的初学者介绍二 + /2020/03/15/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E4%BA%8C/ + 限制下 docker 的 cpu 使用率

    这里我们开始玩一点有意思的,我们在容器里装下 vim 和 gcc,然后写这样一段 c 代码

    +
    #include <stdio.h>
    +int main(void)
    +{
    +    int i = 0;
    +    for(;;) i++;
    +    return 0;
    +}
    +

    就是一个最简单的死循环,然后在容器里跑起来

    +
    $ gcc 1.c 
    +$ ./a.out
    +

    然后我们来看下系统资源占用(CPU)
    Xs562iawhHyMxeO
    上图是在容器里的,可以看到 cpu 已经 100%了
    然后看看容器外面的
    ecqH8XJ4k7rKhzu
    可以看到一个核的 cpu 也被占满了,因为是个双核的机器,并且代码是单线程的
    然后呢我们要做点啥
    因为已经在这个 ubuntu 容器中装了 vim 和 gcc,考虑到国内的网络,所以我们先把这个容器 commit 一下,

    +
    docker commit -a "nick" -m "my ubuntu" f63c5607df06 my_ubuntu:v1
    +

    然后再运行起来

    +
    docker run -it --cpus=0.1 my_ubuntu:v1 bash
    +


    我们的代码跟可执行文件都还在,要的就是这个效果,然后再运行一下

    结果是这个样子的,有点神奇是不,关键就在于 run 的时候的--cpus=0.1这个参数,它其实就是基于我前一篇说的 cgroup 技术,能将进程之间的cpu,内存等资源进行隔离

    +

    开始第一个 Dockerfile

    上一面为了复用那个我装了 vim 跟 gcc 的容器,我把它提交到了本地,使用了docker commit命令,有点类似于 git 的 commit,但是这个不是个很好的操作方式,需要手动介入,这里更推荐使用 Dockerfile 来构建镜像

    +
    From ubuntu:latest
    +MAINTAINER Nicksxs "nicksxs@hotmail.com"
    +RUN  sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
    +RUN apt-get clean
    +RUN apt-get update && apt install -y nginx
    +RUN echo 'Hi, i am in container' \
    +    > /usr/share/nginx/html/index.html
    +EXPOSE 80
    +

    先解释下这是在干嘛,首先是这个From ubuntu:latest基于的 ubuntu 的最新版本的镜像,然后第二行是维护人的信息,第三四行么作为墙内人你懂的,把 ubuntu 的源换成阿里云的,不然就有的等了,然后就是装下 nginx,往默认的 nginx 的入口 html 文件里输入一行欢迎语,然后暴露 80 端口
    然后我们使用sudo docker build -t="nicksxs/static_web" .命令来基于这个 Dockerfile 构建我们自己的镜像,过程中是这样的


    可以看到图中,我的 Dockerfile 是 7 行,里面就执行了 7 步,并且每一步都有一个类似于容器 id 的层 id 出来,这里就是一个比较重要的东西,docker 在构建的时候其实是有这个层的概念,Dockerfile 里的每一行都会往上加一层,这里有还注意下命令后面的.,代表当前目录下会自行去寻找 Dockerfile 进行构建,构建完了之后我们再看下我们的本地镜像

    我们自己的镜像出现啦
    然后有个问题,如果这个构建中途报了错咋办呢,来试试看,我们把 nginx 改成随便的一个错误名,nginxx(不知道会不会运气好真的有这玩意),再来 build 一把

    找不到 nginxx 包,是不是这个镜像就完全不能用呢,当然也不是,因为前面说到了,docker 是基于层去构建的,可以看到前面的 4 个 step 都没报错,那我们基于最后的成功步骤创建下容器看看
    也就是sudo docker run -t -i bd26f991b6c8 /bin/bash
    答案是可以的,只是没装成功 nginx

    还有一点注意到没,前面的几个 step 都有一句 Using cache,说明 docker 在构建镜像的时候是有缓存的,这也更能说明 docker 是基于层去构建镜像,同样的底包,同样的步骤,这些层是可以被复用的,这就是 docker 的构建缓存,当然我们也可以在 build 的时候加上--no-cache去把构建缓存禁用掉。

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

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

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

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

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

    +
    public class Configuration {
     
    -

    内部的sqlSource 就是StaticSqlSource

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

    这个BoundSql的内容也比较简单

    -
    public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    -  this.sql = sql;
    -  this.parameterMappings = parameterMappings;
    -  this.parameterObject = parameterObject;
    -  this.additionalParameters = new HashMap<>();
    -  this.metaParameters = configuration.newMetaObject(additionalParameters);
    -}
    + protected boolean safeRowBoundsEnabled; + protected boolean safeResultHandlerEnabled = true; + protected boolean mapUnderscoreToCamelCase; + protected boolean aggressiveLazyLoading; + protected boolean multipleResultSetsEnabled = true; + protected boolean useGeneratedKeys; + protected boolean useColumnLabel = true; + protected boolean cacheEnabled = true; + protected boolean callSettersOnNulls; + protected boolean useActualParamName = true; + protected boolean returnInstanceForEmptyRow; + protected boolean shrinkWhitespacesInSql; + protected boolean nullableOnForEach; + protected boolean argNameBasedConstructorAutoMapping; -

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

    -
    @Override
    -public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    -  Statement stmt = null;
    +  protected String logPrefix;
    +  protected Class<? extends Log> logImpl;
    +  protected Class<? extends VFS> vfsImpl;
    +  protected Class<?> defaultSqlProviderType;
    +  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    +  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    +  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
    +  protected Integer defaultStatementTimeout;
    +  protected Integer defaultFetchSize;
    +  protected ResultSetType defaultResultSetType;
    +  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    +  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    +  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
    +
    +  protected Properties variables = new Properties();
    +  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
    +  protected ObjectFactory objectFactory = new DefaultObjectFactory();
    +  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
    +
    +  protected boolean lazyLoadingEnabled = false;
    +  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
    +
    +  protected String databaseId;
    +  /**
    +   * Configuration factory class.
    +   * Used to create Configuration for loading deserialized unread properties.
    +   *
    +   * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
    +   */
    +  protected Class<?> configurationFactory;
    +
    +  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    +  protected final InterceptorChain interceptorChain = new InterceptorChain();
    +  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
    +  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    +  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
    +
    +  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
    +      .conflictMessageProducer((savedValue, targetValue) ->
    +          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    +  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    +  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    +  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    +  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
    +
    +  protected final Set<String> loadedResources = new HashSet<>();
    +  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
    +
    +  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
    +  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
    +  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
    +  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
    + +

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

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

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

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

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

    +

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

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

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

    -
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    +

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

    +
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    +  executorType = executorType == null ? defaultExecutorType : executorType;
    +  Executor executor;
    +  if (ExecutorType.BATCH == executorType) {
    +    executor = new BatchExecutor(this, transaction);
    +  } else if (ExecutorType.REUSE == executorType) {
    +    executor = new ReuseExecutor(this, transaction);
    +  } else {
    +    // ---------> 会走到这个分支
    +    executor = new SimpleExecutor(this, transaction);
    +  }
    +  if (cacheEnabled) {
    +    executor = new CachingExecutor(executor);
    +  }
    +  executor = (Executor) interceptorChain.pluginAll(executor);
    +  return executor;
    +}
    - switch (ms.getStatementType()) { - case STATEMENT: - delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); - break; - case PREPARED: - delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); - break; - case CALLABLE: - delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); - break; - default: - throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); +

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

    +
    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    +    this.configuration = configuration;
    +    this.executor = executor;
    +    this.dirty = false;
    +    this.autoCommit = autoCommit;
    +  }
    + +

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

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

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

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

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

    +

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

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

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

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

    然后是

    +
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    +  List<E> list;
    +  localCache.putObject(key, EXECUTION_PLACEHOLDER);
    +  try {
    +    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    +  } finally {
    +    localCache.removeObject(key);
    +  }
    +  localCache.putObject(key, list);
    +  if (ms.getStatementType() == StatementType.CALLABLE) {
    +    localOutputParameterCache.putObject(key, parameter);
    +  }
    +  return list;
    +}
    + +

    然后就是 simpleExecutor 的执行过程

    @Override
     public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
       Statement stmt = null;
    @@ -7485,8 +7631,7 @@ separator CDATA #IMPLIED
       }
     }
    - -

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

    +

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

    @Override
     public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
       PreparedStatement ps = (PreparedStatement) statement;
    @@ -7494,834 +7639,729 @@ separator CDATA #IMPLIED
       return resultSetHandler.handleResultSets(ps);
     }
    -

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

    -
    <transactionManager type="JDBC"/>
    +

    com.mysql.cj.jdbc.ClientPreparedStatement#execute

    +
    public boolean execute() throws SQLException {
    +        try {
    +            synchronized(this.checkClosed().getConnectionMutex()) {
    +                JdbcConnection locallyScopedConn = this.connection;
    +                if (!this.doPingInstead && !this.checkReadOnlySafeStatement()) {
    +                    throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.exceptionInterceptor);
    +                } else {
    +                    ResultSetInternalMethods rs = null;
    +                    this.lastQueryIsOnDupKeyUpdate = false;
    +                    if (this.retrieveGeneratedKeys) {
    +                        this.lastQueryIsOnDupKeyUpdate = this.containsOnDuplicateKeyUpdate();
    +                    }
     
    -

    因此在parseConfiguration中配置environment时

    -
    private void parseConfiguration(XNode root) {
    -    try {
    -      // issue #117 read properties first
    -      propertiesElement(root.evalNode("properties"));
    -      Properties settings = settingsAsProperties(root.evalNode("settings"));
    -      loadCustomVfs(settings);
    -      loadCustomLogImpl(settings);
    -      typeAliasesElement(root.evalNode("typeAliases"));
    -      pluginElement(root.evalNode("plugins"));
    -      objectFactoryElement(root.evalNode("objectFactory"));
    -      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    -      reflectorFactoryElement(root.evalNode("reflectorFactory"));
    -      settingsElement(settings);
    -      // read it after objectFactory and objectWrapperFactory issue #631
    -      // ----------> 就是这里
    -      environmentsElement(root.evalNode("environments"));
    -      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    -      typeHandlerElement(root.evalNode("typeHandlers"));
    -      mapperElement(root.evalNode("mappers"));
    -    } catch (Exception e) {
    -      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    -    }
    -  }
    + this.batchedGeneratedKeys = null; + this.resetCancelledState(); + this.implicitlyCloseAllOpenResults(); + this.clearWarnings(); + if (this.doPingInstead) { + this.doPingInstead(); + return true; + } else { + this.setupStreamingTimeout(locallyScopedConn); + Message sendPacket = ((PreparedQuery)this.query).fillSendPacket(((PreparedQuery)this.query).getQueryBindings()); + String oldDb = null; + if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) { + oldDb = locallyScopedConn.getDatabase(); + locallyScopedConn.setDatabase(this.getCurrentDatabase()); + } -

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

    -
    private void environmentsElement(XNode context) throws Exception {
    -  if (context != null) {
    -    if (environment == null) {
    -      environment = context.getStringAttribute("default");
    -    }
    -    for (XNode child : context.getChildren()) {
    -      String id = child.getStringAttribute("id");
    -      if (isSpecifiedEnvironment(id)) {
    -        // -------> 找到这里
    -        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
    -        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
    -        DataSource dataSource = dsFactory.getDataSource();
    -        Environment.Builder environmentBuilder = new Environment.Builder(id)
    -            .transactionFactory(txFactory)
    -            .dataSource(dataSource);
    -        configuration.setEnvironment(environmentBuilder.build());
    -        break;
    -      }
    -    }
    -  }
    -}
    + CachedResultSetMetaData cachedMetadata = null; + boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue(); + if (cacheResultSetMetadata) { + cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql()); + } -

    是通过以下方法获取的,

    -
    // 方法全限定名 org.apache.ibatis.builder.xml.XMLConfigBuilder#transactionManagerElement
    -private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    -    if (context != null) {
    -      String type = context.getStringAttribute("type");
    -      Properties props = context.getChildrenAsProperties();
    -      TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    -      factory.setProperties(props);
    -      return factory;
    -    }
    -    throw new BuilderException("Environment declaration requires a TransactionFactory.");
    -  }
    +                        locallyScopedConn.setSessionMaxRows(this.getQueryInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
    +                        rs = this.executeInternal(this.maxRows, sendPacket, this.createStreamingResultSet(), this.getQueryInfo().getFirstStmtChar() == 'S', cachedMetadata, false);
    +                        if (cachedMetadata != null) {
    +                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), cachedMetadata, rs);
    +                        } else if (rs.hasRows() && cacheResultSetMetadata) {
    +                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), (CachedResultSetMetaData)null, rs);
    +                        }
     
    -// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveClass
    -protected <T> Class<? extends T> resolveClass(String alias) {
    -    if (alias == null) {
    -      return null;
    -    }
    -    try {
    -      return resolveAlias(alias);
    -    } catch (Exception e) {
    -      throw new BuilderException("Error resolving class. Cause: " + e, e);
    -    }
    -  }
    +                        if (this.retrieveGeneratedKeys) {
    +                            rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
    +                        }
     
    -// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveAlias
    -  protected <T> Class<? extends T> resolveAlias(String alias) {
    -    return typeAliasRegistry.resolveAlias(alias);
    -  }
    -// 方法全限定名 org.apache.ibatis.type.TypeAliasRegistry#resolveAlias
    -  public <T> Class<T> resolveAlias(String string) {
    -    try {
    -      if (string == null) {
    -        return null;
    -      }
    -      // issue #748
    -      String key = string.toLowerCase(Locale.ENGLISH);
    -      Class<T> value;
    -      if (typeAliases.containsKey(key)) {
    -        value = (Class<T>) typeAliases.get(key);
    -      } else {
    -        value = (Class<T>) Resources.classForName(string);
    -      }
    -      return value;
    -    } catch (ClassNotFoundException e) {
    -      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    -    }
    -  }
    -

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

    -
    public Configuration() {
    -  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    + if (oldDb != null) { + locallyScopedConn.setDatabase(oldDb); + } -

    所以我们在这

    -
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    -  Transaction tx = null;
    -  try {
    -    final Environment environment = configuration.getEnvironment();
    -    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    + if (rs != null) { + this.lastInsertId = rs.getUpdateID(); + this.results = rs; + } -

    获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后

    -
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    -```java
    +                        return rs != null && rs.hasRows();
    +                    }
    +                }
    +            }
    +        } catch (CJException var11) {
    +            throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
    +        }
    +    }
    -创建的transaction就是JdbcTransaction -```java - @Override - public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) { - return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose); - }
    +]]>
    + + Java + Mybatis + + + Java + Mysql + Mybatis + +
    + + mybatis系列-第一条sql的更多细节 + /2022/12/18/mybatis%E7%B3%BB%E5%88%97-%E7%AC%AC%E4%B8%80%E6%9D%A1sql%E7%9A%84%E6%9B%B4%E5%A4%9A%E7%BB%86%E8%8A%82/ + 执行细节
    首先设置了默认的languageDriver
    org/mybatis/mybatis/3.5.11/mybatis-3.5.11-sources.jar!/org/apache/ibatis/session/Configuration.java:215
    configuration的构造方法里

    +
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    -

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

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

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

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

    createSqlSource 就会调用

    +
    @Override
    +public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    +  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    +  return builder.parseScriptNode();
    +}
    + +

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

    +
    public SqlSource parseScriptNode() {
    +  MixedSqlNode rootSqlNode = parseDynamicTags(context);
    +  SqlSource sqlSource;
    +  if (isDynamic) {
    +    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
       } else {
    -    return connection;
    +    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
       }
    -}
    + return sqlSource; +}
    -

    即调用了

    -
      @Override
    -  public Connection getConnection() throws SQLException {
    -    if (connection == null) {
    -      openConnection();
    +

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

    +
    protected MixedSqlNode parseDynamicTags(XNode node) {
    +    List<SqlNode> contents = new ArrayList<>();
    +    NodeList children = node.getNode().getChildNodes();
    +    for (int i = 0; i < children.getLength(); i++) {
    +      XNode child = node.newXNode(children.item(i));
    +      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
    +        String data = child.getStringBody("");
    +        TextSqlNode textSqlNode = new TextSqlNode(data);
    +        // ---------> 主要是这边的逻辑
    +        if (textSqlNode.isDynamic()) {
    +          contents.add(textSqlNode);
    +          isDynamic = true;
    +        } else {
    +          contents.add(new StaticTextSqlNode(data));
    +        }
    +      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
    +        String nodeName = child.getNode().getNodeName();
    +        NodeHandler handler = nodeHandlerMap.get(nodeName);
    +        if (handler == null) {
    +          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
    +        }
    +        handler.handleNode(child, contents);
    +        isDynamic = true;
    +      }
         }
    -    return connection;
    -  }
    +    return new MixedSqlNode(contents);
    +  }
    - protected void openConnection() throws SQLException { - if (log.isDebugEnabled()) { - log.debug("Opening JDBC Connection"); - } - connection = dataSource.getConnection(); - if (level != null) { - connection.setTransactionIsolation(level.getLevel()); - } - setDesiredAutoCommit(autoCommit); - } - @Override - public Connection getConnection() throws SQLException { - return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); - } +

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

    +
    public boolean isDynamic() {
    +  DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    +  // ----------> 主要是这里的方法
    +  GenericTokenParser parser = createParser(checker);
    +  parser.parse(text);
    +  return checker.isDynamic();
    +}
    -private PooledConnection popConnection(String username, String password) throws SQLException { - boolean countedWait = false; - PooledConnection conn = null; - long t = System.currentTimeMillis(); - int localBadConnectionCount = 0; +

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

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

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

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

    如果不是的话就创建RawSqlSource

    +
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    +```java
     
    -    if (conn == null) {
    -      if (log.isDebugEnabled()) {
    -        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    -      }
    -      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    -    }
    +但是这不是一个真实可用的 `sqlSource` ,
    +实际创建的时候会走到这
    +```java
    +public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    +    this(configuration, getSql(configuration, rootSqlNode), parameterType);
    +  }
     
    -    return conn;
    -  }
    + public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); + Class<?> clazz = parameterType == null ? Object.class : parameterType; + sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); + }
    -

    其实就是调用的

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

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

    +
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    +  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    +  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    +  String sql;
    +  if (configuration.isShrinkWhitespacesInSql()) {
    +    sql = parser.parse(removeExtraWhitespaces(originalSql));
    +  } else {
    +    sql = parser.parse(originalSql);
       }
    -```java
    +  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    +}
    -然后就是 -```java -private Connection doGetConnection(String username, String password) throws SQLException { - Properties props = new Properties(); - if (driverProperties != null) { - props.putAll(driverProperties); - } - if (username != null) { - props.setProperty("user", username); - } - if (password != null) { - props.setProperty("password", password); - } - return doGetConnection(props); - }
    +

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

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

    继续这个逻辑

    -
      private Connection doGetConnection(Properties properties) throws SQLException {
    -    initializeDriver();
    -    Connection connection = DriverManager.getConnection(url, properties);
    -    configureConnection(connection);
    -    return connection;
    -  }
    -    @CallerSensitive
    -    public static Connection getConnection(String url,
    -        java.util.Properties info) throws SQLException {
    +

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

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

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

    +
    public BoundSql getBoundSql(Object parameterObject) {
    +    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    +    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    +    if (parameterMappings == null || parameterMappings.isEmpty()) {
    +      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
         }
    -private static Connection getConnection(
    -        String url, java.util.Properties info, Class<?> caller) throws SQLException {
    -        /*
    -         * When callerCl is null, we should check the application's
    -         * (which is invoking this class indirectly)
    -         * classloader, so that the JDBC driver class outside rt.jar
    -         * can be loaded from here.
    -         */
    -        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    -        synchronized(DriverManager.class) {
    -            // synchronize loading of the correct classloader.
    -            if (callerCL == null) {
    -                callerCL = Thread.currentThread().getContextClassLoader();
    -            }
    -        }
     
    -        if(url == null) {
    -            throw new SQLException("The url cannot be null", "08001");
    +    // check for nested result maps in parameter mappings (issue #30)
    +    for (ParameterMapping pm : boundSql.getParameterMappings()) {
    +      String rmId = pm.getResultMapId();
    +      if (rmId != null) {
    +        ResultMap rm = configuration.getResultMap(rmId);
    +        if (rm != null) {
    +          hasNestedResultMaps |= rm.hasNestedResultMaps();
             }
    +      }
    +    }
     
    -        println("DriverManager.getConnection(\"" + url + "\")");
    +    return boundSql;
    +  }
    - // Walk through the loaded registeredDrivers attempting to make a connection. - // Remember the first exception that gets raised so we can reraise it. - SQLException reason = null; +

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

    +
    @Override
    +public BoundSql getBoundSql(Object parameterObject) {
    +  return sqlSource.getBoundSql(parameterObject);
    +}
    - for(DriverInfo aDriver : registeredDrivers) { - // If the caller does not have permission to load the driver then - // skip it. - if(isDriverAllowed(aDriver.driver, callerCL)) { - try { - // ----------> driver[className=com.mysql.cj.jdbc.Driver@64030b91] - println(" trying " + aDriver.driver.getClass().getName()); - Connection con = aDriver.driver.connect(url, info); - if (con != null) { - // Success! - println("getConnection returning " + aDriver.driver.getClass().getName()); - return (con); - } - } catch (SQLException ex) { - if (reason == null) { - reason = ex; - } - } +

    内部的sqlSource 就是StaticSqlSource

    +
    @Override
    +public BoundSql getBoundSql(Object parameterObject) {
    +  return new BoundSql(configuration, sql, parameterMappings, parameterObject);
    +}
    - } else { - println(" skipping: " + aDriver.getClass().getName()); - } +

    这个BoundSql的内容也比较简单

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

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

    +
    @Override
    +public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    +  Statement stmt = null;
    +  try {
    +    Configuration configuration = ms.getConfiguration();
    +    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    +    stmt = prepareStatement(handler, ms.getStatementLog());
    +    return handler.query(stmt, resultHandler);
    +  } finally {
    +    closeStatement(stmt);
    +  }
    +}
    - // if we got here nobody could connect. - if (reason != null) { - println("getConnection failed: " + reason); - throw reason; - } +

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

    +
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
     
    -        println("getConnection: no suitable driver found for "+ url);
    -        throw new SQLException("No suitable driver found for "+ url, "08001");
    -    }
    + switch (ms.getStatementType()) { + case STATEMENT: + delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case PREPARED: + delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case CALLABLE: + delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + default: + throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); + } +}
    -

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

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

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

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

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

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

    结合这个

    +

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

    @Override
    -public Connection getConnection() throws SQLException {
    -  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
    -}
    - -

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

    -]]>
    - - Java - Mybatis - - - Java - Mysql - Mybatis - -
    - - nginx 日志小记 - /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/ - nginx 默认的日志有特定的格式,我们也可以自定义,

    -

    默认的格式是预定义的 combined

    -
    log_format combined '$remote_addr - $remote_user [$time_local] '
    -                    '"$request" $status $body_bytes_sent '
    -                    '"$http_referer" "$http_user_agent"';
    +public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { + PreparedStatement ps = (PreparedStatement) statement; + ps.execute(); + return resultSetHandler.handleResultSets(ps); +}
    -

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

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

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

    +
    <transactionManager type="JDBC"/>
    -

    而如果需要额外的一些配置的话可以自己定义 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
    +

    因此在parseConfiguration中配置environment时

    +
    private void parseConfiguration(XNode root) {
    +    try {
    +      // issue #117 read properties first
    +      propertiesElement(root.evalNode("properties"));
    +      Properties settings = settingsAsProperties(root.evalNode("settings"));
    +      loadCustomVfs(settings);
    +      loadCustomLogImpl(settings);
    +      typeAliasesElement(root.evalNode("typeAliases"));
    +      pluginElement(root.evalNode("plugins"));
    +      objectFactoryElement(root.evalNode("objectFactory"));
    +      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    +      reflectorFactoryElement(root.evalNode("reflectorFactory"));
    +      settingsElement(settings);
    +      // read it after objectFactory and objectWrapperFactory issue #631
    +      // ----------> 就是这里
    +      environmentsElement(root.evalNode("environments"));
    +      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    +      typeHandlerElement(root.evalNode("typeHandlers"));
    +      mapperElement(root.evalNode("mappers"));
    +    } catch (Exception e) {
    +      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    +    }
    +  }
    -
    log_format combined_extend '$remote_addr - $remote_user [$time_local] '
    -                    '"$request" $status $body_bytes_sent '
    -                    '"$http_referer" "$http_user_agent" "$request_time"';
    +

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

    +
    private void environmentsElement(XNode context) throws Exception {
    +  if (context != null) {
    +    if (environment == null) {
    +      environment = context.getStringAttribute("default");
    +    }
    +    for (XNode child : context.getChildren()) {
    +      String id = child.getStringAttribute("id");
    +      if (isSpecifiedEnvironment(id)) {
    +        // -------> 找到这里
    +        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
    +        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
    +        DataSource dataSource = dsFactory.getDataSource();
    +        Environment.Builder environmentBuilder = new Environment.Builder(id)
    +            .transactionFactory(txFactory)
    +            .dataSource(dataSource);
    +        configuration.setEnvironment(environmentBuilder.build());
    +        break;
    +      }
    +    }
    +  }
    +}
    -

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

    -

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

    -

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

    -
    map $status $loggable {
    -    ~^[23]  0;
    -    default 1;
    -}
    +

    是通过以下方法获取的,

    +
    // 方法全限定名 org.apache.ibatis.builder.xml.XMLConfigBuilder#transactionManagerElement
    +private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    +    if (context != null) {
    +      String type = context.getStringAttribute("type");
    +      Properties props = context.getChildrenAsProperties();
    +      TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    +      factory.setProperties(props);
    +      return factory;
    +    }
    +    throw new BuilderException("Environment declaration requires a TransactionFactory.");
    +  }
     
    -access_log /path/to/access.log combined if=$loggable;
    +// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveClass +protected <T> Class<? extends T> resolveClass(String alias) { + if (alias == null) { + return null; + } + try { + return resolveAlias(alias); + } catch (Exception e) { + throw new BuilderException("Error resolving class. Cause: " + e, e); + } + } -

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

    -

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

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

    -

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

    -

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

    -
    http {
    -  lua_code_cache off;
    -}
    -
    -location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
    -   default_type 'application/json;charset=utf-8';
    -   content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
    -}
    +// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveAlias + protected <T> Class<? extends T> resolveAlias(String alias) { + return typeAliasRegistry.resolveAlias(alias); + } +// 方法全限定名 org.apache.ibatis.type.TypeAliasRegistry#resolveAlias + public <T> Class<T> resolveAlias(String string) { + try { + if (string == null) { + return null; + } + // issue #748 + String key = string.toLowerCase(Locale.ENGLISH); + Class<T> value; + if (typeAliases.containsKey(key)) { + value = (Class<T>) typeAliases.get(key); + } else { + value = (Class<T>) Resources.classForName(string); + } + return value; + } catch (ClassNotFoundException e) { + throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e); + } + }
    +

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

    +
    public Configuration() {
    +  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    -

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

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

    所以我们在这

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

    获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后

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

    使用总结

    -

    后续:

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

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

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

      -
      local decode = require("cjson").decode
      +创建的transaction就是JdbcTransaction 
      +```java
      +  @Override
      +  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
      +    return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose);
      +  }
      -function json_decode( str ) - local ok, t = pcall(decode, str) - if not ok then - return nil - end +

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

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

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

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

    即调用了

    +
      @Override
    +  public Connection getConnection() throws SQLException {
    +    if (connection == null) {
    +      openConnection();
    +    }
    +    return connection;
    +  }
     
    -local t = json.decode(str)
    -if t then
    -    ngx.say(" --> ", type(t))
    -end
    -

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

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

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

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

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

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

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

    -
    -

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

    - + protected void openConnection() throws SQLException { + if (log.isDebugEnabled()) { + log.debug("Opening JDBC Connection"); + } + connection = dataSource.getConnection(); + if (level != null) { + connection.setTransactionIsolation(level.getLevel()); + } + setDesiredAutoCommit(autoCommit); + } + @Override + public Connection getConnection() throws SQLException { + return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); + } -

    Regular Expression Syntax

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

    -

    PCRE函数介绍

    -

    pcre_compile
    原型:

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

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

    -
    -

    pcre_exec
    原型:

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

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

    -

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

    -

    一个丑陋的封装

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

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

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

    -
    -

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

    -
    -

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

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

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

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

    然后我们上面的StudentMapper.xml

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

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

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

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

    -
    public interface StudentMapper {
    +private PooledConnection popConnection(String username, String password) throws SQLException {
    +    boolean countedWait = false;
    +    PooledConnection conn = null;
    +    long t = System.currentTimeMillis();
    +    int localBadConnectionCount = 0;
     
    -    public StudentDO selectStudent(Long id);
    -}
    -

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

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

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

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

    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 - -
    - - mybatis系列-typeAliases系统 - /2023/01/01/mybatis%E7%B3%BB%E5%88%97-typeAliases%E7%B3%BB%E7%BB%9F/ - 其实前面已经聊到过这个概念,在mybatis的配置中,以及一些初始化逻辑都是用了typeAliases,

    -
    <typeAliases>
    -  <typeAlias alias="Author" type="domain.blog.Author"/>
    -  <typeAlias alias="Blog" type="domain.blog.Blog"/>
    -  <typeAlias alias="Comment" type="domain.blog.Comment"/>
    -  <typeAlias alias="Post" type="domain.blog.Post"/>
    -  <typeAlias alias="Section" type="domain.blog.Section"/>
    -  <typeAlias alias="Tag" type="domain.blog.Tag"/>
    -</typeAliases>
    -

    可以在这里注册类型别名,然后在mybatis中配置使用时,可以简化这些类型的使用,其底层逻辑主要是一个map,

    -
    public class TypeAliasRegistry {
    +    if (conn == null) {
    +      if (log.isDebugEnabled()) {
    +        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    +      }
    +      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    +    }
     
    -  private final Map<String, Class<?>> typeAliases = new HashMap<>();
    -

    以string作为key,class对象作为value,比如我们在一开始使用的配置文件

    -
    <dataSource type="POOLED">
    -    <property name="driver" value="${driver}"/>
    -    <property name="url" value="${url}"/>
    -    <property name="username" value="${username}"/>
    -    <property name="password" value="${password}"/>
    -</dataSource>
    -

    这里使用的dataSource是POOLED,那它肯定是个别名或者需要对应处理
    而这个别名就是在Configuration的构造方法里初始化

    -
    public Configuration() {
    -    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    -    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    +    return conn;
    +  }
    - typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); - typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); - typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); +

    其实就是调用的

    +
    // org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
    +  @Override
    +  public Connection getConnection() throws SQLException {
    +    return doGetConnection(username, password);
    +  }
    +```java
     
    -    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    -    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    -    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    -    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    -    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    +然后就是
    +```java
    +private Connection doGetConnection(String username, String password) throws SQLException {
    +    Properties props = new Properties();
    +    if (driverProperties != null) {
    +      props.putAll(driverProperties);
    +    }
    +    if (username != null) {
    +      props.setProperty("user", username);
    +    }
    +    if (password != null) {
    +      props.setProperty("password", password);
    +    }
    +    return doGetConnection(props);
    +  }
    - typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); +

    继续这个逻辑

    +
      private Connection doGetConnection(Properties properties) throws SQLException {
    +    initializeDriver();
    +    Connection connection = DriverManager.getConnection(url, properties);
    +    configureConnection(connection);
    +    return connection;
    +  }
    +    @CallerSensitive
    +    public static Connection getConnection(String url,
    +        java.util.Properties info) throws SQLException {
     
    -    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    -    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    +        return (getConnection(url, info, Reflection.getCallerClass()));
    +    }
    +private static Connection getConnection(
    +        String url, java.util.Properties info, Class<?> caller) throws SQLException {
    +        /*
    +         * When callerCl is null, we should check the application's
    +         * (which is invoking this class indirectly)
    +         * classloader, so that the JDBC driver class outside rt.jar
    +         * can be loaded from here.
    +         */
    +        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    +        synchronized(DriverManager.class) {
    +            // synchronize loading of the correct classloader.
    +            if (callerCL == null) {
    +                callerCL = Thread.currentThread().getContextClassLoader();
    +            }
    +        }
     
    -    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    -    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    -    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    -    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    -    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    -    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    -    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
    +        if(url == null) {
    +            throw new SQLException("The url cannot be null", "08001");
    +        }
     
    -    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    -    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
    +        println("DriverManager.getConnection(\"" + url + "\")");
     
    -    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    -    languageRegistry.register(RawLanguageDriver.class);
    -  }
    -

    正是通过typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);这一行,注册了
    POOLED对应的别名类型是PooledDataSourceFactory.class
    具体的注册方法是在

    -
    public void registerAlias(String alias, Class<?> value) {
    -  if (alias == null) {
    -    throw new TypeException("The parameter alias cannot be null");
    -  }
    -  // issue #748
    -  // 转换成小写,
    -  String key = alias.toLowerCase(Locale.ENGLISH);
    -  // 判断是否已经注册过了
    -  if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
    -    throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    -  }
    -  // 放进map里
    -  typeAliases.put(key, value);
    -}
    -

    而获取的逻辑在这

    -
    public <T> Class<T> resolveAlias(String string) {
    -    try {
    -      if (string == null) {
    -        return null;
    -      }
    -      // issue #748
    -      // 同样的转成小写
    -      String key = string.toLowerCase(Locale.ENGLISH);
    -      Class<T> value;
    -      if (typeAliases.containsKey(key)) {
    -        value = (Class<T>) typeAliases.get(key);
    -      } else {
    -        // 这里还有从路径下处理的逻辑
    -        value = (Class<T>) Resources.classForName(string);
    -      }
    -      return value;
    -    } catch (ClassNotFoundException e) {
    -      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    -    }
    -  }
    -

    逻辑比较简单,但是在mybatis中也是不可或缺的一块概念

    + // Walk through the loaded registeredDrivers attempting to make a connection. + // Remember the first exception that gets raised so we can reraise it. + SQLException reason = null; + + for(DriverInfo aDriver : registeredDrivers) { + // If the caller does not have permission to load the driver then + // skip it. + if(isDriverAllowed(aDriver.driver, callerCL)) { + try { + // ----------> driver[className=com.mysql.cj.jdbc.Driver@64030b91] + println(" trying " + aDriver.driver.getClass().getName()); + Connection con = aDriver.driver.connect(url, info); + if (con != null) { + // Success! + println("getConnection returning " + aDriver.driver.getClass().getName()); + return (con); + } + } catch (SQLException ex) { + if (reason == null) { + reason = ex; + } + } + + } else { + println(" skipping: " + aDriver.getClass().getName()); + } + + } + + // if we got here nobody could connect. + if (reason != null) { + println("getConnection failed: " + reason); + throw reason; + } + + println("getConnection: no suitable driver found for "+ url); + throw new SQLException("No suitable driver found for "+ url, "08001"); + }
    + + +

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

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

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

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

    结合这个

    +
    @Override
    +public Connection getConnection() throws SQLException {
    +  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
    +}
    + +

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

    ]]>
    Java @@ -8334,1373 +8374,795 @@ Fatal error: Abstract function abst1::abstra1() cannot contain body in php on li
    - rabbitmq-tips - /2017/04/25/rabbitmq-tips/ - rabbitmq 介绍

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

    -

    简单的使用经验

    通俗的理解

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

    -

    集群经验

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

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

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

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

    其他可以参考官方文档

    -

    一些坑

    消息丢失

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

    -

    集群搭建

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

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

    +
    +

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

    +
    +

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

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

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

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

    然后我们上面的StudentMapper.xml

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

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

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

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

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

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

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

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

    ]]>
    - php + Java + Mybatis - php - mq - im + Java + Mysql + Mybatis
    - 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;
    +    pcre-intro-and-a-simple-package
    +    /2015/01/16/pcre-intro-and-a-simple-package/
    +    Pcre
    +

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

    +
    +

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

    + -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, 他在扩容的时候也使用了类似的操作。

    +

    Regular Expression Syntax

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

    +

    PCRE函数介绍

    +

    pcre_compile
    原型:

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

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

    +
    +

    pcre_exec
    原型:

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

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

    +

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

    +

    一个丑陋的封装

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

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

    ]]>
    - Redis - 数据结构 - 源码 - C - Redis + C++ - redis - 数据结构 - 源码 + c++ + mfc
    - 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;
    +    powershell 初体验
    +    /2022/11/13/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C/
    +    powershell变量

    变量命名类似于php

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

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

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

    而在powershell中可以这样

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

    还可以通过这个

    +
    PS C:\Users\Nicks> ls variable:
     
    -/* 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

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

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

    ]]>
    - Redis - 数据结构 - 源码 - C - Redis + 语言 - redis - 数据结构 - 源码 + powershell
    - 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出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。这里其实更巧妙了,在细节上去优化会产生大量页异常中断的情况。

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

    +

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

    +

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

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

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

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

    使用总结

    +

    后续:

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

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

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

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

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

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

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

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

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

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

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

      +
    14. +
    ]]>
    - redis + nginx - redis + openresty + nginx
    - 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;
    +    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;
    +    }
    +}
    -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) 依次递推。

    +

    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
    ]]>
    - Redis - 数据结构 - 源码 - C - Redis + php - redis - 数据结构 - 源码 + php
    - 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
    • -
    + rabbitmq-tips + /2017/04/25/rabbitmq-tips/ + rabbitmq 介绍

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

    +

    简单的使用经验

    通俗的理解

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

    +

    集群经验

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

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

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

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

    其他可以参考官方文档

    +

    一些坑

    消息丢失

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

    +

    集群搭建

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

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

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

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

    -
    public class Configuration {
    -
    -  protected Environment environment;
    -
    -  protected boolean safeRowBoundsEnabled;
    -  protected boolean safeResultHandlerEnabled = true;
    -  protected boolean mapUnderscoreToCamelCase;
    -  protected boolean aggressiveLazyLoading;
    -  protected boolean multipleResultSetsEnabled = true;
    -  protected boolean useGeneratedKeys;
    -  protected boolean useColumnLabel = true;
    -  protected boolean cacheEnabled = true;
    -  protected boolean callSettersOnNulls;
    -  protected boolean useActualParamName = true;
    -  protected boolean returnInstanceForEmptyRow;
    -  protected boolean shrinkWhitespacesInSql;
    -  protected boolean nullableOnForEach;
    -  protected boolean argNameBasedConstructorAutoMapping;
    -
    -  protected String logPrefix;
    -  protected Class<? extends Log> logImpl;
    -  protected Class<? extends VFS> vfsImpl;
    -  protected Class<?> defaultSqlProviderType;
    -  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    -  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    -  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
    -  protected Integer defaultStatementTimeout;
    -  protected Integer defaultFetchSize;
    -  protected ResultSetType defaultResultSetType;
    -  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    -  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    -  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
    -
    -  protected Properties variables = new Properties();
    -  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
    -  protected ObjectFactory objectFactory = new DefaultObjectFactory();
    -  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
    -
    -  protected boolean lazyLoadingEnabled = false;
    -  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
    -
    -  protected String databaseId;
    -  /**
    -   * Configuration factory class.
    -   * Used to create Configuration for loading deserialized unread properties.
    -   *
    -   * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
    -   */
    -  protected Class<?> configurationFactory;
    +    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"';
    - protected final MapperRegistry mapperRegistry = new MapperRegistry(this); - protected final InterceptorChain interceptorChain = new InterceptorChain(); - protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this); - protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); - protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry(); +

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

    +
    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
    - protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection") - .conflictMessageProducer((savedValue, targetValue) -> - ". please check " + savedValue.getResource() + " and " + targetValue.getResource()); - protected final Map<String, Cache> caches = new StrictMap<>("Caches collection"); - protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection"); - protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection"); - protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection"); +

    而如果需要额外的一些配置的话可以自己定义 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
    - protected final Set<String> loadedResources = new HashSet<>(); - protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); +
    log_format combined_extend '$remote_addr - $remote_user [$time_local] '
    +                    '"$request" $status $body_bytes_sent '
    +                    '"$http_referer" "$http_user_agent" "$request_time"';
    - protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>(); - protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>(); - protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>(); - protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
    +

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

    +

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

    +

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

    +
    map $status $loggable {
    +    ~^[23]  0;
    +    default 1;
    +}
     
    -

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

    -
    public SqlSessionFactory build(Configuration config) {
    -  return new DefaultSqlSessionFactory(config);
    -}
    +access_log /path/to/access.log combined if=$loggable;
    -

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

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

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

    -

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

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

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

    -
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    -  executorType = executorType == null ? defaultExecutorType : executorType;
    -  Executor executor;
    -  if (ExecutorType.BATCH == executorType) {
    -    executor = new BatchExecutor(this, transaction);
    -  } else if (ExecutorType.REUSE == executorType) {
    -    executor = new ReuseExecutor(this, transaction);
    -  } else {
    -    // ---------> 会走到这个分支
    -    executor = new SimpleExecutor(this, transaction);
    -  }
    -  if (cacheEnabled) {
    -    executor = new CachingExecutor(executor);
    -  }
    -  executor = (Executor) interceptorChain.pluginAll(executor);
    -  return executor;
    -}
    - -

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

    -
    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    -    this.configuration = configuration;
    -    this.executor = executor;
    -    this.dirty = false;
    -    this.autoCommit = autoCommit;
    -  }
    - -

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

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

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

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

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

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

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

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

    然后是

    -
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    -  List<E> list;
    -  localCache.putObject(key, EXECUTION_PLACEHOLDER);
    -  try {
    -    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    -  } finally {
    -    localCache.removeObject(key);
    -  }
    -  localCache.putObject(key, list);
    -  if (ms.getStatementType() == StatementType.CALLABLE) {
    -    localOutputParameterCache.putObject(key, parameter);
    -  }
    -  return list;
    -}
    - -

    然后就是 simpleExecutor 的执行过程

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

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

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

    com.mysql.cj.jdbc.ClientPreparedStatement#execute

    -
    public boolean execute() throws SQLException {
    -        try {
    -            synchronized(this.checkClosed().getConnectionMutex()) {
    -                JdbcConnection locallyScopedConn = this.connection;
    -                if (!this.doPingInstead && !this.checkReadOnlySafeStatement()) {
    -                    throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.exceptionInterceptor);
    -                } else {
    -                    ResultSetInternalMethods rs = null;
    -                    this.lastQueryIsOnDupKeyUpdate = false;
    -                    if (this.retrieveGeneratedKeys) {
    -                        this.lastQueryIsOnDupKeyUpdate = this.containsOnDuplicateKeyUpdate();
    -                    }
    -
    -                    this.batchedGeneratedKeys = null;
    -                    this.resetCancelledState();
    -                    this.implicitlyCloseAllOpenResults();
    -                    this.clearWarnings();
    -                    if (this.doPingInstead) {
    -                        this.doPingInstead();
    -                        return true;
    -                    } else {
    -                        this.setupStreamingTimeout(locallyScopedConn);
    -                        Message sendPacket = ((PreparedQuery)this.query).fillSendPacket(((PreparedQuery)this.query).getQueryBindings());
    -                        String oldDb = null;
    -                        if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
    -                            oldDb = locallyScopedConn.getDatabase();
    -                            locallyScopedConn.setDatabase(this.getCurrentDatabase());
    -                        }
    -
    -                        CachedResultSetMetaData cachedMetadata = null;
    -                        boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
    -                        if (cacheResultSetMetadata) {
    -                            cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql());
    -                        }
    -
    -                        locallyScopedConn.setSessionMaxRows(this.getQueryInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
    -                        rs = this.executeInternal(this.maxRows, sendPacket, this.createStreamingResultSet(), this.getQueryInfo().getFirstStmtChar() == 'S', cachedMetadata, false);
    -                        if (cachedMetadata != null) {
    -                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), cachedMetadata, rs);
    -                        } else if (rs.hasRows() && cacheResultSetMetadata) {
    -                            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), (CachedResultSetMetaData)null, rs);
    -                        }
    -
    -                        if (this.retrieveGeneratedKeys) {
    -                            rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
    -                        }
    -
    -                        if (oldDb != null) {
    -                            locallyScopedConn.setDatabase(oldDb);
    -                        }
    -
    -                        if (rs != null) {
    -                            this.lastInsertId = rs.getUpdateID();
    -                            this.results = rs;
    -                        }
    -
    -                        return rs != null && rs.hasRows();
    -                    }
    -                }
    -            }
    -        } catch (CJException var11) {
    -            throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
    -        }
    -    }
    - -]]>
    - - Java - Mybatis - - - Java - Mysql - Mybatis - -
    - - redis数据结构介绍六 快表 - /2020/01/22/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%85%AD/ - 这应该是 redis 系列的最后一篇了,讲下快表,其实最前面讲的链表在早先的 redis 版本中也作为 list 的数据结构使用过,但是单纯的链表的缺陷之前也说了,插入便利,但是空间利用率低,并且不能进行二分查找等,检索效率低,ziplist 压缩表的产生也是同理,希望获得更好的性能,包括存储空间和访问性能等,原来我也不懂这个快表要怎么快,然后明白了一个道理,其实并没有什么银弹,只是大牛们会在适合的时候使用最适合的数据结构来实现性能的最大化,这里面有一招就是不同数据结构的组合调整,比如 Java 中的 HashMap,在链表节点数大于 8 时会转变成红黑树,以此提高访问效率,不费话了,回到快表,quicklist,这个数据结构主要使用在 list 类型中,如果我说其实这个 quicklist 就是个链表,可能大家不太会相信,但是事实上的确可以认为 quicklist 是个双向链表,看下代码

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

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

    -

    ziplist

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

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

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

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

    压缩

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

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

    插入过程

    简单说下插入元素的过程

    -
    /* Wrapper to allow argument-based switching between HEAD/TAIL pop */
    -void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
    -                   int where) {
    -    if (where == QUICKLIST_HEAD) {
    -        quicklistPushHead(quicklist, value, sz);
    -    } else if (where == QUICKLIST_TAIL) {
    -        quicklistPushTail(quicklist, value, sz);
    -    }
    -}
    -
    -/* Add new entry to head node of quicklist.
    - *
    - * Returns 0 if used existing head.
    - * Returns 1 if new head created. */
    -int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    -    quicklistNode *orig_head = quicklist->head;
    -    if (likely(
    -            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
    -        quicklist->head->zl =
    -            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
    -        quicklistNodeUpdateSz(quicklist->head);
    -    } else {
    -        quicklistNode *node = quicklistCreateNode();
    -        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
    -
    -        quicklistNodeUpdateSz(node);
    -        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    -    }
    -    quicklist->count++;
    -    quicklist->head->count++;
    -    return (orig_head != quicklist->head);
    -}
    -
    -/* Add new entry to tail node of quicklist.
    - *
    - * Returns 0 if used existing tail.
    - * Returns 1 if new tail created. */
    -int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    -    quicklistNode *orig_tail = quicklist->tail;
    -    if (likely(
    -            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
    -        quicklist->tail->zl =
    -            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
    -        quicklistNodeUpdateSz(quicklist->tail);
    -    } else {
    -        quicklistNode *node = quicklistCreateNode();
    -        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
    -
    -        quicklistNodeUpdateSz(node);
    -        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    -    }
    -    quicklist->count++;
    -    quicklist->tail->count++;
    -    return (orig_tail != quicklist->tail);
    -}
    -
    -/* Wrappers for node inserting around existing node. */
    -REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
    -                                             quicklistNode *old_node,
    -                                             quicklistNode *new_node) {
    -    __quicklistInsertNode(quicklist, old_node, new_node, 0);
    -}
    -
    -REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
    -                                            quicklistNode *old_node,
    -                                            quicklistNode *new_node) {
    -    __quicklistInsertNode(quicklist, old_node, new_node, 1);
    -}
    -
    -/* Insert 'new_node' after 'old_node' if 'after' is 1.
    - * Insert 'new_node' before 'old_node' if 'after' is 0.
    - * Note: 'new_node' is *always* uncompressed, so if we assign it to
    - *       head or tail, we do not need to uncompress it. */
    -REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
    -                                        quicklistNode *old_node,
    -                                        quicklistNode *new_node, int after) {
    -    if (after) {
    -        new_node->prev = old_node;
    -        if (old_node) {
    -            new_node->next = old_node->next;
    -            if (old_node->next)
    -                old_node->next->prev = new_node;
    -            old_node->next = new_node;
    -        }
    -        if (quicklist->tail == old_node)
    -            quicklist->tail = new_node;
    -    } else {
    -        new_node->next = old_node;
    -        if (old_node) {
    -            new_node->prev = old_node->prev;
    -            if (old_node->prev)
    -                old_node->prev->next = new_node;
    -            old_node->prev = new_node;
    -        }
    -        if (quicklist->head == old_node)
    -            quicklist->head = new_node;
    -    }
    -    /* If this insert creates the only element so far, initialize head/tail. */
    -    if (quicklist->len == 0) {
    -        quicklist->head = quicklist->tail = new_node;
    -    }
    -
    -    if (old_node)
    -        quicklistCompress(quicklist, old_node);
    -
    -    quicklist->len++;
    -}
    -

    前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。

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

    -
    -

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

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

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

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

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

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

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

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

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

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

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

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

    -]]>
    - - redis - - - redis - 淘汰策略 - 应用 - Evict - -
    - - redis系列介绍七-过期策略 - /2020/04/12/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E4%B8%83/ - 这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
    redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,

    -

    redis过期策略

    redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
    懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
    主要代码是

    -
    /* This function is called when we are going to perform some operation
    - * in a given key, but such key may be already logically expired even if
    - * it still exists in the database. The main way this function is called
    - * is via lookupKey*() family of functions.
    - *
    - * The behavior of the function depends on the replication role of the
    - * instance, because slave instances do not expire keys, they wait
    - * for DELs from the master for consistency matters. However even
    - * slaves will try to have a coherent return value for the function,
    - * so that read commands executed in the slave side will be able to
    - * behave like if the key is expired even if still present (because the
    - * master has yet to propagate the DEL).
    - *
    - * In masters as a side effect of finding a key which is expired, such
    - * key will be evicted from the database. Also this may trigger the
    - * propagation of a DEL/UNLINK command in AOF / replication stream.
    - *
    - * The return value of the function is 0 if the key is still valid,
    - * otherwise the function returns 1 if the key is expired. */
    -int expireIfNeeded(redisDb *db, robj *key) {
    -    if (!keyIsExpired(db,key)) return 0;
    -
    -    /* If we are running in the context of a slave, instead of
    -     * evicting the expired key from the database, we return ASAP:
    -     * the slave key expiration is controlled by the master that will
    -     * send us synthesized DEL operations for expired keys.
    -     *
    -     * Still we try to return the right information to the caller,
    -     * that is, 0 if we think the key should be still valid, 1 if
    -     * we think the key is expired at this time. */
    -    if (server.masterhost != NULL) return 1;
    -
    -    /* Delete the key */
    -    server.stat_expiredkeys++;
    -    propagateExpire(db,key,server.lazyfree_lazy_expire);
    -    notifyKeyspaceEvent(NOTIFY_EXPIRED,
    -        "expired",key,db->id);
    -    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
    -                                         dbSyncDelete(db,key);
    -}
    -
    -/* Check if the key is expired. */
    -int keyIsExpired(redisDb *db, robj *key) {
    -    mstime_t when = getExpire(db,key);
    -    mstime_t now;
    -
    -    if (when < 0) return 0; /* No expire for this key */
    -
    -    /* Don't expire anything while loading. It will be done later. */
    -    if (server.loading) return 0;
    -
    -    /* If we are in the context of a Lua script, we pretend that time is
    -     * blocked to when the Lua script started. This way a key can expire
    -     * only the first time it is accessed and not in the middle of the
    -     * script execution, making propagation to slaves / AOF consistent.
    -     * See issue #1525 on Github for more information. */
    -    if (server.lua_caller) {
    -        now = server.lua_time_start;
    -    }
    -    /* If we are in the middle of a command execution, we still want to use
    -     * a reference time that does not change: in that case we just use the
    -     * cached time, that we update before each call in the call() function.
    -     * This way we avoid that commands such as RPOPLPUSH or similar, that
    -     * may re-open the same key multiple times, can invalidate an already
    -     * open object in a next call, if the next call will see the key expired,
    -     * while the first did not. */
    -    else if (server.fixed_time_expire > 0) {
    -        now = server.mstime;
    -    }
    -    /* For the other cases, we want to use the most fresh time we have. */
    -    else {
    -        now = mstime();
    -    }
    -
    -    /* The key expired if the current (virtual or real) time is greater
    -     * than the expire time of the key. */
    -    return now > when;
    -}
    -/* Return the expire time of the specified key, or -1 if no expire
    - * is associated with this key (i.e. the key is non volatile) */
    -long long getExpire(redisDb *db, robj *key) {
    -    dictEntry *de;
    -
    -    /* No expire? return ASAP */
    -    if (dictSize(db->expires) == 0 ||
    -       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    -
    -    /* The entry was found in the expire dict, this means it should also
    -     * be present in the main dict (safety check). */
    -    serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    -    return dictGetSignedIntegerVal(de);
    -}
    -

    这里有几点要注意的,第一是当惰性删除时会根据lazyfree_lazy_expire这个参数去判断是执行同步删除还是异步删除,另外一点是对于 slave,是不需要执行的,因为会在 master 过期时向 slave 发送 del 指令。
    光采用这个策略会有什么问题呢,假如一些key 一直未被访问,那这些 key 就不会过期了,导致一直被占用着内存,所以 redis 采取了懒汉式过期加定期过期策略,定期策略是怎么执行的呢

    -
    /* This function handles 'background' operations we are required to do
    - * incrementally in Redis databases, such as active key expiring, resizing,
    - * rehashing. */
    -void databasesCron(void) {
    -    /* Expire keys by random sampling. Not required for slaves
    -     * as master will synthesize DELs for us. */
    -    if (server.active_expire_enabled) {
    -        if (server.masterhost == NULL) {
    -            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
    -        } else {
    -            expireSlaveKeys();
    -        }
    -    }
    -
    -    /* Defrag keys gradually. */
    -    activeDefragCycle();
    -
    -    /* Perform hash tables rehashing if needed, but only if there are no
    -     * other processes saving the DB on disk. Otherwise rehashing is bad
    -     * as will cause a lot of copy-on-write of memory pages. */
    -    if (!hasActiveChildProcess()) {
    -        /* We use global counters so if we stop the computation at a given
    -         * DB we'll be able to start from the successive in the next
    -         * cron loop iteration. */
    -        static unsigned int resize_db = 0;
    -        static unsigned int rehash_db = 0;
    -        int dbs_per_call = CRON_DBS_PER_CALL;
    -        int j;
    -
    -        /* Don't test more DBs than we have. */
    -        if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
    -
    -        /* Resize */
    -        for (j = 0; j < dbs_per_call; j++) {
    -            tryResizeHashTables(resize_db % server.dbnum);
    -            resize_db++;
    -        }
    -
    -        /* Rehash */
    -        if (server.activerehashing) {
    -            for (j = 0; j < dbs_per_call; j++) {
    -                int work_done = incrementallyRehash(rehash_db);
    -                if (work_done) {
    -                    /* If the function did some work, stop here, we'll do
    -                     * more at the next cron loop. */
    -                    break;
    -                } else {
    -                    /* If this db didn't need rehash, we'll try the next one. */
    -                    rehash_db++;
    -                    rehash_db %= server.dbnum;
    -                }
    -            }
    -        }
    -    }
    -}
    -/* Try to expire a few timed out keys. The algorithm used is adaptive and
    - * will use few CPU cycles if there are few expiring keys, otherwise
    - * it will get more aggressive to avoid that too much memory is used by
    - * keys that can be removed from the keyspace.
    - *
    - * Every expire cycle tests multiple databases: the next call will start
    - * again from the next db, with the exception of exists for time limit: in that
    - * case we restart again from the last database we were processing. Anyway
    - * no more than CRON_DBS_PER_CALL databases are tested at every iteration.
    - *
    - * The function can perform more or less work, depending on the "type"
    - * argument. It can execute a "fast cycle" or a "slow cycle". The slow
    - * cycle is the main way we collect expired cycles: this happens with
    - * the "server.hz" frequency (usually 10 hertz).
    - *
    - * However the slow cycle can exit for timeout, since it used too much time.
    - * For this reason the function is also invoked to perform a fast cycle
    - * at every event loop cycle, in the beforeSleep() function. The fast cycle
    - * will try to perform less work, but will do it much more often.
    - *
    - * The following are the details of the two expire cycles and their stop
    - * conditions:
    - *
    - * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
    - * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
    - * microseconds, and is not repeated again before the same amount of time.
    - * The cycle will also refuse to run at all if the latest slow cycle did not
    - * terminate because of a time limit condition.
    - *
    - * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
    - * executed, where the time limit is a percentage of the REDIS_HZ period
    - * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
    - * fast cycle, the check of every database is interrupted once the number
    - * of already expired keys in the database is estimated to be lower than
    - * a given percentage, in order to avoid doing too much work to gain too
    - * little memory.
    - *
    - * The configured expire "effort" will modify the baseline parameters in
    - * order to do more work in both the fast and slow expire cycles.
    - */
    -
    -#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
    -#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
    -#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
    -#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
    -                                                   we do extra efforts. */
    -void activeExpireCycle(int type) {
    -    /* Adjust the running parameters according to the configured expire
    -     * effort. The default effort is 1, and the maximum configurable effort
    -     * is 10. */
    -    unsigned long
    -    effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
    -    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
    -                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
    -    config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
    -                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
    -    config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
    -                                  2*effort,
    -    config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
    -                                    effort;
    -
    -    /* This function has some global state in order to continue the work
    -     * incrementally across calls. */
    -    static unsigned int current_db = 0; /* Last DB tested. */
    -    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    -    static long long last_fast_cycle = 0; /* When last fast cycle ran. */
    -
    -    int j, iteration = 0;
    -    int dbs_per_call = CRON_DBS_PER_CALL;
    -    long long start = ustime(), timelimit, elapsed;
    -
    -    /* When clients are paused the dataset should be static not just from the
    -     * POV of clients not being able to write, but also from the POV of
    -     * expires and evictions of keys not being performed. */
    -    if (clientsArePaused()) return;
    -
    -    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
    -        /* Don't start a fast cycle if the previous cycle did not exit
    -         * for time limit, unless the percentage of estimated stale keys is
    -         * too high. Also never repeat a fast cycle for the same period
    -         * as the fast cycle total duration itself. */
    -        if (!timelimit_exit &&
    -            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
    -            return;
    -
    -        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
    -            return;
    -
    -        last_fast_cycle = start;
    -    }
    -
    -    /* We usually should test CRON_DBS_PER_CALL per iteration, with
    -     * two exceptions:
    -     *
    -     * 1) Don't test more DBs than we have.
    -     * 2) If last time we hit the time limit, we want to scan all DBs
    -     * in this iteration, as there is work to do in some DB and we don't want
    -     * expired keys to use memory for too much time. */
    -    if (dbs_per_call > server.dbnum || timelimit_exit)
    -        dbs_per_call = server.dbnum;
    +

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

    +

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

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

    +

    Linux中的CopyOnWrite

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

    +

    CopyOnWrite的好处:

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

    +

    Redis中的CopyOnWrite

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

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

    +

    SDS 简单动态字符串

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

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

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

    +

    链表

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

    +
    typedef struct listNode {
    +    // 前置节点
    +    struct listNode *prev;
    +    // 后置节点
    +    struct listNode *next;
    +    // 值
    +    void *value;
    +} listNode;
     
    -    /* We can use at max 'config_cycle_slow_time_perc' percentage of CPU
    -     * time per iteration. Since this function gets called with a frequency of
    -     * server.hz times per second, the following is the max amount of
    -     * microseconds we can spend in this function. */
    -    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    -    timelimit_exit = 0;
    -    if (timelimit <= 0) timelimit = 1;
    +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;
     
    -    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
    -        timelimit = config_cycle_fast_duration; /* in microseconds. */
    +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;
     
    -    /* Accumulate some global stats as we expire keys, to have some idea
    -     * about the number of keys that are already logically expired, but still
    -     * existing inside the database. */
    -    long total_sampled = 0;
    -    long total_expired = 0;
    +/* 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;
     
    -    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
    -        /* Expired and checked in a single loop. */
    -        unsigned long expired, sampled;
    +typedef struct dict {
    +    dictType *type;
    +    void *privdata;
    +    dictht ht[2];
    +    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    +    int iterators; /* number of iterators currently running */
    +} dict;
    +

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

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

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

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

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

    +

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

    +
    $nums=1..5
    +


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

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


    还有判断类型可以用-is

    创建一个空数组

    +
    $array=@()
    +


    数组添加元素

    +
    $array+="a"
    +


    数组删除元素

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

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

    +
    typedef struct intset {
    +    // 编码方式
    +    uint32_t encoding;
    +    // 集合包含的元素数量
    +    uint32_t length;
    +    // 保存元素的数组
    +    int8_t contents[];
    +} intset;
     
    -        redisDb *db = server.db+(current_db % server.dbnum);
    +/* 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/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;
     
    -        /* Increment the DB now so we are sure if we run out of time
    -         * in the current DB we'll restart from the next. This allows to
    -         * distribute the time evenly across DBs. */
    -        current_db++;
    +typedef struct zskiplist {
    +    struct zskiplistNode *header, *tail;
    +    unsigned long length;
    +    int level;
    +} zskiplist;
     
    -        /* Continue to expire if at the end of the cycle more than 25%
    -         * of the keys were expired. */
    -        do {
    -            unsigned long num, slots;
    -            long long now, ttl_sum;
    -            int ttl_samples;
    -            iteration++;
    +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;
     
    -            /* If there is nothing to expire try next DB ASAP. */
    -            if ((num = dictSize(db->expires)) == 0) {
    -                db->avg_ttl = 0;
    -                break;
    -            }
    -            slots = dictSlots(db->expires);
    -            now = mstime();
    +/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
    + * 'sz' is byte length of 'compressed' field.
    + * 'compressed' is LZF data with total (compressed) length 'sz'
    + * NOTE: uncompressed length is stored in quicklistNode->sz.
    + * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
    +typedef struct quicklistLZF {
    +    unsigned int sz; /* LZF size in bytes*/
    +    char compressed[];
    +} quicklistLZF;
     
    -            /* When there are less than 1% filled slots, sampling the key
    -             * space is expensive, so stop here waiting for better times...
    -             * The dictionary will be resized asap. */
    -            if (num && slots > DICT_HT_INITIAL_SIZE &&
    -                (num*100/slots < 1)) break;
    +/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
    + * 'count' is the number of total entries.
    + * 'len' is the number of quicklist nodes.
    + * 'compress' is: -1 if compression disabled, otherwise it's the number
    + *                of quicklistNodes to leave uncompressed at ends of quicklist.
    + * 'fill' is the user-requested (or default) fill factor. */
    +typedef struct quicklist {
    +    quicklistNode *head;
    +    quicklistNode *tail;
    +    unsigned long count;        /* total count of all entries in all ziplists */
    +    unsigned long len;          /* number of quicklistNodes */
    +    int fill : 16;              /* fill factor for individual nodes */
    +    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
    +} quicklist;
    +

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

    +

    ziplist

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

    +
    size_t offset = (-fill) - 1;
    +if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
    +    if (sz <= optimization_level[offset]) {
    +        return 1;
    +    } else {
    +        return 0;
    +    }
    +} else {
    +    return 0;
    +}
     
    -            /* The main collection cycle. Sample random keys among keys
    -             * with an expire set, checking for expired ones. */
    -            expired = 0;
    -            sampled = 0;
    -            ttl_sum = 0;
    -            ttl_samples = 0;
    +/* Optimization levels for size-based filling */
    +static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
     
    -            if (num > config_keys_per_loop)
    -                num = config_keys_per_loop;
    +/* Create a new quicklist.
    + * Free with quicklistRelease(). */
    +quicklist *quicklistCreate(void) {
    +    struct quicklist *quicklist;
     
    -            /* Here we access the low level representation of the hash table
    -             * for speed concerns: this makes this code coupled with dict.c,
    -             * but it hardly changed in ten years.
    -             *
    -             * Note that certain places of the hash table may be empty,
    -             * so we want also a stop condition about the number of
    -             * buckets that we scanned. However scanning for free buckets
    -             * is very fast: we are in the cache line scanning a sequential
    -             * array of NULL pointers, so we can scan a lot more buckets
    -             * than keys in the same time. */
    -            long max_buckets = num*20;
    -            long checked_buckets = 0;
    +    quicklist = zmalloc(sizeof(*quicklist));
    +    quicklist->head = quicklist->tail = NULL;
    +    quicklist->len = 0;
    +    quicklist->count = 0;
    +    quicklist->compress = 0;
    +    quicklist->fill = -2;
    +    return quicklist;
    +}
    +

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

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

    压缩

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

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

    插入过程

    简单说下插入元素的过程

    +
    /* Wrapper to allow argument-based switching between HEAD/TAIL pop */
    +void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
    +                   int where) {
    +    if (where == QUICKLIST_HEAD) {
    +        quicklistPushHead(quicklist, value, sz);
    +    } else if (where == QUICKLIST_TAIL) {
    +        quicklistPushTail(quicklist, value, sz);
    +    }
    +}
     
    -            while (sampled < num && checked_buckets < max_buckets) {
    -                for (int table = 0; table < 2; table++) {
    -                    if (table == 1 && !dictIsRehashing(db->expires)) break;
    +/* Add new entry to head node of quicklist.
    + *
    + * Returns 0 if used existing head.
    + * Returns 1 if new head created. */
    +int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    +    quicklistNode *orig_head = quicklist->head;
    +    if (likely(
    +            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
    +        quicklist->head->zl =
    +            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
    +        quicklistNodeUpdateSz(quicklist->head);
    +    } else {
    +        quicklistNode *node = quicklistCreateNode();
    +        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
     
    -                    unsigned long idx = db->expires_cursor;
    -                    idx &= db->expires->ht[table].sizemask;
    -                    dictEntry *de = db->expires->ht[table].table[idx];
    -                    long long ttl;
    +        quicklistNodeUpdateSz(node);
    +        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    +    }
    +    quicklist->count++;
    +    quicklist->head->count++;
    +    return (orig_head != quicklist->head);
    +}
     
    -                    /* Scan the current bucket of the current table. */
    -                    checked_buckets++;
    -                    while(de) {
    -                        /* Get the next entry now since this entry may get
    -                         * deleted. */
    -                        dictEntry *e = de;
    -                        de = de->next;
    +/* Add new entry to tail node of quicklist.
    + *
    + * Returns 0 if used existing tail.
    + * Returns 1 if new tail created. */
    +int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    +    quicklistNode *orig_tail = quicklist->tail;
    +    if (likely(
    +            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
    +        quicklist->tail->zl =
    +            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
    +        quicklistNodeUpdateSz(quicklist->tail);
    +    } else {
    +        quicklistNode *node = quicklistCreateNode();
    +        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
     
    -                        ttl = dictGetSignedIntegerVal(e)-now;
    -                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
    -                        if (ttl > 0) {
    -                            /* We want the average TTL of keys yet
    -                             * not expired. */
    -                            ttl_sum += ttl;
    -                            ttl_samples++;
    -                        }
    -                        sampled++;
    -                    }
    -                }
    -                db->expires_cursor++;
    -            }
    -            total_expired += expired;
    -            total_sampled += sampled;
    +        quicklistNodeUpdateSz(node);
    +        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    +    }
    +    quicklist->count++;
    +    quicklist->tail->count++;
    +    return (orig_tail != quicklist->tail);
    +}
     
    -            /* Update the average TTL stats for this database. */
    -            if (ttl_samples) {
    -                long long avg_ttl = ttl_sum/ttl_samples;
    +/* Wrappers for node inserting around existing node. */
    +REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
    +                                             quicklistNode *old_node,
    +                                             quicklistNode *new_node) {
    +    __quicklistInsertNode(quicklist, old_node, new_node, 0);
    +}
     
    -                /* Do a simple running average with a few samples.
    -                 * We just use the current estimate with a weight of 2%
    -                 * and the previous estimate with a weight of 98%. */
    -                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
    -                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
    -            }
    +REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
    +                                            quicklistNode *old_node,
    +                                            quicklistNode *new_node) {
    +    __quicklistInsertNode(quicklist, old_node, new_node, 1);
    +}
     
    -            /* We can't block forever here even if there are many keys to
    -             * expire. So after a given amount of milliseconds return to the
    -             * caller waiting for the other active expire cycle. */
    -            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
    -                elapsed = ustime()-start;
    -                if (elapsed > timelimit) {
    -                    timelimit_exit = 1;
    -                    server.stat_expired_time_cap_reached_count++;
    -                    break;
    -                }
    -            }
    -            /* We don't repeat the cycle for the current database if there are
    -             * an acceptable amount of stale keys (logically expired but yet
    -             * not reclained). */
    -        } while ((expired*100/sampled) > config_cycle_acceptable_stale);
    +/* Insert 'new_node' after 'old_node' if 'after' is 1.
    + * Insert 'new_node' before 'old_node' if 'after' is 0.
    + * Note: 'new_node' is *always* uncompressed, so if we assign it to
    + *       head or tail, we do not need to uncompress it. */
    +REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
    +                                        quicklistNode *old_node,
    +                                        quicklistNode *new_node, int after) {
    +    if (after) {
    +        new_node->prev = old_node;
    +        if (old_node) {
    +            new_node->next = old_node->next;
    +            if (old_node->next)
    +                old_node->next->prev = new_node;
    +            old_node->next = new_node;
    +        }
    +        if (quicklist->tail == old_node)
    +            quicklist->tail = new_node;
    +    } else {
    +        new_node->next = old_node;
    +        if (old_node) {
    +            new_node->prev = old_node->prev;
    +            if (old_node->prev)
    +                old_node->prev->next = new_node;
    +            old_node->prev = new_node;
    +        }
    +        if (quicklist->head == old_node)
    +            quicklist->head = new_node;
    +    }
    +    /* If this insert creates the only element so far, initialize head/tail. */
    +    if (quicklist->len == 0) {
    +        quicklist->head = quicklist->tail = new_node;
         }
     
    -    elapsed = ustime()-start;
    -    server.stat_expire_cycle_time_used += elapsed;
    -    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
    +    if (old_node)
    +        quicklistCompress(quicklist, old_node);
     
    -    /* Update our estimate of keys existing but yet to be expired.
    -     * Running average with this sample accounting for 5%. */
    -    double current_perc;
    -    if (total_sampled) {
    -        current_perc = (double)total_expired/total_sampled;
    -    } else
    -        current_perc = 0;
    -    server.stat_expired_stale_perc = (current_perc*0.05)+
    -                                     (server.stat_expired_stale_perc*0.95);
    -}
    -

    执行定期清除分成两种类型,快和慢,分别由beforeSleepdatabasesCron调用,快版有两个限制,一个是执行时长由ACTIVE_EXPIRE_CYCLE_FAST_DURATION限制,另一个是执行间隔是 2 倍的ACTIVE_EXPIRE_CYCLE_FAST_DURATION,另外这还可以由配置的server.active_expire_effort参数来控制,默认是 1,最大是 10

    -
    onfig_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
    -                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort
    -

    然后会从一定数量的 db 中找出一定数量的带过期时间的 key(保存在 expires中),这里的数量是由

    -
    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
    -                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort
    -```                                 
    -控制,慢速的执行时长是
    -```C
    -config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
    -                                  2*effort
    -timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    -

    这里还有一个额外的退出条件,如果当前数据库的抽样结果已经达到我们所允许的过期 key 百分比,则下次不再处理当前 db,继续处理下个 db

    + quicklist->len++; +}
    +

    前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。

    ]]>
    Redis 数据结构 - 源码 C + 源码 Redis @@ -9710,10 +9172,37 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    - redis系列介绍八-淘汰策略 - /2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/ - LRU

    说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
    ,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
    当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
    redis 针对这个策略的改进做了一个实验,这里借用下图

    首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

    -
    typedef struct redisObject {
    +    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
    @@ -9721,521 +9210,568 @@ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    -

    对于 lru 策略来说,lru 字段记录的就是redisObj 的LRU time,
    redis 在访问数据时,都会调用lookupKey方法

    -
    /* Low level key lookup API, not actually called directly from commands
    - * implementations that should instead rely on lookupKeyRead(),
    - * lookupKeyWrite() and lookupKeyReadWithFlags(). */
    -robj *lookupKey(redisDb *db, robj *key, int flags) {
    -    dictEntry *de = dictFind(db->dict,key->ptr);
    -    if (de) {
    -        robj *val = dictGetVal(de);
    +} 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/19/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E5%9B%9B/ + 在 redis 中还有一类表型数据结构叫压缩表,ziplist,它的目的是替代链表,链表是个很容易理解的数据结构,双向链表有前后指针,有带头结点的有的不带,但是链表有个比较大的问题是相对于普通的数组,它的内存不连续,碎片化的存储,内存利用效率不高,而且指针寻址相对于直接使用偏移量的话,也有一定的效率劣势,当然这不是主要的原因,ziplist 设计的主要目的是让链表的内存使用更高效

    +
    +

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

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

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

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

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

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

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

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

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

    +]]>
    + + Redis + 数据结构 + C + 源码 + Redis + + + redis + 数据结构 + 源码 + +
    + + redis系列介绍七-过期策略 + /2020/04/12/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E4%B8%83/ + 这一篇不再是数据结构介绍了,大致的数据结构基本都介绍了,这一篇主要是查漏补缺,或者说讲一些重要且基本的概念,也可能是经常被忽略的,很多讲 redis 的系列文章可能都会忽略,学习 redis 的时候也会,因为觉得源码学习就是讲主要的数据结构和“算法”学习了就好了。
    redis 的主要应用就是拿来作为高性能的缓存,那么缓存一般有些啥需要注意的,首先是访问速度,如果取得跟数据库一样快,那就没什么存在的意义,第二个是缓存的字面意思,我只是为了让数据读取快一些,通常大部分的场景这个是需要更新过期的,这里就把我要讲的第一点引出来了(真累,

    +

    redis过期策略

    redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
    懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
    主要代码是

    +
    /* This function is called when we are going to perform some operation
    + * in a given key, but such key may be already logically expired even if
    + * it still exists in the database. The main way this function is called
    + * is via lookupKey*() family of functions.
    + *
    + * The behavior of the function depends on the replication role of the
    + * instance, because slave instances do not expire keys, they wait
    + * for DELs from the master for consistency matters. However even
    + * slaves will try to have a coherent return value for the function,
    + * so that read commands executed in the slave side will be able to
    + * behave like if the key is expired even if still present (because the
    + * master has yet to propagate the DEL).
    + *
    + * In masters as a side effect of finding a key which is expired, such
    + * key will be evicted from the database. Also this may trigger the
    + * propagation of a DEL/UNLINK command in AOF / replication stream.
    + *
    + * The return value of the function is 0 if the key is still valid,
    + * otherwise the function returns 1 if the key is expired. */
    +int expireIfNeeded(redisDb *db, robj *key) {
    +    if (!keyIsExpired(db,key)) return 0;
     
    -        /* Update the access time for the ageing algorithm.
    -         * Don't do it if we have a saving child, as this will trigger
    -         * a copy on write madness. */
    -        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
    -            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    -                // 这个是后面一节的内容
    -                updateLFU(val);
    -            } else {
    -                //  对于这个分支,访问时就会去更新 lru 值
    -                val->lru = LRU_CLOCK();
    -            }
    -        }
    -        return val;
    -    } else {
    -        return NULL;
    +    /* If we are running in the context of a slave, instead of
    +     * evicting the expired key from the database, we return ASAP:
    +     * the slave key expiration is controlled by the master that will
    +     * send us synthesized DEL operations for expired keys.
    +     *
    +     * Still we try to return the right information to the caller,
    +     * that is, 0 if we think the key should be still valid, 1 if
    +     * we think the key is expired at this time. */
    +    if (server.masterhost != NULL) return 1;
    +
    +    /* Delete the key */
    +    server.stat_expiredkeys++;
    +    propagateExpire(db,key,server.lazyfree_lazy_expire);
    +    notifyKeyspaceEvent(NOTIFY_EXPIRED,
    +        "expired",key,db->id);
    +    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
    +                                         dbSyncDelete(db,key);
    +}
    +
    +/* Check if the key is expired. */
    +int keyIsExpired(redisDb *db, robj *key) {
    +    mstime_t when = getExpire(db,key);
    +    mstime_t now;
    +
    +    if (when < 0) return 0; /* No expire for this key */
    +
    +    /* Don't expire anything while loading. It will be done later. */
    +    if (server.loading) return 0;
    +
    +    /* If we are in the context of a Lua script, we pretend that time is
    +     * blocked to when the Lua script started. This way a key can expire
    +     * only the first time it is accessed and not in the middle of the
    +     * script execution, making propagation to slaves / AOF consistent.
    +     * See issue #1525 on Github for more information. */
    +    if (server.lua_caller) {
    +        now = server.lua_time_start;
    +    }
    +    /* If we are in the middle of a command execution, we still want to use
    +     * a reference time that does not change: in that case we just use the
    +     * cached time, that we update before each call in the call() function.
    +     * This way we avoid that commands such as RPOPLPUSH or similar, that
    +     * may re-open the same key multiple times, can invalidate an already
    +     * open object in a next call, if the next call will see the key expired,
    +     * while the first did not. */
    +    else if (server.fixed_time_expire > 0) {
    +        now = server.mstime;
    +    }
    +    /* For the other cases, we want to use the most fresh time we have. */
    +    else {
    +        now = mstime();
         }
    +
    +    /* The key expired if the current (virtual or real) time is greater
    +     * than the expire time of the key. */
    +    return now > when;
     }
    -/* This function is used to obtain the current LRU clock.
    - * If the current resolution is lower than the frequency we refresh the
    - * LRU clock (as it should be in production servers) we return the
    - * precomputed value, otherwise we need to resort to a system call. */
    -unsigned int LRU_CLOCK(void) {
    -    unsigned int lruclock;
    -    if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
    -        // 如果服务器的频率server.hz大于 1 时就是用系统预设的 lruclock
    -        lruclock = server.lruclock;
    -    } else {
    -        lruclock = getLRUClock();
    +/* Return the expire time of the specified key, or -1 if no expire
    + * is associated with this key (i.e. the key is non volatile) */
    +long long getExpire(redisDb *db, robj *key) {
    +    dictEntry *de;
    +
    +    /* No expire? return ASAP */
    +    if (dictSize(db->expires) == 0 ||
    +       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    +
    +    /* The entry was found in the expire dict, this means it should also
    +     * be present in the main dict (safety check). */
    +    serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    +    return dictGetSignedIntegerVal(de);
    +}
    +

    这里有几点要注意的,第一是当惰性删除时会根据lazyfree_lazy_expire这个参数去判断是执行同步删除还是异步删除,另外一点是对于 slave,是不需要执行的,因为会在 master 过期时向 slave 发送 del 指令。
    光采用这个策略会有什么问题呢,假如一些key 一直未被访问,那这些 key 就不会过期了,导致一直被占用着内存,所以 redis 采取了懒汉式过期加定期过期策略,定期策略是怎么执行的呢

    +
    /* This function handles 'background' operations we are required to do
    + * incrementally in Redis databases, such as active key expiring, resizing,
    + * rehashing. */
    +void databasesCron(void) {
    +    /* Expire keys by random sampling. Not required for slaves
    +     * as master will synthesize DELs for us. */
    +    if (server.active_expire_enabled) {
    +        if (server.masterhost == NULL) {
    +            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
    +        } else {
    +            expireSlaveKeys();
    +        }
         }
    -    return lruclock;
    -}
    -/* Return the LRU clock, based on the clock resolution. This is a time
    - * in a reduced-bits format that can be used to set and check the
    - * object->lru field of redisObject structures. */
    -unsigned int getLRUClock(void) {
    -    return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
    -}
    -

    redis 处理命令是在这里processCommand

    -
    /* If this function gets called we already read a whole
    - * command, arguments are in the client argv/argc fields.
    - * processCommand() execute the command or prepare the
    - * server for a bulk read from the client.
    - *
    - * If C_OK is returned the client is still alive and valid and
    - * other operations can be performed by the caller. Otherwise
    - * if C_ERR is returned the client was destroyed (i.e. after QUIT). */
    -int processCommand(client *c) {
    -    moduleCallCommandFilters(c);
     
    -    
    +    /* Defrag keys gradually. */
    +    activeDefragCycle();
     
    -    /* Handle the maxmemory directive.
    -     *
    -     * Note that we do not want to reclaim memory if we are here re-entering
    -     * the event loop since there is a busy Lua script running in timeout
    -     * condition, to avoid mixing the propagation of scripts with the
    -     * propagation of DELs due to eviction. */
    -    if (server.maxmemory && !server.lua_timedout) {
    -        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
    -        /* freeMemoryIfNeeded may flush slave output buffers. This may result
    -         * into a slave, that may be the active client, to be freed. */
    -        if (server.current_client == NULL) return C_ERR;
    +    /* Perform hash tables rehashing if needed, but only if there are no
    +     * other processes saving the DB on disk. Otherwise rehashing is bad
    +     * as will cause a lot of copy-on-write of memory pages. */
    +    if (!hasActiveChildProcess()) {
    +        /* We use global counters so if we stop the computation at a given
    +         * DB we'll be able to start from the successive in the next
    +         * cron loop iteration. */
    +        static unsigned int resize_db = 0;
    +        static unsigned int rehash_db = 0;
    +        int dbs_per_call = CRON_DBS_PER_CALL;
    +        int j;
     
    -        /* It was impossible to free enough memory, and the command the client
    -         * is trying to execute is denied during OOM conditions or the client
    -         * is in MULTI/EXEC context? Error. */
    -        if (out_of_memory &&
    -            (c->cmd->flags & CMD_DENYOOM ||
    -             (c->flags & CLIENT_MULTI &&
    -              c->cmd->proc != execCommand &&
    -              c->cmd->proc != discardCommand)))
    -        {
    -            flagTransaction(c);
    -            addReply(c, shared.oomerr);
    -            return C_OK;
    +        /* Don't test more DBs than we have. */
    +        if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
    +
    +        /* Resize */
    +        for (j = 0; j < dbs_per_call; j++) {
    +            tryResizeHashTables(resize_db % server.dbnum);
    +            resize_db++;
    +        }
    +
    +        /* Rehash */
    +        if (server.activerehashing) {
    +            for (j = 0; j < dbs_per_call; j++) {
    +                int work_done = incrementallyRehash(rehash_db);
    +                if (work_done) {
    +                    /* If the function did some work, stop here, we'll do
    +                     * more at the next cron loop. */
    +                    break;
    +                } else {
    +                    /* If this db didn't need rehash, we'll try the next one. */
    +                    rehash_db++;
    +                    rehash_db %= server.dbnum;
    +                }
    +            }
             }
         }
    -}
    -

    这里只摘了部分,当需要清理内存时就会调用, 然后调用了freeMemoryIfNeededAndSafe

    -
    /* This is a wrapper for freeMemoryIfNeeded() that only really calls the
    - * function if right now there are the conditions to do so safely:
    +}
    +/* Try to expire a few timed out keys. The algorithm used is adaptive and
    + * will use few CPU cycles if there are few expiring keys, otherwise
    + * it will get more aggressive to avoid that too much memory is used by
    + * keys that can be removed from the keyspace.
      *
    - * - There must be no script in timeout condition.
    - * - Nor we are loading data right now.
    + * Every expire cycle tests multiple databases: the next call will start
    + * again from the next db, with the exception of exists for time limit: in that
    + * case we restart again from the last database we were processing. Anyway
    + * no more than CRON_DBS_PER_CALL databases are tested at every iteration.
      *
    - */
    -int freeMemoryIfNeededAndSafe(void) {
    -    if (server.lua_timedout || server.loading) return C_OK;
    -    return freeMemoryIfNeeded();
    -}
    -/* This function is periodically called to see if there is memory to free
    - * according to the current "maxmemory" settings. In case we are over the
    - * memory limit, the function will try to free some memory to return back
    - * under the limit.
    + * The function can perform more or less work, depending on the "type"
    + * argument. It can execute a "fast cycle" or a "slow cycle". The slow
    + * cycle is the main way we collect expired cycles: this happens with
    + * the "server.hz" frequency (usually 10 hertz).
      *
    - * The function returns C_OK if we are under the memory limit or if we
    - * were over the limit, but the attempt to free memory was successful.
    - * Otehrwise if we are over the memory limit, but not enough memory
    - * was freed to return back under the limit, the function returns C_ERR. */
    -int freeMemoryIfNeeded(void) {
    -    int keys_freed = 0;
    -    /* By default replicas should ignore maxmemory
    -     * and just be masters exact copies. */
    -    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
    + * However the slow cycle can exit for timeout, since it used too much time.
    + * For this reason the function is also invoked to perform a fast cycle
    + * at every event loop cycle, in the beforeSleep() function. The fast cycle
    + * will try to perform less work, but will do it much more often.
    + *
    + * The following are the details of the two expire cycles and their stop
    + * conditions:
    + *
    + * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
    + * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
    + * microseconds, and is not repeated again before the same amount of time.
    + * The cycle will also refuse to run at all if the latest slow cycle did not
    + * terminate because of a time limit condition.
    + *
    + * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
    + * executed, where the time limit is a percentage of the REDIS_HZ period
    + * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
    + * fast cycle, the check of every database is interrupted once the number
    + * of already expired keys in the database is estimated to be lower than
    + * a given percentage, in order to avoid doing too much work to gain too
    + * little memory.
    + *
    + * The configured expire "effort" will modify the baseline parameters in
    + * order to do more work in both the fast and slow expire cycles.
    + */
     
    -    size_t mem_reported, mem_tofree, mem_freed;
    -    mstime_t latency, eviction_latency;
    -    long long delta;
    -    int slaves = listLength(server.slaves);
    +#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
    +#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
    +#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
    +#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
    +                                                   we do extra efforts. */
    +void activeExpireCycle(int type) {
    +    /* Adjust the running parameters according to the configured expire
    +     * effort. The default effort is 1, and the maximum configurable effort
    +     * is 10. */
    +    unsigned long
    +    effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
    +    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
    +                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
    +    config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
    +                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
    +    config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
    +                                  2*effort,
    +    config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
    +                                    effort;
    +
    +    /* This function has some global state in order to continue the work
    +     * incrementally across calls. */
    +    static unsigned int current_db = 0; /* Last DB tested. */
    +    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    +    static long long last_fast_cycle = 0; /* When last fast cycle ran. */
    +
    +    int j, iteration = 0;
    +    int dbs_per_call = CRON_DBS_PER_CALL;
    +    long long start = ustime(), timelimit, elapsed;
     
         /* When clients are paused the dataset should be static not just from the
          * POV of clients not being able to write, but also from the POV of
          * expires and evictions of keys not being performed. */
    -    if (clientsArePaused()) return C_OK;
    -    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
    -        return C_OK;
    -
    -    mem_freed = 0;
    -
    -    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
    -        goto cant_free; /* We need to free memory, but policy forbids. */
    -
    -    latencyStartMonitor(latency);
    -    while (mem_freed < mem_tofree) {
    -        int j, k, i;
    -        static unsigned int next_db = 0;
    -        sds bestkey = NULL;
    -        int bestdbid;
    -        redisDb *db;
    -        dict *dict;
    -        dictEntry *de;
    -
    -        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
    -            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
    -        {
    -            struct evictionPoolEntry *pool = EvictionPoolLRU;
    -
    -            while(bestkey == NULL) {
    -                unsigned long total_keys = 0, keys;
    -
    -                /* We don't want to make local-db choices when expiring keys,
    -                 * so to start populate the eviction pool sampling keys from
    -                 * every DB. */
    -                for (i = 0; i < server.dbnum; i++) {
    -                    db = server.db+i;
    -                    dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
    -                            db->dict : db->expires;
    -                    if ((keys = dictSize(dict)) != 0) {
    -                        evictionPoolPopulate(i, dict, db->dict, pool);
    -                        total_keys += keys;
    -                    }
    -                }
    -                if (!total_keys) break; /* No keys to evict. */
    +    if (clientsArePaused()) return;
     
    -                /* Go backward from best to worst element to evict. */
    -                for (k = EVPOOL_SIZE-1; k >= 0; k--) {
    -                    if (pool[k].key == NULL) continue;
    -                    bestdbid = pool[k].dbid;
    +    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
    +        /* Don't start a fast cycle if the previous cycle did not exit
    +         * for time limit, unless the percentage of estimated stale keys is
    +         * too high. Also never repeat a fast cycle for the same period
    +         * as the fast cycle total duration itself. */
    +        if (!timelimit_exit &&
    +            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
    +            return;
     
    -                    if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
    -                        de = dictFind(server.db[pool[k].dbid].dict,
    -                            pool[k].key);
    -                    } else {
    -                        de = dictFind(server.db[pool[k].dbid].expires,
    -                            pool[k].key);
    -                    }
    +        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
    +            return;
     
    -                    /* Remove the entry from the pool. */
    -                    if (pool[k].key != pool[k].cached)
    -                        sdsfree(pool[k].key);
    -                    pool[k].key = NULL;
    -                    pool[k].idle = 0;
    +        last_fast_cycle = start;
    +    }
     
    -                    /* If the key exists, is our pick. Otherwise it is
    -                     * a ghost and we need to try the next element. */
    -                    if (de) {
    -                        bestkey = dictGetKey(de);
    -                        break;
    -                    } else {
    -                        /* Ghost... Iterate again. */
    -                    }
    -                }
    -            }
    -        }
    +    /* We usually should test CRON_DBS_PER_CALL per iteration, with
    +     * two exceptions:
    +     *
    +     * 1) Don't test more DBs than we have.
    +     * 2) If last time we hit the time limit, we want to scan all DBs
    +     * in this iteration, as there is work to do in some DB and we don't want
    +     * expired keys to use memory for too much time. */
    +    if (dbs_per_call > server.dbnum || timelimit_exit)
    +        dbs_per_call = server.dbnum;
     
    -        /* volatile-random and allkeys-random policy */
    -        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
    -                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
    -        {
    -            /* When evicting a random key, we try to evict a key for
    -             * each DB, so we use the static 'next_db' variable to
    -             * incrementally visit all DBs. */
    -            for (i = 0; i < server.dbnum; i++) {
    -                j = (++next_db) % server.dbnum;
    -                db = server.db+j;
    -                dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
    -                        db->dict : db->expires;
    -                if (dictSize(dict) != 0) {
    -                    de = dictGetRandomKey(dict);
    -                    bestkey = dictGetKey(de);
    -                    bestdbid = j;
    -                    break;
    -                }
    -            }
    -        }
    +    /* We can use at max 'config_cycle_slow_time_perc' percentage of CPU
    +     * time per iteration. Since this function gets called with a frequency of
    +     * server.hz times per second, the following is the max amount of
    +     * microseconds we can spend in this function. */
    +    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    +    timelimit_exit = 0;
    +    if (timelimit <= 0) timelimit = 1;
     
    -        /* Finally remove the selected key. */
    -        if (bestkey) {
    -            db = server.db+bestdbid;
    -            robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
    -            propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
    -            /* We compute the amount of memory freed by db*Delete() alone.
    -             * It is possible that actually the memory needed to propagate
    -             * the DEL in AOF and replication link is greater than the one
    -             * we are freeing removing the key, but we can't account for
    -             * that otherwise we would never exit the loop.
    -             *
    -             * AOF and Output buffer memory will be freed eventually so
    -             * we only care about memory used by the key space. */
    -            delta = (long long) zmalloc_used_memory();
    -            latencyStartMonitor(eviction_latency);
    -            if (server.lazyfree_lazy_eviction)
    -                dbAsyncDelete(db,keyobj);
    -            else
    -                dbSyncDelete(db,keyobj);
    -            latencyEndMonitor(eviction_latency);
    -            latencyAddSampleIfNeeded("eviction-del",eviction_latency);
    -            latencyRemoveNestedEvent(latency,eviction_latency);
    -            delta -= (long long) zmalloc_used_memory();
    -            mem_freed += delta;
    -            server.stat_evictedkeys++;
    -            notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
    -                keyobj, db->id);
    -            decrRefCount(keyobj);
    -            keys_freed++;
    +    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
    +        timelimit = config_cycle_fast_duration; /* in microseconds. */
     
    -            /* When the memory to free starts to be big enough, we may
    -             * start spending so much time here that is impossible to
    -             * deliver data to the slaves fast enough, so we force the
    -             * transmission here inside the loop. */
    -            if (slaves) flushSlavesOutputBuffers();
    +    /* Accumulate some global stats as we expire keys, to have some idea
    +     * about the number of keys that are already logically expired, but still
    +     * existing inside the database. */
    +    long total_sampled = 0;
    +    long total_expired = 0;
     
    -            /* Normally our stop condition is the ability to release
    -             * a fixed, pre-computed amount of memory. However when we
    -             * are deleting objects in another thread, it's better to
    -             * check, from time to time, if we already reached our target
    -             * memory, since the "mem_freed" amount is computed only
    -             * across the dbAsyncDelete() call, while the thread can
    -             * release the memory all the time. */
    -            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
    -                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
    -                    /* Let's satisfy our stop condition. */
    -                    mem_freed = mem_tofree;
    -                }
    -            }
    -        } else {
    -            latencyEndMonitor(latency);
    -            latencyAddSampleIfNeeded("eviction-cycle",latency);
    -            goto cant_free; /* nothing to free... */
    -        }
    -    }
    -    latencyEndMonitor(latency);
    -    latencyAddSampleIfNeeded("eviction-cycle",latency);
    -    return C_OK;
    +    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
    +        /* Expired and checked in a single loop. */
    +        unsigned long expired, sampled;
     
    -cant_free:
    -    /* We are here if we are not able to reclaim memory. There is only one
    -     * last thing we can try: check if the lazyfree thread has jobs in queue
    -     * and wait... */
    -    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
    -        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
    -            break;
    -        usleep(1000);
    -    }
    -    return C_ERR;
    -}
    -

    这里就是根据具体策略去淘汰 key,首先是要往 pool 更新 key,更新key 的方法是evictionPoolPopulate

    -
    void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    -    int j, k, count;
    -    dictEntry *samples[server.maxmemory_samples];
    +        redisDb *db = server.db+(current_db % server.dbnum);
     
    -    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
    -    for (j = 0; j < count; j++) {
    -        unsigned long long idle;
    -        sds key;
    -        robj *o;
    -        dictEntry *de;
    +        /* Increment the DB now so we are sure if we run out of time
    +         * in the current DB we'll restart from the next. This allows to
    +         * distribute the time evenly across DBs. */
    +        current_db++;
     
    -        de = samples[j];
    -        key = dictGetKey(de);
    +        /* Continue to expire if at the end of the cycle more than 25%
    +         * of the keys were expired. */
    +        do {
    +            unsigned long num, slots;
    +            long long now, ttl_sum;
    +            int ttl_samples;
    +            iteration++;
     
    -        /* If the dictionary we are sampling from is not the main
    -         * dictionary (but the expires one) we need to lookup the key
    -         * again in the key dictionary to obtain the value object. */
    -        if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
    -            if (sampledict != keydict) de = dictFind(keydict, key);
    -            o = dictGetVal(de);
    -        }
    +            /* If there is nothing to expire try next DB ASAP. */
    +            if ((num = dictSize(db->expires)) == 0) {
    +                db->avg_ttl = 0;
    +                break;
    +            }
    +            slots = dictSlots(db->expires);
    +            now = mstime();
     
    -        /* Calculate the idle time according to the policy. This is called
    -         * idle just because the code initially handled LRU, but is in fact
    -         * just a score where an higher score means better candidate. */
    -        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
    -            idle = estimateObjectIdleTime(o);
    -        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    -            /* When we use an LRU policy, we sort the keys by idle time
    -             * so that we expire keys starting from greater idle time.
    -             * However when the policy is an LFU one, we have a frequency
    -             * estimation, and we want to evict keys with lower frequency
    -             * first. So inside the pool we put objects using the inverted
    -             * frequency subtracting the actual frequency to the maximum
    -             * frequency of 255. */
    -            idle = 255-LFUDecrAndReturn(o);
    -        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
    -            /* In this case the sooner the expire the better. */
    -            idle = ULLONG_MAX - (long)dictGetVal(de);
    -        } else {
    -            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
    -        }
    +            /* When there are less than 1% filled slots, sampling the key
    +             * space is expensive, so stop here waiting for better times...
    +             * The dictionary will be resized asap. */
    +            if (num && slots > DICT_HT_INITIAL_SIZE &&
    +                (num*100/slots < 1)) break;
     
    -        /* Insert the element inside the pool.
    -         * First, find the first empty bucket or the first populated
    -         * bucket that has an idle time smaller than our idle time. */
    -        k = 0;
    -        while (k < EVPOOL_SIZE &&
    -               pool[k].key &&
    -               pool[k].idle < idle) k++;
    -        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
    -            /* Can't insert if the element is < the worst element we have
    -             * and there are no empty buckets. */
    -            continue;
    -        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
    -            /* Inserting into empty position. No setup needed before insert. */
    -        } else {
    -            /* Inserting in the middle. Now k points to the first element
    -             * greater than the element to insert.  */
    -            if (pool[EVPOOL_SIZE-1].key == NULL) {
    -                /* Free space on the right? Insert at k shifting
    -                 * all the elements from k to end to the right. */
    +            /* The main collection cycle. Sample random keys among keys
    +             * with an expire set, checking for expired ones. */
    +            expired = 0;
    +            sampled = 0;
    +            ttl_sum = 0;
    +            ttl_samples = 0;
    +
    +            if (num > config_keys_per_loop)
    +                num = config_keys_per_loop;
    +
    +            /* Here we access the low level representation of the hash table
    +             * for speed concerns: this makes this code coupled with dict.c,
    +             * but it hardly changed in ten years.
    +             *
    +             * Note that certain places of the hash table may be empty,
    +             * so we want also a stop condition about the number of
    +             * buckets that we scanned. However scanning for free buckets
    +             * is very fast: we are in the cache line scanning a sequential
    +             * array of NULL pointers, so we can scan a lot more buckets
    +             * than keys in the same time. */
    +            long max_buckets = num*20;
    +            long checked_buckets = 0;
     
    -                /* Save SDS before overwriting. */
    -                sds cached = pool[EVPOOL_SIZE-1].cached;
    -                memmove(pool+k+1,pool+k,
    -                    sizeof(pool[0])*(EVPOOL_SIZE-k-1));
    -                pool[k].cached = cached;
    -            } else {
    -                /* No free space on right? Insert at k-1 */
    -                k--;
    -                /* Shift all elements on the left of k (included) to the
    -                 * left, so we discard the element with smaller idle time. */
    -                sds cached = pool[0].cached; /* Save SDS before overwriting. */
    -                if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
    -                memmove(pool,pool+1,sizeof(pool[0])*k);
    -                pool[k].cached = cached;
    +            while (sampled < num && checked_buckets < max_buckets) {
    +                for (int table = 0; table < 2; table++) {
    +                    if (table == 1 && !dictIsRehashing(db->expires)) break;
    +
    +                    unsigned long idx = db->expires_cursor;
    +                    idx &= db->expires->ht[table].sizemask;
    +                    dictEntry *de = db->expires->ht[table].table[idx];
    +                    long long ttl;
    +
    +                    /* Scan the current bucket of the current table. */
    +                    checked_buckets++;
    +                    while(de) {
    +                        /* Get the next entry now since this entry may get
    +                         * deleted. */
    +                        dictEntry *e = de;
    +                        de = de->next;
    +
    +                        ttl = dictGetSignedIntegerVal(e)-now;
    +                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
    +                        if (ttl > 0) {
    +                            /* We want the average TTL of keys yet
    +                             * not expired. */
    +                            ttl_sum += ttl;
    +                            ttl_samples++;
    +                        }
    +                        sampled++;
    +                    }
    +                }
    +                db->expires_cursor++;
                 }
    -        }
    +            total_expired += expired;
    +            total_sampled += sampled;
     
    -        /* Try to reuse the cached SDS string allocated in the pool entry,
    -         * because allocating and deallocating this object is costly
    -         * (according to the profiler, not my fantasy. Remember:
    -         * premature optimizbla bla bla bla. */
    -        int klen = sdslen(key);
    -        if (klen > EVPOOL_CACHED_SDS_SIZE) {
    -            pool[k].key = sdsdup(key);
    -        } else {
    -            memcpy(pool[k].cached,key,klen+1);
    -            sdssetlen(pool[k].cached,klen);
    -            pool[k].key = pool[k].cached;
    -        }
    -        pool[k].idle = idle;
    -        pool[k].dbid = dbid;
    -    }
    -}
    -

    Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些键的空闲时间还大)就可以进poolpool更新之后,就淘汰pool中空闲时间最大的键。

    -

    estimateObjectIdleTime用来计算Redis对象的空闲时间:

    -
    /* Given an object returns the min number of milliseconds the object was never
    - * requested, using an approximated LRU algorithm. */
    -unsigned long long estimateObjectIdleTime(robj *o) {
    -    unsigned long long lruclock = LRU_CLOCK();
    -    if (lruclock >= o->lru) {
    -        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
    -    } else {
    -        return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
    -                    LRU_CLOCK_RESOLUTION;
    -    }
    -}
    -

    空闲时间第一种是 lurclock 大于对象的 lru,那么就是减一下乘以精度,因为 lruclock 有可能是已经预生成的,所以会可能走下面这个

    -

    LFU

    上面介绍了LRU 的算法,但是考虑一种场景

    -
    ~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
    -~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
    -~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
    -~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
    -

    可以发现,当采用 lru 的淘汰策略的时候,D 是最新的,会被认为是最值得保留的,但是事实上还不如 A 跟 B,然后 antirez 大神就想到了LFU (Least Frequently Used) 这个算法, 显然对于上面的四个 key 的访问频率,保留优先级应该是 B > A > C = D
    那要怎么来实现这个 LFU 算法呢,其实像LRU,理想的情况就是维护个链表,把最新访问的放到头上去,但是这个会影响访问速度,注意到前面代码的应该可以看到,redisObject 的 lru 字段其实是两用的,当策略是 LFU 时,这个字段就另作他用了,它的 24 位长度被分成两部分

    -
          16 bits      8 bits
    -+----------------+--------+
    -+ Last decr time | LOG_C  |
    -+----------------+--------+
    -

    前16位字段是最后一次递减时间,因此Redis知道 上一次计数器递减,后8位是 计数器 counter。
    LFU 的主体策略就是当这个 key 被访问的次数越多频率越高他就越容易被保留下来,并且是最近被访问的频率越高。这其实有两个事情要做,一个是在访问的时候增加计数值,在一定长时间不访问时进行衰减,所以这里用了两个值,前 16 位记录上一次衰减的时间,后 8 位记录具体的计数值。
    Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:

    -

    volatile-lfu:对有过期时间的key采用LFU淘汰策略
    allkeys-lfu:对全部key采用LFU淘汰策略
    还有2个配置可以调整LFU算法:

    -
    lfu-log-factor 10
    -lfu-decay-time 1
    -```  
    -`lfu-log-factor` 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
    +            /* Update the average TTL stats for this database. */
    +            if (ttl_samples) {
    +                long long avg_ttl = ttl_sum/ttl_samples;
     
    -`lfu-decay-time`是一个以分钟为单位的数值,可以调整counter的减少速度
    -这里有个问题是 8 位大小够计么,访问一次加 1 的话的确不够,不过大神就是大神,才不会这么简单的加一。往下看代码
    -```C
    -/* Low level key lookup API, not actually called directly from commands
    - * implementations that should instead rely on lookupKeyRead(),
    - * lookupKeyWrite() and lookupKeyReadWithFlags(). */
    -robj *lookupKey(redisDb *db, robj *key, int flags) {
    -    dictEntry *de = dictFind(db->dict,key->ptr);
    -    if (de) {
    -        robj *val = dictGetVal(de);
    +                /* Do a simple running average with a few samples.
    +                 * We just use the current estimate with a weight of 2%
    +                 * and the previous estimate with a weight of 98%. */
    +                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
    +                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
    +            }
     
    -        /* Update the access time for the ageing algorithm.
    -         * Don't do it if we have a saving child, as this will trigger
    -         * a copy on write madness. */
    -        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
    -            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    -                // 当淘汰策略是 LFU 时,就会调用这个updateLFU
    -                updateLFU(val);
    -            } else {
    -                val->lru = LRU_CLOCK();
    +            /* We can't block forever here even if there are many keys to
    +             * expire. So after a given amount of milliseconds return to the
    +             * caller waiting for the other active expire cycle. */
    +            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
    +                elapsed = ustime()-start;
    +                if (elapsed > timelimit) {
    +                    timelimit_exit = 1;
    +                    server.stat_expired_time_cap_reached_count++;
    +                    break;
    +                }
                 }
    -        }
    -        return val;
    -    } else {
    -        return NULL;
    +            /* We don't repeat the cycle for the current database if there are
    +             * an acceptable amount of stale keys (logically expired but yet
    +             * not reclained). */
    +        } while ((expired*100/sampled) > config_cycle_acceptable_stale);
         }
    -}
    -

    updateLFU 这个其实个入口,调用了两个重要的方法

    -
    /* Update LFU when an object is accessed.
    - * Firstly, decrement the counter if the decrement time is reached.
    - * Then logarithmically increment the counter, and update the access time. */
    -void updateLFU(robj *val) {
    -    unsigned long counter = LFUDecrAndReturn(val);
    -    counter = LFULogIncr(counter);
    -    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
    -}
    -

    首先来看看LFUDecrAndReturn,这个方法的作用是根据上一次衰减时间和系统配置的 lfu-decay-time 参数来确定需要将 counter 减去多少

    -
    /* If the object decrement time is reached decrement the LFU counter but
    - * do not update LFU fields of the object, we update the access time
    - * and counter in an explicit way when the object is really accessed.
    - * And we will times halve the counter according to the times of
    - * elapsed time than server.lfu_decay_time.
    - * Return the object frequency counter.
    - *
    - * This function is used in order to scan the dataset for the best object
    - * to fit: as we check for the candidate, we incrementally decrement the
    - * counter of the scanned objects if needed. */
    -unsigned long LFUDecrAndReturn(robj *o) {
    -    // 右移 8 位,拿到上次衰减时间
    -    unsigned long ldt = o->lru >> 8;
    -    // 对 255 做与操作,拿到 counter 值
    -    unsigned long counter = o->lru & 255;
    -    // 根据lfu_decay_time来算出过了多少个衰减周期
    -    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    -    if (num_periods)
    -        counter = (num_periods > counter) ? 0 : counter - num_periods;
    -    return counter;
    -}
    -

    然后是加,调用了LFULogIncr

    -
    /* Logarithmically increment a counter. The greater is the current counter value
    - * the less likely is that it gets really implemented. Saturate it at 255. */
    -uint8_t LFULogIncr(uint8_t counter) {
    -    // 最大值就是 255,到顶了就不加了
    -    if (counter == 255) return 255;
    -    // 生成个随机小数
    -    double r = (double)rand()/RAND_MAX;
    -    // 减去个基础值,LFU_INIT_VAL = 5,防止刚进来就被逐出
    -    double baseval = counter - LFU_INIT_VAL;
    -    // 如果是小于 0,
    -    if (baseval < 0) baseval = 0;
    -    // 如果 baseval 是 0,那么 p 就是 1了,后面 counter 直接加一,如果不是的话,得看系统参数lfu_log_factor,这个越大,除出来的 p 越小,那么 counter++的可能性也越小,这样子就把前面的疑问给解决了,不是直接+1 的
    -    double p = 1.0/(baseval*server.lfu_log_factor+1);
    -    if (r < p) counter++;
    -    return counter;
    -}
    -

    大概的变化速度可以参考

    -
    +--------+------------+------------+------------+------------+------------+
    -| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
    -+--------+------------+------------+------------+------------+------------+
    -| 0      | 104        | 255        | 255        | 255        | 255        |
    -+--------+------------+------------+------------+------------+------------+
    -| 1      | 18         | 49         | 255        | 255        | 255        |
    -+--------+------------+------------+------------+------------+------------+
    -| 10     | 10         | 18         | 142        | 255        | 255        |
    -+--------+------------+------------+------------+------------+------------+
    -| 100    | 8          | 11         | 49         | 143        | 255        |
    -+--------+------------+------------+------------+------------+------------+
    -

    简而言之就是 lfu_log_factor 越大变化的越慢

    -

    总结

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

    + + elapsed = ustime()-start; + server.stat_expire_cycle_time_used += elapsed; + latencyAddSampleIfNeeded("expire-cycle",elapsed/1000); + + /* Update our estimate of keys existing but yet to be expired. + * Running average with this sample accounting for 5%. */ + double current_perc; + if (total_sampled) { + current_perc = (double)total_expired/total_sampled; + } else + current_perc = 0; + server.stat_expired_stale_perc = (current_perc*0.05)+ + (server.stat_expired_stale_perc*0.95); +}
    +

    执行定期清除分成两种类型,快和慢,分别由beforeSleepdatabasesCron调用,快版有两个限制,一个是执行时长由ACTIVE_EXPIRE_CYCLE_FAST_DURATION限制,另一个是执行间隔是 2 倍的ACTIVE_EXPIRE_CYCLE_FAST_DURATION,另外这还可以由配置的server.active_expire_effort参数来控制,默认是 1,最大是 10

    +
    onfig_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
    +                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort
    +

    然后会从一定数量的 db 中找出一定数量的带过期时间的 key(保存在 expires中),这里的数量是由

    +
    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
    +                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort
    +```                                 
    +控制,慢速的执行时长是
    +```C
    +config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
    +                                  2*effort
    +timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    +

    这里还有一个额外的退出条件,如果当前数据库的抽样结果已经达到我们所允许的过期 key 百分比,则下次不再处理当前 db,继续处理下个 db

    +]]>
    + + 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 - 数据结构 - 源码 - C - Redis + redis redis - 数据结构 - 源码 + 淘汰策略 + 应用 + Evict
    @@ -10265,315 +9801,658 @@ uint8_t LFULogIncr(uint8_t counter) { # 100 only in environments where very low latency is required. hz 10
    -

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

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

    -
    fn main() {
    -    let mut s = String::from("hello world");
    +

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

    +]]> + + redis + + + redis + 应用 + 过期策略 + + + + 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) {
    +    s.push_str(", world");
    +}
    +


    这里可能就是具体版本在实现上的一个差异,我用的 rustc 是 1.44.0 版本
    其实上面的主要是由 rust 想要避免这类多重可变更导致的异常问题,总结下就是三个点

    +
      +
    • 两个或两个以上的指针同时同时访问同一空间
    • +
    • 其中至少有一个指针会想空间中写入数据
    • +
    • 没有同步数据访问的机制
      并且我们不能在拥有不可变引用的情况下创建可变引用
    • +
    +

    悬垂引用

    还有一点需要注意的就是悬垂引用

    +
    fn main() {
    +    let reference_to_nothing = dangle();
    +}
    +
    +fn dangle() -> &String {
    +    let s = String::from("hello");
    +    &s
    +}
    +

    这里可以看到其实在 dangle函数返回后,这里的 s 理论上就离开了作用域,但是由于返回了 s 的引用,在 main 函数中就会拿着这个引用,就会出现如下错误

    +

    总结

    最后总结下

    +
      +
    • 在任何一个段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
    • +
    • 引用总是有效的。
    • +
    +]]>
    + + 语言 + Rust + + + Rust + 所有权 + 内存分布 + 新语言 + 可变引用 + 不可变引用 + +
    + + redis系列介绍八-淘汰策略 + /2020/04/18/redis%E7%B3%BB%E5%88%97%E4%BB%8B%E7%BB%8D%E5%85%AB/ + LRU

    说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
    ,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
    当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
    redis 针对这个策略的改进做了一个实验,这里借用下图

    首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下

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

    对于 lru 策略来说,lru 字段记录的就是redisObj 的LRU time,
    redis 在访问数据时,都会调用lookupKey方法

    +
    /* Low level key lookup API, not actually called directly from commands
    + * implementations that should instead rely on lookupKeyRead(),
    + * lookupKeyWrite() and lookupKeyReadWithFlags(). */
    +robj *lookupKey(redisDb *db, robj *key, int flags) {
    +    dictEntry *de = dictFind(db->dict,key->ptr);
    +    if (de) {
    +        robj *val = dictGetVal(de);
    +
    +        /* Update the access time for the ageing algorithm.
    +         * Don't do it if we have a saving child, as this will trigger
    +         * a copy on write madness. */
    +        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
    +            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    +                // 这个是后面一节的内容
    +                updateLFU(val);
    +            } else {
    +                //  对于这个分支,访问时就会去更新 lru 值
    +                val->lru = LRU_CLOCK();
    +            }
    +        }
    +        return val;
    +    } else {
    +        return NULL;
    +    }
    +}
    +/* This function is used to obtain the current LRU clock.
    + * If the current resolution is lower than the frequency we refresh the
    + * LRU clock (as it should be in production servers) we return the
    + * precomputed value, otherwise we need to resort to a system call. */
    +unsigned int LRU_CLOCK(void) {
    +    unsigned int lruclock;
    +    if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
    +        // 如果服务器的频率server.hz大于 1 时就是用系统预设的 lruclock
    +        lruclock = server.lruclock;
    +    } else {
    +        lruclock = getLRUClock();
    +    }
    +    return lruclock;
    +}
    +/* Return the LRU clock, based on the clock resolution. This is a time
    + * in a reduced-bits format that can be used to set and check the
    + * object->lru field of redisObject structures. */
    +unsigned int getLRUClock(void) {
    +    return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
    +}
    +

    redis 处理命令是在这里processCommand

    +
    /* If this function gets called we already read a whole
    + * command, arguments are in the client argv/argc fields.
    + * processCommand() execute the command or prepare the
    + * server for a bulk read from the client.
    + *
    + * If C_OK is returned the client is still alive and valid and
    + * other operations can be performed by the caller. Otherwise
    + * if C_ERR is returned the client was destroyed (i.e. after QUIT). */
    +int processCommand(client *c) {
    +    moduleCallCommandFilters(c);
    +
    +    
    +
    +    /* Handle the maxmemory directive.
    +     *
    +     * Note that we do not want to reclaim memory if we are here re-entering
    +     * the event loop since there is a busy Lua script running in timeout
    +     * condition, to avoid mixing the propagation of scripts with the
    +     * propagation of DELs due to eviction. */
    +    if (server.maxmemory && !server.lua_timedout) {
    +        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
    +        /* freeMemoryIfNeeded may flush slave output buffers. This may result
    +         * into a slave, that may be the active client, to be freed. */
    +        if (server.current_client == NULL) return C_ERR;
    +
    +        /* It was impossible to free enough memory, and the command the client
    +         * is trying to execute is denied during OOM conditions or the client
    +         * is in MULTI/EXEC context? Error. */
    +        if (out_of_memory &&
    +            (c->cmd->flags & CMD_DENYOOM ||
    +             (c->flags & CLIENT_MULTI &&
    +              c->cmd->proc != execCommand &&
    +              c->cmd->proc != discardCommand)))
    +        {
    +            flagTransaction(c);
    +            addReply(c, shared.oomerr);
    +            return C_OK;
    +        }
    +    }
    +}
    +

    这里只摘了部分,当需要清理内存时就会调用, 然后调用了freeMemoryIfNeededAndSafe

    +
    /* This is a wrapper for freeMemoryIfNeeded() that only really calls the
    + * function if right now there are the conditions to do so safely:
    + *
    + * - There must be no script in timeout condition.
    + * - Nor we are loading data right now.
    + *
    + */
    +int freeMemoryIfNeededAndSafe(void) {
    +    if (server.lua_timedout || server.loading) return C_OK;
    +    return freeMemoryIfNeeded();
    +}
    +/* This function is periodically called to see if there is memory to free
    + * according to the current "maxmemory" settings. In case we are over the
    + * memory limit, the function will try to free some memory to return back
    + * under the limit.
    + *
    + * The function returns C_OK if we are under the memory limit or if we
    + * were over the limit, but the attempt to free memory was successful.
    + * Otehrwise if we are over the memory limit, but not enough memory
    + * was freed to return back under the limit, the function returns C_ERR. */
    +int freeMemoryIfNeeded(void) {
    +    int keys_freed = 0;
    +    /* By default replicas should ignore maxmemory
    +     * and just be masters exact copies. */
    +    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
    +
    +    size_t mem_reported, mem_tofree, mem_freed;
    +    mstime_t latency, eviction_latency;
    +    long long delta;
    +    int slaves = listLength(server.slaves);
     
    -    let word = first_word(&s);
    +    /* When clients are paused the dataset should be static not just from the
    +     * POV of clients not being able to write, but also from the POV of
    +     * expires and evictions of keys not being performed. */
    +    if (clientsArePaused()) return C_OK;
    +    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
    +        return C_OK;
     
    -    s.clear();
    +    mem_freed = 0;
     
    -    // 这时候虽然 word 还是 5,但是 s 已经被清除了,所以就没存在的意义
    -}
    -

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

    -
    let s = String::from("hello world")
    +    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
    +        goto cant_free; /* We need to free memory, but policy forbids. */
     
    -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();
    +    latencyStartMonitor(latency);
    +    while (mem_freed < mem_tofree) {
    +        int j, k, i;
    +        static unsigned int next_db = 0;
    +        sds bestkey = NULL;
    +        int bestdbid;
    +        redisDb *db;
    +        dict *dict;
    +        dictEntry *de;
     
    -    for (i, &item) in bytes.iter().enumerate() {
    -        if item == b' ' {
    -            return &s[0..i];
    -        }
    -    }
    +        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
    +            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
    +        {
    +            struct evictionPoolEntry *pool = EvictionPoolLRU;
     
    -    &s[..]
    -}
    -fn main() {
    -    let mut s = String::from("hello world");
    +            while(bestkey == NULL) {
    +                unsigned long total_keys = 0, keys;
     
    -    let word = first_word(&s);
    +                /* We don't want to make local-db choices when expiring keys,
    +                 * so to start populate the eviction pool sampling keys from
    +                 * every DB. */
    +                for (i = 0; i < server.dbnum; i++) {
    +                    db = server.db+i;
    +                    dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
    +                            db->dict : db->expires;
    +                    if ((keys = dictSize(dict)) != 0) {
    +                        evictionPoolPopulate(i, dict, db->dict, pool);
    +                        total_keys += keys;
    +                    }
    +                }
    +                if (!total_keys) break; /* No keys to evict. */
     
    -    s.clear(); // error!
    +                /* Go backward from best to worst element to evict. */
    +                for (k = EVPOOL_SIZE-1; k >= 0; k--) {
    +                    if (pool[k].key == NULL) continue;
    +                    bestdbid = pool[k].dbid;
     
    -    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/ - 最近在看 《rust 权威指南》,还是难度比较大的,它里面的一些概念跟之前的用过的都有比较大的差别
    比起有 gc 的虚拟机语言,跟像 C 和 C++这种主动释放内存的,rust 有他的独特点,主要是有三条

    -
      -
    • Rust中的每一个值都有一个对应的变量作为它的所有者。
    • -
    • 在同一时间内,值有且只有一个所有者。
    • -
    • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

      这里有两个重点:
    • -
    • s 在进入作用域后才变得有效
    • -
    • 它会保持自己的有效性直到自己离开作用域为止
    • -
    -

    然后看个案例

    -
    let x = 5;
    -let y = x;
    -

    这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
    但是对于非基础类型

    -
    let s1 = String::from("hello");
    -let s2 = s1;
    +                    if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
    +                        de = dictFind(server.db[pool[k].dbid].dict,
    +                            pool[k].key);
    +                    } else {
    +                        de = dictFind(server.db[pool[k].dbid].expires,
    +                            pool[k].key);
    +                    }
     
    -println!("{}, world!", s1);
    -

    有可能认为有两种内存分布可能
    先看下 string 的内存结构

    第一种可能是

    第二种是

    我们来尝试编译下

    发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了

    这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法

    -
    let s1 = String::from("hello");
    -let s2 = s1.clone();
    +                    /* Remove the entry from the pool. */
    +                    if (pool[k].key != pool[k].cached)
    +                        sdsfree(pool[k].key);
    +                    pool[k].key = NULL;
    +                    pool[k].idle = 0;
     
    -println!("s1 = {}, s2 = {}", s1, s2);
    -

    这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配

    -]]>
    - - 语言 - Rust - - - Rust - 所有权 - 内存分布 - 新语言 - -
    - - spark-little-tips - /2017/03/28/spark-little-tips/ - spark 的一些粗浅使用经验

    工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
    返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
    读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
    数据的流式计算,例如map reduce,在Spark中可以非常方便地实现

    -

    简单的mapreduce word count示例

    textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
    -data = textFile.reduceByKey(lambda x, y: x + y).collect()
    -for _ in data:
    -    print(_)
    + /* If the key exists, is our pick. Otherwise it is + * a ghost and we need to try the next element. */ + if (de) { + bestkey = dictGetKey(de); + break; + } else { + /* Ghost... Iterate again. */ + } + } + } + } + /* volatile-random and allkeys-random policy */ + else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || + server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM) + { + /* When evicting a random key, we try to evict a key for + * each DB, so we use the static 'next_db' variable to + * incrementally visit all DBs. */ + for (i = 0; i < server.dbnum; i++) { + j = (++next_db) % server.dbnum; + db = server.db+j; + dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ? + db->dict : db->expires; + if (dictSize(dict) != 0) { + de = dictGetRandomKey(dict); + bestkey = dictGetKey(de); + bestdbid = j; + break; + } + } + } -

    结果

    (3, 2)
    -(1, 2)
    -(4, 2)
    -(2, 2)
    -(5, 2)
    -]]>
    - - data analysis - - - spark - python - -
    - - spring event 介绍 - /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ - spring框架中如果想使用一些一部操作,除了依赖第三方中间件的消息队列,还可以用spring自己的event,简单介绍下使用方法
    首先我们可以建一个event,继承ApplicationEvent

    -
    
    -public class CustomSpringEvent extends ApplicationEvent {
    +        /* Finally remove the selected key. */
    +        if (bestkey) {
    +            db = server.db+bestdbid;
    +            robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
    +            propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
    +            /* We compute the amount of memory freed by db*Delete() alone.
    +             * It is possible that actually the memory needed to propagate
    +             * the DEL in AOF and replication link is greater than the one
    +             * we are freeing removing the key, but we can't account for
    +             * that otherwise we would never exit the loop.
    +             *
    +             * AOF and Output buffer memory will be freed eventually so
    +             * we only care about memory used by the key space. */
    +            delta = (long long) zmalloc_used_memory();
    +            latencyStartMonitor(eviction_latency);
    +            if (server.lazyfree_lazy_eviction)
    +                dbAsyncDelete(db,keyobj);
    +            else
    +                dbSyncDelete(db,keyobj);
    +            latencyEndMonitor(eviction_latency);
    +            latencyAddSampleIfNeeded("eviction-del",eviction_latency);
    +            latencyRemoveNestedEvent(latency,eviction_latency);
    +            delta -= (long long) zmalloc_used_memory();
    +            mem_freed += delta;
    +            server.stat_evictedkeys++;
    +            notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
    +                keyobj, db->id);
    +            decrRefCount(keyobj);
    +            keys_freed++;
     
    -    private String message;
    +            /* When the memory to free starts to be big enough, we may
    +             * start spending so much time here that is impossible to
    +             * deliver data to the slaves fast enough, so we force the
    +             * transmission here inside the loop. */
    +            if (slaves) flushSlavesOutputBuffers();
     
    -    public CustomSpringEvent(Object source, String message) {
    -        super(source);
    -        this.message = message;
    -    }
    -    public String getMessage() {
    -        return message;
    -    }
    -}
    -

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

    -
    public abstract class ApplicationEvent extends EventObject {
    -    private static final long serialVersionUID = 7099057708183571937L;
    -    private final long timestamp;
    +            /* Normally our stop condition is the ability to release
    +             * a fixed, pre-computed amount of memory. However when we
    +             * are deleting objects in another thread, it's better to
    +             * check, from time to time, if we already reached our target
    +             * memory, since the "mem_freed" amount is computed only
    +             * across the dbAsyncDelete() call, while the thread can
    +             * release the memory all the time. */
    +            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
    +                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
    +                    /* Let's satisfy our stop condition. */
    +                    mem_freed = mem_tofree;
    +                }
    +            }
    +        } else {
    +            latencyEndMonitor(latency);
    +            latencyAddSampleIfNeeded("eviction-cycle",latency);
    +            goto cant_free; /* nothing to free... */
    +        }
    +    }
    +    latencyEndMonitor(latency);
    +    latencyAddSampleIfNeeded("eviction-cycle",latency);
    +    return C_OK;
     
    -    public ApplicationEvent(Object source) {
    -        super(source);
    -        this.timestamp = System.currentTimeMillis();
    -    }
    +cant_free:
    +    /* We are here if we are not able to reclaim memory. There is only one
    +     * last thing we can try: check if the lazyfree thread has jobs in queue
    +     * and wait... */
    +    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
    +        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
    +            break;
    +        usleep(1000);
    +    }
    +    return C_ERR;
    +}
    +

    这里就是根据具体策略去淘汰 key,首先是要往 pool 更新 key,更新key 的方法是evictionPoolPopulate

    +
    void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    +    int j, k, count;
    +    dictEntry *samples[server.maxmemory_samples];
     
    -    public ApplicationEvent(Object source, Clock clock) {
    -        super(source);
    -        this.timestamp = clock.millis();
    -    }
    +    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
    +    for (j = 0; j < count; j++) {
    +        unsigned long long idle;
    +        sds key;
    +        robj *o;
    +        dictEntry *de;
     
    -    public final long getTimestamp() {
    -        return this.timestamp;
    -    }
    -}
    + de = samples[j]; + key = dictGetKey(de); -

    然后就是事件生产者和监听消费者

    -
    @Component
    -public class CustomSpringEventPublisher {
    +        /* If the dictionary we are sampling from is not the main
    +         * dictionary (but the expires one) we need to lookup the key
    +         * again in the key dictionary to obtain the value object. */
    +        if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
    +            if (sampledict != keydict) de = dictFind(keydict, key);
    +            o = dictGetVal(de);
    +        }
     
    -    @Resource
    -    private ApplicationEventPublisher applicationEventPublisher;
    +        /* Calculate the idle time according to the policy. This is called
    +         * idle just because the code initially handled LRU, but is in fact
    +         * just a score where an higher score means better candidate. */
    +        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
    +            idle = estimateObjectIdleTime(o);
    +        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    +            /* When we use an LRU policy, we sort the keys by idle time
    +             * so that we expire keys starting from greater idle time.
    +             * However when the policy is an LFU one, we have a frequency
    +             * estimation, and we want to evict keys with lower frequency
    +             * first. So inside the pool we put objects using the inverted
    +             * frequency subtracting the actual frequency to the maximum
    +             * frequency of 255. */
    +            idle = 255-LFUDecrAndReturn(o);
    +        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
    +            /* In this case the sooner the expire the better. */
    +            idle = ULLONG_MAX - (long)dictGetVal(de);
    +        } else {
    +            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
    +        }
     
    -    public void publishCustomEvent(final String message) {
    -        System.out.println("Publishing custom event. ");
    -        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
    -        applicationEventPublisher.publishEvent(customSpringEvent);
    -    }
    -}
    -

    这里的 ApplicationEventPublisher 是 Spring 的方法接口

    -
    @FunctionalInterface
    -public interface ApplicationEventPublisher {
    -    default void publishEvent(ApplicationEvent event) {
    -        this.publishEvent((Object)event);
    -    }
    +        /* Insert the element inside the pool.
    +         * First, find the first empty bucket or the first populated
    +         * bucket that has an idle time smaller than our idle time. */
    +        k = 0;
    +        while (k < EVPOOL_SIZE &&
    +               pool[k].key &&
    +               pool[k].idle < idle) k++;
    +        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
    +            /* Can't insert if the element is < the worst element we have
    +             * and there are no empty buckets. */
    +            continue;
    +        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
    +            /* Inserting into empty position. No setup needed before insert. */
    +        } else {
    +            /* Inserting in the middle. Now k points to the first element
    +             * greater than the element to insert.  */
    +            if (pool[EVPOOL_SIZE-1].key == NULL) {
    +                /* Free space on the right? Insert at k shifting
    +                 * all the elements from k to end to the right. */
     
    -    void publishEvent(Object var1);
    -}
    -

    具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲

    -

    事件监听者:

    -
    @Component
    -public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    -    @Override
    -    public void onApplicationEvent(CustomSpringEvent event) {
    -        System.out.println("Received spring custom event - " + event.getMessage());
    -    }
    -}
    -

    这里的也是 spring 的一个方法接口

    -
    @FunctionalInterface
    -public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    -    void onApplicationEvent(E var1);
    +                /* Save SDS before overwriting. */
    +                sds cached = pool[EVPOOL_SIZE-1].cached;
    +                memmove(pool+k+1,pool+k,
    +                    sizeof(pool[0])*(EVPOOL_SIZE-k-1));
    +                pool[k].cached = cached;
    +            } else {
    +                /* No free space on right? Insert at k-1 */
    +                k--;
    +                /* Shift all elements on the left of k (included) to the
    +                 * left, so we discard the element with smaller idle time. */
    +                sds cached = pool[0].cached; /* Save SDS before overwriting. */
    +                if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
    +                memmove(pool,pool+1,sizeof(pool[0])*k);
    +                pool[k].cached = cached;
    +            }
    +        }
     
    -    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
    -        return (event) -> {
    -            consumer.accept(event.getPayload());
    -        };
    -    }
    -}
    + /* Try to reuse the cached SDS string allocated in the pool entry, + * because allocating and deallocating this object is costly + * (according to the profiler, not my fantasy. Remember: + * premature optimizbla bla bla bla. */ + int klen = sdslen(key); + if (klen > EVPOOL_CACHED_SDS_SIZE) { + pool[k].key = sdsdup(key); + } else { + memcpy(pool[k].cached,key,klen+1); + sdssetlen(pool[k].cached,klen); + pool[k].key = pool[k].cached; + } + pool[k].idle = idle; + pool[k].dbid = dbid; + } +}
    +

    Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些键的空闲时间还大)就可以进poolpool更新之后,就淘汰pool中空闲时间最大的键。

    +

    estimateObjectIdleTime用来计算Redis对象的空闲时间:

    +
    /* Given an object returns the min number of milliseconds the object was never
    + * requested, using an approximated LRU algorithm. */
    +unsigned long long estimateObjectIdleTime(robj *o) {
    +    unsigned long long lruclock = LRU_CLOCK();
    +    if (lruclock >= o->lru) {
    +        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
    +    } else {
    +        return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
    +                    LRU_CLOCK_RESOLUTION;
    +    }
    +}
    +

    空闲时间第一种是 lurclock 大于对象的 lru,那么就是减一下乘以精度,因为 lruclock 有可能是已经预生成的,所以会可能走下面这个

    +

    LFU

    上面介绍了LRU 的算法,但是考虑一种场景

    +
    ~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
    +~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
    +~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
    +~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
    +

    可以发现,当采用 lru 的淘汰策略的时候,D 是最新的,会被认为是最值得保留的,但是事实上还不如 A 跟 B,然后 antirez 大神就想到了LFU (Least Frequently Used) 这个算法, 显然对于上面的四个 key 的访问频率,保留优先级应该是 B > A > C = D
    那要怎么来实现这个 LFU 算法呢,其实像LRU,理想的情况就是维护个链表,把最新访问的放到头上去,但是这个会影响访问速度,注意到前面代码的应该可以看到,redisObject 的 lru 字段其实是两用的,当策略是 LFU 时,这个字段就另作他用了,它的 24 位长度被分成两部分

    +
          16 bits      8 bits
    ++----------------+--------+
    ++ Last decr time | LOG_C  |
    ++----------------+--------+
    +

    前16位字段是最后一次递减时间,因此Redis知道 上一次计数器递减,后8位是 计数器 counter。
    LFU 的主体策略就是当这个 key 被访问的次数越多频率越高他就越容易被保留下来,并且是最近被访问的频率越高。这其实有两个事情要做,一个是在访问的时候增加计数值,在一定长时间不访问时进行衰减,所以这里用了两个值,前 16 位记录上一次衰减的时间,后 8 位记录具体的计数值。
    Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:

    +

    volatile-lfu:对有过期时间的key采用LFU淘汰策略
    allkeys-lfu:对全部key采用LFU淘汰策略
    还有2个配置可以调整LFU算法:

    +
    lfu-log-factor 10
    +lfu-decay-time 1
    +```  
    +`lfu-log-factor` 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
     
    -

    然后简单包个请求

    -
    
    -@RequestMapping(value = "/event", method = RequestMethod.GET)
    -@ResponseBody
    -public void event() {
    -    customSpringEventPublisher.publishCustomEvent("hello sprint event");
    -}
    +`lfu-decay-time`是一个以分钟为单位的数值,可以调整counter的减少速度 +这里有个问题是 8 位大小够计么,访问一次加 1 的话的确不够,不过大神就是大神,才不会这么简单的加一。往下看代码 +```C +/* Low level key lookup API, not actually called directly from commands + * implementations that should instead rely on lookupKeyRead(), + * lookupKeyWrite() and lookupKeyReadWithFlags(). */ +robj *lookupKey(redisDb *db, robj *key, int flags) { + dictEntry *de = dictFind(db->dict,key->ptr); + if (de) { + robj *val = dictGetVal(de); -


    就能看到接收到消息了。

    + /* Update the access time for the ageing algorithm. + * Don't do it if we have a saving child, as this will trigger + * a copy on write madness. */ + if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){ + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { + // 当淘汰策略是 LFU 时,就会调用这个updateLFU + updateLFU(val); + } else { + val->lru = LRU_CLOCK(); + } + } + return val; + } else { + return NULL; + } +}
    +

    updateLFU 这个其实个入口,调用了两个重要的方法

    +
    /* Update LFU when an object is accessed.
    + * Firstly, decrement the counter if the decrement time is reached.
    + * Then logarithmically increment the counter, and update the access time. */
    +void updateLFU(robj *val) {
    +    unsigned long counter = LFUDecrAndReturn(val);
    +    counter = LFULogIncr(counter);
    +    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
    +}
    +

    首先来看看LFUDecrAndReturn,这个方法的作用是根据上一次衰减时间和系统配置的 lfu-decay-time 参数来确定需要将 counter 减去多少

    +
    /* If the object decrement time is reached decrement the LFU counter but
    + * do not update LFU fields of the object, we update the access time
    + * and counter in an explicit way when the object is really accessed.
    + * And we will times halve the counter according to the times of
    + * elapsed time than server.lfu_decay_time.
    + * Return the object frequency counter.
    + *
    + * This function is used in order to scan the dataset for the best object
    + * to fit: as we check for the candidate, we incrementally decrement the
    + * counter of the scanned objects if needed. */
    +unsigned long LFUDecrAndReturn(robj *o) {
    +    // 右移 8 位,拿到上次衰减时间
    +    unsigned long ldt = o->lru >> 8;
    +    // 对 255 做与操作,拿到 counter 值
    +    unsigned long counter = o->lru & 255;
    +    // 根据lfu_decay_time来算出过了多少个衰减周期
    +    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    +    if (num_periods)
    +        counter = (num_periods > counter) ? 0 : counter - num_periods;
    +    return counter;
    +}
    +

    然后是加,调用了LFULogIncr

    +
    /* Logarithmically increment a counter. The greater is the current counter value
    + * the less likely is that it gets really implemented. Saturate it at 255. */
    +uint8_t LFULogIncr(uint8_t counter) {
    +    // 最大值就是 255,到顶了就不加了
    +    if (counter == 255) return 255;
    +    // 生成个随机小数
    +    double r = (double)rand()/RAND_MAX;
    +    // 减去个基础值,LFU_INIT_VAL = 5,防止刚进来就被逐出
    +    double baseval = counter - LFU_INIT_VAL;
    +    // 如果是小于 0,
    +    if (baseval < 0) baseval = 0;
    +    // 如果 baseval 是 0,那么 p 就是 1了,后面 counter 直接加一,如果不是的话,得看系统参数lfu_log_factor,这个越大,除出来的 p 越小,那么 counter++的可能性也越小,这样子就把前面的疑问给解决了,不是直接+1 的
    +    double p = 1.0/(baseval*server.lfu_log_factor+1);
    +    if (r < p) counter++;
    +    return counter;
    +}
    +

    大概的变化速度可以参考

    +
    +--------+------------+------------+------------+------------+------------+
    +| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
    ++--------+------------+------------+------------+------------+------------+
    +| 0      | 104        | 255        | 255        | 255        | 255        |
    ++--------+------------+------------+------------+------------+------------+
    +| 1      | 18         | 49         | 255        | 255        | 255        |
    ++--------+------------+------------+------------+------------+------------+
    +| 10     | 10         | 18         | 142        | 255        | 255        |
    ++--------+------------+------------+------------+------------+------------+
    +| 100    | 8          | 11         | 49         | 143        | 255        |
    ++--------+------------+------------+------------+------------+------------+
    +

    简而言之就是 lfu_log_factor 越大变化的越慢

    +

    总结

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

    ]]>
    - Java - Spring + Redis + 数据结构 + C + 源码 + Redis - Java - Spring - Spring Event + redis + 数据结构 + 源码
    - 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) {
    -    s.push_str(", world");
    -}
    -


    这里可能就是具体版本在实现上的一个差异,我用的 rustc 是 1.44.0 版本
    其实上面的主要是由 rust 想要避免这类多重可变更导致的异常问题,总结下就是三个点

    + rust学习笔记-所有权一 + /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 最近在看 《rust 权威指南》,还是难度比较大的,它里面的一些概念跟之前的用过的都有比较大的差别
    比起有 gc 的虚拟机语言,跟像 C 和 C++这种主动释放内存的,rust 有他的独特点,主要是有三条

      -
    • 两个或两个以上的指针同时同时访问同一空间
    • -
    • 其中至少有一个指针会想空间中写入数据
    • -
    • 没有同步数据访问的机制
      并且我们不能在拥有不可变引用的情况下创建可变引用
    • +
    • Rust中的每一个值都有一个对应的变量作为它的所有者。
    • +
    • 在同一时间内,值有且只有一个所有者。
    • +
    • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

      这里有两个重点:
    • +
    • s 在进入作用域后才变得有效
    • +
    • 它会保持自己的有效性直到自己离开作用域为止
    -

    悬垂引用

    还有一点需要注意的就是悬垂引用

    -
    fn main() {
    -    let reference_to_nothing = dangle();
    -}
    +

    然后看个案例

    +
    let x = 5;
    +let y = x;
    +

    这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
    但是对于非基础类型

    +
    let s1 = String::from("hello");
    +let s2 = s1;
     
    -fn dangle() -> &String {
    -    let s = String::from("hello");
    -    &s
    -}
    -

    这里可以看到其实在 dangle函数返回后,这里的 s 理论上就离开了作用域,但是由于返回了 s 的引用,在 main 函数中就会拿着这个引用,就会出现如下错误

    -

    总结

    最后总结下

    -
      -
    • 在任何一个段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
    • -
    • 引用总是有效的。
    • -
    +println!("{}, world!", s1);
    +

    有可能认为有两种内存分布可能
    先看下 string 的内存结构

    第一种可能是

    第二种是

    我们来尝试编译下

    发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了

    这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法

    +
    let s1 = String::from("hello");
    +let s2 = s1.clone();
    +
    +println!("s1 = {}, s2 = {}", s1, s2);
    +

    这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配

    ]]>
    语言 @@ -10584,163 +10463,194 @@ for _ in data: 所有权 内存分布 新语言 - 可变引用 - 不可变引用 - -
    - - summary-ranges-228 - /2016/10/12/summary-ranges-228/ - problem

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

    -

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

    -

    题解

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

    -

    Code

    class Solution {
    -public:
    -    vector<string> summaryRanges(vector<int>& nums) {
    -        int i = 0, j = 1, n;
    -        vector<string> res;
    -        n = nums.size();
    -        while(i < n){
    -            j = 1;
    -            while(j < n && nums[i+j] - nums[i] == j) j++;
    -            res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
    -            i += j;
    -        }
    -        return res;
    -    }
    -};
    ]]>
    - - leetcode - - - leetcode - c++ - -
    - - 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/ - 最近看了下这本垃圾回收算法手册,看到了第三章的标记-整理回收算法,做个简单的读书笔记

    -

    双指针整理算法

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

    -

    Lisp 2 整理算法

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

    -

    引线整理算法

    这个真的长见识了,

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

    -

    单次遍历算法

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

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

    -]]>
    - - Java - gc - jvm - - - java - gc - 标记整理 - 垃圾回收 - jvm
    - powershell 初体验 - /2022/11/13/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C/ - powershell变量

    变量命名类似于php

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

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

    -
    $tmp=$a
    -$a=$b
    -$b=$tmp
    -

    而在powershell中可以这样

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

    还可以通过这个

    -
    PS C:\Users\Nicks> ls variable:
    +    spark-little-tips
    +    /2017/03/28/spark-little-tips/
    +    spark 的一些粗浅使用经验

    工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
    返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
    读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
    数据的流式计算,例如map reduce,在Spark中可以非常方便地实现

    +

    简单的mapreduce word count示例

    textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
    +data = textFile.reduceByKey(lambda x, y: x + y).collect()
    +for _ in data:
    +    print(_)
    -Name Value ----- ----- -$ $b -? True -^ $b -a 2 -args {} -b 1
    -

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

    + +

    结果

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

    + spring event 介绍 + /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ + spring框架中如果想使用一些一部操作,除了依赖第三方中间件的消息队列,还可以用spring自己的event,简单介绍下使用方法
    首先我们可以建一个event,继承ApplicationEvent

    +
    
    +public class CustomSpringEvent extends ApplicationEvent {
    +
    +    private String message;
    +
    +    public CustomSpringEvent(Object source, String message) {
    +        super(source);
    +        this.message = message;
    +    }
    +    public String getMessage() {
    +        return message;
    +    }
    +}
    +

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

    +
    public abstract class ApplicationEvent extends EventObject {
    +    private static final long serialVersionUID = 7099057708183571937L;
    +    private final long timestamp;
    +
    +    public ApplicationEvent(Object source) {
    +        super(source);
    +        this.timestamp = System.currentTimeMillis();
    +    }
    +
    +    public ApplicationEvent(Object source, Clock clock) {
    +        super(source);
    +        this.timestamp = clock.millis();
    +    }
    +
    +    public final long getTimestamp() {
    +        return this.timestamp;
    +    }
    +}
    + +

    然后就是事件生产者和监听消费者

    +
    @Component
    +public class CustomSpringEventPublisher {
    +
    +    @Resource
    +    private ApplicationEventPublisher applicationEventPublisher;
    +
    +    public void publishCustomEvent(final String message) {
    +        System.out.println("Publishing custom event. ");
    +        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
    +        applicationEventPublisher.publishEvent(customSpringEvent);
    +    }
    +}
    +

    这里的 ApplicationEventPublisher 是 Spring 的方法接口

    +
    @FunctionalInterface
    +public interface ApplicationEventPublisher {
    +    default void publishEvent(ApplicationEvent event) {
    +        this.publishEvent((Object)event);
    +    }
    +
    +    void publishEvent(Object var1);
    +}
    +

    具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲

    +

    事件监听者:

    +
    @Component
    +public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    +    @Override
    +    public void onApplicationEvent(CustomSpringEvent event) {
    +        System.out.println("Received spring custom event - " + event.getMessage());
    +    }
    +}
    +

    这里的也是 spring 的一个方法接口

    +
    @FunctionalInterface
    +public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    +    void onApplicationEvent(E var1);
    +
    +    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
    +        return (event) -> {
    +            consumer.accept(event.getPayload());
    +        };
    +    }
    +}
    + +

    然后简单包个请求

    +
    
    +@RequestMapping(value = "/event", method = RequestMethod.GET)
    +@ResponseBody
    +public void event() {
    +    customSpringEventPublisher.publishCustomEvent("hello sprint event");
    +}
    + +


    就能看到接收到消息了。

    ]]>
    - 读后感 - 生活 + Java + Spring - 生活 - 读后感 + Java + Spring + Spring Event
    - 一个 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;
    -  }
    +    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");
     
    -  upstream nme {
    -    server 127.0.0.1:8000;
    -  }
    -  upstream ncom {
    -    server 127.0.0.1:8001;
    -  }
    +    let word = first_word(&s);
     
    -  server {
    -    listen 443 reuseport;
    -    proxy_pass $stream_map;
    -    ssl_preread on;
    -  }
    -}
    -

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

    -

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

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

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

    ]]>
    - nginx + 语言 + Rust - nginx + Rust + 所有权 + 内存分布 + 新语言 + 可变引用 + 不可变引用 + 切片
    @@ -10861,37 +10771,98 @@ user3: ![QK8EU5`9TQNYIG_4YFU@DJN.png">

    ]]> - php + php + + + websocket + 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/ + 最近看了下这本垃圾回收算法手册,看到了第三章的标记-整理回收算法,做个简单的读书笔记

    +

    双指针整理算法

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

    +

    Lisp 2 整理算法

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

    +

    引线整理算法

    这个真的长见识了,

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

    +

    单次遍历算法

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

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

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

    +]]>
    + + 读后感 + 生活 - websocket - swoole + 生活 + 读后感
    - powershell 初体验二 - /2022/11/20/powershell-%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BA%8C/ - powershell创建数组也很方便
    可以这样

    -
    $nums=2,0,1,2
    -

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

    -

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

    -
    $nums=1..5
    -


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

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


    还有判断类型可以用-is

    创建一个空数组

    -
    $array=@()
    -


    数组添加元素

    -
    $array+="a"
    -


    数组删除元素

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

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

    ]]>
    - 语言 + nginx - powershell + nginx
    @@ -10907,6 +10878,35 @@ user3: 生活 + + summary-ranges-228 + /2016/10/12/summary-ranges-228/ + problem

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

    +

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

    +

    题解

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

    +

    Code

    class Solution {
    +public:
    +    vector<string> summaryRanges(vector<int>& nums) {
    +        int i = 0, j = 1, n;
    +        vector<string> res;
    +        n = nums.size();
    +        while(i < n){
    +            j = 1;
    +            while(j < n && nums[i+j] - nums[i] == j) j++;
    +            res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
    +            i += j;
    +        }
    +        return res;
    +    }
    +};
    ]]>
    + + leetcode + + + leetcode + c++ + +
    介绍一下 RocketMQ /2020/06/21/%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B-RocketMQ/ @@ -10976,6 +10976,46 @@ user3: 中间件 + + 从丁仲礼被美国制裁聊点啥 + /2020/12/20/%E4%BB%8E%E4%B8%81%E4%BB%B2%E7%A4%BC%E8%A2%AB%E7%BE%8E%E5%9B%BD%E5%88%B6%E8%A3%81%E8%81%8A%E7%82%B9%E5%95%A5/ + 几年前看了柴静的《穹顶之下》觉得这个记者调查得很深入,挺有水平,然后再看到了她跟丁仲礼的采访,其实没看完整,也没试着去理解,就觉得环境问题挺严重的,为啥柴静这个对面的这位好像对这个很不屑的样子,最近因为丁仲礼上了美国制裁名单,B 站又有人把这个视频发了出来,就完整看了下,就觉得自己挺惭愧的,就抱着对柴静的好感而没来由的否定了丁老的看法和说法,所以人也需要不断地学习,改正之前错误的观点,当然不是说我现在说的就是百分百正确,只是个人的一些浅显的见解

    +

    先聊聊这个事情,整体看下来我的一些理解,IPCC给中国的方案其实是个很大的陷阱,它里面有几个隐藏的点是容易被我们外行忽略的,第一点是基数,首先发达国家目前(指2010年采访或者IPCC方案时间)的人均碳排放量已经是远高于发展中国家的了,这也就导致了所谓的发达国家承诺减排80%是个非常有诚意的承诺其实就是忽悠;第二点是碳排放是个累计过程,从1900年开始到2050年,或者说到2010年,发达国家已经排的量是远超过发展中国家的,这是非常不公平的;第三点其实是通过前两点推导出来的,也就是即使发达国家这么有诚意地说减排80%,扒开这层虚伪的外衣,其实是给他们11亿人分走了48%的碳排放量,留给发展中国家55亿人口的只剩下了52%;第四点,人是否因为国家的发达与否而应受到不平等待遇,如果按国家维度,丁老说的,摩纳哥要跟中国分同样的排放量么,中国人还算不算人;第五点,这点算是我自己想的,也可能是柴静屁股决定脑袋想不到的点,她作为一个物质生活条件已经足够好了,那么对于那些生活在物质条件平均线以下的,他们是否能像城里人那样有空调地暖,洗澡有热水器浴霸,上下班能开车,这些其实都直接或者间接地导致了碳排放;他们有没有改善物质生活条件地权利呢,并且再说回来,其实丁老也给了我们觉得合理地方案,我们保证不管发达国家不管减排多少,我们都不会超过他们的80%,我觉得这是真正的诚意,而不是接着减排80%的噱头来忽悠人,也是像丁老这样的专家才能看破这个陷阱,碳排放权其实就是发展权,就是人权,中国人就不是人了么,或者说站在贫困线以下的人民是否有改善物质条件的权利,而不是说像柴静这样,只是管她自己,可能觉得小孩因为空气污染导致身体不好,所以做了穹顶之下这个纪录片,想去改善这个事情,空气污染不是说对的,只是每个国家都有这个过程,如果不发展,哪里有资源去让人活得好,活得好了是前提,然后再去各方面都改善。

    +

    对于这个问题其实更想说的是人的认知偏差,之前总觉得美帝是更自由民主,公平啥的,或者说觉得美帝啥都好,有种无脑愤青的感觉,外国的月亮比较圆,但是经历了像川普当选美国总统以来的各种魔幻操作,还有对于疫情的种种不可思议的美国民众的反应,其实更让人明白第一是外国的月亮没比较圆,第二是事情总是没那么简单粗暴非黑即白,美国不像原先设想地那么领先优秀,但是的确有很多方面是全球领先的,天朝也有体制所带来的优势,不可妄自菲薄,也不能忙不自大,还是要多学习知识,提升认知水平。

    +]]>
    + + 生活 + 吐槽 + 疫情 + 美国 + + + 生活 + 吐槽 + 疫情 + 美国 + +
    + + 从清华美院学姐聊聊我们身边的恶人 + /2020/11/29/%E4%BB%8E%E6%B8%85%E5%8D%8E%E7%BE%8E%E9%99%A2%E5%AD%A6%E5%A7%90%E8%81%8A%E8%81%8A%E6%88%91%E4%BB%AC%E8%BA%AB%E8%BE%B9%E7%9A%84%E6%81%B6%E4%BA%BA/ + 前几天清华美院学姐的热点火了,然后仔细看了下,其实是个学姐诬陷以为其貌不扬的男同学摸她屁股

    然后还在朋友圈发文想让他社死,我也是挺晚才知道这个词什么意思,然后后面我看到了这个图片,挺有意思的

    本来其实也没什么想聊这个的,是在 B 站看了个吐槽这个的,然后刚好晚上乘公交的时候又碰到了有点类似的问题
    故事描述下,我们从始发站做了公交,这辆公交司机上次碰到过一回,就是会比较关注乘客的佩戴情况,主要考虑到目前国内疫情,然后这次在差不多人都坐满的情况下,可能在提示了三次让车内乘客戴好口罩,但是他指的那个中年女性还是没有反应,司机就转头比较大声指着这个乘客(中年女性)让戴好口罩,然后这个乘客(中年女性)就大声的说“我口罩是滑下来了,你指着我干嘛,你态度这么差,要吃了我一样,我要投诉你”等等,然后可能跟她一块的一个中年女性也是这么帮腔指责司机,比较基本的理解,车子里这么多乘客,假如是处于这位乘客口罩滑下来了而不自知的情况下,司机在提示了三次以后回头指着她说,我想的是没什么问题的,但是这位却反而指责这位司机指着她,并且说是态度差,要吃了她,完全是不可理喻的,并且一直喋喋不休说她口罩滑掉了有什么错,要投诉这个司机,让他可以提前退休了,在其他乘客的劝说下司机准备继续开车时,又口吐芬芳“你个傻,你来打我呀”,真的是让我再次体会到了所谓的恶人先告状的又一完美呈现,后面还有个乘客还是表示要打死司机这个傻,让我有点不明所以,俗话说有人是得理不饶人,前提是得理,这种理亏不饶人真的是挺让人长见识的,试想下,司机在提示三次后,这位乘客还是没有把口罩戴好,如何在不指着这位乘客的情况下能准确的提示到她呢,并且觉得语气态度不好,司机要载着一车的人,因为你这一个乘客不戴好口罩而不能正常出发,有些着急应该很正常吧,可能是平时自己在家里耀武扬威的使唤别人习惯了吧,别人不敢这么大声跟她说话,其实想想这位中年女性应该年纪不会很大,还比较时髦的吧,像一些常见的中年杭州本地人可能是不会说傻*这个词的吧。
    杭州的公交可能是在二月份疫情还比较严重的时候是要求上车出示健康码,后面比较缓和以后只要求佩戴好口罩,但是在我们小绍兴,目前还是一律要求检验健康码和佩戴口罩,对于疫情中,并且目前阶段国内也时有报出小范围的疫情的情况下,司机尽职要求佩戴好口罩其实也是为了乘客着想,另一种情况如果司机不严格要求,万一车上有个感染者,这位中年女性被传染了,如果能找到这个司机的话,是不是想“打死”这个司机,竟然让感染者上了车,反正她自己是不可能有错的,上来就是对方态度差,要投诉,自己不戴好口罩啥都没错,我就想知道如果因为自己没戴好口罩被感染了,是不是也是司机的错,毕竟没有像仆人那样点头哈腰求着她戴好口罩。
    再说回来,整个车就她一个人没戴好口罩,并且还有个细节,其实这个乘客是上了车之后就没戴好了,本来上车的时候是戴好的,这种比较有可能是觉得上车的时候司机那看一眼就好了,如果好好戴着口罩,一点事情都没有,唉,纯粹是太气愤了,调理逻辑什么的就忽略吧

    +]]>
    + + 生活 + 吐槽 + 疫情 + 口罩 + + + 生活 + 吐槽 + 疫情 + 公交车 + 口罩 + 杀人诛心 + +
    介绍下最近比较实用的端口转发 /2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ @@ -11023,39 +11063,6 @@ user3: 健康码 - - 从清华美院学姐聊聊我们身边的恶人 - /2020/11/29/%E4%BB%8E%E6%B8%85%E5%8D%8E%E7%BE%8E%E9%99%A2%E5%AD%A6%E5%A7%90%E8%81%8A%E8%81%8A%E6%88%91%E4%BB%AC%E8%BA%AB%E8%BE%B9%E7%9A%84%E6%81%B6%E4%BA%BA/ - 前几天清华美院学姐的热点火了,然后仔细看了下,其实是个学姐诬陷以为其貌不扬的男同学摸她屁股

    然后还在朋友圈发文想让他社死,我也是挺晚才知道这个词什么意思,然后后面我看到了这个图片,挺有意思的

    本来其实也没什么想聊这个的,是在 B 站看了个吐槽这个的,然后刚好晚上乘公交的时候又碰到了有点类似的问题
    故事描述下,我们从始发站做了公交,这辆公交司机上次碰到过一回,就是会比较关注乘客的佩戴情况,主要考虑到目前国内疫情,然后这次在差不多人都坐满的情况下,可能在提示了三次让车内乘客戴好口罩,但是他指的那个中年女性还是没有反应,司机就转头比较大声指着这个乘客(中年女性)让戴好口罩,然后这个乘客(中年女性)就大声的说“我口罩是滑下来了,你指着我干嘛,你态度这么差,要吃了我一样,我要投诉你”等等,然后可能跟她一块的一个中年女性也是这么帮腔指责司机,比较基本的理解,车子里这么多乘客,假如是处于这位乘客口罩滑下来了而不自知的情况下,司机在提示了三次以后回头指着她说,我想的是没什么问题的,但是这位却反而指责这位司机指着她,并且说是态度差,要吃了她,完全是不可理喻的,并且一直喋喋不休说她口罩滑掉了有什么错,要投诉这个司机,让他可以提前退休了,在其他乘客的劝说下司机准备继续开车时,又口吐芬芳“你个傻,你来打我呀”,真的是让我再次体会到了所谓的恶人先告状的又一完美呈现,后面还有个乘客还是表示要打死司机这个傻,让我有点不明所以,俗话说有人是得理不饶人,前提是得理,这种理亏不饶人真的是挺让人长见识的,试想下,司机在提示三次后,这位乘客还是没有把口罩戴好,如何在不指着这位乘客的情况下能准确的提示到她呢,并且觉得语气态度不好,司机要载着一车的人,因为你这一个乘客不戴好口罩而不能正常出发,有些着急应该很正常吧,可能是平时自己在家里耀武扬威的使唤别人习惯了吧,别人不敢这么大声跟她说话,其实想想这位中年女性应该年纪不会很大,还比较时髦的吧,像一些常见的中年杭州本地人可能是不会说傻*这个词的吧。
    杭州的公交可能是在二月份疫情还比较严重的时候是要求上车出示健康码,后面比较缓和以后只要求佩戴好口罩,但是在我们小绍兴,目前还是一律要求检验健康码和佩戴口罩,对于疫情中,并且目前阶段国内也时有报出小范围的疫情的情况下,司机尽职要求佩戴好口罩其实也是为了乘客着想,另一种情况如果司机不严格要求,万一车上有个感染者,这位中年女性被传染了,如果能找到这个司机的话,是不是想“打死”这个司机,竟然让感染者上了车,反正她自己是不可能有错的,上来就是对方态度差,要投诉,自己不戴好口罩啥都没错,我就想知道如果因为自己没戴好口罩被感染了,是不是也是司机的错,毕竟没有像仆人那样点头哈腰求着她戴好口罩。
    再说回来,整个车就她一个人没戴好口罩,并且还有个细节,其实这个乘客是上了车之后就没戴好了,本来上车的时候是戴好的,这种比较有可能是觉得上车的时候司机那看一眼就好了,如果好好戴着口罩,一点事情都没有,唉,纯粹是太气愤了,调理逻辑什么的就忽略吧

    -]]>
    - - 生活 - 吐槽 - 疫情 - 口罩 - - - 生活 - 吐槽 - 疫情 - 公交车 - 口罩 - 杀人诛心 - -
    - - 分享一次折腾老旧笔记本的体验-续篇 - /2023/02/12/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C-%E7%BB%AD%E7%AF%87/ - 接着上一篇的折腾记,因为这周又尝试了一些新的措施和方法,想继续记录分享下,上周的整体情况大概是 Ubuntu 系统能进去了,但是 Windows 进不去,PE 也进不去,Windows 启动盘也进不去,因为我的机器加过一个 msata 的固态,Windows 是装在 msata 固态硬盘里的,Ubuntu 是装在机械硬盘里的,所以有了一种猜测就是可能这个固态硬盘有点问题,还有就是还是怀疑内存的问题,正好家里还有个msata 的固态硬盘,是以前想给LD 的旧笔记本换上的,因为买回来放在那没有及时装,后面会又找不到,直到很后面才找到,LD 也不怎么用那个笔记本了,所以就一直放着了,这次我就想拿来换上。
    周末回家我就开始尝试了,换上了新的固态硬盘后,插上 Windows 启动 U 盘,这次一开始看起来有点顺利,在 BIOS 选择 U 盘启动,进入了 Windows 安装界面,但是装到一半,后面重启了之后就一直说硬盘有问题,让重启,但是重启并没有解决问题,变成了一直无效地重复重启,再想进 U 盘启动,就又进不去了,这时候怎么说呢,感觉硬盘不是没有问题,但是呢,问题应该不完全出在这,所以按着总体的逻辑来讲,主板带着cpu 跟显卡,都换掉了,硬盘也换掉了,剩下的就是内存了,可是内存我也尝试过把后面加的那条金士顿拔掉,可还是一样,也尝试过用橡皮擦金手指,这里感觉也很奇怪了,找了一圈了都感觉没啥明确的原因,比如其实我的猜测,主板电池的问题,一些电阻坏掉了,但是主板是换过的,那如果内存有问题,照理我用原装的那条应该会没问题,也有一种非常小的可能,就是两条内存都坏了,或者说这也是一种不太可能的可能,所以最后的办法就打算试试把两条内存都换掉,不过现在网上都找不到这个内存的确切信息了,只能根据大致的型号去买来试试,就怕买来的还是坏的,其实也怕是这个买来的主板因为也是别的拆机下来的,不一定保证完全没问题,要是有类似的问题或者也有别的问题导致开不起来就很尴尬,也没有很多专业的仪器可以排查原因,比如主板有没有什么短路的,对了还有一个就是电源问题,但是电源的问题也就可能是从充电器插的口子到主板的连线,因为 LD 的电源跟我的口子一样,也试过,但是结果还是一样,顺着正常逻辑排查,目前也没有剩下很明确的方向了,只能再尝试下看看。

    -]]>
    - - Windows - 小技巧 - - - Windows - -
    分享一次折腾老旧笔记本的体验 /2023/02/05/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C/ @@ -11069,26 +11076,6 @@ user3: Windows - - 从丁仲礼被美国制裁聊点啥 - /2020/12/20/%E4%BB%8E%E4%B8%81%E4%BB%B2%E7%A4%BC%E8%A2%AB%E7%BE%8E%E5%9B%BD%E5%88%B6%E8%A3%81%E8%81%8A%E7%82%B9%E5%95%A5/ - 几年前看了柴静的《穹顶之下》觉得这个记者调查得很深入,挺有水平,然后再看到了她跟丁仲礼的采访,其实没看完整,也没试着去理解,就觉得环境问题挺严重的,为啥柴静这个对面的这位好像对这个很不屑的样子,最近因为丁仲礼上了美国制裁名单,B 站又有人把这个视频发了出来,就完整看了下,就觉得自己挺惭愧的,就抱着对柴静的好感而没来由的否定了丁老的看法和说法,所以人也需要不断地学习,改正之前错误的观点,当然不是说我现在说的就是百分百正确,只是个人的一些浅显的见解

    -

    先聊聊这个事情,整体看下来我的一些理解,IPCC给中国的方案其实是个很大的陷阱,它里面有几个隐藏的点是容易被我们外行忽略的,第一点是基数,首先发达国家目前(指2010年采访或者IPCC方案时间)的人均碳排放量已经是远高于发展中国家的了,这也就导致了所谓的发达国家承诺减排80%是个非常有诚意的承诺其实就是忽悠;第二点是碳排放是个累计过程,从1900年开始到2050年,或者说到2010年,发达国家已经排的量是远超过发展中国家的,这是非常不公平的;第三点其实是通过前两点推导出来的,也就是即使发达国家这么有诚意地说减排80%,扒开这层虚伪的外衣,其实是给他们11亿人分走了48%的碳排放量,留给发展中国家55亿人口的只剩下了52%;第四点,人是否因为国家的发达与否而应受到不平等待遇,如果按国家维度,丁老说的,摩纳哥要跟中国分同样的排放量么,中国人还算不算人;第五点,这点算是我自己想的,也可能是柴静屁股决定脑袋想不到的点,她作为一个物质生活条件已经足够好了,那么对于那些生活在物质条件平均线以下的,他们是否能像城里人那样有空调地暖,洗澡有热水器浴霸,上下班能开车,这些其实都直接或者间接地导致了碳排放;他们有没有改善物质生活条件地权利呢,并且再说回来,其实丁老也给了我们觉得合理地方案,我们保证不管发达国家不管减排多少,我们都不会超过他们的80%,我觉得这是真正的诚意,而不是接着减排80%的噱头来忽悠人,也是像丁老这样的专家才能看破这个陷阱,碳排放权其实就是发展权,就是人权,中国人就不是人了么,或者说站在贫困线以下的人民是否有改善物质条件的权利,而不是说像柴静这样,只是管她自己,可能觉得小孩因为空气污染导致身体不好,所以做了穹顶之下这个纪录片,想去改善这个事情,空气污染不是说对的,只是每个国家都有这个过程,如果不发展,哪里有资源去让人活得好,活得好了是前提,然后再去各方面都改善。

    -

    对于这个问题其实更想说的是人的认知偏差,之前总觉得美帝是更自由民主,公平啥的,或者说觉得美帝啥都好,有种无脑愤青的感觉,外国的月亮比较圆,但是经历了像川普当选美国总统以来的各种魔幻操作,还有对于疫情的种种不可思议的美国民众的反应,其实更让人明白第一是外国的月亮没比较圆,第二是事情总是没那么简单粗暴非黑即白,美国不像原先设想地那么领先优秀,但是的确有很多方面是全球领先的,天朝也有体制所带来的优势,不可妄自菲薄,也不能忙不自大,还是要多学习知识,提升认知水平。

    -]]>
    - - 生活 - 吐槽 - 疫情 - 美国 - - - 生活 - 吐槽 - 疫情 - 美国 - -
    关于读书打卡与分享 /2021/02/07/%E5%85%B3%E4%BA%8E%E8%AF%BB%E4%B9%A6%E6%89%93%E5%8D%A1%E4%B8%8E%E5%88%86%E4%BA%AB/ @@ -11109,22 +11096,33 @@ user3: - 分享记录一下一个 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

    + 分享一次折腾老旧笔记本的体验-续篇 + /2023/02/12/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%8A%98%E8%85%BE%E8%80%81%E6%97%A7%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%9A%84%E4%BD%93%E9%AA%8C-%E7%BB%AD%E7%AF%87/ + 接着上一篇的折腾记,因为这周又尝试了一些新的措施和方法,想继续记录分享下,上周的整体情况大概是 Ubuntu 系统能进去了,但是 Windows 进不去,PE 也进不去,Windows 启动盘也进不去,因为我的机器加过一个 msata 的固态,Windows 是装在 msata 固态硬盘里的,Ubuntu 是装在机械硬盘里的,所以有了一种猜测就是可能这个固态硬盘有点问题,还有就是还是怀疑内存的问题,正好家里还有个msata 的固态硬盘,是以前想给LD 的旧笔记本换上的,因为买回来放在那没有及时装,后面会又找不到,直到很后面才找到,LD 也不怎么用那个笔记本了,所以就一直放着了,这次我就想拿来换上。
    周末回家我就开始尝试了,换上了新的固态硬盘后,插上 Windows 启动 U 盘,这次一开始看起来有点顺利,在 BIOS 选择 U 盘启动,进入了 Windows 安装界面,但是装到一半,后面重启了之后就一直说硬盘有问题,让重启,但是重启并没有解决问题,变成了一直无效地重复重启,再想进 U 盘启动,就又进不去了,这时候怎么说呢,感觉硬盘不是没有问题,但是呢,问题应该不完全出在这,所以按着总体的逻辑来讲,主板带着cpu 跟显卡,都换掉了,硬盘也换掉了,剩下的就是内存了,可是内存我也尝试过把后面加的那条金士顿拔掉,可还是一样,也尝试过用橡皮擦金手指,这里感觉也很奇怪了,找了一圈了都感觉没啥明确的原因,比如其实我的猜测,主板电池的问题,一些电阻坏掉了,但是主板是换过的,那如果内存有问题,照理我用原装的那条应该会没问题,也有一种非常小的可能,就是两条内存都坏了,或者说这也是一种不太可能的可能,所以最后的办法就打算试试把两条内存都换掉,不过现在网上都找不到这个内存的确切信息了,只能根据大致的型号去买来试试,就怕买来的还是坏的,其实也怕是这个买来的主板因为也是别的拆机下来的,不一定保证完全没问题,要是有类似的问题或者也有别的问题导致开不起来就很尴尬,也没有很多专业的仪器可以排查原因,比如主板有没有什么短路的,对了还有一个就是电源问题,但是电源的问题也就可能是从充电器插的口子到主板的连线,因为 LD 的电源跟我的口子一样,也试过,但是结果还是一样,顺着正常逻辑排查,目前也没有剩下很明确的方向了,只能再尝试下看看。

    ]]>
    - shell + Windows 小技巧 - scp + Windows + +
    + + 分享一次比较诡异的 Windows 下 U盘无法退出的经历 + /2023/01/29/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%AF%94%E8%BE%83%E8%AF%A1%E5%BC%82%E7%9A%84-Windows-%E4%B8%8B-U%E7%9B%98%E6%97%A0%E6%B3%95%E9%80%80%E5%87%BA%E7%9A%84%E7%BB%8F%E5%8E%86/ + 作为一个 Windows 的老用户,并且也算是 Windows 系统的半个粉丝,但是秉承一贯的优缺点都应该说的原则,Windows 系统有一点缺点是真的挺难受,相信 Windows 用过比较久的都会经历过,就是 U盘无法退出的问题,在比较远古时代,这个问题似乎能采取的措施不多,关机再拔 U盘的方式是一种比较保险的方式,其他貌似有 360这种可以解除占用的,但是需要安装 360 软件,对于目前的使用环境来说有点得不偿失,也是比较流氓的一类软件了,目前在 Windows 环境我主要就安装了个火绒,或者就用 Windows 自带的 defender。

    +

    第一种

    最近这次经历也是有火绒的一定责任,在我尝试推出 U盘的时候提示了我被另一个大流氓软件,XlibabaProtect.exe 占用了,这个流氓软件真的是充分展示了某里的技术实力,试过 N 多种办法都关不掉也删不掉,尝试了很多种办法也没办法删除,但是后面换了种思路,一般这种情况肯定是有进程在占用 U盘里的内容,最新版本的 Powertoys 会在文件的右键菜单里添加一个叫 File Locksmith 的功能,可以用于检查正在使用哪些文件以及由哪些进程使用,但是可能是我的使用姿势不对,没有仔细看文档,它里面有个”以管理员身份重启”,可能会有用。
    这算是第一种方式,

    +

    第二种

    第二种方式是 Windows 任务管理器中性能 tab 下的”打开资源监视器”,
    ,假如我的 U 盘的盘符是F:
    就可以搜索到占用这个盘符下文件的进程,这里千万小心‼️‼️,不可轻易杀掉这些进程,有些系统进程如果轻易杀掉会导致蓝屏等问题,不可轻易尝试,除非能确认这些进程的作用。
    对于前两种方式对我来说都无效,

    +

    第三种

    所以尝试了第三种,
    就是磁盘脱机的方式,在”计算机”右键管理,点击”磁盘管理”,可以找到 U 盘盘符右键,点击”脱机”,然后再”推出”,这个对我来说也不行

    +

    第四种

    这种是唯一对我有效的,在开始菜单搜索”event”,可以搜到”事件查看器”,
    ,这个可以看到当前最近 Windows 发生的事件,打开这个后就点击U盘推出,因为推不出来也是一种错误事件,点击下刷新就能在这看到具体是因为什么推出不了,具体的进程信息
    最后发现是英特尔的驱动管理程序的一个进程,关掉就退出了,虽然前面说的某里的进程是流氓,但这边是真的冤枉它了

    +]]>
    + + Windows + 小技巧 + + + Windows
    @@ -11165,26 +11163,6 @@ user3: 干活 - - 在老丈人家的小工记三 - /2020/09/13/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%B8%89/ - 小工记三

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

    -]]>
    - - 生活 - 运动 - 跑步 - 干活 - - - 生活 - 小技巧 - 运动 - 减肥 - 跑步 - 干活 - -
    在老丈人家的小工记五 /2020/10/18/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%BA%94/ @@ -11213,33 +11191,16 @@ user3: 生活 运动 - 跑步 - 干活 - - - 生活 - 小技巧 - 运动 - 减肥 - 跑步 - 干活 - - - - 分享一次比较诡异的 Windows 下 U盘无法退出的经历 - /2023/01/29/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E6%AF%94%E8%BE%83%E8%AF%A1%E5%BC%82%E7%9A%84-Windows-%E4%B8%8B-U%E7%9B%98%E6%97%A0%E6%B3%95%E9%80%80%E5%87%BA%E7%9A%84%E7%BB%8F%E5%8E%86/ - 作为一个 Windows 的老用户,并且也算是 Windows 系统的半个粉丝,但是秉承一贯的优缺点都应该说的原则,Windows 系统有一点缺点是真的挺难受,相信 Windows 用过比较久的都会经历过,就是 U盘无法退出的问题,在比较远古时代,这个问题似乎能采取的措施不多,关机再拔 U盘的方式是一种比较保险的方式,其他貌似有 360这种可以解除占用的,但是需要安装 360 软件,对于目前的使用环境来说有点得不偿失,也是比较流氓的一类软件了,目前在 Windows 环境我主要就安装了个火绒,或者就用 Windows 自带的 defender。

    -

    第一种

    最近这次经历也是有火绒的一定责任,在我尝试推出 U盘的时候提示了我被另一个大流氓软件,XlibabaProtect.exe 占用了,这个流氓软件真的是充分展示了某里的技术实力,试过 N 多种办法都关不掉也删不掉,尝试了很多种办法也没办法删除,但是后面换了种思路,一般这种情况肯定是有进程在占用 U盘里的内容,最新版本的 Powertoys 会在文件的右键菜单里添加一个叫 File Locksmith 的功能,可以用于检查正在使用哪些文件以及由哪些进程使用,但是可能是我的使用姿势不对,没有仔细看文档,它里面有个”以管理员身份重启”,可能会有用。
    这算是第一种方式,

    -

    第二种

    第二种方式是 Windows 任务管理器中性能 tab 下的”打开资源监视器”,
    ,假如我的 U 盘的盘符是F:
    就可以搜索到占用这个盘符下文件的进程,这里千万小心‼️‼️,不可轻易杀掉这些进程,有些系统进程如果轻易杀掉会导致蓝屏等问题,不可轻易尝试,除非能确认这些进程的作用。
    对于前两种方式对我来说都无效,

    -

    第三种

    所以尝试了第三种,
    就是磁盘脱机的方式,在”计算机”右键管理,点击”磁盘管理”,可以找到 U 盘盘符右键,点击”脱机”,然后再”推出”,这个对我来说也不行

    -

    第四种

    这种是唯一对我有效的,在开始菜单搜索”event”,可以搜到”事件查看器”,
    ,这个可以看到当前最近 Windows 发生的事件,打开这个后就点击U盘推出,因为推不出来也是一种错误事件,点击下刷新就能在这看到具体是因为什么推出不了,具体的进程信息
    最后发现是英特尔的驱动管理程序的一个进程,关掉就退出了,虽然前面说的某里的进程是流氓,但这边是真的冤枉它了

    -]]>
    - - Windows - 小技巧 + 跑步 + 干活 - Windows + 生活 + 小技巧 + 运动 + 减肥 + 跑步 + 干活
    @@ -11276,6 +11237,25 @@ user3: 囤物资 + + 分享记录一下一个 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 + 小技巧 + + + scp + +
    我是如何走上跑步这条不归路的 /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/ @@ -12806,45 +12786,256 @@ user3: return response; } - response.setCode(ResponseCode.TOPIC_NOT_EXIST); - response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic() - + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); - return response; - }
    + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic() + + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); + return response; + }
    + +

    首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteDataorg.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。

    +

    简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。

    +]]>
    + + MQ + RocketMQ + 消息队列 + RocketMQ + 中间件 + RocketMQ + + + MQ + 消息队列 + RocketMQ + 削峰填谷 + 中间件 + 源码解析 + NameServer + +
    + + 聊一下 RocketMQ 的消息存储之 MMAP + /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

    +
    read(file, tmp_buf, len);
    +write(socket, tmp_buf, len);
    + + + +

    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;
    +}
    +]]>
    + + MQ + RocketMQ + 消息队列 + + + MQ + 消息队列 + RocketMQ + +
    + + 在老丈人家的小工记三 + /2020/09/13/%E5%9C%A8%E8%80%81%E4%B8%88%E4%BA%BA%E5%AE%B6%E7%9A%84%E5%B0%8F%E5%B7%A5%E8%AE%B0%E4%B8%89/ + 小工记三

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

    +]]>
    + + 生活 + 运动 + 跑步 + 干活 + + + 生活 + 小技巧 + 运动 + 减肥 + 跑步 + 干活 + +
    + + 聊一下 RocketMQ 的消息存储三 + /2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/ + ConsumeQueue 其实是定位到一个 topic 下的消息在 CommitLog 下的偏移量,它也是固定大小的

    +
    // ConsumeQueue file size,default is 30W
    +private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
    +
    +public static final int CQ_STORE_UNIT_SIZE = 20;
    + +

    所以文件大小是5.7M 左右

    +

    5udpag

    +

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

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

    看一下 doReput 的逻辑

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

    首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteDataorg.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。

    -

    简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。

    +

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

    +

    AA6Tve

    +

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

    ]]>
    MQ RocketMQ 消息队列 - RocketMQ - 中间件 - RocketMQ MQ 消息队列 RocketMQ - 削峰填谷 - 中间件 - 源码解析 - NameServer - -
    - - 给小电驴上牌 - /2022/03/20/%E7%BB%99%E5%B0%8F%E7%94%B5%E9%A9%B4%E4%B8%8A%E7%89%8C/ - 三八节活动的时候下决心买了个小电驴,主要是上下班路上现在通勤条件越来越恶劣了,之前都是觉得坐公交就行了,实际路程就比较短,但是现在或者说大概是年前那两个月差不多就开始了,基本是堵一路,个人感觉是天目山路那边在修地铁,而且蚂蚁的几个空间都在那,上班的时间点都差不多,前一个修地铁感觉挺久了,机动车保有量也越来越多,总体是古墩路就越来越堵,还有个原因就是早上上班的点共享单车都被骑走了,有时候整整走一路都没一辆,有时候孤零零地有一辆基本都是破的;走路其实也是一种选择,但是因为要赶着上班,走得太慢就要很久,可能要 45 分钟这样,走得比较快就一身汗挺难受的。所以考虑自行车和电动车,这里还有一点就是不管是乘公交还是骑共享单车,其实都要从楼下走出去蛮远,公司回来也是,也就是这种通勤方式在准备阶段就花了比较多时间,比如总的从下班到到家的时间是半小时,可能在骑共享单车和公交车上的时间都不到十分钟,就比较难受。觉得这种比例太浪费时间,如果能有这种比较点对点的方式,估计能省时省力不少,前面说的骑共享单车的方式其实在之前是比较可行的,但是后来越来越少车,基本都是每周的前几天,周一到周三都是没有车,走路到公司再冷的天都是走出一身的汗,下雨天就更难受,本来下雨天应该是优先选择坐公交,但是一般下雨天堵车会更严重,而且车子到我上车的那个站,下雨天就挤得不行,总体说下来感觉事情都不打,但是几年下来,还是会挺不爽的。

    -

    电驴看的比较草率,主要是考虑续航,然后锂电池外加 48v 和 24AH,这样一般来讲还是价格比较高的,只是原来没预料到这个限速,以为现在的车子都比较快,但是现在的新国标车子都是 25km/h 的限速,然后 15km/h 都是会要提醒,虽然说有一些特殊的解除限速的方法,但是解了也就 35km/h ,差距不是特别大,而且现在的车子都是比较小,也不太能载东西,特别是上下班路程也不远的情况下,其实不是那么需要速度,就像我朋友说的,可能骑车的时间还不如等红绿灯多,所以就还好,也不打算解除限速,只是品牌上也仔细看,后来选了绿源,目前大部分还是雅迪,爱玛,台羚,绿源,小牛等,路上看的话还是雅迪比较多,不过价格也比较贵一点,还有就是小牛了,是比较新兴的品牌,手机 App 什么的做得比较好,而且也比较贵,最后以相对比较便宜的价格买了个锂电 48V24AH 的小车子,后来发现还是有点不方便的点就是没有比较大的筐,也不好装,这样就是下雨天雨衣什么的比较不方便放。

    -

    聊回来主题上牌这个事情,这个事情也是颇费心力,提车的时候店里的让我跟他早上一起去,但是因为不确定时间,也比较远就没跟着去,因为我是线上买的,线下自提,线下的店可能没啥利润可以拿,就不肯帮忙代上牌,朋友说在线下店里买是可以代上的,自己上牌过程也比较曲折,一开始是头盔没到,然后是等开发票,主要的东西就是需要骑着车子去车管所,不能只自己去,然后需要预约,附近比较近的都是提前一周就预约完了号了,要提前在支付宝上进行预约,比较空的就是店里推荐的景区大队,但是随之而来就是比较蛋疼的,这个景区大队太远了,看下骑车距离有十几公里,所以就有点拖延症,但是总归要上的,不然一直不能开是白买了,上牌的材料主要是车辆合格证,发票,然后车子上的浙品码,在车架上和电池上,然后车架号什么的都要跟合格证上完全对应,整体车子要跟合格证上一毛一样,如果有额外的反光镜,后面副座都需要拆掉,脚踏板要装上,到了那其实还比较顺利,就是十几公里外加那天比较冷,吹得头疼。

    -]]>
    - - 生活 - - - 生活
    @@ -13204,223 +13395,18 @@ user3: Thread.sleep(1); } catch (InterruptedException ignored) { } - } - } finally { - if (req != null && isSuccess) - // 通知前面等待的 - req.getCountDownLatch().countDown(); - } - return true; - }
    - - - - -]]>
    - - 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;
    -
    -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 的消息存储之 MMAP - /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ - 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,

    -
    read(file, tmp_buf, len);
    -write(socket, tmp_buf, len);
    - - - -

    vms95Z

    -

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

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

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

    -

    mmap

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

    -
    -

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

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

    I68mFx

    -
    -

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

    -

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

    -

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

    -

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

    -

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

    -

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

    -

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

    -
    if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
    -    perror("kernel lease set signal");
    -    return -1;
    -}
    -/* l_type can be F_RDLCK F_WRLCK */
    -if(fcntl(fd, F_SETLEASE, l_type)){
    -    perror("kernel lease set type");
    -    return -1;
    -}
    + } + } finally { + if (req != null && isSuccess) + // 通知前面等待的 + req.getCountDownLatch().countDown(); + } + return true; + }
    + + + + ]]> MQ @@ -13434,211 +13420,158 @@ user3: - 聊一下 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 {
    +    聊一下 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 {
     
         @Override
    -    protected Object determineCurrentLookupKey() {
    -        if (DatabaseContextHolder.getDatabaseType() != null) {
    -            return DatabaseContextHolder.getDatabaseType().getName();
    +    public void dispatch(DispatchRequest request) {
    +        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    +            DefaultMessageStore.this.indexService.buildIndex(request);
             }
    -        return DatabaseType.MASTER1.getName();
    -    }
    -}
    - -

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

    -

    -

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

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

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

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

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

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

    配置的数量

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

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

    -
    public interface StudentService {
    +

    最核心的其实是 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;
     
    -    @DataSource("master1")
    -    public Student queryOne();
    +            FileLock fileLock = null;
     
    -    @DataSource("master2")
    -    public Student queryAnother();
    +            try {
     
    -}
    + // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize, + // false); + int slotValue = this.mappedByteBuffer.getInt(absSlotPos); + if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) { + slotValue = invalidIndex; + } -

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

    -
    @Aspect
    -@Component
    -@Order(-1)
    -public class DataSourceAspect {
    +                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
     
    -    @Pointcut("execution(* com.nicksxs.springdemo.service..*.*(..))")
    -    public void pointCut() {
    +                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);
     
    -    @Before("pointCut()")
    -    public void before(JoinPoint point)
    -    {
    -        Object target = point.getTarget();
    -        System.out.println(target.toString());
    -        String method = point.getSignature().getName();
    -        System.out.println(method);
    -        Class<?>[] classz = target.getClass().getInterfaces();
    -        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
    -                .getMethod().getParameterTypes();
    -        try {
    -            Method m = classz[0].getMethod(method, parameterTypes);
    -            System.out.println("method"+ m.getName());
    -            if (m.isAnnotationPresent(DataSource.class)) {
    -                DataSource data = m.getAnnotation(DataSource.class);
    -                System.out.println("dataSource:"+data.value());
    -                DatabaseContextHolder.putDatabaseType(DatabaseType.getDatabaseType(data.value()));
    -            }
    +              // 存放的是数量位移,不是绝对位置
    +                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
     
    -        } catch (Exception e) {
    -            e.printStackTrace();
    -        }
    -    }
    +                if (this.indexHeader.getIndexCount() <= 1) {
    +                    this.indexHeader.setBeginPhyOffset(phyOffset);
    +                    this.indexHeader.setBeginTimestamp(storeTimestamp);
    +                }
     
    -    @After("pointCut()")
    -    public void after() {
    -				DatabaseContextHolder.clearDatabaseType();
    -    }
    -}
    + this.indexHeader.incHashSlotCount(); + this.indexHeader.incIndexCount(); + this.indexHeader.setEndPhyOffset(phyOffset); + this.indexHeader.setEndTimestamp(storeTimestamp); -

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

    -

    这是我 master1 的数据,

    -

    -

    master2 的数据

    -

    -

    然后跑一下简单的 demo,

    -
    @Override
    -public void run(String...args) {
    -	LOGGER.info("run here");
    -	System.out.println(studentService.queryOne());
    -	System.out.println(studentService.queryAnother());
    +                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; + }
    -

    看一下运行结果

    -

    -

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

    +

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

    ]]>
    - Java - SpringBoot + MQ + RocketMQ + 消息队列 - Java - Spring - SpringBoot - Druid - 数据源动态切换 + MQ + 消息队列 + RocketMQ
    @@ -13762,273 +13695,118 @@ user3: if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) { ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10); break; - } - // 获取消费消息。此处和并发消息请求不同,并发消息请求已经带了消费哪些消息。 - final int consumeBatchSize = - ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); - - List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize); - defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup()); - if (!msgs.isEmpty()) { - final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue); - - ConsumeOrderlyStatus status = null; - - ConsumeMessageContext consumeMessageContext = null; - if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { - consumeMessageContext = new ConsumeMessageContext(); - consumeMessageContext - .setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup()); - consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace()); - consumeMessageContext.setMq(messageQueue); - consumeMessageContext.setMsgList(msgs); - consumeMessageContext.setSuccess(false); - // init the consume context type - consumeMessageContext.setProps(new HashMap<String, String>()); - ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext); - } - // 执行消费 - long beginTimestamp = System.currentTimeMillis(); - ConsumeReturnType returnType = ConsumeReturnType.SUCCESS; - boolean hasException = false; - try { - this.processQueue.getLockConsume().lock(); // 锁定处理队列 - if (this.processQueue.isDropped()) { - log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}", - this.messageQueue); - break; - } - - status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context); - } catch (Throwable e) { - log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", - RemotingHelper.exceptionSimpleDesc(e), - ConsumeMessageOrderlyService.this.consumerGroup, - msgs, - messageQueue); - hasException = true; - } finally { - this.processQueue.getLockConsume().unlock(); // 解锁 - } - - if (null == status - || ConsumeOrderlyStatus.ROLLBACK == status - || ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) { - log.warn("consumeMessage Orderly return not OK, Group: {} Msgs: {} MQ: {}", - ConsumeMessageOrderlyService.this.consumerGroup, - msgs, - messageQueue); - } - - long consumeRT = System.currentTimeMillis() - beginTimestamp; - if (null == status) { - if (hasException) { - returnType = ConsumeReturnType.EXCEPTION; - } else { - returnType = ConsumeReturnType.RETURNNULL; - } - } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) { - returnType = ConsumeReturnType.TIME_OUT; - } else if (ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) { - returnType = ConsumeReturnType.FAILED; - } else if (ConsumeOrderlyStatus.SUCCESS == status) { - returnType = ConsumeReturnType.SUCCESS; - } - - if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { - consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name()); - } - - if (null == status) { - status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; - } - - if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { - consumeMessageContext.setStatus(status.toString()); - consumeMessageContext - .setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status); - ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); - } - - ConsumeMessageOrderlyService.this.getConsumerStatsManager() - .incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT); - - continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this); - } else { - continueConsume = false; - } - } - } else { - if (this.processQueue.isDropped()) { - log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue); - return; - } - - ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100); - } - } - }
    - -

    获取到锁对象后,使用synchronized尝试申请线程级独占锁。

    -

    如果加锁成功,同一时刻只有一个线程进行消息消费。

    -

    如果加锁失败,会延迟100ms重新尝试向broker端申请锁定messageQueue,锁定成功后重新提交消费请求

    -

    创建消息拉取任务时,消息客户端向broker端申请锁定MessageQueue,使得一个MessageQueue同一个时刻只能被一个消费客户端消费。

    -

    消息消费时,多线程针对同一个消息队列的消费先尝试使用synchronized申请独占锁,加锁成功才能进行消费,使得一个MessageQueue同一个时刻只能被一个消费客户端中一个线程消费。
    这里其实还有很重要的一点是对processQueue的加锁,这里其实是保证了在 rebalance的过程中如果 processQueue 被分配给了另一个 consumer,但是当前已经被我这个 consumer 再消费,但是没提交,就有可能出现被两个消费者消费,所以得进行加锁保证不受 rebalance 影响。

    -]]> - - MQ - RocketMQ - 消息队列 - - - MQ - 消息队列 - RocketMQ - - - - 聊一下 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 {
    -
    -    @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;
    -            }
    -
    -            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;
    +                        }
    +												// 获取消费消息。此处和并发消息请求不同,并发消息请求已经带了消费哪些消息。
    +                        final int consumeBatchSize =
    +                            ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
     
    -            FileLock fileLock = null;
    +                        List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
    +                        defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
    +                        if (!msgs.isEmpty()) {
    +                            final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
     
    -            try {
    +                            ConsumeOrderlyStatus status = null;
     
    -                // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
    -                // false);
    -                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    -                if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
    -                    slotValue = invalidIndex;
    -                }
    +                            ConsumeMessageContext consumeMessageContext = null;
    +                            if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
    +                                consumeMessageContext = new ConsumeMessageContext();
    +                                consumeMessageContext
    +                                    .setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup());
    +                                consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
    +                                consumeMessageContext.setMq(messageQueue);
    +                                consumeMessageContext.setMsgList(msgs);
    +                                consumeMessageContext.setSuccess(false);
    +                                // init the consume context type
    +                                consumeMessageContext.setProps(new HashMap<String, String>());
    +                                ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
    +                            }
    +														// 执行消费
    +                            long beginTimestamp = System.currentTimeMillis();
    +                            ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
    +                            boolean hasException = false;
    +                            try {
    +                                this.processQueue.getLockConsume().lock(); // 锁定处理队列
    +                                if (this.processQueue.isDropped()) {
    +                                    log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
    +                                        this.messageQueue);
    +                                    break;
    +                                }
     
    -                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
    +                                status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
    +                            } catch (Throwable e) {
    +                                log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
    +                                    RemotingHelper.exceptionSimpleDesc(e),
    +                                    ConsumeMessageOrderlyService.this.consumerGroup,
    +                                    msgs,
    +                                    messageQueue);
    +                                hasException = true;
    +                            } finally {
    +                                this.processQueue.getLockConsume().unlock();  // 解锁
    +                            }
     
    -                timeDiff = timeDiff / 1000;
    +                            if (null == status
    +                                || ConsumeOrderlyStatus.ROLLBACK == status
    +                                || ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {
    +                                log.warn("consumeMessage Orderly return not OK, Group: {} Msgs: {} MQ: {}",
    +                                    ConsumeMessageOrderlyService.this.consumerGroup,
    +                                    msgs,
    +                                    messageQueue);
    +                            }
     
    -                if (this.indexHeader.getBeginTimestamp() <= 0) {
    -                    timeDiff = 0;
    -                } else if (timeDiff > Integer.MAX_VALUE) {
    -                    timeDiff = Integer.MAX_VALUE;
    -                } else if (timeDiff < 0) {
    -                    timeDiff = 0;
    -                }
    +                            long consumeRT = System.currentTimeMillis() - beginTimestamp;
    +                            if (null == status) {
    +                                if (hasException) {
    +                                    returnType = ConsumeReturnType.EXCEPTION;
    +                                } else {
    +                                    returnType = ConsumeReturnType.RETURNNULL;
    +                                }
    +                            } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) {
    +                                returnType = ConsumeReturnType.TIME_OUT;
    +                            } else if (ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {
    +                                returnType = ConsumeReturnType.FAILED;
    +                            } else if (ConsumeOrderlyStatus.SUCCESS == status) {
    +                                returnType = ConsumeReturnType.SUCCESS;
    +                            }
     
    -              // 计算索引存放位置,头部 + 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);
    +                            if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
    +                                consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name());
    +                            }
     
    -              // 存放的是数量位移,不是绝对位置
    -                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    +                            if (null == status) {
    +                                status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
    +                            }
     
    -                if (this.indexHeader.getIndexCount() <= 1) {
    -                    this.indexHeader.setBeginPhyOffset(phyOffset);
    -                    this.indexHeader.setBeginTimestamp(storeTimestamp);
    -                }
    +                            if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
    +                                consumeMessageContext.setStatus(status.toString());
    +                                consumeMessageContext
    +                                    .setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status);
    +                                ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
    +                            }
     
    -                this.indexHeader.incHashSlotCount();
    -                this.indexHeader.incIndexCount();
    -                this.indexHeader.setEndPhyOffset(phyOffset);
    -                this.indexHeader.setEndTimestamp(storeTimestamp);
    +                            ConsumeMessageOrderlyService.this.getConsumerStatsManager()
    +                                .incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);
     
    -                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);
    +                            continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
    +                        } else {
    +                            continueConsume = false;
    +                        }
    +                    }
    +                } else {
    +                    if (this.processQueue.isDropped()) {
    +                        log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
    +                        return;
                         }
    +
    +                    ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
                     }
                 }
    -        } else {
    -            log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
    -                + "; index max num = " + this.indexNum);
    -        }
    -
    -        return false;
    -    }
    + }
    -

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

    +

    获取到锁对象后,使用synchronized尝试申请线程级独占锁。

    +

    如果加锁成功,同一时刻只有一个线程进行消息消费。

    +

    如果加锁失败,会延迟100ms重新尝试向broker端申请锁定messageQueue,锁定成功后重新提交消费请求

    +

    创建消息拉取任务时,消息客户端向broker端申请锁定MessageQueue,使得一个MessageQueue同一个时刻只能被一个消费客户端消费。

    +

    消息消费时,多线程针对同一个消息队列的消费先尝试使用synchronized申请独占锁,加锁成功才能进行消费,使得一个MessageQueue同一个时刻只能被一个消费客户端中一个线程消费。
    这里其实还有很重要的一点是对processQueue的加锁,这里其实是保证了在 rebalance的过程中如果 processQueue 被分配给了另一个 consumer,但是当前已经被我这个 consumer 再消费,但是没提交,就有可能出现被两个消费者消费,所以得进行加锁保证不受 rebalance 影响。

    ]]> MQ @@ -14224,240 +14002,314 @@ user3: - 聊一下 SpringBoot 设置非 web 应用的方法 - /2022/07/31/%E8%81%8A%E4%B8%80%E4%B8%8B-SpringBoot-%E8%AE%BE%E7%BD%AE%E9%9D%9E-web-%E5%BA%94%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95/ - 寻找原因

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

    -

    解决方案

    老版本 设置 spring 不带 web 功能

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

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

    新版本

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

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

    或者

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

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

    -
    public enum WebApplicationType {
    +    给小电驴上牌
    +    /2022/03/20/%E7%BB%99%E5%B0%8F%E7%94%B5%E9%A9%B4%E4%B8%8A%E7%89%8C/
    +    三八节活动的时候下决心买了个小电驴,主要是上下班路上现在通勤条件越来越恶劣了,之前都是觉得坐公交就行了,实际路程就比较短,但是现在或者说大概是年前那两个月差不多就开始了,基本是堵一路,个人感觉是天目山路那边在修地铁,而且蚂蚁的几个空间都在那,上班的时间点都差不多,前一个修地铁感觉挺久了,机动车保有量也越来越多,总体是古墩路就越来越堵,还有个原因就是早上上班的点共享单车都被骑走了,有时候整整走一路都没一辆,有时候孤零零地有一辆基本都是破的;走路其实也是一种选择,但是因为要赶着上班,走得太慢就要很久,可能要 45 分钟这样,走得比较快就一身汗挺难受的。所以考虑自行车和电动车,这里还有一点就是不管是乘公交还是骑共享单车,其实都要从楼下走出去蛮远,公司回来也是,也就是这种通勤方式在准备阶段就花了比较多时间,比如总的从下班到到家的时间是半小时,可能在骑共享单车和公交车上的时间都不到十分钟,就比较难受。觉得这种比例太浪费时间,如果能有这种比较点对点的方式,估计能省时省力不少,前面说的骑共享单车的方式其实在之前是比较可行的,但是后来越来越少车,基本都是每周的前几天,周一到周三都是没有车,走路到公司再冷的天都是走出一身的汗,下雨天就更难受,本来下雨天应该是优先选择坐公交,但是一般下雨天堵车会更严重,而且车子到我上车的那个站,下雨天就挤得不行,总体说下来感觉事情都不打,但是几年下来,还是会挺不爽的。

    +

    电驴看的比较草率,主要是考虑续航,然后锂电池外加 48v 和 24AH,这样一般来讲还是价格比较高的,只是原来没预料到这个限速,以为现在的车子都比较快,但是现在的新国标车子都是 25km/h 的限速,然后 15km/h 都是会要提醒,虽然说有一些特殊的解除限速的方法,但是解了也就 35km/h ,差距不是特别大,而且现在的车子都是比较小,也不太能载东西,特别是上下班路程也不远的情况下,其实不是那么需要速度,就像我朋友说的,可能骑车的时间还不如等红绿灯多,所以就还好,也不打算解除限速,只是品牌上也仔细看,后来选了绿源,目前大部分还是雅迪,爱玛,台羚,绿源,小牛等,路上看的话还是雅迪比较多,不过价格也比较贵一点,还有就是小牛了,是比较新兴的品牌,手机 App 什么的做得比较好,而且也比较贵,最后以相对比较便宜的价格买了个锂电 48V24AH 的小车子,后来发现还是有点不方便的点就是没有比较大的筐,也不好装,这样就是下雨天雨衣什么的比较不方便放。

    +

    聊回来主题上牌这个事情,这个事情也是颇费心力,提车的时候店里的让我跟他早上一起去,但是因为不确定时间,也比较远就没跟着去,因为我是线上买的,线下自提,线下的店可能没啥利润可以拿,就不肯帮忙代上牌,朋友说在线下店里买是可以代上的,自己上牌过程也比较曲折,一开始是头盔没到,然后是等开发票,主要的东西就是需要骑着车子去车管所,不能只自己去,然后需要预约,附近比较近的都是提前一周就预约完了号了,要提前在支付宝上进行预约,比较空的就是店里推荐的景区大队,但是随之而来就是比较蛋疼的,这个景区大队太远了,看下骑车距离有十几公里,所以就有点拖延症,但是总归要上的,不然一直不能开是白买了,上牌的材料主要是车辆合格证,发票,然后车子上的浙品码,在车架上和电池上,然后车架号什么的都要跟合格证上完全对应,整体车子要跟合格证上一毛一样,如果有额外的反光镜,后面副座都需要拆掉,脚踏板要装上,到了那其实还比较顺利,就是十几公里外加那天比较冷,吹得头疼。

    +]]>
    + + 生活 + + + 生活 + + + + 聊一下 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 {
     
    -	/**
    -	 * The application should not run as a web application and should not start an
    -	 * embedded web server.
    -	 */
    -	NONE,
    +    @Override
    +    protected Object determineCurrentLookupKey() {
    +        if (DatabaseContextHolder.getDatabaseType() != null) {
    +            return DatabaseContextHolder.getDatabaseType().getName();
    +        }
    +        return DatabaseType.MASTER1.getName();
    +    }
    +}
    + +

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

    +

    +

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

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

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

    +
    public class DatabaseContextHolder {
    +    public static final ThreadLocal<DatabaseType> databaseTypeThreadLocal = new ThreadLocal<>();
     
    -	/**
    -	 * The application should run as a servlet-based web application and should start an
    -	 * embedded servlet web server.
    -	 */
    -	SERVLET,
    +    public static DatabaseType getDatabaseType() {
    +        return databaseTypeThreadLocal.get();
    +    }
     
    -	/**
    -	 * The application should run as a reactive web application and should start an
    -	 * embedded reactive web server.
    -	 */
    -	REACTIVE
    +    public static void putDatabaseType(DatabaseType databaseType) {
    +        databaseTypeThreadLocal.set(databaseType);
    +    }
     
    -}
    -

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

    -]]>
    - - Java - SpringBoot - - - Java - Spring - SpringBoot - 自动装配 - AutoConfiguration - -
    - - 聊在东京奥运会闭幕式这天-二 - /2021/08/19/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9-%E4%BA%8C/ - 前面主要还是说了乒乓球的,因为整体还是乒乓球的比赛赛程比较长,比较激烈,扣人心弦,记得那会在公司没法看视频直播,就偶尔看看奥运会官网的比分,还几场马龙樊振东,陈梦被赢了一局就吓尿了,已经被混双那场留下了阴影,其实后面去看看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 static void clearDatabaseType() { + databaseTypeThreadLocal.remove(); + } +} +public enum DatabaseType { + MASTER1("master1", "1"), + MASTER2("master2", "2"); -

    就获取了自适应拓展,

    -
    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);
    -            }
    -        }
    +    private final String name;
    +    private final String value;
     
    -        return (T) instance;
    -    }
    + DatabaseType(String name, String value) { + this.name = name; + this.value = value; + } -

    这里也使用了 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);
    -        }
    +    public String getName() {
    +        return name;
         }
     
    -private Class<?> getAdaptiveExtensionClass() {
    -  			// 这里会获取拓展类,如果没有自适应的拓展类,那么就需要调用createAdaptiveExtensionClass
    -        getExtensionClasses();
    -        if (cachedAdaptiveClass != null) {
    -            return cachedAdaptiveClass;
    +    public String getValue() {
    +        return value;
    +    }
    +
    +    public static DatabaseType getDatabaseType(String name) {
    +        if (MASTER2.name.equals(name)) {
    +            return MASTER2;
             }
    -        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    +        return MASTER1;
         }
    -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;
    +

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

    +
    @Retention(RetentionPolicy.RUNTIME)
    +@Target(ElementType.METHOD)
    +public @interface DataSource {
    +    String value();
    +}
    -import com.alibaba.dubbo.common.extension.ExtensionLoader; +

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

    +
    public interface StudentService {
    +
    +    @DataSource("master1")
    +    public Student queryOne();
     
    +    @DataSource("master2")
    +    public Student queryAnother();
     
    -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!");
    -    }
    +}
    + +

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

    +
    @Aspect
    +@Component
    +@Order(-1)
    +public class DataSourceAspect {
    +
    +    @Pointcut("execution(* com.nicksxs.springdemo.service..*.*(..))")
    +    public void pointCut() {
     
    -    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");
    +    @Before("pointCut()")
    +    public void before(JoinPoint point)
    +    {
    +        Object target = point.getTarget();
    +        System.out.println(target.toString());
    +        String method = point.getSignature().getName();
    +        System.out.println(method);
    +        Class<?>[] classz = target.getClass().getInterfaces();
    +        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
    +                .getMethod().getParameterTypes();
    +        try {
    +            Method m = classz[0].getMethod(method, parameterTypes);
    +            System.out.println("method"+ m.getName());
    +            if (m.isAnnotationPresent(DataSource.class)) {
    +                DataSource data = m.getAnnotation(DataSource.class);
    +                System.out.println("dataSource:"+data.value());
    +                DatabaseContextHolder.putDatabaseType(DatabaseType.getDatabaseType(data.value()));
    +            }
    +
    +        } catch (Exception e) {
    +            e.printStackTrace();
             }
    +    }
     
    -        com.alibaba.dubbo.common.URL url = arg0.getUrl();
    -        String extName = ((url.getProtocol() == null) ? "dubbo"
    -                                                      : url.getProtocol());
    +    @After("pointCut()")
    +    public void after() {
    +				DatabaseContextHolder.clearDatabaseType();
    +    }
    +}
    - if (extName == null) { - throw new IllegalStateException( - "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + - url.toString() + ") use keys([protocol])"); - } +

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

    +

    这是我 master1 的数据,

    +

    +

    master2 的数据

    +

    +

    然后跑一下简单的 demo,

    +
    @Override
    +public void run(String...args) {
    +	LOGGER.info("run here");
    +	System.out.println(studentService.queryOne());
    +	System.out.println(studentService.queryAnother());
     
    -        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); - } +

    看一下运行结果

    +

    +

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

    +]]> + + Java + SpringBoot + + + Java + Spring + SpringBoot + Druid + 数据源动态切换 + + + + 聊一下 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 {
     
    -    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");
    -        }
    +	/**
    +	 * The application should not run as a web application and should not start an
    +	 * embedded web server.
    +	 */
    +	NONE,
     
    -        com.alibaba.dubbo.common.URL url = arg1;
    -      // 其实前面所说的逻辑就在这里呈现了
    -        String extName = ((url.getProtocol() == null) ? "dubbo"
    -                                                      : url.getProtocol());
    +	/**
    +	 * The application should run as a servlet-based web application and should start an
    +	 * embedded servlet web server.
    +	 */
    +	SERVLET,
     
    -        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);
    +	/**
    +	 * The application should run as a reactive web application and should start an
    +	 * embedded reactive web server.
    +	 */
    +	REACTIVE
     
    -        return extension.refer(arg0, arg1);
    -    }
    -}
    -
    +}
    +

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

    ]]>
    Java - Dubbo - RPC - SPI - Dubbo - SPI - Adaptive + SpringBoot Java - Dubbo - RPC - SPI - Adaptive - 自适应拓展 + Spring + SpringBoot + 自动装配 + AutoConfiguration + +
    + + 聊在东京奥运会闭幕式这天-二 + /2021/08/19/%E8%81%8A%E5%9C%A8%E4%B8%9C%E4%BA%AC%E5%A5%A5%E8%BF%90%E4%BC%9A%E9%97%AD%E5%B9%95%E5%BC%8F%E8%BF%99%E5%A4%A9-%E4%BA%8C/ + 前面主要还是说了乒乓球的,因为整体还是乒乓球的比赛赛程比较长,比较激烈,扣人心弦,记得那会在公司没法看视频直播,就偶尔看看奥运会官网的比分,还几场马龙樊振东,陈梦被赢了一局就吓尿了,已经被混双那场留下了阴影,其实后面去看看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 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。

    +

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

    +]]>
    + + 生活 + 运动 + + + 生活 + 运动 + 东京奥运会 + 乒乓球 + 跳水
    @@ -14534,426 +14386,323 @@ app.setWebAp */ 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 - - - - 聊聊 Dubbo 的容错机制 - /2020/11/22/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ - 之前看了 dubbo 的一些代码,在学习过程中,主要关注那些比较“高级”的内容,SPI,自适应扩展等,却忘了一些作为一个 rpc 框架最核心需要的部分,比如如何通信,序列化,网络,容错机制等等,因为其实这个最核心的就是远程调用,自适应扩展其实就是让代码可扩展性,可读性,更优雅等,写的搓一点其实也问题不大,但是一个合适的通信协议,序列化方法,如何容错等却是真正保证是一个 rpc 框架最重要的要素。
    首先来看这张图
    cluster
    在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
    各节点关系:

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

    集群容错模式

    Failover Cluster

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

    -

    重试次数配置如下:

    -

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

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

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

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

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

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

    + clazz.getConstructor(type); + return true; + } catch (NoSuchMethodException e) { + return false; + } + }
    +

    是否是 wrapperClass 其实就看构造函数的。

    ]]> Java - Dubbo - RPC Dubbo - 容错机制 + RPC + SPI + Dubbo + SPI Java Dubbo RPC - 容错机制 - - - - 聊一下关于怎么陪伴学习 - /2022/11/06/%E8%81%8A%E4%B8%80%E4%B8%8B%E5%85%B3%E4%BA%8E%E6%80%8E%E4%B9%88%E9%99%AA%E4%BC%B4%E5%AD%A6%E4%B9%A0/ - 这是一次开车过程中结合网上的一些微博想到的,开车是之前LD买了车后,陪领导练车,其实在一开始练车的时候,我们已经是找了相对很空的封闭路段,路上基本很少有车,偶尔有一辆车,但是LD还是很害怕,车速还只有十几的时候,还很远的对面来车的时候就觉得很慌了,这个时候如果以常理肯定会说这样子完全不用怕,如果克服恐惧真的这么容易的话,问题就不会那么纠结了,人生是很难完全感同身受的,唯有降低预设的基准让事情从头理清楚,害怕了我们就先休息,有车了我们就停下,先适应完全没车的情况,变得更慢一点,如果这时候着急一点,反而会起到反效果,比如只是说不要怕,接着开,甚至有点厌烦了,那基本这个练车也不太成得了了,而正好是有耐心的一起慢慢练习,还有就是第二件是切身体会,就是当道路本来是两条道,但是封了一条的时候,这时候开车如果是像我这样的新手,如果开车时左右边看着的话,车肯定开不好,因为那样会一直左右调整,反而更容易控制不好左右的距离,蹭到旁边的隔离栏,正确的方式应该是专注于正前方的路,这样才能保证左右边距离尽可能均匀,而不是顾左失右或者顾右失左,所以很多陪伴学习需要注意的是方式和耐心,能够识别到关键点那是最好的,但是有时候更需要的是耐心,纯靠耐心不一定能解决问题,但是可能会找到问题关键点。

    -]]>
    - - 生活 - - - 生活 + 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)
    + 聊聊 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();
    -
    enum { locked_value             = 0,
    -         unlocked_value           = 1,
    -         monitor_value            = 2,
    -         marked_value             = 3,
    -         biased_lock_pattern      = 5
    -};
    -
    +

    就获取了自适应拓展,

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

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

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

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

    这里也使用了 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);
    +        }
    +    }
     
    -

    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());
    +private Class<?> getAdaptiveExtensionClass() {
    +  			// 这里会获取拓展类,如果没有自适应的拓展类,那么就需要调用createAdaptiveExtensionClass
    +        getExtensionClasses();
    +        if (cachedAdaptiveClass != null) {
    +            return cachedAdaptiveClass;
             }
    -        synchronized (l) {
    -            System.out.println("2\n" + ClassLayout.parseInstance(l).toPrintable());
    +        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");
             }
    -		}
    -}
    -

    看下运行结果

    -

    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());
    +        if (arg0.getUrl() == null) {
    +            throw new IllegalArgumentException(
    +                "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
             }
    -				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());
    +        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])");
             }
    -        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();
    +
    +        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);
         }
    -}
     
    -class L {
    -    private boolean myboolean = true;
    -}
    + 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"); + } -

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

    -

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

    + 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 - Synchronized - 偏向锁 - 轻量级锁 - 重量级锁 - 自旋 + Dubbo + RPC + SPI + Adaptive + 自适应拓展
    - 聊聊 Java 的 equals 和 hashCode 方法 - /2021/01/03/%E8%81%8A%E8%81%8A-Java-%E7%9A%84-equals-%E5%92%8C-hashCode-%E6%96%B9%E6%B3%95/ - Java 中的这个话题也是比较常遇到的,关于这块原先也是比较忽略的,但是仔细想想又有点遗忘了就在这里记一下
    简单看下代码
    java.lang.Object#equals

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

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

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

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

    + 聊一下关于怎么陪伴学习 + /2022/11/06/%E8%81%8A%E4%B8%80%E4%B8%8B%E5%85%B3%E4%BA%8E%E6%80%8E%E4%B9%88%E9%99%AA%E4%BC%B4%E5%AD%A6%E4%B9%A0/ + 这是一次开车过程中结合网上的一些微博想到的,开车是之前LD买了车后,陪领导练车,其实在一开始练车的时候,我们已经是找了相对很空的封闭路段,路上基本很少有车,偶尔有一辆车,但是LD还是很害怕,车速还只有十几的时候,还很远的对面来车的时候就觉得很慌了,这个时候如果以常理肯定会说这样子完全不用怕,如果克服恐惧真的这么容易的话,问题就不会那么纠结了,人生是很难完全感同身受的,唯有降低预设的基准让事情从头理清楚,害怕了我们就先休息,有车了我们就停下,先适应完全没车的情况,变得更慢一点,如果这时候着急一点,反而会起到反效果,比如只是说不要怕,接着开,甚至有点厌烦了,那基本这个练车也不太成得了了,而正好是有耐心的一起慢慢练习,还有就是第二件是切身体会,就是当道路本来是两条道,但是封了一条的时候,这时候开车如果是像我这样的新手,如果开车时左右边看着的话,车肯定开不好,因为那样会一直左右调整,反而更容易控制不好左右的距离,蹭到旁边的隔离栏,正确的方式应该是专注于正前方的路,这样才能保证左右边距离尽可能均匀,而不是顾左失右或者顾右失左,所以很多陪伴学习需要注意的是方式和耐心,能够识别到关键点那是最好的,但是有时候更需要的是耐心,纯靠耐心不一定能解决问题,但是可能会找到问题关键点。

    ]]>
    - java + 生活 - 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的运行时状态中。

    + 聊聊 Dubbo 的容错机制 + /2020/11/22/%E8%81%8A%E8%81%8A-Dubbo-%E7%9A%84%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ + 之前看了 dubbo 的一些代码,在学习过程中,主要关注那些比较“高级”的内容,SPI,自适应扩展等,却忘了一些作为一个 rpc 框架最核心需要的部分,比如如何通信,序列化,网络,容错机制等等,因为其实这个最核心的就是远程调用,自适应扩展其实就是让代码可扩展性,可读性,更优雅等,写的搓一点其实也问题不大,但是一个合适的通信协议,序列化方法,如何容错等却是真正保证是一个 rpc 框架最重要的要素。
    首先来看这张图
    cluster
    在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
    各节点关系:

      -
    • 初始化

      类的初始化阶段是类加载过程的最后一个步骤,也是除了自定义类加载器之外将主动权交给了应用程序,其实就是执行类构造器()方法的过程,()并不是我们在 Java 代码中直接编写的方法,它是 Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的复制动作和静态句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问,同时还要保证父类的执行先于子类,然后保证多线程下的并发问题
    • +
    • 这里的 InvokerProvider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
    • +
    • Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
    • +
    • ClusterDirectory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
    • +
    • Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
    • +
    • LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
    -

    最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

    +

    集群容错模式

    Failover Cluster

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

    +

    重试次数配置如下:

    +

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

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

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

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

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

    +
    @Override
    +    @SuppressWarnings({"unchecked", "rawtypes"})
    +    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    +        List<Invoker<T>> copyInvokers = invokers;
    +        checkInvokers(copyInvokers, invocation);
    +        String methodName = RpcUtils.getMethodName(invocation);
    +        // 获取重试次数,这里默认是 2 次,还有可以注意下后面的+1
    +        int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
    +        if (len <= 0) {
    +            len = 1;
    +        }
    +        // retry loop.
    +        RpcException le = null; // last exception.
    +        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
    +        Set<String> providers = new HashSet<String>(len);
    +        // 循环调用,失败重试
    +        for (int i = 0; i < len; i++) {
    +            //Reselect before retry to avoid a change of candidate `invokers`.
    +            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
    +            if (i > 0) {
    +                checkWhetherDestroyed();
    +                // 在进行重试前重新列举 Invoker,这样做的好处是,如果某个服务挂了,
    +                // 通过调用 list 可得到最新可用的 Invoker 列表
    +                copyInvokers = list(invocation);
    +                // check again
    +                // 对 copyinvokers 进行判空检查
    +                checkInvokers(copyInvokers, invocation);
    +            }
    +            // 通过负载均衡来选择 invoker
    +            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
    +            // 将其添加到 invoker 到 invoked 列表中
    +            invoked.add(invoker);
    +            // 设置上下文
    +            RpcContext.getContext().setInvokers((List) invoked);
    +            try {
    +                // 正式调用
    +                Result result = invoker.invoke(invocation);
    +                if (le != null && logger.isWarnEnabled()) {
    +                    logger.warn("Although retry the method " + methodName
    +                            + " in the service " + getInterface().getName()
    +                            + " was successful by the provider " + invoker.getUrl().getAddress()
    +                            + ", 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 + 容错机制 +
    聊聊 Java 中绕不开的 Synchronized 关键字 @@ -15116,12 +14865,88 @@ public Result invoke(final Invocation invocation) throws RpcException { Java - Java - Synchronized - 偏向锁 - 轻量级锁 - 重量级锁 - 自旋 + Java + Synchronized + 偏向锁 + 轻量级锁 + 重量级锁 + 自旋 + + + + 聊聊 Java 的类加载机制一 + /2020/11/08/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/ + 一说到这个主题,想到的应该是双亲委派模型,不过讲的包括但不限于这个,主要内容是参考深入理解 Java 虚拟机书中的介绍,
    一个类型的生命周期包含了七个阶段,加载,验证,准备,解析,初始化,使用,卸载。

    +
      +
    • 加载

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

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

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

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

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

    +]]>
    + + java + + + java
    @@ -16622,131 +16447,6 @@ t2 Sharding-Jdbc - - 聊聊 Sharding-Jdbc 的简单原理初篇 - /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ - 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
    就是我在执行 query 的时候犯过一个比较难发现的错误,

    -
    ResultSet resultSet = ps.executeQuery(sql);
    -

    实际上应该是

    -
    ResultSet resultSet = ps.executeQuery();
    -

    而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

    这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
    那么在我上面的写错的代码里

    -
    @Override
    -public final ResultSet executeQuery(final String sql) throws SQLException {
    -    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
    -}
    -

    这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

    看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

    在前一篇的 demo 中的

    -
    Connection conn = dataSource.getConnection();
    -

    其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
    然后获得java.sql.PreparedStatement

    -
    PreparedStatement ps = conn.prepareStatement(sql)
    -

    就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
    然后就是执行

    -
    ResultSet resultSet = ps.executeQuery();
    -

    然后获得结果
    org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

    -

    其实像 mybatis 也是基于这样去实现的

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

    -
      -
    • FixedThreadPool:创建一个复用固定个数线程的线程池。
      简单看下代码
      public Executor getExecutor(URL url) {
      -        String name = url.getParameter("threadname", "Dubbo");
      -        int threads = url.getParameter("threads", 200);
      -        int queues = url.getParameter("queues", 0);
      -        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      -    }
      -可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
    • -
    • LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
      public Executor getExecutor(URL url) {
      -        String name = url.getParameter("threadname", "Dubbo");
      -        int cores = url.getParameter("corethreads", 0);
      -        int threads = url.getParameter("threads", 200);
      -        int queues = url.getParameter("queues", 0);
      -        return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      -    }
      -这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
    • -
    • EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
      public Executor getExecutor(URL url) {
      -        String name = url.getParameter("threadname", "Dubbo");
      -        int cores = url.getParameter("corethreads", 0);
      -        int threads = url.getParameter("threads", 2147483647);
      -        int queues = url.getParameter("queues", 0);
      -        int alive = url.getParameter("alive", 60000);
      -        TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
      -        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      -        taskQueue.setExecutor(executor);
      -        return executor;
      -    }
      -这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
    • -
    • CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
      public Executor getExecutor(URL url) {
      -        String name = url.getParameter("threadname", "Dubbo");
      -        int cores = url.getParameter("corethreads", 0);
      -        int threads = url.getParameter("threads", 2147483647);
      -        int queues = url.getParameter("queues", 0);
      -        int alive = url.getParameter("alive", 60000);
      -        return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      -    }
      -这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
      只是非常简略的介绍下,有兴趣可以自行阅读代码。
    • -
    -]]>
    - - Java - Dubbo - 线程池 - Dubbo - 线程池 - ThreadPool - - - Java - Dubbo - ThreadPool - 线程池 - FixedThreadPool - LimitedThreadPool - EagerThreadPool - CachedThreadPool - -
    - - 聊聊 mysql 的 MVCC 续续篇之锁分析 - /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/ - 看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
    看一段 mysql官方文档

    -
    -

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

    -
    -

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

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

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

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

    -]]>
    - - Mysql - C - 数据结构 - 源码 - Mysql - - - mysql - 数据结构 - 源码 - mvcc - read view - gap lock - next-key lock - 幻读 - -
    聊聊 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/ @@ -16849,7 +16549,97 @@ t2 Java - Sharding-Jdbc + Sharding-Jdbc + + + + 聊聊 Sharding-Jdbc 的简单原理初篇 + /2021/12/26/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E5%8E%9F%E7%90%86%E5%88%9D%E7%AF%87/ + 在上一篇 sharding-jdbc 的介绍中其实碰到过一个问题,这里也引出了一个比较有意思的话题
    就是我在执行 query 的时候犯过一个比较难发现的错误,

    +
    ResultSet resultSet = ps.executeQuery(sql);
    +

    实际上应该是

    +
    ResultSet resultSet = ps.executeQuery();
    +

    而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系

    这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
    那么在我上面的写错的代码里

    +
    @Override
    +public final ResultSet executeQuery(final String sql) throws SQLException {
    +    throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
    +}
    +

    这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础

    看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,

    在前一篇的 demo 中的

    +
    Connection conn = dataSource.getConnection();
    +

    其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
    然后获得java.sql.PreparedStatement

    +
    PreparedStatement ps = conn.prepareStatement(sql)
    +

    就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
    然后就是执行

    +
    ResultSet resultSet = ps.executeQuery();
    +

    然后获得结果
    org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet

    +

    其实像 mybatis 也是基于这样去实现的

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

    +
      +
    • FixedThreadPool:创建一个复用固定个数线程的线程池。
      简单看下代码
      public Executor getExecutor(URL url) {
      +        String name = url.getParameter("threadname", "Dubbo");
      +        int threads = url.getParameter("threads", 200);
      +        int queues = url.getParameter("queues", 0);
      +        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      +    }
      +可以看到核心线程数跟最大线程数一致,也就是说就不会在核心线程数和最大线程数之间动态变化了
    • +
    • LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在。
      public Executor getExecutor(URL url) {
      +        String name = url.getParameter("threadname", "Dubbo");
      +        int cores = url.getParameter("corethreads", 0);
      +        int threads = url.getParameter("threads", 200);
      +        int queues = url.getParameter("queues", 0);
      +        return new ThreadPoolExecutor(cores, threads, 9223372036854775807L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      +    }
      +这个特点主要是创建了保活时间特别长,即可以认为不会被回收了
    • +
    • EagerThreadPool :创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列。
      public Executor getExecutor(URL url) {
      +        String name = url.getParameter("threadname", "Dubbo");
      +        int cores = url.getParameter("corethreads", 0);
      +        int threads = url.getParameter("threads", 2147483647);
      +        int queues = url.getParameter("queues", 0);
      +        int alive = url.getParameter("alive", 60000);
      +        TaskQueue<Runnable> taskQueue = new TaskQueue(queues <= 0 ? 1 : queues);
      +        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, taskQueue, new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      +        taskQueue.setExecutor(executor);
      +        return executor;
      +    }
      +这个是改动最多的一个了,因为需要实现这个机制,有兴趣的可以详细看下
    • +
    • CachedThreadPool: 创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程
      public Executor getExecutor(URL url) {
      +        String name = url.getParameter("threadname", "Dubbo");
      +        int cores = url.getParameter("corethreads", 0);
      +        int threads = url.getParameter("threads", 2147483647);
      +        int queues = url.getParameter("queues", 0);
      +        int alive = url.getParameter("alive", 60000);
      +        return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
      +    }
      +这里可以看到线程池的配置,核心是 0,最大线程数是 2147483647,保活时间是一分钟
      只是非常简略的介绍下,有兴趣可以自行阅读代码。
    • +
    +]]>
    + + Java + Dubbo - 线程池 + Dubbo + 线程池 + ThreadPool + + + Java + Dubbo + ThreadPool + 线程池 + FixedThreadPool + LimitedThreadPool + EagerThreadPool + CachedThreadPool
    @@ -16892,6 +16682,121 @@ void ReadView::prepare(trx_id_t id) { 幻读 + + 聊聊 mysql 的 MVCC 续续篇之锁分析 + /2020/05/10/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC-%E7%BB%AD%E7%BB%AD%E7%AF%87%E4%B9%8B%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/ + 看完前面两篇水文之后,感觉不得不来分析下 mysql 的锁了,其实前面说到幻读的时候是有个前提没提到的,比如一个select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,
    看一段 mysql官方文档

    +
    +

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

    +
    +

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

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

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

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

    +]]>
    + + Mysql + C + 数据结构 + 源码 + Mysql + + + mysql + 数据结构 + 源码 + mvcc + read view + gap lock + next-key lock + 幻读 + +
    + + 聊聊 mysql 的 MVCC + /2020/04/26/%E8%81%8A%E8%81%8A-mysql-%E7%9A%84-MVCC/ + 很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢

    +

    其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行

    +

    腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下

    +
    /* Precise data types for system columns and the length of those columns;
    +NOTE: the values must run from 0 up in the order given! All codes must
    +be less than 256 */
    +#define DATA_ROW_ID 0     /* row id: a 48-bit integer */
    +#define DATA_ROW_ID_LEN 6 /* stored length for row id */
    +
    +/** Transaction id: 6 bytes */
    +constexpr size_t DATA_TRX_ID = 1;
    +
    +/** Transaction ID type size in bytes. */
    +constexpr size_t DATA_TRX_ID_LEN = 6;
    +
    +/** Rollback data pointer: 7 bytes */
    +constexpr size_t DATA_ROLL_PTR = 2;
    +
    +/** Rollback data pointer type size in bytes. */
    +constexpr size_t DATA_ROLL_PTR_LEN = 7;
    + +

    一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了

    +

    一个是 DATA_TRX_ID,这个表示这条记录的事务 ID

    +

    还有一个是 DATA_ROLL_PTR 指向回滚段的指针

    +

    指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,

    +

    m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id

    +

    m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id

    +

    m_ids, 这个是 read view 创建时所有的活跃事务 id 数组

    +

    m_creator_trx_id 这个是当前记录的创建事务 id

    +

    判断事务的可见性主要的逻辑是这样,

    +
      +
    1. 当记录的事务 id 小于最小活跃事务 id,说明是可见的,
    2. +
    3. 如果记录的事务 id 等于当前事务 id,说明是自己的更改,可见
    4. +
    5. 如果记录的事务 id 大于最大的活跃事务 id, 不可见
    6. +
    7. 如果记录的事务 id 介于 m_low_limit_idm_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
      具体的代码捞一下看看
      /** Check whether the changes by id are visible.
      +  @param[in]	id	transaction id to check against the view
      +  @param[in]	name	table name
      +  @return whether the view sees the modifications of id. */
      +  bool changes_visible(trx_id_t id, const table_name_t &name) const
      +      MY_ATTRIBUTE((warn_unused_result)) {
      +    ut_ad(id > 0);
      +
      +    if (id < m_up_limit_id || id == m_creator_trx_id) {
      +      return (true);
      +    }
      +
      +    check_trx_id_sanity(id, name);
      +
      +    if (id >= m_low_limit_id) {
      +      return (false);
      +
      +    } else if (m_ids.empty()) {
      +      return (true);
      +    }
      +
      +    const ids_t::value_type *p = m_ids.data();
      +
      +    return (!std::binary_search(p, p + m_ids.size(), id));
      +  }
      +剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据
    8. +
    +]]>
    + + Mysql + C + 数据结构 + 源码 + Mysql + + + mysql + 数据结构 + 源码 + mvcc + read view + +
    聊聊 mysql 索引的一些细节 /2020/12/27/%E8%81%8A%E8%81%8A-mysql-%E7%B4%A2%E5%BC%95%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%86%E8%8A%82/ @@ -17008,139 +16913,59 @@ $ }

    上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中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 - - - - 聊聊 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);
      +    // 首先,也是先堵死 new Singleton() 这条路,将构造方法变成私有
      +    private Singleton2() {}
      +    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
      +    private static volatile Singleton2 instance = null;
       
      -    if (id >= m_low_limit_id) {
      -      return (false);
      +    private int m = 9;
       
      -    } else if (m_ids.empty()) {
      -      return (true);
      +    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 {
       
      -    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. -
    + private Singleton3() {} + // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性 + private static class Holder { + private static Singleton3 instance = new Singleton3(); + } + public static Singleton3 getInstance() { + return Holder.instance; + } +}
    +

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

    +

    枚举单例

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

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

    ]]> - Mysql - C - 数据结构 - 源码 - Mysql + Java + Design Patterns + Singleton - mysql - 数据结构 - 源码 - mvcc - read view + 设计模式 + Design Patterns + 单例 + Singleton @@ -17493,6 +17318,59 @@ constexpr size_t DATA_ROLL_PTR_LEN AutoConfiguration + + 聊聊一次 brew update 引发的血案 + /2020/06/13/%E8%81%8A%E8%81%8A%E4%B8%80%E6%AC%A1-brew-update-%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/ + 熟悉我的人(谁熟悉你啊🙄)知道我以前写过 PHP,虽然现在在工作中没用到了,但是自己的一些小工具还是会用 PHP 来写,但是在 Mac 碰到了一个环境相关的问题,因为我也是个更新狂魔,用了 brew 之后因为 gfw 的原因,如果长时间不更新,有时候要装一个用它装一个软件的话,前置的更新耗时就会让人非常头大,所以我基本会隔天 update 一下,但是这样会带来一个很心烦的问题,就是像这样,因为我是要用一个固定版本的 PHP,如果一直升需要一直配扩展啥的也很麻烦,如果一直升级 PHP 到最新版可能会比较少碰到这个问题

    +
    dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
    +

    这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下

    +
    +

    ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。

    +
    +

    然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
    第一步,我们到 brew的目录下

    +
    cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
    +

    这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
    第二步看看它的提交历史

    +
    git log --follow icu4c.rb
    +

    在 git log 的海洋中寻找,寻找它的(64版本)的身影

    第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支

    +
    git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
    +

    其实注意 commit id 旁边的红框,这个是有tag 的,可以直接

    +
    git checkout icu4c-64
    +

    PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
    第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

    +
    brew reinstall ./icu4c.rb
    +

    然后就是第五步,切换版本

    +
    brew switch icu4c 64.2
    +

    最后把分支切回来

    +
    git checkout master
    +

    是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

    +
    # zsh
    +function hiicu64() {
    +  local last_dir=$(pwd)
    +
    +  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
    +  git checkout icu4c-4
    +  brew reinstall ./icu4c.rb
    +  brew switch icu4c 64.2
    +  git checkout master
    +
    +  cd $last_dir
    +}
    +

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

    +]]>
    + + Mac + PHP + Homebrew + PHP + icu4c + + + Mac + PHP + Homebrew + icu4c + zsh + +
    聊聊传说中的 ThreadLocal /2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/ @@ -17613,56 +17491,74 @@ constexpr size_t DATA_ROLL_PTR_LEN - 聊聊一次 brew update 引发的血案 - /2020/06/13/%E8%81%8A%E8%81%8A%E4%B8%80%E6%AC%A1-brew-update-%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/ - 熟悉我的人(谁熟悉你啊🙄)知道我以前写过 PHP,虽然现在在工作中没用到了,但是自己的一些小工具还是会用 PHP 来写,但是在 Mac 碰到了一个环境相关的问题,因为我也是个更新狂魔,用了 brew 之后因为 gfw 的原因,如果长时间不更新,有时候要装一个用它装一个软件的话,前置的更新耗时就会让人非常头大,所以我基本会隔天 update 一下,但是这样会带来一个很心烦的问题,就是像这样,因为我是要用一个固定版本的 PHP,如果一直升需要一直配扩展啥的也很麻烦,如果一直升级 PHP 到最新版可能会比较少碰到这个问题

    -
    dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
    -

    这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下

    -
    -

    ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。

    -
    -

    然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
    第一步,我们到 brew的目录下

    -
    cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
    -

    这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
    第二步看看它的提交历史

    -
    git log --follow icu4c.rb
    -

    在 git log 的海洋中寻找,寻找它的(64版本)的身影

    第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支

    -
    git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
    -

    其实注意 commit id 旁边的红框,这个是有tag 的,可以直接

    -
    git checkout icu4c-64
    -

    PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
    第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本

    -
    brew reinstall ./icu4c.rb
    -

    然后就是第五步,切换版本

    -
    brew switch icu4c 64.2
    -

    最后把分支切回来

    -
    git checkout master
    -

    是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法

    -
    # zsh
    -function hiicu64() {
    -  local last_dir=$(pwd)
    -
    -  cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
    -  git checkout icu4c-4
    -  brew reinstall ./icu4c.rb
    -  brew switch icu4c 64.2
    -  git checkout master
    -
    -  cd $last_dir
    -}
    -

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

    + 聊聊厦门旅游的好与不好 + /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ + 这几天去了趟厦门,原来几年前就想去了,本来都请好假了,后面因为一些事情没去成,这次刚好公司组织,就跟 LD 一起去了厦门,也不洋洋洒洒地写游记了,后面可能会有,今天先来总结下好的地方和比较坑的地方。
    这次主要去了中山路、鼓浪屿、曾厝(cuo)垵、植物园、灵玲马戏团,因为住的离环岛路比较近,还有幸现场看了下厦门马拉松,其中

    +

    中山路

    这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。

    +

    鼓浪屿

    鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆

    +

    曾厝垵

    一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆

    +

    植物园

    植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆

    +

    灵玲马戏团

    对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。

    +

    总结

    厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦‍♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。

    ]]>
    - Mac - PHP - Homebrew - PHP - icu4c + 生活 + 旅游 - Mac - PHP - Homebrew - icu4c - zsh + 生活 + 杭州 + 旅游 + 厦门 + 中山路 + 局口街 + 鼓浪屿 + 曾厝垵 + 植物园 + 马戏团 + 沙茶面 + 海蛎煎 + +
    + + 聊聊我刚学会的应用诊断方法 + /2020/05/22/%E8%81%8A%E8%81%8A%E6%88%91%E5%88%9A%E5%AD%A6%E4%BC%9A%E7%9A%84%E5%BA%94%E7%94%A8%E8%AF%8A%E6%96%AD%E6%96%B9%E6%B3%95/ + 因为传说中的出身问题,我以前写的是PHP,在使用 swoole 之前,基本的应用调试手段就是简单粗暴的 var_dump,exit,对于单进程模型的 PHP 也是简单有效,技术栈换成 Java 之后,就变得没那么容易,一方面是需要编译,另一方面是一般都是基于 spring 的项目,如果问题定位比较模糊,那框架层的是很难靠简单的 System.out.println 或者打 log 解决,(PS:我觉得可能我写的东西比较适合从 PHP 这种弱类型语言转到 Java 的小白同学)这个时候一方面因为是 Java,有了非常好用的 idea IDE 的支持,可以各种花式调试,条件断点尤其牛叉,但是又因为有 Spring+Java 的双重原因,有些情况下单步调试可以把手按废掉,这也是我之前一直比较困惑苦逼的点,后来随着慢慢精(jiang)进(you)之后,比如对于一个 oom 的情况,我们可以通过启动参数加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xx/xx 来配置溢出时的堆dump 日志,获取到这个文件后,我们可以通过像 Memory Analyzer (MAT)[https://www.eclipse.org/mat/] (The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.)来查看诊断问题所在,之前用到的时候是因为有个死循环一直往链表里塞数据,属于比较简单的,后来一次是由于运维进行应用迁移时按默认的统一配置了堆内存大小,导致内存的确不够用,所以溢出了,
    今天想说的其实主要是我们的 thread dump,这也是我最近才真正用的一个方法,可能真的很小白了,用过 ide 的单步调试其实都知道会有一个一层层的玩意,比如函数从 A,调用了 B,再从 B 调用了 C,一直往下(因为是 Java,所以还有很多🤦‍♂️),这个其实也是大部分语言的调用模型,利用了栈这个数据结构,通过这个结构我们可以知道代码的调用链路,由于对于一个 spring 应用,在本身框架代码量非常庞大的情况下,外加如果应用代码也是非常多的时候,有时候通过单步调试真的很难短时间定位到问题,需要非常大的耐心和仔细观察,当然不是说完全不行,举个例子当我的应用经常启动需要非常长的时间,因为本身应用有非常多个 bean,比较难说究竟是 bean 的加载的确很慢还是有什么异常原因,这种时候就可以使用 thread dump 了,具体怎么操作呢

    如果在idea 中运行或者调试时,可以直接点击这个照相机一样的按钮,右边就会出现了左边会显示所有的线程,右边会显示线程栈,

    +
    "main@1" prio=5 tid=0x1 nid=NA runnable
    +  java.lang.Thread.State: RUNNABLE
    +	  at TreeDistance.treeDist(TreeDistance.java:64)
    +	  at TreeDistance.treeDist(TreeDistance.java:65)
    +	  at TreeDistance.treeDist(TreeDistance.java:65)
    +	  at TreeDistance.treeDist(TreeDistance.java:65)
    +	  at TreeDistance.main(TreeDistance.java:45)
    +

    这就是我们主线程的堆栈信息了,main 表示这个线程名,prio表示优先级,默认是 5,tid 表示线程 id,nid 表示对应的系统线程,后面的runnable 表示目前线程状态,因为是被我打了断点,所以是就许状态,然后下面就是对应的线程栈内容了,在TreeDistance类的 treeDist方法中,对应的文件行数是 64 行。
    这里使用 thread dump一般也不会是上面我截图代码里的这种代码量很少的,一般是大型项目,有时候跑着跑着没反应,又不知道跑到哪了,特别是一些刚接触的大项目或者需要定位一个大项目的一个疑难问题,一时没思路时,可以使用这个方法,个人觉得非常有帮助。

    +]]>
    + + Java + Thread dump + 问题排查 + 工具 + + + Java + Thread dump + +
    + + 聊聊如何识别和意识到日常生活中的各类危险 + /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/ + 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
    还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
    最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。

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

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

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

    -

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

    -]]> - - 分布式事务 - 两阶段提交 - 三阶段提交 - - - 分布式事务 - 两阶段提交 - 三阶段提交 - 2PC - 3PC - -
    - - 聊聊厦门旅游的好与不好 - /2021/04/11/%E8%81%8A%E8%81%8A%E5%8E%A6%E9%97%A8%E6%97%85%E6%B8%B8%E7%9A%84%E5%A5%BD%E4%B8%8E%E4%B8%8D%E5%A5%BD/ - 这几天去了趟厦门,原来几年前就想去了,本来都请好假了,后面因为一些事情没去成,这次刚好公司组织,就跟 LD 一起去了厦门,也不洋洋洒洒地写游记了,后面可能会有,今天先来总结下好的地方和比较坑的地方。
    这次主要去了中山路、鼓浪屿、曾厝(cuo)垵、植物园、灵玲马戏团,因为住的离环岛路比较近,还有幸现场看了下厦门马拉松,其中

    -

    中山路

    这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。

    -

    鼓浪屿

    鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆

    -

    曾厝垵

    一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆

    -

    植物园

    植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆

    -

    灵玲马戏团

    对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。

    -

    总结

    厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦‍♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。

    +
  • 第二是协调者的单点问题,万一挂了,参与者就会在那傻等着
  • + +

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

    +

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

    ]]>
    - 生活 - 旅游 + 分布式事务 + 两阶段提交 + 三阶段提交 - 生活 - 杭州 - 旅游 - 厦门 - 中山路 - 局口街 - 鼓浪屿 - 曾厝垵 - 植物园 - 马戏团 - 沙茶面 - 海蛎煎 + 分布式事务 + 两阶段提交 + 三阶段提交 + 2PC + 3PC
    - 聊聊如何识别和意识到日常生活中的各类危险 - /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/ - 这篇博客的灵感又是来自于我从绍兴来杭州的路上,在我们进站以后上电梯快到的时候,突然前面不动了,右边我能看到的是有个人的行李箱一时拎不起来,另一边后面看到其实是个小孩子在那哭闹,一位妈妈就在那停着安抚或者可能有点手足无措,其实这一点应该是在几年前慢慢意识到是个非常危险的场景,特别是像绍兴北站这样上去站台是非常长的电梯,因为最近扩建改造,车次减少了很多,所以每一班都有很多人,检票上站台的电梯都是满员运转,试想这种情况,如果刚才那位妈妈再多停留一点时间,很可能就会出现后面的人上不来被挤下去,再严重点就是踩踏事件,但是这类情况很少人真的意识到,非常明显的例子就是很多人拿着比较大比较重的行李箱,不走垂梯,并且在快到的时候没有提前准备好,有可能在玩手机啥的,如果提不动,后面又是挤满人了,就很可能出现前面说的这种情况,并且其实这种是非紧急情况,大多数人都没有心理准备,一旦发生后果可能就会很严重,例如火灾地震疏散大部分人或者说负责引导的都是指示要有序撤离,防止踩踏,但是普通坐个扶梯,一般都不会有这个意识,但是如果这个时间比较长,出现了人员站不住往后倒了,真的会很严重。所以如果自己是带娃的或者带了很重的行李箱的,请提前做好准备,看到前面有人带的,最好也保持一定距离。
    还有比如日常走路,旁边有车子停着的情况,比较基本的看车灯有没有亮着,亮着的是否是倒车灯,这种应该特别注意远离,至少保持距离,不能挨着走,很多人特别是一些老年人,在一些人比较多的路上,往往完全无视旁边这些车的状态,我走我的路,谁敢阻拦我,管他车在那动不动,其实真的非常危险,车子本身有视线死角,再加上司机的驾驶习惯和状态,想去送死跟碰瓷的除外,还有就是有一些车会比较特殊,车子发动着,但是没灯,可能是车子灯坏了或者司机通过什么方式关了灯,这种比较难避开,不过如果车子打着了,一般会有比较大的热量散发,车子刚灭了也会有,反正能远离点尽量远离,从轿车的车前面走过挨着走要比从屁股后面挨着走稍微安全一些,但也最好不要挨着车走。
    最后一点其实是我觉得是我自己比较怕死,一般对来向的车或者从侧面出来的车会做更长的预判距离,特别是电瓶车,一般是不让人的,像送外卖的小哥,的确他们不太容易,但是真的很危险啊,基本就生死看刹车,能刹住就赚了,刹不住就看身子骨扛不扛撞了,只是这里要多说点又要谈到资本的趋利性了,总是想法设法的压榨以获取更多的利益,也不扯远了,能远离就远离吧。

    + 聊聊我的远程工作体验 + /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!

    ]]>
    生活 生活 - 糟心事 - 扶梯 - 踩踏 - 安全 - 电瓶车 + 远程办公
    @@ -17776,64 +17638,185 @@ constexpr size_t DATA_ROLL_PTR_LEN - 聊聊我的远程工作体验 - /2022/06/26/%E8%81%8A%E8%81%8A%E6%88%91%E7%9A%84%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C%E4%BD%93%E9%AA%8C/ - 发生疫情之后,因为正好是春节假期,假期结束的时候还不具备回工作地点办公的条件,所以史无前例地开始了远程办公,以前对于远程办公的概念还停留在国外一些有“格局”的企业会允许员工远程办公,当然对于远程办公这个事情本身我个人也并不是全然支持的态度,其中涉及到很多方面,首先远程办公并不代表就是不用去办公地点上班,可以在家里摸鱼,相对能够得到较高报酬的能够远程办公的企业需要在远程办公期间能够有高效的产出,并且也需要像在公司办公地点一样,能随时被联系到,第二点是薪资福利之外的社保公积金,除非薪资相比非远程办公的企业高出比较多,不然没法 cover 企业额外缴纳的社保公积金,听说有部分企业也会远程办公点给员工上社保,但是毕竟能做到这点的很少,在允许远程办公的企业数量这个本来就不大的基数里,大概率是少之又少了。
    疫情这个特殊原因开始的远程办公体验也算是开了个之前不太容易开的头,也跟我前面说的第一点有关系,大部分的企业也会担心员工远程办公是否有与在公司办公地点办公一样或者比较接近的办公效率。同时我们在开始远程办公的时候也碰到了因为原先没做过相应准备而导致的许多问题,首先基础设施上就有几个问题,第一个是办公电脑的问题,因为整个公司各个部门的工作性质和内容不同,并不是每个部门都是配笔记本的,或者有些部门并不需要想研发一样带上电脑 on call,所以那么使用台式机或者没有将笔记本带回家的则需要自己准备电脑或者让公司邮寄。第二个是远程网络的问题,像我们公司有研发团队平时也已经准备好了 vpn,但是在这种时候我们没准备好的是 vpn 带宽,毕竟平时只会偶尔有需要连一下 vpn 到公司网络,像这样大量员工都需要连接 vpn 进行工作的话,我们的初步体验就是网络卡的不行,一些远程调试工作没法进行,并且还有一些问题是可能只有我们研发会碰到,比如我们的线上测试服务器网络在办公地点是有网络打通的,但是我们在家就没办法连接,还有就是沟通效率相关,因为这是个全国性的情况,线上会议工具原先都是为特定用户使用,并且视频音频实时传输所需要的带宽质量要求也是比较高的,大规模的远程会议沟通需求让这些做线上会议的服务也算是碰上了类似双十一的大考了,我们是先后使用了 zoom,腾讯会议跟钉钉视频会议,使用体验上来说是 zoom 做得相对比较成熟和稳定,不过后面腾讯会议跟钉钉视频会议也开始赶上来。
    前面说的这几个点都是得有远程办公经验的公司才会提前做好相应的准备,比如可以做动态网络扩容,能够在需要大量员工连接公司网络的情况下快速响应提升带宽,另一些则是偏软性的,比如如如何在远程办公的条件下控制我们项目进度,如果保证沟通信息是否能像当面沟通那样准确传达,这方面其实我的经验也是边实操边优化的,最开始我们可能为了高效同步消息,会频繁的使用视频会议沟通,这其实并不能解决沟通效率问题,反而打扰了正常的工作,后续我们在特别是做项目过程中就通过相对简单的每日早会和日报机制,将每天的进度与问题风险点进行同步确认,只与相关直接干系人进行视频电话沟通确认,并且要保持一个思维,即远程办公比较适宜的是相对比较成熟的团队,平常工作和合作都已经有默契或者说规则并且能够遵守,在这个前提下,将目光专注于做的事情而不是管到具体的人有没有全天都在高效工作。同样也希望国内的环境能够有更多的远程火种成长起来,让它成为更好的工作方式,WLB!

    -]]>
    - - 生活 - - - 生活 - 远程办公 - -
    - - 聊聊我刚学会的应用诊断方法 - /2020/05/22/%E8%81%8A%E8%81%8A%E6%88%91%E5%88%9A%E5%AD%A6%E4%BC%9A%E7%9A%84%E5%BA%94%E7%94%A8%E8%AF%8A%E6%96%AD%E6%96%B9%E6%B3%95/ - 因为传说中的出身问题,我以前写的是PHP,在使用 swoole 之前,基本的应用调试手段就是简单粗暴的 var_dump,exit,对于单进程模型的 PHP 也是简单有效,技术栈换成 Java 之后,就变得没那么容易,一方面是需要编译,另一方面是一般都是基于 spring 的项目,如果问题定位比较模糊,那框架层的是很难靠简单的 System.out.println 或者打 log 解决,(PS:我觉得可能我写的东西比较适合从 PHP 这种弱类型语言转到 Java 的小白同学)这个时候一方面因为是 Java,有了非常好用的 idea IDE 的支持,可以各种花式调试,条件断点尤其牛叉,但是又因为有 Spring+Java 的双重原因,有些情况下单步调试可以把手按废掉,这也是我之前一直比较困惑苦逼的点,后来随着慢慢精(jiang)进(you)之后,比如对于一个 oom 的情况,我们可以通过启动参数加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xx/xx 来配置溢出时的堆dump 日志,获取到这个文件后,我们可以通过像 Memory Analyzer (MAT)[https://www.eclipse.org/mat/] (The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.)来查看诊断问题所在,之前用到的时候是因为有个死循环一直往链表里塞数据,属于比较简单的,后来一次是由于运维进行应用迁移时按默认的统一配置了堆内存大小,导致内存的确不够用,所以溢出了,
    今天想说的其实主要是我们的 thread dump,这也是我最近才真正用的一个方法,可能真的很小白了,用过 ide 的单步调试其实都知道会有一个一层层的玩意,比如函数从 A,调用了 B,再从 B 调用了 C,一直往下(因为是 Java,所以还有很多🤦‍♂️),这个其实也是大部分语言的调用模型,利用了栈这个数据结构,通过这个结构我们可以知道代码的调用链路,由于对于一个 spring 应用,在本身框架代码量非常庞大的情况下,外加如果应用代码也是非常多的时候,有时候通过单步调试真的很难短时间定位到问题,需要非常大的耐心和仔细观察,当然不是说完全不行,举个例子当我的应用经常启动需要非常长的时间,因为本身应用有非常多个 bean,比较难说究竟是 bean 的加载的确很慢还是有什么异常原因,这种时候就可以使用 thread dump 了,具体怎么操作呢

    如果在idea 中运行或者调试时,可以直接点击这个照相机一样的按钮,右边就会出现了左边会显示所有的线程,右边会显示线程栈,

    -
    "main@1" prio=5 tid=0x1 nid=NA runnable
    -  java.lang.Thread.State: RUNNABLE
    -	  at TreeDistance.treeDist(TreeDistance.java:64)
    -	  at TreeDistance.treeDist(TreeDistance.java:65)
    -	  at TreeDistance.treeDist(TreeDistance.java:65)
    -	  at TreeDistance.treeDist(TreeDistance.java:65)
    -	  at TreeDistance.main(TreeDistance.java:45)
    -

    这就是我们主线程的堆栈信息了,main 表示这个线程名,prio表示优先级,默认是 5,tid 表示线程 id,nid 表示对应的系统线程,后面的runnable 表示目前线程状态,因为是被我打了断点,所以是就许状态,然后下面就是对应的线程栈内容了,在TreeDistance类的 treeDist方法中,对应的文件行数是 64 行。
    这里使用 thread dump一般也不会是上面我截图代码里的这种代码量很少的,一般是大型项目,有时候跑着跑着没反应,又不知道跑到哪了,特别是一些刚接触的大项目或者需要定位一个大项目的一个疑难问题,一时没思路时,可以使用这个方法,个人觉得非常有帮助。

    + 聊聊 Java 中绕不开的 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 - Thread dump - 问题排查 - 工具 Java - Thread dump - -
    - - 聊聊最近平淡的生活之《花束般的恋爱》观后感 - /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/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)一篇了

    + Synchronized + 偏向锁 + 轻量级锁 + 重量级锁 + 自旋 + +
    + + 聊聊最近平淡的生活之《花束般的恋爱》观后感 + /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 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。

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

    + 聊聊最近平淡的生活之看看老剧 + /2021/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)一篇了

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

    +]]>
    + + 生活 + 开车 + + + 生活 + 开车 + 加塞 + 糟心事 + 规则 + +
    聊聊部分公交车的设计bug /2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/ @@ -17922,6 +17922,75 @@ constexpr size_t DATA_ROLL_PTR_LEN 杭州 + + 记录下 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 + +
    记录下 phpunit 的入门使用方法之setUp和tearDown /2022/10/23/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E4%B9%8BsetUp%E5%92%8CtearDown/ @@ -17968,79 +18037,177 @@ OK (2 t

    其实就是很简单的会在每个test方法前后都执行setUptearDown

    ]]> - php + php + + + php + +
    + + 记录下 phpunit 的入门使用方法 + /2022/10/16/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ + 这周开始打算写个比较简单的php工具包,然后顺带学习使用下php的单元测试,通过phpunit还是比较方便的,首先就composer require phpunit/phpunit
    安装下 phpunit, 前面包就是通过 composer init 创建,装完依赖后就可以把自动加载代码生成下 composer dump-autoload
    目录结构差不多这样

    +
    .
    +├── composer.json
    +├── composer.lock
    +├── oldfile.txt
    +├── phpunit.xml
    +├── src
    +│   └── Rename.php
    +└── tests
    +    └── RenameTest.php
    +
    +2 directories, 6 files
    +

    src/是源码,tests/是放的单测,比较重要的是phpunit.xml

    +
    <?xml version="1.0" encoding="UTF-8"?>
    +<phpunit colors="true" bootstrap="vendor/autoload.php">
    +    <testsuites>
    +        <testsuite name="php-rename">
    +            <directory>./tests/</directory>
    +        </testsuite>
    +    </testsuites>
    +</phpunit>
    +

    其中bootstrap就是需要把依赖包的自动加载入口配上,因为这个作为一个package,也会指出命名空间
    然后就是testsuite的路径,源码中

    +
    <?php
    +namespace Nicksxs\PhpRename;
    +
    +class Rename
    +{
    +    public static function renameSingleFile($file, $newFileName): bool
    +    {
    +        if(!is_file($file)) {
    +            echo "it's not a file";
    +            return false;
    +        }
    +        $fileInfo = pathinfo($file);
    +        return rename($file, $fileInfo["dirname"] . DIRECTORY_SEPARATOR . $newFileName . "." . $fileInfo["extension"]);
    +    }
    +}
    +

    就是一个简单的重命名
    然后test代码是这样,

    +
    <?php
    +
    +// require_once 'vendor/autoload.php';
    +
    +use PHPUnit\Framework\TestCase;
    +use Nicksxs\PhpRename\Rename;
    +use function PHPUnit\Framework\assertEquals;
    +
    +class RenameTest extends TestCase 
    +{
    +    public function setUp() :void
    +    {
    +        $myfile = fopen(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "w") or die("Unable to open file!");
    +        $txt = "file test1\n";
    +        fwrite($myfile, $txt);
    +        fclose($myfile);
    +    }
    +    public function testRename()
    +    {
    +        Rename::renameSingleFile(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "newfile");
    +        assertEquals(is_file(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt"), true);
    +    }
    +
    +    protected function tearDown(): void
    +    {
    +        unlink(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt");
    +    }
    +}
    +

    setUptearDown 就是初始化跟结束清理的,但是注意如果不指明 __DIR__ ,待会的目录就会在执行 vendor/bin/phpunit 下面,
    或者也可以指定在一个 tmp/ 目录下
    最后就可以通过vendor/bin/phpunit 来执行测试
    执行结果

    +
    ❯ vendor/bin/phpunit
    +PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
    +
    +.                                                                   1 / 1 (100%)
    +
    +Time: 00:00.005, Memory: 6.00 MB
    +
    +OK (1 test, 1 assertion)
    +]]>
    + + php + + + php + +
    + + 记录下 redis 的一些使用方法 + /2022/10/30/%E8%AE%B0%E5%BD%95%E4%B8%8B-redis-%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ + 虽然说之前讲解过一些redis源码相关的,但是说实话,redis的各种使用其实有时候有点生疏,或者在一些特定的使用场景中,一些使用方法还是需要学习和记录的

    +

    获取所有数据

    获取list类型的所有元素,可以使用 lrange , 直接用lrange key 0 -1
    比如

    这里有一些方便的就是可以不用知道长度,直接全返回,或者如果想拿到特定区间的就可以直接指定起止范围,

    这样就不用一个个pop出来

    +

    裁剪list

    前面用了lrange取得了一个范围的数据,如果想将数据直接移除,那可以用 ltrim ,

    这两个命令就可以从list里取出批量数据,并且能从list里删除这部分数据

    +]]>
    + + redis + + + redis + +
    + + 记录下 zookeeper 集群迁移和易错点 + /2022/05/29/%E8%AE%B0%E5%BD%95%E4%B8%8B-zookeeper-%E9%9B%86%E7%BE%A4%E8%BF%81%E7%A7%BB/ + 前阵子做了zk 的集群升级迁移,大概情况是原来是一个三节点的 zk 集群(最小可用
    大概是

    +
    zk1 192.168.2.1
    +zk2 192.168.2.2
    +zk3 192.168.2.3
    +

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

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

    加节点

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

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

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

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

    然后 zk5 的配置

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

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

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

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

    +

    下节点

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

    +

    第一种

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

    +

    第二种

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

    +]]>
    + + java - php + zookeeper
    - 记录下 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 结构

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

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

    + 闲话篇-也算碰到了为老不尊和坏人变老了的典型案例 + /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,感觉不是老太太拦着点他马上就要冲上来揍我了一样,事后我想想,这种情况我大概只能躺地上装死了,当这个事情发生之前我真的快把前面说的事情(水管阀坏了)给忘了,虽然这是理论上不该我来处理,除非是老头老太太请求我帮忙,这事后面我也从没说起过,本来完全没交集,对他们的是怎么样的人也没概念,总觉得年纪大了可能还比较心宽和蔼点,结果没想到就是一典型的坏人变老了,我说你们这么砸门,我老婆都被吓得不敢开门,结果对面老头老太太的儿子也出来了说,“我们就是敲下门,我母亲是机关单位退休的,所以肯定不会敲门很大声的,你老婆觉得吓到了是你们人生观价值观有问题”,听到这话我差点笑出来,连着两个可笑至极的脑残逻辑,无语他妈给无语开门,无语到家了。对门家我们之前有个印象就是因为我们都是顶楼,这边老小区以前都是把前后阳台包进来的,然后社区就来咨询大家的意见是不是统一把包进来的违建拆掉,还特地上来六楼跟他们说,结果对面的老头就说,“我要去住建局投诉你们”,本来这个事情是违法的,但是社区的意思也是征求各位业主的意见,结果感觉是社区上门强拆了一样,为老不尊,坏人变老了的典范了。

    ]]>
    生活 - 运动 - 跑步 - 干活 生活 - 运动 - 减肥 - 跑步 - 干活
    @@ -18142,20 +18290,6 @@ OK (2 t 大扫除 - - 记录下 redis 的一些使用方法 - /2022/10/30/%E8%AE%B0%E5%BD%95%E4%B8%8B-redis-%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ - 虽然说之前讲解过一些redis源码相关的,但是说实话,redis的各种使用其实有时候有点生疏,或者在一些特定的使用场景中,一些使用方法还是需要学习和记录的

    -

    获取所有数据

    获取list类型的所有元素,可以使用 lrange , 直接用lrange key 0 -1
    比如

    这里有一些方便的就是可以不用知道长度,直接全返回,或者如果想拿到特定区间的就可以直接指定起止范围,

    这样就不用一个个pop出来

    -

    裁剪list

    前面用了lrange取得了一个范围的数据,如果想将数据直接移除,那可以用 ltrim ,

    这两个命令就可以从list里取出批量数据,并且能从list里删除这部分数据

    -]]>
    - - redis - - - redis - -
    记一个容器中 dubbo 注册的小知识点 /2022/10/09/%E8%AE%B0%E4%B8%80%E4%B8%AA%E5%AE%B9%E5%99%A8%E4%B8%AD-dubbo-%E6%B3%A8%E5%86%8C%E7%9A%84%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/ @@ -18290,138 +18424,4 @@ OK (2 t dubbo - - 记录下 zookeeper 集群迁移和易错点 - /2022/05/29/%E8%AE%B0%E5%BD%95%E4%B8%8B-zookeeper-%E9%9B%86%E7%BE%A4%E8%BF%81%E7%A7%BB/ - 前阵子做了zk 的集群升级迁移,大概情况是原来是一个三节点的 zk 集群(最小可用
    大概是

    -
    zk1 192.168.2.1
    -zk2 192.168.2.2
    -zk3 192.168.2.3
    -

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

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

    加节点

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

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

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

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

    然后 zk5 的配置

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

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

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

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

    -

    下节点

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

    -

    第一种

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

    -

    第二种

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

    -]]>
    - - java - - - zookeeper - -
    - - 记录下 phpunit 的入门使用方法 - /2022/10/16/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/ - 这周开始打算写个比较简单的php工具包,然后顺带学习使用下php的单元测试,通过phpunit还是比较方便的,首先就composer require phpunit/phpunit
    安装下 phpunit, 前面包就是通过 composer init 创建,装完依赖后就可以把自动加载代码生成下 composer dump-autoload
    目录结构差不多这样

    -
    .
    -├── composer.json
    -├── composer.lock
    -├── oldfile.txt
    -├── phpunit.xml
    -├── src
    -│   └── Rename.php
    -└── tests
    -    └── RenameTest.php
    -
    -2 directories, 6 files
    -

    src/是源码,tests/是放的单测,比较重要的是phpunit.xml

    -
    <?xml version="1.0" encoding="UTF-8"?>
    -<phpunit colors="true" bootstrap="vendor/autoload.php">
    -    <testsuites>
    -        <testsuite name="php-rename">
    -            <directory>./tests/</directory>
    -        </testsuite>
    -    </testsuites>
    -</phpunit>
    -

    其中bootstrap就是需要把依赖包的自动加载入口配上,因为这个作为一个package,也会指出命名空间
    然后就是testsuite的路径,源码中

    -
    <?php
    -namespace Nicksxs\PhpRename;
    -
    -class Rename
    -{
    -    public static function renameSingleFile($file, $newFileName): bool
    -    {
    -        if(!is_file($file)) {
    -            echo "it's not a file";
    -            return false;
    -        }
    -        $fileInfo = pathinfo($file);
    -        return rename($file, $fileInfo["dirname"] . DIRECTORY_SEPARATOR . $newFileName . "." . $fileInfo["extension"]);
    -    }
    -}
    -

    就是一个简单的重命名
    然后test代码是这样,

    -
    <?php
    -
    -// require_once 'vendor/autoload.php';
    -
    -use PHPUnit\Framework\TestCase;
    -use Nicksxs\PhpRename\Rename;
    -use function PHPUnit\Framework\assertEquals;
    -
    -class RenameTest extends TestCase 
    -{
    -    public function setUp() :void
    -    {
    -        $myfile = fopen(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "w") or die("Unable to open file!");
    -        $txt = "file test1\n";
    -        fwrite($myfile, $txt);
    -        fclose($myfile);
    -    }
    -    public function testRename()
    -    {
    -        Rename::renameSingleFile(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "newfile");
    -        assertEquals(is_file(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt"), true);
    -    }
    -
    -    protected function tearDown(): void
    -    {
    -        unlink(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt");
    -    }
    -}
    -

    setUptearDown 就是初始化跟结束清理的,但是注意如果不指明 __DIR__ ,待会的目录就会在执行 vendor/bin/phpunit 下面,
    或者也可以指定在一个 tmp/ 目录下
    最后就可以通过vendor/bin/phpunit 来执行测试
    执行结果

    -
    ❯ vendor/bin/phpunit
    -PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
    -
    -.                                                                   1 / 1 (100%)
    -
    -Time: 00:00.005, Memory: 6.00 MB
    -
    -OK (1 test, 1 assertion)
    -]]>
    - - php - - - php - -
    diff --git a/sitemap.xml b/sitemap.xml index 006f435342..3f34ca8d9d 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -380,7 +380,7 @@ - https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ + https://nicksxs.me/2020/08/22/Filter-Intercepter-Aop-%E5%95%A5-%E5%95%A5-%E5%95%A5-%E8%BF%99%E4%BA%9B%E9%83%BD%E6%98%AF%E5%95%A5/ 2022-06-11 @@ -389,7 +389,7 @@ - https://nicksxs.me/2020/08/22/Filter-Intercepter-Aop-%E5%95%A5-%E5%95%A5-%E5%95%A5-%E8%BF%99%E4%BA%9B%E9%83%BD%E6%98%AF%E5%95%A5/ + https://nicksxs.me/2022/02/27/Disruptor-%E7%B3%BB%E5%88%97%E4%BA%8C/ 2022-06-11 @@ -461,7 +461,7 @@ - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ 2022-06-11 @@ -470,7 +470,7 @@ - https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ + https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ 2022-06-11 @@ -479,7 +479,7 @@ - https://nicksxs.me/2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E6%89%80%E6%9C%89%E6%9D%83%E4%BA%8C/ + https://nicksxs.me/2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/ 2022-06-11 @@ -506,7 +506,7 @@ - https://nicksxs.me/2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ + https://nicksxs.me/2020/11/29/%E4%BB%8E%E6%B8%85%E5%8D%8E%E7%BE%8E%E9%99%A2%E5%AD%A6%E5%A7%90%E8%81%8A%E8%81%8A%E6%88%91%E4%BB%AC%E8%BA%AB%E8%BE%B9%E7%9A%84%E6%81%B6%E4%BA%BA/ 2022-06-11 @@ -515,7 +515,7 @@ - https://nicksxs.me/2020/11/29/%E4%BB%8E%E6%B8%85%E5%8D%8E%E7%BE%8E%E9%99%A2%E5%AD%A6%E5%A7%90%E8%81%8A%E8%81%8A%E6%88%91%E4%BB%AC%E8%BA%AB%E8%BE%B9%E7%9A%84%E6%81%B6%E4%BA%BA/ + https://nicksxs.me/2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ 2022-06-11 @@ -533,7 +533,7 @@ - https://nicksxs.me/2021/09/12/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%BA%8C/ + https://nicksxs.me/2021/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 @@ -551,7 +551,7 @@ - https://nicksxs.me/2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ + https://nicksxs.me/2021/09/12/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%BA%8C/ 2022-06-11 @@ -560,7 +560,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/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/ 2022-06-11 @@ -569,7 +569,7 @@ - https://nicksxs.me/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/ + 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 @@ -578,7 +578,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 @@ -596,7 +596,7 @@ - https://nicksxs.me/2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ + https://nicksxs.me/2021/06/13/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%BA%8C/ 2022-06-11 @@ -605,7 +605,7 @@ - https://nicksxs.me/2021/06/13/%E8%81%8A%E8%81%8A-Java-%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%BA%8C/ + https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/ 2022-06-11 @@ -614,7 +614,7 @@ - https://nicksxs.me/2020/08/02/%E8%81%8A%E8%81%8A-Java-%E8%87%AA%E5%B8%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E9%80%86%E5%A4%A9%E5%B7%A5%E5%85%B7/ + https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ 2022-06-11 @@ -623,7 +623,7 @@ - https://nicksxs.me/2021/03/28/%E8%81%8A%E8%81%8A-Linux-%E4%B8%8B%E7%9A%84-top-%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/2022/01/09/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E4%B8%8B%E7%9A%84%E5%88%86%E9%A1%B5%E6%96%B9%E6%A1%88/ 2022-06-11 @@ -632,7 +632,7 @@ - https://nicksxs.me/2022/01/09/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E4%B8%8B%E7%9A%84%E5%88%86%E9%A1%B5%E6%96%B9%E6%A1%88/ + https://nicksxs.me/2021/12/12/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/ 2022-06-11 @@ -659,7 +659,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/2020/12/27/%E8%81%8A%E8%81%8A-mysql-%E7%B4%A2%E5%BC%95%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%86%E8%8A%82/ 2022-06-11 @@ -668,7 +668,7 @@ - https://nicksxs.me/2020/12/27/%E8%81%8A%E8%81%8A-mysql-%E7%B4%A2%E5%BC%95%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%86%E8%8A%82/ + https://nicksxs.me/2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/ 2022-06-11 @@ -677,7 +677,7 @@ - https://nicksxs.me/2021/05/30/%E8%81%8A%E8%81%8A%E4%BC%A0%E8%AF%B4%E4%B8%AD%E7%9A%84-ThreadLocal/ + https://nicksxs.me/2021/06/27/%E8%81%8A%E8%81%8A-Java-%E4%B8%AD%E7%BB%95%E4%B8%8D%E5%BC%80%E7%9A%84-Synchronized-%E5%85%B3%E9%94%AE%E5%AD%97-%E4%BA%8C/ 2022-06-11 @@ -1550,7 +1550,7 @@ - https://nicksxs.me/2019/12/10/Redis-Part-1/ + https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ 2020-01-12 @@ -1559,7 +1559,7 @@ - https://nicksxs.me/2015/03/11/Reverse-Bits/ + https://nicksxs.me/2019/12/10/Redis-Part-1/ 2020-01-12 @@ -1568,7 +1568,7 @@ - https://nicksxs.me/2015/03/13/Reverse-Integer/ + https://nicksxs.me/2015/03/11/Reverse-Bits/ 2020-01-12 @@ -1577,7 +1577,7 @@ - https://nicksxs.me/2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/ + https://nicksxs.me/2015/03/13/Reverse-Integer/ 2020-01-12 @@ -1586,7 +1586,7 @@ - https://nicksxs.me/2016/09/29/binary-watch/ + https://nicksxs.me/2015/01/14/Two-Sum/ 2020-01-12 @@ -1595,7 +1595,7 @@ - https://nicksxs.me/2016/08/14/docker-mysql-cluster/ + https://nicksxs.me/2016/09/29/binary-watch/ 2020-01-12 @@ -1604,7 +1604,7 @@ - https://nicksxs.me/2015/01/14/Two-Sum/ + https://nicksxs.me/2016/08/14/docker-mysql-cluster/ 2020-01-12 @@ -1703,7 +1703,7 @@ - https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ + https://nicksxs.me/2019/09/23/AbstractQueuedSynchronizer/ 2020-01-12 @@ -1712,7 +1712,7 @@ - https://nicksxs.me/2019/09/23/AbstractQueuedSynchronizer/ + https://nicksxs.me/2014/12/30/Clone-Graph-Part-I/ 2020-01-12 @@ -1793,7 +1793,7 @@ - https://nicksxs.me/2016/10/12/summary-ranges-228/ + https://nicksxs.me/2016/07/13/swoole-websocket-test/ 2020-01-12 @@ -1802,7 +1802,7 @@ - https://nicksxs.me/2016/07/13/swoole-websocket-test/ + https://nicksxs.me/2016/10/12/summary-ranges-228/ 2020-01-12 @@ -1968,14 +1968,14 @@ - https://nicksxs.me/tags/2019/ + https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/ + https://nicksxs.me/tags/2019/ 2023-02-12 weekly 0.2 @@ -1995,6 +1995,20 @@ 0.2 + + https://nicksxs.me/tags/2022/ + 2023-02-12 + weekly + 0.2 + + + + https://nicksxs.me/tags/2023/ + 2023-02-12 + weekly + 0.2 + + https://nicksxs.me/tags/c/ 2023-02-12 @@ -2066,21 +2080,21 @@ - https://nicksxs.me/tags/value/ + https://nicksxs.me/tags/environment/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ + https://nicksxs.me/tags/value/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/environment/ + https://nicksxs.me/tags/%E6%B3%A8%E8%A7%A3/ 2023-02-12 weekly 0.2 @@ -2150,84 +2164,77 @@ - https://nicksxs.me/tags/G1/ - 2023-02-12 - weekly - 0.2 - - - - https://nicksxs.me/tags/GC/ + https://nicksxs.me/tags/Filter/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Garbage-First-Collector/ + https://nicksxs.me/tags/Interceptor/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/2022/ + https://nicksxs.me/tags/AOP/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/2023/ + https://nicksxs.me/tags/Spring/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Filter/ + https://nicksxs.me/tags/Tomcat/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Interceptor/ + https://nicksxs.me/tags/Servlet/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/AOP/ + https://nicksxs.me/tags/Web/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Spring/ + https://nicksxs.me/tags/G1/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Tomcat/ + https://nicksxs.me/tags/GC/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Servlet/ + https://nicksxs.me/tags/Garbage-First-Collector/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Web/ + https://nicksxs.me/tags/headscale/ 2023-02-12 weekly 0.2 @@ -2282,13 +2289,6 @@ 0.2 - - https://nicksxs.me/tags/headscale/ - 2023-02-12 - weekly - 0.2 - - https://nicksxs.me/tags/Shift-2D-Grid/ 2023-02-12 @@ -2332,21 +2332,21 @@ - https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ + https://nicksxs.me/tags/linked-list/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/First-Bad-Version/ + https://nicksxs.me/tags/Lowest-Common-Ancestor-of-a-Binary-Tree/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/string/ + https://nicksxs.me/tags/First-Bad-Version/ 2023-02-12 weekly 0.2 @@ -2360,14 +2360,14 @@ - https://nicksxs.me/tags/linked-list/ + https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Median-of-Two-Sorted-Arrays/ + https://nicksxs.me/tags/string/ 2023-02-12 weekly 0.2 @@ -2451,42 +2451,42 @@ - https://nicksxs.me/tags/Maven/ + https://nicksxs.me/tags/mfc/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/C/ + https://nicksxs.me/tags/Maven/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Redis/ + https://nicksxs.me/tags/C/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Distributed-Lock/ + https://nicksxs.me/tags/Redis/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/ + https://nicksxs.me/tags/Distributed-Lock/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/mfc/ + https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/ 2023-02-12 weekly 0.2 @@ -2507,35 +2507,35 @@ - https://nicksxs.me/tags/Docker/ + https://nicksxs.me/tags/hadoop/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/namespace/ + https://nicksxs.me/tags/cluster/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/cgroup/ + https://nicksxs.me/tags/Docker/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/hadoop/ + https://nicksxs.me/tags/namespace/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/cluster/ + https://nicksxs.me/tags/cgroup/ 2023-02-12 weekly 0.2 @@ -2619,21 +2619,21 @@ - https://nicksxs.me/tags/nginx/ + https://nicksxs.me/tags/powershell/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ + https://nicksxs.me/tags/openresty/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/openresty/ + https://nicksxs.me/tags/nginx/ 2023-02-12 weekly 0.2 @@ -2660,6 +2660,13 @@ 0.2 + + https://nicksxs.me/tags/%E6%97%A5%E5%BF%97/ + 2023-02-12 + weekly + 0.2 + + https://nicksxs.me/tags/redis/ 2023-02-12 @@ -2751,13 +2758,6 @@ 0.2 - - https://nicksxs.me/tags/%E5%88%87%E7%89%87/ - 2023-02-12 - weekly - 0.2 - - https://nicksxs.me/tags/spark/ 2023-02-12 @@ -2780,63 +2780,63 @@ - https://nicksxs.me/tags/WordPress/ + https://nicksxs.me/tags/%E5%88%87%E7%89%87/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/tags/websocket/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/gc/ + https://nicksxs.me/tags/swoole/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ + https://nicksxs.me/tags/WordPress/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ + https://nicksxs.me/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/jvm/ + https://nicksxs.me/tags/gc/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/powershell/ + https://nicksxs.me/tags/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/websocket/ + https://nicksxs.me/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/swoole/ + https://nicksxs.me/tags/jvm/ 2023-02-12 weekly 0.2 @@ -2878,126 +2878,126 @@ - https://nicksxs.me/tags/ssh/ + https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ + https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ + https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ + https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ + https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ + https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ + https://nicksxs.me/tags/ssh/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ + https://nicksxs.me/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ + https://nicksxs.me/tags/%E5%BC%80%E8%BD%A6/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ + https://nicksxs.me/tags/%E5%8A%A0%E5%A1%9E/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ + https://nicksxs.me/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%90%90%E6%A7%BD/ + https://nicksxs.me/tags/%E8%A7%84%E5%88%99/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E7%96%AB%E6%83%85/ + https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%85%AC%E4%BA%A4%E8%BD%A6/ + https://nicksxs.me/tags/%E8%B7%AF%E6%94%BF%E8%A7%84%E5%88%92/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%8F%A3%E7%BD%A9/ + https://nicksxs.me/tags/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%9D%80%E4%BA%BA%E8%AF%9B%E5%BF%83/ + https://nicksxs.me/tags/%E6%9D%AD%E5%B7%9E/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Windows/ + https://nicksxs.me/tags/%E5%81%A5%E5%BA%B7%E7%A0%81/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E7%BE%8E%E5%9B%BD/ + https://nicksxs.me/tags/Windows/ 2023-02-12 weekly 0.2 @@ -3024,13 +3024,6 @@ 0.2 - - https://nicksxs.me/tags/scp/ - 2023-02-12 - weekly - 0.2 - - https://nicksxs.me/tags/git/ 2023-02-12 @@ -3087,6 +3080,13 @@ 0.2 + + https://nicksxs.me/tags/scp/ + 2023-02-12 + weekly + 0.2 + + https://nicksxs.me/tags/%E5%AD%97%E7%AC%A6%E9%9B%86/ 2023-02-12 @@ -3165,28 +3165,28 @@ - https://nicksxs.me/tags/Druid/ + https://nicksxs.me/tags/cglib/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ + https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/cglib/ + https://nicksxs.me/tags/Druid/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E4%BA%8B%E5%8A%A1/ + https://nicksxs.me/tags/%E6%95%B0%E6%8D%AE%E6%BA%90%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/ 2023-02-12 weekly 0.2 @@ -3578,203 +3578,203 @@ - https://nicksxs.me/tags/ThreadLocal/ + https://nicksxs.me/tags/Mac/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ + https://nicksxs.me/tags/PHP/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ + https://nicksxs.me/tags/Homebrew/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/WeakReference/ + https://nicksxs.me/tags/icu4c/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Mac/ + https://nicksxs.me/tags/zsh/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/PHP/ + https://nicksxs.me/tags/ThreadLocal/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/Homebrew/ + https://nicksxs.me/tags/%E5%BC%B1%E5%BC%95%E7%94%A8/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/icu4c/ + https://nicksxs.me/tags/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/zsh/ + https://nicksxs.me/tags/WeakReference/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ + https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ + https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ + https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/2PC/ + https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/3PC/ + https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%97%85%E6%B8%B8/ + https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%8E%A6%E9%97%A8/ + https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E4%B8%AD%E5%B1%B1%E8%B7%AF/ + https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%B1%80%E5%8F%A3%E8%A1%97/ + https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E9%BC%93%E6%B5%AA%E5%B1%BF/ + https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%9B%BE%E5%8E%9D%E5%9E%B5/ + https://nicksxs.me/tags/Thread-dump/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%A4%8D%E7%89%A9%E5%9B%AD/ + https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E9%A9%AC%E6%88%8F%E5%9B%A2/ + https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%B2%99%E8%8C%B6%E9%9D%A2/ + https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%B5%B7%E8%9B%8E%E7%85%8E/ + https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E6%89%B6%E6%A2%AF/ + https://nicksxs.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E8%B8%A9%E8%B8%8F/ + https://nicksxs.me/tags/%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%AE%89%E5%85%A8/ + https://nicksxs.me/tags/%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E7%94%B5%E7%93%B6%E8%BD%A6/ + https://nicksxs.me/tags/2PC/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/ + https://nicksxs.me/tags/3PC/ 2023-02-12 weekly 0.2 @@ -3788,7 +3788,7 @@ - https://nicksxs.me/tags/Thread-dump/ + https://nicksxs.me/tags/%E9%AA%91%E8%BD%A6/ 2023-02-12 weekly 0.2 @@ -3844,35 +3844,35 @@ - https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ + https://nicksxs.me/tags/zookeeper/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ + https://nicksxs.me/tags/%E7%9C%8B%E4%B9%A6/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ + https://nicksxs.me/tags/%E9%AB%98%E9%80%9F/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/dubbo/ + https://nicksxs.me/tags/%E5%A4%A7%E6%89%AB%E9%99%A4/ 2023-02-12 weekly 0.2 - https://nicksxs.me/tags/zookeeper/ + https://nicksxs.me/tags/dubbo/ 2023-02-12 weekly 0.2 @@ -3909,14 +3909,14 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ + https://nicksxs.me/categories/leetcode/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/leetcode/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2020/ 2023-02-12 weekly 0.2 @@ -3937,35 +3937,35 @@ - https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ + https://nicksxs.me/categories/Linked-List/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Binary-Tree/ + https://nicksxs.me/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ + https://nicksxs.me/categories/Binary-Tree/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ + https://nicksxs.me/categories/C/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Linked-List/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/ 2023-02-12 weekly 0.2 @@ -3979,7 +3979,7 @@ - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/leetcode/java/ 2023-02-12 weekly 0.2 @@ -3992,6 +3992,13 @@ 0.2 + + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/2019/ + 2023-02-12 + weekly + 0.2 + + https://nicksxs.me/categories/Java/%E5%B9%B6%E5%8F%91/ 2023-02-12 @@ -4000,7 +4007,7 @@ - https://nicksxs.me/categories/leetcode/java/ + https://nicksxs.me/categories/java/ 2023-02-12 weekly 0.2 @@ -4014,28 +4021,28 @@ - https://nicksxs.me/categories/Java/Apollo/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/java/ + https://nicksxs.me/categories/Java/Apollo/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ + https://nicksxs.me/categories/leetcode/java/Linked-List/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/2020/ + https://nicksxs.me/categories/Java/%E9%9B%86%E5%90%88/ 2023-02-12 weekly 0.2 @@ -4084,21 +4091,21 @@ - https://nicksxs.me/categories/Java/Apollo/value/ + https://nicksxs.me/categories/stack/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/stack/ + https://nicksxs.me/categories/Java/Apollo/value/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Linked-List/ + https://nicksxs.me/categories/linked-list/ 2023-02-12 weekly 0.2 @@ -4112,56 +4119,56 @@ - https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ + https://nicksxs.me/categories/Java/leetcode/Rotate-Image/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/linked-list/ + https://nicksxs.me/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2-online/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Java/leetcode/Rotate-Image/ + https://nicksxs.me/categories/Interceptor-AOP/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Linux/ + https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Interceptor-AOP/ + https://nicksxs.me/categories/Linux/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/Binary-Tree/DFS/ + https://nicksxs.me/categories/Java/Maven/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Java/Maven/ + https://nicksxs.me/categories/Redis/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Redis/ + https://nicksxs.me/categories/leetcode/java/DP/ 2023-02-12 weekly 0.2 @@ -4175,21 +4182,21 @@ - https://nicksxs.me/categories/Docker/ + https://nicksxs.me/categories/data-analysis/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/DP/ + https://nicksxs.me/categories/Docker/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/data-analysis/ + https://nicksxs.me/categories/leetcode/java/stack/ 2023-02-12 weekly 0.2 @@ -4203,7 +4210,7 @@ - https://nicksxs.me/categories/leetcode/java/stack/ + https://nicksxs.me/categories/leetcode/java/linked-list/ 2023-02-12 weekly 0.2 @@ -4224,63 +4231,63 @@ - https://nicksxs.me/categories/nginx/ + https://nicksxs.me/categories/Spring/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/leetcode/java/linked-list/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/php/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/ + https://nicksxs.me/categories/nginx/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Spring/ + https://nicksxs.me/categories/php/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/redis/ + https://nicksxs.me/categories/Redis/Distributed-Lock/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + https://nicksxs.me/categories/redis/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Redis/Distributed-Lock/ + https://nicksxs.me/categories/Redis/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/ + https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ 2023-02-12 weekly 0.2 @@ -4293,13 +4300,6 @@ 0.2 - - https://nicksxs.me/categories/Docker/%E4%BB%8B%E7%BB%8D/ - 2023-02-12 - weekly - 0.2 - - https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2023-02-12 @@ -4336,84 +4336,84 @@ - https://nicksxs.me/categories/ssh/ + https://nicksxs.me/categories/Java/Mybatis/Mysql/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/ + https://nicksxs.me/categories/ssh/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Windows/ + https://nicksxs.me/categories/Mysql/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%85%AC%E4%BA%A4/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/shell/ + https://nicksxs.me/categories/Windows/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/git/ + https://nicksxs.me/categories/Mybatis/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Mysql/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Java/Mybatis/Mysql/ + https://nicksxs.me/categories/git/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Mybatis/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/ + https://nicksxs.me/categories/Spring/Servlet/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Java/SpringBoot/ + https://nicksxs.me/categories/shell/ 2023-02-12 weekly 0.2 @@ -4426,6 +4426,13 @@ 0.2 + + https://nicksxs.me/categories/Java/SpringBoot/ + 2023-02-12 + weekly + 0.2 + + https://nicksxs.me/categories/Java/Dubbo/RPC/ 2023-02-12 @@ -4448,7 +4455,7 @@ - https://nicksxs.me/categories/Spring/Servlet/ + https://nicksxs.me/categories/C/ 2023-02-12 weekly 0.2 @@ -4469,49 +4476,49 @@ - https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ + https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ + https://nicksxs.me/categories/Dubbo-%E7%BA%BF%E7%A8%8B%E6%B1%A0/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Java/Design-Patterns/ + https://nicksxs.me/categories/Redis/%E5%BA%94%E7%94%A8/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Redis/%E6%BA%90%E7%A0%81/ + https://nicksxs.me/categories/Java/Design-Patterns/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/SpringBoot/ + https://nicksxs.me/categories/Mac/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/C/ + https://nicksxs.me/categories/SpringBoot/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Mac/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ 2023-02-12 weekly 0.2 @@ -4525,7 +4532,7 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E6%97%85%E6%B8%B8/ + https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ 2023-02-12 weekly 0.2 @@ -4539,14 +4546,14 @@ - https://nicksxs.me/categories/%E8%AF%AD%E8%A8%80/Rust/ + https://nicksxs.me/categories/Rust/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Rust/ + https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ 2023-02-12 weekly 0.2 @@ -4560,14 +4567,14 @@ - https://nicksxs.me/categories/Linux/%E5%91%BD%E4%BB%A4/echo/ + https://nicksxs.me/categories/MQ/RocketMQ/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/MQ/RocketMQ/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/ 2023-02-12 weekly 0.2 @@ -4581,7 +4588,7 @@ - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/ + https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ 2023-02-12 weekly 0.2 @@ -4595,14 +4602,14 @@ - https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ + https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ + https://nicksxs.me/categories/%E8%AF%BB%E5%90%8E%E6%84%9F/%E7%99%BD%E5%B2%A9%E6%9D%BE/%E5%B9%B8%E7%A6%8F%E4%BA%86%E5%90%97/ 2023-02-12 weekly 0.2 @@ -4616,28 +4623,28 @@ - https://nicksxs.me/categories/Mysql/Sql%E6%B3%A8%E5%85%A5/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E8%BF%90%E5%8A%A8/%E8%B7%91%E6%AD%A5/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Mybatis/%E7%BC%93%E5%AD%98/ + https://nicksxs.me/categories/Spring/Servlet/Interceptor/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%BD%B1%E8%AF%84/2020/ + https://nicksxs.me/categories/shell/%E5%B0%8F%E6%8A%80%E5%B7%A7/ 2023-02-12 weekly 0.2 @@ -4665,7 +4672,7 @@ - https://nicksxs.me/categories/Spring/Servlet/Interceptor/ + https://nicksxs.me/categories/C/Redis/ 2023-02-12 weekly 0.2 @@ -4713,13 +4720,6 @@ 0.2 - - https://nicksxs.me/categories/C/Redis/ - 2023-02-12 - weekly - 0.2 - - https://nicksxs.me/categories/Mac/PHP/ 2023-02-12 @@ -4749,14 +4749,14 @@ - 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/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E7%BE%8E%E5%9B%BD/ 2023-02-12 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/ + https://nicksxs.me/categories/%E7%94%9F%E6%B4%BB/%E5%90%90%E6%A7%BD/%E7%96%AB%E6%83%85/%E5%8F%A3%E7%BD%A9/ 2023-02-12 weekly 0.2 @@ -4777,35 +4777,35 @@ - https://nicksxs.me/categories/MQ/RocketMQ/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ + https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/ 2023-02-12 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/ + https://nicksxs.me/categories/MQ/RocketMQ/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Dubbo/SPI/ + https://nicksxs.me/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/grep/%E6%9F%A5%E6%97%A5%E5%BF%97/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ + https://nicksxs.me/categories/Dubbo/SPI/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Spring/Servlet/Interceptor/AOP/ + https://nicksxs.me/categories/Dubbo/%E5%AE%B9%E9%94%99%E6%9C%BA%E5%88%B6/ 2023-02-12 weekly 0.2 @@ -4875,35 +4875,35 @@ - https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ThreadPool/ + https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/ + https://nicksxs.me/categories/Dubbo/%E7%BA%BF%E7%A8%8B%E6%B1%A0/ThreadPool/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/PHP/ + https://nicksxs.me/categories/%E7%BC%93%E5%AD%98/%E7%A9%BF%E9%80%8F/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/ + https://nicksxs.me/categories/PHP/ 2023-02-12 weekly 0.2 - https://nicksxs.me/categories/Dubbo/SPI/Adaptive/ + https://nicksxs.me/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/ 2023-02-12 weekly 0.2 diff --git a/tags/Dubbo/index.html b/tags/Dubbo/index.html index b2db42af73..00919315d7 100644 --- a/tags/Dubbo/index.html +++ b/tags/Dubbo/index.html @@ -1 +1 @@ -标签: dubbo | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    orks best with JavaScript enabled
    \ No newline at end of file +标签: dubbo | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    \ No newline at end of file diff --git a/tags/java/page/2/index.html b/tags/java/page/2/index.html index 29b24d9437..a9152353f3 100644 --- a/tags/java/page/2/index.html +++ b/tags/java/page/2/index.html @@ -1 +1 @@ -标签: Java | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    \ No newline at end of file +标签: java | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    \ No newline at end of file diff --git a/tags/java/page/5/index.html b/tags/java/page/5/index.html index 71b5745d55..50b19f4598 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%
    script src=/js/third-party/comments/disqus.js> \ No newline at end of file diff --git a/tags/java/page/7/index.html b/tags/java/page/7/index.html index 93146808bc..4ca492e694 100644 --- a/tags/java/page/7/index.html +++ b/tags/java/page/7/index.html @@ -1 +1 @@ -标签: java | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    \ No newline at end of file +标签: java | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    ipt> \ No newline at end of file diff --git a/tags/mysql/page/2/index.html b/tags/mysql/page/2/index.html index 96a8cec610..15db9ed599 100644 --- a/tags/mysql/page/2/index.html +++ b/tags/mysql/page/2/index.html @@ -1 +1 @@ -标签: Mysql | Nicksxs's Blog

    Nicksxs's Blog

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

    0%
    \ No newline at end of file +标签: mysql | Nicksxs's Blog

    Nicksxs's Blog

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

    mysql 标签

    2016
    0%
    \ No newline at end of file