深入Redis内存模型2:对象
1、对象
Redis构建了自己的类型系统,主要包括:
- redisObject对象
- 基于redisObject对象的类型检查
- 基于redisObject对象的显示多态函数
- 对redisObject进行分配、共享和销毁的机制
- type key :判断对象的数据类型
- prt指针指向对象底层的数据结构,而数据结构由encoding属性来决定
- 每种类型的对象至少使用了两种不同的编码,而这些编码对用户是完全透明的。
- 一种命令可以对任何类型的键执行,比如说del命令、expire命令、rename命令、type命令、object命令等,
- 另一种命令只能对特定类型的键执行。
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增一;
- 当对象不再被一个程序使用时,它的引用计数值会被减一;
- 当对象的引用计数值变为0时,对象所占用的内存会被释放。
- 将数据库键的值指针指向一个现有的值对象;
- 将被共享的值对象的引用计数增一。
- Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
- 可以使用OBJECT REFCOUNT命令查看键A的值对象的引用计数。
- 这些共享对象不单单只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象,以及zset编码的有序集合对象)都可以使用这些共享对象。
- OBJECT IDLETIME命令可以打印出键的空转时长。
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。
- set:设置key对应的值为string类型的value (多次set name会覆盖)
- get:获取key对应的值
- mset 批量设置多个key的值,如果成功表示所有值都被设置,否则返回0表示没有任何值被设置
- mget批量获取多个key的值,如果不存在则返回null
- incr对key对应的值进行加加操作,并返回新的值,incrby加指定的值
- decr对key对应的值进行减减操做,并返回新的值,decrby减指定的值
- 设置Key对应的值为string类型的值,如果已经存在则返回0
- 设置key对应的值为string类型的value,并设定有效期
- setrange从指定位置替换字符串
- getrange获取key对应value子字符串
- 列表对象保存的所有字符串元素的长度都小于 64 字节;
- 列表对象保存的元素数量小于 512 个;
- list-max-ziplist-value、list-max-ziplist-entries
- 列表对象包含的元素比较少时,Redis使用压缩列表作为底层实现,因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到缓存中。
- 随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、更适合保存大量元素的双端链表。
- lpush: 从头部加入元素
- rpush:从尾部加入元素
- lpop: 从list的头部删除元素,并返回删除的元素
- rpop:从list的尾部删除元素,并返回删除的元素
- rpoplpush: 第一步从尾部删除元素,第二步从首部插入元素 结合着使用
- linsert :插入方法 linsert listname before [集合的元素] [插入的元素]
- lindex: 返回list中指定位置的元素
- llen: 返回list中的元素的个数
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值
- 哈希对象保存的所有键值对的键和值字符串长度都小于64字节。
- 哈希对象保存的键值对数量小于512个。
- hash-max-ziplist-value、hash-max-ziplist-entries
- hset/hget
- hmset/hmget
- hsetnx/hgetnx
- hincrby/hdecrby
- 集合对象保存的所有元素都是整数值。
- 集合对象保存的元素数量不超过512个。
- set-max-intset-entries
- sadd: 向集合中添加元素 (set不允许元素重复)
- smembers: 查看集合中的元素
- srem: 删除集合元素
- spop: 随机返回删除的key
- sdiff :返回两个集合的不同元素 (哪个集合在前就以哪个集合为标准)
- sinter: 返回两个集合的交集
- sinterstore: 返回交集结果,存入目标集合
- sunion: 取两个集合的并集
- sunionstore: 取两个集合的并集,并存入目标集合
- smove: 将一个集合中的元素移动到另一个集合中
- scard: 返回集合中的元素个数
- sismember: 判断某元素是否存在某集合中,0代表否 1代表是
- srandmember: 随机返回一个元素
- zsl 跳跃表按分值从小到大保存了所有集合元素, 每个跳跃表节点都保存了一个集合元素:跳跃表节点的 object 属性保存了元素的成员,score 属性则保存了元素的分值。 通过这个跳跃表,程序可以对有序集合进行范围型操作,比如 ZRANK 、 ZRANGE 等命令。
- dict 字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,字典的值则保存了元素的分值。通过这个字典,程序可以用 O(1) 复杂度查找给定成员的分值,如 ZSCORE 命令。
- 有序集合每个元素的成员都是一个字符串对象, 而每个元素的分值都是一个 double 类型的浮点数。
- 有序集合保存的元素数量小于128个。
- 有序集合保存的所有元素成员的长度都小于64字节。
- zset-max-ziplist-entries、zset-max-ziplist-value。
- 如果只使用字典来实现有序集合, 那么虽然以 O(1) 复杂度查找成员的分值这一特性会被保留, 但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作 —— 比如 ZRANK 、 ZRANGE 等命令时, 程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少 O(N log N) 时间复杂度,以及额外的 O(N) 内存空间(因为要创建一个数组来保存排序后的元素)。
- 如果只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从 O(1) 上升为 O(log N) 。
- 值得一提的是,虽然 zset 结构同时使用跳跃表和字典来保存有序集合元素, 但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
- zrem: 删除集合中名称为key的元素member
- zincrby: 以指定值去自动递增
- zcard: 查看元素集合的个数
- zcount: 返回score在给定区间中的数量
- zrangebyscore: 找到指定区间范围的数据进行返回
- 客户端缓冲,指的是所有接入到redis服务器TCP连接的输入输出缓冲,输入缓冲无法控制,最大空间为1G,如果超过将断开连接,输出缓冲区通过client-output-buffer-limit控制。
- 复制积压缓冲区,redis2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,根据repl-backlog-size参数控制,默认是1M。
- AOF缓冲区,这部分空间用于在redis重写期间保存最近写入命令,此缓冲区空间消耗用户无法控制。
- 惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空。但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
- 定时任务删除:Redis内部维护一个定时任务,默认每秒运行10次。定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒。如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
- noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时Redis只响应读操作。
- volatile-lru:根据LRU算法删除设置了超时属性的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
- allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
- allkeys-random:随机删除所有键,直到腾出足够空间为止。
- volatile-random:随机删除过期键,直到腾出足够空间为止。
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
- 可以使用scan + object idletime命令批量查询哪些键长时间未被访问,找出长时间不访问的健进行清理,可降低内存占用。
- 高并发写入场景中,在条件允许的情况下,建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数从而提高性能。
- 缩小键值长度,在设计key时,在完整描述业务情况下,键值越短越好。
- 共享对象池,Redis内部维护[0-9999]的整数对象池以节省内存。每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。
- 第一次创建len属性等于数据实际大小,free等于0,不做预分配。
- 修改后如果已有free空间不够且数据小于1M,每次预分配一倍容量。
- 修改后如果已有free空间不够且数据大于1MB,每次预分配1MB数据。
- ziplist在hash、list、zset内存和速度测试
- 使用ziplist可以分别作为hash,list,zset数据类型实现。
- 使用ziplist编码类型可以大幅降低内存占用。
- ziplist实现的数据类型相比原生结构,命令操作更加耗时,不同类型耗时排序:list < hash < zset。
- 使用intset编码的集合时,要防止个别大整数触发集合升级操作,产生内存浪费。