主题
redis 面试题
1.redis的使用场景
1、会话缓存(Session Cache)
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务
器,从而更容易实现高可用性以及可伸缩性。
2、全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress
的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以
最快速度加载你曾浏览过的页面。
3、分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现
4、排行榜/计数器
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
5、消息队列(发布/订阅)
List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用Kafka、RabbitMQ 等消息中间件。
2.redis常见的数据结构

2.1String 字符串命令
| set key value | 字符串键值对 |
|---|---|
| mset key value [key value...] | 批量存入字符串键值对 |
| get key | 获取字符串键对应的值 |
| mget key [key...] | 批量获取键值对应的值 |
| del key [key...] | 删除一个键或者多个键 |
| expire key seconds | 设置一个键的过期时间 |
| incr key | 将key中存储的数值加1 |
| decr key | 将key中存储的数减1 |
| incrby key increment | 将key所存储的值加上increment |
| decrby key decrement | 将key所存储的值减去decrement |
| 单值缓存 set key value对象缓存set key json value(对象的json格式)setnx key value 分布式锁,返回0代表获取锁失败,返回1代表成功获取锁 |

2.2hah常用的命令
| hset key filed value | 存储一个哈希表key的键值 |
|---|---|
| hsetnx key filedvalue | 存储一个不存在的哈希表的key的键值 |
| hmset key filed value [filed] | 在一个哈希表key中存储多个键值对 |
| hget key filed | 获取哈希表key对应的field的值 |
| hmget key field [field...] | 批量获取哈希表key对应的field的值 |
| hdel key field [field...] | 批量删除哈希表key对应的field的值 |
| hlen key | 返回哈希表key中field的数量 |
| hgetall key | 返回哈希表key中所用key的键值 |
| hincrby key field increment | 为哈希表key中field键的值加上增量increment |
| 对象缓存hmset user name zhangsan age 20 |

hash的优点
同类数据归类整合存储,方便数据管理
相比string操作消耗内存与cpu更小
相比string存储更节省空间
hash缺点
过期功能不能使用在field上,只能用在key上
redis集群架构下不适合大规模使用
2.3list链表常用操作
| lpush key value [value...] | 将一个或者多个值value插入到key列表的表头最左边 |
|---|---|
| rpush key value [value...] | 将一个或者多个值value插入到key列表的表尾最右边 |
| lpop key | 移除并返回key列表的头元素 |
| rpop key | 移除并返回key列表的尾元素 |
| lrange key start stop | 返回key列表内指定区间的元素,偏移量start到stop |
| blpop key [key...] timeout | 从key列表表头弹出一个元素,若列表中没有,则阻塞等待 |
| brpop key [key...] timeout | 从key列表表尾弹出一个元素,若列表中没有,则阻塞等待创建新队列。设置为0 阻塞的原因是因为队列里元素为空(brpop L 0)设置为0 。当队列元素不为空时。brpop L 0 就会将元素取出来 |
![]() | 常用数据结构 |
| stack 栈 = lpush + lpop | |
| queue 队列 = lpush + rpop | |
| blocking mq(阻塞队列)= lpush + brpop |

2.4、set不重复的数据集合
| sadd key member [member...] | 往集合中添加元素,存在则忽略,key不存在则新建 |
|---|---|
| srem key member [member...] | 从集合key中删除元素 |
| scard key | 获取集合中key的元素个数 |
| smembers key | 获取集合key中的所有元素 |
| sismember key member | 判断集合中是否包含member元素 |
| srandmember key [count] | 从集合中选出count个元素,元素不从集合中删除 |
| spop key | 从集合中选出1个元素,元素从key中删除 |
| 集合相关操作 | |
| sinter key [key....] | 求集合的交集 |
| sinterstore destination key [key....] | 将交集的结果存入新集合destination中 |
| sunion key [key....] | 并集运算 |
| sunionstore destination key [key....] | 将并集的结果存入新集合destination中 |
| sdiff key [key....] | 差集运算,key有 key...没有的 |
| sdiffstore destination key [key....] | 将差集的结果存入新集合destination中 |

2.5、sorted set有序的set集合
| zadd key score member [score member...] | 向集合中添加带分值的元素 |
|---|---|
| zscore key member | 返回集合中member元素的分数 |
| zrem key member[key member.....] | 从集合中删除元素 |
| zincrby key increament member | 为有序集合member元素加上分数increment |
| zcard key | 返回集合key中元素的个数 |
| zrange key start stop | 正序获取有序集合元素,偏移量为start到stop |
| zrevrange key start stop | 反序获取有序集合元素,偏移量为start到stop |
| 集合操作 | |
| zunionstore destkey numkeys key [key...] | 并集计算 |
| zinterstore destkey numkeys key [key....] | 交集计算 |


