Dunwu Blog

大道至简,知易行难

Mysql 高可用

复制

复制是解决系统高可用的常见手段。其思路就是:不要把鸡蛋都放在一个篮子里。

复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到多台备库上,备库本身也可以被配置成另外一台服务器的主库。主库和备库之 间可以有多种不同的组合方式。

MySQL 支持两种复制方式:基于行的复制和基于语句的复制。这两种方式都是通过在主库上记录 bin log、在备库重放日志的方式来实现异步的数据复制。这意味着:复制过程存在时延,这段时间内,主从数据可能不一致。

复制如何工作

在 Mysql 中,复制分为三个步骤,分别由三个线程完成:

  • binlog dump 线程 - 主库上有一个特殊的 binlog dump 线程,负责将主服务器上的数据更改写入 binlog 中。
  • I/O 线程 - 备库上有一个 I/O 线程,负责从主库上读取 binlog,并写入备库的中继日志(relay log)中。
  • SQL 线程 - 备库上有一个 SQL 线程,负责读取中继日志(relay log)并重放其中的 SQL 语句。

这种架构实现了数据备份和数据同步的异步解耦。但这种架构也限制了复制的过程,其中最重要 的一点是在主库上并发运行的查询在备库只能串行化执行,因为只有一个 SQL 线程来重放 中继日志中的事件。

主备配置

假设需要配置一对 Mysql 主备节点,环境如下:

  • 主库节点:192.168.8.10
  • 备库节点:192.168.8.11

主库上的操作

(1)修改配置并重启

执行 vi /etc/my.cnf ,添加如下配置:

1
2
3
[mysqld]
server-id=1
log_bin=/var/lib/mysql/binlog
  • server-id - 服务器 ID 号。在主从架构中,每台机器的 ID 必须唯一。
  • log_bin - 同步的日志路径及文件名,一定注意这个目录要是 mysql 有权限写入的;

修改后,重启 mysql 使配置生效:

1
systemctl restart mysql

(2)创建用于同步的用户

进入 mysql 命令控制台:

1
2
$ mysql -u root -p
Password:

执行以下 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
-- a. 创建 slave 用户
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '密码';
-- 为 slave 赋予 REPLICATION SLAVE 权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';

-- b. 或者,创建 slave 用户,并指定该用户能在任意主机上登录
-- 如果有多个备库,又想让所有备库都使用统一的用户名、密码认证,可以考虑这种方式
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '密码';
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';

-- 刷新授权表信息
FLUSH PRIVILEGES;

注意:在 Mysql 8 中,默认密码验证不再是 password。所以在创建用户时,create user 'username'@'%' identified by 'password'; 客户端是无法连接服务的。所以,需要加上 IDENTIFIED WITH mysql_native_password BY 'password'

