Redis设计与实现-读书笔记

redis能做什么

redis是一款优秀的缓存开源项目。
提供多种数据结构支持,包括但不限于 字符串、链表、字典表、集合、位图。
可用于会话缓存、全页缓存、队列、排行榜、计数器、发布订阅等功能。
下面讲解的内容包含redis的底层数据结构、数据持久化的设计、高性能、高可用。

基本数据结构

字符串(String)、列表(list)、字典(hash)、集合(set)、有序集合(sortSet).

字符串

  1. 源码
/*
- 保存字符串对象的结构
*/
struct sdshdr {

   // buf 中已占用空间的长度
   int len;

   // buf 中剩余可用空间的长度
   int free;

   // 数据空间
   char buf[];
};

  1. sds与c字符串的区别
  • sds获取字符串长度的复杂度为O(1),c的是O(n)
  • c的api是不安全的,可能造成缓冲区溢出,sds的是安全的。
  • 修改N次字符串,c需要N次内存分配。sds最多需要N次分配。sds有空间预分配和惰性回收。
  • sds可以使用部分c的字符串库。

链表

  1. 源码
/*
* 双端链表节点
*/
typedef struct listNode {

  // 前置节点
  struct listNode *prev;

  // 后置节点
  struct listNode *next;

  // 节点的值
  void *value;

} listNode;

/*
* 双端链表结构
*/
typedef struct list {

  // 表头节点
  listNode *head;

  // 表尾节点
  listNode *tail;

  // 节点值复制函数
  void *(*dup)(void *ptr);

  // 节点值释放函数
  void (*free)(void *ptr);

  // 节点值对比函数
  int (*match)(void *ptr, void *key);

  // 链表所包含的节点数量
  unsigned long len;

} list;

hash

  1. 源码
/*
* 哈希表节点
*/
typedef struct dictEntry {
  
  // 键
  void *key;

  // 值
  union {
      void *val;
      uint64_t u64;
      int64_t s64;
  } v;

  // 指向下个哈希表节点,形成链表
  struct dictEntry *next;

} dictEntry;

/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
  
  // 哈希表数组
  dictEntry **table;

  // 哈希表大小
  unsigned long size;
  
  // 哈希表大小掩码,用于计算索引值
  // 总是等于 size - 1
  unsigned long sizemask;

  // 该哈希表已有节点的数量
  unsigned long used;

} dictht;

/*
* 字典
*/
typedef struct dict {

  // 类型特定函数
  dictType *type;

  // 私有数据
  void *privdata;

  // 哈希表
  dictht ht[2];

  // rehash 索引
  // 当 rehash 不在进行时,值为 -1
  int rehashidx; /* rehashing not in progress if rehashidx == -1 */

  // 目前正在运行的安全迭代器的数量
  int iterators; /* number of iterators currently running */

} dict;


2. reHash

扩容场景:
有BGSAVE或者BGREWRITEAOF时,负载因子超过5.
无BGSAVE或者BGREWRITEAOF时,负载因子超过1.
扩容为第一个大于等于ht[0].used*2的2的幂

缩容场景:
负载因子小于0.1
缩容为第一个大于等于ht[0].used的2的幂

负载因子:ht[0].used/ht[0].size

渐进式rehash:
rehashidx指向正在hash的索引。rehashidx=-1表示未进行rehash
每次访问dict结构时rehash一个索引

跳跃表

  1. 源码
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {

   // 成员对象
   robj *obj;

   // 分值
   double score;

   // 后退指针
   struct zskiplistNode *backward;

   // 层
   struct zskiplistLevel {

       // 前进指针
       struct zskiplistNode *forward;

       // 跨度
       unsigned int span;

   } level[];

} zskiplistNode;

/*
* 跳跃表
*/
typedef struct zskiplist {

   // 表头节点和表尾节点
   struct zskiplistNode *header, *tail;

   // 表中节点的数量
   unsigned long length;

   // 表中层数最大的节点的层数
   int level;

} zskiplist;

整数集合

  1. 源码
typedef struct intset {
   
   // 编码方式
   uint32_t encoding;

   // 集合包含的元素数量
   uint32_t length;

   // 保存元素的数组
   int8_t contents[];

} intset;


2. 编码升级
添加一个元素可能导致编码升级。编码升级需要做三件事

  • 扩展空间
  • 转变现有元素的类型并发至在合适的位置上。保证原有顺序防止
  • 防止新加入的元素。新加入的元素一定是最大或最小的,所以放在最前或者最后

压缩列表

压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值

  1. 源码
/*
* 保存 ziplist 节点信息的结构
*/
typedef struct zlentry {

   // prevrawlen :前置节点的长度
   // prevrawlensize :编码 prevrawlen 所需的字节大小
   unsigned int prevrawlensize, prevrawlen;

   // len :当前节点值的长度
   // lensize :编码 len 所需的字节大小
   unsigned int lensize, len;

   // 当前节点 header 的大小
   // 等于 prevrawlensize + lensize
   unsigned int headersize;

   // 当前节点值所使用的编码类型
   unsigned char encoding;

   // 指向当前节点的指针
   unsigned char *p;

} zlentry;
  1. 示意图

RedisObject

Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。

  1. 源码
typedef struct redisObject {

   // 类型
   unsigned type:4;

   // 编码
   unsigned encoding:4;

   // 对象最后一次被访问的时间
   unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

   // 引用计数
   int refcount;

   // 指向实际值的指针
   void *ptr;

} robj;


2. 类型和编码的对应关系

3. 字符串对象

编码
可以用long保存的整形int
可以用long,double保存的浮点embstr或row
长度太长,不可以用long,double保存的浮点embstr或row
小于39字节的字符串embstr
大于39字节的字符串row

row要两次申请内存,两次释放内存。为这俩对象RedisObject、SdsStr。embstr申请一次释放一次,而且申请的是连续的内存空间,能更好的利用缓存。

  1. 链表对象

inkedList和zipList的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用zipList

  1. hash对象

hashtable和zipList的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用zipList

  1. 集合对象

hashtable和intset的转换临界值:每个元素都是int、元素个数小于512。满足以上两个条件才能用intset

  1. 内存回收

redis是用C实现的,C没有自动回收内存的机制。RedisObject中的refCount记录对象的引用个数,当refCount=0的时候自动释放内存。

  1. 对象共享

0- 9999这1w个整数是共享对象。字符串不做共享对象,因为对比匹配太复杂

单机数据库的实现

数据库

  1. 源码
struct redisServer {
   .
   .
   .
   redisDb *db;//数据库
   int dbnum;//初始化服务器时,创建数据库的数量。默认16
   .
   .
   .
}

typedef struct redisDb {

   // 数据库键空间,保存着数据库中的所有键值对
   dict *dict;                 /* The keyspace for this DB */

   // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
   dict *expires;              /* Timeout of keys with a timeout set */

   // 正处于阻塞状态的键
   dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */

   // 可以解除阻塞的键
   dict *ready_keys;           /* Blocked keys that received a PUSH */

   // 正在被 WATCH 命令监视的键
   dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

   struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */

   // 数据库号码
   int id;                     /* Database ID */

   // 数据库的键的平均 TTL ,统计信息
   long long avg_ttl;          /* Average TTL, just for stats */

} redisDb;

  1. 过期键
  • 删除过期键的策略

    • 定时:对内存友好,对cpu不友好
    • 惰性:对内存不友好,对cpu友好
    • 定期:是定时和惰性的折中方案。执行时长和频率是衡量一个定期策略好坏的标准。
  • rdb再说生成和加载过程中过滤掉过期的键

  • aof模式下,过期的键只有真正del的时候才会记录在aof文件。重写aof文件不会包含过期的键。

  • 主从复制:从服务器遇到过期键不会del,接收到主服务器的del命令才会删除。

  1. 内存淘汰策略(内存不足时)
  • noeviction:抛异常
  • allkeys-lru:在所有的key中按lru淘汰
  • allkeys-random:在所有的key中随机淘汰
  • volatile-lru:在设置了过期时间的key中lru淘汰
  • volatile-random:在设置了过期时间的key中随机淘汰
  • volatile-ttl:删除快过期的key

