2024年7月

一、AOF
1、AOF  是什么
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

2、AOF默认不开启,需要手动在配置文件中配置

# AOF 持久化开启  
# appendonly no

# AOF 文件的名称
# appendfilename
"appendonly.aof"# AOF 持久化策略
# appendfsync always
# appendfsync everysec
# appendfsync no

3、可以在redis.conf中配置文件名称,默认为 appendonly.aof

# AOF 文件的名称  
# appendfilename
"appendonly.aof"

4、AOF文件的保存路径,同RDB的路径一致
5、AOF和RDB同时开启,redis听谁的?
以 AOF 为准,因为 AOF 里面保存的数据比较完整。

6、AOF文件故障备份
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载;

AOF和RDB同时开启,系统默认取AOF的数据;

7、AOF文件故障恢复
AOF文件的保存路径,同RDB的路径一致。

如遇到AOF文件损坏,可通过下面的命令进行恢复:

redis-check-aof  --fix  appendonly.aof   进行恢复

遇到的报错一般如下:

# oO0OoO0OoO0Oo Redis isstarting oO0OoO0OoO0Oo
# Redis version
=5.0.10, bits=64, commit=00000000, modified=0, pid=121, just started
# Configuration loaded
* Running mode=sentinel, port=26379.
# Sentinel ID
isb9715439b50e476eac19d423906edfad904006fb
#
+monitor master redismaster 100.127.64.21 6379 quorum 2# oO0OoO0OoO0Oo Redisisstarting oO0OoO0OoO0Oo
# Redis version
=5.0.10, bits=64, commit=00000000, modified=0, pid=120, just started
# Configuration loaded
* +slave slave 100.127.64.19:6379 100.127.64.19 6379 @ redismaster 100.127.64.21 6379 * Running mode=standalone, port=6379.
# Server initialized
* Reading RDB preamble fromAOF file...*Reading the remaining AOF tail...
# Bad file format reading the append only file: make a backup of your AOF file, then use .
/redis-check-aof --fix <filename> * +sentinel sentinel 09533b1fd689c178ace11264b1474948dff10189 100.127.64.21 26379 @ redismaster 100.127.64.21 6379 * +sentinel sentinel 7a6a997019bdf8a95c57b494701f4877a5cb5edc 100.127.64.19 26379 @ redismaster 100.127.64.21 6379

8、AOF同步频率设置
AOF 提供了三种保存策略:

# AOF 持久化策略  
# appendfsync always
# appendfsync everysec
# appendfsync no

说明:

appendfsync always:始终同步,每次Redis的写入都会立刻记入日志
appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no:把不主动进行同步,把同步时机交给操作系统
 

9、Rewrite
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

10、Redis如何实现重写?
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

11、何时重写
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb
  • auto-aof-rewrite-percentage:这个配置项指定了AOF文件增长率的百分比阈值。当AOF文件的大小超过上一次重写时的大小的这个百分比时,Redis会触发AOF重写。默认值通常为100%,意味着AOF文件大小翻倍时会触发重写。

  • auto-aof-rewrite-min-size:这个配置项指定了AOF文件的最小大小阈值。只有当AOF文件的大小超过这个值时,才会考虑上述的百分比增长条件来触发重写。默认值通常为64MB。

12.AOF的优缺点

AOF的优点

  1. 数据完整性高:AOF通过保存每次写操作的命令来确保数据的完整性,即使发生宕机,也可以通过重新执行AOF文件中的命令来恢复数据,从而最小化数据丢失。
  2. 实时性:AOF可以配置为每秒同步一次或每次写操作都同步,从而提供较高的实时性。
  3. 易于理解和修改:AOF文件是以纯文本形式存储的,易于理解和修改(尽管不推荐直接修改AOF文件)。
  4. 支持多种同步方式:Redis提供了多种AOF同步策略,可以根据性能和数据安全性的需求进行选择。

AOF的缺点

  1. 文件体积大:随着时间的推移,AOF文件可能会变得非常大,占用大量的磁盘空间,并影响Redis的启动和恢复速度。
  2. 恢复速度慢:在Redis启动时,需要重新执行AOF文件中的命令来恢复数据,如果AOF文件很大,恢复过程可能会非常耗时。
  3. 对写性能的影响:当使用较高的同步策略时(如
    always
    ),每个写操作都需要等待磁盘I/O操作完成,可能会降低Redis的写性能。
  4. 重写过程可能占用资源:AOF重写是一个资源密集型的操作,需要创建子进程来执行,可能会占用大量的CPU和内存资源。

13.RDB的优缺点

RDB的优点

  1. 文件体积小:RDB文件是Redis数据库在某个时间点的内存快照,通常比AOF文件小得多,便于存储和传输。
  2. 恢复速度快:由于RDB文件只保存了内存快照,恢复数据时只需要加载一次文件,可以快速地恢复数据。
  3. 对系统性能影响小:RDB持久化过程由子进程负责,减少了主线程的负载,对系统性能影响较小。
  4. 适用于备份和容灾:RDB文件是一个紧凑的二进制文件,非常适合用于备份和容灾恢复。

