接口防重和幂等性解决方案

305 阅读4分钟
  • 接口防重是指避免产生重复数据
  • 幂等性是指除了避免产生重复数据以外,还要求每次请求都返回一样的结果

如何避免产生重复数据

多次请求插入数据接口,可能会导致重复数据,解决思路可以分为前端处理和后端处理

前端处理

  • 表单提交按钮只允许点击一次
  • 表单提交后跳转页面,防止F5刷新重复提交

后端处理

唯一索引

数据字段添加唯一索引,这样重复提交也只有一次生效

衍生问题:

在逻辑删除的场景下,唯一索引会有些问题,因为逻辑删除不是DELETE操作,而是UPDATE,所以当A数据被删除,SQL语句是这样的:

UPDATE table SET delete=1;

这样会导致后面新增一模一样的数据失败,因为旧数据还存在。

不过这问题可以解决,修改一下唯一索引,将本来的唯一字段和delete逻辑删除标记字段一起做成唯一索引就好,但是delete时不能再是1和0了,不然仍然会导致新增数据无法添加,好的解决办法是删除的SQL语句改成这样:

UPDATE table SET delete=(id值);

这样即可保证有逻辑删除,还能保证唯一性约束,只是数据查询时条件得改成:WHERE delete != 0,而不是:WHERE delete != 1

防重表

创建一个防重表,业务数据插入前先插入防重表,插入成功才继续插入业务数据。防重表需要有唯一索引(注意字段不允许为NULL,否则唯一性约束会失效)。

防重表可以用于解决上面唯一索引的方案下,逻辑删除的问题(虽然已经有了解决办法)

防重表的作用是实现加锁的性能

乐观锁

乐观锁有两种实现方式:

  1. 数据库层面:一般是基于版本记录字段来实现,通过给表增加一个version字段,每次修改数据前先获取version,然后在修改数据时将version添加到WHERE条件中,如果提交的version等于获取的version,则进行更新,否则视为过期数据不允许更新

乐观锁比悲观锁少了很多的加锁开销,大大提升了并发场景下的整体性能表现

  1. Java层面:CAS操作,如果内存位置V的值等于预期的A值,则将该位置更新为新值B,返回true。CAS包含了compare和swap两个操作,它是如何保证原子性呢?答案是CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

    CAS的缺点:

    1. 一次性只能保证一个共享变量的原子性:当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就要用锁来保证原子性
    2. 循环会耗时:CAS失败会一直进行尝试,如果长时间不成功会给CPU带来很大的开销。所以应该引入退出机制
    3. 存在ABA问题:线程A读取的数据被线程B修改后又改了回来,CAS仍然是可以操作成功的,但实际上数据已经被修改过了。存在如栈顶问题:一个栈的栈顶经过两次或多次变化后又变成了原值,栈可能已经发生了变化。解决办法可以是引入版本号,每次内存中的值发生变化时版本号+1,进行CAS操作时,不仅比较内存中的值,还比较版本号

综上可知,乐观锁适合读多写少的场景,不会锁住数据导致其他线程无法访问,但是在并发冲突概率大的场景下,乐观锁更新容易频繁失败,不断重试,耗费CPU资源,此时可以考虑使用悲观锁

悲观锁

在数据处理过程中,将数据处于锁定状态

数据库层面,可以通过FOR UPDATE语句给查询到的数据进行加锁,如:

SELECT * FROM table WHERE id = 10 FOR UPDATE;

代码层面,可以通过Synchronized关键字和Lock接口相关类加锁

分布式锁

可以使用Redis分布式锁来代替防重表的性能

全局token

  1. 进入表单提交页面之前,就向后端请求token,后端将该token存到Redis中
  2. 前端提交表单的时候将该token一并提交
  3. 后端判断Redis中是否存在该token,如果存在则删除Redis中的token并执行请求,否则打回

状态机幂等

如书籍状态有三种:已预定、已借阅、已归还,那么更新状态的时候应该以顺序依次进行:

update table set 状态=已借阅 where 状态=已预订

参考资料

接口幂等 & 防重

明明加了唯一索引,为什么还是产生重复数据?

百度狂搜