今天是个好天气 logo 今天是个好天气
  • Home
  • Go
  • MySQL
  • Redis
  • LeetCode
  • Hello World
↩️README 数据结构 Redis是怎么运行的 持久化 缓存过期与内存淘汰 集群模式 缓存 分布式锁 事务机制 Hot Key / Big Key

数据结构

关键点

常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。

支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。

  • 每种数据对象都各自的应用场景,说出它们各自的应用场景
  • 针对具体的应用场景,使用哪种Redis数据类型来实现
  • 掌握特性、实现、时间复杂度、各种操作
  • 超热点:跳表、字典

  • 采用多种底层实现编码的角度
    • 数据量小的情况,节省内存
    • 数据量大的情况,提高查找性能
    • 不同编码方式,查找的效率,对应到应用场景问题

对象

Redis是key-value存储,key和value在Redis中都被抽象为对象,key只能是String对象,而Value支持丰富的对象种类。

Object在内存当中包括:

  • type:是哪种Redis对象
  • encoding:表示用哪种底层编码,用OBJECT ENCODING[key]可以看到对应的编码方式
  • lru:记录对象访问信息,用于内存淘汰
  • refcount:引用计数,用来描述有多少个指针,指向该对象
  • ptr:内容指针,指向实际内容

String

是什么

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512MB。

适用场景:一般用来存字节数据、文本数据、序列化后的对象数据等。

使用

  • 常用操作聚焦于创建、查询、更新和删除
  • 创建:SET name lin,MSET key1 value1 key2 value2 ,SETNX key value
  • 查询:GET name,MGET key1 key2 ,STRELEN name,TTL name
  • 更新:INCR number,EXPIRE name 60
  • 删除:DEL name

内部实现

采用不同编码方式来应对不同的场景,以达到高性能。

包含了三种编码方式:

  • INT:存放可以用long表示的整数
  • EMBSTR:字符串小于等于阈值字节,32 字节(redis 2.+版本)
  • RAW:字符串大于阈值字节

其中EMBSTR和RAW由redisObject和**SDS(Simple Dynamic String)**构成,EMBSTR下的redisObject和SDS是连续的内存,而RAW是离散的。

  • EMBSTR可以一次性分配空间,连续分布的。

    • 创建字符串对象所需的内存分配次数就只需要一次,而raw需要两次
    • 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数,连续分布的
    • 所有数据保存在一块连续的内存里面可以更好的利用CPU缓存提升性能
    • 缺点是在修改后需要整体重新分配空间,会先将其转换为raw格式,再执行修改命令
  • 因此EMBSTR在发生写操作后,会变成RAW,默认当作发生过修改的字符串通常是易变的。

  • 编码可能会发生转换

    • INT->RAW:当存的内容不再是整数,或者大小超过了long的时候。
    • EMBSTR->RAW:任何写操作之后EMBSTR都会变成RAW,原因前面有解释。

为什么使用SDS

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。

    • 使用 len 属性的值而不是空字符来判断字符串是否结束
    • SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据
    • SDS 能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据
  • SDS 获取字符串长度的时间复杂度是 O(1):增加了len长度字段,原生C是O(n)

  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出:增加了空余空间(alloc - len),有了预留空间,节约性能。SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

应用场景

  • 缓存对象

  • 常规计数

  • 分布式锁

List

是什么

  • Redis List是一组连接起来的字符串集合,按照插入顺序排序,可以从头部和尾部向List列表添加元素。
  • List的最大元素个数是2^64 - 1。
  • 作为一个列表存储,属于比较底层的数据结构,可以用于存储一批任务数据、存储一批消息等。

使用

# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...] 
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key     
# 移除并返回key列表的尾元素
RPOP key 

# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop

# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout

内部实现

主要包括两种编码方式,ZIPLIST和LINKEDLIST。

当满足如下条件时,用ZIPLIST(压缩列表)编码

  • 列表对象保存的所有字符串对象长度都小于64字节(默认值,可由 list-max-ziplist-value 配置)
  • 列表对象元素个数少于512个,注意,这是LIST的限制,而不是ZIPLIST的限制

ZIPLIST底层用压缩列表实现,相邻元素紧凑压缩在一起,可以有效节约内存空间。

当不满足ZIPLIST编码条件,则使用LINKEDLIST(双向链表)编码,是几个STRING对象的链接结构。

  • 通过链表形式,便于删除和插入
  • 列表个数或者节点数据长度比较大的时候,使用LINKEDLIST编码可以加快处理(增长,删除)

ZIPLIST是为了在数据较少情况时节约内存,LINKEDLIST是为了数据多时提高更新效率。

为了优化解决上述问题,引入了QUICKLIST,二者的结合体,在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

  • LINKEDLIST原来是单个节点,只能存一个数据,现在单个节点存的是一个ZIPLIST,存放了多个数据。

  • 当数据较少,QUICKLIST节点只有一个时,此时相当于就是一个ZIPLIST。

ZIPLIST优化:

