Redis基础之事务

来源:互联网 发布:java中demo是什么意思 编辑:程序博客网 时间:2024/05/29 10:59

Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。事务的应用非常普遍,如银行转账过程中A给B汇款,首先系统从A账户中将钱划走,然后在B的账户中增加相应的金额。这两个步骤必须属于同一事务,要么全执行,要么都不执行。


1. 基本原理(MULTI)

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

127.0.0.1:6379> MULTIOK127.0.0.1:6379> SADD "user:1:following" 2QUEUED127.0.0.1:6379> SADD "user:2:followers" 1QUEUED127.0.0.1:6379> EXEC1) (integer) 12) (integer) 1
上面的代码演示了事务的使用方式。首先是MULTI命令告诉Redis:"下面我发给你的命令属于同一个事务,你先不要执行,而是把他们暂时存起来。”Redis回答“OK”。

而后我们发送了两个SADD命令来实现关注和被关注操作,可以看到Redis遵守了承若,没有执行这些命令,而是返回了QUEUED,表示这两条命令已经进入等待执行的事务队列了。

当把所有要在同一个事务中执行的命令都发给Redis后,我们使用EXEC命令告诉Redis将等待执行的事务队列中的所有命令按照发送顺序依次执行。EXEC命令的返回值就是这些命令的返回值组成的列表,返回值顺序和命令的顺序相同。

Redis保证一个事务中的所有命令要么执行,要么都不执行。如果客户端发送了EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就会都被执行,即使此后客户端断线也没关系,因为Redis已经记录了所有要执行的命令。

除此之外,Redis的事务还能保证一个事务的命令依次执行而不被他们的命令插入。试想客户端A需要执行几条命令,同时客户端B发送了一条命令,如果不使用事务,则客户端B的命令可能会插入到客户端A的几条命令中执行,如果不希望发生这种情况,也可以使用事务。

2. 取消事务(DISCARD)

可以调用DISCARD来取消一个事务,discard命令其实就是清空事务的命令队列并退出事务上下文。
127.0.0.1:6379> get a"1"127.0.0.1:6379> get b"2"127.0.0.1:6379> MULTIOK127.0.0.1:6379> set a 3QUEUED127.0.0.1:6379> set b 4QUEUED127.0.0.1:6379> DISCARDOK127.0.0.1:6379> get a"1"127.0.0.1:6379> get b"2"
可以看到,a和b的初值分别为1、2,将set a 和set b 加入到一个事务后,并没有执行,而是用DISCARD将其取消掉,最后a、b的值并没有改变。

3. 错误处理

如果一个事务中的某个命令出错了会发生什么情况?要弄清楚这个问题,首先需要知道什么原因导致了命令执行错误。
  • 语法错误
语法错误是指错误命令不存在或者命令参数的个数不对。比如:
127.0.0.1:6379> MULTIOK127.0.0.1:6379> SET key valueQUEUED127.0.0.1:6379> SET key(error) ERR wrong number of arguments for 'set' command127.0.0.1:6379> ERRORCOMMAND key(error) ERR unknown command 'ERRORCOMMAND'127.0.0.1:6379> EXEC(error) EXECABORT Transaction discarded because of previous errors.
跟在MULTI命令后执行了3个命令:一个正确的命令,成功加入了事务队列;其余两个都有语法错误。而之哟啊有一个命令语法错误,执行EXEC命令后Redis就会直接返回错误,而语法正确的命令也不会执行。(Redis 2.6.5之前的版本会忽略有语法错误的命令,执行事务中其他正确的命令)
  • 运行错误
运行错误是指在命令执行时出现的错误,比如使用散列类型的命令操作集合类的键,这种错误在实际执行之前Redis无法发现,所以在事务里这样的命令会被Redis接受并执行。如果事务里的一条命令出现了执行错误,事务里的其他的命令依然会继续执行(包括错误命令之后的命令)
127.0.0.1:6379> SET key 1QUEUED127.0.0.1:6379> SADD key 2QUEUED127.0.0.1:6379> SET key 3QUEUED127.0.0.1:6379> EXEC1) OK2) (error) WRONGTYPE Operation against a key holding the wrong kind of value3) OK
可见,虽然SADD key 2出现了错误,但是SET key 3依然执行了。而Redis的事务没有关系型数据库事务提供的回滚(rollback)功能。为此开发者必须在事务执行出错后收拾剩下的摊子。为什么Redis不支持错误回滚?以下是这种做法的优点:
  1. 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
  2. Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

4. WATCH命令

在一个事务中,只有当所有命令都依次执行完后才能得到每个结果的返回值,可能有些情况下需要先获得一条命令的返回值,然后再根据这个值执行下一条命令。例如,使用GET和SET两个命令来自己实现INCR函数,伪代码为:
def incr($key)    $value = GET $key    if not $value        $value = 0    $value = $value+1    SET $key,$value    return $value
如果两个客户端同时连接到Redis时有可能出现竞态条件(race condition)。例如,有两个客户端A和B都要执行incr函数,他们可能同时执行到了函数的第二行,得到某个值,比如1,然后+1并用SET赋值,最终的结果为2,而非预想中的3.
能够用事务来解决这一竞态条件呢?因为事务中的每个命令的执行结果都是最后一起返回的,所以无法将前一条命令的结果作为下一条命令的参数,即在执行SET命令时无法获得GET命令的返回值,也就无法做到增1的功能了。
换一种思路,在GET获得键值后保证该键值不被其他客户端修改,直到函数完成后才允许其他客户端修改该键值,这样也可以防止竞态条件。而这正是WATCH的功能。WATCH命令可以监控一个或者多个键,一旦其中有一个键被修改(或删除),之后的事务就不执行。监控一直持续到EXEC命令。(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的值),如:
127.0.0.1:6379> SET key 1OK127.0.0.1:6379> WATCH keyOK127.0.0.1:6379> SET key 2OK127.0.0.1:6379> MULTIOK127.0.0.1:6379> SET key 3QUEUED127.0.0.1:6379> EXEC(nil)127.0.0.1:6379> GET key"2"
上例中执行WATCH命令后,事务执行前修改了key的值(SET key 2),所以最后导致了事务执行的失败。那么,学会了WATCH命令后就可以通过事务自己实现incr函数了,伪代码为:
def inc($key)    WATCH $key    $value = GET $key    if not $value    $value = 0    $value = $value + 1    MULTI    SET $key,$value    result = EXEC    return result(0)
因为EXEC命令返回值是多行字符串类型,所以代码使用result[0]来获得其中的第一个结果。
提示:由于WATCH命令的作用只是当被监控的键值被修改后阻止后一个事务的执行,而不能保证其他客户端不修改这一键值,所以需要在EXEC执行失败后重新执行这个函数。
执行EXEC命令后会取消对所有键的监控,如果不行执行事务中的命令,也可以使用UNWATCH命令来取消监控。比如,要实现hsetxx函数,作用于HSETNX命令类似,只不过仅当字段值存在时才赋值。为了必变竞态条件,使用事务来完成这一功能。
def hsetxx($key, $field, $value)    WATCH $key    $isFieldExists = HEXISTS $key, $field    if $isFieldExists        MULTI        HSET $key, $field, $value        EXEC    else        UNWATCH    return $isFieldExists
在代码中,会判断赋值字段是否存在,如果字段不存在的话就不执行事务中的命令,但需要使用UNWATCH命令来保证下一个事务的执行不受影响。





0 0
原创粉丝点击