面对分布式架构和微服务复杂的系统架构和网络超时服务器异常等带来的系统稳定性问题,分布式接口的幂等性设计显得尤为重要。本文简要介绍了几种分布式接口幂等性设计实现,包括Token去重机制、乐观锁机制、数据库主键和状态机实现等,以加深理解。
1、分布式接口幂等性相关概念
1.1 什么是幂等性
幂等性来源自数学领域,数学上的幂等性是指对于某一元运算为幂等的操作,在任意元素上多次执行的结果是相同的。例如,函数f(x) = f(x)对于任意的x,在x上的第一次和第二次执行可以得到相同的结果。
在HTTP/1.1规范中幂等性的定义如下:
Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
在HTTP协议中,HTTP GET是一个清晰的幂等操作,HTTP DELETE/POST是非幂等的,HTTP PUT也是幂等的,因为对同一个URI进行多次PUT的side-effetcs是一致的。
在分布式架构或者微服务架构中,由于分布式自身的时序问题以及系统网络的稳定性,接口具有成功、失败和无响应的三种状态,为了提供系统的可用性,重复提交是不可避免的,而重试就会引发幂等性的问题。
1.2 幂等性的使用场景
分布式接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。幂等性适用于以下场景:
- 前端重复提交:在订单系统中用户在前端提交订单,快速重复点击多次,造成后端生成多个内容重复的订单,但是后台应该只产生一个订单。
- 接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。防止外部多次调用对系统数据状态的发生多次改变,将服务接口设计成幂等,就是为了防止多次重试造成系统不一致的问题。比如账户扣款操作超时重试了多次,理应只扣款一次。
- 消息重复消费:MQ消息中间件,消息重复消费,相同请求条件下这次消费的结果与下一次应该保持一致。
1.3 分布式接口幂等性的实现方案
接口幂等性的解决方案可以在客户端和服务端实现,但是客户端控制效果不佳,比如按钮置灰、不可点击等,由于涉及到多设备兼容性以及接口调用的问题,并不能真正实现幂等。因此安全的措施还是从后端接口层进行控制,有以下几种方案:
- Token去重:根据业务的操作和内容生成一个Token值(全局唯一ID),在执行操作前先根据这个全局唯一ID进行校验,来判断这个操作是否已经执行。如果存在则表示该方法已经执行。
- 乐观锁机制:适用于更新操作。在查询和删除操作中使用乐观锁机制,保证一次处理结果,避免重复操作。设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等。
- 数据库主键:适用于插入时的幂等性。利用数据库中主键唯一约束的特性,保证一张表中只能存在一条带该唯一主键的记录。
- 状态机幂等:根据业务表的状态特性设计,只支持状态的单向改变,在执行的时候加上状态信息,实现幂等。
幂等性设计简化了客户端的处理逻辑,却增加了服务端逻辑处理和设计上的复杂性,增加额外控制幂等的业务逻辑的同时,将并行执行改为串行降低了执行效率。
2、几种接口幂等性方案介绍
2.1 Token去重
Token机制是通过在服务端生成一个唯一的Token,并将其存储在客户端中,来保证多个客户端之间对同一个服务的请求结果的一致性。Token机制的实现原理如下:
- 服务端生成Token:服务端需要生成一个唯一的Token,可以使用时间戳、随机数等信息来生成。生成Token后,将其存储在服务端的数据库中。
- 客户端获取Token:客户端在每次请求服务时,需要向服务端发送一个请求Token。请求Token是服务端根据Token生成的唯一标识,客户端通过该Token来识别自己的身份,并在服务端的数据库中查找对应的Token。
- 如果找到了对应的Token,则说明该请求是第一次请求,服务端将其存储在数据库中,并返回一个唯一的标识符;如果在数据库中找不到该Token,则说明该请求是重复请求,服务端不返回任何结果,并提示用户重新操作。
- 如果在数据库中也找不到该Token,则说明该请求是幂等请求,服务端可以直接返回结果,不做任何操作。
Token机制的优点是实现简单、易于部署和维护,能够保证分布式系统的幂等性。但是,它也存在一些局限性,例如需要在服务端和客户端之间传递Token,可能会导致性能问题;另外,如果Token被滥用,也可能会带来安全问题。因此,在使用Token机制时,需要根据具体情况进行权衡和选择。
2.2 乐观锁机制
数据库乐观锁方案一般适用于更新操作的幂等性,实现逻辑是在对应的数据表中添加一个version字段,作为当前数据的的版本标识。这样每次对这条数据执行更新时,都会将该版本标识作为一个条件,值需要为上次待更新数据中的版本标识的值。
1)先根据条件查询数据,得到对应的版本号version
select version from tablename where xxx
2)更新数据时带上版本号version,只有版本号匹配才会更新数据,如果不匹配则不更新
update tablename set count=count+1, version=version+1 where version=#{version}
3)更新数据的时候,同时需要更新数据对应的版本号version,这样可以解决ABA问题。
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
乐观锁机制实际上是牺牲了并发性来实现更新操作的幂等性,在并发场景下会导致大量的锁冲突等待和性能问题。
2.3 数据库主键
数据库唯一主键机制是利用主键的唯一性约束,适用于插入操作的幂等性,当插入主键重复的数据时会抛出异常,保证数据的一致性。表结构设计如下所示:
CREATE TABLE `t_check` (
`id` int(11) NOT NULL COMMENT 'ID',
`serial_no` varchar(255) NOT NULL COMMENT '唯一序列号',
`source_type` varchar(255) NOT NULL COMMENT '资源类型',
`status` int(4) DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`)
UNIQUE KEY `key_s` (`serial_no`,`source_type`) COMMENT '保证业务唯一性'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='幂等性校验表';
唯一主键UNIQUE KEY的关键性字段如下:
- serial_no:唯一序列号的值,在分布式架构下是全局唯一的ID
- source_type:业务类型,区分不同的业务,订单,支付等。
具体处理逻辑如下图所示:
2.4 状态机实现幂等
对于很多业务是有业务流转状态的,如订单的待提交,待支付,已支付,取消等,在业务逻辑处理的时候只支持状态的单向改变。业务表在设计的时候增加状态字段status,这样在更新的时候加上“status=期望的status”,多次调用的话实际也只会执行一次。
update xx where id=1 and status=1
3、总结
分布式架构下幂等性是保证接口能够重复执行的重要机制,幂等性和防重又有所不同,防重是在第一次请求已经成功的情况下人为多次重复操作导致的状态改变,幂等性是在不确定第一次请求结果的情况下,发起多次请求不会出现状态的变化。实际使用中,通过数据库主键的唯一性可以实现幂等性和防重,乐观锁的version机制能够实现并发更新下的幂等性,也可以通过数据库悲观锁机制在业务操作前获取锁资源实现唯一性操作。总而言之,分布式接口的幂等性是在牺牲一定的并发和性能的前提下,以实现系统的稳定性和容错性。
参考资料:
- https://blog.csdn.net/tengxvincent/article/details/81773745
- https://www.cnblogs.com/jajian/p/10926681.html
- https://blog.csdn.net/qq_41863849/article/details/123973348
- https://zhuanlan.zhihu.com/p/70748661