Redis

107 阅读34分钟

什么是 Redis?

RedisREmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。

为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。

Redis 为什么这么快?

Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:

  1. Redis 基于内存,内存的访问速度比磁盘快很多
  2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
  3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
  4. Redis 通信协议实现简单且解析高效。

[Redis的优点?]

1、访问速度更快

传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。

2、高并发

一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。

QPS(Query Per Second):服务器每秒可以执行的查询次数;

用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发

3、功能全面

Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!

  • 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。

  • 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。

  • 消息队列:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。

  • 延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。

  • 分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据(持久化),所有的服务器都可以访问。

  • 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。

String 还是 Hash 存储对象数据更好呢?

  • String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
  • String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。

在绝大部分情况,我们建议使用 String 来存储对象数据即可!

Redis 的有序集合Zset底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?

跳表是一种基于链表的数据结构,用于实现高效的搜索、插入和删除操作。它通过在不同层级的链表上跳跃,实现了类似平衡树的时间复杂度O(log n),默认最多32层。跳表的基本结构如下:

  1. 底层链表

    • 最底层是一个有序的链表,包含所有的元素。
  2. 多级索引

    • 在底层链表之上,跳表构建了多个级别的索引链表。
    • 每一级链表的节点是从下一层链表中选出的,通常是随机选取的节点。
    • 较高层级的链表节点数逐渐减少,最高层的链表节点最少。
  3. 头节点和尾节点

    • 每一层都有一个特殊的头节点,用于标记链表的起始。
    • 通常还有一个尾节点用于标记链表的结束。
  • 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n) 。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点(只需指针变化)。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多
  • 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
  • B+树 vs 跳表:B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。

image.png

Redis持久化机制详解

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file, AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

RDB

Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

RDB 创建快照时会阻塞主线程吗?

Redis 提供了两个命令来生成 RDB 快照文件:

  • save : 同步保存操作,会阻塞 Redis 主线程;
  • bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。

AOF

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令(写命令),Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof

AOF 工作基本流程是怎样的?

AOF 持久化功能的实现可以简单分为 5 步:

  1. 命令追加(append) :所有的写命令会追加到 AOF 缓冲区中。
  2. 文件写入(write) :将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘
  3. 文件同步(fsync) :AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync阻塞直到写入磁盘完成后返回,保证了数据持久化。
  4. 文件重写(rewrite) :随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的
  5. 重启加载(load) :当 Redis 重启时,可以加载 AOF 文件进行数据恢复

AOF 持久化方式有哪些?

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync策略),它们分别是:

  1. appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write + fsync)。
  2. appendfsync everysec:主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)
  3. appendfsync no:主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsyncfsync 的时机由操作系统决定)。

可以看出:这 3 种持久化方式的主要区别在于 fsync 同步 AOF 文件的时机(刷盘)

为什么AOF是在执行完命令之后记录日志呢?

  • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
  • 在命令执行完之后再记录,不会阻塞当前的命令执行

这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):

  • 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
  • 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。

AOF 重写了解吗?

AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小
由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行
AOF 文件重写期间,Redis 还会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。

AOF 重写期间的增量数据如何处理一直是个问题,在过去写期间的增量数据需要在内存中保留,写结束后再把这部分增量数据写入新的 AOF 文件中以保证数据完整性。可以看出来 AOF 写会额外消耗内存和磁盘 IO,这也是 Redis AOF 写的痛点。采用 base(全量数据)+inc(增量数据)独立文件存储的方式,彻底解决内存和 IO 资源的浪费,同时也支持对历史 AOF 文件的保存管理,结合 AOF 文件中的时间信息还可以实现 PITR 按时间点恢复(阿里云企业版 Tair 已支持),这进一步增强了 Redis 的数据可靠性,满足用户数据回档等需求。

采用 base(全量数据)+inc(增量数据)独立文件存储的方式,彻底解决内存和 IO 资源的浪费,同时也支持对历史 AOF 文件的保存管理,结合 AOF 文件中的时间信息还可以实现 PITR 按时间点恢复(阿里云企业版 Tair 已支持),这进一步增强了 Redis 的数据可靠性,满足用户数据回档等需求。

RDB 比 AOF 优秀的地方(更小更快)

  • RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次
  • 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快

AOF 比 RDB 优秀的地方(简单安全)

  • RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比较繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。
  • RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题
  • AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。

综上

  • Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。
  • 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
  • 如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。

Redis的线程

文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗

Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。 那 Redis6.0 之前为什么不使用多线程? 我觉得主要原因有 3 点:

  • 单线程编程容易并且更容易维护
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

Redis6.0 之后为何引入了多线程?

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

