Clawdbot 配置国内 Minimax 教程

Clawdbot 配置国内 Minimax 教程

Clawdbot 默认使用国际站 Minimax API,但国内用户访问国际站可能存在网络延迟或访问受限问题。本文介绍如何将 Clawdbot 配置为使用国内 Minimax 地址。

问题背景

Clawdbot 默认配置的 Minimax 地址为国际站,可能存在以下问题:

  • 网络延迟高
  • 访问不稳定
  • 需要特殊网络环境

国内站地址:https://api.minimaxi.com/anthropic

配置步骤

步骤一:修改 Agent 模型配置

编辑 ~/.clawdbot/agents/main/agent/models.json

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
{
"providers": {
"minimax": {
"baseUrl": "https://api.minimaxi.com/anthropic",
"api": "anthropic-messages",
"models": [
{
"id": "MiniMax-M2.1",
"name": "MiniMax M2.1",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 15,
"output": 60,
"cacheRead": 2,
"cacheWrite": 10
},
"contextWindow": 200000,
"maxTokens": 8192
},
{
"id": "MiniMax-VL-01",
"name": "MiniMax VL 01",
"reasoning": false,
"input": ["text", "image"],
"cost": {
"input": 15,
"output": 60,
"cacheRead": 2,
"cacheWrite": 10
},
"contextWindow": 200000,
"maxTokens": 8192
}
],
"apiKey": "sk-cp-xxx"
}
}
}

步骤二:修改全局配置

编辑 /root/.clawdbot/clawdbot.json

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
{
"models": {
"mode": "merge",
"providers": {
"minimax": {
"baseUrl": "https://api.minimaxi.com/anthropic",
"api": "anthropic-messages",
"models": [
{
"id": "MiniMax-M2.1",
"name": "MiniMax M2.1",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 15,
"output": 60,
"cacheRead": 2,
"cacheWrite": 10
},
"contextWindow": 200000,
"maxTokens": 8192
}
]
}
}
}
}

配置说明

配置项 说明
baseUrl API 基础地址,国内站为 https://api.minimaxi.com/anthropic
api API 类型,使用 anthropic-messages
models 模型列表,可配置 MiniMax-M2.1 和 MiniMax-VL-01
apiKey 你的 API Key

重启验证

配置完成后,重启 Clawdbot:

1
clawdbot gateway restart

然后检查状态:

1
clawdbot status

常见问题

Q: 配置后无法连接?

检查:

  1. API Key 是否正确
  2. 网络是否可访问 https://api.minimaxi.com
  3. 配置文件语法是否正确(JSON 格式)

Q: 如何验证配置生效?

在 Clawdbot 中执行:

1
/status

查看当前使用的模型配置。

参考链接


文章标签: Clawdbot, Minimax, AI配置, 国内站

Clawdbot 配置国内 Minimax 教程

Clawdbot 默认使用国际站 Minimax API,但国内用户访问国际站可能存在网络延迟或访问受限问题。本文介绍如何将 Clawdbot 配置为使用国内 Minimax 地址。

问题背景

Clawdbot 默认配置的 Minimax 地址为国际站,可能存在以下问题:

  • 网络延迟高
  • 访问不稳定
  • 需要特殊网络环境

国内站地址:https://api.minimaxi.com/anthropic

配置步骤

步骤一:修改 Agent 模型配置

编辑 ~/.clawdbot/agents/main/agent/models.json

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
{
"providers": {
"minimax": {
"baseUrl": "https://api.minimaxi.com/anthropic",
"api": "anthropic-messages",
"models": [
{
"id": "MiniMax-M2.1",
"name": "MiniMax M2.1",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 15,
"output": 60,
"cacheRead": 2,
"cacheWrite": 10
},
"contextWindow": 200000,
"maxTokens": 8192
},
{
"id": "MiniMax-VL-01",
"name": "MiniMax VL 01",
"reasoning": false,
"input": ["text", "image"],
"cost": {
"input": 15,
"output": 60,
"cacheRead": 2,
"cacheWrite": 10
},
"contextWindow": 200000,
"maxTokens": 8192
}
],
"apiKey": "sk-cp-xxx"
}
}
}

步骤二:修改全局配置

编辑 /root/.clawdbot/clawdbot.json

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
{
"models": {
"mode": "merge",
"providers": {
"minimax": {
"baseUrl": "https://api.minimaxi.com/anthropic",
"api": "anthropic-messages",
"models": [
{
"id": "MiniMax-M2.1",
"name": "MiniMax M2.1",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 15,
"output": 60,
"cacheRead": 2,
"cacheWrite": 10
},
"contextWindow": 200000,
"maxTokens": 8192
}
]
}
}
}
}

配置说明

配置项 说明
baseUrl API 基础地址,国内站为 https://api.minimaxi.com/anthropic
api API 类型,使用 anthropic-messages
models 模型列表,可配置 MiniMax-M2.1 和 MiniMax-VL-01
apiKey 你的 API Key

重启验证

配置完成后,重启 Clawdbot:

1
clawdbot gateway restart

然后检查状态:

1
clawdbot status

常见问题

Q: 配置后无法连接?

检查:

  1. API Key 是否正确
  2. 网络是否可访问 https://api.minimaxi.com
  3. 配置文件语法是否正确(JSON 格式)

Q: 如何验证配置生效?

在 Clawdbot 中执行:

1
/status

查看当前使用的模型配置。

参考链接


文章标签: Clawdbot, Minimax, AI配置, 国内站

事务到底是隔离的还是不隔离的?

begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句,事务才真正启动。使用start transation with consistent snapshot 这个命令可以马上启动一个事务。

在MySQL中有两个视图的概念:

  • 一个是view。它是一个用于查询语句定义的虚拟表,它的语法是:create view ...

  • 另一个是InnoDB 在实现MVCC时用到的一致性读视图,即 consistent read view,用于支持RC(Read Commit 读提交)和RR(Repeatable 可重复读)隔离级别实现的。

