在Redis等嵌入Lua的环境中,Lua脚本的执行具有原子性,即一个Lua脚本在执行期间不会被其他命令或脚本中断。这是通过以下机制实现的:
1. 单线程模型保证原子性
Redis采用单线程处理命令(6.0后多线程仅用于IO,命令执行仍是单线程)。当执行Lua脚本时:
- 独占执行:脚本作为一个整体任务被Redis主线程执行,期间不会处理其他客户端请求。
- 无并发冲突:所有脚本内的操作(如多次
GET/SET
)如同一个事务,中间状态对外不可见。
-- 示例:原子性递增计数器
local current = redis.call('GET', 'counter')
current = tonumber(current) or 0
redis.call('SET', 'counter', current + 1)
-- 其他客户端在此脚本执行期间无法看到中间的counter值
2. 与事务(MULTI/EXEC)的区别
特性 | Lua脚本 | Redis事务(MULTI/EXEC) |
---|---|---|
原子性 | 绝对原子(整体执行或失败) | 非原子(仅保证命令顺序执行) |
中间结果可见性 | 不可见 | 可见(但会被DISCARD取消) |
错误处理 | 脚本报错则全部回滚 | 某条命令失败后继续执行后续命令 |
-- Lua脚本错误会回滚
redis.call('SET', 'key1', 'value1')
redis.call('INCR', 'key1') -- 如果key1不是数字,整个脚本回滚
3. 原子性的边界条件
- 脚本执行超时:默认5秒(可通过
lua-time-limit
配置),超时后Redis会记录警告但不会自动终止脚本(需用SCRIPT KILL
或SHUTDOWN NOSAVE
干预)。 - 网络中断:客户端断开连接不会停止脚本执行,服务器会继续执行直至完成。
- 资源限制:脚本中若包含慢操作(如
KEYS *
),会阻塞整个Redis实例。
4. 实际应用场景
- 计数器竞赛条件:
-- 原子性实现计数器递增 local current = redis.call('GET', 'counter') current = tonumber(current) or 0 redis.call('SET', 'counter', current + 1)
- 分布式锁:
-- 原子性获取锁 if redis.call('SETNX', 'mylock', '1') == 1 then redis.call('EXPIRE', 'mylock', 10) return true else return false end
5. 注意事项
- 避免长脚本:长时间运行的脚本会导致Redis无法响应其他请求。
- 禁用危险操作:如
KEYS
、FLUSHDB
等,应在脚本中避免。 - 复用脚本:使用
SCRIPT LOAD
和EVALSHA
减少网络传输。
总结
Lua脚本在Redis中的原子性由单线程模型和脚本整体执行机制保证,适用于需要强一致性的场景,但需注意性能影响和错误处理。