补充用户管理 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 查看所有用户
SELECT DISTINCT CONCAT('User: ''', user, '''@''', host, ''';') AS query
FROM mysql.user;

-- 查看用户权限
SHOW GRANTS FOR 'root'@'%';

-- 创建用户
-- a. 创建 slave 用户,并指定该用户只能在主机 192.168.8.11 上登录
CREATE USER 'slave'@'192.168.8.11' IDENTIFIED WITH mysql_native_password BY '密码';
-- 为 slave 赋予 REPLICATION SLAVE 权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'192.168.8.11';

-- 删除用户
DROP USER 'slave'@'192.168.8.11';

(3)加读锁

为了主库与从库的数据保持一致,我们先为 mysql 加入读锁,使其变为只读。

1
mysql> FLUSH TABLES WITH READ LOCK;

(4)查看主库状态

1
2
3
4
5
6
7
mysql> show master status;
+------------------+----------+--------------+---------------------------------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+---------------------------------------------+-------------------+
| mysql-bin.000001 | 4202 | | mysql,information_schema,performance_schema | |
+------------------+----------+--------------+---------------------------------------------+-------------------+
1 row in set (0.00 sec)

注意:需要记录下 FilePosition,后面会用到。

(5)导出 sql

1
mysqldump -u root -p --all-databases --master-data > dbdump.sql

(6)解除读锁

1
mysql> UNLOCK TABLES;

(7)将 sql 远程传送到备库上

1
scp dbdump.sql root@192.168.8.11:/home

备库上的操作

(1)修改配置并重启

执行 vi /etc/my.cnf ,添加如下配置:

1
2
3
[mysqld]
server-id=2
log_bin=/var/lib/mysql/binlog
  • server-id - 服务器 ID 号。在主从架构中,每台机器的 ID 必须唯一。
  • log_bin - 同步的日志路径及文件名,一定注意这个目录要是 mysql 有权限写入的;

修改后,重启 mysql 使配置生效:

1
systemctl restart mysql

(2)导入 sql

1
mysql -u root -p < /home/dbdump.sql

(3)在备库上建立与主库的连接

进入 mysql 命令控制台:

1
2
$ mysql -u root -p
Password:

执行以下 SQL:

1
2
3
4
5
6
7
8
9
10
-- 停止备库服务
STOP SLAVE;

-- 注意:MASTER_USER 和
CHANGE MASTER TO
MASTER_HOST='192.168.8.10',
MASTER_USER='slave',
MASTER_PASSWORD='密码',
MASTER_LOG_FILE='binlog.000001',
MASTER_LOG_POS=4202;
  • MASTER_LOG_FILEMASTER_LOG_POS 参数要分别与 show master status 指令获得的 FilePosition 属性值对应。
  • MASTER_HOST 是主库的 HOST。
  • MASTER_USERMASTER_PASSWORD 是在主节点上注册的用户及密码。

(4)启动 slave 进程

1
mysql> start slave;

(5)查看主从同步状态

1
mysql> show slave status\G;

说明:如果以下两项参数均为 YES,说明配置正确。

  • Slave_IO_Running
  • Slave_SQL_Running

(6)将备库设为只读

1
2
3
4
5
6
7
8
9
10
11
mysql> set global read_only=1;
mysql> set global super_read_only=1;
mysql> show global variables like "%read_only%";
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_read_only | OFF |
| read_only | ON |
| super_read_only | ON |
| transaction_read_only | OFF |
+-----------------------+-------+

注:设置 slave 服务器为只读,并不影响主从同步。

复制的原理

读写分离

主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。

读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。

MySQL 读写分离能提高性能的原因在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
  • 从服务器可以配置 MyISAM 引擎,提升查询性能以及节约系统开销;
  • 增加冗余,提高可用性。

参考资料

Redis 事件

Redis 服务器是一个事件驱动程序,服务器需要处理两类事件:

  • 文件事件(file event) - Redis 服务器通过套接字(Socket)与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。服务器与客户端(或其他的服务器)的通信会产生文件事件,而服务器通过监听并处理这些事件来完成一系列网络通信操作。
  • 时间事件(time event) - Redis 服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。

关键词:文件事件时间事件

文件事件

Redis 基于 Reactor 模式开发了自己的网络时间处理器。

  • Redis 文件事件处理器使用 I/O 多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答、读取、写入、关闭操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

这样的好处非常明显:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。

文件事件处理器有四个组成部分:

  • 多个 Socket(客户端连接)
  • IO 多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将 Socket 关联到相应的事件处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

img

时间事件

时间事件又分为:

  • 定时事件:是让一段程序在指定的时间之内执行一次;
  • 周期性事件:是让一段程序每隔指定时间就执行一次。

Redis 将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。

事件的调度与执行

服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。

事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def aeProcessEvents():

## 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()

## 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()

## 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0

## 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)

## 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)

## 处理所有已产生的文件事件
procesFileEvents()

## 处理所有已到达的时间事件
processTimeEvents()

将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
def main():

## 初始化服务器
init_server()

## 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()

## 服务器关闭,执行清理操作
clean_server()

从事件处理的角度来看,服务器运行流程如下:

## 线程模型

虽然说 Redis 是单线程模型,但实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。

不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。

为此,Redis 4.0 之后新增了几个异步命令:

  • UNLINK:可以看作是 DEL 命令的异步版本。
  • FLUSHALL ASYNC:用于清空所有数据库的所有键,不限于当前 SELECT 的数据库。
  • FLUSHDB ASYNC:用于清空当前 SELECT 数据库中的所有键。

总的来说,直到 Redis 6.0 之前,Redis 的主要操作仍然是单线程处理的。

Redis6.0 之前为什么不使用多线程? 我觉得主要原因有 3 点:

  • 单线程编程容易并且更容易维护;
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 redis.conf

1
io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为23个线程,8核的建议设置为6个线程

另外:

  • io-threads 的个数一旦设置,不能通过 config 动态设置。
  • 当设置 ssl 后,io-threads 将不工作。

开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 redis.conf :

1
io-threads-do-reads yes

但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启

参考资料

Redis 管道

关键词:Pipeline

Pipeline 简介

Redis 是一种基于 C/S 模型以及请求/响应协议的 TCP 服务。通常情况下,一个 Redis 命令的请求、响应遵循以下步骤:

  • 客户端向服务端发送一个查询请求,并监听 Socket 返回(通常是以阻塞模式,等待服务端响应)。
  • 服务端处理命令,并将结果返回给客户端。

显然,如果每个 Redis 命令都发起一次请求、响应,会很低效。因此,Redis 客户端提供了一种批量处理技术,即

管道技术(Pipeline。Pipeline 的工作原理就是:将多个 Redis 命令一次性发送给服务端,服务端处理后,统一返回给客户端。由于减少了通信次数,自然提升了处理效率。

Pipeline 限制

在使用 Redis 管道技术时,要注意一些限制,避免踩坑:

  • Pipeline 不能保证原子性 - Pipeline 只是将客户端发送命令的方式改为批量发送,而服务端在接收到 Pipeline 发来的命令后,将其拆解为一条条命令,然后依然是串行执行。执行过程中,服务端有可能执行其他客户端的命令,所以无法保证原子性。如需保证原子性,可以考虑使用事务或 Lua 脚本。
  • Pipeline 不支持回滚 - Pipeline 没有事务的特性,如果待执行命令的前后存在依赖关系,请勿使用 Pipeline。
  • Pipeline 命令不宜过大 - 使用管道发送命令时,Redis Server 会将部分请求放到缓存队列中(占用内存),执行完毕后一次性发送结果。如果需要发送大量的命令,会占用大量的内存,因此应该按照合理数量分批次的处理。
  • Pipeline 不支持跨 slot 访问 - 由于 Pipeline 不支持跨 slot 访问,因此,在 Redis 集群模式下使用 Pipeline 时要确保访问的 key 都在同一 slot 中。

Pipeline 案例

主流的 Redis 客户端,一般都会支持管道技术。

【示例】Jedis 管道使用示例

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
public class Demo {

public static void main(String[] args) {

String host = "localhost";
int port = 6379;
Jedis jedis = new Jedis(host, port);

String key = "pipeline:test";
jedis.del(key);

// -------- 方法1
method1(jedis, key);

//-------- 方法2
method2(jedis, key);
}

private static void method2(Jedis jedis, String key) {
System.out.println("-----方法2-----");
jedis.del(key);//初始化
Pipeline pipeline = jedis.pipelined();
//需要先声明Response
Response<Long> r1 = pipeline.incr(key);
System.out.println("Pipeline发送请求");
Response<Long> r2 = pipeline.incr(key);
System.out.println("Pipeline发送请求");
Response<Long> r3 = pipeline.incr(key);
System.out.println("Pipeline发送请求");
Response<Long> r4 = pipeline.incr(key);
System.out.println("Pipeline发送请求");
Response<Long> r5 = pipeline.incr(key);
System.out.println("Pipeline发送请求");
try {
// 此时还未开始接收响应,所以此操作会出错
r1.get();
} catch (Exception e) {
System.out.println(" <<< Pipeline error:还未开始接收响应 >>> ");
}
// 发送请求完成,开始接收响应
System.out.println("发送请求完成,开始接收响应");
pipeline.sync();
System.out.println("Pipeline 接收响应 Response: " + r1.get());
System.out.println("Pipeline 接收响应 Response: " + r2.get());
System.out.println("Pipeline 接收响应 Response: " + r3.get());
System.out.println("Pipeline 接收响应 Response: " + r4.get());
System.out.println("Pipeline 接收响应 Response: " + r5.get());
jedis.close();
}

private static void method1(Jedis jedis, String key) {
Pipeline pipeline = jedis.pipelined();
System.out.println("-----方法1-----");
for (int i = 0; i < 5; i++) {
pipeline.incr(key);
System.out.println("Pipeline 发送请求");
}
// 发送请求完成,开始接收响应
System.out.println("发送请求完成,开始接收响应");
List<Object> responses = pipeline.syncAndReturnAll();
if (responses == null || responses.isEmpty()) {
jedis.close();
throw new RuntimeException("Pipeline error: 没有接收到响应");
}
for (Object resp : responses) {
System.out.println("Pipeline 接收响应 Response: " + resp.toString());
}
System.out.println();
}

}

参考资料

Redis 发布订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。

Redis 有两种发布订阅模式

  • 基于频道(Channel)的发布订阅
  • 基于模式(Pattern)的发布订阅

关键词:订阅SUBSCRIBEPSUBSCRIBEPUBLISH观察者模式

观察者模式

Redis 发布订阅应用了设计模式中经典的“观察者模式”。

观察者模式(Observer)是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个 “观察” 该对象的其他对象。

  • 当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。
  • 当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。

观察者模式

Redis 订阅模式

Redis 有两种发布订阅模式:

(1)基于频道(Channel)的发布订阅

服务器状态在 pubsub_channels 字典保存了所有频道的订阅关系: SUBSCRIBE 命令负责将客户端和被订阅的频道关联到这个字典里面, 而 UNSUBSCRIBE 命令则负责解除客户端和被退订频道之间的关联。

【示例】订阅指定频道示例

打开客户端一,执行以下命令

1
2
3
4
5
6
7
8
> SUBSCRIBE first second
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "first"
3) (integer) 1
1) "subscribe"
2) "second"
3) (integer) 2

打开客户端二,执行以下命令

1
2
> PUBLISH second Hello
1) "1"

此时,客户端一会收到以下内容

1
2
3
1) "message"
2) "second"
3) "Hello"

(2)基于模式(Pattern)的发布订阅

服务器状态在 pubsub_patterns 链表保存了所有模式的订阅关系: PSUBSCRIBE 命令负责将客户端和被订阅的模式记录到这个链表中, 而 UNSUBSCRIBE 命令则负责移除客户端和被退订模式在链表中的记录。

【示例】订阅符合指定模式的频道

打开客户端一,执行以下命令

1
2
3
4
5
> PSUBSCRIBE news.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "news.*"
3) (integer) 1

打开客户端二,执行以下命令

1
2
> PUBLISH news.A Hello
1) "1"

打开客户端三,执行以下命令

1
2
> PUBLISH news.B World
1) "1"

此时,客户端一会收到以下内容

1
2
3
4
5
6
7
8
1) "pmessage"
2) "news.*"
3) "news.A"
4) "Hello"
1) "pmessage"
2) "news.*"
3) "news.B"
4) "World"

发布订阅命令

Redis 提供了以下与订阅发布有关的命令:

命令 描述
SUBSCRIBE 订阅指定频道
UNSUBSCRIBE 取消订阅指定频道
PSUBSCRIBE 订阅符合指定模式的频道
PUNSUBSCRIBE 取消订阅符合指定模式的频道
PUBLISH 发送信息到指定的频道
PUBSUB 查看发布订阅状态

参考资料

《HBase: A NoSQL database》笔记

简介

HBase 是一种 NoSQL 数据库,它是Java版本的 Google’s Big Table 实现,它原本是 Hadoop 的子项目,现在已独立出来,并成为 apache 的顶级项目。

HBase 的设计目标是用于存储大规模数据集。HBase 是列式数据库,与传统行式数据库相比,其非常适合用于存储稀疏性的数据。

HBase 是基于 HDFS 实现的。

HBase 和历史

HBase 关键特性:

  • 水平扩展
  • 分区容错性
  • 支持并行处理
  • 支持 HDFS 和 MapReduce
  • 近实时查询
  • 适用于存储大规模数据集
  • 适用于存储稀疏型数据(宽表)
  • 表的动态负载均衡
  • 对于大规模的查询,支持块缓存和布隆过滤器

HBase 发展历史

2007 - Mike Cafarella 发布 BigTable 的开源实现——HBase

2008 ~ 2010 - HBase 成为 Apache 顶级项目。

HBase 数据结构和架构

HBase 表可以用于 MapReduce 任务的输入、输出对象。

HBase 由行、列族、列、时间戳组成。

HBase 表会被分成多个分区,每个分区会定义起始key、结束key。它们被存于 HDFS 文件中。

HBase 的架构通常为一个 master server,多个 region server,以及 ZooKeeper 集群。

  • master server
    • 在 ZooKeeper 的帮助下,为分区分配 region server,控制 region server 的负载均衡。
    • 负责 schema 的变更
    • 管理和监控 Hadoop 集群
  • region server
    • region server 负责处理来自客户端的 CRUD 操作
    • region server 包括内存存储和 HFile
    • region server 运行在 HDFS 的数据节点上
    • region server 有四个核心组件:Block cache(读缓存)、MemStore(写缓存)、WAL、HFile(存储行数据,键值对结构)
  • Zookeeper
    • 当 region server 宕机并重新工作时,HBase 会使用 ZooKeeper 作为协调工具,对其进行恢复
    • Zookeeper 是客户端和 master server 的中心,它维护着 master server 和 region server 注册的元数据信息。例如:有多少有效的 region server;任意 region server 持有哪些 data node
    • ZooKeeper 可以用于追踪服务器错误

HBase 和大数据

HBase 相比于其他 NoSQL,最显著的优势在于,它属于 Hadoop 生态体系中的重要一环,被广泛用于大数据领域。但是,近些年,有 MongoDB、Cassandra 等一些数据库挑战着其地位。

HBase 的应用

Facebook 的消息平台使用 HBase 存储数据,每月产生约 13.5 亿条信息。

HBase 还被用于存储各种海量操作数据。

HBase 的挑战和限制

HBase 采用主从架构,一旦 master server 不可用,需要很长时间才能恢复。

HBase 不支持二级索引。

参考资料

《The Log-Structured Merge-Tree (LSM-Tree)》笔记

LSM 被广泛应用于很多以文件结构存储数据的数据库,如:HBase, Cassandra, LevelDB, SQLite。

LSM 的设计目标:通过顺序写来提高写操作吞吐量,替代传统的 B+ 树或 ISAM。

参考资料

Redis 内存管理

关键词:定时删除惰性删除定期删除LRULFU

Redis 过期删除

Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。

设置键的生存时间或过期时间

Redis 中,和键的生存时间相关的命令如下所示:

命令 描述
EXPIRE 设置 key 的过期时间,单位为秒
PEXPIRE 设置 key 的过期时间,单位为毫秒
EXPIREAT 设置 key 的过期时间为指定的秒级时间戳
PEXPIREAT 设置 key 的过期时间为指定的毫秒级时间戳
TTL 返回 key 的剩余生存时间,单位为秒
PTTL 返回 key 的剩余生存时间,单位为毫秒
PERSIST 移除 key 的过期时间,key 将持久保持

【示例】EXPIRE、TTL 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> set key value
OK
# 设置 key 的生存时间为 60s
> expire key 60
(integer) 1
# 查看 key 的剩余生存时间
> ttl key
(integer) 58
# 60s 之内
> get key
"value"
# 60s 之外
> get key
(nil)

【示例】EXPIREAT、TTL 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> set key value
OK
# 设置 key 的生存时间为 1692419299
> expireat key 1692419299
(integer) 1
# 查看 key 的剩余生存时间
> ttl key
(integer) 9948
# 1692419299 之前
> get key
"value"
# 1692419299 之后
> get key
(nil)

如何保存过期时间

在 Redis 中,redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,这个字典称为过期字典:

  • 过期字典的键是一个指针,这个指针指向某个键对象
  • 过期字典的值是一个 long long 类型的整数,这个整数保存了键的过期时间——一个毫秒精度的 UNIX 时间戳。
1
2
3
4
5
6
7
8
9
10
typedef struct redisDb {

// 数据库键空间,保存着数据库中的所有键值对
dict *dict;

// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires;

// ...
} redisDb;

下图是一个带有过期字典的示例:

当执行 EXPIREPEXPIREEXPIREATPEXPIREAT 命令,Redis 都会将其转为 PEXPIREAT 形式的时间戳,然后维护在 expires 字典中。

过期键的判定

过期键的判定流程如下:

  • 检查指定 key 是否存在于过期字典;如果存在,则取得 key 的过期时间。
  • 检查当前时间戳是否大于 key 的过期时间:如果是,key 已过期;反之,key 未过期。

过期删除策略

  • 定时删除 - 在设置 key 的过期时间的同时,创建一个定时器,让定时器在 key 的过期时间来临时,立即执行对 key 的删除操作。
    • 优点 - 保证过期 key 被尽可能快的删除,释放内存。
    • 缺点 - 如果过期 key 较多,可能会占用相当一部分的 CPU,从而影响服务器的吞吐量和响应时延
  • 惰性删除 - 放任 key 过期不管,但是每次访问 key 时,都检查 key 是否过期,如果过期的话,就删除该 key ;如果没有过期,就返回该 key。
    • 优点 - 占用 CPU 最少。程序只会在读写键时,对当前键进行过期检查,因此不会有额外的 CPU 开销。
    • 缺点 - 过期的 key 可能因为没有被访问,而一直无法释放,造成内存的浪费,有内存泄漏的风险
  • 定期删除 - 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期 key 。至于要删除多少过期 key ,以及要检查多少个数据库,则由算法决定。定期删除是前两种策略的一种折中方案。定期删除策略的难点是删除操作执行的时长和频率。
    • 执行太频或执行时间过长,就会出现和定时删除相同的问题;
    • 执行太少或执行时间过短,就会出现和惰性删除相同的问题;

Redis 的过期删除策略

Redis 同时采用了惰性删除和定期删除策略,以此在合理使用 CPU 和内存之间取得平衡。

Redis 定期删除策略的实现 - 由 redis.c/activeExpireCycle 函数实现,每当 Redis 周期性执行 redis.c/serverCron 函数时,activeExpireCycle 函数就会被调用。activeExpireCycle 函数会在规定时间内,遍历各个数据库,从 expires 字典中随机检查一部分键的过期时间,并删除过期的键。

Redis 惰性删除策略的实现 - 由 db.c/expireIfNeeded 函数实现,所有读写命令在执行之前都会调用 expireIfNeeded 函数对输入键进行检查:如果输入键已过期,将输入键从数据库中删除;否则,什么也不做。

AOF、RDB 和复制对过期键的处理

  • 生成 RDB 文件 - 执行 SAVE 命令或者 BGSAVE 命令,所产生的新 RDB 文件“不会包含已经过期的键”
  • 载入 RDB 文件 - 主服务器“不会载入已过期的键”从服务器会载入“会载入已过期的键”
  • 生成 AOF 文件 - 当一个过期键未被删除时,不会影响 AOF 文件;当一个过期键被删除之后, 服务器会追加一条 DEL 命令到现有 AOF 文件的末尾, 显式地删除过期键。
  • 重写 AOF 文件 - 执行 BGREWRITEAOF 命令所产生的重写 AOF 文件“不会包含已经过期的键”
  • 复制 - 当主服务器删除一个过期键之后, 它会向所有从服务器发送一条 DEL 命令, 显式地删除过期键。从服务器即使发现过期键, 也不会自作主张地删除它, 而是等待主节点发来 DEL 命令, 这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当 Redis 命令对数据库进行修改之后, 服务器会根据配置, 向客户端发送数据库通知。

Redis 内存淘汰

Redis 内存淘汰要点

  • 失效时间 - 作为一种定期清理无效数据的重要机制,在 Redis 提供的诸多命令中,EXPIREEXPIREATPEXPIREPEXPIREAT 以及 SETEXPSETEX 均可以用来设置一条键值对的失效时间。而一条键值对一旦被关联了失效时间就会在到期后自动删除(或者说变得无法访问更为准确)。
  • 最大缓存 - Redis 允许通过 maxmemory 参数来设置内存最大值。当内存达设定的阀值,就会触发内存淘汰
  • 内存淘汰 - 内存淘汰是为了更好的利用内存——清理部分缓存,以此换取内存的利用率,即尽量保证 Redis 缓存中存储的是热点数据。
  • 非精准的 LRU - 实际上 Redis 实现的 LRU 并不是可靠的 LRU,也就是名义上我们使用 LRU 算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的。

Redis 内存淘汰策略

内存淘汰只是 Redis 提供的一个功能,为了更好地实现这个功能,必须为不同的应用场景提供不同的策略,内存淘汰策略讲的是为实现内存淘汰我们具体怎么做,要解决的问题包括淘汰键空间如何选择?在键空间中淘汰键如何选择?

Redis 提供了下面几种内存淘汰策略供用户选:

  • 不淘汰
    • noeviction - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。这是 Redis 默认的策略。
  • 在过期键中进行淘汰
    • volatile-random - 在设置了过期时间的键空间中,随机移除某个 key。
    • volatile-ttl - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。
    • volatile-lru - 在设置了过期时间的键空间中,优先移除最近未使用的 key。
    • volatile-lfu (Redis 4.0 新增)- 淘汰所有设置了过期时间的键值中,最少使用的键值。
  • 在所有键中进行淘汰
    • allkeys-lru - 在主键空间中,优先移除最近未使用的 key。
    • allkeys-random - 在主键空间中,随机移除某个 key。
    • allkeys-lfu (Redis 4.0 新增) - 淘汰整个键值中最少使用的键值。

如何选择淘汰策略

  • 如果数据呈现幂等分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lruallkeys-lfu
  • 如果数据呈现平均分布,也就是所有的数据访问频率都相同,则使用 allkeys-random
  • 若 Redis 既用于缓存,也用于持久化存储时,适用 volatile-lruvolatile-lfuvolatile-random。但是,这种情况下,也可以部署两个 Redis 集群来达到同样目的。
  • 为 key 设置过期时间实际上会消耗更多的内存。因此,如果条件允许,建议使用 allkeys-lruallkeys-lfu,从而更高效的使用内存。

参考资料

Redis 数据结构

关键词:对象SDS链表字典跳表整数集合压缩列表

SDS

SDS 简介

SDS 是 Simple Dynamic String 的缩写,即简单动态字符串。Redis 为 SDS 做了一些优化,以替代 C 字符串来表示字符串内容。此外,SDS 还被 Redis 用作缓冲区(buffer),如:AOF 模块中的 AOF 缓冲区;客户端状态中的输入缓冲区。

SDS 相比 C 字符串,具有以下优势:

C 字符串 SDS
获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。
只能保存文本数据。 可以保存文本或者二进制数据。
可以使用所有 <string.h> 库中的函数。 可以使用一部分 <string.h> 库中的函数。

SDS 实现

每个 sds.h/sdshdr 结构表示一个 SDS 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct sdshdr {

// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;

// 记录 buf 数组中未使用字节的数量
int free;

// 字节数组,用于保存字符串
char buf[];

};

SDS 遵循 C 字符串以空字符结尾的惯例, 保存空字符的 1 字节空间不计算在 SDS 的 len 属性里面, 并且为空字符分配额外的 1 字节空间, 以及添加空字符到字符串末尾等操作都是由 SDS 函数自动完成的, 所以这个空字符对于 SDS 的使用者来说是完全透明的。

SDS 特性

SDS 与 C 字符串相比,做了一些优化,具有以下优势:

常数复杂度获取字符串长度

  • C 字符串 - 因为 C 字符串并不记录自身的长度信息, 所以为了获取一个 C 字符串的长度, 程序必须遍历整个字符串, 对遇到的每个字符进行计数, 直到遇到代表字符串结尾的空字符为止, 这个操作的复杂度为 O(N)
  • SDS - 因为 SDS 在 len 属性中记录了 SDS 本身的长度, 所以获取一个 SDS 长度的复杂度仅为 O(1) 。设置和更新 SDS 长度的工作是由 SDS 的 API 在执行时自动完成的, 使用 SDS 无须进行任何手动修改长度的工作。

杜绝缓冲区溢出

  • C 字符串 - C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。
  • SDS - 当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求, 如果不满足的话, API 会自动将 SDS 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作, 所以使用 SDS 既不需要手动修改 SDS 的空间大小, 也不会出现前面所说的缓冲区溢出问题。

减少修改字符串长度时所需的内存重分配次数

  • C 字符串 - 对于一个包含了 N 个字符的 C 字符串来说, 这个 C 字符串的底层实现总是一个 N+1 个字符长的数组(额外的一个字符空间用于保存空字符)。因为 C 字符串的长度和底层数组的长度之间存在着这种关联性, 所以每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作。
    • 增长字符串时,如果没有内存重分配,就会产生缓冲区溢出。
    • 缩减字符串是,如果没有内存重分配,就会产生内存泄露。
  • SDS - 因为内存重分配涉及复杂的算法, 并且可能需要执行系统调用, 所以它通常是一个比较耗时的操作。SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联: 在 SDS 中, buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节, 而这些字节的数量就由 SDS 的 free 属性记录。通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略。
    • 空间预分配 - 空间预分配用于优化 SDS 的字符串增长操作。 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。通过空间预分配, SDS 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。
    • 惰性空间 - 惰性空间释放用于优化 SDS 的字符串缩短操作。当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。通过惰性空间释放策略, SDS 避免了缩短字符串时所需的内存重分配操作, 并为将来可能有的增长操作提供了优化。

二进制安全

  • C 字符串 - C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
  • SDS - SDS 的 API 都是二进制安全的(binary-safe): 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。通过使用二进制安全的 SDS , 使得 Redis 不仅可以保存文本数据, 还可以保存任意格式的二进制数据

兼容部分 C 字符串函数

虽然 SDS 的 API 都是二进制安全的, 但也会遵循 C 字符串惯例,将保存的数据的末尾设置为空字符, 并且总会在为 buf 数组分配空间时多分配一个字节来容纳这个空字符, 这是为了让那些保存文本数据的 SDS 可以重用一部分 <string.h> 库定义的函数。因此,SDS 可以兼容部分 C 字符串函数。

链表

链表简介

链表被广泛用于实现 Redis 的各种功能,比如 List 键,订阅与发布,慢查询,监视器等。此外,Redis 服务器本身还使用链表来保存多个客户端的状态信息, 以及使用链表来构建客户端输出缓冲区(output buffer)。

由于 C 语言没有内置的链表,因此 Redis 自实现了一个链表:Redis 的链表实现其实就是一个双链表

  • 每个链表使用一个 list 结构来表示,这个结构带有表头节点指针、表尾节点指针、以及链表长度等信息。
  • 因为链表表头节点的前置节点和表尾节点的后置节点都指向 NULL,所以 Redis 的链表实现是无环链表。
  • 通过为链表设置不同的类型特定函数,Redis 的链表可以用于保存各种不同类型的值。

链表实现

每个链表节点由一个 adlist.h/listNode 结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以 Redis 的链表实现是双链表。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct listNode {

// 前置节点
struct listNode *prev;

// 后置节点
struct listNode *next;

// 节点的值
void *value;

} listNode;

多个 listNode 可以通过 prevnext 指针组成双链表。

虽然仅仅使用多个 listNode 结构就可以组成链表, 但使用 adlist.h/list 来持有链表的话, 操作起来会更方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct list {

// 表头节点
listNode *head;

// 表尾节点
listNode *tail;

// 链表所包含的节点数量
unsigned long len;

// 节点值复制函数
void *(*dup)(void *ptr);

// 节点值释放函数
void (*free)(void *ptr);

// 节点值对比函数
int (*match)(void *ptr, void *key);

} list;

list 结构为链表提供了表头指针 head 、表尾指针 tail , 以及链表长度计数器 len , 而 dupfreematch 成员则是用于实现多态链表所需的类型特定函数:

  • dup 函数 - 用于复制链表节点所保存的值;
  • free 函数 - 用于释放链表节点所保存的值;
  • match 函数 - 用于对比链表节点所保存的值和另一个输入值是否相等。

字典

字典简介

字典是一种用于保存键值对(key-value pair)的抽象数据结构。字典中的每个键都是独一无二的, 程序可以在字典中根据键查找与之关联的值, 或者通过键来更新值, 又或者根据键来删除整个键值对, 等等。

由于 C 语言没有内置的链表,因此 Redis 自实现了一个字典。

字典被广泛用于实现 Redis 的各种功能, 其中包括数据库和 Hash 键。

字典实现

Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。

Redis 字典所使用的哈希表由 dict.h/dictht 结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dictht {

// 哈希表数组
dictEntry **table;

// 哈希表大小
unsigned long size;

// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;

// 该哈希表已有节点的数量
unsigned long used;

} dictht;
  • table 属性是一个数组, 数组中的每个元素都是一个指向 dict.h/dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对。
  • size 属性记录了哈希表的大小, 也即是 table 数组的大小, 而 used 属性则记录了哈希表目前已有节点(键值对)的数量。
  • sizemask 属性的值总是等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。

哈希表节点使用 dictEntry 结构表示, 每个 dictEntry 结构都保存着一个键值对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dictEntry {

// 键
void *key;

// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;

// 指向下个哈希表节点,形成链表
struct dictEntry *next;

} dictEntry;
  • key 属性保存着键值对中的键, 而 v 属性则保存着键值对中的值, 其中键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。
  • next 属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一次, 以此来解决键冲突(collision)的问题。

Redis 中的字典由 dict.h/dict 结构表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dict {

// 类型特定函数
dictType *type;

// 私有数据
void *privdata;

// 哈希表
dictht ht[2];

// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:

  • type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。
  • privdata 属性则保存了需要传给那些类型特定函数的可选参数。
  • ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。
  • rehashidx 属性记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1

哈希算法

当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用 MurmurHash2 算法来计算键的哈希值

Redis 计算哈希值和索引值的方法如下:

1
2
3
4
5
6
# 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);

# 使用哈希表的 sizemask 属性和哈希值,计算出索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

哈希冲突

当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。

Redis 使用链地址法(separate chaining)来解决哈希冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。

rehash

rehash 的步骤

  1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值)。
  2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
  3. ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

rehash 的条件

当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5

其中哈希表的负载因子可以通过公式计算得出:

1
2
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

渐进式 rehash

渐进式 rehash 的详细步骤:

  1. ht[1] 分配空间, 让字典同时持有 ht[0]ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

跳表

跳表简介

跳表(skiplist)是一种有序数据结构, 它通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。

跳表支持平均 O(log N) 最坏 O(N) 复杂度的节点查找, 还可以通过顺序性操作来批量处理节点。

在大部分情况下, 跳表的效率可以和平衡树相媲美, 并且因为跳表的实现比平衡树要来得更为简单, 所以有不少程序都使用跳表来代替平衡树。

Redis 使用跳表作为有序集合键的底层实现之一: 如果一个有序集合包含的元素数量比较多, 又或者有序集合中元素的成员(member)是比较长的字符串时, Redis 就会使用跳表来作为有序集合键的底层实现。

此外,Redis 还在集群节点中用跳表作为内部数据结构。

跳表实现

Redis 的跳表实现由 zskiplistzskiplistNode 两个结构组成, 其中 zskiplist 用于保存跳表信息(比如表头节点、表尾节点、长度), 而 zskiplistNode 则用于表示跳表节点。

zskiplist 结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct zskiplist {

// 表头节点和表尾节点
struct zskiplistNode *header, *tail;

// 表中节点的数量
unsigned long length;

// 表中层数最大的节点的层数
int level;

} zskiplist;
  • headertail 指针分别指向跳表的表头和表尾节点, 通过这两个指针, 程序定位表头节点和表尾节点的复杂度为 O(1) 。
  • 通过使用 length 属性来记录节点的数量, 程序可以在 O(1) 复杂度内返回跳表的长度。
  • level 属性则用于在 O(1) 复杂度内获取跳表中层高最大的那个节点的层数量, 注意表头节点的层高并不计算在内。每个跳表节点的层高都是 132 之间的随机数

跳表节点的实现由 redis.h/zskiplistNode 结构定义:

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 zskiplistNode {

// 后退指针
struct zskiplistNode *backward;

// 分值
double score;

// 成员对象
robj *obj;

// 层
struct zskiplistLevel {

// 前进指针
struct zskiplistNode *forward;

// 跨度
unsigned int span;

} level[];

} zskiplistNode;
  • 层(level):每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
  • 后退(backward)指针:它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
  • 分值(score):在跳表中,节点按各自所保存的分值从小到大排列。在同一个跳表中, 多个节点可以包含相同的分值, 但每个节点的成员对象必须是唯一的。跳表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序。
  • 成员对象(obj):各个节点中的 o1o2o3 是节点所保存的成员对象。

整数集合

整数集合简介

整数集合(intset)是集合键的底层实现之一。当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。

整数集合的底层实现为数组, 这个数组以有序、无重复的方式保存集合元素, 在有需要时, 程序会根据新添加元素的类型, 改变这个数组的类型

升级操作为整数集合带来了操作上的灵活性, 并且尽可能地节约了内存

整数集合只支持升级操作, 不支持降级操作

整数集合实现

整数集合是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 int16_tint32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素。

每个 intset.h/intset 结构表示一个整数集合:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct intset {

// 编码方式
uint32_t encoding;

// 集合包含的元素数量
uint32_t length;

// 保存元素的数组
int8_t contents[];

} intset;
  • contents 数组是整数集合的底层实现: 整数集合的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。
  • length 属性记录了整数集合包含的元素数量, 也即是 contents 数组的长度。
  • 虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组并不保存任何 int8_t 类型的值 —— contents 数组的真正类型取决于 encoding 属性的值:
    • 如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t 类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )。
    • 如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t 类型的整数值 (最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。
    • 如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t 类型的整数值 (最小值为 -9,223,372,036,854,775,808 ,最大值为 9,223,372,036,854,775,807 )。

整数集合升级

每当我们要将一个新元素添加到整数集合里面, 并且新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级(upgrade), 然后才能将新元素添加到整数集合里面。

升级整数集合并添加新元素共分为三步进行:

  1. 根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。

因为每次向整数集合添加新元素都可能会引起升级, 而每次升级都需要对底层数组中已有的所有元素进行类型转换, 所以向整数集合添加新元素的时间复杂度为 O(N) 。

压缩列表

压缩列表简介

压缩列表是一种为节约内存而开发的顺序型数据结构

压缩列表(ziplist)被用作列表键和哈希键的底层实现之一

  • 当一个列表键只包含少量列表项, 并且每个列表项要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做列表键的底层实现。
  • 当一个哈希键只包含少量键值对, 并且每个键值对的键和值要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做哈希键的底层实现。

压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值

添加新节点到压缩列表, 或者从压缩列表中删除节点, 可能会引发连锁更新操作, 但这种操作出现的几率并不高。

压缩列表实现

压缩列表各个组成部分的详细说明

属性 类型 长度 用途
zlbytes uint32_t 4 字节 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
zltail uint32_t 4 字节 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
zllen uint16_t 2 字节 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。
entryX 列表节点 不定 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlend uint8_t 1 字节 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

对象

Redis 并没有直接使用这些数据结构来实现键值对数据库, 而是基于这些数据结构创建了一个对象系统, 这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象, 每种对象都用到了至少一种我们前面所介绍的数据结构。

对象简介

Redis 数据库中的每个键值对的键和值都是一个对象。

Redis 共有字符串、列表、哈希、集合、有序集合五种类型的对象, 每种类型的对象至少都有两种或以上的编码方式, 不同的编码可以在不同的使用场景上优化对象的使用效率。

服务器在执行某些命令之前, 会先检查给定键的类型能否执行指定的命令, 而检查一个键的类型就是检查键的值对象的类型。

基于引用计数技术的内存回收机制 - Redis 的对象系统带有引用计数实现的内存回收机制, 当一个对象不再被使用时, 该对象所占用的内存就会被自动释放。

基于引用计数技术的对象共享机制 - Redis 会共享值为 09999 的字符串对象。

计算数据库键的空转时长 - 对象会记录自己的最后一次被访问的时间, 这个时间可以用于计算对象的空转时间。

对象的类型

Redis 使用对象来表示数据库中的键和值。每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。

Redis 中的每个对象都由一个 redisObject 结构表示, 该结构中和保存数据有关的三个属性分别是 type 属性、 encoding 属性和 ptr 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct redisObject {

// 类型
unsigned type:4;

// 编码
unsigned encoding:4;

// 指向底层实现数据结构的指针
void *ptr;

// ...

} robj;

对象的 type 属性记录了对象的类型,有以下类型:

对象 对象 type 属性的值 TYPE 命令的输出
字符串对象 REDIS_STRING "string"
列表对象 REDIS_LIST "list"
哈希对象 REDIS_HASH "hash"
集合对象 REDIS_SET "set"
有序集合对象 REDIS_ZSET "zset"

Redis 数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。

【示例】通过 TYPE 命令查看数据库键的值对象的类型

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
# 键为字符串对象,值为字符串对象
> SET msg "hello world"
OK
> TYPE msg
string

# 键为字符串对象,值为列表对象
> RPUSH numbers 1 3 5
(integer) 6
> TYPE numbers
list

# 键为字符串对象,值为哈希对象
> HMSET profile name Tome age 25 career Programmer
OK
> TYPE profile
hash

# 键为字符串对象,值为集合对象
> SADD fruits apple banana cherry
(integer) 3
> TYPE fruits
set

# 键为字符串对象,值为有序集合对象
> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
> TYPE price
zset

对象的编码

对象的 ptr 指针指向对象的底层实现数据结构, 而这些数据结构由对象的 encoding 属性决定。

encoding 属性记录了对象所使用的编码, 也即是说这个对象使用了什么数据结构作为对象的底层实现。

Redis 中每种类型的对象都至少使用了两种不同的编码,不同的编码可以在不同的使用场景上优化对象的使用效率

Redis 支持的编码如下所示:

类型 编码 对象 OBJECT ENCODING 命令输出
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象。 “int”
REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象。 “embstr”
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象。 “raw”
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象。 “ziplist”
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象。 “linkedlist”
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象。 “ziplist”
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象。 “hashtable”
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象。 “intset”
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象。 “hashtable”
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象。 “ziplist”
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳表和字典实现的有序集合对象。 “skiplist”

【示例】使用 OBJECT ENCODING 命令可以查看数据库键的值对象的编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> SET msg "hello wrold"
OK
> OBJECT ENCODING msg
"embstr"

> SET story "long long long long long long ago ..."
OK
> OBJECT ENCODING story
"raw"

> SADD numbers 1 3 5
(integer) 3
> OBJECT ENCODING numbers
"intset"

> SADD numbers "seven"
(integer) 1
> OBJECT ENCODING numbers
"hashtable"

类型检查与命令多态

Redis 中用于操作键的命令基本上可以分为两种类型。

  • 多态命令 - 可以对任何类型的键执行。如 DEL、 EXPIRE 、 RENAME 、 TYPE 、 OBJECT 等命令。
  • 特定类型命令
    • SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行;
    • HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行;
    • RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行;
    • SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行;
    • ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行;

为了确保只有指定类型的键可以执行某些特定的命令,Redis 在执行一个类型特定的命令之前, Redis 会先检查输入键的类型是否正确, 然后再决定是否执行给定的命令。类型特定命令所进行的类型检查是通过 redisObject 结构的 type 属性来实现的:

  • 在执行一个类型特定命令之前, 服务器会先检查输入数据库键的值对象是否为执行命令所需的类型, 如果是的话, 服务器就对键执行指定的命令;
  • 否则, 服务器将拒绝执行命令, 并向客户端返回一个类型错误。

Redis 除了会根据值对象的类型来判断键是否能够执行指定命令之外, 还会根据值对象的编码方式, 选择正确的命令实现代码来执行命令。

内存回收

由于 C 语言不支持内存回收,Redis 内部实现了一套基于引用计数的内存回收机制。

每个对象的引用计数信息由 redisObject 结构的 refcount 属性记录:

1
2
3
4
5
6
7
8
9
10
typedef struct redisObject {

// ...

// 引用计数
int refcount;

// ...

} robj;

对象的引用计数信息会随着对象的使用状态而不断变化:

  • 在创建一个新对象时, 引用计数的值会被初始化为 1
  • 当对象被一个新程序使用时, 它的引用计数值会被增一;
  • 当对象不再被一个程序使用时, 它的引用计数值会被减一;
  • 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。

对象共享

在 Redis 中, 让多个键共享同一个值对象需要执行以下两个步骤:

  1. 将数据库键的值指针指向一个现有的值对象;
  2. 将被共享的值对象的引用计数增一。

共享对象机制对于节约内存非常有帮助, 数据库中保存的相同值对象越多, 对象共享机制就能节约越多的内存。

Redis 会在初始化服务器时, 共享值为 09999 的字符串对象。

对象的空转时长

redisObjectlru 属性记录了对象最后一次被命令程序访问的时间:

1
2
3
4
5
6
7
8
9
typedef struct redisObject {

// ...

unsigned lru:22;

// ...

} robj;

OBJECT IDLETIME 命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的 lru 时间计算得出的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> SET msg "hello world"
OK

# 等待一小段时间
> OBJECT IDLETIME msg
(integer) 20

# 等待一阵子
> OBJECT IDLETIME msg
(integer) 180

# 访问 msg 键的值
> GET msg
"hello world"

# 键处于活跃状态,空转时长为 0
> OBJECT IDLETIME msg
(integer) 0

注意

OBJECT IDLETIME 命令的实现是特殊的, 这个命令在访问键的值对象时, 不会修改值对象的 lru 属性。

除了可以被 OBJECT IDLETIME 命令打印出来之外, 键的空转时长还有另外一项作用: 如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru , 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。

参考资料

《极客时间教程 - 玩转 Spring 全家桶》笔记

第一章:初识 Spring (4 讲)

01 | Spring 课程介绍

02 | 一起认识 Spring 家族的主要成员

Spring Framework - 用于构建企业级应用的轻量级一站式解决方案

Spring Boot - 快速构建基于 Spring 的应用程序

Spring Cloud - 简化分布式系统的开发

03 | 跟着 Spring 了解技术趋势

04 | 编写你的第一个 Spring 程序

第二章:JDBC 必知必会 (10 讲)

05 | 如何配置单数据源

直接配置所需的 Bean

数据源相关

  • DataSource(根据选择的连接池实现决定)

事务相关(可选)

  • PlatformTransactionManager(DataSourceTransactionManager)
  • TransactionTemplate

操作相关(可选)

  • JdbcTemplate

Spring Boot 做了哪些配置

DataSourceAutoConfiguration

  • 配置 DataSource

DataSourceTransactionManagerAutoConfiguration

  • 配置 DataSourceTransactionManager

JdbcTemplateAutoConfiguration

  • 配置 JdbcTemplate

符合条件时才进行配置

数据源相关配置属性

通用

  • spring.datasource.url=jdbc:mysql://localhost/test
  • spring.datasource.username=dbuser
  • spring.datasource.password=dbpass
  • spring.datasource.driver-class-name=com.mysql.jdbc.Driver(可选)

初始化内嵌数据库

  • spring.datasource.initialization-mode=embedded|always|never
  • spring.datasource.schema 与 spring.datasource.data 确定初始化 SQL ⽂文件
  • spring.datasource.platform=hsqldb | h2 | oracle | mysql | postgresql(与前者对应)

06 | 如何配置多数据源

配置多数据源的注意事项

不同数据源的配置要分开

关注每次使用的数据源

  • 有多个 DataSource 时系统如何判断
  • 对应的设施(事务、ORM 等)如何选择 DataSource

Spring Boot 中的多数据源配置

手工配置两组 DataSource 及相关内容

与 Spring Boot 协同工作(二选一)

  • 配置@Primary 类型的 Bean
  • 排除 Spring Boot 的自动配置
  • DataSourceAutoConfiguration
  • DataSourceTransactionManagerAutoConfiguration
  • JdbcTemplateAutoConfiguration

07 | 那些好用的连接池们:HikariCP

在 Spring Boot 中的配置

Spring Boot 2.x

  • 默认使用 HikariCP
  • 配置 spring.datasource.hikari.* 配置

Spring Boot 1.x

  • 默认使用 Tomcat 连接池,需要移除 tomcat-jdbc 依赖
  • spring.datasource.type=com.zaxxer.hikari.HikariDataSource

常用 HikariCP 配置参数

常用配置

  • spring.datasource.hikari.maximumPoolSize=10
  • spring.datasource.hikari.minimumIdle=10
  • spring.datasource.hikari.idleTimeout=600000
  • spring.datasource.hikari.connectionTimeout=30000
  • spring.datasource.hikari.maxLifetime=1800000

其他配置详见 HikariCP 官网

08 | 那些好用的连接池们:Alibaba Druid

数据源配置

直接配置 DruidDataSource

通过 druid-spring-boot-starter

  • spring.datasource.druid.*

Filter 配置

  • spring.datasource.druid.filters=stat,config,wall,log4j (全部使用默认值)

密码加密

  • spring.datasource.password=<加密密码>
  • spring.datasource.druid.filter.config.enabled=true
  • spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=<public-key>

SQL 防注入

  • spring.datasource.druid.filter.wall.enabled=true
  • spring.datasource.druid.filter.wall.db-type=h2
  • spring.datasource.druid.filter.wall.config.delete-allow=false
  • spring.datasource.druid.filter.wall.config.drop-table-allow=false

Druid Filter

  • 用于定制连接池操作的各种环节
  • 可以继承 FilterEventAdapter 以便方便地实现 Filter
  • 修改 META-INF/druid-filter.properties 增加 Filter 配置

09 | 如何通过 Spring JDBC 访问数据库

Spring 的 JDBC 操作类

spring-jdbc

  • core,JdbcTemplate 等相关核心接口和类
  • datasource,数据源相关的辅助类
  • object,将基本的 JDBC 操作封装成对象
  • support,错误码等其他辅助工具

常用的 Bean 注解

通过注解定义 Bean

  • @Component
  • @Repository
  • @Service
  • @Controller
  • @RestController

简单的 JDBC 操作

JdbcTemplate

  • query
  • queryForObject
  • queryForList
  • update
  • execute

SQL 批处理

JdbcTemplate

  • batchUpdate
  • BatchPreparedStatementSetter

NamedParameterJdbcTemplate

  • batchUpdate
  • SqlParameterSourceUtils.createBatch

10 | 什么是 Spring 的事务抽象(上)

11 | 什么是 Spring 的事务抽象(下)

Spring 的事务抽象

一致的事务模型

  • JDBC/Hibernate/myBatis
  • DataSource/JTA

事务抽象的核心接口

PlatformTransactionManager

  • DataSourceTransactionManager
  • HibernateTransactionManager
  • JtaTransactionManager

TransactionDefinition

  • Propagation
  • Isolation
  • Timeout
  • Read-only status

事务传播特性

传播性 描述
PROPAGATION_REQUIRED 0 当前有事务就用当前的,没有就用新的
PROPAGATION_SUPPORTS 1 事务可有可无,不是必须的
PROPAGATION_MANDATORY 2 当前一定要有事务,不然就抛异常
PROPAGATION_REQUIRES_NEW 3 无论是否有事务,都起个新的事务
PROPAGATION_NOT_SUPPORTED 4 不支持事务,按非事务方式运行
PROPAGATION_NEVER 5 不支持事务,如果有事务则抛异常
PROPAGATION_NESTED 6 当前有事务就在当前事务里再起一个事务

事务隔离特性

隔离性 脏读 不可重复读 幻读
ISOLATION_READ_UNCOMMITTED 1 ✔️️️ ✔️️️ ✔️️️
ISOLATION_READ_COMMITTED 2 ✔️️️ ✔️️️
ISOLATION_REPEATABLE_READ 3 ✔️️️
ISOLATION_SERIALIZABLE 4

编程式事务

TransactionTemplate

  • TransactionCallback
  • TransactionCallbackWithoutResult

PlatformTransactionManager

  • 可以传入 TransactionDefinition 进行定义

声明式事务

开启事务注解的方式

  • @EnableTransactionManagement
  • <tx:annotation-driven/>

一些配置

  • proxyTargetClass
  • mode
  • order

@Transactional

  • transactionManager
  • propagation
  • isolation
  • timeout
  • readOnly
  • 怎么判断回滚

12 | 了解 Spring 的 JDBC 异常抽象

Spring 的 JDBC 异常抽象

Spring 会将数据操作的异常转换为 DataAccessException

无论使用何种数据访问方式,都能使用一样的异常

Spring 是怎么认识那些错误码的

通过 SQLErrorCodeSQLExceptionTranslator 解析错误码

ErrorCode 定义

  • org/springframework/jdbc/support/sql-error-codes.xml
  • Classpath 下的 sql-error-codes.xml

13 | 课程答疑(上)

14 | 课程答疑(下)

第三章:O/R Mapping 实践 (9 讲)

15 | 认识 Spring Data JPA

Java Persistence API

JPA 为对象关系映射提供了一种基于 POJO 的持久化模型

  • 简化数据持久化代码的开发工作
  • 为 Java 社区屏蔽不同持久化 API 的差异

Spring Data

在保留底层存储特性的同时,提供相对一致的、基于 Spring 的编程模型

主要模块

  • Spring Data Commons
  • Spring Data JDBC
  • Spring Data JPA
  • Spring Data Redis
  • ……

16 | 定义 JPA 的实体对象

常用 JPA 注解

实体

  • @Entity、@MappedSuperclass
  • @Table(name)

主键

  • @Id
  • @GeneratedValue(strategy, generator)
  • @SequenceGenerator(name, sequenceName)

映射

  • @Column(name, nullable, length, insertable, updatable)
  • @JoinTable(name)、@JoinColumn(name)

关系

  • @OneToOne、@OneToMany、@ManyToOne、@ManyToMany
  • @OrderBy

Lombok

Project Lombok 能够自动嵌入 IDE 和构建工具,提升开发效率

常用功能

  • @Getter / @Setter
  • @ToString
  • @NoArgsConstructor / @RequiredArgsConstructor / @AllArgsConstructor
  • @Data
  • @Builder
  • @Slf4j / @CommonsLog / @Log4j2

17 | 开始我们的线上咖啡馆实战项目:SpringBucks

18 | 通过 Spring Data JPA 操作数据库

Repository

@EnableJpaRepositories

Repository<T, ID> 接口

  • CrudRepository<T, ID>
  • PagingAndSortingRepository<T, ID>
  • JpaRepository<T, ID>

定义查询

根据方法名定义查询

  • find…By… / read…By… / query…By… / get…By…
  • count…By…
  • …OrderBy…[Asc / Desc]
  • And / Or / IgnoreCase
  • Top / First / Distinct

分页查询

  • PagingAndSortingRepository<T, ID>
  • Pageable / Sort
  • Slice<T> / Page<T>

19 | Spring Data JPA 的 Repository 是怎么从接口变成 Bean 的

Repository Bean 是如何创建的

JpaRepositoriesRegistrar

  • 激活了 @EnableJpaRepositories
  • 返回了 JpaRepositoryConfigExtension

RepositoryBeanDefinitionRegistrarSupport.registerBeanDefinitions

  • 注册 Repository Bean(类型是 JpaRepositoryFactoryBean)

RepositoryConfigurationExtensionSupport.getRepositoryConfigurations

  • 取得 Repository 配置

JpaRepositoryFactory.getTargetRepository

  • 创建了 Repository

接口中的方法是如何被解释的

RepositoryFactorySupport.getRepository 添加了 Advice

  • DefaultMethodInvokingMethodInterceptor
  • QueryExecutorMethodInterceptor

AbstractJpaQuery.execute 执行具体的查询

语法解析在 Part 中

20 | 通过 MyBatis 操作数据库

在 Spring 中使用 MyBatis

简单配置

  • mybatis.mapper-locations = classpath*:mapper/**/*.xml
  • mybatis.type-aliases-package = 类型别名的包名
  • mybatis.type-handlers-package = TypeHandler 扫描包名
  • mybatis.configuration.map-underscore-to-camel-case = true

Mapper 的定义与扫描

  • @MapperScan 配置扫描位置
  • @Mapper 定义接口
  • 映射的定义—— XML 与注解

21 | 让 MyBatis 更好用的那些工具:MyBatis Generator

MyBatis Generator(http://www.mybatis.org/generator/)

22 | 让 MyBatis 更好用的那些工具:MyBatis PageHelper

MyBatis PageHepler(https://pagehelper.github.io)

23 | SpringBucks 实战项目进度小结

第四章:NoSQL 实践 (7 讲)

24 | 通过 Docker 辅助开发

Docker 常用命令

镜像相关

  • docker pull <image>
  • docker search <image>

容器相关

  • docker run
  • docker start/stop <容器名>
  • docker ps <容器名>
  • docker logs <容器名>

docker run 的常用选项

docker run [OPTIONS] IMAGE [COMMAND] [ARG…]

选项说明

  • -d,后台运行容器
  • -e,设置环境变量
  • –expose / -p 宿主端口:容器端口
  • –name,指定容器名称
  • –link,链接不同容器
  • -v 宿主目录:容器目录,挂载磁盘卷

国内 Docker 镜像配置

官方 Docker Hub

官方镜像

阿里云镜像

25 | 在 Spring 中访问 MongoDB

Spring 对 MongoDB 的支持

  • Spring Data MongoDB
    • MongoTemplate
    • Repository 支持

Spring Data MongoDB 的基本用法

注解

  • @Document
  • @Id

MongoTemplate

  • save / remove
  • Criteria / Query / Update

Spring Data MongoDB 的 Repository

@EnableMongoRepositories

对应接口

  • MongoRepository<T, ID>
  • PagingAndSortingRepository<T, ID>
  • CrudRepository<T, ID>

26 | 在 Spring 中访问 Redis

Spring 对 Redis 的支持

  • Spring Data Redis
    • 支持的客户端 Jedis / Lettuce
    • RedisTemplate
    • Repository 支持

Jedis 客户端的简单使用

  • Jedis 不是线程安全的
  • 通过 JedisPool 获得 Jedis 实例
  • 直接使用 Jedis 中的方法

27 | Redis 的哨兵与集群模式

  • JedisSentinelPool
  • JedisCluster

28 | 了解 Spring 的缓存抽象

Spring 的缓存抽象

为不同的缓存提供一层抽象

  • 为 Java 方法增加缓存,缓存执行结果
  • 支持 ConcurrentMap、EhCache、Caffeine、JCache(JSR-107)
  • 接口
    • org.springframework.cache.Cache
    • org.springframework.cache.CacheManager

基于注解的缓存

@EnableCaching

  • @Cacheable
  • @CacheEvict
  • @CachePut
  • @Caching
  • @CacheConfig

29 | Redis 在 Spring 中的其他用法

与 Redis 建立连接

配置连接工厂

  • LettuceConnectionFactory 与 JedisConnectionFactory
    • RedisStandaloneConfiguration
    • RedisSentinelConfiguration
    • RedisClusterConfiguration

读写分离

Lettuce 内置支持读写分离

  • 只读主、只读从
  • 优先读主、优先读从

LettuceClientConfiguration

LettucePoolingClientConfiguration

LettuceClientConfigurationBuilderCustomizer

RedisTemplate

RedisTemplate<K, V>

  • opsForXxx()

StringRedisTemplate

Redis Repository

实体注解

  • @RedisHash
  • @Id
  • @Indexed

处理不同类型数据源的 Repository

如何区分这些 Repository

  • 根据实体的注解
  • 根据继承的接口类型
  • 扫描不同的包

30 | SpringBucks 实战项目进度小结

第五章:数据访问进阶 (8 讲)

31 | Project Reactor 介绍(上)

32 | Project Reactor 介绍(下)

一些核心的概念

Operators - Publisher / Subscriber

  • Nothing Happens Until You subscribe()
  • Flux [ 0..N ] - onNext()、onComplete()、onError()
  • Mono [ 0..1 ] - onNext()、onComplete()、onError()

Backpressure

  • Subscription
  • onRequest()、onCancel()、onDispose()

线程调度 Schedulers

  • immediate() / single() / newSingle()
  • elastic() / parallel() / newParallel()

错误处理

  • onError / onErrorReturn / onErrorResume
  • doOnError / doFinally

33 | 通过 Reactive 的方式访问 Redis

Spring Data Redis

Lettuce 能够支持 Reactive 方式

Spring Data Redis 中主要的支持

  • ReactiveRedisConnection
  • ReactiveRedisConnectionFactory
  • ReactiveRedisTemplate
  • opsForXxx()

34 | 通过 Reactive 的方式访问 MongoDB

Spring Data MongoDB

MongoDB 官方提供了支持 Reactive 的驱动

  • mongodb-driver-reactivestreams

Spring Data MongoDB 中主要的支持

  • ReactiveMongoClientFactoryBean
  • ReactiveMongoDatabaseFactory
  • ReactiveMongoTemplate

35 | 通过 Reactive 的方式访问 RDBMS

Spring Data R2DBC

R2DBC (https://spring.io/projects/spring-data-r2dbc)

  • Reactive Relational Database Connectivity

支持的数据库

  • Postgres(io.r2dbc:r2dbc-postgresql)
  • H2(io.r2dbc:r2dbc-h2)
  • Microsoft SQL Server(io.r2dbc:r2dbc-mssql)

一些主要的类

  • ConnectionFactory
  • DatabaseClient
    • execute().sql(SQL)
    • inTransaction(db -> {})
  • R2dbcExceptionTranslator
    • SqlErrorCodeR2dbcExceptionTranslator

R2DBC Repository 支持

一些主要的类

  • @EnableR2dbcRepositories
  • ReactiveCrudRepository<T, ID>
  • @Table / @Id
  • 其中的方法返回都是 Mono 或者 Flux
  • 自定义查询需要自己写 @Query

36 | 通过 AOP 打印数据访问层的摘要(上)

37 | 通过 AOP 打印数据访问层的摘要(下)

Spring AOP 的一些核心概念

概念 含义
Aspect 切面
Join Point 连接点,Spring AOP 里总是代表一次方法执行
Advice 通知,在连接点执行的动作
Pointcut 切入点,说明如何匹配连接点
Introduction 引入,为现有类型声明额外的方法和属性
Target object 目标对象
AOP proxy AOP 代理对象,可以是 JDK 动态代理,也可以是 CGLIB 代理
Weaving 织入,连接切面与目标对象或类型创建代理的过程

常用注解

  • @EnableAspectJAutoProxy
  • @Aspect
  • @Pointcut
  • @Before
  • @After / @AfterReturning / @AfterThrowing
  • @Around
  • @Order

如何打印 SQL

HikariCP

Alibaba Druid

38 | SpringBucks 实战项目进度小结

第六章:Spring MVC 实践 (14 讲)

39 | 编写第一个 Spring MVC Controller

认识 Spring MVC

DispatcherServlet

  • Controller
  • xxxResolver
  • ViewResolver
  • HandlerExceptionResolver
  • MultipartResolver
  • HandlerMapping

Spring MVC 中的常⽤用注解

  • @Controller
  • @RestController
  • @RequestMapping
  • @GetMapping / @PostMapping
  • @PutMapping / @DeleteMapping
  • @RequestBody / @ResponseBody / @ResponseStatus

40 | 理解 Spring 的应用上下文

Spring 的应用程序上下文

关于上下文常用的接口

  • BeanFactory
  • DefaultListableBeanFactory
  • ApplicationContext
  • ClassPathXmlApplicationContext
  • FileSystemXmlApplicationContext
  • AnnotationConfigApplicationContext
  • WebApplicationContext

41 | 理解请求的处理机制

一个请求的大致处理流程

绑定一些 Attribute

  • WebApplicationContext / LocaleResolver / ThemeResolver

处理 Multipart

  • 如果是,则将请求转为 MultipartHttpServletRequest

Handler 处理

  • 如果找到对应 Handler,执行 Controller 及前后置处理器逻辑处理返回的 Model ,呈现视图

42 | 如何定义处理方法(上)

定义映射关系

@Controller

@RequestMapping

  • path / method 指定映射路路径与⽅方法
  • params / headers 限定映射范围
  • consumes / produces 限定请求与响应格式

一些快捷方式

  • @RestController
  • @GetMapping / @PostMapping / @PutMapping / @DeleteMapping / @PatchMapping

定义处理方法

  • @RequestBody / @ResponseBody / @ResponseStatus
  • @PathVariable / @RequestParam / @RequestHeader
  • HttpEntity / ResponseEntity

定义类型转换

自己实现 WebMvcConfigurer

  • Spring Boot 在 WebMvcAutoConfiguration 中实现了一个
  • 添加自定义的 Converter
  • 添加自定义的 Formatter

定义校验

  • 通过 Validator 对绑定结果进行校验
    • Hibernate Validator
  • @Valid 注解
  • BindingResult

Multipart 上传

  • 配置 MultipartResolver
  • Spring Boot 自动配置 MultipartAutoConfiguration
  • 支持类型 multipart/form-data
  • MultipartFile 类型

43 | 如何定义处理方法(下)

44 | Spring MVC 中的视图解析机制(上)

45 | Spring MVC 中的视图解析机制(下)

视图解析的实现基础

ViewResolver 与 View 接口

  • AbstractCachingViewResolver
  • UrlBasedViewResolver
  • FreeMarkerViewResolver
  • ContentNegotiatingViewResolver
  • InternalResourceViewResolver

DispatcherServlet 中的视图解析逻辑

  • initStrategies()
    • initViewResolvers() 初始化了了对应 ViewResolver
  • doDispatch()
    • processDispatchResult()
      • 没有返回视图的话,尝试 RequestToViewNameTranslator
      • resolveViewName() 解析 View 对象

使用 @ResponseBody 的情况

  • 在 HandlerAdapter.handle() 的中完成了 Response 输出
    • RequestMappingHandlerAdapter.invokeHandlerMethod()
      • HandlerMethodReturnValueHandlerComposite.handleReturnValue()
        • RequestResponseBodyMethodProcessor.handleReturnValue()

重定向

两种不同的重定向前缀

  • redirect:
  • forward:

46 | Spring MVC 中的常用视图(上)

Spring MVC 支持的视图

支持的视图列表

配置 MessageConverter

  • 通过 WebMvcConfigurer 的 configureMessageConverters()
  • Spring Boot 自动查找 HttpMessageConverters 进行注册

Spring Boot 对 Jackson 的支持

  • JacksonAutoConfiguration
    • Spring Boot 通过 @JsonComponent 注册 JSON 序列化组件
    • Jackson2ObjectMapperBuilderCustomizer
  • JacksonHttpMessageConvertersConfiguration
    • 增加 jackson-dataformat-xml 以支持 XML 序列化

47 | Spring MVC 中的常用视图(下)

使用 Thymeleaf

添加 Thymeleaf 依赖

  • org.springframework.boot:spring-boot-starter-thymeleaf

Spring Boot 的自动配置

  • ThymeleafAutoConfiguration
    • ThymeleafViewResolver
Thymeleaf 的一些默认配置
  • spring.thymeleaf.cache=true
  • spring.thymeleaf.check-template=true
  • spring.thymeleaf.check-template-location=true
  • spring.thymeleaf.enabled=true
  • spring.thymeleaf.encoding=UTF-8
  • spring.thymeleaf.mode=HTML
  • spring.thymeleaf.servlet.content-type=text/html
  • spring.thymeleaf.prefix=classpath:/templates/
  • spring.thymeleaf.suffix=.html

48 | 静态资源与缓存

Spring Boot 中的静态资源配置

核心逻辑

  • WebMvcConfigurer.addResourceHandlers()

常用配置

  • spring.mvc.static-path-pattern=/**
  • spring.resources.static-locations=classpath:/META-INF/
    resources/,classpath:/resources/,classpath:/static/,classpath:/public/

Spring Boot 中的缓存配置

常用配置(默认时间单位都是秒)

  • ResourceProperties.Cache
  • spring.resources.cache.cachecontrol.max-age=时间
  • spring.resources.cache.cachecontrol.no-cache=true/false
  • spring.resources.cache.cachecontrol.s-max-age=时间

49 | Spring MVC 中的异常处理机制

Spring MVC 的异常解析

核心接口

  • HandlerExceptionResolver

实现类

  • SimpleMappingExceptionResolver
  • DefaultHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • ExceptionHandlerExceptionResolver

异常处理方法

处理方法

  • @ExceptionHandler

添加位置

  • @Controller / @RestController
  • @ControllerAdvice / @RestControllerAdvice

50 | 了解 Spring MVC 的切入点

Spring MVC 的拦截器

核心接口

  • HandlerInteceptor
    • boolean preHandle()
    • void postHandle()
    • void afterCompletion()

针对 @ResponseBody 和 ResponseEntity 的情况

  • ResponseBodyAdvice

针对异步请求的接口

  • AsyncHandlerInterceptor

拦截器的配置方式

常规方法

  • WebMvcConfigurer.addInterceptors()

Spring Boot 中的配置

  • 创建一个带 @Configuration 的 WebMvcConfigurer 配置类
  • 不能带 @EnableWebMvc(想彻底自己控制 MVC 配置除外)

51 | SpringBucks 实战项目进度小结

52 | 课程答疑

第七章:访问 Web 资源 (5 讲)

53 | 通过 RestTemplate 访问 Web 资源

Spring Boot 中的 RestTemplate

  • Spring Boot 中没有自动配置 RestTemplate
  • Spring Boot 提供了 RestTemplateBuilder
  • RestTemplateBuilder.build()

常用方法

GET 请求

  • getForObject() / getForEntity()

POST 请求

  • postForObject() / postForEntity()

PUT 请求

  • put()

DELETE 请求

  • delete()

构造 URI

构造 URI

  • UriComponentsBuilder

构造相对于当前请求的 URI

  • ServletUriComponentsBuilder

构造指向 Controller 的 URI

  • MvcUriComponentsBuilder

54 | RestTemplate 的高阶用法

传递 HTTP Header

  • RestTemplate.exchange()
  • RequestEntity<T> / ResponseEntity<T>

类型转换

  • JsonSerializer / JsonDeserializer
  • @JsonComponent

解析泛型对象

  • RestTemplate.exchange()
  • ParameterizedTypeReference<T>

55 | 简单定制 RestTemplate

RestTemplate ⽀支持的 HTTP 库

通用接口

  • ClientHttpRequestFactory

默认实现

  • SimpleClientHttpRequestFactory

Apache HttpComponents

  • HttpComponentsClientHttpRequestFactory

Netty

  • Netty4ClientHttpRequestFactory

OkHttp

  • OkHttp3ClientHttpRequestFactory

优化底层请求策略

连接管理

  • PoolingHttpClientConnectionManager
  • KeepAlive 策略

超时设置

  • connectTimeout / readTimeout

SSL 校验

  • 证书检查策略

56 | 通过 WebClient 访问 Web 资源

了解 WebClient

WebClient

  • 一个以 Reactive 方式处理 HTTP 请求的非阻塞式的客户端

支持的底层 HTTP 库

  • Reactor Netty - ReactorClientHttpConnector
  • Jetty ReactiveStream HttpClient - JettyClientHttpConnector

WebClient 的基本用法

创建 WebClient

  • WebClient.create()
  • WebClient.builder()

发起请求

  • get() / post() / put() / delete() / patch()

获得结果

  • retrieve() / exchange()

处理 HTTP Status

  • onStatus()

应答正文

  • bodyToMono() / bodyToFlux()

57 | SpringBucks 实战项目进度小结

第八章: Web 开发进阶 (9 讲)

58 | 设计好的 RESTful Web Service(上)

59 | 设计好的 RESTful Web Service(下)

如何实现 Restful Web Service

  • 识别资源
  • 选择合适的资源粒度
  • 设计 URI
  • 选择合适的 HTTP 方法和返回码
  • 设计资源的表述

识别资源

  • 找到领域名词
  • 能用 CRUD 操作的名词
  • 将资源组织为集合(即集合资源)
  • 将资源合并为复合资源
  • 计算或处理函数

资源的粒度

站在客户端的角度,要考虑

  • 可缓存性
  • 修改频率
  • 可变性

站在服务端的角度,要考虑

  • 网络效率
  • 表述的多少
  • 客户端的易用程度

构建更好的 URI

  • 使用域及子域对资源进行合理的分组或划分
  • 在 URI 的路径部分使用斜杠分隔符 ( / ) 来表示资源之间的层次关系
  • 在 URI 的路径部分使用逗号 ( , ) 和分号 ( ; ) 来表示非层次元素
  • 使用连字符 ( - ) 和下划线 ( _ ) 来改善长路径中名称的可读性
  • 在 URI 的查询部分使用“与”符号 ( & ) 来分隔参数
  • 在 URI 中避免出现文件扩展名 ( 例例如 .php,.aspx 和 .jsp )

60 | 什么是 HATEOAS

61 | 使用 Spring Data REST 实现简单的超媒体服务(上)

62 | 使用 Spring Data REST 实现简单的超媒体服务(下)

认识 HAL

HAL

  • Hypertext Application Language
  • HAL 是一种简单的格式,为 API 中的资源提供简单一致的链接

HAL 模型

  • 链接
  • 内嵌资源
  • 状态

Spring Data REST

Spring Boot 依赖

  • spring-boot-starter-data-rest

常用注解与类

  • @RepositoryRestResource
  • Resource<T>
  • PagedResource<T>

如何访问 HATEOAS 服务

配置 Jackson JSON

  • 注册 HAL 支持

操作超链接

  • 找到需要的 Link
  • 访问超链接

63 | 分布式环境中如何解决 Session 的问题

常见的会话解决方案

  • 粘性会话 Sticky Session
  • 会话复制 Session Replication
  • 集中会话 Centralized Session

认识 Spring Session

Spring Session

  • 简化集群中的用户会话管理
  • 无需绑定容器特定解决方案

支持的存储

  • Redis
  • MongoDB
  • JDBC
  • Hazelcast

实现原理

定制 HttpSession

  • 通过定制的 HttpServletRequest 返回定制的 HttpSession
  • SessionRepositoryRequestWrapper
  • SessionRepositoryFilter
  • DelegatingFilterProxy

基于 Redis 的 HttpSession

引入依赖

  • spring-session-data-redis

基本配置

  • @EnableRedisHttpSession
  • 提供 RedisConnectionFactory
  • 实现 AbstractHttpSessionApplicationInitializer
  • 配置 DelegatingFilterProxy

64 | 使用 WebFlux 代替 Spring MVC(上)

65 | 使用 WebFlux 代替 Spring MVC(下)

认识 WebFlux

什么是 WebFlux

  • 用于构建基于 Reactive 技术栈之上的 Web 应用程序
  • 基于 Reactive Streams API ,运行在非阻塞服务器上

为什么会有 WebFlux

  • 对于非阻塞 Web 应用的需要
  • 函数式编程

关于 WebFlux 的性能

  • 请求的耗时并不会有很大的改善
  • 仅需少量固定数量的线程和较少的内存即可实现扩展

WebMVC v.s. WebFlux

  • 已有 Spring MVC 应⽤用,运行正常,就别改了
  • 依赖了大量阻塞式持久化 API 和网络 API,建议使用 Spring MVC
  • 已经使用了非阻塞技术栈,可以考虑使用 WebFlux
  • 想要使用 Java 8 Lambda 结合轻量级函数式框架,可以考虑 WebFlux

WebFlux 中的编程模型

两种编程模型

  • 基于注解的控制器
  • 函数式 Endpoints

基于注解的控制器

常用注解

  • @Controller
  • @RequestMapping 及其等价注解
  • @RequestBody / @ResponseBody

返回值

  • Mono<T> / Flux<T>

66 | SpringBucks 实战项目进度小结

第九章:重新认识 Spring Boot (8 讲)

67 | 认识 Spring Boot 的组成部分

Spring Boot 的特性

  • 方便地创建可独立运行的 Spring 应用程序
  • 直接内嵌 Tomcat、Jetty 或 Undertow
  • 简化了项目的构建配置
  • 为 Spring 及第三方库提供自动配置
  • 提供生产级特性
  • 无需生成代码或进行 XML 配置

Spring Boot 的四大核心

  • 自动配置 - Auto Configuration
  • 起步依赖 - Starter Dependency
  • 命令行界面 - Spring Boot CLI
  • Actuator

68 | 了解自动配置的实现原理

了解自动配置

自动配置

  • 基于添加的 JAR 依赖自动对 Spring Boot 应⽤用程序进行配置
  • spring-boot-autoconfiguration

开启自动配置

  • @EnableAutoConfiguration
    • exclude = Class<?>[]
  • @SpringBootApplication

自动配置的实现原理

@EnableAutoConfiguration

  • AutoConfigurationImportSelector
  • META-INF/spring.factories
    • org.springframework.boot.autoconfigure.EnableAutoConfiguration

条件注解

  • @Conditional
  • @ConditionalOnClass
  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • @ConditionalOnProperty
  • ……

了解自动配置的情况

观察自动配置的判断结果

  • –debug

ConditionEvaluationReportLoggingListener

  • Positive matches
  • Negative matches
  • Exclusions
  • Unconditional classes

69 | 动手实现自己的自动配置

主要工作内容

  • 编写 Java Config
    • @Configuration
  • 添加条件
    • @Conditional
  • 定位自动配置
    • META-INF/spring.factories

条件注解

条件注解

  • @Conditional

类条件

  • @ConditionalOnClass
  • @ConditionalOnMissingClass

属性条件

  • @ConditionalOnProperty

Bean 条件

  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • **@ConditionalOnSingleCandidate**

资源条件

  • @ConditionalOnResource

Web 应用条件

  • @ConditionalOnWebApplication
  • @ConditionalOnNotWebApplication

其他条件

  • @ConditionalOnExpression
  • @ConditionalOnJava
  • @ConditionalOnJndi

自动配置的执行顺序

执行顺序

  • @AutoConfigureBefore
  • @AutoConfigureAfter
  • @AutoConfigureOrder

70 | 如何在低版本 Spring 中快速实现类似自动配置的功能

需求与问题

核心的诉求

  • 现存系统,不打算重构
  • Spring 版本 3.x,不打算升级版本和引入 Spring Boot
  • 期望能够在少改代码的前提下实现一些功能增强

面临的问题

  • 3.x 的 Spring 没有条件注解
  • 无法自动定位需要加载的自动配置

核心解决思路

条件判断

  • 通过 BeanFactoryPostProcessor 进行判断

配置加载

  • 编写 Java Config 类
  • 引入配置类
  • 通过 component-scan
  • 通过 xml 文件 import

Spring 的扩展点

BeanPostProcessor

  • 针对 Bean 实例

  • 在 Bean 创建后提供定制逻辑回调

BeanFactoryPostProcessor

  • 针对 Bean 定义

  • 在容器创建 Bean 前获取配置元数据

  • Java Config 中需要定义为 static 方法

关于 Bean 的一些定制

生命周期回调

  • InitializingBean / @PostConstruct / init-method
  • DisposableBean / @PostDestory / destroy-method

XXXAware

  • ApplicationContextAware
  • BeanFactoryAware
  • BeanNameAware

一些常用操作

判断类是否存在

  • ClassUtils.isPresent()

判断 Bean 是否已定义

  • ListableBeanFactory.containsBeanDefinition()
  • ListableBeanFactory.getBeanNamesForType()

注册 Bean 定义

  • BeanDefinitionRegistry.registerBeanDefinition()

    • GenericBeanDefinition
  • BeanFactory.registerSingleton()

71 | 了解起步依赖及其实现原理

Maven 依赖管理技巧

了解你的依赖

  • mvn dependency:tree
  • IDEA Maven Helper 插件

排除特定依赖

  • exclusion

统一管理依赖

  • dependencyManagement
  • Bill of Materials - bom

Spring Boot 的 starter 依赖

Starter Dependencies

  • 直接面向功能
  • 一站获得所有相关依赖,不再复制粘贴

官方的 Starters

  • spring-boot-starter-*

72 | 定制自己的起步依赖

主要内容

  • autoconfigure 模块,包含自动配置代码
  • starter 模块,包含指向自动配置模块的依赖及其他相关依赖

命名方式

  • xxx-spring-boot-autoconfigure
  • xxx-spring-boot-starter

注意事项

  • 不要使用 spring-boot 作为依赖的前缀
  • 不要使用 spring-boot 的配置命名空间
  • starter 中仅添加必要的依赖
  • 声明对 spring-boot-starter 的依赖

73 | 深挖 Spring Boot 的配置加载机制

外化配置加载顺序

  • 开启 DevTools 时,~/.spring-boot-devtools.properties
  • 测试类上的 @TestPropertySource 注解
  • @SpringBootTest#properties 属性
  • 命令行参数( --server.port=9000
  • SPRING_APPLICATION_JSON 中的属性
  • ServletConfig 初始化参数
  • ServletContext 初始化参数
  • java:comp/env 中的 JNDI 属性
  • System.getProperties()
  • 操作系统环境变量
  • random.* 涉及到的 RandomValuePropertySource
  • jar 包外部的 application-{profile}.properties 或 .yml
  • jar 包内部的 application-{profile}.properties 或 .yml
  • jar 包外部的 application.properties 或 .yml
  • jar 包内部的 application.properties 或 .yml
  • @Configuration 类上的 @PropertySource
  • SpringApplication.setDefaultProperties() 设置的默认属性

application.properties

默认位置

  • ./config
  • ./
  • CLASSPATH 中的 /config
  • CLASSPATH 中的 /

修改名字或路路径

  • spring.config.name
  • spring.config.location
  • spring.config.additional-location

Relaxed Binding

命名风格 使用范围 示例
短划线分隔 Properties 文件
YAML 文件
系统属性
geektime.spring-boot.first-demo
驼峰式 Properties 文件
YAML 文件
系统属性
geektime.springBoot.firstDemo
下划线分割 Properties 文件
YAML 文件
系统属性
geektime.spring_boot.first_demo
全⼤大写,下划线分隔 环境变量 GEEKTIME_SPRINGBOOT_FIRSTDEMO

74 | 理解配置背后的 PropertySource 抽象

添加 PropertySource

  • <context:property-placeholder>
  • PropertySourcesPlaceholderConfigurer
  • PropertyPlaceholderConfigurer
  • @PropertySource
  • @PropertySources

Spring Boot 中的 @ConfigurationProperties

  • 可以将属性绑定到结构化对象上
  • 支持 Relaxed Binding
  • 支持安全的类型转换
  • @EnableConfigurationProperties

定制 PropertySource

主要步骤

  • 实现 PropertySource<T>
  • Environment 取得 PropertySources
  • 将自己的 PropertySource 添加到合适的位置

切入位置

  • EnvironmentPostProcessor
  • BeanFactoryPostProcessor

第十章:运行中的 Spring Boot (11 讲)

75 | 认识 Spring Boot 的各类 Actuator Endpoint

Actuator

目的

  • 监控并管理应用程序

访问方式

  • HTTP
  • JMX

依赖

  • spring-boot-starter-actuator

一些常用 Endpoint

ID 说明 默认开启 默认 HTTP 默认 JMX
beans 显示容器中的 Bean 列表 Y N Y
caches 显示应用中的缓存 Y N Y
conditions 显示配置条件的计算情况 Y N Y
configprops 显示 @ConfigurationProperties 的信息 Y N Y
env 显示 ConfigurableEnvironment 中的属性 Y N Y
health 显示健康检查信息 Y Y Y
httptrace 显示 HTTP Trace 信息 Y N Y
info 显示设置好的应用信息 Y Y Y
loggers 显示并更新日志配置 Y N Y
metrics 显示应用的度量信息 Y N Y
mappings 显示所有的 @RequestMapping 信息 Y N Y
scheduledtasks 显示应用的调度任务信息 Y N Y
shutdown 优雅地关闭应用程序 N N Y
threaddump 执行 Thread Dump Y N Y
heapdump 返回 Heap Dump 文件,格式为 HPROF Y N N/A
prometheus 返回可供 Prometheus 抓取的信息 Y N N/A

如何访问 Actuator Endpoint

HTTP 访问

  • /actuator/<id>

端口与路径

  • management.server.address=
  • management.server.port=
  • management.endpoints.web.base-path=/actuator
  • management.endpoints.web.path-mapping.<id>=路径

开启 Endpoint

  • management.endpoint.<id>.enabled=true
  • management.endpoints.enabled-by-default=false

暴露 Endpoint

  • management.endpoints.jmx.exposure.exclude=
  • management.endpoints.jmx.exposure.include=*
  • management.endpoints.web.exposure.exclude=
  • management.endpoints.web.exposure.include=info, health

76 | 动手定制自己的 Health Indicator

Spring Boot 自带的 Health Indicator

目的

  • 检查应用程序的运行状态

状态

  • DOWN - 503
  • OUT_OF_SERVICE - 503
  • UP - 200
  • UNKNOWN - 200

机制

  • 通过 HealthIndicatorRegistry 收集信息
  • HealthIndicator 实现具体检查逻辑

配置项

  • management.health.defaults.enabled=true|false
  • management.health.<id>.enabled=true
  • management.endpoint.health.show-details=never|whenauthorized|always

内置 HealthIndicator 清单

  • CassandraHealthIndicator

  • ElasticsearchHealthIndicator

  • MongoHealthIndicator

  • SolrHealthIndicator

  • CouchbaseHealthIndicator

  • InfluxDbHealthIndicator

  • Neo4jHealthIndicator

  • DiskSpaceHealthIndicator

  • JmsHealthIndicator

  • RabbitHealthIndicator

  • DataSourceHealthIndicator

  • MailHealthIndicator

  • RedisHealthIndicator

自定义 Health Indicator

方法

  • 实现 HealthIndicator 接口
  • 根据自定义检查逻辑返回对应 Health 状态
  • Health 中包含状态和详细描述信息

77 | 通过 Micrometer 获取运行数据

认识 Micrometer

特性

  • 多维度度量量
  • 支持 Tag
  • 预置大量探针
  • 缓存、类加载器器、GC、CPU 利利⽤用率、线程池……
  • 与 Spring 深度整合

支持多种监控系统

  • Dimensional

    • AppOptics, Atlas, Azure Monitor, Cloudwatch, Datadog, Datadog StatsD, Dynatrace, Elastic, Humio, Influx, KairosDB, New Relic, Prometheus, SignalFx, Sysdig StatsD, Telegraf
      StatsD, Wavefront
  • Hierarchical

    • Graphite, Ganglia, JMX, Etsy StatsD

一些核心度量指标

核心接口

  • Meter

内置实现

  • Gauge, TimeGauge
  • Timer, LongTaskTimer, FunctionTimer
  • Counter, FunctionCounter
  • DistributionSummary

Micrometer in Spring Boot 2.x

一些 URL

  • /actuator/metrics
  • /actuator/prometheus

一些配置项

  • management.metrics.export.*
  • management.metrics.tags.*
  • management.metrics.enable.*
  • management.metrics.distribution.*
  • management.metrics.web.server.auto-time-requests

核心度量项

  • JVM、CPU、文件句柄数、日志、启动时间

其他度量项

  • Spring MVC、Spring WebFlux
  • Tomcat、Jersey JAX-RS
  • RestTemplate、WebClient
  • 缓存、数据源、Hibernate
  • Kafka、RabbitMQ

自定义度量指标

  • 通过 MeterRegistry 注册 Meter
  • 提供 MeterBinder Bean 让 Spring Boot ⾃自动绑定
  • 通过 MeterFilter 进⾏行行定制

78 | 通过 Spring Boot Admin 了解程序的运行状态

Spring Boot Admin

目的

  • 为 Spring Boot 应用程序提供一套管理界面

主要功能

  • 集中展示应用程序 Actuator 相关的内容
  • 变更通知

快速上手

服务端

  • de.codecentric:spring-boot-admin-starter-server:2.1.3
  • @EnableAdminServer

客户端

  • de.codecentric:spring-boot-admin-starter-client:2.1.3
  • 配置服务端及 Endpoint
  • spring.boot.admin.client.url=http://localhost:8080
  • management.endpoints.web.exposure.include=*

安全控制

安全相关依赖

  • spring-boot-starter-security

服务端配置

  • spring.security.user.name
  • spring.security.user.password

79 | 如何定制 Web 容器的运行参数

内嵌 Web 容器

可选容器列表

  • spring-boot-starter-tomcat
  • spring-boot-starter-jetty
  • spring-boot-starter-undertow
  • spring-boot-starter-reactor-netty

修改容器器配置

端口

  • server.port
  • server.address

压缩

  • server.compression.enabled
  • server.compression.min-response-size
  • server.compression.mime-types

Tomcat 特定配置

  • server.tomcat.max-connections=10000
  • server.tomcat.max-http-post-size=2MB
  • server.tomcat.max-swallow-size=2MB
  • server.tomcat.max-threads=200
  • server.tomcat.min-spare-threads=10

错误处理

  • server.error.path=/error
  • server.error.include-exception=false
  • server.error.include-stacktrace=never
  • server.error.whitelabel.enabled=true

其他

  • server.use-forward-headers
  • server.servlet.session.timeout

编程方式

  • WebServerFactoryCustomizer<T>
  • TomcatServletWebServerFactory
  • JettyServletWebServerFactory
  • UndertowServletWebServerFactory

80 | 如何配置容器支持 HTTP/2(上)

配置 HTTPS 支持

通过参数进行配置

  • server.port=8443
  • server.ssl.*
    • server.ssl.key-store
    • server.ssl.key-store-type,JKS 或者 PKCS12
    • server.ssl.key-store-password=secret

生成证书文件

命令

  • keytool -genkey -alias 别名
    • -storetype 仓库类型 -keyalg 算法 -keysize 长度
    • -keystore 文件名 -validity 有效期

说明

  • 仓库类型,JKS、JCEKS、PKCS12 等
  • 算法,RSA、DSA 等
  • 长度,例如 2048

客户端 HTTPS 支持

配置 HttpClient ( >= 4.4 )

  • SSLContextBuilder 构造 SSLContext
  • setSSLHostnameVerifier(new NoopHostnameVerifier())

配置 RequestFactory

  • HttpComponentsClientHttpRequestFactory
  • setHttpClient()

81 | 如何配置容器支持 HTTP/2(下)

配置 HTTP/2 支持

前提条件

  • Java >= JDK 9
  • Tomcat >= 9.0.0
  • Spring Boot 不支持 h2c,需要先配置 SSL

配置项

  • server.http2.enabled

客户端 HTTP/2 支持

HTTP 库选择

  • OkHttp( com.squareup.okhttp3:okhttp:3.14.0 )
  • OkHttpClient

RestTemplate 配置

  • OkHttp3ClientHttpRequestFactory

82 | 如何编写命令行运行的程序

关闭 Web 容器

控制依赖

  • 不添加 Web 相关依赖

配置方式

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

编程方式

  • SpringApplication
  • setWebApplicationType()
  • SpringApplicationBuilder
  • web()
  • 在调用 SpringApplicationrun() 方法前设置 WebApplicationType

常用工具类

不同的 Runner

  • ApplicationRunner
  • 参数是 ApplicationArguments
  • CommandLineRunner
  • 参数是 String[]

返回码

  • ExitCodeGenerator

83 | 了解可执行 Jar 背后的秘密

认识可执行 Jar

其中包含

  • Jar 描述,META-INF/MANIFEST.MF
  • Spring Boot Loader,org/springframework/boot/loader
  • 项目内容,BOOT-INF/classes
  • 项目依赖,BOOT-INF/lib

其中不包含

  • JDK / JRE

如何找到程序的入口

Jar 的启动类

  • MANIFEST.MF
  • Main-Class: org.springframework.boot.loader.JarLauncher

项目的主类

  • @SpringApplication
  • MANIFEST.MF
  • Start-Class: xxx.yyy.zzz

84 | 如何将 Spring Boot 应用打包成 Docker 镜像文件

什么是 Docker 镜像

  • 镜像是静态的只读模板
  • 镜像中包含构建 Docker 容器器的指令
  • 镜像是分层的
  • 通过 Dockerfile 来创建镜像

Dockerfile

通过 Maven 构建 Docker 镜像

准备工作

  • 提供一个 Dockerfile
  • 配置 dockerfile-maven-plugin 插件

执行构建

  • mvn package
  • mvn dockerfile:build

检查结果

  • docker images

85 | SpringBucks 实战项目进度小结

第十一章:Spring Cloud 及 Cloud Native 概述 (5 讲)

86 | 简单理解微服务

微服务就是一些协同工作的小而自治的服务。

微服务的优点

  • 易于部署
  • 与组织结构对齐
  • 可组合性
  • 可替代性

微服务的代价

  • 架构复杂
  • 运维复杂

87 | 如何理解云原生(Cloud Native)

云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。

云原生应用要求

  • DevOps
  • 持续交付
  • 微服务
  • 容器

Cloud Native Computing Foundation,缩写 CNCF

88 | 12-Factor App(上)

89 | 12-Factor App(下)

12-Factor 为构建 SaaS 应用提供了方法论。

参考资料:https://12factor.net/zh_cn/

  • 基准代码 - 一份基准代码,多份部署。解决方案:git

  • 依赖 - 显式声明依赖关系。解决方案:maven、gradle

  • 配置 - 在环境中存储配置。解决方案:apollo

  • 后端服务 - 把后端服务当作附加资源

  • 构建,发布,运行 - 严格分离构建和运行。解决方案:CI/CD(如:jenkins、sonar 等)

  • 进程 - 以一个或多个无状态进程运行应用

  • 端口绑定 - 通过端口绑定提供服务

  • 并发 - 通过进程模型进行扩展

  • 易处理 - 快速启动和优雅终止可最大化健壮性

  • 开发环境与线上环境等价 - 尽可能的保持开发,预发布,线上环境相同

  • 日志 - 把日志当作事件流

  • 管理进程 - 后台管理任务当作一次性进程运行

90 | 认识 Spring Cloud 的组成部分

Spring Cloud 的主要功能

  • 服务发现
  • 服务熔断
  • 配置服务
  • 服务安全
  • 服务网关
  • 分布式消息
  • 分布式跟踪
  • 各种云平台支持

第十二章:服务注册与发现 (9 讲)

91 | 使用 Eureka 作为服务注册中心

  • SpringCloud 启动包
    • 服务端 - spring-cloud-starter-netflix-eureka-server
    • 客户端 - spring-cloud-starter-netflix-eureka-client
  • 注解
    • 服务端启动注解 - @EnableEurekaServer
    • 客户端启动注解
      • 通用注解 - @EnableDiscoveryClient
      • Eureka 特定注解 - @EnableEurekaClient
  • 要点
    • Eureka 默认端口 8761
  • 配置
    • eureka.client.serviceUrl.defaultZone - 注册地址,如 http://localhost:10001/eureka/
    • eureka.client.register-with-eureka - 是否将自己注册到 Eureka Server,默认为 true
    • eureka.client.fetch-registry - 是否从 Eureka Server 获取注册信息,默认为 true

92 | 使用 Spring Cloud Loadbalancer 访问服务

  • 如何获得服务地址
    • org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient
    • org.springframework.cloud.client.discovery.DiscoveryClient - 通用接口,推荐方式
  • 负载均衡客户端
    • @LoadBalanced
    • 实际是通过 ClientHttpRequestInterceptor 实现的
    • LoadBalancerInterceptor
    • LoadBalancerClient
    • RibbonLoadBalancerClient

93 | 使用 Feign 访问服务

声明式 REST Web 服务客户端

  • SpringCloud 启动包
    • spring-cloud-starter-openfeign
  • 注解
    • 启动注解 - @EnableFeignClients
    • 定义接口注解 - @FeignClient
  • 配置 - org.springframework.cloud.openfeign.FeignClientsConfiguration

94 | 深入理解服务发现背后的 DiscoveryClient

  • 服务端抽象接口 - org.springframework.cloud.client.serviceregistry.ServiceRegistry
    • EurekaServiceRegistry
    • EurekaRegistration
    • EurekaAutoServiceRegistration
    • EurekaClientAutoConfiguration
  • 客户端抽象接口 - org.springframework.cloud.client.discovery.DiscoveryClient
    • @EnableDiscoveryClient
  • 负载均衡抽象接口 - org.springframework.cloud.client.loadbalancer.LoadBalancerClient

95 | 使用 Zookeeper 作为服务注册中心

  • SpringCloud 启动包
    • spring-cloud-starter-zookeeper-discovery
  • 配置
    • ZookeeperAutoConfiguration
    • ZookeeperDiscoveryAutoConfiguration

96 | 使用 Consul 作为服务注册中心

  • SpringCloud 启动包
    • spring-cloud-starter-consul-discovery
  • 配置
    • ConsulAutoConfiguration

97 | 使用 Nacos 作为服务注册中心

  • SpringCloud 启动包
    • spring-cloud-starter-alibaba-nacos-discovery
  • 配置
    • NacosDiscoveryAutoConfiguration

98 | 如何定制自己的 DiscoveryClient

DiscoveryClient

  • EurekaDiscoveryClient
  • ZooKeeperDiscoveryClient
  • ConsulDiscoveryClient
  • NacosDiscoveryClient

LoadBalancerClient

  • RibbonLoadBalancerClient

自定义 DiscoveryClient 步骤

  • 返回该 DiscoveryClient 能提供的服务名列表
  • 返回指定服务对应的 ServiceInstance 列表
  • 返回 DiscoveryClient 的顺序
  • 返回 HealthIndicator 里显示的描述

自定义 RibbonClient 支持

  • 实现 ServerList<T extends Server>
  • Ribbon 提供了 AbstractServerList
  • 提供一个配置类,声明 ServerList Bean 实例

99 | SpringBucks 实战项目进度小结

第十三章:服务熔断 (7 讲)

100 | 使用 Hystrix 实现服务熔断(上)

断路器模式

在断路器对象中封装受保护的方法调用

该对象监控调用和断路情况

调用失败触发阈值后,后序调用直接由断路器返回错误,不再执行实际调用

101 | 使用 Hystrix 实现服务熔断(下)

  • Hystrix 应用

    • 注解
    • @HystrixCommand
      • fallbackMethod
      • commandProperties
        • @HystrixProperty
  • SpringCloud 启动包

    • spring-cloud-starter-netflix-hystrix
  • 注解

    • @EnableCircuitBreaker - 断路器开启注解
  • Feign 支持

    • feign.hystrix.enabled=true
    • @FeignClientfallback / fallbackFactory
  • 配置

    • HystrixCircuitBreakerAutoConfiguration

102 | 如何观察服务熔断

Spring Cloud 对于熔断的监控支持

  • Hystrix Metrics Stream
    • spring-boot-starter-actuator
    • /actuator/hystrix.stream
  • Hystrix Dashboard
    • spring-cloud-starter-netflix-hystrix-dashboard
    • @EnableHystrixDashboard
    • /hystirx

聚合集群熔断信息

  • SpringCloud 启动包 - spring-cloud-starter-netflix-turbines
  • 注解 - @EnableTurbine
  • /turbine/stream?cluster=集群名

103 | 使用 Resilience4j 实现服务熔断

Hystrix 官方已经停止维护,因此建议选择其他产品来替代。例如:Resilience4J

  • Resilience4J 实现

    • 基于 ConcurrentHashMap 的内存断路器
    • CircuitBreakerRegistry
    • CircuitBreakerConfig
  • Resilience4J 依赖

    • resilience4j-spring-boot2
  • 注解

    • @CircuitBreaker
  • 配置

    • CircuitBreakerProperties

104 | 使用 Resilience4j 实现服务限流(上)

Bulkhead

  • 目的
    • 防止下游依赖被并发请求冲击
    • 防止发生雪崩
  • 用法
    • BulkheadRegistry / BulkheadConfig
    • @Bulkhead(name = “xxx”)

105 | 使用 Resilience4j 实现服务限流(下)

RateLimit

  • 目的
    • 限制特定时间内的执行次数
  • 用法
    • RateLimiterRegistry / RateLimiterConfig
    • @RateLimiter
  • 配置
  • RateLimiterPropertis

106 | SpringBucks 实战项目进度小结

第十四章:服务配置 (7 讲)

107 | 基于 Git 的配置中心(上)

目的

提供针对外置配置的 HTTP API

  • SpringCloud 启动包 - spring-cloud-config-server
  • 注解 - @EnableConfigServer

108 | 基于 Git 的配置中心(下)

109 | 基于 Zookeeper 的配置中心

110 | 深入理解 Spring Cloud 的配置抽象

实现

  • 类似于 Spring 的 Environment 和 PropertySource
  • 在上下文中增加 Spring Cloud Config 的 PropertySource

PropertySource 子类

  • ZooKeeperPropertySource
  • ConsulPropertySource
  • ConsulFilePropertySource

PropertySourceLocator

EnvironmentRepositry

配置刷新

  • /actuator/refresh
  • Spring Cloud Bus - RegfreshRemoteApplicationEvent

ZooKeeperConfigBootstrapConfiguration

ZooKeeperConfigAutoConfiguration

111 | 基于 Consul 的配置中心

SpringCloud 启动包 - spring-cloud-starter-consual-config

配置文件 - bootstrap.propertis | yml

112 | 基于 Nacos 的配置中心

SpringCloud 启动包 - spring-cloud-starter-alibaba-nacos-config

配置文件 - bootstrap.propertis | yml

113 | SpringBucks 实战项目进度小结

第十五章:Spring Cloud Stream (4 讲)

114 | 认识 Spring Cloud Stream

Spring Cloud Stream 是一款用于构建消息驱动的微服务应用程序的轻量级框架。

特性

  • 声明式编程模型
  • 引入多种概念抽象:发布订阅、消费组、分区
  • 支持多种消息中间件:RabbitMQ、Kafka

概念

  • Binding
    • 生产者、消费者与 MQ 之间的桥梁
    • @EnableBinding
    • @Input /SubscribableChannel
    • @Output / MessageChannel
  • 消费组
  • 分区

生产消息

  • 使用 MessageChannel 的 send()
  • @SendTo

消费消息

  • @StreamListener
  • @Payload / @Headers / @Header

115 | 通过 Spring Cloud Stream 访问 RabbitMQ

SpringCloud 启动包 - spring-cloud-starter-stream-rabbit

SpringBoot 启动包 - spring-boot-starter-amqp

配置

org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration

116 | 通过 Spring Cloud Stream 访问 Kafka

SpringCloud 启动包 - spring-cloud-starter-stream-kafka

配置 - org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration

Spring 定时任务

  • TaskScheduler / Trigger / TriggerContext
  • 配置定时任务
    • @EnableScheduling
    • <task:scheduler />
    • @Scheduled

Spring 事件机制

  • ApplicationEvent
  • 发送事件
    • ApplicationEventPublisher
    • ApplicationEventPublisherAware
  • 监听事件
    • ApplicationListener<T>
    • @EventListener

117 | SpringBucks 实战项目进度小结

第十六章:服务链路追踪 (6 讲)

118 | 通过 Dapper 理解链路治理

119 | 使用 Spring Cloud Sleuth 实现链路追踪

SpringCloud 启动包 - spring-cloud-starter-sleuth、spring-cloud-starter-zipkin

120 | 如何追踪消息链路

121 | 除了链路还要治理什么

122 | SpringBucks 实战项目进度小结

参考资料

《24 讲吃透分布式数据库》笔记

开篇词 吃透分布式数据库,提升职场竞争力

导论:什么是分布式数据库?聊聊它的前世今生

分布式数据库的核心

数据分片

  • 水平分片:按行进行数据分割,数据被切割为一个个数据组,分散到不同节点上。

  • 垂直分片:按列进行数据切割,一个数据表的模式(Schema)被切割为多个小的模式。

数据同步

数据库同步用于帮助数据库恢复一致性。

分布式数据库发展就是一个由合到分,再到合的过程

  1. 早期的关系型商业数据库的分布式能力可以满足大部分用户的场景,因此产生了如 Oracle 等几种巨无霸数据库产品;
  2. OLAP 领域首先寻求突破,演化出了大数据技术与 MPP 类型数据库,提供功能更强的数据分析能力;
  3. 去 IOE 引入数据库中间件,并结合应用平台与开源单机数据库形成新一代解决方案,让商业关系型数据库走下神坛,NoSQL 数据库更进一步打破了关系型数据库唯我独尊的江湖地位;
  4. 新一代分布式 OLTP 数据库正式完成了分布式领域对数据库核心特性的完整支持,它代表了分布式数据库从此走向了成熟,也表明了 OLAP 与 OLTP 分布式场景下,分别在各自领域内取得了胜利;
  5. HTAP 和多模式数据处理的引入,再一次将 OLAP 与 OLTP 融合,从而将分布式数据库推向如传统商业关系型数据库数十年前那般的盛况,而其产生的影响要比后者更为深远。

SQL vs NoSQL:一次搞清楚五花八门的“SQL”

数据分片:如何存储超大规模的数据?

数据分片概论

想提升系统对于数据的处理,有两种思路:

垂直扩展:提升硬件设备,获得更好的 CPU、更大的内存。但这种方式容易达到瓶颈。

水平扩展:采用分而治之的思想,将数据拆分成多个分区,分散到一组便宜的机器上。这种方式性价比更高,不过也会引入数据同步等复杂的问题。

分片算法

哈希分片

范围分片

分布式 ID

UUID:性能较差,且离散度不高

雪花算法

数据复制:如何保证数据在分布式场景下的高可用?

主从复制

  • 复制同步模式
    • 同步复制:如果由于从库已崩溃,存在网络故障或其他原因而没有响应,则主库也无法写入该数据。
    • 半同步复制:其中部分从库进行同步复制,而其他从库进行异步复制。也就是,如果其中一个从库同步确认,主库可以写入该数据。
    • 异步复制:不管从库的复制情况如何,主库可以写入该数据。而此时,如果主库失效,那么还未同步到从库的数据就会丢失。
  • 复制延迟 - 提高系统的查询性能,可以通过添加从节点来实现。但是如果使用同步复制,每次写入都需要同步所有从节点,会造成一部分从节点已经有数据,但是主节点还没写入数据。而异步复制的问题是从节点的数据可能不是最新的。
  • 复制与高可用性
    • 从节点故障。由于每个节点都复制了从主库那里收到的数据更改日志,因此它知道在发生故障之前已处理的最后一个事务,由此可以凭借此信息从主节点或其他从节点那里恢复自己的数据。
    • 主节点故障。在这种情况下,需要在从节点中选择一个成为新的主节点,此过程称为故障转移,可以手动或自动触发。其典型过程为:第一步根据超时时间确定主节点离线;第二步选择新的主节点,这里注意新的主节点通常应该与旧的主节点数据最为接近;第三步是重置系统,让它成为新的主节点。
  • 复制方式
    • 基于语句的复制:主库记录它所执行的每个写请求(一般以 SQL 语句形式保存),每个从库解析并执行该语句,就像从客户端收到该语句一样。但这种复制会有一些潜在问题,如语句使用了获取当前时间的函数,复制后会在不同数据节点上产生不同的值。另外如自增列、触发器、存储过程和函数都可能在复制后产生意想不到的问题。但可以通过预处理规避这些问题。使用该复制方式的分布式数据库有 VoltDB、Calvin。
    • 日志(WAL)同步:WAL 是一组字节序列,其中包含对数据库的所有写操作。它的内容是一组低级操作,如向磁盘的某个页面的某个数据块写入一段二进制数据,主库通过网络将这样的数据发送给从库。这种方法避免了上面提到的语句中部分操作复制后产生的一些副作用,但要求主从的数据库引擎完全一致,最好版本也要一致。如果要升级从库版本,那么就需要计划外停机。PostgreSQL 和 Oracle 中使用了此方法。
    • 行复制:它由一系列记录组成,这些记录描述了以行的粒度对数据库表进行的写操作。它与特定存储引擎解耦,并且第三方应用可以很容易解析其数据格式。
    • ETL 工具:该功能一般是最灵活的方式。用户可以根据自己的业务来设计复制的范围和机制,同时在复制过程中还可以进行如过滤、转换和压缩等操作。但性能一般较低,故适合处理子数据集的场景。

一致性与 CAP 模型:为什么需要分布式一致性?

实践:设计一个最简单的分布式数据库

概要:什么是存储引擎,为什么需要了解它?

存储引擎

数据库的一般架构

  1. 传输层:它是接受客户端请求的一层。用来处理网络协议。同时,在分布式数据库中,它还承担着节点间互相通信的职责。
  2. 查询层:请求从传输层被发送到查询层。在查询层,协议被进行解析,如 SQL 解析;后进行验证与分析;最后结合访问控制来决定该请求是否要被执行。解析完成后,请求被发送到查询优化器,在这里根据预制的规则,数据分布并结合数据库内部的统计,会生成该请求的执行计划。执行计划一般是树状的,包含一系列相关的操作,用于从数据库中查询到请求希望获取的数据。
  3. 执行层:执行计划被发送到执行层去运行。执行层一般包含本地运行单元与远程运行单元。根据执行计划,调用不同的单元,而后将结果合并返回到传输层。执行层本地运行单元其实就是存储引擎。它一般包含如下一些功能
    1. 事务管理器:用来调度事务并保证数据库的内部一致性(这与模块一中讨论的分布式一致性是不同的);
    2. 锁管理:保证操作共享对象时候的一致性,包括事务、修改数据库参数都会使用到它;
    3. 存储结构:包含各种物理存储层,描述了数据与索引是如何组织在磁盘上的;
    4. 内存结构:主要包含缓存与缓冲管理,数据一般是批量输入磁盘的,写入之前会使用内存去缓存数据;
    5. 提交日志:当数据库崩溃后,可以使用提交日志恢复系统的一致性状态。

内存与磁盘

内存特点:查询快、更昂贵、持久化比较复杂

行式存储与列式存储

列式存储非常适合处理分析聚合类型的任务,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。

数据文件与索引文件

数据文件最传统的形式为堆组织表(Heap-Organized Table),数据的放置没有一个特别的顺序,一般是按照写入的先后顺序排布。这种数据文件需要一定额外的索引帮助来查找数据。

索引文件的分类模式一般为主键索引与二级索引两类。前者是建立在主键上的,它可能是一个字段或多个字段组成。而其他类型的索引都被称为二级索引。主键索引与数据是一对一关系,而二级索引很有可能是一对多的关系,即多个索引条目指向一条数据。

面向分布式的存储引擎特点

内存型数据库会倾向于选择分布式模式来进行构建。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。

列式存储也与分布式数据库存在天然的联系。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。

分布式索引:如何在集群中快速定位数据?

读取路径

存储引擎处理查询请求一般流程:

  1. 寻找分片和目标节点;
  2. 检查数据是否在缓存与缓冲中;
  3. 检查数据是否在磁盘文件中;
  4. 合并结果。

索引数据表

SSTable 文件是一个排序的、不可变的、持久化的键值对结构,其中键值对可以是任意字节的字符串,支持使用指定键来查找值,或通过给定键范围遍历所有的键值对。每个 SSTable 文件包含一系列的块。SSTable 文件中的块索引(这些块索引通常保存在文件尾部区域)用于定位块,这些块索引在 SSTable 文件被打开时加载到内存。在查找时首先从内存中的索引二分查找找到块,然后一次磁盘寻道即可读取到相应的块。另一种方式是将 SSTable 文件完全加载到内存,从而在查找和扫描中就不需要读取磁盘。

内存缓冲

内存中常用的快速搜索数据结构是跳表。典型代表:Redis 使用跳表实现 zset。

布隆过滤

日志型存储:为什么选择它作为底层存储?

事务处理与恢复(上):数据库崩溃后如何保证数据不丢失?

事务处理与恢复(下):如何控制并发事务?

引擎拓展:解读当前流行的分布式存储引擎

概要:分布式系统都要解决哪些问题?

错误侦测:如何保证分布式系统稳定?

领导选举:如何在分布式系统内安全地协调操作?

再谈一致性:除了 CAP 之外的一致性模型还有哪些?

数据可靠传播:反熵理论如何帮助数据库可靠工作?

分布式事务(上):除了 XA,还有哪些原子提交算法吗?

分布式事务(下):Spanner 与 Calvin

共识算法:一次性说清楚 Paxos、Raft 等算法的区别

知识串讲:如何取得性能和可扩展性的平衡?

发展与局限:传统数据库在分布式领域的探索

数据库中间件:传统数据库向分布式数据库的过渡

现状解读:分布式数据库的最新发展情况

加餐 1 概念解析:云原生、HTAP、图与内存数据库

加餐 2 数据库选型:我们该用什么分布式数据库?

参考资料