RDB的缺点

  1. 数据实时性低:RDB是间隔一段时间进行持久化,无法做到实时持久化,如果在这一间隔内发生故障,可能会丢失数据。
  2. 可能存在兼容性问题:Redis演进过程中存在多个格式的RDB版本,可能存在老版本Redis无法兼容新版本RDB文件的问题。
  3. 恢复时可能丢失数据:如果Redis在生成RDB文件之前崩溃,将会丢失最后一次持久化后的数据。
  4. 可读性低:RDB文件是一个二进制文件,不是易于读取和理解的文本文件,不便于进行数据恢复、备份和分析。

本文分享自华为云社区
《【华为云MySQL技术专栏】MySQL中为什么要使用索引合并(Index Merge)?》
,作者:GaussDB 数据库。

在生产环境中,MySQL语句的where查询通常会包含多个条件判断,以AND或OR操作进行连接。然而,对一个表进行查询最多只能利用该表上的一个索引,其他条件需要在回表查询时进行判断(不考虑覆盖索引的情况)。当回表的记录数很多时,需要进行大量的随机IO,这可能导致查询性能下降。因此,MySQL 5.x 版本推出索引合并(Index Merge)来解决该问题。

本文将基于MySQL 8.0.22版本对MySQL的索引合并功能、实现原理及场景约束进行详细介绍,同时也会结合原理对其优缺点进行浅析,并通过例子进行验证。

什么是索引合并(Index Merge)?

索引合并是通过对一个表同时使用多个索引进行条件扫描,并将满足条件的多个主键集合取交集或并集后再进行回表,可以提升查询效率。

索引合并主要包含交集(intersection),并集(union)和排序并集(sort-union)三种类型:

  • intersection:将基于多个索引扫描的结果集取交集后返回给用户;
  • union:将基于多个索引扫描的结果集取并集后返回给用户;
  • sort-union:与union类似,不同的是sort-union会对结果集进行排序,随后再返回给用户;

MySQL中有四个开关(index_merge、index_merge_intersection、index_merge_union以及index_merge_sort_union)对上述三种索引合并类型提供支持,可以通过修改optimizer_switch系统参数中的四个开关标识来控制索引合并特性的使用。

假设创建表T,并插入如下数据:

