TARGET DECK: Redis
Redis 基础
Redis 的介绍、优缺点
Redis 是 C 语言开发的数据库,数据存储在内存中,读写速度更快。
优点:
Redis 作为一个内存数据库。
1、性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS;
2、单进程,是线程安全的,采用 IO 多路复用机制
3、丰富的数据类型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等;
4、支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载;
5、主从复制,哨兵,高可用;
6、可以用作分布式锁;
7、可以作为消息中间件使用,支持发布订阅。
缺点:
- 收到物理内存的限制。
- 主机宕机后,如果之前存在部分数据没有同步到从机上,从机变为主机时,会引起数据不一致。
为什么不使用 map 做缓存
缓存分为本地缓存和分布式缓存。
本地缓存,如 Java 的 map,特点是轻量、快速,生命周期随着 JVM 的销毁而结束。多实例情况下,每个实例保存一份缓存,不具有一致性。
分布式缓存,如 Redis,多实例情况下,实例共用一份缓存,缓存具有一致性。缺点是,需要保存 Redis 的高可用性,实现结构复杂。
选择 Redis:
- Redis 可以存储几十 G 数据,Java 的 map 大小受限于 JVM 大小,一般几个 G。
- Redis 缓存可以持久化,Java 只能存在内存中,重启程序就消失。
- Redis 可以实现分布式缓存,Java 的 map 不可以。
- Redis 可以处理每秒百万级的并发,Java 的 map 只是一个普通的对象。
- Redis 有过期机制,Java 的 map 没有。
Redis 数据结构
Redis 数据结构和应用场景
string-SDS 简单动态字符串-可保存二进制数据
应用场景:需要计数 (访问、点赞、转发数量) 或 k-v 存储情况
常见指令:set get incr decr expire setnx setex
setnx 命令 —— 申请特殊的 Key,最后删除 Key —— 出错,Key 无法删除
setnx 命令+Key 过期时间 —— 长,则效率低;短,则将后面的进程申请的锁过期了
setnx 命令+Key 过期时间+随机 UUID —— 释放时判断是否是当前锁对应的 UUID 是否相同,缺乏原子性
Lua 脚本完成上述功能
list-双向链表
应用场景:发布与订阅、消息队列、慢查询
常见指令:rpush rpop lpush lpop lrange
发布和订阅 (pub/sub)-但是如果接受者下线,数据会丢失,此时需要 RabbitMQ 等消息队列。
使用 list 进行异步队列-lpop、rpush,没有消息时可以 sleep 等待,或者使用 blpop 进行阻塞等待。
hash-类似 hashmap 的映射
应用场景:系统中对象数据的存储
常见指令:hset hget hkeys
set-类似 hashset
应用场景:存放不重复数据
常见指令:sadd spop
sortedsort-权重参数 score
应用场景:直播间在线用户列表、礼物排行榜
常见指令:zadd zscore
bitmap
应用场景:状态信息并进一步分析的场景 (签到、登录、点赞)
常见指令:含签到登录点赞的每日任务、统计用户当月登录情况
HyperLogLogs
应用场景:基数统计,省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时 UV、在线用户数,共同好友数等。给出输入流中不重复的个数,可能存在非常小的误差,可忽略不计
geospatial
应用场景:地理位置
Redis 数据结构的底层实现
string-SDS 简单动态字符串-使用数组存储+len 字段约束长度-可以存储任意信息
三种编码格式
OBJ_ENCODING_INT (整数)
OBJ_ENCODING_EMBSTR (小于 40 字节)
OBJ_ENCODING_RAW (大于 39 字节)
list-双向无环链表
OBJ_ENCODING_QUICKLIST
hash
OBJ_ENCODING_LISTPACK-紧凑列表实现的哈希-每个元素大小小于 64 字节且元素数小于 512 个
OBJ_ENCODING_HT-字典实现的哈希表(某个元素大小大于 64 字节或元素数大于 512 个)
set
OBJ_ENCODING_INTSET-整数集合实现-每个元素都是整数且元素数不超过 512 个
OBJ_ENCODING_HT-字典实现
zset
OBJ_ENCODING_LISTPACK-紧凑列表-每个元素大小小于 64 字节且元素数小于 128 个
OBJ_ENCODING_SKIPLIST-跳表和字典实现
Redis 跳表实现 zset
数据小的时候,采用 ziplist;数据大的时候,采用 skiplist。
跳表属性
跳表的查询时间复杂度为 O (logn),空间复杂度为 O (n),n+n/2+n/4+n/8+…=2n

