Linux C/C++ 学习日记(63):Redis(四):事务

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、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 脚本的完整内容计算后得到的结果:

  1. SHA-1 会将任意长度的 Lua 脚本内容,转化为一个 ** 固定长度 160 位(40 个十六进制字符)** 的字符串(例如 a29c7b2f6f3c8b2e928d2c8e7f6a5d4c3b2a1f0e);
  2. 这个 SHA-1 值是脚本的唯一标识:脚本内容只要有任何细微修改(包括空格、换行、字符大小写、命令顺序),计算出的 SHA-1 值就会完全不同;
  3. Redis 执行 SCRIPT LOAD 命令时,会同时完成两件事:计算脚本的 SHA-1 哈希值 + 将脚本内容与 SHA-1 值绑定后缓存到服务器内存中(缓存永久有效,除非 Redis 重启或手动清空)。

简单来说:SHA 编码就是 Lua 脚本的 "身份证号",Redis 通过它可以快速找到缓存中的脚本,无需重复传递完整脚本内容。

4.2 优势

直接使用 EVAL 命令执行脚本虽然简单,但在脚本频繁执行、脚本内容较长 的场景下存在明显短板,而 SHA 编码(配合 EVALSHA)正是为了解决这些问题:

  1. 大幅减少网络传输开销 若 Lua 脚本内容较长(比如几百行逻辑),每次用 EVAL 执行都需要将完整脚本内容通过网络传递给 Redis 服务器,频繁执行会占用大量带宽,尤其在分布式环境中影响显著。而 SHA-1 值仅 40 个字符,传递成本几乎可以忽略不计。
  2. 提升脚本执行效率 Redis 每次执行 EVAL 命令时,都需要重新解析完整 Lua 脚本、校验语法、计算 SHA-1 哈希;而通过 EVALSHA 执行时,Redis 直接通过 SHA-1 值在缓存中查找已解析好的脚本,无需重复做语法解析和哈希计算,执行速度更快。
  3. 提高脚本的可复用性 脚本通过 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

关键补充:EVALSHAEVAL 的参数一致性

EVALSHA 的参数(numkeyskey[]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 LOADSET等 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 重启后,通过以下方式重新缓存:

  1. 手动加载 :登录 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
  2. 自动加载 :编写初始化脚本(如 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表示脚本已成功加载。

六、总结

  1. Redis 事务是批量命令的有序集合,核心是 "一次性打包、一次性执行",不满足严格 ACID 特性;
  2. 核心特点:批量队列化、无传统回滚、弱 ACID、支持乐观锁(WATCH);
  3. 实现方式:原生事务(MULTI/EXEC,适合简单批量命令)、Lua 脚本(适合复杂逻辑,性能更优);
  4. 注意:Redis 事务不保证持久性,敏感数据需配合合适的持久化策略(如 AOF always)。
相关推荐
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-41-表大小快速估算
linux·运维·数据库·sql·学习
DYS_房东的猫2 小时前
《 C++ 零基础入门教程》第8章:多线程与并发编程 —— 让程序“同时做多件事”
开发语言·c++·算法
prettyxian2 小时前
【Linux】环境变量
linux·运维·服务器
REDcker2 小时前
AIGCJson 库介绍与使用指南
c++·json·aigc·c
MediaTea2 小时前
Python OOP 设计思想 13:封装服务于演化
linux·服务器·前端·数据库·python
setary03012 小时前
c++泛型编程之Typelists
开发语言·c++
松涛和鸣2 小时前
DAY53 UART Serial Communication
c语言·单片机·嵌入式硬件·tcp/ip·51单片机
傻乐u兔2 小时前
C语言初阶————调试实用技巧1
c语言·开发语言
阿拉伯柠檬2 小时前
MySQL复合查询
linux·数据库·mysql·面试