RDB持久化

  1. save和bgsave源码
void saveCommand(redisClient *c) {

   // BGSAVE 已经在执行中,不能再执行 SAVE
   // 否则将产生竞争条件
   if (server.rdb_child_pid != -1) {
       addReplyError(c,"Background save already in progress");
       return;
   }

   // 执行 
   if (rdbSave(server.rdb_filename) == REDIS_OK) {
       addReply(c,shared.ok);
   } else {
       addReply(c,shared.err);
   }
}

void bgsaveCommand(redisClient *c) {

   // 不能重复执行 BGSAVE
   if (server.rdb_child_pid != -1) {
       addReplyError(c,"Background save already in progress");

   // 不能在 BGREWRITEAOF 正在运行时执行
   } else if (server.aof_child_pid != -1) {
       addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");

   // 执行 BGSAVE
   } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
       addReplyStatus(c,"Background saving started");

   } else {
       addReply(c,shared.err);
   }
}
  1. 自动间隔性保存

服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。

  1. rdb文件结构

    其中database结构如下:

    其中ksy_value_pairs结构如下

type指定了value的类型。value的编码方式不同期存储结构也不同。

AOF持久化

  1. aof的实现
    命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。
    文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。
    文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。

flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);

  1. aof的载入与还原
  2. AOF重写

事件

  1. 文件事件

使用I/O多路复用监听多个套接字,并为之关联事件处理器。
I/O多路复用程序会将并发的套接字事件放入队列,以同步的方式给文件事件分派器。
I/O多路复用使用select、epoll、evport实现。
事件类型:读事件、写事件
文件事件处理器:连接应答处理器、命令请求处理器、命令回复处理器。

  1. 时间事件

时间事件类型:定时、周期
时间事件属性:id、when、timeProc(事件处理器)
周期类型时间,每次事件处理器执行完会返回一个整数,用此计算下次执行时间

  1. 实现

所有时间事件存在一个无序链表中,当时间事件执行器运行时就遍历整个链表。

  1. 事件的调度

事件调度决定何时处理时间事件、何时处理文件事件、以及花多少时间来处理

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        // 获取最近的时间事件
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            // 如果时间事件存在的话
            // 那么根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
            long now_sec, now_ms;

            /* Calculate the time missing for the nearest
             * timer to fire. */
            // 计算距今最近的时间事件还要多久才能达到
            // 并将该时间距保存在 tv 结构中
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }

            // 时间差小于 0 ,说明事件已经可以执行了,将秒和毫秒设为 0 (不阻塞)
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            
            // 执行到这一步,说明没有时间事件
            // 那么根据 AE_DONT_WAIT 是否设置来决定是否阻塞,以及阻塞的时间长度

            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                // 设置文件事件不阻塞
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                // 文件事件可以阻塞直到有事件到达为止
                tvp = NULL; /* wait forever */
            }
        }

        // 处理文件事件,阻塞时间由 tvp 决定
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            // 从已就绪数组中获取事件
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

           /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            // 读事件
            if (fe->mask & mask & AE_READABLE) {
                // rfired 确保读/写事件只能执行其中一个
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            // 写事件
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }

            processed++;
        }
    }

    /* Check time events */
    // 执行时间事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
因为时间事件在文件事件之后执行,所以,理论上,时间事件的执行是滞后的。

客户端

一个redisServer支持多个redisClient连接。多个redisClient以链表的方式存储在redisServer中。redisClient结构如下。

  1. 源码
/* With multiplexing we need to take per-client state.
 * Clients are taken in a liked list.
 *
 * 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
 *
 * 多个客户端状态被服务器用链表连接起来。
 */
typedef struct redisClient {

    // 套接字描述符
    int fd;

    // 当前正在使用的数据库
    redisDb *db;

    // 当前正在使用的数据库的 id (号码)
    int dictid;

    // 客户端的名字
    robj *name;             /* As set by CLIENT SETNAME */

    // 查询缓冲区
    sds querybuf;

    // 查询缓冲区长度峰值
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */

    // 参数数量
    int argc;

    // 参数对象数组
    robj **argv;

    // 记录被客户端执行的命令
    struct redisCommand *cmd, *lastcmd;

    // 请求的类型:内联命令还是多条命令
    int reqtype;

    // 剩余未读取的命令内容数量
    int multibulklen;       /* number of multi bulk arguments left to read */

    // 命令内容的长度
    long bulklen;           /* length of bulk argument in multi bulk request */

    // 回复链表
    list *reply;

    // 回复链表中对象的总大小
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */

    // 已发送字节,处理 short write 用
    int sentlen;            /* Amount of bytes already sent in the current
                               buffer or object being sent. */

    // 创建客户端的时间
    time_t ctime;           /* Client creation time */

    // 客户端最后一次和服务器互动的时间
    time_t lastinteraction; /* time of the last interaction, used for timeout */

    // 客户端的输出缓冲区超过软性限制的时间
    time_t obuf_soft_limit_reached_time;

    // 客户端状态标志
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */

    // 当 server.requirepass 不为 NULL 时
    // 代表认证的状态
    // 0 代表未认证, 1 代表已认证
    int authenticated;      /* when requirepass is non-NULL */

    // 复制状态
    int replstate;          /* replication state if this is a slave */
    // 用于保存主服务器传来的 RDB 文件的文件描述符
    int repldbfd;           /* replication DB file descriptor */

    // 读取主服务器传来的 RDB 文件的偏移量
    off_t repldboff;        /* replication DB file offset */
    // 主服务器传来的 RDB 文件的大小
    off_t repldbsize;       /* replication DB file size */
    
    sds replpreamble;       /* replication DB preamble. */

    // 主服务器的复制偏移量
    long long reploff;      /* replication offset if this is our master */
    // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
    long long repl_ack_off; /* replication ack offset, if this is a slave */
    // 从服务器最后一次发送 REPLCONF ACK 的时间
    long long repl_ack_time;/* replication ack time, if this is a slave */
    // 主服务器的 master run ID
    // 保存在客户端,用于执行部分重同步
    char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    // 从服务器的监听端口号
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */

    // 事务状态
    multiState mstate;      /* MULTI/EXEC state */

    // 阻塞类型
    int btype;              /* Type of blocking op if REDIS_BLOCKED. */
    // 阻塞状态
    blockingState bpop;     /* blocking state */

    // 最后被写入的全局复制偏移量
    long long woff;         /* Last write global replication offset. */

    // 被监视的键
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */

    // 这个字典记录了客户端所有订阅的频道
    // 键为频道名字,值为 NULL
    // 也即是,一个频道的集合
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */

    // 链表,包含多个 pubsubPattern 结构
    // 记录了所有订阅频道的客户端的信息
    // 新 pubsubPattern 结构总是被添加到表尾
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */

    /* Response buffer */
    // 回复偏移量
    int bufpos;
    // 回复缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES];

} redisClient;

cmd属性的设置:argv[0]保存的是命令的名字,用此值在命令dict表中查找,并复制给cmd;

