人的知识就好比一个圆圈,圆圈里面是已知的,圆圈外面是未知的。你知道得越多,圆圈也就越大,你不知道的也就越多。

0%

Redis 采用的是基于内存的、单线程模型的 KV 数据库。官方提供的数据是可以达到 10w+ 的 QPS。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差。

理解单线程模型

单线程模型实例

  1. Redis 会将每个客户端都关联一个指令队列。客户端的指令通过队列来按顺序处理,先到先服务。
  2. 在一个客户端的指令队列中的指令是顺序执行的,但是多个指令队列中的指令是无法保证顺序的,例如执行完 client-0 的队列中的 command-0 后,接下去是执行哪个队列中的第一个指令是无法确定的,但是肯定不会同时执行两个指令。
  3. Redis 同样也会为每个客户端关联一个响应队列,通过响应队列来顺序地将指令的返回结果回复给客户端。
  4. 同样,一个响应队列中的消息可以顺序的回复给客户端,多个响应队列之间是无法保证顺序的。
  5. 所有的客户端的队列中的指令或者响应,Redis 每次都只能处理一个,同一时间绝对不会处理超过一个指令或者响应。

为什么 Redis 使用单线程模型还能保证高性能

  • 纯内存访问
    Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 Redis 的 QPS 过万的重要基础。

  • 非阻塞式 IO

    • 什么是阻塞式 IO
      当我们调用 Scoket 的读写方法,默认它们是阻塞的。
      read() 方法要传递进去一个参数 n,表示读取这么多字节后再返回,如果没有读够 n 字节线程就会阻塞,直到新的数据到来或者连接关闭了, read 方法才可以返回,线程才能继续处理。
      write() 方法会首先把数据写到系统内核为 Scoket 分配的写缓冲区中,当写缓存区满溢,即写缓存区中的数据还没有写入到磁盘,就有新的数据要写道写缓存区时,write() 方法就会阻塞,直到写缓存区中有空闲空间。

    • 什么是非阻塞式 IO
      非阻塞 IO 在 Scoket 对象上提供了一个选项 Non_Blocking ,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。
      能读多少取决于内核为 Scoket 分配的读缓冲区的大小,能写多少取决于内核为 Scoket 分配的写缓冲区的剩余空间大小。读方法和写方法都会通过返回值来告知程序实际读写了多少字节数据。

有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。

  • IO 多路复用
    “多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)。可以直接理解为:单线程的原子操作,避免上下文切换的时间和性能消耗;加上对内存中数据的处理速度,很自然的提高 Redis 的吞吐量

  • 数据结构简单
    单线程可以简化数据结构和算法的实现。并发数据结构实现不但困难而且开发测试比较麻。
    Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表对短数据进行压缩存储,再如跳表使用有序的数据结构加快读取的速度。

  • 单线程避免了线程切换和竞态产生的消耗
    单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。

单线程的问题

  • 对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,所以 redis 适用于那些需要快速执行的场景。
  • 无法发挥多核CPU性能,不过可以通过在单机开多个 Redis 实例来完善。

过期策略

Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key。所谓惰性删除就是在客户端访问这个 key 的时候,Redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除时零散处理。

定时扫描策略

Redis 默认会每秒进行 10 次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

  1. 从过期字典中随机 20 个 key;
  2. 删除这 20 个 key 中已经过期的 key;
  3. 如果过期的 key 比率超过了 1/4,那就重复步骤 1;

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。

如果一个大型的 Redis 实例中所有的 key 在同一时间过期,Redis 会持续扫描过期字典(循环多次),直到过期字典中过期的 key 变得稀疏,或等待时间超过 25ms,这就会导致线上读写请求出现明显的卡顿现象,甚至大量的链接因为超时而关闭,业务端就会出现很多异常。而且这是我们还无法从 Redis 的 showlog 中看到慢查询记录,因为慢查询指的时逻辑处理过程慢,不包含等待时间。

所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置一个随机范围,从而分散过期处理的压力

从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里面增加一条 del 指令,然后同步到所有的从库,从库通过执行这条 del指令来删除过期的 key。

因为指令同步时异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致。

内存淘汰策略

当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换(swap)。交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。

在生产环境中我们是不允许 Redis 出现交换行为,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。

当实际内存超出 maxmemory 时,Redis 提供了几种可选策略(maxmemory-policy)来让用户自己觉醒该如何腾出新的空间以继续提供读写服务。

noeviction

