Redis

Redis

NoSQL数据库,数据都在内存中,支持持久化,可以用作数据库,缓存,或者消息中间件,支持多种数据类型,一般作为缓存数据库辅助持久化的数据库。

对数据的操作是单线程原子性操作,并且支持主从和集群模式。

应用场景

  1. 配合关系型数据库做高速缓存
    • 高频次,热门访问的数据,降低数据库IO
    • 旁路缓存模式
  2. 大数据场景下的数据缓存
    • 需要高频次访问
    • 持久化数据访问比较慢
    • 用key查询
  3. 大数据场景下的临时数据
    • 读写时效性高
    • 总数据量不大
    • 临时性
    • 用key查询
  4. 大数据场景下的计算结果
    • 高频次写入
    • 高频次查询
    • 总数据量不大
  5. 利用Redis的特殊数据结构解决一些特殊问题
    • 分布式锁 ——>单线程、原子性
    • 排行榜,TopN——>利用zset特性
    • 时效性的数据,比如手机验证码——>Expire过期设置
    • 计数器,秒杀,自增主键——>单线程,原子性,自增方法incr,decr
    • 去重——>利用set集合
    • 构建队列、栈——>利用list集合

单线程及多路IO复用技术

多路io复用 ,指的是同一个进程用一个线程处理多个IO数据流。

原理:多路Io复用是利用select、poll、epoll(不同的监控策略)可以同时监察多个流的IO事件的能力,在空闲的时候会把当前线程阻塞,当有一个或多个流有IO事件发生时,就从阻塞态中唤醒,处理就绪的流。

线程模型:redis基于reactor模式开发了自己的网络事件处理器,又称为文件事件处理器,由socket,I/O多路复用程序,文件事件分派器,事件处理器四部分组成。通过多路复用程序来监听多个socket,然后将事件压入队列,由分派器分派到相应的事件处理器进行处理。

优势:当处理的消耗对比IO几乎可以忽略不计时,可以处理大量的并发IO,而不用消耗太多CPU/内存。

新版 Redis 6.x

虽然io多路复用已经不错了,但是面临很多大键值的访问时,其中IO操作还是容易出现高延迟的问题,为了进一步优化,Redis 6.x把IO的部分做成允许多线程的模式。

注意这个IO部分只是处理网络数据的读写和协议解析,执行API命令仍然使用单线程。所以这个多线程并不会让redis存在并发的情况。

另外,多线程IO默认也是不开启的,需要再配置文件中配置

io-threads-do-reads yes

io-threads 4

常用数据类型及命令

键(key)
  • keys * 查看当前库所有key (匹配:keys *1)
  • exists key 判断某个key是否存在
  • type key 查看你的key是什么类型
  • del key 删除指定的key数据
  • expire key 10 10秒钟:为给定的key设置过期时间
  • ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
  • select 命令切换数据库
  • dbsize 查看当前数据库的key的数量
  • flushdb 清空当前库
  • flushall 通杀全部库
字符串(String)
  • String类型是Redis最基本的数据类型,一个key对应一个value,一个Redis中字符串value最多可以是512M

  • set 添加键值对

  • get 查询对应键值

  • append 将给定的 追加到原值的末尾

  • setnx 只有在 key 不存在时 设置 key 的值

  • incr

    ​ 将 key 中储存的数字值增1

    ​ 只能对数字值操作,如果为空,新增值为1

  • decr

    ​ 将 key 中储存的数字值减1

    ​ 只能对数字值操作,如果为空,新增值为-1

  • incrby / decrby <步长> 将 key 中储存的数字值增减。自定义步长。

  • mset … 同时设置一个或多个 key-value对

  • mget … 同时获取一个或多个 value

  • msetnx … 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。原子性,有一个失败则都失败

  • setex <过期时间> 设置键值的同时,设置过期时间,单位秒。

列表(List)

单键多值

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

  • lpush/rpush … 从左边/右边插入一个或多个值。

  • lpop/rpop 从左边/右边吐出一个值。值在键在,值光键亡。

  • rpoplpush 列表右边吐出一个值,插到列表左边。

  • lrange 按照索引下标获得元素(从左到右)

  • lrange mylist 0 -1 从0开始,-1表示获取所有

  • lindex 按照索引下标获得元素(从左到右)

  • llen 获得列表长度

  • linsert before 的后面插入 插入值

  • lrem 从左边删除n个value(从左到右)

场景: 队列 ,可重复集合

集合(set)

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

在符合条件的前提下(集合对象全部是整数,set-max-intset-entries 512)采用intset存储,否则采用dict存储,intset转dict的操作是不可逆的。

  • sadd … 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

  • smembers 取出该集合的所有值。

  • sismember 判断集合是否为含有该值,有1,没有0

  • scard 返回该集合的元素个数。

  • srem … 删除集合中的某个元素。

  • sinter 返回两个集合的交集元素。

  • sunion 返回两个集合的并集元素。

  • sdiff 返回两个集合的差集元素(key1中的,不包含key2中的)

场景: 去重 判存 集合间运算

哈希(hash)

Redis hash 是一个键值对集合,采用dict存储,在条件满足(未超过阈值)的情况下采用ziplist来进行存储(采用追加方式,先保存key在保存value),超过阈值后会由ziplist转向dict,且过程不可逆,所以尽量控制元素长度及数量。

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

类似Java里面的Map

  • hset 集合中的 键赋值
  • hget 集合 取出 value
  • hmset … 批量设置hash的值
  • hexists 查看哈希表 key 中,给定域 field 是否存在。
  • hgetall 列出该hash集合的所有field和value
  • hincrby 为哈希表 key 中的域 field 的值加上增量 1 -1

场景: 经常有字段变化的对象、 键值对集合

有序集合Zset(sorted set)

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

底层使用ziplist或者skiplist存储,转换操作也是不可逆的。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的 。

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

  • zadd …将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
  • zrange [WITHSCORES] 返回有序集 key 中,下标在 之间的元素带WITHSCORES,可以让分数一起和值返回到结果集。
  • zrevrange [WITHSCORES] 逆序返回下标范围的数据
  • zrangebyscore key min max [withscores] [limit offset count]返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
  • zrevrangebyscore key max min [withscores] [limit offset count] 同上,改为从大到小排列。
  • zincrby 为元素的score加上增量
  • zrem 删除该集合下,指定值的元素

此类型可用于排序或者构建二级索引,比如按年龄段搜索姓名的时候,可以将分数设置为年龄根据zrangebyscore来获取值,并且当分数相同时,redis会将值按字典序进行排序。

