乐趣区

关于redis:Redis-实战-12-降低内存占用

简介

升高 Redis 的内存占用有助于缩小创立快照和加载快照所需的工夫、晋升载入 AOF 文件和重写 AOF 文件时的效率、缩短从服务器进行同步所需的工夫(快照、AOF 文件重写在 长久化选项 中进行了介绍,从服务器同步在 复制、解决故障、事务及性能优化 中进行了介绍),并且能让 Redis 存储更多的数据而无需增加额定的硬件。P208

短构造 (short structure) P208

Redis 为列表、汇合、散列和有序汇合提供了一组配置选项,这些选项能够让 Redis 以更节约空间的形式存储长度较短的构造(前面简称“短构造”)。P208

在列表、散列和有序汇合的长度较短或者体积较小的时候,Redis 能够抉择应用一种名为压缩列表 (ziplist) 的紧凑存储形式来存储这些机构。压缩列表是列表、散列和有序汇合这 3 种不同类型的对象的一种非结构化 (unstructured) 示意:与 Redis 在通常状况下应用双向链表示意列表、应用散列表示意散列、应用散列表加上跳表 (skiplist) 示意有序汇合的做法不同,压缩列表会以序列化的形式存储数据,这些序列化数据每次被读取的时候都要进行解码,每次被写入的时候也要进行部分的从新编码,并且可能须要对内存外面的数据进行挪动。P209

压缩列表示意 P209

本节以最简略的列表进行察看比照。

双向链表 P209

列表不进行压缩时应用双向链表 (doubly linked list) 进行存储,链表的每个结点都有三个指针:P209

  • 指向前一个结点的指针
  • 指向后一个结点的指针
  • 指向结点蕴含的字符串值的指针

其中字符串值又分为三个局部:P209

  • 字符串的长度
  • 字符串残余可用的字节数
  • 以空字符结尾的字符串自身

能够发现未压缩前,每存储一个字符串,起码须要 21 字节的额定开销 (overhead)。(三个指针每个占 4 个字节,两个整数每个占 4 个字节,字符串结尾的空字符占 1 个字节)P209

压缩列表 P209

压缩列表是由结点(非实在结点)组成的序列 (sequence),每个结点都由两个长度值和一个字符串组成。P209

  • 第一个长度值:前一个结点的长度,用于从后向前的遍历(个别以一个字节存储)
  • 第二个长度值:以后结点的长度(个别以一个字节存储)
  • 字符串:长度等于字节数,没有空字符

能够发现压缩后,每存储一个字符串,起码须要 2 字节的额定开销。P210

应用压缩列表编码 P210

不同构造对于应用压缩列表的配置选项 P210

# 列表应用压缩列表示意的限度条件
list-max-ziplist-entries 512
list-max-ziplist-value 64

# 散列应用压缩列表示意的限度条件
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# 有序汇合应用压缩列表示意的限度条件
zset-max-ziplist-entries 512
zset-max-ziplist-value 64

其中,...-entries 选项阐明列表、散列和有序汇合在被编码为压缩列表的状况下,容许蕴含的最大元素数量;...-value 选项阐明了压缩列表每个结点的最大体积是多少字节。当这些选项设置的限度条件中的任意一个被冲破时,Redis 就会将对应的列表、散列和有序汇合从压缩列表编码转换为其余构造,而内存占用也会因而减少,并且即便其未来从新满足限度条件,也不会再转换回压缩列表。P210

调试 P210

OBJECT 命令容许从外部查看给定 key 的 Redis 对象,它通常用在调试 (debugging) 或者理解为了节俭空间而对 key 应用非凡编码的状况。当将 Redis 用于进行缓存时,也能够通过 OBJECT 命令中的信息,决定 key 的驱赶策略 (eviction policies)。

  • OBJECT REFCOUNT <key>: 返回给定 key 援用所贮存的值的次数。次要用于调试
  • OBJECT ENCODING <key>: 返回给定 key 所贮存的值所应用的外部示意 (representation)
  • OBJECT IDLETIME <key>: 返回给定 key 自贮存以来的闲暇工夫 (idle,没有被读取也没有被写入),以秒为单位
