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

0%

Maven 翻译为”专家”、”内行”,是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。

Maven 也可被用于构建和管理各种项目,例如 C#,Ruby,Scala 和其他语言编写的项目。Maven 曾是 Jakarta 项目的子项目,现为由 Apache 软件基金会主持的独立 Apache 项目。

maven也是我们日常项目中实用的包管理工具,相比以前需要用把包下载下来,放进 lib 中,在平时工作中使用的话,其实像 idea 这样的 ide 工具都会自带 maven 工具和插件

maven的基本操作

  • mvn -v
    查看 maven 信息
  • mvn compile
    将 Java 编译成 class 文件
  • mvn test
    执行 test 包下的测试用例
  • mvn package
    将项目打成 jar 包
  • mvn clean
    删除package 在 target 目录下面打出来的 jar 包和 target 目录
  • mvn install
    将打出来的 jar 包复制到 maven 的本地仓库里
  • mvn deploy
    将打出来的 jar 包上传到远程仓库里

与 composer 对比

因为我也是个 PHP 程序员,所以对比一下两种语言,很容易想到在 PHP 的 composer 跟 Java 的 maven 是比较类似的作用,有一点两者是非常相似的,就是原仓库都是因为某些原因连接拉取都会很慢,所以像 composer 会有一些国内源,前阵子阿里也出了一个,类似的 maven 一般也会使用阿里的镜像仓库,通过在 setting.xml 文件中的设置

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>aliyun</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

这算是个尴尬的共同点,然后因为 PHP 是解释型脚本语言,所以 php 打出来的 composer 包其实就是个 php 代码包,使用SPL Autoload等方式加载代码包,maven 包则是经过编译的 class 包,还有一点是 composer 也可以直接使用 github 地址作为包代码的拉取源,这点也是比较大的区别,maven使用 pom 文件管理依赖

