事务管理
特性
局限性很大,不能实现真正意义上的事务
redis的事务本质是命令队列,依次执行,不具备原子性和隔离性
即事务中如果某操作出错,之前的操作并不会回滚,之后的操作也不会终止
它只能保证事务执行时其他命令不能插队(由单线程保证)
常见用途:批量操作,伪乐观锁(不能从根本上避免插队,只适合不关注原值的场景)
实现多指令的原子性应借助Lua脚本
MULTI|EXEC|DISCARD
127.0.0.1:6379> SELECT 9 OK # 开启事务 127.0.0.1:6379[9]> MULTI OK # 操作一一入队 127.0.0.1:6379[9]> SET k1 v1 QUEUED 127.0.0.1:6379[9]> SET k2 v2 QUEUED 127.0.0.1:6379[9]> SET k3 v3 QUEUED 127.0.0.1:6379[9]> GET k1 QUEUED # 把入队的操作一次性执行完毕 127.0.0.1:6379[9]> EXEC 1) OK 2) OK 3) OK 4) "v1" # 开启事务 127.0.0.1:6379[9]> MULTI OK # 操作入队 127.0.0.1:6379[9]> SET k4 v1 QUEUED 127.0.0.1:6379[9]> LPUSH ls1 e1 e2 e3 QUEUED # 放弃事务 127.0.0.1:6379[9]> DISCARD OK # 查看结果 127.0.0.1:6379[9]> KEYS * 1) "k3" 2) "k1" 3) "k2" # 以下操作展示了顺序性 127.0.0.1:6379[9]> FLUSHDB OK 127.0.0.1:6379[9]> MULTI OK 127.0.0.1:6379[9]> SET k1 v1 QUEUED 127.0.0.1:6379[9]> GET k2 QUEUED 127.0.0.1:6379[9]> SET k2 v2 QUEUED 127.0.0.1:6379[9]> EXEC 1) OK 2) (nil) 3) OK # 执行时错误不回滚、不终止 # 127.0.0.1:6379[9]> FLUSHDB OK 127.0.0.1:6379[9]> MULTI OK 127.0.0.1:6379[9]> SET k1 v1 QUEUED 127.0.0.1:6379[9]> INCR k1 QUEUED 127.0.0.1:6379[9]> SET k2 v2 QUEUED # 执行结果,错误被跳过 127.0.0.1:6379[9]> EXEC 1) OK 2) (error) ERR value is not an integer or out of range 3) OK # 执行前错误(类似编译错误)事务被强制取消 127.0.0.1:6379[9]> MULTI OK 127.0.0.1:6379[9]> SETN k1 v1 (error) ERR unknown command 'SETN' 127.0.0.1:6379[9]> SET k2 v2 QUEUED # 执行结果,事务被取消 127.0.0.1:6379[9]> EXEC (error) EXECABORT Transaction discarded because of previous errors.
WATCH|UNWACH
# 监视或取消监视必须在开启事务之前 # 即:即使在事务中首先进行了取消监视,如果事务执行前监视的键被修改,事务依然会执行失败 # 这实现了对redis数据库本身的乐观锁,保证了在监视操作和事务操作之间不会被其他操作插队 # 但请注意:一般事务操作前势必要先获取键(根据值决定事务逻辑或是否进行执行),而这个获取键和对其设置监视的过程中仍然存在插队的可能性,因此监视并不能完全实现真正的乐观锁 # 不关注原值,即不需要根据原值来决定事务逻辑或是否执行的场景,监视是完全可行的,例如计数器,标记设置(频率限制、库存扣减等就不适用)等 127.0.0.1:6379[9]> FLUSHDB OK # 设置两个键 127.0.0.1:6379[9]> SET total 100 OK 127.0.0.1:6379[9]> SET sale 0 OK # 开始监视 127.0.0.1:6379[9]> WATCH total sale OK # 在执行前另开客户端对监视的键的值进行操作,例如 127.0.0.1:6379[9]> INCRBY total 100 (integer) 200 # 开启事务 127.0.0.1:6379[9]> MULTI OK # 操作入队 127.0.0.1:6379[9]> DECRBY total 10 QUEUED 127.0.0.1:6379[9]> INCRBY sale 10 QUEUED # 执行事务,但发现监视的键被改变,执行失败 127.0.0.1:6379[9]> EXEC (nil) # 一旦事务 执行或取消 之后,会自动取消原来监视的键 # 重新执行 127.0.0.1:6379[9]> MULTI OK 127.0.0.1:6379[9]> INCRBY sale 10 QUEUED 127.0.0.1:6379[9]> DECRBY total 10 QUEUED # 执行成功 127.0.0.1:6379[9]> EXEC 1) (integer) 10 2) (integer) 190 # 事务中UNWATCH 127.0.0.1:6379[9]> FLUSHDB OK # 设置两个键 127.0.0.1:6379[9]> SET total 100 OK 127.0.0.1:6379[9]> SET sale 0 OK # 开始监视 127.0.0.1:6379[9]> WATCH total sale OK # 插队修改了被监视的键 127.0.0.1:6379[9]> INCRBY total 100 (integer) 200 # 开启事务 127.0.0.1:6379[9]> MULTI OK # 取消监视,此时取消无效 127.0.0.1:6379[9]> UNWATCH QUEUED 127.0.0.1:6379[9]> DECRBY total 10 QUEUED 127.0.0.1:6379[9]> INCRBY sale 10 QUEUED # 执行失败 127.0.0.1:6379[9]> EXEC (nil) # 事务前的UNWATCH 127.0.0.1:6379[9]> FLUSHDB OK 127.0.0.1:6379[9]> SET total 100 OK 127.0.0.1:6379[9]> SET sale 0 OK # 监视 127.0.0.1:6379[9]> WATCH total sale OK # 插队 127.0.0.1:6379[9]> INCRBY total 100 (integer) 200 # 取消监视 127.0.0.1:6379[9]> UNWATCH OK # 或者在这插队 # 127.0.0.1:6379[9]> INCRBY total 100 # (integer) 200 # 开启事务 127.0.0.1:6379[9]> MULTI OK 127.0.0.1:6379[9]> DECRBY total 10 QUEUED 127.0.0.1:6379[9]> INCRBY sale 10 QUEUED 127.0.0.1:6379[9]> INCRBY total 100 (integer) 200 # 执行成功 127.0.0.1:6379[9]> EXEC 1) (integer) 190 2) (integer) 10