背景

在字节跳动实习,因为要做新人串讲,选了这个题目,飞书搬过来标题什么的都没有了,可能有点不够直观

 

1.启动服务器
示例:(启动之后的效果)
1.1 初始化struct redisServer结构
主要会执行一个initServerConfig函数进行 redisServer的初始化,包括默认配置文件的路径,端口号,RDB,AOF持久化条件,命令表等,都是redis的默认配置。
 
1.2 载入配置文件
如果需要不同的配置的话,自己就可以编写配置文件,启动服务器的时候会自动检测有无配置文件,有的话载入配置。
 
1.3 初始化服务器数据结构
  • 主要是为redisServer中的一些数据结构分配内存,不在初始化redisServer结构的时候分配内存的原因是,如果后面载入配置文件的时候可能有变动,就需要做修改,比较麻烦。
  • 创建共享对象
    • 目的:节省内存
    • 动作:创建了0-9999,和一些类似OK的命令回复
    • 原理:类似于C++的智能指针,使用refcount做引用计数,释放一个key的时候引用计数-1,只有当引用计数为0的时候才释放内存,因为redis的字符串对象是下面这个结构,所以相比于使用指针的4byte节省了很多内存。
struct sdshdr{
     int len;         //字符串长度
     int free;        //空闲空间长度
     char buf[];     //保存字符串
}
测试共享对象
Object refcount : 查看当前key的val有多少个对象在引用
1.4 还原数据库状态(RDB,AOF持久化)
背景:为了防止机器因为一些意外原因导致机器关机,出现故障等,因为redis状态都是保存在内存中的,内存中的数据重新开机后是不存在的,所以为了防止机器故障等情况需要把数据保存在磁盘中。
 
先简单说一下两个持久化的想法
RDB:触发保存条件的时候,是将当前时刻数据库状态保存在了磁盘。
AOF:触发保存条件的时候,是将当前执行的命令保存在了磁盘。
 
RDB文件结构
大写 REDIS EOF SELECT 都是常量,用来标记后续是什么。
REDIS:代表RDB文件。
EOF:代表databases结尾。
SELECT:代表后面紧跟数据库号。
 
check_sum:代表根据前面其他值求出的一个校验和,在还原数据库状态的时候会计算校验和看是否是一致的来判断RDB文件是否损坏。
Type key value :代表一个键值对,如果有过期时间,就再加上一个过期时间。
 
RDB文件保存就是各个数据库的键值对,然后再将这些导入到数据库中即可。
 
AOF文件结构
文件中保存都是客户端发送的命令,导入数据库状态的时候,创建一个伪客户端,然后一条一条执行命令就可以完成还原。
 
 
2.等待客户端连接
2.1 Redis 6.0 以前
redis是单线程的,但是使用了IO多路复用,处理连接的速度也很快
多个事件有可能同时发生,会把发生的事件都放入一个队列,然后单线程去处理。
IO多路复用:select,epoll, evport, kqueue都各自封装了一个文件,API都是一样的,底层实现可以互换。
底层原理可看 部门传奇张健的文章:KiteX串讲 里面有IO多路复用的讲解
 
时间开销:
  • 逻辑计算的开销
  • 同步IO的开销
    • read系统调用读socket,需要将数据从内核态拷贝到用户态
    • write系统调用写socket,需要将数据从用户态拷贝到内核态
 
单线程缺点:
  • 只能使用一个CPU核(这里说的是work线程,后台处理线程除外)
  • 如果value太大,IO是性能下降很多
 
2.2 Redis 6.0及以后
新推出了 IO多线程
  • Redis.conf 配置文件
  • 默认不打开IO多线程功能
#打开IO多线程
io-threads-do-reads yes
#多线程数量
io-threads 4
优点:
  • 能够使用CPU多个核
  • 优化了IO的时间消耗
 
 
 
3.客户端与服务器通信
 
3.1 struct结构
#客户端结构
struct redisClient{
    redisDb *db;             // 记录了客户端当前使用的数据库]
    multiState mstate;      // 事务队列
    int fd;                  // 套接字描述符
    robj *name;              // 客户端名字(默认没有)  
    int flags;               // 标志当前客户端处于什么角色与状态,
    //输入缓冲区
    sds querybuf;            // 输入缓冲区  保存了客户端发送的命令请求(resp协议)
    robj **argv;             // 一个由命令参数组成的一个数组
    int argc;                // 命令参数个数,包括命令本身,比如set
    struct redisCommand *cmd;// 指向命令的实现函数等信息
    //输出缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES];  //固定缓冲区
    int bufpos;                         //记录固定缓冲区已使用的字节数量(长度)
    list *reply;                        //可变长缓冲区
   