不会继续服务器写请求(DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。

volatile-lru

尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。

volatile-ttl

跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。

volatile-random

跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。

allkeys-lru

区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。

allkeys-random

跟上面一样,不过淘汰的策略是随机的 key。

小结

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果我们只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间;如果我们还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。

Redis 的数据默认全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。

Redis 的持久化机制有两种:

  • RDB 快照
  • AOF 日志

RDB 快照是一次全量备份,AOF 日志是连续的增量备份;快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。

对比

RDB 快照

优点:

  • RDB 快照是紧凑的二进制文件,比较适合做冷备,全量复制的场景。
  • 相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 Redis 进程,更加快速。

缺点:

  • 如果想要在 Redis 实例发生故障时,尽可能少的丢失数据,那么 RDB 没有 AOF 好。
  • RDB 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。
  • RDB 无法实现实时或者秒级持久化。

AOF 日志

优点:

  • AOF 日志可以更好的保护数据不丢失。
  • AOF 日志文件以 append-only 模式写入,写入性能比较高。
  • AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
  • 适合做灾难性的误删除紧急恢复。

缺点:

  • 对于同一份数据来说,AOF 日志文件通常比 RDB 快照文件更大,恢复速度慢。
  • AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件。(当然,每秒一次 fsync,性能也还是很高的。)
  • 以前 AOF 发生过bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

配置

RDB 快照

默认情况下,Redis 是 RDB 快照的持久化方式,将内存中的数据以快照的方式写入二进制文件中,默认的文件名是 dump.rdb。

redis.conf 默认配置:

1
2
3
save 900 1
save 300 10
save 60 10000

配置含义:

  • 900 秒内,如果超过 1 个 key 被修改,则发起快照保存。
  • 300 秒内,如果超过 10 个 key 被修改,则发起快照保存。
  • 60 秒内,如果 1 万个 key 被修改,则发起快照保存。

AOF 日志

如果要开启 AOF 日志持久化方式,需要将 appendonly 设置为 yes。(开启后在服务端会发现多了一个 appendonly.aof 文件。)

1
2
3
4
appendonly yes

# 默认每秒持久化一次
# appendfsync everysec

默认是每秒持久化一次。通过指定不同的 appendfsync 值,可以实现不同的持久化策略。

  • no:不主动进行同步操作,默认 30s 一次
  • everysec:每秒持久化一次(默认配置)
  • always:每次操作都会立即写入 aof 文件中

混合持久化

1
aof-use-rdb-preamble yes

原理

RDB 快照

Redis 使用操作系统的多进程 COW(Copy On Write)机制来实现快照持久化。

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求

子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。我们可以将父子进程想象成一个连体婴儿,共享身体。这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。

子进程做数据持久化,它不会修改现有的的内容数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。

这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。

随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4k,一个 Redis 实例里一般都会由成千上万的页面。

子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫做“快照”的原因。

AOF 日志

AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。

假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是“重放”,来恢复 Redis 当前实例的内存数据结构的状态。

Redis 会在收到客户端修改指令后,进行参数校验进行逻辑处理后,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先执行指令才将日志存盘

Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。

AOF 重写

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

fsync

AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。

这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办?

Linux 的 glibc 提供了 fsync(int fd) 函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的地位就不保了。

所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。

Redis 同样也提供了另外两种策略,一个是从不 fsync–让操作系统来决定何时同步磁盘,很不安全,另一个是来一个指令就 fsync 一次–非常慢。这两种策略在生产环境基本不会使用。

混合持久化

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。

Redis 4.0 为了解决这个问题,带来了一个新的持久化选项:混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

当然混合持久化也是有缺点的,就是 aof 日志里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。

运维

通常 Redis 的主节点不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。

但是如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别是在网络分区初出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网络分无的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失。

  1. 创建 network

    1
    docker network create --driver bridge --subnet 172.22.0.0/16 --gateway 172.22.0.1  op_net
  2. 创建 redis.yml

    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
    version: '3.6'

    services:
    redis-1:
    image: redis
    restart: always
    hostname: redis-1
    container_name: redis-1
    ports:
    - 6379:6379
    networks:
    default:
    ipv4_address: 172.22.0.26

    redis-2:
    image: redis
    restart: always
    hostname: redis-2
    container_name: redis-2
    ports:
    - 6380:6379
    command: redis-server --slaveof redis-1 6379
    networks:
    default:
    ipv4_address: 172.22.0.27

    redis-3:
    image: redis
    restart: always
    hostname: redis-3
    container_name: redis-3
    ports:
    - 6381:6379
    command: redis-server --slaveof redis-1 6379
    networks:
    default:
    ipv4_address: 172.22.0.28

    networks:
    default:
    external:
    name: op_net
  3. 启动 Redis 集群

    1
    docker-compose -f redis.yml up -d

Redis 中所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一的 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。

String(字符串)

String 类型是 Redis 的最基本的数据类型。

Redis String 类型是二进制安全的。意思是 String 可以包含任何数据,比如 jpg 图片或者序列化的对象。

Redis String 是动态字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配(字符串实际分配的空间 capacity 一般要高于字符串实际长度 length)。当字符串长于小于 1M 时,扩容都是加倍现有的空间。如果超过 1M,扩容时一次只会多扩 1M 的空间。字符串的最大长度为 512M。

使用场景:

  • 常规 key-value 缓存引用
  • 常规计数:微博数、粉丝数

命令:

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
# 设置 key-value
set key value

# 获取 key 值
get key

# 检查是否存在 key
exists key

# 删除 key
del key

# 批量设置 key-value list
mset key1 value1 key2 value2 key3 value3

# 批量获取 keys
mget key1 key2 key3

# 设置 key 5s 后过期
expire key 5

# 设置 key-value,key 在 5s 后过期
setex key 5 value

# 如果 key 不存在,设置 key-value
setnx key value


# 如果 value 是一个整数,还可以对它进行自增/自减操作。
set age 30

# 自增 +1
incr age

# 自增 +5
incrby age 5

# 自减 -1
decr age

# 自减 -5
decrby age 5

List(列表)

Redis List 是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。

Redis List 相当于 Java 里面的 LinkedList,注意它是链表而不是数组,这意味着 List 的插入和删除操作非常快,事件复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。

当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

使用场景:

  • 消息队列
  • 取最新 N 个数据的操作

命令:

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
# 在头部添加元素
lpush books java

# 在尾部添加元素
rpush books java

# 获取队列长度
llen books

# 获取所有元素(O(n) 慎用)
lrange books 0 -1

# 获取第 i 个元素(O(n) 慎用)
lindex books 0

# 保留第一个元素之后的值(O(n) 慎用)
ltrim books 1 -1

# 队列(右边进左边出)
rpush books python java golang

lpop books

lpop books

lpop books

# 栈(右边进右边出)
rpush books python java golang

rpop books

rpop books

rpop books

补充:
Redis List 底层存储的不是一个简单的 LinkedList,而是称之为快速链表 quicklist 的一个结构。
在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候就会改成 quicklist。
因为普通的链表需要用到附加指针(prev 和 next),会比较浪费空间,而且会加重内存的碎片化。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

Hash(字典)

Redis Hash 相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的:同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。不同的是,Redis 的字典的值只能是字符串。

Redis 为了高性能,不能阻塞服务,所以采用了渐进式的 rehash 策略。渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当迁移完成了,就会使用新的 hash 结构五而代之。

当 Hash 移除了最后一个元素之后,该数据结构会自动被删除,内存被回收。

Hash 结构特别适合用于存储对象。缺点在于其存储消耗要高于单个字符串。到底该使用 Hash 还是字符串,需要根据实际情况再三权衡。

使用场景:

  • 存储部分变更数据,如用户信息等。

命令:

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
# put key-value
hset books java "think in java"

hset books golang "concurrency in go"

hset books python "python cookbook"

# 获取所有键值对
hgetall books

# 获取元素个数
hlen books

# 获取某个 key 的值
hget books java

# 设置某个 key 的值
hset books golang "learning go programming"

# 删除某个 key 的值
hdel books java

# 同字符串一样,hash 结构中的单个子 key 也可以进行计数
hset user age 30

hincrby user age 1

Set(集合)

Redis Set 相当于 Java 语言里面的 HashSet,它内部的键值对是唯一且无序的。它的内部实现相当于一个特殊的字典,字典中所有的value 都是一个值 NULL。

当 Set 移除了最后一个元素之后,该数据结构会自动被删除,内存被回收。

使用场景:

  • 交集,并集,差集
  • 获取某段时间所有数据去重值

命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 添加元素
sadd books python

# 重复添加
sadd books python

# 批量添加
sadd books java golang

# 查看集合成员(顺序不一定和插入的一致)
smembers books

# 获取元素个数
scard books

# 检查某个 value 是否在集合中
sismember books java

# 弹出一个元素
spop books

# 删除成员
srem books python

zset(有序集合)

类似于 Java 的 SortedSet 和 HashMap 的集合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做跳跃链表的数据结构。

zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

使用场景:

  • 排行榜应用,取 TOP N 操作
  • 范围查找
  • 优先级队列
  • 需要精确设定过期时间的应用(score 值设置成过期时间的时间戳)

命令:

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
# 添加元素
zadd books 9.0 "think in java"
zadd books 8.9 "java concurrency"
zadd books 8.6 "java cookbook"

# 按 score 排序列出
zrange books 0 -1

# 按 score 逆序列出
zrevrange books 0 -1

# 获取元素个数
zcard books

# 获取指定元素的 score
zscore books "java concurrency"

# 获取指定元素的 score 排名
zrank books "java concurrency"

# 根据分值区间遍历 zset
zrangebyscore books 0 8.91

# 根据分值区间((-=, 8.91])遍历 zset,同时返回分值
zrangebyscore books -inf 8.91 withscores

# 删除成员
zrem books "java concurrency"

BitMap(位图)

BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现

BitMap 是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。

使用场景:

  • 用户签到
  • 统计活跃用户
  • 用户在线状态

命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 零存零取
setbit w 1 1
getbit w 1

# 整存零取
set w hello
getbit w 1

# 统计指定范围内 1 的个数
bitcount w 0 -1

# 查找第一个 1 位
bitpos w 1

# ...
bitfield w ...

HyperLogLog(基数统计)

Redis HyperLogLog 是用来做基数统计的算法。HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
另外,HyperLogLog 提供的去重计数方案是不精确的,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。

使用场景:

  • 页面实时 UV
  • 不适合单个用户的数据统计

命令:

1
2
3
4
5
6
7
8
9
10
11
# 添加计数
pfadd codehole user1

# 获取计数
pfcount codehole

# 添加计数
pfadd home user1

# 合并计数(并集计算)
pfmerge xx codehole home

GEO(地理位置)

用于存储用户给定的地理位置信息,并对这些信息进行操作。GEO 数据结构总共有 6 个命令:geoadd、geopos、geodist、georadius、georadiusbymember。

容器类型数据结构的通用规则

list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  • create if not exists:如果容器不存在,那就创建一个,再进行操作
  • drop if no elements:如果容器里元数没有了,那么立即删除元数,释放内存。

过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。

需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。

还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后调用了 set 方法修改了它,它的过期时间就会消失

参考资料

  1. Redis的8种数据类型
  2. 几率大的Redis面试题(含答案)

映射(Mapping)类似数据库中的 schema 的定义,作用如下:

  • 定义索引中的字段的名称
  • 定义字段的数据类型,如字符串、数字、布尔…
  • 字段倒排索引的相关设置,如 Analyzed or Not Analyzed

字段数据类型

  • 简单类型

    • Text / Keyword
    • Date
    • Integer
    • Floating
    • Boolean
    • IPv4 & IPv6
  • 复杂类型

    • 对象类型
    • 嵌套类型
  • 特殊类型

    • geo_point /geo_shape
    • percolator
  • 数组类型
    Elasticsearch 中不提供专门的数组类型。但是任何字段,都可以包含多个相同类型的数值。

Dynamic Mapping

Dynamic Mapping,是指在写入文档前无需指定 Mapping,Elasticsearch 会自动根据文档信息推算出字段的类型。这种机制使得我们无需手动定义 Mapping。当然有时候会推算的不对,这时候就需要手动设置 Mapping 了。

类型的自动识别:

JSON 类型 Elasticsearch 类型
字符串 日期格式 -> Date;数字 -> float
布尔值 boolean
浮点数 float
整数 long
对象 Object
数组 由第一个非空数值的类型所决定
空值 忽略

显示指定 Mapping

语法:

1
2
3
4
5
6
PUT index_name
{
"mappings": {
// define your mappings here
}
}

建议:
为了减少输入的工作量,减少出错概率,可以依照以下步骤设置 Mapping:

  1. 创建一个临时的 idex,写入一些样本数据;
  2. 通过访问 Mapping API 获得该临时文件的动态 Mapping 定义;
  3. 修改后用,使用该配置创建真实索引;
  4. 删除临时索引

语法

index

控制当前字段是否被索引。默认为 true。如果设置成 false,该字段不可被搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT users
{
"mappings": {
"properties": {
"firstname": {
"type": "text"
},
"lastname": {
"type": "text"
},
"mobile": {
"type": "text",
"index": false
}
}
}
}

index_options

控制倒排索引记录的内容,有四种不同级别的配置:

  • doc:记录 doc id
  • freqs:记录 doc id 和 term frequencies
  • positions:记录 doc id / term frequencies / term position,用于距离查询,默认设置
  • offsets:记录 doc id / term frequencies / term position / character offsets,用于高亮显示

Text 类型默认记录级别为 positions,其它类型默认为 docs。记录内容越多,占用的存储空间越大。

null_value

当我们需要对 Null 值实现搜索时用到,只有 keyword 类型支持设置 null_value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT users
{
"mappings": {
"properties": {
"firstname": {
"type": "text"
},
"lastname": {
"type": "text"
},
"mobile": {
"type": "text",
"null_value": "NULL"
}
}
}
}

copy_to

copy_to 将字段的数值拷贝到目标字段,目标字段不出现在 _source 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT users
{
"mappings": {
"properties": {
"firstname": {
"type": "text",
"copy_to": "fullName"
},
"lastname": {
"type": "text",
"copy_to": "fullName"
}
}
}
}

GET users/_search?q=fullName:(Ruan Yiming)

多字段

可以给 Text 字段增加 keyword 字段以实现精确匹配(默认),还可以为字段的搜索和索引指定不同的 analyzer。

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
PUT products
{
"mappings": {
"properties": {
"company": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"comment": {
"type": "text",
"fields": {
"english_comment": {
"type": "text",
"analyzer": "english",
"search_analyzer": "english",
}
}
}
}
}
}

更新 Mapping

  • 新增字段

    • dynamic 为 true:一旦有新增字段的文档写入,Mapping 也同时被更新。
    • dynamic 为 false:Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在 _source 中
    • dynamic 为 strict:文档写入失败
  • 已有字段:一旦已经有数据写入,就不再支持修改字段定义

分词(Analysis)是把全文本转换成一系列单词(term/token)的过程。
分词是通过分词器(Analyzer)来实现的。我们可以使用 ELasticsearch 内置的分词器,也可以按需定制化分词器。
除了在数据写入时转换词条,匹配 Query 语句时也需要用相同的分词器对查询语句进行分析。

分词器组成

分词器由三部分组成:

  • Character Filters:针对原始文本处理,例如去除 html
  • Tokenizer:按照规则切分为单词
  • Token Filter:将切分的单词进行加工:小写、删除 stopwords、增加同义词..

分词器处理流程示例

Elasticsearch 内置分词器

  • Standard Analyzer:默认分词器,按词切分,小写处理
  • Simple Analyzer:按照非字母切分(符号被过滤),小写处理
  • Stop Analyzer:停用词过滤(the, a, is),小写处理
  • Whitespace Analyzer:按照空格切分,不转小写
  • Keyword Analyzer:正则表达式,默认 \W+(非字符分割)
  • Language:提供了30多种常见语言的分词器
  • Customer Analyzer:自定义分词器

中文分词

中文分词有它特定的一些难点:

  • 中文句子,切分成一个一个词,而不是一个个字。(英文中,单词有自然的空格作为分割)
  • 一句中文,在不同的上下文中,有不同的理解。(eg: 这个苹果,不大好吃/这个苹果,不大,好吃)

中文分词器:

  • IK
  • THULAC

Document

index

如果 id 不存在,创建新的文档。否则,先删除现有的文档,再创建新的文档,版本号加 1。

1
2
3
4
5
PUT my_index/_doc/1
{
"user": "mike",
"comment": "You konw, for search"
}

create

  • 自动创建 id
1
2
3
4
5
POST my_index/_doc
{
"user": "mike",
"comment": "You konw, for search"
}
  • 指定 id(如果 id 已经存在,会失败)
1
2
3
4
5
PUT my_index/_create/1
{
"user": "mike",
"comment": "You konw, for search"
}

update

文档必须已经存在,更新只会对相应字段做增量修改。

1
2
3
4
5
6
7
POST my_index/_update/1
{
"doc" : {
"user": "mike",
"comment": "You konw, for search"
}
}

get

找到文档,返回 HTTP 200,否则返回 HTTP 404

1
GET my_index/_doc/1

delete

删除文档。

1
DELETE my_index/_doc/1

bulk

  • 支持在一次 API 调用中,对不同的索引进行操作
  • 支持 index、create、update、delete 四种操作类型
  • 可以在 URI 中指定 index,也可以在请求的 payload 中进行
  • 操作中单条操作失败,并不会影响其它操作
  • 返回结果中包括了每一条操作执行的结果
1
2
3
4
5
6
7
8
POST _bulk
{"index": {"_index": "test", "_id": "1"}}
{"field1": "value1"}
{"delete": {"_index": "test", "_id": "2"}}
{"create": {"_index": "test2", "_id": "3"}}
{"field1": "value3"}
{"update": {"_index": "test", "_id": "1"}}
{"doc": {"field2": "value2"}}

mget

批量操作,可以减少网路连接所产生的开销,提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET _mget
{
"docs": [
{
"_index": "user,
"_id": 1
},
{
"_index": "comment,
"_id": 1
}
]
}

msearch

批量查询。

1
2
3
4
5
6
7
POST users/_msearch
{}
{"query": {"match_all" : {}}, "from": 0, size": 10}
{}
{"query": {"match_all" : {}}}
{"index": "twitter2"}
{"query": {"match_all" : {}}}
语法 范围
/_search 集群上所有的索引
/index1/_search index1
/index1,index2/_search index1 和 index2
/index*/_search 以 index 开头的索引

在 URL 中使用查询参数。

  • q:指定查询语句,使用 Query String Syntax,KV 键值对
  • df:默认字段,不指定时,会对所有字段进行查询
  • sort:排序
  • from/size:用于分页
  • profile:查看查询是如何被执行的

eg:

1
2
3
4
GET /movies/_search?q=2012&df=title&sort=tear:desc&from=0&size=10&timeout=1s
{
"profile": true
}

指定字段 vs 范查询

1
q=title:2012 / q=2012

Term vs Phrase

1
2
Beautiful Mind => Beautiful OR Mind
"Beautiful Mind" => Beautiful AND Mind (Phrase Query)

分组与引号

1
2
3
title: (Beautiful Mind) (Term Query)

title: "Beautiful Mind" (Phrase Query)

布尔操作

AND / OR /NOT (必须大写)或者 && / || / !

1
title:(matrix NOT reloaded)

分组

  • => must
  • => must_not
1
title:(+matrix -reloaded)

范围查询

[] => 闭区间
{} => 开区间

1
2
year:{2019 TO 2018]
year:[* TO 2018]

算数符号

1
2
3
year:>2010
year:(>2010 && <= 2018)
year:(+>2010 +<= 2018)
  • 通配符查询
    ? => 1 个字符
  • 0 或多个字符

通配符查询效率低,占用内存大,不建议使用。特别是放在最前面。

1
2
title:mi?d
title:be*

正则表达

1
title:[bt]oy

模糊匹配与近似查询

1
2
title:befutifl~1
title:"lord rings"~2

使用 Elasticsearch 提供的,基于 JSON 格式的更加完备的 DSL。

eg:

1
2
3
4
5
6
GET kibana_sample_data_ecommerce/_search
{
"query": {
"match_all": {}
}
}

分页

from 从 0 开始,默认返回 10 个结果。获取靠后的翻页成本较高。

1
2
3
4
5
6
7
8
GET kibana_sample_data_ecommerce/_search
{
"from": 10,
”size": 5,
"query": {
"match_all": {}
}
}

排序

最好在“数字型”与“日期型”字段上排序,因为对于多值类型或分析过的字段排序,系统会选一个值,无法得知该值。

1
2
3
4
5
6
7
8
9
GET kibana_sample_data_ecommerce/_search
{
"sort": [{"order_date": "desc}],
"from": 10,
”size": 5,
"query": {
"match_all": {}
}
}

_source filtering

如果 _source 没有存储,那就只返回匹配的文档的元数据。_source 支持使用通配符,如 _source[“name*”,”desc*”]。

1
2
3
4
5
6
7
8
9
GET kibana_sample_data_ecommerce/_search
{
"_source": ["order_date", "category.keyword"],
"from": 10,
”size": 5,
"query": {
"match_all": {}
}
}

match

默认 OR 查询:

1
2
3
4
5
6
7
8
GET /comments/_doc/_search
{
"query": {
"match": {
"comment": "Last Christmas"
}
}
}

AND 查询:

1
2
3
4
5
6
7
8
9
GET /comments/_doc/_search
{
"query": {
"match": {
"comment": "Last Christmas",
"operator": "AND"
}
}
}

match_phrase

1
2
3
4
5
6
7
8
9
GET /comments/_doc/_search
{
"query": {
"match_phrase": {
"comment": "Song Last Chrismas",
"slop": 1
}
}
}

query_string

单字段:

1
2
3
4
5
6
7
8
9
GET /users/_search
{
"query": {
"query_string": {
"default_field": "name",
"query": "Ruan AND Yiming"
}
}
}

多字段:

1
2
3
4
5
6
7
8
9
GET /users/_search
{
"query": {
"query_string": {
"fields": ["name", "about"],
"query": "(Ruan AND Yiming) OR (Java AND Elasticsearch)"
}
}
}

simple_query_string

类似 query_string,但是会忽略错误的语法,同时只支持部分查询语法;不支持 AND OR NOT,会当作字符串处理;Term 之间默认的关系是 OR,可以指定 Operator;支持部分逻辑:+ 替代 AND,| 替代 OR,- 替代 NOT。

1
2
3
4
5
6
7
8
9
10
GET /users/_search
{
"query": {
"simple_query_string": {
"fields": ["name"],
"query": "Ruan -Yiming",
"default_operator": "AND"
}
}
}

Aggregation

Bucket

1
2
3
4
5
6
7
8
9
10
11
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
}
}
}
}

