一文读懂 Redis 持久化与事务

【Redis 核心原理】彻底弄懂持久化(RDB+AOF)与事务机制,一篇吃透面试高频考点

本文详细讲解 Redis 持久化机制 RDB、AOF、混合持久化,以及 Redis 事务原理与实现,内容偏硬核,适合后端开发、面试复习与深度理解 Redis

Redis持久化

Redis与Mysql这类关系型数据库相比,快的原因,就是他是在内存中的数据库。但是这种带电存储的介质也有自己的问题,无法持久化,所以Redis引入了RDB和AOF两种机制,在机器启动时,从持久化的数据中完成内存中数据库的初始化,是原来的内容不会因为服务器重启而丢失

RDB (Redis DataBase)

具有两种触发方式,一种是手动触发,另一种是自动触发

手动触发 。使用save或者bgsave命令。前者会阻塞Redis服务器,所以不推荐使用;后者会使用fork创建新的子进程,去完成持久化操作,不会阻塞服务器(在调用fork时会阻塞一下,但时间一般很短)

自动触发 。Redis可以在/etc/redis/redis.conf中 添加自动出发的条件,形式为save [seconds] [changes],意思为至少在给了seconds的秒数,并且至少修改了changes个键值对,才会进行RDB文件的生成;除此之外Redis还有两种情况会触发RDB文件的的生成,一种是Redis服务器正常关闭(比如使用service redis-server restart),另一种是主从复制时,主节点会生成自己的RDB文件拷贝给从节点

  • 可以在配置文件中,使用save ""关闭自动触发
  • Redis在启动时,如果有dump.rdb的话,会从中读取数据,但是如果其损坏的话,就无法启动(也看是损坏的那部分,如果是原来存储的数据的话可能还能启动,但是也会有问题),所以Redis提供了redis-check-rdb来提前检查dump.rdb是否有问题

💡 结合以上两种方式,不难发现因为RDB的成本比较高,所以不会太过频繁的进行,但这也意味着RDB文件和Redis中的键值对可能存在偏差

✅ RDB的优点
  1. RDB是一种紧凑的二进制文件,用来表示Redis在某一时刻的数据备份
  2. Redis的读取RDB文件的速度要远快于AOF
❌ RDB的缺点
  1. 不是实时生成,可能会导致备份数据不完全
  2. 不同版本的Redis中RDB的格式可能不同,前后兼容性不是特别好
AOF(AppendOnlyFile)

AOF通过文本的形式实时记录Redis操作,默认是关闭的,可以在/etc/redis/redis.conf中设置是否开启或者配置.aof文件的名称和路径等。在AOF文件中,使用特殊的符号作为分隔符去记录每一条命令

因为AOF文件能保证实时的备份,所以当既有RDB文件又有AOF文件时,会以AOF文件为准

那么AOF会实时的记录我们的命令到AOF文件,会导致服务器变慢吗?其实AOF文件并不是每次都直接往硬盘中写,而是会写到内存中的缓冲区,到了一定大小后再统一写入,因为性能更多受到磁盘写入次数的影响,而不是写入数据的大小影响,同时统一写入磁盘是顺序读写,比随机访问还是要快很多的

那么多久刷新一次呢?比如说服务器掉电关机,那内存中的缓冲区中的数据不就丢失了吗?是会丢失的,所以需要在性能和同步持久化的可靠性之间做取舍。写入磁盘越频繁,性能就越差,同时也越不容易丢数据(或者丢的时候丢的更少)。

Redis对于AOF文件的刷新策略有三种

  1. always,只要对缓冲区进行了写入,就立刻同步到磁盘

  2. everysec,每秒写入磁盘一次,有通知线程负责

  3. no,交给操作系统去决定什么时候写入磁盘

⚠️ Redis默认的刷新策略是everysec

AOF文件重写的触发机制
  1. 手动触发 ,使用bgrewriteaof即可在后台重写AOF文件,不阻塞服务器
  2. 自动触发 ,在配置文件中,有两个标准,一个是auto-rewrite-percentage,另一个是auto-rewrite-min-size,前者默认是100%,后者默认是64mb。只有当AOF文件的大小大于64MB并且比上次重写时的大小大一倍才会自动触发AOF文件的重写
AOF的重写机制

