redis

Posted by jabin on December 28, 2019

一、什么是redis

  1. 开源的,支持网络,基于内存,键值对存储数据库
  2. redis默认创建16个数据库,通过select语句可以切换数据库, 即单个实例可以有多个数据库,类似MySQL

二、redis底层实现

2.1. 数据结构

2.1.1 简单的动态字符串

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

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

    // 数据空间
    char buf[];
};
  1. 空间预分配, buf的长度可能不是实际字符串的长度,通过len获取

2.1.2 链表

/*
 * 双端链表节点
 */
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;

  1. 双端链表
  2. 通过list 来维护整个链表

2.1.3 字典-hashtable

/*
 * 哈希表节点
 */
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;
  1. 机遇哈希表实现字典
  2. 通过链表(小尾巴)解决哈希可能出现的冲突
  3. 两个哈希表,来实现哈希表的扩缩容
  4. 通过rehashidx来计数,渐进式地把ht[0]的键值对,迁移到ht[1] 双写,避免集中式带来的cpu压力

2.1.4 跳表

/* ZSETs use a specialized version of Skiplists */
/*
 * 跳跃表节点
 */
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. 一个List维护表头、表尾,和节点数量
  2. 一个ListNote包含obj, 多个forward指针(跨度), 一个backward指针,一个score
  3. ListNote是按score的从小到大排序的, 当查找到某个节点时, 所经过的跨度(span)之和,即是该节点的排序

2.2 核心对象-redisObject

typedef struct redisObject {

    // 类型 string list hash set zset
    unsigned type:4;

    // 编码  比如string 的编码可以是int , raw(sds)
    unsigned encoding:4;

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

    // 引用计数
    int refcount;

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

} robj;
  1. redisObject为对底层数据的封装, 是最小的操作单位
  2. 针对不同的使用场景可以用相同的对象来实现
  3. 引入了引用计数和lru字段,来对不用的对象进行回收、优先删除(golang 用的三色标(white\grey\black)记法)

三、单机数据库实现

3.1 数据库

  1. 过期键删除策略:惰性(访问删除) 和 定时删除(相对空闲时)配合

3.2 数据持久化–rdb&aof

3.2.1 全量策略

即rdb快照,通过配置实现n分钟内对数据库修改了m次(所以这里维护一个计数, save后重置),则从内存dump数据形成rdb文件进行保存到相应目录。

  1. 原理:当redis需要做持久化时,redis会fork一个子进程;子进程将数据写到磁盘上一个临时RDB文件中;当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处就是可以copy-on-write
  2. 优点:因为存储的快照文件为内存映射,所以恢复快。缺点:断电时会导致部分数据丢失

3.2.2 增量策略

aof日志,将执行的命令写入txt文件进行记录

  1. 为了进行命令的合并, 需要进行重写。 由子进程进行重写,防止阻塞。
  2. 为了解决父子进程带来的数据不一致问题, 会有一块缓冲区来记录,重写过程中,写命令的操作。 待子进程重写完, 再告知父进程把缓冲区的内容写到新AOF文件中
  3. 优点:数据不容易丢失。缺点:恢复速度慢

3.3 事件

3.3.1 文件事件

accept read write connect req rsp sockets套接字->I/O多路复用(就绪的事件)->事件分发器->处理器(accept, 命令请求等) loop会从就绪队列中,取出事件进行处理。

3.3.2 时间事件

/* Time event structure
 *
 * 时间事件结构
 */
typedef struct aeTimeEvent {

    // 时间事件的唯一标识符
    long long id; /* time event identifier. */

    // 事件的到达时间
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */

    // 事件处理函数
    aeTimeProc *timeProc;

    // 事件释放函数
    aeEventFinalizerProc *finalizerProc;

    // 多路复用库的私有数据
    void *clientData;

    // 指向下个时间事件结构,形成链表
    struct aeTimeEvent *next;

} aeTimeEvent;
  1. 将所有的时间事件,放到一个链表,每次tick 判断是否有相应的事件需要处理
  2. 正常模式下,只有一个serverCron时间事件
  3. 当然,一定的误差还是有的

3.4 客户端与服务器

/* 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;

    // 创建客户端的时间
    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 */

    // 用于保存主服务器传来的 RDB 文件的文件描述符
    int repldbfd;           /* replication DB file descriptor */

    // 从服务器的监听端口号
    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 */

    // 被监视的键
    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;

四、独立功能实现

4.1 事务(ACID)

  1. MULTI, 将多个执行操作放入queued队列中,exec后再执行
  2. WATCH 若某个key被修改了, 对应监听的客户端,会打上REDIS_DIRTY_CAS 标签,标识事务安全性已被破坏,exec会执行失败
  3. 不支持回滚功能
  4. 单进程, 事务串行运行
  5. 持久性 rdb Or aof

五、常用命令

1. 进程命令

  1. stop: redis-server: kill -9;
  2. restart: /usr/local/bin/redis-server restart /etc/redis/redis.conf
  3. 设置密码: config set requirepass qqgame

2. 操作命令

  1. 可用expire或者pexpire命令,以秒或者毫秒设置某个key的生存时间,到期后服务器会自动删除键
  2. TTL命令和PTTL命令返回一个键的生存时间
  3. PERSIST可以已出一个键的生存时间
  4. 其它基本命令

六、单线程设计下的分布式实现

1. 主从同步

slave 起来后,向master发同步请求, master将快照给slave, slave通过快照载入数据,以后每次请求, master都发一份给slave

2. 实现-tredis

  1. 单线程下, 乐观锁、事务等都可以玩。 在一个队列里依次执行,不会产生并发问题
  2. 分布式后, 就要考虑分布式锁的各种问题了(乐观锁), 做了层proxy, 相同的key路由到后端某台redis实例. 所以单key不能过分大, 比如上亿。

    3. 为何单线程还能这么快

  3. 基于内存
  4. 数据结构简单
  5. 上下文切换
  6. 瓶颈可能是内存大小和带宽,不是cpu, 所以就使用单线程了(官方)