什么是幂等性
接口幂等性就是用户对于统一操作发起的一次或多次请求的结果是一致的,不会因为多次点击而产生副作用。例如支付场景,用户购买商品支付成功扣款后,但是返回结果时网络异常,此时钱已经扣了,但如果此时用户再次发起同一个请求,如果进行第二次扣款,返回成功,用户的流水对于同一个订单就会有两条记录,这就是没有保证幂等性
解决方案
1. Token 机制
服务端提供发送token的接口,在需要使用幂等性的业务之前,先去获取token,服务器将token保存起来;调用业务请求接口时,将token一起传过去,服务器判断当前token是否存在,如果存在则是第一次操作,验证通过删除token并执行相关的业务逻辑;如果不存在,则表示重复操作,直接返回给用户提示,这样就保证了业务代码不会被重复执行
但是token机制存在一定的危险性,就是先删token在执行业务逻辑,还是先执行业务逻辑在删token,如果是先删可能导致业务还没执行,或者在执行中途出现了异常,重新发请求还是之前的token,由于防重设计先删除了token,系统中已经查不到token信息了就会认为不是第一次提交的,所以不能执行业务逻辑;如果是后删,业务逻辑执行成功,但是在删除token时出现异常导致没有删除,重新发请求依旧能执行逻辑代码成功;
因此,为了保证token和业务逻辑的正确,必须保证token的获取、比较删除是原子性,否则在高并发下任意一个出现异常,都有可能导致业务被执行多次;
如果使用token机制,我们可以设计为先删除token,如果业务逻辑执行失败,则重新生成token发给用户,用户再次请求时携带上新的token;或者使用Redis的lua脚本完成这个操作,从Redis中获取指定的token与前端进行比较,相等就删除令牌,不相等就返回错误,示例:
if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
2. 各种锁机制
数据库悲观锁
悲观锁使用时一般是伴随着事务一起使用,数据库锁定时间可能会很长,需要根据实际情况使用;另外需要注意,id字段必须是唯一索引或主键,不然可能会造成锁表的结果,处理起来非常麻烦。示例:
select * from table where id = 1 for update
数据库乐观锁
这种方法适合更新操作中,处理读多写少的问题。例如定义一个字段version版本,在执行操作之前,先对比版本号是否一致,一致则执行;如果服务又重新调用一次,发送的数据携带的版本号还是之前的版本号,这个是否版本号不一致则不能执行数据库的操作,这样就保证了只会真正的处理一次。示例:
update table set version = version + 1 where version = 1
业务层分布式锁
如果多个服务可能在同一时间处理相同的数据,我们就可以加分布式锁锁定此数据,处理完成后释放锁,但是获取锁的服务必须先判断这个数据是否被处理过
3. 各种唯约束
数据库唯一约束
这个机制利用数据库的主键唯一约束的特性,解决在insert场景时幂等性问题,但主键要求不自增,这就需要业务生成全局唯一的主键
Redis set 防重
例如我们可以将数据的MD5放在Redis的set中,每次处理数据,先判断这个MD5是否存在,存在就不处理;至于为啥用MD5,因为不管多大的数据使用MD5加密后都是一样长度的字符串
4. 防重表
例如使用订单号作为防重表的唯一索引,把唯一索引插入到防重表中,在进行业务逻辑,并且他们是在同一个事务中。这个保证了在重复请求时,因为防重表中有唯一约束而导致请求失败,避免了幂等性;但是需要注意,防重表和业务表应该在同一个数据库,这样就保证了在同一个事务中,即使某一个失败了,另一个表的数据也会回滚,很好的保证了数据一致性
5. 全局请求唯一ID
接口调用时生成一个唯一ID,Redis将数据保存到集合中(去重),存在即处理过