文章目录
redis学习2——单机数据库
数据库
redis服务器在初始化时,会根据dbnum属性创建多个数据库
redis客户端可以根据select命令对数据库进行切换
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> get hello
(nil)
服务器中每个数据库都由一个redisDb表示
typedef struct redisDb {
dict *dict; //键空间
dict *expires; //键的过期集合
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 */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb
其中dict字典保存了数据库中所有的键值对,即键空间,通过字典数据结构实现对键的添加、删除、更新、获取。当使用redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写命令,还会执行一些额外的维护操作,包括:
- 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中次数或者键空间不命中次数
- 在读取一个键之后,服务器会更新键的lru时间
- 如果服务器在读取一个键时发现该键已经国企,那么服务器会删除这个过期键
- 如果有客户端通过watch监视了某个键,那么服务器对被监视的键修改以后,把这个键标记为dirty
- 服务器每次修改一个键之后,ditry键计数器值加1
- 如果服务器开启了数据库通知功能,那么对键进行修改之后,服务器将按配置发送相应的数据库通知
设置过期时间
命令 | 作用 |
---|---|
EXPIRE <key> <ttl> | 将键key的生存时间设置为ttl秒 |
PEXPIRE <key> <ttl> | 将键key的生存时间设置为ttl毫秒 |
EXPIREAT <key> <ttl> | 将键key的生存时间设置为指定的秒数时间戳 |
PEXPIREAT <key> <ttl> | 将键key的生存时间设置为指定的毫秒数时间戳 |
过期键删除策略
- 定时删除,到点删除
- 惰性删除,发生读取时删除
- 定期删除,每隔一段时间删除
过期策略
- volatile-lru:只对设置了过期时间的key进行LRU(默认值)
- allkeys-lru : 删除lru算法的key
- volatile-random:随机删除即将过期key
- allkeys-random:随机删除
- volatile-ttl : 删除即将过期的
- noeviction : 永不过期,返回错误
AOF、RDB和复制功能对过期键的处理
RDB
- 生成RDB文件时,自动忽略过期键
- 载入RDB文件时,主服务器忽略过期键,从服务器不忽略过期键
AOF
- AOF写入,如果是定期或者惰性删除,那么在实际删除的时候添加一个删除指令
- AOF重写,忽略过期键
复制
- 主服务器删除了一个过期键后,显示地向从服务器发通知
- 从服务器执行接收到的读命令时,如果没有接收到来自主服务器的删除通知,就执行读操作,返回value
RDB持久化
RDB持久化功能,可以将redis在内存中的数据库状态保存到磁盘里,避免数据意外丢失
- save指令,阻塞redis服务器进程,知道rdb文件创建完毕为止
- bgsave指令,派生出一个子进程,然后由子进程负责创建rdb文件
由于AOF文件的更新频率通常比RDB文件的更新频率高,所以:
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态
- 只有在AOF持久化功能处于关闭状态的时候,服务器才会使用RDB文件来还原数据库状态
BGSAVE命令执行时的服务器状态
在BGSAVE命令执行期间,客户端发送的SAVE、BGSAVE、BGWRITER指令会被拒绝
自动间隔保存
用户可以通过save选项设置多个保存条件,只要满足其中一个,服务器就会执行BGSAVE命令
save intervals times
AOF持久化
如果开启了AOF,则每条命令执行完毕后都会同步写入aof_buf中,aof_buf是个全局的SDS类型的缓冲区。
AOF持久化最终需要将缓冲区中的内容写入一个文件,写文件通过操作系统提供的write函数执行。但是write之后数据只是保存在kernel的缓冲区中,真正写入磁盘还需要调用fsync函数。fsync是一个阻塞并且缓慢的操作,所以Redis通过appendfsync配置控制执行fsync的频次。具体有如下3种模式。
- no:不执行fsync,由操作系统负责数据的刷盘。数据安全性最低但Redis性能最高。
- always:每执行一次写入就会执行一次fsync。数据安全性最高但会导致Redis性能降低。
- everysec:每1秒执行一次fsync操作。属于折中方案,在数据安全性和性能之间达到一个平衡。
AOF文件的载入与数据还原
- 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写指令,伪客户端执行命令的效果和带网络连接的客户端执行命令效果完全一样
- 从AOF文件中分析并读取出一条写命令
- 使用伪客户端执行被读出的写命令
- 重复2和3的流程
AOF重写
为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。
首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录该键值对的多个命令;
def AOF_REWRITE(tmp_tile_name):
f = create(tmp_tile_name)
# 遍历所有数据库
for db in redisServer.db:
# 如果数据库为空,那么跳过这个数据库
if db.is_empty(): continue
# 写入 SELECT 命令,用于切换数据库
f.write_command("SELECT " + db.number)
# 遍历所有键
for key in db:
# 如果键带有过期时间,并且已经过期,那么跳过这个键
if key.have_expire_time() and key.is_expired(): continue
if key.type == String:
# 用 SET key value 命令来保存字符串键
value = get_value_from_string(key)
f.write_command("SET " + key + value)
elif key.type == List:
# 用 RPUSH key item1 item2 ... itemN 命令来保存列表键
item1, item2, ..., itemN = get_item_from_list(key)
f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)
elif key.type == Set:
# 用 SADD key member1 member2 ... memberN 命令来保存集合键
member1, member2, ..., memberN = get_member_from_set(key)
f.write_command("SADD " + key + member1 + member2 + ... + memberN)
elif key.type == Hash:
# 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令来保存哈希键
field1, value1, field2, value2, ..., fieldN, valueN =\
get_field_and_value_from_hash(key)
f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\
... + fieldN + valueN)
elif key.type == SortedSet:
# 用 ZADD key score1 member1 score2 member2 ... scoreN memberN
# 命令来保存有序集键
score1, member1, score2, member2, ..., scoreN, memberN = \
get_score_and_member_from_sorted_set(key)
f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\
... + scoreN + memberN)
else:
raise_type_error()
# 如果键带有过期时间,那么用 EXPIREAT key time 命令来保存键的过期时间
if key.have_expire_time():
f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())
# 关闭文件
f.close()
后台重写
aof_rewrite
函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间的阻塞,因为Redis服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE
函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求;
- Redis不希望AOF重写会造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程(后台)里执行。这样处理的最大好处是:
- 子进程进行AOF重写期间,主进程可以继续处理命令请求;
- 子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。
子进程在进行AOF重写期间,服务器进程还要继续处理命令请求,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致。
为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区