幂等性解决方案
什么是幂等性
幂等性就是一次请求和多次请求的效果一样,在计算机中就是一次请求和多次请求的参数和响应结果都是一样的。
为什么需要幂等性
- 调用其他的接口,可能由于网络抖动,会出现超时重试策略,那么会重复发几次请求,譬如dubbo的超时重试机制。
- 消息队列重复消费问题,譬如rocketmq。
- 网络不好,客户端多点了几次表单提交。
如何保证幂等性
一锁、二判、三更新
锁:主要是为了防止在分布式环境下,针对服务和接口可能出现并发问题。
判:主要是为了判断是否已经有重复记录了。
更新:就是将数据更新持久化的数据库。
锁
分布式环境未使用锁的问题
分布式环境下,为了保持不同的请求被代理服务器负载均衡到不同的机器上,那么可能出现多个请求打在不同的机器上,出现数据不一致的问题,甚至还会出现ABA的问题。
redisson分布式锁方案
导包
<!--使用redisson作为分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version>
</dependency>
yml配置
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 1800000
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 5
min-idle: 0
初始化RedisClient的bean
@Configuration
public class InitRedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;
@Value("${spring.redis.database}")
private Integer redisDatabase;
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
// 创建配置 指定redis地址及节点信息
Config config = new Config();
String redisAddr = "redis://" + redisHost + ":" + redisPort;
config.useSingleServer()
.setAddress(redisAddr)
.setPassword(null)
.setDatabase(redisDatabase);
// 根据config创建出RedissonClient实例
return Redisson.create(config);
}
}
获取锁例子
public void testRedissionCase() {
// 锁名称
String redisLock = "SPY-LOCK";
// 获取锁
RLock rLock = redissonClient.getLock(redisLock);
try {
// 获取非阻塞锁
boolean flag = rLock.tryLock(10, 30, TimeUnit.SECONDS);
if (!flag) {
log.info("++++++++++++++++++++++++++> 没有获取到锁");
return;
}
// 业务代码。。。
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
rLock.unlock();
}
}
判
为什么判断是否重复问题
这步主要是为了判断是否已经有重复记录;如果已经有重复记录,那么直接更新;如果没有重复记录,那么直接插入记录。
判断是否重复的方案
方案1. 防重表
由于系统中会调用幂等号,就是根据一笔业务生成的不变的编码,只有不同业务的情况,幂等号才不同。我们需要设计一个幂等表,它脱离于业务表,幂等表中设计一个幂等号字段,设置为唯一索引的特性。我们去根据幂等号查询记录的时候,发现幂等号已经存在,那么就直接返回。
方案2. 状态幂等
需要入侵业务表,根据业务表的状态进行判断是否更新,如果状态为已更新,那么其他的重复请求就不能再更新。
方案3. token + redis
客户端第一次请求,请求里面携带幂等号,服务器程序将幂等号作为key,业务处理的结果作为value,存放到redis中,并且设置了过期时间,同时其他的请求携带相同的幂等号进行业务处理的时候,服务器程序发现redis中幂等号这个key已经存在,那么直接返回对应的value值,如果幂等号这个key不存在,那么继续进行业务处理,将处理结果存放到value,并且返回处理结果。
自定义注解实现
注解:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotRepeatSubmit {
/**
* token存活时间
* 默认为30天
* 单位为秒
* @return
*/
int survivalTime() default 24*60*60*30;
}
扫描注解的切面:
@Slf4j
@Component
@Aspect
public class NotRepeatSubmitAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(com.sunpeiyu.demoitem.idempotent.NotRepeatSubmit)")
public Object doNotRepeatSubmit(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取分布式锁
RLock rLock = redissonClient.getLock("SPY-LOCK");
rLock.lock(30, TimeUnit.SECONDS);
// 获取Http请求中的请求参数token
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getParameter("token");
try {
// 校验幂等号是否存在,幂等号为必输项
if (StrUtil.isBlank(token)) {
throw new RuntimeException("请传入幂等号");
}
// 获取redis中获取幂等号键
// redis中已存在对应的value情况,说明已经处理过一次请求,直接返回
Object value = redisTemplate.opsForValue().get(token);
if (!Objects.isNull(value)) {
return value;
}
// redis中键不存在情况
NotRepeatSubmit notRepeatSubmit = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(NotRepeatSubmit.class);
int time = notRepeatSubmit.survivalTime();
Object result = joinPoint.proceed();
String jsonStr = JSONUtil.toJsonStr(result);
redisTemplate.opsForValue().set(token, jsonStr, time, TimeUnit.SECONDS);
return result;
} finally {
// 释放分布式锁
// 注意,此处必须要加判断
if (Objects.nonNull(rLock) && rLock.isLocked() && rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
}
注意:
if (Objects.nonNull(rLock) && rLock.isLocked() && rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
在释放锁的时候必须要加这个锁是否存在的校验,否则可能出现异常
java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread
tryLock情况时,如果没有判断返回是否为true或false,那么可能会出现执行下面的finally中unlock方法,如果未持有锁却unlock就会报异常。