String
1.String是什么?
String就是字符串,最大为512MB。
2.String怎么用?
适用存储字节数据、文本数据、序列化后的对象数据等。
缓存场景,Value存Json字符串等信息。
计数场景,因为Redis处理命令是单线程,所以执行命令的过程时原子的。因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等。
3.常用操作
创建、查询、更新、删除。
- SET 写操作(创建、更新)
SET key value [EX seconds] [PX milliseconds] [NX|XX]
参数:
- EX second:设置键的过期时间为多少秒。
- PX millisecond:设置键的过期时间为多少毫秒。
- NX:只在键不存在时,才对键进行设置操作。SET key value NX等同于SETNX key value。
- XX:只在键存在时,才对键操作。
示例:
SET user:1 "Alice" EX 60 # 设置并在 60 秒后过期
SET counter 100 NX # 仅在 counter 不存在时设置值
- GET 读操作
GET key
示例:
GET user:1 # 获取 user:1 的值
3. MGET 读操作
MGET key1 key2 ... keyN
示例:
MGET user:1 user:2 user:3 # 获取多个键的值
4. SETNX 写操作
SETNX key value
示例:
SETNX user:1 "Alice" # 只有在 user:1 不存在时设置其值
5. SETEX 写操作
SETEX key seconds value
- 设置
key的值并设置过期时间(秒)。
示例:
SETEX session:12345 3600 "user_token" # 设置并在 3600 秒后过期
4.底层实现?
String有三种编码方式:
- INT编码:就是存一个整型,可以用long表示的整数就以这种编码存储。
- EMBSTR编码:如果字符串小于等于阈值字节,使用EMBSTR编码。
- RAW编码:如果字符串大于阈值字节,则用RAW编码。
redis3.0-4.0阈值是39字节,redis5.0是44字节。
EMBSTR和RAW都是由redisObject和SDS两个结构组成,它们的差异在于,EMBSTR下redisObject和SDS是连续的内存,RAW编码下redisObject和SDS内存是分开的。
EMBSTR优点是redisObject和SDS两个结构可以一次性分配空间,缺点在于如果重新分配空间,整体都需要再分配,所以EMBSTR设计为只读,任何写操作之后EMBSTR都会变成RAW,理念是发生过修改的字符串通常会认为是易变的。
我们注意到,EMBSTR和RAW里都有一个叫SDS的结构,那么它是什么呢?
1.增加长度字段len,快速返回长度;
2.增加空余空间alloc-len,为后续追加数据留余地;
3.不再以'\0'作为判断标准,二进制安全。
List
1.简介
Redis List是一组连接起来的字符串集合。
List最大的元素个数是2的32次方-1,新版本4.0之后是64次方-1。
2.使用场景
List作为一个列表存储,属于比较底层的数据结构,比如存储一批任务数据、存储一批消息等。
3.常用操作
- 创建:LPUSH、RPUSH
- 查询:LLEN、LRANGE
- 更新:LPUSH、RPUSH、LPOP、RPOP、LREN
- 删除:DEL、UNLINK
1.写操作
1、LPUSH从左侧插入,RPSH从右侧插入。
2、LPOP移出并获取列表的第一个元素;RPOP移出并获取列表最后一个元素。
3、LREN key count value,移出值等于value的元素,当count=0,则移出素有等于value的元素,当count>0,则从左到右移出count个,当count<0,则从右到左移出count个。返回被移除元素的数量。
4、DEL和UNLINK,DEL是同步删除命令,UNLINK是异步删除命令不会阻塞客户端。
2.读操作
1、LLEN,查看list的长度。
2、LRANGE,查看从start到stop的元素。
4.底层实现
3.2版本之前,List对象有两种编码方式,ZIPLIST和LINKEDLIST。
ZIPLIST使用条件:
- 列表对象保存的所有字符串对象长度都小于64字节。
- 列表对象元素个数少于512个。
LinkedList:
ZIPLIST和LInkedList相比,ZIPLIST内存更加紧凑,所以只有在列表个数或节点数据比较大的时候,才会使用到LINKEDLIST编码。
所以,ZIPLIST是为了在数据较少时节约内存,LInkedList是为了数据多时提高更新效率,而ZIPLIST数据稍多时会导致很多内存复制。
后来,引入了QUICKLIST,其实就是ZIPLIST和LInkedLIST的结合体。
原来LInkedList是单个节点,只能存一个数据,现在单个节点存的是一个ZIPLIST,即多个数据。
5.压缩列表的优化
平常说的压缩列表一般是指ZIPLIST,一种是LISTPACK 5.0引入的,直到7.0完全替代了ZIPLIST。
1.ZIPLIST结构
- zlbytes:占用4个字节,记录了整个ziplist占用的总字节数。
- zltail:占用4个字节,指向最后一个entry偏移量,用于快速定位最后一个entry。
- zllen:占用2字节,记录entry总数。
- entry:列表元素。
- zlend:ziplist结束标志,占用1字节,值等于255。
ziplist节点结构
<prevlen> <encoding> <entry-data>
prevlen:表示上一个节点的数据长度。如果前一节点的长度,也就是entry的大小 小于254字节,那么prevlen需要用1字节长的空间来保存这个长度值,255是特殊字符,被zlend占用了。
encoding:编码类型,包含了一个entry的长度信息,可用于正向遍历。
entry-data:实际的数据。
2.ziplist更新数据
更新操作可能带来连锁更新。连锁更新是指这个后移,发生了不止一次,而是多次。
什么是连锁更新?
比如增加一个头部新节点,后面依赖它的节点,需要prevlen记录它的大小,原本只用1字节记录,因为更新可能膨胀为5字节,然后这个entry的大小就也膨胀了。所以,当这个新数据插入导致的后移完成之后,还需要逐步迭代更新。
3.LISTPACK优化
ziplist需要支持LIST,LIST是双端访问的结构,所以需要能从后向前遍历。
<prevlen> <encoding> <entry-data>
其中,prevlen就表示上一个节点的数据长度,通过这个字段可以定位上一个节点的数据。
那么,我们可不可以改为不记录这个prevlen,但是又能找到上一个节点的起始位置的办法?
<encoding-type> <element-data> <element-tot-len>
encoding-type:是编码类型;element-datt:是数据内容;element-tot-len:存储整个节点除它自身的长度。
要找到上一个节点的秘密就需要element-tot-len。
element-tot-len所占用的每个字节的第一个bit用于标识是否结束。0是结束,1是继续,剩下7个bit来存储数据大小。
当我们需要找到当前元素的上一个元素时,我们可以从后向前依次查找每个字节,找到上一个Entry的element-tot-len字段的结束标识。
Set
1.SET是什么?
Redis的Set是一个不重复、无序的字符串集合。
2.适用场景
适用于无序集合场景,比如某个用户关注了哪些公众号,这些信息就可以放进一个集合,Set提供了查交集、并集的功能,可以很方便地实现共同关注的功能。
3.常用操作
- 创建:SADD
- 查询:SISMEMBR、SCARD、SMEMBERS、SSCAM、SINTER、SUNION、SDIFF
- 更新:SADD、SREM
- 删除:DEL
1.写操作
1、SADD,添加元素,返回成功添加了几个元素。
2、SREM,删除元素,返回值为成功删除了几个元素。
2.读操作
1、SISMEMBER,查询元素是否存在。
2、SCARD,查询集合元素个数。可以查看成员个数:SCARD key
3、SMEMBERS,查看集合的所有元素。可以查看所有成员:SMEMBERS set1
4、SSCAN
5、SINTER,返回在第一个集合里,同时在后面所有集合都存在的元素。A交B,交集。
6、SUNION,返回第一个集合里有,且在后续集合中不存在的元素。并集。
4.底层实现
Set对象编码方式:INTSET、HASHTABLE。
INSET编码
如果集合元素都是整数,且元素数量不超过512个,就可以用INTSET编码。INTSET编码排列比较紧凑,内存占用少,但是查询时需要二分查找(INSET下是有序的)。
HASHTABLE
如果不满足INTSET的条件,就需要用HASHTABLE。
Hash
1.hash是什么?
Redis Hash是一个field、value都为string的hash表,存储在Redis的内存中。
2.适用场景
适用于O(1)时间字典查找某个field对应数据的场景,比如任务信息的配置,就可以任务类型为field,任务配置参数为value。
3.常用操作
- 创建:HSET、HSETNX
- 查询:HGETALL、HGET、HLEN、HSCAN
- 更新:HSET、HSETNX、HDEL
- 删除:DEL
1.写操作
1、HSET,为集合对应field设置value数据。字段+值。
HSET key field value [field value ...]
2、HSETNX,如果field不存在,则为集合对应设置value数据。如果存在则不设置。
3、HDEL,删除指定字段field,可以一次删除多个。
4、DEL,删除Hash对象。
5、HMSET,可以设置多个键值对。在Redis4.0之前,HSET只能设置单个键值对,4.0之后,弃用HMSET,改用HSET。
2.读操作
1、HGETALL,查找全部数据。
2、HGET,查询field对应的value。
3、HLEN,查找Hash中元素总数。
4、HSCAN,从指定位置查询一定数量的数据。
4.原理
Hash底层有两种编码结构,压缩列表和HASHTABLE。同时满足以下两个条件,用压缩列表:
- Hash对象保存的所有值和键的长度都小于64字节;
- Hash对象元素个数少于512个。
两个条件任何一条不满足,编码结构就用HASHTABLE。
ZIPLIST其实就是在数据量小的时候将数据紧凑排列,对应到Hash,就是将field-value当做entry放入ZIPLIST。查找key的时间复杂度O(N)。
HASHTABLE在之前无序集合SET中也有应用,区别就是,在SET中value始终为null,但是Hash中是有对应的值。查找key的时间复杂度O(1)。
5.总结
1、Hash的编码方式是什么?
一个是ZIPLIST,一个是HASHTABLE。ZIPLIST适用于元素较少且单个元素长度较小的情况,其他情况使用HASHTABLE。
2、HASH为什么要用两种编码方式?
采用两种编码方式的原因是ZIPLIST更节约内存,所以在小数据量使用,而数据多时,需要使用HASHTABLE提高更高的查找、更新性能。
HASTABLE
别,这个模块还没结束呢。学了SET和HASH之后,我们都见到了底层有一个叫HASHTABLE的结构,接下来就去探究一下这是个啥。
1.HASHTABLE简述
简单点说,就是哈希表。那么有什么用呢?
就好比一本书,如果让你一页一页去找是不是很麻烦,要是有一个目录可以直接根据关键字就能定位,是不是效率就更高了。
2.HASHTABLE结构
// redis 5.0.5
typedef struct dictht {
dictEntry **table; /* 哈希桶数组,指向实际的hash存储 */
unsigned long size; /* 哈希表大小(桶数) */
unsigned long sizemask; /* 哈希表大小掩码 */
unsigned long used; /* 哈希表已使用的桶数量 */
} dictht;
3.渐进式扩容,缩容
// redis 5.0.5
typedef struct dict {
dictht ht[2]; /* 目前使用的两个哈希表(用于rehash) */
dictType *type; /* 数据类型 */
void *privdata; /* 私有数据(通常为 NULL),保存需要传给那些类型特定函数的可选参数*/
long rehashidx; /* 正在进行的 rehash 操作的桶索引 */
unsigned long iterators; /* 迭代器数量 */
} dict;
为了实现渐进式扩容,redis没有直接把dictht暴露给上层,而是再封装一层,如上。
可以看到dict结构里面,包含了两个dictht结构,也就是两个HASHTABLE结构。dictEntry是链表结构,也就是用拉链法解决哈希冲突,用的头插法。
实际上平时用的都是一个HASHTABLE,在触发扩容之后,就会两个HASHTABLE同时使用,以下是详细流程:
- 首先,为新Hash表ht[1]分配空间。新表大小为第一个大于等于原表2倍used(已使用的桶数量)的2次方幂。然后迁移ht[0]数据到ht[1]。在ReHash(是指重新计算键的哈希值和索引值)进行期间,每次对字典执行增删改查操作,程序会顺带迁移当前rehashidx在ht[0]上对应的数据,并更新偏移索引。同时,部分情况周期函数也会进行迁移。(这里解释一下这个rehashidx是什么意思:字典同时是拥有ht[0]和ht[1],将rehashidx设置为0,表示rehash开始;在rehash期间,每次对字典crud,会顺带将ht[0]哈希表在rehashidx索引上的所有kv rehash到ht[1],当rehash完成后rehashidx+1;随着字典不断操作,最终ht[0]所有键值都会被rehash到ht[1],这时将rehashidx设置为-1,表示操作结束。注意:在渐进式rehash的过程,如果有crud,如果index大于rehashidx,访问ht[0],否则访问ht[1]。)
- 然后,随着字典不断执行,最终在某个时间点上,ht[0]的所有键值都会被Rehash至ht[1],此时再将ht[1]和ht[0]指针对象互换,同时把偏移索引rehashidx的值设为-1,表示Rehash已完成。
既然知道了扩容的流程,那么扩容时机是什么时候呢?
redis会根据负载因子的情况来扩容:
- 负载因子大于等于1,说明此时空间已经非常紧张。
- 负载因子大于5,此时即使有复制命令,也要进行Rehash扩容。
负载因子:k=ht[0].used / ht[0].size
如果扩容太大,但是数据已经减少了,就需要进行缩容,缩容也是渐进式的。那么什么时机缩容呢?
当负载因子小于0.1,即负载率小于10%,此时进行缩容,新表大小为第一个等于原表used的2次方幂。
总之,ZIPLIST、HASHTABLE面试超级热点,不仅学习这些大致思路,还要掌握一些细节。
Sorted SET
跳表
1.跳表是什么?
跳表是Redis有序集合ZSet底层的数据结构,跳表在ZSET中尤其重要。
跳表的本质还是链表,只是在普通链表的基础上,增加了多级的索引,通过索引可以一次实现多个节点的跳跃,提高性能。
跳表的结构
标准的跳表(Redis不是使用标准的跳表)有以下限制:
- score值不能重复;
- 只有向前指针,没有回退指针。
2.Redis的跳表实现
Redis跳表单个节点有几层?
层次的决定,需要比较随机,Redis是使用概率均衡的思路来确定新插入节点的层数。
Redis跳表决定每一个节点,是否能增加一层的概率为25%,而最大层数限制在Redis5.0是64层,Redis7.0是32层。
Redis跳表优化了多少?
O(N)降低到log(N)。
ZSET
1.ZSET是什么?
ZSET就是有序集合,也叫SORTED SET,是一组按关联积分有序的字符串集合,这里的分数是个抽象概念,任何指标都可以抽象为分数,以满足不同场景。积分相同的情况下,按字典序排序。
2.适用场景
用于需要排序集合的场景,最为典型的就是游戏排行榜。
3.常用操作
- 创建:ZADD
- 查询:ZRANGE、ZCOUNT、ZRANK、ZCARD、ZSCORE
- 更新:ZADD、ZREN
- 删除:DEL、UNLINK
1.写操作
1、ZADD key scoremember [score member ...]
向ZSET增加数据,如果key已经存在,则更新对应数据。
扩展参数:
- XX:仅更新存在的成员,不添加新成员。
- NX:不更新存在的成员,只添加新成员。
- LT:更新新的分值比当前分值小的成员,不存在则新增。
- GT:更新新的分值比当前分值大的成员,不存在则新增。
2、ZREM key member[member ...] ,删除ZSET中的元素。
2.读操作
1、ZCARD key,查看成员总数。
2、ZRANGE key start stop,查看从start到stop范围的ZSET数据。
3、ZREVRANGE key start stop,从大到小遍历。
4、ZCOUNT key min max,计算min-max积分范围的成员个数。
5、ZRANK key member,查看ZSET中的member的排名索引。
6、ZSCORE key member,查询ZSET中成员的分数。
4.底层实现
ZSET编码有两种方式,一种是ZIPLIST,另一种是SKIPLIST+HASHTABLE。
ZIPLIST编码的使用条件:
- 列表对象保存的所有字符串对象长度都小于64字节。
- 列表对象元素个数少于128个。
若有一条不满足,编码就使用SKIPLIST+HASHTABLE。
SKIPLIST是一种可以快速查找的多级链表结构。并且还使用HASHTABLE来配合查询O(1)。
完结撒花