Redis 09_Redis事务

一、MySQL事务回顾

1.1 MySQL 事务的概念与特性

MySQL 事务(Transaction) 是一种用于管理数据库操作的机制,它确保一组相关的 SQL 操作要么全部成功执行,要么全部失败,从而维护数据的一致性 和 完整性。

事务(Transaction) 是数据库管理系统中的核心概念之一,它通常遵循 ACID 属性:

  • 原子性(Atomicity):事务是一个原子操作,要么全部执行成功,要么全部失败。如果其中任何一部分操作失败,整个事务将被回滚,数据库状态将保持不变。
  • 一致性(Consistency):事务的执行将数据库从一个一致的状态转移到另一个一致的状态。这意味着事务执行后,数据库必须满足事先定义的完整性约束。
  • 隔离性(Isolation):隔离性定义了多个并发事务之间的相互影响程度。不同的隔离级别提供不同的隔离程度,包括读未提交、读已提交、可重复读和串行化等级。
  • 持久性(Durability):一旦事务被提交,其结果将永久存储在数据库中,并在系统故障后保持不变。

1.2 MySQL 事务的管理

在MySQL中,可以使用以下关键字来管理事务:

  • BEGIN:开始一个新的事务;
  • COMMIT:提交事务,将更改保存到数据库;
  • ROLLBACK:回滚事务,撤销未提交的更改;
  • SAVEPOINT:创建一个保存点,可以在事务中的特定位置回滚;
  • SET TRANSACTION:设置事务的属性,如隔离级别;

以下是一个简单的MySQL事务示例:

1
2
3
4
5
6
7
BEGIN;  -- 开始事务

-- 在事务中执行一些SQL操作
INSERT INTO users (id, username) VALUES (1, 'user1');
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;

COMMIT; -- 如果一切正常,提交事务

如果在事务执行期间发生错误或者需要取消事务,可以使用 ROLLBACK命令 来回滚事务,这将撤销所有在事务中的更改。

1
2
3
4
5
6
BEGIN;  -- 开始事务

-- 在事务中执行一些SQL操作
INSERT INTO users (id, username) VALUES (2, 'user2');

ROLLBACK; -- 发生错误,回滚事务

使用事务可以确保数据库的数据完整性,特别是在多个用户同时访问数据库时。根据具体的应用场景和需求,可以选择不同的隔离级别,以平衡并发性和一致性之间的需求。在实际应用中,正确使用事务是确保数据的一致性和可靠性的关键。

二、对 Redis 事务的认识

2.1 Redis 事务的概念

Redis事务(Transaction) 允许将一组 Redis 命令组合成一个单独的、不可中断的操作序列。这意味着,一旦开始了一个 Redis 事务,任何在事务执行期间的其它客户端的命令都不会中断这个事务,而是会排队等待执行,直到事务被提交或取消执行。

Redis 事务的核心概念涉及在 Redis 服务器上创建一个称为 事务队列 的缓冲区。当开启一个事务后,所有与该事务相关的命令都将按顺序排队进入该队列,只有当发送执行事务的命令时,队列中的命令才会以原子的方式依次执行,这确保了这组命令要么全部成功,要么全部失败。

未收到执行命令的事务在 Redis 服务器中处于排队等待状态,虽然将命令发送给了服务器,但并没有执行,同时也不会影响其它客户端命令的正常执行。

在事务队列中,可以包括任意数量的 Redis 命令,包括读取 和 写入操作,以便进行一系列复杂的操作。然而,需要注意的是,Redis 事务并不支持像传统数据库那样的隔离级别和回滚机制。因此,应用程序需要自行处理错误 和 回滚逻辑。

总之,Redis 事务提供了一种 将多个命令打包成一个原子操作的能力 ,这在需要 确保一系列操作的一致性时 非常有用。然而,它与传统数据库的事务有一些区别,特别是在隔离性和持久性方面。

Redis的一个事务从开始到执行会经历以下三个阶段:

  • MULTI 命令开启事务;
  • 命令入队;
  • EXEC 命令按顺序执行队列里的所有命令;

Redis事务 可以一次执行多个命令,并且带有以下两个重要的保证:

  • Redis事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行;
  • Redis事务在执行的过程中,不会被其它客户端发送来的命令请求所打断;