3.redis持久化
redis支持RDB和AOF两种持久化机制,在redis4.0之后支持混合持久化,持久化功能有效的避免了因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可以实现数据恢复。
1、RDB持久化
在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功之后再替换之前的文件,用二进制压缩存储。
fork一个子进程的原因:如果不fork子线程去处理,那么就会阻塞redis的请求 也就读、写,fork子线程就相当于异步处理,对redis的请求的读写没啥影响,影响只是fork子线程的那么一瞬间。
在默认情况下,redis将内存数据库快照保存在名字为dump.rdb的二进制文件中,你可以对二进制进行设置,让他在"N秒内数据集至少有M个改动",这一条件被满足时,自动保存一次数据集。
java
#在60秒内至少有1000个键被改动,则自动保存一次数据集
save 60 1000
#关闭RDB只需将所有的save保存策略注释即可bgsave和save的区别
| 命令 | save | bgsave |
|---|---|---|
| IO类型 | 同步 | 异步 |
| 是否阻塞redis其他命令 | 是 | 否(在fork子进程执行调用fork函数时短暂阻塞) |
| 优点 | 不消耗额外内存 | 不阻塞客户端命令 |
| 缺点 | 阻塞客户端命令 | 需要fork子进程,消耗内存 |
触发条件
rdb持久化的触发分为自动触发和手动触发两种,每次执行命令都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。
手动触发
手动触发分别对应save和bgsave命令:
save命令:会阻塞当前Redis服务器的进程,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用,再redis服务器阻塞期间,服务器不能处理任何命令请求。
bgsave命令:fork一个子进程,RDB持久化过程由子进程负责,完成后自动结束。父进程则继续处理请求。堵塞只发生在fork阶段,一般时间很短;BGSAVE命令是针对SAVE堵塞问题做的优化。
自动触发
save m n:在配置文件中进行配置,指定当m秒内发生n次变化,会触发bgsave

- 在主从复制场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件发送给从节点
- 执行shutdown命令时,自动执行rdb的持久化
save m n 的实现原理
是将数据先储存在内存,然后当数据累计达到某些设定的阈值的时候,就会触发一次dump操作,将变化的数据一次写入数据文件。
redis的save m n是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。
- dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。 例如,如果Redis执行了set mykey helloworld,则dirty值会+1;如果执行了sadd myset v1 v2 v3,则dirty值会+3;注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令.
- serverCron是redis服务器的周期性操作函数,默认每隔100ms执行一次,该函数对服务器的状态进行维护,其中一项工作就是检查save m n配置的条件是否满足,满足则触发bgsave
- lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。
save m n的原理如下:每隔100ms,执行serverCron函数;在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save m n条件,只有下面两条同时满足时才算满足:
(1)当前时间-lastsave > m
(2)dirty >= n
bgsave的执行过程

1).redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof的子进程不能同时执行,主要是基于性能方面的考虑,两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
2).父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,redis不能执行来自客户端的任何命令。
3).父进程fork后,bgsave命令返回“background saving started”信息并不在阻塞父进程,并可以响应其他命令。
4).子进程创建rdb文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换
5).子进程发送信号给父进程表示完成,父进程更新统计信息
RDB的优点:
- 整个Redis数据库只包含一个文件dump.rdb,方便持久化。
- 容灾性好,方便备份。
- 性能最大化,fork子进程来完成写操作,让子进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能。
- 相对于数据集大时,比AOF的启动效率更高。
RDB的缺点:
- 数据安全性低。RDB是间隔一段时间进行持久化,如果持久化期间发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
2、AOF持久化
RDB快照持久化数据可用并不是很高;当redis因为某种原因造成故障宕机,则服务器将丢失最近写入,且仍未保存到快照中的那些数据。在1.1版本后,redis增加了AOF持久化,AOF是redis持久化的重要手段之一,将修改的每一条指令记录到appendonly.aof文件中(先写入os cache,每隔一段时间fsync到磁盘)。
aof开启
redis服务器默认开启rdb,关闭奥法,要开启aof,需要在配置文件中配置

java
appendonly yes执行流程