总结
数据类型 适用场景 备注
字符串(string) 缓存;计数器 简单型的。如set stunum studentInfo。 计数器如限流
列表(list) lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpush+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息队列) 如阻塞队列,关注列表
哈希(hash) 对象属性(尤其不定长的) 如缓存studentInfo,hmset stunum stunum 1 stuname dinghaha age 18
集合(set) 适用社交场景 赞/踩、粉丝、共同好友/喜好、推送
有序集合(zset) 排行榜;优先队列;

持久化

Redis 提供了2个不同形式的持久化方式。

  • RDB(Redis DataBase)
  • AOF(Append Only File)
RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

#查看rdb文件命令

od -cx dump.rdb
备份如何执行的

save:save会阻塞redis的主进程,直到rdb文件创建完成,在整个过程中,redis不处理任何请求。

bgsave:Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 fork过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

Fork
  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了写时复制技术
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
如何触发快照,也就是保存策略

在conf配置文件中设置

save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。

bgsaveRedis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

可以通过lastsave 命令获取最后一次成功执行快照的时间

执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义

RDB是整个内存的压缩过的Snapshot,RDB的数据结构

可以配置复合的快照触发条件,默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。

stop-writes-on-bgsave-error 当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes.

rdb生成源码

rdb文件由rdbSave函数创建,无论是save还是bgsave都会调用这个函数,可以在src里找到rdb.c进行查看

#save

void saveCommand(client *c) {
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);
    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {	//调用rdbSave
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}

---------------------------------------------------------------------------------
#bgsave

void bgsaveCommand(client *c) {
    int schedule = 0;

    /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
     * is in progress. Instead of returning an error a BGSAVE gets scheduled. */
    if (c->argc > 1) {
        if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
            schedule = 1;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }

    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);

    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
    } else if (hasActiveChildProcess()) {
        if (schedule) {
            server.rdb_bgsave_scheduled = 1;
            addReplyStatus(c,"Background saving scheduled");
        } else {
            addReplyError(c,
            "Another child process is active (AOF?): can't BGSAVE right now. "
            "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
            "possible.");
        }
    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) { //调用rdbSaveBackground
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}


-------------------------------------------------------------------------------
#rdbSaveBackground
    
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;

    if (hasActiveChildProcess()) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);
    openChildInfoPipe();

    if ((childpid = redisFork()) == 0) {
        int retval;

        /* Child */
        redisSetProcTitle("redis-rdb-bgsave");
        redisSetCpuAffinity(server.bgsave_cpulist);
        
        /*调用rdbSave*/
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent */
        if (childpid == -1) {
            closeChildInfoPipe();
            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 %d",childpid);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        return C_OK;
    }
    return C_OK; /* unreached */
}
----------------------------------------------------------------------
#rdbSave

int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
    FILE *fp;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Failed opening the RDB file %s (in server root dir %s) "
            "for saving: %s",
            filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        return C_ERR;
    }

    rioInitWithFile(&rdb,fp);
    startSaving(RDBFLAGS_NONE);

    if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);

    if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Error moving temp DB file %s on the final "
            "destination %s (in server root dir %s): %s",
            tmpfile,
            filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        unlink(tmpfile);
        stopSaving(0);
        return C_ERR;
    }

    serverLog(LL_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = C_OK;
    stopSaving(1);
    return C_OK;

werr:
    serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    fclose(fp);
    unlink(tmpfile);
    stopSaving(0);
    return C_ERR;
}
优势
  • 节省磁盘空间
  • 恢复速度快
劣势
  • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
AOF

​ 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

​ AOF默认不开启,开启需配置appendonly yes。 可以在redis.conf中配置文件名称,默认为 appendonly.aof ,AOF文件的保存路径,同RDB的路径一致。

生成过程

执行写操作命令
命令追加到aof_buf缓冲区
写入内核缓冲区
内核进行同步操作
  1. redis成功执行写操作命令后,写命令追加到aof_buf缓冲区
  2. redis 主进程将aof_buf中的数据写入到内核缓冲区
  3. 根据同步策略适时的将内核缓冲区的数据同步到磁盘
AOF和RDB同时开启,redis听谁的?

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失或者说丢失数据的概率很小)

AOF同步频率设置

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好,这种模式数据写入及备份操作都由主进程进行,两个操作都会阻塞主进程

appendfsync everysec

理论上每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。实际执行过程中会丢长时间数据会由日志同步的快慢来决定,并且这边由于是由子线程来进行同步,不会阻塞主进程,但子线程完成的快慢会影响主进程的写入阻塞时长

判断逻辑如下

if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
        /* With this append fsync policy we do background fsyncing.
         * If the fsync is still in progress we can try to delay
         * the write for a couple of seconds. */
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                /* No previous write postponing, remember that we are
                 * postponing the flush and return. */
                server.aof_flush_postponed_start = server.unixtime;
                return;
            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                /* We were already waiting for fsync to finish, but for less
                 * than two seconds this is still ok. Postpone again. */
                return;
            }
            /* Otherwise fall trough, and go write since we can't wait
             * over two seconds. */
            server.aof_delayed_fsync++;
            serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
        }
    }
正在执行同步
运行超过两秒
直接return
执行写入不进行同步但是写入操作需要等待先完成同步操作
距离上次完成同步超过一秒
执行写入不进行同步
执行写入及同步

appendfsync no

redis不主动进行同步,把同步时机交给操作系统。同样的写入及备份都会由主进程进行,也都会阻塞主进程,其触发时机在当关闭Redis或者AOF的时候及系统的缓存被刷新的时候触发。

Rewrite压缩

AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

重写原理,如何实现重写

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。 这里子进程也会用到写时复制技术。

为了保证在重写期间的数据一致性,在重写期间会将命令同时追加到AOF重写缓冲区,在此次重写完成后,将AOF重写缓冲区的内容追加到新的AOF文件中并rename,在将AOF重写缓冲区的数据写入新的AOF文件和rename阶段主进程是阻塞的,这是原子性操作。

#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10)    /* 10 MB per block */

