redis服务器怎么订阅频道_Redis 设计与实现 学习笔记

源码版本:redis 4.0.6

前置工作

配置vscode 调试源码:https://www.cxc233.com/blog/e1d54234.html

要点提要

  • 1.pub/sub
  • 2.Redis哨兵模式(采用Raft算法)
  • 3.数据结构相关
  • 4.LRU实现
  • 5.Redis事务
  • 6.关于持久化设计

// TODO + 7.内存碎片整理 + 8.redis 线程模型及实现 + 9.哨兵raft算法实现

1.pub/sub (发布/订阅)

从Redis实现来看pub/sub

订阅频道redisServer结构存储如下图:

32f40c4e926ed79d4875f70f33e142ac.png

订阅模式消息传递关系如下图:

35434d3bd50e3135f425a22092bf4021.png

订阅模式redisServer结构存储如下图:

7ee7447c7ded761646ebdf3eccd86596.png

订阅的channels 与 patterns 均存在于server.h/redisServer中

struct redisServer{
    // 字典,键为频道,值为链表
    // 链表中保存了所有订阅某个频道的客户端
    // 新客户端总是被添加到链表的表尾
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */
    // 这个链表记录了客户端订阅的所有模式的名字
    list *pubsub_patterns;  /* A list of pubsub_patterns */
}

redisServer 中每个pubsub_pattern 节点的结构如下:

struct pubsubPattern {

    // 订阅模式的客户端
    redisClient *client;

    // 被订阅的模式
    robj *pattern;

} pubsubPattern;
struct client{
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
}

向某个channel发布信息(message)伪代码如下:

def pubsubPublishMessage(channel, message):
    for client in redisServer.pubsub_channels[channel]:
        send_message(client, message)

    for pattern, client in redisServer.pubsub_patterns:
        if match(pattern, channel):
            send_message(client, message)

订阅频道 伪代码描述如下:

def pubsubSubscribeChannel(client, channel):
    # 将channel添加到client.pubsub_channels

    # 将channel添加到server.pubsub_channels

    # 将client添加到server.pubsub_channels[channel]链表的末尾

    # 比如原来是server.pubsub_channels[channel] = [c1,c2],添加c3
    # 则变为 [c1,c2,c3]

订阅模式 伪代码如下:

def pubsubSubscribePattern(client, pattern):
    # 如果pattern没有在client[pubsub_patterns] 里则将该模式添加到末尾

    # 将pattern添加到server.pubsub_patterns

    # 对客户端进行回复

2.Redis哨兵(Sentinel)模式(采用raft算法) 保证高可用性

raft的演示动画:http://thesecretlivesofdata.com/

Sentinel 系统用于管理多个 Redis 服务器(instance),该系统会执行三个任务:

1.监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常,raft算法的心跳机制。

2.提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

3.自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

关于服务器下线的判断: 主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。 客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。

发生故障迁移时Sentinel的选举leader流程:

1.Sentinel 认定master 客观下线,Sentinel会先看看自己有没有投过票,如果投过票给了其他Sentinel,则2倍故障转移时间内自己仍然是Follower.

2.如果还没有投过票则成为Candidate (候选人)

3.成为Candidate做以下操作

  • 1) 将故障转移状态更新为start
  • 2) 当前epoch + 1 ,相当于进入一个新term,在Sentinel中epoch就是Raft协议中的term。
  • 3) 更新自己的超时时间为当前时间随机加上一段时间,随机时间为1s内的随机毫秒数。 用于当出现没有拿到足够的票数成为leader的时候,重新成为Candidate(候选人),重复竞选Leader步骤。
  • 4) 向其他节点发送is-master-down-by-addr命令请求投票。命令会带上自己的epoch。
  • 5) 在Sentinel 中,投票的方式是把自己master 结构体里的leader 和 leader_epoch 改成投给的Sentinel 和它的epoch.
  • 6)Candidate不断的统计自己的票数,直到他发现认同他成为Leader的票数超过一半而且超过它配置的quorum( sentinel.conf 可以做相关配置)
  • 7)如果在一个选举时间内,Candidate没有获得超过一半且超过它配置的quorum的票数,自己的这次选举就失败了
  • 8)如果在一个epoch内,没有一个Candidate获得足够成为leader的票数。那么等待超过2倍故障转移的超时时间后,Candidate增加epoch重新投票。
  • 9)与Raft协议不同,Leader并不会把自己成为Leader的消息发给其他Sentinel。而是通过 sentinel:hello(每个sentinel 都对这个channel进行了订阅) 这个channel来对主服务器的信息进行传播 sentinel:hello 里面包含了完整的master 配置,如果如果接收的Sentinel 具有给定master 比接收到的信息还要早的话,那么立即更新配置,从而达到了把leader信息进行传播。