由于需要记录redis每条写命令,因此aof不需要触发,aof的执行流程如下:
命令追加 - 将redis的写命令追加到缓冲区aof_buf
文件写入和文件同步 - 根据不同的同步策略将aof_buf中的内容同步到磁盘。
文件重写 - 定期重写aof文件,达到压缩的目的。
1、命令追加
redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入磁盘,导致硬盘IO成为redis负载的瓶颈。
2、文件写入和文件同步
redis提供了多种aof缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数
注意:为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。
java
#aof缓存区的同步文件策略由参数appendfsync控制
always:命令写入aof_buf后,直接调用系统的fsync操作同步到AOF文件。fsync完成后线程返回,安全但是性能低,redis只能支持大约几百tps写入,即便是固态硬盘,每秒大约可除了几万个命令
everysec:命令写入aof_buf后,调用系统的write操作,write操作完成后线程返回。fsync同步文件操作由专门线程每秒调用一次。
no:命令写入aof_buf后调用系统的write操作,write完成后线程返回;不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒。这种情况下,文件同步时间不可控,可能造成大量数据丢失。3、文件重写

AOF的优点:
- 数据安全,redis中提供了三种同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也 是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据 将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁 盘中。
- 通过 append 模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis- check-aof 工具解决数据一致性问题。
- AOF 机制的 rewrite 模式。定期对AOF文件进行重写,以达到压缩的目的
AOF的缺点:
- AOF 文件比 RDB 文件大,且恢复速度慢。
- 数据集大的时候,比 rdb 启动效率低。
- 运行效率没有RDB高
3、方案选择与常见问题
| 命令 | RDB | AOF |
|---|---|---|
| 启动优先级 | 低 | 高 |
| 体积 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 安全性 | 易丢失数据 | 根据策略定 |
如果既开启了rdb持久化又开启了aof持久化,则redis启动的时候优先加载aof文件,因为aof文件更安全一些,但是因为aof加载速度慢,故redis4.0支持混合持久化
redis4.0混合持久化
重启redis时,我们很少使用RDB来恢复内存状态,因为会丢失大量数据,我们通常会使用AOF日志重放,但是重放AOF日志相对于RDB来说要慢很多,这样在redis实例很大的情况下,启动需要花费很长时间。redis4.0为了解决这个问题,带来了一个新的持久化选项——混合持久化。
通过如下配置可以开启混合持久化
java
aof-use-rdb-preamble yes如果开启混合持久化,aof在重写时,不再是单纯讲内存数据转换未resp命令写入aof文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量aof修改内存数据的命令存在一起,都写入新的aof文件,新的文件一开始不叫appendonly.aof,等到重写完新的aof文件才会进行改名,覆盖原有的aof文件。
于是在redis重启的时候,可以先加载RDB的内容,然后在重放增量aof日志就可以完全替代之前的aof全量文件重放,因此重启效率大幅提升。
混合持久化文件结构

