概念

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因 为多次点击而系统造成改变。

容易出现幂等性问题的场景

  1. 用户多次重复提交表单
  2. 恶意刷单
  3. 用户页面回退再次提交
  4. 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
  5. 消息中间件重复消费
  6. 其他业务情况

幂等性解决方案

唯一约束

利用数据库主键唯一约束的特性实现幂等,解决了在 insert 和update 场景时幂等问题。但主键 要求不是自增的主键,这样就需要业务生成全局唯一的主键。例如订单场景使用订单号作为唯一约束。

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

锁机制

1、数据库悲观锁

乐观锁主要使用于处理写多读少的场景。如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

1
select * from xxxx where id = 1 for update;

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

2、数据库乐观锁

这种方法适合在更新的场景中,需要数据库对应业务表中添加额外字段。

乐观锁主要使用于处理读多写少的问题。

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

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

我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务 version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订 单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变 为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。

3、业务层分布式锁

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

Token机制

客户端连续点击或者调用方的超时重试等情况,就可以用 Token 的机制实现防止重复提交。

我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。

流程:

  1. 服务端提供了获取token 的接口。
  2. 客户端调用接口获取Token,然后服务端存储该 Token 到 Redis ,并设置过期时间。
  3. 客户端调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  4. 服务端接收请求并判断 Token 是否存在 Redis 中,如果存在则表示第一次请求,删除 Token 继续执行业务。
  5. 如果 Token 不存在 Redis 中,则表示重复标记信息。

Token机制危险性:

1、先删除 token 还是后删除 token;
  1. 先删除可能导致,业务确实没有执行,重试还带上之前token,由于防重设计导致, 请求还是不能执行。

  2. 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,别 人继续重试,导致业务被执行两边

  3. 我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求。

2、Token 获取、比较和删除必须是原子性
  1. redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导 致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行

  2. 可以在redis使用lua脚本完成这个操作

全局唯一请求ID

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

1
proxy_set_header X-Request-Id $request_id;

分布式系统全局唯一ID生成方案

防重表

这种也是网上找的方式,没怎么用过。

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

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