汇合的整数汇合编码 P211

如果汇合的所有成员都能够被解释为十进制整数(在平台的有符号整数范畴内),并且汇合成员的数量足够少,那么 Redis 就会以有序整数数组的形式存储汇合,这种存储形式又被称为整数汇合 (intset)。整数汇合不仅能够升高内存耗费,还能够晋升所有规范汇合操作的执行速度。P211

整数汇合的配置选项 P211

# 汇合应用整数汇合示意的限度条件
set-max-intset-entries 512

当整数汇合蕴含当元素数量超过配置选项设定的限度时,整数汇合将被转换为散列表示意。P212

长压缩列表和大整数汇合带来的性能问题 P212
压缩列表结点数 性能
< 1000 差异不大
5000 ~ 10000 开始降落
50000 降落显著
> 100000 低到无奈应用

举荐将压缩列表的长度限度在 1024 个元素内,并且每个元素的体积不能超过 64 字节,对于大多数散列利用来说,这种配置能够同时兼顾低内存占用和高性能这两方面长处。P214

Redis 在 3.2 版本后的列表底层默认应用 quicklist,这种数据结构兼顾了双向链表和压缩列表的长处,因而列表目前来说已应用最优配置。

咱们在设计 Redis 时同时也要放弃键名简短(包含数据键、散列的域、汇合和有序汇合的成员以及所有列表的结点),当存储结点的数据量达到上百万个或者数十亿个时,将能节俭 MB 升至 GB 级的空间。P214

分片构造 P214

分片 (sharding) 实质上就是基于某些简略的规定将数据划分为更小的局部,而后依据数据所属的局部来决定将数据发送到哪个地位下面。这种技术能够扩大存储空间并进步所能解决的负载量。P214

接下来将把分片的概念利用到散列、汇合和有序汇合上,并在解说实现这些数据结构的其中一部分规范性能的办法。这种状况下,程序不再是将值 X 存储到键 Y 外面,而是将值 X 存储到键 Y:<shardid> 外面。P214

对列表进行分片 P214

在不应用 Lua 脚本的状况下对列表进行调配十分艰难,因而将在前面介绍应用 Lua 脚本构建一个分片式的列表,并反对以阻塞和非阻塞两种形式从列表的两端进行推入和弹出操作。

对有序汇合进行分片 P215

因为 ZRANGE, ZRANGEBYSCORE, ZRANK, ZCOUNT, ZREMRANGE, ZREMRANGEBYSCORE 这类命令的分片版本须要对有序汇合的所有分片进行操作能力计算出命令的最终后果,所以这些操作无奈运行得像一般的有序汇合操作那么快,因而对有序汇合进行分片的作用不大。

如果须要将残缺的信息存储到一个体积较大的有序汇合中,但只会对分值排名前 n 位和后 n 位对元素进行操作,那么能够应用上面介绍对散列分片对办法对有序汇合进行分片,并保护额定对最高分值对有序汇合和最低分值对有序汇合,而后通过 ZADD 命令为这两个有序汇合增加新元素,并通过 ZREMRANGEBYRANK 命令确保元素对数量不会超过限度。P215

分片式散列 P215

对散列的键进行划分时,能够把散列存储的键作为一个信息源,并应用散列函数为键计算出一个数字散列值,而后依据须要存储的键的总数量以及每个分片须要存储的键数量,计算出所需的分片数,最初应用分片数和散列只决定应把键存储到哪个分片外面。P215

所思

其实咱们平时在思考分片这种模式的时候是不太会思考到键的总数量的这种条件,基本上是依据现有的数据进行剖析后设定一个分片数量 shard_num,这样当有一个键 key 须要计算对应的分片时,只须要 cal_hash(key) % shard_num 即可失去对应的 shard_id。但相似 CRC32MD5 这种形式进行散列值时有一个问题,就是书中提到的当分片数量扭转时,会有大量键的新旧散列值不同,就须要将数据迁徙至新散列值对应的 shard_id。为了防止这样的状况,就须要一致性哈希算法,使得分片数量扭转时须要迁徙的数据尽量小一点,并保障迁徙后的数据仍可能较为平均的在每个分片上。

