首页 » SEO优化 » phpredis寄存json技巧_Redis 内存优化神技小内存保存大年夜数据

phpredis寄存json技巧_Redis 内存优化神技小内存保存大年夜数据

duote123 2024-11-23 0

扫一扫用手机浏览

文章目录 [+]

如何用更少的内存保存更多的数据?

phpredis寄存json技巧_Redis 内存优化神技小内存保存大年夜数据

我们该当从 Redis 是如何保存数据的事理展开,剖析键值对的存储构造和事理。

phpredis寄存json技巧_Redis 内存优化神技小内存保存大年夜数据
(图片来自网络侵删)

从而连续延展出每种数据类型底层的数据构造,针对不同场景利用更恰当的数据构造和编码实现更少的内存占用。

为了保存数据, Redis 须要先申请内存,数据过期或者内存淘汰须要回收内存,从而拓展出内存碎片优化。

末了,说下 key、value 利用规范和技巧、 Bitmap 等高阶数据类型,利用这些技巧奥妙办理有限内存去存储更多数据难题……

这一套组合拳下来直接封神。

详细详情,且看「码哥」逐一道来。

紧张优化神技如下:

键值对优化;小数据凑集的编码优化;利用工具共享池;利用 Bit 比特位或 byte 级别操作利用 hash 类型优化;内存碎片优化;利用 32 位的 Redis。

在优化之前,我们先节制 Redis 是如何存储数据的。

Redis 如何存储键值对

Redis 以 redisDb为中央存储,redis 7.0 源码在 https://github.com/redis/redis/blob/7.0/src/server.h:

redisDb

dict:最主要的属性之一,便是靠这个定义了保存了工具数据键值对,dcit 的底层构造是一个哈希表。
expires:保存着所有 key 的过期信息.blocking_keys 和 ready_keys 紧张为了实现 BLPOP 等壅塞命令watched_keys用于实现watch命令,记录正在被watch的一些key,与事务干系。
id 为当前数据库的id,redis 支持单个做事多数据库,默认有16个;clusterSlotToKeyMapping:cluster 模式下,存储key 与哈希槽映射关系的数组。

Redis 利用「dict」构造来保存所有的键值对(key-value)数据,这是一个全局哈希表,以是对 key 的查询能以 O(1) 韶光得到。

所谓哈希表,我们可以类比 Java 中的 HashMap,实在便是一个数组,数组的每个元素叫做哈希桶。

dict 构造如下,源码在 https://github.com/redis/redis/blob/7.0/src/dict.h:

struct 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_table,ht_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_BITS:LRU 策略下工具末了一次被访问的韶光,如果是 LFU 策略,那么低 8 位表示访问频率,高 16 位表示访问韶光。
refcount :表示引用计数,由于 C 措辞并不具备内存回收功能,以是 Redis 在自己的工具系统中添加了这个属性,当一个工具的引用计数为 0 时,则表示该工具已经不被任何工具引用,则可以进行垃圾回收了。
ptr 指针:指向工具的底层实现数据构造,指向值的指针。

如下图是由 redisDb、dict、dictEntry、redisObejct 关系图:

redis存储构造

「码哥」再唠叨几句,void key 和 void value 指针指向的是 redisObject,Redis 中每个工具都是用 redisObject 表示。

知道了 Redis 存储事理以及不同数据类型的存储数据构造后,我们连续看如何做性能优化。

1. 键值对优化

当我们实行 set key value 的命令,key指针指向 SDS 字符串保存 key,而 value 的值保存在 ptr 指针指向的数据构造,花费的内存:key + value。

第一个优化神技:降落 Redis 内存利用的最粗暴的办法便是缩减键(key)与值(value)的长度。

在《Redis 很强,不懂利用规范就挥霍了》中我说过关于键值对的利用规范,对付 key 的命名利用「业务模块名:表名:数据唯一id」这样的办法方便定位问题。

比如:users:firends:996 表示用户系统中,id = 996 的朋友信息。
我们可以简写为:u:fs:996

对付 key 的优化:利用单词简写办法优化内存占用。

对付 value 的优化那就更多了:

过滤不必要的数据:不要大而全的一股脑将所有信息保存,想办法去掉一些不必要的属性,比如缓存登任命户的信息,常日只须要存储昵称、性别、账号等。
精简数据:比如用户的会员类型:0 表示「屌丝」、1 表示 「VIP」、2表示「VVIP」。
而不是存储 VIP 这个字符串。
数据压缩:对数据的内容进行压缩,比如利用 GZIP、Snappy。
利用性能好,内存占用小的序列化办法。
比如 Java 内置的序列化不管是速率还是压缩比都弗成,我们可以选择 protostuff,kryo等办法。
如下图 Java 常见的序列化工具空间压缩比:序列化工具压缩比❝靓仔们,我们常日利用 json 作为字符串存储在 Redis,用 json 存储与二进制数据存储有什么优缺陷呢?json 格式的优点:方便调试和跨措辞;缺陷是:同样的数据比较字节数组占用的空间更大。
一定要 json 格式的话,那就先通过压缩算法压缩 json,再把压缩后的数据存入 Redis。
比如 GZIP 压缩后的 json 可降落约 60% 的空间。
2. 小数据凑集编码优化

