前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >搞了个线上故障,被老板骂了....

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

作者头像
微观技术
发布2022-12-29 14:40:56
2580
发布2022-12-29 14:40:56
举报
文章被收录于专栏:微观技术微观技术

大家好,我是Tom哥。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

技术方案

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

大致步骤:

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

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

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

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

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

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

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

注意事项:

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

项目实战

1、引入 redis 组件

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

代码语言:javascript
复制
<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相关服务配置

代码语言:javascript
复制
spring.redis.host=127.0.0.1
spring.redis.port=6379

3、定义注解类

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

代码语言:javascript
复制
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented

public @interface IdempotentRule {

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

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

4、接口拦截器

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

代码语言:javascript
复制
@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、上层业务接口

代码语言:javascript
复制
@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、模拟重复,连续多次快速提交请求,请求会被拦截,并抛出异常

代码地址:

https://github.com/aalansehaiyang/redis-limit-demo

·············· END ··············

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微观技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术方案
  • 项目实战
    • 1、引入 redis 组件
      • 2、redis 变量配置
        • 3、定义注解类
          • 4、接口拦截器
            • 5、上层业务接口
            • 测试结果
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档