# 5.2 Redis事务操作

## 5.2.1 事务的概念与ACID特性

ACID特性:

* A:Atomicity,表示原子性.即事务是一个不可分割的实体,事务中的操作要么都做,要么全都不执行.
* C:Consistency.表示一致性.即事务前后数据完整性必须一致,假设数据库里有很多完整性约束,比如ID字段不能为空,且必须是10位,在事务执行前后,这些完整性约束不能被违反
* I:Isolation.即一个事务内部操对其他事务是隔离的,并发执行的各事务间不能互相干扰
* D:Durability.指一个事务一旦提交,它对数据库的改变就是永久性的,哪怕数据库出现故障,事务执行后的操作也该丢失

## 5.2.2 实现Redis事务的相关命令

* `MULTI`
  * 功能:标记一个事务块的开始
* `DISCARD`
  * 功能:取消事务,放弃执行事务块内的所有命令
* `WATCH`
  * 语法:`WATCH key [key ...]`
  * 功能:监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断
* `UNWATCH`
  * 功能:取消`WATCH`命令对所有key的监控
  * 注意:该命令不能取消对指定键的监控,只能取消所有键的监控

注意:当你执行`EXEC`或`DISCARD`之后,Redis会自动取消对所有WATCH过的键的监视.所以,事务成功执行后(使用`EXEC`)或放弃事务后(使用 `DISCARD`),你不需要显式地调用`UNWATCH`

例:

```
127.0.0.1:6379> SET name 'Peter'
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET id '001'
QUEUED
127.0.0.1:6379(TX)> GET id
QUEUED
127.0.0.1:6379(TX)> SET depName 'Dev'
QUEUED
127.0.0.1:6379(TX)> SET age 25
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) "001"
3) OK
4) OK
127.0.0.1:6379> GET age
"25"
```

对于命令`SET name 'Peter'`而言,此时还没有开启事务,因此返回的结果是`OK`

然后通过`MULTI`命令开启了事务.可以看到开启事务后,命令提示符尾部多了个`(TX)`

从`SET id '001'`命令的返回值(`QUEUED`)可以看出,该命令返回的结果并不是`OK`,而是`QUEUED`,表示该命令当前没有执行,而是放到了事务队列中

对于`GET id`命令而言也是一样,该命令的返回值并不是`id`对应的值,而是`QUEUED`,同样表示该命令被放到了事务队列中

最终通过`EXEC`命令执行事务.该命令会一次性地返回包含在事务队列中所有命令的执行结果.一旦执行完`EXEC`命令,就退出了事务状态.因此可以看到`GET age`命令会立即返回结果

本例表明:当通过`MULTI`命令开启事务状态后,**之后的命令不是立即执行,而是会被放入事务队列**.当exec命令出现后,则会一次性地执行事务队列中的命令

## 5.2.3 通过`DISCARD`命令撤销事务中的操作

例:

```
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET name 'Peter'
QUEUED
127.0.0.1:6379(TX)> SET age 19
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> GET age
(nil)
127.0.0.1:6379> GET name
(nil)
```

注:开启事务前执行过`FLUSHDB`

可以看到,执行了`DISCARD`命令后,退出了事务状态

在实际的应用中,如果能确保`MULTI`后的命令均能正确执行,那么可以通过exec来提交事务;反之,则可以通过`DISCARD`命令来撤销操作,以此体现事务的"原子性".

## 5.2.4 通过`WATCH`命令监听键的变化

注:本节使用的是我本地的redis,而非是容器中的redis

在Redis中,`WATCH`命令是为了实现乐观锁提供的一种机制.它使得我们可以监视一个或多个键,然后根据这些键的值是否被其他客户端修改来决定是否继续执行事务.

乐观锁的核心思想是假设数据在大部分时间都不会产生冲突,因此先进行数据操作,如果后续发现数据有冲突(比如被其他客户端修改过)再进行相应的处理.

`WATCH`的工作流程如下:

1. 使用`WATCH`命令监视一个或多个键
2. 执行一系列命令,这些命令会基于你所监视的键的当前值
3. 使用`MULTI`开始一个事务
4. 将你想在事务中执行的所有命令加入队列
5. 使用`EXEC`命令尝试执行事务.如果从执行`WATCH`命令开始,所监视的任何键都没有被其他客户端修改过,那么事务将成功执行.否则,事务将不执行任何命令并返回一个错误,告诉你有至少一个被监视的键已被修改

这个机制允许你确保在执行事务时,被监视的键的值与你上次检查时的值保持一致.

例如,考虑一个简单的场景,你想从一个键`counter`中递增一个值，但只有在该值没有被更改过的情况下才这样做。你可以使用`WATCH`来确保在你检查`counter`值和递增它之间,没有其他客户端修改它

* step1. 确保存在名为`counter`的键存在:

```
127.0.0.1:6379> SET counter 5
OK
```

* step2. 监视这个键

```
127.0.0.1:6379> WATCH counter
OK
```

* step3. 查看该键当前的值

```
127.0.0.1:6379> GET counter
"5"
```

* step4. 再开一个`redis-cli`客户端,在该客户端中修改`counter`的值

```
(base) root@yuanhong ~ % redis-cli
127.0.0.1:6379> SET counter 6
OK
```

* step5. 回到原始的`redis-cli`,开启一个事务并尝试递增`counter`

```
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR counter
QUEUED
127.0.0.1:6379(TX)> EXEC
(nil)
```

由于`counter`在你开始事务之前已经被修改,所以`EXEC`返回`nil`,表示事务没有执行.

## 5.2.5 Redis持久化与事务持久性

Redis会把数据缓存在内存中,这在带来性能便利的同时,会给事务的持久性带来一定的障碍.事务的持久性是指,一旦Redis的事务通过`EXEC`命令执行完成后,对Redis数据库的影响应当是永久性的.不过假设某事务执行后Redis服务器因断电重启,那么保存在该服务器内存里的Redis数据就会丢失,所以在这种场景里出现故障时无法确保事务的持久性

对此,可以通过Redis的持久化来确保事务的持久性.Redis持久化是指把Redis缓存数据从内存中保存到硬盘上.Redis持久化的方式有两种:

* AOF:Append Only File(AOF)
* RDB:Redis DataBase(RDB)

在后继章节里会详细讲述这两种能确保事务持久性的方式,这里仅给出实现AOF和RDB持久化的基本配置,以此来讨论这两种持久化与事务持久性的关系

**AOF持久化方式能确保事务的持久性,而RDB方式则不能**.

设置Redis服务器基于AOF的持久化:

```
127.0.0.1:6379> CONFIG SET appendfsync always
OK
127.0.0.1:6379> CONFIG REWRITE
OK
```

AOF持久化的方式,针对Redis数据的事务能即时存入硬盘文件中.这样一旦出现故障,数据也不会丢失,由此能确保事务的持久性

设置Redis服务器基于RDB的持久化:

```
127.0.0.1:6379> CONFIG SET dir /StudyRedisBaseOnDocker/conf/chapter5/section5-1/
OK
127.0.0.1:6379> CONFIG SET dbfilename redisRDB.rdb
OK
127.0.0.1:6379> CONFIG REWRITE
OK
```

* `CONFIG SET dir`:设置待持久化文件的路径
* `CONFIG SET dbfilename`:设置持久化文件的文件名

基于RDB的持久化方式是需要满足一定条件后(例如1分钟内至少有100个键被修改)才会触发持久性,反之不触发

所以,在某些场合里无法即时把Redis的修改记录到硬盘上,如果此时发生故障,数据就无法恢复,也就是说,Redis的RDB持久化方式无法确保事务的持久性