typedef struct aofrwblock {
    unsigned long used, free;
    char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;

no-appendfsync-on-rewrite:

​ 如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

触发机制,何时重写

Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。

例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,

如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

优势
  • 备份机制更稳健,丢失数据概率更低。
  • 可读的日志文本,通过操作AOF文件,可以处理误操作。
劣势
  • 比起RDB占用更多的磁盘空间。
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别Bug,造成不能恢复。
用哪个好

官方推荐两个都启用。

如果对数据不敏感,可以选单独用RDB。

不建议单独用 AOF,因为可能会出现Bug。

如果只是做纯内存缓存,可以都不用。(谨慎,冗余出现雪崩)

主从复制

主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主,可以用作容灾快速恢复,但是他不能保证完全不丢失数据,因为这个主从节点之间的数据复制是异步的,在master写入完成并向客户端返回OK的时候挂掉的话,那么数据此时没有向slaver发送,会丢失数据

执行slaveof ip port,成为相应节点从机,5.0之后使用replicaof ip port

执行slaveof no one表示断开主从复制

可以设置sentinel哨兵模式,当主机挂掉后,自动从从机里选一个来当主机,但是这点应注意避免脑裂问题,可以设置超时时间,或者保证硬件的健康度来一定程度避免此问题。

同时我们可以通过设置以下两个参数来保证丢失尽可能少的数据

min-slaves-to-write 1
min-slaves-max-lag 10

如果要保证完全不丢数据的话可以在数据写入redis前增加一层备份层,可以先写入缓存或者落磁盘,也可以写入kafka这类的消息队列。

主从复制作用:

  • 数据冗余
  • 负载均衡,主从复制的基础上配合读写分离,均摊服务器压力,在写少读多的基础上可以设置通过多个从节点读取数据,大大提高并发量
  • 高可用基础,主从复制是哨兵和集群能够实施的基础

主从复制类型:

  1. 全量复制:
  • 有盘复制
    • 主节点先用bgsave生成一份rdb文件,并在此时记录所有写命令
    • 在serverCron定时任务中,将rdb文件发给从
    • 从收到rdb后记录在本地并进行加载
    • 进入命令传播阶段
  • 无盘复制
    • 在serverCron中,主进程fork一个子进程,持久化一个rdb数据在内存中,然后将数据发送给从节点
    • 从将收到的rdb文件记录在本地并进行加载
    • 进入命令传播阶段
  • 使用场景:
    • 首次复制
    • 不在复制积压缓冲区范围内
    • 主节点发生变化
    • 当从库开启AOF后,redis会优先加载AOF文件,但是由于aof文件中没有记录复制信息,所以在重启后从节点会使用全量复制
  1. 增量复制:全量复制代价太大,因此redis会使用复制积压缓冲区和带备份的masterid以及持久化主节点信息来尽可能的使用增量复制
  • 使用场景:在复制积压缓冲区内,主从节点网络闪断或从节点重启

集群模式

可以通过集群模式来进行水平扩容,通过在配置文件中开启集群模式,设置带副本的集群模式。

在集群模式下,当某一段插槽的主从节点全部挂掉,由cluster-require-full-coverage参数来决定集群状态,设置为yes时集群挂掉不可用,设置为no时,则这一段插槽不能使用,包括读和写

集群模式一共有16384个插槽,每个节点会负责相应的一部分。

//启动多个实例后,将其组合成集群模式
redis-cli --cluster create --cluster-replicas 1 ip1:6379 ip2:6380 ip3:6381 ip4:6389 ip5:6390 ip6:6391
    
    
//连接集群需要指定 -c ,来进行自动切换数据对应主机
redis-cli -c -p 6379
优点
  • 实现扩容
  • 分摊压力
  • 无中心配置相对简单
缺点
  • 多键操作是不被支持的
  • 多键的Redis事务是不被支持的。lua脚本不被支持

pipelining

当需要不断的执行多条命令的时候,可以考虑使用流水线加速Redis的操作,当我们执行一次请求时,数据的往返会有时间消耗,并且执行的时间消耗很少,大部分时间都消耗在每次read()和write()函数的调用,上下文切换这些。这样即使我们的服务器能一秒处理十万个请求,但是我们的RTT(往返时延)比较长的话,比如RTT达到250毫秒,那么一秒也就只能处理4条请求。此时我们考虑使用流水线进行操作,他相当于将我们的命令进行一个批次处理,以减少RTT对我们执行效率的影响,此时,需要我们将大量的命令进行分批处理,因为此模式会在内存维护一个队列用以存储执行请求的返回结果,此时如果对批次不加以限制的话会非常耗费内存资源。

以下为本机测试效率对比

对比结果

----get Jedis----
    共插入:[10000]条 .. 
1,未使用PIPE批量设值耗时4746毫秒..
    PIPE共插入:[10000]条 .. 
2,使用PIPE批量设值耗时75毫秒 ..
    共取值:[10000]条 .. 
3,未使用PIPE批量取值耗时 4434毫秒 ..
    PIPE共取值:[10000]条 .. 
4,使用PIPE批量取值耗时24毫秒 ..

支持消息的发布订阅功能

此模式简单来说就是通过一个中间件将服务端和客户端连接起来。

通过通道订阅的就是通过channel将两者连接起来进行一个解耦,服务端会有一个pubsub_channels属性,这个属性是一个字典,key为不同的channel,value为订阅了该频道的所有客户端。

通过模式来订阅的话,在服务端会有一个pubsub_patterns属性,此属性为链表结构,每一个节点都是保存的订阅的模式及订阅这个模式的客户端。

所以当服务端发送数据的时候,服务端会将数据发送给channel,然后再对channel对应的链表值进行遍历,将数据发送给所有订阅了该channel的客户端,接着,就会遍历模式对应的链表,匹配到对应模式后将数据发送给订阅该模式的客户端,所以,如果一个客户端既订阅了具体的频道,又订阅了包含该频道的模式,那么这个客户端将收到多次服务端发送的消息。

主要命令有

#发布消息
	publish <channel> <message>
    
#订阅
    subscribe <channel>
    psubscribe <pattern>

#取消订阅
    unsubscribe <channel>
    punsubscribe <pattern>

内存优化

  • 能采用hash的情况下尽量采用hash结构

  • 对于Hashes, Lists, Sets composed of just integers, and Sorted Sets这些结构,redis提供了一种特殊的编码形式,可以将数据进行压缩,相当于是在于CPU和内存之间的一种权衡

    具体配置如下:
    hash-max-ziplist-entries 512 //代表元素个数
    hash-max-ziplist-value 64 //代表元素的最大字符数
    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64
    set-max-intset-entries 512
    

    当满足我们设置的参数情况下,redis会自动帮我们进行一个特殊编码,使用的内存大概可以节省5-10倍,当值超过我们设定的范围时redis会自动帮我们转换为正常编码。

基于redis对分布式锁的实现方案

通用方案

在获得锁的时候使用setnx获取,并且设置过期时间,时间到了就自动释放锁,对于过期时间的设定长短问题,可以根据处理逻辑的具体执行效率来设置一个合理的值,也可以采用守护线程的方式,检测到时间过去超时时间的多少比例时自动将过期时间后延,当然也可以设置一个单调递增的数字,在获取锁的时候附带发送送给客户端,在对数据库操作的时候会先对这个数字进行判断,对于小的数字请求不进行处理,以避免冲突问题。并且对于锁的值的设定需要随机且一定,用以后续释放锁的判断,防止产生多进程之间互相释放锁的操作。同时对于锁的操作都应该是原子性操作,以防止对锁的误操作。

此种方式在redis是单机部署的情况下,要防止redis故障停机

在集群或者主从模式下,在极端情况下无法保证锁的安全性(在当一个客户端刚获得锁的时候master宕机并且此时数据还未同步到slaver,然后slaver被选举为新的master后,就会被另一个客户端拿到新的锁,此时就会有两个客户端同时持有了锁)

红锁方案RedLock

此方案主要是解决redis故障时的锁安全问题

红锁方案获取锁的步骤:官网推荐N=5

  • 获取当前时间
  • 使用通用方案按顺序依次向N个redis节点执行获取锁的操作,有所不同的是,在获取锁的时候会设置一个远小于过期时间的超时时间,比如如果过期时间10秒,那么可能获取锁的超时时间就设置为50毫秒,当前节点获取锁失败后会立即向下一个节点执行获取锁的操作。
  • 步骤2完成后会计算整个过程一共消耗多长时间,如果消耗时间没有超过过期时间并且成功获取锁的个数过半(>=N/2+1),我们认为获取锁成功,否则失败
  • 如果获得了锁,则其有效时间被认为是初始有效时间减去经过的时间,如步骤 3 中计算的那样。
  • 如果客户端由于某种原因未能获得锁(它无法锁定 N/2+1 个实例或有效时间为负数),会立即向所有节点发送释放锁的操作。

针对于此种方案需要注意

如果我们有五个节点1,2,3,4,5

  1. 进程A锁定了1,2,3
  2. 节点1崩溃了进行重启,且加锁数据没有持久化恢复
  3. 进程B锁定了1,4,5

当此种情况发生时就会造成针对于共享资源同时有两把锁,针对于这一问题,redlock的提出者提出了延迟重启的策略,也就是一个节点崩溃后,先不重启他,会设置一个超过锁的过期时间的时间后重启,这样就可以避免此问题影响。

同时红锁算法比较依赖于系统时钟,当系统时钟出现跳跃的时候会直接影响到锁的安全性

总结

redis实现分布式锁的常见问题及解决方案:

  • 死锁:设置过期时间
  • 锁过期时间无法准确把控:守护线程,自动延长时间,可以参考Redisson的实现
  • 锁被其他进程释放:设置随机唯一标识
  • NPC(网络延迟,进程GC,时钟跳跃)问题:通过恰当的运维来尽量避免

基于redis实现的分布式锁大致观点:在极端情况下无法保证结果的正确性,我们可以允许结果延迟,但是结果错误的话对于安全性来说是一个致命的问题。

所以整体而言的话,用redis来实现分布式锁,如果是用在提升效率上,比如只是为了不要做同样的工作,那单节点的就完全可以了,如果使用在对正确性要求很高的场景中,无论是单节点还是redlock都无法满足要求,在考虑时需要准备好兜底策略。

使用redis进行缓存的注意事项

缓存穿透

缓存穿透是指查询一个一定不存在的数据。由于缓存命不中时会去查询数据库,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决方案:

① 是将空对象也缓存起来,并给它设置一个很短的过期时间,最长不超过5分钟

② 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存雪崩

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,就会造成缓存雪崩。

解决方案:

​ 尽量让失效的时间点不分布在同一个时间点

缓存击穿

缓存击穿,是指一个key非常热点,在不停的扛着大并发,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

解决方案:

可以设置key永不过期

基准测试

使用redis-benchmark -q进行基准测试

PING_INLINE: 105485.23 requests per second
PING_BULK: 106044.54 requests per second
SET: 106837.61 requests per second
GET: 105152.48 requests per second
INCR: 102669.41 requests per second
LPUSH: 104166.67 requests per second
RPUSH: 106837.61 requests per second
LPOP: 104493.20 requests per second
RPOP: 107991.36 requests per second
SADD: 102145.05 requests per second
HSET: 107181.13 requests per second
SPOP: 108342.37 requests per second
LPUSH (needed to benchmark LRANGE): 91575.09 requests per second
LRANGE_100 (first 100 elements): 98039.22 requests per second
LRANGE_300 (first 300 elements): 99206.34 requests per second
LRANGE_500 (first 450 elements): 99900.09 requests per second
LRANGE_600 (first 600 elements): 103199.18 requests per second
MSET (10 keys): 112233.45 requests per second

基本可以看到,常用命令QPS基本在十万级

pipeline测试

不使用pipeline

redis-benchmark -t set,get,incr -n 1000000 -q

SET: 108014.69 requests per second
GET: 106780.57 requests per second
INCR: 102030.41 requests per second

使用pipeline

redis-benchmark -t set,get,incr -n 1000000 -q -P 10

SET: 865051.88 requests per second
GET: 907441.00 requests per second
INCR: 850340.12 requests per second

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

redis-benchmark -t set,get,incr -n 1000000 -q -P 20

SET: 1237623.75 requests per second
GET: 1398601.50 requests per second
INCR: 1658374.88 requests per second

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

redis-benchmark -t set,get,incr -n 1000000 -q -P 30

SET: 1838235.25 requests per second
GET: 1912045.88 requests per second
INCR: 1953124.88 requests per second

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

redis-benchmark -t set,get,incr -n 1000000 -q -P 50

SET: 2087682.62 requests per second
GET: 2380952.50 requests per second
INCR: 2283105.00 requests per second

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

redis-benchmark -t set,get,incr -n 1000000 -q -P 100

SET: 2237136.50 requests per second
GET: 2645502.75 requests per second
INCR: 2624671.75 requests per second

源码解析

redis是一个键值对数据库,每一个键值对都存储在dicEntry结构体中,其中key为这个键值对的键,是一个SDS,val为一个用redisObject包裹着的值,next则指向下一个键值对

sds
key = "key"
dicEntry
void key
void val
struct next
redisObject
unsigned type
void ptr
SDS
ptr = "value"

基本数据结构

  • 简单动态字符串(SDS)
typedef char* sds
/*
 __attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法
*/
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 数据长度 */
uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度*/
unsigned char flags; /* 3 lsb of type, 5 unused bits,小端*/
//变长数据
char buf[];
};
  • 链表,双向链表
  • 字典
  • intset 是用来保存整数值的集合抽象数据结构,保存类型为16,32或者64位的整数值,数据不重复且由小到大排列,查找采用二分查找
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) {
    if (v < INT32_MIN || v > INT32_MAX)
        return INTSET_ENC_INT64;
    else if (v < INT16_MIN || v > INT16_MAX)
        return INTSET_ENC_INT32;
    else
        return INTSET_ENC_INT16;
}
  • ziplist,适合存储小对象和长度有限的数据

