WangYu::Space

Study, think, create, and grow. Teach yourself and teach others.

Redis 事务

分类:Redis创建时间:2021-12-18 00:00:00

什么是事务

在数据库的语境下,事务(Transaction)指一组对数据库的操作。事务创造了一种抽象,用户在执行多条数据库操作指令时,不必担心部分失败和并发带来的竞争。说起事务,自然会提到事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation) 和持久性(Durability),这四个特性被简称为 ACID。

原子性

事务中的多个操作需要原子性地执行,这些操作要么全部执行成功,要么一个也不执行,不能出现部分执行而部分未执行的情况。如果事务在执行过程中出现错误,则会回滚已经执行过的操作,以满足要么全部执行成功,要么不执行的特点。

一致性

一致性的概念比较难理解,一致性是什么?怎样才算一致?是否一致由谁说了算?一致指的是应用层对数据库中数据提出的约束条件。比如某个字段不能确实,某个值取值不能小于零,这些约束是由应用层决定的。如果数据库中的数据满足约束,则认为当前数据库中的数据是一致的。若数据库中存在约束,那么在执行事务的前后,这些约束都要满足。如果对数据的修改导致约束不再满足,则事务会执行失败。

隔离性

数据库可同时服务多个客户端,隔离性要求事务间不能相互干扰,每个事务都像是独占整个数据库一样。隔离性分多个级别,因为不是本文重点,此处不再详述。

持久性

事务成功执行后,事务对数据库的修改要能持久化地保持下来,保证数据不会丢失。

关系型数据库中的事务

关系型数据库中使用 BEGIN, ROLLBACK, COMMIT 来实现事务,使用 BEGIN 开始一个事务,然后执行一些 SQL 命令,使用 COMMIT 来提交事务。如果想要取消事务,可以执行 ROLLBACK 来回滚到事务开始前的状态。

mysql> begin;  # 开始事务
Query OK, 0 rows affected (0.00 sec)
 
mysql> insert into test_transaction value(1);
Query OK, 1 rows affected (0.00 sec)
 
mysql> insert into test_transaction value(2);
Query OK, 1 rows affected (0.00 sec)
 
mysql> commit; # 提交事务
Query OK, 0 rows affected (0.01 sec)

Redis 事务的原理

Redis 中的事务和 SQL 中的事务大不相同,Redis 使用 MULTI、EXEC、DISCARD、WATCH 这几个命令来实现事务。MULTI 开启一个事务,EXEC 执行事务,DISCARD 取消事务,WATCH 用于保证事务的隔离性。

给 Redis 发送 MULTI 命令开启一个事务,之后此客户端发来的命令不会被立刻执行,而是被保存下来,当客户端发送 EXEC 时候,服务端一次性执行全部命令。因为 Redis 在单个线程中执行来自不同客户端的命令,因此这一组命令的执行不会被打断,这就保证了事务的原子性。

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a 123
QUEUED
127.0.0.1:6379(TX)> set b 234
QUEUED
127.0.0.1:6379(TX)> get a
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "123"

开启事务后,发送的命令不会立刻执行,因此只会返回结果 QUEUED,事务中所有命令的执行结果会在执行 EXEC 命令后一次性返回。

当事务中某个命令执行出错时,Redis 不会回滚之前的命令,而是继续执行,下面是个例子:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a 123
QUEUED
127.0.0.1:6379(TX)> lpush a 234
QUEUED
127.0.0.1:6379(TX)> get a
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) "123"

如果在事务提交之前,即发送 EXEC 之前,想要终止事务,此时可以执行 DISCARD 命令:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a 234
QUEUED
127.0.0.1:6379(TX)> set b 123
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> get a
"123"

执行 discard 之后,先前发送的命令会被清空,因为这些命令实际上并未执行,所以不会对数据造成任何影响。

事务中的命令只有在提交后才会执行,那在事务中该如何读取数据呢?如果要在一个事务中实现客户 A 给 客户 B 转账 N 元的逻辑,该怎么处理呢?

a_balance = GET A_balance
if a_balance < N:
    return