可变大小缓冲区不能无限的大,有两个限制。软性限制、硬性限制。到达硬性限制直接关闭客户端。到达软性限制则记录obuf_soft_limit_reached_time,如果持续一段时间一直超过软性限制,则关闭客户端。

  1. 关闭客户端
  • 客户端进程被kill或者退出
  • 客户端发送了不符合协议格式的命令
  • 客户端成为client kill对象
  • 客户端空转太长时间
  • 客户端发送命令超出输入缓冲区
  • 超出输出缓冲区

服务器

服务器处理多客户端的连接、存储产生的数据。管理资源来维护自身运行。

  1. 源码
struct redisServer {

    /* General */

    // 配置文件的绝对路径
    char *configfile;           /* Absolute config file path, or NULL */

    // serverCron() 每秒调用的次数
    int hz;                     /* serverCron() calls frequency in hertz */

    // 数据库
    redisDb *db;

    // 命令表(受到 rename 配置选项的作用)
    dict *commands;             /* Command table */
    // 命令表(无 rename 配置选项的作用)
    dict *orig_commands;        /* Command table before command renaming. */

    // 事件状态
    aeEventLoop *el;

    // 最近一次使用时钟
    unsigned lruclock:REDIS_LRU_BITS; /* Clock for LRU eviction */

    // 关闭服务器的标识
    int shutdown_asap;          /* SHUTDOWN needed ASAP */

    // 在执行 serverCron() 时进行渐进式 rehash
    int activerehashing;        /* Incremental rehash in serverCron() */

    // 是否设置了密码
    char *requirepass;          /* Pass for AUTH command, or NULL */

    // PID 文件
    char *pidfile;              /* PID file path */

    // 架构类型
    int arch_bits;              /* 32 or 64 depending on sizeof(long) */

    // serverCron() 函数的运行次数计数器
    int cronloops;              /* Number of times the cron function run */

    // 本服务器的 RUN ID
    char runid[REDIS_RUN_ID_SIZE+1];  /* ID always different at every exec. */

    // 服务器是否运行在 SENTINEL 模式
    int sentinel_mode;          /* True if this instance is a Sentinel. */


    /* Networking */

    // TCP 监听端口
    int port;                   /* TCP listening port */

    int tcp_backlog;            /* TCP listen() backlog */

    // 地址
    char *bindaddr[REDIS_BINDADDR_MAX]; /* Addresses we should bind to */
    // 地址数量
    int bindaddr_count;         /* Number of addresses in server.bindaddr[] */

    // UNIX 套接字
    char *unixsocket;           /* UNIX socket path */
    mode_t unixsocketperm;      /* UNIX socket permission */

    // 描述符
    int ipfd[REDIS_BINDADDR_MAX]; /* TCP socket file descriptors */
    // 描述符数量
    int ipfd_count;             /* Used slots in ipfd[] */

    // UNIX 套接字文件描述符
    int sofd;                   /* Unix socket file descriptor */

    int cfd[REDIS_BINDADDR_MAX];/* Cluster bus listening socket */
    int cfd_count;              /* Used slots in cfd[] */

    // 一个链表,保存了所有客户端状态结构
    list *clients;              /* List of active clients */
    // 链表,保存了所有待关闭的客户端
    list *clients_to_close;     /* Clients to close asynchronously */

    // 链表,保存了所有从服务器,以及所有监视器
    list *slaves, *monitors;    /* List of slaves and MONITORs */

    // 服务器的当前客户端,仅用于崩溃报告
    redisClient *current_client; /* Current client, only used on crash report */

    int clients_paused;         /* True if clients are currently paused */
    mstime_t clients_pause_end_time; /* Time when we undo clients_paused */

    // 网络错误
    char neterr[ANET_ERR_LEN];   /* Error buffer for anet.c */

    // MIGRATE 缓存
    dict *migrate_cached_sockets;/* MIGRATE cached sockets */


    /* RDB / AOF loading information */

    // 这个值为真时,表示服务器正在进行载入
    int loading;                /* We are loading data from disk if true */

    // 正在载入的数据的大小
    off_t loading_total_bytes;

    // 已载入数据的大小
    off_t loading_loaded_bytes;

    // 开始进行载入的时间
    time_t loading_start_time;
    off_t loading_process_events_interval_bytes;

    /* Fast pointers to often looked up command */
    // 常用命令的快捷连接
    struct redisCommand *delCommand, *multiCommand, *lpushCommand, *lpopCommand,
                        *rpopCommand;


    /* Fields used only for stats */

    // 服务器启动时间
    time_t stat_starttime;          /* Server start time */

    // 已处理命令的数量
    long long stat_numcommands;     /* Number of processed commands */

    // 服务器接到的连接请求数量
    long long stat_numconnections;  /* Number of connections received */

    // 已过期的键数量
    long long stat_expiredkeys;     /* Number of expired keys */

    // 因为回收内存而被释放的过期键的数量
    long long stat_evictedkeys;     /* Number of evicted keys (maxmemory) */

    // 成功查找键的次数
    long long stat_keyspace_hits;   /* Number of successful lookups of keys */

    // 查找键失败的次数
    long long stat_keyspace_misses; /* Number of failed lookups of keys */

    // 已使用内存峰值
    size_t stat_peak_memory;        /* Max used memory record */

    // 最后一次执行 fork() 时消耗的时间
    long long stat_fork_time;       /* Time needed to perform latest fork() */

    // 服务器因为客户端数量过多而拒绝客户端连接的次数
    long long stat_rejected_conn;   /* Clients rejected because of maxclients */

    // 执行 full sync 的次数
    long long stat_sync_full;       /* Number of full resyncs with slaves. */

    // PSYNC 成功执行的次数
    long long stat_sync_partial_ok; /* Number of accepted PSYNC requests. */

    // PSYNC 执行失败的次数
    long long stat_sync_partial_err;/* Number of unaccepted PSYNC requests. */


    /* slowlog */

    // 保存了所有慢查询日志的链表
    list *slowlog;                  /* SLOWLOG list of commands */

    // 下一条慢查询日志的 ID
    long long slowlog_entry_id;     /* SLOWLOG current entry ID */

    // 服务器配置 slowlog-log-slower-than 选项的值
    long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */

    // 服务器配置 slowlog-max-len 选项的值
    unsigned long slowlog_max_len;     /* SLOWLOG max number of items logged */
    size_t resident_set_size;       /* RSS sampled in serverCron(). */
    /* The following two are used to track instantaneous "load" in terms
     * of operations per second. */
    // 最后一次进行抽样的时间
    long long ops_sec_last_sample_time; /* Timestamp of last sample (in ms) */
    // 最后一次抽样时,服务器已执行命令的数量
    long long ops_sec_last_sample_ops;  /* numcommands in last sample */
    // 抽样结果
    long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
    // 数组索引,用于保存抽样结果,并在需要时回绕到 0
    int ops_sec_idx;


    /* Configuration */

    // 日志可见性
    int verbosity;                  /* Loglevel in redis.conf */

    // 客户端最大空转时间
    int maxidletime;                /* Client timeout in seconds */

    // 是否开启 SO_KEEPALIVE 选项
    int tcpkeepalive;               /* Set SO_KEEPALIVE if non-zero. */
    int active_expire_enabled;      /* Can be disabled for testing purposes. */
    size_t client_max_querybuf_len; /* Limit for client query buffer length */
    int dbnum;                      /* Total number of configured DBs */
    int daemonize;                  /* True if running as a daemon */
    // 客户端输出缓冲区大小限制
    // 数组的元素有 REDIS_CLIENT_LIMIT_NUM_CLASSES 个
    // 每个代表一类客户端:普通、从服务器、pubsub,诸如此类
    clientBufferLimitsConfig client_obuf_limits[REDIS_CLIENT_LIMIT_NUM_CLASSES];


    /* AOF persistence */

    // AOF 状态(开启/关闭/可写)
    int aof_state;                  /* REDIS_AOF_(ON|OFF|WAIT_REWRITE) */