基础对象

redisObject

#define LRU_BITS 24

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    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;

type对应type命令,保存对象类型

encoding对应object encoding命令,保存对象的底层数据结构,不同情况可能会有不同的结构

lru记录的是最后一次访问的时间

refcount 对应object refcount命令

ptr 模拟多态,可以存储任何类型的对象

通信协议

Redis基于**RESP(Redis Serialization Protocol)**协议来完成客户端和服务端通信。RESP本质是一种文本协议,实现简单、易于解析。

我们常用的客户端结果显示是经过redis-cli.c文件进行转换而来,除了常用的客户端登陆外,我们也可以尝试使用nc命令来代替redis-cli

nc 127.0.0.1 6379
auth XXXX
select 15.....

其中用来管理redis命令的对象是通过redisCommand数据结构来管理的

typedef void redisCommandProc(client *c);
typedef int *redisGetKeysProc(struct redisCommand *cmd, robj **argv, int argc, int *numkeys);
struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    long long microseconds, calls;
};

arity : 用来限制命令个数,-N表示至少N个参数,包含命令本身

sflags : 字符串方式设置命令的属性,比如 w表示这是个写入命令,r表示这是个只读命令

flags : 将sflags字符串类型转为整型,多个属性之间用 |运算,通过内部的populateCommandTable函数自动解析

