事务
事务在业务开发过程中是比较重要的一环,小到任务执行,更新状态;大到购物支付及定单发货流程、转换流程等,都需要分布式事务去解决一些数据一致的问题。在Redis中,也会遇到相似的情况,比如在实现一个用户收藏功能时,需要显示资源的总收藏量,同时完成用户收藏列表的更新,此时希望两个指令都能完成,不至于出现完成了收藏量增加,而用户收藏列表添加失败的情况。
Redis的事务同Java的事务有相似的地方,也有很大的不同,以下几点为Redis的事务主要特点,在运用Redis事务时,需要牢记其特点,不至于和Java的事务概念混用,导致业务出现问题。
- Redis保证事务中的所有指令要么都被执行,要么都不被执行;如果用户已完整创建事务,发送了执行事务指令后,即使用户与Redis连接被断开,也能保证事务都被执行
- Redis事务通过
MULTI
指令标识开始,之后的所有指令都被放入队列进行缓存,并不会被执行,只有发送EXEC
后,事务才会被执行 - Redis事务开始执行后,能够保证事务内的指令按创建顺序依次执行,并且在执行过程中,能够保证不被其他客户端的指令插入到事务内执行
- Redis的事务不是原子操作,当事务中某一条指令执行失败,其之前已执行的指令不会被回滚(同Java事务区别),该出错指令之后的指令,仍然会被执行。如果要实现事务失败回滚操作,业务系统只能接受Redis事务错误信息,得到错误信息后,自行进行数据恢复操作
- Redis事务中,如果某条指令错误,如指令本身使用方式错误,或者指令不存在,即在Redis指令语法检查阶段出现错误,那么在执行EXEC指令后,所有的指令都不会被执行
MULTI/EXEC
MULTI:开启一个事务
MULTI与EXEC之间的所有指令都会加入到对列进行缓存,等待执行
EXEC:执行事务中的指令
Redis的事务执行流程可以分为三个阶段
- MULTI标记一个事务的开始
- 指令加入到事务队列中
- EXEC开始执行事务,并返回执行结果
EXEC的返回结果,就是事务队列中所有指令的执行结果,以队列的形式返回,并且队列的顺序,与事务队列指令的顺序一一对应。
# step 1
SET account:a 120
SET account:b 100
# step 2
MULTI
# OK
# step 3
DECRBY account:b 30
# QUEUED
INCRBY account:a 30
# QUEUED
# step 4
EXEC
# 1) (integer) 70
# 2) (integer) 150
GET account:a
# "150"
GET account:b
# "70"
- step 1:首先构建了两个账户,分别存款为a账户120,b账户100
- step 2:要进行账号b给账号a转账30的操作,使用事务保证b账号转出、a账号转入指令顺序都成功执行
- step 3:从账号b转出30,指令被加入到缓存队列中,对账号a转入30,指令同样被加入到缓存队列,此时指令都没有被执行
- step 4:使用EXEC指令执行事务队列中的指令,此时b账号转出指令先执行,该指令成功减额后,返回b账号的余额;a账号转入指令接着执行,该指令成功后返回a账号加额后的余额。这两条指令的执行顺序与创建指令的顺序完全相同,EXEC将指令返回结果按同样的顺序加入到一个新的列表中,并将结果列表返回,分别显示了账号b、a的余额。之后进行检查a、b账号的数据,同事务执行的结果完全一致。
在列出Redis事务特点是,有两点都是跟错误有关的,分别是语法检查错误,此时只要有一条指令错误,所有的指令都不会被执行;如果不是语法错误,如指令操作类型错误,这种形式的错误,不会被执行,但其余的指令都将被执行,无法回滚。
语法错误的不执行事务示例,仍然使用上例中的转账后的账户信息,a=150,b=70
MULTI
# OK
DECRBY account:a 50 # 从a账号转出50
# QUEUED
INCR account:b 50 # 向b账号转入50,指令INCR使用错误
# ERR wrong number of arguments for 'incr' command
INCRBY account:b 50
# QUEUED
EXEC # 语法检查错误,此时所有的任务列表指令都不会被执行,EXEC后,指令列表被丢弃
# EXECABORT Transaction discarded because of previous errors.
GET account:a
# "150"
GET account:b
# "70"
上述指令中由于INCR指令使用错误,立即报出了错误信息,之后修正指令,并执行了EXEC,之后返回了事务被丢弃的错误信息。查询账户a、b发现数据未发生变化,这说明当遇到语法错误,或指令不存在之类的错误,事务指令将被丢弃,所有指令都不执行。
在运行时,事务中的某些指令发生错误,此时正确的指令都将被执行,只有错误指令返回错误信息。由于Redis事务不具备回滚功能,因此这种情况的错误是比较麻烦的,如果数据比较重要,需要业务自己恢复数据。
MULTI
# OK
DECRBY account:a 50 # 从a账号转出50
# QUEUED
INCRBY account:b 50.0 # 向b账号转入50,参数必须是整数,但错输入浮点数
# QUEUED
EXEC # 此时指令从a账户转账成功,但向b账户入账失败
# 1) (integer) 100
# 2) (error) ERR value is not an integer or out of range
GET account:a
# "100"
GET account:b # 整个账号体系丢失了50
# "70"
一旦发生了这种错误,不容易发现,且只能执行后,根据返回的数据列表中的错误信息校验到,如果是上述这种转账业务,一旦一个指令发生错误,需要业务自己实现对a账户的复原操作。
WATCH/UNWATCH
WATCH key [key ...]
UNWATCH
WATCH指令用于监控一个或多个键,如果被监控的键在事务执行前,被其他命令改变了,则事务不执行。
UNWATCH指令用于取消所有的键监控。
被监控的key,在事务EXEC后,将被释放;
如果执行了UNWATCH,则所有的被监控键都将释放。
# 恢复账号a为150
GET account:a
# "100"
WATCH account:a # 监控账号a
SET account:a 150
# OK
MULTI
# OK
INCRBY account:a 20 # 对账号a增加20,但是由于监控账号a之后,又改变了账号的总额,因此该事务将不被执行
# QUEUED
EXEC
# nil
GET account:a
# "150"
上述使用WATCH监控账号a,在事务之前改变了该账号,导致事务不被执行,EXEC返回nil。
事务只能保证所有指令被打包,都被顺序执行,但无法解决竞争得状态。如下订单时必须具有库存,如果库存不存在时,则不能让用户在拍商品。以下示例采用了两个客户端来模拟用户下定单过程,在下订单时,需要减库存,同时减用户的帐户余额。库存数量为1,价值为10,账号分别为account:a,account:b
# 客户端A,在该客户端,采用事务实现用户下订单过程
SET product 1 # 库存
# OK
MULTI
# OK
DECR product
# QUEUED
DECRBY account:a 10
# QUEUED
# 此处等待客户端B下订单,客户端执行完成后,执行EXEC
EXEC # 此时库存被减为-1,用户的钱也被减
# 1) (integer) -1
# 2) (integer) 140
# 客户端B,登录到A-Redis
redis-cli -h 192.168.113.145 -p 6300
auth *******
# 直接拍商品,不使用事务
DECR product
# (integer) 0
DECRBY account:b 10
# (integer) 60
再次使用WATCH解决竞争关系,这里只是使用WATCH指令,不考虑实际场景,WATCH监控库存,当库存被别的客户端修改时,客户端A不在执行事务,并对库存恢复product=1,account:a=140,account:b=60
# 客户端A
WATCH product
# OK
MULTI
# OK
DECR product
# QUEUED
DECRBY account:a 10
# QUEUED
# 此处等待客户端B下订单,客户端执行完成后,执行EXEC
EXEC
# nil
GET product
# "0"
GET account:a
# "140"
# 客户端B
# 直接拍商品,不使用事务
DECR product
# (integer) 0
DECRBY account:b 10
# (integer) 50
以上两个客户端中,A对product进行了监控,并启用了拍商品的事务,EXEC指令等待客户端B执行完毕后,在执行;B直接减库存,减余额都成功;回到客户端A执行EXEC,因为在事务执行前,product键被客户端修改,因此客户端A事务不被执行,执行EXEC后,WATCH product也被释放。
UNWATCH指令用于释放所有键的监控。当使用WATCH时,会和MULTI/EXEC配合使用,当事务执行EXEC时,将释放被监控的键;如果有一段脚本,监控键后,对结果进行了判断,条件满足时,才执行事务,那么这段脚本将可能存在不是放key监控的情况,可能就会影响下一个执行脚本的客户端。此时可以使用UNWATCH在条件不满足的分支中释放所有key,可以解决这种问题。
WATCH key
local t = condition(key)
if t then
MULTI
command list
EXEC
else
UNWATCH -- 没有满足条件,不执行事务,因此业务发释放键,下一个客户端可能会有问题
end
DISCARD
DISCARD
DISCARD指令用于取消事务,一旦执行该指令,事务内的所有指令都将被放弃。
MULTI
# OK
DECRBY account:a 10
# QUEUED
DISCARD # 该指令执行后,整个事务都被取消了,即当前窗口内不存在事务,当执行EXEC时,将会报无事务错误
# OK
EXEC
# ERR EXEC without MULTI