- 接口防重是指避免产生重复数据
- 幂等性是指除了避免产生重复数据以外,还要求每次请求都返回一样的结果
如何避免产生重复数据
多次请求插入数据接口,可能会导致重复数据,解决思路可以分为前端处理和后端处理
前端处理
- 表单提交按钮只允许点击一次
- 表单提交后跳转页面,防止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,否则唯一性约束会失效)。
防重表可以用于解决上面唯一索引的方案下,逻辑删除的问题(虽然已经有了解决办法)
防重表的作用是实现加锁的性能
乐观锁
乐观锁有两种实现方式:
- 数据库层面:一般是基于版本记录字段来实现,通过给表增加一个version字段,每次修改数据前先获取version,然后在修改数据时将version添加到WHERE条件中,如果提交的version等于获取的version,则进行更新,否则视为过期数据不允许更新
乐观锁比悲观锁少了很多的加锁开销,大大提升了并发场景下的整体性能表现
-
Java层面:CAS操作,如果内存位置V的值等于预期的A值,则将该位置更新为新值B,返回true。CAS包含了compare和swap两个操作,它是如何保证原子性呢?答案是CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
CAS的缺点:
- 一次性只能保证一个共享变量的原子性:当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就要用锁来保证原子性
- 循环会耗时:CAS失败会一直进行尝试,如果长时间不成功会给CPU带来很大的开销。所以应该引入退出机制
- 存在ABA问题:线程A读取的数据被线程B修改后又改了回来,CAS仍然是可以操作成功的,但实际上数据已经被修改过了。存在如栈顶问题:一个栈的栈顶经过两次或多次变化后又变成了原值,栈可能已经发生了变化。解决办法可以是引入版本号,每次内存中的值发生变化时版本号+1,进行CAS操作时,不仅比较内存中的值,还比较版本号
综上可知,乐观锁适合读多写少的场景,不会锁住数据导致其他线程无法访问,但是在并发冲突概率大的场景下,乐观锁更新容易频繁失败,不断重试,耗费CPU资源,此时可以考虑使用悲观锁
悲观锁
在数据处理过程中,将数据处于锁定状态
数据库层面,可以通过FOR UPDATE语句给查询到的数据进行加锁,如:
SELECT * FROM table WHERE id = 10 FOR UPDATE;
代码层面,可以通过Synchronized关键字和Lock接口相关类加锁
分布式锁
可以使用Redis分布式锁来代替防重表的性能
全局token
- 进入表单提交页面之前,就向后端请求token,后端将该token存到Redis中
- 前端提交表单的时候将该token一并提交
- 后端判断Redis中是否存在该token,如果存在则删除Redis中的token并执行请求,否则打回
状态机幂等
如书籍状态有三种:已预定、已借阅、已归还,那么更新状态的时候应该以顺序依次进行:
update table set 状态=已借阅 where 状态=已预订