Redis 详解

简介

Redis 的全称是 Remote Dictionary Server,它是一个基于内存的 NoSQL(非关系型)数据库,数据以 键值对 存储,支持各种复杂的数据结构

为什么会出现 Redis?

Redis 的出现是为了弥补传统数据库在高性能要求下的不足传统的关系型数据库在读写速度、并发处理和扩展性上有时不能满足某些高并发场景的需求。尤其是在互联网应用中,对于海量数据的处理、低延迟和高吞吐量的要求越来越高,Redis 提供了一种高效且灵活的解决方案

Redis的作用

大量在以下方面使用:
缓存系统: Redis 经常被用作缓存数据库,减轻传统关系型数据库的压力,提升访问速度。例如,在 Web 应用中,Redis 可以缓存数据库查询结果,当用户请求相同的数据时,可以直接从 Redis 获取,而不需要每次都访问数据库。这样可以大幅提高响应速度和降低数据库负载

高性能的数据存储: Redis 是一个内存数据库,它将数据存储在内存中,因而读取和写入速度非常快。它也支持持久化功能,可以定期将数据存储到硬盘上,避免丢失数据。

消息队列Redis 支持发布/订阅模式以及基于列表的数据结构,可以用作消息队列系统处理高并发的消息传递。例如,生产者将消息写入 Redis 队列,消费者从队列中获取消息进行处理,支持高效的异步任务处理。

分布式锁: Redis 提供了分布式锁的实现,可以解决多个进程或服务对同一资源的并发控制问题确保在分布式环境下只有一个线程能够执行特定的操作

实时分析和计数: Redis 可以处理实时的数据分析和计数,比如网站访问量统计、实时数据处理等。它的高性能使得它能够在大规模、高并发的环境下工作。

Redis的特点:

高性能:Redis 是内存存储,数据操作速度极快,读写性能远高于传统的磁盘数据库。
持久化:虽然 Redis 是内存数据库,但它提供了数据持久化的机制,可以将内存中的数据异步地保存到硬盘,防止数据丢失。
丰富的数据类型:Redis 支持多种复杂的数据结构,如字符串、哈希、列表、集合、排序集合等。
原子操作:支持原子操作,可以通过管道(Pipelining)方式批量执行多个命令。
发布/订阅模式:支持消息队列模式,可以实现不同服务之间的异步通信。
高可用与分布式:通过 Redis 的主从复制、哨兵模式以及集群模式,可以实现高可用性和水平扩展

Redis的常用数据类型:

常用类型有String,list,hash,set,zset
高级类型Bitmap(位图)、HyperLogLog、Geospatial(地理位置)、Streams(流)

String(字符串):

最简单的类型,一个字符串最多可以是 512MB。

使用场景
缓存:比如缓存用户的会话信息、网页、API 响应等。
计数器:通过 INCR 或 DECR 命令可以很方便地实现简单的计数器(例如访问量、点赞数等)。
限流:通过字符串类型和 INCR 命令,可以实现请求次数的计数,用于实现 API 的限流。

List(列表):

按插入顺序存储多个字符串。

使用场景:
消息队列:利用 Redis 列表,可以实现一个高效的消息队列。比如生产者将消息推送到列表的一端,消费者从另一端弹出并处理消息。
任务调度:可以将待处理的任务以列表形式存储,消费者通过从队列中弹出任务来进行处理。

Set(集合):

存储不重复的字符串元素。

使用场景
去重:适用于需要去重的场景,比如存储用户的浏览记录或点赞信息。
标签或兴趣管理:比如管理用户兴趣标签,用户可以关注不同的兴趣,而集合确保每个兴趣标签只会出现一次。
推荐系统:通过集合的交集、并集等操作,可以实现推荐系统的功能,例如找出共同兴趣的用户。

Sorted Set(有序集合):

和 Set 类似,但每个元素都有一个分数,可以根据分数对元素进行排序。

