Redis的持久化操作

前言

        redis是一个内存数据库,并且按照对应的数据结构来存储,这样直接访问内存+特殊数据结构的方式让数据库可以应对很高的访问量。但如果服务器出现异常,比如:断电、某种原因宕机等。这时如果只是在数据放在内存,就会造成数据的丢失。应对这种异常的做法就是要对数据进行持久化操作,当重启服务器时再读取数据载入内存即可完成恢复。

 

本章内容

  • 服务器中的数据库
  • 过期键的删除策略
  • AOF、RDB及复制功能对过期键的处理
  • RDB持久化
  • AOF持久化

 

服务器中的数据库

         一个redis服务器可以包含多个数据库,并且保存在redisdb对象的数组中,此数组中的每一项就是redis的一个数据库,默认情况下服务器会创建16个数据库,编号从0至15,由dbnum属性决定数据库的数量。在未使用select 命令的情况下,默认是使用0号数据库。用户可以使用select idx 命令来切换不同的数据库进行操作。

redis是一个K-V数据库服务器,所有的K-V都保存在对应的redisdb中的dict字典当中,我们操作数据的命令都是对此dict进行操作。例如:

SET msg "hello" 命令就是向dict中添加一对键值。

RPUSH ids "1001" "1002" "1003" 命令就是向dict中添加一个键为ids 值是List类型的键值对并且向此list中添加三个元素。

DEL msg 命令就是从此dict中删除此键值对。

除这些基本的操作外 还有一些不常使用的命令,例如:

FLUSHDB命令是清空数据库中所有的键值。

RANDOMKEY命令是从数据库中随机返回一个键。

另外还有EXISTS、RENAME、KEYS等。

当使用命令对数据库进行读写操作时,redis不仅会执行这些操作,同时还会有额外的一些维护操作,例如:

  • 读写一个键操作后,服务器会根据键是否存在更新服务器当中的键空间命中次数(hit)和未命中次数(miss),并且可以使用INFO stats命令来查看
  • 读写一个键后,服务器更新LRU时间(最后一次使用时间),这个值可以计算键的闲置时间。
  • 如果读取时发现键已过期,则会先执行删除,再执行相关的命令。
  • 若客户端使用了WATCH监视一个键,在对此键进行修改后,服务器会标记此键为脏(dirty),从而可以让事务程序注意到键的变化。
  • 若开启了数据库通知功能,则在对键进行修改后,嗠顺将按配置发送相应的数据库通知。

设置键的生存时间或过期时间操作:

  • EXPIRE key ttl 用于将键的生存时间设置为ttl秒
  • PEXPIRE key ttl 用于将键的生存时间设置为ttl毫秒
  • EXPIREAT key timestamp 用于将键的过期时间设置为timestamp所指定的秒数时间戳
  • PEXPIREAT key timestamp 用于将键的过期时间设置为timestamp所指定的毫秒数时间戳

上面的这四个命令,最终使用的都是PEXPIREAT命令来实现的。那么过期时间是如何保存的?它也是采用了一个名称为expires的dict来保存的,其保存格式是:

  • 过期字典的键是一个指针,指向键空间中的某个键对象。
  • 过期字典的值是一个long类型的整数,保存对应的过期时间,是一个毫秒精度的UNIX时间戳。

 

过期键的删除策略

        通过一些命令我们可以为一个键设置过期时间,也可以通过TTL或PTTL命令来判断键是否过期,那么,如果一外键过期了,redis是如果处理它的呢?主要有三种策略:

  • 定时删除
  • 惰性删除
  • 定期删除

定时删除

       在设置键过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。这种策略是对内存友好的,它可以尽可能快的删除过期键,从而释放内存;但同时它对CPU时间是不友好的,在过期键比较多的情况下,删除这些键可能会消耗大量的CPU时间,从而导致处理客户端请求的速度减慢。

显然,这是不可取的,因为把CPU的时间浪费在与业务无关的操作上,虽然可以节约内存,但却不能更好的处理请求。

 

惰性删除

        只会在使用键时,才会对键的过期时间进行检查。这可以保证删除过期键的操作只会在非做不可的情况下才会执行,而且删除过期键的操作仅仅局限于当前的键,不会对大量的过期键操作。所以它是对CPU友好,但对内存不友好的操作。

在使用这种策略时,如果数据库中有非常多的过期键,而程序恰好又没有访问到,那么它们就可能一直存在内存中而得不到释放。从某种意义上来说这也是一种内存泄漏。