# 对给定实例执行定期操作
def sentinelHandleRedisInstance(instance)

    # 判断Instance是否处于主观下线(SDOWN)的状态
    sentinelCheckSubjectivelyDown(instance)

    # 对主服务器进行处理
    if instance->flags & SRI_MASTER:
        # 判断是否是客观下线
        sentinelCheckObjectivelyDown(instance)

        # 如果主服务器进入了客观下线的状态,那么开始故障转移
        if(sentinelStartFailoverIfNeeded(instance))

        # 强制向其他 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
        # 刷新其他 Sentinel 关于主服务器的状态
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);

        # 执行故障转移
        sentinelFailoverStateMachine(instance)

3.数据结构相关

平时对redis 的直接操作如下:

127.0.0.1:6379> set foo "str"
OK
127.0.0.1:6379> type foo
string
127.0.0.1:6379> OBJECT ENCODING foo
"embstr"
127.0.0.1:6379> RPUSH lst 1 2 3
(integer) 9
127.0.0.1:6379> type lst
list
127.0.0.1:6379> OBJECT ENCODING lst
"quicklist"

是一个对key 和 value 的操作, key 只能是字符串对象,而value可以是 字符串对象、列表对象、哈希对象、集合对象、有序集合对象

五种对象的底层数据结构

字符串对象

1.simple dynamic string (SDS) 简单动态字符串。

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 记录buf数组中已经使用的字节数量 */
    uint64_t alloc; /* 分配的总字节数 */
    unsigned char flags; /* 用0、1、2、3、4代表类型 */
    char buf[];
};

C字符串只能做为字符串字面量。redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值,SDS还被用来做缓冲区,AOF模块中的AOF缓冲区以及客户端状态的输入缓冲区都是由SDS实现的。 优点 SDS也存在惰性释放的规则,避免了缩短字符串时候要内存进行重新分配的操作。但同时也提供了API在有需要时真正释放 1)常数复杂度获取字符串长度 2)二进制安全如何保证? 读入怎么样读取就是怎么样 3)杜绝了缓冲区的情况,有free字段如果不够则进行扩容 4)修改字符串减少内存需要重新分配的次数。

列表对象

2.链表 adlist.h adlist.c redis实现的是双端链表,头指针和尾指针都是指向NULL所以是无环的链表。void* 指针来保存节点的值,可以通过dup,free,match三个属性为节点设置值类型的特定函数,所以链表可以用于不同类型的值。

/*
 * 双端链表结构
 */
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;
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

哈希对象

3.字典 字典底层用哈希表实现,每个字典有两个哈希表,一个平时使用,另一个仅在rehash时候使用 采用链地址法解决了哈希冲突。链地址法其实是将相同元素构成一个同义词的单链表。

  • rehash
    • 内存不足时扩容会淘汰大量的key
    • scan在扩容和缩容时会导致key清理不彻底
      • 美团通过改变scan方法,如果在进行rehash则通过高位进1的方式来扫描key

dict.h/dict

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;

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

    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

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

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

} dictht;

/*
 * 哈希表节点
 */
typedef struct dictEntry {

    // 键
    void *key;

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

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

} dictEntry;

4.压缩列表 (ziplist.h)用来优化list 压缩列表是一种为节约内存而开发的顺序型数据结构,当一个列表键只包含少量列表项,并且列表项要么是小正整数,要么是比较短的字符串,那么redis则会使用压缩列表来做列表键的底层实现。

quicklist 则是Redis对外暴露的list数据类型,quicklist的每个节点都是一个ziplist。

quicklist.h/quicklist定义:

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklistLZF {
    unsigned int sz; /* LZF size in bytes*/
    char compressed[];
} quicklistLZF;
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

