基础
redis为什么快呢?
- 单线程
- 基于io多路复用
- 底层C语言对数据结构做了优化
- 完全内存的操作
Redis6.0使用多线程是怎么回事?
Redis不是说用单线程的吗?怎么6.0成了多线程的?
Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。
持久化
redis持久化方式有哪些?有什么区别?
持久化分为rdb和aof两种。
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。分别使用命令save或者bgsave。
同时rdb是一个二进制的压缩文件,
以下几个场景会自动触发rdb持久化
- 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
- 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
- 执行debug reload命令重新加载Redis时,也会自动触发save操作
- 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的,整体工作过程为:
- 所有的写入命令会追加到aof_buf(缓冲区)中。
- AOF缓冲区根据对应的策略向硬盘做同步操作。
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩 的目的。
- 当Redis服务器重启时,可以加载AOF文件进行数据恢复。
rdb和aof各自有什么优缺点?
rdb优点:
- 只有一个紧凑的二进制文件 dump.rdb,非常适合备份、全量复制的场景。
- 容灾性好,可以把RDB文件拷贝道远程机器或者文件系统张,用于容灾恢复。
- 恢复速度快,RDB恢复数据的速度远远快于AOF的方式
rdb的缺点:
- 实时性低,RDB 是间隔一段时间进行持久化,没法做到实时持久化/秒级持久化。如果在这一间隔事件发生故障,数据会丢失。
- 存在兼容问题,Redis演进过程存在多个格式的RDB版本,存在老版本Redis无法兼容新版本RDB的问题。
aof优点:
- 实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
- 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
aof缺点:
- AOF 文件比 RDB 文件大,且 恢复速度慢。
- 数据集大 的时候,比 RDB 启动效率低。
rdb和aof如何选择?
- 如果想达到足以媲美数据库的 数据安全性,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- 如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化。
- 有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
- 如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。
redis的数据恢复如何做到的?
AOF
持久化开启且存在AOF
文件时,优先加载AOF
文件。AOF
关闭或者AOF
文件不存在时,加载RDB
文件。- 加载
AOF/RDB
文件成功后,Redis
启动成功。 AOF/RDB
文件存在错误时,Redis
启动失败并打印错误信息。
redis4.0的持久化了解嘛?
将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
缓存问题以及解决思路
缓存穿透
尽管我们将数据库中某些数据换到到内存中,但是若有些攻击者使用一些数据库中不存在的key进行恶意攻击,这时候,所有的查询请求就像穿透了缓存中间件一样直接在数据库中进行查询操作,在高并发场景,这样的攻击就会使得数据压力过大,从而导致数据库性能瓶颈。
- 使用过滤器,我们可以使用布隆过滤器来减少对数据库的请求,布隆过滤器的原理是将数据库的数据哈希到 bitmap 中
(在initialBean阶段将数据缓存到内存中)
,每次查询之前,先使用布隆过滤器过滤掉一定不存在的无效请求,从而避免了无效请求给数据库带来的查询压力。 - 缓存空结果,我们可以把每次从数据库查询的数据都保存到缓存中,为了提高前台用户的使用体验
(解决长时间内查询不到任何信息的情况)
,我们可以将空结果的缓存时间设置得短一些,例如 3~5 分钟,但是有可能导致数据一致性问题,所以我们建议查询或者更新的时候要对这个类型的缓存上个锁进行进一步的操作。
缓存击穿
和上述问题情况一样,也是缓存中查不到用户数据,大量请求打到数据库上,但是这种情况的发生原因却非恶意攻击者所为,原因大抵如下:
1. 大量用户查询的某个数据,刚刚好在缓存中过期。
2. 大量用户查询的值都在数据中,缓存中没有。
解决策略
- 加锁排队。
- 设置热点数据永不过期。
缓存雪崩
大量缓存数据同一时间到期,所有查询一下子都打到数据库上。导致数据库压力过大进而直接宕机。
解决策略
- 加锁排队,示例代码如下所示,如果数据库中没有值的话直接上锁到数据库查在放到缓存中,有点类似于单例模式的双重锁校验。
// 缓存 key
String cacheKey = "userlist";
// 查询缓存
String data = jedis.get(cacheKey);
if (StringUtils.isNotBlank(data)) {
// 查询到数据,直接返回结果
return data;
} else {
// 先排队查询数据库,再放入缓存
synchronized (cacheKey) {
data = jedis.get(cacheKey);
if (!StringUtils.isNotBlank(data)) { // 双重判断
// 查询数据库
data = findUserInfo();
// 放入缓存
jedis.set(cacheKey, data);
}
return data;
}
}
- 设计缓存时,对缓存设置随机时间
// 缓存原本的失效时间
int exTime = 10 * 60;
// 随机数生成类
Random random = new Random();
// 缓存设置
jedis.setex(cacheKey, exTime + random.nextInt(1000) , value);
缓存污染(缓存空间全满)
某些数据查询一次就被缓存在数据库中,随着时间推移,缓存空间已经满了,这时候redis
就要根据缓存策略进行缓存置换。这就造成没意义的数据需要通过缓存置换策略来淘汰数据,而且还可能出现淘汰热点数据的情况。
解决方案
选定合适的缓存置换策略,而redis
缓存策略主要分三类
不淘汰的
- noeviction (v4.0后默认的):不会淘汰任何过期键,满了就报错,对设置了过期时间的数据中进行淘汰
- volatile-random:随机删除过期key
- volatile-ttl:根据过期时间进行排序,越早过期的数据就优先被淘汰。
- volatile-lru:即最近最少使用算法(推荐),redis的lru缓存置换算法相比传统的算法做了一定优化,根据 maxmemory-samples从缓存中随机取出几个key值,然后进行比较在进行淘汰,这样就避免了缓存置换时需要操作一个大链表进行key值淘汰了。
- volatile-lfu:lru只知晓用户最近使用次数,而不知道该数据使用频率,所以lfu就是基于lru进一步的优化,进行淘汰时随机取出访问次数最少的数据,如果最少的数据有多个,按按照lru算法进行淘汰。但是redis只用8bit记录访问次数,超过255就无法进行自增了,所以我们可以使用
lfu-log-factor
和lfu-decay-time
来用户访问次数增加的频率。 - lfu-decay-time:控制访问次数衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。若设置为0,则意味着每次扫描访问次数都会扣减。
- lfu-log-factor:用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。
全部数据进行淘汰 - allkeys-random:从所有键值对中使用lru淘汰
- allkeys-lru:从所有键值对中随机删除
- allkeys-lfu:从所有键值对中使用lfu随机淘汰
具体可以查看redis配置文件描述
MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用 keys 指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
redis如何保证命令原子性
使用原子命令
- Redis 提供了 INCR/DECR/SETNX 命令,把RMW三个操作转变为一个原子操作
- Redis 是使用单线程串行处理客户端的请求来操作命令,所以当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的
加锁
加锁主要是考虑多个客户端对相同业务方法进行修改操作,我们可以使用加锁的方式保证原子性,大致的方式为:
- 使用setnx上锁
- 上锁成功后,执行业务修改操作
- 使用del释放锁。
这期间你可能会遇到两个问题:
- 假如在操作期间出现了业务异常(或者服务器宕机了),就会导致key未能及时释放,进而导致锁无法释放,我们必须对这个锁设置时效,并且在操作期间定时监测和续命。
SET key value [EX seconds | PX milliseconds] [NX]
- 误删除,比如用户1持有锁,用户2拿不到锁,用del命令把这个锁删除,对此我们可以使用setnx的value比对看看上锁和用户和解锁的用户是不是同一个进行进一步的操作。
SET lock_key unique_value NX PX 10000
- 使用lua脚本:多个操作写到一个 Lua 脚本中(Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性)
local current current = redis.call("incr",KEYS[1])
if tonumber(current) == 1
then redis.call("expire",KEYS[1],60)
end
怎么处理热key
什么是热Key? 所谓的热key,就是访问频率比较的key。
比如,热门新闻事件或商品,这类key通常有大流量的访问,对存储这类信息的 Redis来说,是不小的压力。
假如Redis集群部署,热key可能会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过 Redis本身能够承受的OPS。
怎么处理热key?
热key处理 对热key的处理,最关键的是对热点key的监控,可以从这些端来监控热点key:
客户端 客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。
代理端 像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。
Redis服务端 使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。
只要监控到了热key,对热key的处理就简单了:
把热key打散到不同的服务器,降低压⼒
加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询
缓存预热怎么做?
所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:
- 直接写个缓存刷新页面或者接口,上线时手动操作
- 数据量不大,可以在项目启动的时候自动进行加载(我们目前就是执行这种操作,通过继承InitializingBean实现)
- 定时任务刷新缓存.
热点key重建问题了解过?你是如何解决的呢?
开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
- 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
怎么处理呢?
要解决这个问题也不是很复杂,解决问题的要点在于:
-
减少重建缓存的次数。
-
数据尽可能一致。
-
较少的潜在危险。
所以一般采用如下方式: -
互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
-
永远不过期 “永远不过期”包含两层意思:
-
从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期,注意数据更新后要实时加锁更新。
-
从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
redis运维
redis阻塞问题如何解决
API或数据结构使用不合理
通常Redis执行命令速度非常快,但是不合理地使用命令,可能会导致执行速度很慢,导致阻塞,对于高并发的场景,应该尽量避免在大对象上执行算法复杂 度超过O(n)的命令。
对慢查询的处理分为两步:
发现慢查询: slowlog get{n}
命令可以获取最近 的n条慢查询命令;
发现慢查询后,可以从两个方向去优化慢查询: 1)修改为低算法复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命 令 2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
CPU饱和的问题
单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis单核CPU使用率跑到接近100%。
针对这种情况,处理步骤一般如下:
判断当前Redis并发量是否已经达到极限,可以使用统计命令redis-cli-h{ip}-p{port}--stat
获取当前 Redis使用情况
如果Redis的请求几万+,那么大概就是Redis的OPS已经到了极限,应该做集群化水品扩展来分摊OPS压力
如果只有几百几千,那么就得排查命令和内存的使用
持久化相关的阻塞
对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。
fork阻塞 fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
AOF刷盘阻塞 当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等 待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了 数据安全性它会阻塞直到后台线程执行fsync操作完成。
HugePage写操作阻塞 对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
大key问题了解嘛?
Redis使用过程中,有时候会出现大key的情况, 比如:
单个简单的key存储的value很大,size超过10KB
hash, set,zset,list 中存储过多的元素(以万为单位)
大key会造成什么问题呢?
- 客户端耗时增加,甚至超时
- 对大key进行IO操作时,会严重占用带宽和CPU
- 造成Redis集群中数据倾斜
- 主动删除、被动删等,可能会导致阻塞
如何找到大key?
- bigkeys命令:使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Key
- redis-rdb-tools:redis-rdb-tools是由Python写的用来分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成报表用来分析Redis的使用详情。
如何处理大key?
- 删除大key
当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
- 压缩和拆分key
当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
redis常见的性能问题和解决方案了解嘛?
Master
最好不要做任何持久化工作,包括内存快照和AOF
日志文件,特别是不要启用内存快照做持久化。- 如果数据比较关键,某个
Slave
开启AOF
备份数据,策略为每秒同步一次。 - 为了主从复制的速度和连接的稳定性,
Slave
和Master
最好在同一个局域网内。 尽量避免在压力较大的主库上增加从库。 Master
调用BGREWRITEAOF
重写AOF
文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。- 为了
Master
的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…
,这样的结构也方便解决单点故障问题,实现Slave
对Master
的替换,也即,如果Master
挂了,可以立马启用Slave1
做Master
,其他不变。
redis的应用
redis管道了解嘛?
Pipelining(管道)
Redis 管道是三者之中最简单的,当客户端需要执行多条 redis
命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time)
对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis
服务端。
Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。 Pipelining示意图`
Pipelining的优势
在性能方面, Pipelining 有下面两个优势:
节省了RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。
redis分布式锁了解嘛?
Redis是分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
V1:setnx命令
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。 setnx(set if not exists)
> setnx lock:fighter true
OK
... do something critical ...
> del lock:fighter
(integer) 1
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
V2:锁超时释放
所以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。 锁超时释放
> setnx lock:fighter true
OK
> expire lock:fighter 5
... do something critical ...
> del lock:fighter
(integer) 1
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。
V3:set指令
这个问题在Redis 2.8 版本中得到了解决,这个版本加入了 set 指令的扩展参数,使得 setnx 和expire 指令可以一起执行。 set原子指令
set lock:fighter3 true ex 5 nx OK ... do something critical ... > del lock:codehole
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,这个就算是比较完善的分布式锁了。
当然实际的开发,没人会去自己写分布式锁的命令,因为有专业的轮子——Redisson。
如何保证redis和数据库一致性
- 可以引入消息队列,把要删除的key或者删除失败的key丢尽消息队列,利用消息队列的重试机制,重试删除对应的key。但是对业务有侵入性。
- 数据库订阅+消息队列保证key被删除 可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据,但是实现比较复杂。
- 延时双删,简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。更新数据库期间(缓存还没更新),某个线程读取到旧的缓存。
- 设置key的时效
(这种我们比较常用,我们的缓存基本都是配置数据,或者一些用户个人信息接口,数据基本不会有太大的变化)
参考文献
Redis常见面试题总结(下)
Redis进阶 - 缓存问题:一致性, 穿击, 穿透, 雪崩, 污染等
Redis 缓存雪崩、缓存穿透、缓存击穿、缓存预热
【Redis】如何保证原子操作
面渣逆袭(Redis面试题八股文)必看👍