    int authenticated;                  //用于记录客户端是否通过身份验证 0未通过 1通过
    //时间属性
    time_t ctime;                      //创建客户端的时间
    time_t lastinteraction;            //客户端与服务器最后一次交互(计算客户端空转时长)
    time_t obuf_soft_limit_reached_time;//输出缓冲区第一次到达软性限制的时间
 
    int cronloops;             //serverCron函数执行次数(用来判断执行N次执行特殊代码)
    ...
}
 
flags:代表当前客户端处于什么状态
比如:
角色:REDIS_MASTER 主服务器        REDIS_SLAVE  从服务器
状态:
REDIS_MONITOR 正在实时监听服务器接收的命令
REDIS_MULTI  正在执行事务
REDIS_DIRTY_CAS   事务使用watch命令监听的key被修改,事务安全性被破坏
...
 
 
# 命令表中命令对应的结构
struct redisCommand{
    char *name;            //命令名
    redisCommandProc proc; //命令实现函数
    int  arity;            //命令参数个数
    char *sflags;          //字符串形式的标识值
    int flags;             //二进制标识,方便进行位操作
    long long calls;       //总共执行了多少次这个命令
    long long milliseconds;//执行命令耗费的总时长
}
 
 
 
#服务器结构
struct redisServer{
     redisDb *db;                     // 一个db数组,保存所有的数据库
     list *clients                   //记录连接当前服务器的所有客户端,一个链表结构
     struct saveparam *saveparams;    //记录了保存条件的数组(RDB文件间隔性保存)
     long long dirty;                 //修改计数器
     time_t lastsave;                 //上一次执行保存的时间
 
     sds aof_buf;                     //AOF缓冲区
     redisClient *lua_client;         //lua脚本的伪客户端
 
    time_t unixtime;                //保存了秒级精度的系统当前unix时间戳
    long long mstime;               //保存了毫秒级精度的系统的当前unix时间戳
    unsigned  lruclock:22;          //
    //服务器QPS
    long long ops_sec_last_sample_time;//上一次进行抽样的时间
    long long ops_sec_last_sample_ops; //上一次抽样,服务器已经执行的命令数量
    long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];//抽样结果数组
    int ops_sec_idx;//数组索引值
    
    size_t stat_peak_memory;           //已使用内存峰值
    int shutdown_asap;             //关闭服务器标识 值1:关闭服务器  值0:不做动作
    int aof_rewrite_scheduled;     //标识是否有BGREWRITEAOF命令被延迟了
    
    pid_t rdb_child_pid;           //记录执行BGSAVE命令的子进程ID,没有为-1
    pid_t aof_child_pid;           //记录执行BGREWRITEAOF命令的子进程ID,没有为-1
    ...
}
 
#记录了保存条件的数组(RDB文件间隔性保存)
struct saveparam{
    time_t seconds;       //秒数
    int changes;          //修改数
}
 
#数据库结构
struct redisDb{
    dict *dict;    // 键空间,保存了数据库里面所有键值对
    dict *expires; // 过期字典,保存了键的过期时间
 
# list这种lpop这种阻塞命令才会使用到
    dict *blocking_keys; //阻塞中的key
    dict *ready_keys;    //阻塞中的key被添加了数据
 
    dict *watched_keys;  // 事务中被监视的key
    int id;
    long long avg_ttl;
    unsigned long expires_cursor;
    list *defrag_later;
}
 
struct redisObject{
    unsigned lru:22;     //对象的最后一次被命令访问的时间
}
 
3.2 通信过程
🌰:事务执行的流程
  1. 建立连接
  2. 事务MULTI命令开始,会将redisClient的flags属性打开 REDIS_MULTI标识,代表事务开始
  3. 后面其他命令除了watch ,exec,unwatch , discard命令,服务端都会根据REDIS_MULT标识将命令放入一个事务队列中
  4. 如果有WATCH监视key的命令
  5. 其他客户端每次执行完set等命令后会去查看redisClient的redisDB的watched_keys如果监视的key被修改,就会把监视该key的客户端的REDIS_DIRTY_CAS标识打开,代表事务安全性已经被破坏
  6. EXEC
    1. 如果没有REDIS_DIRTY_CAS标识,执行所有命令,并且返回所有命令回复
  • 如果有REDIS_DIRTY_CAS标识,服务器拒绝执行事务
 
