域名是建网站之前申请吗,建设工程新工艺网站,网络营销案例题,虚拟主机上的网站上传方式这次跟大家分享一些优化神技如何用更少的内存保存更多的数据#xff1f;我们应该从 Redis 是如何保存数据的原理展开#xff0c;分析键值对的存储结构和原理。从而继续延展出每种数据类型底层的数据结构#xff0c;针对不同场景使用更恰当的数据结构和编码实现更少的内存占用…这次跟大家分享一些优化神技如何用更少的内存保存更多的数据我们应该从 Redis 是如何保存数据的原理展开分析键值对的存储结构和原理。从而继续延展出每种数据类型底层的数据结构针对不同场景使用更恰当的数据结构和编码实现更少的内存占用。为了保存数据 Redis 需要先申请内存数据过期或者内存淘汰需要回收内存从而拓展出内存碎片优化。最后说下 key、value 使用规范和技巧、 Bitmap 等高阶数据类型运用这些技巧巧妙解决有限内存去存储更多数据难题……这一套组合拳下来直接封神。主要优化神技如下键值对优化小数据集合的编码优化使用对象共享池使用 Bit 比特位或 byte 级别操作使用 hash 类型优化内存碎片优化使用 32 位的 Redis。在优化之前我们先掌握 Redis 是如何存储数据的。1 Redis 如何存储键值对Redis 以 redisDb 为中心存储Redis 7.0 源码在 https://github.com/redis/redis/blob/7.0/src/server.hdict最重要的属性之一就是靠这个定义了保存了对象数据键值对dcit 的底层结构是一个哈希表expires保存着所有 key 的过期信息blocking_keys 和 ready_keys 主要为了实现 BLPOP 等阻塞命令watched_keys 用于实现 watch 命令记录正在被 watch 的一些 key与事务相关id 为当前数据库的 id。Redis 支持单个服务多数据库默认有1 6 个clusterSlotToKeyMappingCluster 模式下存储 key 与哈希槽映射关系的数组。Redis 使用「dict」结构来保存所有的键值对key-value数据这是一个全局哈希表所以对 key 的查询能以 O(1) 时间得到。所谓哈希表我们可以类比 Java 中的 HashMap其实就是一个数组数组的每个元素叫做哈希桶。dict 结构如下源码在 https://github.com/redis/redis/blob/7.0/src/dict.hstruct dict {// 特定类型的处理函数dictType *type;// 两个全局哈希表指针数组与渐进式 rehash 有关dictEntry **ht_table[2];// 记录 dict 中现有的数据个数。unsigned long ht_used[2];// 记录渐进式 rehash 进度的标志 -1 表示当前没有执行 rehash long rehashidx;// 小于 0 表示 rehash 暂停int16_t pauserehash;signed char ht_size_exp[2];
};dictType存储了 hash 函数key 和 value 的复制等函数ht_table长度为 2 的 数组正常情况使用 ht_table[0] 存储数据当执行 rehash 的时候使用 ht_table[1] 配合完成 。key 的哈希值最终会映射到 ht_table 的一个位置如果发生哈希冲突则拉出一个哈希链表。大家重点关注 dictEntry 类型的 ht_tableht_table 数组每个位置我们也叫做哈希桶就是这玩意保存了所有键值对。Redis 支持那么多的数据类型哈希桶咋保存哈希桶的每个元素的结构由 dictEntry 定义typedef struct dictEntry {// 指向 key 的指针void *key;union {// 指向实际 value 的指针void *val;uint64_t u64;int64_t s64;double d;} v;// 哈希冲突拉出的链表struct dictEntry *next;
} dictEntry;key 指向键值对的键的指针key 都是 string 类型value 是个 union联合体当它的值是 uint64_t、int64_t 或 double 类型时就不再需要额外的存储这有利于减少内存碎片。为了节省内存操碎了心当然val 也可以是 void 指针指向值的指针以便能存储任何类型的数据next 指向另一个 dictEntry 结构 多个 dictEntry 可以通过 next 指针串连成链表 从这里可以看出 ht_table 使用链地址法来处理键碰撞当多个不同的键拥有相同的哈希值时哈希表用一个链表将这些键连接起来。哈希桶并没有保存值本身而是指向具体值的指针从而实现了哈希桶能存不同数据类型的需求。而哈希桶中键值对的值都是由一个叫做 redisObject 的对象定义源码地址https://github.com/redis/redis/blob/7.0/src/server.h。typedef struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS;int refcount;void *ptr;
} robj;type记录了对象的类型string、set、hash 、Lis、Sorted Set 等根据该类型才可以确定是哪种数据类型使用什么样的 API 操作encoding编码方式表示 ptr 指向的数据类型具体数据结构即这个对象使用了什么数据结构作为底层实现保存数据。同一个对象使用不同编码实现内存占用存在明显差异内部编码对内存优化非常重要lru:LRU_BITSLRU 策略下对象最后一次被访问的时间如果是 LFU 策略那么低 8 位表示访问频率高 16 位表示访问时间refcount表示引用计数由于 C 语言并不具备内存回收功能所以 Redis 在自己的对象系统中添加了这个属性当一个对象的引用计数为 0 时则表示该对象已经不被任何对象引用则可以进行垃圾回收了ptr 指针指向对象的底层实现数据结构指向值的指针。如下图是由 redisDb、dict、dictEntry、redisObejct 关系图这里再唠叨几句void *key 和 void *value 指针指向的是 redisObjectRedis 中每个对象都是用 redisObject 表示。知道了 Redis 存储原理以及不同数据类型的存储数据结构后我们继续看如何做性能优化。2 键值对优化当我们执行 set key value 的命令*key指针指向 SDS 字符串保存 key而 value 的值保存在 *ptr 指针指向的数据结构消耗的内存key value。第一个优化神技降低 Redis 内存使用的最粗暴的方式就是缩减键key与值value的长度。对于 key 的命名使用「业务模块名:表名:数据唯一id」这样的方式方便定位问题。比如users:firends:996 表示用户系统中id 996 的朋友信息。我们可以简写为u:fs:996对于 key 的优化使用单词简写方式优化内存占用。对于 value 的优化那就更多了过滤不必要的数据不要大而全的一股脑将所有信息保存想办法去掉一些不必要的属性比如缓存登录用户的信息通常只需要存储昵称、性别、账号等精简数据比如用户的会员类型0 表示「屌丝」、1 表示 「VIP」、2表示「VVIP」。而不是存储 VIP 这个字符串数据压缩对数据的内容进行压缩比如使用 GZIP、Snappy使用性能好内存占用小的序列化方式。比如 Java 内置的序列化不管是速度还是压缩比都不行我们可以选择 protostuffkryo等方式。如下图 Java 常见的序列化工具空间压缩比我们通常使用 JSON 作为字符串存储在 Redis用 JSON 存储与二进制数据存储有什么优缺点呢JSON 格式的优点方便调试和跨语言缺点是同样的数据相比字节数组占用的空间更大。一定要 JSON 格式的话那就先通过压缩算法压缩 JSON再把压缩后的数据存入 Redis。比如 GZIP 压缩后的 JSON 可降低约 60% 的空间。3 小数据集合编码优化key 对象都是 String 类型value 对象主要有五种基本数据类型String、List、Set、Zset、Hash。数据类型与底层数据结构的关系如下所示特别说明下在最新版非稳定版本时间 2022-7-3ziplist 压缩列表由 quicklist 代替3.2 版本引入而双向链表由 listpack 代替。另外同一数据类型会根据键的数量和值的大小也有不同的底层编码类型实现。在 Redis 2.2 版本之后存储集合数据Hash、List、Set、SortedSet在满足某些情况下会采用内存压缩技术来实现使用更少的内存存储更多的数据。当这些集合中的数据元素数量小于某个值且元素的值占用的字节大小小于某个值的时候存储的数据会用非常节省内存的方式进行编码理论上至少节省 10 倍以上内存平均节省 5 倍以上。比如 Hash 类型里面的数据不是很多虽然哈希表的时间复杂度是 O(1)ziplist 的时间复杂度是 O(n)但是使用 ziplist 保存数据的话会节省了内存并且在少量数据情况下效率并不会降低很多。所以我们需要尽可能地控制集合元素数量和每个元素的内存大小这样能充分利用紧凑型编码减少内存占用。并且这些编码对用户和 API 是无感知的当集合数据超过配置文件的配置的最大值 Redis 会自动转成正常编码。3.1 各数据类型对应的编码规则各数据类型对应的编码规则如下所示3.1.1 String 字符串int整数且数字长度小于 20直接保存在 *ptr 中embstr开辟一块连续分配的内存字符串长度小于等于 44 字节raw动态字符串大于 44 字节的字符串同时字符串小于 512 MB。3.1.2 List 列表ziplist元素个数小于 hash-max-ziplist-entries 配置同时所有的元素的值大小都小于 hash-max-ziplist-value 配置linkedlist3.0 版本之前当列表类型无法满足 ziplist 的条件时Redis会使用 linkedlist 作为列表的内部实现quicklistRedis 3.2 引入并作为 List 数据类型的底层实现不再使用双端链表 linkedlist 和 ziplist 实现。3.1.3 Set 集合intset 整数集合元素都是整数且元素个数小于 set-max-intset-entries配置hashtable 哈希表集合类型无法满足intset的条件时就会使用hashtable 编码。3.1.4 Hash 哈希表ziplist元素个数小于 hash-max-ziplist-entries 配置同时任意一个 value 的占用字节大小都小于 hash-max-ziplist-value hashtablehash 类型无法满足 intset 的条件时就会使用 hashtable。3.1.5 Sorted Set 有序集合ziplist元素个数小于 zset-max-ziplist-entries 同时每个元素的 value 小于 zset-max-ziplist-value 配置skiplist当ziplist条件不满足时有序集合会使用skiplist作为内部实现。3.2 各数据类型默认编码阈值配置以下是 Redis redis.conf 配置文件默认编码阈值配置hash-max-ziplist-entries 512
hash-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512下图是 reidsObject 对象的 type 和 encoding 对应关系图3.3 为什么对一种数据类型实现多种不同编码方式主要原因是想通过不同编码实现效率和空间的平衡。比如当我们的存储只有100个元素的列表当使用双向链表数据结构时需要维护大量的内部字段。比如每个元素需要前置指针、后置指针、数据指针等造成空间浪费。如果采用连续内存结构的压缩列表(ziplist)将会节省大量内存而由于数据长度较小存取操作时间复杂度即使为O(n) 性能也相差不大因为 n 值小 与 O(1) 并明显差别。3.4 数据编码优化技巧ziplist 存储 list 时每个元素会作为一个 entry存储 hash 时 key 和 value 会作为相邻的两个 entry。存储 zset 时 member 和 score 会作为相邻的两个entry当不满足上述条件时ziplist 会升级为 linkedlist, hashtable 或 skiplist 编码。由于目前大部分Redis运行的版本都是在3.2以上所以 List 类型的编码都是quicklist。quicklist 是 ziplist 和 linkedlist 的混合体它将 linkedlist 按段切分每一段使用 ziplist 来紧凑存储多个 ziplist 之间使用双向指针串接起来。考虑了综合平衡空间碎片和读写性能两个维度所以使用了新编码 quicklist。每次修改都可能触发 realloc 和 memcopy可能导致连锁更新数据可能需要挪动。因此修改操作的效率较低在 ziplist 的元素很多时这个问题更加突出。优化手段key 尽量控制在 44 字节以内走 embstr 编码集合类型的 value 对象的元素个数不要太多太大充分利用 ziplist 编码实现内存压缩。4 对象共享池整数我们经常在工作中使用Redis 在启动的时候默认生成一个 0 ~9999 的整数对象共享池用于对象复用减少内存占用。比如执行 set 码哥 18; set 吴彦祖 18;key 等于 「码哥」 和「吴彦祖」的 value 都指向同一个对象。如果 value 可以使用整数表示的话尽可能使用整数这样即使大量键值对的 value 大量保存了 0~9999 范围内的整数在实例中其实只有一份数据。这里有两个大坑需要注意它会导致对象共享池失效。Redis 中设置了 maxmemory 限制最大内存占用大小且启用了 LRU 策略allkeys-lru 或 volatile-lru 策略。因为 LRU 需要记录每个键值对的访问时间都共享一个整数 对象LRU 策略就无法进行统计了。集合类型的编码采用 ziplist 编码并且集合内容是整数也不能共享一个整数对象。这里由于使用了 ziplist 紧凑型内存结构存储数据判断整数对象是否共享的效率很低。5 使用 Bit 比特位或 byte 级别操作比如在一些「二值状态统计」的场景下使用 Bitmap 实现对于网页 UV 使用 HyperLogLog 来实现大大减少内存占用。二值状态统计就是集合中的元素的值只有 0 和 1 两种。在签到打卡和用户是否登陆的场景中只需记录签到 (1) 或 未签到( 0)已登录 (1) 或未登陆 (0)。假如我们在判断用户是否登陆的场景中使用 Redis 的 String 类型实现key - userIdvalue - 0 表示下线1 - 登陆假如存储 100 万个用户的登陆状态如果以字符串的形式存储就需要存储 100 万个字符串内存开销太大。String 类型除了记录实际数据以外还需要额外的内存记录数据长度、空间使用等信息。Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组Redis 把每个字节数组的 8 个 bit 位利用起来每个 bit 位 表示一个元素的二值状态不是 0 就是 1。可以将 Bitmap 看成是一个 bit 为单位的数组数组的每个单元只能存储 0 或者 1数组的下标在 Bitmap 中叫做 offset 偏移量。为了直观展示我们可以理解成 buf 数组的每个字节用一行表示每一行有 8 个 bit 位8 个格子分别表示这个字节中的 8 个 bit 位如下图所示8 个 bit 组成一个 Byte所以 Bitmap 会极大地节省存储空间。 这就是 Bitmap 的优势。6 妙用 Hash 类型优化尽可能把数据抽象到一个哈希表里。比如说系统中有一个用户对象我们不需要为一个用户的昵称、姓名、邮箱、地址等单独设置一个 key而是将这个信息存放在一个哈希表里。如下所示hset users:深圳:999 姓名 码哥
hset users:深圳:999 年龄 18
hset users:深圳:999 爱好 女为什么使用 String 类型为每个属性设置一个 key 会占用大量内存呢因为 Redis 的数据类型有很多不同数据类型都有些相同的元数据要记录比如最后一次访问的时间、被引用的次数等。所以Redis 会用一个 RedisObject 结构体来统一记录这些元数据用 *prt 指针指向实际数据。当我们为每个属性都创建 key就会创建大量的 redisObejct 对象占用内存。如下所示 redisObject 内存占用用 Hash 类型的话每个用户只需要设置一个 key。7 内存碎片优化Redis 释放的内存空间可能并不是连续的这些不连续的内存空间很有可能处于一种闲置的状态。虽然有空闲空间Redis 却无法用来保存数据不仅会减少 Redis 能够实际保存的数据量还会降低 Redis 运行机器的成本回报率。比如 Redis 存储一个整形数字集合需要一块占用 32 字节的连续内存空间当前虽然有 64 字节的空闲但是他们都是不连续的导致无法保存。7.1 内存碎片是如何形成呢两个层面原因导致操作系统内存分配机制内存分配策略决定了无法做到按需分配。因为分配器是按照固定大小来分配内存键值对被修改和删除从而导致内存空间的扩容和释放。碎片优化可以降低内存使用率提高访问效率在 4.0 以下版本我们只能使用重启恢复重启加载 RDB 或者通过高可用主从切换实现数据的重新加载减少碎片。在 4.0 以上版本Redis 提供了自动和手动的碎片整理功能原理大致是把数据拷贝到新的内存空间然后把老的空间释放掉这个是有一定的性能损耗的。因为 Redis 是单线程在数据拷贝时Redis 只能等着。这就导致 Redis 无法处理请求性能就会降低。7.2 手动整理碎片执行 memory purge 命令即可。7.3 自动整理内存碎片使用 config set activedefrag yes 指令或者在 redis.conf 配置 activedefrag yes 将 activedefrag 配置成 yes 表示启动自动清理功能。这个配置还不够至于啥时候清理还需要看下面的两个配置active-defrag-ignore-bytes 200mb内存碎片的大小达到 200MB开始清理active-defrag-threshold-lower 6表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 6% 时开始清理。只有满足这两个条件 Redis 才会执行内存碎片自动清理。除此之外Redis 为了防止清理碎片对 Redis 正常处理指令造成影响有两个参数用于控制清理操作占用 CPU 的时间比例上下限。active-defrag-cycle-min 15自动清理过程所用 CPU 时间的比例不低于 15%保证清理能有效展开active-defrag-cycle-max 50表示自动清理过程所用 CPU 时间的比例不能大于 50%一旦超过就停止清理从而避免在清理时大量的内存拷贝阻塞 Redis执行命令。8 使用 32 位的 Redis使用 32 位的 Redis对于每一个 key 将使用更少的内存。因为 32 位程序指针占用的字节数更少。但是32 位 Redis 整个实例使用的内存将被限制在 4G 以下。我们可以通过 cluster 模式将多个小内存节点构成一个集群从而保存更多的数据。另外小内存的节点 fork 生成 rdb 的速度也更快。RDB 和 AOF 文件是不区分 32 位和 64 位的包括字节顺序所以你可以使用 64 位的 Redis 恢复 32 位的 RDB 备份文件相反亦然。