策略的实现:

  • 所有读写数据库的命令执行之前都会调用db.c/expireIfNeeded函数对输入的键进行检查,它就像是一个过滤器,在真正的命令执行前先过滤掉过期的键
    • 过期,此函数则删除键值
    • 未过期,什么也不做
  • 由于键可能会被expireIfNeeded函数删除,所以每个命令都要处理键存在和不存在这两种情况
    • 存在时,命令按键存在的情况执行
    • 不存在时,或过期被删除时命令按不存在的情况执行。

定期删除

        定期删除策略是对上两种策略的折中,它是每隔一段时间执行一次删除过期操作并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。所以使用这种策略的时候需要设定一个时长和频率,如果删除操作执行的太频繁或执行的时间太长,它就退化成了定时策略对CPU是一种浪费,但如果执行的太少或时间太短的话又和惰性删除策略相似,出现内存浪费的情况。所以必须合理的设置删除过期操作的时间和执行频率。

策略的实现:

        由redis.c/activeExpireCycle函数实现,每当redis服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被掉用,它在规定的时间内,分多次遍历服务器中的各数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。

        redis使用的是惰性删除和定期删除两种策略,通过配合使用这两种删除策略,服务器可以很好地合理使用CPU时间并且避免浪费太多的内存空间。

 

AOF、RDB及复制功能对过期键的处理

 

操作类型

实现方式

是否影响

生成RDB文件

在执行SAVE或BGSAVE创建一个RDB文件时会先对数据库中的键进行检查,已过期的键不会被保存到RDB文件中

载入RDB文件

1.如果是主服务器载入,会先对RDB文件中保存的键进行检查,过期的键将会被忽略,不会载入到数据库中。

2.如果是从服务器载入,则文件中保存的键不会检查都会被载入到数据库中。因主服务器向从服务器同步,从服务器的数据会先被清空掉,所以 这种策略也不会对从服务器载入造成影响

AOF文件写入

无论是惰性删除还是定期删除过期键,都会向AOF文件追加一条DEL命令来显示的记录键被删除

AOF重写

重写是重新对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。

复制

1.从服务器的过期键删除动作由主服务器来控制,当主服务器删除一个过期键后,会显式的向从服务器发送一个DEL命令。

2.而从服务器在执行客户端发送的读命令时即使遇到过期键也不处理,还是像未过期键一样来处理。3.从服务器只有接到主服务器发送的DEL命令后才会删除过期键。(会造成主从服务器数据一段时间内不一致的现象)

数据库通知

       2.8版本后新增的功能,可以让客户端通过订阅给定的频道或模式,来获知数据库中键的变化,以及数据库中命令的执行情况。

 

RDB持久化

        redis为了解决内存数据保存到磁盘的问题,提供了RDB持久化模式,它可以手动执行或根据服务器的配置选项定期执行,将某个时间点上的数据库状态保存到一个RDB文件中,此文件是一个压缩的二进制文件。服务器可以根据此文件还原那一时刻之前的数据。

redis提供两个命令来创建一个rdb文件,这两个命令的执行是互斥的:

  1. SAVE命令,它会阻塞redis服务器进程,直到文件创建完成。在此期间不处理任何客户端请求。
  2. BGSAVE命令,它会创建一个子进程,由这个子进程进行创建RDB的工作,服务器进程仍然可以接收客户端的请求不阻塞。

 

自动创建RDB文件

         BGSAVE命令是不阻塞服务器进程的,只是由子进程负责创建RDB。所以redis允许用户通过配置save相关的参数,实现每隔一段时间自动创建一个rdb文件。并且用户可以设置多个保存条件,只要有任一条件被满足,就会执行BGSAVE,例如:

  • save 900 1:在900秒内,对数据库至少有1次修改
  • save 300 10:在300秒内,对数据库至少有10次修改
  • save 60 10000:在60秒内,对数据库至少有10000次修改

这些条件只要有一个满足,就会执行。

除了使用saveparams数组保存这些条件外,还有dirty计数器和lastsave属性:

  • dirty,记录距离上一次成功执行bgsave或save命令之后,服务器对数据库状态进行了多少次修改(如果向一个集合中添加5个元素,表示的是有5次修改而不是1次)。
  • lastsave,是一个UNIX时间戳,记录了服务器上一次成功执行的时间。

检查创建条件:

       周期性函数serverCron,默认每隔100毫秒就会执行一次。此函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save设置的条件是否满足,如果满足就执行保存。

RDB文件的结构

RDB文件是一个二进制文件,它也是按redis设定的格式进行保存的,方便恢复时读取,一个完整的RDB文件包含:

REDIS

db_version

databases

