redis.py
StrictRedis实现了大部分的官方命令,Redis是StrictRedis的子类,用于向后兼容旧版本的redis.py
1 | |
部署方式
使用容器部署
1 | |
常用命令
1 | |
配置文件
可以在部署redis的时候,手动指定redis的配置文件路径,在启动redis后,通过redis-cli,可以使用命令config get *获取所有配置信息
1 | |
原理
数据结构
简单动态字符串SDS
struct sdshdr{
int len;//buf中字符数组的长度
int free;//buf中未使用的字节数量
char buf[];
}
空间预分配,修改sds时,如果修改后的sds的长度小于1MB,那么程序分配和len相同大小的未使用空间,否则,分配1MB的未使用空间; 惰性空间释放,当缩短字符串时,将释放的空间当做未使用字节。
链表
1 | |
字典
typedef struct dictht{
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,总是size-1,和size-1与,等于和size取余,位运算效率高
unsigned long sizemask;
//哈希表已有节点数量
unsigned long used;
}dictht;
typedef struct dictEntry{
void *key;
union {
void *val;
unit64_tu64;
int64_ts64;
} v;
struct dictEntry *next;
}
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx;
}dict;
hash冲突时,使用链地址方法解决冲突,并且将新的键值对插入链表头,时间为O(1)
为了维护负载因子,需要对哈希表进行rehash,如果是扩展操作,ht[1]为第一个>=ht[0].used*2的2^n (2的n次方幂),如果是收缩,ht[1]大小为第一个大于等于ht[0].used的2^n ;将ht[0]中的值重新计算hash放到ht[1];释放ht[0],将ht[1]和ht[0]互换。 扩容条件:没有执行BGSAVE或者BGREWRITEAOF,并且哈希表的负载因子大于等于1;执行BGSAVE或者BGREWRITEAOF,且负载因子大于等于5 缩容条件:负载因子小于0.1
跳跃表
平均O(logN)、最坏O(N)的查找
使用场景
- 有序集合键
- 集群节点中内部数据结构
整数集合intset
保存整数,int16_t、int32_t或者int64_t,保证集合中不会出现重复元素
typedef struct intset{
uint32_t encoding;
uint32_t length;
int8_t contents[];
}intset;
压缩列表
为了节约内存,格式如下
zlbytes,zltail,zllen,entry1,entry2,…,entryN,zlend
对象
typedef struct redisObject {
// 对象类型,可选值是REDIS_STRING、REDIS_LIST、REDIS_HASH、REDIS_SET、REDIS_ZSET
unsigned type:4;
// 编码,每种类型都提供了至少俩种编码
unsigned encoding:4;
void *ptr;
// ...
}robj;
redis> set msg "hello world" #键是字符串对象,值是字符串对象
redis> rpush numbers 1 3 5#键是字符串对象,值是列表对象
redis> hmset profile name Tom age 25 career Prohrammer #键是字符串对象,值是哈希对象
redis> sadd fruits apple banana #键是字符串对象,值是集合对象
redis> zadd price 8.5 apple 5.0 #键是字符串对象,值是有序集合对象
字符串对象
编码可以是int、raw、embstr,其中embstr是专门用于保存短字符串的优化编码
相关命令
set get append incrbyfloat incrby decrby strlen setrange gettange
列表对象
列表的编码可以是ziplist或者linkedlist
哈希对象
编码可以是ziplist或者hashtable
集合对象
编码可以是intset或者hashtable
有序集合对象
编码可以是ziplist或者skiplist
对象空转时长
typedef struct redisObject {
// ...
// 最后一次被访问的时间,object idletime可以查看
unsigned lru:22;
// ...
}robj;
服务器中的数据库
切换数据库,select 1
读写键空间时的维护操作
- 在读取一个键之后,如果键存在,会增加命中次数,keyspace_hits,否则,增加不命中次数,keyspace_misses
- 更新键的lru
- 如果这个键已经过期,那么会删除这个过期的键
- 如果有客户端watch了这个键,服务器会在对被监视的键进行修改之后,将这个键标记为脏,
- 每次修改一个键之后,会对脏键的计数器的值增加1,这个计数器会增加持久化以及复制操作
- 如果服务器开通了数据库通知功能,那么对键修改之后,服务器将按配置发送通知
设置键的过期时间
1 | |
redis数据库中,有一个过期字典,存放过期的键和过期时间,
删除策略
- 定时删除,设置键的过期时间时,创建定时器,键以过期,就立马删除,比较消耗cpu,定时器需要使用时间事件,查找一个事件的时间复杂度O(N)较高
- 惰性删除,放任不管,每次检查的键是否过期,如果过期,就删除,对cpu友好,对内存不友好
- 定期删除,每隔一段时间,删除过期键。可以通过限制删除操作的时长和执行频率,来平衡
AOF、RDB和复制对过期键的处理
生成RDB文件,不会保存过期的键 载入RDB文件,主服务器会忽略过期的键,从服务器全部载入
在AOF模式时,当过期的键被都行删除或定期删除,程序会向aof文件追加del命令,aof重写的时候,忽略过期的键
当服务器在复制模式下,主服务器删除一个过期键,向从服务器发送del命令;从服务器在执行客户端发送的读命令时,不会将过期的键删除;从服务器接到主服务器的del命令,才会删除过期的命令
数据库通知
键空间通知:某个键执行了什么命令 subscribe __keyspace@0__:message
键事件通知:某个命令被什么键执行了 subscribe __keyevevnt@0__:del
notify-keyspace-events选项决定了服务器发送通知的类型
- AKE
- AK
- AE
- K$
- EI
RDB持久化
持久化
save:阻塞redis服务器进程,直到rdb文件创建完成 bgsave:派生子进程,创建rdb文件
如果开启aof,优先使用aof文件还原数据库状态,当aof出于关闭的时候,才会使用rdb还原数据库状态
bgsave的时候,服务器会拒绝save命令和bgsave,同时bgrewriteaof和bgsave不会同时执行
服务器中,saveparam数据保存着描述和修改数的参数,用于定期保存数据库,更具一定时间内数据库有修改就进行保存 dirty计数器记录距离上一次成功执行save命令或者bgsave之后,服务器对数据库状态进行了多少次修改; lastsave记录着上一次成功执行save或者bgsave的时间 serverCron默认每个100毫秒执行一次,其中一项就是检查save设置是否满足,如果满足,就执行bgsave命令
文件结构
redis db_version databases eof check_sum
其中database部分保存着每个非空数据库,每个非空数据库在文件中的部分,都可以分为
SELECTDB db_number key_value_pairs
对于不带过期时间的key_value_pairs,其结构如下
type key value
带有过期时间的键值对存放结构如下
EXPIRETIME_MS ms TYPE key value
AOF持久化
aof是将执行的写命令写到aof文件中
redis服务器有aof_buf缓冲区,当默认的配置中appendfsync参数配置如下时:
- always 将aof_buf缓冲区的内容写入并同步到aof文件
- everysec 将aof_buf缓冲区的内容写入aof文件,如果上次同步超过1秒,就同步,这个是默认配置
- no 写入aof文件,有操作系统决定同步
读取aof过程
- 创建一个伪客户端,
- 从aof文件中读取命令
- 使用伪客户端执行读取的命令
- 循环以上
长时间redis运行下来,会导致aof文件体积很大,为了解决这个问题,使用aof重写功能,新的aof文件会去除冗余,比如多个写同样对象的命令合并成一个写命令
实际上,aof重写的时候,会读取当前数据库的状态,生成写命令,忽略已过期的键,使用子进程写,不会阻塞主进程,不需要使用锁,为了避免aof重写时数据库更新,redis设置了aof重写缓冲区,当开启了aof重写时,redis执行了写命令后,会将这个命令发送给aof缓冲区和aof重写缓冲区,当子进程完成aof重写后,父进程将aof重写缓冲区内容写入新的aof文件,并替换原有的aof文件
事件驱动
文件事件
- 使用io多路复用监听多个套接字,按照套接字执行的任务关联不同的事件处理器
- 当被监听的套接字准备好accept、read、wrte、close时,与操作相对应的文件事件就会产生,就会调用之前关联好的事件处理器
文件事件处理器的构成
套接字、io多路复用程序、文件事件分派器,事件处理器
io多路复用将产生事件的套接字放到一个队列,有序、同步、每次一个向文件事件分排气传送套接字,
io多路复用底层实现
- select
- epoll
- evport
- kqueue
文件事件处理器
- 连接应答处理器
- 命令请求处理器
- 命令回复处理器
- 复制处理器
时间事件
- 定时事件
- 周期性事件
一个时间事件主要包括
- id
- when
- timeProc
实现
服务器将所有的时间事件都存放一个无序列表,无序指的是不按照时间的时间排序,
serverCron事件
- 更新服务器各类统计信息,比如时间、内存、数据库占用
- 清理数据库中的过期键值对
- 关闭和清理失效的连接客户端
- 尝试aof或rdb持久化
- 主服务器对从服务器的同步
- 对集群定期同步和连接测试
redis.conf中的hz选项可以配置运行频率
客户端
通用属性
套接字描述符fd
- -1,伪客户端,读取aof和处理lua脚本
- 大于-1的整数,普通客户端
CLIENT list列出所有的客户端
名字,默认是空,可以使用client setname hello设置名字
标志,flags,
- REDIS_MASTER 客户端是一个主服务器
- REDIS_BLOCKED 客户端正在被列表命令阻塞
-
REDIS_MULTI REDIS_DIRTY_CAS 客户端正在执行事务,但事务的安全性已经被破坏 -
REDIS_SLAVE REDIS_PRE_PSYNC 客户端是从服务器
输入缓冲区 客户端发送的命令都会先缓存到这个区
1 | |
输出缓冲区
1 | |
authenticated,配置文件中的requirepass设置是否需要密码认证
- 0 客户端未认证
- 1 客户端已认证
obuf_soft_limit_reached_time,软性限制时间,输出缓冲区大小超过软性限制,记录此时间,当时长超过限制,关闭客户端
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
1 | |
服务器
命令执行器
redisCommand
| name | 命令名字,比如set |
| proc | 实现函数指针 |
| arity | 命令参数个数 |
| sflags | 标识,读命令、写命令、是否允许载入时使用 |
| flags | sflags的二进制标识 |
| calls | 服务器总共执行次数 |
| milliseconds | 执行命令总耗时 |
预备操作
- cmd指针是否指向NULL
- 检查参数是否正确
- 检查客户端是否经过认证
- 打开maxmemory设置,就会检查内存占用
- 如果 bgsave出错、服务器打开stop-writes-on-bgsave-error、服务器执行一个写命令,那么服务器将拒绝这个命令
- 如果服务器正在载入数据,那么标识为I的命令才能执行
- 。。。
调用命令的实现函数
后续操作
- 添加慢查询日志
- 写入命令耗时
- 写入aof
- 发送给从服务器
初始化服务器
initServerConfig
- 设置服务器运行id
- 默认运行频率
- 默认配置文件路径
- 运行架构
- 默认端口号
- rdb持久化和aof持久化条件
- lru时钟
- 创建命令表
载入配置项
redis.conf
initSever 初始化服务器数据结构
- server.clients
- server.db
- server.pubsub_channels
- server.lua
- server.slowlog
还原数据库状态
执行事件循环
复制
1 | |
旧的复制
同步sync
- 从服务器向主服务器发送sync
- 主服务器执行bgsave,并生成缓冲区记录从现在开始的命令
- 主服务器将rdb文件发送给从服务器,从服务器载入
- 主服务器将缓冲区的写命令发送给从服务器,从服务器执行这些命令
命令传播
问题:sync比较消耗资源,从服务器断链后,需要重新sync
新的复制
psync
- 完整重同步,用于初次同步,和sync一样
- 部分重同步,从服务器断连后,主服务器将断连期间的命令发送给从服务器
部分重同步
主服务器的复制偏移量 主服务器的复制积压缓冲区,默认大小为1MB的FIFO队列 服务器的运行id
哨兵
当主服务器下线的时候,会从从服务器中挑选一个升级为主服务器,哨兵会向从服务器发送新的复制指令,让这些从服务器重新设置主服务器,等原主服务重新上线后,将其设置为从服务器
1 | |
集群
配置文件中的cluster-enabled中的参数决定是否开启集群
将节点加入集群
1 | |
实现过程,类似TCP三次握手
- 节点A为节点B创建一个clusterNode结构,添加到自己的clusterState.nodes字典里面
- 向节点B发送一条MEET消息
- 节点B为节点A创建一个clusterNode结构,存放到自己的clusterState.nodes字典里面
- 节点B回复节点A一条PONG消息
- 节点A向节点B返回一条PING消息
槽
redis集群通过分片的方式保存数据库中的键值对,整个集群被分为16384个槽,当这些槽都有节点在处理的时候,集群出于上线状态,否则,集群是下线状态
手动执行命令分配槽,只有将所有的槽分配完,集群的状态才是ok,注意不要在redis终端执行以下命令,直接在shell里面执行以下语句
1 | |
计算键属于哪个槽,使用CRC-16校验和,与16383,可以通过以下命令获取
1 | |
当计算后发现当前的槽不在本节点,就会返回客户端,让客户端去正确的节点去执行 redis单点和集群客户端是不一样的,如果用单点客户端访问集群,客户端是无法再去寻找集群中的其他节点,只会在客户端显示错误和相关信息
在集群中,节点会用clusterState结构中slots_to_keys跳跃表来保存槽和键之间的关系
redis集群可以将任意数量的已经指派给某个节点的槽指派给另一个节点,通过redis-trib负责重分片,步骤如下
- redis-trib向目标节点发送
cluster setslot <slot> importing <source_id>,将目标节点准备从源节点导入属于slot的键值对 - redis-trib对源节点发送
cluster setslot <slot> migrateing <target_id>,让源节点准备好属于槽slot的键值对迁移至目标节点 - redis-trib向源节点发送
cluster getkeysinslot <slot> <count>,获取键名 - redis-trib向源节点发送
migrate <target ip> <target port> <key name> <timeout>迁移键值对 - 重复以上俩个步骤
- redis-trib向集群中的任意一节点发送
cluster setslot <slot> node <target_id>将槽指派给目标节点,发送个整个集群
当一个槽正在迁移的时候,需要处理这个槽中的某个键,而这个键已经被迁移到新的节点中,那么将返回一个ASK错误,指引客户端转向新的节点。当客户端向新的节点发送命令时,先发送ask命令,新的节点发现节点正在导入槽,那么节点将破例去处理。
副本
cluster replicate <node id>
将当前的节点变成node id的副本,在配置文件中slaveof ip port,即将此节点配置为ip:port中的副本,5.0之后的版本参数为replicaof ip port
故障转移
订阅与退订
redisServer中的字典pubsub_channels记录着所有订阅的频道和订阅的客户端
慢查询
slowlog-log-slower-than 慢查询的时间阈值,单位是微妙 slowlog-max-len 保存多少条慢查询日志
slowlog get查看慢查询日志
slowlog reset清除所有的慢查询日志