Metric

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
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
},
"aggs": {
"average_price": {
"avg": {
"field": "AbgTicketPrice"
}
},
"max_price": {
"max": {
"field": "AbgTicketPrice"
}
},
"min_price": {
"min": {
"field": "AbgTicketPrice"
}
}
}
}
}
}

Nested

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
},
"aggs": {
"average_price": {
"avg": {
"field": "AbgTicketPrice"
}
},
"weather": {
"terms": {
"field": DestWeather
}
}
}
}
}
}

Mapping

  • 查看 Mapping

    1
    GET users/_mapping
  • 设置 Mapping

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    PUT users
    {
    "mappings": {
    "properties": {
    "firstname": {
    "type": "text"
    },
    "lastname": {
    "type": "text"
    },
    }
    }
    }

Analyzer

analyze

  • 指定 Analyzer 进行测试
1
2
3
4
5
GET _analyze
{
"analyzer": "standard",
"text": "2 running Quik brown-foxes leap over lazy dogs in the summer evening."
}
  • 指定索引的字段进行测试

    1
    2
    3
    4
    5
    POST books/_analyze
    {
    "field": "title",
    "text": "Mastering Elasticsearch"
    }
  • 自定义分词器进行测试

    1
    2
    3
    4
    5
    6
    POST _analyze
    {
    "tokenizer": "standard",
    "filter": ["lowercase"],
    "text": "Mastering Elasticsearch"
    }

