在编程领域,幂等性是指对同一个系统,使用一样的条件,一次请求和重复的屡次请求对系统资源的影响是一致的。redis
在分布式系统里,服务一般经过 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 步:学习
锁的状态转换以下所示(expire 为 redis key 过时):在此我向你们推荐一个架构学习交流群。交流学习群号:821169538 ui
使用 Redis 实现,key 为互斥锁的标识,value 为锁的状态:spa
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 应该比正常的交易时间大,不然会致使多个进程都能拿到锁不能保证幂等。可是又不能设得太大,不然会致使交易执行失败时要过好久才能从新执行交易。
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 到了后,其余进程仍然能够拿到锁并执行交易,这时候也不能保证幂等。