您的位置:首页 > 数据库 > Redis

redis启动加载过程、数据持久化

2017-12-06 14:14 435 查看

背景

  公司一年的部分业务数据放在redis服务器上,但数据量比较大,单纯的string类型数据一年就将近32G,而且是经过压缩后的。

  所以我在想能否通过获取string数据的时间改为保存list数据类型,或者将数据持久化到硬盘上,或者放在不同库上,解决未来数据过大导致down机的问题。

相关知识点

string数据类型

数据持久化

数据加载

Redis的字符串(string)的实现原理

Redis是由C语言编写的,以高效和轻量著称。

比如一个简单的字符串”hello world”,其实是一个如下的字符的数组:

[‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘\0’]

最后的一个’\0’是空字符,表示字符串的结尾。

Redis由于各种原因,并没有直接使用了C语言的字符串结构,而是对其做了一些封装,得到了自己的简单动态字符串(simple dynamic string, SDS)的抽象类型

Redis中,默认以SDS作为自己的字符串表示。只有在一些字符串不可能出现变化的地方使用C字符串。

SDS定义中有三个参数:

  buf是一块可用的内存空间,通常大小会大于等于需要存储的字符串的大小

  len表示字符串的长度,也表示buf中已经被使用的空间的大小

  free表示buf中没有被使用的空间的大小。

要注意的是,buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的

那么这么封装到底有什么好处呢?

  1.常数复杂度获取字符串长度

在C语言中的字符串只是简单的字符的数组,当使用strlen获取字符串长度的时候,C语言内部其实是直接顺序遍历数组的内容,找到对应的’\0’对应的字符,从而计算出字符串的长度。显然这个算法复杂度和字符串的长度成正比,即O(N)。而对于SDS来说,只需要访问SDS的len属性就能得到字符串的长度,复杂度为O(1)。这样,获取字符串长度的操作就不会成为Redis的瓶颈。

  2.杜绝缓冲区溢出

C++里面的字符串使用了STL的string类型,我们开发者不太需要关注内存的分配和释放的过程。但是Redis是C语言编写的,并没有这么方便的数据类型。对于字符串的拼接、复制等操作,C语言开发者必须确保目标字符串的空间足够大,不然就会出现溢出的情况。

  char a[10]="hello";

  strcar(a,"world");

  strcpy(a,"hello world");

上面的三句代码,就是C语言的字符串拼接和复制的使用,但是明显出现了缓冲区溢出的问题。字符数组a的长度是10,而”hello world”字符串的长度为11,则需要12个字节的空间来存储(不要忘记了’\0’)。

然而当使用SDS的API对字符串进行修改的时候,API内部第一步会检测字符串的大小是否满足。

如果空间已经满足要求,那么就像C语言一样操作即可。

如果不满足,则拓展buf的空间,使得满足操作的需求,之后再进行操作。

每次操作之后,len和free的值会做相应的修改。

这就是SDS的全部的高明之处了吗?当然不!

当API发现SDS的buf的容量不够的时候,并不是简单申请正好适合的大小,而是额外申请了一倍的空间!我们以sds的API sdscat函数为例,该函数实现了sds的拼接的功能。

  3.减少修改字符串时带来的内存重新分配次数

c语言底层是一个N+1长的字符数组,长度的变化都会引起内存的重新分配。而对于SDS则通过一些策略去解决这些问题:

空间预分配

这种方式用于处理字符串长度增加的问题。

如果对字符串的修改使得字符串的长度增加,API首先会判断buf的空间大小是否满足,如果满足则直接操作,如果不满足,则进行如下操作:

如果对SDS进行修改之后的,SDS的长度(即len的值)小于1MB。程序将额外分配和len一样大小的未使用空间。以上面的”hello” + ” world”的操作为例。

在这个例子中”hello”的len是5(不考虑’\0′),修改之后的字符串”hello world”长度为11,那么新的SDS的buf的容量就是11*2+1。其中len和free都是11,多余的1字节用来存储’\0’。

惰性空间释放

当执行字符串长度缩短的操作的时候,SDS并不直接重新分配多出来的字节,而是修改len和free的值(len相应减小,free相应增大,buf的空间大小不变化)。通过惰性空间释放,可以很好的避免缩短字符串需要的内存重分配的情况。而且多余的空间也可以为将来可能有的字符串增长的操作做优化。

当然,SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。

数据持久化

filesnapshotting(快照)

Append-only(aof)

