微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Redis数据持久化机制AOF原理分析一---转

本文所引用的源码全部来自Redis2.8.2版本。

Redis AOF数据持久化机制的实现相关代码是redis.c,redis.h,aof.c,bio.c,rio.c,config.c

在阅读本文之前请先阅读Redis数据持久化机制AOF原理分析之配置详解文章,了解AOF相关参数的解析,文章链接

转载请注明,文章出自

下面将介绍AOF数据持久化机制的实现

 

Server启动加载AOF文件数据

 

Server启动加载AOF文件数据的执行步骤为:main() -> initServerConfig() -> loadServerConfig() -> initServer() -> loadDataFromDisk()。initServerConfig()主要为初始化默认的AOF参数配置;loadServerConfig()加载配置文件redis.conf中AOF的参数配置,覆盖Server的默认AOF参数配置,如果配置appendonly on,那么AOF数据持久化功能将被激活,server.aof_state参数被设置为REDIS_AOF_ON;loadDataFromDisk()判断server.aof_state == REDIS_AOF_ON,结果为True就调用loadAppendOnlyFile函数加载AOF文件中的数据,加载的方法就是读取AOF文件中数据,由于AOF文件中存储的数据与客户端发送的请求格式相同完全符合Redis的通信协议,因此Server创建伪客户端fakeClient,将解析后的AOF文件数据像客户端请求一样调用各种指令,cmd->proc(fakeClient),将AOF文件中的数据重现到Redis Server数据库中。

