数据库幂等
数据库幂等
从读写层面来说,写请求有可能会对数据造成改变;从分层架构层面来说,数据访问层(
- 查询对于结果是不会有改变的(无)
- 删除操作需要根据
sql 语句来定义(绝对值的修改是幂等的比如delete from user where age=10 ,相对值的修改不是幂等的比如delete from user bottom 10 ) - 更新操作需要根据
sql 语句来定义(比如update user set age=10 where uid=20 (绝对值修改)是幂等的,而update user set age++ where uid=20 (相对值的修改)是非幂等的) insert 基本上不是幂等, 除非表设计了一些约束(比如唯一主键等)
要实现幂等技术就是要在数据服务层保证幂等。
- 数据表使用唯一主键(unique)或者联合主键保证
token 机制(redis+token) :数据提交前要向服务的申请token ,token 放到redis ,token 有效时间;提交后后台校验token ,同时删除token ,完成相关业务逻辑。- 通过数据库悲观锁和乐观锁机制
- 分布式锁
- 根据业务进行状态机幂等设计
insert 前先select
通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在

该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。我在这里提一下,是为了避免大家踩坑。
加悲观锁
在支付场景中,用户
update user amount = amount-100 where id=123;
如果出现多次相同的请求,可能会导致用户
select * from user id=123 for update;

具体步骤:
- 多个请求同时根据
id 查询用户信息。 - 判断余额是否不足
100 ,如果余额不足,则直接返回余额不足。 - 如果余额充足,则通过
for update 再次查询用户信息,并且尝试获取锁。 - 只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
- 第一个请求获取到锁之后,判断余额是否不足
100 ,如果余额足够,则进行update 操作。 - 如果余额不足,说明是重复请求,则直接返回成功。
需要特别注意的是:如果使用的是
在这里顺便说一下,防重设计和幂等设计,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
加乐观锁
既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个
select id,amount,version from user id=123;
如果数据存在,假设查到的
update user set amount=amount+100,version=version+1
where id=123 and version=1;
更新数据的同时
update user set amount=amount+100,version=version+1
where id=123 and version=1;
该

具体步骤:
- 先根据
id 查询用户信息,包含version 字段 - 根据
id 和version 字段值作为where 条件的参数,更新用户信息,同时version+1 - 判断操作影响行数,如果影响
1 行,则说明是一次请求,可以做其他数据操作。 - 如果影响
0 行,说明是重复请求,则直接返回成功。
加唯一索引
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。
alter table `order` add UNIQUE KEY `un_code` (`code`);
加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报 Duplicate entry '002' for key 'order.un_code
异常,表示唯一索引有冲突。虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。如果是

具体步骤:
- 用户通过浏览器发起请求,服务端收集数据。
- 将该数据插入
mysql - 判断是否执行成功,如果成功,则操作其他数据(可能还有其他的业务逻辑
) 。 - 如果执行失败,捕获唯一索引冲突异常,直接返回成功。
建防重表
有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。针对这种情况,我们可以通过建防重表来解决问题。该表可以只包含两个字段:

具体步骤:
- 用户通过浏览器发起请求,服务端收集数据。
- 将该数据插入
mysql 防重表 - 判断是否执行成功,如果成功,则做
mysql 其他的数据操作(可能还有其他的业务逻辑) 。 - 如果执行失败,捕获唯一索引冲突异常,直接返回成功。
根据状态机
很多时候业务表是有状态的,比如订单表中有:
update `order` set status=3 where id=123 and status=2;
第一次请求时,该订单的状态是已支付,值是

具体步骤:
- 用户通过浏览器发起请求,服务端收集数据。
- 根据
id 和当前状态作为条件,更新成下一个状态 - 判断操作影响行数,如果影响了
1 行,说明当前操作成功,可以进行其他数据操作。 - 如果影响了
0 行,说明是重复请求,直接返回成功。
主要特别注意的是,该方案仅限于要更新的表有状态字段,并且刚好要更新状态字段的这种特殊情况,并非所有场景都适用。
加分布式锁
其实前面介绍过的加唯一索引或者加防重表,本质是使用了数据库的分布式锁,也属于分布式锁的一种。但由于数据库分布式锁的性能不太好,我们可以改用:
目前主要有三种方式实现
setNx 命令set 命令Redission 框架
具体流程图如下:

具体步骤:
- 用户通过浏览器发起请求,服务端会收集数据,并且生成订单号
code 作为唯一业务字段。 - 使用
redis 的set 命令,将该订单code 设置到redis 中,同时设置超时时间。 - 判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。
- 如果设置失败,说明是重复请求,则直接返回成功。
需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费
获取token
除了上述方案之外,还有最后一种使用
- 第一次请求获取
token - 第二次请求带着这个
token ,完成业务操作。
具体流程图如下:
第一步,先获取

第二步,做具体业务操作。

具体步骤:
- 用户访问页面时,浏览器自动发起获取
token 请求。 - 服务端生成
token ,保存到redis 中,然后返回给浏览器。 - 用户通过浏览器发起请求时,携带该
token 。 - 在
redis 中查询该token 是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。 - 如果存在,说明是重复请求,则直接返回成功。
- 在
redis 中token 会在过期时间之后,被自动删除。
以上方案是针对幂等设计的。如果是防重设计,流程图要改改:
