1、接口幂等性
一、接口幂等性
接口幂等性就是用户对同一操作发起了一次或多次请求的对数据的影响是一致不变的,不会因为多次的请求而产生副作用。
接口不做幂等处理会怎样?
支付场景:用户购买商品后,发起支付操作,支付系统处理支付成功后,由于网络原因没有及时返回给用户结果,其实这个时候订单已经扣过款,相应的支付流水也都已经生成,这个时候用户又点击支付操作,此时会进行第二次扣款,扣款成功后返回给用户。用户去查看支付订单和流水会发现自己支付两次,完蛋了要用户被投诉了,这就是接口没有处理幂等造成的。
二、各种情况下的幂等性
1、查询操作
select
查询操作不会产生副作用,天然自带幂等性
2、新增操作
情况1:自增主键插入,不具备幂等性
多次执行时,每次操作都会插入一条数据,对结果集产生影响
情况2:业务主键插入,具备幂等性
多次执行时,除了第一次操作会插入数据,其他操作都不会插入数据(前提是每次插入的业务主键一样)
3、删除操作
第一种情况:绝对删除,具有幂等性。
delete from order where id = 3
无论该sql执行多少次,对结果集产生的效果都是一样只删除了一条数据。
第二种情况: 相对删除,不具有幂等性。
delete from order where id > 23
该操作每执行一次,对结果集产生的结果,可能都不一样,同一操作多次执行对数据产生了副作用。
4、更新操作
第一种情况:绝对更新,具有幂等性。
update good set stock= 586 where goodId = 10
该操作无论执行多少次操作对结果的影响都是一样。
第二种情况:相对更新,不具有幂等性。
update good set stock = stock+10 where goodid= 10
每次执行该操作库存数量都会加10,所以不具备幂等操作。
总结:以上都是基于单库,单表的操作幂等性的分析,其实在具体业务当中,可能要设计多个表,多个库,甚至跨服务操作。比如分布式系统中,我们一个接口,可能需要调用多个服务来完成任务。那么这种情况,如何保证接口的幂等性呢?
三、解决方案
1、token+redis 机制
比如订单支付场景,该支付分为两个步骤:
- 接口处理生成唯一标识(token) 存储到redis中,并返回给调用客户端。(进入页面,未调用接口时)
- 发起支付操作并附带token
- 获得分布式锁(处理并发情况)
- 判断redis中是否存在token
- 存在 执行支付业务逻辑,否则返回该订单已经支付
- 释放分布式锁
思考:为什么要加分布式锁?
原因1:在高并发请求中 ,token判断是否存在是非线程安全的,所以要加分布式锁来保证 该条件的判断为线程安全
注释:也可redis用删除操作来判断token,删除成功代表token校验通过 这个删除是原子操作的
原因2:在支付业务中,判断支付订单是否已经存在,存在说明该订单已经支付过了,不存在就执行扣款操作,如果相同操作并发两个请求来到判断条件可能两个请求都能判断支付订单不存在,造成重复扣款。 所以也要加分布式锁保证线程的安全。
@Transactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//通过lua脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + "订单号"),// 参数1: KEYS集合
orderToken); // 参数2、3、4...: ARGV集合
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功,执行业务逻辑
...
}
}
2、CAS
状态机制幂等(状态不可逆)
针对更新操作:
例如 电商订单,订单支付状态 0 待支付 , 1 支付中 , 3 支付成功 4 支付失败。
update order set status = 1 where status =0 and orderId = "201251487987"
该sql语句利用状态CAS 保证该操作的幂等。
比如要进行订单支付,上来先用CAS更新订单状态,
返回影响说为1 代表修改成功,可以支付,继续执行支付业务代码
返回影响数 0 代表修改失败,该订单已经不是待支付订单了。
3、乐观锁
背景由来:
为什么要有幂等这种场景?因为在大的系统中,都是分布式部署,如:订单业务 和 库存业务有可能都是独立部署的,都是单独的服务。用户下订单,会调用到订单服务和库存服务。
比如:订单系统:
订单服务 —> 库存服务 (PRC远程调用(服务接口))
因为分布式部署,很有可能在调用库存服务时,因为网络等原因,订单服务调用失败,但其实库存服务已经处理完成,只是返回给订单服务处理结果时出现了异常。这个时候一般系统会作补偿方案,也就是订单服务再此放起库存服务的调用,库存减1
update t_goods set count = count -1 where good_id=2
这样就出现了问题,其实上一次调用已经减了1,只是订单服务没有收到处理结果。现在又调用一次,又要减1,这样就不符合业务了,多扣了。
幂等这个概念就是,不管库存服务在相同条件下调用几次,处理结果都一样。这样才能保证补偿方案的可行性。
乐观锁方案
借鉴数据库的乐观锁机制,如:
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1