<div class="dp-highlighter bg_cpp">
<div class="bar">
<div class="tools">
[cpp] <a class="ViewSource" title="view plain" href="
http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
      
  1.  loadDataFromdisk() {  
  2.       start = ustime();  
  3.      (server.aof_state == REdis_AOF_ON) {  
  4.          (loadAppendOnlyFile(server.aof_filename) == REdis_OK)  
  5.             redisLog(REdis_NOTICE,,()(ustime()-start)/1000000);  
  6.     }  {  
  7.          (rdbLoad(server.rdb_filename) == REdis_OK) {  
  8.             redisLog(REdis_NOTICE,disk: %.3f seconds",  
  9.                 ()(ustime()-start)/1000000);  
  10.         }   (errno != ENOENT) {  
  11.             redisLog(REdis_WARNING,%s. Exiting.",strerror(errno));  
  12.             exit(1);  
  13.         }  
  14.     }  
  15. }  

Server首先判断加载AOF文件是因为AOF文件中的数据要比RDB文件中的数据要新。

<div class="dp-Highlighter bgcpp">
<div class="bar">
<div class="tools">
@H
404_151@[cpp] <a class="ViewSource" title="view plain" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
     loadAppendOnlyFile( *filename) {  
  1.      redisClient *fakeClient;  
  2.      *fp = fopen(filename,);  
  3.      redis_stat sb;  
  4.      old_aof_state = server.aof_state;  
  5.      loops = 0;  
  6.   
  7.     dis_fstat就是fstat64函数,通过fileno(fp)得到文件描述符,获取文件的状态存储于sb中,  
  8.     函数,st_size就是文件的字节数  
  9.      (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {  
  10.         server.aof_current_size = 0;  
  11.         fclose(fp);  
  12.          REdis_ERR;  
  13.     }  
  14.   
  15.      (fp == NULL) {文件失败  
  16.         redisLog(REdis_WARNING,%s",strerror(errno));  
  17.         exit(1);  
  18.     }  
  19.   
  20.     disable AOF, to prevent EXEC from Feeding a MULTI 
  21.   
  22.     server.aof_state = REdis_AOF_OFF;  
  23.   
  24.     fakeClient = createFakeClient();   
  25.     startLoading(fp);   
  26.   
  27.     (1) {  
  28.          argc, j;  
  29.         unsigned  len;  
  30.         robj **argv;  
  31.          buf[128];  
  32.         sds argsds;  
  33.          redisCommand *cmd;  
  34.   
  35.           
  36.         函数得到文件的当前位置,返回值为long  
  37.          (!(loops++ % 1000)) {  
  38.             loadingProgress(ftello(fp));文件读取的位置,ftellno(fp)获取文件当前位置  
  39.             aeProcessEvents(server.el, AE_FILE_EVENTS|AE_DONT_WAIT);  
  40.         }  
  41.           
  42.          (fgets(buf,(buf),fp) == NULL) {  
  43.              (feof(fp))文件尾EOF  
  44.                 ;  
  45.               
  46.                  readerr;  
  47.         }  
  48.         文件中的命令,依照Redis的协议处理  
  49.          (buf[0] !=  fmterr;  
  50.         argc = atoi(buf+1);  
  51.          (argc < 1)  fmterr;  
  52.   
  53.         argv = zmalloc((robj*)*argc);  
  54.          (j = 0; j < argc; j++) {  
  55.              (fgets(buf,fp) == NULL)  readerr;  
  56.              (buf[0] !=  fmterr;  
  57.             len = strtol(buf+1,NULL,10);  
  58.             argsds = sdsnewlen(NULL,len);一个空sds  
  59.               
  60.              (len && fread(argsds,len,1,fp) == 0)  fmterr;  
  61.             argv[j] = createObject(REdis_STRING,argsds);  
  62.              (fread(buf,2,fp) == 0)  fmterr; discard CRLF 跳过\r\n*/  
  63.         }  
  64.   
  65.           
  66.         cmd = lookupCommand(argv[0]->ptr);  
  67.          (!cmd) {  
  68.             redisLog(REdis_WARNING,Nown command '%s' reading the append only file", (*)argv[0]->ptr);  
  69.             exit(1);  
  70.         }  
  71.           
  72.         fakeClient->argc = argc;  
  73.         fakeClient->argv = argv;  
  74.         cmd->proc(fakeClient);  
  75.   
  76.           
  77.         redisAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);  
  78.           
  79.         redisAssert((fakeClient->flags & REdis_BLOCKED) == 0);  
  80.   
  81.          
  82.   
  83.          (j = 0; j < fakeClient->argc; j++)  
  84.             decrRefCount(fakeClient->argv[j]);  
  85.         zfree(fakeClient->argv);  
  86.     }  
  87.   
  88.      
  89.   
  90.      (fakeClient->flags & REdis_MULTI)  readerr;  
  91.   
  92.     fclose(fp);  
  93.     freeFakeClient(fakeClient);  
  94.     server.aof_state = old_aof_state;  
  95.     stopLoading();  
  96.     aofUpdateCurrentSize(); 文件大小  
  97.     server.aof_rewrite_base_size = server.aof_current_size;  
  98.      REdis_OK;  
  99.     …………  
  100. }  

在前面一篇关于AOF参数配置的博客遗留了一个问题,server.aof_current_size参数的初始化,下面解决这个疑问。

<div class="dp-Highlighter bgcpp">
<div class="bar">
<div class="tools">
@H
404_151@[cpp] <a class="ViewSource" title="view plain" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
     aofUpdateCurrentSize() {  
  1.      redis_stat sb;  
  2.   
  3.      (redis_fstat(server.aof_fd,&sb) == -1) {  
  4.         redisLog(REdis_WARNING,%s",  
  5.             strerror(errno));  
  6.     }  {  
  7.         server.aof_current_size = sb.st_size;  
  8.     }  
  9. }  

redis_fstat是作者对Linux中fstat64函数重命名,该还是就是获取文件相关的参数信息,具体可以Google之,sb.st_size就是当前AOF文件的大小。这里需要知道server.aof_fd即AOF文件描述符,该参数的初始化在initServer()函数

<div class="dp-Highlighter bgcpp">
<div class="bar">
<div class="tools">
@H
404_151@[cpp] <a class="ViewSource" title="view plain" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
      
  1.      (server.aof_state == REdis_AOF_ON) {  
  2.         server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);  
  3.          (server.aof_fd == -1) {  
  4.             redisLog(REdis_WARNING, %s",strerror(errno));  
  5.             exit(1);  
  6.         }  
  7.     }  

 

至此,Redis Server启动加载硬盘中AOF文件数据的操作就成功结束了。

 

Server数据库产生新数据如何持久化到硬盘

当客户端执行Set等修改数据库中字段的指令时就会造成Server数据库中数据被修改,这些修改的数据应该被实时更新到AOF文件中,并且也要按照一定的fsync机制刷新到硬盘中,保证数据不会丢失。

在上一篇博客中,提到了三种fsync方式:appendfsync always, appendfsync everysec, appendfsync no. 具体体现在server.aof_fsync参数中。

首先看当客户端请求的指令造成数据被修改,Redis是如何将修改数据的指令添加到server.aof_buf中的。

call() -> propagate() -> feedAppendOnlyFile(),call()函数判断执行指令后是否造成数据被修改。

feedAppendOnlyFile函数首先会判断Server是否开启了AOF,如果开启AOF,那么根据Redis通讯协议将修改数据的指令重现成请求的字符串,注意在超时设置的处理方式,接着将字符串append到server.aof_buf中即可。该函数最后两行代码需要注意,这才是重点,如果server.aof_child_pid != -1那么表明此时Server正在重写rewrite AOF文件,需要将被修改的数据追加到server.aof_rewrite_buf_blocks链表中,等待rewrite结束后,追加到AOF文件中。具体见下面代码的注释。

<div class="dp-highlighter bg_cpp">
<div class="bar">
<div class="tools">
[cpp] <a class="ViewSource" title="view plain" href="
http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
     
  1.  
  2.  
  3.  
  4. dis_PROPAGATE_NONE (no propagation of command at all) 
  5. dis_PROPAGATE_AOF (propagate into the AOF file if is enabled) 
  6. dis_PROPAGATE_REPL (propagate into the replication link) 
  7.   
  8.  propagate( redisCommand *cmd,  dbid, robj **argv,  argc,  
  9.                 flags)  
  10. {  
  11.     文件中  
  12.      (server.aof_state != REdis_AOF_OFF && flags & REdis_PROPAGATE_AOF)  
  13.         FeedAppendOnlyFile(cmd,dbid,argv,argc);  
  14.      (flags & REdis_PROPAGATE_REPL)  
  15.         replicationFeedSlaves(server.slaves,argc);  
  16. }  
Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
    修改了数据,先将更新的数据写到server.aof_buf中  
  1.  FeedAppendOnlyFile( redisCommand *cmd,  dictid,  argc) {  
  2.     sds buf = sdsempty();  
  3.     robj *tmpargv[3];  
  4.   
  5.     targeting is not the same as the last command 
  6.   
  7.     数据库  
  8.      (dictid != server.aof_selected_db) {  
  9.          seldb[64];  
  10.   
  11.         snprintf(seldb,(seldb),,dictid);  
  12.         buf = sdscatprintf(buf,%s\r\n",  
  13.             (unsigned )strlen(seldb),seldb);  
  14.         server.aof_selected_db = dictid;  
  15.     }  
  16.   
  17.       
  18.      (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||  
  19.         cmd->proc == expireatCommand) {  
  20.           
  21.         buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);  
  22.     }  
  23.       (cmd->proc == setexCommand || cmd->proc == psetexCommand) {  
  24.           
  25.         tmpargv[0] = createStringObject(,3);  
  26.         tmpargv[1] = argv[1];  
  27.         tmpargv[2] = argv[3];  
  28.         buf = catAppendOnlyGenericCommand(buf,3,tmpargv);  
  29.         decrRefCount(tmpargv[0]);  
  30.         buf = catAppendOnlyExpireAtCommand(buf,argv[2]);  
  31.     }  {  
  32.          
  33.  
  34.   
  35.         buf = catAppendOnlyGenericCommand(buf,argc,argv);  
  36.     }  
  37.   
  38.     disk just before 
  39.  
  40.   
  41.     文件中,并且根据情况fsync刷新到硬盘  
  42.      (server.aof_state == REdis_AOF_ON)  
  43.         server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));  
  44.   
  45.     rewriting is in progress we want to 
  46.  
  47.  
  48.   
  49.     文件(已经开始rewrite),  
  50.       
  51.     文件末尾,保证数据不丢失  
  52.     数据库中的数据,  
  53.     文件,但是此时客户端执行指令又将该值修改了,因此造成了差异  
  54.      (server.aof_child_pid != -1)  
  55.         aofRewriteBufferAppend((unsigned *)buf,sdslen(buf));  
  56.      
  57. 文件打开的时候,会不断将这份数据写入到AOF文件中。 
  58. 用户主动触发了写AOF文件的命令时,比如 config set appendonly yes命令 
  59. dis会fork创建一个后台进程,也就是当时的数据快照,然后将数据写入到一个临时文件中去。 
  60. 后台进程完成AOF临时文件写后,serverCron定时任务 
  61. 退出动作,然后就会调用backgroundRewriteDoneHandler进而调用aofRewriteBufferWrite函数 
  62. 文件中,然后再unlink替换正常的AOF文件 
  63.  
  64.   
  65.   
  66.     sdsfree(buf);  
  67. }  

 

Server在每次事件循环之前会调用一次beforeSleep函数,下面看看这个函数做了什么工作?

<div class="dp-Highlighter bgcpp">
<div class="bar">
<div class="tools">
@H
404_151@[cpp] <a class="ViewSource" title="view plain" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
    dis is entering the 
  1.  
  2.   
  3.  beforeSleep( aeEventLoop *eventLoop) {  
  4.     REdis_NOTUSED(eventLoop);  
  5.     listNode *ln;  
  6.     redisClient *c;  
  7.   
  8.      
  9.   
  10.      (server.active_expire_enabled && server.masterhost == NULL)  
  11.         activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);  
  12.   
  13.       
  14.      (listLength(server.unblocked_clients)) {  
  15.         ln = listFirst(server.unblocked_clients);  
  16.         redisAssert(ln != NULL);  
  17.         c = ln->value;  
  18.         listDelNode(server.unblocked_clients,ln);  
  19.         c->flags &= ~REdis_UNBLOCKED;  
  20.   
  21.           
  22.           
  23.          (c->querybuf && sdslen(c->querybuf) > 0) {  
  24.             server.current_client = c;  
  25.             processInputBuffer(c);  
  26.             server.current_client = NULL;  
  27.         }  
  28.     }  
  29.   
  30.     disk */  
  31.     文件中并fsync到硬盘上  
  32.     flushAppendOnlyFile(0);  
  33. }  

通过上面的代码及注释可以发现,beforeSleep函数做了三件事:1、处理过期键,2、处理阻塞期间的客户端请求,3、将server.aof_buf中的数据追加到AOF文件中并fsync刷新到硬盘上,flushAppendOnlyFile函数给定了一个参数force,表示是否强制写入AOF文件,0表示非强制即支持延迟写,1表示强制写入。

<div class="dp-Highlighter bgcpp">
<div class="bar">
<div class="tools">
@H
404_151@[cpp] <a class="ViewSource" title="view plain" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;view plain<a class="CopyToClipboard" title="copy" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;copy<a class="PrintSource" title="print" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;print<a class="About" title="?" href="http://blog.csdn.net/acceptedxukai/article/details/18136903"&gt;?<a title="在CODE上查看代码片" href="https://code.csdn.net/snippets/152074" target="_blank">

<img src="https://code.csdn.net/assets/CODE_ico.png" alt="在CODE上查看代码片" width="12" height="12">

<a title="派生到我的代码片" href="https://code.csdn.net/snippets/152074/fork" target="_blank">

<img src="https://code.csdn.net/assets/ico_fork.svg" alt="派生到我的代码片" width="12" height="12">

 
     flushAppendOnlyFile( force) {  
  1.     ssize_t nwritten;  
  2.      sync_in_progress = 0;  
  3.      (sdslen(server.aof_buf) == 0) ;  
  4.     后台正在等待执行的 fsync 数量  
  5.      (server.aof_fsync == AOF_FSYNC_EVERYSEC)  
  6.         sync_in_progress = bioPendingJobsOfType(REdis_BIO_AOF_FSYNC) != 0;  
  7.   
  8.       
  9.      (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {  
  10.          
  11.  
  12.   
  13.           
  14.          (sync_in_progress) {  
  15.               
  16.              (server.aof_flush_postponed_start == 0) {  
  17.                 IoUs write postponinig, remember that we are 
  18.   
  19.                 server.aof_flush_postponed_start = server.unixtime;  
  20.                 ;  
  21.             }   (server.unixtime - server.aof_flush_postponed_start < 2) {  
  22.                   
  23.                  
  24.   
  25.                 ;  
  26.             }  
  27.              
  28.   
  29.             server.aof_delayed_fsync++;  
  30.             redisLog(REdis_NOTICE,disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");  
  31.         }  
  32.     }  
  33.      
  34.   
  35.     server.aof_flush_postponed_start = 0;  
  36.   
  37.     aranteed atomic 
  38. stem we are writing is a real physical one. 
  39.  
  40.  
  41.   
  42.     文件,如果一切幸运的话,写入会原子性地完成  
  43.     nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));  
  44.      (nwritten != ()sdslen(server.aof_buf)) {  
  45.         Now is 
  46.  
  47.   
  48.          (nwritten == -1) {  
  49.             redisLog(REdis_WARNING,%s",strerror(errno));  
  50.         }  {  
  51.             redisLog(REdis_WARNING,  
  52.                                    %s (nwritten=%ld, "  
  53.                                    ,  
  54.                                    strerror(errno),  
  55.                                    ()nwritten,  
  56.                                    ()sdslen(server.aof_buf));  
  57.   
  58.              (ftruncate(server.aof_fd, server.aof_current_size) == -1) {  
  59.                 redisLog(REdis_WARNING, Could not remove short write "  
  60.                          dis may refuse "  
  61.                            
  62.                          runcate: %s", strerror(errno));  
  63.             }  
  64.         }  
  65.         exit(1);  
  66.     }  
  67.     server.aof_current_size += nwritten;  
  68.   
  69.      
  70.   
  71.       
  72.      ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {  
  73.         sdsclear(server.aof_buf);  
  74.     }  {  
  75.         sdsfree(server.aof_buf);  
  76.         server.aof_buf = sdsempty();  
  77.     }  
  78.   
  79.      
  80. I/O in the background. */  
  81.     不支持fsync并且aof rdb子进程正在运行,那么直接返回,  
  82.     文件中,只是没有刷新到硬盘  
  83.      (server.aof_no_fsync_on_rewrite &&  
  84.         (server.aof_child_pid != -1 || server.rdb_child_pid != -1))  
  85.             ;  
  86.   
  87.       
  88.      (server.aof_fsync == AOF_FSYNC_ALWAYS) {  
  89.          
  90. Metadata. */  
  91.         aof_fsync(server.aof_fd); disk */  
  92.         server.aof_last_fsync = server.unixtime;  
  93.     }   ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&  
  94.                 server.unixtime > server.aof_last_fsync)) {  
  95.          (!sync_in_progress) aof_background_fsync(server.aof_fd);后台线程进行fsync  
  96.         server.aof_last_fsync = server.unixtime;  
  97.     }  
  98. }  

上述代码中请关注server.aof_fsync参数,即设置Redis fsync AOF文件到硬盘的策略,如果设置为AOF_FSYNC_ALWAYS,那么直接在主进程中fsync,如果设置为AOF_FSYNC_EVERYSEC,那么放入后台线程中fsync,后台线程的代码在bio.c中。

 

小结

文章写到这,已经解决的了Redis Server启动加载AOF文件和如何将客户端请求产生的新的数据追加到AOF文件中,对于追加数据到AOF文件中,根据fsync的配置策略如何将写入到AOF文件中的新数据刷新到硬盘中,直接在主进程中fsync或是在后台线程fsync。

至此,AOF数据持久化还剩下如何rewrite AOF,接受客户端发送的BGREWRITEAOF请求,此部分内容待下篇博客中解析。

感谢此篇博客给我在理解Redis AOF数据持久化方面的巨大帮助,

本人Redis-2.8.2的源码注释已经放到Github中,有需要的读者可以下载,我也会在后续的时间中更新,

本人不怎么会使用Git,望有人能教我一下。

--------------------------------------------------------------------------------------------------------------------------------------------------------------

本文所引用的源码全部来自Redis2.8.2版本。

Redis AOF数据持久化机制的实现相关代码是redis.c,config.c

在阅读本文之前请先阅读Redis数据持久化机制AOF原理分析之配置详解文章,了解AOF相关参数的解析,文章链接

接着上一篇文章,本文将介绍Redis是如何实现AOF rewrite的。

转载请注明,文章出自

 

AOF rewrite的触发机制

 

如果Redis只是将客户端修改数据库的指令重现存储在AOF文件中,那么AOF文件的大小会不断的增加,因为AOF文件只是简单的重现存储了客户端的指令,而并没有进行合并。对于该问题最简单的处理方式,即当AOF文件满足一定条件时就对AOF进行rewrite,rewrite是根据当前内存数据库中的数据进行遍历写到一个临时的AOF文件,待写完后替换掉原来的AOF文件即可。

 

Redis触发AOF rewrite机制有三种:

1、Redis Server接收到客户端发送的BGREWRITEAOF指令请求,如果当前AOF/RDB数据持久化没有在执行,那么执行,反之,等当前AOF/RDB数据持久化结束后执行AOF rewrite

2、在Redis配置文件redis.conf中,用户设置了auto-aof-rewrite-percentage和auto-aof-rewrite-min-size参数,并且当前AOF文件大小server.aof_current_size大于auto-aof-rewrite-min-size(server.aof_rewrite_min_size),同时AOF文件大小的增长率大于auto-aof-rewrite-percentage(server.aof_rewrite_perc)时,会自动触发AOF rewrite

3、用户设置“config set appendonly yes”开启AOF的时,调用startAppendOnly函数会触发rewrite

下面分别介绍上述三种机制的处理.

 

接收到BGREWRITEAOF指令

 
[cpp] 在CODE上查看代码片

派生到我的代码片

 
    > bgrewriteaofCommand(redisClient *c) {  
  1.       
  2.      (server.aof_child_pid != -1) {  
  3.         addReplyError(c,rewriting already in progress");  
  4.     }   (server.rdb_child_pid != -1) {  
  5.           
  6.         后执行AOF rewrite  
  7.         server.aof_rewrite_scheduled = 1;  
  8.         addReplyStatus(c,rewriting scheduled");  
  9.     }   (rewriteAppendOnlyFileBackground() == REdis_OK) {  
  10.           
  11.         addReplyStatus(c,rewriting started");  
  12.     }  {  
  13.         addReply(c,shared.err);  
  14.     }  
  15. }  
当AOF rewrite请求被挂起时,在serverCron函数中,会处理。
Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
     
  1.   
  2.     用户执行 BGREWRITEAOF 命令的话,在后台开始 AOF 重写  
  3.     用户执行BGREWRITEAOF命令时,如果RDB文件正在写,那么将server.aof_rewrite_scheduled标记为1  
  4.     文件写完后开启AOF rewrite  
  5.      (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&  
  6.         server.aof_rewrite_scheduled)  
  7.     {  
  8.         rewriteAppendOnlyFileBackground();  
  9.     }  

Server自动对AOF进行rewrite

在serverCron函数中会周期性判断
[cpp] 在CODE上查看代码片

派生到我的代码片

 
      
  1.          文件  
  2.           (server.rdb_child_pid == -1 &&  
  3.              server.aof_child_pid == -1 &&  
  4.              server.aof_rewrite_perc &&  
  5.              server.aof_current_size > server.aof_rewrite_min_size)  
  6.          {  
  7.               base = server.aof_rewrite_base_size ?  
  8.                             server.aof_rewrite_base_size : 1;  
  9.               growth = (server.aof_current_size*100/base) - 100;  
  10.              (growth >= server.aof_rewrite_perc) {  
  11.                 redisLog(REdis_NOTICE,rewriting of AOF on %lld%% growth",growth);  
  12.                 rewriteAppendOnlyFileBackground();  
  13.             }  
  14.          }  

config set appendonly yes

当客户端发送该指令时,config.c中的configSetCommand函数会做出响应,startAppendOnly函数会执行AOF rewrite
[cpp] 在CODE上查看代码片

派生到我的代码片

 
     (!strcasecmp(c->argv[2]->ptr,)) {  
  1.      enable = yesnotoi(o->ptr);  
  2.   
  3.      (enable == -1)  badfmt;  
  4.      (enable == 0 && server.aof_state != REdis_AOF_OFF) {关闭AOF  
  5.         stopAppendOnly();  
  6.     }   (enable && server.aof_state == REdis_AOF_OFF) {  
  7.          (startAppendOnly() == REdis_ERR) {  
  8.             addReplyError(c,  
  9.                 );  
  10.             ;  
  11.         }  
  12.     }  
  13. }  
Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
     startAppendOnly() {  
  1.     server.aof_last_fsync = server.unixtime;  
  2.     server.aof_fd = open(server.aof_filename,0644);  
  3.     redisAssert(server.aof_state == REdis_AOF_OFF);  
  4.      (server.aof_fd == -1) {  
  5.         redisLog(REdis_WARNING,dis needs to enable the AOF but can't open the append only file: %s",strerror(errno));  
  6.          REdis_ERR;  
  7.     }  
  8.      (rewriteAppendOnlyFileBackground() == REdis_ERR) {  
  9.         close(server.aof_fd);  
  10.         redisLog(REdis_WARNING,dis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");  
  11.          REdis_ERR;  
  12.     }  
  13.     Now wait for the rerwite to be complete 
  14. disk. */  
  15.     server.aof_state = REdis_AOF_WAIT_REWRITE;  
  16.      REdis_OK;  
  17. }  

Redis AOF rewrite机制的实现

从上述分析可以看出rewrite的实现全部依靠rewriteAppendOnlyFileBackground函数,下面分析该函数,通过下面的代码可以看出,Redis是fork出一个子进程来操作AOF rewrite,然后子进程调用rewriteAppendOnlyFile函数,将数据写到一个临时文件temp-rewriteaof-bg-%d.aof中。如果子进程完成会通过exit(0)函数通知父进程rewrite结束,在serverCron函数中使用wait3函数接收子进程退出状态,然后执行后续的AOF rewrite的收尾工作,后面将会分析。

父进程的工作主要包括清楚server.aof_rewrite_scheduled标志,记录子进程IDserver.aof_child_pid = childpid,记录rewrite的开始时间server.aof_rewrite_time_start = time(NULL)等。
[cpp] 在CODE上查看代码片

派生到我的代码片

 
     rewriteAppendOnlyFileBackground() {  
  1.     pid_t childpid;  
  2.       start;  
  3.   
  4.     后台重写正在执行  
  5.      (server.aof_child_pid != -1)  REdis_ERR;  
  6.     start = ustime();  
  7.      ((childpid = fork()) == 0) {  
  8.          tmpfile[256];  
  9.   
  10.           
  11.         closeListeningSockets(0);  
  12.         redisSetProcTitle(dis-aof-rewrite");  
  13.         snprintf(tmpfile,256,, () getpid());  
  14.          (rewriteAppendOnlyFile(tmpfile) == REdis_OK) {  
  15.              private_dirty = zmalloc_get_private_dirty();  
  16.   
  17.              (private_dirty) {  
  18.                 redisLog(REdis_NOTICE,  
  19.                     copy-on-write",  
  20.                     private_dirty/(1024*1024));  
  21.             }  
  22.             exitFromChild(0);  
  23.         }  {  
  24.             exitFromChild(1);  
  25.         }  
  26.     }  {  
  27.           
  28.         server.stat_fork_time = ustime()-start;  
  29.          (childpid == -1) {  
  30.             redisLog(REdis_WARNING,  
  31.                 %s",  
  32.                 strerror(errno));  
  33.              REdis_ERR;  
  34.         }  
  35.         redisLog(REdis_NOTICE,  
  36.             rewriting started by pid %d",childpid);  
  37.         server.aof_rewrite_scheduled = 0;  
  38.         server.aof_rewrite_time_start = time(NULL);  
  39.         server.aof_child_pid = childpid;  
  40.         updateDictResizePolicy();  
  41.          
  42. FeedAppendOnlyFile() to issue a SELECT command, so the differences 
  43.  
  44.   
  45.         server.aof_selected_db = -1;  
  46.         replicationScriptCacheFlush();  
  47.          REdis_OK;  
  48.     }  
  49.      REdis_OK;   
  50. }  

接下来介绍rewriteAppendOnlyFile函数,该函数的主要工作为:遍历所有数据库中的数据,将其写入到临时文件temp-rewriteaof-%d.aof中,写入函数定义在rio.c中,比较简单,然后将数据刷新到硬盘中,然后将文件名rename为其调用者给定的临时文件名,注意仔细看代码,这里并没有修改为正式的AOF文件名。

在写入文件时如果设置server.aof_rewrite_incremental_fsync参数,那么在rioWrite函数中fwrite部分数据就会将数据fsync到硬盘中,来保证数据的正确性。
Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
     rewriteAppendOnlyFile( *filename) {  
  1.     dictIterator *di = NULL;  
  2.     dictEntry *de;  
  3.     rio aof;  
  4.      *fp;  
  5.      tmpfile[256];  
  6.      j;  
  7.       Now = mstime();  
  8.   
  9.      
  10.   
  11.     snprintf(tmpfile,, () getpid());  
  12.     fp = fopen(tmpfile,);  
  13.      (!fp) {  
  14.         redisLog(REdis_WARNING, opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));  
  15.          REdis_ERR;  
  16.     }  
  17.   
  18.     rioInitWithFile(&aof,fp); 函数,rio.c  
  19.     io.file.autosync = bytes;每32M刷新一次  
  20.      (server.aof_rewrite_incremental_fsync)  
  21.         rioSetAutoSync(&aof,REdis_AOF_AUTOSYNC_BYTES);  
  22.      (j = 0; j < server.dbnum; j++) {数据库  
  23.          selectcmd[] = ;  
  24.         redisDb *db = server.db+j;  
  25.         dict *d = db->dict;  
  26.          (dictSize(d) == 0) ;  
  27.         di = dictGetSafeIterator(d);  
  28.          (!di) {  
  29.             fclose(fp);  
  30.              REdis_ERR;  
  31.         }  
  32.   
  33.           
  34.          (rioWrite(&aof,selectcmd,(selectcmd)-1) == 0)  werr;  
  35.          (rioWriteBulkLongLong(&aof,j) == 0)  werr;  
  36.   
  37.         terate this DB writing every entry */  
  38.         ((de = dictNext(di)) != NULL) {  
  39.             sds keystr;  
  40.             robj key, *o;  
  41.               expiretime;  
  42.   
  43.             keystr = dictGetKey(de);  
  44.             o = dictGetVal(de);  
  45.             initStaticStringObject(key,keystr);  
  46.   
  47.             expiretime = getExpire(db,&key);  
  48.   
  49.               
  50.              (expiretime != -1 && expiretime < Now;  
  51.   
  52.               
  53.              (o->type == REdis_STRING) {  
  54.                   
  55.                  cmd[]=;  
  56.                  (rioWrite(&aof,(cmd)-1) == 0)  werr;  
  57.                   
  58.                  (rioWriteBulkObject(&aof,&key) == 0)  werr;  
  59.                  (rioWriteBulkObject(&aof,o) == 0)  werr;  
  60.             }   (o->type == REdis_LIST) {  
  61.                  (rewriteListObject(&aof,&key,o) == 0)  werr;  
  62.             }   (o->type == REdis_SET) {  
  63.                  (rewriteSetobject(&aof,o) == 0)  werr;  
  64.             }   (o->type == REdis_ZSET) {  
  65.                  (rewriteSortedSetobject(&aof,o) == 0)  werr;  
  66.             }   (o->type == REdis_HASH) {  
  67.                  (rewriteHashObject(&aof,o) == 0)  werr;  
  68.             }  {  
  69.                 redisPanic(Nown object type");  
  70.             }  
  71.               
  72.              (expiretime != -1) {  
  73.                  cmd[]=;  
  74.                  (rioWrite(&aof,(cmd)-1) == 0)  werr;  
  75.                  (rioWriteBulkObject(&aof,&key) == 0)  werr;  
  76.                  (rioWriteBulkLongLong(&aof,expiretime) == 0)  werr;  
  77.             }  
  78.         }  
  79.         dictReleaseIterator(di);  
  80.     }  
  81.   
  82.       
  83.     fflush(fp);  
  84.     aof_fsync(fileno(fp));文件刷新到硬盘  
  85.     fclose(fp);  
  86.   
  87.      
  88.   
  89.      (rename(tmpfile,filename) == -1) {重命名文件名,注意rename后的文件也是一个临时文件  
  90.         redisLog(REdis_WARNING,%s", strerror(errno));  
  91.         unlink(tmpfile);  
  92.          REdis_ERR;  
  93.     }  
  94.     redisLog(REdis_NOTICE,);  
  95.      REdis_OK;  
  96.   
  97. werr:  
  98.     fclose(fp);  
  99.     unlink(tmpfile);  
  100.     redisLog(REdis_WARNING,disk: %s", strerror(errno));  
  101.      (di) dictReleaseIterator(di);  
  102.      REdis_ERR;  
  103. }  

AOF rewrite工作到这里已经结束一半,上一篇文章提到如果server.aof_state != REdis_AOF_OFF,那么就会将客户端请求指令修改的数据通过FeedAppendOnlyFile函数追加到AOF文件中,那么此时AOF已经rewrite了,必须要处理此时出现的差异数据,记得在FeedAppendOnlyFile函数中有这么一段代码

Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
     (server.aof_child_pid != -1)  
  1.         aofRewriteBufferAppend((unsigned *)buf,sdslen(buf));  

如果AOF rewrite正在进行,那么就将修改数据的指令字符串存储到server.aof_rewrite_buf_blocks链表中,等待AOF rewrite子进程结束后处理,处理此部分数据的代码在serverCron函数中。需要指出的是wait3函数我不了解,可能下面注释会有点问题。

Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
      
  1. 获取子进程的退出状态,对后续的工作进行处理  
  2.  (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {  
  3.      statloc;  
  4.     pid_t pid;  
  5.   
  6.      ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {  
  7.          exitcode = WEXITSTATUS(statloc);获取退出的状态  
  8.          bysignal = 0;  
  9.   
  10.          (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);  
  11.   
  12.          (pid == server.rdb_child_pid) {  
  13.             backgroundSaveDoneHandler(exitcode,bysignal);  
  14.         }   (pid == server.aof_child_pid) {  
  15.             backgroundRewriteDoneHandler(exitcode,bysignal);  
  16.         }  {  
  17.             redisLog(REdis_WARNING,  
  18.                 ,  
  19.                 ()pid);  
  20.         }  
  21.           
  22.         updateDictResizePolicy();  
  23.     }  
  24. }  

对于AOF rewrite期间出现的差异数据,Server通过backgroundSaveDoneHandler函数server.aof_rewrite_buf_blocks链表中数据追加到新的AOF文件中。

backgroundSaveDoneHandler函数执行步骤:
1、通过判断子进程的退出状态,正确的退出状态为exit(0),即exitcode为0,bysignal我不清楚具体意义,如果退出状态正确,backgroundSaveDoneHandler函数才会开始处理
2、通过对rewriteAppendOnlyFileBackground函数的分析,可以知道rewrite后的AOF临时文件名为temp-rewriteaof-bg-%d.aof(%d=server.aof_child_pid)中,接着需要打开此临时文件
3、调用aofRewriteBufferWrite函数将server.aof_rewrite_buf_blocks中差异数据写到该临时文件
4、如果旧的AOF文件未打开,那么打开旧的AOF文件,将文件描述符赋值给临时变量oldfd
5、将临时的AOF文件名rename为正常的AOF文件
6、如果旧的AOF文件未打开,那么此时只需要关闭新的AOF文件,此时的server.aof_rewrite_buf_blocks数据应该为空;如果旧的AOF是打开的,那么将server.aof_fd指向newfd,然后根据相应的fsync策略将数据刷新到硬盘上
7、调用aofUpdateCurrentSize函数统计AOF文件的大小,更新server.aof_rewrite_base_size,为serverCron中自动AOF rewrite做相应判断
8、如果之前是REdis_AOF_WAIT_REWRITE状态,则设置server.aof_state为REdis_AOF_ON,因为只有“config set appendonly yes”指令才会设置这个状态,也就是需要写完快照后,立即打开AOF;而BGREWRITEAOF不需要打开AOF
9、调用后台线程去关闭旧的AOF文件
下面是backgroundSaveDoneHandler函数的注释代码
 
Highlighter bg_cpp">
404_151@[cpp] 在CODE上查看代码片

派生到我的代码片

 
    rewriting (BGREWRITEAOF) terminated its work. 
  1.   
  2.  backgroundRewriteDoneHandler( exitcode,  bysignal) {  
  3.      (!bysignal && exitcode == 0) {退出状态正确  
  4.          newfd, oldfd;  
  5.          tmpfile[256];  
  6.           Now = ustime();  
  7.   
  8.         redisLog(REdis_NOTICE,  
  9.             );  
  10.   
  11.          
  12.   
  13.         snprintf(tmpfile,  
  14.             ()server.aof_child_pid);  
  15.         newfd = open(tmpfile,O_WRONLY|O_APPEND);  
  16.          (newfd == -1) {  
  17.             redisLog(REdis_WARNING,  
  18.                 %s", strerror(errno));  
  19.              cleanup;  
  20.         }  
  21.           
  22.          (aofRewriteBufferWrite(newfd) == -1) {  
  23.             redisLog(REdis_WARNING,  
  24.                 %s", strerror(errno));  
  25.             close(newfd);  
  26.              cleanup;  
  27.         }  
  28.   
  29.         redisLog(REdis_NOTICE,  
  30.             , aofRewriteBufferSize());  
  31.   
  32.          
  33. figured file and switch the file descriptor used to do AOF 
  34.  
  35.  
  36.  
  37.  
  38.  
  39. disABLED and this was a one time rewrite. The temporary 
  40. figured file. When this file already 
  41.  
  42.  
  43.  
  44.  
  45. figured file, the original AOF file descriptor will be closed. 
  46.  
  47.  
  48.  
  49.  
  50.  
  51.  
  52. irst, we 
  53. opening the target file 
  54.  
  55.  
  56. arantee atomicity for this switch has already happened by then, so 
  57.  
  58.   
  59.          (server.aof_fd == -1) {  
  60.             disabled */  
  61.   
  62.               
  63.  
  64.   
  65.              oldfd = open(server.aof_filename,O_RDONLY|O_NONBLOCK);  
  66.         }  {  
  67.               
  68.             oldfd = -1;   
  69.         }  
  70.   
  71.          
  72.   
  73.         文件改名为正常的AOF文件名。由于当前oldfd已经指向这个之前的正常文件名的文件  
  74.         文件没有指向了,就删除之。  
  75.          (rename(tmpfile,server.aof_filename) == -1) {  
  76.             redisLog(REdis_WARNING,  
  77.                 %s", strerror(errno));  
  78.             close(newfd);  
  79.              (oldfd != -1) close(oldfd);  
  80.              cleanup;  
  81.         }  
  82.         关闭了,那只要处理新文件,直接关闭这个新的文件即可  
  83.         文件的最后一个fd了,不会的,  
  84.         文件在本函数不会写入数据,因为stopAppendOnly函数会清空aof_rewrite_buf_blocks列表。  
  85.          (server.aof_fd == -1) {  
  86.             disabled, we don't need to set the AOF file descriptor 
  87.   
  88.             close(newfd);  
  89.         }  {  
  90.               
  91.             oldfd = server.aof_fd;  
  92.             文件名  
  93.             server.aof_fd = newfd;  
  94.               
  95.              (server.aof_fsync == AOF_FSYNC_ALWAYS)  
  96.                 aof_fsync(newfd);  
  97.               (server.aof_fsync == AOF_FSYNC_EVERYSEC)  
  98.                 aof_background_fsync(newfd);  
  99.             server.aof_selected_db = -1;   
  100.             aofUpdateCurrentSize();  
  101.             server.aof_rewrite_base_size = server.aof_current_size;  
  102.   
  103.              
  104.   
  105.               
  106.             sdsfree(server.aof_buf);  
  107.             server.aof_buf = sdsempty();  
  108.         }  
  109.   
  110.         server.aof_lastbgrewrite_status = REdis_OK;  
  111.   
  112.         redisLog(REdis_NOTICE, );  
  113.           
  114.           
  115.          (server.aof_state == REdis_AOF_WAIT_REWRITE)  
  116.             server.aof_state = REdis_AOF_ON;  
  117.   
  118.           
  119.         后台线程去关闭这个旧的AOF文件FD,只要CLOSE就行,会自动unlink的,因为上面已经有rename  
  120.          (oldfd != -1) bioCreateBackgroundJob(REdis_BIO_CLOSE_FILE,(*)()oldfd,NULL);  
  121.   
  122.         redisLog(REdis_VERBOSE,  
  123.             , ustime()-Now);  
  124.     }   (!bysignal && exitcode != 0) {  
  125.         server.aof_lastbgrewrite_status = REdis_ERR;  
  126.   
  127.         redisLog(REdis_WARNING,  
  128.             );  
  129.     }  {  
  130.         server.aof_lastbgrewrite_status = REdis_ERR;  
  131.   
  132.         redisLog(REdis_WARNING,  
  133.             , bysignal);  
  134.     }  
  135.   
  136. cleanup:  
  137.     aofRewriteBufferReset();  
  138.     aofRemoveTempFile(server.aof_child_pid);  
  139.     server.aof_child_pid = -1;  
  140.     server.aof_rewrite_time_last = time(NULL)-server.aof_rewrite_time_start;  
  141.     server.aof_rewrite_time_start = -1;  
  142.       
  143.      (server.aof_state == REdis_AOF_WAIT_REWRITE)  
  144.         server.aof_rewrite_scheduled = 1;  
  145. }  
 

至此,AOF数据持久化已经全部结束了,剩下的就是一些细节的处理,以及一些Linux库函数的理解,对于rename、unlink、wait3等库函数的深入认识就去问Google吧。

 

小结

 
Redis AOF数据持久化的实现机制通过三篇文章基本上比较详细的分析了,但这只是从代码层面去看AOF,对于AOF持久化的优缺点网上有很多分析,Redis的官方网站也有英文介绍,Redis的数据持久化还有一种方法叫RDB,更多RDB的内容等下次再分析。
感谢此篇博客给我在理解Redis AOF数据持久化方面的巨大帮助,,此篇博客对AOF的分析十分的详细。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