1. Redis中的Lua脚本是原子性操作吗?
在回答这个问题之前,我们首先要明确,Lua脚本中所指的原子性与我们通常意义上的原子性不一样。
我们通常所说的原子性是数据库中事务四大特性ACID(即原子性、一致性、隔离性、持久性)中的原子性,它是指一个事务中的所有操作要么全部成功执行,要么不执行。不执行的情况是指如果在事务执行的过程中,发生了异常或者死锁等,会将之前对数据库所做的全部操作撤销,将数据库的状态回滚到事务执行前的状态(具体原理可以了解一下MVCC)。而Lua脚本的原子性是指Redis在执行lua脚本的过程中将其视为一个不可分割的整体,在执行过程中不会被其他命令或请求打断,会严格按照脚本的流程顺序执行完毕。
1.1. Lua脚本的原子性与回滚
我们都知道Redis是不支持事务回滚的,因为Redis的设计初衷就是简单、高效,常用作缓存,而要实现事务的回滚需要额外的机制和数据结果来保证(参考MySQL中的MVCC和数据项中的回滚指针及事务id),因此Redis不支持回滚,而Lua脚本是不仅支持原子性也支持回滚,即Lua脚本在执行过程中如果发生了异常,对于异常发生前已经执行的指令,会进行撤销,以此来实现回滚的效果。
例如,假设有一个Lua脚本:
redis.call("SET", "key1", "value1")
local result = redis.call("INCR", "key2")
if result > 10 thenerror("Value too large!") -- 触发异常
end
redis.call("SET", "key3", "value3")
如果 INCR key2
返回的结果大于10,脚本会抛出异常,在这种情况下:
SET key1 "value1"
已经执行,但因为脚本中途抛出了异常,Redis 并不会将该操作实际应用到数据库中。- 同样地,
SET key3 "value3"
也不会被执行,因为脚本在之前就已经中止了。 - 整个 Lua 脚本要么全部成功,要么在异常时所有的操作都不会生效。
1.2. 为什么Lua脚本发生异常时,已修改的部分不会生效?
当Lua脚本在执行过程中发生异常时,Redis会中止脚本的运行,并且确保已经执行的命令不会对Redis服务器中的数据产生任何影响,这是因为:Redis在Lua脚本执行期间会”预执行“所有命令。当Lua脚本调用Redis.call()
来执行Redis命令时,这些命令实际上并没有立即作用到Redis数据库中,只有当整个脚本成功执行完毕后,Redis才会将这些命令应用到Redis数据库中。 如果脚本中途抛出异常,Redis 会抛弃脚本执行过程中产生的所有操作,而不会将任何结果写入数据库。因此Lua脚本能够支持回滚操纵。
1.3. Lua脚本原子性实现的技术原理
Redis通过以下机制来确保Lua脚本的原子性:
- 单线程执行:由于Redis的网络I/O和键值对的读写操作是单线程的,Redis执行Lua脚本时,会把Lua脚本作为一个整体并把它当作一个任务加入到一个队列中,然后单线程按照队列的顺序依次执行这些任务,确保了Lua脚本从头到尾作为一个整体独立进行执行。
- 延迟应用:Redis在执行Lua脚本时,所有的命令实际上并不会立即作用到数据库(区别于MySQL),而是”预执行“所有命令,将结果放入内部缓存中。当脚本全部成功执行完毕后,Redis会一次性将脚本中所有的命令结果应用到Redis数据库中,相反如果发生异常,则不会有任何命令结果被应用。