Redis 的多路IO复用
多路I/O复用是一种同时监听多个文件描述符(如Socket)的状态变化,并能在某个文件描述符就绪时执行相应操作的技术。在Redis中,多路I/O复用技术主要用于处理客户端的连接请求和读写操作,以实现高并发、高性能的服务。Redis支持多种多路I/O复用机制,包括select、poll、epoll和kqueue等。其中,epoll是Linux系统下性能最好的一种机制,Redis在Linux系统下默认使用epoll。
-
select机制
select是最早的多路I/O复用机制,通过调用select函数来监视多个文件描述符的状态变化。但是,select机制存在性能瓶颈和文件描述符数量限制的问题。在高并发场景下,select机制的性能会受到较大影响。
-
poll机制
poll机制是对select机制的改进,解决了文件描述符数量限制的问题。但是,poll机制在大量文件描述符的情况下仍然存在性能瓶颈。
-
epoll机制
epoll是Linux特有的I/O多路复用机制,采用事件驱动的方式,通过epoll_ctl函数注册文件描述符的事件,然后通过epoll_wait函数等待I/O事件的发生。epoll机制在处理大量连接时具有更好的扩展性和性能。Redis在Linux系统下默认使用epoll机制。
Redis 单线程抗高QPS的原因
Redis作为一个单线程的内存数据库,却能够抗住如此高的QPS(每秒查询率),这主要得益于以下几个方面:
-
非阻塞I/O与多路I/O复用
Redis采用了非阻塞I/O与多路I/O复用技术,使得单个线程可以同时处理多个客户端的连接请求和读写操作。当某个文件描述符就绪时,Redis会立即执行相应的操作,而不会阻塞整个线程。这种机制大大提高了Redis的并发处理能力。
-
纯内存操作
Redis的所有数据都存储在内存中,因此读写操作都非常快。相比于磁盘数据库,Redis的读写性能要高出几个数量级。这也是Redis能够抗住高QPS的重要原因之一。
-
高效的 数据结构
Redis内部使用了多种高效的数据结构,如哈希表、跳表、整数集合等。这些数据结构在存储和查询数据时都具有较高的性能。同时,Redis还针对这些数据结构进行了大量的优化,以确保在高并发场景下仍然能够保持稳定的性能。
-
单线程避免了 线程切换和加锁的开销
虽然多线程可以提高系统的并行处理能力,但同时也带来了线程切换和加锁的开销。Redis采用单线程模型,避免了这些开销,使得Redis在处理单个请求时更加高效。此外,单线程模型也使得Redis的编程模型相对简单,易于维护和管理。
Redis在实际应用中抗住高QPS的关键在于其内部设计、数据结构的高效利用以及配置优化。
1. 内部设计
多路I/O复用
- 机制:Redis通过多路I/O复用技术(如epoll在Linux系统中)实现同时监听多个客户端连接,当某个连接准备就绪时,Redis会立即处理,无需等待或轮询其他连接。
- 假设Redis服务器同时处理1万个客户端连接,当有100个连接同时发送请求时,Redis不会逐一检查每个连接,而是通过epoll机制立即处理这100个连接,从而实现高效并发。
单线程模型
- Redis采用单线程模型,避免了多线程间的竞争和锁的开销,使得每个请求都能得到快速处理。
- 在高并发场景下,多线程模型可能因为线程切换和锁的竞争而导致性能下降。而Redis的单线程模型能够确保每个请求都在同一线程中处理,从而避免了这些开销。
2. 数据结构的高效利用
合理选择数据结构
-
Redis提供了多种数据结构,如字符串、列表、哈希、集合和有序集合。根据业务需求选择合适的数据结构能够大大提高性能。例如,使用哈希表存储用户信息,可以通过用户ID快速定位到用户数据。
缩短键值对存储长度
-
缩短键值对的存储长度可以减少内存占用和网络传输开销,从而提高性能。例如,可以使用更短的key和value,或者对数据进行压缩后再存储。
3. 配置优化
使用Pipeline
- 机制:Pipeline允许客户端将多个命令打包成一个请求发送给Redis服务器,从而减少网络往返时间。
- 假设客户端需要执行10个命令,如果使用普通的请求-响应模式,需要发送10次请求和接收10次响应;而使用Pipeline,只需要发送一次请求和接收一次响应,从而大大提高了性能。
启用持久化
- 机制:Redis提供了RDB和AOF两种持久化方式,以确保数据在服务器重启后不会丢失。
- 根据业务需求选择合适的持久化方式,并调整相关参数以优化性能。例如,可以关闭不必要的持久化功能,或者调整AOF的刷新频率和文件大小等参数。
监控和优化
- 机制:定期监控Redis的性能指标,如内存使用、QPS、响应时间等,并使用Redis自带的INFO命令或第三方监控工具进行分析和优化。
- 如果发现Redis的内存使用率过高,可以通过增加内存、优化数据结构或调整缓存策略等方式来降低内存占用;如果发现某个命令的响应时间过长,可以通过优化该命令或调整相关参数来提高性能。
Redis Big Key的定位以及解决方案
Redis最常见的用途就是缓存数据来提高系统的性能。但是如果缓存使用不当,如下场景:
- 业务中使用了不恰当的redis数据结构。如使用String的value存储某个较大二进制文件数据。
- 业务预估不准确;如规划的时候没有对key的成员进行合理的拆分,导致key的成员数据量过多。
- 没有及时清理无用的数据;如List结构中数据持续增加而没有弹出数据的机制,那么数据会越来越多。
- 某个key存放的数据突然的波动很大;如存放某个明星热点粉丝列表或者评论的列表由于明星出轨或者离婚导致热点数据量激增,也就是value存放过多数据。
以上情况都会导致key对应的value数据量比较大,也就是value所占的内存空间较大,这就是所谓的big key问题。针对Redis的big key一般的判定标准如下:
总结起来其实就分成两类,一类是字符型,一类是非字符型(Hash、List、Set、Zset),针对字符型是判断value值大小,针对字符型就是判断其存放的元素个数,如下图的整理:
上面是判定big key的一般标准,具体到每个系统的判断标准还需要根据自身的业务场景来确定。
1、Big Key的常见场景
- 排行榜: 在排行榜系统中,可能会使用Redis的
Sorted Set
数据结构来存储用户的分数和排名。如果用户数量非常多,那么这个Sorted Set
的大小就会非常大,从而形成大Key。 - 在线课程系统: 在线课程系统可能会使用Redis来存储每个课程的学生列表。如果一个课程的学生数量非常多,那么这个列表就可能会形成大Key。
- 直播系统: 直播系统可能会使用Redis来存储每个直播间的观众列表。如果一个直播间的观众数量非常多,那么这个列表就可能会形成大Key。
- 社交网络: 社交网络可能会使用Redis来存储用户的好友列表或者粉丝列表。如果一个用户的好友或者粉丝数量非常多,那么这个列表就可能会形成大Key。
- 实时计算: 在实时计算场景中,可能会使用Redis来存储中间计算结果。如果这些结果的数据量非常大,那么就可能会形成大Key。
2、Big Key的危害
- 内存占用过大: Redis是基于内存的数据存储系统,大Key会占用大量的内存空间,可能导致内存不足,影响系统的正常运行。
- 性能下降: 当Redis需要对大Key进行操作时,如读取、写入、删除等,都会消耗大量的CPU和内存资源,导致Redis的性能下降。
- 阻塞问题: Redis是单线程模型,对大Key的操作可能会阻塞其他的请求,导致Redis服务的响应时间增加。
- 数据备份和恢复问题: 如果Redis中存在大Key,那么在进行数据备份和恢复时,可能会因为单个Key的数据过大而导致备份和恢复过程变得非常慢。
- 网络带宽压力: 当Redis需要将大Key的数据传输到客户端时,可能会占用大量的网络带宽,影响网络的性能。
- 内存分配不均匀: 集群架构下,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。
3、Big Key的检测
Redis大Key的检测可以通过以下几种方式进行:
1). 使用Redis自带命令进行检测
keys *
:这个命令可以列出所有的key(全量扫描,生产环境不建议)randomkey
:这个命令可以随机返回一个key,可以通过这个命令多次执行来随机检查key的大小。debug object key
:这个命令可以查看指定key的详细信息,包括它的大小。(开销较大、不建议)STRLEN、LLEN、HLEN、SCARD、ZCARD、XLEN
等命令,返回对应Key的列表长度或数量。
2). 使用第三方工具进行检测
- Redis-cli:Redis的命令行工具,可以通过脚本来批量检查key的大小。
- Redis-rdb-tools:这是一个Python的库,可以解析Redis的dump.rdb文件,然后分析出大Key。
- Redis-sampler:这是一个可以抽样分析Redis数据的工具,也可以用来检测大Key。
3). 实时监控系统的设计与实现
- 可以通过定期执行脚本,将Redis中的key和它们的大小信息发送到监控系统,然后在监控系统中设置阈值,当key的大小超过阈值时,发送告警通知。
- 可以使用开源的监控系统,如Prometheus和Grafana,或者商业的监控系统,如Datadog和New Relic,来实现Redis的实时监控。
4、Big Key 的预防与处理
1. 数据结构优化
-
根据实际的业务需求,选择更合适的数据结构。例如,如果数据具有唯一性,可以使用
Set
代替List
;如果数据具有键值对关系,可以使用Hash
代替String
。 -
对于
Hash
类型的数据,如果field
数量非常多,可以考虑将一个大Hash
拆分成多个小Hash
。
2. 数据存储策略调整
-
对于大量的小数据,可以考虑使用
Hash
类型进行存储,将多个小Key
合并成一个大Key,减少Key的数量,提高存储效率。 -
对于大数据,可以考虑使用分片的方式进行存储,将一个大Key拆分成多个小Key,每个小Key存储一部分数据。
3. 数据清理机制的设计与实现
-
对于不再需要的数据,应及时清理,避免占用过多的内存空间。
-
可以设置Key的过期时间,让Redis自动清理过期的数据。
-
可以设计定期清理的机制,通过脚本或者定时任务,定期检查和清理大Key。
Redis 不实时删除过期数据原因
设置过期时间的作用
内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。Redis 自带了给缓存数据设置过期时间的功能,比如:
expire key 60 # 数据在 60s 后过期
(integer) 1
setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
注意:Redis 中除了字符串类型有自己独有设置过期时间的命令
setex
外,其他方法都需要依靠expire
命令来设置过期时间 。另外,persist
命令可以移除一个键的过期时间。
很多时候,业务只需要某个数据只在某个时间段内存在,比如短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。如果使用传统的数据库来处理的话,通常都是自己判断过期,这样更麻烦并且性能要差很多。
Redis 判断数据是否过期
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
Redis 过期字典
过期字典是存储在 redisDb 这个结构里的:
typedef struct redisDb {...dict *dict; //数据库键空间,保存着数据库中所有键值对dict *expires // 过期字典,保存着键的过期时间...
} redisDb;
在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。
Redis 过期 key 删除策略
- 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
- 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
- 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
Redis 采用的删除策略
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。
Redis 的定期删除
Redis 的定期删除过程是随机的(周期性地随机从设置了过期时间的 key 中抽查一批),所以并不保证所有过期键都会被立即删除。这也就解释了为什么有的 key 过期了,并没有被删除。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
另外,定期删除还会受到执行时间和过期 key 的比例的影响:
- 执行时间已经超过了阈值,那么就中断这一次定期删除循环,以避免使用过多的 CPU 时间。
- 如果这一批过期的 key 比例超过一个比例,就会重复执行此删除流程,以更积极地清理过期 key。相应地,如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。
Redis 7.2 版本的执行时间阈值是 25ms,过期 key 比例设定值是 **10%**。
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which we do extra efforts. */
每次随机抽查数量是多少?
expire.c
中定义了每次随机抽查的数量,Redis 7.2 版本为 20 ,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
如何控制定期删除的执行频率?
在 Redis 中,定期删除的频率是由 hz 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
类似的参数还有 dynamic-hz,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力,这两个参数都在 Redis 配置文件 redis.conf
中:
# 默认为 10
hz 10
# 默认开启
dynamic-hz yes
除了定期删除过期 key 这个定期任务之外,还有一些其他定期任务例如关闭超时的客户端连接、更新统计信息,这些定期任务的执行频率也是通过 hz 参数决定。
为什么定期删除不把所有过期 key 都删除?
这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。
为什么 key 过期之后不立马删掉,以避免浪费内存空间?
这种删除方式的成本太高了。假如使用延迟队列作为删除策略,这样存在下面这些问题:
- 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
- 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。
Redis 大量 key 集中过期处理
如果存在大量 key 集中过期的问题,可能会使 Redis 的请求延迟变高。可以采用下面的可选方案来应对:
- 尽量避免 key 集中过期,在设置键的过期时间时尽量随机一点。
- 对过期的 key 开启 lazyfree 机制(修改
redis.conf
中的lazyfree-lazy-expire
参数即可),这样会在后台异步删除过期的 key,不会阻塞主线程的运行。
实现 Redis 和 MySQL 数据双写一致性
在实际开发中,可使用redis缓存一些常用的数据(如热点数据)用来提高系统的吞吐量。
但是不可以避免的出现了数据的修改场景,这就导致了数据库中的数据和Redis中出现不一致性的情况。如何保证数据一致性就显得非常重要了,下面介绍一下保证数据的双写一致性的方案。
1、先删缓存再操作数据库方案
在redis一般写的场景下对数据的更新操作是不推荐使用的,推荐使用删除缓存数据的操作,因为删除操作的效率更高。下图展示先删除缓存再操作数据库的过程图:
在这种方式下会存在数据不一致的问题,如下图所示:
(1)线程1要更新数据,它先删除redis中的缓存数据,然后由于网络堵塞导致暂短的停顿,没有继续执行操作数据库。
(2)线程2要查询数据,首先查询数据库,但是由于Redis中的数据已经被线程1删除了,那么它会去数据库中查询数据X并且要将数据X同步到Redis中。
(3)线程1网络堵塞结束,执行了数据库操作将数据X更改为Y。
经过上述的过程就导致了Redis的数据和数据库中的数据不一致了,即就是Redis中存放的依据是老数据。为了解决上述的问题,我们采用缓存延迟双删的策略,如下图所示的缓存延迟双删的过程:
采用缓存延迟双删策略最多在X毫秒内读取的数据是老数据,在X毫秒之后读取的数据都是最新的数据。X的具体值如何确定那就需要根据自身的业务了来确定。延迟双删策略只能保证最终的一致性,不能保证强一致性。由于对Redis的操作和Mysql的操作不是原子性操作,所以如果想保证数据的强一致性就需要加锁控制,如下图所示:
加锁之后势必会带来系统的吞吐量的下降,所以需要衡量利弊来确定是否使用加锁。
2、先操作数据库再删除缓存方案
此方案就是先操作数据库,数据库写入成功之后再来删除Redis缓存中的数据。多个线程之间的数据读取和更新如下图所示:
这种方案下,在数据库更新成功后到删除Redis缓存数据之前的这段时间中,其他线程读取的数据都是旧数据,等Redis删除缓存后会重新从数据库中读取最新数据同步到Redis,这样可以在一定程度上保证数据的最终一致性。极端情况下会出现数据不一致的情况,如下图所示:
(1)线程1先成功的更新数据到数据库中,然后执行删除Redis缓存中的数据的时候失败了。
(2)线程2要读取数据,此时优先从Redis中查询数据,由于此时Redis中老数据没有删除,所以线程2可以拿到旧数据直接返回。直到Redis中缓存的数据过期之后才可以从数据库中获取最新的到Redis中。
3、删除重试机制
无论是先删除缓存再操作数据还是先操作数据库再删除缓存的机制,都有可能会出现删除缓存失败的情况,如下图所示:
为了应对删除缓存失败的情况发生,于是加入了删除重试机制,如下图所示:
通过canal监听binlog感知数据的变动后,canal客户端执行删除Redis缓存数据,如果缓存数据删除失败那么发送一条MQ消息让canal客户端继续执行删除操作,这样可以保证数据的最终一致性。但是这样也增加了系统的复杂性。
总结
(1)实际开发中推荐使用先操作数据库再删除缓存的方案,因为此方案最大程度上保证了数据的一致性并且实现也最简单。
(2)无论是先操作数据库再删除缓存还是先删除缓存再操作数据库都有可能会出现删除缓存失败的情况,所以需要加入删除重试机制。
(3)如果想要Redis和MySQL的数据强一致性,可以考虑使用加锁的方式实现。
Redis 延迟队列
1、通过过期key通知实现
实现思路:首先开启redis的key过期通知,然后在业务中给key设置过期时间,到了过期时间后redis会自动的将过期的key消息推送给监听者,从而实现延迟任务。
核心的代码实现:
#1、开始redis的过期通知
notify-keyspace-events Ex#2、监听redis的过期key
@Component
@Slf4j
public class RedisExpireKeyService extends KeyExpirationEventMessageListener {public RedisExpireKeyService(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}/*** 监听过期的key**/@Overridepublic void onMessage(Message message, byte[] pattern) {String expireKey = message.toString();//执行具体的业务System.out.println("监听到key=" + expireKey + ",已经过期");}
}
生产环境是不推荐使用此方案,原因Redis 的过期策略采用的是惰性删除和定期删除相结合的方式,redis并不保证 key 在过期时会被立即删除操作。
2、通过Zset+定时任务实现
实现思路:ZSet 是一种有序集合类型,它可以存储不重复的元素,并且给每个元素赋予一个 double 类型的排序权重值(score),所以可以将元素的过期时间作为分值,通过定时任务扫描的方式判断是否达到过期时间,从而实现延迟队列。
核心的代码实现:
#使用xxl-job
@JobName("consumerTaskJob")
public void consumerTaskJob() {String expireKey = "ExPIRE_KEY";try {//获取当前时间double currentTime = System.currentTimeMillis();//获取超时的数据Set<String> expiredMemberSet = redisTemplate.opsForZSet().rangeByScore(expireKey, Double.MIN_VALUE, currentTime);//过期keyfor (String expiredMember : expiredMemberSet) {//todo 做实际的延迟任务//从ZSet中移除数据redisTemplate.opsForZSet().remove(expireKey, expiredMember);} } catch (Exception e) {log.error("数据处理失败",e);}}
Zset+定时任务的实现延迟任务的方式虽然比监听过期key方案合理一些,但是它还是存在一定的缺陷,如无重试机制、延迟时间固定化(依赖定时任务的执行时间)、不适用于大规模的延迟任务。
3、Redisson实现延迟队列
Redisson是一个操作Redis的 Java 客户框架,它提供了RDelayedQueue 接口和 RQueue 接口可以实现延迟队列(Redisson 提供的延迟队列底层也是基于 Zset 数据结构实现的)。
核心的代码实现:
#2、添加数据到队列中
//创建RedissonClient实例
RedissonClient redissonClient = Redisson.create();
//创建阻塞队列
RBlockingDeque<String> queue = redissonClient.getBlockingDeque("delayQueue");
//创建延迟队列并关联到阻塞队列
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);
//添加延迟任务
delayedQueue.offer("Task1", 5000, TimeUnit.MILLISECONDS);#3、消费数据
while (true) {try {//获取并移除队首元素,如果队列为空,则阻塞等待String task = queue.take();System.out.println("Task: " + task);} catch (Exception e) {log.error("消费失败",e);}
}
Redis实现的延迟队列适用于处理一些比较简单的业务,如发送邮件、发送通知等,对于复杂的业务不适用于Redis的延迟任务方案。
Redis 缓存预热
- 把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动的加载数据并缓存数据;
- 把需要缓存的方法挂载到某个页面或后端接口上,手动触发缓存预热;
- 设置定时任务,定时自动进行缓存预热。
Redis 缓存雪崩
缓存雪崩是指在某个时间段内,大量的缓存数据同时过期失效,导致大量的请求直接击穿至数据库,引起数据库压力骤增,甚至引起宕机的现象。这种现象类似于雪崩,一旦开始,就会迅速扩散并严重影响系统的稳定性和可用性。
原因分析
- 缓存失效时间同步:当多个缓存数据的失效时间设置相同时,它们可能会在同一时间点同时过期,导致大量请求涌入数据库。
- 热点数据访问集中:在高并发情况下,某些热点数据的访问量非常大,当这些热点数据同时失效时,大量的请求会集中在数据库上,造成数据库压力激增。
解决方案
- 设置合理的过期时间:缓存中的数据过期时间应该分散设置,避免在同一时间大量数据同时过期。
- 使用热点数据预加载:提前加载热点数据到缓存中,避免在缓存失效时大量请求同时访问数据库。
- 使用备份机制:在缓存失效时,可以通过备份机制或从其他缓存源加载数据,减轻对数据库的直接压力。
- 限流和降级:在高峰期采取限流策略,控制请求的并发量,避免缓存雪崩的发生。同时,考虑在极端情况下采取缓存降级策略,直接访问数据库以保证系统的可用性。
- 加锁排队:加锁排队可以起到缓冲的作用,防止大量的请求同时操作数据库,但它的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了一部分用户体验。加锁排队的代码实现,如下所示:
// 缓存 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;}
}
以上为加锁排队的实现示例,读者可根据自己的实际项目情况做相应的修改。
-
设置二级缓存
二级缓存指的是除了 Redis 本身的缓存,再设置一层缓存,当 Redis 失效之后,先去查询二级缓存。例如可以设置一个本地缓存,在 Redis 缓存失效的时候先去查询本地缓存而非查询数据库。
Redis 缓存穿透
缓存穿透是指恶意或非法请求访问不存在于缓存中的数据,导致请求直接访问数据库,增加数据库负载。这种情况下,大量的无效请求会直接穿透缓存层,导致数据库被频繁访问,影响系统的性能和稳定性。
原因分析
- 恶意查询:恶意用户可能会发起针对不存在数据的查询请求,导致缓存无法命中,直接访问数据库。
- 业务逻辑缺陷:在没有对用户输入进行有效过滤的情况下,某些用户可能会发起非法或无效的请求,导致缓存穿透。
解决方案
- 布隆过滤器(Bloom Filter):使用布隆过滤器对请求进行预先过滤,判断请求是否有效,有效则继续访问缓存,无效则直接拒绝,避免访问数据库。
- 空对象缓存:将数据库中不存在的键也缓存起来,设置一个较短的过期时间,防止恶意请求频繁查询。
- 合理校验和处理:在业务逻辑层对用户输入进行校验,排除非法请求,避免将无效请求传递给缓存层。
Redis 缓存并发
缓存并发是指大量请求同时访问同一缓存资源,可能引发缓存雪崩、缓存击穿等问题。在高并发的情况下,如果没有有效的并发控制机制,会导致缓存失效或缓存命中率下降,进而影响系统的性能和稳定性。
原因分析
- 热点数据访问:某些热点数据的访问量较大,在高并发情况下,大量请求会同时访问同一缓存资源。
- 缓存失效策略不当:缓存的失效策略过于简单,导致大量请求在缓存失效后同时访问数据库
解决方案
- 分布式锁:使用分布式锁控制对缓存资源的并发访问,确保同一时间只有一个请求能够更新缓存。
- 限流控制:实施限流算法来限制对缓存的并发访问数量,避免过多请求同时访问缓存
Redis 缓存击穿
缓存击穿是指某个热点数据突然失效或过期,导致大量请求直接访问数据库,增加数据库负载。与缓存雪崩不同的是,缓存击穿通常是针对某个特定的缓存键失效,而不是整个缓存层失效。
原因分析
- 热点数据失效:热点数据的访问量较大,当这些数据的缓存失效时,大量请求会直接访问数据库,造成数据库压力激增。
解决方案
- 对于热点数据,可以设置其永不过期,或设置较长的过期时间,避免频繁的缓存失效。
- 在缓存失效时,使用互斥锁阻止大量请求同时访问数据库,等待缓存数据更新后再释放锁。
-
加锁排队
此处理方式和缓存雪崩加锁排队的方法类似,都是在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力。
Redis 缓存降级
缓存降级是指在系统压力过大或缓存失效时,暂时关闭或降级缓存功能,直接访问数据库,保证系统的稳定性。
原因分析
- 系统压力过大:在系统高峰期或异常情况下,缓存无法承受大量请求的同时访问。
- 缓存失效:缓存失效或缓存层出现故障,无法提供正常的缓存服务
解决方案
- 备用方案:在缓存失效或压力过大时,设置备用方案直接访问数据库,保证系统的可用性。
- 自动降级:使用自动降级策略根据系统负载情况自动调整缓存功能,避免系统崩溃或性能下降。
Redis pipeline(管道)
Redis pipeline 使得客户端可以一次性将要执行的多条命令封装成块一起发送给服务端。
Redis 服务端在收到来自管道发送的多条命令之后,会先把这些命令按序执行,并将执行结果保存到缓存中,直到最后命令执行完成,再把命令执行的结果一起返回给客户端。Redis 使用 pipeline 主要有以下两个好处:
- 节省了 RTT
RTT(Round Trip Time)即往返时间。Redis 客户端将要执行的多条指令一次性给客户端,显然减少了往返时间。
- 减少了上下文切换带来的开销
当服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作。其中涉及到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 100 条 Redis 指令的时候,就发生 100 次用户态到内核态之间上下文的切换,但是如果使用管道的话,其将多条命令一同发送给服务端,就只需要进行一次上下文切换就好了,这样就可以节约性能。
这里需要注意 pipeline 不宜包装过多的命令,因为会导致客户端长时间的等待,且服务器需要使用内存存储响应,所以官方推荐最多一次 10k 命令。还有一点需要注意 pipeline 命令执行的原子性不能保证,如果要保证原子性则使用 lua 脚本或者事务。
Redis 应用场景
缓存:Redis 可以作为应用程序的缓存层,减少数据库的读取压力,提高数据访问速度。
会话存储:在 Web 应用中,Redis 可以用来存储用户的会话信息,如登录状态、购物车内容等。
排行榜和计数器:Redis 支持原子操作,非常适合实现实时排行榜、点赞数、访问计数等功能。
消息队列:Redis 可以作为消息队列系统,用于处理异步任务,例如邮件发送、后台任务处理等。使用Redis的发布/订阅功能来实现任务队列。
实时分析:Redis 可以用于实时分析,如用户行为分析、实时统计信息等。使用Redis的Sorted Set来实现用户在线时长统计和分析功能。
分布式锁:在分布式系统中,Redis 可以用于实现分布式锁,确保在多个节点之间共享资源的一致性。使用Redisson作为客户端来实现分布式锁。
发布/订阅:Redis 提供了发布/订阅模式,可以用于实现消息广播,例如实时通知系统。
限流:Redis 可以用于实现限流功能,防止系统过载,如 API 调用频率限制。
数据过期:Redis 支持设置数据的过期时间,自动清理过期数据,适用于临时数据存储。
全页缓存:Redis 可以缓存整个页面的输出,减少数据库查询和页面渲染时间。
社交功能:在社交网络应用中,Redis 可以用于存储好友关系、用户状态更新等。
实时推荐系统:Redis 可以用于存储用户的行为数据和偏好,实现实时推荐。
地理位置信息:Redis 支持 Geospatial 索引,可以用于实现地理位置相关的查询和推荐。
时间序列数据:Redis 可以存储时间序列数据,用于监控和分析。
任务调度:Redis 可以用于任务调度,例如定时任务的执行。使用Redis的延迟队列特性来实现任务调度。
数据共享:在微服务架构中,Redis 可以作为服务间共享数据的媒介。
持久化:虽然 Redis 是内存数据库,但它也支持数据持久化,可以在系统故障后恢复数据。使用Spring的@Scheduled注解与Redisson结合来实现任务调度。
Redis缓存常用设计模式
写操作
以Redis统一视图为准:先更新缓存,后更新数据库。
1) Write Through Pattern 直写模式
首先将数据写入缓存,再将数据立即同步到数据库。
优点:
数据一致性:每次写操作都要同时更新缓存和数据库,保证了缓存和数据库之间的数据一致性。
即时的数据访问:由于缓存始终保持最新状态,读取操作可以立即从缓存中获取最新的数据,提高了数据访问的速度。
缺点:
写操作延迟:对于写操作频繁的场景,每次写操作都要同时更新缓存和数据库,导致写操作延迟。
资源消耗:缓存和数据库的同步更新会消耗更多的计算和内存资源。
适用场景:
适用于对数据一致性要求较高,写操作不频繁的场景。
例如:电商平台的订单处理,当用户下单时,订单信息既写入缓存,也同步写入数据库,保证了数据的实时性和一致性。
2) Write Behind Pattern 写后模式
首先将数据写入缓存,再将数据异步的批量同步到数据库。
优点:
提高写操作性能:写操作首先发生在缓存中,通常比写入数据库快得多。
减轻数据库负载:异步批量写入数据库,减少对数据源的即时写操作。
提高响应时间:写操作首先发生在缓存中,可以更快的响应写请求。
缺点:
数据一致性问题:由于数据是异步写入数据库的,导致缓存和数据库之间在一定时间内的数据不一致。
适用场景:
适用于写操作远多于读操作,且对数据一致性要求不高的场景。
例如:用户行为日志收集,用户在网站上的点击行为被记录在缓存中,然后异步批量写入到日志数据库。
写操作不经过缓存。
3) Write Around Pattern 绕写模式
数据直接写入数据库,不经过缓存。
优点:
提高缓存效率:写操作不需要同步到缓存,缓存不会应为写操作而频繁的失效或更新。
提高内存利用率:防止那些不会再次被读取到的数据占用缓存空间,提高资源利用率。
缺点:
无法保障数据一致性:如果更新的数据同时存在于缓存和数据库中,则会造成缓存和数据库中的数据不一致。由于缓存数据没有被及时更新,导致从缓存中获取到脏数据。
适用场景:
适用于数据写入后很少被读取的场景。
例如:对于数据备份操作直接写入到备份存储中,不经过缓存;或者是针对报告、归档信息的操作。
读操作
4)Read Through Pattern 读穿透
如果缓存未命中,缓存层自动从数据库中获取数据,然后将数据写入缓存中,最终由缓存返回数据给应用程序。缓存系统自动处理数据加载,使得数据的读写操作对应用更加透明,通常与write Through结合使用,这意味这所有的操作都会通过缓存层。
优点:
降低数据库的负载:一旦数据被加载到缓存中,后续的读取请求将直接从缓存中获取数据,减少了对数据库的直接访问。
提高系统的性能和并发读取能力:读操作从缓存中进行,缓存的读取速度快,从而提高了系统的性能。
缺点:
高并发请求下的数据不一致:连续两次写入请求,由于写入操作存在先后顺序问题,当数据被更新时,其它并发请求可能还在读取缓存中的旧数据,导致数据不一致。
回源延迟:如果缓存未命中,回源操作(缓存未命中,缓存系统就需要从原始数据源(如数据库或远程服务器)获取这个数据项,然后再提供给请求者)会导致数据的获取有一定的延迟,特别是当数据量较大时,延迟会更加明显。
解决方案:
设置合适的缓存数据过期时间,采用适当的缓存数据过期策略和缓存淘汰策略确保缓存的有效性。
“定期删除+惰性删除”策略:用于删除过期的缓存数据。
内存淘汰策略:用于在内存不足时,选择要淘汰的缓存数据。
适用场景:
适用于读取频繁、写入较少,对数据一致性要求不高,对速度和性能要求较高的场景。
缓存中放的是当前在线用户的活跃数据,例如游戏中的换皮肤、换装备,用户登录系统后,用户的所有行为在缓存中生成副本(统一视图)。
读写操作
5)Cache Aside Pattern 旁路模式
缓存操作是由应用程序显式控制的,开发者可以根据特定业务需求来自定义管理缓存数据,更加灵活可控。
优点:
确保缓存中存放的是真热点数据:只有在实际需要时,才加载数据到缓存,避免缓存中填充未使用或很少使用的数据,保证缓存中存放的是当前窗口的活跃数据。
内存占用小:只缓存真正的热点数据,减少缓存空间的浪费,更有效的利用缓存空间。
提高灵活性:缓存操作是由应用程序显式控制的,开发者可以根据特定业务需求来管理缓存数据。
缺点:
代码复杂性:需要额外的代码逻辑去处理缓存的加载和失效。
数据一致性问题:由于缓存更新依赖于应用程序逻辑,如果处理不当,可能会导致缓存和数据库之间的数据不一致。
适用场景:
适用于读多写少,对数据实时性要求不高的场景。
例如:新闻内容展示、博客文章的阅读。
Redis过期策略、内存淘汰机制
缓存中存储当前的热点数据,Redis为每个key值都设置了过期时间,以提高缓存命中率。假设你设置了一批key只能存活2个小时,那么当这批key过期后,Redis选择“定期删除+惰性删除”策略。如果该策略失效,Redis内存使用率会越来越高,一般应采用内存淘汰机制来解决。
定期删除
Redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。
随机抽取:是因为假如redis存了几十万个key,每隔100ms就遍历所有设置了过期时间的key,cpu的负载会很高。
“定期”而不是“定时”:是因为定时删除需要用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是在高并发请求下,cpu应该把更多的时间用在处理请求上,而不是浪费在删除key上。
惰性删除
定期删除可能会导致很多过期的key到了时间并没有被删除掉,此时就要用到惰性删除。在你请求某个key的时候,redis会检查这个key是否设置了过期时间,并判断是否过期了,如果过期就删除。
- 定时删除:用cpu的性能换取内存空间(以时间换空间)
- 定期删除:可能会导致过期的key没被删除
- 惰性删除:用内存换取cpu的处理时间(以空间换时间)
定期删除+惰性删除的问题
如果定期删除漏掉了很多过期的key,这些key还占用着内存。然后也没即时去请求key,即惰性删除也没生效。这种场景下,Redis默认的“定期删除+惰性删除”策略就失效了。此时,如果大量过期的key堆积在内存中,Redis内存使用率会越来越高,最后导致Redis的内存块耗尽。对此,可采用内存淘汰机制解决。
Redis中的缓存淘汰策略
当Redis使用的内存达到maxmemory参数配置的阈值时,Redis就会根据配置的内存淘汰策略把key从内存中移除。
1. LRU 最近最少使用 --- 基于缓存中数据的访问顺序
使用一个链表来跟踪key的访问顺序,当key被访问时将其移动到链表前面。
2. LFU 最不经常使用 ----基于缓存中每个数据的使用频率
更加关注缓存的使用频率,使用计数器来记录每个key的访问次数,哈希表用于快速查找key。
3. Random 随机淘汰策略
4. TTL 生存时间:从设置了过期时间的key里面,挑选出即将要过期的key优先移除。
序号 淘汰策略 策略说明 1 noeviction 当内存不足以容纳新写入数据时,写入操作会报错(一般不用) 2 volatile-lru 对设置了过期时间的key使用LRU算法,最近最少使用的淘汰
缓存数据有明显的冷热之分,即数据的访问频率相差较大3 volatile-lfu 对设置了过期时间的key使用LFU算法,最不经常使用的淘汰
缓存需要关注数据的历史访问频率4 volatile-random 对设置了过期时间的key使用随机删除 5 volatile-ttl 从设置了过期时间的key里面,挑选出快要过期的key淘汰 6 allkeys-lru 对所有的key使用LRU算法,最近最少使用的淘汰 7 allkeys-lfu 对所有的key使用LFU算法,最不经常使用的淘汰 8 allkeys-random 对所有的key使用随机删除
缓存数据没有明显的冷热之分,即数据的访问频率差距不大总结
“定期删除+惰性删除”策略:用于删除过期的缓存数据。
内存淘汰策略:用于在内存不足时,选择要淘汰的缓存数据。
Redis 内存分析
1、Redis默认内存
在 64bit 系统下,默认不限制内存大小,不设置内存大小和maxmemory = 0表示不限制 Redis 内存使用
2、查看Redis最大内存
-
命令行
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
-
配置文件 redis.conf
# maxmemory <bytes>
3、查看Redis内存使用情况
127.0.0.1:6379> info memory
4、内存配置和修改
-
临时方案,通过命令修改
127.0.0.1:6379> config set maxmemory 104857600
OK
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "104857600"
-
永久方案,通过配置文件
5、生产环境内存配置
建议:一般取物理内存的3/4。
Redis 过期键删除
1、立刻删除
立即删除能保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是删除操作会占用cpu的时间,造成CPU额外的压力。redis.conf 中,通过调整过期键的检测频率:
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10
但是这会产生大量的性能消耗,同时也会影响数据的读取操作。
2、惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,
- 如果未过期,返回数据 ;
- 发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
#开启憜性淘汰
lazyfree-lazy-eviction=yes
3、定期删除
这种方案有效规避上述两种极端情况, 定期删除策略的难点是确定删除操作执行的时长和频率:
- 如果删除操作执行得太频繁或者执行的时间太长,定期删除策略就会退化成立即删除策略。
- 如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。
- 因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
Redis 内存淘汰策略
- 定期删除时,从来没有被抽查到
- 惰性删除时,也从来没有被点中使用过
上述两个步骤,依然会有大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽。因此,需要更好的兜底方案,淘汰策略。
1、LRU 和 LFU 的区别
LRU(Least Recently Used,最近最少使用页面置换算法)
假设我们有一个容量为3的LRU缓存,访问数据的顺序如下:
- 访问数据1,缓存中现在有:[1]
- 访问数据2,缓存中现在有:[1, 2]
- 访问数据3,缓存中现在有:[1, 2, 3]
- 再次访问数据1,缓存中现在有:[2, 3, 1](因为1被重新访问,它被移到了列表的末尾)
- 访问数据4,由于缓存已满,最不常用的数据2将被淘汰,缓存中现在有:[3, 1, 4]
-
原理:如果数据最近被访问过,那么在不久的将来它很可能再次被访问。因此,LRU会淘汰最长时间未被访问的数据。
-
适用场景:适用于最近被访问的数据在未来某个时间点很可能再次被访问。
LFU(Least Frequently Used,最近最不常用页面置换算法)
假设我们有一个容量为3的LFU缓存,访问数据的顺序如下:
- 访问数据1,计数器:{1: 1}
- 访问数据2,计数器:{1: 1, 2: 1}
- 访问数据1,计数器:{1: 2, 2: 1}
- 访问数据3,计数器:{1: 2, 2: 1, 3: 1}
- 访问数据1,计数器:{1: 3, 2: 1, 3: 1}
- 访问数据4,由于缓存已满,访问次数最少的数据2将被淘汰,计数器:{1: 3, 3: 1, 4: 1}
-
原理:LFU算法会跟踪每个页面在特定时间段内被访问的频率。当需要淘汰页面时,LFU算法会淘汰在该时间段内访问次数最少的页面。
-
适用场景:LFU算法适用于那些访问模式可能随时间变化的场景,或者访问频率能够较好地反映页面重要性的情况。
总结
- LRU关注数据的最近访问
时间
,淘汰最长时间未被访问的数据。 - LFU关注数据的访问
频率
,淘汰访问次数最少的数据。
2、Redis 的淘汰策略
redis.config 配置文件中,
# 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.
解释下:
volatile-lru
:使用近似的最近最少使用(LRU)算法淘汰键。但是,只有那些设置了过期时间的键(即“volatile”键)才会被考虑淘汰。allkeys-lru
:使用近似的LRU算法淘汰任何键,无论它们是否设置了过期时间。volatile-lfu
:使用近似的最少频率使用(LFU)算法淘汰键。同样,只有设置了过期时间的键会被考虑。allkeys-lfu
:使用近似的LFU算法淘汰任何键,不考虑它们是否设置了过期时间。volatile-random
:随机淘汰一个设置了过期时间的键。allkeys-random
:随机淘汰任何键,不论它们是否设置了过期时间。volatile-ttl
:淘汰具有最短剩余生存时间(TTL)的键,即那些最接近过期时间的键。noeviction
:不淘汰任何键。当内存达到最大容量时,Redis将不会进行任何淘汰操作,而是在写入新数据时返回错误。
3、生产如何选择淘汰策略?
在生产环境中选择缓存淘汰策略时,通常需要根据应用的具体需求和数据特性来定。这里给出常见案例:
电商平台的商品推荐
电商平台需要为用户展示个性化的商品推荐,其中热门商品的访问频率较高。可选择:LFU(Least Frequently Used)
redis-cli config set maxmemory-policy allkeys-lfu
因为LFU策略可以保留访问频率高
的商品,确保推荐列表中展示用户最可能感兴趣的商品。
金融交易平台的实时数据
金融交易平台需要提供实时的股票价格和交易数据,数据的实时性至关重要。过期的时价被淘汰。
可选择:TTL(Time To Live)结合LRU(Least Recently Used)
redis-cli config set maxmemory-policy volatile-lru
TTL确保数据在一定时间后自动过期,而LRU保证最近访问的数据被优先保留。
电信运营商的用户数据管理
电信运营商需要处理和缓存大量用户的通话记录、短信记录等,用户通常更关心最近的通信记录。
可选择:LRU(Least Recently Used)
redis-cli config set maxmemory-policy allkeys-lru
因为LRU策略可以确保最近生成的通话记录和短信记录被优先缓存。
社交媒体平台的用户动态
社交媒体平台需要为用户展示好友的最新动态和帖子,用户通常对最新动态感兴趣。
可选择:LRU
redis-cli config set maxmemory-policy allkeys-lru
因为LRU可以保证最新的帖子被优先展示。
Redis Big/Hot Key
-
大 key
指的是一个键中包含了大量的数据。(总结一个字就是大
)
占用空间:大key
通常指的是一个键包含了大量的数据,使得该键对应值的占用的内存超出了正常范围。这个大小的阈值并不是固定的,而是相对于 Redis 实例的可用内存而言。当一个键的大小超出了 Redis 实例可用内存时,就可以认为它是一个大key
。
操作耗时:如果对一个 key 的操作所需的时间过长,导致性能下降或者影响其他请求的处理速度,也可以说这个 key 是 大key
。因为这种情况通常是由于该 key 下包含了大量的数据。
-
热 key
指的是频繁访问的键。(总结就是热
,访问频繁。)
频繁访问:在某一段时间内被频繁访问的 key 就是 热key
。
业务方面:比如商城促销的场景下,某个商品的缓存可能就会成为 热key
。这种情况下 热key
反应的不仅是该键的访问频率高,还反映了用户对某个业务功能的热度。
性能方面:热key
的频繁访问造成 Redis 的 CPU 占用率过高,造成响应时间延长或者请求阻塞,从而造成系统崩溃。
key
的大与不大,热与不热要根据自己的业务,从实际情况进行评估。
大 key
的影响
-
内存消耗:在进行缓存时降低缓存的效率,占用大量的内存空间,使得 Redis 的内存消耗急剧增加,还可能导致 Redis 实例的内存资源不足,甚至出发内存淘汰策略,从而影响系统的正常运行。
-
性能下降:处理大的 key,会耗费更多的 CPU 时间以及带宽,导致 Redis 性能下降。由于 Redis 还是单线程的,处理
大key
的操作进而会阻塞其他请求的处理,从而影响系统性能。 -
持久化效率降低:在进行持久化操作时,
AOF
与RDB
都会因为该大key
耗费更多的时间,从而延迟持久化时间,分布式环境下甚至会造成缓存不一致。 -
网络传输延迟:
大key
在进行网络传输时会增加网络传输的延迟,在分布式环境下进行数据同步时可能会造成数据的不一致。
热 key
的影响
-
CPU占用率高:因为是
热key
,所以 CPU 一直占用,进而导致Redis实例的CPU负载增加。 -
请求阻塞:如果 key 有访问优先级,
热key
的存在可能导致请求队列中其他的请求被阻塞。 -
响应时间延长:因为
热key
,其他的请求被阻塞了造成响应时间延长。 -
性能不均衡:流量访问造成突刺,系统性能的不均衡。
大key
与 热key
都会给 Redis 实例造成一系列的影响,如内存占用过高,CPU 负载增加,持久化时间变长,性能下降等。
大 key
产生的原因
产生 大key
的原因有很多种,下面咱就一起看一下工作中经常遇到的这几种。
存储大量数据
存储了大量数据也是我们经常遇到 大key
的最多的原因了。
比如 String
类型直接保存了一个大的文本或者二进制数据;Hash
结构中存储大量的键值对。
-
String
SET zuiyu_large_text_key "very large text content..."
-
Hash
HMSET zuiyu_large_hash_key field1 value1 field2 value2 ... fieldN valueN
缓存时间设置不合理
缓存时间设置不合理这个造成 大key
的原因大概是个隐藏挺深的老 bug,有的业务场景,使用 Redis 缓存数据,业务是定时往该 key 上写数据,由于该 key 是没有设置缓存时间的造成这个 key 随着时间的流逝,占用的内存越来越多,对于该点,只需要设置一个合理的过期时间即可。
前提是多次写入
不是覆盖
,而是追加
才会有该问题。
SETEX zuiyu_key_with_expiry value 3600 # 设置过期时间为3600秒
数据结构使用不当
在使用 List 数据结构存储数据时,重复的添加数据,造成该 key 越来越大,实际上业务是不需要有重复的数据存在的。
-
List
LPUSH zuiyu_large_list_key value
大key
的产生根本原因就是在一个 key 下面存储的数据多了。
热 key
产生的原因
热门数据
热key
的产生一般意味着系统访问火爆了,但是火爆的只是其中一个点或者n个点。类似微博中某个明星的瓜,当上头条的时候,大量的人去访问,造成了该明星所对应的 key 成为 热key
。
频繁的更新
某些业务场景,单位时间内一直频繁的对 key 进行更新,该 key 也会成为 热key
。
热门搜索
类似于第一中的热门数据,产生了热门数据,该数据对应的热门关键词也被大量的用户去搜索,造成该关键词被频繁访问,最终导致该 key 也称为 热key
。
热key
的产生无外乎热门数据,热门数据产生的热门关键词以及对同一个 key 在某段时间内的频繁访问。
大key的解决方案
- 合理的数据结构
- 合理的缓存时间
大key
进行拆分为多个小key
- 定期对
大key
进行清理
热key的解决方案
- 合理的缓存淘汰策略
- 热点数据分片
将热点数据分散到不同的Redis实例,提升系统的吞吐量。
- 缓存预热
在系统启动或者活动高峰开启之前进行缓存预热,提前将需要的数据加载到缓存,减少热点数据首次访问的时间。
- 随机缓存失效时间
避免大量的key同一时间批量失效,造成缓存雪崩与缓存穿透。
- 缓存穿透
使用布隆过滤器进行缓存请求过滤,防止无效请求进入到缓存层。
针对 大key
我们要尽可能的避免同一个 key 下大量的数据。针对 热key
我们要合理设置过期时间,增加布隆过滤器等技术实现无效请求过滤,对即将到来的数据进行缓存预热、热点数据分片处理。