Redis 后台线程了解吗?

我们虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作:AOF和RDB

Redis 过期 key 删除策略了解么?

如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?

常用的过期数据的删除策略就下面这几种(重要!自己造缓存轮子的时候需要格外考虑的东西):

  1. 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
  3. 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
  4. 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。

Redis 采用的那种删除策略呢?

Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。

Redis 内存淘汰策略了解么?

Redis 提供了 6 种内存淘汰策略(内存满了,淘汰一些):

  1. volatile-lru(least recently used) :从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  4. allkeys-lru(least recently used) :从(所有数据)数据集(server.db[i].dict)中移除最近最少使用的数据淘汰。
  5. allkeys-random:从(所有数据)数据集(server.db[i].dict)中任意选择数据淘汰。
  6. no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。(不淘汰,满了直接报错

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used) :从已设置过期时间的数据集(server.db[i].expires)中挑选最少使用的数据淘汰。
  2. allkeys-lfu(least frequently used) :从(所有数据)数据集(server.db[i].dict)中移除最少使用的数据淘汰。

Redis 事务支持原子性吗?

Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:1. 原子性2. 隔离性3. 持久性4. 一致性

  1. 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;(InoDB中使用undo log来回滚日志保证)
  2. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;(通过MVCC或锁保证,MVCC多版本并发控制,读数据时用快照保存,即使过程中有其他事务修改,本事务也是根据快照上的内容进行的,不会受影响)
  3. 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。(InoDB中使用redo log来备份保证持久性)
  4. 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
    先undolog在redolog再binlog
    Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性

Redis 事务支持持久性吗?

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file, AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

AOF 持久化的fsync策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。

因此,Redis 事务的持久性也是没办法保证的。

Redis事务具备隔离和一致性。

事务并发问题有哪些

脏读:事务A读到了事务B修改还未提交的数据
幻读,也叫虚读:事务A两次读取相同条件的数据,两次查询到的数据“条数”不一致,是由于事务B再这两次查询中插入或删除了数据造成的
不可重复读:事务A两次读取相同条件的数据,结果读取出不同的结果,是由于事务B再这两次查询中修改了数据造成的
第一类丢失更新:也叫回滚丢失,事务A和事务B更新同一条数据,事务B先完成了修改,此时事务A异常终止,回滚后造成事务B的更新也丢失了
第二类丢失更新:也叫覆盖丢失,事务A和事务B更新同一条数据,事务B先完成了修改,事务A再次修改并提交,把事务B提交的数据给覆盖了

事务隔离级别:

