redis数据库内部机制

Jul 1, 2016


1.多个数据库

一个Redis实例可以由多个数据库,默认为16个,Redis客户端默认连接0号数据库,通过select命令可以切换数据库。

redisServer结构体中,保存redisDb数组,初始化服务时,会根据dbnum属性来创建多少个数据库。

struct redisServer{
    ......
   //保存服务器的所有数据库
   redisDb *db;
   //服务器的数据库数量
   int dbnum;
   //一个链表,保存了所有客户端状态结构
   list *clients;
}
select切换数据库

redis服务器会为每个客户端创建一个redisClient结构体(保存在redisServer.client链表中),来保存相关数据,其中有一个属性为db,记录当前选择的数据库。

select命令的原理就是:通过修改redisClinet.db指针,让它指向不同的数据库,实现select切换功能。

typedef struct redisClient{
//....
//记录客户端当前正在使用的数据库
redisDb *db;
//....
}

select命令源码:

/*
 * 将客户端的目标数据库切换为 id 所指定的数据库
 */
int selectDb(redisClient *c, int id) {
    // 确保 id 在正确范围内
    if (id < 0 || id >= server.dbnum)
        return REDIS_ERR;
    // 切换数据库(更新指针)
    c->db = &server.db[id];
    return REDIS_OK;
}

2.数据库键空间

redis是kv数据库,其中保存着众多的k-v键值对,每个数据库的所有键值对都保存在redisDb.dict中,这个dict也被称为数据库的键空间。

typedef struct redisDb{
    //...
    //键空间,保存所有的键值对
    dict *dict;
}redisDb

其中,键一般是个字符串对象,其值可以是常见的redis对象,比如string,list,set,szset,hash。

图片地址:http://img.blog.csdn.net/20160505160854320

通过对dict的操作,即对字典的操作,就能实现对整个数据库键值对的操作,比如增加、删除、查询键值对。对字典的操作,可以查阅dict.c上的源码。

举个栗子:从数据库db中获取指定键lookupKey,

/*
 * 从数据库 db 中取出键 key 的值(对象)
 * 如果 key 的值存在,那么返回该值;否则,返回 NULL 。
 */
robj *lookupKey(redisDb *db, robj *key) {
    // 查找键空间,dictFind是字典操作的API
    dictEntry *de = dictFind(db->dict,key->ptr);
    // 节点存在
    if (de) {
        // 取出值
        robj *val = dictGetVal(de);
        // 更新LRU时间信息(只在不存在子进程时执行,防止破坏 copy-on-write 机制)
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
            val->lru = LRU_CLOCK();
        // 返回值
        return val;
    } else {
        // 节点不存在
        return NULL;
    }
}

此外,还有一系列键值操作的API:

  • setKey 设置键值对
  • dbExist 判断是否存在key
  • dbAdd 增加
  • dbDelete 删除
  • lookupKey 查询

键空间的其他维护操作

redis命令对数据库进行读写时,会执行一些额外操作。比如:

  • 更新服务器的hit和miss属性
  • 更新键的LRU时间
  • 读取键时,假若是过期的,则需要删除
  • 如果对该键有监视,则需要标记为脏,即标记client.flags |= REDIS_DIRTY_CAS
  • 如果开启了通知功能,需要发送通知

3.键的生存时间

通过xpire,pexpire,expireat,pexpireat命令,可以设置键的生存时间。

注:上述四个命令,最后都转换成pexpireat命令来实现的。

redisDb结构的expires属性保存了数据库中所有键的过期时间,expires是一个字典,键是一个指针,指向键空间的某个对象,值是一个毫秒级的时间戳,即过期时间。

过期键的删除

Redis才用了两个过期键删除策略:

  1. 惰性删除,服务器在读取键时,进行过期检查,过期的话就删除之。
  2. 定期删除,每隔一段时间,就对数据库进行一个检查,删除里面的过期键。
惰性删除策略

db.c/expireIfNeeded函数实现,读写数据库之前都会调用该函数对键进行检查。

/*
 * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
 * 返回 0 表示键没有过期时间,或者键未过期。
 * 返回 1 表示键已经因为过期而被删除了。
 */
int expireIfNeeded(redisDb *db, robj *key) {
    // 取出键的过期时间
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 没有过期时间
    if (when < 0) return 0; /* No expire for this key */
    // 如果服务器正在进行载入,那么不进行任何过期检查
    if (server.loading) return 0;
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    if (server.masterhost != NULL) return now > when;
    // 运行到这里,表示键带有过期时间,并且服务器为主节点
    // 如果未过期,返回 0
    if (now <= when) return 0;
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
}
定期删除策略

数据库的定期删除由redis.c/activeExpireCycle函数实现,当redis的周期性操作redis.c/serverCron执行时,就会调用activeExpireCycle函数,在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除过期键。

源码:见redis.c/activeExpireCycle

4.AOF,RDB,主动对于过期键的处理

RDB

  1. 生成RDB时,过期键不会被保存到RDB中
  2. 载入RDB时,主服务器不会载入过期键,从服务器会

AOF

当过期键,被惰性删除或者定期删除后,程序会向AOF追加一个DEL命令,来显式地记录该键被删除。

主从

从服务器的过期键删除动作由主服务器控制:

  • 主服务器删除一个过期键,会向从服务器传播一个DEL命令
  • 从服务器读取到一个过期键,不会进行删除,正常处理,只有在收到主服务器传播过来的DEL命令才会删除。

5.Rerference

  1. 黄健宏. Redis设计与实现[M]. 机械工业出版社, 2014.

上一篇博客:乐视编程题_困兽之斗
下一篇博客:redis中事件模型实现分析