初级篇
字符串string
string类型是二进制安全的
string类型是最基本的数据类型,一个Redis字符串value最大为512M
{k : v}
列表List
Redis列表是字符串列表,可以将元素插入到列表的头部或尾部
底层是个双端链表,最多包含2^32 - 1个元素
哈希表Hash
k : {field : v}
每个Hash可存储2^32 - 1个键值对
集合Set
string类型的无序集合,集合中不存在重复的数据,通过哈希表实现,添加,查找,删除的复杂度都是O(1)
集合中最大成员数量为2^32 - 1
有序集合ZSet
string类型的无序集合,集合中不存在重复的数据,但每个元素都会关联一个double类型的分数:
k score v
score可以重复,可以根据score来给成员从小到大排序
通过哈希表实现,添加,查找,删除的复杂度都是O(1)
集合中最大成员数量为2^32 - 1
地理空间GEO
基数统计HyperLogLog
HyperLogLog 是 是用来基数统计的 的算法, HyperLogLog的优点是, 在输入元素的数量或者体积非常非常大时, 计算基数所需的空间总是固定且是很小的 Redis 里面, 每个 HyperLogLog 键只需要花费1 12 KB 内存, 就可以计算接近2^64 个不同元素的基数。 这和计算基数时, 元素越多耗费内存就越多的集合形成鲜明对比。 但是,因为HyperLogLog 只会根据输入元素来计算基数, 而不会储存输入元素本身, 所以 HyperLogLog 不能像集合那样, 返回输入的各个元素。
位图bitmap
由0,1状态表现的二进制位的bit数组
位域bitfield
比特位域:连续的多个比特位
通过bitfield命令可以一次性对多个比特位域进行操作
流Stream
主要用于消息队列
Redis自身的pub/sub发布订阅可以实现消息队列,但无法持久化;
而Stream提供了消息持久化和主备复制功能
key操作命令
keys * 列出所有的key
EXISTS k 判断k是否存在,返回1存在,0不存在;EXISTS k1, k2, k3... 存在几个就返回数字几
type k 查看k是什么数据类型
del k 删除指定的k数据,删除成功返回1,否则返回0。阻塞式删除,原子的
unlink k 非阻塞删除,真正的删除在后续异步中操作
ttl key 查看key还剩多少秒过期
expire key sec 设置过期时间
move key dbindex 把key移动到序号为 dbindex 的数据库
select dbindex 切换数据库
dbsize 查看当前数据库key的数量
flushdb 清空当前数据库所有的key
flushall 清空所有数据库所有的key
命令不区分大小写,key区分
字符串操作
set
模板:==SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]==
set key value [nx|xx]
nx: key不存在时才创建key,并返回OK,若key已存在,则返回nil
xx: key存在时才创建key,并且覆盖原始value
set key value [get]
先返回key的原始value,再用新的value覆盖原始value
set key value [ex|px] time
ex: 设置秒级的过期时间
px: 设置毫秒级的过期时间
set key value [exat|pxat]
exat: 设置秒级的UNIX时间戳过期时间
pxat: 设置毫秒级的UNIX时间戳过期时间
set key value [keepttl]
如果之前设置过key的过期时间:set key value 30
现在更新key value: set key newvalue,那默认会把过期时间覆盖为-1(永不过期),为了避免覆盖过期时间,需要用keepttl参数来延续之前的过期时间
mset
批量set命令:mset k1 v1 k2 v2.....
mget
批量get命令:mget k1, k2....
msetnx
key不存在时才设置key value
msetnx k1 v1 k2 v2
只有所有的key都不存在时才返回1,只要有一个key已经存在,就返回0
getrange
相当于substr:
set key abcd1234
getrange key 0 -1:返回整个字符串
getrange key 0 3:返回闭区间[0, 3]的字符串
setrange
set key abcd1234
setrange key 2 xyz
将abcd1234从下标2开始的三个字符替换为xyz,即:abxyz234
返回修改后字符串的长度
INCR
递增
set key 100
INCR key 变成101
INCRBY key increment 步长为increment的递增
DECR
同上
STRLEN
获取字符串长度
strlen key
APPEND
追加字符串
APPEND key str
在字符串key后追加str
分布式锁
set key value, expire key 10;
上面两个命令是非原子性的,可以合并为下面一条原子性的命令:
setex key 10 value
setnx key value,相当于set key value nx
getset
getset key newvalue
先返回旧的value,再用newvalue覆盖旧的value
等价于set key newvalue get
列表List操作
底层是双端链表。
如果键不存在,创建新的链表;
如果键已存在,新增内容:
如果值全移除 对应的键也就消失
对两端操作性能很好,但通过下标操作中间节点性能很差
LPUSH
在list的左边插入元素
LPUSH list1 1 2 3 4 5
先插入1,然后在1的左边插入2,在2的左边插入3......
最后list1为:5 4 3 2 1
RPUSH
在list的右边插入元素(类似push_back)
RPUSH list2 1 2 3 4 5
list2为1 2 3 4 5
LRANGE
遍历list
LRANGE list1 0 -1
输出:5 4 3 2 1
LRANGE list2 0 -1
输出:1 2 3 4 5
LPOP
弹出列表最左边的元素
LPOP list1
弹出5
RPOP
弹出列表最右边的元素
RPOP list1
弹出1
lindex
按照下标获取元素(从左到右)
lindex list1 3 输出2
LLEN
获取list中元素个数
LLEN list1输出5
LREM key N v1
从左往右删除N个值为v1的元素,当N=0时,就是删除全部值为v1的元素
LTRIM
ltrim key startindex endindex
截取[startindex, endindex]范围内的值,并重新赋值给key
RPOPLPUSH
rpoplpush list1 list2
将List1的最右边元素弹出到list2的最左边
LSET key index newvalue
将key的下标为index的元素设为newvalue
LINSERT key before/after value newvalue
在已有元素value前面/后面插入新元素newvalue
Hash操作
hset
hset key field value field value.....
hset user:001 id 11 name zhangsan age 25
key是user:001, field是id, name, age
hget
hget key field
hget user:001 name 输出zhangsan
hmset
现在和hset没区别
hmget
hmget key field1 filed2....
获取多个field的值
hgetall
hgetall key
获取key的全部field和value
hdel
hdel key field1 field2...
删除对应field及其value
hlen
hlen key
获取key中的键值对的数量
hexists
hexists key field
判断key中是否含有field字段
hkeys
hkeys key
返回key的全部field
hvals
hvals key
返回key的全部value
HINCRBY
HINCRBY key field increment
将key的field字段的值增加increment
HINCRBYFLOAT
同上,但针对小数
HSETNX
HSETNX key field value
仅当field不存在时才设置filed和value;
如果 key 不存在,则创建一个新的包含哈希值的键。如果 field 已经存在,则此操作无效。
集合Set操作
set中无重复元素
SADD
SADD key member [member ...]
将指定的成员添加到存储在 key 的集合中。已是此集合成员的指定成员将被忽略。如果 key 不存在,则在添加指定成员之前创建一个新的集合。
SADD set1 1 1 1 2 2 2 3 4 5
往集合set1中添加元素,且自动去重
**SMEMBERS **
SMEMBERS set1
遍历set1的所有元素
SUSMEMBER
SISMEMBER key member
判断元素member是否是集合key中的元素
SREM
SREM key member
将元素member从集合key中删除,如果member本来就不存在,则返回0
SCARD
SCARD key
统计集合key中的元素数量
SRANDMEMBER
SRANDMEMBER key 2
随机输出集合key中的2个元素(不删除)
SPOP
SPOP key 2
从集合key中随机删除两个元素
SMOVE
smove key1 key2 在key1中已存在的某个值
将在key1中已存在的某个值移动到key2
集合操作
差集运算SDIFF
SDIFF set1 set2
输出属于set1但不属于set2的元素
并集运算SUNION
SUNION key1 key2
返回集合key1和key2的并集
交集运算SINTER
SINTER key1 key2
返回集合key1和key2的交集
SINTERCARD
SINTERCARD keynum key1 key2.....[limit]
返回集合key1,key2.....的交集(set自动去重后)的个数,limit可以限制返回的数量
有序集合zset操作
ZADD
zadd zset1 score1 value1 score2 value2......
ZRANGE
==按照score从小到大==遍历输出zset:
ZRANGE zset1 0 -1 只会输出主值value
ZRANGE zset 0 -1 withscores 输出主值value和权值score
ZREVRANGE
==按照score从大到小==遍历输出zset:
ZRANGEBYSCORE
zrangebyscore zset1 60 90
输出分数在[60——90](闭区间)的元素的value(输出的value也是按score升序排序的)
zrangebyscore zset1 (60 (90 输出分数在60——90(开区间)的元素的value(输出的value也是按score升序排序的)
zrange zset1 60 90 limit 0 3
从分数在60—90的元素从第一个(下标0)开始选,选3个,作为输出
ZSCORE
zscore zset1 v1 获取元素v1的分数
ZCARD
zcard zset1 获取集合zset1中的元素个数
ZREM
zrem zset1 v5 删除元素v5及其分数
ZINCRBY
zincrby zset1 3 v1 将元素v1对应的score加3
ZCOUNT
zount zset1 20 50 统计score在闭区间[20, 50]的元素个数
ZMPOP
ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count]
ZMPOP 2 myzset myzset2 MIN COUNT 3 从myzset和myzset2两个集合中,弹出三个分数最小的元素(这里的元素指的是键值对{score, value})。优先弹出写在前面的zset中的元素,如果count参数大于第一个zset中元素的个数,则只会弹出第一个zset中的全部元素:
127.0.0.1:6379> ZADD myzset 1 "element1"
(integer) 1
127.0.0.1:6379> ZADD myzset 2 "element2"
(integer) 1
127.0.0.1:6379> ZADD myzset 3 "element3"
(integer) 1
127.0.0.1:6379> ZADD myzset2 4 "four"
(integer) 1
127.0.0.1:6379> ZADD myzset2 5 "five"
(integer) 1
127.0.0.1:6379> ZADD myzset2 6 "six"
(integer) 1
127.0.0.1:6379> ZMPOP 2 myzset myzset2 MIN COUNT 4
1) "myzset"
2) 1) 1) "element1"
2) "1"
2) 1) "element2"
2) "2"
3) 1) "element3"
2) "3"
ZRANK
zrank zset1 v2 获得元素v2在集合zset1中的排名(按照score升序排列)
ZREVRANK
zrevrank zset1 v2 获得元素v2在集合zset1中的排名(按照score降序排列)
位图bitmap操作
用String类型作为底层数据结构实现的 一种统计二值状态的数据类型
位图本质是数组, 它是基于String数据类型的按位的操作。该数组由多个 二进制位组成, 每个二进制位都对应 一个偏移量(我们称之为一个索引)。
bitmap支持的最大位数是 2^32位,极大节约存储空间
bitmap的类型是string,即type bitmap会输出string
setbit
setbit key offset [0|1] 将bitmap的第offset位设为0/1
getbit
getbit key offset 获取bitmap的第offset位的值
strlen
统计bitmap占用了多少字节,每8位算一个字节,不足一个字节,也算一个字节
bitcount
bitcount key [start end] 统计值为1的位的数量(范围可选)
bitop
BITOP命令是Redis中的一个命令,用于在包含字符串值的多个键之间执行位操作,并将结果存储在目标键中。
BITOP命令支持四种位操作:AND,OR,XOR和NOT。操作的结果始终存储在destkey中。
以下是BITOP命令的语法: BITOP <AND | OR | XOR | NOT> destkey key [key ...]
例如,要在两个键之间执行位AND操作并将结果存储在目标键中,可以使用以下命令: BITOP AND destkey key1 key2 Copy code
如果键的字符串长度不同,较短的字符串将被视为在最长字符串的长度上进行了零填充。
基数统计HyperLogLog操作
HyperLogLog是一种用于估计集合基数(不重复元素数量)的概率数据结构
统计去重后的元素个数,不存储元素本身,只存储元素个数(因此get hllo并不会输出元素值,而是会输出奇怪的东西)
PFADD
将元素添加到HyperLogLog数据结构中
PFADD key element [element ...]
PFCOUNT
统计HyperLogLog的基数
例如:
PFADD hllo1 1 3 5 7 9
PFADD hllo2 1 2 4 4 4 5 9 10
PFCOUNT hllo2 输出6
PFMERGE
合并多个HyperLogLog为一个
例如:
PFMERGE dst hllo1 hllo2 将hllo1和hllo2合并到dst中
地理空间Geo操作
GEOADD
geoadd city 经度1 纬度1 value1 经度2 纬度2 value2..........
type city 输出zset
redis-cli -a password --raw 解决中文乱码
ZRANGE city 0 -1 输出所有value
GEOPOS
返回经纬坐标(相当于zset的score)
GEOPOS key value1, value2......
GEOHASH
返回经纬坐标的哈希表示(三维坐标转一维)
GEOHASH key value1, value2......
GEODIST
GEODIST命令是Redis中的一个命令,用于计算在由有序集合表示的地理空间索引中两个成员之间的距离。
您需要提供键、两个成员和计量单位(米、千米、英里或英尺),命令会返回两个成员之间的距离,并以指定的单位进行表示。
这是一个示例: GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
GEODIST Sicily Palermo Catania km Copy code 这将返回"Palermo"和"Catania"之间的距离(以千米为单位)。
GEORADIUS
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
key参数是用于存储地理空间索引的有序集合的键。 longitude和latitude是用作中心点的经度和纬度坐标。
radius是搜索半径,单位可以是米、千米、英尺或英里。
m|km|ft|mi是用于指定搜索半径单位的标志。
WITHCOORD选项会将搜索结果中的成员的经度和纬度坐标一起返回。
WITHDIST选项会将搜索结果中的成员与中心点的距离一起返回。
WITHHASH选项会将搜索结果中的成员的Geohash编码一起返回。
COUNT count选项可以限制返回的搜索结果数量。
ASC|DESC选项可以指定结果排序的顺序。
STORE key选项可以将搜索结果存储在指定的键中。
STOREDIST key选项可以将搜索结果存储在指定键中,并将成员与中心点的距离一起存储。
GEORADIUSBYMEMBER
和GEORADIUS差不多,只是以某成员的经纬度为中心查找
流Stream操作
Stream = Redis版的MQ(Message Queue, 消息队列)消息中间件 + 阻塞队列
支持消息持久化
type stream => stream
队列相关指令:
XADD
向stream队列中添加消息,如果指定的stream队列不存在,则新建
xadd key * field1 value1 field2 value2........
星号表示系统自动生成MessageID
例如:
xadd mystream * index 11 name lisi
XRANGE
获取消息列表
XRANGE key start end count
例如:
XRANGE mystream - + count 10
按照MessageID从小到大显示mystream中的10条消息
XREVRANGE
和XRANGE类似,只不过是从大到小
XDEL
XDEL key messageid
删除指定MessageID的消息
XLEN
XLEN Key
统计消息队列中的消息个数
XTRIM
XTRIM key maxlen n
只保留时间戳最大的n条消息,剩下的都删掉
XTRIM Key minid messageid
比minid时间戳小的消息都会被删除
XREAD
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
count表示最多读取多少条消息
BLOCK 是否以阻塞的方式读取消息,默认不阻塞,如果milliseconds设为0,表示永远阻塞
XREAD count 2 streams mystream $
$符号表示当前mystream中最大的消息ID的下一个ID,由于mystream中不存在最大的消息ID的下一个ID,所以返回nil
XREAD count 2 streams mystream 0-0
0-0表示从最小的ID开始读取消息;若不指定count,则输出所有消息
XREAD count 1 block 0streams mystream $ 阻塞,监听比最大的消息ID还要大的新消息
新开一个终端,XADD mystream * k8 v8
此时监听到了新消息,立刻输出新消息
消费组相关指令:
XGROUP
XGROUP CREATE mystream groupA [$|0]
$表示从 Stream 尾部开始消费0 表示从 Stream 头部开始消费
创建消费者组的时候必须指定 ID, ID 为 0 表示从头开始消费,为$表示只消费新的消息
XREADGROUP
XREADGROUP GROUP groupA consumer1 STREAMS mystream >
让groupA中的消费者consumer1(大于号>代表游标)依次向前读取消息队列mystream中的所有消息
此时如果让groupA中的消费者consumer2再读取一次,发现输出为 nil:
XREADGROUP GROUP groupA consumer2 STREAMS mystream >
stream 中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取,即同一个消费组里的消费者不能消费同一条消息。刚才的 XREADGROUP 命令再执行一次,此时读到的就是空值
但是,不同消费组的消费者可以消费同一条消息:
XREADGROUP GROUP groupB consumer1 STREAMS mystream >
count参数可以限制消费者读几条消息:
XREADGROUP group groupc consumer3 count 2 streams mystream >
XPENDING
查询每个消费组内所有消费者「已读取、但尚未确认] 的消息
XPENDING mystream groupA
查看groupA中的消费者在mystream中已经读取但还未确认的情况。
XPENDING mystream groupC - + 10 consumer2
-+表示不限制消息ID的范围
10表示限制返回消息的数量
consumer2表示只查看消费者consumer2已经读取但未确认的消息
输出示例如下:
1) 1) 1526984818136-0
2) "consumer-123"
3) (integer) 196415
4) (integer) 1
- The ID of the message. 信息 ID。
- The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. 获取该信息但尚未确认的用户名称。我们称之为消息的当前所有者。
- The number of milliseconds that elapsed since the last time this message was delivered to this consumer. 自上次向该消费者发送此信息以来的毫秒数。
- The number of times this message was delivered. 该信息被传送的次数。
XACK
确认C组已读的某条消息:
XACK mystream groupC messageid
XINFO
打印Stream/Group/Consumer的详细信息
XINFO STREAM key
位域bitfield(了解即可)
持久化
RDB(Redis Database)持久化
实现类似照片记录效果的方式,就是把某一时刻的数据和状态以文件的形式写到磁盘上,也就是快照。这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件dump.rdb),其中, RDB 就是 Redis DataBase 的缩 写。
RDB自动触发
redis.conf
配置文件中修改:
save ,多少秒内修改多少次,自动触发snapshot
rdb文件保存路径:dir /myredis/dumpfiles
rdb文件保存名称:dbfilename dump+端口号.rdb
config get dir查看修改是否生效
执行 flushdb/flushall命令也会生成dump.rdb文件,只不过是个空文件
最后一次shutdown命令,会把当前所有数据都保存下来存到rdb文件中
RDB手动触发
SAVE
手动保存,redis主进程会fork出一个子进程,子进程将当前数据写到一个临时rdb文件中,写入完成后用临时rdb文件替换旧的rdb文件。在子进程进行持久化时,redis主进程被阻塞,因此在执行save命令期间,redis不能处理其他命令。因此禁止使用save命令
BGSAVE
类似save,但是不阻塞主进程
LASTSAVE命令获取最后一次snapshot的时间戳,data -d @时间戳 可以将unix时间戳转为人类能看懂的时间
RDB持久化优缺点
优点:
适合大规模的数据恢复
按照业务定时备份
对数据完整性和一致性求不高
RDB 文件在内存中的加载速度要比 AOF 快得多
缺点:
在一定间隔时间做一次备份,所以如果 redis 意外宕机的话,就会去失从当前至最近一次快照期间的数据,快照之间的数据会丢失
内存数据的全量同步,如果数据量太大会导致 I/O 严重影响服务器性能
RDB 依赖于主进程的 fork(fork操作也需要时间) ,在更大的数据集中,这可能会导致服务请求的瞬间延迟。fork 的时候内存中的数据被克隆了一份,大致 2 倍的膨胀性,需要考虑
检查并修复RDB文件
cd /usr/local/bin
redis-check-rdb rdb文件路径
哪些情况会触发RDB快照
配置文件中默认的快照配置手动
save/bgsave命令
执行 flushall/flushdb 命令也会产生 dump.rdb 文件,但里面是空的,无意义
执行 shutdown 且没有设置开启 AOF 持久化
主从复制时,主节点自动触发
禁用RDB快照
仅本次生效:redis-cli config set save ""
永久生效:redis.conf文件中改为:save ""
RDB优化参数
stop-writes-on-bgsave-error yes
当后台快照失败时,立刻停止接受写请求,立刻停止写入RDB文件,保证数据一致性
rdbcompression yes
通过LZF算法压缩rdb文件大小
rdbchecksum yes
存储快照后,redis使用CRC64算法进行数据校验,但会增加10%性能消耗
rdb-del-sync-files
rdb-del-sync-files是Redis配置文件中的一个选项,用于在主节点或复制节点在初始同步或向复制节点传输时删除在磁盘上持久化的RDB文件。只有当AOF和RDB持久化都被禁用时,此选项才有效。默认情况下,此选项处于禁用状态。
AOF(Append Only File)持久化
以日志的形式来记录每个写操作,将redis执行过的每个写操作记录下来(读操作不记录;先把这些写命令保存到AOF缓存区,当AOF缓存区中的写命令到达一定数量后再写入磁盘,避免频繁的磁盘IO操作),只允许追加文件,不允许改写文件。当redis启动时,会读取aof文件,并重新执行文件中的全部写操作,来完成数据的恢复工作。
AOF持久化工作流程:
AOF功能默认不开启
AOF三种写回策略
Always
同步写回,每个写命令执行完立刻同步地将日志写回磁盘 磁盘I/O频繁
默认everysec
每秒写回,每个写命令执行完,只是先把目志写到 AOF 文件的内存缓冲区,每隔 1 秒把缓冲区中的内容写入磁盘
NO
操作系统控制的写回,每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
三种写回策略对比
Redis 7的Multi Part AOF机制
在redis.conf文件中,若:
RDB文件的保存路径为dir "myredis"
AOF文件的保存路径为appenddirname "appendonlydir"
那么AOF文件将被保存在dir/appenddirname,即myredis/appendonlydir目录下。
在Redis 6及以前版本中,AOF只有一个.aof文件
在Redis 7中,引入了multi part AOF机制,AOF文件包括:
- 基本文件:appendonly.aof.1.base.aof
- 增量文件:appendonly.aof.1.incr.aof(写操作保存在增量文件中)
- 清单文件:appendonly.aof.manifest
修复出错的AOF文件
cd usr/local/bin
redis-check-aof --fix appendonly.aof.1.incr.aof(只修复增量文件)
AOF持久化的优缺点
优点:
- 更好的保护数据不丢失、性能高、可做紧急恢复
缺点:
- 相同数据集的数据而言 aof 文件要远大于 rdb 文件,恢复速度慢于 rdb 。
- AOF运行效率要慢于 rdb ,每秒同步策略效率较好,不同步效率和 rdb 相同
AOF重写机制
由于AOF 持久化是 Redis 不断将写命令记录到 AOF 文件中,随着 Redis 不断的进行, AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。
为了解决这个问题, Redis 新增了重写机制,当 AOF 文件的大小超过所设定的峰值时, Redis 就会自动启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集或者可以手动使用命令 bgrewriteaof 来重新。
例如:
set k1 v1
set k1 v2
set k1 v3
其实只需要保存最后一条指令即可。
也就是说 AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有键值对,然后用一条命令弋替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件·
AOF重写原理:
1:在重写开始前, redis 会创建一个“重写子进程”,这个子进程会读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中.
2 :与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程出现意外。
3:当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中
4 :当追加结束后, redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中
5 :重写 aof 文件的操作,并没有读取旧的 aof 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 aof 文件,这点和快照有点类似
自动触发AOF重写机制
redis.conf文件中:
- 关闭RDB-AOF混合持久化:aof-use-rdb-preamble no
- auto-aof-rewrite- percentage 100 根据上次重写后的 aof 大小,判断当前 aof 文件大小是不是增长了 1 倍
- auto-aof-rewrite-min-size 64mb 重写时满足的文件大小(64Mb)
以上两个条件,同时满足才会触发
触发后基本文件和增量文件的序号+1:
- 基本文件:appendonly.aof.1.base.aof => appendonly.aof.2.base.aof
- 增量文件:appendonly.aof.1.incr.aof => appendonly.aof.2.incr.aof
- 清单文件:appendonly.aof.manifest 不变
且重写瘦身后的最小指令集保存在基本文件base中,增量文件清空。
手动触发AOF重写机制
bgrewriteaof
:手动执行命令,基本文件和增量文件的序号立刻+1,不需要满足以上两点要求。
RDB、AOF混合持久化
开启混合持久化:aof-use-rdb-preamble yes
如果有AOF文件,重启时只加载AOF文件;如果没有AOF文件,则加载RDB文件,即==AOF文件优先级高于RDB==:
开启混合模式后:AOF文件包括RDB和AOF两种文件。
纯缓存模式
同时关闭RDB和AOF持久化即可:
Redis事务Transactions
什么是Redis事务:
可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞。
Redis事务的作用:
一个队列中,一次性、顺序性、排他性的执行一系列命令
Redis事务和数据库事务的区别:
Redis事务的常用命令
正常执行:MULTI开始和EXEC执行
MULTI和EXEC之间是一系列的命令
MULTI
set k1 v1
set k2 v2
set k3 v3
INCR count
EXEC
放弃事务:MULTI和DISCARD放弃执行
MULTI
set k1 v1
set k2 v2
set k3 v3
INCR count
DISCARD
全体连坐
如果MULTI之后的某条指令出现错误(比如语法错误,set k3),那么这一系列指令都不执行:
MULTI
set k1 v1
set k2 v2
set k3 // 语法error
INCR count
此时k1,k2还是原来的值
类似于编译时错误
冤有头债有主
如果MULTI之后的某条指令出现错误但未被检查出,那么这一系列指令中错误的指令不执行,正确的指令执行:
MULTI
set k1 v1
set k2 v2
set k3 v3
INCR email //email是个字符串,不可以递增,但语法没问题
EXEC
执行后,k1,k2,k3的值都改变了,但email的值不变
类似于运行时错误
watch监控
乐观锁和悲观锁:
乐观锁和悲观锁是两种常见的并发控制机制,用于解决多个线程或进程对共享资源的并发访问问题。它们在处理数据一致性和避免冲突方面有不同的方法。
乐观锁(Optimistic Locking)
概念: 乐观锁基于这样一种假设:大部分情况下,多个事务对同一数据的访问不会发生冲突。因此,在数据操作前并不锁定资源,而是通过在提交事务时检查数据是否被其他事务修改来实现并发控制。
实现方式:
- 版本号机制: 在数据表中增加一个版本号字段,每次更新数据时,版本号加1。在更新数据时,检查当前数据的版本号是否与读取时的一致,如果一致则进行更新,否则认为冲突发生,操作失败。
- 时间戳机制: 类似版本号机制,只不过使用时间戳来标记数据的最后修改时间。
优点:
- 避免了锁的开销,减少了系统的阻塞,提高了吞吐量。
- 适用于读多写少的场景。
缺点:
- 可能会出现多个事务频繁冲突导致重复操作的问题。
- 需要在业务逻辑中增加额外的冲突检测和重试机制。
悲观锁(Pessimistic Locking)
概念: 悲观锁基于这样一种假设:多个事务对同一数据的访问会经常发生冲突。因此,在数据操作前先锁定资源,防止其他事务对该资源进行修改,直到锁被释放。
实现方式:
- 行级锁: 数据库在读取数据时对数据行加锁,防止其他事务读取或修改该行数据。锁在事务结束时释放。
- 表级锁: 数据库对整个表加锁,防止其他事务读取或修改该表中的数据。
- 分布式锁: 在分布式系统中,通过Redis、Zookeeper等工具实现跨节点的锁机制。
优点:
- 可以有效避免并发冲突,确保数据的一致性。
- 适用于写操作频繁且冲突较多的场景。
缺点:
- 锁的开销较大,容易导致系统阻塞,降低并发性能。
- 容易导致死锁问题,需要额外的机制来检测和处理死锁。
使用场景
- 乐观锁: 适用于读操作多、写操作少的场景,如查询业务数据、报表生成等。
- 悲观锁: 适用于写操作多、冲突频繁的场景,如银行转账、订单处理等。
选择建议
在选择乐观锁和悲观锁时,需要根据具体业务场景进行权衡。如果系统中大部分操作是读而非写,且写操作之间的冲突概率较低,乐观锁是更合适的选择。如果系统写操作频繁且容易产生冲突,悲观锁则可能更有效。
Redis的watch 命令是一种乐观锁的实现, Redis 在修改数据的时候会检测数据是否被更改,如果更改 了, 则执行失败返回nil,例如:
watch balance //监控balance键 balance原来=100
MULTI
set k1 v1
set k2 v2
set balance 200 //此时另一个Redis客户端通过set balance 300,修改了balance的值
EXEC
执行会返回nil,因为balance的值在EXEC之前被更改了
unwatch
取消监控
watch小结
一旦执行了 exec ,之前加的监控锁都会被取消掉
当客户端连接丢失的时候(比如退出链接),所有东西都会被取消监视
Redis事务总结
开启:以 MULTI 开始一个事务
入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面执行:EXEC 命令触发事务
Redis管道(PipeLine)
为什么需要管道?:
Redis 是一种基于客户端.服务端模型以及清求/响应协议的 TCP 服务
一个请求会遵循以下步骤:
客户端向服务端发送命令分四步(发送命令一命令排队一命令执行一返回结果),并监听 socket 返回,通常以阻塞模式等待服务端响应。
服务端处理命令,并将结果返回给客户端。
上述两步称为: Round Trip Time (简称 RTT 数据包往返于两端的时间 )
如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了 RTT ( Round Time Trip ),还频繁调用系统I/O,发送网络清求,同时需要 redis 调用多次 read() 和 write() 系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好。
管道( pipe|ine )可以一次性发送多条命令给服务端,服务端依次处理完毕后,通过一条响应一次性将结果返回,通过减少客户端与 redis 的通信次数来实现降低往返延时时间. pipe|ine 实现的原理是队列,先进先出特性就保证数据的顺序性。
管道的定义:
Pipeline 是为了解决 RTT往返回时,仅仅是将命令打包一次性发送,对整个 Redis 的执行不造成其它任何影响
管道的使用:
cat command.txt | redis-cli -a password --pipe
:将command.txt里的内容(一系列指令)作为参数传递给Linux的 | 管道分隔符后面的命令。
管道总结
管道和Redis原生批量命令(mset, mget等)的区别:
- 原生批量命令是原子性(例如: mset mget) , pipeline 是非原子性
- 原生批量命令一次只能执行一种命令, pipeline 支持批量执行不同命令(不同的数据类型)
- 原生批命令是服务端实现,而 pipeline 需要服务端与客户端共同完成
管道和事务的区别:
- 事务具有原子性,管道不具有原子性
- 管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到 exec 命令后才会执行,管道不会
- 执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会
管道的使用注意事项:
- pipe|ine 缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
- 使用 pipeline 组装的命令个数不能太多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存
Redis的发布、 订阅(了解)
发布/订阅其实是一个轻量的队列,只不过数据不会被持久化,一般用来处理实时性较高的异步消息
缺点:
发布的消息在 Redis 系统中不能持久化,因此,必须先执行订阅,再等待消息发布。如果先发布了消息,那么该消息由于没有订阅者,消息将被丢弃
消息只管发送,对于发布者而言消息是即发即失的,不管接收,也没有 ACK 机制,无法保证消息的消费成功。
以上的缺点导致 Redis 的 pub / sub 模式就像个小玩具,在生产环境中几乎无用武之地,为此Redis5.0版本新增了stream数据结构, 不但支持多播,还支持数据持久化,相比 pub / sub 更加的强大
Redis主从复制
案例:一主(6379端口)二从(6380、6381端口)
修改redis+端口号.conf配置文件:
主机配置:
主机master只需要配置前 10 项即可
从机配置:
除了配置前边 10 步外,还必须配置下面这项:
replicaof <masterip主机IP地址> <masterport主机端口>
例如:replicaof 192.168.111.185 6379
masterauth <主机密码>
例如:masterauth "111111"
Redis主从复制相关操作
查看主从关系
info replication
从机可以写吗
从机只能读,不能进行写操作
从机切入点问题
slave 是从头开始复制还是从切入点开始复制?
例如:
master 启动,写到 k3
slavel 跟着 master 同时启动,跟着写到 k3
slave2 在 k3 后才启动,那之前k1, k2的是否也可以复制?
答案:可以,slave首次启动会全量复制master的数据,后面master写什么,slave就跟着复制
主机SHUTDOWN后,slave会上位成为主机吗
主机 shutdown 后,从机是上位还是原地待命?
从机不动,原地待命,从机数据可以正常使用;等待主机重启动归来
主机 shutdown 后重启,主从关系还在吗?从机还能否顺利复制?
是的,主机shutdown重启后仍然是主机,主从关系依旧,从机还可以顺利复制
从机 shutdown 后, master 继续,从机重启后它能跟上大部队吗?
可以
slaveof <主机IP> <主机Port>
如果不是在 redis.conf
配置文件中指明主从关系,而是通过命令行配置主从关系,那主从关系只单次生效,从机关机重启后,主从关系就不存在了。
薪火相传
slaveof <主机IP> <主机Port>
slave下面还可以再连slave,即上一个 slave 可以是下一个 slave 的 master ,slave 同样可以接收其他 slaves 的连接和同步请求,那么该 slave 作为了链条中下一个的 master,可以有效==减轻主 master 的写压力==;
某slave之前是master1的从机,现在该slave变更转向,成为master2的slave,那么该slave会==清除之前从master1复制的数据,重新复制master2的数据==
反客为主
slaveof no one
Redis主从复制总结
主从复制工作流程:
- slave 启动,同步初请
- slave 启动成功连接到 master 后会发送一个 sync 命令
- slave 首次全新连接 master, ,一次完全同步(全量复制)将被自动执行,slave 自身原有数据会被master的数据覆盖
- 首次连接,全量复制
- master 节点收到 sync 命令后会开始在后台保存快照(即 RDB 持久化,主从复制时会触发 RDB ),同时收集所有接收到的用于修改数据集命令缓存起来, master 节点执行完RDB持久化后,master 将 rdb 快照文件和所有缓存的命令发送到所有slave ,以完成一次完全同步
- 而slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化
- 心跳持续,保持通信
- repl-ping-replica-period 10:master每隔10秒发送PING包(心跳信号),保持通信
- 进入平稳,增量复制
- Master 继续将新的所有收集到的修改命令自动依次传 slave ,完成同步
- 从机下线,重连续传
- master 会检查 backlog 里面的 offset, master 和 slave 都会保存一个复制的 offset 还有一个 masterld。offset 是保存在 backlog 中的。 Master 只会把已经复制的 offset 后面的数据复制给 Slave ,类似断点续传
缺点:
复制延时,信号衰减。
由于所有的写操作都是先在 Master 上操作,然后同步更新到 slave 上,所以从 Master 同步到 slave 机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重;slave 机器数量的增加也会使这个问题更加严重。
master宕机怎么办?
默认情况下,master宕机后,所有的slave原地待命,不会自动上位成为master,此时只能读不能写,处于半瘫痪状态
Redis哨兵(Sentinel)
定义:哨兵巡查监控后台 master 主机是否故障,如果故障了根据==投票数==自动将某一个从机转换为新主机,继续对外服务
作用:
1 、监控 redis 运行状态、包括master 和 Slave
2 、当 master宕机能自动将 slave切换成 新 master
主从监控
消息通知
故障转移
配置中心
监控主从 redis 库运行是否正常哨兵可以将故障转移的结果发送给客户端如果 Master 异常,则会进行主从切换,将其中一个 SIave 作为新 Master客户端通过连接哨兵来获得当前 Redis 服务的主节点地址
哨兵相关操作
修改所有哨兵的sentinel.conf配置文件
除了以上配置,主要是以下两项:
sentinel monitor <master-name> <master-iP> <master-port> <quorum>
设置要监控的 master 服务器,quorum 表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数。
PS:一个哨兵可以监控多个master,但很少用
sentinel auth-pass <master-name> <password>
此外,主机master的redis.conf文件要配置 masterauth "password"
,因为现在的master在以后可能会成为slave,slave要访问master是需要密码验证的
启动哨兵
redis-sentinel sentine126379.conf --sentinel
redis-sentinel sentine126380.conf --sentinel
redis-sentinel sentine126381.conf --sentinel
SHUTDOWN关闭master模拟主机宕机
两台从机的数据仍然OK
哨兵会从剩下的两台从机中投票出新的master
如果宕机的原master重启回来,则成为新master的slave
其它哨兵还是slave,只不过跟随的master变了
redis.conf和sentinel.conf配置文件的内容,在运行期间会被 sentinel 动态进行更改
Master-SIave 切换后, master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即 原master的redis.conf 中会多一行 slaveof 配置(因为原master现在已经不是master了), sentinel.conf的监控目标会随之调换
哨兵的运行流程
主观下线SDOWN
所谓主观下线 (Subjectively Down ,简称 SDOW№)指的是单个 Sentinel 实例对服务器做出的下线判断,即单个 sentinel 认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。主观下线就是说如果服务器在[ sentinel down-after-milliseconds](在sentinel.conf配置文件中)给定的毫秒数之内没有回应PING 命令或者返回一个错误消息,那么这个 Sentinel 会主观的(单方面的)认为这个 master 不可以用了
sentinel down-after-milliseconds <mastername> <timeout>
客观下线ODOWN
quorum 这个参数是进行客观下线的一个依据,法定人数/法定票数
至少有 quorum 个 sentinel 认为这个 master 有故障才会对这个 master 进行下线以及故障转移。因为有的时候,某个 sentinel 节点可能因为自身网络原因导无法连接 master. 而此时 master 并没有出现故障,所以这就需要多个 sentinel 都 一致 认为 该master 有问题,才可以进行下一步操作,这就保证了公平性和高可用。
sentinel monitor <mastername> <masterIP> <quorum>
选举leader哨兵进行故障迁移
当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(兵王)并由该领导者节点,也即被选举出的兵王进行 failover (故障迁移)
==leader哨兵是怎么选出来的?——RAFT算法==
箭头的意思是要票的意思,比如1——>3,意思是哨兵1请求哨兵3把票投给自己。
首先,1向2,3要票,此时2和3还未投给过别人,因此哨兵1得到2票;
然后,2向1,3要票,哨兵1还未投给过别人,但哨兵3投给过1,因此哨兵2只得到来自1的一票;
最后,哨兵3向1,2要票,但1,2都已经投给过别人了,因此哨兵3得到0票;
所以,哨兵1被选举为leader
故障迁移选举新master流程
选举某个slave为新master:
选举新master规则:
在redis.conf文件中 replica-priority设置的越小,优先级越高
replication offset越大优先级越高:比如master有两个slave,master(含有10条数据)宕机了,此时slave1复制了9条数据,slave2复制了10条数据,那么为了以最小的代价恢复数据,应选择slave2作为新master
如果以上两项都相同,那么Run ID越小(字典顺序,ASCII码),优先级越高
执行 slaveof no one 命令让选出来的从节点成为新的主节点,并通过 slave of 命令让其他节点成为其从节点;
sentinel leader 会对选举出的新 master 执行 slaveof no one 操作,将其提升为 master 节点
sentinel leader 向其它 slave 发送命令,让剩余的 slave 成为新的 master的 slave
将之前已下线的老 master 设置为新选出的新 master 的从节点,当老 master 重新上线后,它会成为新master的slave
Sentinel leader 会让原来的 老master 降级为 slave 并恢复正常工作。
哨兵的使用建议
- 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
- 哨兵节点的数量应该是奇数
- 各个哨兵节点的硬件配置应一致
- 如果哨兵节点部署在 Docker 等容器里面,尤其要注意端口的正确映射
- ==哨兵集群+主从复制,并不能保证数据零丢失==(如果master宕机了,那在产生新master这段时间里,redis是只能读不能写的,所以会丢失掉产生新master这段时间的数据)
Redis集群(Cluster)
集群定义
由于数据量过大,单个 Master 复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展,每个复制集只负贵存储整个数据集的一部分,这就是 Redis 的集群,其作用是提供在多个 Redis 节点间共享数据的程序集。
一个集群最多有16384个master节点,但官方建议不要超过1000个。
一个集群至少要有 6 台机器
作用
- Redis 集群支持多个 Master, 每个 Maste 仅可以挂载多个slave
- 由于 Cluster 自带 sentinel 的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能
- 客户端与 Redis 的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
- 槽位 slot 负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系
集群的槽位Slot
Redis 集群没有使用一致性哈希,而是引入哈希槽的概念
Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽.集群的每个节点负责一部分 hash槽
举个例子,当前集群有 3 个节点,那么:
数据是跟随着槽位移动的,槽位移动,则槽里的数据也跟着移动
集群的分片
分片是什么:
使用 Redis 集群时我们会将存储的数据分散到多台 redis 机器上,这称为分片。简言之,集群中的每个 Redis 实例都被认为是整个数据的一个分片。(一个小区分为A,B,C三栋楼,每一栋楼都是该小区的一个分片)
如何找到给定key 的分片:
为了找到给定 key 的分片,我们对 key 进行 CRC16(key)算法处理并通过对总分片数量取模。然后,使用==确定性哈希函数==,这意味着==给定的 key 将多次始终映射到同一个分片==,我们可以推断将来取特定 key 的位置。
Redis集群使用槽位和分片的好处
最大优势,方便扩缩容和数据分派查找:
这种结构很容易添加或者删除节点.比如如果我想新添加个节点 D ,我需要将节点 A, B, C 中的部分槽移动到 D上. 如果想移除节点A, 需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或改变某个节点的哈希槽的数量不会造成集群不可用的状态.
槽位映射方法
哈希取余分区
hash(key)对服务器个数取模
一致性哈希算法分区
目的:解决分布式缓存数据变动和映射问题,某个机器宕机了,分母数量改变了,自然取余数不 OK 了。提出一致性 Hash 解决方案。当服务器个数发生变动时,尽量减少影响客户端到服务器的映射关系
步骤:
算法构建一致性哈希环:0—2^32 - 1首尾相连,hash(key)对2^32取模
redis服务器IP节点映射:
key到服务器的洛键规则:
假如下载要存储一个键值对,首先根据key对2^32取模,获得该键值对在哈希环上的位置,然后顺时针沿着哈希环走,碰到的第一个服务器就是该键值对应该存储的位置
一致性哈希算法的优点:
容错性:
扩展性:
一致性哈希算法的缺点:
数据倾斜:
只有落在A, B之间的数据才存储在服务器B上,其余的数据都存储在服务器A上。
总结:
为了在节点数目发生改变时尽可能少的迁移数据
将所有的存储节点排列在收尾相接的 Hash 环上,每个 key 在计算 Hash 后会顺时针找到临近的存储节点存放。而当有节点加入或退出时仅影响该节点在 Hash 环上顺时针相邻的后续节点。
优点:加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。
缺点:数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在进行存储时达不到均匀分布的效果。
哈希槽分区
为什么 redis 集群的最大槽数是 16384 个?
Redis 集群不保证强一致性,这意味着在特定的条件下, Redis 集群可能会丢掉一些被系统收到的写入请求命令
集群实操
以 三主三从 为例,每台虚拟机上运行一个主机,一个从机:
集群配置
每台虚拟机都新建两个配置文件(一主一从):myredis/cluster/redisCluster+端口号.conf,文件内容如下:
这样就建立好了6个独立的redis实例服务
其他配置:
是否需要集群完整才对外提供服务
cluster-require full-coverage yes
构建集群关系
redis-cli -a password --cluster create --cluster-replicas 1 <主机1IP:端口号 从机1IP:端口号> <主机2IP:端口号 从机2IP:端口号>.....<主机nIP:端口号 从机nIP:端口号>
//--cluster-replicas 1 表示为每个master创建一个slave节点
此时就构建好了一个三主三从的集群
集群常用命令
redis-cli -a password -p 6381 //开启客户端,必须指定端口号,否则会默认端口号6379
CLUSTER NODES //查看节点主从关系
CLUSTER INFO //查看集群总体信息
mget k1 k2 k3会报错,因为不在同一slot槽位下的键值无法使用mset, mget等多键操作
可以由通识占位符解决:
mset k1{x} v1 k2{x} v2 k3{x} v3,{}表示k1,k2,k3在同一组,这样k1,k2,k3都会被映射到同一个slot
CLUSTER COUNTKEYSINSLOT SLOT槽位编号:返回某哈希槽中的键数
CLUSTER KEYSLOT k1:查看k1应该存储在哪个槽位
集群读写
如果直接写数据set k1 v1,可能会报错:
error MOVED 槽号 IP:Port
原因是每个主机负责一部分哈希槽,而给k1分配的槽号不是由当前主机管理的
因此需要路由到位,防止路由失效,需要价格参数 -c:
redis-cli -a password -p port -c
CLUSTER KEYSLOT k1:查看k1所在的槽位
主从容错切换
如果主机宕机了,那它的从机会成为主机
如果宕机的原主机重启,那原主机将是slave
如果仍然相维持宕机前的从属关系,也就是宕机的原主机重启后还是主机,则需要节点从属调整:
CLUSTER FAILOVER
集群扩容
三主三从想要扩容为四主四从:
1、新建两个redisClusterPort.conf文件(例如:6387主机,6388从机),并启动:redis-server redisClusterPort.conf,此时6387和6388是独立的。
2、将新增的6387作为master节点加入原有集群:
redis-cli -a password --cluster add-node 6387端口的实际IP:6387 6381端口的实际IP:6381
6387通过6381这个领路人(可以是原集群中任意一个master节点)加入原集群
查看集群概况:
redis-cli -a password --cluster check 6381端口的实际IP:6381
3、发现,此时新加入的6387master节点还没有被分配slot(空槽位),所以需要重新分配槽位(reshard):
redis- cli a 密码 --cluster reshard 192.168.111.185:6381
全部槽位重新分配成本太高,所以原来的三个主机master每个都匀一部分slot给新加入的6387master,最后每个master都有16384/4=4096个槽位。
4、为6387master节点分配从节点6388:
此时,集群扩容结束。
集群缩容
从四主四从恢复成三主三从:
- 首先获取从机6388的ID
redis-cli -a password --cluster check 192.168.111.184:6388
从集群中删除从节点6388
redis-cli -a password --cluster del-node <slaveIP:6388> <6388ID>
此时check一下集群,发现现在只剩下7台机器
redis-cli -a password --cluster check 192.168.111.184:6385
将6387的槽号全都给6381(提前准备好6381和6387的ID)
redis- cli a 密码 --cluster reshard 192.168.111.185:6381
此时6387成为了6381的slave
删除6387
redis-cli -a password --cluster del-node <slaveIP:6387> <6387ID>
此时,缩容完成
CRC16校验算法
集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分 hash 槽:
SLOT = CRC16(key) / 16384
redis源码中是直接调用的C语言的crc16函数:
return crc16(key,keylen) & 0x3FFF;
高级篇
Redis线程
Redis是单线程还是多线程
说Redis是单线程是什么意思
Redis 3.x单线程版本但性能依旧很好的原因
- 基于内存操作: Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
- 数据结构简单: Redis 的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是 0(1), 因此性能比较高
- I/O多路复用和非阻塞 I/O : Redis 使用I/O多路复用功能来监听多个 socket 连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了 I/O 阻塞懌作
- 免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
Redis4.0 之前一直采用单线程的主要原因
简单来说, Redis4.0 之前一直采用单线程的主要原因有以下三个:
1 使用单线程模型使 Redis 的开发和维护更简单,因为单线程模型方便开发和调试:
2 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO
3 对于 Redis 系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU 。
既然单线程这么好,为什么逐渐又加入了多线程特性?
大Key删除问题
正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。
这就是 redis3.x单线程时代最经典的故障,大 key 删除的头疼问题,
由于 redis 是单线程的, del bigKey
等待很久这个线程才会释放,类似加了一个 synchronized 锁,你可以想象高并发下,程序堵成什么样子?
解决大Key删除
比如当我 (Redis) 需要删除一个很大的数据时,因为是单线程原子命令操作,这就会导致 Redis 服务卡顿,
于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。
因为 Redis 是单个主线程处理, redis 之父antirez一直强调 "Lazy Redis is better Redis'
当Redis检测到需要删除一个大Key时,为了避免阻塞主线程的正常处理(例如读写请求),它会使用一个异步子线程来进行删除操作。这样做的目的是将耗时的删除操作移到后台,从而保证Redis的高性能和低延迟。 这个异步删除机制是Redis 4.0引入的特性,主要用于删除大型的数据结构(例如大型的哈希表、集合、列表或有序集合),通过异步的方式避免阻塞Redis的主线程。这种删除操作在Redis中被称为“lazy free”或者“bio delete”。
而 lazy free 的本质就是把某些 cost (主要时间复制度,占用主线程 cpu 时间片)较高删除操作,从 redis 主线程剥离让 bio 子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
Redis为什么快
影响Redis性能的因素有:CPU、内存、网络IO,对于 Redis 主要的性能瓶颈是内存或者网络带宽而并非 CPU
随着网络硬件的性能提升, Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,==单个主线程处理网络请求的速度跟不上底层网络硬件的速度,==
为了应对这个问题:采用多个 IO 线程来处理网络清求,提高网络清求处理的并行度, rdis6 / 7 就是采用的这种方法。但是, ==Redis 的多 IO 线程只是用来处理网络请求的,对于读与操作命令 Redis 仍然使用单线程来处理。==
这是因为, Redis 处理请求时,网络处理经常是瓶颈,通过多个 IO 线程并行处理网络操作,可以提升实例的整体处理性能。
而继续使用单线程执行命令操作,就不用为了保证 Lua 脚本、事务的原子性,额外开发多线程==互斥加锁机制了(不管加锁操作处理)==,这样一来, Redis 线程模型实现就简单了
主线程和 IO 线程是怎么协作完成请求处理的
Redis6,7 将网络数据读写(读取socket并绑定到线程、回写socket)、请求协议解析通过多个 IO 线程的来处理
对于真正的 命令执行来说,仍然使用主线程操作,一举两得,便宜占尽
UNIX网络编程的五种IO模型
IO多路复用
文件描述符
文件描述符 (File descriptor) 是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设汁中,文件描述符这一概念往往只适用 UNIX 、 Linux 这样的操作系统。
IO多路复用是什么
一种同步的IO 模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪就能够通知到对应应用程序进行相应的读写操作,没有文件句柄就绪时就会阻塞应用程序,从而释放 CPU 资源
举例:
IO多路复用模型
哪个socket有消息到达,单线程就连接该socket处理消息,处理完后就断开连接,转头处理其它socket的消息,就像拨开关一样
客户端请求服务端时,实际就是在服务端的 Socket 文件中写入客户端对应的文件 描述 符 (FiIeDescriptor),如果有多个客户端同时清求服务端,为每次请求分配一个线程,类似每次来都 new— 个如此就会比较耗费服务端资源
因此,我们只使用一个线程来监听多个文件描述符,这就是IO多路复用
采用IO多路复用技术可以让单个线程高效的处理多个连接请求,一个服务端进程可以同时处理多个套接字描述符。
==IO多路复用+ epoll函数使用,才是 redis 为什么这么快的直接原因,而不是仅仅单线程命令+ redis 安装在内存中。==
Redis开启多线程
如果你在实际应用中,发现 Redis 实例的 CPU 开销不大但吞吐量却没有提升,可以考虑使用 Redis7 的多线程机制,加速网络处理,进而提升实例的吞吐量
Redis7 将所有数据放在内存中,内存的响应时长大约为 100 纳杪,对于小数据包, Redis 服务器可以处理 8W 到 10W 的 QPS ,这也是 Redis 处理的极限了,对于 80 %的公司来说,单线程的 Redis 己经够使用了。
Redis6.0及7之后,多线程默认是关闭的,需要在redis.conf文件中开启:
io-threads 6 //8核用6线程, 4核用2/3线程,线程数必须 < 核数
io-threads-do-reads yes
Redis线程总结
BigKey问题
往Redis中插入一百万条数据:
for((i=1;i<=100*10000;i ++));do echo "set k$i v$i" >> /tmp/redisTest.txt; done; //先把一百万条set命令写入txt
cat /tmp/redisTest.txt | redis-cli -h 127.0.0.1 -p 6379 -a 111111 --pipe //再用管道写入redis
key *这个指令有致命的弊端,在实际环境中最好不要使用
这个指令没有 offset 、 limit 参数,是要一次性吐出所有满足条件的 key ,由于 redis 是单线程的,其所有操作都是原子的,而 keys 是遍历算法,复杂度是 O(n), 如果实例中有干万级以上的 key, 这个指令就会导致 Redis 服务卡顿,所有读与 Redis 的其它的指令都会被延后甚至会超时报错,可能会引起缓存雪崩甚至数据库宕机
生产上如何限制 keys * | flushdb | flushall 等危险命令以防止误删误用?
redis.conf配置文件中的SECURITY选项下:
把要禁用的命令重命名为空字符串
rename-command keys ""
rename-command flushdb ""
rename-command flushall ""
不用Keys *用什么?
SCAN命令
SCAN cursor [MATCH pattern] [COUNT count]
基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
以 0 作为游标开始一次新的迭代,直到命令返回游标 0 完成一次遍历
不保证每次执行都返回某个给定数量的元素,支持模糊查询
一次返回的数量不可控,只能是大概率符合 count 参数
SCAN遍历所有类型的Key;
SSCAN、ZSACN、HSCAN是遍历Key对应的Value中的每个field
多大才算BigKey
set k1 v1
大的不是k1,而是k1对应的value v1
- string 是 value, 最大 512MB 但是 >= 10KB 就是 BigKey
- list 、 hash 、 set 和 zset, 元素个数超过 5000 就是 bigkey
BigKey的危害
- 内存不均,集群迁移困难
- 超时删除,大 key 删除作梗(超过expire time会自动调用Del命令)
- 网络流量阻塞
如何发现BigKey
redis-cli --bigkeys参数
memory usage命令
MEMORY USAGE K100:返回的是为管理K100所分配的内存总字节数
如何删除BigKey
渐进式删除
例如 HSET user:001 id 10 name lisi score 50.......
不可以直接整个删除:HDEL user:001
而是应该逐步删除filed及其对应值:HDEL user:001 score
string类型的BigKey:
可以DEL,但如果过于庞大使用UNLINK异步删除
Hash类型的BigKey:
Hscan 每次获取少量 field-value, 再使用 hdel 删除每个 field
List类型的BigKey:
使用 Ltrim 渐进式逐步删除,直到全部删除完
Set类型的BigKey:
使用 SSCAN 每次获取部分元素,再使用 srem 命令删除每个元素
Zset类型的BigKey:
使用 zscan 每次获取部分元素,再使用 ZREMRANGEBYRANK 命令删除每个元素
BigKey生产调优
redis.conf文件 LAZY FREEING
lazyfree-lazy-eviction 针对redis内存使用达到maxmeory,并设置有淘汰策略时;在被动淘汰键时,是否采用lazy free机制;
因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。此场景使用时,请结合业务测试。
lazyfree-lazy-expire 针对设置有TTL的键,达到过期后,被redis清理删除时是否采用lazy free机制;
此场景建议开启,因TTL本身是自适应调整的速度。
lazyfree-lazy-server-del 针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。 此参数设置就是解决这类问题,建议可开启。
lazyfree-lazy-user-del
修改del
命令的默认行为,使之与unlink
命令一毛一样
lazyfree-lazy-user-del
支持yes
或者no
。默认是no
。- 如果设置为
yes
,那么del
命令就等价于unlink
,也是非阻塞删除。
缓存双写一致性
如何理解缓存双写一致性
如果 redis 中有数据:需要和数据库中的值相同,数据的数量也要相同
如果 redis 中无数据:数据库中的值要是最新值,且尽快回写 redis
缓存按照操作来分,细分 2 种:
- 只读缓存:只查询,不回写
- 读写缓存:
- 同步直写策略:写数据库后也同步写 redis 缓存,缓存和数据库中的数据一致
- 异步缓写策略:
- 正常业务运行中,MySQL数据变动了,但是可以在业务上容许出现一定时间后才作用于 redis ,比如仓库、物流系统
- 异常情况出现了,不得不将失败的动作重新修补,有可能需要借助 kafka 或者 RabbitMQ 等消息中间件,实现重试重写
双检加锁策略
当高并发情况下,很多线程请求同时(时间间隔很小)查询Redis,发现redis上没有要查找的数据,立刻向MySQL查找(时间间隔很小),这可能会击穿MySQL,而且这些线程还会同时回写Redis,造成数据重复写(覆盖)。
原因是由于查询MySQL和回写Redis这两步不是原子性操作
为了解决这个问题,引入双检加锁策略
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查洵数据的请求上使用一个互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现己经有缓存了,就直接走缓存。
数据库和缓存一致性的更新策略
目的:最终一致性
==数据库指的是MYSQL,缓存指的是REDIS==
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。
也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,
切记,要以 mysql 的数据库写入库为准。上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是 100 %绝对正确,不保证绝对适配全部情况,请同学们自行酌情选择打法,合适自己的最好。
可以停机的情况下:
- 挂牌报错,凌晨升级,温馨提示,服务降级
- 单线程,这样重量级的数据操作最好不要多线程
不能停机的情况下:
先更新数据库,再更新缓存
问题1:读取Redis时读到的是脏数据(旧数据)
先更新 mysql 的某商品的库存,当前商品的库存是 100 ,更新为 99 个。
先更新 mysql 修改为 99 成功,然后更新 redis
此时异常出现,更新 redis 失败了,这导致 mysql 里面的库存是 99 ,而 redis 里面的还是 100
上述发生,会让数据库mysql里面和缓存redis 里面的数据不一致,读到 redis 脏数据
问题2:数据覆盖,MYSQL和REDIS的最终数据不一致
先更新缓存,再更新数据库
不推荐:业务上一般把 mysql 作为底单数据库,保证最后解释
问题:数据覆盖,MYSQL和REDIS的最终数据不一致
先删除缓存,再更新数据库
问题:数据覆盖,MYSQL和REDIS的最终数据不一致
两个并发操作,一个是更新操作A,另一个是查询操作B,A 删除缓存后, B 查询操作没有命中缓存, B 先把数据库中的老数据读出来后放到缓存中,然后 A 更新操作更新了数据库。于是,缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
解决方法:延时双删
A先删除Redis缓存,在A更新MYSQL数据库期间,B线程读取了MYSQL中的旧数据并把旧数据回写到REDIS,然后A更新完MYSQL;
接着,A再删除一次redis,这样就把B回写的脏数据删掉了;
此时,线程C读取REDIS,发现redis为空,就来读取MYSQL,而MYSQL中的数据是刚被A更新过的最新数据,这样C读取到的就是正确的最新数据。
由于A删除了两次REDIS缓存,所以叫做延时双删
1、这个删除该休眠多久呢
2、这种同步淘汰策略,吞吐量降低怎么办?
A删除完REDIS后再起一个线程,监控线程A,A删除完REDIS后无需休眠就去更新MYSQL,当A更新完MYSQL后,该线程再删除一次REDIS缓存,异步删除,这样更新线程A就不用sleep休眠了,从而增大了吞吐量
3、后续看门狗 WatchDog 源码分析
先更新数据库,再删除缓存
问题:
假如缓存删除失败或者来不及,导致请求再次访问 redis 时命中,读取到的是缓存旧值。
解决方案
怎样和MYSQL数据库保持一致?BinLog
怎样选择更新策略
更新策略优缺点
Canal
Canal项目地址:『alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件』https://github.com/alibaba/canal
我想 mysql 有记录改动了(有增删改写操作),立刻同步反应到 redis 该如何做?
答:MYSQL有写操作,有增量的变更,能够被某种技术捕捉监控到,且能够通知RREDIS做出一模一样的更改,以达到数据一致性
怎样知道 mysql有改动?
答:BINLOG日志,二进制日志记录所有导致数据更改的事件。通过监控二进制日志,可以确定数据库的变化。
BINLOG日志介绍:
Binlog 是 MySQL 和 MariaDB 数据库中的二进制日志 (Binary Log),在数据库管理和操作中起着重要作用。下面详细介绍 binlog 的定义及其用途:
Binlog 的定义
Binlog(Binary Log,二进制日志)是 MySQL 和 MariaDB 用于记录所有对数据库进行的更改(如数据修改和表结构变化)的日志文件。这些更改以二进制格式存储,便于高效处理。
Binlog 的用途
数据复制:
- 主从复制:Binlog 是实现主从复制(Master-Slave Replication)的核心机制。主服务器将所有数据更改记录到 binlog 中,从服务器读取这些日志并重放,以保持数据的一致性和同步。
- 多主复制:在 MariaDB 中,binlog 还支持多主复制(Multi-Source Replication),即一个从服务器可以从多个主服务器接收并应用 binlog。
数据恢复:
- 时间点恢复:Binlog 可用于灾难恢复,通过回放 binlog 中记录的所有事务,可以将数据库恢复到某个特定时间点。这对于防止数据丢失和错误操作引起的数据破坏非常有用。
Binlog 的组成部分
- 事件:Binlog 由一系列事件组成,每个事件记录一次数据库操作,例如插入、更新、删除操作。
- 格式:
- 基于语句的日志 (Statement-Based Logging):记录导致数据变化的 SQL 语句。
- 基于行的日志 (Row-Based Logging):记录实际的数据变化,这种方式更精确,适用于复杂的事务。
- 混合日志 (Mixed Logging):结合了语句和行两种模式。
Binlog 的管理
- 启用 Binlog:在 MySQL 配置文件(如
my.cnf
或my.ini
)中设置log-bin
参数以启用二进制日志。 - Binlog 格式:通过设置
binlog_format
参数为STATEMENT
、ROW
或MIXED
来选择日志格式。 - 日志保留:
expire_logs_days
参数指定二进制日志的保留天数,超过此天数的日志将被自动删除。
Canal工作原理
- MYSQL主从复制工作原理