ziplist本身也是一个能维持数据项先后顺序的列表(按插入位置),而且是一个内存紧缩的列表(各个数据项在内存上前后相邻)。比如,一个包含3个节点的quicklist,如果每个节点的ziplist又包含4个数据项,那么对外表现上,这个list就总共包含12个数据项。 quicklist 这样设计是一个空间和时间的折中: 1.双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。 2.ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。

集合对象与有序集合对象

5.跳跃表 redis 有序集合的实现之一。每个跳跃表节点层高是1到32的随机数。 跳跃表结构 在 server.h内进行定义

//跳跃表节点
typedef struct zskiplistNode {
    sds ele;
    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;

内存编码数据结构 整数集合与压缩列表

6.整数集合 (intset.h) 底层实现为数组,这个数组以有序、无重复的方式保存集合元素。在有需要的时候会根据添加元素的类型改变这个数组的类型。 升级操作为整数操作带来灵活性并尽可能的节约了内存,如果只存在int16_t和int32_t类型,那么整数集合则不会升级到int64_t。整数集合只支持升级不支持降级。

7.对象 (object.c 【redis的对象系统实现】)

typedef struct redisObject {
    //类型
    unsigned type:4;
    //编码模式
    unsigned encoding:4;
    // 用8位存储最近访问次数和16位来存储最近访问时间
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    //引用计数
    int refcount;
    //指向实际值的指针
    void *ptr;
} robj;

redis 中每个键值对的键和值都是一个对象。redis共有字符串、列表、哈希、集合、有序集合五种对象。每种对象至少都有两种或者以上的编码方式。不同的编码方式可以在不同的使用场景上优化对象的使用效率。

Redis 会根据不同的使用场景来对一个对象进行不同的编码,例如type 都为list,因为ziplist 比 双端列表更节约内存,并且在元素较少的时候,内存以连续块方式保存的ziplist 比起双端列表可以更快被载入到缓存中。随着列表对象包含元素越来越多,ziplist保存元素的优势就逐渐消失,对象就会讲底层实现从ziplist转向功能更强也更适合大量元素保存的双端列表上面。quicklist是redis做出的又一个折中方案,上面已经提及。

redis 会共享值为0到9999的字符串。 redis对象用引用计数器来实现内存的回收。 对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间,可以回收长期不用的对象。

4.LRU具体实现(evict.c 内存淘汰机制) 重点考虑了性能和内存

LRU算法:最近最久未使用 Least Recent Used 假设内存只能容纳3个页大小,按照 7 0 1 2 0 3 0 4 的次序访问页。假设内存按照栈的方式来描述访问时间,在上面的,是最近访问的,在下面的是,最远时间访问的,LRU就是这样工作的。

hashmap的实现方式需要额外的空间去存储next 和 prev 指针,会牺牲较大的内存空间。Redis之所以不使用真正的LRU实现,就是因为它需要更多的内存。redis 采用的做法则是通过采样的方式取若干key ,通过访问的时间进行排序后,淘汰掉最不常使用的key 并且redis 通过server.maxmemory_samples 来选取固定采样数目的key。越大则越接近严格的LRU算法。 Redis的LRU算法是“基于采样数据”的处理,而不是针对全部数据。 可以通过 CONFIG SET maxmemory-samples 命令来设置采样的样本。 可以通过redis.conf 进行maxmemory(最大内存)的配置

redis 实现的LRU策略: 1.noeviction:不淘汰数据,由于内存已经用尽,此时会直接返回客户端报错信息。 2.allkeys-lru:对所有的key进行LRU算法淘汰 3.volatile-lru:针对设置了过期时间的数据进行LRU数据淘汰。 4.allkeys-random:随机从所有数据中抽取key进行淘汰。 5.volatile-random:随机从所有设置了过期时间的数据中进行淘汰。 6.volatile-ttl:从设置了缓存过期时间的数据中,抽取TTL时间更短的数据进行淘汰。

server.c

//执行客户端命令的时候如果需要会进行内存释放。
int processCommand(client *c) {
    //判断是否有内存限制并进行淘汰
    if (server.maxmemory) {
        ...
        //释放需要释放的内存
        int retval = freeMemoryIfNeeded();
        ...
    }
}

server.c 中freeMemoryIfNeeded()方法中关于LRU实现部分 伪代码描述:

// 根据配置的样本数量,随机抽取待抽查数据集(dict或者expires表)中的数据。
count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);

// 从这些样本数据中删除其中空闲时间最长的数据(bestkey)。