“快照”在 MVCC 里是怎么工作的?

在可重复读隔离级别下,事务在启动的时候就“拍个快照”,这个快照时基于整库的。

如果库有100G,那么启动一个事务就需要拷贝100G数据,这样实现是不现实的。

快照是怎么实现的?

InnoDB 里面每个事务都有一个唯一的事务ID,叫做transaction id,它是在事务开始的时候向InnoDB 的事务系统申请的,是按照顺序严格递增的。

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

如图下所示,就是一个记录被多个事务连续更新后的状态。

图中的三个虚线箭头,就是undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。

事务启动的时候,以启动的时刻为准,如果一个数据版本低于在事务启动之前生成的,那就可见,如果在启动之后才生成的,那就不可见。

在实现上,InnoDB为每个事务构造了一个数据,用来保存这个事务的启动瞬间,当前正在“活跃”的所有事务ID,“活跃”指的是,启动了但还没提交。

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

  3. 如果落在黄色部分,那就包括两种情况
    a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

** InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。**

更新逻辑

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

当前读,读取的是最新版本,并且需要先获取对应记录的锁,如以下这些 SQL 类型:

1
2
3
4
5
select ... lock in share mode

select ... for update

update 、delete 、insert

例如,要 update 一条记录,在事务执行过程中,如果不加锁,那么另一个事务可以 delete 这条数据并且能成功 commit ,就会产生冲突了。所以 update 的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。

事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;

  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句,事务才真正启动。使用start transation with consistent snapshot 这个命令可以马上启动一个事务。

在MySQL中有两个视图的概念:

  • 一个是view。它是一个用于查询语句定义的虚拟表,它的语法是:create view ...

  • 另一个是InnoDB 在实现MVCC时用到的一致性读视图,即 consistent read view,用于支持RC(Read Commit 读提交)和RR(Repeatable 可重复读)隔离级别实现的。

“快照”在 MVCC 里是怎么工作的?

在可重复读隔离级别下,事务在启动的时候就“拍个快照”,这个快照时基于整库的。

如果库有100G,那么启动一个事务就需要拷贝100G数据,这样实现是不现实的。

快照是怎么实现的?

InnoDB 里面每个事务都有一个唯一的事务ID,叫做transaction id,它是在事务开始的时候向InnoDB 的事务系统申请的,是按照顺序严格递增的。

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

如图下所示,就是一个记录被多个事务连续更新后的状态。

图中的三个虚线箭头,就是undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。

事务启动的时候,以启动的时刻为准,如果一个数据版本低于在事务启动之前生成的,那就可见,如果在启动之后才生成的,那就不可见。

在实现上,InnoDB为每个事务构造了一个数据,用来保存这个事务的启动瞬间,当前正在“活跃”的所有事务ID,“活跃”指的是,启动了但还没提交。

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

  3. 如果落在黄色部分,那就包括两种情况
    a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

** InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。**

更新逻辑

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

当前读,读取的是最新版本,并且需要先获取对应记录的锁,如以下这些 SQL 类型:

1
2
3
4
5
select ... lock in share mode

select ... for update

update 、delete 、insert

例如,要 update 一条记录,在事务执行过程中,如果不加锁,那么另一个事务可以 delete 这条数据并且能成功 commit ,就会产生冲突了。所以 update 的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。

事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;

  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

行锁功过:怎么减少行锁对性能的影响?

从两阶段锁说起

实际上事务B的update语句会被阻塞,直至事务A执行commit之后,事务B才能继续执行。

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是等到事务结束才释放,这就是两阶段锁协议。

如果你的事务中需要锁多个行,要把最可能造成冲突、最可能影响并发的锁尽量往后放。

死锁和死锁检测

当并发系统出现资源循环依赖,就会导致这几个线程处于无限等待状态,称为死锁

两种解决死锁的策略:

  • 一种策略是直接进入等待,直至超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。

  • 另外一种策略是发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

使用show variables like 'innodb_deadlock_detect';可以查看系统参数。

问题

如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:

  • 第一种,直接执行 delete from T limit 10000;

  • 第二种,在一个连接中循环执行 20 次 delete from T limit 500;

  • 第三种,在 20 个连接中同时执行 delete from T limit 500。

一般会选择第二种。

第一种方式单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。

第三种方式会人为造成锁冲突。

从两阶段锁说起

实际上事务B的update语句会被阻塞,直至事务A执行commit之后,事务B才能继续执行。

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是等到事务结束才释放,这就是两阶段锁协议。

如果你的事务中需要锁多个行,要把最可能造成冲突、最可能影响并发的锁尽量往后放。

死锁和死锁检测

当并发系统出现资源循环依赖,就会导致这几个线程处于无限等待状态,称为死锁

两种解决死锁的策略:

  • 一种策略是直接进入等待,直至超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。

  • 另外一种策略是发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

使用show variables like 'innodb_deadlock_detect';可以查看系统参数。

问题

如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:

  • 第一种,直接执行 delete from T limit 10000;

  • 第二种,在一个连接中循环执行 20 次 delete from T limit 500;

  • 第三种,在 20 个连接中同时执行 delete from T limit 500。

一般会选择第二种。

第一种方式单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。

第三种方式会人为造成锁冲突。

深入浅出索引(下)

覆盖索引

如果执行语句如:select ID from T where k between 3 and 5,这是只需要查ID的值,而ID的值已经在k索引树上了,因此可以直接提供查询结果,不需要回表,这种情况我们称为覆盖索引

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

最左前缀原则


可以看到,索引是按照索引定义里面出现的字段排序的。

只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

在建立联合索引的时候,如何安排索引内的字段顺序?

第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。

索引下推

MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

例如以下查询语句:

1
mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;
无索引下推

