Redis与MySQL双写一致性如何保证?

1.redis 为什么响应快

1.1 数据保存在内存中

redis 数据保存在内存中,读写操作只要访问内存,不需要磁盘 IO。

1.2 底层数据结构

redis 的数据以 key:value 的格式存储在散列表中,时间复杂度 O(1)。
redis 为 value 定义了丰富的数据结构,包括动态字符串、双向链表、压缩列表、hash、跳表和整数数组,可以根据 value 的特性选择选择最高效的数据结构。

1.3 单线程模型

redis 的网络 IO 和数据读写使用单线程模型,可以绑定 CPU,这避免了线程上下文切换带来的开销。
「注意:redis6.0 对网络请求引入了多线程模型,读写操作还是用单线程。」

1.4 IO 多路复用

redis 采用 epoll 网络模型,如下图:
内核会一直监听新的 socket 连接事件的和已建立 socket 连接的读写事件,把监听到的事件放到事件队列,redis 使用单线程不停的处理这个事件队列。这避免了阻塞等待连接和读写事件到来。
这些事件绑定了回调函数,会调用 redis 的处理函数进行处理。

2.redis 底层数据结构

redis 有 5 种数据类型,包括「字符串string、列表list、集合set、有序集合zset和字典hash」。
redis 底层的数据结构有 6 种,包括「动态字符串、双向链表、压缩列表(ziplist)、hash 表、跳表(skip list)和整数数组」。

2.1.字符串类型

底层数据结构是动态字符串。

2.2.列表

如果同时满足下面条件,就使用压缩列表,否则使用双向链表。
列表中单个元素小于 64 字节
列表中元素个数少于 512

2.3.集合

如果同时满足下面条件,就使用有序整数数组,否则使用 hash 表。
集合中元素都是整数类型
集合中元素个数不超过 512 个

2.4.有序集合

如果同时满足下面 2 个条件,就使用压缩列表,否则使用跳表。
集合中元素都小于 64 字节
集合中元素个数小于 128 个
「注意:有序集合还有一个 HASH 表用于保存集合中元素的分数,做 ZSCORE 操作时,查询的就是这个 HASH 表,所以效率很高。」

跳表的每一层都是一个有序链表,最下面一层保存了全部数据。跳表插入、删除、查询的时间复杂度是 o(logN)。跳表需要存储额外的索引节点,会增加额外的空间开销。

2.5.字典

如果同时满足下面 2 个条件,就使用压缩列表,否则使用 hash 表。
字典中每个 entry 的 key/value 都小于 64 字节
字典中元素个数小于 512 个

3.redis 缓存淘汰策略

redis 总共有 8 种淘汰策略,如下图:

名称说明
noeviction不淘汰数据缓存满了,有新的请求会拒绝
volatile-random随机删除
volatile-ttl按照过期时间先后删除
volatile-lru使用 lru 淘汰算法删除
volatile-lft使用 lfu 淘汰算法删除
allkeys-random随机删除
allkeys-lru使用 lru 淘汰算法删除
allkeys-lfu使用 lfu 淘汰算法删除

volatile-lfu 和 allkeys-lfu 策略是 4.0 版本新增的。

  • lru」是按照数据的最近最少访问原则来淘汰数据,可能存在的问题是如果大批量冷数据最近被访问了一次,就会占用大量内存空间,如果缓存满了,部分热数据就会被淘汰掉。(LRU 算法中不记录次数,仅仅标记为了最近的使用时间)
  • lfu」是按照数据的最小访问频率访问次数原则来淘汰数据,如果两个数据的访问次数相同,则把访问时间较早的数据淘汰。

4.redis 数据持久化

redis 持久化的方式有 2 种,一种是写后日志(AOF),一种是内存快照(RDB)

4.1.AOF 日志

AOF 日志记录了每一条收到的命令,redis 故障宕机恢复时,可以加载 AOF 日志中的命令进行重放来进行故障恢复。AOF 有 3 种同步策略,如下图:

写回策略策略说明优点缺点
always执行命令同步写盘基本不丢失命令性能损耗大
everysec每秒写一次磁盘比 always 的性能要好可能丢失 1s 内的命令
no操作系统控制写盘性能损耗最小可能会丢失很多的命令

如果不是对丢失数据特别敏感的业务,推荐使用 everysec,对主线程的阻塞少,故障后丢失数据只有 1s。

4.2.RDB 快照

RDB 快照是一个内存快照,记录了 redis 某一时刻的全部数据。

4.3.混合日志

从 redis4.0 开始,AOF 文件也可以保存 RDB 快照,AOF 重写的时候 redis 会把 AOF 文件内容清空,先记录一份 RDB 快照,这份数据以"REDIS"开头。记录 RDB 内容后,AOF 文件会接着记录 AOF 命令。故障恢复时,先加载 AOF 文件中 RDB 快照,然后回放 AOF 文件中后面的命令。

4.4.主从同步

redis 主从同步时,主节点会先生成一份 RDB 快照发送给从节点,把快照之后的命令写入主从同步缓存区(replication buffer),从节点把 RDB 文件加载完成后,主节点把缓存区命令发送给从节点。

4.5.AOF 重写

AOF 日志是用记录命令的方式追加的,这样可能存在对同一个 key 的多条命令,这些命令是可以合并成 1 条的。比如对同一个 key 的多个 set 操作日志,可以合成一条。

4.6.阻塞点

AOF 重写和 RDB 快照执行的过程中,redis 都会 fork 一个子进程来执行操作,子进程执行过程中是不是阻塞主线程的。

「但是要注意 2 点:」

  • 1.fork 子进程的过程中,redis 主线程会拷贝一份内存页表(记录了虚拟内存和物理内存的映射关系)给子进程,这个过程是阻塞的,redis 主线程内存越大,阻塞时间越长;
  • 2.子进程和 redis 主线程共用一块儿物理内存,如果新的请求到来,必须使用 copy on write 的方式,拷贝要修改的数据页到新的内存空间进行修改。

5.redis 的高可用

高可用的方式是用 redis-sential(哨兵来实现)

6.redis 为什么变慢了

6.1 主线程阻塞

6.1.1.AOF 重写和 RDB 快照

前面已经讲过了,redis 在 AOF 重写时,主线程会 fork 出一个 bgrewriteaof 子进程。redis 进行 RDB 快照时主线程会 fork 出一个 bgsave 子进程。
这两个操作表面上看不阻塞主线程,但 fork 子进程的这个过程是在主线程完成的。fork 子进程时 redis 需要拷贝内存页表,如果 redis 实例很大,这个拷贝会耗费大量的 CPU 资源,阻塞主线程的时间也会变长。

6.1.2.内存大页

redis 默认支持内存大页是 2MB,使用内存大页,一定程度上可以减少 redis 的内存分配次数,但是对数据持久化会有一定影响。
redis 在 AOF 重写和 RDB 快照过程中,如果主线程收到新的写请求,就需要 CopyOnWrite。使用了内存大页,即使 redis 只修改其中一个大小是 1kb 的 key,也需要拷贝一整页的数据,即 2MB。在写入量较多时,大量拷贝就会导致 redis 性能下降

6.1.3.命令复杂度高

执行复杂度高的命令是造成 redis 阻塞的常见原因。比如对一个 set 或者 list 数据类型执行 SORT 操作,复杂度是O(N+M*log(M))

6.1.4.bigkey 操作

如果一个 key 的 value 非常大,创建的时候分配内存会很耗时,删除的时候释放内存也很耗时。
redis4.0 以后引入了 layfree 机制,可以使用子进程异步删除,从而不影响主线程执行。用 UNLINK 命令替代 DEL 命令,就可以使用子进程异步删除。
redis6.0 增加了配置项 lazyfree-lazy-user-del,配置成 yes 后,del 命令也可以用子进程异步删除。

如果 lazyfree-lazy-user-del 不设置为 yes,那 redis 是否采用异步删除,是要看删除的时机的。对于 String 类型和底层采用整数数组和压缩列表的数据类型,redis 是不会采用异步删除的。

6.1.5.从节点全量同步

从节点全量同步过程中,需要先清除内存中的数据,然后再加载 RDB 文件,这个过程中是阻塞的,如果有读请求到来,只能等到加载 RDB 文件完成后才能处理请求,所以响应会很慢。
另外,如果 redis 实例很大,也会造成 RDB 文件太大,从库加载时间长。所以尽量保持 redis 实例不要太大,比如单个实例限制 4G,如果超出就采用切片集群。

6.1.6.AOF 同步写盘

appendfsync 策略有 3 种:always、everysec、no,如果采用 always,每个命令都会同步写盘,这个过程是阻塞的,等写盘成功后才能处理下一条命令。
除非是严格不能丢数据的场景,否则尽量不要选择 always 策略,推荐尽量选择 everysec 策略,如果对丢失数据不敏感,可以采用 no。

6.1.7.内存达到 maxmemory

内存达到 maxmemory,需要使用淘汰策略来淘汰部分 key。即使采用 lazyfree 异步删除,选择 key 的过程也是阻塞的。
可以选择较快的淘汰策略,比如用随机淘汰来替换 LRU 和 LFU 算法淘汰。也可以扩大切片数量来减轻淘汰 key 的时间消耗。

6.2 操作系统限制

6.2.1.使用了 swap

使用 swap 的原因是操作系统不能给 redis 分配足够大的内存,如果操作其他开启了 swap,内存数据就需要不停地跟 swap 换入和换出,对性能影响非常大。
操作系统没有能力分配内存的原因也可能是其他进程使用了大量的内存。

6.2.2.网络问题

如果网卡负载很大,对 redis 性能影响会很大。这一方面有可能 redis 的访问量确实很高,另一方面也可能是有其他流量大的程序占用了带宽。
这个最好从运维层面进行监控。

6.2.3.线程上下文切换

redis 虽然是单线程的,但是在多核 cpu 的情况下,也可能会发生上下文切换。如果主线程从一个物理核切换到了另一个物理核,那就不能使用 CPU 高效的一级缓存和二级缓存了。如下图所示:
cpu缓存图
为防止这种情况,可以把 redis 绑定到一个 CPU 物理核。

6.2.4. 磁盘性能低

对于 AOF 同步写盘的使用场景,如果磁盘性能低,也会影响 redis 的响应。可以优先采用性能更好的 SSD 硬盘。

7.设计排行榜功能

redis 的 zset 类型保存了分数值,可以方便的实现排行榜的功能。
比如要统计 10 篇文章的排行榜,可以先建立一个存放 10 篇文章的 zset,每当有读者阅读一篇文章时,就用 ZINCRBY 命令给这篇文章的分数加 1,最后可以用 range 命令统计排行榜前几位的文章。

8.redis 实现分布式锁

8.1.redis 单节点的分布式锁

一个服务部署了 2 个客户端,获取分布式锁时一个成功,另一个就失败了。
redis 一般使用 setnx 实现分布式锁,命令如下:

SETNX KEY_NAME VALUE

设置成功返回 1,设置失败返回 0。
使用单节点分布式锁存在一些问题。

8.1.1.客户端 1 获取锁后发生了故障

结果锁就不能释放了,其他客户端永远获取不到锁。解决方法是用下面命令对 key 设置过期时间:

SET key value [EX seconds] [PX milliseconds] NX

8.1.2 客户端 2 误删除了锁

解决方法是对 key 设置 value 时加入一个客户端表示,比如在客户端 1 设置 key 时在 value 前拼接一个字符串 application1,删除的时候做一下判断.