Tips: 事务不会被其它 client连接打断,当一个 client在一个连接中发出 multi命令 后,这个连接会进入一个事务上下文,该连接后续的命令并不是立即执行,而是先放到一个队列中。当从此连接收到 exec命令 后,redis会顺序的执行队列中的所有命令,并将所有命令的运行结果打包一起返回给client,然后此连接就结束事务上下文。

EXEC命令负责触发并执行事务中的所有命令:

  • 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行EXEC,那么事务中的所有命令都不会被执行;
  • 如果客户端成功在开启事务之后执行 EXEC,那么事务中的所有命令都会被执行;

当使用AOF方式做持久化的时候,Redis会将事务中命令转化为单个命令后将其写入到磁盘中。 如果Redis服务器因为某些原因被管理员杀死,或者遇上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。 如果Redis在重新启动时发现AOF文件出了这样的问题,那么它会退出,并汇报一个错误。 使用redis-check-aof程序可以修复这一问题,它会移除AOF文件中不完整事务的信息,确保服务器可以顺利启动。

2.2 Redis 事务与 MySQL 事务的比较

虽然 Redis 和 MySQL 都涉及事务的概念,但在实现和应用方面存在一些关键差异。

1、事务的粒度不同

  • Redis 事务通常以单个命令的粒度工作。多个 Redis 命令可以放入一个事务中,但每个命令本身是不可分割的,这允许执行一系列简单的操作,但不能跨多个 Redis 命令执行复杂的事务。
  • MySQL 事务具有更广泛的粒度,允许跨多个 SQL 语句执行复杂的事务,包括事务的开始和结束。

2、ACID 属性支持度

  • Redis 事务支持 原子性(Atomicity)一致性(Consistency),确保一组命令要么全部成功,要么全部失败,并在事务执行后维护数据的一致性。但 Redis 不支持传统数据库中的 隔离性(Isolation)持久性(Durability)
  • MySQL 事务支持完整的 ACID 属性,包括原子性、一致性、隔离性和持久性。这使得 MySQL 事务在需要严格事务完整性的应用中非常有用,如金融和电子商务系统。

3、并发控制差异

  • Redis 事务是单线程的,一次只能执行一个事务。这限制了并发性,因为其它客户端的命令必须排队等待执行。同时 Redis 使用乐观锁定来控制并发。
  • MySQL 具有强大的并发控制机制,支持多个事务同时执行,可以根据需要选择不同的隔离级别,从而平衡并发性和一致性。

4、错误处理和回滚

  • Redis 事务提供了回滚操作的能力,可以使用 DISCARD 命令来取消当前事务并清除已排队的命令。但 Redis 不会自动回滚事务中的错误命令。
  • MySQL事务支持显式的回滚操作,可以使用 ROLLBACK 来撤销已执行的事务中的命令,并恢复到事务开始之前的状态。

5、应用场景

  • Redis 事务通常用于缓存、队列、计数器等特定用例,其中需要简单的原子操作,但不强调严格的隔离性 和 持久性。
  • MySQL 事务广泛用于支持事务完整性的应用程序,如电子商务、金融系统等,这些应用需要强大的 ACID 支持。

总之,Redis事务和MySQL事务在粒度、ACID属性、并发控制、错误处理和应用场景等方面存在重要差异。选择使用哪种类型的事务取决于项目需求和性能要求。在某些情况下,Redis 和 MySQL可以共同使用,以满足不同类型的事务需求。

三、Redis 事务相关命令

Redis 事务是一种强大的功能、允许将一系列 Redis 命令打包在一起,以确保它们要么全部成功执行,要么全部失败 的机制。在 Redis 中,有一些关键的命令用于处理事务,包括 MULTIEXECDISCARDWATCHUNWATCH,下面将详细介绍这些命令的作用和用法。

3.1 MULTI 命令

MULTI 命令用于开启一个事务(标记事务块的开始),在执行 MULTI 命令后,Redis 会进入事务模式,接下来的命令都会被逐个加入到事务队列中,等待用户输入 EXEC命令 开启事务执行。如果 MULTI 命令执行成功,会返回 OK 等待输入要操作命令。 命令语法:

1
2
3
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)>  # TX 表示客户端连接开启事务中,等待 用户输入操作命令 或 EXEC 命令

3.2 WATCH 和 UNWATCH 命令