❓ 现在还有一个问题,当客户端非常多数据量非常大时,AOF文件可能会非常大。虽说磁盘空间并不值钱,但是前面我们说过,Redis服务器在启动时会读取AOF文件或者RDB文件,这个过程的用时可能会大大增加。

💡 所以Redis引入了AOF的重写机制,以开始重写时的内存状态为依据,为AOF文件进行"瘦身",例如以下几个例子

shell 复制代码
# 这两步就不需要在重写的AOF文件中体现
lpush list1 111
del list1
shell 复制代码
lpush list1 111
lpush list1 222 
lpush list1 333
# 上面的三步就可以直接修改为
lpush list1 111 222 333

AOF的重写也是有自己的实现过程的。当手动触发或者自动触发时,会fork出一个子进程,他会通过写时拷贝的方式获得一个AOF文件,它自己去完成对AOF文件的重写,这个过程也不需要去读取原来的AOF文件,保证最后结果和触发时Redis内存中数据的状态一样即可;在此期间主进程也继续执行自己对请求的处理,不过这个时候主进程对于AOF文件的写入会往两个方向进行,一个是原来的缓冲区,另一个是专门为子进程准备的缓冲区:因为父子进程各自有各自的进程地址空间,所以为了能让最后被重写的AOF文件也能有重写过程中新来到的数据,当AOF文件被子进程重写完毕后,子进程会通知父进程,父进程在把这个为子进程准备的缓冲区中的数据写入最后的AOF文件,替代磁盘中重写之前的AOF文件,也就完成AOF的重写过程

为什么Redis对于AOF和RDB的写入磁盘操作、重写操作等都是多进程而不是多线程呢?

  1. 多线程会出现多个线程同时在同一个进程地址空间中修改内存的情况,可能会出现数据不安全的情况,而多进程在只需要遍历读取这类操作时可以父子进程共享物理内存,需要修改时又可以写时拷贝,相比多线程安全很多
  2. 多线程为了维护性能、线程安全需要引入复杂的同步、竞争等等机制,大量的锁、信号量等等会带来不必要的性能开销,同时设计上也更加繁琐复杂;对于RDB文件而言,它的实现就是想保存下内存在某一时刻的快照,但是多线程的环境导致这一点无法实现,而多进程在创建时,写时拷贝使得天然就形成了一个快照,提供给RDB文件的生成

当执行bgrewriteaof时,如果已经在执行AOF的重写,那么就不再手动触发执行;当执行bgrewriteaof时,如果正在进行RDB文件的磁盘写入,就会等待其完成后再进行AOF的重写

混合持久化

在Redis的配置文件中,混合持久化是默认开启的,结合了RDB和AOF两者的特点:在触发AOF重写操作的时候,会按照RDB的方式将内存中数据的状态二进制写入AOF文件,后续追加AOF文件的时候仍然以文本的方式追加,这样在再次启动读取AOF文件时就能减少读取文本记录所消耗的时间

Redis事务

首先我们来看一下Mysql的事务,那必然离不开ACID

  • 原子性,将多个操作打包成一个操作,不能再细分,要么全都完成,要么全都不完成,就像二进制位,要么是0,要么是1
  • 一致性,操作前后,数据不能离谱,是正确的操作
  • 持久性,操作能够在硬盘上做持久化
  • 隔离性,能够解决脏读、幻读、不可重复读的问题

⚠️ 关于脏读、幻读、不可重复读

  1. 脏读,就是读到了其他事务处理了一半的中间临时数据
  2. 幻读,就是一个事务在第一次读时,一个数据还在;但是第二次读时这个数据又不在了,原因是其他事务对数据进行了增删操作。当然第一次读不存在第二次都又存在了也是幻读,即:幻读考虑的是数据存在性不一致的问题
  3. 不可重复读,第一次读时,数据是一个样,第二次读时数据又发生了改变,原因是其他事务的操作落实到了数据上,与幻读相比,不可重复读考虑的是已经存在的数据在一个事务中改变的问题

那接下来我们看Redis的事务,依次与Mysql对比

1. 原子性

这个其实有争议,争议不在Redis的实现,而是何谓"原子性"。

Mysql的原子性是一组操作,只有全部都执行了,并且全部都执行成功了,才会落实到数据上;而Redis的原子性,虽然也像Mysql一样进行了封装成为一个整体,但是它只需要全部执行,是否成功,Redis并不会过问,所以如果是说Redis有没有像Mysql一样的原子性,那答案一定是没有的

