Redis 防止接口幂等性
在分布式系统中,接口的幂等性是一个重要的设计考量。幂等性是指无论操作执行多少次,结果都是一致的,且不会对系统产生副作用。例如,在支付系统中,一个用户请求支付操作时,幂等性保证了即使该请求被重复执行多次,用户账户不会被重复扣款。
Redis 在处理接口幂等性问题时,可以发挥很大的作用,特别是在保证操作不被重复执行或重复提交的情况下。常见的使用 Redis 保证接口幂等性的方法包括利用 Redis 的 Setnx 操作、分布式锁、以及唯一标识符 (UUID) 等策略。
1. 使用 Redis SETNX
实现幂等性
SETNX
(SET if Not eXists)是 Redis 提供的一种原子性操作,用于在 Redis 中设置键值对,仅当该键不存在时才会成功。这个特性可以用来防止接口的重复执行,确保某个操作只被执行一次。
工作原理
- 当用户请求一个操作时,首先通过 Redis 检查一个标识符(例如,用户操作的唯一 ID)是否已存在。
- 如果该标识符不存在,则可以执行操作,并将标识符存储到 Redis。
- 如果该标识符已存在,则说明该操作已经执行过,避免重复执行。
具体操作步骤
- 客户端生成一个唯一标识符,例如 UUID,来标识此次请求。
- 通过
SETNX
命令将该标识符存储到 Redis 中。如果存储成功,表示该请求尚未处理,执行相应操作。 - 如果存储失败,表示该请求已经执行过,拒绝再次执行该操作。
示例代码
假设我们在一个支付系统中,需要防止用户重复支付。我们可以使用 Redis 来保证每个支付请求只处理一次。
// 生成唯一的请求ID(例如,UUID) String requestId = UUID.randomUUID().toString(); // Redis SETNX 操作:设置请求ID作为键值,value 可以是当前时间或支付状态等 boolean isProcessed = jedis.setnx(requestId, "processed") == 1; // 判断是否为第一次请求,若是则处理支付 if (isProcessed) { // 执行支付操作 processPayment(requestId); } else { // 处理重复请求 System.out.println("请求已处理,避免重复支付"); }
setnx(requestId, "processed")
:如果请求 ID 不存在,则成功将其设置为"processed"
,表示该请求已经处理。isProcessed
:返回1
表示成功设置键值对,即该请求为首次请求,可以继续执行操作;如果返回0
,说明该请求已处理过,不会重复执行。
优缺点
优点:
- 简单易用:
SETNX
操作非常简单,适用于很多简单的幂等性场景。 - 性能好:Redis 是内存数据库,操作非常快速。
缺点:
- 过期时间问题:如果操作过期没有被清除,可能会导致无效的标识符长时间存在。可以结合
EXPIRE
设置过期时间来避免这一问题。 - 有可能引发竞争条件:如果多个请求同时到来并且标识符检查操作与请求执行操作不是在同一个事务中执行,可能会发生并发竞争的问题。
解决方案:加上过期时间
// 使用 SETNX + EXPIRE 保证标识符过期后会自动清除 jedis.setnx(requestId, "processed"); jedis.expire(requestId, 60 * 5); // 设置过期时间为5分钟
这样可以确保操作在短时间内只能执行一次,即便 Redis 重启或请求异常,也不会造成长时间的阻塞。
2. 使用 Redis 分布式锁保证幂等性
分布式锁可以用来控制分布式环境下多个请求对同一个资源的访问,以避免并发请求导致的数据冲突。Redis 提供了多种方式实现分布式锁,最常见的实现方式是 SETNX 和 Redisson。
工作原理
- 请求在执行操作之前,首先需要获取锁。如果锁获取成功,则继续执行操作;如果获取失败,则表明该操作已经被其他请求处理过,拒绝重复处理。
- Redis 分布式锁一般使用
SETNX
来实现,如果加锁成功,则执行业务操作,操作完成后释放锁。
具体操作步骤
- 客户端请求执行某个操作时,先尝试通过
SETNX
在 Redis 中设置一个锁。 - 如果锁设置成功,表示当前操作没有被其他请求占用,继续执行操作。
- 如果锁已经存在,则说明该操作已经被其他请求处理,拒绝执行当前请求。
示例代码(SETNX 方式)
// 生成唯一锁标识符 String lockKey = "payment_lock"; String lockValue = UUID.randomUUID().toString(); // 尝试获取分布式锁 boolean lockAcquired = jedis.setnx(lockKey, lockValue) == 1; if (lockAcquired) { try { // 设置锁的过期时间,防止死锁 jedis.expire(lockKey, 10); // 锁 10 秒过期 // 执行支付操作 processPayment(requestId); } finally { // 释放锁 String currentValue = jedis.get(lockKey); if (lockValue.equals(currentValue)) { jedis.del(lockKey); } } } else { // 锁已存在,说明操作正在执行中,拒绝重复执行 System.out.println("操作正在进行,请稍后再试"); }
优缺点
优点:
- 强一致性:通过 Redis 锁机制,保证每次只有一个请求能够成功执行操作,避免重复执行。
- 分布式支持:适用于分布式系统,多个节点可以共享同一个锁。
缺点:
- 死锁问题:如果锁没有正确释放,可能会造成死锁。可以通过设置锁的过期时间来避免死锁。
- 性能损耗:每个请求都需要通过 Redis 进行网络交互来获取和释放锁,性能开销较大,尤其是高并发情况下。
3. 使用 Redis + 唯一标识符(UUID)防止重复请求
除了 SETNX
和分布式锁外,还可以通过生成唯一标识符(UUID)来实现幂等性控制。每个请求使用一个唯一的 ID,当请求到达后,Redis 会检查这个唯一 ID 是否已经处理过,避免重复操作。
工作原理
- 请求生成一个唯一的 UUID。
- 将这个 UUID 存储到 Redis 中,作为标识符。
- 如果该 UUID 已经存在,说明该操作已经执行过,拒绝重复操作。
- 否则,将 UUID 存储到 Redis 并标记操作已完成。
示例代码
// 生成请求的唯一标识符(UUID) String requestId = UUID.randomUUID().toString(); String processedKey = "processed:" + requestId; // 判断请求是否已处理 if (!jedis.exists(processedKey)) { // 执行操作 processPayment(requestId); // 标记请求已处理 jedis.set(processedKey, "true"); jedis.expire(processedKey, 60 * 5); // 设置过期时间5分钟 } else { // 请求已处理,拒绝重复执行 System.out.println("请求已处理,避免重复支付"); }
优缺点
优点:
- 简单且高效:通过 UUID 标识符,简单地实现了幂等性控制,操作非常快速。
- 无锁机制:不像分布式锁那样依赖锁机制,不会有死锁问题,避免了分布式锁的复杂性。
缺点:
- 可能有一定的误判:如果标识符的过期时间设置不合理,可能会导致某些操作在标识符过期后被重新执行。
- 依赖于 Redis 的存储:标识符存储在 Redis 中,如果 Redis 宕机或数据丢失,可能导致幂等性保证失效。
Redis的碎碎念 文章被收录于专栏
Redis面试中的碎碎念