1、WATCH 命令 WATCH 命令标记所有指定的 key 被监视起来,在事务中有条件的执行(乐观锁)。在开启事务时,如果某个事务中的键被其它客户端修改,可能会导致数据不一致的问题。WATCH 命令用于解决这个问题,它可以监控一组指定的键,如果这些键在事务执行前被其它客户端修改,事务将被取消。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 客户端1 开始监控 k1
127.0.0.1:6379> WATCH k1
OK

# 客户端2 修改 k1
127.0.0.1:6379> SET k1 100
OK

# 客户端1 执行事务,但由于 k1 被修改,事务将被取消
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 200
QUEUED
127.0.0.1:6379> EXEC
(nil)
# 获取 k1 的值
127.0.0.1:6379> GET k1
"100"

WATCH 的原理可以简单概括为以下几个步骤:

  • 客户端执行 WATCH 命令并指定要监视的键;
  • Redis 在服务器端创建一个 监视器(Watcher) 来监视这些键,监视器会记录被监视键的当前状态,通常是版本号或时间戳;
  • 客户端进入事务模式,可以执行多个命令,但这些命令不会立即执行,而是进入一个队列中等待执行指令;
  • 在执行事务之前,客户端要求服务器检查监视器中被监视键的状态是否发生变化,这是通过比较监视器中的状态与当前键的状态来完成的;
  • 如果监视器发现被监视键的状态在执行事务之前已经发生变化,它会通知客户端取消事务,这是乐观锁的核心思想:如果其它客户端在执行 WATCH 和 EXEC 之间修改了被监视的键,那么事务将失败
  • 如果监视器未发现变化,客户端可以执行事务中的命令,这些命令会按顺序执行;

总之,WATCH 的原理是通过在服务器端创建监视器来监控一组键的状态,并在执行事务前检查这些键的状态是否发生变化。如果有其它客户端修改了被监视键,事务将被取消,以确保数据的一致性。这使得 Redis 能够在并发环境中实现事务操作,而不会引发竞态条件等问题。

2、UNWATCH 命令 UNWATCH 命令用于取消对键的监控,是 WATCH 命令的逆操作。如果不再需要监控某些键,可以使用 UNWATCH 来取消监控。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
127.0.0.1:6379> set key 1
OK
127.0.0.1:6379> WATCH key
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key 2
QUEUED
127.0.0.1:6379> set key 3
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> 

3.3 EXEC 命令

EXEC 命令用于执行先前放入事务中的命令队列,。在执行 EXEC 命令时,Redis 会按照事务队列中命令的顺序,以原子的方式依次执行这些命令。如果事务中的所有命令都执行成功,EXEC 命令返回一个包含每个命令执行结果的数组;如果有命令执行失败,事务中的所有命令都会被取消。然后恢复正常的连接状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 1
QUEUED
127.0.0.1:6379> SET k2 2
QUEUED
127.0.0.1:6379> SET k3 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) OK

3.3 DISCARD 命令

DISCARD 命令用于放弃当前事务。执行 DISCARD 命令会清空事务队列中的所有命令,之前的操作都不会被真正执行。然后恢复正常的连接状态。这个命令的返回值是一个简单的字符串,总是OK。

如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 1
QUEUED
127.0.0.1:6379> SET k2 2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> GET k1
(nil)
127.0.0.1:6379> GET k2
(nil)

四、Redis事务中的错误

4.1 Redis事务中的错误

在一个事务的运行期间,可能会遇到两种类型的命令错误:

  • 事务在执行 EXEC之前,入队的命令可能会出错,比如说,命令可能会产生语法错误(参数数量错误,参数名错误等等),或者其它更严重的错误,比如内存不足(如果服务器使用maxmemory设置了最大内存限制的话)等;
  • 命令可能在 EXEC调用之后失败,举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类;

对于发生在EXEC执行之前的错误,客户端以前的做法是检查命令入队所得的返回值,如果命令入队时返回QUEUED,那么入队成功;否则,就是入队失败;

如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。

不过,从Redis 2.6.5开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC命令时,拒绝执行并自动放弃这个事务。

在Redis 2.6.5以前,Redis只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的回复都只需要和服务器进行一次通讯。至于那些在EXEC命令执行之后所产生的错误,并没有对它们进行特别处理,即使事务中有某个/某些命令在执行时产生了错误,事务中的其它命令仍然会继续执行。