4.redis的淘汰策略
一、概述
redis的内存淘汰策略是指redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
二、redis的内存淘汰策略有哪些?
全局的键空间选择性移除
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入的数据时,在键空间中,移除最近最少使用的key。(这个是最常用的。)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
三、总结
redis的内存淘汰策略的选取并不影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
5.redis使用时的注意事项
1、key-value设计
1.1key名设计
- 可读性、可管理性:项目名/数据库名:模块名/表名:id
java
TRADE:ORDER:100000- 简洁性:设计不要过于复杂
- 强制包含特殊字符,如空格、换行、当双引号以及其它转义字符。
1.2value设计
- 强制禁止使用bigkey(防止网卡流量、慢查询)
在redis中,一个字符串最大是512MB,一个二级数据结构(hash、list、set、zset)可以存储大约40亿数据
(2^32-1),但是在实际应用中并非如此。
1)、字符串类型:当一个value值大于10kb的时候,就认为是bigkey。
2)、非字符串类型:hash、list、set、zset集合个数建议不超过5000个。
非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如:一个200万的zset设置1个小时过期,会触发del操作,造成阻塞)
bigkey的危害
- 导致redis阻塞
- 网络阻塞
bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问
量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务
器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey
可能会对其他实例也造成影响,其后果不堪设想。
- 过期删除
如果未使用redis4.0的过期异步删除(lazyfree-lazy-expire yes)就会存在阻塞redis的可能性
- bigkey的产生与优化
产生
一般来说,bigkey的产生都是由于程序设计不当,或者是对数据规模预料不清
优化
分段存储
选择合适的数据类型,如hmset可以用双key代替set的单key
- 控制key的生命周期,尽量打散过期时间,防止集中过期。
2、命令使用
1、O(N)命令关注N的数量
例如 hgetall、lrange、smembers、zrange、sinter 等并非不能使用,但是需要明确 N 的值。有遍历的需求可 以使用 hscan、sscan、zscan 代替
2、禁用命令
禁止线上使用 keys、flushall、flushdb 等,通过 redis 的 rename 机制禁掉命令,或者使用 scan 的方式渐进 式处理。
3、合理使用select
redis 的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。
4、使用批量操作提高效率
原生命令:例如 mget、mset。
非原生命令:可以使用 pipeline 提高效率。
java
原生是原子操作,pipeline是非原子操作。
pipeline可以打包不同的命令,原生做不到
pipeline需要客户端和服务端同时支持。但要注意控制一次批量操作的元素个数(例如 500 以内,实际也和元素字节数有关)。
5、 Redis 事务功能较弱,不建议过多使用
Redis 的事务功能较弱 (不支持回滚),而且集群版本 (自研和官方) 要求一次事务操作的 key 必须在一个 slot 上 (可以使用 hashtag 功能解决) hashtag的解决方案:可以使用twitter的 twemproxy
6、Redis 集群版本在使用 Lua 上有特殊要求
所有 key 都应该由 KEYS 数组来传递,redis.call/pcall 里面调用的 redis 命令,key 的位置,必须是 KEYS array, 否则直接返回 error
java
"-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array"所有 key,必须在 1 个 slot 上,否则直接返回 error
java
-ERR eval/evalsha command keys must in same slot3、客户端的使用
- 避免多个应用使用一个 Redis 实例
例:不相干的业务拆分,公共数据做服务化。
- 使用带有连接池的数据库,可以有效控制连接,同时提高效率,标准使用方式
java
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(5);
jedisPoolConfig.setMaxIdle(2);
jedisPoolConfig.setTestOnBorrow(true);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
// 具体的命令
jedis.executeCommand()
} catch (Exception e) {
logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
// 注意这里不是关闭连接,在 JedisPool 模式下,Jedis 会被归还给资源池。
if (jedis != null)
jedis.close();
}| 参数名 | 含义 | 默认值 | 使用建议 |
|---|---|---|---|
| maxTotal | 资源池中最大的连接数 | 8 | |
| maxIdle | 资源池允许最大空闲连接数 | 8 | |
| minIdle | 资源池最少空闲连接数 | 0 | |
| blockWhenExhausted | 当资源池用尽后,调用者是否要等待,为true时,下面的maxWaitMillis才会生效 | true | 建议使用默认值 |
| maxWaitMillis | 当资源池用尽后,调用者的最大等待时间(单位毫秒) | -1 表示永不超时 | 不建议使用默认值 |
| testOnBorrow | 向资源池申请连接时,是否做连接有效性检查(ping),无效连接会被移除 | false | 业务量很大时建议设置false,多一次ping开销 |
| testOnReturn | 向资源池归还连接时是否做连接有效性检查(ping),无效连接会被移除 | false | 业务量很大时建议设置false,多一次ping开销 |
| jmxEnabled | 是否开启jmx监控 | true | 建议开启,应用本身也开启 |
- 高并发下建议客户端添加熔断功能 (例如 netflix hystrix)
- 设置合理的密码,如有必要可以使用 SSL 加密访问(阿里云 Redis 支持)
4、redis过期键清除策略
- 被动删除:当读或写一个已经过期的key时,会触发惰性删除策略,直接删除这个过期key
- 主动删除:由于惰性删除无法保证冷数据被及时删除,所以redis会定期主动淘汰一批过期key
- 当前已用内存超过maxmemory限定时,触发主动清理策略,redis4.0之前一共实现了6种内存淘汰策略,4.0后又增加了2种。如下:主动清理策略
主动清理策略
A)、针对设置了过期时间的key做处理
- volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删
除,越早过期的越先被删除。
- volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除
- volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
- volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除
B)、针对设置了过期时间的key做处理
- allkeys-random:从所有键值对中随机选择并删除数据。
- allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
- allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。
C) 不处理
noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error), OOM command not allowed when used memory",此时Redis只响应读操作。
LRU算法 - least recently used 最近最少使用
淘汰很久没被访问过的数据,以最近一次访问时间作为参考。
LFU算法 - least frequently used 最不经常使用
淘汰最近一段时间被访问次数最少的数据,以次数作为参考
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下
降,缓存污染情况比较严重。这时使用LFU可能更好点。
根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如
果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交
换 (swap),会让 Redis 的性能急剧下降。
当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同
步到从结点删除数据。
6.数据库缓存的一致性
缓存读取流程

