搞了个线上故障,被老板骂了....

开发 架构
本文的案例有点特殊,订单号是后端系统生成的,前后两次请求无法区分重复状态,所以系统会创建两条不同订单 ID 记录,绕过了「唯一索引约束」这个限制。

大家好,我是Tom哥。

前几天跟一位小伙伴聊天,心情特别沮丧,刚被老板骂完.....

差点丢了饭碗,还好老板没说 “滚”。

就今年这就业行情,满眼都是泪哇。

小伙伴在一家初创公司,团队规模很小,老板为了节省成本,也没配置什么豪华阵容。

他的工作时间也不长,负责交易订单,前几天接到用户投诉,「我的订单列表」有多条一模一样的订单。

虽没造成什么资损,但严重影响用户体验。

看到这里,有经验的同学可能猜到,应该是接口没做防重控制。

日常开发中,重复提交也是蛮常见问题。

比如:用户提交一个表单,鼠标点的太快,正好前端又是个新兵蛋子,没做任何控制,瞬间就会有多个请求发到后端系统。

如果后端同学也没做兜底方案的话,悲剧就发生了。

常见的解决方案是借助数据库自身的「唯一索引约束」,来保证数据的准确性,这种方案一般在插入场景用的多些。

变种方案可以考虑单独创建一个防重表。

本文的案例有点特殊,订单号是后端系统生成的,前后两次请求无法区分重复状态,所以系统会创建两条不同订单 ID 记录,绕过了「唯一索引约束」这个限制,这.....

另外,MySQL 性能也单薄了点,单机 QPS 在「千」维度,如果是面对一个高并发接口,性能也有点吃紧。

接下来,我们就来讲下,借助 Redis 来实现接口防重复提交。

技术方案

首先,我们来看下整理的流程,如下图所示:

图片

大致步骤:

1、客户端发送请求到服务端。

2、服务端接收请求,然后从请求参数中提取唯一标识。这个标识可以没有什么特殊业务含义,client 端随机生成即可。

3、服务端系统将唯一标识先尝试写入 Redis 缓存中,可以认为是加锁操作。

4、加锁失败,说明请求还在处理,此次是重复请求,可以丢弃。

5、加锁成功,继续后面正常业务逻辑处理。

6、业务逻辑处理完成后,删除加锁的标记。

7、最后,将处理成功的结果返回给客户端。

注意事项:

  • 重复提交场景一般都是在极短时间内,同时发送了多次请求(比如:页面表单重复提交),我们只认第一次请求为有效请求。
  • 锁用完后,要记得手动删除。为了防止锁没有正常释放,我们可以为锁设置一个极短的过期时间(比如 10 秒)。

项目实战

1、引入 redis 组件

实战的项目采用 Spring Boot 搭建,这里需要引入 Redis 相关依赖。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

2、redis 变量配置

application.properties 配置文件中,添加redis相关服务配置。

spring.redis.host=127.0.0.1
spring.redis.port=6379

3、定义注解类

定义一个注解,配置在需要防重复的接口方法上,提高开发效率,同时降低代码的耦合度。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented

public @interface IdempotentRule {

/**
* 业务自定义前缀
*/
String prefix() default "";

/**
* 业务重复标识
*/
String key() default "";
}

4、接口拦截器

上面定义了IdempotentRule​注解,需要通过拦截器对正常的业务方法做拦截,增加一些特殊逻辑处理。

@Aspect
@Component
@Slf4j
public class IdempotentAspect {

@Autowired
private RedisTemplate<String, Serializable> idempotentRedisTemplate;

@Around("execution(public * *(..)) && @annotation(com.onyone.idempotent.annotation.IdempotentRule)")
public Object limit(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();


Object[] params = pjp.getArgs();
String[] paramNames = signature.getParameterNames();

Method method = signature.getMethod();
IdempotentRule idempotentRule = method.getAnnotation(IdempotentRule.class);
String key = idempotentRule.key();
String prefix = idempotentRule.prefix();

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable(paramNames[0], params[0]);
String repeatKey = (String) parser.parseExpression(key).getValue(context);

try {
// 先在缓存中做个标记
Boolean lockResult = idempotentRedisTemplate.opsForValue().setIfAbsent(prefix + repeatKey, "正在处理....", 20, TimeUnit.SECONDS);
if (lockResult) {
// 业务逻辑处理
return pjp.proceed();
} else {
throw new Exception("重复提交..................");
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
// 处理完成后,将标记删除
idempotentRedisTemplate.delete(prefix + repeatKey);
}

return null;
}


}

这里,比较特殊的是提取请求的唯一标识,由于不同的业务请求唯一标识不一样。

所以,这里采用 SPEL 表达式,将规则设置能力开放出去,由业务方自己定义,比如:

@IdempotentRule(key = "#userParam.cardNumber", prefix = "repeat_")。

拦截器根据 SPEL 表达式( 如 "#userParam.cardNumber")以及请求参数对象,计算当前请求唯一标识的值,

然后将值写入 Redis 中,并设置过时间。

如果设置成功,说明是第一次请求,继续下面的业务逻辑处理;否则,判定为重复请求,直接丢弃。

5、上层业务接口

@RestController
@RequestMapping("/user")
public class UserController {
/**
* 创建一个新的用户
*/
@RequestMapping(value = "/create_user")
@IdempotentRule(key = "#userParam.cardNumber", prefix = "repeat_")
public String createUser(@RequestBody UserParam userParam) {
// 模拟业务处理

return "创建用户成功!";
}
}
@Data
public class UserParam {
private String cardNumber;
private String name;
}

测试结果

1、构造客户端请求,第一次处理成功。

图片

2、 Redis 缓存中,能查到请求设置的锁标记。

图片

3、模拟重复,连续多次快速提交请求,请求会被拦截,并抛出异常。

图片

责任编辑:姜华 来源: 微观技术
相关推荐

2022-03-23 18:00:34

循环CPU线程

2022-07-28 14:22:50

元宇宙AI

2023-04-24 08:12:59

2020-07-09 09:08:24

Java系统故障

2022-04-08 08:48:16

线上事故日志订阅者

2020-07-13 07:49:38

Kubernetes容器架构

2009-10-28 09:14:43

员工分析软件

2021-09-27 10:15:10

故障业务方电脑

2024-03-11 08:51:08

JVMSWAP内存

2021-08-05 06:46:39

P0故障公司

2020-04-13 17:17:28

MySQL8.0功能

2019-06-27 22:23:56

谷歌Android开发者

2021-04-13 17:17:08

线上故障交付

2021-05-12 09:15:48

Facebook 开发技术

2020-07-15 08:10:00

浏览器安全Fetch

2022-02-07 15:12:17

系统日志定位

2019-03-29 10:22:08

Linux系统故障技巧

2020-05-18 07:50:47

线上故障排查

2018-12-27 09:10:45

2023-06-08 11:27:10

模型AI
点赞
收藏

51CTO技术栈公众号