幂等是指一次和多次请求某一个资源应该具有同样的作用。
什么是分布式的幂等
首先,我们来设想以下几个场景:
- 场景一:在 App 上确认订单的时候,点击多次没有反应,只能反复点击几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单的问题。
- 场景二:在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费的消息影响可能会非常大。
通常,为了满足高可用、高性能和高可扩展的特性,分布式系统拥有众多服务节点,但是却容易导致服务调用链冗长复杂,服务节点之间网络通信复杂度指数级增加,一处轻微网络故障就可能导致整个服务异常。
同时,考虑到硬件设备自身故障的可能性,所以分布式系统面临一个共同的问题:一个消息(任务)可能会被重复消费。因此,如何确保同一个消息单次消费和多次重复消费具有相同的效果,也就是消息的幂等性成为一个热点问题。
其他常见的分布式解决方案
- 数据库添加唯一索引,比如订单号是唯一索引,防止生产重复订单;
- 使用分布式锁,防止应用程序出现并发操作;
- 采用 Token 机制,有效防止重复提交。提交后台时带 Token 值,需要先判断 Token 是否存在,若是存在,则删除 Token,执行业务逻辑;
- 数据库通过乐观锁;(update table_name set version=version+1 where version=0)
- 数据库悲观锁,通常是通过主键或唯一索引和事务一起实现。
以上是其他常见的幂等解决方案,往往在不同的业务场景下会采用不同的方法。由于我们的主要业务是分布式存储,IO 从 Client 到 Server 的链路都不会很长,所以我们参考 Token 机制,实现了一套“新”机制来处理业务场景里存在的幂等问题。
不同状态的消息处理
如图所示,这是一个简单的任务系统,Server 成功执行了 ① 发送来的命令,但是并没有收到对应的结果。这是因为有网络因素的存储遇到问题,Client 都需要去重试任务,以排除网络不稳定带来的影响,但是在不同情况下,Server 收到重试消息时,需要有不同的应对方案。这时,可能会出现以下三种情况:
第一种情况,Server 并没有收到对应的消息,因为有网络不稳定的情况存在,Client 是需要去重试的,这时 Server 会重新收到消息并且进行处理,正常处理之后返回对应的结果。
第二种情况,Server 收到了对应的消息,还正在处理,由于其他因素导致任务执行时间过长。这是因为任务仍然在执行,所以为了保证幂等,我们需要等之前的任务执行完成后,再获取其执行的结果返回给客户端。
第三种情况,Server 收到了对应的消息,并且已经处理完成,但是网络因素导致 Client 没有收到 ② 的结果,Server 收到重试的请求之后,直接获取已经收到结果返回给 Client。
为了做到上面的功能,我们需要有一套合理的机制来保存已经收到的消息状态,并且需要在能够判断消息已经被 Client 收到结果时清理这个状态,这里我们引入了 SeqNumber 来做到这些事情。
SeqNumber 的机制
我们每次的请求都会带着一个之前已经完成的请求序号 ACK 和当前序号 Seq,Server 在处理时会根据 Seq 来判断当前消息的状态,保证一个 Seq 不会被执行多次。同时,会释放 ACK 对应的序号,因为只有在这个时候才能保证 Client 已经正确的完成了对应的请求。
我们在 Client 端有两个队列 finished 和 running,分别存有正在执行的和已经完成的任务 ID。在 Server 端有一个 map,保存了所有未清理的消息(包括正在执行,执行完成)。
我们一个请求有以下几个步骤:
- 生成一个未使用的 Seq,如图在 request 里生成了12;
- 将这个 Seq 插入 running 队列;
- 从已经完成的 finished 队列获取最小的 Seq 作为 ACK,如图 ACK=1,到这里 Client 将准备好的消息发送给 Server;
- Server 收到请求,首先会将 ACK 对应的请求的缓存释放掉,然后获取对应 Seq 的消息,如果没有就说明是新的消息,直接插入,否则根据 map 之中的状态做对应的处理(对应到上文中三种状态的处理方式);
- 完成任务之后,返回结果给 Client,到这里 Server 已经完成了任务处理;
- Client 收到了消息的结果,可以确认 Server 已经完成任务,这时候释放 finished 队列中 ACK=1 的项,然后将刚刚完成是 Seq=12 的任务移动到 finished 之中。
总结
分布式系统是一个非常庞大且复杂的系统,幂等只是其中非常重要且复杂的问题,在分布式系统的构建中,不同的系统对于幂等有着不同的需求,希望这篇文章能够对大家有所帮助。