什么是幂等性
HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
幂等性和防重
防重大多数情况下可以等同于幂等性,都是为了防止多次请求同一个资源造成多次调用多次状态改变。
幂等性场景
用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
什么时候需要幂等
除了天然幂等的操作(不被唯一标识限制的操作),而业务逻辑有需要防重,就需要保持其的幂等。
天然幂等性
比如在数据库中,以某一个唯一标识进行查询,新增,删除都是天然幂等的,也就是被唯一标识限制的操作都是幂等的,
幂等常见解决方案
1.token
工作原理:
服务端为需要保持幂等的操作提供token,操作提交的时候在请求中携带token给到后端,后端将请求带来的token和服务端存储的token进行比较,一致则允许其操作资源。
风险
何时删除token?
无论在业务完成前操作token,还是在业务完成后操作token,都是有对应的缺点的,先删除可能导致, 业务因为某些原因没有执行,重试时携带的是之前token,由于防重设计导致,请求不能执行。后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,别人继续重试,导致业务被执行两遍。
采用token的核心目的是保证幂等性,后删除token的风险解决起来难度很高,而先删除token的所带来的风险解决起来则相对而言是比较容易的。
优化先删除token
先删除token的思路是如果业务调用失败,就重新获取token再次请求。
但是此时先删除token依旧有着风险:
在高并发的场景下,判断入参token和后端存储的token相等的操作没有保证原子性,就像锁保护共享资源一样是有可能重复执行的,从而损失幂等性。
这个问题也是很好解决的,只要保证判断这一步的原子性就可以了。
解决方案:
redis执行lua脚本
// redis+lua脚本 原子验证令牌防止重复提交攻击
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = "现在的令牌";
// return 0 失败 1 成功
Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList("要验证的KEY"), orderToken);
根据result判断是否可以继续进行业务逻辑。
2.锁
悲观锁和乐观锁
悲观锁和乐观锁其实都是一种控制并发保证共享资源的原子性的一种思想,而不是仅仅局限于数据库上。
悲观锁
在修改数据之前对要修改的数据进行加锁操作,排除己身外其他请求获取共享资源的机会。
具体实现:
sql:for update
java:锁
乐观锁
对比悲观锁,他的不同体现在,其是在操作数据的时候再对数据的原子性进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁相比于悲观锁大大提高了程序的TPS。
具体实现:
对数据增加版本控制。
分布式锁
分布式锁和锁一样,其实就是并行边串行,也可以保证幂等性。
3.唯一约束
数据库的唯一约束
redis set:很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理。
4.防重表
使用订单号orderNo做为去重表的唯一索引, 把唯一索引插入去重表, 再进行业务操作,且他们在同一个事中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
5.全局请求唯一id
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。
网友评论