EOF

check_num

这五大部分:

  • REDIS,最开头的部分,占用5个字节,固定为REDIS,表示文件是一个REDIS文件
  • db_version,记录RDB文件的版本号,长度为4个字节,是一个字符串表示的整数,例如:“0006”就表示RDB文件的版本是第六版。
  • databases,包含0个或多个数据库,及数据库中的所有键值,如果服务器所有的数据库都是空的,那么daabases也是空的。它只记录非空数据库中的键值。例如:0和6号数据库非空,则只记录0和6 ,而不会是0到6.
    • 每个database的结构都是:SELECTDB/DB_NUMBER/KEY_VALUE_PAIRS这三部分组成,SELECTDB占1字节,表示接下来是执行select哪个库,DB_NUMBER数据库的编号,KEY_VALUE_PAIRS具体的所有键值对,每个键值对的结构是:TYPE/KEY/VALUE。TYPE就是REDIS定义的数据类型,例如:REDIS_RDB_TYPE_LIST/REDIS_RDB_TYPE_ZSET等等,KEY总是一个REDIS_RDB_TYPE_STRING类型的字节串,VALUE是按REDIS每种数据结构定义的值。
    • 带过期时间的键值结构:EXPIRETIME_MS/MS/TYPE/KEY/VALUE。EXPIRETIME_MS是redis定义的常量,长度为1字节,表示接下来是一个以毫秒为单位的值,MS具体的毫秒时间戳。
  • EOF,是一个常量标志着RDB文件的正文结束,长度为1字节。当读入程序遇到这个值的时候就知道所有的数据库都已经装载完成。
  • check_num,保存着一个校验和(签名),长度为8字节,它是通过对REDIS、db_version、databases、EOF四个部分进行综合计算得出的。载入文件时会重新计算校验和与此值比较,以此来检查RDB文件是否有出错或损坏的情况。

分析RDB文件

我们可以使用od命令来分析redis的结构,可以给定参数:

  • -c,以ASCII编码的方式打印输入文件
  • -x,可以以十六进制的方式打印输入文件

例如,可以输入命令:od -cx dump.rdb就可以查看此rdb文件 的内容。

 

AOF持久化

       这是redis服务器提供的另一种持久化文件,与RDB通过保存数据库中的键值不同,它是通过保存服务器所有执行的写命令来记录数据库的状态的,当需要恢复数据库状态时,按顺序执行AOF文件中的所有命令即可还原数据库的状态。

AOF的实现

       AOF持久化的实现可以分为三个步骤来完成:追加命令(append)、文件写入、文件同步。

追加命令(append)

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

文件写入

        redis的服务器进程就是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。由于每个写命令都会被追加到aof_buf缓冲区中,在每次事件循环之中它都会调用flushAppendOnlyFile函数将缓冲区中的命令写入到aof文件里,此函数的行为可以由参数appendfsync决定:

  • always 将aof_buf中的所有内容写入并同步到AOF文件
    • 数据安全性高,是多损失一个事件循环中所产生的命令数据
    • 由于是一直同步的,所以效率上也是最慢的
  • everysec (默认值)将aof_buf中的所有内容写入到AOF,如果上次同步AOF文件的时间距离现在超过1秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行。
    • 每隔1秒同步,从效率上讲足够快,并且如果出现宕机,也只会丢失1秒钟的命令数据。
  • no 将aof_buf中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统来决定。
    • 由于在这种模式下,无须调用强制刷新,效率也是最快的
    • 但同步缓存是由操作系统决定的,所以单次同步时长也是最长的。
    • 其实从综合上看,no模式与everysec模式的效率很相似。

文件同步

        这一步主要是由操作系统完成的,为了提高文件的写入效率,一般在操作系统中,当用户调用write命令将数据写入文件时,操作系统通常会将写入的数据暂时保存在一个内存缓冲区中,等到此缓冲区被填满或超过了指定的时限时,才真正的将数据写入到磁盘。这种方式,虽然提高了效率,但也带来了数据安全的问题,如果出现宕机等情况,那么缓冲区内的数据将会丢失。

       为此,系统提供了fsync和fdatasync两个同步函数,可以强制让操作系统立即将缓冲区的数据写入磁盘。

 

数据的载入与还原

       一个AOF文件包含了重建数据库的所有命令,所以只要按顺序读取并执行这些命令就可以恢复数据库的状态,REDIS的读取和执行步骤:

  1. 创建一个不带网络连接的伪客户端(fake client),因为redis命令只能在客户端上下文中执行,载入的数据来自aof文件而不是网络,所以创建一个没有网络连接的伪客户端即可。
  2. 从AOF文件中读取写命令交给伪客户端执行。
  3. 一直循环,直到所有的命令都被执行完成为止。

 

