一:Redis Java 集成到 Spring Boot
1.1 使用 Spring Boot 连接 Redis 单机
- 在创建项目时,勾选 NoSQL 分类下的 Spring Data Redis,同时勾选 Web 分类下的 Spring Web。这样既能方便集成 Redis,又能通过 Web 接口进行后续测试,简化开发流程。
- 配置 redis 服务地址
spring:
redis:
host: 127.0.0.1
port: 8888
- 创建 Controller
@RestController
public class MyController {
@Autowired
private StringRedisTemplate redisTemplate;
// 可以在此添加方法来使用 redisTemplate
}
- 使用 String
@GetMapping("/testString")
@ResponseBody
public String testString() {
// 使用 StringRedisTemplate 的 opsForValue 操作设置键值对
redisTemplate.opsForValue().set("key", "value");
// 获取键 "key" 的值
String value = redisTemplate.opsForValue().get("key");
System.out.println(value); // 打印获取到的值到控制台
// 删除键 "key"
redisTemplate.delete("key");
// 返回操作结果
return "OK";
}
- 使用 List
@GetMapping("/testList")
@ResponseBody
public String testList() {
// 向列表 "key" 中左侧插入元素 "a"
redisTemplate.opsForList().leftPush("key", "a");
// 向列表 "key" 中左侧批量插入元素 "b", "c", "d"
redisTemplate.opsForList().leftPushAll("key", "b", "c", "d");
// 获取列表 "key" 中从索引 1 到索引 2 的元素
List<String> values = redisTemplate.opsForList().range("key", 1, 2);
System.out.println(values); // 打印获取到的元素列表到控制台
// 删除键 "key"
redisTemplate.delete("key");
// 返回操作结果
return "OK";
}
- 使用 Hash
@GetMapping("/testHashmap")
@ResponseBody
public String testHashmap() {
// 在哈希表 "key" 中存储键值对 "name" -> "zhangsan"
redisTemplate.opsForHash().put("key", "name", "zhangsan");
// 从哈希表 "key" 中获取键 "name" 的值
String value = (String) redisTemplate.opsForHash().get("key", "name");
System.out.println(value); // 打印获取到的值到控制台
// 删除哈希表 "key" 中的键 "name"
redisTemplate.opsForHash().delete("key", "name");
// 检查哈希表 "key" 中是否存在键 "name"
boolean ok = redisTemplate.opsForHash().hasKey("key", "name");
System.out.println(ok); // 打印检查结果到控制台
// 删除整个哈希表 "key"
redisTemplate.delete("key");
// 返回操作结果
return "OK";
}
- 使用 Set
@GetMapping("/testSet")
@ResponseBody
public String testSet() {
// 向集合 "key" 中添加元素 "aaa", "bbb", "ccc"
redisTemplate.opsForSet().add("key", "aaa", "bbb", "ccc");
// 检查集合 "key" 是否包含元素 "aaa"
boolean ok = redisTemplate.opsForSet().isMember("key", "aaa");
System.out.println(ok); // 打印检查结果到控制台
// 从集合 "key" 中移除元素 "aaa"
redisTemplate.opsForSet().remove("key", "aaa");
// 获取集合 "key" 的元素数量
long n = redisTemplate.opsForSet().size("key");
System.out.println(n); // 打印集合大小到控制台
// 删除集合 "key"
redisTemplate.delete("key");
// 返回操作结果
return "OK";
}
- 使用 Zset
@GetMapping("/testZSet")
@ResponseBody
public String testZSet() {
// 向有序集合 "key" 中添加元素 "吕布",分值为 100
redisTemplate.opsForZSet().add("key", "吕布", 100);
// 向有序集合 "key" 中添加元素 "赵云",分值为 98
redisTemplate.opsForZSet().add("key", "赵云", 98);
// 向有序集合 "key" 中添加元素 "典韦"(注:原文中“典⻙”为“典韦”),分值为 95
redisTemplate.opsForZSet().add("key", "典韦", 95);
// 获取有序集合 "key" 中索引范围 [0, 2] 的元素
Set<String> values = redisTemplate.opsForZSet().range("key", 0, 2);
System.out.println(values); // 打印获取到的元素到控制台
// 统计有序集合 "key" 中分值范围 [95, 100] 的元素数量
long n = redisTemplate.opsForZSet().count("key", 95, 100);
System.out.println(n); // 打印统计结果到控制台
// 删除整个有序集合 "key"
redisTemplate.delete("key");
// 返回操作结果
return "OK";
}
1.2 使用 Spring Boot 连接 Redis 集群
- 配置 redis 集群地址
spring:
redis:
cluster:
nodes:
- 172.30.0.101:6379
- 172.30.0.102:6379
- 172.30.0.103:6379
- 172.30.0.104:6379
- 172.30.0.105:6379
- 172.30.0.106:6379
- 172.30.0.107:6379
- 172.30.0.108:6379
- 172.30.0.109:6379
lettuce:
cluster:
refresh:
adaptive: true # 自适应刷新,当集群中有节点宕机/加⼊新节点之后, 我们的代码能够⾃动感知到集群的变化.
period: 2000 # 刷新周期,单位为毫秒
配置完成后,无需对其他代码进行任何调整即可正常运行。但需要注意,由于上述 IP 是 Docker 容器的内部 IP,无法直接在 Windows 主机上访问。因此,需要将程序打包成 JAR 文件后部署到 Linux 环境中,通过命令 java -jar [jar包名] 来执行。总结一下:Spring Boot 2 系列内置的 Redis 客户端是 Lettuce,与 Jedis 在使用上存在一定差异。Jedis 的方法基本与 Redis 命令一致,而集成到 Spring Boot 后,Lettuce 的接口与原始 Redis 命令略有不同,但只要熟悉 Redis 的基本操作,理解和使用这些方法并不困难。
二: 持久化
2.1 RDB
RDB 持久化是将当前进程的数据生成快照并保存到硬盘的过程,可通过手动触发或自动触发来实现。
2.1.1 触发机制
手动触发持久化可以通过执行 save 或 bgsave 命令实现,Redis 内部的所有涉及 RDB 的操作都采用类似 bgsave 的⽅式。
命令 | 描述 | 特点 |
---|---|---|
save | 阻塞当前 Redis 服务器,直到 RDB 过程完成为止。对于内存较大的实例可能导致长时间阻塞,因此基本不使用。 | 阻塞时间较长,不适合高并发场景。 |
bgsave | Redis 进程通过 fork 操作创建子进程,子进程负责完成 RDB 持久化过程,结束后自动退出。阻塞仅发生在 fork 阶段,时间较短。 | 性能更优,阻塞时间短,适合生产环境,广泛使用。 |
除了手动触发外,Redis 还支持自动触发 RDB 持久化机制,这种触发方式在实际应用中更具价值。
触发机制 | 描述 |
---|---|
使用 save 配置 | 配置规则为 “save m n”,表示在 m 秒内数据集发生 n 次修改时,自动触发 RDB 持久化。 |
从节点进行全量复制操作 | 当从节点进行全量复制操作时,主节点会自动触发 RDB 持久化,并将生成的 RDB 文件内容发送给从节点。 |
执行 shutdown 命令关闭 Redis | 当执行 shutdown 命令关闭 Redis 时,会自动触发 RDB 持久化以保存数据。 |
2.1.2 流程说明
步骤 | 描述 |
---|---|
步骤 1 | 执行 bgsave 命令时,父进程会判断是否存在其他正在执行的子进程(如 RDB 或 AOF 进程),如果存在,bgsave 命令直接返回,不执行操作。 |
步骤 2 | 父进程通过 fork 操作创建子进程,fork 过程中父进程会阻塞。可以通过 info stats 命令查看 latest_fork_usec 选项,获取最近一次 fork 操作的耗时(单位为微秒)。 |
步骤 3 | fork 完成后,bgsave 命令返回 Background saving started 信息,父进程不再阻塞,可以继续响应其他命令。 |
步骤 4 | 子进程生成 RDB 文件,通过父进程内存创建临时快照文件,完成后原子替换原有 RDB 文件。可以通过执行 lastsave 命令获取最后一次生成 RDB 的时间(对应 info 中的 rdb_last_save_time)。 |
步骤 5 | 子进程完成后向父进程发送信号,通知完成操作,父进程随即更新相关统计信息。 |
2.1.3 RDB 文件的处理
功能 | 描述 |
---|---|
保存 | RDB 文件保存在 dir 配置指定的目录下(默认 /var/lib/redis/),文件名由 dbfilename 配置(默认 dump.rdb)指定。可通过 config set dir {newDir} 和 config set dbfilename {newFilename} 动态修改,下次运行时 RDB 文件会保存到新目录。 |
压缩 | Redis 默认使用 LZF 算法对生成的 RDB 文件进行压缩处理,压缩后的文件远小于内存大小。默认开启,可通过 config set rdbcompression {yes |
校验 | 如果 Redis 启动时检测到损坏的 RDB 文件会拒绝启动,可使用 redis-check-dump 工具检测 RDB 文件并生成错误报告。 |
2.1.4 RDB 的优缺点
特点 | 描述 |
---|---|
定义 | RDB 是一个紧凑压缩的二进制文件,表示 Redis 在某个时间点的数据快照,非常适用于备份和全量复制等场景。比如每隔 6 小时执行 bgsave,并将 RDB 文件复制到远程机器或文件系统(如 hdfs)进行灾备。 |
恢复速度 | Redis 加载 RDB 文件恢复数据的速度远快于使用 AOF 的方式。 |
实时性限制 | RDB 无法实现实时或秒级持久化,因为每次执行 bgsave 都需要 fork 创建子进程,这是一种重量级操作,频繁执行成本较高。 |
兼容性风险 | RDB 文件使用特定的二进制格式保存,随着 Redis 版本的演进存在多个 RDB 版本,因此可能存在兼容性风险。 |
2.2 AOF
AOF(Append Only File)持久化以独立日志的方式记录每次写操作的命令,并在重启时通过重新执行 AOF 文件中的命令来恢复数据。AOF 的主要优势在于解决了数据持久化的实时性问题,目前已成为 Redis 持久化的主流方式。深入理解和掌握 AOF 持久化机制,有助于在实际应用中更好地兼顾数据的安全性和系统性能。
2.2.1 使用 AOF
要开启 AOF 功能,需要设置配置项 appendonly yes,默认情况下 AOF 是关闭的。AOF 文件名可以通过配置项 appendfilename 设置(默认值为 appendonly.aof),保存目录与 RDB 持久化方式一致,由配置项 dir 指定。AOF 的工作流程包括四个主要步骤:命令写入(append)、文件同步(sync)、文件重写(rewrite)以及重启加载(load)。
步骤 | 描述 |
---|---|
步骤 1 | 所有写入命令会追加到 AOF 缓冲区(aof_buf)中。 |
步骤 2 | AOF 缓冲区根据配置的同步策略将数据同步到硬盘。 |
步骤 3 | 随着 AOF 文件不断增大,需定期对其进行重写以达到压缩文件的目的,减少存储占用。 |
步骤 4 | 当 Redis 服务器启动时,可以加载 AOF 文件来恢复数据。 |
2.2.2 命令写入
AOF 命令写入的内容使用文本协议格式。例如,命令 set hello world 在 AOF 缓冲区中会被追加为以下内容:*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n。这遵循 Redis 的协议格式,Redis 选择文本协议的可能原因包括:兼容性好、实现简单且具备可读性。在 AOF 过程中,缓冲区 aof_buf 的作用是提升性能。由于 Redis 使用单线程响应命令,如果每次写 AOF 文件都直接同步到硬盘,IO 操作将极大降低性能。如果先写入缓冲区就可以减少 IO 次数,并且 Redis 提供多种缓冲区同步策略,用户可以根据需求在性能与数据安全性之间做出合理平衡。
2.2.3 文件同步
Redis 提供了多种 AOF 缓冲区同步文件的策略,可通过参数 appendfsync 进行配置和控制。
配置值 | 描述 | 建议 |
---|---|---|
always | 每次命令写入 aof_buf 后立即调用 fsync 同步到硬盘,待同步完成后再返回。性能较差,在普通 SATA 硬盘上仅支持几百 TPS 写入,适用于极其重要的数据场景。 | 仅在需要保证数据绝对安全且能容忍性能下降的情况下使用,例如金融级别的关键场景。 |
everysec | 命令写入 aof_buf 后仅执行 write 操作,同步操作由后台线程每秒调用一次 fsync 完成。该配置是默认值,兼顾数据安全性和性能,理论上最多丢失 1 秒的数据。 | 默认推荐配置,适用于绝大多数场景,兼顾性能和数据安全性,适合生产环境。 |
no | 命令写入 aof_buf 后仅执行 write 操作,fsync 频率由操作系统自行控制,性能较高但数据丢失风险增加。 | 适用于对数据安全性要求较低的场景,例如缓存类数据或短期内可快速恢复的非关键数据。 |
系统调用 write 和 fsync 说明:
系统调用 | 描述 |
---|---|
write | 触发延迟写机制,将数据写入系统缓冲区后立即返回。实际同步到硬盘依赖系统调度机制(如缓冲区空间写满或达到特定时间周期触发)。在同步前若系统宕机,缓冲区内的数据将丢失。 |
fsync | 针对单个文件进行强制硬盘同步操作,会阻塞当前进程直到数据完全写入硬盘,确保数据安全性,但性能开销较大。 |
2.2.4 重写机制
随着命令不断写入 AOF 文件,其体积会逐渐增大。为了解决这一问题,Redis 引入了 AOF 重写机制,通过压缩文件体积来优化存储。AOF 文件重写的过程是将 Redis 内存中的数据重新生成写命令,并同步到新的 AOF 文件中。重写后的 AOF 文件体积减小的原因主要包括以下几点:
原因 | 描述 |
---|---|
超时数据不再写入 | 进程内已经超时的数据在重写时不会写入新的 AOF 文件,从而减少文件体积。 |
无效命令被移除 | 旧的 AOF 文件中无效的命令(如 del、hdel、srem 等)在重写后会被删除,仅保留数据的最终版本,避免冗余操作。 |
合并多条写操作 | 多条写操作可以合并为一条。例如,将 lpush list a、lpush list b、lpush list c 合并为 lpush list a b c,从而减少命令数量。 |
较小的 AOF 文件不仅能减少硬盘空间的占用,还能显著提升 Redis 启动时的数据恢复速度。AOF 重写过程可以通过手动触发或自动触发来完成:
触发方式 | 描述 |
---|---|
手动触发 | 调用 bgrewriteaof 命令手动执行 AOF 重写。 |
自动触发 | auto-aof-rewrite-min-size:表示触发重写的最小 AOF 文件大小,默认值为 64MB。
auto-aof-rewrite-percentage:表示当前 AOF 文件大小相较于上次重写时的增长比例。 |
步骤 | 描述 |
---|---|
执行请求 | 如果当前进程正在执行 AOF 重写,请求将不执行;如果当前进程正在执行 bgsave 操作,AOF 重写命令会延迟到 bgsave 完成后再执行。 |
创建子进程 | 父进程执行 fork 操作以创建子进程。 |
重写过程 | 主进程 fork 后继续响应其他命令,修改操作写入 AOF 缓冲区并根据 appendfsync 策略同步到硬盘,确保旧 AOF 文件机制正常。
子进程仅保留 fork 前的内存信息,父进程需将 fork 后的修改操作写入 AOF 重写缓冲区中。 |
合并命令 | 子进程根据内存快照,将命令合并并写入新的 AOF 文件中。 |
完成重写 | 子进程完成重写后,发送信号通知父进程。
父进程将 AOF 重写缓冲区中的临时命令追加到新 AOF 文件中。 最终用新 AOF 文件替换旧 AOF 文件。 |
2.2.5 启动时数据恢复
当 Redis 启动时,会根据 RDB 文件和 AOF 文件的内容进行数据恢复,确保数据状态与上次持久化时保持一致,如图所示:
三: Redis 事务
Redis 的事务与 MySQL 的事务在概念上类似,都是将一系列操作绑定为一个整体,以便批量执行。然而,两者在实现和特性上存在显著区别,需要重点理解其中的差异。
特性 | Redis 的事务特点 | 与 MySQL 的对比 |
---|---|---|
弱化的原子性 | Redis 没有回滚机制,事务只能实现操作的批量执行,无法在某个操作失败时恢复到初始状态。 | MySQL 支持回滚,确保事务的完全原子性,即所有操作要么全部成功,要么全部回滚。 |
不保证一致性 | Redis 不涉及约束,也没有回滚机制,可能会出现中间的非法状态。 | MySQL 的一致性确保事务前后的状态均合理有效,避免出现不一致或非法的状态。 |
不需要隔离性 | Redis 不提供隔离级别,因为事务不会并发执行,所有请求由单线程依次处理。 | MySQL 提供多种隔离级别(如读提交、可重复读等)来管理并发事务间的影响。 |
不需要持久性 | Redis 的数据存储在内存中,事务的持久性取决于是否开启持久化,与事务本身无关。 | MySQL 的事务持久性通过日志和磁盘存储来保障,即使系统崩溃也能确保数据完整。 |
Redis 事务的本质是通过在服务器上维护一个“事务队列”,客户端在事务中执行的每个操作都会被发送到服务器并加入队列,而不会立即执行。只有当收到 EXEC 命令时,队列中的所有操作才会一次性执行。相比于 MySQL,Redis 的事务功能较为弱化,仅能保证事务中的操作是连续执行的,不会被其他客户端的命令插入,但无法提供更高级的事务特性如回滚和一致性保障,下面介绍一些常用的事务操作:
3.1 MULTI
MULTI 命令用于开启一个事务,执行成功后返回 OK。
127.0.0.1:6379> MULTI
OK
3.2 EXEC
EXEC 命令用于真正执行事务队列中的所有操作。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> set k2 2
QUEUED
127.0.0.1:6379> set k3 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) OK
每次添加操作时,都会提示 “QUEUED”,表示命令已成功加入客户端的事务队列。当执行 EXEC 时,客户端会将这些操作发送到服务器并开始执行,此时才能获取到相关 key 的实际值。
3.3 DISCARD
DISCARD 命令用于放弃当前事务,直接清空事务队列,使之前的所有操作都不会被执行。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> set k2 2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> get k2
(nil)
3.4 WATCH
WATCH 命令用于监控事务中的关键值,如果某个事务中修改的值被别的客户端修改了,此时会导致数据不一致的问题。
# 客户端 1 开始事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key 100
QUEUED
# 客户端 2 修改 key 的值
127.0.0.1:6379> set key 200
OK
# 客户端 1 提交事务
127.0.0.1:6379> EXEC
1) OK
此时 key 的值为 “100”。从命令的输入顺序看,客户端 1 先执行了 set key 100,客户端 2 后执行了 set key 200;但从实际执行时间看,客户端 2 的操作先生效,而客户端 1 在提交事务时覆盖了客户端 2 的修改。这种情况容易引发歧义,因此,虽然 Redis 不保证严格的隔离性,但需要提醒用户操作可能存在被其他客户端修改的风险。为了解决这一问题,Redis 提供了 WATCH 命令,用于在客户端上监控一组特定的 key,从而避免数据冲突。
功能 | 描述 |
---|---|
监控 key | WATCH 命令允许客户端监控一组特定的 key,在事务开启时锁定这些 key 的状态。 |
记录版本号 | 当监控的 key 被修改时,Redis 会记录该 key 的版本号(一个递增的整数,每次修改都会增加)。 |
事务提交校验 | 在提交事务时,Redis 会检查 key 的版本号是否发生变化。如果版本号超过事务开始时的记录,事务将失败。 |
解决冲突 | 如果事务失败,客户端可以选择重新尝试或采取其他逻辑,从而有效解决并发修改导致的数据冲突问题。 |
127.0.0.1:6379> watch k1 # 开始监控 k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 100 # 进行修改,记录 k1 的版本号为 0 (还未实际修改,版本号不变)
QUEUED
127.0.0.1:6379> set k2 1000 # 将操作入队列,但未提交事务
QUEUED
# 客户端 2 进行操作
127.0.0.1:6379> set k1 200 # 修改成功,服务器端 k1 的版本号从 0 增加到 1
OK
# 客户端 1 提交事务
127.0.0.1:6379> EXEC # 尝试执行事务,发现 k1 的版本号不一致 (客户端为 0,服务器为 1),事务失败
(nil)
# 查看当前值
127.0.0.1:6379> get k1
"200"
127.0.0.1:6379> get k2
(nil)
3.5 UNWATCH
NWATCH 用于取消对 key 的监控,是 WATCH 命令的逆操作。通过 NWATCH,可以在事务执行前取消对所有被监控 key 的观察,从而恢复 key 的正常状态。
# 客户端 1 开始操作
127.0.0.1:6379> watch k1 # 监控 key k1
OK
127.0.0.1:6379> get k1 # 查看 k1 当前值
(nil)
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379> set k1 100 # 事务中设置 k1 为 100
QUEUED
127.0.0.1:6379> set k2 200 # 事务中设置 k2 为 200
QUEUED
# 客户端决定取消监控
127.0.0.1:6379> nwatch # 取消监控所有 key
OK
# 此时可以正常提交事务,操作不再受监控的限制
127.0.0.1:6379> EXEC
1) OK # 成功设置 k1 为 100
2) OK # 成功设置 k2 为 200
# 查看结果
127.0.0.1:6379> get k1
"100"
127.0.0.1:6379> get k2
"200"