    // 所使用的 fsync 策略(每个写入/每秒/从不)
    int aof_fsync;                  /* Kind of fsync() policy */
    char *aof_filename;             /* Name of the AOF file */
    int aof_no_fsync_on_rewrite;    /* Don't fsync if a rewrite is in prog. */
    int aof_rewrite_perc;           /* Rewrite AOF if % growth is > M and... */
    off_t aof_rewrite_min_size;     /* the AOF file is at least N bytes. */

    // 最后一次执行 BGREWRITEAOF 时, AOF 文件的大小
    off_t aof_rewrite_base_size;    /* AOF size on latest startup or rewrite. */

    // AOF 文件的当前字节大小
    off_t aof_current_size;         /* AOF current size. */
    int aof_rewrite_scheduled;      /* Rewrite once BGSAVE terminates. */

    // 负责进行 AOF 重写的子进程 ID
    pid_t aof_child_pid;            /* PID if rewriting process */

    // AOF 重写缓存链表,链接着多个缓存块
    list *aof_rewrite_buf_blocks;   /* Hold changes during an AOF rewrite. */

    // AOF 缓冲区
    sds aof_buf;      /* AOF buffer, written before entering the event loop */

    // AOF 文件的描述符
    int aof_fd;       /* File descriptor of currently selected AOF file */

    // AOF 的当前目标数据库
    int aof_selected_db; /* Currently selected DB in AOF */

    // 推迟 write 操作的时间
    time_t aof_flush_postponed_start; /* UNIX time of postponed AOF flush */

    // 最后一直执行 fsync 的时间
    time_t aof_last_fsync;            /* UNIX time of last fsync() */
    time_t aof_rewrite_time_last;   /* Time used by last AOF rewrite run. */

    // AOF 重写的开始时间
    time_t aof_rewrite_time_start;  /* Current AOF rewrite start time. */

    // 最后一次执行 BGREWRITEAOF 的结果
    int aof_lastbgrewrite_status;   /* REDIS_OK or REDIS_ERR */

    // 记录 AOF 的 write 操作被推迟了多少次
    unsigned long aof_delayed_fsync;  /* delayed AOF fsync() counter */

    // 指示是否需要每写入一定量的数据,就主动执行一次 fsync()
    int aof_rewrite_incremental_fsync;/* fsync incrementally while rewriting? */
    int aof_last_write_status;      /* REDIS_OK or REDIS_ERR */
    int aof_last_write_errno;       /* Valid if aof_last_write_status is ERR */
    /* RDB persistence */

    // 自从上次 SAVE 执行以来,数据库被修改的次数
    long long dirty;                /* Changes to DB from the last save */

    // BGSAVE 执行前的数据库被修改次数
    long long dirty_before_bgsave;  /* Used to restore dirty on failed BGSAVE */

    // 负责执行 BGSAVE 的子进程的 ID
    // 没在执行 BGSAVE 时,设为 -1
    pid_t rdb_child_pid;            /* PID of RDB saving child */
    struct saveparam *saveparams;   /* Save points array for RDB */
    int saveparamslen;              /* Number of saving points */
    char *rdb_filename;             /* Name of RDB file */
    int rdb_compression;            /* Use compression in RDB? */
    int rdb_checksum;               /* Use RDB checksum? */

    // 最后一次完成 SAVE 的时间
    time_t lastsave;                /* Unix time of last successful save */

    // 最后一次尝试执行 BGSAVE 的时间
    time_t lastbgsave_try;          /* Unix time of last attempted bgsave */

    // 最近一次 BGSAVE 执行耗费的时间
    time_t rdb_save_time_last;      /* Time used by last RDB save run. */

    // 数据库最近一次开始执行 BGSAVE 的时间
    time_t rdb_save_time_start;     /* Current RDB save start time. */

    // 最后一次执行 SAVE 的状态
    int lastbgsave_status;          /* REDIS_OK or REDIS_ERR */
    int stop_writes_on_bgsave_err;  /* Don't allow writes if can't BGSAVE */


    /* Propagation of commands in AOF / replication */
    redisOpArray also_propagate;    /* Additional command to propagate. */


    /* Logging */
    char *logfile;                  /* Path of log file */
    int syslog_enabled;             /* Is syslog enabled? */
    char *syslog_ident;             /* Syslog ident */
    int syslog_facility;            /* Syslog facility */


    /* Replication (master) */
    int slaveseldb;                 /* Last SELECTed DB in replication output */
    // 全局复制偏移量(一个累计值)
    long long master_repl_offset;   /* Global replication offset */
    // 主服务器发送 PING 的频率
    int repl_ping_slave_period;     /* Master pings the slave every N seconds */

    // backlog 本身
    char *repl_backlog;             /* Replication backlog for partial syncs */
    // backlog 的长度
    long long repl_backlog_size;    /* Backlog circular buffer size */
    // backlog 中数据的长度
    long long repl_backlog_histlen; /* Backlog actual data length */
    // backlog 的当前索引
    long long repl_backlog_idx;     /* Backlog circular buffer current offset */
    // backlog 中可以被还原的第一个字节的偏移量
    long long repl_backlog_off;     /* Replication offset of first byte in the
                                       backlog buffer. */
    // backlog 的过期时间
    time_t repl_backlog_time_limit; /* Time without slaves after the backlog
                                       gets released. */

    // 距离上一次有从服务器的时间
    time_t repl_no_slaves_since;    /* We have no slaves since that time.
                                       Only valid if server.slaves len is 0. */

    // 是否开启最小数量从服务器写入功能
    int repl_min_slaves_to_write;   /* Min number of slaves to write. */
    // 定义最小数量从服务器的最大延迟值
    int repl_min_slaves_max_lag;    /* Max lag of <count> slaves to write. */
    // 延迟良好的从服务器的数量
    int repl_good_slaves_count;     /* Number of slaves with lag <= max_lag. */


    /* Replication (slave) */
    // 主服务器的验证密码
    char *masterauth;               /* AUTH with this password with master */
    // 主服务器的地址
    char *masterhost;               /* Hostname of master */
    // 主服务器的端口
    int masterport;                 /* Port of master */
    // 超时时间
    int repl_timeout;               /* Timeout after N seconds of master idle */
    // 主服务器所对应的客户端
    redisClient *master;     /* Client that is master for this slave */
    // 被缓存的主服务器,PSYNC 时使用
    redisClient *cached_master; /* Cached master to be reused for PSYNC. */
    int repl_syncio_timeout; /* Timeout for synchronous I/O calls */
    // 复制的状态(服务器是从服务器时使用)
    int repl_state;          /* Replication status if the instance is a slave */
    // RDB 文件的大小
    off_t repl_transfer_size; /* Size of RDB to read from master during sync. */
    // 已读 RDB 文件内容的字节数
    off_t repl_transfer_read; /* Amount of RDB read from master during sync. */
    // 最近一次执行 fsync 时的偏移量
    // 用于 sync_file_range 函数
    off_t repl_transfer_last_fsync_off; /* Offset when we fsync-ed last time. */
    // 主服务器的套接字
    int repl_transfer_s;     /* Slave -> Master SYNC socket */
    // 保存 RDB 文件的临时文件的描述符
    int repl_transfer_fd;    /* Slave -> Master SYNC temp file descriptor */
    // 保存 RDB 文件的临时文件名字
    char *repl_transfer_tmpfile; /* Slave-> master SYNC temp file name */
    // 最近一次读入 RDB 内容的时间
    time_t repl_transfer_lastio; /* Unix time of the latest read, for timeout */
    int repl_serve_stale_data; /* Serve stale data when link is down? */
    // 是否只读从服务器?
    int repl_slave_ro;          /* Slave is read only? */
    // 连接断开的时长
    time_t repl_down_since; /* Unix time at which link with master went down */
    // 是否要在 SYNC 之后关闭 NODELAY ?
    int repl_disable_tcp_nodelay;   /* Disable TCP_NODELAY after SYNC? */
    // 从服务器优先级
    int slave_priority;             /* Reported in INFO and used by Sentinel. */
    // 本服务器(从服务器)当前主服务器的 RUN ID
    char repl_master_runid[REDIS_RUN_ID_SIZE+1];  /* Master run id for PSYNC. */
    // 初始化偏移量
    long long repl_master_initial_offset;         /* Master PSYNC offset. */