如何解决缓存一致性问题
1.先更新数据库在更新缓存(个人认为不合适)
不合适的原因:
原因一:从线程安全角度考虑:
如果有A、B两个用户同时请求,进行更新操作,则会出现下列问题:
- A更新了数据库
- B更新了数据库
- B更新了缓存
- A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
原因二:从业务场景去考虑:
在写操作频繁、读操作少的情况下,采用这种方案就会导致,数据还没读到,缓存就被频繁的更新,浪费性能。
2.先删除缓存,再更新数据库(会出现数据不一致的情况)
导致数据不一致的原因:
同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现下列问题:
- 请求A进行写操作,删除缓存
- 请求B查询发现缓存不存在
- 请求B去数据库查询得到的是旧的值
- 请求B将旧值写入缓存
- 请求A将新值写入数据库
上述情况就会出现数据不一致的情形。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
如何解决数据不一致的问题
采用延时双删策略:
- 先淘汰缓存
- 再写数据库
- 休眠一秒,再次淘汰缓存。
伪代码如下:
java
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}休眠一秒,可以将一秒内造成的缓存脏数据再次删除。
采用双删策略,出现吞吐量降低怎么处理?
将第二次删除作为异步的,new 一个新得线程,做异步删除处理。这样加大了吞吐量。
如果第二次删除失败了,那该怎么处理?
假设再同一个数据库中,同时有一个请求A进行更新操作,另一个请求B进行查询操作。
- 请求A进行写操作,删除缓存
- 请求B查询发现缓存不存在
- 请求B去数据库查询得到旧值
- 请求B将旧值写入缓存
- 请求A将新值写入数据库
- 请求A试图去删除请求B写入对缓存值,结果失败了。
怎么解决这种问题呢?(请看第三种方案)
3.先更新数据库,再删除缓存
- 先从cache中取到数据,如果未取到数据;则数据库中取数据,成功之后再放入缓存中。
- 如果从cache中取到了数据,取到数据后返回。
- 如果数据库里也没有数据,则先把数据存放到数据库中,成功后再让缓存失效。
那么缓存删除失败了,该怎么办呢??同样还是会出现缓存不一致的情况。那该如何处理呢?(依然没有好的方案来解决缓存一致性问题的方案)
7.缓存雪崩、缓存穿透、缓存击穿
1、缓存穿透
概念
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且处于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求的时候都到存储层去查询,这样就失去了缓存的意义。
原因
- 自身业务代码或者是数据问题
- 恶意攻击,爬虫等造成大量空命中
解决方案
1、接口校验
在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。
2、缓存空值
当访问缓存和DB都没有查询到值得时候,可以将空值写入缓存,但是设置较短得过期时间,该时间需要根据产品得特性来设置。
java
String getValue(String key){
//从缓存中获取数据
String cacheValue = cache.get(key);
//缓存为空
if(cacheValue==null){
//从存储中获取
String storageValue = storage.get(key);
if(storageValue==null){
cacahe.set(key,storageValue);
}else{
cache.expire(key," ");
}
//需要设置一个过期时间(300秒)
cache.expire(key,60*5);
return storageValue;
}else{
//缓存非空
return cacheValue;
}
}3、布隆过滤器
对于恶意攻击,像服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器都能过滤掉,不让其请求后端。当布隆过滤器说某个值存在时,这个值可能不存在;当他说不存在时没那就肯定是不存在。