将字符串存储到散列外面 P217

如果发现将很多相关联的短字符串或者数字存储到了字符串键外面,并且继续地将这些键命名为 namespace:id 这样的模式,那么能够思考将这些值存储到分片散列外面,在某些状况下,这种做法能够显著缩小内存占用。P217

分片汇合 P218

汇合一样能够通过相似散列的形式解决键取得分片 id,进而革新相应的命令反对分片式操作。

如果键是整数且最大值绝对较小,那么除了间接应用键获取分片 id,还能够应用位图 (bitmap) 记录每个键是否在“汇合”中。P221

如果键是整数,数量十分多,无奈全副存下,但又能容忍肯定的误差,那么能够应用布隆过滤器记录每个键是否在“汇合”中(判断为不存在时,则必然不存在;判断为存在时,有极低概率不存在)。

打包存储二进制位和字节 P221

后面提到当应用相似 namespace:id 这样当字符串键去存储短字符串或者计数器时,应用分片散列能够无效升高存储这些数据所需当内存。然而,如果被存储的是一些简短并且长度固定当间断 id,那么咱们还有应用比分片散列更为节约内存当数据存储办法可用。P221

Redis 数据结构常用命令简介 中介绍过可用于高效打包和更新 Redis 字符串的四个命令:P221

  • GETRANGE: 用于读取被存储字符串的其中一部分内容
  • SETRANGE: 用于对存储在字符串外面的其中一部分内容进行设置
  • GETBIT: 用于获取字符串外面某个二进制位的值
  • SETBIT: 用于对字符串外面某个二进制位进行设置

通过这四个命令,咱们能够在不对数据进行压缩的状况下,应用 Redis 字符串以尽可能紧凑的格局去存储计数器、定长字符串、布尔值等数据。P221

决定被存储地位信息的格局 P221

咱们以存储的信息是用户的地位信息为例,不同内存的使用量决定了不同的地位精度:P221

  • 1 字节:准确到国家
  • 2 字节:准确到国家及所在州 / 省
  • 3 字节:准确到邮政编码
  • 4 字节:准确到经纬度(2 米)

这里咱们用 2 字节存储地位信息,首先咱们能够应用一个数组存储所有国家(或地区)的 ISO3 国家(或地区)编码,而后用第一个字节存储所在国家(或地区)在数组中的下标。而后咱们能够应用一个 map,同样应用数组存储每个国家(或地区)的州 / 省信息,用第二个字节存储所在州 / 省在对应数组中的下标。P222

存储打包后的数据 P223

获取到地位信息对应到两个字节到数据后,就能够应用 SETRANGE 命令将其存储到字符串键外面去了。然而还须要思考用户的总量,如果用户数量达到 7.5 亿,那么须要 1.5 GB 内存存储所有用户的数据,但 Redis 的字符串键最大只能存储 512 MB 数据,并且 Redis 在对现有的字符串进行设置的时候,如果被设置的局部超过了现有字符串的开端,那么 Redis 可能须要调配更多的内存以存储新数据,因而对一个长字符串的开端进行设置,耗时要比执行一个简略的 SETBIT 调用多得多。为了解决上述问题,咱们须要将数据分片到多个字符串键外面。P223

咱们能够在每个字符串外面存储 2^20 个用户的地位信息,这相当于在字符串外面构建 100 多万个节点,而这样的字符串须要占 2 MB 的内存。P223

对分片字符串进行聚合计算 P224

对所有用户的地位信息进行聚合计算 P224

找到提前存储的最大的用户 id,而后计算最大分片 id,遍历每个字符串分片中的每个用户的数据(应用 GETRANGE 分块获取数据),依据两个字节对应的下标找到对应的国家(或地区)及州 / 省信息,而后统计即可。

对指定用户的地位信息进行聚合计算 P226

遍历每个指定的用户 id,计算其对应的分片 id 和分片中的偏移量,应用 GETRANGE 获取对应的两个字节,依据两个字节对应的下标找到对应的国家(或地区)及州 / 省信息,而后统计即可。

本文首发于公众号:满赋诸机(点击查看原文)开源在 GitHub:reading-notes/redis-in-action

退出移动版