Redis学习其二(事务,SpringBoot整合,持久化RDB和AOF)

发布于:2025-07-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

5,事务

Redis 事务虽然具备一次性、顺序性、排他性(即事务中的命令会按顺序执行,且执行期间不会被其他命令插入)。Redis的单条命令是保证原子性的,但是redis事务不能保证原子性。

Redis 事务的本质是一组命令的集合,其执行过程可概括为三个阶段:

  1. 开启事务MULTI):标记事务的开始,后续命令会被加入队列而非立即执行。
  2. 命令入队:所有输入的命令(如SETHSET等)会按顺序存入事务队列,等待执行。
  3. 执行事务EXEC):一次性执行队列中的所有命令,执行期间不会被其他客户端的命令干扰。

5.1Redis 事务不保证原子性的原因

原子性的核心是 “要么全部成功,要么全部失败”,但 Redis 事务不满足这一点:

  • 若命令语法错误(如命令不存在):在EXEC执行前,Redis 会检测到错误并拒绝执行事务,此时所有命令都不执行(类似 “全部失败”)。
  • 若命令逻辑错误(如对字符串执行INCR):EXEC会正常执行其他命令,错误命令仅自身失败,不会影响其他命令(即 “部分成功,部分失败”)。

例如:

MULTI
SET key1 "123"   // 成功
INCR key1        // 逻辑错误(字符串无法自增)
SET key2 "456"   // 成功
EXEC

执行后,key1key2都会被创建,仅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的客户端。

  1. 导入依赖

    <!--导入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>
    
  2. 这里直接在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.propertiesapplication.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工具类

Redis工具类-CSDN博客

7,持久化RDB

7.1RDB持久化原理

RDB 是 Redis 默认的持久化方式,其核心机制是:

  • 定期执行:Redis 在指定时间间隔内(如 5 分钟),将内存中的数据快照保存为二进制文件(默认名为dump.rdb可以通过配置文件修改)。
  • fork 子进程:执行快照时,Redis 主进程会fork一个子进程,由子进程负责将数据写入磁盘,主进程继续处理客户端请求。
  • 全量复制:RDB 文件包含某一时刻 Redis 的全部数据,恢复时直接加载整个文件。

fork是操作系统提供的一个系统调用,用于创建一个与父进程几乎完全相同的子进程。RDB 持久化需要将内存中的全量数据写入磁盘,如果由主线程直接执行,会导致长时间阻塞(尤其数据量大时),影响 Redis 的响应性能。

在进行 RDB 的时候,redis 的主线程是不会做 io 操作的,主线程会 fork 一个子线程来完成该操作;

  1. Redis 调用forks。同时拥有父进程和子进程。
  2. 子进程将数据集写入到一个临时 RDB 文件中。
  3. 当子进程完成对新 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 会执行原子性的文件替换操作

  1. 子进程完成写入后,通知主进程 “临时文件已就绪”。
  2. 主进程通过操作系统的rename(或类似)系统调用,将临时文件重命名为目标 RDB 文件名(如dump.rdb)。
    • 例如:temp-xxx.rdbdump.rdb
  3. 替换完成后,删除旧的dump.rdb文件(如果存在)。这意味着旧的数据会被新的数据覆盖。

7.2触发机制

  1. save的规则(配置文件中设值,在快照模块,如下)满足的情况下,会自动触发rdb原则
  2. 执行flushall命令,也会触发我们的rdb原则
  3. 退出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优缺点

优点:

  1. 适合大规模的数据恢复,相比AOF恢复速度快。
  2. 对数据的完整性要求不高

缺点:

  1. 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了,破快数据完整性。
  2. fork进程的时候,会占用一定的内容空间。

8,持久化AOF

8.1AOF持久化原理

Append Only File

将我们所有的命令都记录下来,恢复的时候就把这个文件全部再执行一遍。

AOF 通过追加写命令到日志文件的方式实现持久化:

  1. 命令实时记录:Redis 执行写命令后,会将命令追加到 AOF 缓冲区(内存)。
  2. 缓冲区同步到磁盘:根据配置的同步策略(fsync),将缓冲区中的命令写入磁盘 AOF 文件。
  3. 文件重写(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),旧命令变得冗余。就是舍去大量的中间状态,并记录当前数据库最新的状态。

触发方式

  1. 自动触发:当满足以下两个条件时,Redis 自动触发重写:

    auto-aof-rewrite-percentage 100  # 当前AOF文件大小超过上次重写后大小的100%
    auto-aof-rewrite-min-size 64mb   # AOF文件最小达到64MB才考虑重写
    
  2. 手动触发:执行命令:

    redis-cli BGREWRITEAOF
    

重写执行的流程。

  1. 主线程 fork 子进程
    • 主线程通过fork()创建子进程,子进程获得当前内存数据的副本。
    • 关键特性:写时复制(Copy-On-Write),父子进程共享内存,仅在修改数据时复制内存页,避免全量内存拷贝。
  2. 子进程生成新 AOF 文件
    • 子进程遍历内存中的所有键值对,生成对应的写命令序列(如SET k vHSET hash f v)。
    • 新 AOF 文件仅包含重建当前数据库所需的最小命令集,不包含任何冗余命令。
  3. 处理增量写命令
    • 在子进程重写期间,主线程继续处理客户端请求,并将新的写命令同时追加到:
      • 旧 AOF 文件:确保当前持久化过程不受影响。
      • AOF 重写缓冲区:保存重写期间的增量命令。
  4. 替换旧文件
    • 子进程完成重写后,向主线程发送信号。
    • 主线程将 AOF 重写缓冲区中的增量命令追加到新 AOF 文件中。
    • 主线程使用原子性的rename()系统调用,将新 AOF 文件替换旧文件。

8.3优缺点

优点

  1. 每一次修改都会同步,文件的完整性会更加好
  2. 每秒同步一次,可能会丢失一秒的数据
  3. 从不同步,效率最高

缺点

  1. 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
  2. Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化

网站公告

今日签到

点亮在社区的每一天
去签到