幂等性及各种解决方案

什么是幂等性

幂等性简单的说就是相同条件下,一次请求和多次重复的请求接口的运行结果是相同的。

那什么情况下会出现幂等性问题呢?

  1. 前端重复提交表单:如用户在提交表单的时候,由于网络波动没有及时给用户做出提交成功响应,导致用户认为没有提交成功,一直点击提交按钮,此时就会发生表单重复提交。
  2. 接口超时重试:很多远程接口调用为了防止由于网络抖动导致的请求失败,都会引入重试机制,如feign的重试机制,导致一个请求发出多次。
  3. 消息重复消费:在使用消息中间件时,一个消息可能会被重复发送,此时就会发生重复消费。

天然具备幂等性的操作

以SQL为例,有些操作是天然具备幂等性的,它们无论执行多少次都不会改变状态。

  1. 查询语句
  2. 常量更新语句,如update table1 set col1 = 1 where col2 = 2。
  3. 删除语句,如delete from user where id=2等

借用别人的一张图:
幂等性及各种解决方案_第1张图片

其实判断是否具备幂等性的方法很简单,你就假设这条sql语句执行多次状态会不会改变,不会改变就具备幂等性。对于接口也是同样的方法,假设请求参数相同的情况下,执行多次,状态会不会改变。

对于主键自增的插入操作来说总是不具备幂等性的。

幂等性解决方案

数据库主键唯一性约束

利用数据库主键唯一性约束的特性,在插入数据的时候,如果主键已经存在,就会插入失败,从而保证幂等性。在这种方案下,唯一主键不应该是自增的,我们需要生成一个全局唯一主键ID。

数据库乐观锁实现幂等性

乐观锁方案, 一般适合更新操作,需要我们在数据库中多添加一个版本字段,在每次修改数据的时候,先进行版本号的比对,版本号比对成功才会进行更新操作,同时版本号也需进行修改。(相当于没修改一次数据,就升级一次版本)

如果你知道ABA的解决方案,你就很容易理解乐观锁实现幂等性。

乐观锁实现在sql语句上的体现:

update table set price = price + 100,version=version+1 where id = 1 ADN version = 1

token机制

常规token机制

以下订单为例

  1. 用户在提交订单前先向服务器申请一个token(比如在生成订单信息返回一个token),服务器将这个token保存在redis中。
  2. 用户提交订单时,携带该token过去
  3. 服务器判断token在redis中是否存在,存在表明是第一次请求,然后删除token,继续执行业务
  4. 如果不存在,则表明是重复操作,直接返回重复操作提示给客户端,这样就保证了业务代码不会被重复执行。

但是这里存在俩个问题

1、token的获取、比较和删除必须具备原子性

不具备原子性的伪代码

if(token.equals(redis.get(token))){
	redis.del(token);
	//业务代码
}

这种代码可能导致在高并发条件下,都get到了同样的数据,都判断成功,继续业务并发执行。

为了保证原子性,我们需要将这三个操作放在一个Lua脚本中运行,lua脚本如下:

if redis.call('get',KEY[1])==ARGV[1] then return redis.call('del',KEYS[1])) 
else return 0 end

具备原子性的伪代码就应该是:

if(redis.call(luaScript) == 1){
	//执行业务代码
}else{
	//重复执行
}

当然我们也可能在删除token后执行业务代码失败,这样当用户重试携带的还是之前的token时,就会因为我们的防重设计导致不能执行业务代码。

这种情况下,我们可以在出现异常的时候,将token重新放回redis中,伪代码如下:

if(redis.call(luaScript) == 1){
	try{
		//执行业务代码
	}catch(Exception){
		redis.set(token);
	}
}else{
	//重复执行
}

这就万无一失了吗?并没有,如果在执行catch代码块还没执行前,机器宕机了,那不就歇逼了?

所以说,业务调用失败后,用户应该重新获取token然后再请求。

非常规token机制

上面的token机制是网上常用的方法,我认为还可以用另一种方式来处理幂等性问题。流程是这样的:

  1. 同样用户需要先到服务器中申请一个token,不同的是,服务器没有将token存入redis中
  2. 用户请求的时候携带上token,服务器使用redis的setnx尝试将token保存到redis中
  3. 如果保存成功的话,说明是第一次请求,执行业务代码。如果保存失败,说明是重复请求,向用户提示重复请求。

伪代码就是酱紫的:

if (redis.setIfAbsent(token, "1", 30, TimeUnit.MINUTES)){
	//执行业务代码
}else{
	//重复请求
}

当然这种方案,当业务代码执行失败时,用户携带之前token重试,也会因为我们的防重设计不能执行业务代码。我们一样可以这样写:

if (redis.setIfAbsent(token, "1", 30, TimeUnit.MINUTES)){
	try{
		//执行业务代码
	}catch(Exception){
		redis.del(token);
	}
}else{
	//重复请求
}

为了防止出现宕机这样的情况,业务调用失败还是应该让用户重新获取token再发起请求。

上游服务传递全局唯一id实现幂等性

在调用远程接口时,传递一个唯一id过去,远程服务拿着这个全局id自己的认证ID作为redis的键,去redis中查询,进行判断:

  • 存在,说明之前处理过,直接返回重复请求的错误信息
  • 不存在,将用全局id和自己的认证ID作为redis的键,保存在redis中,然后处理业务

注意要设置过期时间,否则可能会导致无限量存入redis中,导致redis不能正常使用

个人认为使用mysql来做幂等性,比较适合并发不是很高的场景,因为如果是高并发场景,mysql的压力本来就比较大,为了做幂等性,我们竟然让mysql承受更大的压力,明显不太合理,这种时候使用redis会更加合适。

受限于目前的水平,如果错误或补充欢迎指出。推荐阅读一口气说出4种幂等性解决方案,面试官露出了姨母笑,写得很详细。

这篇博客的内容如下:
幂等性及各种解决方案_第2张图片
脑图链接地址

你可能感兴趣的:(分布式)