key 工具都是 string 类型,value 工具紧张有五种基本数据类型:String、List、Set、Zset、Hash。

数据类型与底层数据构造的关系如下所示:

编码与数据构造

特殊解释下在最新版(非稳定版本,韶光 2022-7-3),ziplist 压缩列表由 quicklist 代替(3.2 版本引入),而双向链表由 listpack 代替。

其余,同一数据类型会根据键的数量和值的大小也有不同的底层编码类型实现。

在 Redis 2.2 版本之后,存储凑集数据(Hash、List、Set、SortedSet)在知足某些情形下会采取内存压缩技能来实现利用更少的内存存储更多的数据。

当这些凑集中的数据元素数量小于某个值且元素的值占用的字节大小小于某个值的时候,存储的数据会用非常节省内存的办法进行编码,理论上至少节省 10 倍以上内存(均匀节省 5 倍以上)。

比如 Hash 类型里面的数据不是很多,虽然哈希表的韶光繁芜度是 O(1),ziplist 的韶光繁芜度是 O(n),但是利用 ziplist 保存数据的话会节省了内存,并且在少量数据情形下效率并不会降落很多。

以是我们须要尽可能地掌握凑集元素数量和每个元素的内存大小,这样能充分利用紧凑型编码减少内存占用。

并且,这些编码对用户和 api 是无感知的,当凑集数据超过配置文件的配置的最大值, Redis 会自动转成正常编码。

数据类型对应的编码规则如下所示