maven 的个人小技巧

  • maven 拉取依赖时,同时将 snapshot 也更新了,就是 mvn compile加个-U参数,如果还不行就需要将本地仓库的 snapshot 删掉,
    这个命令的 help 命令解释是 -U,–update-snapshots Forces a check for missing releases and updated snapshots on
    remote repositories,这个在日常使用中还是很经常使用的
  • maven 出现依赖冲突的时候的解决方法
    首先是依赖分析,使用mvn dependency:tree分析下依赖关系,如果要找具体某个包的依赖引用关系可以使用mvn dependency:tree -Dverbose -Dincludes=org.springframework:spring-webmvc命令进行分析,如果发现有冲突的依赖关系,本身 maven 中依赖引用有相对的顺序,大致来说是引用路径短的优先,pom 文件中定义的顺序优先,如果要把冲突的包排除掉可以在 pom 中用
    1
    2
    3
    4
    5
    6
    <exclusions>
    <exclusion>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    </exclusion>
    </exclusions>
    将冲突的包排除掉
  • maven 依赖的 jdk 版本管理
    前面介绍的mvn -v可以查看 maven 的安装信息
    比如
    1
    2
    3
    4
    5
    Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
    Maven home: /usr/local/Cellar/maven/3.6.3_1/libexec
    Java version: 1.8.0_201, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre
    Default locale: zh_CN, platform encoding: UTF-8
    OS name: "mac os x", version: "10.14.6", arch: "x86_64", family: "mac"
    这里可以看到用了 mac 自带的 jdk1.8,结合我之前碰到的一个问题,因为使用 homebrew 升级了 gradle,而 gradle 又依赖了 jdk13,因为这个 mvn 的 Java version 也变成 jdk13 了,然后 mvn 编译的时候出现了 java.lang.ExceptionInInitializerError: com.sun.tools.javac.code.TypeTags这个问题,所以需要把这个版本给改回来,但是咋改呢,网上搜来的一大堆都是在 pom 文件里的
    source和 target 版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <encoding>UTF-8</encoding>
    </configuration>
    </plugin>
    </plugins>
    <build>
    或者修改 maven 的 setting.xml中的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <profiles>
    <profile>
    <id>ngmm-nexus</id>
    <activation>
    <jdk>1.8</jdk>
    </activation>
    <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
    </properties>
    </profile>
    </profiles>
    但是这些都没啥用啊,真正有办法的是建个 .mavenrc,这个顾名思义就是 maven 的资源文件,类似于 .bashrc.zshrc,在里面添加 MAVEN_HOME 和 JAVA_HOME,然后执行 source .mavenrc就 OK 啦

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,这个具体在代码层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// We encode the value of the heap region type so the generation can be
// determined quickly. The tag is split into two parts:
//
// major type (young, old, humongous, archive) : top N-1 bits
// minor type (eden / survivor, starts / cont hum, etc.) : bottom 1 bit
//
// If there's need to increase the number of minor types in the
// future, we'll have to increase the size of the latter and hence
// decrease the size of the former.
//
// 00000 0 [ 0] Free
//
// 00001 0 [ 2] Young Mask
// 00001 0 [ 2] Eden
// 00001 1 [ 3] Survivor
//
// 00010 0 [ 4] Humongous Mask
// 00100 0 [ 8] Pinned Mask
// 00110 0 [12] Starts Humongous
// 00110 1 [13] Continues Humongous
//
// 01000 0 [16] Old Mask
//
// 10000 0 [32] Archive Mask
// 11100 0 [56] Open Archive
// 11100 1 [57] Closed Archive
//
typedef enum {
FreeTag = 0,

YoungMask = 2,
EdenTag = YoungMask,
SurvTag = YoungMask + 1,

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,这里也感谢前东家让我有机会转了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 是个双向链表,看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* 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 的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示

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

插入过程

简单说下插入元素的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/* 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 最初的印象应该就是个 key-value 的缓存,类似于 memcache,redis 其实也是个 key-value,key 还是一样的字符串,或者说就是用 redis 自己的动态字符串实现,但是 value 其实就是前面说的那些数据结构,差不多快说完了,还有个 quicklist 后面还有一篇,这里先介绍下 redis 对于这些不同类型的 value 是怎么实现的,首先看下 redisObject 的源码头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
/*
* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

#define LRU_BITS 24
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */

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

主体结构就是这个 redisObject,

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

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

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

1
<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>中的具体结构,

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
* |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 也都有用了。
看个具体的例子(上下有点对不齐,将就看)

1
2
[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 作为底层数据结构,先看一下代码先

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;

/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

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

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

1
2
3
4
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 插入每个元素的时候都是使用随机的方式,看一眼代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;

typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;

typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;

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

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

SDS 简单动态字符串

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

1
2
3
4
5
6
7
8
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 上最早版本的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 同样实现了自己的字典
下面来看看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef struct dictEntry {
void *key;
void *val;
struct dictEntry *next;
} dictEntry;

typedef struct dictType {
unsigned int (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;

typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;

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

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

这是个 Java 面试的高频问题,我也遇到过,以往都是觉得这类题没意思,网上一搜一大堆,也不愿意记,其实说回来,主要还是没静下心来好好去理解,今天无意中看到一个课程,基本帮我把一些疑惑的点讲清楚了,首先单例是啥意思,这个其实是有范围一说,比如我起了个Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的

饿汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton1 {
// 首先,将构造方法变成私有的
private Singleton1() {};
// 创建私有静态实例,这样第一次使用的时候就会进行创建
private static Singleton instance = new Singleton1();

// 使用这个对象都是通过这个 getInstance 来获取
public static Singleton1 getInstance() {
return instance;
}
// 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
// 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
public static Date getDate(String mode) {return new Date();}
}

上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中getDate方法里说的问题,有时候并没有想那这个对象,但是因为我调用了这个类的静态方法,导致对象已经生成了,可能这也是饿汉模式名字的来由,不管三七二十一给你生成个单例就完事了,不管有没有用,但是这种个人觉得也没啥大问题,如果是面试的话最好说出来它的缺点

饱汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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也是不可缺少的

嵌套类

1
2
3
4
5
6
7
8
9
10
11
public class Singleton3 {

private Singleton3() {}
// 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
private static class Holder {
private static Singleton3 instance = new Singleton3();
}
public static Singleton3 getInstance() {
return Holder.instance;
}
}

这个我个人感觉是饿汉模式的升级版,可以在调用getInstance的时候去实例化对象,也是比较推荐的

枚举单例

1
2
3
4
5
6
7
public enum Singleton {
INSTANCE;

public void doSomething(){
//todo doSomething
}
}

枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。