Redis 原理
简介
- Redis作者:antirez
本文档介绍Redis的原理知识,是从设计Redis的角度看Redis。主要包括如下几个方面:
-
为什么开发Redis
-
Redis解决了哪些实际问题
-
Redis的技术架构
-
Redis的具体设计
-
Redis适合哪些业务场景
-
Redis的不足
为什么开发Redis
为了弄明白为什么要开发Redis,我们不妨先来看看Redis是什么,用官方的一句话:
Redis是一个开源的、分布式的基于内存的数据结构化存储,常备用作内存数据库,消息缓存和代理
redis数据持久化
- redis数据持久化有两种方式:rdb和aof
rdb
-
fork一个进程,遍历hash table,利用copy on write,把整个db dump保存下来。save, shutdown, slave 命令会触发这个操作。粒度比较大,如果save, shutdown, slave 之前crash了,则中间的操作没办法恢复
-
RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件
-
RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
-
没办法做到实时持久化/秒级持久化,因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
-
RDB 从4.0开始支持增量复制
-
RDB的工作流程:
1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
2. 父进程执行fork操作创建子进程,fork操作过程中父进程被阻塞。
3. 父进程fork完成后,bgsave命令返回“* Background saving started by pid xxx”信息,并不再阻塞父进程,可以继续响应其他命令。
4. 父进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。根据lastsave命令可以获取最近一次生成RDB的时间,对应info Persistence中的rdb_last_save_time。
5. 进程发送信号给父进程表示完胜,父进程更新统计信息。
对于大多数操作系统来说,fork都是个重量级操作,虽然创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。
子进程通过fork操作产生,占用内存大小等同于父进程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write)。父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而 子进程在fork操作过程中会共享父进程的内存快照。
- 触发机制
1. 手动触发
包括save和bgsave命令。
因为save会阻塞当前Redis节点,所以,Redis内部所有涉及RDB持久化的的操作都通过bgsave方式,save方式已废弃。
2. 自动触发
1> 使用save的相关配置。
2> 从节点执行全量复制操作。
3> 执行debug reload命令。
4> 执行shutdown命令时,如果没有开启AOF持久化功能则会自动执行bgsave。
aof
-
把写操作指令,持续的写到一个类似日志文件里。(类似于从postgresql等数据库导出sql一样,只记录写操作)。粒度较小,crash之后,只有crash之前没有来得及做日志的操作没办法恢复。
-
AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。
-
使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
-
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
-
AOF 不支持重启后的增量复制(可理解为断点续传)
-
AOF的工作流程
1. 所有的写入命令追加到aof_buf缓冲区中。
2. AOF会根据对应的策略向磁盘做同步操作。刷盘策略由appendfsync参数决定。
3. 定期对AOF文件进行重写。重写策略由auto-aof-rewrite-percentage,auto-aof-rewrite-min-size两个参数决定。
appendfsync参数有如下取值:
no: don't fsync, just let the OS flush the data when it wants. Faster. 只调用系统write操作,不对AOF文件做fsync操作,同步硬盘操作由操作系统负责,通常同步周期最长为30s。
always: fsync after every write to the append only log. Slow, Safest. 命令写入到aof_buf后,会调用系统fsync操作同步到文件中。
everysec: fsync only one time every second. Compromise. 只调用系统write操作,fsync同步文件操作由专门进程每秒调用一次。
默认值为everysec,也是建议值。
- AOF重写
为什么要重写?重写后可以加快节点启动时的加载时间。
重写后的文件为什么可以变小?
1. 进程内超时的数据不用再写入到AOF文件中。
2. 存在删除命令。
3. 多条写命令可以合并为一个。
- AOF重写条件
1. 手动触发
直接调用bgrewriteaof命令。
2. 自动触发。
与auto-aof-rewrite-percentage,auto-aof-rewrite-min-size两个参数有关。
触发条件,aof_current_size > auto-aof-rewrite-min-size 并且 (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage。
其中,aof_current_size是当前AOF文件大小,aof_base_size 是上一次重写后AOF文件的大小,这两部分的信息可从info Persistence处获取。
- AOF重写的流程
1. 执行AOF重写请求。
如果当前进程正在执行bgsave操作,重写命令会等待bgsave执行完后再执行。
2. 父进程执行fork创建子进程。
3. fork操作完成后,主进程会继续响应其它命令。所有修改命令依然会写入到aof_buf中,并根据appendfsync策略持久化到AOF文件中。
4. 因fork操作运用的是写时复制技术,所以子进程只能共享fork操作时的内存数据,对于fork操作后,生成的数据,主进程会单独开辟一块aof_rewrite_buf保存。
5. 子进程根据内存快照,按照命令合并规则写入到新的AOF文件中。每次批量写入磁盘的数据量由aof-rewrite-incremental-fsync参数控制,默认为32M,避免单次刷盘数据过多造成硬盘阻塞。
6. 新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息。
7. 父进程将aof_rewrite_buf(AOF重写缓冲区)的数据写入到新的AOF文件中。
8. 使用新AOF文件替换老文件,完成AOF重写。
实际上,当Redis节点执行完一个命令后,它会同时将这个写命令发送到AOF缓冲区和AOF重写缓冲区。
- Redis通过AOF文件还原数据库的流程。
1. 创建一个不带网络连接的伪客户端。因为Redis的命令只能在客户端上下文中执行。
2. 从AOF文件中分析并读取一条命令。
3. 使用伪客户端执行该命令。
4. 反复执行步骤2,3,直到AOF文件中的所有命令都被处理完。
注意:AOF的持久化也可能会造成阻塞。
AOF常用的持久化策略是everysec,在这种策略下,fsync同步文件操作由专门线程每秒调用一次。当系统磁盘较忙时,会造成Redis主线程阻塞。
1. 主线程负责写入AOF缓冲区。
2. AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
3. 主线程负责对比上次AOF同步时间。
1> 如果距上次同步成功时间在2s内,主线程直接返回。
2> 如果距上次同步成功时间超过2s,主线程会阻塞,直到同步操作完成。每出现一次阻塞,info Persistence中aof_delayed_fsync的值都会加1。
所以,使用everysec策略最多会丢失2s数据,而不是1s
redis 的lua脚本
-
redis从2.6开始支持lua脚本
-
redis从3.2开始支持lua脚本调试,调试命令为:ldb
什么是lua脚本
-
Lua 是一个小巧的脚本语言。它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。 其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。
-
Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。
-
Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。
-
一个完整的Lua解释器不过200k,在所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择
使用lua脚本好处
-
减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
-
原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
-
复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
Redis 事件通知
-
Keyspace 通知使得客户端可以通过订阅频道或模式,来接收那些以某种方式改动了 Redis 数据集的事件(触发某些事件后可以向指定的频道发送通知),该功能需要 Redis 版本大于 2.8。
-
事件通过 Redis 的订阅与发布功能(pub/sub)来进行分发,因此所有支持订阅与发布功能的客户端都可以在无须做任何修改的情况下,直接使用此功能。
-
因为 Redis 目前的订阅与发布功能采取的是发送即忘(fire and forget)策略,所以如果你的程序需要可靠事件通知(reliable notification of events),那么目前的键空间通知可能并不适合你:当订阅事件的客户端断线时,它会丢失所有在断线期间分发给它的事件。
事件的类型
-
对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件:键空间通知(key-space)和键事件通知(key-event)。
-
当 del mykey 命令执行时:
键空间频道的订阅者将接收到被执行的事件的名字,在这个例子中,就是 del
键事件频道的订阅者将接收到被执行事件的键的名字,在这个例子中,就是 mykey
配置
-
因为开启键空间通知功能需要消耗一些 CPU,所以在默认配置下,该功能处于关闭状态。
-
修改 redis.conf 中的 notify-keyspace-events 参数,参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知:
K 键空间通知,以__keyspace@<db>__为前缀 db为:0-15
E 键事件通知,以__keysevent@<db>__为前缀 db为:0-15
g del , expipre , rename 等类型无关的通用命令的通知, ...
$ String命令
l List命令
s Set命令
h Hash命令
z 有序集合命令
x 过期事件(每次key过期时生成)
e 驱逐事件(当key在内存满了被清除时生成)
A g$lshzxe的别名,因此”AKE”意味着所有的事件
- 输入的参数中至少要有一个 K 或者 E,否则的话,不管其余的参数是什么,都不会有任何通知被分发。
如: notify-keyspace-events "Ex" 表示对过期事件进行通知发送; notify-keyspace-events "kx" 表示想监控某个 key 的失效事件。将参数设为字符串 AKE 表示发送所有类型的通知。
HyperLogLog
-
用于计算基数的一种算法;
-
基数:就是指一个集合中不同值得数目;例如:[1,2,3,4],基数为:4;[1,2,3,4,2],基数仍然为:4