    /* Replication script cache. */
    // 复制脚本缓存
    // 字典
    dict *repl_scriptcache_dict;        /* SHA1 all slaves are aware of. */
    // FIFO 队列
    list *repl_scriptcache_fifo;        /* First in, first out LRU eviction. */
    // 缓存的大小
    int repl_scriptcache_size;          /* Max number of elements. */

    /* Synchronous replication. */
    list *clients_waiting_acks;         /* Clients waiting in WAIT command. */
    int get_ack_from_slaves;            /* If true we send REPLCONF GETACK. */
    /* Limits */
    int maxclients;                 /* Max number of simultaneous clients */
    unsigned long long maxmemory;   /* Max number of memory bytes to use */
    int maxmemory_policy;           /* Policy for key eviction */
    int maxmemory_samples;          /* Pricision of random sampling */


    /* Blocked clients */
    unsigned int bpop_blocked_clients; /* Number of clients blocked by lists */
    list *unblocked_clients; /* list of clients to unblock before next loop */
    list *ready_keys;        /* List of readyList structures for BLPOP & co */


    /* Sort parameters - qsort_r() is only available under BSD so we
     * have to take this state global, in order to pass it to sortCompare() */
    int sort_desc;
    int sort_alpha;
    int sort_bypattern;
    int sort_store;


    /* Zip structure config, see redis.conf for more information  */
    size_t hash_max_ziplist_entries;
    size_t hash_max_ziplist_value;
    size_t list_max_ziplist_entries;
    size_t list_max_ziplist_value;
    size_t set_max_intset_entries;
    size_t zset_max_ziplist_entries;
    size_t zset_max_ziplist_value;
    size_t hll_sparse_max_bytes;
    time_t unixtime;        /* Unix time sampled every cron cycle. */
    long long mstime;       /* Like 'unixtime' but with milliseconds resolution. */


    /* Pubsub */
    // 字典,键为频道,值为链表
    // 链表中保存了所有订阅某个频道的客户端
    // 新客户端总是被添加到链表的表尾
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */

    // 这个链表记录了客户端订阅的所有模式的名字
    list *pubsub_patterns;  /* A list of pubsub_patterns */

    int notify_keyspace_events; /* Events to propagate via Pub/Sub. This is an
                                   xor of REDIS_NOTIFY... flags. */


    /* Cluster */

    int cluster_enabled;      /* Is cluster enabled? */
    mstime_t cluster_node_timeout; /* Cluster node timeout. */
    char *cluster_configfile; /* Cluster auto-generated config file name. */
    struct clusterState *cluster;  /* State of the cluster */

    int cluster_migration_barrier; /* Cluster replicas migration barrier. */
    /* Scripting */

    // Lua 环境
    lua_State *lua; /* The Lua interpreter. We use just one for all clients */
    
    // 复制执行 Lua 脚本中的 Redis 命令的伪客户端
    redisClient *lua_client;   /* The "fake client" to query Redis from Lua */

    // 当前正在执行 EVAL 命令的客户端,如果没有就是 NULL
    redisClient *lua_caller;   /* The client running EVAL right now, or NULL */

    // 一个字典,值为 Lua 脚本,键为脚本的 SHA1 校验和
    dict *lua_scripts;         /* A dictionary of SHA1 -> Lua scripts */
    // Lua 脚本的执行时限
    mstime_t lua_time_limit;  /* Script timeout in milliseconds */
    // 脚本开始执行的时间
    mstime_t lua_time_start;  /* Start time of script, milliseconds time */

    // 脚本是否执行过写命令
    int lua_write_dirty;  /* True if a write command was called during the
                             execution of the current script. */

    // 脚本是否执行过带有随机性质的命令
    int lua_random_dirty; /* True if a random command was called during the
                             execution of the current script. */

    // 脚本是否超时
    int lua_timedout;     /* True if we reached the time limit for script
                             execution. */

    // 是否要杀死脚本
    int lua_kill;         /* Kill the script if true. */


    /* Assert & bug reporting */

    char *assert_failed;
    char *assert_file;
    int assert_line;
    int bug_report_start; /* True if bug report header was already logged. */
    int watchdog_period;  /* Software watchdog period in ms. 0 = off */
};
  • 命令请求的执行过程
  • 发送命令:客户端将命令转成协议文本发送给服务器
  • 读取命令:这里分为几个步骤。
    • 命令查找:客户端发送来的命令存储在redisClient的argc,argv中。根据这俩参数在命令字典中查找执行命令,并赋值给redisClient.cmd。
    • 执行预备操作:redisClient.cmd是否为空、参数是否合法、授权、
    • 命令调用:调用client->cmd->proc并产生回复。
    • 执行后续工作:慢日志、AOF、主从复制
    • 命令回复发送给客户端
    • 客户端接收回复并打印
  1. serverCron函数

默认100ms执行一次,管理服务器资源

  • 更新系统时间缓存
  • 更新lru时钟
  • 更新每秒执行命令次数
  • 更新服务器内存峰值
  • 检查shutdown_asap决定是否关闭服务器
  • 管理客户端:关闭超时客户端,客户端buf清理,关闭缓冲区超限制的客户端
  • 管理数据库:删除过期键,收缩dict表
  • 检查并执行REWRITEAOF
  • 持久化
  • 将AOF缓冲区写入AOF文件
  • 自增cronloops

多机数据库的实现

复制

  1. 复制的步骤
  • redis的复制分为两个步骤,同步(sync)和命令传播(commond propogate)。
  • 同步:从服务器向主服务器发送sync命令,主服务器生成rdb文件,并将生成rdb期间的数据库修改命令保存在缓冲区,然后都传给从服务器。同步浪费资源体现在:a.生成rdb耗费cpu,内存,i/o;b.传输rdb耗费网络资源;c.载入rdb会阻塞服务器。
  • 命令传播:主服务器收到的修改命令也发给从服务器执行。
  • redis2.8以前的复制缺陷:缺陷场景:主从断线之后的重新复制。实现方式:断线之后,主从重新走一遍同步和命令传播。缺陷原因:同步是很浪费资源的操作,应该尽量避免。
  • redis2.8以后用psync代替sync;psync支持完全重同步和部分重同步。
  • 部分重同步的实现:偏移量(同步的字节量),复制积压缓冲区(固定长度的队列),服务器运行id(判断是不是向同一个服务器发送的同步命令)。
  1. 复制的实现
  • 设置主服务器地址和端口。Slaveof 127.0.0.1 6379。从服务器保存主服务器地址和端口。
  • 建立套接字。从服务器根据保存的主服务器地址和端口号,建立套接字。
  • 从服务器发送ping命令。超时或者错误则重新建立套接字重试;返回“PONG”说明网络连接正常。
  • 身份验证。主从都没有设置身份验证或者设置一致才可以通过身份验证。
  • 从服务器发送监听端口,主服务器将其保存在redisClient的slave_listening_port;
  • 同步。从服务器向主服务发送psync命令。
  1. 心跳检测

