Redis
一、什么是Redis??有什么特点
Redis 是⼀个⾼性能的 key-value 内存数据库,使⽤键值对的⽅式来组织数据,主要使⽤内存存储数据(当然也⽀持持久化存储到硬盘上)。Redis 没有关系型数据库的复杂查询以及约束等功能, 换来的是简单易⽤和更⾼的性能。
特点:
- 使⽤内存存储 (⾼性能).、⽀持多种数据结构、⽀持持久化、单线程处理请求、⽀持主从复制、⽀持哨兵模式、⽀持集群模式、⽀持事务、⽀持多语⾔客⼾端。
二、Redis使用场景
2.1 Redis使用场景
2.2 Redis的key过期淘汰机制实现
Redis通过给key设置过期时间,来作为缓存使用。也就意味着,同一时刻Redis可能存在大量的key,并且有相当部分key已经过期了。Redis如何得知,并将过期的key进行删除呢? Redis提供了两种过期淘汰机制:定期删除和惰性删除!!
- 定期删除:Redis定期抽取一部分key,验证过期时间。保证整个行为足够快!!Redis一般是为MySQL等关系型数据库负重前行的,并且Redis是一个单线程程序。如果一次遍历大量的key,可能会导致Redis短时间内无法对其他客户端的报文进行应答,一旦超时,此时就会去请求MySQL。而MySQL对数据是非常敏感的,一旦并发上来后,就有可能导致MySQL挂掉,这无疑是非常严重的!!
- 惰性删除:当Redis在访问到过期的key时,Redis服务器才将其删除,同时返回il
除了上面两种删除方式,网上还提到了第三种过期key删除策略:
上述说法是错误的: 首先Redis并没有采取这种删除策略;第二如果有多个key,可以通过一个定时器在节省CPU的情况下,高效的的处理多个key(基于优先级队列或时间轮实现)
- 基于优先级队列:将所有设置了过期时间的key全部添加到一个优先级队列中,然后分配一个线程监控队首元素是否过期即可。并且我们可以根据当前时刻和队首过期时间,为该线程设置一个阻塞时间。当达到指定时间后,在唤醒该线程即可,防止频繁检测。 同时每次向队列中添加新成员时,在更新过期时间即可!
- 基于时间轮:类似于环形队列,将时间划分为多个小段,假设间隔为100ms。每一个小段对应一个链表,链表元素保存需要执行的任务。此时将所有的设置了过期时间的key添加到对应链表中即可。 此时按照间隔时间一次遍历环形链表中的每一个元素,然后尝试执行对应链表中的任务。
2.3 Redis单线程,为啥速度快?
Redis 内部的逻辑⽐较简单, ⼀般的性能瓶颈都是出现在 内存 或者 IO 上, 很少是 CPU . 因此使⽤多线程
并没有太⼤的收益, 反⽽可能会引⼊线程安全问题.从 6.0 开始, Redis 引⼊多线程. 此时只是使⽤多个线程去处理⽹络请求+协议解析, 真正执⾏ Redis 命令仍然是单线程完成的. 这样做可以进⼀步提⾼ IO 的处理效率.
- 首先Redis是在内存中操作数据的,MySQL访问磁盘。
- Redis提供的核心功能比MySQL简单。
- Redis执行的任务一般短平,不太吃CPU资源,性能瓶颈在于内存和落磁盘。设置为多线程,反正增加线程切换以及锁冲突。反而效率更低。
- 通过epoll多路复用,处理网络IO。(epoll 在内核中维护了⼀个红⿊树, 来管理所有的 socket, 并且每个节点都关联了⼀个事件回调. 当系统内核感知到⽹卡收到数据了, 进⼀步判定这个数据是给哪个 socket 的, 随之调⽤对应的回调, 进⼀步唤醒⽤⼾线程, 来处理这个收到的数据.)
三、Redis常用类型和相关指令
3.1 基本全局命令
指令 | 作用 | 细节 |
---|---|---|
keys pattern | 返回所有满⾜样式(pattern)的 key | |
EXISTS key [key …] | 判断某个 key 是否存在 | |
DEL key [key …] | 删除指定的 key | |
EXPIRE key seconds | 为指定的 key 添加秒级的过期时间 | |
TTL key | 获取指定 key 的过期时间,秒级 | 返回剩余过期时间。-1 表⽰没有关联过期时间,-2 表⽰ key 不存在。 |
TYPE key | 返回 key 对应的数据类型 | |
object encoding | 查看key编码 |
3.2 Redis 的 5 种常见数据类型
Redis 这样设计有两个好处:
-
可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码,⽆需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了⼀种更为优秀的内部编码实现,⽽对⽤⼾来说基本⽆感知。
-
多种内部编码实现可以在不同场景下发挥各⾃的优势,例如 ziplist ⽐较节省内存,但是在列表元素⽐较多的情况下,性能会下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为linkedlist,整个过程⽤⼾同样⽆感知。
1) string
基本概念和指令
字符串类型是 Redis 最基础的数据类型:
- ⾸先 Redis 中所有的键的类型都是字符串类型,⽽且其他⼏种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型。
- 字符串类型的值实际可以是字符串,包含⼀般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚⾄是⼆进制流数据,例如图⽚、⾳频、视频等。不过⼀个字符串的最⼤值不能超过 512 MB。
- 由于 Redis 内部存储字符串完全是按照⼆进制流的形式保存的,所以 Redis 是不处理字符集编码问题的,客⼾端传⼊的命令中使⽤的是什么字符集编码,就存储什么字符集编码。
字符串类型的内部编码有 3 种:
- int:8 个字节的⻓整型。
- embstr:⼩于等于 39 个字节的字符串。
- raw:⼤于 39 个字节的字符串。
Redis 会根据当前值的类型和⻓度动态决定使⽤哪种内部编码实现。
命令 | 执⾏效果 | 时间复杂度 | 细节补充 |
---|---|---|---|
SET key value [expiration EX seconds |PX milliseconds] [NX |XX] | 设置 key 的值是 value | O(k), k 是键个数 | |
get key | 获取 key 的值 | O(1) | 如果 key 不存在,返回 nil。如果 value 的数据类型不是 string,会报错 |
del key [key …] | 删除指定的 key | O(k), k 是键个数 | |
mset key value [key value…] | 批量设置指定的 key 和 value | O(k), k 是键个数 | 如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。 |
mget key [key …] | 批量获取 key 的值 | O(k), k 是键个数 | |
incr key | 指定的 key 的值 +1 | O(1) | 如果key不存在,会把key当作0处理。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。 |
decr key | 指定的 key 的值 -1 | O(1) | 同incr |
incrby key n | 指定的 key 的值 +n | O(1) | 同incr |
decrby key n | 指定的 key 的值 -n | O(1) | 同incr |
incrbyfloat key n | 指定的 key 的值 +n | O(1) | 同incr |
append key value | 指定的 key 的值追加 value | O(1) | 将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。 |
strlen key | 获取指定 key 的值的⻓度 | O(1) | |
setrange key offset value | 覆盖指定 key 的从 offset 开始的部分值 | O(n),n 是字符串⻓度, 通常视为 O(1) | |
getrange key start end | 获取指定 key 的从 start 到 end 的部分值 | O(n),n 是字符串⻓度, 通常视为 O(1) | 返回 key 对应的 string 的⼦串,由 start 和 end 确定(左闭右闭)。可以使⽤负数表⽰倒数。-1 代表倒数第⼀个字符,-2 代表倒数第⼆个,其他的与此类似。超过范围的偏移量会根据 string 的⻓度调整成正确的值。 |
string常见使用场景
缓存功能:
当前企业大部分是MySQL 作为存储层, Redis 作为缓冲层。绝⼤部分请求的数据都是从 Redis 中获取。由于 Redis 具有⽀撑⾼并发的特性,所以缓存通常能起到加速读写和降低后端压⼒的作⽤。
与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,⽽且也没有对键名有强制要求(除了不能使⽤⼀些特殊字符)。但设计合理的键名,有利于防⽌键冲突和项⽬的可维护性,⽐较推荐的⽅式是使⽤“业务名:对象名:唯⼀标识:属性” 作为键名。例如MySQL 的数据库名为 vs,⽤⼾表名为 user_info,那么对应的键可以使⽤"vs:user_info:6379"、“vs:user_info:6379:name” 来表⽰.
计数功能:
许多应⽤都会使⽤ Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。
实际中要开发⼀个成熟、稳定的真实计数系统,要⾯临的挑战远不⽌如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。
共享会话:
⼀个分布式 Web 服务将⽤⼾的 Session 信息保存在各⾃的服务器中。出于负载均衡的考虑,分布式服务会将⽤⼾的访问请求均衡到不同的服务器上并且通常⽆法保证⽤⼾每次请求都会被均衡到同⼀台服务器上,这样当⽤⼾刷新⼀次访问是可能会发现需要重新登录,这个问题是⽤⼾⽆法容忍的。
为了解决这个问题,可以使⽤ Redis 将⽤⼾的 Session 信息进⾏集中管理。⽆论⽤⼾被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。
⼿机验证码:
很多应⽤出于安全考虑,会在每次进⾏登录时,让⽤⼾输⼊⼿机号并且配合给⼿机发送验证码,然后让⽤⼾再次输⼊收到的验证码并进⾏验证,从⽽确定是否是⽤⼾本⼈。为了短信接⼝不会频繁访问,会限制⽤⼾每分钟获取验证码的频率,例如⼀分钟不能超过 5 次
String 发送验证码(phoneNumber) {
key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使⽤ NX,只在不存在 key 时才能设置成功
bool r = Redis 执⾏命令:set key 1 ex 60 nx
if (r == false) {
// 说明之前设置过该⼿机的验证码了
long c = Redis 执⾏命令:incr key
if (c > 5) {
// 说明超过了⼀分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过⼿机的验证码;要么次数没有超过 5 次
String validationCode = ⽣成随机的 6 位数的验证码();
validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执⾏命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过⼿机短信发送给⽤⼾
return validationCode ;
}
// 验证⽤⼾输⼊的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
validationKey = "validation:" + phoneNumber;
String value = Redis 执⾏命令:get validationKey;
if (value == null) {
// 说明没有这个⼿机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
2)Hash
基本概念和指令
哈希类型中的映射关系通常称为 field-value,⽤于区分 Redis 整体的键值对(key-value),注意这⾥的 value 是指 field 对应的值,不是键(key)对应的值,请注意 value 在不同上下⽂的作⽤。
哈希的内部编码有两种:
- ziplist(压缩列表):当哈希类型元素个数⼩于 hash-max-ziplist-entries 配置(默认 512 个)、同时所有值都⼩于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使⽤ ziplist 作为哈希的内部实现,ziplist 使⽤更加紧凑的结构实现多个元素的连续存储,所以在节省内存⽅⾯⽐hashtable 更加优秀。
- hashtable(哈希表):当哈希类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,⽽ hashtable 的读写时间复杂度为 O(1)。
压缩列表的本质就是对数据重新编码。比如:aaabbbde->3a3bde
节省空间,但存储和读取需要转换,更加耗时相关配置信息全在/etc/redis/redis.conf
命令 | 执⾏效果 | 时间复杂度 | 细节补充 |
---|---|---|---|
HSET key field value [field value …] | 设置值 | O(1) | |
hget key field | 获取值 | O(1) | |
hdel key field [field …] | 删除 field | O(k), k 是 field个数 | del删除key,hedl删除field |
hkeys key | 获取 hash 中的所有字段 | O(N), N 为 field 的个数. | |
hvals key | 获取所有的 value | O(k), k 是 field个数 | |
hlen key | 计算 field 个数 | O(1) | |
hgetall key | 获取所有的 field-value | O(k), k 是 field个数 | |
hmget field [field …] | 批量获取 field-value | O(k), k 是 field个数 | |
hmset field value [field value …] | 批量获取 field-value | O(k), k 是 field个数 | |
hexists key field | 判断 field 是否存在 | O(1) | |
hsetnx key field value | 设置值,但必须在 field 不存在时才能设置成功 | O(1) | |
hincrby key field n | 对应 field-value +n | O(1) | |
hincrbyfloat key field n | 对应 field-value +n | O(1) | |
hstrlen key field | 计算 value 的字符串⻓度 | O(1) |
hash 常见使用场景
hash的最大使用场景就是用于组织数据。但是需要注意的是哈希类型和关系型数据库有两点不同之处:
- 哈希类型是稀疏的,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,⽽关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为 null
- 关系数据库可以做复杂的关系查询,⽽ Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本⾼
缓存⽅式对⽐:
- 原⽣字符串类型⸺使⽤字符串类型,每个属性⼀个键。
set user:1:name James
set user:1:age 23
set user:1:city Beijing
优点:实现简单,针对个别属性变更也很灵活。
缺点:占⽤过多的键,内存占⽤量较⼤,同时⽤⼾信息在 Redis 中⽐较分散,缺少内聚性,所以这种⽅案基本没有实⽤性。
- 序列化字符串类型,例如 JSON 格式
set user:1 经过序列化后的⽤⼾对象字符串
优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内存的使⽤效率很⾼。
缺点:本⾝序列化和反序列需要⼀定开销,同时如果总是操作个别属性则⾮常不灵活。
- 哈希类型
hmset user:1 name James age 23 city Beijing
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较⼤消耗。
3)List (列表)
基本概念和指令
列表中的元素是有序的,并且允许重复的。当前的 List,头和尾都能高效的插入删除元素,就可以把这个 List 当做一个 栈 / 队列 来使用了.
Redis 有一个典型的应用场景,就是作为消息队列.最早的时候,就是通过 List 类型~后来 Redis 又提供了一个 stream 类型。
列表类型的内部编码有两种:
- ziplist(压缩列表):当列表的元素个数⼩于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的⻓度都⼩于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选⽤ziplist 来作为列表的内部编码实现来减少内存消耗。
- linkedlist(链表):当列表类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ linkedlist 作为列表的内部实现。
执⾏效果 | 命令 | 时间复杂度 | 细节补充 |
---|---|---|---|
添加 | rpush key value [value …] | O(k),k 是元素个数 | |
添加 | lpush key value [value …] | O(k),k 是元素个数 | |
添加 | lpushx key value [value …] | O(k),k 是元素个数 | 在 key 存在时,将⼀个或者多个元素从左侧放⼊(头插)到 list 中。不存在,直接返回 |
添加 | rpushx key value [value …] | O(k),k 是元素个数 | 在 key 存在时,将⼀个或者多个元素尾插list 中。不存在,直接返回 |
添加 | linsert key before | after pivot value | O(n),n 是 pivot 距离头尾的距离 | |
查找 | lrange key start end | O(s+n),s 是 start 偏移量,n 是 start 到 end 的范围 | 返回合法区间的所有值,下标越界处理类似于python |
查找 | lindex key index | O(n),n 是索引的偏移量 | 获取从左数第 index 位置的元素. 例子:> LINSERT mylist BEFORE "World" "There" |
查找 | llen key | O(1) | |
删除 | lpop key | O(1) | |
删除 | rpop key | O(1) | |
删除 | lremkey count value | O(k),k 是元素个数 | |
删除 | ltrim key start end | O(k),k 是元素个数 | |
修改 | lset key index value | O(n),n 是索引的偏移量 | |
阻塞操作 | blpop brpop | O(1) |
List 常见使用场景
消息队列:
Redis 可以使⽤ lpush + brpop 命令组合实现经典的阻塞式⽣产者-消费者模型队列,⽣产者客⼾端使⽤ lpush 从列表左侧插⼊元素,多个消费者客⼾端使⽤ brpop 命令阻塞式地从队列中"争抢" 队⾸元素。通过多个客⼾端来保证消费的负载均衡和⾼可⽤性。
栈/队列:
- 同侧存取(lpush + lpop 或者 rpush + rpop)为栈
- 异侧存取(lpush + rpop 或者 rpush + lpop)为队列
4)Set 集合
基本概念和指令
集合类型也是保存多个字符串类型的元素的,但和列表类型不同的是,集合中元素之间是⽆序的、元素不允许重复!
⼀个集合中最多可以存储 个元素。Redis 除了⽀持集合内的增删查改操作,同时还⽀持多个集合取交集、并集、差集,合理地使⽤好集合类型,能在实际开发中解决很多问题。
集合类型的内部编码有两种:
- intset(整数集合):当集合中的元素都是整数并且元素的个数⼩于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选⽤ intset 来作为集合的内部实现,从⽽减少内存的使⽤。
- hashtable(哈希表):当集合类型⽆法满⾜ intset 的条件时,Redis 会使⽤ hashtable 作为集合的内部实现。
命令 | 时间复杂度 | 细节补充 |
---|---|---|
sadd key element [element …] | O(k),k 是元素个数 | |
srem key element [element …] | O(k),k 是元素个数 | 将指定的元素从 set 中删除 |
spop key [count] | O(n), n 是 count | 从 set 中删除并返回⼀个或者多个元素。注意,由于 set 内的元素是⽆序的,所以取出哪个元素实际是未定义⾏为,即可以看作随机的 |
SMOVE source destination member | O(1) | 将⼀个元素从源 set 取出并放⼊⽬标 set 中 |
scard key | O(1) | 获取⼀个 set 的基数(cardinality),即 set 中的元素个数。 |
sismember key element | O(1) | |
srandmember key [count] | O(n),n 是 count | |
smembers key | O(k),k 是元素个数 | 获取⼀个 set 中的所有元素,注意,元素间的顺序是⽆序的 |
sinter key [key …] sitnerstore | O(m * k),k 是⼏个集合中元素最⼩的个数,m 是键个数 | 交集 |
sunion key [key …] sunionstore | O(k),k 是多个集合的元素个数总和 | 并集 |
sdiff key [key …] sdiffstore | O(k),k 是多个集合的元素个数总和 | 差集 |
string常见使用场景
标签:
企业通过分析用户特征,收集相关数据。收集到的用户特征就可以转换为"标签"并保存到set中。同时不太企业之间会存在合作,此时通过set可以很方便的求交集,找到两个用户之间的公共标签。在基于这些标签衍生出一些“用户关系”!
共同好友:
共同好友推荐
统计UV:
一个互联网产品,如何衡量用户量,用户规模??主要的指标,是两方面:
- PV (page view)用户每次访问该服务器,每次访问都会产生一个 pv 。
- UV (user view)每个用户,访问服务器,都会产生一个 uv 。但是同一个用户多次访问,不会使 uv 增加。uv 需要按照用户进行去重~~
5)Zset 集合
基本概念和指令
Zset,也称有序集合。它保留了集合不能有重复成员的特点,但与集合不同的是,有序集合中的每个元素都有⼀个唯⼀的浮点类型的分数(score)与之关联,着使得有序集合中的元素是可以维护有序性的,但这个有序不是⽤下标作为排序依据⽽是⽤这个分数。有序集合中的元素是不能重复的,但分数允许重复!!
有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数⼩于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都⼩于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会⽤ ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使⽤。
- skiplist(跳表):当 ziplist 条件不满⾜时,有序集合会使⽤ skiplist 作为内部实现,因为此时ziplist 的操作效率会下降。
命令 | 时间复杂度 | 细节补充 |
---|---|---|
zadd key score member [score member …] | O(k * log(n)),k 是添加成员的个数,n 是当前有序集合的元素个数 | |
zcard key | O(1) | zset 中的元素个数。 |
ZCOUNT key min max | O(log(n)),n 是当前有序集合的元素个数 | 返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。 比如:(10 (20 --> 11 19 、(10 20 --> 11 20 。 |
ZRANGE key start stop [WITHSCORES] | O(log(N)+M) | 此处的 [start, stop] 为下标构成的区间. 从 0 开始, ⽀持负数 |
ZREVRANGE key start stop [WITHSCORES] | O(log(N)+M) | 返回指定区间⾥的元素,分数按照降序 |
ZRANGEBYSCORE key min max [WITHSCORES] | O(log(N)+M) | 返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 ( 排除 |
ZPOPMAX/ZPOPMIN key [count] | O(log(N) * M) | 删除并返回分数最⾼/低的 count 个元素 |
BZPOPMAX/BZPOPMIN key [key …] timeout | O(log(N)) | |
ZRANK key member | O(log(N)) | 返回指定元素的排名,升序。 |
ZREVRANK key member | O(log(N)) | 返回指定元素的排名,降序。 |
ZSCORE key member | O(1) | 返回指定元素的分数。 |
ZREM key member [member …] | O(M*log(N)) | 删除指定的元素。 |
ZREMRANGEBYRANK key start stop | O(log(N)+M) | 按照排序,升序删除指定范围的元素,左闭右闭。 |
ZREMRANGEBYSCORE key min | O(log(N)+M) | 按照分数删除指定范围的元素,左闭右闭。 |
ZINCRBY key increment member | O(log(N)) | 为指定的元素的关联分数添加指定的分数值 |
ZINTERSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] [AGGREGATE <SUM | MIN | MAX>] | O(n * k) + O(m * log(m)),n 是输⼊的集合最⼩的元素个数,k 是集合个数, m 是⽬标集合元素个数 | 求出给定有序集合中元素的交集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元素对应的分数按照不同的聚合⽅式和权重得到新的分数。 |
ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] [AGGREGATE <SUM | MIN | MAX>] | O(n) + O(m * log(m)),n 是输⼊集合总元素个数,m 是⽬标集合元素个数 | 并集,规则同上 |
Zset常见使用场景
排⾏榜:
使用Zset来统计以及实时更新排行。并且有时排行榜比较复杂,需要综合各自因素。而Zset也允许我们将维度的因素按照不同权值进行交际排序!!
3.3 渐进式遍历。遍历key方式
Redis使⽤ keys * 虽然能⼀次性获取到所有 key, 但是这个操作开销可能⾮常⼤, 会把 Redis 卡死!!
靠谱的⽅法是使⽤ scan 命令.每次 scan 都能返回⼀批 keys 同时告知我们下次应该从哪⾥开始进⾏ scan
需要使⽤多次 scan 才能完成整个遍历
SCAN cursor [MATCH pattern] [COUNT count]
Cursor 表⽰遍历 key 的光标. 从 0 开始, 每次执⾏ scan 都会返回下次开始的光标. 当返回结果为 0, 则说
明遍历结束。通过 count 可以限制每次获取到的 key 的个数。
四、持久化
redis最典型的应用场景之一就是最为一个内存数据库。和mysql这类数据库相比,最大的特点就是在内存中直接操作数据,速度非常快。但缺点就是一旦出现异常,比如掉电,在内存中的就是就会全部丢失。
所以redis在做内存数据库的同时,也提供了两种数据持久化操作 —— RDB、AOF。
- RDB 视为内存的快照,产⽣的内容更为紧凑,占⽤空间较⼩,恢复时速度更快。但产⽣ RDB 的开销较⼤,不适合进⾏实时持久化,⼀般⽤于冷备和主从复制。
- AOF 视为对修改命令保存,在恢复时需要重放命令。并且有重写机制来定期压缩 AOF ⽂件。
- RDB 和 AOF 都使⽤ fork 创建⼦进程,利⽤ Linux ⼦进程拥有⽗进程内存快照的特点进⾏持久化,
尽可能不影响主进程继续处理后续命令。
4.1 RDB
RDB会定期将Redis中的所有的内存数据全部保存到磁盘中,生成一个快照。
1) RDB触发机制
RDB触犯方式分为两大类:
- 手动触发:在执行save 和 bgsave 命令后,会触发RDB持久化操作。 对于save指令,一旦执行后,redis会全力以赴进行备份,导致无法对其他客户端进行响应。一旦需要备份的数据量较大,该过程比较耗时,将会是非常致命的。redis一般是为Mysql负责前行的,其他客户端请求redis超时,此时会将所有的请求操作全部交给Mysql。但Mysql对数据量是非常敏感的。一旦请求上来了,就容易崩。所以该指令不建议使用。 但对于bgsave指令,redis会在内部fork创建出一个子进程,由子进程负责备份操作。此时redis只会在创建子进程的过程中发生短暂阻塞。
- 自动触发:在配置⽂件中通过 save m n 即可设定 m 秒内发⽣ n 次修改, 就触发持久化;从节点进⾏全量复制操作, 触发持久化;执⾏ shutdown 命令, 关闭 redis server, 触发持久化我们
2) bgsave 命令的运作流程
bgsave 是主流的 RDB 持久化⽅式。工作流程如下:
- 执⾏ bgsave 命令,如果存在其他正在执行该行为的子进程,如 RDB/AOF ⼦进程(开启AOF后,以AOF为准),此时bgsave 命令直接返回。
- ⽗进程执⾏ fork 创建⼦进程,fork 过程中⽗进程会阻塞。
- ⽗进程 fork 完成后,bgsave 命令返回 “Background saving started” 信息并不再阻塞⽗进程,可
以继续响应其他命令。 - ⼦进程创建 RDB ⽂件,根据⽗进程内存⽣成临时快照⽂件,完成后对原有⽂件进⾏原⼦替换。
- 进程发送信号给⽗进程表⽰完成,⽗进程更新统计信息。
3) RDB ⽂件的处理
- 保存:RDB ⽂件保存再 dir 配置指定的⽬录(默认 /var/lib/redis/)下,⽂件名通过 dbfilename
配置(默认 dump.rdb)指定。可以通过执⾏ config set dir {newDir} 和 config set dbfilename
{newFilename} 运⾏期间动态执⾏,当下次运⾏时 RDB ⽂件会保存到新⽬录。
- 压缩:Redis 默认采⽤ LZF 算法对⽣成的 RDB ⽂件做压缩处理,压缩后的⽂件远远⼩于内存⼤
⼩,默认开启,可以通过参数 config set rdbcompression {yes|no} 动态修改。虽然压缩 RDB 会消耗 CPU,但可以⼤幅降低⽂件的体积,⽅便保存到硬盘或通过⽹络发送到从节点,因此建议开启。 - 校验:如果 Redis 启动时加载到损坏的 RDB ⽂件会拒绝启动。这时可以使⽤ Redis 提供的 redis-check-dump ⼯具检测 RDB ⽂件并获取对应的错误报告。
4) RDB持久化方式优缺点
- RDB 是⼀个紧凑压缩的⼆进制⽂件,代表 Redis 在某个时间点上的数据快照。⾮常适⽤于备份,全量复制等场景,⽤于灾备。
- Redis 加载 RDB 恢复数据远远快于 AOF 的⽅式。(RDB二进制直接读取,而AOF是文本文件,需要进行一些列字符串切割)
- RDB 持久化方式比较耗时,属于重量级操作,频繁执⾏成本过⾼。无法实时数据备份,可能导致最近的数据在掉电重启后丢失。
- RDB ⽂件使⽤特定⼆进制格式保存,Redis 版本演进过程中有多个 RDB 版本,兼容性可能有⻛险。
4.2 AOF
AOF(Append Only File)持久化:类似于Mysql中的binlog,以独⽴⽇志的⽅式记录每次写命令,重启时再重新执⾏ AOF⽂件中的命令达到恢复数据的⽬的。AOF 的主要作⽤是解决了数据持久化的实时性,⽬前已经是Redis 持久化的主流⽅式。
一旦开启aof,rdb就失效了。重启时,只读取aof文件。
1)AOF开启方式
开启 AOF 功能需要设置配置:appendonly yes,默认不开启。AOF ⽂件名通appendfilename 配置(默认是 appendonly.aof)设置。保存⽬录同 RDB 持久化⽅式⼀致,通过 dir配置指定。
2)AOF ⼯作流程
- 所有的写⼊命令会追加到 aof_buf(缓冲区)中。
- AOF 缓冲区根据对应的策略向硬盘做同步操作。
- 随着 AOF ⽂件越来越⼤,需要定期对 AOF ⽂件进⾏重写,达到压缩的⽬的。
- 当 Redis 服务器启动时,可以加载 AOF ⽂件进⾏数据恢复。
3)重写机制
随着命令不断写⼊ AOF,⽂件会越来越⼤,为了解决这个问题,Redis 引⼊ AOF 重写机制压缩⽂件体积。对中间操作过程进行分析优化,只记录最终结果!
AOF 重写过程可以⼿动触发和⾃动触发:
- ⼿动触发:调⽤ bgrewriteaof 命令。
- ⾃动触发:根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定⾃动触发时
机。
◦ auto-aof-rewrite-min-size:表⽰触发重写时 AOF 的最⼩⽂件⼤⼩,默认为 64MB。
◦ auto-aof-rewrite-percentage:代表当前 AOF 占⽤⼤⼩相⽐较上次重写时增加的⽐例。
4) AOF 重写流程
- 执⾏ AOF 重写请求:如果当前进程正在执⾏ AOF 重写,请求不执⾏。如果当前进程正在执⾏ bgsave 操作,重写命令延迟到 bgsave 完成之后再执⾏。
- ⽗进程执⾏ fork 创建⼦进程。
- 主进程 fork 之后,继续响应其他命令。所有修改操作写⼊ AOF 缓冲区并根据 appendfsync 策略同步到硬盘,保证旧 AOF ⽂件机制正确。
- ⼦进程只有 fork 之前的所有内存信息,⽗进程中需要将 fork 之后这段时间的修改操作写⼊AOF 重写缓冲区中。
- ⼦进程根据内存快照,将命令合并到新的 AOF ⽂件中。
- ⼦进程完成重写
a. 新⽂件写⼊后,⼦进程发送信号给⽗进程。
b. ⽗进程把 AOF重写缓冲区内临时保存的命令追加到新 AOF ⽂件中。
c. ⽤新 AOF ⽂件替换⽼ AOF ⽂件。
4.3 Redis 根据持久化⽂件进⾏数据恢复
五、Redis 事务
5.1 基本概念
Redis事务将多个操作指令打包执行,但该事物是保证了指令集执行还是不执行。执行后,成功还是失败是不保证的。
- 弱化的原⼦性: redis 没有 “回滚机制”. 只能做到这些操作 “批量执⾏”. 不能做到 “⼀个失败就恢复到初始状态”.
- 不保证⼀致性: 不涉及 “约束”. 也没有回滚. MySQL 的⼀致性体现的是运⾏事务前和运⾏后 , 结果都是合理有效的, 不会出现中间⾮法状态.
- 不需要隔离性: 也没有隔离级别, 因为不会并发执⾏事务 (redis 单线程处理请求) .
- 不需要持久性: 是保存在内存的. 是否开启持久化, 是redis-server ⾃⼰的事情, 和事务⽆关
Redis 事务本质上是在服务器上搞了⼀个 “事务队列”. 每次客⼾端在事务中进⾏⼀个操作, 都会把命令先
发给服务器, 放到 “事务队列” 中(但是并不会⽴即执⾏)⽽是会在真正收到 EXEC 命令之后, 才真正执⾏队列中的所有操作.因此, Redis 的事务的功能相⽐于 MySQL 来说, 是弱化很多的. 只能保证事务中的这⼏个操作是 “连续
的”, 不会被别的客⼾端 “加塞”, 仅此⽽已。
5.1 事务大致过程
5.2 事务相关指令
用途 | 指令 |
---|---|
开启⼀个事务 | MULTI |
执⾏事务 | EXEC |
放弃当前事务 | DISCARD |
对key进行监控 | WATCH |
取消对 key 的监控 | UNWATCH |
- 当开启事务的时候, 如果对 watch 的 key 进⾏修改, 就会记录当前 key 的 “版本号”. (版本号是个简单
的整数, 每次修改都会使版本变⼤. 服务器来维护每个 key 的版本号情况)
在真正提交事务的时候, 如果发现当前服务器上的 key 的版本号已经超过了事务开始时的版本号, 就
会让事务执⾏失败. (事务中的所有操作都不执⾏
六、Redis 主从复制
分布式系统,涉及到一个非常关键的问题:单点问题。如果某个服务器程序,只有一个节点 (只搞一个物理服务器,来部署这个服务器程序),会存在以下问题:
- 可用性问题,如果这个机器挂了,意味着服务就中断了~
- 性能 / 支持的并发量也是比较有限的~
引入分布式系统,主要也就是为了解决上述的单点问题~~在分布式系统中,往往希望有多个服务器来部署 redis 服务,从而构成一个 redis 集群~~,此时就可以让这个集群给整个分布式系统中其他的服务,提供更稳定 / 更高效的数据存储功能~~
在分布式系统中,希望使用多个服务器来部署 redis, 存在以下几种 redis 的部署方式~~
- 主从模式
- 主从 + 哨兵模式
- 集群模式
复制功能是⾼可⽤ Redis 的基础,哨兵和集群都是在复制的基础上构建的。主从复制的原理,包括:建⽴复制、全量复制、部分复制、⼼跳检测等
6.1 配置
参与复制的 Redis 实例划分为主节点(master)和从节点(slave)。复制的数据流是单向的,只能由主节点到从节点。配置复制的⽅式有以下三种:
- 在配置⽂件中加⼊ slaveof {masterHost} {masterPort} 随 Redis 启动⽣效。
- 在 redis-server 启动命令时加⼊ --slaveof {masterHost} {masterPort} ⽣效。
- 直接使⽤ redis 命令:slaveof {masterHost} {masterPort} ⽣效。
接下来,我们将 redis.conf 配置⽂件复制⼀份 redis-slave.conf,并且修改其 daemonize 为 yes。
接下来,默认启动的 redis 作为主 Redis,重新通过命令⾏启动⼀个 Redis 实例作为从 Redis:
ubuntu:
redis-server /etc/redis/redis-slave.conf --port 6380 --slaveof 127.0.0.1 6379
centos:
redis-server /etc/redis-slave.conf --port 6380 --slaveof 127.0.0.1 6379
3种配置主从的方式:
a.配置文件slaveof {masterHost} {masterPort}
b. redis-server --slaveof {masterHost} {masterPort}
c. slaveof {masterHost} {masterPort} 命令
断开主从复制关系:从节点执行slaveof no one
切主操作流程:断开与旧主节点的复制关系→与新主节点建立复制关系→删除从节点当前所以数据→对新节点进行复制操作
数据传输延迟repl-disable-tcp-nodelay参数:用于控制是否关闭tcp-nodelay,默认关闭;关闭时,主节点产生的命令数据无论大小都会及时发送给从节点,主从延迟变小,网络带宽消耗增加,开启时,主节点会合并较小的TCP数据包,默认间隔40毫秒发送一次,主从延迟变大,网络带宽消耗减少
2.三种复制拓扑结构:
A.一主一从:用于主节点出现故障时从节点提供故障转移,当应用写命令并发量较高且需要持久化时,可以只在从节点上开启aof,当主节点关闭持久化功能时,要避免自动重启操作,因为主节点没开启持久化功能自动重启后数据集为空,这是从节点如果继续复制主节点将导致从节点数据也被清空,安全的做法是先在从节点上执行slaveof no one命令,与主节点断开复制关系,再重启主节点
B.一主多从:可利用多个从节点实现读写分离,对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力,同时在日常开发中如果需要执行一些比较耗时的读命令,如:keys、sort等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性;但对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了主节点的负载影响服务稳定性
C.树状结构:从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制,通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量, 数据实现了一层一层的向下复制,当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用树状主从结构降低主节点压力
3.复制流程:
A.复制的大体流程:
a.保存主节点信息:执行完slaveof命令后从节点只保存主节点的信息地址便返回,此时建立复制流程还未开始
b.主从建立socket连接:从节点内部通过每秒运行定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接,若无法建立连接,定时任务会无限重试直到连接成功或执行slaveof no one命令为止
c.发送ping命令:用于检测主从之间网络套接字是否可用和主节点当前是否可接受处理命令,若从节点没收到主节点的pong回复或超时,则断开复制连接,下次定时任务会发起重连
d.权限验证:若主节点设置了requirepass参数,则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证,若验证失败则复制终止,从节点重新发起复制流程
e.数据集同步:主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这步操作耗时最长(全量复制)
f.命令持续复制:当主节点把当前的数据同步给从节点后,便完成复制的建立流程,接下来主节点会持续把写命令发送给从节点,保证主从数据一致性
B.数据同步(基于psync命令):
全量复制:用于初次复制场景(不可避免),主节点将全部数据一次性发送给从节点,当数据量较大时,会对主从节点与网络造成很大开销
部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失的场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点,因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销
psync命令:需复制偏移量、复制积压缓冲区、主节点运行id支持
复制偏移量:用于记录主从节点数据同步偏差的量,可用info replication查看
复制积压缓冲区:保存在主节点上的一个固定长度的队列,默认大小1M,当主节点有连接的从节点被创建时,主节点响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区,可用info replication查看,用于数据补救
主节点运行id:每个主节点的唯一标识,运行id变化,则从节点发生全量复制,可使用info server查看(PS:若只是用ip+port方式进行标识,当主节点重启变更整体数据集(如替换RDB/AOF文件),从节点再基于偏移量复制数据将不再安全,redis重启,运行id也会改变)
不改变运行ID的情况下重启:使用debug reload命令(PS:该命令会阻塞当前redis节点主线程,阻塞期间会生成本地RDB快照并清空数据后再加载RDB文件,对于大数据量的主节点和无法容忍阻塞的场景,慎用!)
psync命令流程:psync runId(主节点运行id) offset(从节点复制偏移量)
a.从节点(slave)发送psync命令给主节点,若是第一次参与复制则offset为-1
b.主节点(master)根据psync参数和自身数据情况决定响应结果:
若回复+FULLRESYNC {runId} {offset},则从节点将触发全量复制流程
若回复+CONTINUE,则从节点将触发部分复制流程
若回复+ERR,说明主节点版本低于Redis2.8,无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程
C.全量复制流程:
1.发送psync命令进行数据同步,是第一次进行复制,从节点没有复制 偏移量和主节点的运行id,所以发送psync-1
2.主节点解析出为全量复制,回复+FULLRESYNC响应
3.从节点接收主节点的响应数据保存运行id和偏移量offset
4.主节点执行bgsave保存RDB文件到本地
5.主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件
6.从节点接收RDB快照期间,主节点仍响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区,从节点加载完RDB文件后,主节点再把缓冲区内数据发送给从节点,保证主从数据一致性,若主节点创建和传输RDB的时间过长,在高流量写入场景非常容易造成 主节点复制客户端缓冲区溢出,默认配置为clientoutput-buffer-limit slave 256MB 64MB 60,若60秒内缓冲区消耗持续大于64MB或直接 超过256MB时,主节点将直接关闭复制客户端连接,全量同步失败
7.从节点接收完主节点传送来的全部数据后会清空自身旧数据
8.从节点清空数据后开始加载RDB文件
9.从节点加载完RDB后,若当前节点开启aof功能,会立刻做 bgrewriteaof操作,保证全量复制后aof持久化文件立刻可用
PS:RDB文件大于6G的主节点,复制要格外注意,若复制的总时间超 过repl-timeout所配置的值(默认60s),从节点将放弃接受RDB文件并 清理已下载的临时文件,全量复制将失败,对于数据量较大的节点,建 议跳大repl-timeout参数防止超时 全量复制主要时间开销=主节点bgsave+RDB文件网络传输+从节点清空数据+从节点记载RDB+可能的aof重写
D.部分复制流程:
1.当主从节点之间网络出现中断时,若超过repl- timeout时间,主节点会认为从节点故障并中断复制连接
2.主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,但主节点内部 存在复制积压缓冲区,可保存最近一段时间的写命令数据
3.当主从节点网络恢复,从节点将再次连上主节点
4.当主从连接恢复后,由于从节点之前保存了自身 已复制的偏移量和主节点的运行id,因此会把它们 当作psync参数发送给主节点,要求进行部分复制 操作
5.主节点接到psync命令后首先核对参数runId是否与自身一致,若一致,说明之前复制的是当前主节点,之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应, 表示可进行部分复制
6.主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态
七、哨兵 (Sentinel)
7.1 主从复制缺陷
Redis 的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作⽤:
- 作为主节点的⼀个备份,⼀旦主节点出了故障不可达的情况,从节点可以作为后备 “顶” 上来,并且保证数据尽量不丢失(主从复制表现为最终⼀致性)。
- 第⼆,从节点可以分担主节点上的读压⼒,让主节点只承担写请求的处理,将所有的读请求负载均衡到各个从节点上。
但是主从复制模式并不是万能的,它同样遗留下以下⼏个问题:
- 主节点发⽣故障时,进⾏主备切换的过程是复杂的,需要完全的⼈⼯参与,导致故障恢复时间⽆法保障。
- 主节点可以将读压⼒分散出去,但写压⼒/存储压⼒是⽆法被分担的,还是受到单机的限制。
其中第⼀个问题是⾼可⽤问题,即 Redis 哨兵主要解决的问题。
1)运维⼈员通过监控系统,发现 Redis 主节点故障宕机。
2)运维⼈员从所有节点中,选择⼀个执⾏ slaveof no one,使其作为新的主节点。
3)运维⼈员让剩余从节点执⾏ slaveof {newMasterIp} {newMasterPort} 从新主节点开始数据同步。
4)更新应⽤⽅连接的主节点信息到 {newMasterIp} {newMasterPort}。
5)如果原来的主节点恢复,执⾏ slaveof {newMasterIp} {newMasterPort} 让其成为⼀个从节点。上述过程可以看到基本需要⼈⼯介⼊,⽆法被认为架构是⾼可⽤的。⽽这就是 Redis Sentinel 所要做的。
7.2 Redis Sentinel 架构
Redis Sentinel 具有以下⼏个功能:
- 监控: Sentinel 节点会定期检测 Redis 数据节点、其余哨兵节点是否可达。
- 故障转移: 实现从节点晋升(promotion)为主节点并维护后续正确的主从关系。
- 通知: Sentinel 节点会将故障转移的结果通知给应⽤⽅。
针对主节点故障的情况具体如下:
首先如果一个哨兵节点节点发现主节点,会将主节点判定为主观下线。然后哨兵节点直接会对主节点故障这件事情进⾏投票. 当故障得票数 >= 配置的法定票数之后,此时意味着 redis-master 故障这个事情被做实了,将其状态改为客观下线。
接下来需要哨兵把剩余的 slave 中挑选出⼀个新的 master. 这个⼯作不需要所有的哨兵都参与. 只需要选出个代表 (称为 leader), 由 leader 负责进⾏ slave 升级到 master 的提拔过程。这个选举的过程涉及到 Raft 算法。{每个哨兵节点都给其他所有哨兵节点, 发起⼀个 "拉票请求, 收到拉票请求的节点, 会回复⼀个 “投票响应”. 响应的结果有两种可能, 投 or 不投., ⼀轮投票完成之后, 发现得票超过半数的节点, ⾃动成为 leader。}
leader 挑选出合适的 slave 成为新的 master挑选规则:
- ⽐较优先级. 优先级⾼(数值⼩的)的上位. 优先级是配置⽂件中的配置项( slave-priority 或者replica-priority ).
- ⽐较 replication offset 谁复制的数据多, ⾼的上位.
- ⽐较 run id , 谁的 id ⼩, 谁上位
tips:
- 哨兵节点不能只有⼀个. 否则哨兵节点挂了也会影响系统可⽤性.
- 哨兵节点最好是奇数个. ⽅便选举 leader, 得票更容易超过半数.
- 哨兵节点不负责存储数据. 仍然是 redis 主从节点负责存储.
- 哨兵 + 主从复制解决的问题是 “提⾼可⽤性”, 不能解决 “数据极端情况下写丢失” 的问题.
- 哨兵 + 主从复制不能提⾼数据的存储容量. 当我们需要存的数据接近或者超过机器的物理内存, 这样的结构就难以胜任了.
八、集群 (Cluster)
8.1 哨兵机制确定
哨兵 模式, 提⾼了系统的可⽤性. 但是真正⽤来存储数据的还是 master 和 slave 节点. 所有的数据都需要存储在单个 master 和 slave 节点中。如果数据量很⼤, 接近超出了 master / slave 所在机器的物理内存, 就可能出现严重问题了。
Redis 的集群就是在上述的思路之下, 引⼊多组 Master / Slave , 每⼀组 Master / Slave 存储数据全集的⼀部分, 从⽽构成⼀个更⼤的整体, 称为 Redis 集群 (Cluster)
8.2 数据分⽚算法
Redis cluster 的核⼼思路是⽤多组机器来存数据的每个部分. 那么接下来的核⼼问题就是, 给定⼀个数
据 (⼀个具体的 key), 那么这个数据应该存储在哪个分⽚上? 读取的时候⼜应该去哪个分⽚读取?
围绕这个问题, 业界有三种⽐较主流的实现⽅式.
1) 哈希求余
设有 N 个分⽚, 使⽤ [0, N-1] 这样序号进⾏编号.针对某个给定的 key, 先计算 hash 值, 再把得到的结果 % N, 得到的结果即为分⽚编号.
优点: 简单⾼效, 数据分配均匀.
缺点: ⼀旦需要进⾏扩容, N 改变了, 原有的映射规则被破坏, 就需要让节点之间的数据相互传输, 重新排列, 以满⾜新的映射规则. 此时需要搬运的数据量是⽐较多的, 开销较⼤
2) ⼀致性哈希算法
将数据空间, 映射到⼀个圆环上. 数据按照顺时针⽅向增⻓,同时将不同分片设置在圆环的不同位置。插入key时, 计算得到 hash 值 H,顺时针往下找, 找到的第⼀个分⽚, 即为该 key 所从属的分⽚。 此时在扩容时,只需移动部分数据到新节点即可。
优点: ⼤⼤降低了扩容时数据搬运的规模, 提⾼了扩容操作的效率.
缺点: 数据分配不均匀 (有的多有的少, 数据倾斜)
3)哈希槽分区算法 (Redis 使⽤)
首先会存在多个槽位,然后将所有的hash值全部映射到这些槽位上。然后再把这些槽位⽐较均匀的分配给每个分⽚. 每个分⽚的节点使用位图记录⾃⼰持有哪些槽位。
如果需要进⾏扩容, ⽐如新增⼀个 3 号分⽚, 就可以针对原有的槽位进⾏重新分配.⽐如可以把之前每个分⽚持有的槽位, 各拿出⼀点, 分给新分⽚。{搬运成本⾼ 和 数据分配不均匀}
8.4 主节点宕机处理流程
- 故障判定:
集群中的所有节点, 都会周期性的使⽤⼼跳包进⾏通信。每个节点, 每秒钟, 都会给⼀些随机的节点发起 ping 包。不能如期回应的时候, 此时会尝试重置 tcp 连接,如果仍然连接失败, A 就会把 B 设为 PFAIL 状态(相当于主观下线)
通过 redis 内置的 Gossip 协议, 和其他节点进⾏沟通, 向其他节点确认B的状态。此时 发现其他很多节点, 也认为 B 为 PFAIL, 并且数⽬超过总集群个数的⼀半, 那么标记成 FAIL (相当于客观下线)。⾄此, 对方就彻底被判定为故障节点了。
- 故障迁移
所谓故障迁移, 就是指把从节点提拔成主节点, 继续给整个 redis 集群提供⽀持。
从节点判定⾃⼰是否具有参选资格. 如果从节点和主节点已经太久没通信,数据差异大;时间超过阈值, 就失去竞选资格.。
具有资格的节点,会先休眠⼀定时间. 休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms. offset 的值越⼤, 则排名越靠前(越⼩).
休眠时间一到,就会给其他所有集群中的主节点, 进⾏拉票操作。收到的票数超过主节点数⽬的⼀半,就会晋升成主节点。同时还会把⾃⼰成为主节点的消息, 同步给其他集群的节点. ⼤家也都会更新⾃⼰保存的集群结构信息。(Raft 算法)
九、Redis 典型应⽤ - 缓存 (cache)
9.1 缓存的更新策略
接下来还有⼀个重要的问题, 到底哪些数据才是 “热点数据” 呢?
1) 定期⽣成
每隔⼀定的周期(⽐如⼀天/⼀周/⼀个⽉), 对于访问的数据频次进⾏统计. 挑选出访问频次最⾼的前 N%的数据.
这种做法实时性较低. 对于⼀些突然情况应对的并不好。⽐如春节期间, “春晚” 这样的词就会成为⾮常⾼频的词. ⽽平时则很少会有⼈搜索 "春晚。
2) 实时⽣成
先给缓存设定容量上限。接下来把⽤⼾每次查询:
- 如果在 Redis 查到了, 就直接返回。
- 如果 Redis 中不存在, 就从数据库查, 把查到的结果同时也写⼊ Redis。
如果缓存已经满了(达到上限), 就触发缓存淘汰策略, 把⼀些 “相对不那么热⻔” 的数据淘汰掉:
通⽤的淘汰策略主要有以下⼏种:
- FIFO (First In First Out) 先进先出:把缓存中存在时间最久的 (也就是先来的数据) 淘汰掉。
- LRU (Least Recently Used) 淘汰最久未使⽤的。
- LFU (Least Frequently Used) 淘汰访问次数最少的。
- Random 随机淘汰
Redis 内置的淘汰策略如下:
- volatile-lru 当内存不⾜以容纳新写⼊数据时,从设置了过期时间的key中使⽤LRU(最近最少使⽤)算法进⾏淘汰。
- allkeys-lru 当内存不⾜以容纳新写⼊数据时,从所有key中使⽤LRU(最近最少使⽤)算法进⾏淘汰
- volatile-lfu 4.0版本新增,当内存不⾜以容纳新写⼊数据时,在过期的key中,使⽤LFU算法进⾏删除key.
- allkeys-lfu 4.0版本新增,当内存不⾜以容纳新写⼊数据时,从所有key中使⽤LFU算法进⾏淘汰.
- volatile-random 当内存不⾜以容纳新写⼊数据时,从设置了过期时间的key中,随机淘汰数据.
- allkeys-random 当内存不⾜以容纳新写⼊数据时,从所有key中随机淘汰数据.
- volatile-ttl 在设置了过期时间的key中,根据过期时间进⾏淘汰,越早过期的优先被淘汰.(相当于 FIFO, 只不过是局限于过期的 key)
- noeviction 默认策略,当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错.
9.2 缓存预热, 缓存穿透, 缓存雪崩 和 缓存击穿
1)缓存预热
使⽤ Redis 作为 MySQL 的缓存的时候, 当 Redis 刚刚启动, 或者 Redis ⼤批 key 失效之后, 此时由于Redis ⾃⾝相当于是空着的, 没啥缓存数据。提前把热点数据准备好, 直接写⼊到 Redis 中。(不⼀定⾮得那么 “准确”, 只要能帮助MySQL 抵挡⼤部分请求即可. 随着程序运⾏的推移, 缓存的热点数据会逐渐⾃动调整,)
2)缓存穿透
访问的 key 在 Redis 和 数据库中都不存在. 此时这样的 key 不会被放到缓存上, 后续如果仍然在访问该key, 依然会访问到数据库.
原因可能有⼏种:
- 业务设计不合理. ⽐如缺少必要的参数校验环节, 导致⾮法的 key 也被进⾏查询了.
- 开发/运维误操作. 不⼩⼼把部分数据从数据库上误删了.
- ⿊客恶意攻击
如何解决:
- 针对要查询的参数进⾏严格的合法性校验. ⽐如要查询的 key 是⽤⼾的⼿机号, 那么就需要校验当前key 是否满⾜⼀个合法的⼿机号的格式.
- 针对数据库上也不存在的 key , 也存储到 Redis 中, ⽐如 value 就随便设成⼀个 “”. 避免后续频繁访问数据库.
- 使⽤布隆过滤器先判定 key 是否存在, 再真正查询.
3)缓存雪崩
短时间内⼤量的 key 在缓存上失效, 导致数据库压⼒骤增, 甚⾄直接宕机!!
⼤规模 key 失效, 可能性主要有两种:
- Redis 挂了.
- Redis 上的⼤量的 key 同时过期.
如何解决:
- 部署⾼可⽤的 Redis 集群, 并且完善监控报警体系.
- 不给 key 设置过期时间 或者 设置过期时间的时候添加随机时间因⼦
4)缓存击穿
相当于缓存雪崩的特殊情况. 针对热点 key , 突然过期了, 导致⼤量的请求直接访问到数据库上, 甚⾄引起数据库宕机. (瘫痪)
如何解决?
- 基于统计的⽅式发现热点 key, 并设置永不过期.
- 进⾏必要的服务降级. 例如访问数据库的时候使⽤分布式锁, 限制同时请求数据库的并发数
十、Redis 典型应⽤ - 分布式锁
本质上就是使⽤⼀个公共的服务器, 来记录 加锁状态.这个公共的服务器可以是 Redis, 也可以是其他组件(⽐如 MySQL 或者 ZooKeeper 等), 还可以是我们⾃⼰写的⼀个服务.