当前位置: 首页 > 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
高并发写操作✅ 但注意避免死循环重试
http://www.xdnf.cn/news/637147.html

相关文章:

  • GAN-STD:融合检测器与生成器的方法
  • Prometheus 架构及其特性
  • ModbusRTU转profibusDP网关与RAC400通讯报文解析
  • 历年贵州大学保研上机真题
  • web各类编码笔记
  • 什么是前端工程化?它有什么意义
  • 【MySQL】08.视图
  • 2025年AI代理演进全景:从技术成熟度曲线到产业重构
  • MongoDB | 零基础学习与Springboot整合ODM实现增删改查
  • Windows鼠标掉帧测试与修复
  • Android 性能优化入门(三)—— ANR 问题分析
  • Day36打卡 @浙大疏锦行
  • C#实现MCP Client 与 LLM 连接,抓取网页内容功能!
  • 11|省下钱买显卡,如何利用开源模型节约成本?
  • MIT 6.S081 2020Lab5 lazy page allocation 个人全流程
  • RabbitMQ 集群与高可用方案设计(一)
  • 通过Auto平台与VScode搭建远程开发环境(以Stable Diffusion Web UI为例)
  • 自训练NL-SQL模型
  • IS-IS报文
  • [特殊字符] UI-Trans:字节跳动发布的多模态 UI 转换大模型工具,重塑界面智能化未来
  • 以前端的角度理解 Kubernetes(K8s)
  • C++复习核心精华
  • Docker镜像与容器深度解析:从概念到实践的全面对比
  • PTA刷题笔记(难度预警!!!有详解)
  • 区块链可投会议CCF C--APSEC 2025 截止7.13 附录用率
  • leetcode 131. Palindrome Partitioning
  • Oracle 19c TFA工具的安装与使用详解
  • 【辰辉创聚生物】FGF信号通路相关蛋白:解码生命调控的关键枢纽
  • 第三十一天打卡
  • 医学写作供应商管理全流程优化