// 每次要决定哪个数据被淘汰时,首先都要扫描出maxmemory-samples个数据,在从中选择要淘汰的key

缓存最重要的就是命中。 4.0后新的淘汰模型 LFU Least Frequently Used eviction mode 最少使用淘汰模型 尝试跟踪最少使用的项目来对key进行淘汰。 核心思想:如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小 是为了解决在LRU算法中,一个item虽然最近刚被访问,但是实际上在未来很少会被使用到,并且没有一个过期时间,那么会存在一个风险就是会淘汰掉在未来有更高机会被访问的item。

有两种可选的模式 volatile-lfu :采用接近LFU算法在有过期时间的key中进行淘汰 allkeys-lfu 采用接近LFU的算法淘汰所有的key值,无论有无过期时间

5.事务处理过程 (multi.c事务的实现)

DISCARD 取消事务 EXEC 执行事务 MULTI 开启事务 UNWATCH 取消对key的观测 WATCH 观测某个变量是否在事务执行的时候被其他客户端改变 ,采用乐观锁 WATCH 事务执行失败,如果被破坏则打开CLIENT_DIRTY_CAS,说明事务安全性已经被破坏,事务执行失败。例子:

d12c2be5195b04dfc85b6cca248d0c2f.png

通过watched_keys 字典来保存数据库被监视的key.

MULTI 启动事务 ,但是事务不允许嵌套 EXEC 则执行事务,如果事务中间出现了错误的命令则退出事务。

server.c 的processCommand方法执行事务片段的代码

if (c->flags & CLIENT_MULTI &&
    c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
    c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
    //将命令入队
    queueMultiCommand(c);
    addReply(c,shared.queued);
} else {
    //执行命令
    call(c,CMD_CALL_FULL);
    c->woff = server.master_repl_offset;
    // 处理那些解除了阻塞的键
    if (listLength(server.ready_keys))
        handleClientsBlockedOnLists();
}
return C_OK;

Redis Script 能做到事务所能做到的任何事,并且比事务做的更好。那为什么还要留着事务呢?原因是事务在2.6以前已经存在了很长的时间并且即使不使用redis script 也能避免竞争条件,特别也是因为事务实现的复杂性更小,更符合语义性。

Redis 的事务不支持回滚

如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行。 为什么不支持回滚? 1.只有在使用错误的语法调用时才会失败Redis命令,或者对于持有错误数据类型的键,Redis命令可能会失败:这意味着实际上失败的命令是编程错误的结果,所以操作错误一般在开发时候就会被避免而不会在生产环境中出现。 2.使内部进行了简化并且运行速度更快因为不需要回滚

6.关于持久化 设计

RDB

RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。

RDB文件结构

完整的RDB包含如下5个部分: 这里实际文件保存的是二进制的数据,只是为了方便起见用字符串代替

| Redis | Db_version | database | EOF | Check_sum |

1.最开头通过保存 REDIS 5个字符程序可以载入文件时候快速检查是否为RDB文件。 2.db_version 部分 4个字节,保存了RDB文件的版本号。 3.database部分可以保存人意多个非空数据库。每个非空数据库都可以保存为selectDB、db_number、key_value_pairs三个部分 (1)selectDB 常量长度为1字节。当程序遇到该值的时候知道接下来将要读取的是一个数据库号码 (2)db_number 保存着一个数据库号码,根据号码的大小不同,长度可以分为1字节、2字节或者5字节。读入后服务器调用select命令根据数据库号码进行数据库切换。 (3)key_value_pairs 部分都保存了一个或以上的键值对。如果带过期时间,则过期时间也会被保存在里面。不带过期时间的话在RDB中以 type | key | value 三个部分组成 不同的数据类型RDB会用不同的编码方式进行保存。 Redis 本身对RDB文件检查工具 redis-check-dump.

RDB文件可用于保存和还原Redis服务器所有数据库中的所有键值对数据。(持久化)

AOF持久化 (具体实现位于aof.c)

与RDB持久化的区别: RDB持久化通过保存数据库中的键值对来记录数据库状态,而AOF是通过保存Redis服务器所执行的 写命令 来记录数据库状态。

服务器启动的时候可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。

AOF持久化的实现分为一下步骤:

1.WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。命令追加到aof_buf缓冲区中。

