Redis提高
什么是缓存
-
缓存原指CPU上的一种告诉存储器,它先于内存与CPU交换数据,访问速度很快
-
Cache Aside Pattern(旁路缓存),也是最常用的缓存读写模式
-
读的时候,先读缓存,如果没有缓存,就读数据库,然后取出数据后放入缓存,同时返回响应
-
更新缓存:更新的时候,先更新数据库,在删除缓存
为什么是删除缓存,而不是更新呢? 因为缓存的值是一个结构:hash、list,更新数据需要遍历,效率低
Redis底层数据结构
Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义
当Redis服务器初始化时,会预先分配16个数据库
所有数据库保存到结构redisServer的一个成员redisServer.db数组中 -
RedisDB结构体源码:
typedef struct redisDb {
int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long ave_ttl; //存储数据库对象的平均ttl,用于统计
dict *dict; //存储数据库所有的key-value
dict *expires; //存储key的过期时间
dict *blocking_keys; //blpop存储阻塞key和客服端对象
dict *ready_keys; //阻塞后push响应阻塞客服端,存储阻塞后push的key和客服端对象
dict *watched_keys; //存储watch监控的key和客服端对象
} redisDb
- Redis字符串对象(重点)
struct sdshdr{
//记录buf数组中已使用字节的数量
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字符数组,用于保存字符串
char buf[];
}
buf[]的长度 = len + free + 1
SDS的优势:
1.SDS在C字符串的基础上加入了free和len字段,获取字符串长度:SDS是O(1),C字符串是O(N)
2.SDS记录了长度,在可能造成缓冲区溢出时会自动分配内存,杜绝了缓冲区溢出
3.可以存取二进制数据,以字符串长度len作为结束标识,c则是以 \0 结束标识
使用场景:SDS主要用在存储字符串和整形数据、存储key、AOF缓冲区和用户输入缓冲
-
跳跃表、压缩列表、字典中有个概念叫 rehash(扩容)
- 初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。
- rehashidx=0表示要进行rehash操作。
- 新增加的数据在新的hash表h[1]
- 修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash中)
- 将老的hash表h[0]的数据重新计算索引值后全部迁移到新的hash表h[1]中,这个过程称为rehash。
渐进式rehash
当数据量巨大时rehash的过程是非常缓慢的,所以需要进行优化。
服务器忙,则只对一个节点进行rehash
服务器闲,可批量rehash
-
快速列表
快速列表(quicklist)是Redis底层重要的数据结构,是列表的底层实现。 quicklist是一个双向链表,链表中的每个节点时一个ziplist结构。quicklist中的每个 节点ziplist都能够存储多个数据元素。 数据压缩: quicklist每个节点的实际存储结构为ziplist,这种结构的优势在于节省存储空间。为了进 一步降低ziplist的存储空间,还可以对ziplist进行压缩,Redis采用的压缩算法是LZF。其 基本思想为:数据与前面重复的记录重复位置及其长度,不重复的记录原始数据 压缩过后的数据还可以分成多个片段,每个片段有两个部分:解释字段和数据字段 quicklist结构体如下:
typedef struct quicklistLZF {
unsigned int sz; // LZF压缩后占用的字节数
char compressed[]; // 柔性数组,指向数据部分
} quicklistLZF;
Redis的缓存过期和淘汰策略
Redis的数据删除有定时删除、惰性删除和主动删除三种方式。
Redis目前采用惰性删除+主动删除的方式。
定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除
操作。
需要创建定时器,而且消耗CPU,一般不推荐使用。
惰性删除
在key被访问时如果发现它已经失效,那么就删除它。
主动删除
在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)
LRU
LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
- 在Java中可以使用LinkHashMap(哈希链表)去实现LRU
-
Redis的LRU 数据淘汰机制
LRU 数据淘汰机制是这样的:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。 不可能遍历key 用当前时间-最近访问 越大 说明 访问间隔时间越长 volatile-lru 从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少, 那么在将来一段时间内被使用的可能性也很小。 random 随机 volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 allkeys-random 从数据集(server.db[i].dict)中任意选择数据淘汰 ttl volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。 TTL 数据淘汰机制:从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。 noenviction 禁止驱逐数据,不删除 默认
缓存淘汰策略的选择
- allkeys-lru : 在不确定时一般采用策略。 冷热数据交换
- volatile-lru : 比allkeys-lru性能差 存 :过期时间
- allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)
- 自己控制:volatile-ttl 缓存穿透
Redis及其IO多路复用的模型与选择
Redis客户端与服务器交互采用序列化协议(RESP)。
select,poll,epoll、kqueue都是IO多路复用的机制。
-
select
select 函数监视的文件描述符分3类,分别是:- writefds - readfds - exceptfds
调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。
当select函数返回后,可以 通过遍历fd列表,来找到就绪的描述符优点:select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
缺点:单个进程打开的文件描述是有一定限制的,它由FD_SETSIZE设置,默认值是1024,采用数组存储。
另外在检查数组中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些socket是不是活跃的,都轮询一遍,所以效率比较低。 -
poll
poll使用一个 pollfd的指针实现,pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。优点:采样链表的形式存储,它监听的描述符数量没有限制,可以超过select默认限制的1024
缺点:另外在检查链表中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些socket是不是活跃的,都轮询一遍,所以效率比较低。 -
epoll
优点: epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,举个例子,在1GB内存的机 器上大约是10万左右 效率提升, epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际 的网络环境中, epoll的效率就会远远高于select和poll 。 epoll使用了共享内存,不用做内存拷贝
Redis持久化
-
RDB,是redis默认的存储方式,RDB是通过快照(snapshotting)完成的。
-
出发快照的方式
1.符合自定义配置的快照规则
2.执行save或者bgsave命令
3.执行flushall命令
4.执行主从复制(第一次)
RDB执行流程
1.Redis父进程首先判断:当前是否在执行save,或者bgsave/bgrewriteaof(aof文件重写命令)的子进程,如果在执行则bgsave命令直接返回。
2.父进程执行fork(调用OS函数复制主进程)操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客服端的任何命令。
3.父进程fork后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,并可以响应其他命令。
4.子进程创建RDB文件,根据父进程内存块照生成临时快照文件,完成后对原有文件进行原子替换。(RDB始终完整)。
5.子进程发送信号给父进程表示完成,父进程更新统计信息。
6.父进程fork子进程后,继续工作。
RDB优缺点
优点
RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
主进程fork子进程,可以最大化Redis性能,主进程不能太大,复制过程中主进程阻塞
缺点
不保证数据完整性
AOF
AOF是Redis的另一种持久化方式。Redis默认情况下不开启,开启AOF持久化后
Redis将所有对数据库进行过写入的命令及其参数(RESP)记录到AOF文件,以此达到记录数据库状态的目的
AOF会记录过程,RDB只管结果
-
AOF原理
AOF中存储的是redis的命令,同步命令到AOF文件的整个过程可以分为三个阶段: 命令传播:redis将执行完的命令、命令的参数、命令的参数个数等信息发送到AOF程序中 缓存追加:AOF根据接收到的命令数据,将命令数据转换为网络通信协议的格式,然后将协议 内容追加到AOF缓存中 文件写入和保存:AOF缓存中的内容被写入到AOF文件末尾,如果设定的AOF保存条件满足的话, fsync函数或者fdatasync函数会被调用,将写入的内容真正的保存到磁盘中 命令传播: 当一个Redis客服端要执行命令时,它通过网络连接,将协议文本发送给Redis服务器。 服务器在接到客服端的请求后,他会根据协议的文本内容,选择适当的命令函数,并将 各个参数从字符串文本转换为Redis字符串对象(StringObject)。每当命令执行成功 后,命令参数都会被传播到AOF程序。 缓存追加: 当命令被传播到AOF程序后,程序会根据命令参数以及命令的参数,将命令从字符串对象 转换回原来的协议文本。协议文本生成之后,它会被追加到redis.h/redisServer结构 的aof_buf末尾 redisServer结构维持着Redis服务器的状态,aof_buf域则保持这所有等待写入到AOF 文件的协议文本(RESP)。 文件的写入和保存: 每当服务器常规任务函数被执行、或事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用,这个函数执行以下两个工作: write:根据条件,将aof_buf中的缓存写入到AOF文件 save:根据条件,调用fsync或fdatasync函数,将AOF文件保存在磁盘中
AOF保存模式
Redis目前支持三种AOF保存模式:
AOF_FSYNC_NO: 不保存
AOF_FSYNC_EVERYSEC: 每一秒中保存一次(默认)
AOF_FSYNC_ALWAYS: 没执行一个命令保存一次。(不推荐)
-
不保存
这种模式下,每次调用flushAppendOnlyFile函数,write都会被执行,但是save会被略过。 在这种模式下,save只会在以下任意一种情况中被执行: 1.redis被关闭 2.AOF功能被关闭 3.系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行) 这三种情况下的save操作都会引起redis主进程阻塞。
-
每一秒种保存一次
在这种模式下,save原则上每隔一秒钟就回执行一次,因为save操作是由后台子线程fork调用的,所以它不会引起服务器主进程阻塞 -
每执行一个命令保存一次
在这种模式下,每次执行完一个命令后,write和save都会被执行。 另外,因为save是由Redis主进程执行的,所以在save执行期间,主进程会被阻塞,不能接收 命令请求
AOF保存模式对性能和安全性的影响
AOF重写、触发方式、混合持久化
AOF记录数据的变化过程越来越大,需要重写“瘦身”
Redis可以在AOF体积变得过大时,自动在后台(fork子进程)对AOF进行重写
-
Redis不希望AOF重写造成服务器无法处理请求,所以Redis决定将AOF重写程序放到后台子进程里执行,这样的最大好处是:
1.子进程进行AOF重写期间,主进程可以继续处理命令请求。 2.子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性
-
不过,使用子进程需要解决一个问题:
-
因为子进程在进行AOF重写期间,主进程还需要继续处理命令,而新的命令可能对现有数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致。
-
为了解决这个问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程后开始启用,Redis主进程在接到新的写命令后,除了会将这个写命令的协议内容追加到现有的AOF文件之外,还会追加到这个缓存中。
重写过程分析(整个重写操作是绝对安全的):
Redis在创建新AOF文件的过程中,会继续将命令追加到现有AOF文件里面,即便重写过程中发生
停机,现有的AOF文件也不会丢失。而一旦新AOF文件创建完毕,Redis就会从旧AOF文件切换到
新AOF文件,并开始对新AOF文件进行追加操作。
当子进程在执行AOF重写时,主进程需要执行以下三个工作:
1.处理命令请求。
2.将写命令追加到现有AOF文件中。
3.将写命令追加到AOF缓存中。
这样一来,现有的AOF功能会继续执行,即使在AOF重写期间发生停机,也不会有任何数据丢失。
所有对数据库进行修改的命令都会被记录到AOF重写缓存中。
当子进程完成AOF重写后,它会向父进程发送一个完成信号,父进程在接到完成信号后,会调用一个信号
处理函数,并完成以下工作:
将AOF重写缓存中的内容全部写入到新AOF文件中。
对新AOF文件进行改名,覆盖原有的AOF文件。
这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接收命令请求了。在整个AOF后台
重写的过程中,只有最后的写入缓存和文件改名会造成主进程阻塞,在其他时候,AOF后台重写都
不会对主进程造成阻塞,这将AOF重写性能造成的影响降到了最低。
以上就是AOF后台重写,也即是BGERWRITEAOF命令(AOF重写)的工作原理。
混合持久化
RDB和AOF各有优缺点,Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开,
aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。
RDB的头+AOF的身体---->appendonly.aof
开启混合持久化
在redis.conf中: aof-use-rdb-preamble yes
-
在加载时,首先会识别AOF文件是否以 REDIS字符串开头,如果是就按RDB格式加载,加载完RDB后继续按AOF格式加载剩余部分。
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执 行一遍AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态 Redis读取AOF文件并还原数据库状态的详细步骤如下: 1、创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上 下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器 使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和 带网络连接的客户端执行命令的效果完全一样 2、从AOF文件中分析并读取出一条写命令 3、使用伪客户端执行被读出的写命令 4、一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止
当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来,整个过程如下图所示:
RDB与AOF对比
- RDB存某个时刻的数据快照,采用二进制压缩存储,AOF存操作命令,采用文本存储(混合)
- RDB性能高,AOF性能较低
- RDB在配置出发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多丢2秒数据
- Redis以主服务器模式运行,RDB不会保存过期的键值对数据,Redis以从服务器运行,RDB会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。
- AOF写入文件时,对过期的key会追加一条del命令,当执行AOF重写时,会忽略过期key和del命令
应用场景
- 内存数据库 rdb+aof 数据不容易丢
- 缓存服务器 rdb 性能高
- 不建议 只使用 aof (性能差)
- 在数据还原时
- 有rdb+aof 则还原aof,因为RDB会造成文件的丢失,AOF相对数据要完整。
- 只有rdb,则还原rdb