redisCommandProc :函数指针类型,用来指向命令实现函数

microseconds : 命令执行耗时

calls : 命令执行总次数

命令运算规则

例:

struct redisCommand redisCommandTable[] = {
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0}
}
char *name set
redisCommandProc 回调函数setCommand
int arity -3,表示至少三个参数(包括命令本身)
char *sflags wm,写入操作,同时执行前检查内存,内存不够拒绝执行
int flags 0,表示没有任何额外设置
redisGetKeysProc null,表示没有任何额外设置
int firstkey 1,表示第一个参数是key
int lastkey 1,表示最后一个参数是key
int keystep 1,表示步长
microseconds 0,表示执行该命令耗时初始化为0
calls 0,表示执行该命令次数初始化为0

服务端启动过程

  1. 模式选择

根据启动参数,判断是否是哨兵模式

server.sentinel_mode = checkForSentinelMode(argc,argv);

int checkForSentinelMode(int argc, char **argv) {
    int j;

    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    for (j = 1; j < argc; j++)
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}
  1. 初始化服务端,主要函数为initServerConfig,主要是初始化redisServer数据结构中的各个成员变量
  2. 如果设置了哨兵模式,初始化哨兵
  3. 根据参数确定是否进行rdb、aof校验
/* Check if we need to start in redis-check-rdb/aof mode. We just execute
     * the program main. However the program is part of the Redis executable
     * so that we can easily execute an RDB check on loading errors. */
    if (strstr(argv[0],"redis-check-rdb") != NULL)
        redis_check_rdb_main(argc,argv,NULL);
    else if (strstr(argv[0],"redis-check-aof") != NULL)
        redis_check_aof_main(argc,argv);
  1. 解析命令行参数,如果设置配置文件,则将对其进行解析并覆盖默认参数

  2. 判断是否后台运行

  3. 初始化服务,

    1. 会对SIGHUP和SIGPIPE进行忽略设置

    2. 对于其他信号会由setupSignalHandlers函数对其进行捕获,比如SIGINT信号捕获后由redis进行收尾工作,避免进程被暴力kill

      	signal(SIGHUP, SIG_IGN);
          signal(SIGPIPE, SIG_IGN);
          setupSignalHandlers();
      
    3. 根据createSharedObjects函数创建共享对象,对于常用的对象,采用引用计数的方式进行复用

    4. 根据系统设置调整打开的文件数adjustOpenFilesLimit()

    5. 网络模型初始化,redis支持Unix和TCP两种模型,当客户端和服务端都在本机时,Unix通信更快(不需要解析协议头)

    6. LRU过期策略初始化evictionPoolAlloc(); /* Initialize the LRU keys pool. */

    7. 初始化rdb和aof信息

    8. 初始化状态信息resetServerStats()

    9. 注册事件

    10. 慢日志初始化slowlogInit

    11. 后台线程创建

  4. 加载外部模块moduleLoadFromQueue

  5. 加载磁盘数据(优先加载AOF)loadDataFromDisk

  6. 进入事件主循环

主从复制

命令传播

所谓命令传播,就是在复制的过程中,Redis是继续对外服务的,这将导致主从数据存在差异,此时会进行命令传播,需要一个命令传播机制,传播的时候会通过propagate() 函数调用 replicationFeedSlaves(),在复制过程中,会将数据拷贝一份到复制积压缓冲区和从服务器 输出缓冲区中。

复制积压缓冲区作用:

​ 在复制期间,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。

​ 积压缓冲区是一个由主服务器维护的固定长度、先进先出的队列,默认大小为1M。如果超过了1M,那么会进行覆盖操作。而且对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区。

优先部分复制的场景:

  • 一主一从发生切换,A->B变成B->A
  • 一主多从发生切换,兄弟节点变成父子节点
  • 级联复制发生切换,A->B->C变成B->C->A

slaveof no one

void replicaofCommand(client *c) {
    
    /* The special host/port combination "NO" "ONE" turns the instance
     * into a master. Otherwise the new master address is set. */
    if (!strcasecmp(c->argv[1]->ptr,"no") &&
        !strcasecmp(c->argv[2]->ptr,"one")) {
        if (server.masterhost) {
            replicationUnsetMaster();
            sds client = catClientInfoString(sdsempty(),c);
            serverLog(LL_NOTICE,"MASTER MODE enabled (user request from '%s')",
                client);
            sdsfree(client);
        }
    } else {
       ...
    }
    addReply(c,shared.ok);
}