有索引下推

可以明显的看出,有索引下推的情况下减少了回表的次数。

覆盖索引

如果执行语句如:select ID from T where k between 3 and 5,这是只需要查ID的值,而ID的值已经在k索引树上了,因此可以直接提供查询结果,不需要回表,这种情况我们称为覆盖索引

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

最左前缀原则


可以看到,索引是按照索引定义里面出现的字段排序的。

只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

在建立联合索引的时候,如何安排索引内的字段顺序?

第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。

索引下推

MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

例如以下查询语句:

1
mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;
无索引下推

有索引下推

可以明显的看出,有索引下推的情况下减少了回表的次数。

深入浅出索引(上)

索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。

索引常见模型

哈希表

哈希表是一种以键值对存储的数据结构,只要输入key,就可以根据key找到对应的vaule。不可避免的是会存在hash冲突,处理这种情况方法是拉出一个链表。(类比Java中HashMap结构)。

优点:

  • 查找速度快,新增速度也快。

缺点:

  • 因为不是有序的,如果需要范围查询,速度是很慢的。

哈希表这种结构适用于只有等值查询的场景,比如 Memcached 及其他一些 NoSQL 引擎。

有序数组

有序数组查询时,可以使用二分法进行搜索,时间复杂度是O(log(N)),并且有序数据还支持范围查询。但是需要插入的时候,就需要进行数据挪动(因为要保证顺序),成本是非常高的。

因此,有序数组索引只适用于静态存储引擎

搜索树

二叉树的特点是:每个节点的左儿子小于父节点,父节点又小于右儿子。这样如果你要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径得到。这个时间复杂度是 O(log(N))。

当然为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))。

二叉树的主要缺点是:当数据量很大的时候,树高会非常高,假如树高为20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。

InnoDB 的索引模型

在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB使用的是B+树索引模型,所以数据都是存储在B+树中的。

每一个索引在InnoDB中对应一颗B+树。

从上图来看,索引类型分为:主键索引和非主键索引。

主键索引在InnoDB中也成为聚簇索引(clustered index),非主键索引在InnoDB中成为二级索引(secondary index)。

基于主键索引和非主键索引搜索的区别:

  • 基于主键索引查询只需要搜索对应的这颗B+树。

  • 基于非主键索引首先先查到对应值的ID(假设ID为主键),再到ID索引树中搜索一次,这个过程称为回表。

索引的维护

页分裂

B+ 树为了维护索引的有序性,在插入新值的时候需要做必要的维护,如果插入的数据需要在页的中间,那么就需要进行数据的挪动,空出位置,如果插入的页刚好满了,就会触发页分裂,页分裂除了会影响性能,而且会使整体空间利用率降低50%(因为之前一个页,分裂成了两个)。

当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

基于业务字段做主键,往往不能保证插入的有序性,更容易造成页分裂,基于自增ID做主键,每插入一条记录都是追加操作,一般不会触发叶子结点分裂。由于每个非主键索引叶子结点都是主键的值,如果用整型做主键,只需要4个字节,如果是长整型,则是8个字节。

显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。

索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。

索引常见模型

哈希表

哈希表是一种以键值对存储的数据结构,只要输入key,就可以根据key找到对应的vaule。不可避免的是会存在hash冲突,处理这种情况方法是拉出一个链表。(类比Java中HashMap结构)。

优点:

  • 查找速度快,新增速度也快。

缺点:

  • 因为不是有序的,如果需要范围查询,速度是很慢的。

哈希表这种结构适用于只有等值查询的场景,比如 Memcached 及其他一些 NoSQL 引擎。

有序数组

有序数组查询时,可以使用二分法进行搜索,时间复杂度是O(log(N)),并且有序数据还支持范围查询。但是需要插入的时候,就需要进行数据挪动(因为要保证顺序),成本是非常高的。

因此,有序数组索引只适用于静态存储引擎

搜索树

二叉树的特点是:每个节点的左儿子小于父节点,父节点又小于右儿子。这样如果你要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径得到。这个时间复杂度是 O(log(N))。

当然为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))。

二叉树的主要缺点是:当数据量很大的时候,树高会非常高,假如树高为20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。

InnoDB 的索引模型

在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB使用的是B+树索引模型,所以数据都是存储在B+树中的。

每一个索引在InnoDB中对应一颗B+树。

从上图来看,索引类型分为:主键索引和非主键索引。

主键索引在InnoDB中也成为聚簇索引(clustered index),非主键索引在InnoDB中成为二级索引(secondary index)。

基于主键索引和非主键索引搜索的区别:

  • 基于主键索引查询只需要搜索对应的这颗B+树。

  • 基于非主键索引首先先查到对应值的ID(假设ID为主键),再到ID索引树中搜索一次,这个过程称为回表。

索引的维护

页分裂

B+ 树为了维护索引的有序性,在插入新值的时候需要做必要的维护,如果插入的数据需要在页的中间,那么就需要进行数据的挪动,空出位置,如果插入的页刚好满了,就会触发页分裂,页分裂除了会影响性能,而且会使整体空间利用率降低50%(因为之前一个页,分裂成了两个)。

当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

基于业务字段做主键,往往不能保证插入的有序性,更容易造成页分裂,基于自增ID做主键,每插入一条记录都是追加操作,一般不会触发叶子结点分裂。由于每个非主键索引叶子结点都是主键的值,如果用整型做主键,只需要4个字节,如果是长整型,则是8个字节。

显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。

事务隔离:为什么你改了我还看不见?

ACID (Atomicity、Consistency、Isolation、Durability)即原子性、一致性、隔离性、持久性。

当数据库上有多个事务同时执行的时候,就可能会出现脏读(dirty read)、不可重复读(nonrepeatable read)、幻读(phantom read)的问题。

隔离级别

