Redis的事务

虽然我们通常把Redis作为缓存中间件,但是Redis对数据库事务还是提供了简单的支持。数据库事务的目的是为了保证操作能够原子的完成,不同事务之间的操作能够保证数据的一致性,事务之间相互隔离,且提交了的数据库操作能够持久生效,也就是我们常说的数据库ACID特性。那么Redis的事务是否满足我们常说的ACID特性呢?

Redis事务的用法

Redis的事务是通过一组命令完成,这组命令主要是:

  1. multi: 声明事务开始
  2. exec: 执行事务队列中的操作,也就是commit操作
  3. discard: 放弃事务执行
  4. watch: 监视一个key,如果在事务提交(exec)之前key发生了修改操作,那么事务提交会失败

下面是一个常见的事务操作流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> set lock 1
OK
127.0.0.1:6379> watch lock
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello world
QUEUED
127.0.0.1:6379> set nick name
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> mget hello nick
1) "world"
2) "name"

在事务开始之前,通过watch监视lock这个key,通过multi告诉服务器,我要开始执行一个事务了。其后的命令提交后,Redis服务器不会直接执行,而是加入一个FIFO的队列,并返回QUEUED给客户端。事务准备完毕后,发送exec命令提交事务。服务器收到exec命令后,将事务队列中的命令按照先后顺序出队并执行,并把每个命令的运行结果收集起来一起返回给客户端。

watch的作用
下面让我们来看看watch之后,被watch的key发生修改的情况下,结果是什么。

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> watch lock
OK
127.0.0.1:6379> set lock 2
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set dogname duoduo
QUEUED
127.0.0.1:6379> set dogage 1
QUEUED
127.0.0.1:6379> exec
(nil)

可以看到,如果watch的key在事务执行exec之前发生了修改,那么服务器收到客户端发来的exec命令后,会不会允许,而是返回一个null。客户单如果只选exec收到null,也就知道事务提交失败了。

执行错误的语句
由于事务期间,服务器只是把指令入队,而不会执行,服务能够知道命令格式是否正确,但是执行能不能够成功,此时并不知道,只有到执行过后,才会知道执行的结果。
下面,看看如果事务期间的某个命令格式不合法时,服务器的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> watch lock
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello world
QUEUED
127.0.0.1:6379> nonexist command
(error) ERR unknown command 'nonexist'
127.0.0.1:6379> incr hello
QUEUED
127.0.0.1:6379> set hello changed
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

这里在事务期间执行了一个错误的语句,命令nonexist并不存在,最后我们执行事务的时候,服务器并没有执行,而是提示事务由于前置错误已经被放弃。

再看看下面这种错误情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> watch lock
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello duoduohaha
QUEUED
127.0.0.1:6379> incr hello
QUEUED
127.0.0.1:6379> set weather sunny
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get weather
"sunny

这里,我们在事务期间执行了三条命令,其中第二条对一个非整数类型进行自增操作,这是会失败的。当我们提交事务之后,服务器返回的结果显示,第一个和第三个命令都成功执行,第二个命令错了。事务结束之后,我们可以验证第一条和第三条命令都成功执行了。所以说,这里Redis并不支持在其中一条命令发生错误的情况下回滚整个事务。Redis作者的说法时,事务期间不正确的操作主要发生在开发环境,有问题都是程序猿的锅。

Discard用法
接下来,我们看看discard的作用:

1
2
3
4
5
6
7
8
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello world
QUEUED
127.0.0.1:6379> disard
(error) ERR unknown command 'disard'
127.0.0.1:6379> discard
OK

discard的作用其实就是通知服务器,我不想执行这个事务了,你把我的事务队列清了吧。

Redis事务的实现

Redis的事务是通过服务器为每个client维护一个FIFO的事务队列,当客户端提交事务的时候,将FIFO队列中的请求挨个pop出来处理。
watch的实现则是Redis的每个数据库都维护一个watchded_keys字典,key是被WATCH的键,value则是一个链表,链表中的每个节点都是watch了该key的客户端。当一个客户端执行了watch之后,服务器就是将客户端信息添加到该key对应的客户端链表中。当一个会对数据库发生修改的命令被执行后,服务器会判断该key是否存在于watched_keys中,如果是,那么久将所有挂在该key下的客户端的REDIS_DIRTY_CAS标志位打开。当客户端执行exec时,会判断该标志位状态,如果被打开,那么会拒绝事务提交操作。


Redis的事务完美吗?

相信通过前面的讨论,大家心中都有一个问题,Redis的事务是否能够满足ACID特性?下面我们一一讨论。

原子性

个人认为Redis的事务不具备原子性,原因是Redis的事务不支持回滚操作,同时Redis的日志是先执行修改,后写日志(AOF),并且Redis的AOF是异步的,那么我们不能保证AOF刷新文件的时机,如果Redis在事务期间挂掉了,很可能出现某些命令得到了执行,而后面没有执行的情况。

一致性

这里,我认为Redis事务同样不具备一致性,理由和反驳原子性的理由一样,Redis在执行事务期间宕机,重写启起来通过AOF恢复的数据可能是事务执行到一半的状态,那么这种情况我认为Redis数据处于一种不一致状态。

隔离性

由于Redis的线程模型是多路复用单线程模式,天然具有隔离线,不同客户端之间的事务不可能相互打扰,事实上依次只有一个客户端的事务处于exec状态。所以,Redis的事务具有隔离性。

持久性

虽然Redis提供了各种的持久化策略,但是由于Redis是先写数据后写日志的机制,所以持久性很难得到保障,所以不要期望Redis像Mysql这些数据库一样持久(火车污污污污污污)

总结

Redis为我们提供简单的事务支持,但是事务并不是Redis的强项,且ACID中最有把握的只有隔离性,所以对于事务要求很严格场景不推荐使用Redis。

Scala函数循环 用JMH来验证伪共享

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×