Redis 的全称是 Remote Dictionary Server,它是一个基于内存的 NoSQL(非关系型)数据库,数据以 键值对 存储,支持各种复杂的数据结构
Redis 的出现是为了弥补传统数据库在高性能要求下的不足。传统的关系型数据库在读写速度、并发处理和扩展性上有时不能满足某些高并发场景的需求。尤其是在互联网应用中,对于海量数据的处理、低延迟和高吞吐量的要求越来越高,Redis 提供了一种高效且灵活的解决方案。
大量在以下方面使用:
缓存系统: Redis 经常被用作缓存数据库,减轻传统关系型数据库的压力,提升访问速度。例如,在 Web 应用中,Redis 可以缓存数据库查询结果,当用户请求相同的数据时,可以直接从 Redis 获取,而不需要每次都访问数据库。这样可以大幅提高响应速度和降低数据库负载。
高性能的数据存储: Redis 是一个内存数据库,它将数据存储在内存中,因而读取和写入速度非常快。它也支持持久化功能,可以定期将数据存储到硬盘上,避免丢失数据。
消息队列: Redis 支持发布/订阅模式以及基于列表的数据结构,可以用作消息队列系统,处理高并发的消息传递。例如,生产者将消息写入 Redis 队列,消费者从队列中获取消息进行处理,支持高效的异步任务处理。
分布式锁: Redis 提供了分布式锁的实现,可以解决多个进程或服务间对同一资源的并发控制问题,确保在分布式环境下只有一个线程能够执行特定的操作。
实时分析和计数: Redis 可以处理实时的数据分析和计数,比如网站访问量统计、实时数据处理等。它的高性能使得它能够在大规模、高并发的环境下工作。
高性能:Redis 是内存存储,数据操作速度极快,读写性能远高于传统的磁盘数据库。
持久化:虽然 Redis 是内存数据库,但它提供了数据持久化的机制,可以将内存中的数据异步地保存到硬盘,防止数据丢失。
丰富的数据类型:Redis 支持多种复杂的数据结构,如字符串、哈希、列表、集合、排序集合等。
原子操作:支持原子操作,可以通过管道(Pipelining)方式批量执行多个命令。
发布/订阅模式:支持消息队列模式,可以实现不同服务之间的异步通信。
高可用与分布式:通过 Redis 的主从复制、哨兵模式以及集群模式,可以实现高可用性和水平扩展。
常用类型有String,list,hash,set,zset
高级类型Bitmap(位图)、HyperLogLog、Geospatial(地理位置)、Streams(流)
最简单的类型,一个字符串最多可以是 512MB。
使用场景:
缓存:比如缓存用户的会话信息、网页、API 响应等。
计数器:通过 INCR 或 DECR 命令可以很方便地实现简单的计数器(例如访问量、点赞数等)。
限流:通过字符串类型和 INCR 命令,可以实现请求次数的计数,用于实现 API 的限流。
按插入顺序存储多个字符串。
使用场景:
消息队列:利用 Redis 列表,可以实现一个高效的消息队列。比如生产者将消息推送到列表的一端,消费者从另一端弹出并处理消息。
任务调度:可以将待处理的任务以列表形式存储,消费者通过从队列中弹出任务来进行处理。
存储不重复的字符串元素。
使用场景:
去重:适用于需要去重的场景,比如存储用户的浏览记录或点赞信息。
标签或兴趣管理:比如管理用户兴趣标签,用户可以关注不同的兴趣,而集合确保每个兴趣标签只会出现一次。
推荐系统:通过集合的交集、并集等操作,可以实现推荐系统的功能,例如找出共同兴趣的用户。
和 Set 类似,但每个元素都有一个分数,可以根据分数对元素进行排序。
使用场景:
排行榜:例如网站的用户积分排行榜,玩家的游戏得分排行榜,Redis 会自动根据分数排序并提供快速查询。
实时推荐:根据用户活跃度、评分等进行动态排序,并实时更新推荐结果。
限时优惠:可以存储和查询具有过期时间的优惠活动信息,例如商场的打折商品列表。
存储键值对的集合,适合存储对象。
使用场景:
用户信息存储:可以存储一个用户的各种属性(例如用户的姓名、年龄、邮箱等)。每个属性对应哈希中的一个字段。
配置项:存储一些系统的配置信息,便于快速读取和修改。
redis是数据存储在内存中,那么如果服务器重启之后或者redis崩溃之后数据不就丢失了吗
所以redis为了防止这种情况支持多种持久化方式,以便在服务器重启后仍然能够恢复数据。Redis 主要有两种持久化方式:**RDB(快照持久化)**和 AOF(追加文件持久化)。这两种持久化方式可以单独使用,也可以结合使用。
RDB 是 Redis 的默认持久化方式,它通过在指定时间点生成数据库的快照来实现持久化。快照是一个包含所有 Redis 数据的二进制文件,通常是 .rdb 文件。
工作原理:
Redis 会根据配置定期执行“生成快照”的操作,将当前内存中的数据保存到硬盘上的 RDB 文件中。
默认情况下,可以配置触发快照的条件,如某个时间内有多少次写操作,或者某段时间内数据是否发生了变化等。
RDB 文件存储的是 Redis 的完整数据快照,而不是增量更新。
优点:
快照生成过程相对较快。
数据文件较小,适合用于数据恢复。
不会影响 Redis 的读写操作,因为生成快照时 Redis 会通过 **fork(多进程)**创建子进程。
缺点:
如果 Redis 在生成快照期间崩溃,可能会丢失未写入磁盘的数据。
恢复速度可能较慢,特别是在数据量很大的时候。
配置:在 redis.conf 配置文件中,常见的 RDB 配置项如下:
save 900 1 # 900秒内有1次写操作时生成快照
save 300 10 # 300秒内有10次写操作时生成快照
save 60 10000 # 60秒内有10000次写操作时生成快照
假设你设置 Redis 每 1 分钟生成一个 RDB 快照:
第一分钟:假设数据库有 3 条记录,RDB 会生成包含这 3 条记录的快照文件。此时,dump.rdb 文件中包含了这 3 条记录。
第一到第二分钟之间新增了 5 条记录:在第二分钟到来时,Redis 会根据配置的策略生成新的 RDB 快照。新的快照会覆盖掉上一个快照,并且这次的快照会包含所有的记录,包括之前的 3 条和新增的 5 条记录。所以此时,新的 RDB 文件会包含 8 条记录。
第二到第三分钟之间新增了 7 条记录:如果 Redis 在此时崩溃或重启,RDB 文件会保留最后一次成功生成的快照,也就是第二分钟时的 8 条记录,而不会包含第三分钟内新增的 7 条记录。
总结:RDB 文件确实是每次生成快照时覆盖之前的文件。即使 Redis 崩溃后重启,它也只会恢复到最后一次成功保存的快照,这可能导致部分数据丢失,尤其是在上次快照之后发生的修改数据,这就是为什么RDB效率高,但是不安全。
AOF 通过记录所有的写操作(每次修改数据的命令)到一个日志文件中来实现数据持久化。这个文件通常是 appendonly.aof。
工作原理:
Redis 会将每个写操作(如 SET、HSET 等)追加到 AOF 文件中,这些操作在 Redis 重启后会重新执行以恢复数据。
AOF 文件会包含所有的写操作,Redis 会按照命令顺序依次执行,从而恢复出原来的数据。
AOF 提供了三种不同的同步策略:
always:每次操作后都将命令写入 AOF 文件,性能最差,但数据安全性最好。
everysec:每秒钟将 AOF 文件写入一次,是默认选项,兼顾性能和数据安全。
no:完全不进行同步,性能最好,但可能丢失所有未同步的数据。
优点:
提供更高的数据安全性,AOF 可以保证数据尽可能不丢失。
可以配置同步策略,灵活调整性能和持久化的安全性。
缺点:
AOF 文件会随着写操作的增多而变得较大。
由于每次写操作都需要记录到文件中,AOF 会对性能产生一定的影响。
在 Redis 重启时,恢复过程可能比 RDB 更慢,尤其是在写操作很多的情况下。
配置:在 redis.conf 配置文件中,常见的 AOF 配置项如下:
appendonly yes # 启用 AOF 持久化
appendfsync everysec # 每秒同步一次 AOF 文件
no-appendfsync-on-rewrite yes # 重写期间不进行同步
Redis 允许同时开启 RDB 和 AOF 持久化,这样可以利用两者的优点,避免单独使用时的缺点。
自redis4之后对更改了RDB + AOF 混合持久化的流程
在 Redis 4.0 之后,混合持久化将 RDB 数据和 AOF 日志存放在同一个文件中。这意味着 Redis 在生成持久化文件时,会先生成一个 RDB 快照(通常是某个时间点的全量快照),然后将自上次持久化以来的增量写操作(AOF)追加到这个文件中。这个文件是一个混合文件,它包含了两部分内容:
RDB 格式数据:在混合持久化文件的开头部分,包含了当前数据库的 RDB 快照内容。这个部分的数据格式与普通的 RDB 文件相同。
AOF 格式的增量操作:在 RDB 数据之后,紧跟着是自上次 RDB 持久化之后发生的增量操作,这部分是 AOF 格式的,记录了每个写操作(如 SET、DEL 等)。
假设你设置了每 1 分钟生成一个 RDB 快照,而每分钟新增 5 条数据。混合持久化在生成文件时的存储内容大致如下:
假设场景:
每 1 分钟生成一个 RDB 快照。
每分钟新增 5 条数据,假设这些数据的操作是 SET key:value(这里以 5 条为例)。
混合持久化文件的结构:
第 1 分钟的 RDB 快照:在文件的开头会有一个完整的 RDB 格式的快照,包含你数据库的所有数据(比如 10 条记录)。
这部分数据是全量快照,比如:
RDB header
数据条目 1 (key1: value1)
数据条目 2 (key2: value2)
...
数据条目 N (keyN: valueN)
自上次 RDB 快照之后的增量 AOF 操作:在 RDB 快照之后,会有一段 AOF 格式的增量操作,这部分操作会记录每一个写操作。
假设在第 1 分钟到第 2 分钟之间新增了 5 条数据(SET key1 value1, SET key2 value2, …),那么在混合持久化文件中会紧跟 RDB 数据之后记录这些操作:
AOF操作 1 (SET key1 value1)
AOF操作 2 (SET key2 value2)
AOF操作 3 (SET key3 value3)
AOF操作 4 (SET key4 value4)
AOF操作 5 (SET key5 value5)
然后里面的格式大致如下
RDB快照:
key1: "value1"
key2: "value2"
key3: "value3"
AOF增量操作:
SET key4 "value4"
SET key5 "value5"
SET key6 "value6"
SET key7 "value7"
SET key8 "value8"
下一个 RDB 快照:在第 2 分钟时,Redis 会生成一个新的 RDB 快照,这时会记录整个数据库的当前状态(例如,除了第 1 分钟的 10 条记录外,再新增了 5 条)。这个快照会覆盖前一个 RDB 快照。
自第 2 分钟以来的增量 AOF 操作:在第 2 分钟到第 3 分钟之间新增的 5 条数据,也会记录在增量 AOF 部分。
混合持久化将 RDB 快照 和 增量 AOF 操作 存放在同一个文件中。
RDB 文件存储的是一个完整的数据库快照,而AOF 文件存储的是自上次 RDB 快照后的增量操作(如每个 SET、DEL 等命令)。
在 Redis 重启时,Redis 会先加载 RDB 快照文件,恢复大部分数据,然后根据增量 AOF 操作补充那些在 RDB 快照之后的修改操作,从而恢复到最后的数据库状态。
RDB 是通过生成一个数据库的“快照”来实现数据持久化的。具体来说,Redis 会将当前内存中的数据存储到一个二进制的 .rdb 文件中。这个过程是由 Redis 的子进程(通过 fork 创建)来执行的,主进程继续处理客户端的请求。快照生成时,子进程会复制当前的内存数据并保存到磁盘。
RDB 更高效的原因:
RDB 是通过一次性保存所有数据的快照来实现持久化的,整个过程是比较简单且高效的,尤其是在数据量比较小或保存间隔较长时。
生成快照的过程不会影响 Redis 主进程的性能,因为它是通过 fork 创建子进程的,主进程不需要等待子进程的写入操作,从而可以继续响应客户端请求。
但是,需要注意的是,RDB 生成快照的过程在数据量比较大的时候可能会变得较慢,因为它需要将所有数据(无论是大还是小)都写入硬盘。
AOF 是通过将每个写操作(如 SET、HSET 等)追加到日志文件中来记录数据。每次写操作发生时,Redis 会将相应的命令记录到 AOF 文件中,从而确保可以在 Redis 重启时通过重新执行这些命令恢复数据。
AOF 更安全的原因:
AOF 提供了三种不同的同步策略,可以精确控制数据持久化的安全性。特别是 appendfsync always(每次写操作都同步到磁盘)能够最大程度地保证数据不会丢失。
即便 Redis 崩溃,AOF 文件也能记录所有的写操作,因此通过重放 AOF 文件中的命令可以恢复所有的操作。
AOF 的安全性来源于它记录了每一个写操作,在 Redis 重启时,只需要按顺序执行这些操作,就可以精确地恢复数据。
首先,AOF 文件记录了所有的写操作,包括 SET、HSET、DEL 等命令。这意味着每个写操作(无论是插入、修改还是删除)都会被记录下来。而因为 Redis 是一个高性能的内存数据库,通常对数据进行频繁的修改,比如 SET 命令可能会多次修改相同的键值。
这样,随着时间的推移,AOF 文件会记录下大量的“修改操作”,这些操作的执行结果有可能是“覆盖之前的数据”,但是它并不意味着那些修改操作本身是必要的。实际上,我们可以通过重写 AOF 文件来去掉这些不必要的操作,保留最新的数据状态。
因此为什么需要 AOF 重写?
AOF 重写的目的是: 缩小文件的大小,同时去除不必要的历史修改操作(例如,某个键的多个 SET 操作),确保 AOF 文件只记录当前数据的最终状态,而不需要记录每一步修改。通过这种方式,AOF 文件的大小可以被有效控制。
举个例子:
假设有以下操作序列:
SET key “hello”
SET key “world”
DEL key
假设你的 key 原始值是 “hello”,然后它被 SET 改为 “world”,最后被删除。如果不进行重写,AOF 文件会记录三条命令:
SET key "hello"
SET key "world"
DEL key
但是在 Redis 重新启动时,这三条命令会依次执行,导致 “world” 变成了 nil(因为最后是删除操作)。实际上,你只需要在 AOF 文件中保留一个DEL key 操作,而不需要再保留之前的 SET 操作。
AOF 重写过程:
重写过程中,Redis 会重新扫描当前的数据库状态,并生成一个新的 AOF 文件,其中仅记录恢复当前数据所必需的最少命令。
比如,AOF 重写后,新的文件可能只包含 DEL key,因为这是恢复当前状态的唯一必要命令。
通过 AOF 重写,Redis 会减少不必要的历史修改操作,从而减少文件大小。
咱们使用redis的时候经常作为缓存,因为是内存数据库具有高性能,但是作为缓存会有以下问题,所以设计的时候需要考虑到以下场景的情况
定义:
缓存穿透是指请求的数据在缓存中没有命中,并且请求的数据在数据库中也不存在。也就是说,缓存没有这个数据,数据库查询也没有这个数据。由于没有缓存,可以直接访问数据库,导致大量无效请求打到数据库,造成数据库压力,无法利用缓存的优势。
为什么会有缓存穿透:
不存在的数据:有些请求可能查询的是根本不存在的数据(例如恶意请求、非法请求等)。
缓存未命中:缓存中的数据不存在或已过期,因此会查询数据库。
解决方案:
对于查询结果为空的数据(即数据库没有这个数据),可以在缓存中也存一个空值,设置一个较短的过期时间。这样,下次如果再有相同的请求,就能从缓存中直接返回空值,避免再次访问数据库。
通过接口层或前端校验来过滤非法请求,确保不向缓存或数据库发送无效请求。
定义:
缓存击穿是指某一时刻,大量请求访问一个过期的缓存数据,也叫热点数据,而缓存数据已经过期或失效。此时,所有的请求都会直接访问数据库,导致数据库瞬间压力增大,性能下降。
解决方案:
在缓存过期时,只允许一个请求去查询数据库并更新缓存,其它请求需要等待。
代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CacheBreakdownSolution {
private RedisClient redisClient;
private DatabaseClient databaseClient;
private Lock lock = new ReentrantLock();
public String getData(String key) {
// 先尝试从缓存中获取数据
String cacheValue = redisClient.get(key);
if (cacheValue == null) { // 如果缓存中没有数据
lock.lock(); // 获取锁,防止多个线程同时查询数据库
try {
// 再次检查缓存是否已经被其他线程填充
cacheValue = redisClient.get(key);
if (cacheValue == null) {
// 查询数据库
String dbValue = databaseClient.query(key);
if (dbValue == null) {
redisClient.set(key, "empty", 60); // 缓存空数据
return null;
}
redisClient.set(key, dbValue, 3600); // 缓存数据
return dbValue;
}
} finally {
lock.unlock(); // 释放锁
}
}
return cacheValue; // 如果缓存中有数据,直接返回
}
}
通过延长热点数据过期时间,减少发生的概率
定义
缓存雪崩是指在同一时刻,大量缓存数据集中失效(比如缓存批量过期),这时大量请求直接访问数据库,导致数据库承受巨大压力,甚至崩溃。
可能情况:
缓存同时过期:如果大量缓存设置了相同的过期时间,缓存一旦失效,就会在同一时刻大量过期,从而导致大量请求直接访问数据库。
解决方案:
避免所有缓存设置相同的过期时间,可以给不同的数据设置不同的过期时间,降低缓存同时过期的概率。
对于某些数据,可以通过将缓存设置为永不过期,避免频繁失效,但需要定期通过后台线程或任务来清理过期的数据。
当缓存失效并且访问量较大时,可以设计降级机制,对于一些不重要的数据返回默认值或者静态页面,减少数据库的负载。
常见的部署模式包括 单机模式、主从模式、哨兵模式 和 集群模式。每种模式适用于不同的场景
特点:单机模式是 Redis 最简单的部署方式,所有数据都存储在一个 Redis 实例中,只有一个 Redis 进程在运行。
用途:适用于数据量较小,或者对高可用性和扩展性要求不高的场景。通常用于开发和测试环境,或者一些简单的小型应用。
特点:主从模式下,存在一个主节点(Master)和多个从节点(Slave)。主节点负责写操作和数据更新,而从节点负责读取数据,且从节点会从主节点同步数据。主从模式提高了读取性能,但主节点的故障会导致整个系统不可用。
用途:适用于读取压力较大的场景,通过从节点分担读取压力,提高读取性能。数据的高可用性较低,因为如果主节点宕机,从节点只能继续提供读服务,无法进行写操作。
使用场景:适用于读多写少的应用场景,例如缓存、日志存储、排行榜等。
配置方法:从节点通过 slaveof 指令指定主节点。 在从节点的 redis.conf 配置文件中,加入以下配置:
slaveof <master-ip> <master-port>
也可以运行直接使用命令
redis-server --slaveof <master-ip> <master-port>
特点:哨兵模式是 Redis 为了实现高可用性而提供的解决方案。哨兵(Sentinel)是一个独立的进程,它监控 Redis 实例的状态。如果主节点宕机,哨兵会自动选举一个从节点提升为主节点,并通知客户端新的主节点地址。哨兵还可以提供自动故障转移和配置更新。
用途:适用于要求高可用性的场景,通过自动故障转移来保证系统的稳定性。在主节点发生故障时,不会影响系统的正常运行。
使用场景:适用于生产环境中需要高可用的应用,尤其是对数据写入要求较高的场景,如分布式系统、实时数据处理等。
配置方法:每个哨兵需要独立的配置文件。配置文件名通常为 sentinel.conf,并在文件中指定监控的主节点。
创建一个哨兵配置文件 sentinel.conf,并在文件中配置要监控的主节点信息:
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
解释:
mymaster 是要监控的主节点名称。
127.0.0.1 和 6379 是主节点的 IP 地址和端口。
2 是最少的哨兵数(需要多少个哨兵同意才能执行故障转移)。
down-after-milliseconds 是判定主节点宕机的时间。
failover-timeout 和 parallel-syncs 分别设置故障转移的超时和同步数量。
其中为什么不配置从节点,因为哨兵模式是基于主从模式的,我们知道主节点之后就知道从节点了
启动多个哨兵来提供冗余和容错能力:
redis-server /path/to/sentinel1.conf --sentinel
redis-server /path/to/sentinel2.conf --sentinel
**主从和哨兵都没有解决一个问题:单个节点的存储能力和访问能力是有限的。**集群模式把数据进行分片存储。集群的键空间被分割成16384个slots(即hash卡槽),通过hash的方式将数据分到不同的分片上。某个主节点宕机,那这个主节点下的从节点会通过选举产生一个主节点,替换原来的故障节点
特点:集群模式是 Redis 为了解决水平扩展(sharding)问题而设计的。它将数据分布到多个 Redis 节点上,通过哈希槽(Hash Slot)将数据分散到不同的节点。集群模式支持自动分片、自动故障转移等特性,支持多个主节点和从节点,具有较高的扩展性和可用性。
用途:适用于数据量大、需要水平扩展的场景,能够提供更高的性能和可用性。集群模式通过分片来提高存储容量和处理能力。
使用场景:适用于需要大规模数据存储、高并发访问的应用,如社交网络、大数据分析、推荐系统等。
主要是Redis有一些4个特征
内存访问速度:内存是计算机中 最快的存储介质,其读取速度和写入速度非常高,通常在纳秒级别(ns)。内存是直接由 CPU 控制的,数据可以直接被读取到 CPU 中进行处理,不需要额外的操作。
磁盘访问速度:与内存相比,传统的硬盘(特别是机械硬盘)访问速度慢得多。机械硬盘的读写速度通常在毫秒级(ms)或微秒级(µs),远远慢于内存。即使是 固态硬盘(SSD),它的访问速度也只能达到内存的一个较低级别,大约是纳秒级的数千倍到万倍,虽然 SSD 要比传统硬盘快,但还是不及内存。
Redis在设计过程中使用的是单线程,为什么不使用多线程?
首先,Redis 使用的是 单线程模型。所有的客户端请求都通过一个线程进行处理,看似这是一个限制,但实际上,这反而是 Redis 性能高的关键之一。
没有严重的上下文切换,锁的性能开销,以及实现的复杂度,Redis单线程已经够用
这里虽然Redis是单线程,但是使用i/o多路复用,以及利用事件驱动
事件驱动:Redis 使用事件循环的方式处理请求,所有的请求(无论是读还是写操作)都通过一个事件循环队列进行排队,Redis 会按照事件的顺序依次处理请求。这个事件循环是非阻塞的,意味着当 Redis 正在处理某个请求时,它不会因等待某个 I/O 操作(如网络读取或磁盘操作)而阻塞其他操作。
通过这种方式,Redis 不会在处理请求的过程中空闲等待,而是会一直保持高效地执行其他任务。
I/O 多路复用:I/O 多路复用技术(如 epoll,select等)允许 Redis 在同一线程中同时监听多个网络连接的状态。Redis 可以在一个线程中同时处理多个客户端的请求,而不是每次都为每个客户端请求创建一个新的线程。这是通过 I/O 多路复用机制来完成的。
具体来说,Redis 通过 I/O 多路复用机制,能够在网络层同时 监听多个客户端的连接,并且在数据准备好时,迅速响应。这与传统的阻塞 I/O 有本质的不同。传统的阻塞 I/O 会让线程阻塞在等待数据准备的状态,直到数据准备好才能继续执行,而 Redis 通过 I/O 多路复用,能够在同一线程中 高效地轮询多个客户端的连接,确保在等待数据的过程中不浪费任何 CPU 时间。
Redis 在处理缓存过期数据时,采用了 定期删除 和 惰性删除 两种策略
惰性删除是 Redis 在访问过期数据时进行的处理方式。具体来说,只有当客户端访问一个已经过期的键时,Redis 才会删除这个键。
过程:
客户端尝试访问一个缓存键,如果这个键已经过期,Redis 会发现该键已过期并将其删除。
删除过程是在访问时动态进行的,因此不会在 Redis 的整个内存空间中提前清理过期的数据。
这种方式的优点是删除操作是按需进行的,避免了 Redis 在空闲时清理过期数据的额外负担。
缺点:
性能问题:如果大量的过期键在同一时间段被访问,可能会带来一定的性能影响。因为 Redis 会逐一检测每个访问的键的过期时间并执行删除操作。
内存浪费:如果过期的数据没有被访问到,它们会一直占用内存,直到 Redis 自动清理这些键。
为了避免惰性删除带来的性能问题,Redis 定期会触发删除过期数据的操作。这个操作是通过 Redis 的 过期检查机制 来实现的。
过程:
Redis 会在后台定期执行一个任务,扫描一部分键的过期时间,并删除已过期的键。
这种机制是通过 定时任务 来实现的,通常每 100ms 执行一次。
Redis 在每次周期内会随机选择一定数量的键,检查它们的过期时间,并删除过期的键。
定期删除的关键点:
采样机制:为了减少每次检查的开销,Redis 并不会每次都扫描所有的键,而是会随机选取一定数量的键进行过期检查,这样可以避免全量扫描带来的性能瓶颈。
删除策略:定期删除的操作会删除所有过期的键,但如果某些键在这次扫描中没有被扫描到,它们会在下一个周期里被检查。这样可以确保过期键最终会被清除。
当 Redis 达到配置的最大内存限制时(maxmemory 设置),Redis 会根据配置的 内存淘汰策略 自动删除一些键,以释放内存空间。这个策略是避免 Redis 内存溢出的关键机制。常见的淘汰策略有:
noeviction:当内存达到上限时,Redis 会拒绝写入操作,直到有足够的内存空间释放。这是最保守的策略。
allkeys-lru:对所有键使用 LRU(Least Recently Used)算法,删除最久未使用的数据来释放内存。
volatile-lru:仅对设置了过期时间的键使用 LRU 策略。
allkeys-random:随机淘汰所有键,以释放内存。
volatile-random:随机淘汰设置了过期时间的键。
volatile-ttl:优先删除设置了过期时间的键,且过期时间最短的键会最先被删除。
惰性删除默认开启,定期删除需要自己设置时间,以及开启策略
Redis 配置文件示例,包含了内存限制、过期时间、淘汰策略等设置:
# 最大内存设置为 2GB
maxmemory 2gb
# 设置内存淘汰策略为 allkeys-lru(针对所有键的 LRU 淘汰)
maxmemory-policy allkeys-lru
# 定期删除检查间隔
hz 10 # 默认每 100ms 执行一次
# 设置内存警告阈值,达到最大内存限制时,Redis 会开始使用内存淘汰策略
maxmemory-samples 5 # 用 5 个样本来评估淘汰策略
# 启用过期键自动删除
notify-keyspace-events Ex # 监控过期事件
对于咱们项目而已,一般来说引入依赖,编写个redisUtil工具类就行了,但是这只是最简单的功能,我们通常还需要编写redisConfig配置类来实现更高级的功能
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
private static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String NORM_DATE_PATTERN = "yyyy-MM-dd";
private static final String NORM_TIME_PATTERN = "HH:mm:ss";
@Resource
private RedisProperties redisProperties;
@Bean(destroyMethod = "destroy")
public LettuceConnectionFactory redisConnectionFactory() {
// redis单节点
if (null == redisProperties.getCluster() || null == redisProperties.getCluster().getNodes()) {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisProperties.getHost(),
redisProperties.getPort());
configuration.setPassword(redisProperties.getPassword());
return new LettuceConnectionFactory(configuration);
}
// redis集群
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
redisClusterConfiguration.setPassword(redisProperties.getPassword());
redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
genericObjectPoolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
genericObjectPoolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
// genericObjectPoolConfig.setMaxWaitMillis(redisProperties.getLettuce().getPool().getMaxWait().toMillis());
genericObjectPoolConfig.setMaxWait(Duration.ofMillis(redisProperties.getLettuce().getPool().getMaxWait().toMillis()));
// 支持自适应集群拓扑刷新和动态刷新源
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enableAllAdaptiveRefreshTriggers()
// 开启自适应刷新
.enableAdaptiveRefreshTrigger()
// 开启定时刷新
.enablePeriodicRefresh(Duration.ofSeconds(5))
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
.topologyRefreshOptions(clusterTopologyRefreshOptions).build();
LettuceClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig)
.readFrom(ReadFrom.REPLICA_PREFERRED)
.clientOptions(clusterClientOptions).build();
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
lettuceConnectionFactory.setShareNativeConnection(false);// 是否允许多个线程操作共用同一个缓存连接,默认 true,false 时每个操作都将开辟新的连接
lettuceConnectionFactory.resetConnection();// 重置底层共享连接, 在接下来的访问时初始化
return lettuceConnectionFactory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//处理日期/时间格式化
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
simpleModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(NORM_DATE_PATTERN)));
simpleModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(NORM_TIME_PATTERN)));
simpleModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
simpleModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(NORM_DATE_PATTERN)));
simpleModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(NORM_TIME_PATTERN)));
objectMapper.registerModule(simpleModule);
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper,Object.class);
//key 和 hashkey 设置 String 序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
//value 和 hashValue设置 json 序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfiguration = getRedisCacheConfigurationWithTtl(60 * 60 * 24 * 30);
return new CustomizedRedisCacheManager(redisConnectionFactory, cacheConfiguration, getRedisCacheConfigurationMap());
}
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put("User", this.getRedisCacheConfigurationWithTtl(60 * 60 * 24 * 29));
return redisCacheConfigurationMap;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
ObjectMapper objectMapper = new ObjectMapper();
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(seconds))
.disableCachingNullValues()
.computePrefixWith(cacheName -> cacheName.concat(":"))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(createJackson2JsonRedisSerializer(objectMapper)));
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
private RedisSerializer<Object> createJackson2JsonRedisSerializer(ObjectMapper objectMapper) {
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
}
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
作用:声明为配置类,确保在Spring Boot默认的RedisAutoConfiguration之后加载,覆盖或增强默认配置。
private static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String NORM_DATE_PATTERN = "yyyy-MM-dd";
private static final String NORM_TIME_PATTERN = "HH:mm:ss";
作用:统一Java 8日期类型(LocalDateTime等)的序列化格式,确保Redis中存储的日期数据可读且格式一致。
@Resource
private RedisProperties redisProperties;
作用:自动注入Spring Boot的Redis配置(来自application.yml或application.properties),用于动态构建连接参数。
这里redis配置在nacos中
spring.cloud.nacos.config.extension-configs[6].group=HTYC_GROUP
spring.cloud.nacos.config.extension-configs[6].data-id=shared.redis.properties
spring.cloud.nacos.config.extension-configs[6].refresh=true
# Redis服务器地址
spring.data.redis.host=redis
# Redis服务器连接端口
spring.data.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.data.redis.password=123456
# Redis数据库索引(默认为0)
spring.data.redis.database=0
# 连接超时时间(毫秒)
spring.data.redis.timeout=1000
spring.data.redis.commandTimeout=5000
# 连接池最大连接数(使用负值表示没有限制)
spring.data.redis.jedis.pool.max-active=1000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.data.redis.jedis.pool.max-wait=5000
# 连接池中的最大空闲连接
spring.data.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.data.redis.jedis.pool.min-idle=2
#连接耗尽时是否阻塞, false报异常,ture阻塞直到超时
spring.data.redis.block-when-exhausted=true
if (null == redisProperties.getCluster() || null == redisProperties.getCluster().getNodes()) {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(
redisProperties.getHost(), redisProperties.getPort());
configuration.setPassword(redisProperties.getPassword());
return new LettuceConnectionFactory(configuration);
}
逻辑:若未配置集群节点,则使用单节点配置。
关键参数:
host和port:从配置读取。
password:支持认证。
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
redisClusterConfiguration.setPassword(redisProperties.getPassword());
redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
作用:初始化集群配置。
参数:
nodes:集群节点地址列表(如127.0.0.1:6379,127.0.0.1:6380)。
maxRedirects:最大重定向次数(用于MOVED/ASK错误处理)。
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
genericObjectPoolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
genericObjectPoolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
genericObjectPoolConfig.setMaxWait(Duration.ofMillis(redisProperties.getLettuce().getPool().getMaxWait().toMillis()));
作用:配置Lettuce连接池参数,避免频繁创建连接。
参数:
maxTotal:最大连接数。
maxIdle/minIdle:最大/最小空闲连接。
maxWait:获取连接的最大等待时间(避免线程长时间阻塞)。
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enableAllAdaptiveRefreshTriggers() // 自动触发刷新(如节点不可用、MOVED重定向)
.enableAdaptiveRefreshTrigger() // 启用自适应刷新
.enablePeriodicRefresh(Duration.ofSeconds(5)) // 定时刷新集群拓扑
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
.topologyRefreshOptions(clusterTopologyRefreshOptions).build();
作用:确保客户端能感知集群节点变化(如节点故障转移、扩容)。
触发条件:
自适应刷新:在收到MOVED、ASK错误或节点断开时自动刷新。
定时刷新:每5秒强制刷新一次。
LettuceClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig)
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从副本读取
.clientOptions(clusterClientOptions)
.build();
关键配置:
readFrom:定义读取策略,REPLICA_PREFERRED表示优先读副本,提升读性能(写仍由主节点处理)。
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
lettuceConnectionFactory.setShareNativeConnection(false); // 每个操作使用独立连接
lettuceConnectionFactory.resetConnection(); // 初始化时重置连接
setShareNativeConnection(false):禁用连接共享,确保线程安全(默认true可能引发并发问题)。
resetConnection():初始化时强制刷新连接,避免陈旧连接。
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
作用:定制JSON序列化行为。
关键配置:
允许反序列化未知字段(FAIL_ON_UNKNOWN_PROPERTIES=false)。
启用类型信息(activateDefaultTyping),解决泛型反序列化问题(如List还原为ArrayList的问题)。
SimpleModule simpleModule = new SimpleModule();
// 添加序列化与反序列化器
simpleModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
simpleModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
// 其他日期类型类似...
objectMapper.registerModule(simpleModule);
确保日期类型以指定格式(如yyyy-MM-dd HH:mm:ss)存储,而非默认的数组或数字格式。、
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
redisTemplate.setKeySerializer(RedisSerializer.string()); // Key使用字符串序列化
redisTemplate.setValueSerializer(jsonRedisSerializer); // Value使用JSON序列化
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
优势:
Key可读性强(如user:1而非二进制)。
Value支持复杂对象(嵌套结构、泛型集合)。
RedisCacheConfiguration cacheConfiguration = getRedisCacheConfigurationWithTtl(60 * 60 * 24 * 30); // 默认30天
作用:全局缓存默认过期时间(30天),适用于未单独配置的缓存
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> map = new HashMap<>();
map.put("User", this.getRedisCacheConfigurationWithTtl(60 * 60 * 24 * 29)); // User缓存29天
return map;
}
作用:为特定缓存(如名为User的缓存)设置独立TTL(29天),实现细粒度控制。
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(seconds)) // 过期时间
.disableCachingNullValues() // 禁止缓存null值
.computePrefixWith(cacheName -> cacheName.concat(":")) // Key前缀(如User:1)
.serializeKeysWith(StringRedisSerializer.UTF_8) // Key序列化
.serializeValuesWith(new GenericJackson2JsonRedisSerializer()); // Value序列化
关键点:
前缀隔离:避免不同业务缓存Key冲突。
空值过滤:节省存储空间,避免缓存穿透。
序列化统一:与RedisTemplate保持一致,确保缓存数据可被正确读取。
该配置类通过深度定制连接管理、序列化策略和缓存控制,解决了以下问题:
高可用:自适应集群拓扑刷新,支持故障转移。
可维护性:JSON序列化使数据可读,日期格式统一。
灵活性:支持不同缓存独立配置TTL。
性能:连接池优化、读写分离提升吞吐量。