2. 一致性

在原子性里已经说过了,Redis只管是否执行,不管是否执行成功,那么一致性自然是无法保证的了

3. 持久性

首先第一点,Redis是内存中的数据库,存储介质上就无法满足持久性的基本条件;第二点,虽然Redis提供了RDB、AOF两种方式的持久化,但是这和事务没有什么关系

4. 隔离性

Redis不涉及这一次点,Redis主要的工作逻辑使用单线程的方式实现的,Mysql解决的那些问题是在并发的条件下才会出现的,Redis这里单线程自然不会出现脏读、幻读、不可重复读的问题,所有请求的处理都是串行执行的

Redis事务的作用
  1. 保证某个客户端的一组命令被服务器一次执行完,然后服务器再去处理其他客户端的请求,这组命令就是被Redis的事务包装起来的。当命令到达服务器后,会先进入一个队列而不是立即执行,当收到执行命令的请求之后,才会不断出队列进行处理

  2. 为什么Redis不实现像Mysql一样的事务机制呢?首先,更复杂的机制意味着需要更多的资源,而我们的Redis作为内存中的数据库,本来最注重的就是性能,空间上也不能太大,这意味着很难引入Mysql那样的回滚机制(毕竟后者是在磁盘上);同时,如果实现Mysql那样的事务机制,在运行时时间也会多消耗很多,这对于Redis也是致命的

  3. 如果Redis是通过集群模式部署,那么不支持事务

Redis事务操作
启动与结束事务
  • 使用multi来开启事务,后续的所有事务中操作都不会直接执行,而是会进入队列等待,返回结果也都会是queued
  • 使用exec来执行事务,服务器在接收到exec后,会一次从队列中出队执行,并返回每个命令的执行情况
  • 使用discard来放弃本次事务,已经入队列的命令全部抛弃
事务监控

使用watch来监控某一个key在事务开始到执行这段时间内是否发生改变,如果改变了的话,事务操作不会执行,返回nil;同时,可以使用unwatch来解除对某个key的监视

shell 复制代码
127.0.0.1:6379> watch key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key 333   # 此时另一个客户端对key进行了修改
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
watch的实现原理

✅ 关于乐观锁和悲观锁

乐观锁 ,是假设当前锁发生锁冲突(多个线程尝试获取同一把锁的情况)的概率是较低的,因此实现的也比较简单,比较轻量;悲观锁,是假设当前所大概率会发生冲突,所以实现的相对复杂。这两种锁都不涉及具体的锁,而是对一类锁的特点的总结。比如生活中很多"防君子不防小人"的操作,都是乐观锁的思想

✅ watch的实现机制

Redis对于每一个watch的key,都会分配一个递增的版本号(不一定是自增)。

watch key时,Redis服务器会为这个key记录版本号,到事务执行前的这段时间,如果有其他客户端给key做出了修改,那么这个key的版本号就会发生改变;在后续事务执行客户端发送exec指令时,如果key前后的版本号不一样,就会discard事务的所有操作。

Redis的这种实现方式就属于乐观锁的范畴,因为已经预想大概率不会发生冲突,所以锁实现的就很简单轻量

相关推荐
Linux蓝魔2 小时前
麒麟官方yum源配置V10SP2-V10SP3-V10SP3-2403
大数据·linux·运维
helloliyh2 小时前
linux 删除指定日期目录(包括目录下文件)
linux·运维·服务器
野犬寒鸦2 小时前
Redis复习记录Day03
服务器·redis·后端·面试·bootstrap·mybatis
半个俗人2 小时前
06.Linux用户权限相关命令
linux·运维·服务器
m0_706653232 小时前
开源键值存储数据库如何实现微秒级响应
数据库
小宇的天下2 小时前
Calibre LVS Circuit Comparison(1)
linux·数据库·lvs
涛声依旧393162 小时前
构建部署kubernetes所需主机
linux·运维·云原生·容器·kubernetes
羊小猪~~2 小时前
【QT】-- 模型与视图简介
开发语言·数据库·c++·后端·qt·前端框架·个人开发
曲幽2 小时前
FastAPI里玩转Redis和数据库的正确姿势,别让异步任务把你坑哭了!
redis·python·mysql·fastapi·web·celery·sqlalchemy·task·backgroundtask