介绍
在微博中,用户之间是关注和被关注的关系。如果要使用Redis存储这样的关系可以使用集合类型。思路是对每个用户使用两个集合类型键,分别名为user:userID:followers和user:userID:following,用来存储关注该用户的用户集合和该用户关注的用户集合。然后使用一个函数来实现关注操作,伪代码如下:
def follow($currentUser, $targetUser)SADD user:$currentUser:following, $targetUserSADD user:$targetUser:followers, $currentUser
如ID为1的用户A想关注ID为2的用户B,只需要执行follow(1, 2)即可。然而完成关注操作需要依次执行两条Redis命令,如果在第一条命令执行完后因为某种原因导致第二条命令没有执行,就会出现一 个奇怪的现象:A查看自己关注的用户列表时会发现其中有B,而B查看关注自己的用户列表时却没有A。针对这种情况,我们可以使用Redis事务来解决。
一、Redis的事务原理
Redis中的事务是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。事务的原理是先将属于一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。如实现用户A和用户B互相关注:
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) 1127.0.0.1:6379>
首先使用MULTI命令告诉Redis:下面发给你的命令属于同一个事务,你先不要执行,而是把它们暂时存起来。Redis回答:OK。而后我们发送了两个SADD命令来实现关注和被关注操作,可以看到Redis遵守了承诺, 没有执行这些命令,而是返回QUEUED表示这两条命令已经进入等待执行的事务队列中了。当把所有要在同一个事务中执行的命令都发给Redis后,我们使用EXEC命令告诉Redis将等待执行的事务队列中的所有命令按照发送顺序依次执行。EXEC命令的返回值就是这些命令的返回值组成的列表,返回值顺序和命令的顺序相同。
Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了 EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录 了所有要执行的命令。除此之外,Redis的事务还能保证一个事务内的命令依次执行而不被其他命令插入。
二、MULTI命令
MULTI命令会告诉Redis接下来开启一个事务,如果一个事务中的某个命令执行出错,Redis会怎样处理呢?这里需要区分是语法错误还是运行错误。
语法错误
语法错误指命令不存在或者命令参数的个数不对,如下:
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`, with args beginning with: `key`,127.0.0.1:6379> exec(error) EXECABORT Transaction discarded because of previous errors.127.0.0.1:6379>
跟在MULTI命令后执行了3个命令:一个是正确的命令,成功地加入事务队列;其余两个命令都有语法错误。而只要有一个命令有语法错误,执行EXEC命令后Redis就会直接返回错误, 连语法正确的命令也不会执行。
运行错误
运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前Redis是无法发现的,所以在事务里这样的命令是会被Redis接受并执行的。如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行(包括出错命令之后的命令),示例如下:
127.0.0.1:6379> multiOK127.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) OK127.0.0.1:6379> get key"3"127.0.0.1:6379>
可见虽然SADD key 2出现了错误,但是SET key 3依然执行了。Redis的事务没有提供回滚功能,所以我们必须在事务执行出错后自己处理相应的逻辑,比如将数据恢复到执行前的状态。
但是我们可以手动取消事务,以实现事务回滚功能,可以使用DISCARD命令:
127.0.0.1:6379> get key"3"127.0.0.1:6379> multiOK127.0.0.1:6379> set key 4QUEUED127.0.0.1:6379> set key 5QUEUED127.0.0.1:6379> discardOK127.0.0.1:6379> get key"3"
可以发现这次2个set key命令都没被执行。discard命令其实就是清空事务的命令队列并退出事务上下文,也就是我们常说的事务回滚。
三、WATCH命令
现在我们已经知道在一个事务中只有当所有命令都依次执行完后才能得到每个结果的返回值,在有些情况下我们需要先获得一条命令的返回值,然后再根据这个值执行下一条命令。比如我们可以使用GET和SET命令自己实现incr函数,首先通过GET命令获取到当前值,然后使用SET命令进行加1操作,但是这样会出现竞态条件,那么Redis事务可以处理这种情况吗?
答案是肯定的,我们可以换一种思路,只要GET获取到键值后保证该键值不被其他客户端修改,直到函数执行完后才允许其他客户端修改该键值,这样也可以防止竞态条件的产生。Redis就是通过WATCH命令实现这种思路的。
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不会执行。监控一直持续到EXEC命令执行完,如:
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"127.0.0.1:6379>
上面的例子中在执行WATCH命令后,事务执行前修改了key的值,所以最后事务中的命令SET key 3没有执行,EXEC命令返回空结果。WATCH命令就相当于对指定的key加了乐观锁。
WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以我们需要在EXEC执行失败后重新执行整个事务操作。执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令可以使用UNWATCH命令来取消监控。
四、Redis事务命令总结
MULTI
MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC
执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值nil。
DISCARD
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
WATCH
WATCH命令可以为Redis事务提供CAS行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
UNWATCH
取消键的监控,不执行事务命令,使事务失效。
推荐阅读

看完本文有收获?请转发分享给更多人
关注「并发编程之美」,一起交流Java学习心得




