Redis版本:Redis 4.0.1



Redis是一个键值对数据库服务器,存储在内存中,如果重启没进行持久化,数据会丢失。所以需要持久化策略RDB和AOF。



 



前提知识归纳



RDB用于保存和恢复数据库所有键值对



使用save或bgsave报错,其中bgsave是fork子进程不会造成阻塞



rdb文件是一个二进制文件



不同类型键,会有不同数据结构存储



 



RDB会采用save、bgsave进行rdb保存



save:生成RDB,会导致Redis阻塞



bgsave:生成RDB,fork子进程处理



优先使用AOF文件加载数据。



 



其中是否执行bgsave这些是根据 RedisServer#saveparams 来判断,其数据结构如下:



struct 
   redisServer 
   {
 
  
....
 
  
    // 
   保存触发
   rdb
   文件存储的参数数组
 
  
    struct 
   saveparam 
   *
   saveparams
   ; 
   /* Save points array for RDB */
 
  
}
 
  
// 在seconds秒内数据库修改changes次就触发RDB文件生成
 
  
struct 
   saveparam 
   {
 
  
    time_t 
   seconds
   ;// 秒数
 
  
    int 
   changes
   ;// 数据库修改次数
 
  
};
 
  

   符合上面的参数生成RDB,举个例子:
  
 
  
• 
     seconds=1, changes=5。即代表1秒内数据库有5次修改就可以重新生成RDB
    

    
  
 
  
bgsave是fork子进程进行处理,其触发方式大概有如下两种:
 
  

    手动触发:执行bgsave命令手动触发
   
 
   

     
   
 
   
sync指令或psync(这个做主从分析的时候再解析)
 
   

     
   
 
   
下面举个定时触发的例子源码(serverCron定时触发,然后扫描saveparams参数)
 
  
/**
 
  
* 
   作为时间事件运行
 
  
*/
 
  
int 
   serverCron
   (
   struct 
   aeEventLoop 
   *eventLoop
   , long long 
   id
   , void 
   *clientData) {
 
  
    ....
 
  
    // 
   单位时间内修改次数达到上限
 
  
    if 
   (server.
   dirty 
   >= sp->
   changes 
   &&
 
  
        server.unixtime-server.lastsave > sp->seconds && 
   (server.unixtime-server.lastbgsave_try > 
   CONFIG_BGSAVE_RETRY_DELAY || 
   server.
   lastbgsave_status 
   == C_OK)) 
   {
 
  
        // 
   打印保存进行中的日志
 
  
        serverLog(LL_NOTICE
   ,
   "%d changes in %d seconds. Saving..."
   ,
 
  
        sp->
   changes
   , 
   (
   int
   )sp->seconds)
   ;
 
  
        rdbSaveInfo 
   rsi
   , 
   *rsiptr
   ;
 
  
        rsiptr = rdbPopulateSaveInfo(&rsi)
   ;
 
  
        // 
   进行
   bgsave
 
  
        rdbSaveBackground(server.
   rdb_filename
   ,
   rsiptr)
   ;
 
  
        break;
 
  
    }
 
  
    ....
 
  
}
 
  

    
  
 
  
