Redis学习-内存优化

如下为我的学习Redis的备忘录--内存优化,基于Redis4.0.2redis

1.随时查看info memory,了解内存使用情况
127.0.0.1:6379> info memory
# Memory
used_memory:2314624 //(字节单位形式)
used_memory_human:2.21M  //Redis已分配的内存总量(易读单位形式)
used_memory_rss:1282048
used_memory_rss_human:1.22M //操做系统为Redis进程分配的内存总量
used_memory_peak:18010560
used_memory_peak_human:17.18M //最大使用内存总量(峰值)
used_memory_peak_perc:12.85% 
used_memory_overhead:2078792
used_memory_startup:963088
used_memory_dataset:235832
used_memory_dataset_perc:17.45%
total_system_memory:4294967296
total_system_memory_human:4.00G
used_memory_lua:37888
used_memory_lua_human:37.00K //缓存Lua脚本占用的内存
maxmemory:0
maxmemory_human:0B //最大内存限制,0表示无限制
maxmemory_policy:noeviction //超过内存限制后的处理策略
mem_fragmentation_ratio:0.55 //碎片率(used_memory_rss/used_memory的比值),>1表示有碎片,<1表示部分Redis的内存被系统交换到硬盘(此时Redis性能变差)
mem_allocator:libc 
active_defrag_running:0
lazyfree_pending_objects:0

2.Redis主进程的内存消耗:
  • Redis自身使用的内存:消耗不多,3MB多点
  • 对象内存
  • 缓冲内存
  • 内存碎片
2.1对象内存:全部key对象长度 + 全部value对象长度
  • 每次建立键值对时,至少建立两个类型对象:key对象、value对象,应该使用短键名
2.2缓冲内存
  • 每一个客户端的输入、输出缓冲内存:
    • 输入缓冲最大1G,超出则关闭该客户端链接;
    • 输出缓冲:16KB的固定缓冲区、动态缓冲区,动态缓冲区可经过client-output-buffer-limit配置参数限制(根据客户端类型normal、slave、pubsub,分开设置)
      • client-output-buffer-limit normal 0 0 0
      • client-output-buffer-limit slave 256mb 64mb 60 //超过256MB时,或者持续超过64MB达60秒,关闭链接
      • client-output-buffer-limit pubsub 32mb 8mb 60
  • 复制积压缓冲内存:用于主从复制的部分复制,全部客户端共享该缓冲区,默认1MB,可经过repl-backlog-size调整,适当调大,可有效避免全量复制;
  • AOF缓冲内存:用于保存在AOF重写期间的写命令,便于重写完毕后把缓冲的命令追加到AOF文件中;
2.3内存碎片
  • 当存储的数据长短差别较大时,就容易出现大量内存碎片,应该尽量地保持数据对齐或使用固定长度的字符串;
  • 内存碎片只能经过彻底重启Redis来清除;
3.Redis子进程内存消耗
  • 在执行AOF重写和RDB快照持久化时,会fork一个子进程,父子进程将共享此刻的内存快照,期间,在Linux下使用写时复制技术:父进程会为新进的写命令请求须要修改的内存页复制出一份副原本完成写操做,子进程结束后,父进程再把该副本覆盖回原来的内存页。
  • Linux默认开启的THP把写时复制期间的内存页复制单位从4KB变为2MB,加大了持久化时的内存消耗,应该关闭该功能:sudo echo never > /sys/kernel/mm/transparent_hugepage/enabled
4.内存管理
  • 设置内存上限,并指定内存回收策略;
  • maxmemory配置参数可限制当前Redis实例可以使用的最大内存;
  • 经过config set maxmemory可根据业务需求,动态调整内存限制;
  • 经过设置内存上限,可方便地在一台服务器上部署多个Redis实例
4.1内存回收策略:
  • 为键设置过时属性,Redis采用惰性删除和定时任务删除机制实现过时键的内存回收;
    • 惰性删除:在读取键时才检查是否过时
    • 定时任务删除:经过hz配置参数设置频率,默认每秒10次;
  • 内存溢出控制策略:共6中策略,经过maxmemory-policy配置参数控制,默认noeviction(不删除,拒绝写入,返回错误)
    • LRU算法表示最近最少使用的,LFU算法表示最不经常使用的:
      • #volatile-lru - >在设置了过时的key中,删除最近最少使用的key,直到空间足够为止
      • #allkeys-lru - >从全部key里删除最近最少使用的key,无论有没设置过时,直到空间足够为止
      • #volatile-lfu - >在设置了过时的key中,删除最少使用的key,直到空间足够为止
      • #allkeys-lfu - >从全部key里删除最少使用的key,无论有没设置过时,直到空间足够为止
      • #volatile-random - >删除一个过时集合中的随机key。
      • #allkeys-random - >删除一个随机key,无论有没设置过时。
      • #volatile-ttl - >删除即将过时的key(次TTL)
      • #noviction - >不删除,拒绝写入,写入操做时返回错误。
  • maxmemory-samples 5 是说每次进行淘汰的时候,会随机抽取5个key 从里面淘汰最少使用的(默认选项)
  • 应避免内存溢出,由于在内存溢出且非noeviction策略时,会频繁触发回收内存的操做,影响Redis性能,如有从节点,还会把删除命令同步给从节点;
  • 对于只作缓存的场景下,可经过调小maxmemory,并执行一次命令,若是使用非noeviction策略,则会一次性回收到maxmemory指定的内存使用量,实现内存的快速回收,但会致使数据丢失和短暂阻塞;