3.3 ServerCron
作用:一个定期执行的时间事件,默认100毫秒运行一次,负责管理服务器的资源,维护服务器的良好运转。
 
  1. 更新服务器时间缓存:服务器内部功能很多需要获取系统当前时间,每次获取当前时间都需要调用一次系统调用,为了减少调用次数,100毫秒调用一次来更新,不过这个精确度不高,主要用于打印日志,更新LRU时钟,是否进行持久化等一些精确度不高的功能上。
  2. 更新服务器的QPS:因为ServerCron的频率是100毫秒一次,采用的方法是100毫秒内隔了多少条命令,然后平均计算一毫秒然后*1000的一个估计值,会有一个数组保存连续十五次100毫秒的估计值,然后求平均值来计算。
  3. 更新服务器内存峰值记录
  4. 管理客户端资源:
    1. 检查一部分客户端,如果很久没有和服务器进行互动就会释放该客户端。
    2. 如果客户端上一次输入缓冲区超过一定长度就会将它释放,避免占用太多内存。
  5. 管理数据库资源:删除其中一些过期key,并对字典进行收缩操作。
  • key的过期删除策略 惰性删除 和 定期删除结合使用
  • 定期删除:隔一段时间去删除key,难点在于一次删除key执行多久时间,多久一次的频率去删除,redis的做法是使用ServerCron 100毫秒一次
  • 每次在规定时间内检查DEFAULT_DB_NUMBERS个数据库,每个数据库检查DEFAULT_KEY_NUMBERS 个key,下次定期删除是接在了这次的进度后面。
  • 惰性删除:下次访问到该key的时候检查如果过期了就删除,缺点是如果大多数key都是只访问了一次,就会在数据库留下很多key占用内存,因为定期删除中间还是隔了100毫秒,并且每次定期删除不是能把所有过期的key都删除,所以需要加上惰性删除来保证过期key不会再被使用。
  1. 将AOF缓冲区中的命令写入AOF文件中:AOF文件的频率是100毫秒一次,最多丢失100毫秒的数据。
  2. 检查RDB自动保存条件是否满足:RDB的自动保存条件可以设置多个,n秒内进行了m次修改就会触发保存。
 
  • RDB(BGSAVE)
    • 优点:服务器重启的时候恢复数据库状态速度快,因为本身就是直接保存的数据库状态。
    • 缺点:保存成RDB文件速度慢,需要将整个当前状态记录下来成RDB文件,所以保存的间隔比较长,如果机器故障,丢失的数据会比AOF多。
  • AOF (BGSAVEAOF)
    • 优点:100毫秒的频率保存一次,因为每次只需要将缓冲区里面新的命令存入就可以了,所以出故障的时候丢失的数据少,只是少100毫秒的数据。
    • 缺点:服务器重启的时候恢复数据库速度慢,因为AOF是命令,需要客户端一条一条的执行,所以IO次数会比一次导入RDB文件的IO多,并且会有命令冗余,比如set一个key100遍。
    • AOF重写:(BGREWRITEAOF)
      • redis4.0之前:会将当前数据库状态的每个key弄成一条命令存入新的AOF文件中,在后台重写的时候会将重写阶段的命令放入重写缓冲区,然后重写完成后再将重写缓冲区的命令放入AOF文件中。
      • redis4.0之后:会将当前数据库状态弄成RDB文件格式存入AOF文件中,目的是因为RDB恢复速度比AOF快,然后重写阶段的命令放入重写缓冲区内,重写完成后再将重写缓冲区的命令放入AOF文件中,达到AOF文件中有RDB和AOF两种文件格式存在。
 
4.集群
4.1 主从复制
背景:服务器因为某种原因无法运行,然后无法正常提供服务。
解决:主服务器会有一些从服务器,主从服务器的数据一致,主服务器挂掉的话可以选举新的主服务器出来代替进行服务,并且如果是读请求,可以请求从服务器起到分担压力的作用。
 
Redis的主从复制主要分为 同步命令传播 两个操作
 
同步 (SYNC ,PSYNC)
  • Redis 2.8之前:
    • 主服务器将RDB文件发送给从服务器并开启缓冲区用来存这阶段客户端写来的命令
    • 从服务器接收到并且导入RDB文件
    • 主服务器将缓冲区内的命令也给从服务器
    • 从服务器写入缓冲区内的命令
      • 缺点如果不是初次复制,是断线后重连,只缺少断线后的数据,但是还是会执行上面的流程,效率非常慢。
  • Redis 2.8之后:
    • 初次复制依然是上面的流程
    • 原理:每个服务器都会保存一个偏移量,断线重连只会对比偏移量,将缺少的数据发送给从服务器。
 
