百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

写一个通用的幂等组件,我觉得很有必要

mhr18 2025-04-27 14:47 38 浏览 0 评论

本文目录

  1. 背景
  2. 简单幂等实现

2.1 数据库记录判断

2.2 并发问题解决

  1. 通用幂等实现

3.1 设计方案

3.1.1 通用存储

3.1.2 使用简单

3.1.3 支持注解

3.1.4 多级存储

3.1.5 并发读写

3.1.6 执行流程

3.2 幂等接口

3.3 幂等注解

3.4 自动区分重复请求

3.5 存储结构

3.6 源码地址

背景

回答群友的问题:幂等有没有什么通用的方案和实践?

关于什么是幂等,本文就不再阐述了。相信大家都知道,并且也都遇到过类似的问题以及有自己的一套解决方案。

基本上所有业务系统中的幂等都是各自进行处理,也不是说不能统一处理,统一处理的话需要考虑的内容会比较多。

我个人认为核心的业务还是适合业务方自己去处理,比如订单支付,会有个支付记录表,一个订单只能被支付一次,通过支付记录表就可以达到幂等的效果。

还有一些不是核心的业务,但是也有幂等的需求。比如网络问题,多次重试。用户点击多次等场景。这种场景下还是需要一个通用的幂等框架来处理,会让业务开发更加简单。

简单幂等实现

幂等的实现其实并不复杂,方案也有很多种,首先介绍下基于数据库记录的方案来实现,后面再介绍通用方案。

数据库记录判断

以文章开头讲的支付场景来举例。业务场景是一个订单只能支付一次,所以我们在支付之前会判断这个订单有没有支付过,如果没有支付过则进行支付,如果支付过了,就反正支付成功,幂等。

这种方式需要有一个额外的表来存储做过的动作,才能判断之前有没有做过这件事情。

就好比你年龄大了,然后还是单身的技术宅。这个时候你家里着急了呀,你老妈天天给你介绍小姐姐。你每个周末都要打扮的非常帅气,去见你老妈给你介绍的小姐姐。

去之前你得记录下吧,8 月第一周我见的 XXX, 第二周我见的 YYY, 如果第三周又让你去见 XXX, 如果这个时候你不喜欢 XXX, 你会翻出你的小本本看下,这个之前见过了,没必要再见了,不然见了多尴尬啊。

并发问题解决

通过查询支付记录,判断能否进行支付在业务逻辑上没一点问题。但是在并发场景就会有问题。

1001 的订单发起了两次支付请求,当前两个请求同时查询支付记录,都没有查询到,然后都开始走支付的逻辑,最后发现同一个订单支付了两次,这就是并发导致的幂等问题。

并发解决的方案也有很多种,简单点的直接用数据库的唯一索引解决,稍微麻烦点的都会用分布式锁来对同一个资源进行加锁。

比如我们对订单 1001 进行加锁,如果同时发起了两次支付请求,那么同一时间只能有一个请求可以获取锁,另一个请求获取不到锁可以直接失败,也可以等待前面的请求执行完成。

如果等待前面的请求执行完成,接着往下处理,就能查到 1001 已经支付过了,直接返回支付成功了。

通用幂等实现

为了能够让大家更专注于业务功能的开发,简单场景的幂等操作我认为可以进行统一封装来处理,下面介绍一下通用幂等的实现。

SJwxerVeyL7CyOr0.png

设计方案

通用存储

一般我们在程序内部做幂等的话都是先查询,然后根据查询的结果做对应的操作。同时会对相同的资源进行加锁来避免并发问题。

加锁是通用的,不通用的部分就是判断这个操作之前有没有操作过,所以我们需要有一个通用的存储来记录所有的操作。

使用简单

提供通用的幂等组件,注入对应的类即可实现幂等,屏蔽加锁,记录判断等逻辑。

支持注解

除了通过代码的方式来进行幂等的控制,同时为了让使用更加简单,还需要提供注解的方式来支持幂等,使用者只需要在对应的业务方法上增加对应的注解,即可实现幂等。

多级存储

需要支持多级存储,比如一级存储可以用 Redis 来实现,优点是性能高,适用于 90%的场景。因为很多场景都是为了防止短时间内请求重复导致的问题,通过设置一定的失效时间,让 Key 自动失效。

二级存储可以支持 Mysql, Mongo 等数据库,适用于时间长或者永久存储的场景。

可以通过配置指定一级存储用什么,二级存储用什么。这个场景非常适合用策略模式来实现。

