前言

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

常见支持事务的数据库或其部分引擎有:Oracle、DB2、MySQL…

可以看到上面的举例都是关系型数据,那对于 Redis 这种非关系性数据库来说。是否也支持 ACID 特性尼? 接着往下看。

ACID分析

Redis 提供了一组命令来实现事务的开启、提交、回滚等。事务的实现是在开启事务开始操作的命令被暂存到了命令队列,还没有实际执行。当执行了EXEC后才会被真正的执行。

命令

含义

备注

MULTI

开启事务

EXEC

提交事务

假如某个(或某些) key 正处于 WATCH 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。

DISCARD

类似回滚事务,只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。

如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH

WATCH

监视一个(或多个) key

如果在事务提交之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

UNWATCH

取消监视

如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

在具体分析前,先模拟一个事务的例子。比如商品 goods_a 目前库存还有10件,同样商品 goods_b 也是还有10件库存。下面用这个例子来进行分析。

原子性

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态。这是原子型的含义。

现在需要分析 Redis 是否支持原子性,我们需要举例当出现一些异常情况发生时候观察 Redis 的行为即可。

1、 在事务中进行语法错误的操作,例如 set 写成了 sset 。如下:

# set goods_a 
127.0.0.1:6379> set goods_a 10
OK
# set goods_b
127.0.0.1:6379> set goods_b 10
OK
// 开启事务
127.0.0.1:6379> multi
OK
// decr goods_a
127.0.0.1:6379> decr goods_a
// 加入了待执行队列
QUEUED
decr goods_b
127.0.0.1:6379> decr goods_b
QUEUED
// 故意写错 set 命令 入队就报错了
127.0.0.1:6379> sset 1
(error) ERR unknown command `sset`, with args beginning with: `1`,
// 提交事务
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.    //失败了

// goods_a goods_b 被回滚了
127.0.0.1:6379> get goods_a
"10"
127.0.0.1:6379> get goods_b
"10"
127.0.0.1:6379>

结论:保证了原子性。

2、退出客户端

127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr goods_a
QUEUED
127.0.0.1:6379> decr goods_b
QUEUED

# 执行 exit 退出客户端

# 重新连接
127.0.0.1:6379> get goods_a
"10"

结论:保证了原子性。

3、watch 操作

# set watch_test
127.0.0.1:6379> set watch_test 1
OK
127.0.0.1:6379>

# 监视 watch_test
127.0.0.1:6379> watch watch_test
OK
# 开启事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr goods_a
QUEUED

# 执行之前在其他的客户端 进行 set watch_test 2 操作
127.0.0.1:6379> exec
(nil)          # return nil 代表事务被放弃回滚了
127.0.0.1:6379> get goods_a
"10"  # 还是之前的值
127.0.0.1:6379> get watch_test
"2"   # 被设置成功的
127.0.0.1:6379>

结论:保证了原子性。

4、类型不匹配的命令操作

# set 一个字符串
127.0.0.1:6379> set string string
OK
# 开启事务
127.0.0.1:6379> multi
OK
# 对字符串类型进行 pop 操作 ,但是入队是成功的
127.0.0.1:6379> lpop string  
QUEUED
127.0.0.1:6379> decr goods_a
QUEUED
127.0.0.1:6379> exec
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 9
127.0.0.1:6379> get goods_a
"9"
127.0.0.1:6379>

结论:不能保证原子性。

5、服务器故障

如在执行 EXEC 时候关机等操作,导致事务执行失败。这时候需要分情况分析:

  1. 没有开启 AOF 日志,这种情况所有的数据都会丢失。谈不上原子性一说,更谈不上持久性。
  2. 若开启 AOF 日志,使用自带程序 redis-check-aof 删掉没提交的事务,重启 Redis。也是能保持原子性。
一致性

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。写入的数据必须完全符合所有的预设规则。这是一致性的含义。

一致性可以用上诉原子型的例子来进行总结即可:

  1. 在加入待执行事务队列时候就发生错误情况下,本次事务会被放弃。所以能保持数据的一致性。
  2. 在加入待执事务行队列时没有异常,但最后提交了发生异常。其中正确命令能够正常执行,错误的命令是不能执行的。所以能保证数据的一致性,但不能保证原子性。
  3. 在使用EXEC执行时候,发出异常,若开启 RDB 或 AOF,则谈不上分析一致性。
    若开启了 RDB 内存快照,由于在事务执行时候是不能进行 RDB 内存快照备份,所以使用 RDB 内存快照恢复也能保证一致性。
    若开启了 AOF 日志,若事务提交执行时候还没有写入 AOF ,则使用 AOF 恢复是能保证一致性;若事务只写入一部分到 AOF 文件,可以使用 redis-check-aof 清除事务中已经完成的操作,也是能保证一致性。
隔离性

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。这是隔离性的含义。

隔离性其实需要验证并发事务同时对其数据进行读写和修改的能力,所以这里分两个阶段进行分析,事务提交前和事务提交后。

事务提交前,也就是 EXEC 执行前。这种情况需要使用 WATCH 来监视:

# set watch_test
127.0.0.1:6379> set watch_test 1
OK
127.0.0.1:6379>

# 监视 watch_test
127.0.0.1:6379> watch watch_test
OK
# 开启事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr goods_a
QUEUED

# 执行之前在其他的客户端 进行 set watch_test 2 操作
127.0.0.1:6379> exec
(nil)          # return nil 代表事务被放弃回滚了
127.0.0.1:6379> get goods_a
"10"  # 还是之前的值
127.0.0.1:6379> get watch_test
"2"   # 被设置成功的
127.0.0.1:6379>

结论:保证了隔离性。

若不使用 WATCH 来监视,是不能保证隔离性的:

127.0.0.1:6379> get goods_b
"10"
127.0.0.1:6379> multi
OK

# 以下是另一个客户端执行的

127.0.0.1:6379> decr goods_b
(integer) 9
127.0.0.1:6379>

# 以上是另一个客户端执行的

127.0.0.1:6379> decr goods_b
QUEUED
127.0.0.1:6379> exec
1) (integer) 8
127.0.0.1:6379> get goods_b
"8"    # 可以看到 goods_b 被减了两次。
127.0.0.1:6379>

结论:不使用 WATCH 是不能保证隔离性的。

事务提交后 也就是 EXEC 执行后,Redis 是单线程执行命令(不要和 IO 多路复用混淆),所以是能保证隔离性。

持久性

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。这是持久性的含义。

Redis 是内存数据库,所以像停电、关机等情况 ARM 的数据是会丢失的。所以只能靠 AOF 和 RDB 来实现数据的持久化。

使用 RDB 内存快照下,若事务执行完还没及时进行备份。机器发生宕机,那这种情况是不能保证持久性的。

使用 AOF 下机器发生宕机,no、everysec 、always 三种写回策略都会一定程度丢失数据,也是不能保证持久性的。

总结

  1. Redis 能在部分情况下保证事务原子性。
  2. Redis 在配合 WATCH 命令下能保证事务隔离性。
  3. Redis 保证事务的一致性。
  4. Redis 不能保证事务的持久性。