5.内存优化:
  • Redis存储的全部数据都使用redisObject来封装,包括string、hash、list、set、zset
  • redisObject的字段:
    • type字段:保存对象使用的数据类型,命令type {key}返回值对象的数据类型
    • encoding字段:保存对象使用的内部编码类型,命令object encoding {key}返回值对象的内部编码类型
    • lru字段:记录对象最后一次被访问的时间(用于内存回收),命令object idletime {key}查看键的空闲时间(可配合scan命令批量查找长期空闲的键进行清理)
    • refcount字段:记录对象的引用计数(用于回收),命令object refcount {key}查看键的引用数
    • *ptr字段:存储值对象的数据或指针,若是是整数,则直接存储数据,不然表示指向数据的指针
  • 字符串长度在39字节之内对象,在建立redisObject封装对象时只需分配内存1次,可提升性能;
  • 缩减键、值对象的长度:简化键名,使用高效的序列化工具来序列化值对象,还可以使用压缩工具(Google Snappy)压缩序列化后的数据;
  • 共享对象池:Redis内部维护[0-9999]的整数对象池,对于0-9999的内部整数类型的元素、整数值对象都会直接引用整数对象池中的对象,所以尽可能使用整数对象可节省内存;
    • 注意:
      • 启用LRU相关的溢出策略时,没法使用共享对象池;
      • 对于ziplist编码的值对象,也没法使用共享对象池(成本太高);
  • Redis对字符串的优化
    • Redis全部key都是string类型,且value对象的数据除了整数以外,最终也都使用string来存储;
    • Redis字符串结构采用SDS(内部简单动态字符串):
      • int len字段:已用字节长度
      • int free字段:未用字节长度
      • char buf[]字段:字节数组
    • SDS字符串特色:
      • 获取字符串长度、未用长度速度快,时间复杂度为O(1)
      • 用字节数组保存数据,支持安全的二进制数据存储
      • 内部实现了预分配内存机制,下降内存分配次数
      • 惰性删除机制,字符串缩减后的空间不释放,做为预分配空间保留
    • SDS字符串内存预分配机制:
      • 首次建立时,不作预分配,数据恰好填满字节数组,len字段为字节数组长度,free字段为0
      • 在修改字符后,若是本来的free空间不足,且当前总数据大小<1MB,则每次预分配1倍容量,而若是总数据大小>1MB,则每次预分配1MB容量。
      • 如:(忽略len、free字段所占内存,只考虑buf所占内存)
        • 对于首次建立的30字节字符串,对它执行append追加10字节,将使用(30+10)+40+1=81字节的内存
        • 而直接set这40字节的字符串,只使用41字节的内存(1字节为结尾标识'\0')
    • 应该尽可能避免频繁执行增加字符串的命令,如append、setrange,改成直接用set一次性建立字符串,减小预分配带来的内存浪费和下降内存碎片率;
    • 字符串重构:编码为ziplist的hash数据结构的妙用1
      • 对于非简单字符串数据,可用hash数据结构代替
      • 由于小hash使用ziplist编码,可节省内存(字符串数据必须小于hash-max-ziplit-value配置的值)
      • 且hash可用使用hmget、hmset命令,支持field-value的部分读取修改,而没必要每次都总体存取 