/* 取消主从复制,将他自己设置为主,恢复最初状态. */
void replicationUnsetMaster(void) {
    if (server.masterhost == NULL) return; /* Nothing to do. */
    sdsfree(server.masterhost);
    server.masterhost = NULL;
    // 
    shiftReplicationId();
    if (server.master) freeClient(server.master);
    replicationDiscardCachedMaster();
    cancelReplicationHandshake();
    
    disconnectSlaves();
    server.repl_state = REPL_STATE_NONE;
 
    server.slaveseldb = -1;
    server.repl_no_slaves_since = server.unixtime;
}

slaveof IP PORT

/* 将主节点的IP和PORT记录在RedisServer数据结构中  */
struct redisServer
{
    ...
        char *masterhost;               /* Hostname of master */
    int masterport;                 /* Port of master */
    ...
}


/* 将主节点信息进行缓存  */
/* Set replication to the specified master address and port. */
void replicationSetMaster(char *ip, int port) {
    int was_master = server.masterhost == NULL;
    //记录新的主节点
    sdsfree(server.masterhost);
    server.masterhost = sdsnew(ip);
    server.masterport = port;
    if (server.master) {
        freeClient(server.master);
    }
    disconnectAllBlockedClients(); /* Clients blocked in master, now slave. */

    /* Force our slaves to resync with us as well. They may hopefully be able
     * to partially resync with us, but we can notify the replid change. */
    disconnectSlaves();
    cancelReplicationHandshake();
    //这里缓存主节点
    if (was_master) replicationCacheMasterUsingMyself();
    server.repl_state = REPL_STATE_CONNECT;
}

全量复制

有盘复制,当有多个slave需要同步的时候,会尽量对rdb进行复用

/* SYNC and PSYNC command implemenation. */
void syncCommand(client *c) {
     
....
    /* Setup the slave as one waiting for BGSAVE to start. The following code
     * paths will change the state if we handle the slave differently. */
    c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
    if (server.repl_disable_tcp_nodelay)
        anetDisableTcpNoDelay(NULL, c->fd); /* Non critical if it fails. */
    c->repldbfd = -1;
    c->flags |= CLIENT_SLAVE;
    listAddNodeTail(server.slaves,c);

    /* Create the replication backlog if needed. */
    if (listLength(server.slaves) == 1 && server.repl_backlog == NULL) {
        /* When we create the backlog from scratch, we always use a new
         * replication ID and clear the ID2, since there is no valid
         * past history. */
        changeReplicationId();
        clearReplicationId2();
        createReplicationBacklog();
    }

    /* CASE 1: BGSAVE is in progress, with disk target. */
    if (server.rdb_child_pid != -1 &&
        server.rdb_child_type == RDB_CHILD_TYPE_DISK)
    {
        /* Ok a background save is in progress. Let's check if it is a good
         * one for replication, i.e. if there is another slave that is
         * registering differences since the server forked to save. */
        client *slave;
        listNode *ln;
        listIter li;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            slave = ln->value;
            if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END) break;
        }
        /* To attach this slave, we check that it has at least all the
         * capabilities of the slave that triggered the current BGSAVE. */
        if (ln && ((c->slave_capa & slave->slave_capa) == slave->slave_capa)) {
            /* Perfect, the server is already registering differences for
             * another slave. Set the right state, and copy the buffer. */
            copyClientOutputBuffer(c,slave);   /*拷贝输出缓存区内容*/
            replicationSetupSlaveForFullResync(c,slave->psync_initial_offset);
            serverLog(LL_NOTICE,"Waiting for end of BGSAVE for SYNC");
        } else {
            /* No way, we need to wait for the next BGSAVE in order to
             * register differences. */
            serverLog(LL_NOTICE,"Can't attach the replica to the current BGSAVE. Waiting for next BGSAVE for SYNC");
        }
        。。。
    }

无盘复制,直接fork放在内存

/* SYNC and PSYNC command implemenation. */
void syncCommand(client *c) {
     
   ...

    /* CASE 2: BGSAVE is in progress, with socket target. */
    } else if (server.rdb_child_pid != -1 &&
               server.rdb_child_type == RDB_CHILD_TYPE_SOCKET)
    {
        /* There is an RDB child process but it is writing directly to
         * children sockets. We need to wait for the next BGSAVE
         * in order to synchronize. */
        serverLog(LL_NOTICE,"Current BGSAVE has socket target. Waiting for next BGSAVE for SYNC");

    /* CASE 3: There is no BGSAVE is progress. */
    } else {
        if (server.repl_diskless_sync && (c->slave_capa & SLAVE_CAPA_EOF)) {
            /* Diskless replication RDB child is created inside
             * replicationCron() since we want to delay its start a
             * few seconds to wait for more slaves to arrive. */
            if (server.repl_diskless_sync_delay)
                serverLog(LL_NOTICE,"Delay next BGSAVE for diskless SYNC");
        } else {
           //这里
            if (server.aof_child_pid == -1) {
                startBgsaveForReplication(c->slave_capa);
            } else {
               
            }
        }
    }
    return;
}

增量复制,主要看从节点的复制偏移量是否位于主节点复制积压缓冲区,从节点的主节点是否发生变化等。

