redis设计与实现——数据库
服务器中的数据库
redis服务器将所有的数据库都保存在服务器状态redis.h/redisServer结构的db数组里,每一个redisDb代表一个数据库,redis默认创建16个数据库。
1 | struct redisServer { |
切换数据库
由于每个Redis客户端都有自己的目标数据库,客户端通过SELECT命令来切换目标数据库。而server.h的结构体client中就有一个指向redisDb的指针。
1 | // 同样client会含有指向db的指针 |
目前没有命令可以获取当前db的index,但可以通过设置唯一名字并获取clientInfo的方法动态获取index:https://stackoverflow.com/questions/50534492/redis-how-to-get-current-database-name
数据库键空间
Redis是一个键值对数据库服务器,由上面可知每个数据库都由一个redisDb结构表示,其中redisDb的dict字段保存了数据库中的所有键值对。
1 | typedef struct redisDb { |
添加新键
添加新键值对,实际上就是将键值对添加到键空间字段中,key为字符串对象,值为任意一种类型的redis对象。
以set命令为例:
1 | void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) { |
在设置过期时间的操作中,可以看到,虽然key和expire是分开存放在redisDb结构体中的,但实际上两者指向了同一个对象。
1 | void setExpire(redisDb *db, robj *key, long long when) { // 设置过期时间 |
其他的删改查操作,都是在dict字段上面封装了一层。
设置键的生存时间或过期时间
设置过期时间
通过EXPIRE或PEXPIRE命令可以为key设置秒级或毫秒级的生存时间(Time To Live,TTL)。也可以用EXPRIEAT或者PEXPIREAT设置一个过期时间戳。事实上,这三个命令的底层实现都是通过时间戳的设置方式来完成过期时间设置的。
参考源码,这个函数是EXPIRE, PEXPIRE, EXPIREAT和PEXPIREAT四个命令的底层实现:
1 | void expireGenericCommand(client *c, long long basetime, int unit) { |
保存过期时间
redisDb结构的expires字段保存了数据库中所有key的过期时间,这是一个字典,其key是一个指向某个键对象的指针,而value则是一个long long类型的整数,一个毫秒级别的unix时间戳。
移除过期时间
使用PERSIST命令可以移除一个键的过期时间,实际上就是删除expires字段中该键与过期时间的项。
1 | void persistCommand(client *c) { |
计算并返回剩余生存时间
TTL和PTTL命令则是返回以秒为单位或者毫秒为单位的键剩余时间,实现比较简单,就是计算键的过期时间与当前时间之间的差。
过期键删除策略
Redis采用了两种删除策略:惰性删除和定期删除。其中,惰性删除是一种对CPU时间最友好的策略,程序只会在取出键时才会对键进行过期检查。
惰性删除策略的实现
过期键的惰性删除策略都必须要经常函数db.c/expireIfNeeded函数实现。
1 | int expireIfNeeded(redisDb *db, robj *key) { |
调用dictDelete的时候不会删除dict对象,只会删除expires对象,尽管它们公用key对象。
1 | // 同步删除,通过调用dictDelte实现 |
定期删除策略的实现
过期键的定期删除策略时由expire.c/activeExpireCycle()实现的,它会在规定的时间内,多次去遍历服务器中的各个数据库,从数据库的expires字段中随机抽查一部分键的过期时间。
AOF、ROB和复制功能对过期键的处理
- 产生的新RDB文件和重写的AOF文件都不会包含已过期的键;
- 当主服务器删除一个键之后,会向所有从服务器发del命令;
- 当一个过期键被删除之后,服务器会追加一条del命令到现有AOF文件的末尾;
- 从服务器即使发现过期键也不删除,而是等待master节点发来del命令;