什么是幂等性
幂等性简单的说就是相同条件下,一次请求和多次重复的请求接口的运行结果是相同的。
那什么情况下会出现幂等性问题呢?
前端重复提交表单:如用户在提交表单的时候,由于网络波动没有及时给用户做出提交成功响应,导致用户认为没有提交成功,一直点击提交按钮,此时就会发生表单重复提交。
接口超时重试:很多远程接口调用为了防止由于网络抖动导致的请求失败,都会引入重试机制,如feign的重试机制,导致一个请求发出多次。
消息重复消费:在使用消息中间件时,一个消息可能会被重复发送,此时就会发生重复消费。
天然具备幂等性的操作
以SQL为例,有些操作是天然具备幂等性的,它们无论执行多少次都不会改变状态。
查询语句
常量更新语句,如update table1 set col1 = 1 where col2 = 2。
删除语句,如delete from user where id=2等
借用别人的一张图:
其实判断是否具备幂等性的方法很简单,你就假设这条sql语句执行多次状态会不会改变,不会改变就具备幂等性。对于接口也是同样的方法,假设请求参数相同的情况下,执行多次,状态会不会改变。
对于主键自增的插入操作来说总是不具备幂等性的。
幂等性解决方案
数据库主键唯一性约束
利用数据库主键唯一性约束的特性,在插入数据的时候,如果主键已经存在,就会插入失败,从而保证幂等性。在这种方案下,唯一主键不应该是自增的,我们需要生成一个全局唯一主键ID。
数据库乐观锁实现幂等性
乐观锁方案, 一般适合更新操作,需要我们在数据库中多添加一个版本字段,在每次修改数据的时候,先进行版本号的比对,版本号比对成功才会进行更新操作,同时版本号也需进行修改。(相当于没修改一次数据,就升级一次版本)
如果你知道ABA的解决方案,你就很容易理解乐观锁实现幂等性。
乐观锁实现在sql语句上的体现:
token机制
常规token机制
以下订单为例
用户在提交订单前先向服务器申请一个token(比如在生成订单信息返回一个token),服务器将这个token保存在redis中。
用户提交订单时,携带该token过去
服务器判断token在redis中是否存在,存在表明是第一次请求,然后删除token,继续执行业务
如果不存在,则表明是重复操作,直接返回重复操作提示给客户端,这样就保证了业务代码不会被重复执行。
但是这里存在俩个问题
1、token的获取、比较和删除必须具备原子性
不具备原子性的伪代码
这种代码可能导致在高并发条件下,都get到了同样的数据,都判断成功,继续业务并发执行。
为了保证原子性,我们需要将这三个操作放在一个Lua脚本中运行,lua脚本如下:
具备原子性的伪代码就应该是:
当然我们也可能在删除token后执行业务代码失败,这样当用户重试携带的还是之前的token时,就会因为我们的防重设计导致不能执行业务代码。
这种情况下,我们可以在出现异常的时候,将token重新放回redis中,伪代码如下:
这就万无一失了吗?并没有,如果在执行catch代码块还没执行前,机器宕机了,那不就歇逼了?
所以说,业务调用失败后,用户应该重新获取token然后再请求。
非常规token机制
上面的token机制是网上常用的方法,我认为还可以用另一种方式来处理幂等性问题。流程是这样的:
同样用户需要先到服务器中申请一个token,不同的是,服务器没有将token存入redis中
用户请求的时候携带上token,服务器使用redis的setnx尝试将token保存到redis中
如果保存成功的话,说明是第一次请求,执行业务代码。如果保存失败,说明是重复请求,向用户提示重复请求。
伪代码就是酱紫的:
当然这种方案,当业务代码执行失败时,用户携带之前token重试,也会因为我们的防重设计不能执行业务代码。我们一样可以这样写:
为了防止出现宕机这样的情况,业务调用失败还是应该让用户重新获取token再发起请求。
上游服务传递全局唯一id实现幂等性
在调用远程接口时,传递一个唯一id过去,远程服务拿着这个全局id和自己的认证ID作为redis的键,去redis中查询,进行判断:
存在,说明之前处理过,直接返回重复请求的错误信息
不存在,将用全局id和自己的认证ID作为redis的键,保存在redis中,然后处理业务
注意要设置过期时间,否则可能会导致无限量存入redis中,导致redis不能正常使用
个人认为使用mysql来做幂等性,比较适合并发不是很高的场景,因为如果是高并发场景,mysql的压力本来就比较大,为了做幂等性,我们竟然让mysql承受更大的压力,明显不太合理,这种时候使用redis会更加合适。
领取专属 10元无门槛券
私享最新 技术干货