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如何读取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常见面试问题
redis和mamcached的比较
Memcached与Redis的区别和选择 1、存储方式:
Memcached 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小 Redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(笔者注:有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。
2、数据支持类型:
Redis在数据支持上要比Memcached多的多。
3、使用底层模型不同:
新版本的Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、运行环境不同:
Redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统 环境上的优化,虽然后来微软有一个小组为其写了补丁。但是没有放到主干上