什么是 Redis?

Redis 本质上是一个 Key-Value 类型的内存数据库,整个数据库统统加载在内存中进行操作,定期通过异步操作把数据保存到硬盘。 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是目前性能最快的 Key-Value 数据库。Redis 支持丰富的数据结构:String、List、Set、Hash、SortedSet 等。因此 Redis 可以用来实现很多有用的功能,比如:缓存(核心)、分布式锁(set + lua 脚本)、排行榜(zset)、计数(incrby)、消息队列(stream)、地理位置(geo)、访客统计(hyperloglog)等。

Redis 和 Memcached 的比较

  1. 数据结构:Memcached 支持简单的 key-value 数据结构,而 Redis 支持丰富的数据结构:String、List、Set、Hash、SortedSet 等。

  2. 数据存储:Memcached 和 Redis 的数据都是全部在内存中。

  3. 持久化:Memcached 不支持持久化,Redis 支持将数据持久化到磁盘

  4. 灾难恢复:实例挂掉后,Memcached 数据不可恢复,Redis 可通过 RDB、AOF 恢复,但是还是会有数据丢失问题

  5. 过期键删除策略:Memcached 使用惰性删除,Redis 使用惰性删除+定期删除

  6. 内存驱逐策略:Memcached 主要为 LRU 算法,Redis 当前支持 8 种淘汰策略

Redis 常见数据类型有哪些

  • String:字符串,最基础的数据类型。

    • 定时缓存,比如短信验证码 5 分钟内有效
    • 把热点信息放到 Redis 中,Redis 作为缓存层,MySQL 做持久化层,降低 MySQL 的读写压力
    • Spring Session + Redis 实现 Session 共享
  • List:有序列表。

    • 消息队列:生产者使用  Lpush  命令从左边插入数据,多个消费者使用  BRpop  命令阻塞的获取列表尾部的数据。
    • 数据分页展示的应用,大大提高查询效率。
  • Hash:哈希对象。

    • 类似  Map  的一种结构,可以缓存一个对象(前提是这个对象没嵌套其他的对象)每次读写缓存的时候,可以操作对象的某个字段
    • 用的比较少。因为大多数对象结果比较复杂,一般我们把对象序列化成 Json,然后缓存到 Redis 字符串里。
  • Set:集合。

    • 在分布式场景下,利用集合的特性做去重,求交集、并集、差集
  • Sorted Set:有序集合,Set 的基础上加了个分数,根据分数排序。

    • 排行榜:按照时间、按照播放量、按照点赞数排序
    • 热搜榜:按照搜索热度排序
  • Bitmap:位图。可以看作一个 bit 为单位的数组,数组的每个单元只能存储 0 或者 1,数组的下标在 Bitmap 中叫做 offset 偏移量。可以用来实现 布隆过滤器(BloomFilter)

    • 只需要统计数据的二值状态,比如用户是否存在、 ip 是否是黑名单、以及签到打卡统计等场景就可以考虑使用 Bitmap。在统计海量数据的时候将大大减少内存占用。
  • HyperLogLog:供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计网站的 UV

  • Geospatial:可以用来保存地理位置,计算位置距离或者根据半径计算位置等

Redis 是单线程还是多线程?

  • Redis 4.0 之前,Redis 是完全单线程的
  • Redis 4.0 时,Redis 引入了多线程,但是额外的线程只是用于后台处理,例如删除对象,核心流程还是完全单线程的(接收命令、解析命令、执行命令、返回结果)
  • Redis 6.0 中,多线程主要用于网络 I/O 阶段,也就是接收命令和写回结果阶段,而在执行命令阶段,还是由单线程串行执行,因此无需考虑并发安全问题。

为什么 Redis 是单线程?

  • Redis 是完全基于内存操作的,Redis 的瓶颈往往在于机器内存的大小或者网络 I/O 的效率,而不在于 CPU。
  • 既然 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
  • 如果使用多线程的话,需要引入上下文切换、加锁等等,会带来额外的性能消耗。
  • 6.0 版本对核心流程引入了多线程,主要为了提升 Redis 在网络 I/O 上的效率。核心的命令执行阶段还是单线程的。

Redis 为什么使用单进程、单线程也很快

  1. 完全基于内存操作
  2. 使用单线程,避免上下文的切换和锁竞争,减少性能消耗
  3. 使用非阻塞的 IO 多路复用模型
  4. C 语言实现,Redis 对基础的数据结构做了大量的优化,性能极高

