概述如今很多的服务都会基于分布式或微服务思想完成对系统的架构设计。那么在一个系统就会存在若干个微服务,而且服务间也会产生通信从而相互调用。那么既然产生了服务调用,就会存在服务调用延迟或失败的问题。当出现这种问题,服务端会进行重试等操作或客户端有可能会进行多次点击提交。如果这样请求多次的话,那最终处理的数据结果就一定要保证统一,如支付场景。此时就需要通过保证业务幂等性方案来完成。 什么是幂等幂等本身是一个数学概念。即 f(n) = 1n ,无论n为多少,f(n)的值永远为1。在编程开发中,对于幂等的定义为:无论对某一个资源操作了多少次,其影响都应是相同的。 换句话说就是:在接口重复调用的情况下,对系统产生的影响是一样的,但是返回值允许不同,如查询。 幂等性不仅仅只是一次或多次操作对资源没有产生影响,还包括第一次操作产生影响后,以后多次操作不会再产生影响。并且幂等关注的是是否对资源产生影响,而不关注结果。
以sql为例
- [backcolor=white]#此SQL无论执行多少次,虽然结果有可能出现不同,都不会对数据产生改变,具备幂等性。
- select * from table where id=1
-
- #此SQL如果id或name有唯一性约束,多次操作只允许插入一条记录,则具备幂等性。如果不是,则不具备幂等性,多次操作会产生多条数据。
- insert into table(id,name) values(1,'heima')
-
- #此SQL无论执行多少次,对数据产生的影响都是相同的。具备幂等性。
- update table set score=100 where id = 1
-
- #此SQL涉及到了计算,每次操作对数据都会产生影响。不具备幂等性。
- update table set score=50+score where id = 1
-
- #此SQL多次操作,产生的结果相同,具备幂等性
- delete from table where id = 1 [/backcolor]
复制代码 幂等性设计主要从两个维度进行考虑:​​空间​​、​​时间​​。 空间:定义了幂等的范围,如生成订单的话,不允许出现重复下单。 时间:定义幂等的有效期。有些业务需要永久性保证幂等,如下单、支付等。而部分业务只要保证一段时间幂等即可。 同时对于幂等的使用一般都会伴随着出现锁的概念,用于解决并发安全问题。
接口幂等解决方案对于幂等的考虑,主要解决两点​​前后端交互​​和​​服务间交互​​。这两点有时都要考虑幂等性的实现。从前端的思路解决的话,主要有三种:前端防重、PRG模式、Token机制。 前端防重通过前端防重保证幂等是最简单的实现方式,前端相关属性和JS代码即可完成设置。可靠性并不好,有经验的人员可以通过工具跳过页面仍能重复提交。主要适用于表单重复提交或按钮重复点击。 PRG模式PRG模式即POST-REDIRECT-GET。当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。同时防止了通过浏览器按钮前进/后退导致表单重复提交。 是一种比较常见的前端防重策略。 token机制方案介绍 通过token机制来保证幂等是一种非常常见的解决方案,同时也适合绝大部分场景。该方案需要前后端进行一定程度的交互来完成。
- 服务端提供获取token接口,供客户端进行使用。服务端生成token后,如果当前为分布式架构,将token存放于redis中,如果是单体架构,可以保存在jvm缓存中。
- 当客户端获取到token后,会携带着token发起请求。
- 服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,则完成进行业务处理,业务处理完成后,再删除token。如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。
思考但是现在有一个问题,当前是先执行业务再删除token。在高并发下,很有可能出现第一次访问时token存在,完成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。 对于这个问题的解决方案的思想就是并行变串行。会造成一定性能损耗与吞吐量降低。 第一种方案:对于业务代码执行和删除token整体加线程锁。当后续线程再来访问时,则阻塞排队。 第二种方案:借助redis单线程和incr是原子性的特点。当第一次获取token时,以token作为key,对其进行自增。 然后将token进行返回,当客户端携带token访问执行业务代码时,对于判断token是否存在不用删除,而是对其继续incr。如果incr后的返回值为2。则是一个合法请求允许执行,如果是其他值,则代表是非法请求,直接返回。 那如果先删除token再执行业务呢?其实也会存在问题,假设具体业务代码执行超时或失败,没有向客户端返回明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进行业务处理。 这种方案无需进行额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。推荐使用先删除token方案 但是无论先删token还是后删token,都会有一个相同的问题。每次业务请求都回产生一个额外的请求去获取token。但是,业务失败或超时,在生产环境下,一万个里最多也就十个左右会失败,那为了这十来个请求,让其他九千九百多个请求都产生额外请求,就有一些得不偿失了。虽然redis性能好,但是这也是一种资源的浪费。
|