Redis 知识点

157 阅读19分钟

redis

作为key-value数据库, 所有的数据以key-value的形式, 同时在运行中, 所有的数据都是存储在内存中的, 整个过程没有磁盘操作.

key值只能为string

value值可以为string, list, hash, set, zset五种类型

每个value值都有不同的编码类型, 这里的编码类型决定了redis底层是如何实现value结构的.

redis通用的对象类型结构

typedef struct redisObject{
     //类型
     unsigned type:4;
     //编码
     unsigned encoding:4;
     //指向底层数据结构的指针
     void *ptr;
     //引用计数
     int refcount;
     //记录最后一次被程序访问的时间
     unsigned lru:22;
}

String

语法

set xiaoming 24

设置key为xiaoming的value为24

可能的编码

int 编码: 针对数字

raw 编码: 针对字符串

int编码

当value是整数值的时候, 按照整数保存, 如果是小数是按照字符串保存

raw编码

redis 实现字符串采用简单动态字符串(SDS)实现string的. 主要是因为redis里, 存在大量对字符串操作的情况.

SDS的结构

SDS结构主要属性 free: 已经为当前字符串分配的内存空间, 但是未使用的长度 len: 已使用的长度 buff: 字符串的具体内容

SDS结构的好处

(1) 常数级获取字符串的长度 因为redis里的所有key都是string类型的, 并且key可能很长, SDS结构通过存储字符串的长度, 来提高获取长度时的效率.

(2) 减少内存分配次数 通过len + free的配合 可以实现 空间预分配: 在字符串变长时, 如果free空间够用, 不会触发内存分配. 并且每次分配多分配空间, 会为free分配出空间, 作为预留 惰性空间释放: 在字符串变短时, 无需真正的缩小字符串的内存空间, 而是修改len和free的值即可

SDS有专用的API会在某些情况下释放free的空间, 不用担心空间会被一直浪费.

(3) 存储字符串的时候无需关注字符串内容 像C语言, 通过/0来区分不同的字符串, 因此无法保存图片,音频,视频,压缩文件这样的二进制数据.

而redis是通过len来区分不同字符串的, 字符串真正的内容就不会成为区分的阻碍

编码的转换

当value值是整数的时候会使用int编码, 一旦被修改成不是整数, 就会使用raw

List

redis中的list对外表现不是一个数组, 本质是一个类似java中的链表, 你可以添加元素到链表头部和尾部

语法

lpush list 0

在key为list的列表中, 头部添加元素0

可能的编码

ziplist编码(压缩列表)

linkedlist编码(双端链表)

ziplist编码

redis采用辅助结构 + 类数组(就是实现后的表现像是数组, 但是并没有直接使用数组, 而是利用更底层的数据量偏移的方式来访问下一个元素的, 不是通过索引)的形式实现ziplist编码.

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个entry可以保存一个字节数组(字符串编码)或者一个整数值(整数编码).

因为可以瞬间访问到头节点和尾节点, 因此可以用来实现list

辅助结构主要属性:

zlbytes: 压缩列表的所有字节数

zltail: 压缩列表的尾节点的相对于头节点的偏移量

zlen: 压缩列表多少个entry

linkedlist编码

redis采用辅助结构 + 链表的形式(每个节点是双向节点)实现linkedlist编码.

辅助结构主要属性:

head: 链表头节点

tail: 链表尾节点

len: 链表长度

编码的转换

满足下面两个条件的使用ziplist编码

(1) 列表保存元素个数小于512个

(2) 每个元素长度小于64字节

不能满足这两个条件的时候使用 linkedlist 编码。

上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置。

Hash

类似于java中的map

语法

hset xiaoming age 24

在key为xiaoming的字典中, 设置属性age的值为24

可能的编码

ziplist编码

hashtale编码

ziplist编码

利用ziplist实现hash的时候, 一个enrty为key, 一个entry为value, 一个键值对key-value总是在压缩列表中挨在一起

采用ziplist编码实现的hash其实根本就不是哈希, 在增删改查的时候, 都是相当于对于一个数组进行操作, 因此也就根本不存在哈希值的计算和rehash的过程.

hashtable编码

redis实现hashtable编码采用字典 + 辅助结构 + 哈希表的形式.

字典结构主要属性:

ht数组: ht是一个长度为2的数组, ht[0]对应于正常使用的辅助结构 + 哈希表, ht[1] 只有在rehash的时候才会使用

rehashIndex: 记录了rehash的进度, -1表示没有rehash, 如果在rehash过程中, 取值范围是0到 table数组的长度, 代表了已经将ht[0].table中的哪些元素rehash到了ht[1].table中