Template

Index Template

Index Template 可以帮助我们设置 Mappings 和 Settings,并按照一定的规则,自动匹配到新创建的索引之上。

  • 模板仅在一个索引被创建时,才会产生作用。修改模板不会影响已创建的索引。
  • 可以设定多个索引模板,这些设置会被“merge”在一起。
  • 可以指定“order”的数值,控制“merging”的过程。(由低到高)
  • 应用创建索引时,用户所指定的 Settings 和 Mappings,会覆盖之前模板中的设定。

设置 Template

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT /_template/template_test
{
"index_patterns": ["test*"],
"order": 1,
"settings": {
"number_of_shards": 1,
"number_of_replicas": 2
},
"mappings": {
"date_detection": false,
"numeric_detection": true
}
}

** 查看 Template:**

1
2
3
GET /_template/template_default

GET /_template/temp*

Dynamic Template

根据 Elasticsearch 识别的数据类型,结合字段名称,来动态设定字段类型。

  • Dynamic Template 是定义在某个索引的 Mapping 中
  • Template 有一个名称
  • 匹配规则是一个数组
  • 为匹配到字段设置 Mapping

比如我们可以:

  • 所有的字符串类型都设定为 keyword,或者关闭 keyword
  • is 开头的字段都设置成 boolean
  • long_开头的都设置成 long 类型
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
PUT my_text_index
{
"mappings": {
"dynamic_templates": [
{
"full_name": {
"path_match": "name.*",
"path_unmatch": "*.middle",
"mapping": {
"type": "text",
"copy_to": "full_name"
}
}
},
{
"string_as_boolean": {
"match_mapping_type": "string",
"match": "is*",
"mapping": {
"type": "boolean"
}
}
}
]
}
}