Redis 在项目中的使用场景

缓存(核心)、分布式锁(set + lua 脚本)、排行榜(zset)、计数(incrby)、消息队列(stream)、地理位置(geo)、访客统计(hyperloglog)等。

什么是缓存击穿、缓存穿透、缓存雪崩?如何解决?

缓存击穿

概念:单个 key 并发访问过高,当这个 key 过期时,所有请求直接打到 DB 上,打崩了 DB

解决方案:

  1. 设置热点数据永远不过期,然后由定时任务去异步加载数据,更新缓存。
  2. 加互斥锁,按 key  维度加锁,对于同一个 key,只允许一个线程去查询数据库,其他线程阻塞等待第一个线程的结果,然后直接走缓存。

缓存穿透

概念:查询缓存和数据库中都没有的数据,每次请求都会打到 DB 上。如果攻击者利用这一点来攻击我们的数据库,严重时会击垮数据库。

解决方案:

  1. 在接口层增加校验,比如用户鉴权校验,参数校验,不合法的参数直接代码 Return,比如 id 0 的直接拦截。
  2. 缓存空值。当访问缓存和数据库都没有查询到值时,可以将空值写进缓存,再设置较短的过期时间,这样可以防止攻击者反复用同一个 id 暴力攻击。
  3. 布隆过滤器。在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 ID 不存在布隆过滤器中,说明查询的数据一定不存在数据库中,就不要去数据库查询了。

缓存雪崩

概念:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,大量的请求直接打到 DB 上,这样可能导致整个系统的崩溃。

解决方案:

  1. 给缓存的过期时间加上一个随机值时间,避免多个 key 同时过期
  2. 设置热点数据永远不过期,然后由定时任务去异步加载数据,更新缓存。
  3. 加互斥锁,按 key  维度加锁,对于同一个 key,只允许一个线程去查询数据库,其他线程阻塞等待第一个线程的结果,然后直接走缓存。

什么是布隆过滤器?

布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求。

布隆过滤器由一个 bitSet 和 一组 Hash 函数组成。在初始化时,bitSet 的每一位被初始化为 0,同时会定义 Hash 函数,例如有 3 组 Hash 函数:hash1、hash2、hash3。

当我们要写入一个值时:

  1. 首先将这个值跟 3 组 Hash 函数分别计算,得到 bitSet 的下标为:1、3、5
  2. 将 bitSet 的这 3 个下标标记为 1

当我们要查询一个值时:

  1. 首先将这个值跟 3 组 Hash 函数分别计算,得到 bitSet 的下标为:1、3、5
  2. 查看 bitSet 的这 3 个下标是否都为 1,如果这 3 个下标不都为 1,则说明该值必然不存在;如果这 3 个下标都为 1,则只能说明可能存在。因为不同的值在跟 Hash 函数计算后,可能会得到相同的下标,所以某个值的标记位,可能会被其他值给标上了。

降低这种误判率的思路也比较简单:

  • 加大 bitSet 的长度,这样不同的值出现“哈希冲突”的概率就降低了,从而降低了误判率。
  • 提升 Hash 函数的个数,Hash 函数越多,每个值对应的 bit 越多,从而降低了误判率。

Redis 的过期策略有哪些?

定时删除

Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。

指向原始笔记的链接

惰性删除

并非是 key 到期了就会被自动删除,而是每次获取 key 时,才检査 key 是否过期,如果过期就删除。

指向原始笔记的链接

Redis 的内存淘汰策略有哪些?

内存淘汰策略

当 Redis 的内存空间(maxmemory 参数配置)已经用满时,Redis 将根据配置的淘汰策略(maxmemory-policy 参数配置),进行相应的动作。

  • noeviction默认策略,不淘汰任何 key,直接返回错误
  • allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key
  • allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key(Redis 4.0 新增)
  • allkeys-random:在所有的 key 中,随机淘汰部分 key
  • volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key
  • volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key(Redis 4.0 新增)
  • volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
  • volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰

LRU:淘汰最长时间没有被使用的 LFU:淘汰一段时间内,使用次数最少的

指向原始笔记的链接

手动实现一个 LRU 算法

手动实现一个 LRU 算法