SQL标准的隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复度(repeatable read)和串行化(serializable)。

  • 读未提交:一个事务还没有提交时,它做的变更就能被别的事务看到。

  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到。

  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务启动的时候看到的数据一致。当然在可重复读的隔离级别下,未提交的变更对其他事务也是不可见的。

  • 串行化:对同一条记录“写”会加“写锁”,“读”会加“读锁”。当出现读写冲突的时候,后访问的事务必须等待前一个事务执行完成,才能继续执行。

事务隔离的实现

数据库隔离的实现上会创建一个视图,访问的时候以视图的逻辑结果为准。

“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都会用这个视图。

“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的

**“读未提交”**隔离级别下直接返回记录上的最新值,没有视图的概念

“串行化”的隔离级别下直接使用加锁的方式避免并行访问

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。

同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。

回滚日志系统会判断,当系统中没有比这个回滚日志更早的read-view的时候,日志会被删除。

建议不要使用长事务

长事务意味着系统里面会存在很老的事务视图,由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

长事务还会占用锁资源,也可能拖垮整个库。

通过以下语句可以查询持续时间超过60s的事务。

1
2
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

事务的启动方式

  1. 显式启动,begin或start transaction。配套的提交语句是commit,回滚语句是commit

  2. set autocommit=0,这个命令会将这个线程自动提交关闭。意味着如果你只执行select语句,这个事务就启动了,而且不会自动提交。这个事务持续存在直到你主动执行commit或rollback语句,或者断开连接。

建议使用set autocommit=1,通过显式语句的方式启动事务。

在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

ACID (Atomicity、Consistency、Isolation、Durability)即原子性、一致性、隔离性、持久性。

当数据库上有多个事务同时执行的时候,就可能会出现脏读(dirty read)、不可重复读(nonrepeatable read)、幻读(phantom read)的问题。

隔离级别

SQL标准的隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复度(repeatable read)和串行化(serializable)。

  • 读未提交:一个事务还没有提交时,它做的变更就能被别的事务看到。

  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到。

  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务启动的时候看到的数据一致。当然在可重复读的隔离级别下,未提交的变更对其他事务也是不可见的。

  • 串行化:对同一条记录“写”会加“写锁”,“读”会加“读锁”。当出现读写冲突的时候,后访问的事务必须等待前一个事务执行完成,才能继续执行。

事务隔离的实现

数据库隔离的实现上会创建一个视图,访问的时候以视图的逻辑结果为准。

“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都会用这个视图。

“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的

**“读未提交”**隔离级别下直接返回记录上的最新值,没有视图的概念

“串行化”的隔离级别下直接使用加锁的方式避免并行访问

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。

同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。

回滚日志系统会判断,当系统中没有比这个回滚日志更早的read-view的时候,日志会被删除。

建议不要使用长事务

长事务意味着系统里面会存在很老的事务视图,由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

长事务还会占用锁资源,也可能拖垮整个库。

通过以下语句可以查询持续时间超过60s的事务。

1
2
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

事务的启动方式

  1. 显式启动,begin或start transaction。配套的提交语句是commit,回滚语句是commit

  2. set autocommit=0,这个命令会将这个线程自动提交关闭。意味着如果你只执行select语句,这个事务就启动了,而且不会自动提交。这个事务持续存在直到你主动执行commit或rollback语句,或者断开连接。

建议使用set autocommit=1,通过显式语句的方式启动事务。

在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

日志系统:一条SQL更新语句是如何执行的?

redo log(重做日志)

InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。

binlog(归档日志)

binlog没有crash-safe能力,只能用于归档。

这两种日志的不同点:

  • redo log是InnoDB引擎特有的,binlog是MySQL的server层实现的,所有引擎都可以使用。

  • redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑。

  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

两阶段提交

1
mysql> update T set c=c+1 where ID=2;

执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程:

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。

  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。

  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。

  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

流程图如下所示:

如何让数据库恢复到半个月任意一秒?

  • 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
  • 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。

总结

建议innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不丢失。

建议sync_binlog 这个参数设置成 1 ,表示每次事务的 binlog 都持久化到磁盘,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

redo log(重做日志)

InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。

binlog(归档日志)

binlog没有crash-safe能力,只能用于归档。

这两种日志的不同点:

  • redo log是InnoDB引擎特有的,binlog是MySQL的server层实现的,所有引擎都可以使用。

  • redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑。

  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

两阶段提交

1
mysql> update T set c=c+1 where ID=2;

执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程:

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。

  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。

  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。

  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

流程图如下所示:

如何让数据库恢复到半个月任意一秒?

  • 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
  • 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。

总结

建议innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不丢失。

建议sync_binlog 这个参数设置成 1 ,表示每次事务的 binlog 都持久化到磁盘,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

基础架构:一条SQL语句是如何执行的?

一条SQL语句是如何执行的?

1
mysql> select * from T where ID=10;

MySQL 分为Server层和存储引擎两部分。