filesnapshotting

  默认redis是会以快照的形式将数据持久化到磁盘的(一个二进 制文件,xx.rdb)

  在配置文件中的格式是:save N M表示在N秒之内,redis至少发生M次修改则redis抓快照到磁盘。

  当然我们也可以手动执行save或者bgsave(异步)做快照。

  工作原理简单介绍:当redis需要做持久化时,redis会fork一个子进程;子进程将数据写到磁盘上一个临时RDB文件中;当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处就是可以copy-on-write

缺点:filesnapshotting方法在redis异常死掉时, 最近的数据会丢失(丢失数据的多少视你save策略的配置),所以这是它最大的缺点,当业务量很大时,丢失的数据是很多的

Appened-only

  可以做到全部数据不丢失,但redis的性能就要差些。AOF就可以做到全程持久化,只需要在配置文件中开启(默认是no),appendonly yes开启AOF之后,可以选择三种不同的策略,都会把它添加到aof文件中,当redis重启时,将会读取AOF文件进行“重放”以恢复到 redis关闭前的最后时刻。

AOF的三种策略

appendfsyncappendfsync always每提交一个修改命令都调用fsync刷新到AOF文件,非常非常慢,但也非常安全;

appendfsync everysec每秒钟都调用fsync刷新到AOF文件,很快,但可能会丢失一秒以内的数据;

appendfsync no依靠OS进行刷新,redis不主动刷新AOF,这样最快,但安全性就差。默认并推荐每秒刷新,这样在速度和安全上都做到了兼顾。

LOG Rewriting随着修改数据的执行AOF文件会越来越大,其中很多内容记录某一个key的变化情况。

因此redis有了一种比较有意思的特性:在后台重建AOF文件,而不会影响client端操作。在任何时候执行BGREWRITEAOF命令,都会把当前内存中最短序列的命令写到磁盘,这些命令可以完全构建当前的数据情况,而不会存在多余的变化情况(比如状态变化,计数器变化等),缩小的AOF文件的大小。

所以当使用AOF时,redis推荐同时使用BGREWRITEAOF

LOG Rewrite的工作原理:同样用到了copy-on-write:首先redis会fork一个子进程;子进程将最新的AOF写入一个临时文件;父进程 增量的把内存中的最新执行的修改写入(这时仍写入旧的AOF,rewrite如果失败也是安全的);当子进程完成rewrite临时文件后,父进程会收到 一个信号,并把之前内存中增量的修改写入临时文件末尾;这时redis将旧AOF文件重命名,临时文件重命名,开始向新的AOF中写入。

Redis启动加载过程

1. 初始化全局服务器配置

2. 加载配置文件(如果指定了配置文件,否则使用默认配置)

3. 初始化服务器

4. 加载数据库

5. 网络监听

下面对上面这些步骤进行介绍

初始化全局服务器配置

初始化全局服务器配置通过initServerConfig()函数完成,主要是初始化server变量

初始化的内容包括下面几个方面:

1. 网络监听相关,如绑定地址,TCP端口等
2. 虚拟内存相关,如swap文件、page大小等
3. 保存机制,多长时间内有多少次更新才进行保存
4. 复制相关,如是否是slave,master地址、端口
5. Hash相关设置
6. 初始化命令表

如其中的保存机制中,服务器初始化策略为:

$ cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$8
mykey001
$10
myvalue001


View Code
在appendonly.aof文件中保存的正是从客户端发过来的请求命令,还可以看到对于GET命令,并没有保存。

既然appendonly.aof中保存了所有写入数据的请求命令,那么在加载数据的时候只要重新执行一遍这些命令即可。

事实上Redis也正是这么做的,在开始加载之前暂时关闭appendonly,然后Redis创建一个假的Redis客户端。

然后读取appendonly.aof文件中的命令,在假的Redis客户端上下文中执行,同时服务器也不对该客户端做任何应答。

如果加载过程中物理内存不够用,并且Redis开启了VM,则还需要处理swap操作,最后加载完成后重新设置appendonly标志。

2. 从dbfile中加载数据:rdbLoad()函数

如果Redis没有开启appendonly,就需要从数据库文件中加载数据到内存,基本步骤如下:

a. 处理SELECT命令,即选择数据库
b. 读取key
c. 读取value
d. 检测key是否过期
e. 添加新的对象到哈希表
f. 设置过期时间(如果需要)
g. 如果开启了VM,处理swap操作

网络监听

在完成了初始化配置和数据加载后,Redis启动监听。Redis的网络库没有使用libevent或者libev,而是作者自己实现的一个非常轻量级的库(主要实现在ae.c文件中)

然而回到我的问题上,如果数据库中数据已经放入了32G的数据,在启动redis时加载数据库这部分必定会相当相当慢,而且涉及到宕机的问题(物理内存到达峰值)

这时可能单台redis很难解决这个问题,而应该考虑集群。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: