Redis 持久化
持久化就是把内存数据写入磁盘,防止服务宕机造成数据丢失,Redis 提供了不同级别的持久化方式:
- RDB(Redis DataBase):RDB持久化一指定的时间间隔执行数据集的时间点快照。
- AOF(Append Only File):AOF持久化记录服务器接收到的每一个写操作,在服务器重启时会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使AOF文件的体积不至于过大。
- 无持久性:如果你希望你的数据只在服务运行的时候存在,那么你可以完全禁用持久化。
- RDB + AOF:可以在同一个实例中结合 AOF 和 RDB。请注意,在这种情况下,当 Redis 重新启动时,AOF 文件将用于重建原始数据集,因为它保证是最完整的。
RDB 持久化
在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb
的二进制文件中。你可以对 Redis 进行设置, 让它在 “ N 秒内数据集至少有 M 个改动” 这一条件被满足时,自动保存一次数据集。你也可以通过调用 SAVE
或者 BGSAVE
,手动让 Redis 进行数据集保存操作。这种持久化方式被称为快照 snapshotting
。
SAVE
:SAVE
命令会使用同步的方式生成 RDB 快照文件,这意味着在这个过程中会阻塞所有其他客户端的请求。因此不建议在生产环境使用这个命令,除非因为某种原因需要去阻止 Redis 使用子进程进行后台生成快照(例如调用fork(2)
出错)。BGSAVE
:BGSAVE
命令使用后台的方式保存 RDB 文件,调用此命令后,会立刻返回OK
返回码。Redis 会产生一个子进程进行处理并立刻恢复对客户端的服务。在客户端我们可以使用LASTSAVE
命令查看操作是否成功。
注意:配置文件里禁用了快照生成功能不影响 SAVE
和 BGSAVE
命令的效果。
RDB 优缺点
优点:
- RDB是一个非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。
- RDB 非常适合灾难恢复,它是一个可以传输到远程数据中心或 Amazon S3(可能已加密)的压缩文件。
- RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。
- 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些。
缺点:
-
不支持拉链,只有一个dump.rdb文件。
-
如果您需要在 Redis 停止工作时(例如断电后)将数据丢失的可能性降到最低,那么 RDB 并不好。您可以配置生成 RDB 的不同保存点(例如,在对数据集至少 5 分钟和 100 次写入之后,您可以有多个保存点)。但是,您通常会每五分钟或更长时间创建一个 RDB 快照,因此,如果 Redis 由于任何原因在没有正确关闭的情况下停止工作,您应该准备好丢失最新分钟的数据。
-
RDB 需要经常 fork() 以便使用子进程在磁盘上持久化。如果数据集很大,fork() 可能会很耗时,并且如果数据集很大并且 CPU 性能不是很好,可能会导致 Redis 停止为客户端服务几毫秒甚至一秒钟。AOF 也需要 fork() 但频率较低,您可以调整要重写日志的频率,而不需要对持久性进行任何权衡。
工作原理
当 Redis 需要保存 dump.rdb
文件时, 服务器执行以下操作:
- Redis调用fork(),产生一个子进程。
- 子进程把数据写到一个临时的RDB文件。
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write
)机制中获益。
父进程的数据可以让子进程看到
Linux中,export的环境变量,子进程的修改不会破坏父进程,父进程的修改不会影响到子进程。
#! /bin/bash
echo $$
echo $num
num=888
echo num:$num
sleep 20
echo $num
RDB 配置
默认Redis会把快照文件存储为当前目录下一个名为dump.rdb
的文件。要修改文件的存储路径和名称,可以通过修改配置文件redis.conf
实现:
# RDB文件名,默认为dump.rdb。
dbfilename dump.rdb
# 文件存放的目录,AOF文件同样存放在此目录下。默认为当前工作目录。
dir /var/lib/redis/6379
# 你可以配置保存点,使Redis如果在每N秒后数据发生了M次改变就保存快照文件。保存点可以设置多个,Redis的配置文件就默认设置了3个保存点:
save 900 1 #900秒后至少1个key有变动
save 300 10 #300秒后至少10个key有变动
save 60 10000 #每60秒,如果数据发生了1000次以上的变动,Redis就会自动保存快照文件
# 如果想禁用快照保存的功能,可以通过注释掉所有"save"配置达到,或者在最后一条"save"配置后添加如下的配置:
save ""
# 默认情况下,如果Redis在后台生成快照的时候失败,那么就会停止接收数据,目的是让用户能知道数据没有持久化成功。但是如果你有其他的方式可以监控到Redis及其持久化的状态,那么可以把这个功能禁止掉。
stop-writes-on-bgsave-error yes
# 默认Redis会采用LZF对数据进行压缩。如果你想节省点CPU的性能,你可以把压缩功能禁用掉,但是数据集就会比没压缩的时候要大。
rdbcompression yes
# 从版本5的RDB的开始,一个CRC64的校验码会放在文件的末尾。这样更能保证文件的完整性,但是在保存或者加载文件时会损失一定的性能(大概10%)。如果想追求更高的性能,可以把它禁用掉,这样文件在写入校验码时会用0替代,加载的时候看到0就会直接跳过校验。
rdbchecksum yes
AOF 持久化
快照并不是很可靠。如果你的电脑突然宕机了,或者电源断了,又或者不小心杀掉了进程,那么最新的数据就会丢失。而AOF文件则提供了一种更为可靠的持久化方式。每当Redis接受到会修改数据集的命令时,就会把命令追加到AOF文件里,当你重启Redis时,AOF里的命令会被重新执行一次,重建数据。
AOF 优缺点
优点:
- 丢失数据少,使用AOF 会让你的Redis更加耐久,你可以使用不同的fsync策略:无fsync、每秒fsync、每次写的时候fsync。使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。
- AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用
redis-check-aof
工具修复这些问题。 - Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
缺点:
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积,且恢复速度慢。
- 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
日志重写
因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 BGREWRITEAOF
命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。Redis 2.2 需要自己手动执行 BGREWRITEAOF
命令; Redis 2.4 则可以自动触发 AOF 重写, 具体信息请查看 2.4 的示例配置文件。
AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制(
copy-on-write
),其工作原理如下:
- Redis 执行 fork() ,现在同时拥有父进程和子进程。
- 子进程开始将新 AOF 文件的内容写入到临时文件。
- 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
- 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
- 搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
可靠性
你可以配置 Redis 多久才将数据 fsync 到磁盘一次。有三种方式:
- 每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全
- 每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
- 从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。
- 推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
数据损坏修复
服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
-
为现有的 AOF 文件创建一个备份。
-
使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复:
$ redis-check-aof --fix
-
(可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。
-
重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。
AOF 配置
redis中,RDB和AOF可以同时开启,如果开启了AOF只会用AOF恢复;4.0以后,AOF是一个混合体,包含了RDB全量,将老数据RDB到AOF文件中,将增量的以指令的方式Append到AOF中
# 开启AOF
appendonly yes
# 文件存放目录,与RDB共用。默认为当前工作目录。
dir ./
# 默认文件名为appendonly.aof
appendfilename "appendonly.aof"
日志重写
# Redis会记住自从上一次重写后AOF文件的大小(如果自Redis启动后还没重写过,则记住启动时使用的AOF文件的大小)。
# 如果当前的文件大小比起记住的那个大小超过指定的百分比,则会触发重写。
auto-aof-rewrite-percentage 100
# 同时需要设置一个文件大小最小值,只有大于这个值文件才会重写,以防文件很小,但是已经达到百分比的情况。
auto-aof-rewrite-min-size 64mb
# 要禁用自动的日志重写功能,我们可以把百分比设置为0:
auto-aof-rewrite-percentage 0
可靠性
appendfsync always # 总是调用fsync
appendfsync everysec # 每秒fsync一次
appendfsync no # 从不fsync
怎样从 RDB 方式切换为 AOF 方式
在 Redis 2.2 或以上版本,可以在不重启的情况下,从 RDB 切换到 AOF :
-
为最新的 dump.rdb 文件创建一个备份。
-
将备份放到一个安全的地方。
-
执行以下两条命令:
- redis-cli config set appendonly yes - redis-cli config set save ""
-
确保写命令会被正确地追加到 AOF 文件的末尾。
-
执行的第一条命令开启了 AOF 功能: Redis 会阻塞直到初始 AOF 文件创建完成为止, 之后 Redis 会继续处理命令请求, 并开始将写入命令追加到 AOF 文件末尾。
执行的第二条命令用于关闭 RDB 功能。 这一步是可选的, 如果你愿意的话, 也可以同时使用 RDB 和 AOF 这两种持久化功能。
重要:别忘了在
redis.conf
中打开 AOF 功能! 否则的话, 服务器重启之后, 之前通过CONFIG SET
设置的配置就会被遗忘, 程序会按原来的配置来启动服务器。
AOF 和 RDB 之间的相互作用
在版本号大于等于 2.4 的 Redis 中, BGSAVE
执行的过程中, 不可以执行 BGREWRITEAOF
。 反过来说, 在 BGREWRITEAOF
执行的过程中, 也不可以执行 BGSAVE
。这可以防止两个 Redis 后台进程同时对磁盘进行大量的 I/O 操作。
如果 BGSAVE
正在执行, 并且用户显示地调用 BGREWRITEAOF
命令, 那么服务器将向用户回复一个 OK 状态, 并告知用户, BGREWRITEAOF
已经被预定执行: 一旦 BGSAVE
执行完毕, BGREWRITEAOF
就会正式开始。 当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。
缓存雪崩、穿透、击穿
缓存穿透
指查询系统并不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
解决方案:
- 查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短
- 布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对 DB 的查询。
缓存击穿
对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案:
- 使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法。
- 热点数据永不过期:物理不过期,但逻辑过期(后台异步线程去刷新)
缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存的大量数据在某一时刻同时失效,请求全部转发到 DB, DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个key 缓存。
解决方案:
- 随机过期时间【时点性无关】:将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 定时任务【零点】,强依赖击穿方案
Redis分布式锁
基本概念
分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源,一般来说,分布式锁需要满足的特性有这么几点:
- 互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
- 高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
- 防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
- 独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了;
业界里可以实现分布式锁效果的工具很多,但操作无非这么几个:加锁、解锁、防止锁超时
分布式锁实现
redis 分布式锁主要使用以下命令:
SETNX key value
:如果不存在,则 SET,设置成功就返回1,否则返回0EXPIRE key seconds
:设置 key 的有效期SETEX key seconds value
:PSETEX key milliseconds value
:
SETNX 用法
可以看出,当把 lock 设置为 ‘akieay’ 后,再设置成别的值就会失败,类似于独占了锁。但也有个致命的问题,就是 key 没有过期时间,这样一来,除非手动删除 key 或者获取锁后设置过期时间,不然其他线程永远拿不到锁。
既然如此,我们可以通过 EXPIRE
来设置过期时间,如下:
$ SETNX lock akieay
$ EXPIRE lock 1000
但这个方案也有个问题,因为获取锁和设置过期时间分成两步了,不是原子性操作,有可能获取锁成功但设置时间失败,Redis 官方给我们提供了 SETEX
命令来解决这个问题。
SETEX 用法
SETEX key seconds value
将值 value
关联到 key
,并将 key
的生存时间设为 seconds
(以秒为单位)。如果 key
已经存在,SETEX
命令将覆写旧值。
这个命令类似于以下两个命令的组合:
$ SET key value
$ EXPIRE key seconds
但这两步动作是原子性的,会在同一时间完成。
PSETEX 用法
PSETEX key milliseconds value
这个命令和 SETEX
命令相似,但它以毫秒为单位设置 key
的生存时间,而不是像 SETEX
命 令那样,以秒为单位。
从 Redis 2.6.12 版本开始,SET 命令可以通过参数来实现和 SETNX、SETEX、PSETEX 三个命令相同的效果。
就比如这条命令
# 加上 NX、EX 参数后,效果就相当于 SETEX,这也是 Redis 获取锁写法里面最常见的
$ SET key value NX EX seconds
查看有效期
# ttl 命令可以查询锁的有效期(秒)
$ ttl lock
释放锁
释放锁只需要将 key 删除就行了;不过需要注意的是,分布式锁必须由锁的持有者自己释放。
通过在 value 中设置当前线程加锁的标识,在删除之前验证 key 对应的 value 判断锁是否是当前线程持有。可生成一个 UUID 标识当前线程,使用 lua 脚本做验证标识和解锁操作。
// 加锁
String uuid = UUID.randomUUID().toString().replaceAll("-","");
SET key uuid NX EX 30
// 解锁
if (redis.call('get', KEYS[1]) == ARGV[1])
then return redis.call('del', KEYS[1])
else return 0
end
分布式锁的缺陷
一、客户端长时间阻塞导致锁失效问题
客户端1得到了锁,因为网络问题或者 GC 等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。
二、redis 服务器时钟漂移问题
如果 redis 服务器的机器时钟发生了向前跳跃,就会导致这个 key 过早超时失效,比如:客户端1 拿到锁后,key 的过期时间是 12:02分,但 redis 服务器本身的时钟比客户端快了 2分钟,导致 key 在12:00的时候就失效了,这时候,如果客户端1 还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
三、单点实例安全问题
如果 redis 是单 master 模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master 加一个 slave,但是因为 redis 的主从同步是异步进行的,可能会出现客户端1 设置完锁后,master 挂掉,slave 提升为 master,因为异步复制的特性,可能客户端1 设置的锁丢失了,这时候客户端2 设置锁也能够成功,导致 客户端1 和 客户端2 同时拥有锁。
为了解决 Redis 单点问题,redis 的作者提出了 RedLock 算法。
RedLock 算法
该算法的实现前提在于 Redis 必须是多节点部署的,可以有效防止单点故障,具体的实现思路是这样的:
- 获取当前时间戳(ms)
- 使用相同的 key 和随机值在N个节点上请求锁。这里获取锁的尝试时间要远远小于锁的超时时间,防止某个 masterDown 了,我们还在不断的获取锁,而被阻塞过长的时间。
- 只有在大多数节点上获取到了锁【实例数>= N/2 + 1 (N为 Redis 实例的数量)】,而且总的获取时间小于锁的超时时间的情况下,认为锁获取成功了。
- 如果锁获取成功了,锁的超时时间就是最初的锁超时时间减去获取锁的总耗时时间。
- 如果锁获取失败了,不管是获取成功的节点的数目没有过半,还是获取锁的耗时超过了锁的释放时间,都会将已经设置了 key 的 master 上的 key 删除,即会开始解锁所有 redis 实例。
根据这样的算法,我们假设有5个 Redis 实例的话,那么 client 只要获取其中3台以上的锁就算是成功了,用流程图演示大概就像这样: (时钟漂移:redis 服务器的时钟漂移误差)
缺陷
RedLock 算法的思想主要是为了有效防止 Redis 单点故障的问题,而且在设计 TTL 的时候也考虑到了服务器时钟漂移的误差,让分布式锁的安全性提高了不少。但是在 RedLock 算法中,锁的有效时间会减去连接 Redis 实例的时长,如果这个过程因为网络问题导致耗时太长的话,那么最终留给锁的有效时长就会大大减少,客户端访问共享资源的时间很短,很可能程序处理的过程中锁就到期了。而且,锁的有效时间还需要减去服务器的时钟漂移,但是应该减多少合适呢,要是这个值设置不好,很容易出现问题。
然后第二点,RedLock 算法虽然考虑到用多节点来防止 Redis 单点故障的问题,但如果有节点发生崩溃重启的话,还是有可能出现多个客户端同时获取锁的情况。
假设一共有 5 个 Redis 节点:A、B、C、D、E,客户端1和2分别加锁
- 客户端1 成功锁住了 A,B,C,获取锁成功(但D和E没有锁住)
- 节点C 的 master 挂了,然后锁还没同步到 slave,slave 升级为 master 后丢失了客户端1加的锁。
- 客户端2 这个时候获取锁,锁住了 C,D,E,获取锁成功
这样,客户端1 和 客户端2 就同时拿到了锁,程序安全的隐患依然存在。除此之外,如果这些节点里面某个节点发生了时间漂移的话,也有可能导致锁的安全问题。
所以说,虽然通过多实例的部署提高了可用性和可靠性,但 RedLock 并没有完全解决Redis单点故障存在的隐患,也没有解决时钟漂移以及客户端长时间阻塞而导致的锁超时失效存在的问题,锁的安全性隐患依然存在。
总结
我们之所以用 Redis 作为分布式锁的工具,很大程度上是因为 Redis 本身效率高且单进程的特点,即使在高并发的情况下也能很好的保证性能,但很多时候,性能和安全不能完全兼顾,如果你一定要保证锁的安全性的话,可以用其他的中间件如 db、zookeeper 来做控制,这些工具能很好的保证锁的安全,但性能方面只能说是差强人意,否则大家早就用上了。
一般来说,用 Redis 控制共享资源并且还要求数据安全要求较高的话,最终的保底方案是对业务数据做幂等控制,这样一来,即使出现多个客户端获得锁的情况也不会影响数据的一致性。当然,具体使用可以根据自身的应用场景进行取舍。
Redis 批量读取
Redis 里面有1亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?
- 使用
keys
指令可以扫出指定模式的 key 列表,但是若这个 redis 正在给线上的业务提供服务,那使用keys
指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复 - 可以使用
scan
指令,scan
指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys
指令长
MySQL与Redis 如何保证双写一致性
我们都知道,只要我们使用redis,就会遇到缓存与数据库的双存储双写,那么只要是双写,就一定会有数据一致性问题,为了保证双写一致性,我们提供了以下几种解决方案:
- 缓存延时双删
- 删除缓存重试机制
- 读取biglog异步删除缓存
延时双删
什么是延时双删呢?流程图如下:
流程如下图所示:
- 先删除缓存
- 再更新数据库
- 休眠一会(比如1秒),再次删除缓存。
这个休眠一会,一般多久呢?都是1秒?
这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
这种方案还算可以,只有休眠那一会(比如就那1秒),可能有脏数据,一般业务也会接受的。但是如果第二次删除缓存失败呢?缓存和数据库的数据还是可能不一致,对吧?给Key设置一个自然的expire过期时间,让它自动过期怎样?那业务要接受过期时间内,数据的不一致咯?还是有其他更佳方案呢?
删除缓存重试机制
因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次,保证删除缓存成功就可以了,所以可以引入删除缓存重试机制。
流程如下图所示:
- 写请求更新数据库
- 缓存因为某些原因,删除失败
- 把删除失败的key放到消息队列
- 消费消息队列的消息,获取要删除的key
- 重试删除缓存操作
读取biglog异步删除缓存
重试删除缓存机制有一个缺点,就是会造成好多业务代码入侵。其实,还可以这样优化:启动一个订阅程序去订阅数据库的 binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
流程如下图所示:
- 更新数据库数据
- 数据库会将操作信息写入binlog日志当中
- 订阅程序提取出所需要的数据以及key
- 另起一段非业务代码,获得该信息
- 尝试删除缓存操作,发现删除失败
- 将这些信息发送至消息队列
- 重新从消息队列中获得该数据,重试操作
以 mysql 为例:可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面,然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。