从服务器每秒发送命令给主服务器,replconf ack offset,有三个作用

  • 检测链接状况
  • 辅助实现min-slaves选项。min-slaves-to-write 3,min-slaves-max-lag 10。表示从服务器少于3个,或者从服务延迟都大于等于10,主服务器将终止写命令。
  • 检测命令丢失

sentinel

负责redis高可用,检测主服务器下线,然后选举一个从服务器成为新的主服务器。

  1. 源码
// Sentinel 会为每个被监视的 Redis 实例创建相应的 sentinelRedisInstance 实例
// (被监视的实例可以是主服务器、从服务器、或者其他 Sentinel )
typedef struct sentinelRedisInstance {
    
    // 标识值,记录了实例的类型,以及该实例的当前状态
    int flags;      /* See SRI_... defines */
    
    // 实例的名字
    // 主服务器的名字由用户在配置文件中设置
    // 从服务器以及 Sentinel 的名字由 Sentinel 自动设置
    // 格式为 ip:port ,例如 "127.0.0.1:26379"
    char *name;     /* Master name from the point of view of this sentinel. */

    // 实例的运行 ID
    char *runid;    /* run ID of this instance. */

    // 配置纪元,用于实现故障转移
    uint64_t config_epoch;  /* Configuration epoch. */

    // 实例的地址
    sentinelAddr *addr; /* Master host. */

    // 用于发送命令的异步连接
    redisAsyncContext *cc; /* Hiredis context for commands. */

    // 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接
    // 仅在实例为主服务器时使用
    redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */

    // 已发送但尚未回复的命令数量
    int pending_commands;   /* Number of commands sent waiting for a reply. */

    // cc 连接的创建时间
    mstime_t cc_conn_time; /* cc connection time. */
    
    // pc 连接的创建时间
    mstime_t pc_conn_time; /* pc connection time. */

    // 最后一次从这个实例接收信息的时间
    mstime_t pc_last_activity; /* Last time we received any message. */

    // 实例最后一次返回正确的 PING 命令回复的时间
    mstime_t last_avail_time; /* Last time the instance replied to ping with
                                 a reply we consider valid. */
    // 实例最后一次发送 PING 命令的时间
    mstime_t last_ping_time;  /* Last time a pending ping was sent in the
                                 context of the current command connection
                                 with the instance. 0 if still not sent or
                                 if pong already received. */
    // 实例最后一次返回 PING 命令的时间,无论内容正确与否
    mstime_t last_pong_time;  /* Last time the instance replied to ping,
                                 whatever the reply was. That's used to check
                                 if the link is idle and must be reconnected. */

    // 最后一次向频道发送问候信息的时间
    // 只在当前实例为 sentinel 时使用
    mstime_t last_pub_time;   /* Last time we sent hello via Pub/Sub. */

    // 最后一次接收到这个 sentinel 发来的问候信息的时间
    // 只在当前实例为 sentinel 时使用
    mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
                                 we received a hello from this Sentinel
                                 via Pub/Sub. */

    // 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间
    // 只在当前实例为 sentinel 时使用
    mstime_t last_master_down_reply_time; /* Time of last reply to
                                             SENTINEL is-master-down command. */

    // 实例被判断为 SDOWN 状态的时间
    mstime_t s_down_since_time; /* Subjectively down since time. */

    // 实例被判断为 ODOWN 状态的时间
    mstime_t o_down_since_time; /* Objectively down since time. */

    // SENTINEL down-after-milliseconds 选项所设定的值
    // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
    mstime_t down_after_period; /* Consider it down after that period. */

    // 从实例获取 INFO 命令的回复的时间
    mstime_t info_refresh;  /* Time at which we received INFO output from it. */

    /* Role and the first time we observed it.
     * This is useful in order to delay replacing what the instance reports
     * with our own configuration. We need to always wait some time in order
     * to give a chance to the leader to report the new configuration before
     * we do silly things. */
    // 实例的角色
    int role_reported;
    // 角色的更新时间
    mstime_t role_reported_time;

    // 最后一次从服务器的主服务器地址变更的时间
    mstime_t slave_conf_change_time; /* Last time slave master addr changed. */

    /* Master specific. */
    /* 主服务器实例特有的属性 -------------------------------------------------------------*/

    // 其他同样监控这个主服务器的所有 sentinel
    dict *sentinels;    /* Other sentinels monitoring the same master. */

    // 如果这个实例代表的是一个主服务器
    // 那么这个字典保存着主服务器属下的从服务器
    // 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
    dict *slaves;       /* Slaves for this master instance. */

    // SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
    // 判断这个实例为客观下线(objectively down)所需的支持投票数量
    int quorum;         /* Number of sentinels that need to agree on failure. */

    // SENTINEL parallel-syncs <master-name> <number> 选项的值
    // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs; /* How many slaves to reconfigure at same time. */

    // 连接主服务器和从服务器所需的密码
    char *auth_pass;    /* Password to use for AUTH against master & slaves. */

    /* Slave specific. */
    /* 从服务器实例特有的属性 -------------------------------------------------------------*/

    // 主从服务器连接断开的时间
    mstime_t master_link_down_time; /* Slave replication link down time. */

    // 从服务器优先级
    int slave_priority; /* Slave priority according to its INFO output. */

    // 执行故障转移操作时,从服务器发送 SLAVEOF <new-master> 命令的时间
    mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */

    // 主服务器的实例(在本实例为从服务器时使用)
    struct sentinelRedisInstance *master; /* Master instance if it's slave. */

    // INFO 命令的回复中记录的主服务器 IP
    char *slave_master_host;    /* Master host as reported by INFO */
    
    // INFO 命令的回复中记录的主服务器端口号
    int slave_master_port;      /* Master port as reported by INFO */

    // INFO 命令的回复中记录的主从服务器连接状态
    int slave_master_link_status; /* Master link status as reported by INFO */

    // 从服务器的复制偏移量
    unsigned long long slave_repl_offset; /* Slave replication offset. */

    /* Failover */
    /* 故障转移相关属性 -------------------------------------------------------------------*/


    // 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
    // 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。
    // 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。
    char *leader;       /* If this is a master instance, this is the runid of
                           the Sentinel that should perform the failover. If
                           this is a Sentinel, this is the runid of the Sentinel
                           that this Sentinel voted as leader. */
    // 领头的纪元
    uint64_t leader_epoch; /* Epoch of the 'leader' field. */
    // 当前执行中的故障转移的纪元
    uint64_t failover_epoch; /* Epoch of the currently started failover. */
    // 故障转移操作的当前状态
    int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */

    // 状态改变的时间
    mstime_t failover_state_change_time;

    // 最后一次进行故障迁移的时间
    mstime_t failover_start_time;   /* Last failover attempt start time. */

    // SENTINEL failover-timeout <master-name> <ms> 选项的值
    // 刷新故障迁移状态的最大时限
    mstime_t failover_timeout;      /* Max time to refresh failover state. */

    mstime_t failover_delay_logged; /* For what failover_start_time value we
                                       logged the failover delay. */
    // 指向被提升为新主服务器的从服务器的指针
    struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */

    /* Scripts executed to notify admin or reconfigure clients: when they
     * are set to NULL no script is executed. */
    // 一个文件路径,保存着 WARNING 级别的事件发生时执行的,
    // 用于通知管理员的脚本的地址
    char *notification_script;

    // 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,
    // 需要执行的脚本的地址
    char *client_reconfig_script;

} sentinelRedisInstance;

/* Main state. */
/* Sentinel 的状态结构 */
struct sentinelState {

    // 当前纪元
    uint64_t current_epoch;     /* Current epoch. */

