注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
一、Redis 事务是什么?
- Redis 事务是一组命令的有序集合(批量操作封装) ,它能将多个 Redis 命令打包成一个整体,在事务执行阶段(
EXEC命令触发),这些命令会被一次性、顺序性、排他性地执行,执行过程中不会被其他客户端的命令插入或打断。- 需要特别注意:Redis 事务不是严格意义上的 "事务"(不满足传统关系型数据库的 ACID 全特性),它的核心目标是保证批量命令的执行顺序和执行的原子性(有限范围内),而非提供完整的事务回滚、强持久性等能力。
- 简单来说:Redis 事务就是 "一次性打包、一次性执行" 的命令集合,用于解决批量命令的顺序执行和排他性问题。
二、Redis 事务的核心特点
1. 批量命令队列化,一次性执行
- 事务开启后(
MULTI),后续发送的所有命令不会立即执行,而是被先放入事务队列 中,Redis 仅返回QUEUED表示命令入队成功;只有当执行EXEC命令时,队列中的所有命令才会被一次性按顺序执行,执行完成后返回所有命令的结果列表。- 这个过程中,Redis 作为单线程模型,会保证事务队列的执行不会被其他客户端的命令打断,具备顺序性和排他性。
2. 不支持传统的事务回滚(Rollback),仅提供简单错误处理
这是 Redis 事务与关系型数据库事务的核心区别之一,Redis 不支持 "执行失败后回滚到事务执行前状态" 的能力,仅处理两种错误场景:
- 场景 1:入队时错误(语法错误、命令不存在等) 命令在放入事务队列时就被 Redis 识别为无效(如拼写错误
INCRR、命令参数个数错误),此时 Redis 会直接返回错误信息。当后续执行EXEC时,Redis 会放弃整个事务的执行,队列中所有命令都不会运行,相当于 "事务整体失效"。 - 场景 2:执行时错误(类型错误、逻辑错误等) 命令入队时语法正确(如对字符串键执行
HSET),但执行时因数据类型不匹配、逻辑错误等失败。此时 Redis 不会中断事务,已执行的命令结果会保留,未执行的命令会继续执行,不会对已执行命令进行回滚。
Redis 设计为不支持回滚的原因:减少事务实现的复杂度,提升执行性能;Redis 认为命令执行错误多是开发者的逻辑漏洞导致,应在开发阶段规避,而非依赖运行时回滚。
3. 弱一致性(弱事务),不满足严格的 ACID 特性
传统关系型数据库事务满足 ACID(原子性、一致性、隔离性、持久性),而 Redis 事务仅满足部分特性,属于 "弱一致性":
-
原子性(Atomicity):部分满足
原生事务:
入队时错误:事务整体不执行,具备原子性;
执行时错误:已执行命令不回滚,不具备原子性;
正常执行:队列命令全部执行,具备原子性。lua脚本:
redis.call():命令执行错误,会直接退出
redis.pcall(): 命令执行错误,不会直接退出,而是记录下错误,然后继续执行。用户可以自己 编写相关的退出逻辑实现
原子性:
a:不可分割
b:要么全部成功,要么全部失败
- 一致性(Consistency):满足
无论事务执行成功、失败(入队 / 执行时错误),Redis 都不会出现数据结构损坏、数据逻辑混乱的情况,始终保持数据一致性。
这个有争议(有的地方说不满足一致性):这里的数据一致性指的是预期的一致性而非异常后的一致性,redis可以保证事务执行前后的数据的完整约束,但是并不满足业务功能上的一致性。比如转账功能,一个扣钱,一个加钱。可能出现扣钱的操作失败,加钱却成功了(用户的代码逻辑有误)
cs
# 1. 给Alice设置字符串类型的"余额"(不是数字,无法扣减)
127.0.0.1:6379> SET account:Alice "not_a_number"
OK
# 2. 给Bob设置初始数字余额0
127.0.0.1:6379> SET account:Bob 0
OK
# 3. 验证两个账户的初始状态
127.0.0.1:6379> GET account:Alice
"not_a_number"
127.0.0.1:6379> GET account:Bob
"0"
# 4. 开启事务
127.0.0.1:6379> MULTI
OK
# 5. 入队:给Alice扣50元(DECRBY 命令,预期执行失败)
127.0.0.1:6379> DECRBY account:Alice 50
QUEUED # 入队成功(语法正确,Redis无法预判执行时的数据类型错误)
# 6. 入队:给Bob加50元(INCRBY 命令,预期执行成功)
127.0.0.1:6379> INCRBY account:Bob 50
QUEUED # 入队成功
# 7. 执行
127.0.0.1:6379> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value # 扣钱命令执行失败(数据类型错误)
2) (integer) 50 # 加钱命令执行成功(不受前一个命令失败影响)
- 隔离性(Isolation):完全满足
Redis 是单线程执行命令,事务执行期间不会被其他客户端的命令插入,事务之间是串行执行的,不存在 "脏读、不可重复读、幻读" 等问题,且无隔离级别之分。 - 持久性(Durability):不满足
事务的持久性依赖 Redis 的持久化策略(RDB/AOF):- RDB 是周期性快照备份,事务执行后未触发快照,断电会丢失数据;
- AOF 有
no(操作系统刷盘)、everysec(每秒刷盘)、always(每次命令刷盘)三种策略,仅always能接近持久性,但会严重影响性能,且仍无法应对硬件故障。
面试时候回答:lua 脚本满足原子性和隔离性;一致性和持久性不满足;
4. 原生无事务锁,仅可通过WATCH实现乐观锁
Redis 原生事务没有像关系型数据库那样的 "行锁、表锁" 等悲观锁机制,无法保证事务执行期间数据不被其他客户端修改。它提供WATCH命令实现乐观锁,基于 CAS(Compare And Swap,比较并交换)思想,用于监控指定键,防止数据被并发修改。
三、Redis 事务的实现方式
Redis 事务的实现分为两种:原生事务(基于MULTI/EXEC系列命令) 和增强版事务(基于 Lua 脚本),其中原生事务是基础,Lua 脚本是更强大的扩展。
方式一:原生事务(核心命令:MULTI/EXEC/DISCARD/WATCH)
原生事务依赖 4 个核心命令完成,流程分为 "开启事务→命令入队→执行 / 取消事务→(可选)乐观锁监控" 四个阶段。
1. 核心命令说明
| 命令 | 功能说明 |
|---|---|
WATCH key1 [key2 ...] |
为事务提供乐观锁支持,监控一个或多个键。EXEC执行前,若监控的键被其他客户端修改,事务会失效。监控在EXEC/DISCARD后自动失效,也可通过UNWATCH手动取消。 |
MULTI |
开启事务,标记事务的开始,后续命令进入事务队列,不再立即执行,返回OK。 |
EXEC |
执行事务队列中的所有命令,返回所有命令的执行结果列表(中间有命令执行错误也会执行下去),事务结束。若WATCH监控的键被修改,事务会被拒绝执行,返回nil。 |
DISCARD |
取消事务,清空事务队列,放弃所有入队命令,回到正常命令执行模式,返回OK。 |
2. 原生事务的典型场景示例
场景 1:正常执行事务
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379> SET user:1 "zhangsan" # 命令入队,返回QUEUED
QUEUED
127.0.0.1:6379> INCR score:1 # 命令入队
QUEUED
127.0.0.1:6379> EXEC # 执行事务,返回所有命令结果
1) OK
2) (integer) 1
场景 2:使用DISCARD取消事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET user:2 "lisi"
QUEUED
127.0.0.1:6379> DISCARD # 取消事务,清空队列
OK
127.0.0.1:6379> GET user:2 # 事务未执行,返回nil
(nil)
场景 3:入队错误(事务整体放弃执行)
cs
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set name
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set user "dcf"
QUEUED
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get user
"dcf after"
127.0.0.1:6379>
场景 4:执行时错误,后面接着执行(不回滚)
cs
127.0.0.1:6379> SET str:1 "hello" # 先创建一个字符串键
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET user:4 "zhaoliu" # 语法正确,入队成功
QUEUED
127.0.0.1:6379> HSET str:1 "field1" "value1" # 对字符串键执行哈希命令,入队成功(语法正确)
QUEUED
127.0.0.1:6379> EXEC # 执行事务
1) OK # 第一个命令执行成功
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value # 第二个命令执行失败
127.0.0.1:6379> GET user:4 # 已执行命令结果保留,不回滚
"zhaoliu"
127.0.0.1:6379> set str:1 "hello" # 先创建一个字符串键
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set user "dcf" # 语法正确,入队成功
QUEUED
127.0.0.1:6379(TX)> HSET str:1 "a" "b" # 对字符串键执行哈希命令,入队成功(语法正确)
QUEUED
127.0.0.1:6379(TX)> set user "dcf after" # 语法正确,入队成功
QUEUED
127.0.0.1:6379(TX)> EXEC # 执行事务
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value # 第二个命令执行失败
3) OK
127.0.0.1:6379> GET user
"dcf after"
127.0.0.1:6379>
场景 5:WATCH乐观锁生效(键被修改,事务失效)
# 客户端A:监控balance并开启事务
127.0.0.1:6379> WATCH balance # 监控balance键
OK
127.0.0.1:6379> GET balance
(integer) 100
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY balance 50 # 命令入队
QUEUED
# 客户端B:修改被监控的balance键
127.0.0.1:6379> SET balance 200
OK
# 客户端A:执行事务,因balance被修改,事务失效
127.0.0.1:6379> EXEC
(nil) # 事务被拒绝执行,返回nil
127.0.0.1:6379> GET balance
(integer) 200
方式二:增强版事务(基于 Lua 脚本)
Redis 支持执行 Lua 脚本,脚本中的所有 Redis 命令会被单线程一次性执行,中间不会被其他客户端命令打断,具备比原生事务更强的能力,可视为 "增强版事务"。
1. Lua 脚本实现事务的核心原理
Redis 执行 Lua 脚本时,会将整个脚本作为一个 "原子操作" 处理:
- 脚本内的所有命令按顺序执行,无外部命令插入;
- 脚本执行失败时,后续命令会停止执行,但已执行的命令结果不会回滚;
- 支持复杂逻辑判断(条件、循环等),弥补原生事务仅能简单打包命令的不足。
2. Lua 脚本的优势
- 更强的灵活性:支持复杂业务逻辑,无需多次网络往返传递命令;
- 更好的性能:单个 Lua 脚本只需一次网络请求,减少网络开销,执行效率高于原生事务;
- 天然的原子性:单线程执行保证脚本内命令的排他性,无需额外使用
WATCH。
3. 示例:Lua 脚本实现 "余额扣减" 事务(带条件判断)
cs
# 前提:先设置balance=100
127.0.0.1:6379> SET balance 100
OK
# 执行Lua脚本:若balance >= 50,扣减50;否则返回nil
127.0.0.1:6379> EVAL "local bal = redis.call('GET', KEYS[1]); if tonumber(bal) >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[1]) else return nil end" 1 balance 50
(integer) 50
# 余额不足时,返回nil
127.0.0.1:6379> SET balance 30
OK
127.0.0.1:6379> EVAL "local bal = redis.call('GET', KEYS[1]); if tonumber(bal) >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[1]) else return nil end" 1 balance 50
(nil)
4. lua脚本的sha编码
4.1 概念
Redis 中 Lua 脚本的 SHA 编码,特指 SHA-1 哈希算法 (安全哈希算法 1)对 Lua 脚本的完整内容计算后得到的结果:
- SHA-1 会将任意长度的 Lua 脚本内容,转化为一个 ** 固定长度 160 位(40 个十六进制字符)** 的字符串(例如
a29c7b2f6f3c8b2e928d2c8e7f6a5d4c3b2a1f0e); - 这个 SHA-1 值是脚本的唯一标识:脚本内容只要有任何细微修改(包括空格、换行、字符大小写、命令顺序),计算出的 SHA-1 值就会完全不同;
- Redis 执行
SCRIPT LOAD命令时,会同时完成两件事:计算脚本的 SHA-1 哈希值 + 将脚本内容与 SHA-1 值绑定后缓存到服务器内存中(缓存永久有效,除非 Redis 重启或手动清空)。
简单来说:SHA 编码就是 Lua 脚本的 "身份证号",Redis 通过它可以快速找到缓存中的脚本,无需重复传递完整脚本内容。
4.2 优势
直接使用 EVAL 命令执行脚本虽然简单,但在脚本频繁执行、脚本内容较长 的场景下存在明显短板,而 SHA 编码(配合 EVALSHA)正是为了解决这些问题:
- 大幅减少网络传输开销 若 Lua 脚本内容较长(比如几百行逻辑),每次用
EVAL执行都需要将完整脚本内容通过网络传递给 Redis 服务器,频繁执行会占用大量带宽,尤其在分布式环境中影响显著。而 SHA-1 值仅 40 个字符,传递成本几乎可以忽略不计。 - 提升脚本执行效率 Redis 每次执行
EVAL命令时,都需要重新解析完整 Lua 脚本、校验语法、计算 SHA-1 哈希;而通过EVALSHA执行时,Redis 直接通过 SHA-1 值在缓存中查找已解析好的脚本,无需重复做语法解析和哈希计算,执行速度更快。 - 提高脚本的可复用性 脚本通过
SCRIPT LOAD缓存后,多个客户端(无论什么语言开发)都可以通过同一个 SHA-1 值调用该脚本,无需各自保存完整的 Lua 脚本内容,便于团队协作和脚本版本统一管理。
4.3 核心命令:
| 命令 | 语法格式 | 功能说明 | 返回值 |
|---|---|---|---|
| SCRIPT LOAD | SCRIPT LOAD <lua-script-content> | 1. 对传入的完整 Lua 脚本计算 SHA-1 哈希值;(如果脚本错误会返回错误) 2. 将脚本内容与 SHA-1 值绑定,缓存到 Redis 服务器内存中(不执行脚本); 3. 缓存永久有效,Redis 重启后清空,或通过 SCRIPT FLUSH 手动清空。 | 该脚本对应的 40 个十六进制字符的 SHA-1 哈希值(脚本的唯一标识)。 |
| EVALSHA | EVALSHA <sha1> <numkeys> <key1> [key2 ...] <arg1> [arg2 ...] | 通过脚本的 SHA-1 哈希值,执行已缓存到 Redis 中的 Lua 脚本。参数规则与 EVAL 完全一致:- numkeys:键名参数个数;- key []:脚本操作的 Redis 键;- arg []:非键名业务参数。 | 与 EVAL 执行结果一致,返回脚本的执行结果;若 SHA-1 对应的脚本未缓存,返回NOSCRIPT No matching script. Please use EVAL.错误。 |
| SCRIPT EXISTS | SCRIPT EXISTS <sha1> [sha1 ...] | 验证一个或多个 SHA-1 哈希值对应的 Lua 脚本是否存在于 Redis 缓存中。 | 返回一个整数列表,1 表示存在,0 表示不存在,顺序与传入的 SHA-1 顺序一致。 |
| SCRIPT FLUSH | SCRIPT FLUSH | 清空 Redis 服务器中所有缓存的 Lua 脚本,同时清除所有 SHA-1 哈希值与脚本的绑定关系。 | 返回OK。 |
关键补充:EVALSHA 与 EVAL 的参数一致性
EVALSHA的参数(numkeys、key[]、arg[])必须与原脚本的EVAL调用参数完全一致,因为:
- 脚本的缓存仅关联 "脚本内容" 和 "SHA-1 值",不关联任何执行参数;
- 执行参数(键、业务数值)是每次执行脚本的动态数据,需要在
EVALSHA中单独传递,与EVAL用法完全相同。
四、lua脚本的SHA编码实际应用中的策略
1. 注意事项:
- SCRIPT LOAD 在lua脚本语法有问题的情况下会返回错误,不会生成SHA-1。
- SHA-1 不会持久化到文件(aof和rdb不会记录SCRIPT LOAD的操作)
- aof记录的是客户端传过来的命令,也就是说,记录的是EVALSHA sha1... ,而不是记录执行的具体lua脚本。如果没有措施,重启时通过aof恢复数据会报错。
(tips:aof是在命令执行成功之后才记录的)
2. 策略:
redis通过aof恢复数据前
2.1 可以让 Redis 在「加载 AOF 文件之前」,先加载脚本到缓存中
Redis 提供了--init-file参数,专门用来指定启动时要执行的 Redis 命令文件(文件中可以写SCRIPT LOAD、SET等 Redis 命令),且这些命令会在加载 AOF/RDB 持久化文件之前执行,正好满足 "预加载脚本到缓存" 的需求
-
创建命令文件 :新建
lua_preload.cmd(文件名任意),写入所有核心脚本的SCRIPT LOAD命令(每行一个命令):# 示例:转账脚本的SCRIPT LOAD命令 SCRIPT LOAD "local from_balance = tonumber(redis.call('GET', KEYS[1])); local transfer_amount = tonumber(ARGV[1]); if from_balance >= transfer_amount then redis.call('DECRBY', KEYS[1], transfer_amount); redis.call('INCRBY', KEYS[2], transfer_amount); return 'success'; end; return 'error: insufficient balance';" # 示例:库存扣减脚本的SCRIPT LOAD命令 SCRIPT LOAD "local stock = tonumber(redis.call('GET', KEYS[1])); local num = tonumber(ARGV[1]); if stock >= num then redis.call('DECRBY', KEYS[1], num); return 'success'; end; return 'error: stock insufficient';" -
启动 Redis 时指定
--init-file参数:-
临时启动(测试):
redis-server --init-file /usr/local/redis/lua_preload.cmd -
永久配置(生产):修改
redis.conf,添加启动参数(不同系统的配置方式略有差异,以 Linux 为例):在redis.conf中找到daemonize yes(若启用后台启动),然后添加:# 指定启动时执行的命令文件 init-file /usr/local/redis/lua_preload.cmd -
保存后重启 Redis:
redis-server /usr/local/redis/redis.conf
-
-
Redis 启动流程验证:Redis 启动时会按以下顺序执行:
加载
redis.conf配置 → 执行init-file指定的命令文件(SCRIPT LOAD预加载脚本) → 加载 AOF/RDB 持久化文件。此时 AOF 中的EVALSHA命令执行时,对应的脚本已被预加载到缓存,不会返回NOSCRIPT错误。
redis通过aof恢复数据后
2.2 客户端采用 "EVALSHA 优先 + NOSCRIPT 错误回退 EVAL" 的策略
这是生产环境的标准做法,成熟的 Redis 客户端(如 Jedis、Redisson、redis-py)都已内置该逻辑:
- 客户端执行脚本时,先尝试用
EVALSHA <sha1> ...; - 若收到
NOSCRIPT错误(说明脚本未缓存),立即回退到EVAL <完整脚本> ...执行; - 同时,客户端会自动执行
SCRIPT LOAD <完整脚本>,将脚本重新缓存到 Redis 中,方便后续用 EVALSHA 调用。
2.3 Redis 重启后,手动 / 自动重新加载常用 Lua 脚本
对于系统核心的常用 Lua 脚本(如转账、支付、库存扣减等),可以在 Redis 重启后,通过以下方式重新缓存:
-
手动加载 :登录
redis-cli,执行SCRIPT LOAD命令,重新加载核心脚本,记录 SHA-1 值;cs# 1.服务器启动 script flush 清空SHA区 script load lua1 hx1 加载SHA区 script load lua2 unordered_map<ID,hx> # 2.后面根据id查找hx执行 evalsha hx -
自动加载 :编写初始化脚本(如 Shell 脚本、Python 脚本),在 Redis 服务启动后(即aof已经恢复数据完毕)自动执行,通过
SCRIPT LOAD加载所有核心 Lua 脚本,实现 "无感恢复"; 注意:自动加载时,需保证 Lua 脚本内容的一致性,避免因脚本修改导致 SHA-1 值变化,影响后续EVALSHA调用。
2.4 数据恢复稳定性优先级远高于性能时
若对 Redis 重启后的 Data Recovery 要求极高,且无法接受 EVALSHA 执行失败的风险,可以放弃 EVALSHA,直接使用 EVAL 执行脚本。
五、redis启动后(恢复数据后)自动加载SHA的实现
步骤 1:准备核心 Lua 脚本的SCRIPT LOAD命令
先将核心 Lua 脚本的内容整理好,比如 "转账脚本" 和 "库存扣减脚本",后续在初始化脚本中执行SCRIPT LOAD
步骤 2:编写初始化脚本(二选一:Shell/Python)
方式 A:Shell 脚本实现(依赖redis-cli)
新建redis_load_lua.sh脚本,内容如下(需替换 Redis 连接信息,如密码、端口):
bash
#!/bin/bash
# 配置Redis连接信息
REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
REDIS_PASSWORD="your_redis_password" # 无密码则删除"-a $REDIS_PASSWORD"
# 核心Lua脚本1:转账脚本
TRANSFER_SCRIPT="local from_balance = tonumber(redis.call('GET', KEYS[1])); local transfer_amount = tonumber(ARGV[1]); if from_balance >= transfer_amount then redis.call('DECRBY', KEYS[1], transfer_amount); redis.call('INCRBY', KEYS[2], transfer_amount); return 'success'; end; return 'error: insufficient balance';"
# 核心Lua脚本2:库存扣减脚本
STOCK_SCRIPT="local stock = tonumber(redis.call('GET', KEYS[1])); local num = tonumber(ARGV[1]); if stock >= num then redis.call('DECRBY', KEYS[1], num); return 'success'; end; return 'error: stock insufficient';"
# 重试机制:确保Redis已启动
retry_count=5
while [ $retry_count -gt 0 ]; do
# 尝试连接Redis,执行SCRIPT LOAD
if redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD SCRIPT LOAD "$TRANSFER_SCRIPT" >/dev/null 2>&1; then
echo "转账脚本加载成功"
redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD SCRIPT LOAD "$STOCK_SCRIPT"
echo "库存脚本加载成功"
exit 0
else
echo "Redis尚未启动,等待5秒后重试..."
sleep 5
retry_count=$((retry_count-1))
fi
done
echo "重试失败,Redis未正常启动"
exit 1
方式 B:Python 脚本实现(依赖redis库)
配置脚本在 Redis 启动后自动执行
先安装依赖:
pip install redis
新建redis_load_lua.py脚本,内容如下:
python
import redis
import time
# 配置Redis连接信息
REDIS_CONFIG = {
"host": "127.0.0.1",
"port": 6379,
"password": "your_redis_password", # 无密码则删除此参数
"db": 0
}
# 核心Lua脚本
TRANSFER_SCRIPT = """
local from_balance = tonumber(redis.call('GET', KEYS[1]));
local transfer_amount = tonumber(ARGV[1]);
if from_balance >= transfer_amount then
redis.call('DECRBY', KEYS[1], transfer_amount);
redis.call('INCRBY', KEYS[2], transfer_amount);
return 'success';
end;
return 'error: insufficient balance';
"""
STOCK_SCRIPT = """
local stock = tonumber(redis.call('GET', KEYS[1]));
local num = tonumber(ARGV[1]);
if stock >= num then
redis.call('DECRBY', KEYS[1], num);
return 'success';
end;
return 'error: stock insufficient';
"""
def load_lua_scripts():
retry_count = 5
while retry_count > 0:
try:
# 连接Redis
r = redis.Redis(**REDIS_CONFIG)
r.ping() # 验证连接
# 执行SCRIPT LOAD
transfer_sha = r.script_load(TRANSFER_SCRIPT)
stock_sha = r.script_load(STOCK_SCRIPT)
print(f"转账脚本加载成功,SHA-1: {transfer_sha}")
print(f"库存脚本加载成功,SHA-1: {stock_sha}")
return
except Exception as e:
print(f"Redis尚未启动,错误:{e},等待5秒后重试...")
time.sleep(5)
retry_count -= 1
print("重试失败,Redis未正常启动")
if __name__ == "__main__":
load_lua_scripts()
步骤 3:配置脚本在 Redis 启动后自动执行
场景 1:Redis 以systemd服务运行(主流 Linux 发行版)
编辑 Redis 的 systemd 服务文件(通常路径:/usr/lib/systemd/system/redis.service),添加ExecStartPost指令(在 Redis 启动后执行脚本):
[Unit]
Description=Redis In-Memory Data Store
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /usr/local/redis/redis.conf
# 新增:Redis启动后执行初始化脚本
ExecStartPost=/usr/local/redis/redis_load_lua.sh # 替换为你的脚本路径
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
重新加载 systemd 配置并重启 Redis:
systemctl daemon-reload
systemctl restart redis
场景 2:Redis 以supervisor管理
在 supervisor 的 Redis 配置文件(如/etc/supervisor/conf.d/redis.conf)中,添加eventlistener监听 Redis 启动事件,执行脚本:
[program:redis]
command=/usr/local/bin/redis-server /usr/local/redis/redis.conf
autostart=true
autorestart=true
user=redis
[eventlistener:load_lua_scripts]
command=/usr/local/redis/redis_load_lua.sh # 替换为你的脚本路径
events=PROCESS_STATE_RUNNING
process_name=redis
更新 supervisor 配置:
supervisorctl update
步骤4:验证效果
Redis 启动后,执行以下命令验证脚本是否加载成功:
redis-cli -h 127.0.0.1 -p 6379 -a your_redis_password SCRIPT EXISTS <转账脚本的SHA-1> <库存脚本的SHA-1>
返回1表示脚本已成功加载。
六、总结
- Redis 事务是批量命令的有序集合,核心是 "一次性打包、一次性执行",不满足严格 ACID 特性;
- 核心特点:批量队列化、无传统回滚、弱 ACID、支持乐观锁(
WATCH);- 实现方式:原生事务(
MULTI/EXEC,适合简单批量命令)、Lua 脚本(适合复杂逻辑,性能更优);- 注意:Redis 事务不保证持久性,敏感数据需配合合适的持久化策略(如 AOF
always)。