概念
接口幂等性就是用户对于统一操作发起的一次请求或多次请求的结果是一致的,不会因为多次点击而产生了副作用。
哪些场景需要保证接口的幂等性?
- 用户多次点击按钮。
- 用户页面回退再次提交
- 微服务之间相互调用,由于网络波动卡顿,导致feign触发重试机制。
- 其他情况...
天然幂等情况
以sql为例:
对于select * from table where id =?这种场景,无论执行多少次,都是幂等的。
update table set col =? where col2 = ?,也是幂等的
delete from table where id =? 也是幂等的
insert into table(id,name....) values (1,name,...),如果id是唯一主键,那么该操作也是幂等的。
幂等解决方案
token机制
1.服务端提供发送token接口,需要幂等的接口,就在执行业务前,获取token,服务器将token保存到redis中。
2.然后调用业务接口时,将token放在请求头中。
3.服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。
4.如果判断token不在redis中,就是重复操作,直接返回重复标记给client,就保证业务不被重复执行。
危险性:
1.先删除token还是后删除token
- 先删除,网络闪断等原因导致业务确实没有执行,然后触发重试机制,由于防重设计,请求不能被执行
- 后删除,业务处理成功,但服务闪断,出现超时,没有删除token,别人继续重试,导致业务执行多次。
- 最好的设计为先删除token,如果业务调用失败,重新获取token再次请求。
2.token获取、比较和删除必须为原子性
redis.get(token)、redis.equals、redis.del(token),如果这些操作不是原子性的,高并发情况下,可能get到同样数据,判断都成功,继续业务
可以使用lua脚本保证redis操作的原子性
if redis.call('get',KEYS[1]) == ARGV[1]
then return redis.call('del',KEYS[1])
else return 0 end
锁机制
悲观锁
select * from table where id = ? for update;
悲观锁使用时一般伴随着事务一起使用,数据库锁定的时间可能会有点长。
注意:id必须时主键或者时唯一索引,不然会导致锁表的结果。
乐观锁
主要适用于读多写少的场景。
更新场景:
例如:update table set col = ? ,version= version+1 where id=? and version =1
可以根据version版本号,操作的时候需要带上version。
当第一次执行后,version变成2后,再次执行上述sql,where条件不成立,也保证了幂等性。
分布式锁
多个机器同时处理相同数据,我们可以加上分布式锁(redis或者zookeeper等),同一时间,只有一个机器能拿到分布式锁,执行业务,处理完成后,释放锁,获取到锁的时候必须判断该业务是否处理过,如果是,则不处理。
唯一约束
数据库唯一约束
插入数据,应该按照唯一索引进行插入,相同的唯一索引只能有一条,可以在数据库中防止重复。但是要保证在同一个业务下发多次请求都生成全局唯一的主键。
分库分表场景时,要保证相同请求落地到同一数据库同一张表。
redis set集合防重复
计算数据的MD5,放入set集合中,每次处理,先看MD5是否已经存在,存在则不处理。
防重表
专门新建一个数据表作为防重表。处理业务时,先将唯一索引(例如订单号)插入防重表,在进行业务操作,并且在同一事务中。
全局请求唯一id
调用接口时,生成一个唯一id,redis将id存在set中,存在则处理过。
可以使用nginx设置每一个请求的唯一id。
proxy_set_header X-Request-Id $request_id
此外,我们通过$request_id 实现客户端->网关服务器->微服务集群A->>微服务集群B.... 实现日志串联。通过trace_id回显,跟踪每次调用路由。