主要特征

从架构的角度出发,ElasticSearch 具有下面这些主要特征:

  • 合理的默认配置,使得用户在简单安装以后能直接使用 ElasticSearch 而不需要任何额外的调试,这包括内置的发现(如字段类型检测)和自动配置功能。

  • 默认的分布式工作模式,每个节点总是假定自己是某个集群的一部分或将是某个集群的一部分,一旦工作启动节点便会加入某个集群。

  • 对等架构(P2P)可以避免单点故障(SPOF),节点会自动连接到集群中的其他节点,进行相互的数据交换和监控操作。这其中就包括索引分片的自动复制。

  • 易于向集群扩充新节点,不论是从数据容量的角度还是数量角度。

  • ElasticSearch 没有对索引中的数据结构强加任何限制,从而允许用户调整现有的数据模型。正如之前描述的那样,ElasticSearch 支持在一个索引中存在多种数据类型,并允许用户调整业务模型,包括处理文档之间的关联(尽管这种功能非常有限)。

  • 准实时(NearRealTime,NRT)搜索和版本同步(versioning)。考虑到 ElasticSearch 的分布式特性,查询延迟和节点之间临时的数据不同步是难以避免的。ElasticSearch 尝试消除这些问题并且提供额外的机制用于版本同步。

Elasticsearch 是一 个基于 Lucene 构建的开源、分布式、RESTful 接口的全文搜索引擎。