原理

将原先的 key-val 再次封装成一个 node。让整体形成一个双链表。然后维护头结点和尾节点,以及一个阈值。 当一个 key 被查询的时候,就将这个 key 从原来的地方删除,然后再次插入到尾节点处。 如果在添加 key 的时候,发现内存达到了阈值,那么就删除头结点的 key。(因为最近调用的节点都重新插入链尾了,在链头结点的就是最近最少使用的 key) 这样就保证了每次淘汰都是最旧的数据。而热点数据可以长时间存在。

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int CACHE_SIZE;
    //这里就是传递进来最多能缓存多少数据
    public LRUCache(int cacheSize) {
        //设置一个hashmap 的初始大小,最后一个true指的是让linkedhashmap
        //按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); 
        CACHE_SIZE = cacheSize;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        //当map中的数据量大于指定的缓存个数的时,自动删除最老的数据
        return size() > CACHE_SIZE; 
    }
}   

指向原始笔记的链接

Redis 的持久化机制有哪几种,各自的实现原理和优缺点?

Redis  的持久化机制有:RDB、AOF、混合持久化(RDB+AOF,Redis 4.0 引入)

RDB

RDB 持久化可以手动执行也可以根据配置定期执行,它的作用是将某个时间点上的数据库状态保存到 RDB 文件中,RDB 文件是一个压缩的二进制文件,通过它可以还原某个时刻数据库的状态。

命令

可以通过 SAVE 或者 BGSAVE 来生成 RDB 文件。

  • SAVE 命令会阻塞主进程,服务器将无法处理客户端发来的命令请求,所以通常不会直接使用该命令。
  • BGSAVE 命令会 fork 子进程来生成 RDB 快照文件,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求。
  • fork:在 Linux 系统中,调用 fork() 时,会创建出一个新进程,称为子进程,子进程会拷贝父进程的 page table。如果进程占用的内存越大,进程的 page table 也会越大,那么 fork 也会占用更多的时间。如果 Redis 占用的内存很大,那么在 fork 子进程时,则会出现明显的停顿现象。

配置

使用 save point 配置,满足 save point 条件后会触发 BGSAVE 来存储一次快照。

Transclude of Redis-持久化#redis-conf-中配置-rdb

RDB 优缺点

优点

  • RDB 文件是是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,适用于备份、全量复制等场景;
  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快;
  • RDB 可以最大化 Redis 的性能。父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

缺点

  • RDB 方式实时性不够,无法做到秒级的持久化;
  • 每次调用 bgsave 都需要 fork 子进程,fork 子进程属于重量级操作,频繁执行成本较高;
  • RDB 文件是二进制的,没有可读性,AOF 文件在了解其结构的情况下可以手动修改或者补全;
  • Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的 2 倍。刚 fork 时,主进程和子进程共享内存,但是随着主进程需要处理写操作,主进程需要将修改的页面拷贝一份出来,然后进行修改。极端情况下,如果所有的页面都被修改,则此时的内存占用是原先的 2 倍。
指向原始笔记的链接

AOF

保存 Redis 服务器执行的所有写操作命令来记录数据库状态。在服务器启动时,通过重新执行这些命令来还原数据集。

开启:AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。 关闭:通过配置 appendonly no 关闭 AOF 持久化。

AOF 通过命令追加、文件写入、文件同步三个步骤来实现持久化机制。

Redis 可以配置(appendfsync)三种不同的 AOF 持久化方式,它们分别是: always:每处理一个命令都将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件 everysec(默认):将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是异步的,由一个后台线程专门负责执行 no:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但不对 AOF 文件进行同步, 何时同步由操作系统来决定

建议使用默认选项 everysec,性能最好也很安全。即使出现系统崩溃,最多只会丢失一秒之内产生的数据。

AOF 的优缺点

优点

  1. AOF 比 RDB 可靠。你可以设置不同的 fsync 策略:no、everysec 和 always。默认是 everysec,在这种配置下,Redis 仍然可以保持良好的性能,即使出现系统崩溃,最多只会丢失一秒之内产生的数据。
  2. AOF 文件是一个纯追加的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。
  3. 当 AOF 文件太大时,Redis 会自动在后台进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。
  4. AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。如果你不小心执行了 FLUSHALL 命令,把所有数据刷掉了,但只要 AOF 文件没有被重写,那么只需停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