辅助结构主要属性:

table: 就是哈希表

size: table中数组的大小.

used: table数组中非null节点的个数

哈希表:

哈希表的实现类似java中map的实现, 采用数组 + 链表的形式

哈希算法

即如何根据一个元素, 找到在哈希表中数组的位置.

(1) 利用字典设置的hash计算

(2) 元素的哈希值 & 哈希表中数组的大小减去1(即table的size属性 - 1)

初始容量以及扩容的时机

初始默认hash长度为4,当元素个数与hash表长度一致时,就发生扩容

rehash操作

扩容后的大小总是为2^n次的数字

(1) 新table数组的大小, 是根据ht[0].used属性得到的

  • 扩展: 新的大小为第一个大于 ht[0].used * 2的 2的n次方的数字
  • 例如ht[0].used = 5; 那么新大小为16, 因为16 > 2 * 5;
  • 缩小: 新的大小为第一个大于 ht[0].used的 2的n次方的数字
  • 也就是redis中的rehash操作是根据table数组的具体使用大小来决定的

(2) 旧元素的拷贝

  • 将ht[0]中的元素逐渐拷贝到ht[1]中, 也就是rehash的元素拷贝并不是一次完成的, 而是对于ht[0].table中每个位置上的元素, 每次拷贝一个位置上的元素到ht[1].table.
  • 也就是第一次rehashIndex一开始为0, 然后将ht[0].table[rehashIndex]的元素, 重新计算哈希值, 拷贝到ht[1].table中. rehashIndex++
  • 第二次的时候, rehashIndex为1, 继续拷贝.
  • 在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。在这个过程中, 就是两个数组中的元素共同提供服务.

(3) 交换ht[1]和ht[0]的位置

rehash完成后, 删除ht[0], 将ht[1]赋值给ht[0].

编码的转换

  • 同时满足下面两个条件时,使用ziplist(压缩列表)编码:
  • (1) 列表保存元素个数小于512个
  • (2) 每个元素长度小于64字节
  • 不能满足这两个条件的时候使用 hashtable 编码。第一个条件可以通过配置文件中的 set-max-intset-entries 进行修改。

Set

set类似于java中的数组, 元素在set中是无序的

语法

  • SADD runoobkey redis
  • 在set key值为runoobkey的集合中, 添加redis字符串
  • SMEMBERS runoobkey
  • 返回key值为runoobkey的集合中所有元素

可能的编码

intset编码

hashtable编码

intset编码

由辅助结构 + 整型数组实现intset编码

  • 辅助结构主要属性:
  • encoding: 规定了整数数组中每个元素的数字类型, 通过编码来确定整数数组中每个位置的空间大小
  • length: 整数数组的长度
  • contents: 整数数组

数组升级

如果set集合中添加的元素超过了编码规定的长度, 则修改编码, 同时整数数组每个元素的大小全部改变

hashtable编码

所有操作遵循hash中的hashtable编码的规则, 只是hash中存的是key-value键值对, 而set中只需要保留key, 所有的value都为null.

java中的hashset也是由hashmap的结构实现的.

set集合如何实现去重

如果是intset编码实现的set, 那么在sadd的时候, 其实并没有去重复, 但是在SMEMBERS的时候, 会对返回的结果进行去重

如果是hashtable编码实现的set, 等同于hash中不能存在相同的key. (即hashcode and equals方法)

编码转换

  • 当集合同时满足以下两个条件时,使用 intset 编码:
  • (1) 集合对象中所有元素都是整数
  • (2) 集合对象所有元素数量不超过512

不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置。

Zset

Redis Zset的元素是String类型的集合,且不允许元素重复,但是每个元素必须关联一个double类型的分值,Redis就是通过分值来为集合中元素进行排序的,分值可以出现重复的值.

语法

ZADD runoobkey 1 redis

添加分数值为1 key值为redis的元素 到runoobkey这个zset集合中

可能的编码

ziplist编码

skiplist编码

ziplist编码

利用ziplist实现zset的时候, 一个enrty为value, 一个entry为分值, 一个键值对元素和分值总是在压缩列表中挨在一起

同时在添加元素的时候, 是按照分值的大小顺序添加的.

skiplist编码

跳表编码是利用辅助结构 + 跳表实现的

  • 辅助结构主要属性:
  • header: 跳表最底层的头节点
  • tail: 跳表最底层的尾节点
  • level: 跳表最底层链表的长度
  • length: 跳表的层数

跳表: 即内存版的B+树

采用多层链表实现, 上层链表只包含分数值, 最底层的链表包含所有分数值以及对应的key