用redission实现布隆过滤器
java
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>java
//布隆过滤器使用
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient client = Redisson.create(config);
//设置一个名称为NAME_LIST的布隆过滤器
RBloomFilter<String> bloomFilter = client.getBloomFilter("NAME_LIST");
//预计元素个数为10000000L,误差率为3%,根据这两个参数会计算出底层bit数据大小
bloomFilter.tryInit(100000000L, 0.03);
bloomFilter.add("大师兄");
System.out.println(bloomFilter.contains("二师兄")); //false
System.out.println(bloomFilter.contains("大师兄")); //true
}使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放,布隆过滤器缓存过滤伪代码
java
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient client = Redisson.create(config);
//设置一个名称为NAME_LIST的布隆过滤器
RBloomFilter<String> bloomFilter = client.getBloomFilter("NAME_LIST");
//预计元素个数为10000000L,误差率为3%,根据这两个参数会计算出底层bit数据大小
bloomFilter.tryInit(100000000L, 0.03);
//获取数据库中的数据
List<String> strList = dao.getStringList();
for(String str : strList){
bloomFilter.add(str);
}
//使用布隆过滤器过滤
// 从布隆过滤器这一级缓存判断下key是否存在
Boolean exist = bloomFilter.contains(key);
if(!exist){
return "";
}
//继续执行查询缓存的逻辑
}注意:布隆过滤器不能删除数据,如果需要删除数据得重新初始化数据。
对于缓存穿透上述三种方案似乎并不能完全解决问题,对于安全性来说,还应该控制同一ip不能连续访问,同一用户不能 连续访问 -- 需要针对某具体场景具体考虑设计方案
2、缓存击穿
某一热点key,由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量新增缓存时最好将这批数据的缓存过去时间设置为一个时间段内的不同时间。(缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。)
java
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//设置一个过期时间(300到600之间的一个随机数)
int expireTime = new Random().nextInt(300) + 300;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}预防缓存击穿
- 设置缓存永不过期
- 使用分布式锁或jvm锁
- 限流(服务层限流,数据层限流)
3、缓存雪崩
大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库宕机。缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。(缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。)
预防缓存雪崩
- 保证缓存高可用,比如使用redis sentinel(redis 哨兵模式)或redis cluster(redis主从模式)
- 依赖隔离组件为后端限流熔断或者降级
- 提前演练,在项目上线前,演练缓存宕机后,应用及后端可能出现的问题。
解决方案
1、过期时间打散
既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
java
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//设置一个过期时间(300到600之间的一个随机数)
int expireTime = new Random().nextInt(300) + 300;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}2、使用分布式锁或者JVM锁
JVM 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好。
Redis 分布式锁,因为这个可以保证只有一个请求会走到数据库,这是一种思路。
8.redis集群原理
一、搭建集群的原因?
为什么要搭建redis集群?因为redis是在内存中保存数据的,但是电脑的内存有限,这就意味着redis不适合存储大数据,适合存储大数据的是Hadoop生态系统的Hbase或者mongodb。redis更适合处理高并发,一台设备的存储能力是有限的,多台设备合作,可以让内存增大很多倍,这里就需要用到集群了。
redis搭建集群的方式有很多种,eg:使用客户端分片、Twemproxy、Codis等、自redis 3.0之后版本支持redis-cluster集群,它是redis官方提出的解决方案。
二、redis-cluster产生背景
自动对数据进行分片,落到各个节点上
即使集群部分分布节点失效或者连接不上,依然可以继续处理命令
三、概述
redis哨兵在进行主从切换时,存在访问瞬断的情况,而且哨兵只有一个主节点对外提供服务,无法支持高并发,单节点内存也不宜设置过大,否则会导致持久化文件过大,影响数据恢复和主从同步;redis-cluster采用无中心结构,节点之间使用gossip协议进行通信,每个节点保存数据和所有节点与槽的映射关系,架构图如下:

redis集群是一个由多个主从节点群组成的分布式服务器群,它具有主从复制、高可用、分片的特性,redis集群不需要sentinel哨兵也能完成节点移除和故障转移等功能,需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩张,据官网文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。
四、redis-cluster数据分片原理
简单的哈希算法
假设只有三台机械ABC,数据落在哪台机械的算法为
java
no = hash(key) % num
//如果key = 0, 则落在A机械上, key = 4 则落在B机械上如果数据量很大,三台机械不足以支撑目前的业务,需要加台机械D,那么在利用哈希算法key=4(4%4=0),此时key为4的数据需要落在D机械上,但是机械D根本没有该值;所以无法查到该值;所以简单的hash算法再增加或者减少机械的时候,会引起大量的缓存击穿或者雪崩。
一致性哈希算法
基本原理
为了解决分布式缓存问题,1997年麻省理工学院karger等人提出一致性哈希算法,在一致性哈希算法中,整个哈希空间就是一个虚拟环,假设有四个节点ABCD,经过ip地址的哈希计算,他们的位置如下

假设有object A、object B、object C、object D,经一致性哈希计算,他们的位置如下,而对于各个object,它真正存储的位置是按_顺时针找到的第一个存储节点_,object A则存储在node A上,以此类推

容错性与扩展性
假如node C节点挂了,object c的存储丢失,那么就会顺时针找到新节点node D,也就是说node c 挂了,node B和node C之间数据回收会影响并转移到node D上进行存储;
假如说数据量太大,需要新增节点,同样会按照上述原理进行数据转移
缺点
一致性哈希算法虽然很好的解决了容错性和扩展性,但是会造成一个严重的问题,就是数据倾斜

redis的哈希算法 crc16(哈希槽)
概念
redis集群使用哈希槽实现,其对key进行哈希计算,采用crc16(key)&0x3fff,每个key通过计算后都会落在具体的槽位上,每个节点负责一部分槽位,槽位的信息存储于每个节点中。
当redis-cluster 的客户端来连接集群时,它会得到一份集群槽位配置信息并将其缓存到客户端本地,这样当客户端要查询某个key时,可以直接定位到目标节点,同时因为槽位的信息可能存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息校验调整。
跳转重定向
当客户端向一个错误的节点发出指令,该节点会发生指令的key所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连接这个节点获取数据,客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有的key将使用新得槽位映射表。