CREATE TABLE T(  `id` intNOT NULL AUTO_INCREMENT,
`a`
intNOT NULL,
`b`
char(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`) USING BTREE,
KEY `idx_b` (`b`) USING BTREE
)ENGINE
=InnoDB AUTO_INCREMENT=1;

INSERT INTO T (a, b) VALUES (
1, 'A'), (2, 'B'),(3, 'C'),(4, 'B'),(1, 'C');

默认情况下,四个开关均为开启状态。如果需要单独使用某个合并类型,需设置index_merge=off,并将相应待启用的合并类型标识(例如,index_merge_sort_union)设置为on。

开关开启后,可通过EXPLAIN执行计划查看当前查询语句是否使用了索引合并。

mysql> explain SELECT * FROM T WHERE a=1 OR b='B';+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key         | key_len | ref  | rows | filtered |Extra|+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | idx_a,idx_b   | idx_a,idx_b | 4,5     | NULL |    4 |   100.00 | Using union(idx_a,idx_b); Using where |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
1 row in set, 1 warning (0.01 sec)

上面代码显示type类型为index_merge,表示使用了索引合并。key列显示使用到的所有索引名称,该语句中同时使用了idx_a和idx_b两个索引完成查询。Extra列显示具体使用了哪种类型的索引合并,该语句显示Using union(...),表示索引合并类型为union。

此外,可以使用index_merge/no_index_merge给查询语句添加hint,强制SQL语句使用/不使用索引合并。

• 如果查询默认未使用索引合并,可以通过添加index_merge强制指定:

mysql> EXPLAIN SELECT * FROM T WHERE a=2 AND b='A';+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | T     | NULL       | ref  | idx_a,idx_b   | idx_a | 4       | const |    1 |    20.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00sec)


mysql
> EXPLAIN SELECT /*+ INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=2 AND b='A';+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------------------+ | 1 | SIMPLE | T | NULL | index_merge | idx_a,idx_b | idx_a,idx_b | 4,5 | NULL | 1 | 100.00 | Using intersect(idx_a,idx_b); Using where; Using index | +----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------------------+ 1 row in set, 1 warning (0.00 sec)

• 使用no_index_merge给查询语句添加hint,可以忽略索引合并优化:

mysql> EXPLAIN SELECT * FROM T WHERE a=1 OR b='A';+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key         | key_len | ref  | rows | filtered | Extra                                 |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | idx_a,idx_b   | idx_a,idx_b | 4,5     | NULL |    3 |   100.00 | Using union(idx_a,idx_b); Using where |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
1 row in set, 1 warning (0.00sec)
mysql
> EXPLAIN SELECT /*+ NO_INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=1 OR b='A';+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | T | NULL | ALL | idx_a,idx_b | NULL | NULL | NULL | 5 | 36.00 | Using where | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec)

索引合并(Index Merge)原理

1. Index Merge Intersection

Index Merge Intersection会在使用到的多个索引上同时进行扫描,并取这些扫描结果的交集作为最终结果集。

以“SELECT * FROM T WHERE a=1 AND b='C'; ”语句为例:

• 未使用索引合并时,MySQL利用索引idx_a获取到满足条件a=1的所有主键id,根据主键id进行回表查询到相关记录,随后再使用条件b='C'对这些记录进行判断,获取最终查询结果。

mysql> explain SELECT /*+ NO_INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=1 AND b='C';+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | T     | NULL       | ref  | idx_a,idx_b   | idx_a | 4       | const |    2 |    40.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

• 使用索引合并时,MySQL分别利用索引idx_a和idx_b获取满足条件a=1和b='C'的主键id集合setA和setB。随后取setA和setB中主键id的交集setC,并使用setC中主键id进行回表,获取最终查询结果。

mysql> explain SELECT * FROM T WHERE a=1 AND b='C';+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key         | key_len | ref  | rows | filtered | Extra                                                  |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | idx_a,idx_b   | idx_a,idx_b | 4,5     | NULL |    1 |   100.00 | Using intersect(idx_a,idx_b); Using where; Using index |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

执行流程如下:

222.PNG

图1 SELECT * FROM T WHERE a=1 AND b='C';执行流程

2. Index Merge Union

Index Merge Union会在使用到的多个索引上同时进行扫描,并取这些扫描结果的并集作为最终结果集。

以“SELECT * FROM T WHERE a=1 OR b='B'; ”语句为例:

• 未使用索引合并时,MySQL通过全表扫描获取所有记录信息,随后再使用条件a=1和b='B'对这些记录进行判断,获取最终查询结果。

mysql> EXPLAIN SELECT /*+ NO_INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=1 OR b='B';+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | T     | NULL       | ALL  | idx_a,idx_b   | NULL | NULL    | NULL |    5 |    50.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

• 使用索引合并算法时,MySQL分别利用索引idx_a和idx_b获取满足条件a=1和b='B'的主键id集合setA和setB。随后,取setA和setB中主键id的并集setC,并使用setC中主键id进行回表,获取最终查询结果。

mysql> EXPLAIN SELECT * FROM T WHERE a=1 OR b='B';+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key         | key_len | ref  | rows | filtered | Extra                                 |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | idx_a,idx_b   | idx_a,idx_b | 4,5     | NULL |    4 |   100.00 | Using union(idx_a,idx_b); Using where |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
1 row in set, 1 warning (0.01 sec)

执行流程如下:

333.PNG

图2 SELECT * FROM T WHERE a=1 OR b='B';执行流程

3. Index Merge Sort-Union

Sort-Union索引合并与Union索引合并原理相似,只是比单纯的Union索引合并多了一步对二级索引记录的主键id排序的过程。由OR连接的多个范围查询条件组成的WHERE子句不满足Union算法时,优化器会考虑使用Sort-Union算法。例如:

mysql> EXPLAIN SELECT * FROM T WHERE a<3 OR b<'B';+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key         | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | idx_a,idx_b   | idx_a,idx_b | 4,5     | NULL |    4 |   100.00 | Using sort_union(idx_a,idx_b); Using where |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+--------------------------------------------+
1 row in set, 1 warning (0.00 sec)

应用场景约束

1. 总体约束

• Index Merge不能应用于全文索引(Fulltext Index)。
• Index Merge只能合并同一个表的索引扫描结果,不能跨表合并。

以上约束适用于Intersection,Union和Sort-Union三种合并类型。此外,Intersection和Union存在特殊的场景约束。

2. Index Merge Intersection

使用Intersection要求AND连接的每个条件必须是如下形式之一:

(1) 当索引包含多个列时,每个列都必须被如下等值条件覆盖,不允许出现范围查询。若使用索引为联合索引时,每个列都必须等值匹配,不能出现只匹配部分列的情况。

key_par1 = const1 AND key_par2 = const2 ... AND key_partN = constN

(2) 若过滤条件中存在主键列,主键列可以进行范围匹配。

mysql> EXPLAIN SELECT * FROM T WHERE id<3 AND b='A';+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key           | key_len | ref  | rows | filtered | Extra                                       |
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | PRIMARY,idx_b | idx_b,PRIMARY | 9,4     | NULL |    1 |   100.00 | Using intersect(idx_b,PRIMARY); Using where |
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
1 row in set, 1 warning (0.00 sec)

上述的要求,本质上是为了确保索引取出的记录是按照主键id有序排列的,因为Index Merge Intersection对两个有序集合取交集更简单。同时,主键有序的情况下,回表将不再是单纯的随机IO,回表的效率也会更高。

3. Index Merge Union

使用Union要求OR连接的每个条件,必须是如下形式之一:

(1) 当索引包含多个列时,则每个列都必须被如下等值条件覆盖,不允许出现范围查询。若使用索引为联合索引时,在联合索引中的每个列都必须等值匹配,不能出现只匹配部分列的情况。

key_par1 = const1 OR key_par2 = const2 ... OR key_partN = constN

(2) 若过滤条件中存在主键列,主键列可以进行范围匹配。

mysql> EXPLAIN SELECT * FROM T WHERE id>3 OR b='A';+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+-----------------------------------------+
| id | select_type | table | partitions | type        | possible_keys | key           | key_len | ref  | rows | filtered | Extra                                   |
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+-----------------------------------------+
|  1 | SIMPLE      | T     | NULL       | index_merge | PRIMARY,idx_b | PRIMARY,idx_b | 4,5     | NULL |    3 |   100.00 | Using union(PRIMARY,idx_b); Using where |
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+-----------------------------------------+
1 row in set, 1 warning (0.00 sec)

Index Merge的优缺点

• Index Merge Intersection在使用到的多个索引上同时进行扫描,并取这些扫描结果的并集作为最终结果集。

当优化器根据搜索条件从某个索引中获取的记录数极多时,适合使用Intersection对取交集后的主键id以顺序I/O进行回表,其开销远小于使用随机IO进行回表。反之,当根据搜索条件扫描出的记录极少时,因为需要多一步合并操作,Intersection反而不占优势。在8.0.22版本,对于AND连接的点查场景,通过建立联合索引可以更好的减少回表。

• Index Merge Union在使用到的多个索引上同时进行扫描,并取这些扫描结果的并集作为最终结果集。

当优化器根据搜索条件从某个索引中获取的记录数比较少,通过Union索引合并后进行访问的代价比全表扫描更小时,使用Union的效果才会更优。

• Index Merge Sort-Union比单纯的Union索引合并多了一步对索引记录的主键id排序的过程。

当优化器根据搜索条件从某个索引中获取的记录数比较少的时,对这些索引记录的主键id进行排序的成本不高,此时可以加速查询。反之,当需要排序的记录过多时,该算法的查询效率不一定更优。

我们以Index Merge Union为例,对上述分析进行验证。

1. 场景构造

# 创建表CREATE TABLE T(  `id` intNOT NULL AUTO_INCREMENT,
`a`
intNOT NULL,`
b`
char(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`) USING BTREE,
KEY `idx_b` (`b`) USING BTREE
)ENGINE
=InnoDB AUTO_INCREMENT=1;

