目录
简介
如何解决幂等性
1、 前端做拦截
2、数据库层面解决
2.1 insert语句前先select
2.2 悲观锁
2.3 乐观锁
2.4 加唯一索引
2.5 防重表
3、业务代码层面
3.1状态机
3.2 业务代码中使用唯一标识符
3.3 接口的合理性设计
4、前后端间交互实现
4.1 token机制
接口幂等性就是说用户使用相同的参数请求同一个接口无论是一次还是多次都应该是一样的。不会因为多次的点击产生不同效果。就相当于同样的消息进行了重复消费。
举个栗子:一个用户在手机APP上提200块钱,然后一不小心点击了两次,那么就应该只提取出200块钱,不应该出来400(当然,真实场景下取钱操作是一个复杂事务,不可能一个接口点击就出来了)。对于这种场景下,即使用户点了两次也应该只取出一次的钱,就是接口幂等性。
什么情况下会出现接口幂等性问题?
对于直接和接口做交互的部分(Web、App)做一层拦截,例如禁止表单重复提交、点击按钮后按钮置灰等操作。
这种解决方法只能是针对于普通用户的常规操作而言,并不会覆盖全场景。很多恶意攻击者都会直接去访问你的后台代码试图写入脏数据。
这种单独前端类型的校验其实是不靠谱的,就像Restful风格设计,虽然人们都知道这个,但是并非人人都会遵守,况且根据这个非强制性要求的风格建议去保证幂等性也不可能。在Restful风格中只有Get才是官方认定幂等性,但是单纯的获取数据的接口,人们通常也不会关心其幂等性
优点:实现简单
缺点:局限性大,效率低
对于新写入数据类型的业务场景,可以在新增数据的时候先select一下关键的字段(操作类型、设备id、设备等),如果存在就update,否则insert。此方法针对局限性小的业务,基本上效率极低,不推荐使用
优点:严格保证防重复
缺点:用事务锁死,效率低,后续大量接口会按序请求,积攒接口请求。不适合高并发
使用sql锁住单行数据
select * from user id=123 for update;
这个语句就是锁定了user表中id为123的数据行,保证其他人不会使用
注意点:
1、mysql使用innodb引擎,这个支持事务
2、要锁住的字段要是主键或者唯一索引,不然会锁表
优点:比悲观锁效率高
缺点:数据从0到1的时候应该如何判断,这点我还没想明白过来
要修改的数据行中加入字段timestamp/version
要执行修改数据前,先查询此数据,
select id, name, update_time from user id=123;
如果别人改了不发生修改(默认别人的修改是正确的)
update user set name = "zhangsan"
where id=123 and update_time = 1695803269;
靠这种方式,如果update_time不是最开始查询的数值,那么写入时也会失效,保证了只会有一个数据对数据进行修改
优点:效率高,一次sql,可以防重
缺点:防重过滤压力在数据库上,访问量大容易导致同库访问效率下降;需要单独写一个错误捕捉返回业务错误给前端
在加入唯一索引后,在业务代码中根据规则生成不可重复的code码,但是相同参数请求接口是一摸一样的
alter table `user` add UNIQUE KEY `un_code` (`code`);
这样在插入更新新数据的时候,如果code相同,会在sql层面进行报错,保证插入数据的唯一性
优点:即使针对同一个表,可能不同操作的防重需求也不同,用此方式可以灵活根据业务进行防重
缺点:加了一个表,存储维护成本上升。业务逻辑复杂性提升
新建一个数据库表,专门用来防重判断用。
具体步骤就是
1、数据来了 我要去写数据了,根据业务请求参数生成唯一code
2、使用code去insert防重表:成功,执行业务数据insert。失败,返回业务失败。
优点:从业务代码进行去重,数据库无压力
缺点:业务代码复杂性上升
这个状态是针对于业务的,很多实际的业务表中有状态顺序。例如电商系统1-下单 、 2-已支付、 3-完成、 4-撤销等状态。根据这些状态字段进行数据更新时,就可以保证是唯一了。
update `dingdan` set status=3 where id=1 and status=2;
使用唯一标识符:为每个操作生成一个唯一的标识符,将该标识符作为参数传递给接口。在接收到重复请求时,检查该标识符是否已被处理,如果已处理,则直接返回结果而不执行具体的操作。
合理设计接口的请求和响应格式,不要在请求中包含非幂等操作的字段或参数。接口返回结果中应包含用于判断重复请求的信息,如操作状态码或标识符。
优点:redis的读取高并发高,数据处理并未依靠业务代码和数据库,鲁棒性高。
缺点:引入redis复杂性提升
这种方案也算是分布式锁的一种了,属于常规解决方案,引入redis实现这一功能。