一、什么是幂等
幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的。
二、什么是接口幂等
同一个接口,对于同一个请求,不管调用多少次,产生的最终结果都应该是一样的。
举个例子,比如账户A给账户B转账100元,最终要么成功要么失败
- 对于成功的情况,这个请求不管再请求多少次,最终的结果:A给B只成功转入了100元,不会出现重复处理的情况
- 对于失败的情况,这个请求不管再请求多少次,转账都是失败
即成功或者失败后,不管如何重试,结果都不会再发生变化,且不会重复进行操作。
三、接口幂等需要考虑的点
(1)如何确定是同一个请求
前端传递的请求携带一个requestId,同一个请求的requestId相同
(2)接口执行结果只有3种情况
对于幂等性的接口,执行结果可以抽象为下面3种情况
- 0:处理中
- 1:处理成功
- -1:处理失败
返回给调用方的结果,只能是其中一种状态。
但是最终,接口的状态只能是成功或者失败,比如刚开始状态可能返回的是0(处理中),经过调用方多次重试以后,状态必须为终态(成功或失败)
四、接口幂等的实现方案
如下,咱们可以借助一张幂等辅助表,如下,通过这张表便可实现接口幂等通用方案
- 幂等接口对于同一个requestId,此表都会产生一条记录
- 此表会记录幂等接口调用的详细信息:请求id、请求的状态、请求参数json字符串、响应结果json格式字符串
- 记录的最终状态,一定会1或者-1,对于状态为0的,经过调用方的不断重试,status会变为1或者-1
- 当status为1或者-1后,response_json即为接口最终返回值
create table t_idempotent_call
(
id varchar(50) primary key comment 'id,主键',
request_id varchar(128) not null comment '请求id,唯一',
status smallint not null default 0 comment '状态,0:处理中,1:处理成功,-1:处理失败',
request_json mediumtext comment '请求参数json格式',
response_json mediumtext comment '响应数据json格式',
version bigint not null default 0 comment '版本号,用于乐观锁,每次更新+1',
create_time datetime comment '创建时间',
update_time datetime comment '最后更新时间',
unique key uq_request_id (request_id)
) comment '幂等调用辅助表';
下面来看下,通过张表如何实现接口幂等的通用方案
五、接口幂等处理的通用流程
六、案例
模拟一个转账案例
-- 幂等调用辅助表
drop table if exists t_idempotent_call;
create table t_idempotent_call
(
id varchar(50) primary key comment 'id,主键',
request_id varchar(128) not null comment '请求id,唯一',
status smallint not null default 0 comment '状态,0:处理中,1:处理成功,-1:处理失败',
request_json mediumtext comment '请求参数json格式',
response_json mediumtext comment '响应数据json格式',
version bigint not null default 0 comment '版本号,用于乐观锁,每次更新+1',
create_time datetime comment '创建时间',
update_time datetime comment '最后更新时间',
unique key uq_request_id (request_id)
) comment '幂等调用辅助表';
-- 创建账户表
drop table if exists t_account_lesson043;
create table t_account_lesson043
(
id varchar(32) not null primary key comment '用户id',
name varchar(50) not null comment '用户名',
balance decimal(12, 2) not null comment '账户余额'
) comment '账户表';
insert ignore into t_account_lesson043 value ('1','路人1','100.00');
insert ignore into t_account_lesson043 value ('2','路人2','0.00');
七、代码实现
7.1 入参、出参
7.2 查询接口幂等辅助表是否存在记录
7.3 请求未处理过,首次处理该请求
7.4 基于MybatisPlus乐观锁插件和@Version注解实现乐观锁
@Configuration
public class MyBatisPlusConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
*/
public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型为mysql
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInnerInterceptor.setMaxLimit(-1L);
return paginationInnerInterceptor;
}
/**
* 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
*/
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
}
sql本身没有携带version = #{version},但是拼接后的条件里出现了version
2024-09-09 08:52:19.473 DEBUG 283384 --- [nio-8080-exec-4] c.i.l.i.m.I.updateById : ==> Preparing: UPDATE t_idempotent_call SET request_id=?, status=?, request_json=?, response_json=?, version=?, create_time=? WHERE id=? AND version=?
2024-09-09 08:52:19.474 DEBUG 283384 --- [nio-8080-exec-4] c.i.l.i.m.I.updateById : ==> Parameters: 100000001(String), 1(Integer), {"requestId":"100000001","data":{"fromAccountId":"1","toAccountId":"2","transferPrice":10}}(String), {"status":1}(String), 1(Long), 2024-09-09T08:52:19.458632400(LocalDateTime), cc98b59e4fa347dfa01db88d15b71b0f(String), 0(Long)
2024-09-09 08:52:19.474 DEBUG 283384 --- [nio-8080-exec-4] c.i.l.i.m.I.updateById : <== Updates: 1