redis 数据库实现

数据库的 server 端和 client 端

server 端

  • 数据库在 server 端的存储

// redisServer 结构
struct redisServer {
	// ...
	// 数据库
    redisDb *db;
    // 数据库数量
    int dbnum;
    
    // ...
}

  • redis server 在启动时, 会初始化 dbnum 个 db

int main(int argc, char **argv) { // ... // 初始化服务器配置 initServerConfig(); // ... // 创建并初始化服务器数据结构 initServer(); // ... }

  • initServerConfig, 设置 dbnum, 默认为 16

#define REDIS_DEFAULT_DBNUM     16

void initServerConfig() {
    // ...
    // 设置数据库数量
    server.dbnum = REDIS_DEFAULT_DBNUM;
    
    // ...
}

  • initServer 创建 dbnum 个数据库

void initServer() {
	// ...
	// 创建 dbnum 个 redisDb
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);
    
    // ...
    // 初始化每个数据库 redisDb 的值
    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].ready_keys = dictCreate(&setDictType,NULL);
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].eviction_pool = evictionPoolAlloc();
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
    }
    
    // ...
}

client 端

  • 数据库在 client 端的存储, 此处涉及到 redisClient 结构, redisClient 对象是在客户端与服务端连接时创建的, 对于客户端与服务端的连接过程此章节仅作大体描述, 深入讲解将在专门章节进行

redis server 端在 initServer 时, 会创建请求接收事件, 当有客户端连接时触发事件

当 redis client 进行连接时, 触发服务端定义的事件 acceptTcpHandler -> 调用 acceptCommonHandler -> 调用 createClient -> selectDb, 设置客户端连接的数据库, 存入 redisClient->db 中

  • redisClient 结构
// 客户端结构
typedef struct redisClient {
	// ...
    // 当前正在使用的数据库
    redisDb *db;
	// 当前正在使用的数据库 id
	int dictid;
	
	// ...
}

db 指向当前客户端选择的数据库

redis 数据库在 redisServer 中的 db 数组中, 选择不同的库, 其实就是从 db 中获取不同的 db 地址

目前 redis 还没有提供获取当前所在数据库的命令, 所以在执行一些危险命令的时候, 最好显式执行 SELECT 命令选择一下库

redis 数据库实例结构

redisDb 结构 (src/redis.h)

// redis 数据库
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;

  • 数据库里面的键值对数据存放在一个大的字典结构 dict 中, 称为键空间 (keyspace)

键空间的键就是数据库的键, 数据库的键均为字符串对象

键空间的值就是数据库的值, 值的类型可以是 REDIS_STRING, REDIS_LIST, REDIS_SET, REDIS_ZSET, REDIS_HASH

键空间是一个字典, 所有对数据库的增删改查操作, 均是使用字典操作来实现的

  • 对数据进行操作过程中, 还掺杂着一些其他的操作

更新缓存命中率/不命中率

当键有修改时, 通知相关客户端

过期删除策略

过期命令

  • EXPIRE (秒为单位, 值为时间间隔)
  • PEXPIRE (毫秒为单位, 值为时间间隔)
  • EXPIREAT (秒为单位, 值为时间戳)
  • PEXPIREAT (毫秒为单位, 值为时间戳)
  • TTL (秒为单位)
  • PTTL (毫秒为单位)
  • PERSIST (移除过期时间)

设置过期时间的命令 EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT 实际上都是转换过后使用 PEXPIREAT 实现的

过期时间的保存

  • 所有间的过期信息均是存储在 redisDb 结构中的 expires 字段
  • expires 字段为 dict 结构, 键为数据库键对象的指针, 值为以毫秒为单位的时间戳
  • expires 字段的键对象与 dict 字段的键对象共享一个地址, 不会出现重复对象浪费空间

过期键的判定逻辑

  • 键存在于 expires 中
  • 当前时间已经超过了 expires 中设置的过期时间

redis 的过期删除策略

  • 定期删除
  • 每隔一段时间, 会有程序主动清除 redis 当中的过期 key, 通过限制删除操作的时长和频率来尽量减少对 cpu 时间的占用, 对 cpu 时间是友好的
  • activeExpireCycle 函数实现, initServer 注册 serverCron 事件, serverCron 在 databasesCron 中调用 activeExpireCycle
  • 函数每次执行时, 都会从一定数量的数据库中随机取出一定数量的随机键进行检查, 并删除其中的过期键
  • 惰性删除
  • expireIfNeeded 函数实现, 对数据库数据进行读写操作时, 会调用 expireIfNeeded 函数, 此函数会判断 key 是否过期, 若过期会删除