从服务器复制主服务器(slaveof)
 
命令传播
为了保证访问主从服务器的数据都是一致的,主服务器接收到一条写命令,会传播给所有从服务器,让从服务器也执行来保证一致。
 
  • 从服务器以一秒的频率向主服务器发送命令 REPLCONF ACK <replication_offset>
    • 主服务器如果超过一秒没有收到REPLCONF ACK代表中间连接出现了问题。
    • 检测命令的丢失,命令传播的时候可能因为某种网络问题,导致命令缺失,没有到达从服务器,如果从服务器的偏移量低于主服务器,主服务器就会把丢失的数据发给从服务器。
 
 
4.2 sentinel (哨兵)
目的:Redis的高可用的解决方案,监视任意多个主从服务器,如果主服务器下线,进行故障迁移
原理:
  1. 启动服务器,因为sentinel其实就是redis服务器,所以启动流程和前面差不多,不过因为sentinel只是用来做故障迁移,所以不需要导入RDB和AOF文件,并且命令表也和普通redis服务器不一样,命令更少。
  2. 10s一次的频率向主服务器发送info命令获取信息
  3. 10s一次的频率向从服务器发送info命令获取信息
  4. 通过发布订阅以2s一次的频率向频道里面发送sentinel和主服务器的信息
  5. 如果接收到其他sentinel发送到频道的消息进行更新
  6. sentinel之间相互连接
  7. 检测主观下线:1s一次的频率向其他 主从服务器,sentinel发送ping命令,如果连续down-after-milliseconds毫秒内都返回无效回复或者不返回就会打开  SRI_S_DOWN代表主观下线。
  8. 检查客观下线:主观下线只能代表有个别认为它下线,会再进行操作向其他监视的进行询问,如果积累到一定票数就会认为下线。
  9. 为了保证只有一个sentinel执行故障转移操作,raft算法选举头Sentinel。
  10. 进行故障迁移,raft算法选举新的主服务器。
  11. 修改从服务器的复制目标
 
4.3 集群启动流程
集群结构
#节点当前状态
struct clusterNode{
    //创建节点的时间
    mstime_t ctime;
 
    //节点的名字,由40个十六进制字符组成
    char name[REDIS_CLUSTER_NAMELEN];
 
    //节点标识
    //使用各种不同标识值记录节点的角色以及状态等
    int flags;
 
    //节点当前配置纪元,用于raft进行故障转移
    uint64_t configEpoch;
 
    //节点IP地址
    char ip[REDIS_IP_STR_LEN];
 
    //节点的端口号
    int port;
    
    //保存连接节点所需的有关信息
    clusterLink *link;
    
    //节点负责处理的槽,一个二进制位数组,指定位置代表当前槽是否是节点负责
    unsigned char slots[16384/8];
    //负责处理的槽的数量
    int numlots; 
 
    ...
}
 
#当前节点连接的节点信息
struct clusterLink{
     //连接的创建时间
    mstime_t ctime;
    
    //TCP套接字描述符
    int fd;
    
    //输出缓冲区,保存着等待发送给其他节点的消息
    sds sndbuf;
    
    //输入缓冲区,保存着从其他节点接收到的信息
    sds rcvbuf;
 
    //与这个连接相关联的节点,如果没有的话就为NULL
    struct clusterNode *node;
    // 槽与节点的关系
    clusterNode *slots[16384];
}
 
#当前节点视角下的集群状态
struct clusterState{
    //指向当前节点的指针
    clusterNode *myself;
 
    //集群当前的配置纪元,用于raft实现故障转移
    uint64_t currentEpoch;
 
    //集群当前的状态:上线,下线
    int state;
    
    //集群中至少处理着一个槽的节点的数量
    int size;
    
    //集群节点名单(包括myself)
    //key为节点名字,val为clusterNode的节点
    dict *nodes;
}
 
流程
 
  1. 开始每个节点是独立的,根据是否开启集群模式来建立集群,cluster meet命令,将B节点拉进A节点的集群,A,B节点互相将对方加入 clusterState 的 nodes结构中,并且用gossip协议通知集群中其他节点与B认识,集群节点间的关系建立完成。
  2. 槽:redis里面进行键值对分片存储的操作,划分成了16384个槽,必须所有槽都有节点负责集群才能是上线状态,因为如果没有槽没负责,通过key->槽的指定算法就没有节点处理。
  •   指派槽:cluster addslots 将槽指派给当前节点负责   建立节点->槽的映射关系。
  1. 传播节点的槽指派信息
  2. 记录集群 槽->节点的映射关系    开始接收客户端的命令
  3. 使用CRC16校验和 % 16384来计算key存在哪个槽位   cluster keyslot命令
  4. 当前节点查看槽是不是自己负责,是就执行命令,不是就通过clusterState里面 slots[16384]找到对应的节点然后推给目标节点处理。
 