使用场景
排行榜:例如网站的用户积分排行榜,玩家的游戏得分排行榜,Redis 会自动根据分数排序并提供快速查询。
实时推荐:根据用户活跃度、评分等进行动态排序,并实时更新推荐结果。
限时优惠:可以存储和查询具有过期时间的优惠活动信息,例如商场的打折商品列表。

Hash(哈希):

存储键值对的集合,适合存储对象。

使用场景:
用户信息存储:可以存储一个用户的各种属性(例如用户的姓名、年龄、邮箱等)。每个属性对应哈希中的一个字段。
配置项:存储一些系统的配置信息,便于快速读取和修改。

持久化

redis是数据存储在内存中,那么如果服务器重启之后或者redis崩溃之后数据不就丢失了吗
所以redis为了防止这种情况支持多种持久化方式,以便在服务器重启后仍然能够恢复数据。Redis 主要有两种持久化方式:**RDB(快照持久化)**和 AOF(追加文件持久化)。这两种持久化方式可以单独使用,也可以结合使用。

RDB(快照持久化)

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次写操作时生成快照

RDB 快照的生成和覆盖过程

假设你设置 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(追加文件持久化)

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 # 重写期间不进行同步

RDB + AOF 混合持久化

Redis 允许同时开启 RDB 和 AOF 持久化,这样可以利用两者的优点,避免单独使用时的缺点。

自redis4之后对更改了RDB + AOF 混合持久化的流程

混合持久化的基本流程
  1. 判断是否开启 AOF 持久化:如果 Redis 配置中开启了 AOF 持久化,Redis 会继续执行混合持久化的后续流程;如果未开启 AOF,Redis 会直接加载 RDB 文件进行恢复。
  2. 检查 appendonly.aof 文件是否存在
    如果 AOF 文件存在,Redis 会继续执行混合持久化恢复流程。
    如果 AOF 文件不存在,Redis 会直接加载 RDB 文件进行恢复。
  3. 判断 AOF 文件是否包含 RDB 格式数据
    Redis 会检查 AOF 文件的开头是否是 RDB 格式的数据头。这个特性是混合持久化的关键。
    如果 AOF 文件的开头包含 RDB 格式的数据,Redis 会首先加载这些 RDB 数据,然后继续加载 AOF 文件中剩余的操作日志。
    如果 AOF 文件的开头没有 RDB 格式数据,Redis 会直接加载整个 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 生成效率更快?

RDB 是通过生成一个数据库的“快照”来实现数据持久化的。具体来说,Redis 会将当前内存中的数据存储到一个二进制的 .rdb 文件中。这个过程是由 Redis 的子进程(通过 fork 创建)来执行的主进程继续处理客户端的请求。快照生成时,子进程会复制当前的内存数据并保存到磁盘。

RDB 更高效的原因

RDB 是通过一次性保存所有数据的快照来实现持久化的,整个过程是比较简单且高效的,尤其是在数据量比较小或保存间隔较长时。
生成快照的过程不会影响 Redis 主进程的性能,因为它是通过 fork 创建子进程的,主进程不需要等待子进程的写入操作,从而可以继续响应客户端请求。
但是,需要注意的是,RDB 生成快照的过程在数据量比较大的时候可能会变得较慢,因为它需要将所有数据(无论是大还是小)都写入硬盘。

AOF 为什么更安全?

AOF 是通过将每个写操作(如 SET、HSET 等)追加到日志文件中来记录数据。每次写操作发生时,Redis 会将相应的命令记录到 AOF 文件中,从而确保可以在 Redis 重启时通过重新执行这些命令恢复数据。

AOF 更安全的原因:

AOF 提供了三种不同的同步策略,可以精确控制数据持久化的安全性。特别是 appendfsync always(每次写操作都同步到磁盘)能够最大程度地保证数据不会丢失。
即便 Redis 崩溃,AOF 文件也能记录所有的写操作,因此通过重放 AOF 文件中的命令可以恢复所有的操作。
AOF 的安全性来源于它记录了每一个写操作,在 Redis 重启时,只需要按顺序执行这些操作,就可以精确地恢复数据。