# 插入数据
DELIMITER $$
CREATE PROCEDURE insertT()
BEGIN
DECLARE i INT DEFAULT
0;
START TRANSACTION;
WHILE i
<=100000 do if (i%100 = 0) then
INSERT INTO T (a, b) VALUES (
10,CHAR(rand()*(90-65)+65));elseINSERT INTO T (a, b) VALUES (i,CHAR(rand()*(90-65)+65));
end
if;
SET i
=i+1;
END WHILE;
COMMIT;
END$$
DELIMITER ;
call insertT();

# 执行测试语句
SQL1: SELECT
* FROM T WHERE a=101 OR b='A';
SQL2: SELECT
/*+ NO_INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=101 OR b='A';
SQL3: SELECT
* FROM T WHERE a=10 OR b='A';
SQL4: SELECT
/*+ NO_INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=10 OR b='A';

2. 执行结果及分析

每条语句查询5次,去掉最大值和最小值,取剩余三次结果平均值。4条语句查询结果如下:

测试语句

第一次查询/ms

第二次查询/ms

第三次查询/ms

第四次查询/ms

第五次查询/ms

平均值/ms

SQL1

5.481

5.422

5.117

4.892

5.426

5.322

SQL2

31.129

32.645

30.943

31.142

32.625

31.632

SQL3

7.872

7.200

7.824

7.955

7.949

7.882

SQL4

31.139

33.318

31.476

31.645

31.27

31.464

对比使用索引合并的SQL1和未使用索引合并的SQL2的查询结果可知,使用索引合并的SQL1具有更高的查询效率,这点从语句的explain analyze分析中也可以看出:

使用索引合并的SQL1代码示例:

EXPLAIN ANALYZE SELECT * FROM T WHERE a=101 OR b='A';-> Filter: ((t.a = 101) or (t.b = 'A'))  (cost=717.14 rows=2056) (actual time=0.064..5.481 rows=2056 loops=1)-> Index range scan on T using union(idx_a,idx_b)  (cost=717.14 rows=2056) (actual time=0.062..5.120 rows=2056 loops=1)

未使用索引合并的SQL2代码示例:

EXPLAIN ANALYZE SELECT /*+ NO_INDEX_MERGE(T idx_a,idx_b)*/ * FROM T WHERE a=101 OR b='A';-> Filter: ((t.a = 101) or (t.b = 'A'))  (cost=10098.75 rows=10043) (actual time=0.038..31.129 rows=2056 loops=1)-> Table scan on T  (cost=10098.75 rows=100425) (actual time=0.031..22.967 rows=100001 loops=1)

未使用索引合并时,SQL2语句需要花费约23ms来扫描全表100001行数据,随后再进行条件判断。而使用索引合并时,通过合并两个索引筛选出的主键id集合,筛选出2056个符合条件的主键id, 随后回表获取最终的数据。这个环节中,索引合并大大减少了需要访问的记录数量。

此外,从SQL1和SQL3的查询结果也可以看出,数据分布也会影响索引合并的效果。相同的SQL模板类型,根据匹配数值的不同,查询时间存在差异。如需要合并的主键id集合越小,需要回表的主键id越少,查询时间越短。

EXPLAIN ANALYZE SELECT * FROM T WHERE a=101 OR b='A';-> Filter: ((t.a = 101) or (t.b = 'A'))  (cost=717.14 rows=2056) (actual time=0.064..5.481 rows=2056 loops=1)-> Index range scan on T using union(idx_a,idx_b)  (cost=717.14 rows=2056) (actual time=0.062..5.120 rows=2056 loops=1)