AOF重写

       从上面的AOF记录的模式可以看出,随着时间的推移。此文件很快会变的很大,如果不控制,可能会对redis服务器造成影响,并且如果aof文件的体积很大的话,用于恢复的时间也就越长。

      所以,为了解决文件体积膨胀的问题,redis提供了AOF重写的功能。虽然命名为重写,但实际上,AOF重写并不需要对现有的AOF文件进行任何读取和分析操作,它是通过读取服务器当前的数据库状态来实现的,具体的实现:

  • 取出数据库中某个键的值,然后合并成一条命令,例如:一个list中有10个元素,有可能在添加的时候,是多次添加的,那么在原AOF文件中就保存了多条这样的命令。现在,可以直接使用RPUSH KEY 10ele 这样的格式来代替。
  • 对于集合中的元素个数很多的情况,重写也会分成多条,目前每条上的个数是64个,如果集合中多于64个,那么会分成两条或多条命令追加到重写的AOF文件中。

 

后台重写(BGREWRITEAOF)

       AOF重写函数在执行的时候会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为redis服务器是单线程工作,所以如果直接由服务器调用rewrite操作的话,在重写期间将不能接收处理客户端的请求,作为数据库的一种维护手段,显然这么做是不合理的。所以redis决定将AOF重写程序放到子进程里执行,子进程带有服务器进程的数据副本,使用子进程而不是线程,同时也可以在避免使用锁的情况下,保障数据安全。

       虽然使用子进程看上去挺完美,但也带来的另一个问题,就是在子进程进行AOF重写期间,服务器进程仍然在处理客户端的请求,这些请求可能会对现有的数据库值进行再次的修改,从而使得当前的数据状态与重写后的AOF文件不一致。为了解决此问题,redis服务器又新增了一个AOF重写缓冲区,当子进程被创建时开始使用,此时当服务器执行完成一个命令后会同时将这个命令写入AOF缓冲区和AOF重写缓冲区中。当子进程完成AOF的重写工作后,它会向父进程发送一个信号,父进程在接收到此信号后,会完成以下步骤:

  • 将AOF重写缓冲区中的内容追加到新的AOF文件中,这时新的AOF文件所保存的状态将和服务器当前的状态一致了
  • 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧AOF文件的替换

        父进程在处理完成上述两个步骤后即可重新处理客户端的请求了。所以在整个AOF后台重写的过程中,只有信号处理函数执行时会对服务器造成阻塞(不会处理客户端请求),其它时候,都不会阻塞服务器进程,对服务器性能影响也降到了最低。

 

疑问:子进程带有服务器进程的数据副本” 怎么理解?在创建子进程的时候,在子进程中保存数据库在那一时刻的镜像吗??但如果不是这样的话,服务器处理接收客户端命令后数据库的值也会发生改变,此时AOF重写读取数据库的值是已经改变后的值,在子进程发送信息给父进程后又重新把命令追加到了AOF文件中,那么在恢复时,被追加的命令是不是相当于被执行了两次?这样数据会出现不一致的现象吧。

例如:

在数据库中有一个key是a,数值是1的数,如下操作后,数值的变化 :

操作

数据库中的值

说明

客户端发送INCR(a)请求

a=2

a数值加1,从1变成了2

AOF子进程开启,此时客户端又请求了一次INCR(a)

,服务器处理后

a=3

此时服务器进程并没有阻塞,仍然处理客户端请求,所以此时a的值变成了3

AOF子进程读取数据库的a值,并将set a 3命令写入到AOF中,同时将INCR(a)

命令写入到了AOF重写缓冲区中

a=3

恰好改变后AOF重写读到了此键值,将向AOF追加set a 3(也有可能是其它形式的命令)

AOF重写完成,发送信息到父进程,把AOF重写缓冲区中的INCR(a)

命令又写进了AOF文件中

a=3

此时服务器进程阻塞,但又把INCR(a)的命令写入到了AOF文件中

此时服务器停机,然后重新启动,载入AOF文件,先载入set a 3,然后又载入了INCR(a)

a=4

此时a变成了4,而不是停机前的数据3??

        除非,这个副本就像是一个镜像copy,在那一时刻这个副本中的值不再发生变化,而后再追加在重写过程中的命令,这样才能达到一致性。但内存数据非常庞大的时候,是如何快速生成这个镜像的???下次更新的时候希望能找到答案。

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值