2.SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。写入到磁盘与同步。现代操作系统每次保存数据都会等待缓冲区被填满后或者是超过制定时限才真正的将缓冲区的数据写入磁盘。虽然提高了效率但是如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。 可以通过redis.conf的 appendfsync选项来实现指定同步时机,默认是everysec(上次同步时间距离现在超过1s则进行同步)。

一般情况下丢失的数据不超过 2 秒钟。

AOF重写 (并无需对现有的AOF文件进行任何读入、分析或者写入操作,只通过数据库中的键值对来实现)

为什么需要aof重写,因为如果每一条命令都存入到aof文件中的话,在经过一段时间后aof文件的体积就会过大。 所以Redis提供了AOF文件重写功能 (bgreweiteaof命令实现重写) 会创建一个新的AOF文件来替代现有的,但是新的AOF文件不会包含任何浪费空间的冗余命令,因为它只包含了还原当前数据库状态所 必须的命令

AOF后台重写

在执行这个aof重写的时候, 调用者线程会被阻塞。所以需要后台重写来规避这个问题。但是又会遇到数据库不一致问题,则用AOF重写缓冲区来解决此问题 启动后台重写之后,redis服务器会维护一个AOF重写缓冲区。记录在子进程创建AOF文件期间的所有写命令,然后子进程完成工作后将所有内容添加到新AOF文件末尾,使新旧两个AOF文件保存的数据库状态一致。最后通过替换旧的AOF文件来完成重写功能。

1.重写触发条件可以由用户通过调用 BGREWRITEAOF 手动触发。 2.服务器在 AOF 功能开启的情况下, 会维持以下三个变量: (1)记录当前 AOF 文件大小的变量 aof_current_size 。 (2)记录最后一次 AOF 重写之后, AOF 文件大小的变aof_rewrite_base_size 。 (3)增长百分比变量 aof_rewrite_perc(默认情况下增长比为100%) 。

每次当 serverCron 函数执行时 则会检查是否满足以下条件来进行重写: (1)没有 BGSAVE 命令在进行。 (2)没有 BGREWRITEAOF 在进行。 (3)当前 AOF 文件大小大于 server.aof_rewrite_min_size (默认值为 1 MB)。 (4)当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比。

AOF保存模式

对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下: 1.不保存(AOF_FSYNC_NO):写入和保存都由主进程执行,两个操作都会阻塞主进程。 2.每一秒钟保存一次(AOF_FSYNC_EVERYSEC):写入操作由主进程执行,阻塞主进程。保存操作由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。 3.每执行一个命令保存一次(AOF_FSYNC_ALWAYS):和模式 1 一样。 默认情况下是采用模式2进行保存。

AOF文件读取与数据还原

AOF 文件保存了 Redis 的数据库状态, 而文件里面包含的都是符合 Redis 通讯协议格式的命令文本。 这也就是说, 只要根据 AOF 文件里的协议, 重新执行一遍里面指示的所有命令, 就可以还原 Redis 的数据库状态了。

Redis 读取 AOF 文件并还原数据库的详细步骤如下:

1.创建一个不带网络连接的伪客户端(fake client)。 2.读取 AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。 3.根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。 4.执行 2 和 3 ,直到 AOF 文件中的所有命令执行完毕。

因为 Redis 的命令只能在客户端的上下文中被执行,所以这里需要一个伪客户端来执行命令,这些命令来自于aof文件。

伪代码实现:

def READ_AND_LOAD_AOF():

    # 打开并读取 AOF 文件
    file = open(aof_file_name)
    while file.is_not_reach_eof():

        # 读入一条协议文本格式的 Redis 命令
        cmd_in_text = file.read_next_command_in_protocol_format()

        # 根据文本命令,查找命令函数,并创建参数和参数个数等对象
        cmd, argv, argc = text_to_command(cmd_in_text)

        # 执行命令
        execRedisCommand(cmd, argv, argc)

    # 关闭文件
    file.close()

慢查询

配置 slowlog-log-slower-than 超过多少微妙则进行记录 配置 slowlog-max-len 慢查询列表

redis-cli> slowlog get 

1) id

2) time

3) duration

4) command + 参数

redis-cli> slow get // 将慢查询日志进行持久化 
redis-cli> slowlog len // 慢查询日志长度 
redis-cli> slowlog reset // 慢查询日志重置,实际是对列表进行清空