数据库通知

redis 通知系统会在后续详细作为一节讲述

db api (src/db.c)

函数

作用

备注

lookupKey

从 db 中取出指定 key 的值对象

robj *lookupKey(redisDb *db, robj *key)

lookupKeyRead

为执行读操作从 db 中取出指定 key 的值, 相对于 lookupKey 来说, 增加了判断 key 是否过期以及更新缓存命中/未命中信息

robj *lookupKeyRead(redisDb *db, robj *key)

lookupKeyWrite

为执行写操作从 db 中取出指定 key 的值, 相对于 lookupKeyRead 来说, 不会更新缓存命中信息

robj *lookupKeyWrite(redisDb *db, robj *key)

lookupKeyReadOrReply

为执行读操作从 db 中取出指定 key 的值, 若未找到, 则向客户端发送 reply 信息

robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply)

lookupKeyWriteOrReply

为执行写操作从 db 中取出指定 key 的值, 若未找到, 则向客户端发送 reply 信息

robj *lookupKeyWriteOrReply(redisClient *c, robj *key, robj *reply)

doAdd

向 db 添加键值对

void dbAdd(redisDb *db, robj *key, robj *val)

doOverwrite

修改 db 中指定 key 的值

void dbOverwrite(redisDb *db, robj *key, robj *val)

setKey

set 命令, 设置 db 中指定 key 的值, 若存在, 则覆盖, 若不存在, 则新增, 会移除 key 的过期时间

void setKey(redisDb *db, robj *key, robj *val)

dbExists

判断 db 中指定的 key 是否存在

int dbExists(redisDb *db, robj *key)

dbRandomKey

从 db 中获取随机的一个 key

robj *dbRandomKey(redisDb *db)

dbDelete

删除 db 中指定 key 的键值对数据以及键的过期时间

int dbDelete(redisDb *db, robj *key)

dbUnshareStringValue

emptyDb

清空所有 db 的数据

long long emptyDb(void(callback)(void*))

selectDb

将客户端的 db 指向 id 指定的数据库

int selectDb(redisClient *c, int id)

signalModifiedKey

每当 db 中的键被改动时, 此函数均会调用, 通知监视这个键的客户端

void signalModifiedKey(redisDb *db, robj *key)

signalFlushedDb

每当清空一个 db 时, 此函数均会调用, 通知相关客户端

void signalFlushedDb(int dbid)

removeExpire

移除指定 key 的过期时间

int removeExpire(redisDb *db, robj *key)

setExpire

设置指定 key 的过期时间为 when

void setExpire(redisDb *db, robj *key, long long when)

getExpire

获取指定 key 的过期时间

long long getExpire(redisDb *db, robj *key)

propagateExpire

若一个 key 在主节点已过期, 删除从节点以及 aof 文件中的此 key, 构造 del 命令并调用

void propagateExpire(redisDb *db, robj *key)

expireIfNeeded

检查给定 key 是否过期, 若过期, 视情况判断是否删除: 服务器正在载入不删除, 从库不删除

int expireIfNeeded(redisDb *db, robj *key)

getKeysUsingCommandTable

获取命令中的所有 key

int *getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, int *numkeys)

getKeysFromCommand

从命令中获取键, 相对于 getKeysUsingCommandTable, 多了集群转向时的支持

int *getKeysFromCommand(struct redisCommand *cmd, robj **argv, int argc, int *numkeys)

getKeysFreeResult

释放 getKeysFromCommand 的结果

void getKeysFreeResult(int *result)

slotToKeyAdd

将给定 key 添加到槽中

void slotToKeyAdd(robj *key)

slotToKeyDel

从槽中删除给定 key

void slotToKeyDel(robj *key)

slotToKeyFlush

清空所有槽保存的所有键

void slotToKeyFlush(void)

getKeysInSlot

从 hashslot 槽中获取 count 个 key

unsigned int getKeysInSlot(unsigned int hashslot, robj **keys, unsigned int count)

delKeysInSlot

删除 hashslot 槽中所有 key

unsigned int delKeysInSlot(unsigned int hashslot)

countKeysInSlot

返回指定 hashslot 槽中 key 的个数

unsigned int countKeysInSlot(unsigned int hashslot)