EXPLAIN ANALYZE SELECT
* FROM T WHERE a=10 OR b='A';-> Filter: ((t.a = 10) or (t.b = 'A')) (cost=983.00 rows=3057) (actual time=0.070..7.872 rows=3035 loops=1)-> Index range scan on T using union(idx_a,idx_b) (cost=983.00 rows=3057) (actual time=0.068..7.496 rows=3035 loops=1)

总结

本文介绍了索引合并(Index Merge)包含的三种类型,即交集(intersection)、并集(union)和排序并集(sort-union),以及索引合并的实现原理、场景约束与通过案例验证的优缺点。在实际使用中,当查询条件列较多且无法使用联合索引时,就可以考虑使用索引合并,利用多个索引加速查询。但要注意,索引合并并非在任何场景下均具有较好的效果,需要结合具体的数据分布进行算法的选择。

点击关注,第一时间了解华为云新鲜技术~

cover

对待外包的态度

外包是来钱最快的方式,通过出售自己的时间和技能换取报酬,一定程度上与上班类似。创业后一直在做自己的产品,从习惯打卡软件:
加一
,到灵动岛软件:
Island Widgets
,然后Mac休息提醒软件:
Nap
,到现在正在开发AI作图软件:AI画图王。做自己的产品最开心的就是自由,设计、交互、功能都是自己定,不需要看任何人的脸色,没有deadline。如果有外包的机会来到面前,该怎么对待呢,说下我的态度。

上周好朋友有一个不错的iOS私活,横跨半个北京,见了对方的老板,互相了解了下对方,相谈甚欢。后来与对方技术经理聊了下需求细节,最后我给了报价后就一直没下文了。我猜是双方的需求并没有匹配上。作为外包公司,希望能用就行,没有UI按照原型图开发一个App自然预算不会太高。而我写代码又不会将就,即便是外包代码也要写得漂亮,按照上班时期的日薪报的价显然不会让人满意。通过这个小插曲,也坚定了自己不合适做外包,可能以后也不会有类似的机会,即便有,也不准备再为此投入时间。

技术选型

准备给AI画图王写代码的时候采用什么语言和使用什么框架费了一点功夫,目前会的技术栈有老古董Objective-C,Swift,Flutter和SwiftUI。OC这门上个世纪的语言是第一需要排除的,在Flutter和SwiftUI之间犹豫了一番,Flutter看起来是最适合的,跨平台,性能也不错。但是,我不擅长,而且性能和开发速度也不如原生,最后选择了继续深耕在Apple原生平台,不同的是这次用的SwiftUI这个Apple近年来强推的新框架。计划下个月上架iOS版本,下下个月上架Mac版本。

卖房

为了降低每月开销,第一步去掉房贷,而且烟台基本也不会回去了,17年中烟台买的那个距离海边2km的房子没有必要再留了,挂的价格是那个小区三居室单价最便宜的(69W 单价5800),值得一提的是,假如卖出去还完贷款,就没剩了。首付蒸发了,过去有几年的努力付之一炬。有时候,选择比努力更重要。

IMG_0374

本周进展

摸鱼的一周,学习新技术,开发新产品,打球,看比赛...


订阅

这个专栏会同步更新在 Solo 社区、公众号、知乎、社群。

微信搜索"
Solo 独立开发者社区
"或者扫描二维码,即可手机订阅。

社区网址:
Solo 独立开发者社区-链接每一位独立开发者, 从 Solo 开始

本文参与 Solo 社区自媒体同步曝光计划,分享自 solo 社区。

GlusterFS 笔记

GlusterFS 是一个可扩展的分布式文件系统,旨在处理大量数据的存储需求。它通过将多个存储服务器的存储空间聚合在一起,提供单一的命名空间。以下是 GlusterFS 中几种常见的卷(Volume)类型及其作用,并用通俗的例子来说明每种类型的特点。

主机名 IP
node1 192.168.200.179
node2 192.168.200.180
node3 192.168.200.172

这里面的每个节点都有3块额外的硬盘,每个硬盘20个G

一、安装和配置 GlusterFS

1. 安装 GlusterFS


每个服务器
节点上执行以下命令安装 GlusterFS:

yum install glusterfs-server

启动 GlusterFS 服务:

systemctl enable --now glusterd

2. 配置 GlusterFS

假设我们有三台服务器
node1

node2

node3
,首先在每个节点上编辑
/etc/hosts
文件,添加所有节点的 IP 地址和主机名:

192.168.200.179 node1
192.168.200.180 node2
192.168.200.172 node3

然后,在一个节点上执行以下命令来设置信任关系:

gluster peer probe node2
gluster peer probe node3

使用
gluster peer status
命令来检查集群状态,确保所有节点都已经连接。

3. 格式化卷

自行将硬盘进行分区,然后格式化,也可以不分区,直接将一整个硬盘格式化,我建议还是分一下区

4. 创建挂载点并挂载

每个节点都要做,我只写一台的操作

# 每个节点都是,我的每个节点的硬盘都是sdb1,sdc1,sdd1
[root@oe01 ~]# mkdir -p /data/sd{c..d}1
[root@oe01 ~]# mount /dev/sdb1 /data/sdb1
[root@oe01 ~]# mount /dev/sdc1 /data/sdc1
[root@oe01 ~]# mount /dev/sdd1 /data/sdd1

