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;

|500

为什么不使用平衡树/红黑树
区间范围查找更方便,可以范围遍历。
跳表层数选择
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”的哈希值,于是形成六个虚拟节点。

一致性 Hash 算法详解 - 知乎

Redis 持久化数据怎么做扩容

如果 Redis 被当做一个持久化存储使用,必须使用固定的 keys-to-nodes 映射关系,节点的数量一旦确定不能变化。否则的话 (即 Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有 Redis 集群可以做到这样。

Redis 事务

Redis 事务三大特性

单独的隔离操作:Redis 中所有事物串行执行,因此不会被其他事务所打扰。
没有隔离级别概念:只有事务提交时,命令才会执行,数据才会被修改;事务未提交时,数据不会被修改。
不保证原子性:一条命令执行失败后,后面的命令继续执行,没有回滚。

Redis 事务的三个阶段

  1. multi 开启事务
  2. 大量指令入队
  3. exec 执行事务块内命令,截止此处一个事务已经结束。
  4. discard 取消事务
  5. watch 监视一个或多个 key,如果事务执行前 key 被改动,事务将打断。unwatch 取消监视。

Redis 事务相关命令

Redis 事务功能是通过 MULTIEXECDISCARDWATCH 四个原语实现的。

  • 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 主从复制的原理

由于主从延迟导致读取到过期数据怎么处理?

  1. 通过 scan 命令扫库:当 Redis 中的 key 被 scan 的时候,相当于访问了该 key,同样也会做过期检测,充分发挥 Redis 惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。
  2. 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 主从架构数据会丢失吗,为什么?

有两种数据丢失的情况:

  1. 异步复制导致的数据丢失:因为 master slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这些部分数据就丢失了。
  2. 脑裂导致的数据丢失:某个 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 哨兵是怎么工作的?

  1. 每个 Sentinel 以每秒钟一次的频率向它所知的 Master,Slave 以及其他 Sentinel 实例发送一个 PING 命令。
  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值,则这个实例会被当前 Sentinel 标记为主观下线。
  3. 如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 的确进入了主观下线状态。
  4. 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 Master 的确进入了主观下线状态,则 Master 会被标记为客观下线。
  5. 当 Master 被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 Master,Slave 发送 INFO 命令 )。
  6. 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
  7. 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(salvemaster)那里得到一个 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 实现分布式锁

  1. setnx 加锁 del 解锁 expire key 设置过期时间
  2. 添加 UUID 标注是自己的锁
  3. 超时时间不可太长
  4. 不可重入解决,state 计数。

了解 RedLock 吗?

Redlock 是一种算法,Redlock 也就是 Redis Distributed Lock,可用实现多节点 Redis 的分布式锁。
RedLock 官方推荐,Redisson 完成了对 Redlock 算法封装。

此种方式具有以下特性:

  • 互斥访问:即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。
  • 容错性:只要大部分 Redis 节点存活(一半以上),就可以正常提供服务

RedLock 的原理

假设有 5 个完全独立的 Redis 主服务器

  1. 获取当前时间戳
  2. client 尝试按照顺序使用相同的 key, value 获取所有 Redis 服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的 Redis 服务。并且试着获取下一个 Redis 实例。
    比如:TTL 为 5s, 设置获取锁最多用 1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
  3. client 通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于 TTL 时间并且至少有 3 个 Redis 实例成功获取锁,才算真正的获取锁成功
  4. 如果成功获取锁,则锁的真正有效时间是 TTL 减去第三步的时间差的时间;比如:TTL 是 5s, 获取所有锁用了 2s, 则真正锁有效时间为 3s (其实应该再减去时钟漂移);
  5. 如果客户端由于某些原因获取锁失败,便会开始解锁所有 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 批次指令的数目。

补充