1. 幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因多次点击而产生副作用。比如支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,流水记录变成2条,这就没有保证接口的幂等性
2. 哪些情况需要防止
- 用户多次点击按钮
- 用户页面回退再次提交
- 微服务之间相互调用,由于网络问题,导致请求失败。feign触发重试机制
- 其他业务情况
3. 哪些情况需要幂等
以SQL为例
- 幂等
- select * from table where id=?
- update tab1 set col1=1 where col2=2
- delete from user where user_id=1
- insert into user(user_id,name) values(1,‘a’),user_id为唯一主键
- 不幂等
- update tab1 set col1=col1+1 where col2=2
- insert into user(user_id,name) values(1,‘a’),user_id不是主键
4. 解决方案
1. token机制
-
服务端提供了发送token的接口。在执行业务之前,先去获取token,服务器会把token保存到redis
-
然后调用业务接口请求,把token携带过去,一般放在请求头
-
服务器判断token是否存在redis中,存在则表示第一次请求,然后删除token,执行业务
-
如果判断token不存在redis中,就表示重复操作,直接返回重复标记给client
-
危险性:
-
先删除token还是后删除token
- 先删除可能导致,业务确实还没有执行,重试带上了之前的token,由于防重设计导致,请求还是不能执行
- 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,继续重试,导致业务执行2次
- 最好设计为先删除token。如果业务调用失败,就重新获取token再次请求
-
Token获取、比较和删除必须是原子性的
-
如果不是原子的,可能导致高并发下,都get到相同的数据,判断都成功,继续业务并发执行
-
可以在redis中使用lua脚本来完成这个操作
if redis.call('get',KEYS[1]==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end)
-
-
2. 各种锁机制
1. 数据库悲观锁
- select * from xxx where id=1 for update
- 悲观锁使用时一般伴随着业务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
- id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦
2. 数据库乐观锁(主要用于处理读多写少的问题)
-
适合在更新场景中
update t_product set count = count - 1.version = version + 1 where product_id=1 and version=1
3. 分布式锁
如果多个机器可能在同一时间处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁,获取到锁的必须先判断这个数据是否被处理过。
3. 各种唯一约束
1. 数据库唯一约束
插入数据应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入
2. redis set防重
比如可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理
4. 防重表
使用订单号作为去重表的唯一索引,把为索引插入去重表,在进行业务操作,且他们同一事务中。去重表和业务表应该在同一库中,这样就保证了在同一事务即使业务操作失败了,也会把去重表的数据回滚,这个很好的保证了数据的一致性
5. 全局请求唯一id
接口调用时,生成一个唯一id,redis将数据保存在集合中去,存在即处理过。
可以使用nginx设置每一个请求的唯一id
proxy_set_header X-Request-Id $request_id;