Redis 事务浅析

事务

概述

事务是并发控制的基本单位。所谓的事务,它是指一个操作序列,这些操作序列要不都执行,要么都不执行,它是一个不可分割的工作单位。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

事务的提出主要是为了解决并发情况下保持数据一致性的问题。

特征

事务具有以下4个特征,简称ACID:
● Atomic(原子性):事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
● Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
● Isolation(隔离性):事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。
● Durability(持久性):事务结束后,事务处理的结果必须能够得到固化。

Redis事务

概述

Redis中的事务是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。

事务的原理是先将一个属于事务的命令发送给Redis,然后再让Redis依次执行这些命令。

redis> MULTI
OK
redis> set name Yann
QUEUED
redis> append name yezixin
QUEUED
redis> EXEC
1) OK
2) (integer) 11

首先使用MULTI命令告诉Redis下面命令将是一个事务,Redis应答OK。 发送set和append命令之后,Redis返回QUEUED表示命令已经进入等待执行的事务队列中了。最后使用EXEC命令来让等待执行的事务队列中的所有命令按照发送顺序依次发送。返回值是这些命令的返回值组成的列表。

错误处理

语法错误,指命令不存在或者命令参数个数不对。

redis> MULTI
OK
redis> set name
(error) ERR wrong number of arguments for 'set' command
redis> errorset name
(error) ERR unknown command 'errorset'
redis> exec
(error) EXECABORT Transaction discarded because of previous errors.

只要Redis事务中存在语法错误,该事务将不会执行。

运行错误,指在命令执行时出现的错误,比如使用散列类型的命令操作字符串类型的键,这种错误在实际执行之前Redis是无法发现的。那么当出现运行错误时Redis是怎么处理的呢?

redis> MULTI
OK
redis> hset name first-name ye
QUEUED
redis> set name yezixin
QUEUED
redis> get name
QUEUED
redis> exec
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) OK
3) "yezixin"

从结果可知道,虽然hset命令出现运行错误,但是set,get命令还是执行了。

缺陷

遇到有查询的命令穿插在事务中,并不会返回结果

有上面的分析我们知道,Redis事务中的命令是进入了等待执行的事务队列中,在事务执行之前并不会返回结果,只是返回QUEUED。

这样会导致什么问题? 如果后序的更新操作需要依赖于前面的查询指令,那么Redis事务便无法有效的完成任务。

redis> MULTI
OK
redis> get age
QUEUED
业务逻辑...
redis> set age ccc
QUEUED
redis> exec
1) "18"
2) OK

事务中的每条命令都与redis服务器进行了一次网络交互

Redis事务指定开始后,执行一个命令都是都返回QUEUED, 那么这个入队操作是在客户端实现还是Redis服务端实现的?

在Redis.c中这么一段源码

int processCommand(redisClient *c) {
/* Exec the command */
if (c->flags & REDIS_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
queueMultiCommand(c); // 将事务中的命令都放入到队列中,然后返回"QUEUED"
addReply(c,shared.queued);
} else {
if (server.vm_enabled && server.vm_max_threads > 0 &&
blockClientOnSwappedKeys(c)) return REDIS_ERR;
//调用该命令函数来处理命令
call(c);
}
return REDIS_OK;
}

由此可知,入队操作是在服务端执行的,这意味着一个事务需要客户端和服务端执行多次网络交互,明明是一个事务中的n条指令却需要通过多次网络交互,有些浪费。

事务特征

Redis事务只保证了一致性和隔离性 , 这倒也不能说是Redis的缺陷,Redis这么做是为了保持简单

原子性

从上面的Redis对事务的运行错误的处理我们知道,Redis事务在执行过程中遇到错误,并不会回滚,而是继续执行命令, 违反了原子性。

Redis的事务并没有关系数据库事务提供的回滚功能, 不过这也使得Redis在事务上可以保持简洁和快速。

事务回滚是指将一个事务已经完成的对数据库的修改操作撤销。

持久性

Redis事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定:

  • 在单纯的内存模式下,事务肯定是不持久的。
  • 在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的。
  • 在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。
  • 其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。

隔离性和一致性

Redis事务在执行的过程中,不会处理其它命令,而是等所有命令都执行完后,再处理其它命令(满足隔离性)
Redis事务在执行过程中发生错误或进程被终结,都能保证数据的一致性;

解决方案

● WATCH命令实现更新操作中的查询

如果不用事务,那么我们更新键值实现增1的流程是这样

redis> get age
"18"
redis> set age 19
OK

但是上述的做法会出现竞态条件, 简单讲就是在获得键值18后,age有可能会被其他客户端修改,导致数据出现不一致性。

竞态条件(race condition)是一个在设备或者系统试图同时执行两个操作的时候出现的不希望的状况,但是由于设备和系统的自然特性,为了正确地执行,操作必须按照合适顺序进行。 

如果我们用事务来做呢?但是事务中的每个命令的执行结果都是最后一起返回的,所以无法将前一条命令的结果作为下命令的参数,即在执行SET命令无法获得GET命令的返回值。

为了实现增1操作,我们需要能够获得键值后保证该键值不被其他客户端修改,直到操作执行完成之后才能修改该键值,这样也能防止竞态条件。

WATCH 命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令。

redis> watch age
OK
redis> set age 19
OK
redis> MULTI
OK
redis> set age 20
QUEUED
redis> exec
(nil)
redis> get age
"19"

exec执行结果为nil, 说明事务执行失败,因为watch age之后修改了age的键值导致后面的事务执行失败

WATCH命令的作用只是监控键值被修改后阻止后一个事务执行,并不能阻止其他客户端不修改这一键值。所以我们需要再EXEC执行之后重新执行整个流程

所以我们就可以这样来处理:将查询业务逻辑提前,所有的Redis查询逻辑放在事务之外, 并对需要进行更新的键值执行WATCH监控, 当事务执行失败重新执行这个流程。

我们可以通过UNWATCH 命令取消WATCH对所有键的监控, 来保证下一个事务执行不会受到影响。

坚持原创技术分享