INCRBY A_balance -N
INCRBY B_balance  N

在 Redis 事务中,命令的结果不能立刻得到,如何执行判断,如何该将数据更新为多少呢?是不是觉得 Redis 弱爆了?其实 Redis 提供了自己的解法。

在关系型数据库中,事务中可以执行查询、插入等操作,而且可以立刻得到结果,因为 MySQL 采用了锁+多版本的方式,但在 Redis 中没有这些复杂的逻辑。Redis 提供了一个 WATCH 命令,它可以让用户监控一组 key,如果这组 key 在事务提交之前改变了,事务就不会成功执行。有了 WATCH 命令,可以按照如下方式实现转账逻辑:

WATCH A_balance

a_balance = GET A_balance
if a_balance < N:
	return

MULTI

INCRBY A_balance -N
INCRBY B_balance  N

EXEC

在事务开始前,先 watch 与当前事务相关的 key,并读取在事务中会用到的数据。开始事务后,只需要使用之前读到的值即可。在提交事务时,如果 Redis 发现此客户端正在 watch 的 key 被修改过,事务的执行会失败。Redis 使用 WATCH 这种机制,保证了事务执行期间,使用到的数据不会被其他客户端修改,以此保证了事务的隔离性。

Redis 事务的 ACID 分析

原子性

Redis 在单个线程中执行所有命令,事务中的一批命令的执行不会被打断,因此 Redis 的事务具备原子性。

一致性

Redis 如果成功执行了事务,数据库的状态满足数据库约束。如果事务未执行完毕系统故障了,那么数据不会被持久化,系统重启后,依然满足数据库的约束。所以,Redis 事务满足一致性约束。

隔离性

从客户端发起事务到执行事务,这期间可能会有大量其他客户端的命令被执行。执行事务的时候,数据库的状态和发起事务时很可能不一样了。Redis 通过提供 WATCH 来保证事务中用到的数据在当前事务执行期间没有被修改。Redis 事务中,用户需要自己使用 WATCH 来保证数据未被修改。至于 Redis 的事务是否具有隔离性,不必下结论,因为它和 MySQL 中的隔离性完全不是一个概念。

持久性

持久性取决于 Redis 的持久化模式,Redis 最严格的持久化策略是使用 AOF 持久化,并配个 appendfsync = always 配置。但即便如此,写入 AOF 的操作也是在当前事件循环结束后才进行。执行完事务后,事务对数据库做的修改不会立刻持久化下来。因此,Redis 的事务不满足持久性。

Redis 事务的实现原理

MULTI 的实现

Redis 收到 MULTI 命令后,仅仅是给当前客户端添加一个CLIENT_MULTI 标志位,此标志位被设置时,后续除了 DISCARD 和 EXEC 之外的命令都会被记录下来。

EXEC 的实现

当 Redis 收到 EXEC 命令后,首先判断当前客户端 watch 的 key 是否有被修改过、是否已经过期,如果是,则事务失败。否则,会执行保存下的命令,然后清空 watch 列表,清除客户端上事务的标记,最后返回结果给客户端。执行命令时候,阻塞命令会失败。

WATCH 的实现

watch 命令可以提供多个 key 作为参数:

WATCH key1 key2 key3

Redis 收到 WATCH 命令后,会将 key 保持在 redisDb->watched_keys 这个哈希表,键为 key,值为 client。并把 key 保持在 client->watched_keys 链表上。

struct redisDb {
    // ...
    dict *watched_keys;
};

struct client {
    // ...
    list *watched_keys;
}

后续命令中,每当有 key 被修改,Redis 会去 redisDb->watched_keys 中查找,如果找到,会将与该 key 关联的 client 都会被设置上标记。执行 EXEC 命令时,就靠这个标记判断 client 目前 watch 的 key 是否被修改过。

DISCARD 的实现

DISCARD 命令撤销事务,它做的事情是删除保存的事务中的命令,清空客户端上的标记,清理 watch 的 key。

评论 (评论内容仅博主可见,不会公开显示)