Redis学习-内存优化

以下为个人学习Redis的备忘录--内存优化,基于Redis4.0.2

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-16、int-32、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中,......

posted on 2017-11-21 20:35  SuriFuture  阅读(9388)  评论(0编辑  收藏  举报

导航