Redis7.0使用LISTPACK(紧凑列表)的编码模式取代了ZIPLIST,本质上都是一种压缩列表。同时在LINKEDLIST中也用LISTPACK替代了ZIPLIST。

LISTPACK是为了解决ZIPLIST连锁更新问题

  • ZIPLIST连锁更新是什么,如何造成的
    • 当前一个节点大小超过254,会导致后面一个节点中prev值变化膨胀,接连后续变化,发生连锁更新
  • LISTPACK是如何解决的
    • 引入一个不记录prevlen的变量,element-tot-len存储整个节点除它自身以外的长度,从这个节点的末尾再遍历element-tot-len长度,就能找到这个节点的开始位置。

消息队列

消息队列在存储消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。

  • 消息保序:使用 LPUSH + RPOP

    • 生产者和消费者分别在两端操作。
  • 阻塞读取:使用 BRPOP

    • 由于生产者不会主动告知新消息,消费者为了及时处理消息,需要不停调用RPOP,即使没有新消息,也会不停调用RPOP,造成了性能损失。
    • 因此使用阻塞式读,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。
  • 重复消息处理

    • 生产者自行实现全局唯一 ID,list不会提供
    • 消费者对比收到消息的ID和记录的已处理过的消息ID,来判断有没有处理过
  • 消息的可靠性

    • 使用 BRPOPLPUSH,让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
  • List作为消息队列有什么缺陷

    • List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。
    • 要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。
    • 这就要说起 Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。

Hash

是什么

Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合用于存储对象。

使用

# 存储一个哈希表key的键值
HSET key field value   
# 获取哈希表key对应的field键值
HGET key field

# 在一个哈希表key中存储多个键值对
HMSET key field value [field value...] 
# 批量获取哈希表key中多个field键值
HMGET key field [field ...]       
# 删除哈希表key中的field键值
HDEL key field [field ...]    

# 返回哈希表key中field的数量
HLEN key       
# 返回哈希表key中所有的键值
HGETALL key 

# 为哈希表key中field键的值加上增量n
HINCRBY key field n    

内部实现

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

应用场景

  • 缓存对象
    • 相比String + Json类型,Hash更适合频繁变化的属性
  • 购物车
    • 用户 id 为 key,商品 id 为 field,商品数量为 value

Set

是什么

Redis的Set是一个不重复、无序的字符串集合。

一个集合最多可以存储 2^32-1 个元素。

适用于无序集合场景,比如某个用户关注了哪些公众号,这些信息可以放进一个集合当中。Set也提供了查交集、并集的功能,可以很方便地实现共同关注的能力。

使用

# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...] 
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key

# 判断member元素是否存在于集合key中
SISMEMBER key member

# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]

内部实现

如果集群元素都是整数,且元素数量不超过512个,就可以用INTSET编码(整数集合),因为INTSET编码比较紧凑,内存占用少,但是查询时使用二分查找(有序集合)。

Set是无序的,IntSet编码是有序的。

如果不满足INTSET的条件,就需要用HASHTABLE(字典),能O(1)时间就能找到一个元素是否存在。

数据指针部分,用的字典存储,能够O(1)查询,适合快速定位的场景。

Set是有序的吗

Set的底层实现是整数集合或字典,前者是有序的,后者是无序的。整体来看,建议不依赖SET的顺序。

应用场景

  • 存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储
    • Set差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。
    • 在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。
  • 点赞:Set 类型可以保证一个用户只能点一个赞,这里举例子一个场景,key 是文章id,value 是用户id。
  • 共同关注:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。

HSet

是什么

Redis Hash是一个field、value都为string的hash表,存储在Redis的内存当中。

适用于O(1)时间字典查找某个field对应数据的场景,比如任务信息的配置,就可以将任务类型设置为filed,任务配置参数为value。

使用

  • 我们还是从创建、查询、更新、删除这几个基本操作来了解HSet
  • 创建即产生一个HSet对象,可以使用HSET、HSETNX创建
  • 查询支持HGET查询单个元素;HGETALL查询所有数据;HLEN查询数据总数;HSCAN进行游标迭代查询
  • 更新的话,HSET可以用于增加新元素,HDEL删除元素
  • 至于删除,和其它对象一样,DEL可以删除一个HSet对象

内部实现

如果满足以下条件使用ZIPLIST(压缩列表):

  • HSet对象保存的所有值和键的长度都小于64字节
  • HSet对象元素少于512个

否则使用HASHTABLE。

ZSet

是什么

ZSet就是有序、不可重复集合,也叫Sorted Set,是一组按关联积分有序的字符串集合。

这里的分数是个抽象概念,任何指标都可以抽象为分数,以满足不同场景。积分相同的情况下,按字典序排序。

使用

# 往有序集合key中加入带分值元素
ZADD key score member [[score member]...]   
# 往有序集合key中删除元素
ZREM key member [member...]                 
# 返回有序集合key中元素member的分值
ZSCORE key member
# 返回有序集合key中元素个数
ZCARD key 

