持久化就是把内存数据写入磁盘,防止服务宕机造成数据丢失,Redis 提供了不同级别的持久化方式:
在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb
的二进制文件中。你可以对 Redis 进行设置, 让它在 “ N 秒内数据集至少有 M 个改动” 这一条件被满足时,自动保存一次数据集。你也可以通过调用 SAVE
或者 BGSAVE
,手动让 Redis 进行数据集保存操作。这种持久化方式被称为快照 snapshotting
。
SAVE
:SAVE
命令会使用同步的方式生成 RDB 快照文件,这意味着在这个过程中会阻塞所有其他客户端的请求。因此不建议在生产环境使用这个命令,除非因为某种原因需要去阻止 Redis 使用子进程进行后台生成快照(例如调用fork(2)
出错)。BGSAVE
:BGSAVE
命令使用后台的方式保存 RDB 文件,调用此命令后,会立刻返回 OK
返回码。Redis 会产生一个子进程进行处理并立刻恢复对客户端的服务。在客户端我们可以使用 LASTSAVE
命令查看操作是否成功。注意:配置文件里禁用了快照生成功能不影响 SAVE
和 BGSAVE
命令的效果。
优点:
缺点:
不支持拉链,只有一个dump.rdb文件。
如果您需要在 Redis 停止工作时(例如断电后)将数据丢失的可能性降到最低,那么 RDB 并不好。您可以配置生成 RDB 的不同保存点(例如,在对数据集至少 5 分钟和 100 次写入之后,您可以有多个保存点)。但是,您通常会每五分钟或更长时间创建一个 RDB 快照,因此,如果 Redis 由于任何原因在没有正确关闭的情况下停止工作,您应该准备好丢失最新分钟的数据。
RDB 需要经常 fork() 以便使用子进程在磁盘上持久化。如果数据集很大,fork() 可能会很耗时,并且如果数据集很大并且 CPU 性能不是很好,可能会导致 Redis 停止为客户端服务几毫秒甚至一秒钟。AOF 也需要 fork() 但频率较低,您可以调整要重写日志的频率,而不需要对持久性进行任何权衡。
当 Redis 需要保存 dump.rdb
文件时, 服务器执行以下操作:
这种工作方式使得 Redis 可以从写时复制(copy-on-write
)机制中获益。
父进程的数据可以让子进程看到
Linux中,export的环境变量,子进程的修改不会破坏父进程,父进程的修改不会影响到子进程。
#! /bin/bash
echo $$
echo $num
num=888
echo num:$num
sleep 20
echo $num
默认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文件则提供了一种更为可靠的持久化方式。每当Redis接受到会修改数据集的命令时,就会把命令追加到AOF文件里,当你重启Redis时,AOF里的命令会被重新执行一次,重建数据。
优点:
redis-check-aof
工具修复这些问题。缺点:
因为 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 多久才将数据 fsync 到磁盘一次。有三种方式:
服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
为现有的 AOF 文件创建一个备份。
使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复:
$ redis-check-aof --fix
(可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。
重启 Redis 服务器,等待服务器载入修复后的 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
在 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
设置的配置就会被遗忘, 程序会按原来的配置来启动服务器。
在版本号大于等于 2.4 的 Redis 中, BGSAVE
执行的过程中, 不可以执行 BGREWRITEAOF
。 反过来说, 在 BGREWRITEAOF
执行的过程中, 也不可以执行 BGSAVE
。这可以防止两个 Redis 后台进程同时对磁盘进行大量的 I/O 操作。
如果 BGSAVE
正在执行, 并且用户显示地调用 BGREWRITEAOF
命令, 那么服务器将向用户回复一个 OK 状态, 并告知用户, BGREWRITEAOF
已经被预定执行: 一旦 BGSAVE
执行完毕, BGREWRITEAOF
就会正式开始。 当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。
指查询系统并不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
解决方案:
对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案:
设置缓存时采用了相同的过期时间,导致缓存的大量数据在某一时刻同时失效,请求全部转发到 DB, DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个key 缓存。
解决方案:
分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源,一般来说,分布式锁需要满足的特性有这么几点:
业界里可以实现分布式锁效果的工具很多,但操作无非这么几个:加锁、解锁、防止锁超时
redis 分布式锁主要使用以下命令:
SETNX key value
:如果不存在,则 SET,设置成功就返回1,否则返回0EXPIRE key seconds
:设置 key 的有效期SETEX key seconds value
:PSETEX key milliseconds value
:可以看出,当把 lock 设置为 ‘akieay’ 后,再设置成别的值就会失败,类似于独占了锁。但也有个致命的问题,就是 key 没有过期时间,这样一来,除非手动删除 key 或者获取锁后设置过期时间,不然其他线程永远拿不到锁。
既然如此,我们可以通过 EXPIRE
来设置过期时间,如下:
$ SETNX lock akieay
$ EXPIRE lock 1000
但这个方案也有个问题,因为获取锁和设置过期时间分成两步了,不是原子性操作,有可能获取锁成功但设置时间失败,Redis 官方给我们提供了 SETEX
命令来解决这个问题。
SETEX key seconds value
将值 value
关联到 key
,并将 key
的生存时间设为 seconds
(以秒为单位)。如果 key
已经存在,SETEX
命令将覆写旧值。
这个命令类似于以下两个命令的组合:
$ SET key value
$ EXPIRE key seconds
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 算法。
该算法的实现前提在于 Redis 必须是多节点部署的,可以有效防止单点故障,具体的实现思路是这样的:
根据这样的算法,我们假设有5个 Redis 实例的话,那么 client 只要获取其中3台以上的锁就算是成功了,用流程图演示大概就像这样: (时钟漂移:redis 服务器的时钟漂移误差)
RedLock 算法的思想主要是为了有效防止 Redis 单点故障的问题,而且在设计 TTL 的时候也考虑到了服务器时钟漂移的误差,让分布式锁的安全性提高了不少。但是在 RedLock 算法中,锁的有效时间会减去连接 Redis 实例的时长,如果这个过程因为网络问题导致耗时太长的话,那么最终留给锁的有效时长就会大大减少,客户端访问共享资源的时间很短,很可能程序处理的过程中锁就到期了。而且,锁的有效时间还需要减去服务器的时钟漂移,但是应该减多少合适呢,要是这个值设置不好,很容易出现问题。
然后第二点,RedLock 算法虽然考虑到用多节点来防止 Redis 单点故障的问题,但如果有节点发生崩溃重启的话,还是有可能出现多个客户端同时获取锁的情况。
假设一共有 5 个 Redis 节点:A、B、C、D、E,客户端1和2分别加锁
这样,客户端1 和 客户端2 就同时拿到了锁,程序安全的隐患依然存在。除此之外,如果这些节点里面某个节点发生了时间漂移的话,也有可能导致锁的安全问题。
所以说,虽然通过多实例的部署提高了可用性和可靠性,但 RedLock 并没有完全解决Redis单点故障存在的隐患,也没有解决时钟漂移以及客户端长时间阻塞而导致的锁超时失效存在的问题,锁的安全性隐患依然存在。
我们之所以用 Redis 作为分布式锁的工具,很大程度上是因为 Redis 本身效率高且单进程的特点,即使在高并发的情况下也能很好的保证性能,但很多时候,性能和安全不能完全兼顾,如果你一定要保证锁的安全性的话,可以用其他的中间件如 db、zookeeper 来做控制,这些工具能很好的保证锁的安全,但性能方面只能说是差强人意,否则大家早就用上了。
一般来说,用 Redis 控制共享资源并且还要求数据安全要求较高的话,最终的保底方案是对业务数据做幂等控制,这样一来,即使出现多个客户端获得锁的情况也不会影响数据的一致性。当然,具体使用可以根据自身的应用场景进行取舍。
Redis 里面有1亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?
keys
指令可以扫出指定模式的 key 列表,但是若这个 redis 正在给线上的业务提供服务,那使用 keys
指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复scan
指令,scan
指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys
指令长我们都知道,只要我们使用redis,就会遇到缓存与数据库的双存储双写,那么只要是双写,就一定会有数据一致性问题,为了保证双写一致性,我们提供了以下几种解决方案:
流程如下图所示:
这个休眠一会,一般多久呢?都是1秒?
这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
这种方案还算可以,只有休眠那一会(比如就那1秒),可能有脏数据,一般业务也会接受的。但是如果第二次删除缓存失败呢?缓存和数据库的数据还是可能不一致,对吧?给Key设置一个自然的expire过期时间,让它自动过期怎样?那业务要接受过期时间内,数据的不一致咯?还是有其他更佳方案呢?
因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次,保证删除缓存成功就可以了,所以可以引入删除缓存重试机制。
流程如下图所示:
重试删除缓存机制有一个缺点,就是会造成好多业务代码入侵。其实,还可以这样优化:启动一个订阅程序去订阅数据库的 binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
流程如下图所示:
以 mysql 为例:可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面,然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。