Lucense

Lucene 是 Apache 软件基金会中一个开放源代码的全文搜索引擎工具包,是一个全文搜索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文搜索引擎。

全文搜索(Full Text Search)

全文搜索是指计算机搜索程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,搜索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户。这个过程类似于通过字典中的搜索字表查字的过程。

倒排索引(Inverted Index)

倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(invertedindex)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(invertedfile)。

倒排索引包含两个部分:

  • 单词字典(Term Dictionary):记录所有文档的单词,记录单词到倒排列表的关联关系。(单词字典一般比较大,可以通过 B+ 树或哈希链表法实现,以满足高性能的插入与查询)
  • 倒排列表(Posting List):记录了单词对应的文档结合,由倒排索引项组成:
    • 文档 ID
    • 词频 TF:该词在文档中出现的次数,用于相关性评分。
    • 位置(Position):单词在文档中分词的位置。用于语句搜索(phrase query)。
    • 偏移(Offset):记录单词的开始结束位置,实现高亮显示。

文档(Document)

Elasticsearch 是面向文档的,文档是所有可搜索数据的最小单位;文档会被序列化成 JSON 格式,其中每个字段都有对应的字段类型(字符串/数值/布尔/日期/二进制/范围类型);每个文档都有一个 Unique ID,我们可以自己指定 ID,也可以通过 Elasticsearch 自动生成。

