MySQL笔记15
一、 Redis 事务管理
1. Redis 的事务特性
Redis事务并非严格意义上的事务,只是用于批量执行多个命令的打包脚本。
单个Redis命令执行是原子性的,但Redis事务没有维持原子性的机制,中间某条指令失败不会导致前面指令回滚,也不影响后续指令执行。
Redis事务是单独的隔离操作,事务内所有命令会序列化、按顺序执行,可防止其他命令插队。
2.事务执行过程
(1)三阶段
一个事务从开始到执行会经历以下三个阶段:
开始事务
命令入队
执行事务
(2)基本指令
| 指令 | 功能 |
| multi | 组装并启动一个事务 |
| exec | 执行一个事务 |
| discard | 取消一个事务 |
| watch | 监视指定key,若这些key在事务执行前被修改,事务将被取消执行 |
(3)过程
执行逻辑:从输入 multi 命令开始,后续输入的命令会依次进入命令队列但不执行;直到输入 exec 后, Redis 会依次执行命令队列中的命令。组队过程中可通过 discard 放弃组队。

示例:
组建事务(先定义一个 key 为 user_id , value 为1。然后输出其值和开始事务, incr 为自增1运算,它的返回值 QUEUED 表示这些命令并没有执行,只是进入执行队列,可以多增几次来检测。最后输入 exec 开始执行事务队列的命令,可以看到值的增加):

注意:
上例演示了一个完整的事务过程,所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的有顺序的执行。
取消事务(执行 multi 开启事务后,入队了三次 incr 命令,随后执行 discard ,即可成功取消该事务。最后查询结果为1说明事务中的命令未被执行):

注意:discard 命令必须在 multi 开启的事务组队阶段使用,即要在 multi 之后、 exec 之前使用,且不带任何参数。
3.事务的错误处理
(1)情况1
若 Redis 事务组队阶段( multi 之后、 exec 之前)某条命令出现报告错误,执行 exec 后整个命令队列会被取消,事务中所有命令都不会执行。

示例:
下图中 Redis 事务因组队阶段命令语法错误( inc 拼写错误),所以执行 exec 时整个事务被放弃, group_id 值未改变。

注意:在 Redis 中,事务取消后,它的命令队列会被清空,这个事务实例就不存在了。之后可以继续使用同名的命令来组建新的事务,因为 Redis 的事务是基于 “每次 multi 开启一个新的事务上下文” 的机制,只要重新执行 multi ,就可以开启一个新的事务。
(2)情况2
在事务执行阶段若某条命令报错,仅该报错命令不执行,其他命令仍会执行且不会回滚。

示例:
开启事务后入队两次 incr group_id 和一次 incr name ,但因字符串无法自增,所以执行 exec 时, incr name 报错,而 incr group_id 正常执行,最终 group_id 变为3。

4.事务的冲突问题
(1)事务场景
有一账户余额为 10000 元,现在三个请求:
第一个请求想给金额减 8000 元
第二个请求想给金额减 5000 元
第三个请求想给金额减 1000 元
如果没有事务,可能会发生如下情况:

最后金额会变为负值。
(2)悲观锁(Pessimistic Lock)
概念:悲观锁认为每次拿数据时别人都会修改,因此拿数据时会上锁,导致其他人操作该数据时会被阻塞,直到拿到锁。
应用:传统关系型数据库(如行锁、表锁、读锁、写锁等)多采用这种锁机制,操作前先上锁。

(3)乐观锁(Optimistic Lock)
概念:乐观锁持乐观态度,拿数据时认为别人不会修改,所以不上锁;更新时会判断期间数据是否被他人修改,可通过版本号等机制实现。
应用:适用于多读的应用类型,能提高吞吐量。Redis 利用 check-and-set (检查并设置)机制实现事务,就是基于乐观锁的思路。

(4)watch 监听
功能:在执行 multi 前,通过 watch key1 [key2] 可监视一个或多个key。若事务执行前这些 key 被其他命令改动,事务会被打断。
示例:
客户端 A (开始监听后,开启事务并入队 decrby plan 2000 命令):

客户端 B (在另一个客户端修改了 plan 的值。):

客户端 A (此时返回原来的客户端执行 exec 后发现,因 plan 已被客户端 B 修改,所以事务提交失败,返回 nil 。):

注意:Redis 禁止在 multi 和 exec 之间执行 watch 指令,且必须在 multi 之前盯住关键变量,否则会出错。
(5)unwatch 取消监听
功能:用于取消 watch 命令对所有 key 的监视。