读未提交:事务读不阻塞其他事务的读和写,事务写阻塞其他事务的写但不阻塞读,能解决第一类丢失更新的问题(阻塞1个
读已提交:事务读不阻塞其他事务读和写,事务写会阻塞其他事务的读和写,能解决第一类丢失更新脏读的问题 。(阻塞2个
可重复读:事务读会阻塞其他事务的写但不阻塞读,事务写会阻塞其他事务读和写,能解决第一类丢失更新,脏读,不可重复读,第二类丢失更新问题。(阻塞3个,解决除幻读外所有问题
串行化:使用表级锁,让事务一个一个的按顺序执行,能解决以上所有并发安全问题。

分布式事务的实现方式

常见的分布式事务实现方式包括两阶段提交(2PC)、三阶段提交(3PC)、基于消息队列的最终一致性等。

1. 两阶段提交(2PC)

两阶段提交是实现分布式事务的一种经典方法,分为准备阶段(Prepare Phase)和提交阶段(Commit Phase):

  • 准备阶段:协调者向所有参与节点发送准备请求,所有节点执行预操作并记录日志,但不提交。如果所有节点都返回成功,进入提交阶段,否则进入回滚阶段。
  • 提交阶段:如果准备阶段所有节点都成功,协调者向所有节点发送提交请求,所有节点正式提交操作。如果有节点失败,协调者向所有节点发送回滚请求,所有节点回滚操作。

优点:简单易实现。 缺点:如果协调者失败,可能会导致阻塞。

2. 基于消息队列的最终一致性

使用消息队列可以实现分布式系统的最终一致性。具体步骤如下:

  • 事务消息:在执行数据库操作时,先发送一条半事务消息到消息队列,数据库操作和发送消息在同一个本地事务中完成。
  • 确认消息:数据库操作成功后,确认消息发送成功,消息队列将消息发送给消费者。
  • 处理消息:消费者收到消息后,执行缓存操作。

使用消息队列实现数据库和缓存的一致性

通过消息队列,可以实现数据库和缓存操作的一致性。具体步骤如下:

  1. 数据库操作:在事务中执行数据库操作,并发送半事务消息到消息队列。
  2. 消息队列:消息队列暂存半事务消息,等待数据库事务提交。
  3. 提交事务:数据库事务提交后,确认消息发送成功,消息队列将消息发送给消费者。
  4. 更新缓存:消费者收到消息后,执行缓存操作,确保数据库和缓存一致。

保证一致性的思路

  • 确保事务消息和数据库操作在同一个本地事务中完成:通过使用分布式事务管理器或消息中间件(如RocketMQ、Kafka等)来保证消息和数据库操作的原子性。
  • 幂等性:确保缓存操作是幂等的,即相同操作执行多次不会产生副作用。
  • 重试机制:在操作失败时,提供重试机制,确保操作最终成功。

脏读问题

使用消息队列可以极大减少脏读的可能性,但仍然需要注意以下情况:

  • 操作延迟:消息队列的延迟可能导致短暂的不一致,但通过快速重试和重发机制可以尽量减少这种情况。
  • 幂等性:确保操作是幂等的,可以防止重复操作带来的不一致。

三阶段提交(3PC)

三阶段提交(Three-phase commit protocol, 3PC)是对两阶段提交(2PC)的改进,它通过引入一个中间阶段来减少协调者和参与者之间的阻塞时间,增强了系统的可靠性。3PC 分为三个阶段:

  1. CanCommit 阶段(投票阶段)

    • 协调者向所有参与者发送 CanCommit 请求,询问是否可以提交事务。
    • 参与者收到请求后,做出初步判断(如检查资源是否可用),并返回 Yes 或 No。
  2. PreCommit 阶段(预提交阶段)

    • 如果所有参与者都返回 Yes,协调者进入 PreCommit 阶段,向所有参与者发送 PreCommit 请求。
    • 参与者收到 PreCommit 请求后,执行事务操作并记录日志,但不提交事务,仅做预提交。
    • 参与者返回 ACK 确认预提交完成。
  3. DoCommit 阶段(提交阶段)

    • 如果协调者在预提交阶段收到所有参与者的 ACK,则进入 DoCommit 阶段,向所有参与者发送 DoCommit 请求,要求正式提交事务。
    • 参与者收到 DoCommit 请求后,正式提交事务并释放资源。
    • 如果在任何阶段遇到失败,协调者会发送 Abort 请求,要求所有参与者回滚事务。

3PC 和 2PC 的区别

  • 增加了预提交阶段:3PC 在两阶段提交的基础上增加了 PreCommit 阶段,减少了协调者和参与者之间的阻塞时间
  • 超时机制:3PC 引入了超时机制,如果在一定时间内没有收到参与者的响应,协调者可以决定回滚事务,避免长期阻塞。

Redis优化

1.使用批量操作减少网络传输
2.Lua脚本解决原子性问题
3.大量 key 集中过期问题(随机过期时间+惰性删除/延迟释放)
4.Bigkey(分割为小key)
5.hotkey占用CPU和网络(读写分离,集群)

Redis bigkey(大 Key)

什么是 bigkey?

简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:

bigkey 通常是由于下面这些原因产生的:

  • 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据
  • 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
  • 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。
如何处理 bigkey?

bigkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):

  • 分割 bigkey:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。(因为一个redis的key对应的value存hash可以存很多个值hmset
  • 手动清理:Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN 命令结合 DEL 命令来分批次删除。
  • 采用合适的数据结构:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
  • 开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程

Redis hotkey(热 Key)

什么是 hotkey?

如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key) 。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。

hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。

hotkey 有什么危害?

处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。

因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。

如何解决 hotkey?

hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):

  • 读写分离:主节点处理写请求,从节点处理读请求。
  • 请求合并: 将对同一个热点键的多个读请求合并为一个请求,减少对 Redis 的压力。
  • 使用 Redis Cluster集群:将热点数据分散存储在多个 Redis 节点上。
  • 二级缓存(本地缓存):hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。

使用 Redis 集群解决热点键问题

  1. 启用 Redis 集群模式: Redis 集群通过分片(sharding)将数据分布到多个节点。每个节点负责一部分数据的存储和处理。集群使用哈希槽(hash slots)来管理数据分布,Redis 集群总共有 16384 个哈希槽。
  2. 数据分片: Redis 集群通过对键进行哈希计算,将其映射到某个哈希槽。然后根据哈希槽将键值对分配到相应的节点上。这样,访问不同键的数据请求会被分布到不同的节点上,减少单个节点的负载。
  3. 选择合适的分片策略: 默认情况下,Redis 使用 CRC16 哈希函数对键进行哈希计算,将其分配到 16384 个哈希槽中的一个。你可以通过调整键的命名方式,来更好地控制键的分布。例如,可以使用特定前缀或标签来确保相关数据被分配到同一个哈希槽。
  4. 监控和重分片: 使用 Redis 提供的工具,如 redis-cliredis-trib.rb,监控各个节点的负载情况。如果某些节点的负载过高,可以通过重分片(resharding)将部分数据迁移到负载较低的节点上。
  5. 缓存分片(Client-Side Sharding) : 在客户端层面实现分片,通过客户端逻辑将不同的键请求分配到不同的 Redis 实例。这样,可以手动控制热点键的分布,避免单个实例的负载过高。

Redis每种存储结构说 4 个命令吧

1.String set key value 设置值
get key 取值
mset key value key value... 设置多个值
mget key key获取多个值
incr key 将key中的值自增1
decre key 将key中的值自减1
2.List
lpush key value value... 从最左边设置值
rpush key value value... 从最右边设置值
lrange key start stop 查询key中指定区间的元素
lpop key 移出并返回key中最左边的元素
rpop key 移出并返回key中最右边的元素
3.Set
sadd key value value 添加元素
smembers key 返回集合key中的所有元素
srem key member 删除集合key中member元素
scard key 查询集合key中的元素数量
4.ZSet
zadd key score value (score value)... 添加元素
zcard key 查询集合key中元素数量
zcount key min max 返回有序集合key中score 在min和max之间的元素
zrange key start stop 返回有序集合key中索引在start和stop之间的元素
5.Hash
hset key field value 添加元素
hget key field 获取key集合中field键对应的值
hmset key field value (field value)... 添加元素并批量添加子键值对
hmget key field field 获取key集合中所有的子键值对

redis主从集群和分片集群有什么区别,他们分别在什么情况下不可用?

Redis 主从集群(Master-Slave Replication)

架构
  • 主节点(Master) :处理所有的写操作和部分读操作。
  • 从节点(Slave) :复制主节点的数据,可以处理读操作。
  • 复制方式:从节点会定期从主节点复制数据,以确保数据的一致性。
特点
  • 读写分离:主节点负责写操作,从节点负责读操作,减轻主节点的负载。
  • 高可用性:如果主节点宕机,从节点可以提升为主节点(需要哨兵机制或手动干预)。
  • 数据一致性:从节点的数据是主节点数据的复制,存在一定的复制延迟。
不可用情况
  • 主节点宕机:如果没有哨兵机制或没有手动提升从节点为主节点,整个集群将无法处理写操作。
  • 网络分区:主从节点之间的网络分区可能导致数据不一致或服务不可用。
使用场景
  • 适用于读多写少的场景:例如缓存、会话管理等。
  • 高可用性需求:需要通过哨兵机制提升高可用性。

Redis 分片集群(Sharding)

架构
  • 分片(Shard) :将数据分布到多个节点,每个节点只保存一部分数据。
  • 节点:每个节点都是一个独立的 Redis 实例,负责存储和处理部分数据。
  • 数据分布:通过一致性哈希或其他分布算法将数据分布到不同的节点上。
特点
  • 水平扩展:通过增加节点可以水平扩展存储容量和处理能力。
  • 无单点故障:数据分布在多个节点上,某个节点的故障不会影响其他节点。
  • 复杂性:需要处理数据分布、路由和集群管理等复杂性。
不可用情况
  • 多个节点宕机:如果多个节点同时宕机,可能导致部分数据不可用。
  • 网络分区:节点之间的网络分区可能导致数据不可访问或操作失败。
使用场景
  • 适用于数据量大、写操作多的场景:例如大规模社交网络、实时分析等。
  • 水平扩展需求:需要通过增加节点扩展存储容量和处理能力。

Redis Cluster既是主从也是分片。

Redis Cluster 作为主从集群

  • 主从复制:每个主节点(Master)都有一个或多个从节点(Slave),从节点复制主节点的数据。这样,即使主节点故障,从节点可以被提升为主节点,确保数据可用性。
  • 高可用性:通过主从复制和故障转移机制,Redis Cluster实现了高可用性。当主节点故障时,Redis Cluster会自动提升从节点为新的主节点,继续提供服务。

Redis Cluster 作为分片集群

  • 数据分片:Redis Cluster将数据划分为16384个哈希槽,每个节点负责一部分哈希槽。通过分片机制,Redis Cluster可以水平扩展,将数据和负载分布到多个节点上,提升性能和容量。
  • 无中心化:在Redis Cluster中,每个节点都可以处理客户端请求,不存在单点故障问题。分片机制使得数据可以分布在多个节点上,每个节点只需处理自己负责的数据片段。