缺点

  1. 对于相同的数据集,AOF 文件的大小一般会比 RDB 文件大。
  2. 根据所使用的 fsync 策略,AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。
  3. AOF 在过去曾经发生过这样的 bug :因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。(举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug ) 。虽然这种 bug 在 AOF 文件中并不常见, 但是相较而言,RDB 几乎是不可能出现这种 bug 的。
指向原始笔记的链接

混合持久化

混合持久化

混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。

整体格式为:[RDB file][AOF tail]

开启:混合持久化的配置参数为 aof-use-rdb-preamble,配置为 yes 时开启混合持久化,在 redis 4 刚引入时,默认是关闭混合持久化的,但是在 redis 5 中默认已经打开了。

关闭:使用 aof-use-rdb-preamble no 配置即可关闭混合持久化。

混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子进程先将当前全量数据以 RDB 方式写入新的 AOF 文件,然后再将 AOF 重写缓冲区(aof_rewrite_buf_blocks)的增量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

优点:结合 RDB 和 AOF 的优点, 更快的重写和恢复。

缺点:AOF 文件里面的 RDB 部分不再是 AOF 格式,可读性差。

指向原始笔记的链接

为什么需要 AOF 重写

AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF 文件中的内容会越来越多,文件的体积也会越来越大。

如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。

举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录。

然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。

为了处理这种情况, Redis 引入了 AOF 重写:可以在不打断服务端处理请求的情况下, 对 AOF 文件进行重建(rebuild)。

介绍下 AOF 重写的过程、AOF 后台重写存在的问题、如何解决 AOF 后台重写存在的数据不一致问题

AOF 重写过程:遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。Redis 生成新的 AOF 文件来代替旧 AOF 文件。

有两个命令可以触发 AOF 重写,一个会阻塞主进程进行重写,另一个会 fork 子进程来进行 AOF 重写,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求。

AOF 后台重写存在的问题:子进程在进行 AOF 重写期间,Redis 主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的数据库状态和重写后的 AOF 文件保存的数据库状态不一致。

如何解决 AOF 后台重写存在的数据不一致问题: Redis 引入了 AOF 重写缓冲区(aof_rewrite_buf_blocks),这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓冲区。

这样一来可以保证:

  1. 现有 AOF 文件的处理工作会如常进行。这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
  2. 从创建子进程开始,也就是 AOF 重写开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。

当子进程完成 AOF 重写工作后,主进程会在 serverCron 中检测到子进程已经重写结束,则会执行以下工作:

  1. 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。
  2. 对新的 AOF 文件进行改名,原子的覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。

RDB、AOF、混合持久,应该用哪一个?

  • 如果想尽量保证数据安全性, 应该同时使用 RDB 和 AOF 持久化功能,同时可以开启混合持久化。
  • 如果可以承受数分钟以内的数据丢失, 可以只使用 RDB 持久化。
  • 如果数据是可以丢失的,那可以关闭持久化功能,在这种情况下,Redis 的性能是最高的。

Redis 实现分布式锁

使用命令

加锁

  • 在老版本,使用 setnx 命令:表示 key 不存在时才设置,如果存在则返回 null。保证在多个线程并发 set 下,只会有 1 个线程成功。为了防止死锁,需要使用 expire 命令给 key 设置过期时间。这样的话加锁就成了两个命令,原子性就得不到保障。
  • Redis 2.6.12 版本之后,Redis 支持原子命令加锁,我们可以通过「set key value PX 毫秒数 NX」  命令,实现原子的加锁操作。

解锁需要两步操作:

  1. 查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,被其他线程获取了,所以在解锁前需要先判断自己是否还持有“锁”
  2. 如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。

使用 lua 脚本

执行 lua 脚本是原子操作,可以把多个 Redis 命令放在同一个 Lua 脚本中运行。

加锁

if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then
    --设置成功返回1,当key不存在或者不能为key设置生存时间时,返回0
    return redis.call('expire', KEYS[1], ARGV[2]);
else
--没有获取到锁
    return 0;
end
  • KEYS[1]:我们要解锁的 key
  • ARGV[1]:我们加锁时的 value,用于判断当“锁”是否还是我们持有,如果被其他线程持有了,value 就会发生变化。

解锁

