如何使用 redis 实现分布式幂等服务中间件

作者 : 开心源码 本文共2727个字,预计阅读时间需要7分钟 发布时间: 2022-05-12 共176人阅读

背景

在编程领域,幂等性是指对同一个系统,使用同样的条件,一次请求和重复的屡次请求对系统资源的影响是一致的。

在分布式系统里,服务通常通过 RPC 或者 HTTP 或者其余形式对外提供。不论怎么,client 调用 server 服务都是将调用数据按特定协议封装好,而后通过网络发送给 server,server 将需要返回的数据同样按特定协议封装而后通过网络发送给 client。因为网络环境的复杂性,client 在发起调用时,数据可能在到 server 链路中丢失,也可能在从 server 返回的链路中丢失。不论哪种情况,对 client 来说都是调用失败。通常 client 会发起一次重试,假如是后者,那 server 就会收到屡次完全一样的请求。假如 server 的服务不是幂等的话,即可能出现问题。

典型的例子是银行扣款服务,用函数表示为?bool withdraw(account_id, amount)?,client 发起一次调用?withdraw(1001, 10)?请求从帐户 1001 中扣除 10 元,假如发生了上图所示的第 2 种错误,这时候 server 端在帐户里已经完成了扣款,但 client 并不知道,假如重试调用?withdraw(1001, 10)?,server 端又会从 帐户 1001 扣除 10 元,显然这并不是 client 想要的。假如将 client 的这次扣款操作和后续的重试用一个统一的 id 来标识,server 针对一个 id 的相同请求只执行一次,这样即可以避免上述的问题了。也就是说扣款服务是幂等的。

为了方便 server 将服务实现成幂等的,本文详情了一种使用 redis 实现的分布式中间件方案。从上面的例子中可以看出,实现幂等服务 client 除了服务正常的参数外还需要传一个额外的 id 。这个 id 通常由 client 根据具体的业务场景决定,要求至少能保证一段时间内不会重复。

实现方案

实际上相当于实现一个特殊的分布式互斥锁,一把锁只能被一个进程锁一次,永远不释放(除非锁过期了,默认过期时间1天,这里为了叙述方便简单认为永远不释放)。

一把互斥锁被一个进程加锁后其余进程都拿不到锁,通过这种方式实现幂等性。

第一个拿到互斥锁的进程任务没有执行完就挂掉,锁又是不会释放的,其余进程也拿不到锁,导致这个失败的任务也不能被其余进程重新执行。 为了避免这种情况,将加锁的操作分成 2 步:

TryAcquire?

两种情况:

1.1 拿到了锁(锁转到 TryAcquired 状态),这时候可以执行正常的业务流程,执行完了需要再调用第二步 Confirm 明确锁已被锁住(锁转到 Confirmed 状态),这之后其余进程都拿不到这把锁;

1.2 没拿到锁,又分为三种情况:

1.2.1 锁处于 Comfirmed 状态,这种情况不应该继续业务流程解决直接返回;

1.2.2 锁处于 TryAcquired 状态,但超时时间没到,说明这个时候有其余进程拿到了锁正在进行相应的业务流程,本进程不应该执行相应的业务流程直接返回;

1.2.3 锁处于 TryAcquired 状态,但超时时间到了,说明已有其余进程拿到了锁,但很久没有 Confirm ,有可能是执行过程中挂掉了,这时候本进程应该要执行相应的业务流程,而后调用第二步 Confirm 。

Confirm?

将锁置成 Confirmed 状态,表示互斥锁被永久锁住。

锁的状态转换如下所示(expire 为 redis key 过期):

使用 Redis 实现,key 为互斥锁的标识,value 为锁的状态:

0:初始状态* -1:Confirmed 状态

其余值:TryAcquired 状态,value 为业务执行截止时间 deadline

server 在添加了保证幂等性的流程图如下(交易表示既定的业务执行流程):

省略了 redis 错误解决的分支,redis 错误 TryAcquire 直接返回 true 。

TryAcqurie 和 Comfirm 实现用伪码形容如下:

// return value:

// true 可以继续业务流程,业务流程解决完后需要调用 Confirm

// false 不能继续业务流程

TryAcquire(id, timeout) {

? ? reply = SET id (now+timeout) EX 86400 NX

? ? // 1.1

? ? if reply == 1 {

? ? ? ? return true

? ? }

? ? // 1.2

? ? reply = GET id

? ? // 1.2.1

? ? if reply == Confirmed {

? ? ? ? return false

? ? }

? ? // 1.2.2

? ? if now < reply {

? ? ? ? return false

? ? }

? ? // 1.2.3

? ? delta = now + timeout – reply

? ? new_reply = INCRBY id delta

? ? if new_reply == reply + delta {

? ? ? ? return true

? ? } else {

? ? ? ? DECRBY id delta

? ? }

? ? return false

}

Comfirm(id) {

? ? SET id -1 XX

}

timeout 的设置

timeout 应该比正常的交易时间大,否则会导致多个进程都能拿到锁不能保证幂等。但是又不能设得太大,否则会导致交易执行失败时要过很久才能重新执行交易。

原子性保证

TryAcquire 和 Confirm 都应该保证原子性,Confirm 只有一个简单的 SET 操作,这个没有问题。TryAcquire 实际上分成两步:1.1 SETNX 和 1.2 GET&SET(不是 redis 是 GETSET 命令)。 上面的伪码中 1.2 GET&SET 的 SET 换成了 INCRBY 并添加了一次返回值比较,相当于乐观锁的实现,所以 GET&SET 的原子性是 OK 的。

下面说明下为什么 1.1 和 1.2 整个过程没有保证原子性也是 OK 的:

最坏的情况下假设进程 a 进入 TryAcquire 执行完了 1.1 而后被操作系统调度出去了,此时进程 b 进入 TryAcquire 执行了整个流程拿到了锁,而后执行了一次交易。这时候进程 a 重新被调度执行,这个时候因为进程 b 升级了 deadline 甚至执行完了 Confirm,进程 a 会在 1.2.1 或者 1.2.2 处退出并且不会执行交易,假如走到了 1.2.3 并且拿到了锁说明进程 b 执行交易时挂掉了,这时由进程 a 重新执行交易也是正确的逻辑。

方案的缺陷

这个方案忽略了 redis 异常情况,这种情况下 TryAcquire 总是返回 true ,可能会使交易重复执行不能保证幂等。也可以将 redis 异常返回给调用者,由调用者根据业务场景来决定能否需要重新执行交易。

另外一种情况进程通过 TryAcquire 拿到锁后执行完了交易,但 Confirm 失败(挂掉或者者网络问题),这种情况在 dealine 到了后,其余进程依然可以拿到锁并执行交易,这时候也不能保证幂等。

参考资料

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 如何使用 redis 实现分布式幂等服务中间件

发表回复