二、GlusterFS 卷的类型

1. 分布式卷(Distributed Volume)

作用:

将文件随机分布到集群中的不同服务器上,没有任何冗余。这种卷适合不需要高可用性和容错性的场景,例如缓存或临时文件存储。

例子:

像是一个图书馆里,所有的书都随意放在不同的书架上,没有特定的顺序。如果一个书架坏了,那上面的书就暂时不可用了。

配置示例:

1. 创建卷
[root@oe01 sdc1]# gluster volume create volume1 node1:/data/sdc1/brick node2:/data/sdc1/brick
volume create: volume1: success: please start the volume to access data

参数解释:

  • 为什么一个用node1的sdc,另一个用node2的sdc呢?
    • 这样做是防止某一台主机故障而导致的数据丢失,glusterfs会根据你创建卷时指定的数据来存放文件
    • 当然我们这里是使用的分布式卷,是没有容错机制的,你也可以全部指定在同一节点的磁盘
  • node1:/data/sdc1/brick
    • 这个参数是指在/data/sdc1/下创建一个brick目录,并将所有的数据都存储在这个目录里面
    • 不能够直接指定/data/sdc1会报错的,因为glusterfs不能直接在根目录下创建(这里的根目录并不是指Linux的根目录),必须得在你指定的目录下的子目录创建
2. 启动卷

现在卷是创建出来了,但是没有处于启动状态,是无法使用的,接下来我们启动这个卷

[root@oe01 sdc1]# gluster volume start volume1 
volume start: volume1: success

启动卷也是很简单,直接告诉他需要启动的卷名就可以了

3. 客户端挂载

客户端需要挂载的话可以通过posix协议来挂载,也就是mount -t glusterfs 这样的方式

# 我是用第三个节点充当客户端
# 1. 安装客户端软件包
[root@oe03 ~]# yum install glusterfs-fuse -y
# 2. 挂载使用
[root@oe03 ~]# mount -t glusterfs node1:/volume1 /mnt/

你在挂载的时候随意指定集群内的任意节点都可以,我这里使用的node1,你可以使用node2:/volume1,或者node3:/volume1,即使我们没有使用node3来创建这个卷依然可以挂载

4. 观察卷的存储方式

我们在客户端往卷里写入文件,然后回到node1和node2查看

[root@oe03 ~]# cd /mnt/
[root@oe03 mnt]# cp /etc/hosts .
[root@oe03 mnt]# ls
hosts

将hosts文件复制到/mnt下,我们回到node1和node2来查看

[root@oe01 ~]# ls /data/sdc1/brick/
hosts
[root@oe02 ~]# ls /data/sdc1/brick/

可以很清楚的看见,这个文件只有node1节点上有,如果文件丢失/损坏是没有副本或者其他机制可以恢复的

2. 复制卷(Replicated Volume)

作用:

提供数据冗余,通过在多个服务器上复制每个文件,确保高可用性和数据容错性。适合重要数据存储,需要高可靠性的场景。

例子:

像是一个图书馆里,每本书都有多个副本,放在不同的书架上。如果一个书架坏了,你还能从其他书架上找到同样的书。

配置示例:

1. 创建卷
[root@oe01 sdb1]# gluster volume create volume2 replica 3 node1:/data/sdb1/brick node2:/data/sdb1/brick node3:/data/sdb1/brick
volume create: volume2: success: please start the volume to access data

参数解释:

  • replica
    • 这个就是指定卷的模式,分布式卷我们没有指定,因为默认就是分布式卷,后面的3代表3副本
2. 启动卷并挂载
[root@oe01 sdb1]# gluster volume start volume2 
volume start: volume2: success
# 我们还是一样在node3上挂载
[root@oe03 mnt]# mkdir /gluster/replicas -p
[root@oe03 mnt]# mount -t glusterfs node2:/volume2 /gluster/replicas
3. 观察卷的存储方式
[root@oe03 replicas]# cp /etc/hosts .
[root@oe03 replicas]# ls
hosts
# node1上
[root@oe01 ~]# ls /data/sdb1/brick/
hosts
# node2上
[root@oe02 ~]# ls /data/sdb1/brick/
hosts
# node3上
[root@oe03 ~]# ls /data/sdb1/brick/
hosts

可以看到,hosts这个文件每个节点都写入了一份,这样做确保了数据的可靠性,丢失了一大部分存储空间空间只有以前的1/3

[root@oe03 ~]# df -hT /gluster/replicas/
Filesystem     Type            Size  Used Avail Use% Mounted on
node2:/volume2 fuse.glusterfs   20G  381M   20G   2% /gluster/replicas

这里显示他的空间就只有20G,我们使用的是3个20G的磁盘做的

3. 条带卷(Striped Volume)

作用:

将文件分块,并将块分布在多个服务器上,以提高读取和写入性能。适合需要高带宽和大文件存储的场景。与Raid0类似

例子:

像是一本大字典,每一页都分给不同的书架。如果你要查阅字典,每个书架会提供一部分内容,从而加快查找速度。

4. 分布式复制卷(Distributed Replicated Volume)

作用:

结合分布式卷和复制卷的优点,将文件分布在不同的服务器组中,并在每个组内复制文件。提供高可用性和扩展性。

例子:

像是一个图书馆,不仅每本书有多个副本,而且这些副本还分散在不同的区域。如果一个区域坏了,你还能从其他区域找到同样的书。

配置示例:

1. 清初卷

要创建分布式复制卷至少需要副本数*2的brick才可以

先停止卷

# 先删除之前使用过的卷,我们指定副本为3,那么就需要6个brick,我们现在是不够的
[root@oe01 brick]# gluster volume stop volume1 
Stopping volume will make its data inaccessible. Do you want to continue? (y/n) y
volume stop: volume1: success
[root@oe01 brick]# gluster volume stop volume2
Stopping volume will make its data inaccessible. Do you want to continue? (y/n) y
volume stop: volume2: success

然后删除卷

[root@oe01 brick]# gluster volume delete volume1 
Deleting volume will erase all information about the volume. Do you want to continue? (y/n) y
volume delete: volume1: success
[root@oe01 brick]# gluster volume delete volume2
Deleting volume will erase all information about the volume. Do you want to continue? (y/n) y
volume delete: volume2: success

然后将每个目录下的brick也删除,如果不删除的话在创建卷的时候会报错

[root@oe01 data]# rm -rf /data/sd{b..d}1/brick
[root@oe02 data]# rm -rf /data/sd{b..d}1/brick
[root@oe03 data]# rm -rf /data/sd{b..d}1/brick
2. 创建分布式副本卷
[root@oe01 data]# gluster volume create volume3 replica 3 node1:/data/sdb1/brick node2:/data/sdb1/brick node3:/data/sdb1/brick node1:/data/sdc1/brick node2:/data/sdc1/brick node3:/data/sdc1/brick
volume create: volume3: success: please start the volume to access data
3. 启动卷并挂载卷
[root@oe01 data]# gluster volume start volume3 
volume start: volume3: success
[root@oe03 ~]# cd /gluster/
[root@oe03 gluster]# mkdir volume3
[root@oe03 gluster]# mount -t glusterfs node3:/volume3 /gluster/volume3/
4. 观察卷的存储方式
[root@oe03 gluster]# cd volume3/
[root@oe03 volume3]# cp /etc/chrony.conf .
[root@oe03 volume3]# ls
chrony.conf
[root@oe01 brick]# ls -R /data/ |grep chrony.conf -B 1
/data/sdb1/brick:
chrony.conf

[root@oe02 ~]# ls -R /data/ |grep chrony.conf -B 1
/data/sdb1/brick:
chrony.conf

[root@oe03 volume3]# ls -R /data/ |grep chrony.conf -B 1
/data/sdb1/brick:
chrony.conf
  • 这个数据会被写入到同一个组,这里的组跟我们的定义顺序就有关系了,我们指定的是node1 /data/sdb1/brick,然后是node2,然后是node3,副本数是3,这样他们3个就是一个组,另外3个就是另一个组了,当我们的node1挂掉之后,数据还会在node2和node3上,这也就是为什么顺序这么关键的原因

  • 如果我们指定的顺序是node1 /data/sdb1/brick, node1 /data/sdc1/brick node1 /data/sdd1/brick,那么某个文件的副本会全部写到node1节点上,如果node1挂掉了,那么数据也就不存在了

5. 分布式条带卷(Distributed Striped Volume)

作用:

结合分布式卷和条带卷的优点,将文件分块,并将块分布在不同的服务器组中,以提高性能和扩展性。

例子:

像是一本大字典,每一页都分给不同的区域,每个区域再分给不同的书架。这样既能加快查找速度,又能扩展存储容量。

配置示例:

6. 异或卷(Dispersed Volume)

作用:

使用纠删码(Erasure Coding)来提供数据冗余,能够在较少的存储开销下提供数据容错性。适合存储大规模数据且需要高效利用存储空间的场景。

例子:

像是一个图书馆,书被分成了很多部分,放在不同的书架上,即使少数几个书架坏了,其他书架上的部分也可以组合起来恢复整个书。

配置示例:

总结

不同类型的卷适用于不同的应用场景,选择合适的卷类型可以根据数据的重要性、性能需求和存储效率来决定。希望这些例子和配置示例能帮助你更好地理解 GlusterFS 卷的类型和作用。

lowbit
的定义

首先了解
lowbit
的定义

\(lowbit(n)\)
,为
\(n\)
的二进制原码中最低的一位
\(1\)
以及其后面的
\(0\)
所表示的数

举个简单的例子:


\(10\)
使用二进制表示为
\(1010\)

其中最低位的
\(1\)
为第2位(
\(_{10}1_0\)
,从右往左数)