redis 命令

原生批量命令是原子的,pipeline 不是原子操作 原生批量命令是一个命令对应多个key , 而Pipeline 支持多个命令 原生批量命令是Redis服务端 支持的,Pipeline 需要服务端和客户端共同实现

fork 操作十分耗时,延迟几秒可能对线上的延迟非常明显,redis在做RDB 和 AOF重写时必不可少的操作就是fork操作创建子进程。优化建议: + 建议redis 线上每个redis实例内存控制在10GB内。 + 优先使用高效支持fork操作的虚拟化技术,避免使用xen + 合理配置linux 分配策略,避免物理内存不足导致fork 失败。 + 降低fork的操作频率,如适度放宽AOF自动触发时机,避免不必要的全量复制。

Redis 适用场景

1.消息队列:List 类型是双向列表,适合做消息队列。
2.计数器:Redis是内存数据库,能支持计数器频繁的读写操作。
3.共同好友:用Set类型求交集就能很容易知道两个用户的共同好友。
4.排行榜:有序集合可以快速的计算出top的数据
5.分布式 Session:多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。
6.通过setNX来防止频繁操作

redis 监控

相关官方文档: https://redis.io/topics/latency-monitor https://redis.io/topics/latency https://redis.io/commands/slowlog

  • linux watch 命令 对redis 进行实时监控
watch -n 1 -d "redis-cli -h <host> -p <port> info |grep -e ..."
  • info 可以查看redis 的信息
  • 操作系统Swap
redis cli -h <host> -p <port> info |grep process_id
cat /proc/<pid>/smaps | grep "Swap"

如果发现大量页被swap,则可以用vmstat和iostat进一步追查原因

源码阅读

http://blog.huangz.me/diary/2014/how-to-read-redis-source-code.html 黄建宏blog:http://blog.huangz.me/index.html https://www.zhihu.com/question/28677076

  • 内存编码数据结构实现(Redis 所特制的结构)
  • 六种数据类型实现
  • 数据库实现相关代码
  • Redis 的数据库实现。 redis.h 文件中的 redisDb 结构, 以及 db.c 文件。
  • Redis 的数据库通知功能实现代码。notify.c
  • Redis 的 RDB 持久化实现代码。rdb.h 和 rdb.c
  • Redis 的 AOF 持久化实现代码。aof.c
  • redis服务器及客户端
  • Redis 的事件处理器实现(基于 Reactor 模式)。ae.c ,以及任意一个 ae_*.c 文件(取决于你所使用的多路复用库)
  • Redis 的网络连接库,负责发送命令回复和接受命令请求, 同时也负责创建/销毁客户端, 以及通信协议分析等工作。networking.c
  • 单机 Redis 服务器的实现。 redis.c 和 redis.h
  • 多机集群
  • replication.c 复制功能的实现代码。
  • Redis Sentinel 的实现代码。sentinel.c
  • cluster.c Redis 集群的实现代码。 集群总线(cluster bus)端口:客户端端口+ 10000 Redis Cluster master-slave model ,每个节点存在1到N个副本

Reference:

《Redis的设计与实现》 http://www.cnblogs.com/loveincode/p/7411911.html https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Redis.md redis 3.0 中文注释源码: https://github.com/huangz1990/redis-3.0-annotated 关于LRU在redis中的实现: https://redis.io/topics/lru-cache https://blog.csdn.net/hangbo216/article/details/70142575 Redis中的LFU: https://yq.aliyun.com/articles/278922 redis 事务相关: http://redisbook.readthedocs.io/en/latest/feature/transaction.html redis script相关: https://redis.io/commands/eval redis AOF 相关: http://redisbook.readthedocs.io/en/latest/internal/aof.html redis 选举算法raft: https://cloud.tencent.com/developer/article/1021467 redis 应用场景: https://www.scienjus.com/redis-use-case/ redis pub sub模式: http://redisbook.readthedocs.io/en/latest/feature/pubsub.html redis 底层数据结构相关: http://zhangtielei.com/posts/blog-redis-quicklist.html redis 2.8 与 4.0数据结构的对比 https://blog.csdn.net/kimichen123/article/details/78229060 quicklist http://zhangtielei.com/posts/blog-redis-quicklist.html Raft 算法过程图解 : http://thesecretlivesofdata.com/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值