文章目录
4. Redis 事务
原子性: 要么都成功,要么都失败
Redis 单条命令是保证原子性的,但是事务不保证原子性
Redis 类似大多数成熟的数据库系统一样,提供了事务机制。Redis的事务机制非常简单,它没有严格的事务模型,无法像关系型数据库一样保证操作的原子性。
Redis 事务最大的作用是保证多个指令的串行执行,它可以借助于Redis单线程读写的特性,保证Redis事务中的指令不会被事务外的指令打搅,不过要注意它不是原子性的。
Redis事务本质:一组命令的集合(如:我要先set 再 get,set,get这组命令就是事务)
Redis事务本质,可以将Redis的事务看成是一个队列,将一组命令进行“入队”,然后一起执行
一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行
Redis事务的三个特性:
单独的隔离操作
事务中的所有命令都会被序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
Redis 事务:
* 开启事务(MULTI)
* 命令入队(SET / GET / INCR .....)
* 执行事务(EXEC)
# 一个完整的事务案例:
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 命令入队(实际上并未执行)
QUEUED
127.0.0.1:6379(TX)> set k2 v2 # 命令入队(实际上并未执行)
QUEUED
127.0.0.1:6379(TX)> get k1 # 命令入队(实际上并未执行)
QUEUED
127.0.0.1:6379(TX)> EXEC # 输入 exec 执行事务,上面入队的命令才全部执行
1) OK
2) OK
3) "v1"
127.0.0.1:6379>
multi开启一个事务之后,所有指令都不执行,而是缓存到事务队列中,直到服务器接收到exec指令,才开始执行整个事务中的指令。事务全部指令执行完毕后,一次性返回全部的结果。
使用Redis事务,一个最需要注意的问题是,指令多,网络开销高;因此我们一定要结合管道pipeline一起使用,这样可以将多次网络io操作压缩成单次。
4.1 指令介绍
Redis事务相关的指令有五个,分别是MULTI、EXEC、DISCARD、WATCH、UNWATCH
指令 | 指令作用 | ** 返回值** |
---|---|---|
MULTI | 标记一个事务块的开始 | 总是返回 OK |
EXEC | 执行所有事务块内的命令 | 事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil,操作错误时,返回错误 |
DISCARD | 取消事务,放弃执行事务块内的所有命令,如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH | 总是返回 OK |
WATCH | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断 | 总是返回 OK |
UNWATCH | 取消 WATCH 命令对所有 key 的监视。如果在执行WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了 | 总是返回 OK |
4.2 MULTI(开启事务)
MULTI用于标记一个事务的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。MULTI指令总是返回OK。
# 示例:
127.0.0.1:6379> MULTI # 开启一个事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
4.3 EXEC(执行事务)
EXEC 用于执行所有事务块内的命令
假如某个(或某些) key 正处于 WATCH 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。
# 示例:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> EXEC # 执行事务
1) OK
2) OK
3) "v1"
127.0.0.1:6379>
错误示例:
编译型异常(代码有问题,命令有错),事务中所有的命令都不会执行
# 示例
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set ke2 # 错误的命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> EXEC # 执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # 所有的命令都不会被执行
(nil)
127.0.0.1:6379>
运行时异常,如果事务队列中存在语法性错误,那么在执行的时候其他的命令是可以正常执行的,错误命令会抛出异常
# 示例:
127.0.0.1:6379> set name tom
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR name # INCR 只能针对数字自增
QUEUED
127.0.0.1:6379(TX)> set age 10
QUEUED
127.0.0.1:6379(TX)> get name
QUEUED
127.0.0.1:6379(TX)> EXEC # 事务的第一条命令报错了,但是其他命令可以正常执行
1) (error) ERR value is not an integer or out of range
2) OK
3) "tom"
127.0.0.1:6379> get age
"10"
127.0.0.1:6379>
4.4 DISCARD(取消事务)
DISCARD用于取消事务,放弃执行事务块内的所有命令。如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH 。DISCARD指令总是返回OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD # 取消事务,事务队列中的命令都没有被执行
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379>
4.5 乐观锁、悲观锁
在处理事务问题是会遇到事务冲突,以及如何解决事务冲突,从而引入锁的概念
参考地址:点击查看
何为事务冲突
在电商场景中经常有的一个场景就是秒杀抢购。秒杀场景就是将拿出少量的某个商品以特价的方式,在极短的时间内销售,在该场景下会出现很多人抢非常少的商品的情况。
举个例子:比如现在有2个特价电冰箱销售,现在同时有三个人进行抢购,每个人限抢1件,如果不做控制的话就会出现超卖的情况。那么如何进行控制呢?这里就需要用到锁机制,锁分为悲观锁和乐观锁。
悲观锁:
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都会认为别人会修改,所有每次在拿数据的时候都会上锁,这样别人想拿这个数据就会被阻塞直到它拿到锁。(换句话说就是对数据加锁,每个人之后获得锁了,才能操作该数据)。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种 check-and-set 机制实现事务的。
WATCH(监视,相当于乐观锁操作)
WATCH用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
WATCH是在事务之间发送的指令,Redis服务在接收到指令时,会记录下该key对应的值,当Redis服务接收到EXEC指令,需要执行事务时,Redis服务首先会检查WATCH的key的值,从WATCH之后是否发生改变即可
# 示例:
# 模拟一个花钱的过程
127.0.0.1:6379> set money 100 # 这里有 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> get money
"100"
127.0.0.1:6379> WATCH money # 在事务之前监视对象
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 30 # 这里减 30
QUEUED
127.0.0.1:6379(TX)> INCRBY out 30 # 这里加 30
QUEUED
127.0.0.1:6379(TX)> EXEC # 数据在监控到执行期间没有发生变化,所以事务正常执行
1) (integer) 70
2) (integer) 30
127.0.0.1:6379>
示例:
模拟在监控期间多线程修改了监控数据内容,再查看执行结果
# 示例:
127.0.0.1:6379> WATCH money # 监视这个数据
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> EXEC # 在事务中修改数据时会先去比较之前的数据是否是被监视时的数据,如果不是则在 exec 执行事务时会失败
(nil)
127.0.0.1:6379>
# 在另外一个线程中对被监视的数据进行修改
127.0.0.1:6379> DECRBY money 1000
(integer) -930
127.0.0.1:6379>
☑️ 关于WATCH的细节和常见误区
监视的持续时间
无论哪种情况,监视都只持续一个事务周期。
如果执行EXEC 或者DISCARD, 则不需要手动执行UNWATCH
测试后发现,下列任一情况后不执行任何其他操作,立即开启新的事务,并在修改原本被监视的键后,执行事务都会成功。
- 被监视的键被修改,事务返回nil未执行成功。
- 被监视的键未被修改,事务成功执行。
- 被监视的键被修改或未被修改,事务未执行,使用DISCARD弃用当前事务。
其他特殊情况
- 被监视的若干键只有一个键被修改:事务不会执行。
- 被监视的键被修改为原值(比如键a的值从1被修改为1):事务不会执行。WATCH监视的是修改操作,不是值比较,不存在任何ABA问题,只要对被监视键执行修改命令,事务就不会执行。
- 被监视的键不存在:事务不会执行。例如监视了一个不存在的键a,监视后使用set a 1等指令修改键a,事务同样不会执行。
- 被监视的键在整个事务队列的命令中并未被使用:事务不会执行。WATCH监视只关注被监视的键本身是否被修改过。
UNWATCH
刷新一个事务中已被监视的所有key。