Redis 事务
先来看看 MySQL 事务的四大特性:
- 原子性:将事务里的多个操作打包成一个整体,要么全部成功,要么全部失败,失败后会进行回滚操作。
- 一致性:确保事务执行前后,其数据的整体变化一致。
- 隔离性:并发执行事务或其他操作时,对数据访问的隔离限制。
- 持久性:事务完成后,对数据的修改持久化存储。
Redis 的事务与 MySQL 的事务相比,是稍逊一筹的。Redis 事务的特性可以说只有一个:“原子性”。
且 Redis 是否是真正的原子性还持有争议。如果原子性按照 MySQL 事务的原子性定义,那么 Redis 事务其实是没有原子性的。
Redis 事务的原子性,就行将多个操作打包成一个整体,要么全部执行,要么全都不执行。这里的全部执行是不保证成功的。
这和 MySQL 事务原子性是不同的,除了执行过程中出现错误的命令外,其他命令都能正常执行,并且,Redis 事务是不支持回滚(roll back)操作的。这与 MySQL 的略有不同。
旧版本 Redis 官网上对于事务的介绍中有这么一句话:
Either all of the commands or none are processed, so Redis transactions is also atomic. The EXEC command…
但现在 Redis 官网把这句话给删掉了,可见官方对于 Redis 事务的原子性也是比较虚的…
根据 MySQL 事务的四大特性,总结 Redis 事务的特性:
- 弱化的原子性:Redis 没有回滚机制,只能做到这些操作批量执行,不能做到某条操作失败后就回到事务执行的初始状态。
- 不保证一致性:Redis 没有回滚机制,也没有 “约束”。MySQL 的一致性体现的是运行事务前和运行后,结果都是合理有效的,不会出现中间非法状态。
- 不需要隔离性:Redis 是单线程模型,并不存在并发执行事务的情况。
- 不需要持久性:Redis 的数据主要还是保存在内存的,是否开启持久化和 redis-server 有关,不在 Redis 事务的考虑范围内。
Redis 事务的主要意义就是提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。也就是当有其他客户端也执行命令时,不会出现 “插队” 的情况。
Redis 是如何实现事务的?在开启事务时,Redis 会为当前客户端额外创建一个队列。后续的命令操作都将进入这个队列,知道后续执行事务时,才将这些命令操作从队列中取出,执行。
在 Redis 主线程执行队列里的命令操作时,是不会处理其他客户端的命令请求的,知道队列的命令操作全部执行完毕。
MySQL 的事务确实非常强大,但是其在空间和时间上都花费了不小的资源和开销,是做出了一定的牺牲的。
而 Redis 主要的特点就是操作简单,速度快,性能高,常常作为 MySQL 服务器上游的缓存来使用。所以,其实并不需要像 MySQL 事务那样的强大。
Redis 事务实际开发中使用的非常少,功能比较鸡肋。往往用在那些会出现线程安全问题的地方,比如计数器等。且 Redis 是支持通过编写 Lua 脚本来执行一系列的操作,这也看出是一种事务。
事务命令
使用 MULTI
开启事务,使用 EXEC
执行事务,使用 DISCARD
取消事务。
注意:开启事务后,将一些命令发送给服务器时,这些命令并不会立即执行,只有输入 EXEC
命令时,才会依此执行这些命令。
取消事务后,之前的发送给服务器的命令就都不会执行。
如果在开启事务时,且已经有几个命令发送给服务器了,此时服务器宕机了,那么就和 DISCARD
一样,取消了事务。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379(TX)> SET name redis
QUEUED
redis 127.0.0.1:6379(TX)> SET age 18
QUEUED
redis 127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
redis 127.0.0.1:6379>
来看这么一个场景:
- 客户端一:
redis 127.0.0.1:6379> SET k1 111 # 0️⃣
redis 127.0.0.1:6379> MULTI # 1️⃣
OK
redis 127.0.0.1:6379(TX)> SET k1 222 # 2️⃣
QUEUED
redis 127.0.0.1:6379(TX)> EXEC # 4️⃣
1) OK
redis 127.0.0.1:6379>
- 客户端二:
redis 127.0.0.1:6379> SET k1 333 # 3️⃣
有上述两个客户端,按照后续的序号执行命令,也就是客户端一在开启事务后,发送服务器将 k1
设为 222 的命令,随即客户端二发送了 SET k1 333
命令并且执行了。最后客户端一执行了事务。
直接看的话,感觉最后 k1
的结果是 333。但结果却是 222。这是因为只有执行了 EXEC
命令,才会真正执行事务,之前的命令都只是进入了事务队列,还没有真正执行。所以,将 k1
设为 222 的命令是发生在客户端二执行之后的。
针对上述场景,会导致最后的结果产生歧义。我们可以在执行事务前通过 WATCH
监听 k1
。
WATCH
可以监听某个 key
是否在执行事务之间,与开启事务前的值有差异。如果监听到值有变化,则不执行修改该 key
的命令。
UNWATCH
可以取消对某个 key
的监听。
- 客户端一:
redis 127.0.0.1:6379> WATCH k1 # 0️⃣
OK
redis 127.0.0.1:6379> SET k1 111 # 1️⃣
OK
redis 127.0.0.1:6379> MULTI # 2️⃣
OK
redis 127.0.0.1:6379(TX)> SET k1 222 # 3️⃣
QUEUED
redis 127.0.0.1:6379(TX)> EXEC # 5️⃣
(nil)
redis 127.0.0.1:6379>
- 客户端二:
redis 127.0.0.1:6379> SET k1 333 # 4️⃣
OK
redis 127.0.0.1:6379>
通过上述操作,可以看到,客户端一执行 EXEC
了后,返回 (nil)
,说明该事务没有执行任何操作。最后 k1
的值也是 333。
这是因为其监听到了 k1
的值发生了变化,所以不执行修改 k1
的命令。
WATCH 的实现
WATCH
的实现是基于 "乐观锁"的,通过 CAS
原子操作判断 key
是否被修改。
在监听某个 key
时,会为该 key
生成一个 “版本号”,后续有其他客户端对该 key
进行修改时,会将该 key
的版本号增加。
在执行事务时,出现了对 key
进行修改的命令,会通过 CAS
操作判断其版本号与最初 WATCH
时的版本号是否一致。如果一致,则执行命令,否则,放弃执行该命令。
至于为什么要是有版本号而不是直接使用 key
的值判断,是为了避免 ABA 问题的出现。
Redis 中的 Lua 脚本,也能起到类似于事务的效果。官网上说,事务这里的任何能实现的效果,都可以使用 Lua 脚本替代。