int masterTryPartialResynchronization(client *c) {

    if (strcasecmp(master_replid, server.replid) &&
        (strcasecmp(master_replid, server.replid2) ||
         psync_offset > server.second_replid_offset))
    {
        /* Run id "?" is used by slaves that want to force a full resync. */
        if (master_replid[0] != '?') {
            if (strcasecmp(master_replid, server.replid) &&
                strcasecmp(master_replid, server.replid2))
            {
                 
            } else {
               
            }
        } else {
            serverLog(LL_NOTICE,"Full resync requested by replica %s",
                replicationGetSlaveName(c));
        }
        goto need_full_resync;
    }

    /* We still have the data our slave is asking for? */
    if (!server.repl_backlog ||
        psync_offset < server.repl_backlog_off ||
        psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
    {
         
        if (psync_offset > server.master_repl_offset) {
            
        }
        goto need_full_resync;
    }
 
need_full_resync:
    /* We need a full resync for some reason... Note that we can't
     * reply to PSYNC right now if a full SYNC is needed. The reply
     * must include the master offset at the time the RDB file we transfer
     * is generated, so we need to delay the reply to that moment. */
    return C_ERR;
}

从节点接收数据:

  • 针对无盘复制,需要将两头的40 字节长的随机字符串去掉
  • 接收到的数据先存放至RDB文件中
  • 全部接收完毕后会清空数据库,暂停AOF持久化,删除可读事件,更新从节点中主节点的复制偏移量和runid,最后加载RDB文件
if(aof_is_enabled) stopAppendOnly();//暂停aof持久化
signalFlushedDb(-1);
        emptyDb(
            -1,
            server.repl_slave_lazy_flush ? EMPTYDB_ASYNC : EMPTYDB_NO_FLAGS,
            replicationEmptyDbCallback);//清空数据库
         
        aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE);//删除可读事件
        serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Loading DB in memory");
        rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
        if (rdbLoad(server.rdb_filename,&rsi) != C_OK) { //加载RDB
            serverLog(LL_WARNING,"Failed trying to load the MASTER synchronization DB from disk");
            cancelReplicationHandshake();
             //暂停
            if (aof_is_enabled) restartAOF();
            return;
        }
        /* Final setup of the connected slave <- master link */
        zfree(server.repl_transfer_tmpfile);
        close(server.repl_transfer_fd);
        replicationCreateMasterClient(server.repl_transfer_s,rsi.repl_stream_db);
        server.repl_state = REPL_STATE_CONNECTED;
        server.repl_down_since = 0;
        /* 记录主节点的runid和偏移量 */
        memcpy(server.replid,server.master->replid,sizeof(server.replid));
        server.master_repl_offset = server.master->reploff;
        clearReplicationId2();
   
        if (server.repl_backlog == NULL) createReplicationBacklog();

        
        if (aof_is_enabled) restartAOF();//重启aof

两个定时检测任务

  • 从节点维护一个REPLCONF ACK,每秒一次
    • 实时监测主从节点网络状况
    • 上报自身偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据
  • 主节点维护ping任务,10秒一次,主要看从节点还在不在

主从复制对过期键的处理:

主:只会载入未过期的

从:全部载入,后期主从会同步

哨兵

哨兵启动模式:因为哨兵和redisServer共用了部分代码,所以通过设置参数有两种启动方式

redis-sentinel sentinel.conf
    
redis-server sentinel.conf –-sentinel

当redis服务以哨兵模式启动后,会创建一个struct sentinelState的全局实例sentinel,sentinel启动后会调用 createSentinelRedisInstance 创建sentinelRedisInstance实例用于处理与master的连接,实例创建好并不代表连接创建完成,只是准备好了连接的结构,后续会在定时任务中进行创建连接,建立连接会创建两条连接,分别是link->cc,link->pc,cc用来发送ping和info信息,pc用来向master注册hello频道并定时发送消息。

其中,监控不同master之间的相同哨兵,会对连接进行共用,以减少频繁的连接建立,比如5个哨兵监控一百个master,只会建立5条连接,而不是500条。

哨兵的状态数据结构

/* Main state. */
struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
    //记录的当前纪元用于故障转移用
    uint64_t current_epoch;         /* Current epoch. */
        //记录了所有被该哨兵监控的主服务器。
        //其中key为主服务器名称,value为sentinelRedisInstances对象
    dict *masters;      /* Dictionary of master sentinelRedisInstances.
                           Key is the instance name, value is the
                           sentinelRedisInstance structure pointer. */
    int tilt;           /* Are we in TILT mode? */
    int running_scripts;    /* Number of scripts in execution right now. */
    mstime_t tilt_start_time;       /* When TITL started. */
    mstime_t previous_time;         /* Last time we ran the time handler. */
    list *scripts_queue;            /* Queue of user scripts to execute. */
    char *announce_ip;  /* IP addr that is gossiped to other sentinels if
                           not NULL. */
    int announce_port;  /* Port that is gossiped to other sentinels if
                           non zero. */
    unsigned long simfailure_flags; /* Failures simulation. */
    int deny_scripts_reconfig; /* Allow SENTINEL SET ... to change script
                                  paths at runtime? */
} sentinel;

sentinelRedisInstance哨兵实例

typedef struct sentinelRedisInstance {
    int flags;      /* See SRI_... defines 用来表示当前实例的类型和状态 */
    char *name;     /*哨兵记录的master名字,从服务器和哨兵的话是用的是IP+Port */
    char *runid;    /* Run ID of this instance, or unique ID if is a Sentinel.*/
    uint64_t config_epoch;  /* 用于实现故障转移. */
    sentinelAddr *addr; /* 实例地址 */
    instanceLink *link; /* Link to the instance, may be shared for Sentinels. */
    mstime_t last_pub_time;   /* Last time we sent hello via Pub/Sub. */
    mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
                                 we received a hello from this Sentinel
                                 via Pub/Sub. */
    mstime_t last_master_down_reply_time; /* Time of last reply to
                                             SENTINEL is-master-down command. */
    mstime_t s_down_since_time; /*主观宕机时间 Subjectively down since time. */
    mstime_t o_down_since_time; /* 可观宕机时间Objectively down since time. */
    mstime_t down_after_period; /*实例无响应多久被认为是主观宕机。 Consider it down after that period. */
    mstime_t info_refresh;  /* Time at which we received INFO output from it. */
    dict *renamed_commands;     /* Commands renamed in this instance:
                                   Sentinel will use the alternative commands
                                   mapped on this table to send things like
                                   SLAVEOF, CONFING, INFO, ... */

    /* 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. */
    dict *sentinels;    /* 用于记录监控相同主节点其他哨兵实例。该字典以哨兵名字为key,以哨兵实例sentinelRedisInstance结构为key;Other sentinels monitoring the same master. */
    dict *slaves;       /* 用于记录该主节点实例的所有从节点实例。该字典以从节点名字为key,以从节点实例sentinelRedisInstance结构为key; Slaves for this master instance. */
    unsigned int quorum;/* 多少哨兵同意下线,才表示可观宕机Number of sentinels that need to agree on failure. */
    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. */
    mstime_t slave_reconf_sent_time; /* 从节点配置Time at which we sent SLAVE OF  */
    struct sentinelRedisInstance *master; /*所属的master Master instance if it's slave. */
    char *slave_master_host;    /*  Master host as reported by INFO */
    int slave_master_port;      /* Master port as reported by INFO */
    int slave_master_link_status; /* Master link status as reported by INFO */
    unsigned long long slave_repl_offset; /* Slave replication offset. */
    /* Failover */
    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; /* 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. */
    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. */
    char *notification_script;
    char *client_reconfig_script;
    sds info; /* cached INFO output */
} sentinelRedisInstance;

