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

使用事务实现原子操作

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

理解 Redis 事务

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

MULTI, EXEC, 和 DISCARD 命令

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

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

示例:基本交易

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

redis 复制代码
MULTI
INCR counter1
INCR counter2
EXEC

在这个例子中:

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

结果将是一个包含 counter1 和 counter2 增量值的数组。

示例:与 DISCARD 的交易

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

redis 复制代码
MULTI
INCR counter1
INCR counter2
DISCARD

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

原子性在 Redis 事务中

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

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

🚫 Redis 不支持回滚的原因

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

实现原子操作

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

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

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

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

shell 复制代码
MULTI
DECRBY account1 100  # Subtract 100 from account1
INCRBY account2 100  # Add 100 to account2
EXEC

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

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

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

redis 复制代码
MULTI
GET counter
INCR counter
EXEC

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

使用 WATCH 进行乐观锁

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

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

shell 复制代码
WATCH counter
GET counter
# Check if the counter is below the threshold
IF counter < threshold THEN
    MULTI
    INCR counter
    EXEC
ELSE
    UNWATCH
ENDIF

在这个例子中:

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

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

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

redis 复制代码
WATCH user:123:requests
GET user:123:requests
IF requests < limit THEN
    MULTI
    INCR user:123:requests
    EXPIRE user:123:requests expiration_time
    EXEC
ELSE
    UNWATCH
    # Reject the request
ENDIF

在这个例子中:

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

🎯 场景目标:用户从账户 userA 向 userB 转账 100 元

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

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

✅ 实现步骤(Redis 乐观锁)

🧱 步骤说明

  1. WATCH userA:监视 userA 的余额

  2. GET userA:读取当前余额

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

  4. MULTI:开启事务

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

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


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

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

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

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

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

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


📌 EXEC 返回值说明

  • 成功:[400, 100](表示执行了 DECRBY 和 INCRBY)

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


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

python 复制代码
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
高并发写操作 ✅ 但注意避免死循环重试
相关推荐
姜鱼问生8 小时前
Nginx 缓存命中率监控:从 X-Cache-Status 到实时统计
运维·nginx·缓存
禾小西11 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
骉马代驾13 小时前
代驾系统实战(三):抢单池的 Redis 锁 + RabbitMQ 异步消费怎么做
redis
行百里er14 小时前
Redis Streams——有确认、能回溯的消息队列
redis
小马同学-15 小时前
Redis消息队列与客户端编程
数据库·redis
weixin1997010801615 小时前
《1688图片空间API踩坑:img.upload 与 album.* 的防盗链与CDN缓存问题》(附Python源码)
开发语言·python·缓存
fengkai454515 小时前
十二、Redis -2
运维·数据库·redis·容器
hweiyu0015 小时前
Redis命令:BLPOP
redis·缓存
敲个大西瓜16 小时前
langchain笔记(一)
redis·笔记·langchain
小小龙学IT21 小时前
C++ Redis 客户端深度解析(hiredis 与 redis-plus-plus)——工业数采实时缓存 C++ 侧实战
c++·redis·缓存