重新分片
如果集群中增加或者删除节点的话就需要进行重新分片,重新指定槽的负责人
 
 
4.4 CAP AKF raft
 
CAP准则
  一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)
一致性:一个集群里,无论从哪台机器访问,数据都是一致的
可用性:在可用节点上,能够正常提供服务,一般指的不会出现大量超时,错误等问题
分区容错性:主服务器出现宕机,依然能够正常提供服务
 
注意:三个准则中只能选择其中两种!!!
冲突原因:
保证C:如果要保证C的话,那么必须要等这条命令的修改执行到了所有服务器上,主从上都有这个修改,那么就必须等主服务器进行了命令传播后,才能回复客户端
保证A:如果要保证A的话,那么一条命令的执行时间就必须在规定的范围内,不能太久,及时给客户端作出响应
保证P:如果要保证P的话,那么一个节点坏了的时候需要有从服务器及时替上
 
 
🌰:选择CA,不用P?
如果选择CA的话,说明我们要保证数据同步保持一致并且不会超时,如果要加上P的话,说明我们要设立主从,代表我们要同步完从服务器才能返回这样才能保证一致性,但是同步所有的从服务器的时间会超时,所以有冲突。
🌰:选择CP,不用A?
如果选择CP的话,说明我们可以给时间去同步所有的从服务器,去保证数据一致,但是相应的时间也会花很久,那么我们就无法保证A。
🌰:选择AP,不用C?
如果选择AP的话,说明我们可以直接主服务器做了修改后就给客户端响应,然后去异步的命令传播给其他从服务器,但是有可能其中某些从服务器的包丢失,那么其他请求过来访问的时候就有可能出现数据不一致的情况。
  
Redis:AP模型
C:没有实现C,但是实现了最终一致性,命令传播发送给其他从服务器,因为网络问题导致命令丢失时,从服务器有一个心跳检测机制,1s一次的频率检测主从服务器的偏移量是否一直来补齐缺失的命令来实现最终一致性。
A:主服务器执行完命令后直接返回,然后后台发送命令给从服务器,响应速度很快。
P:使用主从复制机制来避免主服务器宕机问题。
 
 
AKF原则
在企业设计架构的时候,考虑AKF原则,
x轴:考虑高可用,一个节点的数据需要做备份,越大代表备份越多     ->  对应redis的主从复制。
 
y轴:代表将一个节点的数据细分成多个模块,想微服务将业务上细分成多个模块  -> 对应redis的集群使用多个主服务器,划分成多个模块。
 
z轴:代表当细分成了多个模块之后,一个模块的并发依然很大,还需要细分,那么就需要把一个模块的数据分成多台机器存储,但是需要知道数据存在了哪台服务器上,就需要算法计算在哪台机器
->对应redis的将一个集群的数据库划分成了16384个槽,key存在于一个槽内,每台服务器负责一部分槽。
 
  • A模型:
    • 原理:算法写在客户端这里。
    • 缺点:如果要更改算法,每个客户端都要修改。
 
  • B模型:
    • 原理:找一个代理,每个客户端访问先经过一个代理,代理做计算,然后访问具体机器。
    • 缺点:代理承受的压力太大。
 
  • C模型:
    • 原理:服务器自身进行算法划分,redis的做法就是分了16384个槽,然后每台服务器各自占了其中一部分槽,如果key属于该槽,那么就由这条命令执行。
 
 
raft 
分布式一致性算法,一般用于主从服务器,主服务器挂掉之后如何选举
简单说一下实现,三个角色,leader(主) , follower(从) ,candidate(参与投票的候选人)
每个节点都有一个定时器,每个节点的过期时间不一样,如果一个节点的定时器归零了,那么这个节点就会成为candidate,然后让其他节点给自己投票,出现多个的话就会同时要票,如果没有超超过半数就会重新选举,票是先到先得的,如果超过半数,那么就会成为新的leader节点,这只是在于leader节点挂了的话,才会出现定时器归零,如果leader没有挂掉,那么就会定期发送心跳包,follower节点接收到心跳包,会重新刷新定时器的时间,直到leader挂掉,才会出现定时器归零进行选举leader节点。
 
 
 
 
5.底层数据结构
  1. 查阅资料
《redis设计与实现》