Redis事务
参考:
Scripting with Lua——译文
Redis进阶 - 事务:Redis事务详解
Redis事务与传统事务的区别
说到传统的事务,也就是 ACID,先回顾一下它的特性:
- 原子性(
Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; - 一致性(
Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; - 隔离性(
Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 持久性(
Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
而 Redis 事务通常不能看作传统的事务:
Redis 事务的本质是一组命令的集合。它支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
以下是它们特性的对比:
| 特性 | MySQL事务 | Redis事务 |
|---|---|---|
| 原子性 | 强原子性:要么全部成功,要么全部回滚 | 弱原子性:要么全部执行,要么全部不执行,但执行中若命令错误,事务中后续未执行命令仍会继续执行 |
| 一致性 | 强一致性:保证事务前后数据符合业务约束 | 无一致性保障:Redis无数据约束,操作失败可能导致数据逻辑错误 |
| 隔离性 | 支持多种隔离级别(读未提交、读已提交、可重复读、串行化) | 天然”串行隔离”:Redis是单线程模型,所有命令按顺序执行,无需额外隔离机制 |
| 持久性 | 强持久性:事务提交后数据永久写入磁盘(依赖redo log) | 无直接持久性:依赖RDB/AOF持久化,事务本身不保证持久化 |
可以看出,Redis 的事务对比数据库的 ACID,不同的地方主要是在于 A(Atomic),以及由 A 导致的 C(Consistency)。因为事务中如果有一个命令失败了,Redis 的事务是不会帮你自动回滚的,并且事务中剩下的命令会继续执行下去直到结束事务。这样的操作可能会导致一致性的问题。
Redis事务的上位替代选择
所以在我看来,Redis 的事务的作用,更像是要求先处理一个客户端请求的命令队列(Redis 是单线程工作机制),直到这个命令队列被执行完(保证过程中不会被其他客户端的请求命令打断插入),才去处理其他客户端请求的命令。
redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
这样的话,使用 Lua 脚本可以完美替代 Redis 事务。并且有更多的操作空间。
官方介绍:
Redis 允许用户在服务器上上传和执行 Lua 脚本。脚本可以使用程序控制结构,并在执行时使用大部分命令来访问数据库。由于脚本在服务器中执行,因此从脚本读写数据非常高效。Redis 保证脚本的原子执行。在执行脚本期间,所有服务器活动在其整个运行时都会被阻塞。这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。
脚本提供了在许多情况下非常有价值的几个特性,包括
- 通过在数据所在位置执行逻辑来提供局部性。数据局部性降低了整体延迟并节省了网络资源
- 确保脚本原子执行的阻塞语义。
- 能够组合 Redis 缺失或过于小众而无法成为其一部分的简单功能。
Lua 允许你在 Redis 内部运行部分应用程序逻辑。此类脚本可以跨多个键执行条件更新,可能原子地组合多种不同的数据类型。
脚本在 Redis 中由嵌入式执行引擎执行。目前,Redis 支持单个脚本引擎,即 Lua 5.1 解释器。完整的文档请参考Redis Lua API 参考页面。虽然服务器执行它们,但 Eval 脚本被视为客户端应用程序的一部分,因此它们没有名称、版本或持久化。因此,如果脚本丢失(例如服务器重启、故障转移到副本后),应用程序可能需要在任何时候重新加载所有脚本。自版本 7.0 起,Redis 函数提供了一种替代的可编程性方法,允许使用额外的编程逻辑扩展服务器本身。
总的来说,把 Redis 事务的命令,全写到一个 Lua 脚本就完事了,这种操作更加节省网络IO,更加灵活。
如何让Redis事务有回滚的能力
如上所说,Redis事务没有类似 InnoDB 的 undo log 与 MVCC 的机制,所以肯定是无法完成自动版本回滚的。
但是如果我一定要 Redis 有数据回滚这个功能,可以实现吗?
答案也许是可以的。虽然 Redis 原生事务无法在执行错误后自动阻止后续命令执行,但可以通过 Lua脚本实现这个功能(这就是上位替代的原因之一)。
在 Redis 中,如果某个命令执行失败,Redis 会记录错误并返回的,我们完全可以利用这个返回结果来判断。
我们可以 LUA 脚本中,在执行写操作前,记录原值,添加执行后返回的结果判断,如果第一步失败,直接返回错误,就不执行后续命令。
1 | -- 示例:原子操作,失败则全部回滚 |
当然这个方案并不是毫无缺点。
- 当这个业务比较复杂,涉及的数据比较多、处于并发量高的时候,性能是大大降低的,原因是 Rsdis 的单线程工作机制
- Redis本身没有 InnoDB 的
redo log机制,当命令执行出错时,这个时候如果者Redis 进程终止了或宕机了,是不能完成上一次的数据回滚的,虽然有AOF日志恢复当时的数据,但你可能不知道当时的命令有没有执行错误。
所以如果没有非常强的数据一致性要求,这个自己实现的 Redis 回滚机制其实并不是很推荐()