哈希槽与一致性哈希算法的区别
哈希槽的本质上与一致性算法非常相似,不同点就是哈希空间的定义,一致性哈希的空间是一个圆环,节点分布是基于圆环的,而redis哈希槽空间是自定义分配的(两层映射关系:key-槽的映射,槽-节点映射),用户可以自定义哈希槽与节点的映射关系。
容错性与扩展性
表象上与一致性哈希一致,都是对受影响的数据转移;而哈希槽的本质是对槽位的转移,并未改变key-槽的映射关系。
注意:对于槽位的转移和分配,redis集群是不会自动进行的,而是需要人工操作,而且redis的高可用是依赖于节点的主从复制和主从之间的自动故障转移。
Redis集群间通信机制
维护集群的元数据(集群节点信息、主从角色、节点数量,各节点共享的数据等)有两种方式:集中式和gossip
- 集中式
优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其它节点读取的时候立即就可以感知到,不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力,很多中间件都会借助zookeeper集中式存储元数据
- 分散式
本质就是去中心化的分布式协议,利用一种随机的方式将信息传播到整个网络中,并在一定时间内是的系统内所有节点数据一致,常用协议gossip。
gossip通信的端口:
rediscluster采用gossip通信协议,默认端口为自己提供服务的端口号+10000,每个节点每隔一段时间就会往另外几个节点发送ping消息,同时其他几点接收到ping消息之后返回pong消息。
网络抖动
真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见 的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。
为解决这种问题,Redis Cluster 提供了一种选项cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频 繁切换 (数据的重新复制)。
redis集群选举原理
当slave发现自己的master变为fail状态时,便尝试进行选举,以期成为新的master,由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程:
- slave发现master变为fail
- 将自己记录的集群currentEpoch + 1,并广播FAILOVER_AUTH_REQUEST信息
- 其他节点收到该信息(只有master响应),判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
- 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
- slave收到超过半数master的ack后变成新的master(这里解释redis集群至少有三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点则不能选举成功)
- slave广播pong消息通知其它集群节点
注意:从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待 FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票
延迟公式: DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms -- random 随机数,防止同一时间发起选举 -- SLAVE_RANK是一个与master复制数有关的值,具有最新复制时SLAVE_RANK值为0,第二则为1,以此类推,理论上 持有最新数据的slave将会首先发起选举
集群脑裂
redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。
规避方法可以在redis配置里加上参数(这种方法不可能百分百避免数据丢失,参考集群leader选举机制)
min‐slaves‐to‐write 1 //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集 群总共三个节点可以配置1, 加上leader就是2,超过了半数
注意:这个配置在一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能 提供服务了,需要具体场景权衡选择
发生脑裂的原因:
网络不好,发生网络分区
选主的时候,没有配置过半机制
leader网络中断,其它节点发起选举,广播结束之后,leader又恢复啦。
9.redis为什么快、redis性能为什么高(读、写)
1.redis完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于hashMap,hashMap的优势就是查找和操作的时间复杂度都是O(1);
2.redis数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的。
3.redis对数据的操作是单线程的,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
4.redis使用多路I/O复用模型,非阻塞IO
5.redis使用resp协议,实现简单、解析快、人类可读
10.redis缓存(分布式缓存)与本地缓存的区别
1、特点:
本地缓存存储在本机jvm,只能本机自己访问
分布式缓存单独服务器存储,可以供多个节点使用
2、性能
本地缓存由于不需要跨网络传输,所以性能更高
3、可维护性
分布式缓存,由于需要引入第三方框架,需要单独节点部署,所以可维护性难度大
4、使用难度
分布式缓存,如redis提供了丰富的数据结构,也提供了缓存淘汰策略、过期清理策略,使用起来方便简单。
本地缓存需要借助一些框架如guava或者自己开发
5、数据一致性、
本地缓存由于缓存只有一份,所以只需要维护缓存与数据库一致性即可
分布式缓存由于每个节点都有自己的缓存,所以既需要维护缓存与数据库的一致又需要维护每个节点之间的一致性。
6、数据安全
分布式缓存有持久化机制,宕机重启后,数据可以恢复,本地缓存节点宕机重启数据会消失,需要再次加载
7、空间
本地缓存存储在jvm中,大小有限;
分布式缓存由于是单独的服务,可使用内存较大
所以当数据量较小,且基本不变的数据可以放在本地缓存。
11.redis主从同步的原理
一、什么是主从复制
主从复制,指将一台redis服务器的数据,复制到其他的redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制时单向的,只能从主节点到从节点。
默认情况下,每台redis服务器都是主机节点;且一个主节点可以有多个从节点(或没有从节点),但是一个从节点只能有一个主节点。