并发读写

引入多级存储势必会涉及到并发读写的场景,可以支持两种方式,顺序和并发。

顺序就是先写一级存储,再写二级存储,读也是一样。这样的问题在于性能会有点损耗。

并发就是多线程同时写入,同时读取,提高性能。

幂等执行流程

幂等接口

幂等接口定义

public interface DistributedIdempotent {
    /**
     * 幂等执行
     * @param key 幂等Key
     * @param lockExpireTime 锁的过期时间
     * @param firstLevelExpireTime 一级存储过期时间
     * @param secondLevelExpireTime 二级存储过期时间
     * @param timeUnit 存储时间单位
     * @param readWriteType 读写类型
     * @param execute 要执行的逻辑
     * @param fail Key已经存在,幂等拦截后的执行逻辑
     * @return
     */
    <T> T execute(String key, int lockExpireTime, int firstLevelExpireTime, int secondLevelExpireTime, TimeUnit timeUnit, ReadWriteTypeEnum readWriteType, Supplier<T> execute, Supplier<T> fail);
}

使用方式

/**
 * 代码方式幂等-有返回值
 * @param key
 * @return
 */
public String idempotentCode(String key) {
    return distributedIdempotent.execute(key, 10, 10, 50, TimeUnit.SECONDS, ReadWriteTypeEnum.ORDER, () -> {
        System.out.println("进来了。。。。");
        return "success";
    }, () -> {
        System.out.println("重复了。。。。");
        return "fail";
    });
}

幂等注解

使用注解,能够让使用更加简单,比如我们的事务处理,缓存等都使用了注解来简化逻辑。

幂等的场景也可以定义通用的注解来简化使用难度,在需要支持幂等的业务方法上增加注解,配置基本信息。

idempotentHandler 是触发幂等规则后执行的方法,也就是我们用代码实现幂等时候的 Supplier<T> fail 参数。实现是用的阿里 Sentinel 限流,熔断后的处理那套逻辑。

在幂等的场景下,如果是重复执行,通常返回跟正常执行一样的结果即可。

/**
 * 注解方式幂等-指定幂等规则触发后执行的方法
 * @param key
 */
@Idempotent(spelKey = "#key", idempotentHandler = "idempotentHandler", readWriteType = ReadWriteTypeEnum.PARALLEL, secondLevelExpireTime = 60)
public void idempotent(String key) {
    System.out.println("进来了。。。。");
}
public void idempotentHandler(String key, IdempotentException e) {
    System.out.println(key + ":idempotentHandler已经执行过了。。。。");
}

自动区分重复请求

代码方式处理幂等,需要传入幂等的 Key,注解方式处理幂等,支持配置 Key,支持 SPEL 表达式。这两种都是需要在使用的时候就确定好根据什么来作为幂等的唯一性判断。

还有一种幂等的场景是比较常见的,就是防止重复提交或者网络问题超时重试。同样的操作会请求多次,这种场景下可以在操作之前先申请一个唯一的 ID,每次请求的时候带给后端,这样就能标识整个请求的唯一性。

我目前做了一个自动生成唯一标识的功能,简单来说就是根据请求的信息进行 MD5,如果 MD5 值没有变化就认为是同一次请求。

需要进行 MD5 的内容有请求 URL 参数,请求体,请求头信息。请求头的信息在没有指定用户相关 Key 的场景下会进行全部拼接,如果配置了请求头 userId 为用户的标识,那么只会用 userId。

会在请求的入口处进行幂等 Key 的自动生成,如果在使用幂等注解的时候没有指定 spelKey, 就会使用自动生成的 Key。

存储结构

Redis: 使用 String 类型存储,Key 是幂等 Key, Value 默认为 1。

Mysql: 需要创建一张记录表。(过期的数据需要定时清理,也可以永久存储)

CREATE TABLE `idempotent_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `key` varchar(50) NULL DEFAULT '',
  `value` varchar(50) NOT NULL DEFAULT '',
  `expireTime` timestamp NOT NULL COMMENT '过期时间',
  `addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='幂等记录';

Mongo: 字段跟 Mysql 一样,转换成 Json 格式即可。Mongo 会自动创建集合。

码字不易,可以的话来个三连击,感谢!

关于作者:尹吉欢,简单的技术爱好者,《Spring Cloud 微服务-全栈技术与案例解析》, 《Spring Cloud 微服务 入门 实战与进阶》作者, 公众号猿天地发起人。

微信搜索 猿天地 回复 kitty 获取源码

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: