幂等性:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。select
和delete
操作具有天然幂等性:select
多次结果总是一致,delete
第一次执行后继续再执行也不会对数据有影响;
一般没有幂等性而出现异常的操作:insert
操作,update
操作,混合类型操作(同时包含增删改等)。
1. 使用幂等的场景
- 前端重复提交:前端瞬时点击多次造成表单重复提交;
- 接口超时重试:接口可能会因为某些原因而调用失败,出于容错性考虑会加上失败重试的机制。如果接口调用一半,再次调用就会因为脏数据的存在而出现异常。
- 消息重复消费:在使用消息中间件来处理消息队列,且手动
ack
确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。被其他消费者重新消费时就会导致结果异常,如数据库重复数据,数据库数据冲突,资源重复等。 - 请求重发:网络抖动引发的
nginx
重发请求,造成重复调用;
2. 幂等性设计
update
操作- 根据唯一业务
id
去更新数据。 - 使用乐观锁(增加版本号或修改时间字段)。
- 根据唯一业务
insert
操作- 若该操作具有唯一业务号,则可通过数据库层面的唯一/联合唯一索引来限制重复数据;或通过分布式锁来保证接口幂等性。
- 若该操作没有唯一业务号,可以使用
Token
机制,保证幂等性。
- 混合操作(一个接口包含多种操作)
- 使用
Token
机制,或使用Token
+ 分布式锁的方案来解决幂等性问题。
- 使用
3. 解决方案
3.1 Token机制实现
通过Token
机制实现接口的幂等性,这是一种比较通用性的实现方法。
具体流程步骤:
- 客户端会先发送一个请求去获取
Token
,服务端会生成一个全局唯一的ID
作为Token
保存在Redis
中,同时把这个ID
返回给客户端; - 客户端第二次调用业务请求的时候必须携带这个
Token
; - 服务端会校验这个
Token
,如果校验成功,则执行业务,并删除Redis
中的Token
; - 如果校验失败,说明
Redis
中已经没有对应的Token
,则表示重复操作,直接返回指定的结果给客户端。
3.2 基于MySQL实现
通过MySQL
唯一索引的特性实现接口的幂等性。
具体流程步骤:
- 建立一张去重表,其中某个字段需要建立唯一索引;
- 客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中;
- 因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑;
- 如果插入失败,则代表已经执行过当前请求,直接返回。
3.3 基于Redis实现
通过Redis
的SETNX
命令实现接口的幂等性。
SETNX key value
:当且仅当key
不存在时将key
的值设为value
;若给定的key
已经存在,则SETNX
不做任何动作。设置成功时返回1
,否则返回0
。
具体流程步骤:
- 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段;
- 将该字段以
SETNX
的方式存入Redis
中,并根据业务设置相应的超时时间; - 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑;
- 如果设置失败,则代表已经执行过当前请求,直接返回。
4. 实例:自定义注解实现API幂等处理(基于Redis实现)
4.1 引入redis支持
pom.xml
引入Redis
的依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<!-- redis依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 排除lettuce包,使用jedis代替-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- aop切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>配置文件
application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
#Redis数据库索引(默认为0)
database: 0
#连接池最大连接数(使用负值表示没有限制)
jedis:
pool:
max-active: 50
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
#连接池中的最大空闲连接
max-idle: 20
#连接池中的最小空闲连接
min-idle: 2
#连接超时时间(毫秒)
timeout: 5000测试
Redis
连接1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RedisTest {
private RedisTemplate<String,String > redisTemplate;
void simpleTest() {
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String key = "RedisTemplateTest-simpleTest-001";
valueOperations.set(key,key+key);
System.out.println(valueOperations.get(key));
}
}
4.2 编码实现
- 添加幂等异常
1 | package com.example.demo; |
- 自定义幂等注解
1 | package com.example.demo; |
- 幂等切面
1 | package com.example.demo; |
4.3 幂等注解的使用
接口添加
@Idempotent
注解1
2
3
4
5
public String testApi(Integer id, String str) {
return "测试幂等API:" + id + str;
}连续调用API测试
1
curl -X POST "http://localhost:8080/test?id=1002&str=TestIdempotentParameterString"
测试结果
第一次调用/test
,会正常返回测试幂等API:1002TestIdempotentParameterString
;
并且在5
秒内,Redis
会存在key
为idempotent:testApi:1002:TestIdempotentParameterString
的唯一值。
1 | 127.0.0.1:6379> keys "idempotent:testApi:1002:TestIdempotentParameterString" |
再次调用/test
,会返回异常。
1 | { |