1. 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h / redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:
2. 切换数据库
3. 数据库键空间
Redis是一个键值对数据库服务器,服务器中的每个数据库由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中所有的键值对,称这个字典为键空间。
3.1 添加新键
3.2 删除键
3.3 更新键
3.4 对键取值
3.5 读写键空间时的维护操作
当使用redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,包括:
- 在读取一个键后(读和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数
- 在读取一个键后,服务器会更新键的LRU(最后一次使用)时间
- 如果服务器在读取一个键时,发现键已过期,服务器会先删除过期键,再执行余下操作
- 如果有客户端使用WATCH命令监视了某个键,服务器在对被监视的键进行修改后,会将这个键标记为脏(在事务一章会介绍)
- 服务器每修改一个键之后,都会对脏键计数器的值增1,这个计数器会触发服务器的持久化和复制操作(在持久化和复制议长中介绍)
- 如果服务器开启了数据库通知功能,在对键进行修改之后,服务器将按配置发送相应的数据库通知(发布和订阅一章中介绍)
4. 设置键的生存时间或过期时间
4.1 设置过期时间
- EXPIRE<key> <ttl> 键的生存时间为 ttl ( time to live ) 秒
- PEXPIRE<key> <ttl> 键的生存时间为 ttl ( time to live ) 毫秒
- EXPIREAT <key> <timestamp> 命令用于将键的过期时间设置为timestamp所指定的秒时间戳
- PEXPIREAT <key> <timestamp> 命令用于将键的过期时间设置为timestamp所指定的毫秒时间戳
4.2 移除过期键
PERSIST命令用于移除过期键。
4.3 计算并返回剩余生存时间
TTL 命令以秒返回键的生存时间,PTTL以毫秒返回键的生存时间。
5. 过期键删除策略
- 定时删除(占用太多的CPU时间,影响服务器的响应时间和吞吐量)
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除(浪费内存,有内存泄漏风险)
放任键过期不管,但是每次从键空间获取键时,都检查取得的键是否过期,如果过期,就删除该键;如果没有过期,就返回该键。
- 定期删除
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。
6. Redis过期键删除策略
Redis服务器使用惰性删除和定期删除两种策略。
6.1 惰性删除策略
6.2 定期删除策略的实现
过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数时,activeExpireCycle函数就会被调用,整个过程的伪码描述如下:
# 默认每次检查的数据库数量 DEFAULT_DB_NUMBERS = 16 # 默认每个数据库检查的键数量 DEFAULT_KEY_NUMBERS = 20 # 全局变量,记录检查进度 current_db = 0 def activeExpireCycle(): # 初始化要检查数据库的数量 # 如果服务器的数据库数量比DEFAULT_DB_NUMBERS 要小 # 那么以服务器的数据库数量为准 if server.dbnum < DEFAULT_DB_NUMBERS: db_numbers = server.dbnum else: db_numbers = DEFAULT_DB_NUMBERS # 遍历各个数据库 for i in range(db_numbers): # 如果current_db的值等于服务器的数据库数量 # 这表示检查程序已经遍历了服务器的所有数据库一次 # 将current_db重置为0,开始新的一轮遍历 if current_db == server.dbnum: current_db = 0 # 获取当前要处理的数据库 redisDb = server.db[current_db] # 将数据库索引增1,指向下一个要处理的数据库 current_db += 1 # 检查数据库键 for j in range(DEFAULT_KEY_NUMBERS): # 如果数据库中没有一个键带有过期时间,那么跳过这个数据库 if redisDb.expires.size() == 0 : break # 随机获取一个带有过期时间的键 key_with_ttl = redisDb.expires.get_random_key() # 检查键是否过期,如果过期就删除它 if is_expired(key_with_ttl): delete_key(key_with_ttl) # 已达到时间上限,停止处理 if reach_time_limit() : return
7 AOF、RDB和复制功能对过期键的处理
7.1 生成RDB文件
在执行SAVE或BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
7.2 载入RDB文件
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器会对RDB文件进行载入:
- 如果服务器以主服务器模式运行,在载入RDB文件时,程序会对文件中保存的键进行检查,只有未过期的键会被载入到数据库中
- 如果服务器以从服务器运行,载入RDB文件时,文件中保存的所有键,无论过期与否都会被载入到数据库中
7.3 AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,且被惰性删除或定期删除之后,程序会向AOF文件追加一条DEL命令,显式记录该键已被删除。
7.4 AOF重写
和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中
7.5 复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,即使碰到过期键,也不会将过期键删除
- 从服务器只有在接到主服务器地DEL命令之后,才会删除过期键