Redis的部分使用注意点

1. 用SCAN命令代替KEYS等全数据返回命令

SCAN命令是渐进式遍历,不会导致Redis的堵塞

KEYSSMEMBERS等命令是全量数据返回,会导致整个Redis实例的堵塞。

2. 设置合适的AOF写回策略

如果对数据持久化不敏感,可以不开启AOF。

如果开启,就要设置合适的写回策略。

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled.png

3. 一个Redis实例的数据库不要太大

一个 Redis 实例的数据库不要太大,一个实例大小在几 GB 级别比较合适,这样可以减少 RDB 文件生成、传输和重新加载的开销。

如果业务需求需要存储大量数据,考虑切片集群或者升硬件配置

4. 设置合理的repl_backlog_size值

一般做以下设置

repl_backlog_size = 缓冲空间大小 * 2
									= (主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小) * 2

这个值越大,环形缓冲区repl_backlog_buffer空间就越大,主从模式丢失数据的可能性就更低,但是会带来更大的系统损耗。

5. 主从集群中适当调整down-after-milliseconds

down-after-milliseconds参数可以控制哨兵检测主库超时的时间,最终也就会影响主库挂了后主从切换的时间。适当调大down-after-milliseconds值,当哨兵与主库之间网络存在短时波动时,可以降低误判的概率,但是这也意味着整个主从切换的时间会变长,需要根据业务合理权衡。

6. String的空间利用率

大多数情况我们都是采用值类型为String的数据进行快速key-value缓存, 这样做的好处是显而易见的,那就是快,只需要O(1)的时间复杂度。

但是,String类型会采用**简单动态字符串(Simple Dynamic String,SDS)**结构体来保存:

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%201.png

  • len:占 4 个字节,表示 buf 的已用长度
  • alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。
  • buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。

除此以外,所有数据类型都会有相同的元数据要记录,Redis用RedisObject结构体来描述,事实上我们也可以自定义RedisObject来实现自定义数据结构:

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%202.png

因此,String虽然快,但是会带来额外的内存资源开销,可以考虑用集合类型保存多个单值的键值对

比如哈希类型,在一定数据范围内(设置参数值hash-max-ziplist-xxx),会使用压缩数组来进行实现,这是非常节约内存的方案。

至于多个单值键值对,可以采用基于 Hash 类型的二级编码方法。这里说的二级编码,就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了。

7. 集合类型的四种统计模式

先放一张图:

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%203.png

7.1. 聚合统计

聚合统计,即求若干集合类型数据的交集,并集,差集,建议使用Set类型。

Set类型提供了一些方便的方法进行这些操作:

  • SUNIONSTORE:并集
  • SDIFFSTORE:差集
  • SINTERSTORE:交集

7.2. 排序统计

排序统计,即有排序需求的数据类型,可以使用List或者Sorted Set。

List的最新的元素总是显示在最前方,因此做分页业务时存在一定局限性。

Sorted Set可以自定权重值,实现按照需求进行排序(通过ZRANGEBYSCORE命令),兼容业务更广。

7.3. 二值状态统计

二值状态统计,即集合元素的取值就只有 0 和 1 两种,可以使用Bitmap。

Bitmap并不是Redis的基础数据类型,而是Redis额外扩展的数据类型。Bitmap用 String 类型作为底层数据结构实现。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。你可以把 Bitmap 看作是一个 bit 数组

这样每个bit位可以代表一次二值状态,一组Bitmap数据可以保存若干二值状态。

此外,还带来了一些特性运算,比如:

  • BITCOUNT:统计这个 bit 数组中所有“1”的个数。
  • GETBIT/SETBIT:使用一个偏移值 offset(从0开始)对 bit 数组的某一个 bit 位进行读和写。
  • BITOP:进行AND(与),OR(或),XOR(异或)运算。

7.4. 基数统计

基数统计,即统计一个集合中不重复的元素个数,建议使用Set或者HyperLogLog。

Set一般可以用于快速去重,但是会占用大量的内存空间,结果准确。

因此,Redis提供了专门用于统计基数的数据集合类型HyperLogLog,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小(只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。)。但是结果有误差。

特别注意,Set的结果是准确的,HyperLogLog的统计是基于概率算法完成,标准误算率是 0.81%,要按照业务需求适当选用。

8. 面向LBS应用的GEO数据类型

对于LBS(Location-Based Service,LBS)应用,Redis提供了基于对应的数据类型GEO。

GEO 类型是Redis提供的额外的扩展实现,底层数据结构就是用 Sorted Set 来实现的,Redis采用GeoHash的编码方法,将一组经纬度数据转化为一个值。

GeoHash编码也就是“二分区间,区间编码”,拿经度举例来说:

将经度区间[-180,180]二分为两个区间:左分区[-180,0) 与右分区[0,180],最终实际经度落在左边

记为0,落在右分区记为1,最终经过N次计算,就会得到一组编码。以经度116.37为例:

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%204.png

经纬度都经过计算后会得到两组编码,最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。这样,就得到最终的GeoHash编码了。

9. Redis保存时序数据特性

时序数据其实并不新鲜,时下的工业物料网数据采集上来基本都是存储在时序数据库(比如influxdb)中。

时序数据也就是和发生时间相关的一组数据,有以下特点:

  • 没有严格的关系模型,记录的信息可以表示成键和值的关系。
  • 高并发写入,一般都是若干设备同时写入数据,因此要求不要造成堵塞。
  • 插入性质写入,通常无修改(某一时刻记录某一时刻的数据)
  • 查询模式广泛,包括聚合,单数据查询等。