主从复制的作用
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题,可以由从节点提供相应的服务。
3、负载均衡:在主从复制的基础上,主服务提供写服务,从服务提供读服务,多个从服务提供读服务,可以实现读负载。
4、高可用基石:是哨兵和集群能够实施的基础。
二、启用主从复制的方式
1、配置文件
在从服务器的配置文件中加入:slaveof<masterip><masterport>
2、启动命令
redis-server启动命令后加入--slaveof<masterip><masterport>
3、客户端命令
redis服务启动后,直接通过客户端命令执行slaveof<masterip><masterport>
java
#复制一份redis.conf文件
port 6380
# 把pid进程号写入pidfile配置的文件
pidfile /var/run/redis_6380.pid
logfile "6380.log"
#指定数据存放目录
dir /usr/local/redis‐5.0.3/data/6380
# 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replicaof 192.168.0.60 6379
# 配置从节点只读
replica‐read‐only yes三、主从复制实现原理
主从复制可分为三个过程:建立连接阶段、数据同步阶段、命令传播阶段
3.1主从复制流程图
3.1.1、全量复制

- 如果为master配置了一个slave,不管这个slave是否第一次连接master,它都会向master发送一个psync命令请求复制数据。
- master收到psync命令后,会在后台执行bgsave命令进行数据持久化,生成最新的rdb快照文件
- 持久化期间,master会继续接收客户端请求,它会把这些可能修改数据集的请求缓存在内存中。
- 持久化完成之后,master会把rdb文件发送给slave
- slave会把接收到的数据进行持久化生成rdb,然后在加载到内存中
- 最后master在将之前缓存在内存中的命令发送给slave
注意:masetr与slave之间由于某些原因,连接断开、slave会自动重连
3.1.2、部分复制
由于全量复制时效低,因此redis2.8开始提供部分复制,用于处理网络中断时的数据同步
1.复制偏移量
执行复制的双方,主从节点各维护一个复制偏移量,代表的是主节点向从节点传递的字节数,主节点每次向从节点传播N个字节数据,主节点的offset增加N;从节点每次接收到主节点传来的N个字节数据时,从节点的offset增加N。
offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致,不同则不一致,此时需要找出从节点缺少的部分数据,然后同步差异。
2.复制积压缓冲区
复制积压缓冲区是主要节点维护的、固定长度的、先进先出(FIFO)队列,默认大小是1MB,当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意:无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。
在命令传播阶段,主节点除了将写入命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset),由于复制缓冲区定长,所以主节点只保存最近执行的命令,时间较早的会被排挤出缓冲区。
当主从节点offset的差距过大,超过缓冲区的长度时,将无法进行部分复制,所以我们可以根据实际情况把缓冲区的大小设置的大一些(repl-backlog-size)
从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定是否执行部分复制。

3.服务器运行ID(runid)
每个redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点redis断开重连接的时候,就是根据运行ID来判断同步的进度:
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
过程原理:

四、主从复制风暴
如果一个子节点有很多个从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如下架构。

12.什么是布隆过滤器原理及使用场景
布隆过滤器的使用场景
- 数据库防止穿库:Google Bigtable,HBase和Cassandra 以及Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询的性能。
- 判断用户是否访问过:判断用户是否阅读过某视频、文章,比如抖音或头条 ,当然会导致误判,但不会让用户看到重复的内容。
- 解决缓存穿透:一般判断缓存是否在缓存中,如果在则直接返回结果,不在则查询db;如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存索引,只有在布隆过滤器中才会去查缓存,如果没有查询到则穿透到db。如果不在布隆过滤器中则直接返回。
- web 拦截器,黑名单校验:如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。
13. redis的选举协议及原理
14. redis的线程模型
15.redis 的过期策略
- 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期时间的数据,对内存很友好;但是会大量占用CPU资源去处理过期数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期:只有当访问一个key时,才会去判断该key是否过期,过期则清除。该策略可以最大化的节省CPU资源,但对内存极度不友好。极端情况下可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一定的时间,就会扫描一定数量的数据库的expires字典中一定数量的key,并清除已过期的key。该策略是前两种策略的折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源最优的平衡效果。
redis 中同时使用了惰性过期和定期过期两种策略。
