Redis
2021-06-17 16:38:55 0 举报
AI智能生成
Redis原理脑图框架梳理
作者其他创作
大纲/内容
Redis数据结构
String
存储数据:存储字符串、整数或者浮点数
操作数据范围:可以对整个字符串或者字符串的其中一部分执行操作,对整数和浮点数执行自增或者自减
需要注意的是redis会自动转字符串为浮点数,前提是这个字符串能转为浮点数,不然就会报错,命令操作的都是键,变化的都是值
使用setrange或者setbit命令对字符串写入时,字符串当前长度不能满足写入需求,redis会自动使用空字节(null)先扩容再执行写入或者更新操作,使用getrange读取字符串时,超出字符串末尾的数据会被视为空串,而使用getbit的情况下就会被视为0
List
存储数据:链表,链表上每个节点都包含了一个字符串
操作数据范围:可以操作链表两端,也可以根据偏移量修建链表,读取单个或多个元素,根据值查找或者移除元素
Set
存储数据:无序收集器,每个字符串不能相同
操作数据范围:添加、获取、移除单个元素,随机获取元素,验证元素是否存在集合中,计算交集、并集、差集
Zset
存储数据:有序集合,字符串成员与浮点数分值之间映射,按照浮点数分值拍序
操作数据范围:添加、获取、删除单个元素,根据分值范围或者成员来获取元素
Hash
存储数据:包含键值对的无序散列表
操作数据范围:添加、获取、移除单个键值对,获取所有键值对
全功能排序命令
子主题
可以根据降序而不是默认的升序来排序元素
将元素看出数字或者是二进制字符串来排序
使用被排序元素之外的其他值作为权重来进行排序,甚至可以从输入的列表、集合、有序集合以外的其他地方进行取值
Redis事务
基本概念
Redis中被multi和exec命令包围的所有命令会一个接一个地执行,直到所有命令执行完毕,当一个事务执行完毕之后,Redis才会处理其他命令
需要注意Redis在收到exec命令之后才会执行那些位于multi和exec之间的入队命令
数据持久化AOF与RDB
基本概念
redis使用内存来存储数据,所以会很快,但是服务器一旦重启,数据将全部丢失,这时候就需要持久化数据,以便于重启后能将数据重新从硬盘中加载数据到内存中
快照(RDB):将某一时刻的所有数据写入硬盘里面。在进行RDB持久化时,Redis会fork出一个子进程,子进程将内存中数据写入到一个紧凑的文件中,因此保存的是某个时间点的完整数据
优点
rdb文件体积小,适合备份传输
性能比aof好,aof需要写入日志到文件中
rdb恢复比aof要快
缺点
服务器故障时会丢失最后一次备份之后的数据
Redis保存rdb时,fork子进程的这个操作期间,Redis服务会停止响应(一般是毫秒级),但如果数据量大且cpu时间紧张,则停止响应的时间可能长达1s
只追加文件:在执行写命令是将执行的写命令复制到硬盘里面,每一个修改数据的命令都会添加到aof文件中,不会丢失数据,当redis重启时,将会读取AOF文件进行恢复到redis关闭前的最后时刻
优点
正确的配置最多丢失1s数据或者不丢失数据
aof文件体积很大时会自动重写aof文件,期间不影响正常服务,中途磁盘写满或停机导致失败也不会丢失数据
缺点
redis性能会受到影响
文件会较rdb大
重启时还是会有极低概率导致数据集无法恢复为保存时的模样,但是概率极低
上面两种持久化方法既可以同时使用又可以单独使用
配置参数
快照配置RDB
################################ SNAPSHOTTING ################################
# 快照配置
# 注释掉“save”这一行配置项就可以让保存数据库功能失效
# 设置sedis进行数据库镜像的频率。
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化)
# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化)
# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
save 900 1
save 300 10
save 60 10000
#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作,可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
stop-writes-on-bgsave-error yes
#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes
#是否校验rdb文件。从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗,所以如果你追求高性能,可以关闭该配置。
rdbchecksum yes
#rdb文件的名称
dbfilename dump.rdb
#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
dir /var/lib/redis
只追加文件AOF
############################## APPEND ONLY MODE ###############################
#默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。
appendonly yes
#aof文件名, 保存目录由 dir 参数决定
appendfilename "appendonly.aof"
#aof持久化策略的配置
#no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#always表示每次写入都执行fsync,以保证数据同步到磁盘。
#everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec
# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。
no-appendfsync-on-rewrite no
#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb
#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes
#默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。
appendonly yes
#aof文件名, 保存目录由 dir 参数决定
appendfilename "appendonly.aof"
#aof持久化策略的配置
#no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#always表示每次写入都执行fsync,以保证数据同步到磁盘。
#everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec
# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。
no-appendfsync-on-rewrite no
#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb
#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes
注意事项
Redis存储的数据量只有几个GB的时候使用自动保存快照没问题,当Redis占用数十个GB的时候,并且剩余的内存空间不多时,可能会导致系统长时间停顿或者引发系统大量使用虚拟内存,导致Redis性能降低到无法使用。
这时候可能需要考虑关闭自动保存,从而用手动输入bgsave或者save来持久化,bgsave需要创建子进程来备份redis,内存不够用时候可能会停顿,save会阻塞redis,但是不会创建子进程所以时间可能会快不少,通过手动方式控制Redis的停顿时间
这时候可能需要考虑关闭自动保存,从而用手动输入bgsave或者save来持久化,bgsave需要创建子进程来备份redis,内存不够用时候可能会停顿,save会阻塞redis,但是不会创建子进程所以时间可能会快不少,通过手动方式控制Redis的停顿时间
使用固态硬盘存储的时候谨慎使用appendfsync always选项,这个选项让Redis每次只写入一个命令,而不是像其他appendfsync选项那样一次写入多个命令,这种不断写入少量数据可能会引发严重的写入放大问题,会极大降低固态硬盘寿命,最好的是使用everysec选项让Redis每秒一次同步
对过期键的处理
RDB
生成RDB文件
过期键不会被保存到新创建的RDB文件中
载入RDB文件
服务器以主服务器模式运行,载入RDB文件是会对键进行检查,过期的键会被忽略,违背过期键会被载入到数据库
服务器以从服务器模式运行,那么RDB文件中保存的所有键都会被载入到数据库中。不过因为主从服务器在进行数据同步的时候,从服务器数据库会被清空,所以过期键对载入RDB文件的从服务器不会造成很大影响
AOF
生成AOF文件
键已经过期但并未被删除,aof文件不会变化,如果这时候客户端试图访问该键,服务器会执行:
1)从数据库中删除message键。
2)追加一条DEL message命令到AOF文件。
3)向执行GET命令的客户端返回空回复
1)从数据库中删除message键。
2)追加一条DEL message命令到AOF文件。
3)向执行GET命令的客户端返回空回复
当过期键被删除之后,程序会向AOF文件追加一条DEL命令,显示地记录该键已被删除
AOF重写:类似生成RDB文件的处理
RDB原理详解
生成RDB文件
SAVE命令执行过程
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕,在服务器阻塞期间,服务器不能处理任何请求
BGSAVE命令执行过程
BGSAVE命令会派生一个子进程,由子进程负责创建RDB文件,服务器进程继续处理命令请求
在BGSAVE执行期间,客户端若再发送的SAVE和BGSAVE命令会被服务器拒绝,防止产生竞争条件
如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF(生成AOF文件的命令)命令会被延迟到BGSAVE命令执行完毕之后再执行。
反过来如果BGREWRITEAOF在执行,那个BGSAVE会被直接拒接
反过来如果BGREWRITEAOF在执行,那个BGSAVE会被直接拒接
配置文件的save自动保存参数使用的是BGSAVE命令
save源码(BGSAVE实现过程)
首先设置redisServer结构的saveparams属性
struct redisServer{
//记录了保存条件的数组
struct saveparam *saveparams;
}
数组内部构造
struct saveparam{
//秒数
time_t seconds;
//修改次数
int changes;
};
所以
save 900 1
save 300 10
struct redisServer{
//记录了保存条件的数组
struct saveparam *saveparams;
}
数组内部构造
struct saveparam{
//秒数
time_t seconds;
//修改次数
int changes;
};
所以
save 900 1
save 300 10
除了saveparams数组之外,服务器状态还维持着一个dirty计数器以及一个lastsave属性
dirty记录在上次保存之后,服务器修改数据次数修改,lastsave上一次执行保存的时间
struct redisServer{
//修改计数器
long long dirty
//上一次执行保存的时间
time_t lastsave
.......
}
dirty记录在上次保存之后,服务器修改数据次数修改,lastsave上一次执行保存的时间
struct redisServer{
//修改计数器
long long dirty
//上一次执行保存的时间
time_t lastsave
.......
}
severCron函数检查保存条件的过程
1、遍历所有保存条件,即遍历saveparams参数
2、计算距离上次保存操作过去几秒
3、数据库状态的修改次数超过条件所设置的次数并且距离上次保存条件超过条件所设置的时间
3、执行保存操作
4、dirty置为0,lastsave记录当前时间
1、遍历所有保存条件,即遍历saveparams参数
2、计算距离上次保存操作过去几秒
3、数据库状态的修改次数超过条件所设置的次数并且距离上次保存条件超过条件所设置的时间
3、执行保存操作
4、dirty置为0,lastsave记录当前时间
RDB文件结构
REDIS:长度为5个字节,保存着“REDIS”五个字符【文件里面是二进制数据这里是c字符串】,通过五个字符,程序可以载入文件时快速检查所载入的文件是否RDB文件
db_version:长度为4字节,它的值是一个字符串表示的整数,这个整数记录RDB文件的版本号,比如“0006”代表版本为第六版
databases:包含着0个或任意多个数据库,以及各个数据库中的键值对数据。
如果服务器的数据库状态为空(所有数据库都是空的)那这个部分也为空,长度为0字节
如果服务器的数据库状态为非空,那么这个部分也为非空,这个部分长度不定具体看数据库所保存键值对的数量、类型和内容
如果服务器的数据库状态为空(所有数据库都是空的)那这个部分也为空,长度为0字节
如果服务器的数据库状态为非空,那么这个部分也为非空,这个部分长度不定具体看数据库所保存键值对的数量、类型和内容
每个数据库数据包含的三个部分
SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它之后接下来要读入的将是一个数据库号码
db_number:保存一个数据库号码,可以是1字节,2字节或者5字节。
当程序读入db_number部分之后,服务器会调用SELECT命令根据读入的数据库号码进行数据库切换
当程序读入db_number部分之后,服务器会调用SELECT命令根据读入的数据库号码进行数据库切换
key_value_pairs:部分保存了数据库中的所有键值对数据。
如果键值对带有过期时间,那么也会和键值对保存在一起。长度取决于键值对数量、类型、内容等等
如果键值对带有过期时间,那么也会和键值对保存在一起。长度取决于键值对数量、类型、内容等等
TYPE可以是以下几种类型:
❑REDIS_RDB_TYPE_STRING:字符串对象
❑REDIS_RDB_TYPE_LIST:列表对象
❑REDIS_RDB_TYPE_SET:集合对象
❑REDIS_RDB_TYPE_ZSET:有序集合对象
❑REDIS_RDB_TYPE_HASH:哈希表对象
❑REDIS_RDB_TYPE_LIST_ZIPLIST:压缩列表对象,同样是转为字符串对象存储,解析的时候在转换为原来的压缩列表对象
❑REDIS_RDB_TYPE_SET_INTSET:整数集合对象,RDB文件保存这种对象的方法是先将整数集合转换为字符串对象,然后将这个字符串对象保存到RDB文件里面,解析RDB文件则同样需要转换,反回来
❑REDIS_RDB_TYPE_ZSET_ZIPLIST:压缩列表对象,同样是转为字符串对象存储,解析的时候在转换为原来的压缩列表对象
❑REDIS_RDB_TYPE_HASH_ZIPLIST:压缩列表对象,同样是转为字符串对象存储,解析的时候在转换为原来的压缩列表对象
❑REDIS_RDB_TYPE_STRING:字符串对象
❑REDIS_RDB_TYPE_LIST:列表对象
❑REDIS_RDB_TYPE_SET:集合对象
❑REDIS_RDB_TYPE_ZSET:有序集合对象
❑REDIS_RDB_TYPE_HASH:哈希表对象
❑REDIS_RDB_TYPE_LIST_ZIPLIST:压缩列表对象,同样是转为字符串对象存储,解析的时候在转换为原来的压缩列表对象
❑REDIS_RDB_TYPE_SET_INTSET:整数集合对象,RDB文件保存这种对象的方法是先将整数集合转换为字符串对象,然后将这个字符串对象保存到RDB文件里面,解析RDB文件则同样需要转换,反回来
❑REDIS_RDB_TYPE_ZSET_ZIPLIST:压缩列表对象,同样是转为字符串对象存储,解析的时候在转换为原来的压缩列表对象
❑REDIS_RDB_TYPE_HASH_ZIPLIST:压缩列表对象,同样是转为字符串对象存储,解析的时候在转换为原来的压缩列表对象
key
value
EXPIRETIME_MS:只在有过期时间时候出现,是一个常量字节串。
告诉读入程序,接下来要读入的将是一个以毫秒为单位的过期时间,后面跟着的就是ms
告诉读入程序,接下来要读入的将是一个以毫秒为单位的过期时间,后面跟着的就是ms
ms:只有键有过期时间才出现,8字节长的带符号整数,记录着键的过期时间
EOF:这个常量的长度为1字节,标志着RDB文件正文内容的结束,当读入程序遇到这个值的时候,表明所有键值对都载入完毕
check_sum:是一个8字节长的无符号整数,保存着一个检验和,这个检验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的,服务器在载入RDB文件时,将会载入数据计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否出错
AOF原理详解
第一步:命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
struct redisServer {
//AOF缓冲区
sds aof_buf;
....
};
struct redisServer {
//AOF缓冲区
sds aof_buf;
....
};
第二步:文件的写入与同步
Redis的服务器进程是一个事件循环。
这个循环中的文件事件负责接收客户端的命令请求以及发送命令回复
然后对于写命令就会追加到aof_buf缓冲区中
然后处理时间事件
最后考虑是否要将缓冲区的内容写入和保存到aof文件里面,用的是flushAppendOnlyFile函数
而flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值决定
这个循环中的文件事件负责接收客户端的命令请求以及发送命令回复
然后对于写命令就会追加到aof_buf缓冲区中
然后处理时间事件
最后考虑是否要将缓冲区的内容写入和保存到aof文件里面,用的是flushAppendOnlyFile函数
而flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值决定
Redis为系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立刻将缓冲区中的数据写到硬盘里,从而确保写入数据的安全。
如果在进行写入操作时flushAppendOnlyFile函数被调用,服务器当前appendfsync选项的值为everysec且距离上次同步AOF文件已经超过一秒钟,服务器会将aof_buf中的内容写入到AOF文件中
如果在进行写入操作时flushAppendOnlyFile函数被调用,服务器当前appendfsync选项的值为everysec且距离上次同步AOF文件已经超过一秒钟,服务器会将aof_buf中的内容写入到AOF文件中
appendfsync选项的值直接决定AOF持久化功能的效率和安全性。
always是最安全的但是也是最慢的
everysec是每隔一秒就会同步,这个模式足够快,就算出现故障停机,数据库也只丢失一秒钟的命令数据
no:每个事件循环都会将缓冲区的写入到AOF文件,但是何时对AOF文件同步由操作系统控制
always是最安全的但是也是最慢的
everysec是每隔一秒就会同步,这个模式足够快,就算出现故障停机,数据库也只丢失一秒钟的命令数据
no:每个事件循环都会将缓冲区的写入到AOF文件,但是何时对AOF文件同步由操作系统控制
第三步:文件的载入与数据还原
Redis读取AOF文件并还原数据库状态的详细步骤如下:
1)创建一个不带网络连接的伪客户端:这是因为Redis命令只能在客户端上下文中执行,而载入AOF文件所使用的命令直接来源于AOF文件而不是网络连接。正常服务器是获取网络连接的上下文执行命令,所以服务器使用一个没有网络连接的伪客户端来执行AOF文件保存的写命令
2)从AOF文件中分析并读取出一条写命令
3)用伪客户端执行被读出的写命令
4)一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止
1)创建一个不带网络连接的伪客户端:这是因为Redis命令只能在客户端上下文中执行,而载入AOF文件所使用的命令直接来源于AOF文件而不是网络连接。正常服务器是获取网络连接的上下文执行命令,所以服务器使用一个没有网络连接的伪客户端来执行AOF文件保存的写命令
2)从AOF文件中分析并读取出一条写命令
3)用伪客户端执行被读出的写命令
4)一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止
AOF文件的重写
这个文件重写不是在分析原来的aof文件然后进行重写,而是直接遍历Redis中的非空数据库的键值对,生成现有数据类型的批量读取命令,再由获取到的返回值生成批量写入命令,最后获取键的过期时间信息,使用pexpireat命令重写键的过期时间
为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表,哈希表,集合、有序集合这四种数据结构时会检查键所对应的值中,元素数量是否超过REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值(目前版本中是64),如果超过这个值就会用多条命令来记录这个集合,相当于分开记录
AOF文件重写时调用的是aof_rewrite函数,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器是单线程处理请求的。所以这时会创建一个子进程(不是线程)进行处理。这样就会出现问题,子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令会对现有数据库进行修改。
为了解决子进程与父进程数据不一致的问题,Redis服务器设置一个AOF重写缓冲区。
1)执行客户端发来的命令
2)执行后的写命令追加到AOF缓冲区
3)执行后的写命令追加到AOF重写缓冲区
这样保证AOF缓冲区的内容会定期写入和同步到AOF文件中,现有的AOF文件处理工作会如常进行
从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面
1)执行客户端发来的命令
2)执行后的写命令追加到AOF缓冲区
3)执行后的写命令追加到AOF重写缓冲区
这样保证AOF缓冲区的内容会定期写入和同步到AOF文件中,现有的AOF文件处理工作会如常进行
从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面
在子进程完成AOF重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后调用一个信号处理函数
1)将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前数据库状态一致
2)对新AOF文件改名,覆盖旧AOF文件,完成新旧两个AOF文件的替换
1)将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前数据库状态一致
2)对新AOF文件改名,覆盖旧AOF文件,完成新旧两个AOF文件的替换
主从同步(复制)
准备条件
主库
在正确设置dir选项和dbfilename选项,并且两个选项所指示的路径和文件对于Redis进程来说都是可写的
从库
启动redis时,指定一个包含slaveof host port选项的配置文件,那么redis就可以根据该选项给定的ip和端口来链接主服务器
对于正在运行的服务器与,可以通过发送slaveof no one终止复制操作,或者通过发送slaveof host port命令来让服务器开始复制一个新的主服务器
复制流程
从库
从服务器在进行同步时会清空自己所有数据
主从链
不一定要上上面那样组成树形结构,但是这种方式是可行的,可以使用中间的从库来帮助主库复制
分布式锁
基本概念
主库
不支持主主复制,两个主库相互发送ofslave只会持续占用大量处理器资源并且联系不断尝试与对方通信
一般来说,对于数据加锁,程序要获取排它锁,对数据操作完后再释放锁
Redis通常使用watch命令代替锁,watch命令是乐观锁,它只会在数据被其他客户端抢先修改了的情况下通知执行了这个命令的客户端,而不会阻止其他客户端对数据进行修改,这里需要人为控制
Redis虽然提供了setex来加上基本锁,但是功能并不完整,需要自行构建锁
前提条件
互斥性:在任意时刻,只有一个客户端能持有锁
不会发生死锁:即时一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁
加锁与解锁必须是同一个客户端
加锁代码(jedis)
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
最主要的代码jedis.set(String key, String value, String nxxx, String expx, int time)
这个set()方法一共有五个形参:
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
解锁代码(jedis)
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的
Redis管道模式
基本概念
正常的命令是发送一条命令处理一次数据返回一次结果
管道模式则是批量发送命令,处理完后批量返回结果
管道模式则是批量发送命令,处理完后批量返回结果
在pipeline(管道)链接期间,会独占链接,不能进行非管道类型的其他操作,直到pipeline关闭
为了不干扰链接中的其他操作,可以为pipeline操作新建client链接,让管道链接和其他正常操作分离在不同client中
为了不干扰链接中的其他操作,可以为pipeline操作新建client链接,让管道链接和其他正常操作分离在不同client中
但是每个redis-server同时所能支撑的pipeline链接的个数是有限的,受限于server的物理内存或网络接口的缓冲能力
pipeline打包命令越多,缓存消耗内存越多
jedis代码(基本用法示例)
Pipeline pipe = jedis.pipelined(); // 先创建一个 pipeline 的链接对象
for (int i = 0; i < 10000; i++) {
pipe.set(String.valueOf(i), String.valueOf(i));批量写入10000条数据
}
pipe.sync(); // 获取所有的 response
for (int i = 0; i < 10000; i++) {
pipe.set(String.valueOf(i), String.valueOf(i));批量写入10000条数据
}
pipe.sync(); // 获取所有的 response
Jedis源码简单分析
public Pipeline pipelined() {
Pipeline pipeline = new Pipeline();
pipeline.setClient(client);
return pipeline;
}
//使用管道的sadd
public Response<Long> sadd(String key, String... member) {
getClient(key).sadd(key, member);
return getResponse(BuilderFactory.LONG);//没有手动调用flush(),而是返回一个固定值。
}
//让我们来看看pipeline的sync()方法:
public void sync() {
if (getPipelinedResponseLength() > 0) {
List<Object> unformatted = client.getAll();
for (Object o : unformatted) {
generateResponse(o);
}
}
}
//client.getAll()调用了getAll(0)
public List<Object> getAll(int except) {
List<Object> all = new ArrayList<Object>();
flush();//也就是说在调用pipeline.sync()时手动触发的flush()方法,一次pipeline操作真正意思上只有一次tcp
while (pipelinedCommands > except) {
try {
all.add(readProtocolWithCheckingBroken());
} catch (JedisDataException e) {
all.add(e);
}
pipelinedCommands--;
}
return all;
}
//未使用管道的sadd方法
public Long sadd(final String key, final String... members) {
checkIsInMulti();
client.sadd(key, members);//这里的sadd将会执行下面的sendCommand发送指令
return client.getIntegerReply();//客户端立即发送数据到服务端。客户端等待服务端返回
}
//所有的发送指令都要调用该方法,但是该方法并没有真正发送数据。
protected Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
connect();
Protocol.sendCommand(outputStream, cmd, args);
pipelinedCommands++;
return this;
} catch (JedisConnectionException ex) {
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
public Long getIntegerReply() {
flush();//sendCommand方法调用后,还没有真正将数据写到服务端,当调用flush()后才真正触发发送数据
pipelinedCommands--;
return (Long) readProtocolWithCheckingBroken();
}
Pipeline pipeline = new Pipeline();
pipeline.setClient(client);
return pipeline;
}
//使用管道的sadd
public Response<Long> sadd(String key, String... member) {
getClient(key).sadd(key, member);
return getResponse(BuilderFactory.LONG);//没有手动调用flush(),而是返回一个固定值。
}
//让我们来看看pipeline的sync()方法:
public void sync() {
if (getPipelinedResponseLength() > 0) {
List<Object> unformatted = client.getAll();
for (Object o : unformatted) {
generateResponse(o);
}
}
}
//client.getAll()调用了getAll(0)
public List<Object> getAll(int except) {
List<Object> all = new ArrayList<Object>();
flush();//也就是说在调用pipeline.sync()时手动触发的flush()方法,一次pipeline操作真正意思上只有一次tcp
while (pipelinedCommands > except) {
try {
all.add(readProtocolWithCheckingBroken());
} catch (JedisDataException e) {
all.add(e);
}
pipelinedCommands--;
}
return all;
}
//未使用管道的sadd方法
public Long sadd(final String key, final String... members) {
checkIsInMulti();
client.sadd(key, members);//这里的sadd将会执行下面的sendCommand发送指令
return client.getIntegerReply();//客户端立即发送数据到服务端。客户端等待服务端返回
}
//所有的发送指令都要调用该方法,但是该方法并没有真正发送数据。
protected Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
connect();
Protocol.sendCommand(outputStream, cmd, args);
pipelinedCommands++;
return this;
} catch (JedisConnectionException ex) {
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
public Long getIntegerReply() {
flush();//sendCommand方法调用后,还没有真正将数据写到服务端,当调用flush()后才真正触发发送数据
pipelinedCommands--;
return (Long) readProtocolWithCheckingBroken();
}
Redis哨兵Sentinel
在Redis里面主要负责监控主从节点,主节点挂了就把从拉起来
哨兵之间会互相监测运行状态,并且会交换一下节点监测的状态,同时哨兵也会监测主从节点的状态
如果检测到某一个节点没有正常回复,并且距离上次正常回复的时间超过了某个阈值,那么就认为该节点为主观下线。
这个时候其他哨兵也会来监测该节点是不是真的主观下线,如果有足够多数量的哨兵都认为它确实主观下线了,那么它就会被标记为客观下线,这个时候哨兵会找下线节点的从节点,然后与其他哨兵协商出一个从节点做主节点,并将剩余的从节点指向新的主节点。
这个时候其他哨兵也会来监测该节点是不是真的主观下线,如果有足够多数量的哨兵都认为它确实主观下线了,那么它就会被标记为客观下线,这个时候哨兵会找下线节点的从节点,然后与其他哨兵协商出一个从节点做主节点,并将剩余的从节点指向新的主节点。
主从切换
选取领头哨兵
领头人的选举规则是谁发现客观下线谁就可以马上要求其他哨兵认自己做老大,其他哨兵会无条件接受第一个发过来的人,并告知老大,如果超过一半人都同意了,那他老大的位置就坐实了。
从节点选举
一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程id
如果连接断开的比较久,超过了某个阈值,就直接失去了选举权
如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置,数值越小优先级越高
如果优先级相同,就看谁从master中复制的数据最多,选最多的那个
如果复制数量也相同,就选择进程id最小的那个。
codis原理
https://www.cnblogs.com/pingyeaa/p/11294773.html
使用背景
遇到海量存储需求的时候,单一的主从结构就会出问题,因为BGSAVE指令生成的RDB文件非常巨大,这样主从复制就会很慢
主流方案有三种,分别是Twemproxy、Codis和Redis Cluster。
Twemproxy,是推特开源的,它最大的缺点就是无法平滑的扩缩容,而Codis解决了Twemproxy扩缩容的问题,而且兼容了Twemproxy,它是由豌豆荚开源的,和Twemproxy都是代理模式。其实Codis能发展起来的一个主要原因是它是在Redis官方集群方案漏洞百出的时候率先成熟稳定的。以现在的Redis官方集群方案,这两个好像没有太大差别了,哪个性能好不确定
Redis Cluster是由官方出品的,用去中心化的方式实现,不属于代理模式
Twemproxy,是推特开源的,它最大的缺点就是无法平滑的扩缩容,而Codis解决了Twemproxy扩缩容的问题,而且兼容了Twemproxy,它是由豌豆荚开源的,和Twemproxy都是代理模式。其实Codis能发展起来的一个主要原因是它是在Redis官方集群方案漏洞百出的时候率先成熟稳定的。以现在的Redis官方集群方案,这两个好像没有太大差别了,哪个性能好不确定
Redis Cluster是由官方出品的,用去中心化的方式实现,不属于代理模式
分片算法
如果有存储海量数据的需求,同步就会非常缓慢,所以应该把一个主从结构变成多个,把存储的key分摊到各个主从结构中来分担压力
在Codis里面,它把所有的key分为1024个槽,每一个槽位都对应了一个分组,具体槽位的分配,可以进行自定义。
现在如果有一个key进来,首先要根据CRC32算法,针对key算出32位的哈希值
然后除以1024取余,然后就能算出这个KEY属于哪个槽
然后根据槽与分组的映射关系,就能去对应的分组当中处理数据了。
现在如果有一个key进来,首先要根据CRC32算法,针对key算出32位的哈希值
然后除以1024取余,然后就能算出这个KEY属于哪个槽
然后根据槽与分组的映射关系,就能去对应的分组当中处理数据了。
codis proxy
负责转发key到各个主从结构中,也是redis-client端的直接接触
本身也存在单点问题,需要做集群
集群的proxy使用Zookeeper来保存和同步映射关系。(还支持etcd和本地文件的方式)
Codis-HA是负责监视proxy运行状态,当某个proxy出现异常,则会直接干掉并重新拉起一个proxy
由于Codis-HA自带哨兵功能,所以codis内部没有哨兵Sentinel
Codis Dashboard
codis-ha在Codis整个架构中是没有办法直接操作代理和服务,因为所有的代理和服务的操作都要经过dashboard处理。所以部署的时候会利用k8s的亲和性将codis-ha与dashboard部署在同一个节点上
codis-fe
codis自己开发了集群管理界面,集群管理可以通过界面化的方式更方便的管理集群,这个模块叫codis-fe
Redis所有命令链接
收藏
0 条评论
下一页