Redis应用

Redis高级特性——事务

2021-02-20  本文已影响0人  李小磊_0867

事务

事务在业务开发过程中是比较重要的一环,小到任务执行,更新状态;大到购物支付及定单发货流程、转换流程等,都需要分布式事务去解决一些数据一致的问题。在Redis中,也会遇到相似的情况,比如在实现一个用户收藏功能时,需要显示资源的总收藏量,同时完成用户收藏列表的更新,此时希望两个指令都能完成,不至于出现完成了收藏量增加,而用户收藏列表添加失败的情况。

Redis的事务同Java的事务有相似的地方,也有很大的不同,以下几点为Redis的事务主要特点,在运用Redis事务时,需要牢记其特点,不至于和Java的事务概念混用,导致业务出现问题。

MULTI/EXEC

MULTI:开启一个事务

MULTI与EXEC之间的所有指令都会加入到对列进行缓存,等待执行

EXEC:执行事务中的指令

Redis的事务执行流程可以分为三个阶段

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"

在列出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
上一篇下一篇

猜你喜欢

热点阅读