在redis源码中数据库的结构由server.h/redisDb表示, redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space),redisDb源码:
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
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 */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
源码中redisDb拥有字典属性dict
,字典中存储了数据库中的键,为字符串类型的redisObject。这个redisObject中的ptr属性指向值的redisObject,结构示意图:
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作:
通过EXPIRE命令或者PEXPIRE命令可以设置键的过期时间,那么在数据库中这个过期时间是怎么维护的呢?
redisDb结构体中有一个字典属性expires
便是用来保存键的过期时间的, 我们称这个字典为过期字典。过期字典的键是一个指针,指向键空间中的键;过期字典的值是一个long类型的数,记录了过期时间的时间戳。当客户端执行PEXPIREAT命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间。
如果现在给key设置一个过期时间,在过期时间到的时候,Redis是如何清除这个key的呢?Redis 中提供了三种过期删除的策略:
Redis 中实际采用的策略是惰性删除加定期删除的组合方式,服务器会定期清除掉一部分过期的key,对于那些未清除到的过期key,会在获取这个key的时候进行判断是否过期,过期则删除。惰性删除会带来一个问题就是当从从库获取一个过期key的时候从库是否应该删除这个key呢?
如果一个主库创建的过期键值对,已经过期了,主库在进行定期删除的时候,没有及时的删除掉,这时候从库请求了这个键值对,当执行惰性删除的时候,因为是主库创建的键值对,这时候是不能在从库中删除的。从库会通过惰性删除来判断键值对的是否过期,如果过期则读不到这个键,真正的删除是当主节点触发键过期时,主节点会同步一个del命令给所有的从节点。
我们知道redis 持久化策略中包括RDB持久化功能、AOF持久化,这两种持久化对过期未删除的键处理也是有区别的。RDB持久化不会保存过期未删除的键,而AOF持久化当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显式地记录该键已被删除。
在 Redis 命令中,有一些命令是阻塞模式的,BRPOP, BLPOP, BRPOPLPUSH, 这些命令都有可能造成客户端的阻塞。
比如向客户端发来一个blpop key命令,redis先找到对应的key的list,如果list不为空则pop一个数据返回给客户端;如果对应的list不存在或者里面没有数据,就将该key添加到redisDb 的blockling_keys的字典中,value就是想订阅该key的client链表。并将对应的客户端标记为阻塞。
如果客户端发来一个repush key value命令,先从redisDb的blocking_keys中查找是否存在对应的key,如果存在就往redisDb的ready_keys这个链表中添加该key;同时将value插入到对应的list中,并响应客户端。redis处理完客户端命令后都会遍历ready_keys和blockling_keys来筛选出需要pop出的clinet。
因此,redis客户端的阻塞是通过ready_keys和blockling_keys联合来实现的,blockling_keys 记录阻塞中的key和客户端,ready_keys记录数据已准备好的key。