一、分布式锁解决方案
先说这种方案,在网上有一些文章说可以通过分布式锁来保证幂等性。但是我认为这种方案不能保证幂等性,不可取。看下面分析
①、方案流程介绍
- 用户通过浏览器发起请求,服务端接收请求数据,并且从请求数据中获取订单号code作为唯一业务字段。
- 使用redis的set命令,将该订单code设置到redis中,同时设置超时时间。
- 判断是否设置成功,如果设置成功,说明是请求没有处理过。
- 数据操作完成后释放锁
- 如果设置失败,说明请求请求已经处理过,则直接返回成功(如果需要返回业务数据,那么这里还需要查询出业务数据然后返回)。
②、问题分析
case1、客户端连续发起两次请求(比如用户快速点击按钮的情况),第一次请求先到达服务端,然后第二次请求由于某些原因过了一会儿才到达服务端。等第二次请求达到服务端的时候,第一次请求已经执行完毕并且释放了锁。此时第二次请求仍然能加锁成功,并且执行业务逻辑。这种情况下幂等性失效。
case2、客户端发起第一次请求,服务端正常执行完毕并释放了分布式锁,但由于网络原因客户端没有正常收到服务端的响应,此时客户端再次发起请求。由于第一次请求所加的分布式锁已经过期所以第二次请求仍然能够加锁成功,让后执行业务逻辑。此时幂等性失效。
case3、客户端连续发起多次请求,这多次请求同时到达服务端,此时开始争抢锁,谁抢到锁谁就执行,其他没有抢到锁的请求都统统不执行。这种情况能保证幂等性。
所以这种方案不能保证请求幂等。
二、乐观锁解决方案
这种基于乐观锁的方式局限性太大了,并且该方案应该叫做防止重复提交解决方案,称为幂等解决方案不是那么贴切,适用面比较狭窄!
①、适用操作
- 简单更新操作
②、方案介绍
数据库乐观锁方案一般只能适用于执行更新操作
的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
- 客户端查询后端接口数据,此时返回的数据中包含版本号(比如版本号为 x)
- 客户端提交请求(带上之前查询时得到的版本号)
- 接口接收到请求后,按照提交的版本号去更新数据
- 如果更新失败则说明是重复请求,直接异常中断或者查询出上次执行的结果数据返回即可
③、优缺点
-
实现简单,只需要在表中增加一个字段即可。
-
适用面太狭窄,并且只能针对于极其简单的业务逻辑。并且版本号需要外部传入,不安全。
三、数据库唯一key解决方案
①、适用操作
- 插入操作
②、方案介绍
-
在表设计的时候我们可以规定一些从业务上唯一的字段(比如:身份证号、分布式主键ID),为这些字段建立一个唯一索引。
-
在接口做插入操作的时候,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报
Duplicate entry 'xxx' for key 'xxxxxxx
异常,表示唯一索引有冲突。 -
虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。
如果是
java
程序需要捕获:DuplicateKeyException
异常,如果使用了spring
框架还需要捕获:MySQLIntegrityConstraintViolationException
异常。
具体步骤
- 用户通过浏览器发起请求,服务端接收数据。
- 将该数据插入mysql
- 判断是否执行成功,如果成功,则操作其他数据(可能还有其他的业务逻辑)。
- 如果执行失败,捕获唯一索引冲突异常,直接返回成功。
③、优缺点分析
- 使用起来比较简单,只需要确定好哪个是唯一key,然后建立唯一索引即可
- 编码上比较麻烦,因为每个需要保证幂等的插入类型的接口都需要去做捕获
DuplicateKeyException异常
的操作,代码上比较冗余。 - 适用面不广,只能适用于插入操作,并且只能适用于简单的业务场景,对于稍微复杂的业务场景便不太适用了
- 效率不高,基于数据库的唯一key去做防重和保证插入幂等,那么相当于把压力放到了数据库上。在高并发的情况下很可能出现性能问题。
四、Redis + token解决方案
①、适用操作
- 更新操作
- 新增操作
- 这种方案比较适合于前端界面和后端接口交互的一种幂等方式
- 这种方案由于不依赖于接口内部代码进行判断,所以可以通过拦截器或
AOP切面 + 注解
的方式做的更加通用,仅用一个注解就能让某个接口保证幂等性。
②、方案介绍
- 服务端需要提供一个token获取接口(该 Token 可以是一个序列号,也可以是一个分布式
ID
或者UUID
串,反正要保证唯一性),客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。 - 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
- 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
- 客户端在执行提交表单时,把 Token 存入到
Headers
中,执行业务请求带上该Headers
。 - 服务端接收到请求后从
Headers
中拿到 Token,然后根据 Token 到 Redis 中查找该key
是否存在。 - 服务端根据 Redis 中是否存该
key
进行判断,如果存在就将该key
删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。
③、问题分析
- 这种方案不需要在业务代码里做幂等校验,通过
AOP切面 + 注解
可以做的非常通用,使用起来很方便。但需要前端多发一次请求去请求token - 如果客户端连续发起调用,只要每次使用的token是一样的,那么这些连续的请求只会被处理一次。由此可以正常保证幂等性。
- 如果某个客户端第一次发起请求,然后服务端收到后将token从Redis中删除,接着去执行业务逻辑,但是业务逻辑执行失败了,此时有两种可能:
- 此时服务端可能会向客户端返回执行失败,客户端收到该返回后自动重新请求一个token,然后再次发起请求重试。这样也没有任何问题。
- 如果此时服务端向客户端返回执行失败的过程中,由于网络或其他什么原因导致
客户端无法接收到
服务端返回的执行失败
响应。那么此时客户端会再次使用第一次申请的token
再次向服务端发送请求,但是此时服务端返回的确却是重复请求
或执行成功
- 总的来说这种方案实用性较强,没有明显的缺陷。
一般来说没有任何一种幂等方案可以适用于所有场景,我们需要按照我们的实际情况来选择合适的方案即可。我们也可以采用多种方案组合使用来保证幂等性(我们可以使用token方案 + 数据库唯一key
方案组合使用)。
五、通用幂等表方案
- 该方案是非常稳定并且简单和通用的幂等解决方案!!!推荐使用
- 基于幂等表的通用幂等组件实现,详情见:https://gitee.com/mr_wenpan/basis-enhance/tree/master/enhance-boot-idempotent
六、其他问题
1、如果是上下游接口调用如何保证幂等性呢?
比如:在上游系统调用下游系统的接口向下游接口传输数据,上游系统一般都会采用重试机制,重试调用下游接口。那么下游系统如何保证幂等性?
a)、对于新增操作
- 我们可以考虑使用数据库的唯一key方案来实现
b)、对于修改操作
- 对于
update tbl set age = 20
这种类型的场景我们不需要考虑幂等。 - 对于
update tbl set age = age + 1
类型的场景我们需要考虑幂等。
c)、通用解决方案
- 对于这种情况我们可以让上游系统每次调用接口时都传输一个唯一的key。
- 我们下游系统接收到数据以后,先不要直接操作数据库,而是先拿着这个key,通过setNX命令设置到Redis并设置一定的过期时间(比如:一个月)
- 如果设置成功表示第一次请求,可以正常执行后面的业务逻辑。
- 如果设置失败,那么证明这是重复的请求,返回给上游系统
重复调用
的提示 或者 查询组装上次返回的数据给上游系统。
{
"requestId":"3824abcxxddftb9010",
"data":[
{
"name":"zhangsan",
"age":21
},
{
"name":"zhangsan",
"age":21
}
]
}
💅 注意:方案二中Redis中的key的过期时间的设置需要仔细考量!!!