length就相当于B+树的深度

level就是B+树最后一层所有叶子节点的元素个数.

链表无需维护自平衡, 每次跳转采用二分法(像B+树的自平衡就相当于已知结果的二分法)

编码的转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

  • (1) 保存的元素数量小于128;
  • (2) 保存的所有元素长度都小于64字节。
  • 不能满足上面两个条件的使用 skiplist编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。

redis value对象的一些特点

类型检查

redis 操作不同value值的对象时候, 会使用不同的命令, 因此在操作前redis需要校验命令和value对象是否匹配

多态命令

因为redis 每种value都最少有两种编码(对应两种数据结构), 因此同一个redsi 命令, redis底层会根据底层编码的不同, 转化成不同的底层命令

内存回收

redis采用引用计数法.

每个redis object都有一个引用计数属性, redis中的每个value值就是一个redis object, 当这个value属于某个key的时候, 就代表被引用了一次, 当key被删除时, 相应的value如果没有被其它key引用, 此时的引用计数就为0, 就会被内存回收.

对象共享

默认情况下0-9999这1w个数字对象(具体的数量可以设置), 在redis服务器启动的时候就会提前创建好.

当redis在需要创建这1w个对象时, 不会创建新的, 而是复用这1w个对象.

当然如果这个复用对象需要修改时, 要么重新指向一个复用对象, 要么创建一个新的在这个范围外的对象.

键的过期时间

Redis可以通过为某个键(key)设置过期时间,使得一些无用的key能定期自动释放

设置过期时间的四种方式

  • (1) EXPIRE key seconds
  • 将键key的生存时间设置为 seconds 秒后过期
  • (2) PEXPIRE key milliseconds
  • 将键key的生存时间设置为milliseconds 毫秒后过期
  • (3) EXPIREAT key timestamp
  • 这里是秒级别的时间戳
  • 将键key的生存时间设置为到达timestamp 这一时刻过期
  • (4) PEXPIREAT key milliseconds-timestamp
  • 这里是毫秒级别的时间戳
  • 将键key的生存时间设置为到达milliseconds-timestamp 这一时刻过期

redis如何实现记录过期时间

redis通过维护一个过期字典, 字典每个位置的key为对象对应的地址, value为过期时间.

redis过期策略

定时删除

每个键在设置过期时间的时候, 同时创建一个定时器, 到达过期时间后自动执行删除

优点:

(1) 能尽快释放内存

缺点:

(1) 如果设置了过期时间的键有很多, redis就会创建很多的定时器, 比较消耗资源

(2) 定时器在到期执行的时候, 会占用cpu, 因为定时器不可控, 如果在高峰期执行会降低性能

惰性删除

被设置了过期时间的key在使用的时候, 每次使用会去校验是否过期, 如果过期就删除该key.

优点

(1) 对cpu最友好, 使用的时候才去删除

缺点

(1) 如果该key一直不再使用, 就会造成内存泄漏

定期删除

算是定时删除和惰性删除的折中. 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键.

优点

(1) 即不会造成内存泄漏, 也可以尽量避开cpu使用高峰期

缺点

(1) 定期时间的设置很重要, 时间设置不合理就会退化成定势删除和惰性删除.

RDB对于过期键的处理

  • RDB文件写入:
  • 已过期的键不会写入到RDB文件中
  • 读取RDB文件:
  • 主服务器在载入RDB文件时,会检查键是否过期,并忽略过期键
  • 从服务器在载入RDB文件时,无论是否过期,都会把键载入到数据库中.

AOF对于过期键的处理

AOF文件写入:

当服务器以AOF持久化模式运行时,只有在惰性删除或者定期删除执行的时候,会向AOF文件追加一条 DEL key 命令

AOF重写时:

执行AOF重写时,会对数据库中的键进行校验,已过期的键不会重写到数据库中.

redis数据持久化

redis是内存型数据库, 每次数据的变化都是在内存中完成的. 并不会像mysql那样随时把数据修改持久化到磁盘, 但是redis同样需要持久化到手段, 不然一旦服务器断电, 数据不就全部丢失了?

RDB

通过快照的方式, 在某一时刻执行, 将redis那一时刻的所有数据生成一份文件, 持久化到磁盘. 文件的内容是redis的键值对

如何触发RDB持久化

手动执行:

(1) SAVE 命令

该命令会阻塞服务器进程,来生成RDB文件,直到RDB文件创建完毕,在服务器进程阻塞期间,服务器不能处理任何请求命令

(2) BGSAVE 命令

该命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务进程(父进程)继续处理命令请求.