重写AOF
为什么要重写AOF

首先,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的时候经常作为缓存,因为是内存数据库具有高性能,但是作为缓存会有以下问题,所以设计的时候需要考虑到以下场景的情况

缓存穿透、缓存击穿、缓存雪崩

缓存穿透

定义:
缓存穿透是指请求的数据在缓存中没有命中,并且请求的数据在数据库中也不存在。也就是说,缓存没有这个数据,数据库查询也没有这个数据。由于没有缓存,可以直接访问数据库,导致大量无效请求打到数据库,造成数据库压力,无法利用缓存的优势。

为什么会有缓存穿透:
不存在的数据:有些请求可能查询的是根本不存在的数据(例如恶意请求、非法请求等)。
缓存未命中:缓存中的数据不存在或已过期,因此会查询数据库。

解决方案:

缓存空数据:

对于查询结果为空的数据(即数据库没有这个数据),可以在缓存中也存一个空值,设置一个较短的过期时间。这样,下次如果再有相同的请求,就能从缓存中直接返回空值,避免再次访问数据库。

前端防护:

通过接口层或前端校验来过滤非法请求,确保不向缓存或数据库发送无效请求。

缓存击穿(Cache Breakdown)

定义:
缓存击穿是指某一时刻,大量请求访问一个过期的缓存数据,也叫热点数据,而缓存数据已经过期或失效。此时,所有的请求都会直接访问数据库,导致数据库瞬间压力增大,性能下降

解决方案:

互斥锁机制:

在缓存过期时,只允许一个请求去查询数据库并更新缓存,其它请求需要等待。

代码

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

集群模式(Cluster)

**主从和哨兵都没有解决一个问题:单个节点的存储能力和访问能力是有限的。**集群模式把数据进行分片存储。集群的键空间被分割成16384个slots(即hash卡槽),通过hash的方式将数据分到不同的分片上。某个主节点宕机,那这个主节点下的从节点会通过选举产生一个主节点,替换原来的故障节点

特点:集群模式是 Redis 为了解决水平扩展(sharding)问题而设计的。它将数据分布到多个 Redis 节点上,通过哈希槽(Hash Slot)将数据分散到不同的节点。集群模式支持自动分片、自动故障转移等特性,支持多个主节点和从节点,具有较高的扩展性和可用性。
用途:适用于数据量大、需要水平扩展的场景,能够提供更高的性能和可用性。集群模式通过分片来提高存储容量和处理能力。
使用场景:适用于需要大规模数据存储、高并发访问的应用,如社交网络、大数据分析、推荐系统等。

为什么Redis处理速度快

主要是Redis有一些4个特征

  1. 内存存储
  2. 单线程
  3. 多路i/o复用
  4. 数据结构

内存存储:

内存访问速度:内存是计算机中 最快的存储介质,其读取速度和写入速度非常高,通常在纳秒级别(ns)。内存是直接由 CPU 控制的,数据可以直接被读取到 CPU 中进行处理,不需要额外的操作。
磁盘访问速度:与内存相比,传统的硬盘(特别是机械硬盘)访问速度慢得多。机械硬盘的读写速度通常在毫秒级(ms)或微秒级(µs),远远慢于内存。即使是 固态硬盘(SSD),它的访问速度也只能达到内存的一个较低级别,大约是纳秒级的数千倍到万倍,虽然 SSD 要比传统硬盘快,但还是不及内存。

单线程

Redis在设计过程中使用的是单线程,为什么不使用多线程?
首先,Redis 使用的是 单线程模型。所有的客户端请求都通过一个线程进行处理,看似这是一个限制,但实际上,这反而是 Redis 性能高的关键之一。

没有严重的上下文切换锁的性能开销,以及实现的复杂度,Redis单线程已经够用