文档元数据:

  • _index:文档所属的索引名
  • _type:文档所属的类型名
  • _id:文档唯一 ID
  • _source:文档的原始 Json 数据
  • _all:整合所有字段内容到该字段,已被废除
  • _version:文档的版本信息
  • _score:相关性打分

索引(Index)

索引是具有相同结构的文档集合。它体现了逻辑空间的概念:每个索引都有自己的 Mapping 定义,用于定义包含的文档的字段名和字段类型。我们可以在 Index 上定义 Mapping(文档字段的类型)和 Setting(不同的数据分布)。

类型(Type)

类型是索引的逻辑分区。在 7.0 之前,一个 Index 可以设置多个 Types。从 7.0 开始,每个索引做能创建一个类型:_doc。

映射(Mapping)

映射定义了索引中的每一个字段类型,以及一个索引范围内的设置。一个映射可以事先被定义,或者在第一次存储文档的时候自动识别。

集群(Cluster)

集群由一个或多个节点组成,对外提供服务。一个集群有一个唯一的名称默认为 Elasticsearch。此名称是很重要的,因为每个节点只能是集群的一部分,当该节点被设置为相同的集群名称时,就会自动加入集群。当需要有多个集群的时候,要确保每个集群的名称不能重复,否则,节点可能会加入错误的集群。请注意,一个节点只能加入一个集群。此外,我们还可以拥有多个独立的集群,每个集群都有其不同的集群名称。