三类命令

  • info

    • 每隔十秒,每个哨兵会向master和slave发送info命令获取最新的拓扑结构,当有新的slave加入后会及时感知到,当某个slave的主节点状态变成客观宕机之后,周期会由10s变成1s,因为他随时会变成master

      /* Send INFO to masters and slaves, not sentinels. */
         if ((ri->flags & SRI_SENTINEL) == 0 &&
             (ri->info_refresh == 0 ||
             (now - ri->info_refresh) > info_period))
         {
             retval = redisAsyncCommand(ri->link->cc,
                 sentinelInfoReplyCallback, ri, "%s",
                 sentinelInstanceMapCommand(ri,"INFO"));
             if (retval == C_OK) ri->link->pending_commands++;
         }
      
    • 针对返回信息哨兵会进行分析,可以获取主从节点的连接状态角色,runid,优先级等信息

    • 当从节点转变成为主节点时,会进行故障转移,此时info信息每秒发送一次

    /* If this is a slave of a master in O_DOWN condition we start sending
     * it INFO every second, instead of the usual SENTINEL_INFO_PERIOD
     * period. In this state we want to closely monitor slaves in case they
     * are turned into masters by another Sentinel, or by the sysadmin.
     *
     * Similarly we monitor the INFO output more often if the slave reports
     * to be disconnected from the master, so that we can have a fresh
     * disconnection time figure. 
     * 如果该从节点的主节点可观投票down机了,那么该slave由10s变成1s进行info,因为他随时都有可能变成主节点
     * */
    if ((ri->flags & SRI_SLAVE) &&
        ((ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)) ||
         (ri->master_link_down_time != 0)))
    {
        info_period = 1000;
    } else {
        info_period = SENTINEL_INFO_PERIOD;
    }
    
  • PING

    • 每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。通过下面的定时任务,Sentinel节点对主节点、从节点、其余Sentinel节点都建立起连接,实现了对每个节点的监控,这个定时任务是节点失败判定的重要依据。
    /* Send PING to all the three kinds of instances. */
     if ((now - ri->link->last_pong_time) > ping_period &&
     (now - ri->link->last_ping_time) > ping_period/2) {
     sentinelSendPing(ri);
     }
    
  • hello

    • 默认情况下,哨兵会以每2秒一次的速度向主和从服务器发送消息

      #define SENTINEL_PUBLISH_PERIOD 2000 
      /* PUBLISH hello messages to all the three kinds of instances. */
          if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
              sentinelSendHello(ri);
          }
      
    • 作用为发现新的哨兵节点,每个哨兵会去监听自己监控的每个主从节点对应的hello频道,然后感知到同样在监听这个节点的其他哨兵存在,同时每个哨兵还会和其他哨兵交换对节点的监控配置,并互相进行同步

总体运行流程:

  • 启动初始化

    • 加载配置文件
    • 关闭持久化
    • 初始化支持的命令
  • 定时任务,哨兵模式下,sentinel为了自动管理redis服务器,在serverCron中执行sentinelTimer函数,100ms执行一次

  • 主观下线判断

    • 节点的主观下线依赖ping命令,主观下线并不代表该节点down掉了,要进行客观下线判断
    • 哨兵宕机后也只会触发主观宕机
  • 客观下线判断

    • 首先哨兵会向其他哨兵发送is-master-down-by-addr命令进行查询
    • 目标哨兵收到上述命令后,会回复给源节点该master状态,
    • 源哨兵收到回复后对结果进行解析并更新master状态,并统计票数判断会否可以进行客观宕机
  • 客观下线判定完成后会开始选举领头的哨兵,来进行新主节点的切换

    • 从节点成为新主节点的候选条件如下:

      • 排除主观或客观宕机状态
      • 排除主从节点断线的从节点
      • 排除心跳超过2秒的
      • 排除没有设置优先级的
      • 排除没有及时响应info消息的
      • 排除长时间没有和主节点通信的
    • 针对有多个从节点时,优先级为:

      • 优先选择优先级高的
      • 选择偏移量大的
      • 选择runid小的
      int compareSlavesForPromotion(const void *a, const void *b) {
          sentinelRedisInstance **sa = (sentinelRedisInstance **)a,
                                **sb = (sentinelRedisInstance **)b;
          char *sa_runid, *sb_runid;
      
          if ((*sa)->slave_priority != (*sb)->slave_priority)
              return (*sa)->slave_priority - (*sb)->slave_priority;
      
          /* If priority is the same, select the slave with greater replication
           * offset (processed more data from the master). */
          if ((*sa)->slave_repl_offset > (*sb)->slave_repl_offset) {
              return -1; /* a < b */
          } else if ((*sa)->slave_repl_offset < (*sb)->slave_repl_offset) {
              return 1; /* a > b */
          }
      
          /* If the replication offset is the same select the slave with that has
           * the lexicographically smaller runid. Note that we try to handle runid
           * == NULL as there are old Redis versions that don't publish runid in
           * INFO. A NULL runid is considered bigger than any other runid. */
          sa_runid = (*sa)->runid;
          sb_runid = (*sb)->runid;
          if (sa_runid == NULL && sb_runid == NULL) return 0;
          else if (sa_runid == NULL) return 1;  /* a > b */
          else if (sb_runid == NULL) return -1; /* a < b */
          return strcasecmp(sa_runid, sb_runid);
      }
      
    • 选中哨兵会向被选中的从节点发送slaveof no one ,然后开始发送info命令,来看他是否成为了主节点

    void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
        int retval;
    
        /* We can't send the command to the promoted slave if it is now
         * disconnected. Retry again and again with this state until the timeout
         * is reached, then abort the failover. */
        if (ri->promoted_slave->link->disconnected) {
            if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
                sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
                sentinelAbortFailover(ri);
            }
            return;
        }
    
        /* Send SLAVEOF NO ONE command to turn the slave into a master.
         * We actually register a generic callback for this command as we don't
         * really care about the reply. We check if it worked indirectly observing
         * if INFO returns a different role (master instead of slave). */
        retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0);
        if (retval != C_OK) return;
        sentinelEvent(LL_NOTICE, "+failover-state-wait-promotion",
            ri->promoted_slave,"%@");
        ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_PROMOTION;
        ri->failover_state_change_time = mstime();
    }
    
  • 新的master上线后,会将原来从节点维持的主从关系进行清除,然后指向新的master

你可能感兴趣的:(redis,大数据,数据库)