i/o多路复用

这里虽然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 在处理缓存过期数据时,采用了 定期删除惰性删除 两种策略

惰性删除(Lazy Deletion)

惰性删除是 Redis 在访问过期数据时进行的处理方式。具体来说,只有当客户端访问一个已经过期的键时,Redis 才会删除这个键。

过程
客户端尝试访问一个缓存键,如果这个键已经过期,Redis 会发现该键已过期并将其删除。
删除过程是在访问时动态进行的,因此不会在 Redis 的整个内存空间中提前清理过期的数据。
这种方式的优点是删除操作是按需进行的,避免了 Redis 在空闲时清理过期数据的额外负担。
缺点
性能问题:如果大量的过期键在同一时间段被访问,可能会带来一定的性能影响。因为 Redis 会逐一检测每个访问的键的过期时间并执行删除操作。
内存浪费:如果过期的数据没有被访问到,它们会一直占用内存,直到 Redis 自动清理这些键。

定期删除(Periodic Deletion)

为了避免惰性删除带来的性能问题,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  # 监控过期事件

redis在springboot中的配置

对于咱们项目而已,一般来说引入依赖,编写个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);
    }

}

详细说明

1. 类定义与基础配置

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {

作用:声明为配置类,确保在Spring Boot默认的RedisAutoConfiguration之后加载,覆盖或增强默认配置。

2. 日期格式常量

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中存储的日期数据可读且格式一致。

3.注入RedisProperties

@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

4. 连接工厂配置(核心)

4.1单节点模式
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:支持认证。

4.2集群模式
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错误处理)。

4.3 连接池配置
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:获取连接的最大等待时间(避免线程长时间阻塞)。

4.4 集群拓扑刷新
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
    .enableAllAdaptiveRefreshTriggers()  // 自动触发刷新(如节点不可用、MOVED重定向)
    .enableAdaptiveRefreshTrigger()      // 启用自适应刷新
    .enablePeriodicRefresh(Duration.ofSeconds(5)) // 定时刷新集群拓扑
    .build();

ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
    .topologyRefreshOptions(clusterTopologyRefreshOptions).build();

作用:确保客户端能感知集群节点变化(如节点故障转移、扩容)。

触发条件:

自适应刷新:在收到MOVED、ASK错误或节点断开时自动刷新。

定时刷新:每5秒强制刷新一次。

4.5 Lettuce客户端配置
LettuceClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
    .poolConfig(genericObjectPoolConfig)
    .readFrom(ReadFrom.REPLICA_PREFERRED)  // 优先从副本读取
    .clientOptions(clusterClientOptions)
    .build();

关键配置:

readFrom:定义读取策略,REPLICA_PREFERRED表示优先读副本,提升读性能(写仍由主节点处理)。

4.6 连接工厂行为
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
lettuceConnectionFactory.setShareNativeConnection(false); // 每个操作使用独立连接
lettuceConnectionFactory.resetConnection(); // 初始化时重置连接

setShareNativeConnection(false):禁用连接共享,确保线程安全(默认true可能引发并发问题)。

resetConnection():初始化时强制刷新连接,避免陈旧连接。

5.RedisTemplate序列化配置

5.1 配置ObjectMapper
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的问题)。

5.2 日期序列化模块
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)存储,而非默认的数组或数字格式。、

5.3 RedisTemplate序列化设置
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支持复杂对象(嵌套结构、泛型集合)。

6. 缓存管理器(CacheManager)

6.1 默认缓存配置
RedisCacheConfiguration cacheConfiguration = getRedisCacheConfigurationWithTtl(60 * 60 * 24 * 30); // 默认30天

作用:全局缓存默认过期时间(30天),适用于未单独配置的缓存

6.2 自定义缓存配置
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天),实现细粒度控制。

6.3 缓存配置细节
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。

性能:连接池优化、读写分离提升吞吐量。

你可能感兴趣的:(Redis,redis,数据库,缓存)