Server 层包括连接器、查询缓存、分析器、优化器、执行器等。涵盖MySQL的大多数核心服务功能,以及所有内置函数(如日期、时间、数字和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

存储引擎是插件式的,支持InnoDB、MyISAM、Memory等。从MySQL5.5.5版本以后默认存储引擎为InnoDB。

连接器

连接器主要负责客户端和MySQL服务端进行连接的。可以通过show processlist查看连接信息,客户端和服务端默认连接超时为8小时,可以通过过wait_timeout参数配置。

长连接使用一段时间后会导致MySQL内存长得很快,主要是因为MySQL在执行过程中临时使用的内存是管理在连接对象里面。这些资源会在连接重新断开的时候才释放。
解决方案如下:

  • 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存大的查询后,断开连接,之后要查询再重连。

  • MySQL5.7以后,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存

之前执行的查询结果可能会以key-value对的形式缓存在内存中,如果查询能够直接在缓存中找到key,那么会直接把value返回给客户端。

*** 大多数情况下不建议使用查询缓存。***

如果表中的数据修改的比较频繁,查询缓存的命中率就会非常的低。MySQL提供了以下方式使用查询缓存:

1
mysql> select SQL_CACHE * from T where ID=10;

注意: MySQL8.0版本直接将查询缓存的整块功能删掉了。

分析器

分析器主要是分析语法是否正确,如果不正确就会收到“You have an error in your SQL syntax”的错误提醒,一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容。如果查询语句中包含表中不存在的字段,也是在这一步分析。

优化器

优化器的作用主要是决定使用那个索引,或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

开始执行的时候,要先判断你对这个表有没有执行的查询权限,如果没有,就会返回没有权限的错误。使用慢SQL查询日志中看到rows_examined字段,表示执行过程中扫描了多少行,但有些情况下执行器调用一次,在引擎内部则扫描多行,因此引擎扫描行数跟rows_examined并不是完全相同

一条SQL语句是如何执行的?

1
mysql> select * from T where ID=10;

MySQL 分为Server层和存储引擎两部分。

Server 层包括连接器、查询缓存、分析器、优化器、执行器等。涵盖MySQL的大多数核心服务功能,以及所有内置函数(如日期、时间、数字和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

存储引擎是插件式的,支持InnoDB、MyISAM、Memory等。从MySQL5.5.5版本以后默认存储引擎为InnoDB。

连接器

连接器主要负责客户端和MySQL服务端进行连接的。可以通过show processlist查看连接信息,客户端和服务端默认连接超时为8小时,可以通过过wait_timeout参数配置。

长连接使用一段时间后会导致MySQL内存长得很快,主要是因为MySQL在执行过程中临时使用的内存是管理在连接对象里面。这些资源会在连接重新断开的时候才释放。
解决方案如下:

  • 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存大的查询后,断开连接,之后要查询再重连。

  • MySQL5.7以后,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存

之前执行的查询结果可能会以key-value对的形式缓存在内存中,如果查询能够直接在缓存中找到key,那么会直接把value返回给客户端。

*** 大多数情况下不建议使用查询缓存。***

如果表中的数据修改的比较频繁,查询缓存的命中率就会非常的低。MySQL提供了以下方式使用查询缓存:

1
mysql> select SQL_CACHE * from T where ID=10;

注意: MySQL8.0版本直接将查询缓存的整块功能删掉了。

分析器

分析器主要是分析语法是否正确,如果不正确就会收到“You have an error in your SQL syntax”的错误提醒,一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容。如果查询语句中包含表中不存在的字段,也是在这一步分析。

优化器

优化器的作用主要是决定使用那个索引,或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

开始执行的时候,要先判断你对这个表有没有执行的查询权限,如果没有,就会返回没有权限的错误。使用慢SQL查询日志中看到rows_examined字段,表示执行过程中扫描了多少行,但有些情况下执行器调用一次,在引擎内部则扫描多行,因此引擎扫描行数跟rows_examined并不是完全相同

数据复制

数据复制方案,人们通常希望达到以下几个目的:

  • 使数据在地理位置上更接近用户,从而降低访问延迟。
  • 当部分组件出现故障,系统依然可以继续工作,从而提升可用性。
  • 扩展至多台机器以同时提供数据访问服务,从而提高吞吐量。

三种比较流行的复制数据变化的方法:

  • 主从复制
  • 多主节点复制
  • 无主节点复制

复制技术存在很多折中的方案,例如:同步复制和异步复制,一般情况数据库会采用配置的形式来处理这些策略。

主节点与从节点

主从复制的工作原理如下:

  1. 指定一个副本为主节点,当客户写数据库时,只能通过主节点进行写入。
  2. 其他副本作为从副本,主副本将数据写入本地存储后,将数据更改作为复制日志或更改流发送到从副本。每个副本获取到更改数据后将其应用到本地,并且严格保持与主副本相同的写入顺序。
  3. 客户端读取数据时从主副本或者从副本进行读取,从客户端的角度看,从副本都是只读的。

同步复制与异步复制

同步复制是需要等从节点确认完成了写入之后,才会向用户报告完成。并将最新的写入对其他客户端可见。异步复制是主节点发送完消息之后立即返回,不需要等待从节点完成确认。

同步复制的优点

一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。

同步复制的缺点

如果同步的从节点无法完成确认(例如由于从节点发送崩溃、网络故障或其他原因),写入就不能视为成功。主节点会阻塞其后面所有的写操作,直到同步副本确认完成。

把所有从节点都设置为同步复制有些不切实际,实践中,可以将某一个节点设置为同步复制,其他节点设置为异步复制。万一同步复制的节点变得不可用或者性能下降,则将另外一个节点从异步模式提升为同步模式,这样可以保证至少有两个节点拥有最新的数据副本。这种配置有时称为半同步

异步复制的优点

不管从节点的数据多么滞后,主节点总是可以继续响应客户端的写请求,系统的吞吐性能更好。

异步复制的缺点

如果主节点发生故障并且不可恢复,则所有尚未复制到从节点的数据将会丢失,这就意味着向客户端确认了写请求,但是却无法保证数据的持久化。

配置新的从节点

如何考虑添加新的从节点,怎么保证主从数据一致性呢?

  1. 在某个时间节点对主节点的数据副本生成一个一致性快照。
  2. 将快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定的位置相关联。
  4. 获得日志之后,从节点应用这些快照点之后的所有数据变更,这个过程称之为追赶。接下来,他可以继续处理主节点上的新的数据变化。并重复1~4步骤。

从节点失效:追赶式恢复

从节点如果发生了崩溃或者网络闪断,则根据最后一笔事务的处理日志,从主节点拉取之后的所有数据的变更,收到所有数据的变更后,将其应用到本地用于追赶主节点,之后就和正常情况一样持续接收来自主节点数据流的变化。

主节点失效:节点切换

  1. 确定主节点失效。大部分系统都采用了基于超时的机制判断节点是否失效,节点间频繁地互相发送心跳存活信息,如果某一个节点在一段时间内(例如30s)没有响应,则认为该节点发生了实效。
  2. 选举新的主节点。可以通过选举的方式来选举主节点,候选节点最好与主节点的数据差异最小,这样可以最小化数据丢失的风险。
  3. 重新配置系统使新的主节点生效。如果原主节点重新上线后,可能仍然认为自己是主节点,这是系统要确保原主节点降级为从节点,并认可新的主节点。

以上切换过程可能会发生很多变数

  • 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线加入到集群,这是可能原主节点并未意识到角色的变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是:原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。

  • 如果数据库依赖于外部系统(例如Redis)一起协同使用,丢弃数据的方案就特别危险。例如,在GithHub的一次事故中,某个数据并完全的MySQL从节点提升为主节点,数据库使用了基于自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点(即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部Redis所引用,结果出现MySQL和Redis之间的不一致,最后导致了某些私有数据被错误的泄露给了其它用户。

  • 某些情况下,可能会发生两个节点同时认为自己是主节点,这种情况下被称为脑裂,它非常危险:两个主节点都可能接收写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏,一种安全应急方案会强制关闭其中的一个节点。

  • 如何设置合适的超时来检查主节点失效?主节点失效后,超时时间设置的越长,也就意味着数据恢复的时间越长。如果设置的太短,可能会导致很多不必要的节点切换。例如:突然的负载峰值会导致节点的响应时间变长甚至超时,或者由于网络故障导致延迟增加。如果系统本身已经处于高负载或网络严重拥塞的情况下,不必要的切换只会导致系统的情况变得更糟。

复制日志的实现

基于语句的复制

一些不适用的场景:

  • 任何调用非确定性的语句,如NOW()获取当前时间,或RAND()获取一个随机数等,可能会在不同副本产生不同的值。

  • 如果语句中使用自增列,或者依赖数据库的现有数据(例如:UPDATE … WHERE … <某些条件>),则所有副本必须按照相同的顺序执行,否则可能会带来不同的结果。

  • 有副作用的语句(例如:触发器、存储过程、用户自定义的函数等),可能会在每个副本产生不同的副作用。

可能解决的方案是将不确定的函数替换成确定的结果,不过这种方式仍有许多地方需要考虑。

基于预写日志(WAL)传输

  • 对于日志结构的存储引擎(例如:SSTables和LSM-trees),日志是主要的存储方式。日志段在后台压缩并支持垃圾回收。

  • 对于采用覆盖写磁盘的Btree结构,每次修改会预先写入日志,如果系统崩溃,通过索引更新的方式迅速恢复到此前一直状态。

缺点:

WAL包含了哪些磁盘块的哪些字节发生了改变,诸如此类细节,这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。

基于行的逻辑日志复制

关系数据库的逻辑日志通常是指一些列记录数据行级别的写请求:

  • 对行的插入,日志包含所有相关列的新值。

  • 对于行的删除,日志有足够的的信息来唯一标识已删除的行。

  • 对于行的更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值。

MySQL的二进制日志binlog(当配置基于行的复制时)使用该方式。

基于触发器的复制

基于触发器复制支持更高的灵活性。例如将一种数据库的数据复制到另外一种数据库。触发器支持注册自己的应用层代码,使数据库发生数据更改时自动执行自定义代码。 基于触发器的复制通常比其他方式复制开销更高,也比数据库内置复制更容易出错或者暴露一些限制。

复制滞后的问题

由于并非所有写入都反映到副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的接口,这种不一致的状态只是暂时的,可能经过一段时间后,从节点最终会赶上主节点并与主节点保持一致。这种效应也被称为最终一致性。

读自己的写

读自己的写也被称为读写一致性。实现读写一致性有多重可行方案:

  • 总是从主节点读取当前用户自己的数据,而从从节点读取其他用户的数据。

  • 跟踪数据最近更新时间,如果数据更新后一分钟内总是从主节点读取数据,并监控从节点复制之后的程度,避免从那些滞后时间超过一分钟的从节点读取。

  • 客户端记住最近更新时的时间戳,并附带到请求中,根据此信息,系统可以保证对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,则交给其他副本进行处理,要么等待直到副本接收到了最近的更新。

  • 如果副本分布在多数据中心,必须先把请求路由到主节点所在的数据中心。

单调读

主要是解决用户看到了最新内容之后又读到了过期的内容,好像时间被回拨,此时需要单调读一致性。(出现这种情况主要是因为主从节点数据不一致或同步滞后导致的)

解决方案:确保每个用户总是从固定的同一副本执行读取。(例如:基于用户Id进行哈希的方式选择副本,而不是随机选择)

前缀一致性

在许多分布式数据库中,不同分区独立运行,因此不存在写入顺序。这就导致当用户从数据库中读取数据时,可能会看到数据库的某部分新值和另一部分旧值。

一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但这种方案真是实现效率会大打折扣。

数据复制方案,人们通常希望达到以下几个目的:

  • 使数据在地理位置上更接近用户,从而降低访问延迟。
  • 当部分组件出现故障,系统依然可以继续工作,从而提升可用性。
  • 扩展至多台机器以同时提供数据访问服务,从而提高吞吐量。

三种比较流行的复制数据变化的方法:

  • 主从复制
  • 多主节点复制
  • 无主节点复制

复制技术存在很多折中的方案,例如:同步复制和异步复制,一般情况数据库会采用配置的形式来处理这些策略。

主节点与从节点

主从复制的工作原理如下:

  1. 指定一个副本为主节点,当客户写数据库时,只能通过主节点进行写入。
  2. 其他副本作为从副本,主副本将数据写入本地存储后,将数据更改作为复制日志或更改流发送到从副本。每个副本获取到更改数据后将其应用到本地,并且严格保持与主副本相同的写入顺序。
  3. 客户端读取数据时从主副本或者从副本进行读取,从客户端的角度看,从副本都是只读的。

同步复制与异步复制

同步复制是需要等从节点确认完成了写入之后,才会向用户报告完成。并将最新的写入对其他客户端可见。异步复制是主节点发送完消息之后立即返回,不需要等待从节点完成确认。

同步复制的优点

一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。

同步复制的缺点

如果同步的从节点无法完成确认(例如由于从节点发送崩溃、网络故障或其他原因),写入就不能视为成功。主节点会阻塞其后面所有的写操作,直到同步副本确认完成。

把所有从节点都设置为同步复制有些不切实际,实践中,可以将某一个节点设置为同步复制,其他节点设置为异步复制。万一同步复制的节点变得不可用或者性能下降,则将另外一个节点从异步模式提升为同步模式,这样可以保证至少有两个节点拥有最新的数据副本。这种配置有时称为半同步

异步复制的优点

不管从节点的数据多么滞后,主节点总是可以继续响应客户端的写请求,系统的吞吐性能更好。

异步复制的缺点

如果主节点发生故障并且不可恢复,则所有尚未复制到从节点的数据将会丢失,这就意味着向客户端确认了写请求,但是却无法保证数据的持久化。

配置新的从节点

如何考虑添加新的从节点,怎么保证主从数据一致性呢?

  1. 在某个时间节点对主节点的数据副本生成一个一致性快照。
  2. 将快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定的位置相关联。
  4. 获得日志之后,从节点应用这些快照点之后的所有数据变更,这个过程称之为追赶。接下来,他可以继续处理主节点上的新的数据变化。并重复1~4步骤。

从节点失效:追赶式恢复

从节点如果发生了崩溃或者网络闪断,则根据最后一笔事务的处理日志,从主节点拉取之后的所有数据的变更,收到所有数据的变更后,将其应用到本地用于追赶主节点,之后就和正常情况一样持续接收来自主节点数据流的变化。

主节点失效:节点切换

  1. 确定主节点失效。大部分系统都采用了基于超时的机制判断节点是否失效,节点间频繁地互相发送心跳存活信息,如果某一个节点在一段时间内(例如30s)没有响应,则认为该节点发生了实效。
  2. 选举新的主节点。可以通过选举的方式来选举主节点,候选节点最好与主节点的数据差异最小,这样可以最小化数据丢失的风险。
  3. 重新配置系统使新的主节点生效。如果原主节点重新上线后,可能仍然认为自己是主节点,这是系统要确保原主节点降级为从节点,并认可新的主节点。

以上切换过程可能会发生很多变数

  • 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线加入到集群,这是可能原主节点并未意识到角色的变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是:原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。

  • 如果数据库依赖于外部系统(例如Redis)一起协同使用,丢弃数据的方案就特别危险。例如,在GithHub的一次事故中,某个数据并完全的MySQL从节点提升为主节点,数据库使用了基于自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点(即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部Redis所引用,结果出现MySQL和Redis之间的不一致,最后导致了某些私有数据被错误的泄露给了其它用户。

  • 某些情况下,可能会发生两个节点同时认为自己是主节点,这种情况下被称为脑裂,它非常危险:两个主节点都可能接收写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏,一种安全应急方案会强制关闭其中的一个节点。

  • 如何设置合适的超时来检查主节点失效?主节点失效后,超时时间设置的越长,也就意味着数据恢复的时间越长。如果设置的太短,可能会导致很多不必要的节点切换。例如:突然的负载峰值会导致节点的响应时间变长甚至超时,或者由于网络故障导致延迟增加。如果系统本身已经处于高负载或网络严重拥塞的情况下,不必要的切换只会导致系统的情况变得更糟。

复制日志的实现

基于语句的复制

一些不适用的场景:

  • 任何调用非确定性的语句,如NOW()获取当前时间,或RAND()获取一个随机数等,可能会在不同副本产生不同的值。

  • 如果语句中使用自增列,或者依赖数据库的现有数据(例如:UPDATE … WHERE … <某些条件>),则所有副本必须按照相同的顺序执行,否则可能会带来不同的结果。

  • 有副作用的语句(例如:触发器、存储过程、用户自定义的函数等),可能会在每个副本产生不同的副作用。

可能解决的方案是将不确定的函数替换成确定的结果,不过这种方式仍有许多地方需要考虑。

基于预写日志(WAL)传输

  • 对于日志结构的存储引擎(例如:SSTables和LSM-trees),日志是主要的存储方式。日志段在后台压缩并支持垃圾回收。

  • 对于采用覆盖写磁盘的Btree结构,每次修改会预先写入日志,如果系统崩溃,通过索引更新的方式迅速恢复到此前一直状态。

缺点:

WAL包含了哪些磁盘块的哪些字节发生了改变,诸如此类细节,这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。

基于行的逻辑日志复制

关系数据库的逻辑日志通常是指一些列记录数据行级别的写请求:

  • 对行的插入,日志包含所有相关列的新值。

  • 对于行的删除,日志有足够的的信息来唯一标识已删除的行。

  • 对于行的更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值。

MySQL的二进制日志binlog(当配置基于行的复制时)使用该方式。

基于触发器的复制

基于触发器复制支持更高的灵活性。例如将一种数据库的数据复制到另外一种数据库。触发器支持注册自己的应用层代码,使数据库发生数据更改时自动执行自定义代码。 基于触发器的复制通常比其他方式复制开销更高,也比数据库内置复制更容易出错或者暴露一些限制。

复制滞后的问题

由于并非所有写入都反映到副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的接口,这种不一致的状态只是暂时的,可能经过一段时间后,从节点最终会赶上主节点并与主节点保持一致。这种效应也被称为最终一致性。

读自己的写

读自己的写也被称为读写一致性。实现读写一致性有多重可行方案:

  • 总是从主节点读取当前用户自己的数据,而从从节点读取其他用户的数据。

  • 跟踪数据最近更新时间,如果数据更新后一分钟内总是从主节点读取数据,并监控从节点复制之后的程度,避免从那些滞后时间超过一分钟的从节点读取。

  • 客户端记住最近更新时的时间戳,并附带到请求中,根据此信息,系统可以保证对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,则交给其他副本进行处理,要么等待直到副本接收到了最近的更新。

  • 如果副本分布在多数据中心,必须先把请求路由到主节点所在的数据中心。

单调读

主要是解决用户看到了最新内容之后又读到了过期的内容,好像时间被回拨,此时需要单调读一致性。(出现这种情况主要是因为主从节点数据不一致或同步滞后导致的)

解决方案:确保每个用户总是从固定的同一副本执行读取。(例如:基于用户Id进行哈希的方式选择副本,而不是随机选择)

前缀一致性

在许多分布式数据库中,不同分区独立运行,因此不存在写入顺序。这就导致当用户从数据库中读取数据时,可能会看到数据库的某部分新值和另一部分旧值。

一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但这种方案真是实现效率会大打折扣。

程序员的测试课

时代在要求我们写测试

因为软件变得越来越复杂,测试可以让我们在复杂的软件开发中稳步前行。另一方面软件测试可以让我们在长期的过程中不断回归,让每一步走的更稳。

程序员圈子流传着一个关于测试的段子:** 每个程序员在修改代码时都希望有测试,而在写代码时,都不想写测试。**

大部分程序员都不会写测试

很多程序员反对写测试,本质上的原因是因为他们不会写测试。

你的代码质量真的高吗?

  • 经过测试的代码,质量会更高;
  • 要想写好测试,代码本身质量也要高。

如果你连测试都做不好,你对自己代码的信心从何而来呢?

学习写测试

最好的办法就是跟着会写测试的人一起写一段时间

思考:可以查阅优秀的开源代码是如何写测试的。


ToDo项目的一些基本准备工作

  • 一个项目的自动化;
  • 对需求进行简单设计。

为什么需要自动化呢?简单来说是为了防止一些低级错误。

把核心的业务部分和命令行呈现的部分分开。

任务分解

从离我们需求最近的入口开始。

要想测试一个函数,一个函数最好是可测的。什么是可测的?就是通过函数的接口设计,我们给出特定的输入,它能给我们相应的输出。所以,一个函数最好是有返回值的。

Fail Fast 原则
一条设计规范:对于输入参数的检测,由入口部分代码进行处理。
一条设计规范:Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

项目刚开始时,我们要准备哪些内容:

  • 项目的自动化;
  • 针对需求进行初步的设计。

着手编写代码时,我们要怎么做呢?

  • 对要实现的需求进行任务分解;
  • 在一个具体的需求任务中,我们可以从需求入口开始入手;
  • 设计一个可测试的函数;
  • 针对具体的函数,考虑测试场景;
  • 针对具体的测试场景,将场景具象化成测试用例。

在梳理的过程中,我们还会针对一些统一的情况作出一些约定,成为项目整体的设计规范,比如,在这里我们约定:

  • 对于输入参数的检测,由入口部分代码进行处理;
  • Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

在编码的过程中,我们也看到了:

  • 根据不断增加的需求,逐渐改动我们的设计,这就是演化式设计的基本做法;
  • 我们对待测试也像对待代码一样,会消除代码中存在的一些坏味道。

时代在要求我们写测试

因为软件变得越来越复杂,测试可以让我们在复杂的软件开发中稳步前行。另一方面软件测试可以让我们在长期的过程中不断回归,让每一步走的更稳。

程序员圈子流传着一个关于测试的段子:** 每个程序员在修改代码时都希望有测试,而在写代码时,都不想写测试。**

大部分程序员都不会写测试

很多程序员反对写测试,本质上的原因是因为他们不会写测试。

你的代码质量真的高吗?

  • 经过测试的代码,质量会更高;
  • 要想写好测试,代码本身质量也要高。

如果你连测试都做不好,你对自己代码的信心从何而来呢?

学习写测试

最好的办法就是跟着会写测试的人一起写一段时间

思考:可以查阅优秀的开源代码是如何写测试的。


ToDo项目的一些基本准备工作

  • 一个项目的自动化;
  • 对需求进行简单设计。

为什么需要自动化呢?简单来说是为了防止一些低级错误。

把核心的业务部分和命令行呈现的部分分开。

任务分解

从离我们需求最近的入口开始。

要想测试一个函数,一个函数最好是可测的。什么是可测的?就是通过函数的接口设计,我们给出特定的输入,它能给我们相应的输出。所以,一个函数最好是有返回值的。

Fail Fast 原则
一条设计规范:对于输入参数的检测,由入口部分代码进行处理。
一条设计规范:Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

项目刚开始时,我们要准备哪些内容:

  • 项目的自动化;
  • 针对需求进行初步的设计。

着手编写代码时,我们要怎么做呢?

  • 对要实现的需求进行任务分解;
  • 在一个具体的需求任务中,我们可以从需求入口开始入手;
  • 设计一个可测试的函数;
  • 针对具体的函数,考虑测试场景;
  • 针对具体的测试场景,将场景具象化成测试用例。

在梳理的过程中,我们还会针对一些统一的情况作出一些约定,成为项目整体的设计规范,比如,在这里我们约定:

  • 对于输入参数的检测,由入口部分代码进行处理;
  • Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

在编码的过程中,我们也看到了:

  • 根据不断增加的需求,逐渐改动我们的设计,这就是演化式设计的基本做法;
  • 我们对待测试也像对待代码一样,会消除代码中存在的一些坏味道。