所谓事务是指不可分割的最小工作单位,要么全部执行成功,要么全部不执行
redis 事务从开始到结束需要经过三个阶段:
- 事务开始:multi 命令开启一个事务
- 命令入队:watch、multi、discard、exec 会被立即执行、其他命令依次入队并返回客户端 queued
- 事务执行:exec 命令开始执行事务
exec:执行事务
discard:关闭事务
watch:加分布式锁
multi:开启事务
queued:命令入队成功
一般情况下不建议使用 redis 事务,因为它不支持回滚,举个例子:
multi
incr a
incr b
incr c
exec
此时即使键 a 对应字符串类型,incr a 执行失败,incr b 和 incr c 一样也会执行,也就是说即使执行出错也不会回滚
那么是不是就说 redis 事务不具备原子性呢?答案是否定的,redis 事务实际是能保证原子性的,先看原子性的定义:
原子性:要么全部执行,要么全部不执行
由于不回滚,redis 事务一旦执行肯定全部执行,在以下两种情况会全部不执行:
- 命令本身就是错误的,redis 执行事务前会检查事务命令是否正确,如果存在错误命令就不执行
- watch 监视的键值对被修改
watch 实际就是 CAS 乐观锁。调用 watch 指令的客户端会关注指定键值对,如果在关注期间有其他客户端修改了这个键值对,那么调用 watch 指令的客户端下一次事务操作执行失败
从这里也就可以看出,redis 事务是要么全部执行,要么全部不执行,满足原子性。和 mysql 事务相比,区别在于 mysql 事务执行错误后会全部回滚,但 redis 只检查指令是否正确,指令执行错误不会回滚,这里不回滚的原因主要集中在以下两点:
- 指令执行出错实际是因为开发问题,这种问题应该再开发阶段解决而不是线上
- 由于事务无需回滚,redis 整体事务逻辑简单且快速,保证高效
最后简单聊聊为什么 redis 需要引入事务:之前提到,redis 由于单线程操作,没有多线程并发操作同一块内存,因此天然线程安全。这句话是没错,但如果多个客户端连接同一个 redis,并且业务代码中没有添加同步判断,仍有可能存在异常情况:
举个例子,客户 a、b 分别从客户端 A、客户端 B 购买同一件商品,商品库存保存在 redis 中并且仅剩一件,此时可能产生如下结果:
- 客户端 A 判断库存大于 0 返回 true
- 客户端 B 判断库存大于 0 返回 true
- 客户 a 从客户端 A 下单成功,商品库存变为 0
- 客户 b 由于之前判断有商品,同样下单成功,商品库存变为 -1
出现超卖问题,此时就算使用事务,也无法解决问题。一般需要事务配合 watch 指令一起解决
客户端 B 由于 watch 指令发现库存对应 redis 键值对被修改,事务执行失败,不执行下单逻辑