if (redis.call('get', KEYS[1]) == ARGV[1]) then
    --被删除key的数量
    return redis.call('del', KEYS[1]);
else
    return 0;
end

Redis 分布式锁过期了,还没处理完怎么办

  1. 守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每 1/3 的锁时间检查 1 次),如果线程还持有锁,则刷新过期时间。
  2. 超时回滚:当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。

使用缓存时,先操作数据库 or 先操作缓存

  1. 先操作数据库

可能存在的脏数据时间范围:更新数据库后,失效缓存前。这个时间范围很小,通常不会超过几毫秒。

  1. 先操作缓存

可能存在的脏数据时间范围:更新数据库后,下一次对该数据的更新前。这个时间范围不确定性很大,情况如下:

  • 如果下一次对该数据的更新马上就到来,那么会失效缓存,脏数据的时间就很短。
  • 如果下一次对该数据的更新要很久才到来,那这期间缓存保存的一直是脏数据,时间范围很长。

结论:通过上述案例可以看出,先操作数据库和先操作缓存都会存在脏数据的情况。但是相比之下,先操作数据库,再操作缓存是更优的方式,即使在并发极端情况下,也只会出现很小量的脏数据。

为什么是让缓存失效,而不是更新缓存

  1. 更新缓存

数据库中的数据是请求 B 的,缓存中的数据是请求 A 的,数据库和缓存存在数据不一致。

  1. 删除缓存

由于是删除缓存,所以不存在数据不一致的情况。

如何保证数据库和缓存的数据一致性

由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决,常见的有:2PC、TCC、MQ 事务消息等。

但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的。

所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是保证两者数据的最终一致性。

如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库。

保证数据库和缓存数据最终一致性的常用方案如下:

  1. 更新数据库,数据库产生 binlog。
  2. 监听和消费 binlog,执行失效缓存操作。
  3. 如果步骤 2 失效缓存失败,则引入重试机制,将失败的数据通过 MQ 方式进行重试,同时考虑是否需要引入幂等机制。 兜底:当出现未知的问题时,及时告警通知,人为介入处理。

Redis 的 Java 客户端有哪些?官方推荐哪个?

官方推荐的 3 个:Jedis、Redisson 和 lettuce

Redis 里面有 1 亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?

  1. keys *java* 使用  keys  指令可以扫出指定模式的 key 列表。如果数据量比较大的话,会导致 Redis 服务阻塞一段时间,这种情况可以使用 scan 指令。

  2. scan 0 MATCH *java* 命令,scan  指令可以无阻塞的提取出指定模式的 key 列表,但是有一定的重复概率,在客户端做一次去重就可以了,整体所花费的时间会比直接用 keys 指令长。

Redis 怎么保证高可用

主从复制、哨兵模式、集群模式

主从复制

在 Redis 6.0 中,主从复制的完整过程如下:

  1. 开启主从复制

通常有以下三种方式:

  • 在 slave 直接执行命令:slaveof <masterip> <masterport>
  • 在 slave 配置文件中加入:slaveof <masterip> <masterport>
  • 使用启动命令:--slaveof <masterip> <masterport>

注:在 Redis 5.0 之后,slaveof 相关命令和配置已经被替换成 replicaof,为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。

  1. 建立套接字(socket)连接 slave 将根据指定的 IP 地址和端口,向 master 发起套接字(socket)连接,master 在接受(accept) slave 的套接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。

  2. 发送 PING 命令 slave 向 master 发送一个 PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。

  3. 身份验证 slave 向 master 发送 AUTH password 命令来进行身份验证。

  4. 发送端口信息 在身份验证通过后后, slave 将向 master 发送自己的监听端口号, master 收到后记录在 slave 所对应的客户端状态的 slave_listening_port 属性中。

  5. 发送 IP 地址 如果配置了 slave_announce_ip,则 slave 向 master 发送 slave_announce_ip 配置的 IP 地址,master 收到后记录在 slave 所对应的客户端状态的 slave_ip 属性。