    // 保存了所有被这个 sentinel 监视的主服务器
    // 字典的键是主服务器的名字
    // 字典的值则是一个指向 sentinelRedisInstance 结构的指针
    dict *masters;      /* Dictionary of master sentinelRedisInstances.
                           Key is the instance name, value is the
                           sentinelRedisInstance structure pointer. */

    // 是否进入了 TILT 模式?
    int tilt;           /* Are we in TILT mode? */

    // 目前正在执行的脚本的数量
    int running_scripts;    /* Number of scripts in execution right now. */

    // 进入 TILT 模式的时间
    mstime_t tilt_start_time;   /* When TITL started. */

    // 最后一次执行时间处理器的时间
    mstime_t previous_time;     /* Last time we ran the time handler. */

    // 一个 FIFO 队列,包含了所有需要执行的用户脚本
    list *scripts_queue;    /* Queue of user scripts to execute. */

} sentinel;
  1. 启动并初始化sentinel
  • 启动命令:Redis-sentinel /path/sentinel.conf
  • 初始化服务器
  • 使用sentinel专用代码
  • 初始化sentinel状态。sentinelState{}
  • 初始化masters属性。根据配置文件来生成。
  • 创建两个链接主服务器的异步网络:一个命令链接;一个订阅链接。
  • 获取服务器信息。10s/次向主服务器发送info命令。获取主从信息。更新主从信息。从信息的保存结构如下:
  • 获取从服务器信息。获取从服务器信息。sentinel发现主服务器有从服务器时,保存从服务器并创建两个异步链接:命令链接和订阅链接。10s/次向从服务器发送命令获取信息更新本地存储。
  • 向主从发送信息。2s/次通过命令连接发送信息。发送频道_sentinel_:hello
  • 接收信息。接收频道 sentinel:hello(用于发现新的sentinel)
  • 更新sentinel字典。sentinelResidInstance结构中的sentinels保存这个主服务器的所有sentinel。接收到_sentinel_:hello频道的消息时,更新sentinel.
  • 创建sentinel之间的连接。(不创建订阅链接)
  1. 检测主观下线状态

sentinel每秒向它连接的实例发送ping命令。返回+pong、-loading、-masterdown是有效回复。其他回复或者指定时间没回复则认为无效。配置项down-after-milliseconds指定了sentinel判断主观下线时间长度。如果一个实例在down-after-milliseconds时间内连续向sentinel返回无效,则认为主观下线。

  • sentinelRedisInstance中的master属性有主观下线标识。
  • down-after-milliseconds是master下线的标准,也是master下slaves下线的标准,也是其他sentinel判断master下线的标准。
  • 多个sentinel设置同一个msater的down-after-milliseconds可能不同
  1. 客观下线

sentinel判断一个master已经主观下线,会询问其他sentinel,如果一定数量 的都认为这个master已经主观下线,则判定这个master客观下线。然后做故障转移。

  • 发送sentinel is-master-down-by-addr命令询问其他sentinel
  • sentinel配置中的quorum参数决定有几个sentinel判断下线才能算是下线
  • 当一个msater被判定为客观下线时,监视这个master的各个sentinel之间会选举一个为领头sentinel,做故障转移。
  • 故障转移:选一个先master、slaves同步新的master、旧的master成为新master的slave
  1. 选举领头sentinel

当一个主服务器下线后,在监管这个主服务器的sentinel中选举一个领头sentinel。用于故障转移


6. 故障转移
领头sentinel完成故障转移需要三个步骤:

  • 在从服务器中选一个成为新的主服务器。sentinel在从服务器中选举一个,向其发送命令slaveof no one,使之成为主服务器。发送slaveof no one明后后每秒发送info命令观察role是不是变成了master。
  • 让从服务器同步新的主服务器.
  • 让旧的主服务器成为新的主服务器的从服务器,当其上线时,成为从服务器。

集群

集群是redis的分布式数据库方案。通过分片实现数据共享。提供复制和故障转移功能。

// 保存连接节点所需的有关信息
typedef struct clusterLink {

    // 连接的创建时间
    mstime_t ctime;             /* Link creation time */

    // TCP 套接字描述符
    int fd;                     /* TCP socket file descriptor */

    // 输出缓冲区,保存着等待发送给其他节点的消息(message)。
    sds sndbuf;                 /* Packet send buffer */

    // 输入缓冲区,保存着从其他节点接收到的消息。
    sds rcvbuf;                 /* Packet reception buffer */

    // 与这个连接相关联的节点,如果没有的话就为 NULL
    struct clusterNode *node;   /* Node related to this link if any, or NULL */

} clusterLink;

// 节点状态
struct clusterNode {

    // 创建节点的时间
    mstime_t ctime; /* Node object creation time. */

    // 节点的名字,由 40 个十六进制字符组成
    // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */

    // 节点标识
    // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    // 以及节点目前所处的状态(比如在线或者下线)。
    int flags;      /* REDIS_NODE_... */

    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch; /* Last configEpoch observed for this node */

    // 由这个节点负责处理的槽
    // 一共有 REDIS_CLUSTER_SLOTS / 8 个字节长
    // 每个字节的每个位记录了一个槽的保存状态
    // 位的值为 1 表示槽正由本节点处理,值为 0 则表示槽并非本节点处理
    // 比如 slots[0] 的第一个位保存了槽 0 的保存情况
    // slots[0] 的第二个位保存了槽 1 的保存情况,以此类推
    unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */

    // 该节点负责处理的槽数量
    int numslots;   /* Number of slots handled by this node */

    // 如果本节点是主节点,那么用这个属性记录从节点的数量
    int numslaves;  /* Number of slave nodes, if this is a master */

    // 指针数组,指向各个从节点
    struct clusterNode **slaves; /* pointers to slave nodes */

    // 如果这是一个从节点,那么指向主节点
    struct clusterNode *slaveof; /* pointer to the master node */

    // 最后一次发送 PING 命令的时间
    mstime_t ping_sent;      /* Unix time we sent latest ping */

    // 最后一次接收 PONG 回复的时间戳
    mstime_t pong_received;  /* Unix time we received the pong */

    // 最后一次被设置为 FAIL 状态的时间
    mstime_t fail_time;      /* Unix time when FAIL flag was set */

    // 最后一次给某个从节点投票的时间
    mstime_t voted_time;     /* Last time we voted for a slave of this master */

    // 最后一次从这个节点接收到复制偏移量的时间
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */

    // 这个节点的复制偏移量
    long long repl_offset;      /* Last known repl offset for this node. */

    // 节点的 IP 地址
    char ip[REDIS_IP_STR_LEN];  /* Latest known IP address of this node */

    // 节点的端口号
    int port;                   /* Latest known port of this node */

    // 保存连接节点所需的有关信息
    clusterLink *link;          /* TCP/IP link with this node */

    // 一个链表,记录了所有其他节点对该节点的下线报告
    list *fail_reports;         /* List of nodes signaling this as failing */

};
typedef struct clusterNode clusterNode;