节点(Node)

单个的 ElasticSearch 服务实例称为节点(node)。很多时候部署一个 ElasticSearch 节点就足以应付大多数简单的应用,但是考虑到容错性或在数据膨胀到单机无法应付这些状况时,我们会更倾向于使用多节点的 ElasticSearch 集群。

节点类型:

节点类型 说明 配置参数 默认值
master eligible 可以参加选主流程,成为 Master 节点 node.master true
data 保存分片数据 node.data true
ingest 执行预处理管道,不负责数据也不负责集群相关的事务 node.ingest true
coordination only 接受 Client 请求,将请求分发到合适的节点,最终把结果汇集到一起 每个节点默认都是 coordination 节点。设置其它类型全部为 false。
machine learning 执行机器学习的 Job,用来做异常检测 node.ml true (需 enable x-pack)

在开发环境中,一个节点可以承担多种角色;在生产环境中,应该设置单一的角色节点(dedicated node)。

主分片(Primary Shard)

用以解决数据水平扩展的问题。通过主分片,可以将数据分布到集群内的所有节点之上。主分片数在索引创建时指定,后续不允许修改,除非 Reindex。

对于生产环境中分片的设定,需要提前做好容量规划:

  • 分片数设置过小:导致后续无法增加节点实现水平扩展;单个分片的数据量太大,导致重新分配耗时。
  • 分片数设置过大:影响搜索结果的相关性打分,影响统计结果的准确性;单个节点上过多的分片,会导致资源浪费,同时也会影响性能。

副本分片(Replica Shard)

用以解决数据高可用的问题。副本分片是主分片的拷贝。副本分片数可以动态地调整。