跳表是怎么插入的?
如果不加维护,只插入到第 0 层,那么时间长就会退化成单向链表。因此需要进行维护,随机在 0~m 层中增加索引。
方法为,从 1 开始遍历,若 random<0.5 则在该层增加索引,否则停止增加索引。获取后,按照类似查询的步骤在每层插入索引。
typedef struct zskiplistNode {
// 成员对象
sds ele;
// 分值
double score;
// 后退指针属性,只有第 0 层有效,逆序遍历时使用
struct zskiplistNode *backward;
// 层
struct zskiplistLevel { // 存储每一层的后面节点数据
struct zskiplistNode *forward; // 每一层的前进指针
unsigned int span;// 节点跨度,隔了多少个节点,1-2 是隔了一个
} level[];
} zskiplistNode;
为什么不使用平衡树/红黑树
区间范围查找更方便,可以范围遍历。
跳表层数选择
random 函数,初始为 0 层。通过循环确定层数,若 random 值小于 0.5 且层数不大于最高层数,则层数+1,继续循环;反之,跳出循环。
跳表这种高效的数据结构,值得每一个程序员掌握 - 知乎
Redis 跳表 VS 平衡树 VS 哈希表
- 跳表与平衡树有序,哈希表无序,范围查找。
- 范围查找时,跳表简单,平衡树需要中序遍历,复杂。
- 插入和删除节点,平衡树需要子树调整,跳表只需修改相邻节点指针。
- 内存占用上,跳表每个节点包含的指针数目平均为 1/(1-p)
- 算法实现难度上,跳表更简单。
Redis 持久化
Redis 缓存扩缩容
如果 Redis 被当做缓存使用,使用 “一致性哈希” 实现动态扩容缩容。
“一致性哈希算法” 将整个哈希值空间映射成一个虚拟的圆环,整个哈希空间的取值范围为 0~23^2-1。整个空间按顺时针方向组织,头尾相连。N 个服务器分配在 N 个节点上。
- 当有数据过来时,会计算数据的哈希值,并按照顺时针顺序找到最近的服务器,分配到该服务器上。
- 当需要增加一台服务器时,例如在 A 和 C 服务器之间增加一台 B 服务器。那么只会影响到 A 到 B 哈希范围的数据会被存放到 B 服务器上。
- 当需要减少一台服务器时,例如将 B 从 A 和 C 服务器之间移除,那么 A 到 B 哈希范围的数据会被存放到 C 服务器上。
“一致性哈希算法” 相较于 “求余哈希算法” 在扩缩容时,需要移动的数据量更少。
当然,“一致性哈希算法” 可能存在数据不平衡的问题。针对这种情况,一致性哈希算法引入了虚拟节点机制。即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以先确定每个物理节点关联的虚拟节点数量,然后在 ip 或者主机名后面增加编号。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点。
Redis 持久化数据怎么做扩容
如果 Redis 被当做一个持久化存储使用,必须使用固定的 keys-to-nodes 映射关系,节点的数量一旦确定不能变化。否则的话 (即 Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有 Redis 集群可以做到这样。
Redis 事务
Redis 事务三大特性
单独的隔离操作:Redis 中所有事物串行执行,因此不会被其他事务所打扰。
没有隔离级别概念:只有事务提交时,命令才会执行,数据才会被修改;事务未提交时,数据不会被修改。
不保证原子性:一条命令执行失败后,后面的命令继续执行,没有回滚。
Redis 事务的三个阶段
- multi 开启事务
- 大量指令入队
- exec 执行事务块内命令,截止此处一个事务已经结束。
- discard 取消事务
- watch 监视一个或多个 key,如果事务执行前 key 被改动,事务将打断。unwatch 取消监视。
Redis 事务相关命令
Redis 事务功能是通过 MULTI 、 EXEC 、 DISCARD 和 WATCH 四个原语实现的。
WATCH命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到 EXEC 命令。MULTI命令用于开启一个事务,它总是返回 OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。EXEC: 执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。- 通过调用
DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。 UNWATCH命令可以取消 watch 对所有 key 的监控。
Redis 事务错误处理
事务指令错误,执行队列取消;事务执行中出错,出错指令不执行,其他正常执行。
Redis 的锁机制
Redis 的锁机制是乐观锁,实现方式是 CAS 机制。
缓存问题
缓存和数据库更新一致性
先更新缓存再更新数据库。
数据库失败,缓存为脏数据。应该以数据库数据为主,即先更新数据库。
先更新数据库再更新缓存。
A 和 B 线程同时写,网络原因,A 更数据库,B 更数据库,B 更缓存,A 更缓存,脏数据。写操作较多时,缓存频繁更新,性能低。如果还需要计算再更新缓存,性能更低。
先删除缓存再更新数据库。
A 写 B 读,A 删除缓存,B 读取缓存,B 读取数据库,A 更新数据库,B 旧值写入缓存,脏数据。数据库读写分离时,由于读比写快,读数据库未主从同步,从而查到旧值。
先更新数据库再删除缓存。
缓存失效,A 查到旧值,B 更新数据库,B 删除缓存,A 更缓存,脏数据。基本不发生,因为读比写快,B 删除缓存应该在 A 更缓存后。
先删除缓存再更新数据库解决
方法一:延迟双删策略。
先删除缓存,再更新数据库,休眠 x 秒,再删除缓存。休眠时间,读数据业务耗时基础上加上几百毫秒,读写分离时再修改为主从同步延时。进一步,第二次删除使用异步删除。
方法二:更新与读取操作进行异步串行化
略,查看 PDF 了解。
缓存删除失败的解决
方案一:缓存删除失败后,将其加入消息队列中;业务代码自己消费消息,获得需要删除的 key,然后删除操作。
缺点:业务代码中存在大量侵入。
方案二:更新数据库数据,订阅数据库 binlog 日志并提取需要的 key,非业务代码获得该信息来删除缓存。删除失败则将其加入消费队列,再消费信息,删除缓存。
删除缓存会导致的问题
删除缓存会导致缓存击穿问题。
缓存预热、缓存降级
缓存预热:提前将数据放到缓存中。
- 数据量不大,启动时加入缓存。
- 数据流大,定时脚本更新缓存。
- 数据流太大,优先保证热点数据加入缓存。
缓存降级:缓存失效或服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
Redis 主从模式
Redis Sentinel (哨兵)
Redis Sentinel 是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel 集群和 Redis 数据集群。
其中 Redis Sentinel 集群是由若干 Sentinel 节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel 的节点数量要满足 2n+1(n>=1)的奇数个。
优点:
- Redis Sentinel 集群部署简单;
- 能够解决 Redis 主从模式下的高可用切换问题;
- 很方便实现 Redis 数据节点的线形扩展,轻松突破 Redis 自身单线程瓶颈,可极大满足 Redis 大容量或高性能的业务需求;
- 可以实现一套 Sentinel 监控一组 Redis 数据节点或多组数据节点。
缺点:
- 部署相对 Redis 主从模式要复杂一些,原理理解更繁琐;
- 资源浪费,Redis 数据节点中 slave 节点作为备份节点不提供服务;
- Redis Sentinel 主要是针对 Redis 数据节点中的主节点的高可用切换,对 Redis 的数据节点做失败判定分为主观下线和客观下线两种,对于 Redis 的从节点有对节点做主观下线操作,并不执行故障转移。
- 不能解决读写分离问题,实现起来相对复杂。
Redis Cluster
Redis Cluster 是社区版推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。
Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0 ~ 16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
优点:
- 无中心架构;
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布;
- 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除;
- 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升;
- 降低运维成本,提高系统的扩展性和可用性。
缺点:
- Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅 JedisCluster 相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。
- 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
- 数据通过异步复制,不保证数据的强一致性。
- 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
- Slave 在集群中充当“冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 Slave 资源的利用率。
- Key 批量操作限制,如使用 mset、mget 目前只支持具有相同 slot 值的 Key 执行批量操作。对于映射为不同 slot 值的 Key 由于 Keys 不支持跨 slot 查询,所以执行 mset、mget、sunion 等操作支持不友好。
- Key 事务操作支持有限,只支持多 key 在同一节点上的事务操作,当多个 Key 分布于不同的节点上时无法使用事务功能。
- Key 作为数据分区的最小粒度,不能将一个很大的键值对象如 hash、list 等映射到不同的节点。
- 不支持多数据库空间,单机下的 Redis 可以支持到 16 个数据库,集群模式下只能使用 1 个数据库空间,即 db 0。
- 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
- 避免产生 hot-key,导致主库节点成为系统的短板。
- 避免产生 big-key,导致网卡撑爆、慢查询等。
- 重试时间应该大于 cluster-node-time 时间。
- Redis Cluster 不建议使用 pipeline 和 multi-keys 操作,减少 max redirect 产生的场景。
Redis 自研
Redis 高可用方案具体怎么实施?
使用官方推荐的哨兵 (sentinel) 机制就能实现,当主节点出现故障时,由 Sentinel 自动完成故障发现和转移,并通知应用方,实现高可用性。它有四个主要功能:
- 集群监控,负责监控 Redis master 和 slave 进程是否正常工作。
- 消息通知,如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移,如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心,如果故障转移发生了,通知 client 客户端新的 master 地址。
Redis 主从复制的原理
由于主从延迟导致读取到过期数据怎么处理?
- 通过 scan 命令扫库:当 Redis 中的 key 被 scan 的时候,相当于访问了该 key,同样也会做过期检测,充分发挥 Redis 惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。
- Redis 加入了一个新特性来解决主从不一致导致读取到过期数据问题,增加了 key 是否过期以及对主从库的判断,如果 key 已过期,当前访问的 master 则返回 null;当前访问的是从库,且执行的是只读命令也返回 null。
主从复制的过程中如果因为网络原因停止复制了会怎么样?
如果出现网络故障断开连接了,会自动重连的,从 Redis 2.8 开始,就支持主从复制的断点续传,可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
master 如果发现有多个 slave node 都来重新连接,仅仅会启动一个 rdb save 操作,用一份数据服务所有 slave node。
master node 会在内存中创建一个 backlog,master 和 slave 都会保存一个 replica offset,还有一个 master id,offset 就是保存在 backlog 中的。如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次的 replica offset 开始继续复制。
但是如果没有找到对应的 offset,那么就会执行一次 resynchronization 全量复制。
Redis 主从架构数据会丢失吗,为什么?
有两种数据丢失的情况:
- 异步复制导致的数据丢失:因为 master → slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这些部分数据就丢失了。
- 脑裂导致的数据丢失:某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着,此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master,也就是所谓的脑裂。此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续写向旧 master 的数据可能也丢失了。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。
如何解决主从架构数据丢失的问题?
数据丢失的问题是不可避免的,但是我们可以尽量减少。
在 Redis 的配置文件里设置参数
min-slaves-to-write 1
min-slaves-max-lag 10
min-slaves-to-write 默认情况下是 0,min-slaves-max-lag 默认情况下是 10。
上面的配置的意思是要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
减小 min-slaves-max-lag 参数的值,这样就可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往 master 中写入数据。
那么对于 client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入 master 来保证数据不丢失;也可以将数据写入 kafka 消息队列,隔一段时间去消费 kafka 中的数据。
Redis 哨兵是怎么工作的?
- 每个 Sentinel 以每秒钟一次的频率向它所知的 Master,Slave 以及其他 Sentinel 实例发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值,则这个实例会被当前 Sentinel 标记为主观下线。
- 如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 的确进入了主观下线状态。
- 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 Master 的确进入了主观下线状态,则 Master 会被标记为客观下线。
- 当 Master 被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 Master,Slave 发送 INFO 命令 )。
- 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
- sentinel 节点会与其他 sentinel 节点进行“沟通”,投票选举一个 sentinel 节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。
故障转移时会从剩下的 slave 选举一个新的 master,被选举为 master 的标准是什么?
如果一个 master 被认为 odown 了,而且 majority 哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息。
- 跟 master 断开连接的时长。如果一个 slave 跟 master 断开连接已经超过了 down-after-milliseconds 的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master.
( down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
- slave 优先级。按照 slave 优先级进行排序,slave priority 越低,优先级就越高
- 复制 offset。如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高
- run id 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。
同步配置的时候其他哨兵根据什么更新自己的配置呢?
执行切换的那个哨兵,会从要切换到的新 master(salve→master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行切换,此时会重新获取一个新的 configuration epoch 作为新的 version 号。
这个 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的,其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。
Redis cluster 中是如何实现数据分布的?这种方式有什么优点?
Redis cluster 有固定的 16384 个 hash slot(哈希槽),对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。
Redis cluster 中每个 master 都会持有部分 slot(槽),比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。
hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。每次增加或减少 master 节点都是对 16384 取模,而不是根据 master 数量,这样原本在老的 master 上的数据不会因 master 的新增或减少而找不到。并且增加或减少 master 时 Redis cluster 移动 hash slot 的成本是非常低的。
Redis cluster 节点间通信是什么机制?
Redis cluster 节点间采取 gossip 协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更之后 U 不断地 i 将元数据发送给其他节点让其他节点进行数据变更。
节点互相之间不断通信,保持整个集群所有节点的数据是完整的。主要交换故障信息、节点的增加和移除、hash slot 信息等。
这种机制的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后。
Redis 集群
集群概念、作用、不足
集群是多个服务器,这些服务器共同用于存储 Redis 缓存。
集群解决了 Redis 内存不足的问题。
集群上不支持多键操作,Redis 事务不支持多键操作。
集群的原理
将需要存储的数据分成 N 份,每台服务器存储 1/N 份数据。
具体来说,需要涉及到集群的插槽概念。
集群中包含着 16384 (2^14) 个插槽,集群使用 CRC16 (key) 来计算 key 属于哪个插槽。所有的节点平分这些插槽。
集群高可用的保证
Redis 集群来完成扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储,将整个数据分布存储在多个节点中。
Redis 哨兵机制来完成高可用,在 master 宕机时会自动将 slave 提升为 master,继续提供服务。如果主机恢复后,主机变成从机。
集群中命令问题
节点 A 中传入一个非节点 A 的数据,会提示错误,并将其转移到合适的节点中。
一次更新多个节点上的 key-value,可能会有问题,Redis 事务不支持多建操作。
不在一个 slot 下不能使用 mget 和 mset。
Redis 分布式
什么是分布式锁?为什么用分布式锁?
分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。
思路是:在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西”,可以是 Redis、Zookeeper,也可以是数据库。
一般来说,分布式锁需要满足的特性有这么几点:
1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。
常见的分布式锁有哪些解决方案?
实现分布式锁目前有三种流行方案,即基于关系型数据库、Redis、ZooKeeper 的方案
1、基于关系型数据库,如 MySQL 基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。
缺点:
- 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
- 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
2、基于 Redis 实现
优点:
Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。
缺点:
- Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮;
- key 的过期时间设置多少不明确,只能根据实际情况调整;
- 需要自己不断去尝试获取锁,比较消耗性能。
3、基于 zookeeper
优点:
zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。
缺点:
在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风险。
Redis 实现分布式锁
- setnx 加锁 del 解锁 expire key 设置过期时间
- 添加 UUID 标注是自己的锁
- 超时时间不可太长
- 不可重入解决,state 计数。
了解 RedLock 吗?
Redlock 是一种算法,Redlock 也就是 Redis Distributed Lock,可用实现多节点 Redis 的分布式锁。
RedLock 官方推荐,Redisson 完成了对 Redlock 算法封装。
此种方式具有以下特性:
- 互斥访问:即永远只有一个 client 能拿到锁
- 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。
- 容错性:只要大部分 Redis 节点存活(一半以上),就可以正常提供服务
RedLock 的原理
假设有 5 个完全独立的 Redis 主服务器
- 获取当前时间戳
- client 尝试按照顺序使用相同的 key, value 获取所有 Redis 服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的 Redis 服务。并且试着获取下一个 Redis 实例。
比如:TTL 为 5s, 设置获取锁最多用 1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁 - client 通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于 TTL 时间并且至少有 3 个 Redis 实例成功获取锁,才算真正的获取锁成功
- 如果成功获取锁,则锁的真正有效时间是 TTL 减去第三步的时间差的时间;比如:TTL 是 5s, 获取所有锁用了 2s, 则真正锁有效时间为 3s (其实应该再减去时钟漂移);
- 如果客户端由于某些原因获取锁失败,便会开始解锁所有 Redis 实例;因为可能已经获取了小于 3 个锁,必须释放,否则影响其他 client 获取锁
Redis 业务方面
有什么办法发挥多核 CPU 的性能
我们可以通过在单机开多个 Redis 实例来完善!只要客户端分清哪些 key 放在哪个 Redis 进程上就可以了。redis-cluster 可以帮你做的更好。
警告:这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的 Redis Server 运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!
例如 Redis 进行持久化的时候会以子进程或者子线程的方式执行(具体是子线程还是子进程待读者深入研究)
如何解决 Redis 的并发竞争 Key 问题
这个问题大致就是,同时有多个子系统去 Set 一个 Key。这个时候要注意什么呢?大家基本都是推荐用 Redis 事务机制。
但是我并不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。你一个事务中有涉及到多个 Key 操作的时候,这多个 Key 不一定都存储在同一个 redis-server 上。因此,Redis 的事务机制,十分鸡肋。
Redis 并发竞争 key 不需要顺序
准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。
Redis 并发竞争 key 需要顺序
如果仅仅是希望去更新 key 的话,写入数据库时保留修改的时间戳。
如果当前更新请求的时间戳小于数据库中最后依次修改的时间戳,则不更新;反之,更新。
如果不仅要更新还要计算的话,怎么处理???
假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来
使用 keys 指令可以扫出指定模式的 key 列表。
如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问
这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
不过,增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。
使用过 Redis 做异步队列么,你是怎么用的?如果对方追问可不可以不用 sleep 呢?如果对方接着追问能不能生产一次消费多次呢?如果对方继续追问 pub/sub 有什么缺点?如果对方究极 TM 追问 Redis 如何实现延时队列?
一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。
list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。
使用 pub/sub 主题订阅者模式,可以实现 1: N 的消息队列。
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RocketMQ 等。
使用 sortedset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
Pipeline 有什么好处,为什么要用 pipeline?
可以将多次 IO 往返的时间缩减为一次,前提是 pipeline 执行的指令之间没有因果相关性。使用 redis-benchmark 进行压测的时候可以发现影响 redis 的 QPS 峰值的一个重要因素是 pipeline 批次指令的数目。