自动执行:

  • 配置文件中, 可以设置自动生成RDB文件的条件, 底层调用的是BGSAVE命令, 不会阻塞进程
  • 配置文件中的save选项
  • 例如下面的配置
  • save 900 1 服务器在900秒内 对数据库执行了至少一次修改
  • save 300 10 服务器在300秒内 对数据库进行了至少10次修改
  • save 60 10000 服务器在60秒内 对数据库进行了10000次修改
  • 只要满足三条中的一个,BGSAVE命令就会立马被执行

RDB为啥是fork出一个子进程? 而不是创建一个线程

为什么 Redis 快照使用子进程

image.png

redis如何读取RDB文件

因为RDB文件存的就是键值对, 因此恢复时, 直接将数据还原到内存中即可

AOF

AOF不同于RDB保存数据库中的键值对,AOF持久化通过保存redis服务所执行的写命令来记录数据库状态的.被写入AOF文件的命令都是以Redis命令请求协议格式保存的.

AOF何时追加命令

当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器的aof_buf缓冲区的末尾

AOF何时触发将命令写入文件

redis事件循环指的是redis服务器接收到客户端的命令请求,然后向客户端发送命令回复的过程. 事件循环就是aof触发写入策略的时刻

AOF如何将命令写入文件

文件缓存区:带缓冲区文件操作:高级标准文件I/O操作,将会在用户空间中自动为正在使用的文件开辟内存缓冲区。

  • 这个过程涉及到三个部分:
  • aof_buff缓存区 -> aof文件缓存区 -> aof文件
  • A操作: aof_buff缓存区 -> aof文件缓存区 只是内存间缓存区数据的移动
  • B操作: aof文件缓存区 -> aof文件 真正将内存中的数据持久化到磁盘中

aof写入文件的策略根据配置文件中appednfsync选项来决定

  • always: 每次事件循环结束, 触发 A, B操作, 不会丢失数据, 但是性能开销大
  • everysec:每次事件循环结束, 触发 A, 每隔一秒触发B, 可能丢失这一秒内的数据
  • no: 每次事件循环结束, 触发A, 等到aof文件缓存区填满后(或者操作系统的规定, 例如linux为30s), 才触发B. 性能浪费最小, 但是丢失数据的风险很大.

redis.conf文件中如果不配置appendfsync选项,默认是everysec策略

redis如何读取AOF文件

redis在启动的时候会伪造一个客户端, 然后不断执行AOF命令, 完成redis服务器的重建

AOF重写

AOF存储了大量的命令, 这些命令有很多事可以抵消的, 因此redis会生成一份新的AOF文件, 不会修改数据库的数据, 但是新AOF文件的大小会大大减小

AOF重写的触发时机

手动触发:

bgrewriteaof指令

自动触发:

  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机
  • auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
  • auto-aof-rewrite-percentage:代表当前AOF文件空间和上一次重写后AOF文件空间的比值。

RDB和AOF混合工作(redis 4.0 之后)

之前的redis版本, 一般是RDB和AOF选择一种方式使用, 各有优势, 新版本就将两者结合.

将RDB文件内存和增量的AOF日志文件放在一起,这里的AOF日志不再是全量日志。而是自持久化开始到持久化结束的这段时间的增量日志,通常较小,重启效率因此大幅得到提升. 也就是一旦触发了RDB持久化, AOF就重新开始记录, 这样RDB和AOF就可以一直对应上, 能够配合工作

redis单线程问题

Redis系列 - 单线程的Redis为什么那么快? redis服务端在接受请求的时候, 采用I/O多路复用, 就是我们经常听到的 select/epoll 机制. 服务端会监听多个套接字, 也就能同时收到多个请求, redis收到多个请求后, 会按放到一个fifo队列中, redis内部就每次执行一个队列中弹出的元素, 因此是单线程的.

redis 6.0 多线程版本

唬人的Redis多线程,也就那么回事 redis多线程版本是针对性能瓶颈做的优化, 发现主要问题在网络请求命令的获取,解析, 和响应结果的写回socket的过程上, 因此这两个部分修改成了多线程处理(这两个部分也不会存在多线程并发的问题). 内部处理的时候, 仍然是单线程串行处理.

redis常见面试问题

mp.weixin.qq.com/s/MK91bVj1m…

redis和mamcached的比较

Memcached与Redis的区别和选择 1、存储方式:

Memcached 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小 Redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(笔者注:有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。

2、数据支持类型:

Redis在数据支持上要比Memcached多的多。

3、使用底层模型不同:

新版本的Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

4、运行环境不同:

Redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统 环境上的优化,虽然后来微软有一个小组为其写了补丁。但是没有放到主干上