当前位置: 首页 > news >正文

理解 Redis 事务-21(使用事务实现原子操)

使用事务实现原子操作

Redis 事务是一种在单个步骤中执行一组命令的机制。"要么全部,要么全部不"的方法确保了数据的一致性和完整性,尤其是在需要对相关数据进行多个操作时。没有事务,并发操作可能会导致竞争条件和不一致的数据状态。本课将探讨如何使用 Redis 事务来实现原子操作,保证事务中的所有命令要么全部执行,要么全部不执行。

理解 Redis 事务

Redis 事务提供了一种将多个命令组合为单个原子操作的方式。这意味着事务中的所有命令都是按顺序执行的,并且与其他客户端隔离。如果事务中的任何命令失败,整个事务将被回滚,以确保数据一致性。

MULTI, EXEC, 和 DISCARD 命令

Redis 事务的核心在于三个命令:MULTI, EXEC, 和 DISCARD

  • MULTI: 该命令标记事务块开始。服务器收到的所有后续命令都将排队在事务内执行,直到发出 EXEC 命令。
  • EXEC:该命令触发事务队列中所有命令的执行。Redis 将按照接收顺序执行命令,并返回一个结果数组。如果在执行阶段任何命令失败(例如,由于语法错误或数据类型不正确),执行会继续,错误将在结果数组中报告。
  • DISCARD:该命令取消事务,清空整个事务队列。不会执行任何命令,连接将恢复到正常状态。

示例:基本交易

让我们说明一个简单的交易,该交易递增两个计数器:counter1counter2

MULTI
INCR counter1
INCR counter2
EXEC

在这个例子中:

  1. MULTI 倡导交易。
  2. INCR counter1INCR counter2 被排队。
  3. EXEC 执行排队中的命令。

结果将是一个包含 counter1counter2 增量值的数组。

示例:与 DISCARD 的交易

现在,让我们看看 DISCARD 是如何工作的。

MULTI
INCR counter1
INCR counter2
DISCARD

在这种情况下,counter1counter2 将不会被递增,因为 DISCARD 命令取消了事务。

原子性在 Redis 事务中

Redis 事务在某种程度上提供了原子性,因为事务中的所有命令都是按顺序且独立执行的。然而,Redis 事务在传统数据库意义上并不提供真正的回滚功能。如果在 EXEC 阶段(例如,由于语法错误或操作了错误的数据类型)有命令失败,Redis 会继续执行队列中的剩余命令。错误会在结果数组中报告,但事务不会被完全回滚。

这种行为与传统 ACID(原子性、一致性、隔离性、持久性)数据库不同,后者在发生故障时会将整个事务回滚到初始状态。Redis 优先考虑性能和简单性,而非完全符合 ACID。

🚫 Redis 不支持回滚的原因

Redis 是单线程、无锁的,其设计目标是高性能和简洁性,而不是像传统数据库那样提供事务隔离和回滚机制(如 ACID 中的 Isolation 和 Durability)。

实现原子操作

Redis 事务对于实现原子操作特别有用,其中多个命令必须作为一个不可分割的整体来执行。这对于在并发环境中保持数据一致性至关重要。

场景:在不同账户之间转账

考虑一个场景,你需要将资金从一个账户转移到另一个账户。这涉及两个操作:减少源账户的余额和增加目标账户的余额。为确保数据一致性,这些操作必须原子性地执行。

这里是如何使用 Redis 事务来实现:

MULTI
DECRBY account1 100  # Subtract 100 from account1
INCRBY account2 100  # Add 100 to account2
EXEC

在这个例子中,如果 EXEC 命令成功,account1 将会减少 100,而 account2 将会增加 100。如果交易被中断或因任何原因失败,这两个操作都不会被执行,以确保资金不会丢失或重复。

场景:实现一个原子计数器

另一种常见的用例是实现一个原子计数器。假设你只想在计数器的当前值低于某个阈值时才进行递增。

MULTI
GET counter
INCR counter
EXEC

然而,这种方法不是原子的。另一个客户端可以在 GETINCR 命令之间增加计数器,从而可能超过阈值。要正确实现这一点,通常需要使用 Lua 脚本(将在下一章节中介绍)或使用 WATCH 命令进行乐观锁(如下文所述)。

使用 WATCH 进行乐观锁

WATCH 命令在 Redis 事务中提供了乐观锁的机制。它允许你监控一个或多个键的变化。如果在调用 EXEC 命令之前,任何被监控的键被修改,事务将被中止。

这是如何使用 WATCH 来实现带阈值的原子计数器:

WATCH counter
GET counter
# Check if the counter is below the threshold
IF counter < threshold THENMULTIINCR counterEXEC
ELSEUNWATCH
ENDIF