注意:若在执行 watch 后,先执行了 exec 命令(执行事务)或 discard 命令(取消事务),则无需再执行 unwatch ,因为这两个命令会自动取消 watch 对 key 的监视。
5. Redis 的事务三特性
单独的隔离操作:事务内所有命令会序列化、按顺序执行,执行过程中不会被其他客户端的命令请求打断。
没有隔离级别的概念:事务队列里的命令在提交( exec )前不会实际执行。
不保证原子性:事务中若某条命令执行失败,后续命令仍会执行,不会回滚。
二、数据持久化
1.概念
Redis 数据持久化是为防止 Redis 因内存存储数据在宕机时丢失,将内存中的数据库数据保存到磁盘的机制。其有两种持久化方式:RDB(Redis DataBase)和AOF(Append Of File)
2.RDB
(1)作用
在指定时间间隔内,将内存中所有数据集进行全量快照(Snapshot)写入磁盘。恢复时直接将快照文件读入内存,类似拍照记录某一瞬间的完整数据状态。数据通过 rdbSave 从内存写入磁盘的RDB文件,再通过 rdbLoad 从RDB文件加载回内存。

(2)原理
创建子进程进行持久化,先将数据写入临时文件,持久化完成后替换上次的持久化文件;主进程不进行 IO 操作,保证了高性能。适用于大规模数据恢复且对完整性要求不高的场景,但缺点是最后一次持久化后的数据可能丢失。
(3)Fork
Redis 的 Fork 机制基于写时复制技术(COW)实现快照持久化(在执行快照的同时,正常处理写操作) :
Fork(多进程) 的作用:Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程(所有数据,包括:变量、环境变量、程序计数器等数值都和原进程一致),快照持久化完全交给子进程来处理,父进程继续处理客户端请求
主进程需要修改数据时的处理:
gsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
此时,如果主线程对这些数据也都是读操作(例如下图中的键值对 K1 ),那么,主线程和 bgsave 子进程相互不影响。
但是,如果主线程要修改一块数据(例如下图中的键值对 K3 ),那么,这块数据就会被复制一份,生成该数据的副本。
然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

(4)快照运行过程
当 Redis 需要保存 dump.rdb 文件时, 服务器执行 bgsave 命令后则执行以下操作:
Redis 调用 fork() ,产生一个子进程,此时同时拥有父进程和子进程。
父进程继续处理 client 请求,子进程负责将内存内容写入到临时文件。由于 os 的写时复制机制,父子进程会共享相同的物理页面,当父进程处理写请求时, os 会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数据是 fork 时刻整个数据库的一个快照。
当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

(5)相关配置
配置文件名:根据 redis.conf 中的配置, 快照将被写入 dbfilename 指定的文件中(默认是 dump.rdb 文件)。
配置文件路径: 快照将保存在 dir 选项指定的路径上,我们可以修改为指定目录。
![]()
注意:由于此路径是一个相对路径,它会根据命令执行的所在目录来生成 dump.rdb 文件,因此建议修改指定目录为固定位置,如:/usr/local/redis/data
save:
作用:配置备份规则
格式:save 秒数 修改次数

示例:
先进入配置文件中 vim /etc/redis/redis.conf ,然后找到数据持久化处修改 save (由于文件内容过多,可以输入搜索功能命令来进行定位,如默认配置一般为下图,在 vim 的普通模式下输入 /save 3600 即能定位成功)
![]()
把持久化的策略修改为 save 30 5 (表示 30 秒内写入 5 条数据就产生一次快照,也就是生成 rdb 文件 ):

修改好后,保存并重启 redis 服务,然后打开客户端,执行如下命令:

最后然后查看 dump.rdb 文件的大小,发现文件快照已经产生了。
注意:
Redis的持久化命令有两种:
save :手动保存,会阻塞所有操作,不建议使用。
bgsave :自动保存,Redis在后台异步进行快照操作,同时可响应客户端请求。
其他:
stop-writes-on-bgsave-error:当 Redis 无法写入磁盘的话,直接关掉 Redis 的写操作,推荐配置为 yes。
rdbcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,Redis 会采用 LZF 算法进行压缩。 如果不想消耗CPU来进行压缩,可以设置为关闭此功能。推荐配置为 yes。
rdbchecksum:在存储快照后,还可以让 Redis 使用 CRC64 算法来进行数据校验,检查数据完整性。 但这样做会增加大约 10% 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。推荐配置为 yes。
(6)备份
示例:
先清库:

再重新添加如下数据:

备份 dump.rdb 文件,且关闭 Redis 服务,最后将原来的 dump.rdb 文件删除:

(7)恢复
接着上一步备份操作:
先将 dump.rdb.back 文件名修改为 dump.rdb (并重启Redis服务):
![]()
再连接客户端,执行如下命令来查看(原来删除的内容就恢复了):