此时
\(lowbit(10)\)
使用二进制表示为
\(10\)
,即
\(2\)
. (有关进制转换详见
进制与进制转换


lowbit
的计算

如何计算
\(lowbit(n)\)
呢?

lowbit
的特殊情况

由于
lowbit
会将除最低位
\(1\)
以外所有的位
\(1\)
改为
\(0\)

lowbit
将只会对位
\(1\)
的位数高于1的二进制数产生影响,所以位
\(1\)
只有1位的二进制数和
\(0\)
处理后将得到原数据,即:

\[lowbit(2^n) = 2^n ~~~ ( n >= 0)
\]

\[lowbit(0) = 0
\]

方法一:递归

先暂时假定
\(n\)
为正整数


\(n\)
转换为二进制,可得:
\(00...00x...xxx\)
(
\(x\)

\(0\)

\(1\)
)

此时
\(n · 2\)
转换为二进制可得:
\(00...00x...xxx · 10 ~=~ 00...0xx...xx0\)

假设
\(n\)
转为二进制后,末尾有
\(m\)
个连续位
\(0\)
(显然,此时
\(lowbit(n) ~ = ~ 2^m\)

因此,
\(n · 2\)
转为二进制后,末尾有
\(m + 1\)
个连续位
\(0\)
(此时
\(lowbit(n · 2) ~ = ~ 2^{m + 1} ~ = ~ 2^m · 2\)

于是我们得到了:

\[lowbit(n · 2) = lowbit(n) · 2
\]

此时
\(n · 2\)
表示为二进制是
\(00...0xx...xx0\)
,怎么样,有没有什么想法?


\(n · 2\)
加上
\(1\)
,得:
\(00...0xx...xx0 + 1 ~ = ~ 00...0xx...xx1\)

显然:

\[lowbit(2n + 1) ~ = ~ 1
\]

观察
\(n\)
的二进制形式:
\(00...00x...xxx\)

对比
\(-n\)
的二进制形式:
\(10...00x...xxx\)
(在原码中,我们一般使用第一位存储符号,
\(0\)
为正,
\(1\)
为负)

很明显,
\(lowbit(n) ~ = ~ lowbit(-n)\)

无论
\(n\)
的符号为负还是为正,奇偶性都一致,因此,我们在上面推导出来的公式对负整数仍然成立

综上所述,任意奇数的
lowbit
值都为
\(1\)
,任意偶数的
lowbit
值都为其乘
\(0.5\)
得到的值的
lowbit
值乘
\(2\)

通过这种思路,我们可以编写一个递归函数计算
\(n\)

lowbit
值,遇到奇数直接返回
\(1\)
,遇到偶数辄除以
\(2\)
后继续计算

写成伪代码是这样的:

int lowbit(int n) {
    if (n % 2 == 1) return 1;  // Odd
    else return lowbit(n / 2) * 2;  // Even
}

方法二:公式

在方法一中,我们使用了深度优先搜索,时间复杂度可能有点高,我们当然可以使用记忆化数组降低复杂度,但,我们是否可以推导出一个公式直接计算,将复杂度降低为
\(O(1)\)
呢?

当然是可行的。还是观察
\(n\)
的二进制形式:
\(00...00x...xxx\)
(假定
\(n\)
为非负整数)

还是对比
\(-n\)
的二进制形式:
\(10...00x...xxx\)

如果对
\(10...00x...xxx\)
每一位取反(符号位除外),我们就得到了
\(-n\)
的反码:
\(11...11(-x)...(-x)(-x)(-x)\)

此时
\(-n\)
末尾的
\(0\)
全部变为
\(1\)
,而最低位的
\(1\)
也难逃变为
\(0\)
的命运

如果我们现在将其加上
\(1\)
,我们将得到
\(-n\)
的补码:
\(11...11(-x)...(-x)(-x)(-x) + 1\)
,反码末尾的
\(1\)
将重新变为
\(0\)
,而最低位
\(0\)
将重新变为
\(1\)
,其他位不变,仍然是取反的状态,此时如果将
\(-n\)
的补码与
\(n\)
原码进行按位与的运算(
\(n\)

\(-n\)
的原码只有符号位的不同),由于除符号位、最低位
\(1\)
及其后面的位
\(0\)
,其他位都进行了取反,这些位将被赋值为
0 & 1

1 & 0
,即
\(0\)
,而符号位也会赋值为
0 & 1
,只剩最低位
\(1\)
及其后面的位
\(0\)
分别被赋值为
1 & 1

0 & 0
,即
\(1\)

\(0\)
,最后结果为
\(lowbit(n)\)
(或者说
\(lowbit(-n)\)

那么
\(n\)
的反码、补码呢?上文所述的只是负数的反码与补码的计算方式,实际上,正数的原码、反码、补码都是一样的(对于原码、反码、补码,本博文已经进行了必要的解释,但如果你对其感兴趣,想知道其详细解释,可参考这篇博文:
二进制|原码、反码、补码

众所周知,
C++
中,数字是使用补码表示的,因此,我们可以将
\(n\)
的补码视为
\(n\)
的原码,在
C++
中进行运算。于是,我们得到了
\(n\)
的原码和
\(-n\)
的补码

上文中,我们提到了将
\(n\)
的原码和
\(-n\)
的补码进行按位与运算可以得到
\(lowbit(n)\)

\(lowbit(-n)\)
,现在我们使用
\(n\)
的补码作为其原码(毕竟是一样的),可以得到:

\[lowbit(n) = -n & n
\]

显然
\(n\)
是负数也不会造成影响

于是我们成功地得到了
lowbit
的计算公式,将算法的时间复杂度降低为
\(O(1)\)
,并简化了代码:

#define lowbit(n) (-n & n)

由于使用宏定义,一定要记得打括号,位运算的优先级是最低的


lowbit
的应用

lowbit
的应用也有很多,例如树状数组等,如果你对这方面感兴趣,不妨订阅一下我的博客,我以后会发布更多有趣且有用的算法知识