Redis可以基于 Hash 和 Sorted Set 实现的两种基本数据类型方案来保存时序数据,特点是稳定。此外,还提供了拓展模块RedisTimeSeries来实现。

9.1. Hash类型保存时序数据

用时间戳+id作为key,具体数据内容作为value,但是缺点在于指定时间所有数据查询,只能遍历所有数据,寻找时间戳在这一天的数据(Hash类型设值后无序)

9.2. Sorted Set类型保存时序数据

Sorted Set的好处在于可以以时间为权重设置数据,这样可以很方便的进行指定时间段内查询。

9.3. RedisTimeSeries模块保存时序数据

**RedisTimeSeries 是 Redis 的一个扩展模块,需要额外加载安装**。它专门面向时间序列数据提供了数据类型和访问接口,并且支持在 Redis 实例上直接对数据进行按时间范围的聚合计算。

9.4. 几种方式的比较

  • Hash类型和Sorted Set类型可以组合使用,但是一个是会带来原子性的问题(可以用Redis事务解决),一个是聚合运算必须从Redis实例查询数据到客户端,再由客户端进行计算。大量数据在 Redis 实例和客户端间频繁传输,这会和其他操作命令竞争网络资源,导致其他操作变慢。
  • RedisTimeSeries可以在Redis实例上进行运算,运算完返回数据,减少数据传输量。

10. Redis实现消息队列

消息队列一般有以下要求:

  • 消息保序
  • 处理重复的消息
  • 保证消息可靠性

一般不建议Redis作为消息队列使用,但是Redis支持作为一个轻量消息队列来使用,一般来说有3个方案:

  • List数据类型
  • Pub/Sub 发布订阅模式
  • Streams数据类型(Redis 5.0+ 版本)

10.1. List数据类型实现消息队列

主要是通过一些RPUSH + LPOP 或者 LPUSH + RPOP的方式实现FIFO的消息队列,List的数据类型是天生适合作为消息队列的。

但是,无论是否有数据,消费者(客户端)需要通过不断执行pop命令读取数据,这就会导致CPU资源的浪费。

为此,我们可以通过BRPOP/BLPOP命令进行阻塞式读取

  • 当读取到队列数据时,会立刻返回读取的数据
  • 当读取不到队列数据,自动阻塞直到获取到数据。

BRPOP/BLPOP命令是支持多个客户端同时执行的,不同的客户端被放进一个队列中,按『先阻塞先服务』(first-BLPOP,first-served)的顺序为 key 执行 BRPOP/BLPOP 命令。

List数据类型还支持BRPOPLPUSH命令保证消息可靠性,这个命令是在读取一个队列数据后将数据放入到另一个队列中进行备份,客户端在处理完数据后可以删除备份队列中的数据来确认消息处理完毕。

List实现消息队列有一个严重缺陷,不支持消息多播,一个消息没办法被多个客户端同时消费,也就是在Kafka中的“消息组”的概念。

10.2. Pub/Sub 发布订阅实现消息队列

“发布/订阅"模式同样可以实现进程间的消息传递,其原理如下:

“发布/订阅"模式包含两种角色,分别是发布者订阅者订阅者可以订阅一个或者多个频道(channel),而发布者可以向指定的频道(channel)发送消息,所有订阅此频道的订阅者都会收到此消息。

常用命令如下:

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%205.png

我这里区分一下频道模式

频道我们可以先理解为是个 Redis 的 key 值,而模式,可以理解为是一个类似正则匹配的 Key,只是个可以匹配给定模式的频道。这样就不需要显式地去订阅多个名称了,可以通过模式订阅这种方式,一次性关注多个频道。

10.3. Streams数据类型实现消息队列

使用Streams数据类型需要Redis 版本5.0+。

Streams 是 Redis 专门为消息队列设计的数据类型,它提供了丰富的消息队列操作命令。

  • XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
  • XREAD:用于读取消息,可以按 ID 读取数据;
  • XREADGROUP:按消费组形式读取消息;
  • XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。

11. NUMA 架构的多核CPU环境下Redis的“绑核”优化

在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%206.png

在多 CPU 架构上,应用程序可以在不同的处理器上运行。在刚才的图中,Redis 可以先在 Socket 1 上运行一段时间,然后再被调度到 Socket 2 上运行。

此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。

在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)

为了避免context switch带来的远端内存访问延迟,可以用taskset命令将Redis实例绑在一个CPU核心上

taskset -c 0 ./redis-server

绑核有个缺陷:当我们把 Redis 实例绑到一个 CPU 逻辑核上时,就会导致子进程、后台线程和 Redis 主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加。

12. 主从集群的脑裂问题

主从集群中,会出现一种场景:主库的数据还没有同步到从库,结果主库发生了“假故障”(一定时间不可用,可能是CPU占满等),等从库升级为主库后,未同步的数据就丢失了。

Redis%E7%9A%84%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E7%82%B9%20763b90558c7c4ab0ae32a02616a3e4f2/Untitled%207.png

脑裂问题的核心是主从切换过程中,主库仍然在处理客户端数据。

因此,Redis 已经提供了两个配置项来限制主库的请求处理:

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

这两个配置项组合后的要求是:主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。

13. 数据的过期时间

设置数据过期时间尽量设置到指定时间,而不是一定时间后。

一定时间后自动删除的方式,在主从集群中会不一致。

  • 从库认为,过期时间 = 从库同步到数据的时间 + 指定时间段后
  • 主库认为,过期时间 = 主库写入数据的时间 + 指定时间段后
  • 而主库写入数据时间 在 从库同步到数据的时间 之前

14. Redis事务不具备回滚特性

这点不复赘述,具体看我另一篇Redis笔记。

15. 参考资料

image-20210107190944577