(8)优势
适合大规模数据恢复
对数据完整性和一致性要求不高更适合使用
节省磁盘空间
基于二进制存储的,恢复速度快

(9)劣势
Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
3.AOF
(1)作用
以日志形式增量保存每个写操作(读操作不记录),仅追加文件、不可改写,Redis 启动时读取该文件重新构建数据。Redis 重启时会按 AOF 文件中写指令的顺序执行,完成数据恢复。
默认配置中AOF未开启,需手动开启才能使用。
AOF 通过将写命令追加到文件末尾记录数据变化,执行文件中所有写命令即可恢复数据集。
(2)实现 AOF 日志
数据库常见的写前日志(WAL)是在实际写数据前记录修改到日志文件,实现“日志先行”;而 Redis 的 AOF 日志是写后日志,即 Redis 先执行命令将数据写入内存,之后再记录日志。

注意:
日志先行方式宕机后可通过日志恢复数据;而 AOF 采用后写日志方式,若宕机可能丢失已写入内存但未记录日志的数据。
传统数据库的日志,例如 redo log(重做日志),记录的是修改后的数据,而 AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。
以 Redis 收到 “set k1 v1” 命令后记录的日志为例,看看 AOF 日志的内容。
其中:
“ *2 ” 表示当前命令有两个部分,每部分都是由 “ $+数字 ” 开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。
例如,*2 表示有两个部分,6表示6个字节,也就是下边的 “SELECT” 命令,1 表示 1 个字节,也就是下边的 “0” 命令,合起来就是 SELECT 0,选择 0 库,下边的指令同理。

总结:
为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。
Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。除此之外,AOF 是在命令执行后才记录日志,所以不会阻塞当前的写操作。
(3)原理

客户端的请求写命令会被 append 追加到 AOF 缓冲区内。
AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作 sync 同步到磁盘的 AOF 文件中。
AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量。
Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的。
(4)使用方法
进入配置文件里,把 no 改为 yes 即可开启 AOF 持久化:
![]()
通过 appendfilename 指定日志文件名字(默认为appendonly.aof):
![]()
通过 appendfsync 指定日志记录频率:

appendfsync 的配置选项:
| 选项 | 同步频率 |
| always | 每个redis写命令都要同步写入硬盘,优点是当发生系统崩溃时数据丢失减至最少,缺点是这种策略会产生大量的I/O操作,会严重降低服务器的性能。 |
| everysec | 以每秒一次的频率对 AOF 文件进行同步,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器几乎没有任何影响。 |
| no | 由操作系统决定何时同步,会增加系统崩溃时数据丢失的数量。 |
示例:
确保有 /appendonlydir 目录后,停止 Redis 服务,然后再重新开启 Redis 服务后,就可以在 / 目录下会生成多个以 appendonly 开头的文件:

文件从左到右为:
基本文件,是一个快照,表示文件创建时数据集的完整状态
增量文件, 包含应用于前一个文件之后的数据集的额外命令
清单文件,用于跟踪文件及其创建和应用的顺序
登录添加一些 key ,再退出查看增量内容:

(5)Rewrite(重写)
概念:
AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制。当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。
以使用命令 bgrewriteaof 实现,该操作相当于“瘦身”, 在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了
原理:
AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,转换成一条条的操作指令,再序列化到一个新的 AOF 文件中。
参数 no-appendfsync-on-rewrite=yes ,表示不写入 AOF 文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。 这样做可以降低数据安全性,提高性能。
如果参数 no-appendfsync-on-rewrite=no , 则还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。这样做可以数据安全,但是性能降低。
触发机制:
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
参数 auto-aof-rewrite-percentage :设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
参数 auto-aof-rewrite-min-size :设置重写的基准值,最小文件64MB。达到这个值开始重写。
(6)重写流程
Redis 执行 fork() ,现在同时拥有父进程和子进程,子进程开始将新 AOF 文件的内容写入到临时文件。
对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾:这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
现在 Redis 使用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾,如图:

(7)优势
备份机制更稳健,丢失数据概率更低。
可读的日志文本,通过操作AOF稳健,可以处理误操作。
(8)劣势
比起RDB占用更多的磁盘空间。
恢复备份速度要慢。
每次读写都同步的话,有一定的性能压力。
存在个别Bug,造成恢复不能。
(9)选择
那么,在开发中是选择 RDB 还是选择 AOF 来持久化呢?官网建议如下:
官方推荐使用 RDB 与 AOF 混合式持久化。
若对数据安全性要求不高,则推荐使用纯 RDB 持久化方式。
不推荐使用纯 AOF 持久化方式。
若 Redis 仅用于缓存,则无需使用任何持久化技术。