在这个例子中:

  1. WATCH counter 监控着 counter 键。
  2. GET counter 获取 counter 的当前值。
  3. 代码检查计数器是否低于阈值。
  4. 如果是,使用 MULTI 开始事务,使用 INCR counter 增加计数器,然后使用 EXEC 执行事务。
  5. 如果计数器不低于阈值,则调用 UNWATCH 命令停止监视该键。
  6. 如果另一个客户端在 WATCHEXEC 命令之间修改了 counter 键,事务将被中止,并且 EXEC 命令将返回 NULL。客户端可以重试该操作。

实现一个简单的速率限制器

速率限制是一种控制用户执行某些操作速率的技术。可以使用 Redis 事务来实现一个简单的速率限制器。

WATCH user:123:requests
GET user:123:requests
IF requests < limit THENMULTIINCR user:123:requestsEXPIRE user:123:requests expiration_timeEXEC
ELSEUNWATCH# Reject the request
ENDIF

在这个例子中:

  1. WATCH user:123:requests 监控特定用户的请求次数。
  2. GET user:123:requests 检索当前的请求数量。
  3. 如果请求数量低于限制,则开始一个事务。
  4. 该事务增加请求数量并为该密钥设置过期时间。
  5. 如果请求数量超过限制,请求将被拒绝。

🎯 场景目标:用户从账户 userAuserB 转账 100 元

  • 要求在并发环境中保证数据一致性

  • 防止 userA 在转账过程中余额被其他操作修改

✅ 实现步骤(Redis 乐观锁)

🧱 步骤说明

  1. WATCH userA:监视 userA 的余额

  2. GET userA:读取当前余额

  3. 业务逻辑判断:余额是否充足

  4. MULTI:开启事务

  5. DECRBY userA 100INCRBY userB 100:入队命令

  6. EXEC:提交事务,如果期间 userA 被其他客户端修改,EXEC 会失败


🧪 示例代码(使用 redis-cli 或客户端 SDK 执行)

WATCH userA            # Step 1: 监视 userA 的余额
GET userA              # Step 2: 读取余额(假设是 500)

假设返回结果是 500,则继续进行:

MULTI                  # Step 4: 开启事务
DECRBY userA 100       # Step 5: 扣减 userA
INCRBY userB 100       # 增加 userB
EXEC                   # Step 6: 提交事务

如果 WATCHuserA 没有被修改,EXEC 会成功执行两个命令。

如果在这期间有其他客户端修改了 userA 的值(即使只是 INCR 1 元),EXEC 会失败,整个事务不会执行。


📌 EXEC 返回值说明

  • 成功:[400, 100](表示执行了 DECRBYINCRBY

  • 失败:nil(说明 WATCH 检测到监控键被修改)


💡 实际使用建议(伪代码逻辑)

redis.watch("userA")
balance = redis.get("userA")
if int(balance) >= 100:pipe = redis.pipeline()pipe.multi()pipe.decrby("userA", 100)pipe.incrby("userB", 100)success = pipe.execute()if success:print("转账成功")else:print("余额在事务提交前被其他人改动,重试")
else:print("余额不足")

🧱 使用场景总结

使用场景Redis 乐观锁是否合适
用户余额扣减✅ 推荐使用
秒杀库存控制✅ 推荐使用
非强一致性场景(缓存等)❌ 不需要 WATCH
高并发写操作✅ 但注意避免死循环重试

相关文章:

  • docker 镜像完整生成指南
  • 论文阅读笔记——Janus,Janus Pro
  • RabbitMQ 集群与高可用方案设计(一)
  • 嵌入式硬件---施密特触发器单稳态触发器多谐振荡器
  • Redis实战-缓存篇(万字总结)
  • uniapp报错mongo_cell_decision_not_found
  • TCP 和 UDP 的区别
  • Windows逆向工程提升之x86结构化异常SEH处理机制
  • 非接触式互连:当串扰是您的朋友时
  • npm修改镜像的教程,将npm镜像修改为国内地址增加下载速度
  • SpringBoot-11-基于注解和XML方式的SpringBoot应用场景对比
  • Kubernetes(k8s)全面解析:从入门到实践
  • 以前端的角度理解 Kubernetes(K8s)
  • xy坐标上如何判定两个矩形是否重合
  • 什么是ESLint?它有什么作用?
  • 指针、空间地址
  • 当NLP能模仿人类写作:原创性重构而非终结
  • 华为OD机试真题—— 货币单位换算(2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
  • 佳源科技退卷IPO:曾于2023年7月过会,原计划募资约9亿元
  • CAPL自动化-诊断Demo工程
  • 淘宝网站建设设计模板/百度网首页
  • 网站开发深入浅出 - python篇/网店推广方法有哪些
  • 做网站建设与推广企业/天津seo排名收费
  • 嘉兴云推广网站/google广告投放技巧
  • 高端网站建设公司哪家公司好/室内设计培训
  • 温州小学网站建设/爱站网ip反域名查询