Redis

Posted by 淦 Blog on March 4, 2024

数据结构

String类型

  • 当保存64位有符号整数时,String类型会把它保存为一个8字节的Long类型整数,这种保存方式通常也叫作int编码方式。
  • 当保存的数据中包含字符时,String类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存。

  1. buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis会自动在数组最后加一个”\0”,这就会额外占用1个字节的开销。
  2. len:占4个字节,表示buf的已用长度。
  3. alloc:也占个4字节,表示buf的实际分配长度,一般大于len。

除了SDS的额外开销,还有一个来自于RedisObject结构体(统一记录元数据,同时指向实际数据)。

为了节省内存空间,Redis还对Long类型整数和SDS的内存布局做了专门的设计。

  • 当保存的是Long类型整数时,RedisObject中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。
  • 当保存的是字符串数据,并且字符串小于等于44字节时,RedisObject中的元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为embstr编码方式。
  • 当字符串大于44字节时,SDS的数据量就开始变多了,Redis就不再把SDS和RedisObject布局在一起了,而是会给SDS分配独立的空间,并用指针指向SDS结构。这种布局方式被称为raw编码模式。

Redis会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个dictEntry的结构体,用来指向一个键值对。dictEntry结构中有三个8字节的指针,分别指向key、value以及下一个dictEntry,三个指针共24字节。

Redis使用的内存分配库jemalloc,jemalloc在分配内存时,会根据申请的字节数N,找一个比N大,但是最接近N的2的幂次数作为分配的空间,这样可以减少频繁分配的次数。

压缩列表(ziplist)

类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段zlbytes、zltail和zllen,分别表示列表长度、列表尾的偏移量和列表中的entry个数;压缩列表在表尾还有一个zlend,表示列表结束。

每个entry的元数据包括下面几部分。

  • prev_len:表示前一个entry的长度。prev_len有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节。
  • len:表示自身长度,4字节;
  • encoding:表示编码方式,1字节;
  • key:保存实际数据。

同时满足下面两个条件,采用压缩列表的方式实现:

  • 保存的单个数据(有可能是字符串类型的)小于64字节;
  • 数据个数针对List和Hash少于512个,针对SortSet少于128个。

跳表

在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位(时间复杂度logn)。

集合set

采用有序数组条件:

  • 存储的数据都是整数;
  • 存储的数据元素个数不超过512个。

持久化

AOF

AOF里记录的是Redis收到的每一条命令,这些命令是以文本形式保存的。

三种写回策略

重写机制

Redis根据数据库的现状创建一个新的AOF文件,即读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。

  • “一个拷贝”:每次执行重写时,主线程fork出后台的bgrewriteaof子进程。此时,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
  • “两处日志”:如果有写操作,第一处日志就是指正在使用的AOF日志,Redis会把这个操作写到它的缓冲区。即使宕机了,这个AOF日志的操作仍然是齐全的,可以用于恢复。而第二处日志,就是指新的AOF重写日志。这个操作也会被写到重写日志的缓冲区,也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的AOF文件,以保证数据库最新状态的记录。此时,就可以用新的AOF文件替代旧文件了。

内存快照RBD

内存中的数据在某一个时刻的状态记录。即把某一时刻的状态以文件的形式写到磁盘上,也就是快照。

Redis的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。

Redis提供了两个命令来生成RDB文件,分别是save和bgsave。

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置。

Redis借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

即bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件。

此时,如果主线程对这些数据也都是读操作(例如图中的键值对A),那么,主线程和bgsave子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave子进程会把这个副本数据写入RDB文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

集群模式

哨兵模式

哨兵是一个运行在特殊模式下的Redis进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。

  • 监控。哨兵进程在运行时,周期性地给所有的主从库发送PING命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的PING命令,哨兵就会判定主库“主管下线”。只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,然后开始自动切换主库的流程。
  • 选主。主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则(网络连接状态、优先级、复制进度和ID号小)选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库。
  • 通知。在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。

缓存

缓存淘汰机制

  • 在设置了过期时间的数据中进行淘汰,包括volatile-random、volatile-ttl、volatile-lru、volatile-lfu(Redis 4.0后新增)四种。
  • 在所有数据范围内进行淘汰,包括allkeys-lru、allkeys-random、allkeys-lfu(Redis 4.0后新增)三种。

注:

  • LRU(Least Recently Used):淘汰最长时间未被使用。
  • LFU(Least Frequently Used):淘汰一定时期内被访问次数最少。

数据库与缓存一致性

缓存问题

数据类型使用

用集合类型保存单值的键值对

以终端ID 1101000060和任务ID 3302000080为例,把终端ID的前8位(11010000)作为Hash类型的键,把终端ID的最后2位(60)和图片存储对象ID分别作为Hash类型值中的key和value。

在使用String类型时,每个记录需要消耗64字节,这种方式却只用了16字节,所使用的内存空间是原来的1/4,满足了节省内存空间的需求。

Hash类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash类型就会用哈希表来保存数据了。

  • hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数,默认512。
  • hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大大小,默认64字节。

如果往Hash集合中写入的元素个数超过了hash-max-ziplist-entries,或者写入的单个元素大小超过了hash-max-ziplist-value,Redis就会自动把Hash类型的实现结构由压缩列表转为哈希表。一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存,而不会再转回压缩列表了。在节省内存空间方面,哈希表就没有压缩列表那么高效了。

为了能充分使用压缩列表的精简内存布局,一般要控制保存在Hash集合中的元素个数。所以,在刚才的二级编码中,只用图片ID最后2位作为Hash集合的key,也就保证了Hash集合的元素个数不超过512。

Hash

Redis hash 是一个string类型的field(字段)和value(值)的映射表,hash特别适合用于存储对象。 Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。

1
HMSET $TASK_ID name "redis tutorial" description "redis basic commands for caching" likes 20 visitors 23000

List

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。

twitter、新浪微博、腾讯微博中个人用户的关注列表需要按照用户的关注顺序进行展示,粉丝列表需要将最近关注的粉丝列在前面。

新闻、资讯类网站如何将最新的新闻或资讯按照发生的时间顺序展示。

Set

Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 集合对象的编码可以是intset或者hashtable。Redis中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

1
SADD runoobkey redis

redis set是集合类型的数据结构,那么集合类型就比较适合用于聚合分类。

  • 标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。
  • 共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。
  • 统计网站的独立IP。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。

Sorted Set

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

1
2
ZADD runoobkey 1 redis
ZADD runoobkey 2 mongodb

排行榜