该配置是用于解决服务器返回内网 IP 时,其他服务器无法访问的情况。可以通过该配置直接指定公网 IP

  1. 发送 CAPA CAPA 全称是 capabilities,表示同步复制的能力。slave 会在这一阶段发送 capa 告诉 master 自己具备的(同步)复制能力, master 收到后记录在 slave 所对应的客户端状态的 slave_capa 属性。

  2. 数据同步 slave 将向 master 发送 PSYNC 命令, master 收到该命令后判断是进行部分同步还是全量同步,然后根据策略进行数据的同步。

  3. 命令传播 当完成了同步之后,就会进入命令传播阶段,这时 master 只要一直将自己执行的写命令发送给 slave ,而 slave 只要一直接收并执行 master 发来的写命令,就可以保证 master 和 slave 一直保持一致了。

主从复制的好处

  • 数据冗余,实现数据的热备份
  • 故障恢复,避免单点故障带来的服务不可用
  • 读写分离,负载均衡。主节点负载读写,从节点负责读,提高服务器并发量
  • 高可用基础,是哨兵机制和集群实现的基础

哨兵模式(Sentinel)

监控 Redis 实例(主节点、从节点)运行状态,并在主节点发生故障时,通过一系列的机制实现选主及主从切换,实现故障转移,确保整个 Redis 系统的可用性。

  • 集群监控:负责监控 Redis master 和 slave 是否正常工作
  • 消息通知:Redis 实例故障,哨兵负责发送消息作为告警通知管理员
  • 故障转移:master 节点故障,自动重新选举 master,实现集群自愈
  • 配置中心:故障发生后,通知客户端及其他 slave 新的 master 地址

哨兵故障检测 哨兵会周期性地给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。

如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。

为了减少误判的情况,一般用多个节点部署成哨兵集群(最少需要三台机器来部署哨兵集群)就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。

当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。

当赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,主节点就会被标记为「客观下线」。quorum 的值一般设置为「哨兵个数/2+1」,例如 3 个哨兵就设置 2。

哨兵故障转移 哨兵判断完主节点「客观下线」后,哨兵就要开始在多个「从节点」中,选出一个从节点来做新的主节点,然后开始故障转移流程。

  1. 发起选举,从哨兵集群选出一个 Leader
    • 获得半数以上的赞成票;
    • 赞成票的数量要大于等于配置文件的 quorum 的值。
  2. Leader 哨兵将新主节点的 IP 地址和端口通知给客户端(通过 Redis 的发布者/订阅者机制来实现
  3. Leader 哨兵更新相关的主从配置信息

为什么哨兵节点至少要有 3 个?

如果哨兵集群中只有 2 个哨兵节点,此时如果一个哨兵想要成功成为 Leader,必须获得 2 票,而不是 1 票。

所以,如果哨兵集群中有个哨兵挂掉了,那么就只剩一个哨兵了,如果这个哨兵想要成为 Leader,这时票数就没办法达到 2 票,就无法成功成为 Leader,这时是无法进行主从节点切换的。

缺点:哨兵模式最大的缺点就是所有的数据都放在一台服务器上,无法较好的进行水平扩展。

集群模式(Redis Cluster)

Redis Cluster 解决了大数据量存储导致响应慢的问题,同时也便于横向拓展。

特点

  • 采取去中心化的集群模式,将数据按槽存储分布在多个 Redis 节点上。集群共有 16384(2^14) 个槽,每个节点负责处理部分槽。
  • 每个 key 通过 CRC16 算法计算后,对 16383 取模来决定放置哪个槽。
  • Redis 会自动将 16384 个 哈希槽平均分布在集群实例上,比如 N 个节点,每个节点上的哈希槽数 = 16384 / N 个。
  • 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。
  • 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题。
  • 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

基于 Gossip 协议的故障检测 集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,以此交换各个节点状态信息。

当主节点 A 认为主节点 B 疑似下线(PFAIL)时,通过 Gossip 协议通知其他节点。

如果集群里半数以上的主节点都认为主节点 B 疑似下线,那么主节点 B 将被标记为已下线(FAIL)状态,然后将主节点 B 已下线的消息向整个集群广播,所有收到消息的节点会立即将主节点 B 的状态标记为已下线。

故障转移

  • 当 slave 发现自己的 master 进入已下线状态时
  • 将集群的配置纪元 +1(每次执行故障转移都会 +1),并向集群广播故障转移请求(Failover Request)信息
  • 其他 master 节点收到这条信息后,响应一条 ack 消息,表示支持发广播的 slave 成为新的 master
  • 如果收集到的票超过半数,那么这个 slave 就被选举为新主节点
  • 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

参考链接