幂等性简单的说就是相同条件下,一次请求和多次重复的请求接口的运行结果是相同的。
以SQL为例,有些操作是天然具备幂等性的,它们无论执行多少次都不会改变状态。
其实判断是否具备幂等性的方法很简单,你就假设这条sql语句执行多次状态会不会改变,不会改变就具备幂等性。对于接口也是同样的方法,假设请求参数相同的情况下,执行多次,状态会不会改变。
对于主键自增的插入操作来说总是不具备幂等性的。
利用数据库主键唯一性约束的特性,在插入数据的时候,如果主键已经存在,就会插入失败,从而保证幂等性。在这种方案下,唯一主键不应该是自增的,我们需要生成一个全局唯一主键ID。
乐观锁方案, 一般适合更新操作,需要我们在数据库中多添加一个版本字段,在每次修改数据的时候,先进行版本号的比对,版本号比对成功才会进行更新操作,同时版本号也需进行修改。(相当于没修改一次数据,就升级一次版本)
如果你知道ABA的解决方案,你就很容易理解乐观锁实现幂等性。
乐观锁实现在sql语句上的体现:
update table set price = price + 100,version=version+1 where id = 1 ADN version = 1
以下订单为例
但是这里存在俩个问题
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机制是网上常用的方法,我认为还可以用另一种方式来处理幂等性问题。流程是这样的:
伪代码就是酱紫的:
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作为redis的键,去redis中查询,进行判断:
注意要设置过期时间,否则可能会导致无限量存入redis中,导致redis不能正常使用
个人认为使用mysql来做幂等性,比较适合并发不是很高的场景,因为如果是高并发场景,mysql的压力本来就比较大,为了做幂等性,我们竟然让mysql承受更大的压力,明显不太合理,这种时候使用redis会更加合适。
受限于目前的水平,如果错误或补充欢迎指出。推荐阅读一口气说出4种幂等性解决方案,面试官露出了姨母笑,写得很详细。