文章目录
5,事务
Redis 事务虽然具备一次性、顺序性、排他性(即事务中的命令会按顺序执行,且执行期间不会被其他命令插入)。Redis的单条命令是保证原子性的,但是redis事务不能保证原子性。
Redis 事务的本质是一组命令的集合,其执行过程可概括为三个阶段:
- 开启事务(
MULTI
):标记事务的开始,后续命令会被加入队列而非立即执行。 - 命令入队:所有输入的命令(如
SET
、HSET
等)会按顺序存入事务队列,等待执行。 - 执行事务(
EXEC
):一次性执行队列中的所有命令,执行期间不会被其他客户端的命令干扰。
5.1Redis 事务不保证原子性的原因
原子性的核心是 “要么全部成功,要么全部失败”,但 Redis 事务不满足这一点:
- 若命令语法错误(如命令不存在):在
EXEC
执行前,Redis 会检测到错误并拒绝执行事务,此时所有命令都不执行(类似 “全部失败”)。 - 若命令逻辑错误(如对字符串执行
INCR
):EXEC
会正常执行其他命令,错误命令仅自身失败,不会影响其他命令(即 “部分成功,部分失败”)。
例如:
MULTI
SET key1 "123" // 成功
INCR key1 // 逻辑错误(字符串无法自增)
SET key2 "456" // 成功
EXEC
执行后,key1
和key2
都会被创建,仅INCR key1
失败,事务并未回滚,因此不满足原子性。
5.2事务操作过程
- 开启事务(
multi
) - 命令入队
- 执行事务(
exec
)
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2 # ..
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事务执行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
2) "k2"
3) "k1"
取消事务(discurd
)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI # 当前未开启事务
127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行
(nil)
事务错误
代码语法错误(编译时异常)所有的命令都不执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> error k1 # 这是一条语法错误命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors. # 执行报错
127.0.0.1:6379> get k1
(nil) # 其他命令并没有被执行
代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** >>> 所以不保证事务原子性
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> INCR k1 # 这条命令逻辑错误(对字符串进行增量)
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 运行时报错
4) "v2" # 其他命令正常执行
# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
5.3监控
悲观锁(Pessimistic Locking)
核心思想:总是假设最坏的情况,认为数据随时可能被其他线程修改,因此在访问数据前先加锁,确保只有自己能操作数据。
乐观锁(Optimistic Locking)
核心思想:假设数据一般不会被其他线程修改,因此不上锁,仅在更新数据时检查是否有人在此期间修改过(使用版本号进行检查)。
而Redis使用watch key
监控指定数据,相当于乐观锁加锁。监控某一key后,如果key在一个线程/客户端A执行事务时,有另外一个客户端/线程B对key进行修改,则执行A的事务失败(全部失败)。
并且Redis 的 WATCH
是一次性的,且第二次事务没有重新执行 WATCH money
。
案例一:成功案例
客户端B在客户端A的事务期间执行(multi之后和exec之前)。
-------------客户端A--------------
127.0.0.1:6379[2]> set money 100
OK
127.0.0.1:6379[2]> set use 0
OK
127.0.0.1:6379[2]> watch money
OK
127.0.0.1:6379[2]> multi
OK
127.0.0.1:6379[2]> decrby money 20
QUEUED
127.0.0.1:6379[2]> incrby money 20
QUEUED
127.0.0.1:6379[2]> exec
(nil)
127.0.0.1:6379[2]> get money
"600"
127.0.0.1:6379[2]> get use
"0"
-------------客户端B--------------
127.0.0.1:6379[2]> incrby money 500
(integer) 600
案例二:失败案例
客户端B在客户端A的第二次事务期间执行(multi之后和exec之前)。这就是“Redis 的 WATCH
是一次性的,且第二次事务没有重新执行 WATCH money
。”
-------------客户端A--------------
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set use 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby use 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> get use
"20"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby use 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 560
2) (integer) 40
-------------客户端B--------------
127.0.0.1:6379> incrby money 500
(integer) 580
6,SpringBoot整合Redis
6.1Redis客户端
什么是客户端?在 Spring Boot 应用中,Redis 客户端是指用于连接和操作 Redis 数据库的工具库。spring-boot-starter-data-redis
作为官方提供的启动器,默认集成了Lettuce作为 Redis 客户端。
6.1.1Jedis简单使用
使用Java来操作Redis,Jedis是Redis官方推荐使用的Java连接redis的客户端。
导入依赖
<!--导入jredis的包--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency> <!--fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.70</version> </dependency>
这里直接在springboot的测试类里测试了
@SpringBootTest public class One { @Test public void test() { Jedis jedis = new Jedis("127.0.0.1", 6379); String pong = jedis.ping(); System.out.println(pong); } @Test public void test2() { Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.select(2); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello", "world"); jsonObject.put("name", "Selena"); // 开启事务 Transaction multi = jedis.multi(); String result = jsonObject.toJSONString(); try { multi.set("user1", result); multi.set("user2", result); // 执行事务 multi.exec(); }catch (Exception e){ // 放弃事务 multi.discard(); } finally { // 关闭连接 System.out.println(jedis.get("user1")); System.out.println(jedis.get("user2")); jedis.close(); } } }
6.1.2Lettuce&Jedis
在 Spring Boot 应用中,spring-boot-starter-data-redis
默认使用 Lettuce 作为 Redis 客户端,因为其依赖中包含lettuce-core
包。若要切换为 Jedis,需排除 Lettuce 并引入 Jedis 依赖。
特性 | Lettuce | Jedis |
---|---|---|
连接模型 | 基于 Netty 的响应式、非阻塞 I/O,支持异步和多路复用 | 基于传统 BIO(阻塞 I/O),每次操作需新建连接 |
线程安全性 | 线程安全,可多线程共享连接实例 | 线程不安全,需通过连接池管理连接 |
资源消耗 | 使用 Netty 事件循环,资源消耗少,适合长连接 | 频繁创建 / 销毁连接,资源消耗大,依赖连接池降开销 |
功能特性 | 支持响应式编程(Reactive Redis API)、集群和哨兵模式 | 提供传统同步 API,适合简单业务场景 |
适用场景 | 高并发、异步操作、响应式应用(如 Spring WebFlux) | 简单同步操作、传统 Servlet 应用 |
6.2配置相关
RedisProperties
是 Spring Boot 中用于配置 Redis 连接信息的核心配置类,位于 org.springframework.boot.autoconfigure.data.redis
包下。它通过 @ConfigurationProperties
注解绑定 spring.redis
前缀的配置项,让你可以在 application.properties
或 application.yml
中轻松配置 Redis 连接参数。
此外,RedisAutoConfiguration这个类,顾名思义就是Redis的自动化配置。在这个类中,会引入LettuceConnectionConfiguration 和 JedisConnectionConfiguration 两个配置类,分别对应lettuce和jedis两个客户端。而这个两个类上都是用了ConditionalOnClass注解来进行判断是否加载。
而由于我们的项目自动引入了lettuce-core,而没有引入jedis相关依赖,所以LettuceConnectionConfiguration这个类的判断成立会被加载,而Jedis的判断不成立,所以不会加载。进而lettuce的配置生效,所以我们在使用的使用, 默认就是lettuce的客户端。
配置文件(基础配置):
server: # 服务器配置
port: 8080
servlet:
context-path: /
spring:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
但是有的时候我们想要给我们的redis客户端配置上连接池。就像我们连接mysql的时候,也会配置连接池一样,目的就是增加对于数据连接的管理,提升访问的效率,也保证了对资源的合理利用。
如果使用的是jedis,就把lettuce换成jedis(同时要注意依赖也是要换的)。
spring:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
但是仅仅这在配置文件中加入,其实连接池是不会生效的。还少了最关键的一步,就是要导入一个依赖,不导入的话,这么配置也没有用。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
之后,连接池才会生效。我们可以做一个对比。 在导包前后,观察RedisTemplate对象的值就可以看出来。
添加依赖前:
添加依赖后:
6.3使用
6.3.1使用RedisTemplate
简单案例:
@SpringBootTest
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate1() {
redisTemplate.opsForValue().set("name", "saber");
}
}
在 Spring 的RedisTemplate
中,opsForValue()
是用于获取操作 Redis 字符串(String)类型数据的操作接口,通过它能执行一系列针对字符串的操作。除了opsForValue()
外,RedisTemplate
还提供了opsForHash()
(操作哈希类型数据)、opsForList()
(操作列表类型数据 )、opsForSet()
(操作集合类型数据 )和opsForZSet()
(操作有序集合类型数据)等方法,方便开发者针对不同的 Redis 数据类型进行操作。
拿opsForValue()举例,其常用方法
set(String key, Object value)
:将键值对存储到 Redis 中。如果对应的 key 已经存在,新值会覆盖旧值。get(String key)
:根据给定的 key,从 Redis 中获取对应的字符串值,如果 key 不存在,则返回null
。set(String key, Object value, long timeout, TimeUnit unit)
:存储键值对,并设置该键值对的过期时间。increment(String key, long delta)
:对存储在 Redis 中的数字类型的 key 进行自增操作,delta
表示自增的步长,返回自增后的值。若 key 不存在,会将其初始化为delta
的值。decrement(String key, long delta)
:对存储在 Redis 中的数字类型的 key 进行自减操作,delta
表示自减的步长,返回自减后的值。
6.3.2Redis工具类
7,持久化RDB
7.1RDB持久化原理
RDB 是 Redis 默认的持久化方式,其核心机制是:
- 定期执行:Redis 在指定时间间隔内(如 5 分钟),将内存中的数据快照保存为二进制文件(默认名为
dump.rdb
可以通过配置文件修改)。 - fork 子进程:执行快照时,Redis 主进程会
fork
一个子进程,由子进程负责将数据写入磁盘,主进程继续处理客户端请求。 - 全量复制:RDB 文件包含某一时刻 Redis 的全部数据,恢复时直接加载整个文件。
fork
是操作系统提供的一个系统调用,用于创建一个与父进程几乎完全相同的子进程。RDB 持久化需要将内存中的全量数据写入磁盘,如果由主线程直接执行,会导致长时间阻塞(尤其数据量大时),影响 Redis 的响应性能。
在进行 RDB
的时候,redis
的主线程是不会做 io
操作的,主线程会 fork
一个子线程来完成该操作;
- Redis 调用forks。同时拥有父进程和子进程。
- 子进程将数据集写入到一个临时 RDB 文件中。
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)
1,写时复制(Copy-On-Write):初始时,父子进程共享同一块物理内存;当父进程或子进程修改数据时,才会复制被修改的内存页,避免了全量复制的性能消耗。
2,什么是 “临时的 RDB 文件”?
在 RDB 持久化过程中,子进程并非直接写入最终的 RDB 文件(如默认的dump.rdb
),而是先将内存数据写入一个临时文件(例如命名为temp-xxx.rdb
)。
临时文件的作用是:
- 保证数据完整性:如果子进程在写入过程中意外崩溃(如磁盘满、进程被 kill),临时文件会被丢弃,不会影响原有的 RDB 文件(原文件仍可用于恢复数据)。
- 避免部分写入:如果直接写入目标文件,中途失败会导致目标文件损坏,而临时文件可确保只有当全量数据写入完成后,才会成为有效的 RDB 文件。
3,替换过程是怎样的?
当子进程成功将全量数据写入临时文件后,Redis 会执行原子性的文件替换操作:
- 子进程完成写入后,通知主进程 “临时文件已就绪”。
- 主进程通过操作系统的rename(或类似)系统调用,将临时文件重命名为目标 RDB 文件名(如dump.rdb)。
- 例如:
temp-xxx.rdb
→dump.rdb
。
- 例如:
- 替换完成后,删除旧的
dump.rdb
文件(如果存在)。这意味着旧的数据会被新的数据覆盖。
7.2触发机制
- save的规则(配置文件中设值,在快照模块,如下)满足的情况下,会自动触发rdb原则
- 执行flushall命令,也会触发我们的rdb原则
- 退出redis,也会自动产生rdb文件
配置文件相关内容
# 当至少1个key被修改且时间超过900秒(15分钟)时,执行一次快照
save 900 1
# 当至少10个key被修改且时间超过300秒(5分钟)时,执行一次快照
save 300 10
# 当至少10000个key被修改且时间超过60秒时,执行一次快照
save 60 10000
# RDB文件保存路径
dir ./
# RDB文件名
dbfilename dump.rdb
# 启用压缩(可能影响性能)
rdbcompression yes
save命令
使用 save
命令,会立刻对当前内存中的数据进行持久化 ,但是会阻塞,也就是不接受其他操作了;
由于 save
命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save
命令执行速度会非常慢,阻塞所有客户端的请求。
flushall命令
flushall
命令也会触发持久化 ;
bgsave命令
bgsave
是异步进行,进行持久化的时候,redis
还可以将继续响应客户端请求 ;配置文件中的save规则本质上也是bgsave,异步执行数据持久化。
bgsave和save对比
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
阻塞? | 是 | 是(阻塞发生在fock(),通常非常快) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fock子进程,消耗内存 |
7.3优缺点
优点:
- 适合大规模的数据恢复,相比AOF恢复速度快。
- 对数据的完整性要求不高
缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了,破快数据完整性。
- fork进程的时候,会占用一定的内容空间。
8,持久化AOF
8.1AOF持久化原理
Append Only File
将我们所有的命令都记录下来,恢复的时候就把这个文件全部再执行一遍。
AOF 通过追加写命令到日志文件的方式实现持久化:
- 命令实时记录:Redis 执行写命令后,会将命令追加到 AOF 缓冲区(内存)。
- 缓冲区同步到磁盘:根据配置的同步策略(
fsync
),将缓冲区中的命令写入磁盘 AOF 文件。 - 文件重写(AOF Rewrite):定期通过
BGREWRITEAOF
命令对 AOF 文件进行瘦身,去除冗余命令。
8.2相关配置
如果要使用AOF,需要修改配置文件:
appendonly no yes
则表示启用AOF
默认是不开启的,我们需要手动配置,然后重启redis,就可以生效了!
相关配置:
appendonly yes # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用
appendfilename "appendonly.aof"
# appendfsync always # 每次修改都会sync 消耗性能
appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据
# appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快
# AOF文件重写触发条件
auto-aof-rewrite-percentage 100 # 当前AOF文件大小超过上次重写后大小的100%时触发
auto-aof-rewrite-min-size 64mb # AOF文件最小达到64MB时才考虑重写
8.3aof文件重写
重写的作用:通过生成一个只包含当前数据库最新状态的 AOF 文件,替代旧文件,大幅减小文件体积。随着 Redis 运行,AOF 文件会不断追加写命令,导致文件体积膨胀,例如:对同一 key 多次修改(如SET k 1 → SET k 2 → SET k 3
),旧命令变得冗余。就是舍去大量的中间状态,并记录当前数据库最新的状态。
触发方式
自动触发:当满足以下两个条件时,Redis 自动触发重写:
auto-aof-rewrite-percentage 100 # 当前AOF文件大小超过上次重写后大小的100% auto-aof-rewrite-min-size 64mb # AOF文件最小达到64MB才考虑重写
手动触发:执行命令:
redis-cli BGREWRITEAOF
重写执行的流程。
- 主线程 fork 子进程
- 主线程通过
fork()
创建子进程,子进程获得当前内存数据的副本。 - 关键特性:写时复制(Copy-On-Write),父子进程共享内存,仅在修改数据时复制内存页,避免全量内存拷贝。
- 主线程通过
- 子进程生成新 AOF 文件
- 子进程遍历内存中的所有键值对,生成对应的写命令序列(如
SET k v
、HSET hash f v
)。 - 新 AOF 文件仅包含重建当前数据库所需的最小命令集,不包含任何冗余命令。
- 子进程遍历内存中的所有键值对,生成对应的写命令序列(如
- 处理增量写命令
- 在子进程重写期间,主线程继续处理客户端请求,并将新的写命令同时追加到:
- 旧 AOF 文件:确保当前持久化过程不受影响。
- AOF 重写缓冲区:保存重写期间的增量命令。
- 在子进程重写期间,主线程继续处理客户端请求,并将新的写命令同时追加到:
- 替换旧文件
- 子进程完成重写后,向主线程发送信号。
- 主线程将 AOF 重写缓冲区中的增量命令追加到新 AOF 文件中。
- 主线程使用原子性的
rename()
系统调用,将新 AOF 文件替换旧文件。
8.3优缺点
优点
- 每一次修改都会同步,文件的完整性会更加好
- 每秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高
缺点
- 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
- Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化