接口幂等性

订单防止重复提交 网络不好 用户点击多次

什么是幂等性

用户对于同一操作发起的一次请求或者多次请求的结果是一致的 不会因为多次点击产生副作用 比如说支付场景 用户购买了商品支付扣款成功 但是返回结果的时候网络异常 这时候钱 已经扣掉了 再次点击会进行二次扣款…这就没有保证接口的幂等性

那些情况需要防止

用户多次点击按钮

用户页面回退再次提交

微服务相互调用

由于网络问题导致请求失败 feign触发重试机制

其它业务情况

什么情况下需要幂等

以sql为例子 查询 删除 操作大部分是天然的幂等性 因为在第一次的时候已经删除成功了

叠加状态 每次执行结果都不一样则不是幂等性

幂等性解决方案

image-20200828142430124

索引 订单号 唯一

举例:五种方案

令牌机制

ytmall商城项目采用这个方案

存在问题: 如果业务执行后才删令牌 令牌仍然有可能多次进入

如果先删令牌

保证获取令牌 对比 删除 必须是 原子性

可以在redis 使用lua脚本完成这个原子操作操作

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

商城支付保证幂等性

1
2
3
4
5
6
7
@Autowired
StringRedisTemplate template;

//第一步 验证令牌 对比和删除必须保证原子性 使用脚本...
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//原子验证令牌 和 删除 0令牌校验失败 1删除成功
Long result = template.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), vo.getOrderToken());

各种锁机制

  1. 数据库悲观锁

select * from xxxx where id =1 for update;

悲观锁使用时候一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用.另外要注意的是,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦.

  1. 数据库乐观锁

这种方法适合在更新的场景中

update t_goods set count = count -1 ,version = version +1 where good_id=2 and version = 1

根据version版本,也就是在操作数据库前先获取当前商品的version版本号,然后操作的时候带上版本号.

例如:第一次操作库存的时候,得到版本为1,调用库存服务版本变成2;但是返回给订单服务出现问题,订单服务又一次发起调用库存服务,当订单服务传入的version还是1,在执行上面的sql语句的时候,就不会执行,因为version已经改变为2,where 条件不成立.这样可以保证无论调用几次,只会真正处理一次.乐观锁主要使用与处理读多写少的问题;

  1. 业务层分布式锁

如果多个机器可能统一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据.获取到锁的必须先判断这个数据是否被处理过

各种唯一约束

  1. 数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入.

我们在数据库层面防止重复,利用了数据库主键唯一约束的特性,解决了在insert场景时幂等性问题,但是主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键.

如果是在分库分表的场景下,路由规则要保证相同请求下,落地在同一个数据库和同一个表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关.

  1. redis set防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的md5放入redis的set,每次处理数据,先看这个md5是否已经存在,存在就不处理

防重表

使用订单号orderNo作为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中.这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等性.这里要注意的是,去重表和业务表应该在同一个库中,这样就保证了在同一个事务中,即使业务操作失败了,也会把去重表的数据回滚,这个很好的保证了数据一致性.

全局请求唯一ID

调用接口的时候,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过.可以使用nginx设置每一个请求的唯一id;

1
proxy_set_header X-Request-Id $request_id