6.合理设置内部编码配置参数:
Redis的每种数据结构都有至少两种内部数据编码类型: object encoding {key}  获取key对应的value对象的编码类型
string int 8个字节的长整型
embstr <=39字节的字符串
raw >39字节的字符串(最大不能超过512MB)
hash ziplist 压缩列表(模拟双向链表),内存占用少,但读写时间复杂度为O(n²)
hashtable 哈希表,内存占用较大,但读写时间复杂度为O(1)
list quicklist (ziplist) 快速双向链表(每一个节点都是ziplist)
set intset 整数集合
hashtable 哈希表
zset ziplist 压缩列表
skiplist 跳跃表
  • Redis在写入数据时自动完成编码转换,且在超过配置的限制值时将转换为新的内部编码,动态修改限制参数不会回退为旧编码,只有在重启Redis从新加载数据后才会回退;
  • ziplist编码:
    • ziplist内部结构:
      • zlbytes字段:int-32类型,记录整个ziplist总字节数,便于从新调整ziplist空间;
      • zltail字段:记录距离尾节点的偏移量,便于尾节点的弹出操做;
      • zllen字段:记录节点数量;
      • entry1...entryN节点:记录具体的节点,长度根据具体的数据
        • prev_entry_bytes_length:记录前一节点所占空间,用于快速定位前一节点实现列表的反向迭代;
        • encoding:当前节点编码和长度,前两位表示编码类型(字符串、整数),其他位表示数据长度;
        • contents:保存节点的值,针对实际数据长度作内存占用优化;
      • zlend字段:记录列表结尾,占1个字节
    • ziplist是一块连续的内存,它模拟了双向链表的功能,两端的push和pop速度快,可是对中间元素的修改不方便,每次在中间插入、删除都会引起内存从新分配和数据拷贝,ziplist越长性能越低,因此ziplist仅适合存储小对象和长度有限的数据。
    • 所以,ziplist的长度不宜过长(建议1000个之内),且元素大小不宜过大(建议512字节之内),且最好每一个元素的大小差异不宜过大(不然碎片多)。
    • 对于较小的hash、zset 数据结构,Redis会自动使用ziplist编码,虽然list的编码为quicklist,但list的节点也是ziplist编码。
    • hash同时知足如下条件则使用ziplist编码,超过则使用hashtable编码
      • hash-max-ziplist-entries 64 
      • hash-max-ziplist-value 512 
    • list使用的是quicklist编码,quicklist的每一个节点都是ziplist,如下指定节点的设置
      • list-max-ziplist-size -2 //>0时表示每一个节点最多包含几个数据项,即ziplist的长度。<0时,只能取-5~-1,指每一个节点ziplist的最大字节大小≤64KB~4KB字节(超过该限制时,则新建一个节点)
      • list-compress-depth n //n表示两端不被压缩的节点个数(压缩全部中间节点),默认0不压缩
    • zset同时知足如下条件则使用ziplist编码,超过则使用skiplist编码
      • zset-max-ziplist-entries 128
      • zset-max-ziplist-value 64
    • set全部元素都为整数,且个数小于如下参数时,使用intset编码,不然使用hashtable编码
      • set-max-intset-entries 512
  • intset编码:
    • intset编码是无序集合(set)类型编码的一种,内部表现为存储有序、不重复的整数集合
    • intset结构:
      • encoding:根据集合内最长整数值肯定全部元素的类型(int-1六、int-3二、int-64),当插入一个更长的整数类型时,会触发类型升级操做(会致使从新申请内存空间,并复制数据到新数组)
      • length:集合元素的个数;
      • contents:整数数组,按从小到大顺序保存;
    • 因此,在使用set集合,且为整数时,应该保持整数长度类型的一致性,避免内存浪费;
    • set小集合重构(编码为ziplist的hash数据结构的妙用2):由于当set集合中有一个是非整数时,将使用hashtable编码,没法使用intset实现内存优化,若是集合元素个数和大小知足hash的ziplist编码条件,则此时可用hash类型来模拟集合,把hash的field设为set的元素,而hash的value设为1字节占位符便可;
7.使用32位Redis实例:
  • 假如缓存数据小于4GB,就使用32位Redis实例,由于对于每个key,将使用更少的内存,指针占用的字节数更少。
  • 使用make 32bit命令编译生成32位的redis。但内存受限在4G内,不过他们的RDB和AOF文件是兼容在32位和64位的。
8.尽量的使用hash数据结构
  • 由于Redis在储存小于100个字段的Hash结构上,其存储效率是很是高的。因此在不须要集合(set)操做或list的push/pop操做的时候,尽量的使用hash结构
  • 使用单命令多参数的命令取代多命令单参数的命令:
    • set -> mset
    • get -> mget
    • lset -> lpush, rpush
    • lindex -> lrange
    • hset -> hmset
    • hget -> hmget 
9.减小key的数量(编码为ziplist的hash数据结构的妙用3)
  • 把大量value为string的普通key-value抽象为分组的小hash的field-value,建议field总个数<1000,value的长度<512字节,value越小,越省空间(最好50字节之内)
key = username0000 value =strs 
...
key = username9999 value =strs 
  • 以上可重构为10组hash key,每组1000个field
key = username0 field = 000 value = str ... field =999 value =str 
...
key = username9 field = 000 value = str ... field =999 value =str 
  • 对于只含可计算的field的Hash:
    • 也可以使用分组hash:以下,每100个用户ID共享一个hash key
      • key=userId/100, field1=userId%100, field1Value=str, field2=userId%100, field2Value=str, ...
      • 即:userId为1~100的全部用户的userId-value键值对都存储在key=0的field-value中,而101~200则存在key=1中,......
相关文章
相关标签/搜索