String 字符串int:整数且数字长度小于 20,直接保存在 ptr 中。
embstr:开辟一块连续分配的内存(字符串长度小于即是 44 字节)。
raw:动态字符串(大于 44 字节的字符串,同时字符串小于 512 MB)。
List 列表ziplist:元素个数小于hash-max-ziplist-entries配置,同时所有的元素的值大小都小于 hash-max-ziplist-value配置。
ziplistlinkedlist:3.0 版本之前当列表类型无法知足 ziplist 的条件时,Redis会利用 linkedlist 作为列表的内部实现。
quicklist:Redis 3.2 引入,并作为 List 数据类型的底层实现,不再利用双端链表 linkedlist 和 ziplist 实现。
Set 凑集intset 整数凑集:元素都是整数,且元素个数小于 set-max-intset-entries配置hashtable 哈希表:凑集类型无法知足intset的条件时就会利用hashtable 编码。
Hash 哈希表ziplist:元素个数小于 hash-max-ziplist-entries配置,同时任意一个 value 的占用字节大小都小于hash-max-ziplist-value 。
hashtable:hash 类型无法知足 intset 的条件时就会利用hashtable。
Sorted Set 有序凑集ziplist:元素个数小于 zset-max-ziplist-entries 同时每个元素的value小于``zset-max-ziplist-value`配置。
skiplist:当ziplist条件不知足时,有序凑集会利用skiplist作为内部实现。

以下是 Redis redis.conf 配置文件默认编码阈值配置:

hash-max-ziplist-entries 512hash-max-ziplist-value 64zset-max-ziplist-entries 128zset-max-ziplist-value 64set-max-intset-entries 512

下图是 reidsObject 工具的 type 和 encoding 对应关系图:

type 与编码

码哥,为啥对一种数据类型实现多种不同编码办法?

紧张缘故原由是想通过不同编码实现效率和空间的平衡。

比如当我们的存储只有100个元素的列表,当利用双向链表数据构造时,须要掩护大量的内部字段。

比如每个元素须要:前置指针,后置指针,数据指针等,造成空间摧残浪费蹂躏。

如果采取连续内存构造的压缩列表(ziplist),将会节省大量内存,而由于数据长度较小,存取操作韶光繁芜度纵然为O(n) 性能也相差不大,由于 n 值小 与 O(1) 并明显差别。

数据编码优化技巧

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。

ziplist 的不敷

每次修正都可能触发 realloc 和 memcopy, 可能导致连锁更新(数据可能须要挪动)。

因此修正操作的效率较低,在 ziplist 的元素很多时这个问题更加突出。

优化手段:

key 只管即便掌握在 44 字节以内,走 embstr 编码。
凑集类型的 value 工具的元素个数不要太多太大,充分利用 ziplist 编码实现内存压缩。
3. 工具共享池

整数我们常常在事情中利用,Redis 在启动的时候默认天生一个 0 ~9999 的整数工具共享池用于工具复用,减少内存占用。

比如实行set 码哥 18; set 吴彦祖 18; key 即是 「码哥」 和「吴彦祖」的 value 都指向同一个工具。

如果 value 可以利用整数表示的话尽可能利用整数,这样纵然大量键值对的 value 大量保存了 0~9999 范围内的整数,在实例中,实在只有一份数据。

靓仔们,有两个大坑须要把稳,它会导致工具共享池失落效。

Redis 中设置了 maxmemory 限定最大内存占用大小且启用了 LRU 策略(allkeys-lru 或 volatile-lru 策略)。
❝码哥,为啥呀?由于 LRU 须要记录每个键值对的访问韶光,都共享一个整数 工具,LRU 策略就无法进行统计了。
凑集类型的编码采取 ziplist 编码,并且凑集内容是整数,也不能共享一个整数工具。
❝这又是为啥呢?利用了 ziplist 紧凑型内存构造存储数据,判断整数工具是否共享的效率很低。
4.利用 Bit 比特位或 byte 级别操作

比如在一些「二值状态统计」的场景下利用 Bitmap 实现,对付网页 UV 利用 HyperLogLog 来实现,大大减少内存占用。

码哥,什么是二值状态统计呀?

也便是凑集中的元素的值只有 0 和 1 两种,在签到打卡和用户是否上岸的场景中,只需记录签到(1)或 未签到(0),已登录(1)或未上岸(0)。

如果我们在判断用户是否上岸的场景中利用 Redis 的 String 类型实现(key -> userId,value -> 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 的上风。

关于 Bitmap 的详细解答,大家可移步 -> 《Redis 实战篇:巧用 Bitmap 实现亿级数据统计》。

5. 妙用 Hash 类型优化

尽可能把数据抽象到一个哈希表里。

比如说系统中有一个用户工具,我们不须要为一个用户的昵称、姓名、邮箱、地址等单独设置一个 key,而是将这个信息存放在一个哈希表里。

如下所示:

hset users:深圳:999 姓名 码哥hset users:深圳:999 年事 18hset users:深圳:999 爱好 女

为啥利用 String 类型,为每个属性设置一个 key 会占用大量内存呢?

由于 Redis 的数据类型有很多,不同数据类型都有些相同的元数据要记录(比如末了一次访问的韶光、被引用的次数等)。

以是,Redis 会用一个 RedisObject 构造体来统一记录这些元数据,用 prt 指针指向实际数据。

当我们为每个属性都创建 key,就会创建大量的 redisObejct 工具占用内存。

如下所示 redisObject 内存占用:

redisObejct

用 Hash 类型的话,每个用户只须要设置一个 key。

6. 内存碎片优化

Redis 开释的内存空间可能并不是连续的,这些不连续的内存空间很有可能处于一种闲置的状态。

虽然有空闲空间,Redis 却无法用来保存数据,不仅会减少 Redis 能够实际保存的数据量,还会降落 Redis 运行机器的本钱回报率。

比如, Redis 存储一个整形数字凑集须要一块占用 32 字节的连续内存空间,当前虽然有 64 字节的空闲,但是他们都是不连续的,导致无法保存。

内存碎片是如何形成呢?

两个层面缘故原由导致:

操作系统内存分配机制:内存分配策略决定了无法做到按需分配。
由于分配器是按照固定大小来分配内存。
键值对被修正和删除,从而导致内存空间的扩容和开释。

碎片优化可以降落内存利用率,提高访问效率,在4.0以下版本,我们只能利用重启规复:重启加载 RDB 或者通过高可用主从切换实现数据的重新加载减少碎片。

在4.0以上版本,Redis供应了自动和手动的碎片整理功能,事理大致是把数据拷贝到新的内存空间,然后把老的空间开释掉,这个是有一定的性能损耗的。

由于 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法处理要求,性能就会降落。

手动整理碎片

实行 memory purge命令即可。

自动整理内存碎片

利用 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实行命令。
7. 利用 32 位的 Redis

利用32位的redis,对付每一个key,将利用更少的内存,由于32位程序,指针占用的字节数更少。

但是32的Redis全体实例利用的内存将被限定在4G以下。
我们可以通过 cluster 模式将多个小内存节点构成一个集群,从而保存更多的数据。

其余小内存的节点 fork 天生 rdb 的速率也更快。

RDB和AOF文件是不区分32位和64位的(包括字节顺序),以是你可以利用64位的Redis 规复32位的RDB备份文件,相反亦然。

参考文献

[1]https://redis.io/docs/reference/optimization/memory-optimization/

[2]《Redis 核心技能与实战》

[3] https://segmentfault.com/a/1190000041771534

标签:

相关文章

今日头条算法岗三面算法工程师的面试之路

大数据和人工智能技术逐渐成为各行各业的热门话题。今日头条作为中国领先的资讯平台,其算法岗位一直是求职者争相竞逐的焦点。本文将针对今...

SEO优化 2025-01-31 阅读1 评论0

今日头条算法如何打造个化内容推荐

在移动互联网时代,信息爆炸已成为常态。如何在海量信息中找到自己感兴趣的内容,成为了一个亟待解决的问题。今日头条作为一款备受瞩目的新...

SEO优化 2025-01-31 阅读1 评论0

今日头条算法推荐其弊端与反思

算法推荐已经成为各大平台的核心竞争力。今日头条作为国内领先的资讯平台,凭借其强大的算法推荐系统,吸引了大量用户。在享受算法带来的便...

SEO优化 2025-01-31 阅读1 评论0

今日头条算法思维导图信息推荐的奥秘

信息传播速度越来越快,用户对信息的需求也越来越多样化。今日头条作为一款备受关注的新闻资讯平台,其背后的算法推荐机制一直是业界关注的...

SEO优化 2025-01-31 阅读1 评论0