先说这种方案,在网上有一些文章说可以通过分布式锁来保证幂等性。但是我认为这种方案不能保证幂等性,不可取。看下面分析
case1、客户端连续发起两次请求(比如用户快速点击按钮的情况),第一次请求先到达服务端,然后第二次请求由于某些原因过了一会儿才到达服务端。等第二次请求达到服务端的时候,第一次请求已经执行完毕并且释放了锁。此时第二次请求仍然能加锁成功,并且执行业务逻辑。这种情况下幂等性失效。
case2、客户端发起第一次请求,服务端正常执行完毕并释放了分布式锁,但由于网络原因客户端没有正常收到服务端的响应,此时客户端再次发起请求。由于第一次请求所加的分布式锁已经过期所以第二次请求仍然能够加锁成功,让后执行业务逻辑。此时幂等性失效。
case3、客户端连续发起多次请求,这多次请求同时到达服务端,此时开始争抢锁,谁抢到锁谁就执行,其他没有抢到锁的请求都统统不执行。这种情况能保证幂等性。
所以这种方案不能保证请求幂等。
这种基于乐观锁的方式局限性太大了,并且该方案应该叫做防止重复提交解决方案,称为幂等解决方案不是那么贴切,适用面比较狭窄!
数据库乐观锁方案一般只能适用于执行更新操作
的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
实现简单,只需要在表中增加一个字段即可。
适用面太狭窄,并且只能针对于极其简单的业务逻辑。并且版本号需要外部传入,不安全。
在表设计的时候我们可以规定一些从业务上唯一的字段(比如:身份证号、分布式主键ID),为这些字段建立一个唯一索引。
在接口做插入操作的时候,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry 'xxx' for key 'xxxxxxx
异常,表示唯一索引有冲突。
虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。
如果是
java
程序需要捕获:DuplicateKeyException
异常,如果使用了spring
框架还需要捕获:MySQLIntegrityConstraintViolationException
异常。
具体步骤
DuplicateKeyException异常
的操作,代码上比较冗余。AOP切面 + 注解
的方式做的更加通用,仅用一个注解就能让某个接口保证幂等性。ID
或者 UUID
串,反正要保证唯一性),客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。Headers
中,执行业务请求带上该 Headers
。Headers
中拿到 Token,然后根据 Token 到 Redis 中查找该 key
是否存在。key
进行判断,如果存在就将该 key
删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。AOP切面 + 注解
可以做的非常通用,使用起来很方便。但需要前端多发一次请求去请求token客户端无法接收到
服务端返回的执行失败
响应。那么此时客户端会再次使用第一次申请的token
再次向服务端发送请求,但是此时服务端返回的确却是重复请求
或执行成功
一般来说没有任何一种幂等方案可以适用于所有场景,我们需要按照我们的实际情况来选择合适的方案即可。我们也可以采用多种方案组合使用来保证幂等性(我们可以使用token方案 + 数据库唯一key
方案组合使用)。
比如:在上游系统调用下游系统的接口向下游接口传输数据,上游系统一般都会采用重试机制,重试调用下游接口。那么下游系统如何保证幂等性?
update tbl set age = 20
这种类型的场景我们不需要考虑幂等。update tbl set age = age + 1
类型的场景我们需要考虑幂等。重复调用
的提示 或者 查询组装上次返回的数据给上游系统。{
"requestId":"3824abcxxddftb9010",
"data":[
{
"name":"zhangsan",
"age":21
},
{
"name":"zhangsan",
"age":21
}
]
}
注意:方案二中Redis中的key的过期时间的设置需要仔细考量!!!