8.2.redis 红锁(RedLock)

redis 单节点会有可靠性问题,节点故障后锁操作就会失败。redis 为了应对单点故障的问题,设计了多节点的分布式锁,也叫红锁。主要思想是客户端跟多个 redis 实例请求加锁,只有超过半数的实例加锁成功,才认为成功获取了分布式锁。
如下图,客户端分别跟 3 个实例请求加锁,有 2 个实例加锁成功,所以获取分布式锁成功.

9.缓存雪崩、击穿、穿透

9.1.缓存雪崩

redis 做缓存时,如果同一时间大量缓存数据失效,客户端请求会大量发送到数据库,导致数据库压力激增.

应对方法主要有 3 个:

  • 给 key 设置过期时间时加一个小的随机数
  • 限流
  • 服务降级

9.2.缓存击穿

某个热点 key,突然过期了,大量请求发送到了数据库。解决方案是给热点 key 不设置过期时间。

9.3.缓存穿透

某个热点 key,查询缓存和查询数据库都没有,就发生了缓存穿透。
「应对方法主要有 2 个:」

  • 缓存热点的空值和缺省值
  • 查询数据库之前先查询布隆过滤器

10 数据倾斜

什么是数据倾斜?看下面这个面试题:

如果 redis 有一个热点 key,qps 能达到 100w,该如何存储?

如果这个热点 key 被放到一个 redis 实例上,这个实例面临的访问压力会非常大。 如下图,redis3 这个实例保存了 foo 这个热点 key,访问压力会很大:
造成流量倾斜。

「解决方法主要有两个:」

  1. 使用客户端本地缓存来缓存 key,这样改造会有两个问题:
  • 客户端缓存的热点 key 可能消耗大量内存
  • 客户端需要保证本地缓存和 redis 缓存的一致性
  1. 给热点 key 加一个随机前缀,让它保存到不同的 redis 实例上,这样也会存在两个问题:
  • 客户端在访问的时候需要给这个 key 加前缀
  • 客户端在删除的时候需要根据所有前缀来删除不同实例上保存的这个 key

11 bitmap 使用

有一道经典的面试题,10 亿整数怎么在内存中去重排序?

我们先算一下 10 亿整数占的内存,java 一个整数类型占四字节,占用内存大小约
10 亿 * 4 / 1024 / 1024 = 3.7G
占得内存太大了,如果内存不够,怎么办呢?

11.1.bitmap 介绍

bitmap 类型使用的数据结构是String,底层存储格式是二进制的bit数组。假如我们有1、4、6、9四个数,保存在bit数组中如下图:
大数据去重

在这个bit数组中用10个 bit 的空间保存了四个整数,占用空间非常小。
再回到面试题,我们使用 bit 数组长度是 10 亿整数中 「(最大值 - 最小值 + 1)」

如果有负数,需要进行一个转化,所有数字加最小负数的绝对值。比如{-2, 0, 1, 3},我们转换成{0, 2, 3, 5},因为数组下标必须从 0 开始。

11.2.使用场景

11.2.1.员工打卡记录

在一个有 100 个员工的公司,要统计一个月内员工全勤的人数,可以每天创建一个 bitmap,签到的员工 bit 位置为 1。
要统计当天签到的员工只要用BITCOUNT命令就可以。
要统计当月全勤的员工,只要对当月每天的 bitmap 做交集运算就可以,命令如下:

BITOP AND srckey1 srckey2 srckey3 ... srckey30

srckeyN 表示第 N 天的打卡记录 bitmap

11.2.2.统计网站日活跃用户

比如网站有 10 万个用户,这样我们创建一个长度为 10 万的bitmap,每个用户 id 占一个位,如果用户登录,就把bit位置为 1,日终的时候用BITCOUNT命令统计出当天登录过的用户总数。

关于我
loading