# 为有序集合key中元素member的分值加上increment
ZINCRBY key increment member 

# 正序获取有序集合key从start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]

# 返回有序集合中指定分数区间内的成员,分数由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]

内部实现

底层编码包括两种,ZIPLIST和SKIPLIST(跳表)+ HT。

同样在数据量比较小的时候,使用ZIPLIST

  • 列表对象保存的所有字符串对象长度都小于64字节;
  • 列表对象元素个数少于128个。

两个条件任何一条不满足,编码机构就用SKIPLIST + HT。

SKIPLIST是一种可以快速查找的多级链表结构,可以当作是一个具有高级索引的链表

应用场景

  • 排行榜
    • 有序集合比较典型的使用场景就是排行榜。例如学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。

BitMap

是什么

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

使用

# 设置值,其中value只能是 0 和 1
SETBIT key offset value

# 获取值
GETBIT key offset

# 获取指定范围内值为 1 的个数
# start 和 end 以字节为单位
BITCOUNT key start end

内部实现

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

应用场景

  • 签到统计,用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。
  • 连续签到用户总数:day1这个BitMap,使用offset作为用户ID,连续其他七个BitMap,最后将七个BitMap相与得到一个新的,统计新的BitMap就会得到连续打卡七天的用户。

HyperLogLog

是什么

  • Redis 2.8.9 版本新增的数据类型。

  • 一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。

  • 基于概率完成的,不是非常准确,标准误算率是 0.81%。

  • 提供不精确的去重计数,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。

使用

# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]

# 返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]

# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]

内部实现

难。

应用场景

  • 百万级网页 UV 计数

GEO

存储地理位置信息,并对存储的信息进行操作。

内部实现使用了Sorted Set集合。

Stream

是什么

Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。

在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。

基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

使用

Stream 消息队列操作命令:

  • XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
  • XLEN :查询消息长度;
  • XREAD:用于读取消息,可以按 ID 读取数据;
  • XDEL : 根据消息 ID 删除消息;
  • DEL :删除整个 Stream;
  • XRANGE :读取区间消息
  • XREADGROUP:按消费组形式读取消息;
  • XPENDING 和 XACK:
    • XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
    • XACK 命令用于向消息队列确认消息处理已完成;

过期时间

是什么

给定一个时间点key,到达时间,数据被认为是过期的,由Redis进行回收。

如果不是常驻的数据,设置过期时间,可以有效节约内存。

缓存设置过期时间,保证数据有效性。

使用

  • SET key value EX seconds:设置多少秒后过期
  • SET key value PX seconds:设置多少毫秒后过期
  • TTL key:查询还有多久过期
  • EXPIRE key seconds:对所有对象设置一个key的过期时间,单位秒
  • PEXPIRE key milliseconds:设置一个key的过期时间,单位毫秒

设置过期时间之后会有个字典,专门记录这些Ky和过期时间的关系。

过期删除策略

过期后删除策略包括

  • 定时删除:设置一个定时器,到期后全部删除,可能会短期删除过多
  • 定期删除:定期检查,每次删除部分过期
  • 惰性删除:待到再次访问使用时,判断过期后删除

实际中通过惰性删除 + 定期删除二者结合方式进行删除。

定期删除需要注意:

  • 定期删除的频率:取决于Redis周期任务的执行频率
  • 每次删除的数量:固定

对象引用计数

有点类似C++里面智能指针了,在redisObject结构有一个refcount字段,减少到0时,就会触发对象的释放。

注意这里目前是只有encoding为整数,并且在0-9999时才会有用,因为

  • 0 - 9999这样的对象池,被使用的概率大,复用有场景
  • 整数存储空间比较小,而分配这样的redisObject内部结构至少16字节,比本身还大,频繁分配占用过多的空间。
  • 要复用对象,包含数值比较过程,整数对象进行比较,成本最低。

最大的作用,在内部场景中用refcount进行引用计数,传递参数参数时,避免拷贝。

  1. 关键点
  2. 对象
  3. String
    1. 是什么
    2. 使用
    3. 内部实现
    4. 为什么使用SDS
    5. 应用场景
  4. List
    1. 是什么
    2. 使用
    3. 内部实现
    4. 消息队列
  5. Hash
    1. 是什么
    2. 使用
    3. 内部实现
    4. 应用场景
  6. Set
    1. 是什么
    2. 使用
    3. 内部实现
    4. 应用场景
  7. HSet
    1. 是什么
    2. 使用
    3. 内部实现
  8. ZSet
    1. 是什么
    2. 使用
    3. 内部实现
    4. 应用场景
  9. BitMap
    1. 是什么
    2. 使用
    3. 内部实现
    4. 应用场景
  10. HyperLogLog
    1. 是什么
    2. 使用
    3. 内部实现
    4. 应用场景
  11. GEO
  12. Stream
    1. 是什么
    2. 使用
  13. 过期时间
    1. 是什么
    2. 使用
    3. 过期删除策略
  14. 对象引用计数
Created by shixiaocaia | Powered by idoc
Think less and do more.