// 集群状态,每个节点都保存着一个这样的状态,记录了它们眼中的集群的样子。
// 另外,虽然这个结构主要用于记录集群的属性,但是为了节约资源,
// 有些与节点有关的属性,比如 slots_to_keys 、 failover_auth_count 
// 也被放到了这个结构里面。
typedef struct clusterState {

    // 指向当前节点的指针
    clusterNode *myself;  /* This node */

    // 集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;

    // 集群当前的状态:是在线还是下线
    int state;            /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */

    // 集群中至少处理着一个槽的节点的数量。
    int size;             /* Num of master nodes with at least one slot */

    // 集群节点名单(包括 myself 节点)
    // 字典的键为节点的名字,字典的值为 clusterNode 结构
    dict *nodes;          /* Hash table of name -> clusterNode structures */

    // 节点黑名单,用于 CLUSTER FORGET 命令
    // 防止被 FORGET 的命令重新被添加到集群里面
    // (不过现在似乎没有在使用的样子,已废弃?还是尚未实现?)
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */

    // 记录要从当前节点迁移到目标节点的槽,以及迁移的目标节点
    // migrating_slots_to[i] = NULL 表示槽 i 未被迁移
    // migrating_slots_to[i] = clusterNode_A 表示槽 i 要从本节点迁移至节点 A
    clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];

    // 记录要从源节点迁移到本节点的槽,以及进行迁移的源节点
    // importing_slots_from[i] = NULL 表示槽 i 未进行导入
    // importing_slots_from[i] = clusterNode_A 表示正从节点 A 中导入槽 i
    clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];

    // 负责处理各个槽的节点
    // 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理
    clusterNode *slots[REDIS_CLUSTER_SLOTS];

    // 跳跃表,表中以槽作为分值,键作为成员,对槽进行有序排序
    // 当需要对某些槽进行区间(range)操作时,这个跳跃表可以提供方便
    // 具体操作定义在 db.c 里面
    zskiplist *slots_to_keys;

    /* The following fields are used to take the slave state on elections. */
    // 以下这些域被用于进行故障转移选举

    // 上次执行选举或者下次执行选举的时间
    mstime_t failover_auth_time; /* Time of previous or next election. */

    // 节点获得的投票数量
    int failover_auth_count;    /* Number of votes received so far. */

    // 如果值为 1 ,表示本节点已经向其他节点发送了投票请求
    int failover_auth_sent;     /* True if we already asked for votes. */

    int failover_auth_rank;     /* This slave rank for current auth request. */

    uint64_t failover_auth_epoch; /* Epoch of the current election. */

    /* Manual failover state in common. */
    /* 共用的手动故障转移状态 */

    // 手动故障转移执行的时间限制
    mstime_t mf_end;            /* Manual failover time limit (ms unixtime).
                                   It is zero if there is no MF in progress. */
    /* Manual failover state of master. */
    /* 主服务器的手动故障转移状态 */
    clusterNode *mf_slave;      /* Slave performing the manual failover. */
    /* Manual failover state of slave. */
    /* 从服务器的手动故障转移状态 */
    long long mf_master_offset; /* Master offset the slave needs to start MF
                                   or zero if stil not received. */
    // 指示手动故障转移是否可以开始的标志值
    // 值为非 0 时表示各个主服务器可以开始投票
    int mf_can_start;           /* If non-zero signal that the manual failover
                                   can start requesting masters vote. */

    /* The followign fields are uesd by masters to take state on elections. */
    /* 以下这些域由主服务器使用,用于记录选举时的状态 */

    // 集群最后一次进行投票的纪元
    uint64_t lastVoteEpoch;     /* Epoch of the last vote granted. */

    // 在进入下个事件循环之前要做的事情,以各个 flag 来记录
    int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */

    // 通过 cluster 连接发送的消息数量
    long long stats_bus_messages_sent;  /* Num of msg sent via cluster bus. */

    // 通过 cluster 接收到的消息数量
    long long stats_bus_messages_received; /* Num of msg rcvd via cluster bus.*/

} clusterState;

  1. cluster meet命令

客户端向节点A发送meet命令,指定节点B的ip和端口。将B加入到A的集群。

  • 向A发命令cluster meet B.
  • A为B创建一个clusterNode结构并保存在dict中
  • A向B发一个meet消息
  • B为A创建一个clusterNode结构并保存在dict中
  • B返回A一个PONG消息
  • A收到PONG返回一个PING
  • B收到PING,握手结束
  1. 槽指派

redis通过分片的方式保存键值对,集群的整个数据库被分成16384个槽。每个节点处理一定数量的槽,每个key一定属于其中一个槽。

  • 向节点指派槽的命令:cluster addslots 1 2 3 4
  • clusterNode中Slots数组保存当前节点所有槽信息。Slots是二进制数组。
  • 传播节点的槽指派信息:集群节点会相互发送自己被指派的槽,节点收到其他几点的槽信息会更新相应的dict中的clusterNode结构。
  • clusterState中Slots数组保存所有槽信息。
  • cluster addslots 1 2 3 4的实现:首先遍历clusterState的Slots判断命令中的槽还没有被指派。然后遍历命令中的槽,设置clusterState的Slots和clusterNode的slots。
  1. 集群中执行命令
  • 接收命令的节点计算命令要处理的数据库属于哪个槽,如果是指派给自己的就直接执行;如果不是就向客户端返回一个moved错误指引客户端至正确节点执行。
  • 计算给定key的槽:任何一个key通过这个算法就可以得出一个介于0-16383之间的槽。crc16(key)&16383
  • 集群中的节点只能使用0号数据库。
  1. 重新分片
    集群管理软件redis-trib负责执行。
  • redis-trib向目标节点发送导入命令,并将slot指向目标节点
  • redis-trib向源节点发送导出命令
  • redis-trib从源节点获取导出slot的键,导入到目标节点
  • 键迁移结束。向集群中任意节点发送消息,slot已指向目标节点。
  1. ask错误
  • ask错误:正在被转移的key被查询时,会发生查询转移
  • Cluster setslot importing命令:clusterState的Importing_lots_from记录着导入的槽
  • Cluster setslot migrating命令:clusterState的migrating_slots_to记录着正在导出的槽。ask错误的实现
  1. publish的实现

不是直接向所有节点广播,而是通过一个节点向所有节点广播。因为redis中有一个潜规则:各节点通过消息通信。

typedef struct {

    // 频道名长度
    uint32_t channel_len;

    // 消息长度
    uint32_t message_len;

    // 消息内容,格式为 频道名+消息
    // bulk_data[0:channel_len-1] 为频道名
    // bulk_data[channel_len:channel_len+message_len-1] 为消息
    unsigned char bulk_data[8]; /* defined as 8 just for alignment concerns. */

} clusterMsgDataPublish;

发布和订阅

可理解为基于topic的生产消费模型,其结构如下

  • redisServer{
    Dict *pubsub_channels; //key是订阅频道,value是客户端(链表结构)
    List *pubsub_patterns //模式订阅关系pubsubPatterns结构,记录客户端订阅的模式
    }

事物

事物的实现

事物的实现包含三个阶段:事物开始(multi)、命令入队、命令执行。redisClient中的multiState结构如下,multi命令之后的命令都保存在队列multiState.commands中,遇到exec命令时,顺序执行队列中的命令。

/*
 * 事务状态
 */
typedef struct multiState {

    // 事务队列,FIFO 顺序
    multiCmd *commands;     /* Array of MULTI commands */

    // 已入队命令计数
    int count;              /* Total number of MULTI commands */
    int minreplicas;        /* MINREPLICAS for synchronous replication */
    time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;

watch命令的实现

watch是redis乐观锁,可在exec执行之前监视任意数量的数据库键,并在exec执行之后检查是否有被修改。如果有,拒绝执行事物。

  • redisDb{
    Dict *watched_keys //key是被监视的键,value是客户端链表
    }

当有修改数据库的命令执行时,调用touchWatchKey函数,检查redisDb.watched_keys中的keys是否有修改,如果有,则标记redisClient.REDIS_DIRTY_CAS,执行exec命令时会检查redisClient.REDIS_DIRTY_CAS来决定是否执行事物。

  • redis事物的ADIC
    • 原子 :redis事物没有回滚,如果中间某个命令执行失败,其他命令还是会继续执行。
    • 一致:三方面来保证。入队前命令合法检查,执行时的错误检查,宕机恢复
    • 隔离:单进程单线程来保证
    • 持久:redis的Aof和Rdb来保证

参考文献

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值