/**
 
  
* 
   rdbbgsave
   ,会
   fork
   一个子进程处理
 
  
**/
 
  
int 
   rdbSaveBackground
   (
   char 
   *filename
   , 
   rdbSaveInfo 
   *rsi) {
 
  
    pid_t childpid
   ;
   // 
   子进程
 
  
    // 
   判断是否有活跃的子进程在拷贝,如果有则终止 
   server.child_pid != -1
 
  
    if 
   (hasActiveChildProcess()) 
   return 
   C_ERR
   ;
 
  
    
   // 
   记录当前
   bgsave
   前的数据库被修改次数 和 最后修改的时间
  
 
  
    server.
   dirty_before_bgsave 
   = server.
   dirty
   ;
 
  
    server.lastbgsave_try = time(NULL)
   ;
 
  
    // fork
   子进程
 
  
    if 
   ((childpid = redisFork(CHILD_TYPE_RDB)) == 
   0
   ) {
 
  
        int 
   retval
   ;
 
  

    
  
 
  
        /* Child */
 
  
        redisSetProcTitle(
   "redis-rdb-bgsave"
   )
   ;
   // 
   设置标题
 
  
        redisSetCpuAffinity(server.
   bgsave_cpulist
   )
   ;
   // 
   设置
   CPU
   亲缘属性
 
  
        retval = rdbSave(filename
   ,
   rsi)
   ;
   // 
   开始保存
 
  
        if 
   (retval == C_OK) {
   // 
   发送保存数据
 
  
            sendChildCowInfo(
   CHILD_INFO_TYPE_RDB_COW_SIZE
   , 
   "RDB"
   )
   ;
 
  
        }
 
  
        exitFromChild((retval == C_OK) ? 
   0 
   : 
   1
   )
   ;
   // 
   结束子进程保存
 
  
    } 
   else 
   {
 
  
        /* Parent */
 
  
        // fork
   子进程失败
 
  
        if 
   (childpid == -
   1
   ) {
 
  
            server.
   lastbgsave_status 
   = C_ERR
   ;
 
  
            serverLog(LL_WARNING
   ,
   "Can't save in background: fork: %s"
   ,
 
  
            strerror(errno))
   ;
 
  
            return 
   C_ERR
   ;
 
  
        }
 
  
        // 
   保存状态
 
  
        serverLog(LL_NOTICE
   ,
   "Background saving started by pid %ld"
   ,
   (
   long
   ) childpid)
   ;
 
  
        server.rdb_save_time_start = time(NULL)
   ;
 
  
        server.
   rdb_child_type 
   = RDB_CHILD_TYPE_DISK
   ;
 
  
        return 
   C_OK
   ;
 
  
    }
 
  
    return 
   C_OK
   ; 
   /* unreached */
 
  
}




上面解释了RDB是如何装载进来的,那下面来讲解一下RDB的文件结构。如何存储?如何压缩?



RDB文件大致组成结构



- REDIS:标识RDB文件开头的字符'REDIS'



- db_version:4字节。记录RDB文件版本号



- database:包含0-N个数据库



- EOF:标记RDB结束



- check_sum:保存前面四个的校验和,用于做签名校验




Redis RDB持久化原理解析_RDB源码


 


其中database存储结构如下


- SELECTDB:database存储的开始标识


- db_number:1/2/5字节。数据库号码


key_value_pairs:存储当前数据库中所有的键值对数据, 根据类型不同而结构不同



Redis RDB持久化原理解析_源码解析_02



Redis RDB持久化原理解析_源码解析_03


 


key_value_pairs结构为


不带过期时间的键值对


- TYPE:记录value类型,主要含string、list、zset、hash、list_zipskiplist等


- key:redis key,总是string类型


- value:redis对应的值



Redis RDB持久化原理解析_数据结构_04


带过期时间的键值对


- EXPIRETIME_MS:1字节。开始标识


- ms:8字节。UNIX时间戳


- TYPE:记录value类型,主要含string、list、zset、hash、list_zipskiplist等


- key:redis key,总是string类型


- value:redis对应的值



Redis RDB持久化原理解析_数据结构_05


 


value是怎么根据TYPE编码来生成RDB的对应值数据(大致讲解几个即可,知道什么回事,没必要纠结)


String字符串


字符串<=20字节:len记录原来长度



Redis RDB持久化原理解析_源码解析_06


字符串>20字节:origin_len记录原来长度,compresesed_len记录压缩后长度



Redis RDB持久化原理解析_数据结构_07


 


List列表


记录长度,然后item一个接一个拼起来(每个元素记录长度)



Redis RDB持久化原理解析_数据结构_08



Redis RDB持久化原理解析_redis_09


 


set集合


跟list一样


 


hash哈希


记录有多少个键值对,比如下面:2个键值对,"a"->"apple","b"->"banana"



Redis RDB持久化原理解析_数据结构_10



Redis RDB持久化原理解析_源码解析_11


 


zset有序集合


记录分数和item



Redis RDB持久化原理解析_RDB源码_12



Redis RDB持久化原理解析_RDB解析_13