跳至主要內容

Redis事务

LeoRedis7Redis7约 3438 字大约 11 分钟

1. 什么是Redis事务

Redis事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

官网: https://redis.io/docs/manual/transactions/open in new window

我这里做了一些简单的翻译

image-20230919134639449
image-20230919134639449

可以一次执行多个命令,本质是一组命令的集合,一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不许加塞。

总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

2. Redis中的事务处理

我们对数据库中事务处理的相关理论有了一个基本的认识,或许这个世界上的数据库系统千差万别,但我相信在事务处理这个问题上它们最终会殊途同归,就像我们解决并发过程中的冲突问题,常规的做法依然是加锁一样,这是我之所以要花费精力去理解和解释这些理论知识的原因,技术可谓是日新月异,如果我们总是一味地为新技术而疲于奔命,那么或许我们会渐渐地失去对这个行业的热爱,我相信原理永远比框架更为重要。

redis事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

Redis中的事务是可以视为一个队列,即我们可以通过MULTI开始一个事务,这相当于我们声明了一个命令队列。接下来,我们向Redis中提交的每条命令,都会被排入这个命令队列。当我们输入EXEC命令时,将触发当前事务,这相当于我们从命令队列中取出命令并执行,所以Redis中一个事务从开始到执行会经历 开始事务命令入队执行事务 三个阶段。下面是一个在Redis中使用事务的简单示例:

127.0.0.1:6379> MULTI 
OK 
127.0.0.1:6379> SET name "Leo" 
QUEUED 
127.0.0.1:6379> SADD Program_Language "C++" "C#" "Jave" "Python"  
QUEUED 
127.0.0.1:6379> GET name 
QUEUED 
127.0.0.1:6379> EXEC 
1) OK 
2) (integer) 4 
3) "Leo" 

我们可以注意到Redis中的事务和通常意义上的事务基本上是一致的,即

  • 事务是由一系列操作组成的单个逻辑工作执行单元。特别地,因为在Redis中命令是存储在一个队列中,所以,事务中的所有命令都会按顺序执行,并且在执行事务的过程中不会被客户端发送的其它命令中断。
  • 事务是一个原子操作,事物中的命令只有两种执行结果,即全部执行或者全部不执行。如果客户端在使用MULTI命令开启事务后因为意外而没有执行EXEC命令,则事务中的所有命令都不会执行。同理,如果客户端在使用MULTI命令开启事务后执行EXEC命令,则事务中的所有命令都会执行。
  • Redis中的事务可以使用DISCARD命令来清空一个命令队列,并放弃对事务的执行。如果命令在入队时发生错误,Redis将在客户端调用EXEC命令时拒绝执行并取消事务,但是在EXEC命令执行后发生的错误,Redis将选择自动忽略。

3. Redis事务 VS 数据库事务

1.单独的隔离操作Redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
2.没有隔离级别的概念因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题了
3.不保证原子性Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
4.排它性Redis会保证一个事务内的命令依次执行,而不会被其它命令插入

Redis事务和MySQL事务有以下不同点:

  1. 基于不同的存储方式:Redis是内存数据库,而MySQL是关系型数据库。因此,Redis将所有的数据存储在内存中,而MySQL将数据存储在磁盘上。这导致Redis的事务处理速度更快,但是不够持久。而MySQL的事务处理速度较慢,但更加稳定可靠。
  2. 事务处理的隔离级别不同:MySQL支持ACID属性的事务处理,并支持四种隔离级别(读未提交,读已提交,可重复读和串行化),而Redis不支持隔离级别。因此,在Redis中执行事务时,所有的命令都是按顺序执行的,没有并发执行的概念。
  3. 回滚处理不同:Redis和MySQL的事务都支持回滚操作,但实现方式不同。在Redis中,当事务中的某个命令执行失败时,整个事务将回滚,之前执行的所有命令都不会生效。而在MySQL中,事务可以根据隔离级别进行回滚,只回滚已提交的事务,未提交的事务不会被回滚。
  4. 支持的数据类型不同:Redis支持更多的数据类型,例如字符串、哈希、列表、集合和有序集合等,而MySQL只支持关系型数据。

总的来说,Redis和MySQL事务处理有很多不同之处,它们各自适用于不同的场景。如果需要高并发的数据操作,可以使用Redis事务;如果需要对数据进行持久化存储和复杂的数据关联查询,则可以使用MySQL事务。

4. 如何使用Redis事务

官网: https://redis.io/docs/manual/transactions/open in new window

image-20230919141544674
image-20230919141544674

4.1 Redis事务常用命令

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。

  • MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消WATCH对所有key的监视。
image-20230919142359758
image-20230919142359758

4.2 case1:正常执行 MULTI EXEC

image-20230924213036037
image-20230924213036037

4.3 case2:放弃事务 MULTI DISCARD

image-20230924213203040
image-20230924213203040

4.4 case3:全体连坐

官网说明:

image-20230924213309897
image-20230924213309897

一个语法出错,全体连坐。如果任何一个命令语法有错,Redis会直接返回错误,所有的命令都不会执行

4.5 case4:冤头债主

官网说明:

补充:

注意和传统数据库事务的区别,不一定要么全部成功要么全部失败

4.6 case5:watch监控

  • Redis使用Watch来提供乐观锁定,类似于CAS(Check-and-Set)

    1. 悲观锁:悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁

    2. 乐观锁:乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

      乐观锁策略:提交版本必须大于记录当前版本才能执行更新

    3. CAS

1. watch是如何进行监控的呢

Redis使用WATCH命令来决定事务是继续执行还是回滚,那就需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。

当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。

image-20230927093415912

2. Watch实现命令监控

在事务开始前用WATCH监控k1,之后修改k1为11,说明事务开始前k1值被改变,MULTI开始事务,修改k1值为12,k2为22,执行EXEC,发回nil,说明事务回滚;查看下k1、k2的值都没有被事务中的命令所改变。

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
  • watch key [key ...]

    1. 初始化 k1k2 两个key,先监控在开启multi,保证两个key变动在同一个事务内

      image-20230924213709534
      image-20230924213709534
      1. 有加塞篡改:watch命令是一种乐观锁的实现,Redis在修改的时候会检测数据是否被更改,如果被更改了,则执行失败
      image-20230924214013255
      image-20230924214013255

      图中3和4不管哪个先执行,最终的结果都是整个事务执行失败

  • unwatch

    image-20230924214108199
    image-20230924214108199
  • 小结

    一旦执行了exec之前加的监控锁都会被取消掉

    当客户端连接丢失的时候(比如退出链接),所有东西都会被取消监视

4.7 总结

开启:multi 开始一个事务

入队: 将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面

执行:exec 命令触发事务

5. Redis事务执行步骤

通过上文命令执行,很显然Redis事务执行是三个阶段:

  • 开启:以MULTI开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
  • 执行:由EXEC命令触发事务

当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:

  • 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
  • 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。

那我们如何通过一张图来判断**,服务器的判断命令是否是该入队还是该执行**。

image-20230927093351198
image-20230927093351198

6. 如何Redis与事务的ACID

般来说,事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性。这是基础,但是很多文章对Redis 是否支持ACID有一些异议,我觉的有必要

梳理下:

  • 原子性atomicity

首先通过上文知道 运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。

Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。

  • 一致性consistency

redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。

  • 隔离性Isolation

redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。

但是,Redis不像其它结构化数据库有隔离级别这种设计。

  • 持久性Durability

redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

参考文章

本文主要参考了

公众号封面
公众号封面