如何实现幂等性

什么是幂等

幂等本来是数学上的概念,它的定义是这样的: 如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。比如,求绝对值的函数,abs(x) = abs(abs(x))。

在计算机领域用来描述一个操作、方法或者服务。一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。

非幂等接口带来的问题

我们把系统解耦隔离后,服务间的调用可能会有三个状态,一个是成功(Success),一个是失败(Failed),一个是超时(Timeout)。前两者都是明确的状态,而超时则是完全不知道是什么状态。比如,超时原因是网络传输丢包的问题,可能是请求时就没有请求到,也有可能是请求到了,返回结果时没有正常返回等等情况。于是我们完全不知道下游系统是否收到了请求,而收到了请求是否处理了,成功 / 失败的状态在返回时是否遇到了网络问题。总之,请求方完全不知道是怎么回事。
举几个例子:

  • 订单创建接口,第一次调用超时了,然后调用方重试了一次。是否会多创建一笔订单?
  • 订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次。是否会多扣一次库存?
  • 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。是否会多扣一次钱?

因为系统超时,而调用方重试一下,会给我们的系统带来不一致的副作用。
在这种情况下,一般有两种处理方式。

  • 一种是需要下游系统提供相应的查询接口。上游系统在 timeout 后去查询一下。如果查到了,就表明已经做了,成功了就不用做了,失败了就走失败流程。
  • 另一种是通过幂等性的方式。也就是说,把这个查询操作交给下游系统,我上游系统只管重试,下游系统保证一次和多次的请求结果是一样的。

对于第一种方式,需要对方提供一个查询接口来做配合。而第二种方式则需要下游的系统提供支持幂等性的交易接口。


场景

将林志玲账户的余额加 100 元

方法一(推荐使用): 令牌机制(全局ID) (记录并检查操作)

在发送消息时,给每条消息指定一个全局唯一的ID,消费时,先根据这个ID检查这条消息是否有被消费过,如果没有消费过更新数据,然后将消费状态置为已消费。

  1. 服务器端派发token并将token记录在缓存中, 客户端携带此token请求服务, 如果此token有效证明是有效请求处理请求, 并删除token。token无效忽略此请求。
  2. 客户端请求携带一个唯一标识(流水号),服务器判断此唯一标识是否已经存在,如果已存在忽略此请求。
方法二:利用数据库的唯一约束实现幂等(利用数据库实现幂等性)

我们在数据库中建一张转账流水表,这个表有三个字段:转账单 ID、账户 ID 和变更金额,然后给转账单 ID 和账户 ID 这两个字段联合起来创建一个唯一约束,这样对于相同的转账单 ID 和账户 ID,表里至多只能存在一条记录。

方法三:为更新的数据设置前置条件(将方法实现幂等)
  1. 方法入参传入林志玲的账户余额,拿参数中的余额与数据库中的余额做比较如果相同则执行变更操作。
  2. 最简单的做法给数据增加一个版本号属性,每次更改数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 1。(和乐观锁原理一样)

实现幂等的核心是判断请求是否重复,具体实现方式比较多,想设计一个高可用的幂等方法还需要我们具体业务具体分析具体设计。

其他方式

1.将请求参数通过md5加密或者hashcode()计算后值存入缓存
2.添加个版本号,当前提交版本号大于上次提交版本号


HTTP的幂等性

HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。 比如: GET

http: / /www . bank . com/ account/123456,不会改变资源的状态,不论调用一次还是 N次都没有副作用。请注意,这里强调的是- -次和N次具有相同的副作用,而不是每次GET的结果相同。GET http:/ /www. news . com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。

HTTP HEAD和GET本质是一样的,区别在于HEAD不含有呈现数据,而仅仅是HTTP头信息,不应用有副作用,也是幂等的。 有的人可能觉得这个方法没什么用,其实不是这样的。想象一个业务情景:欲判断某个资源是否存在,我们通常使用GET,但这里用HEAD则意义更加明确。也就是说,HEAD 方法可以用来做探活使用。

HTTP OPTIONS主要用于获取当前URL所支持的方法,所以也是幂等的。 若请求成功,则.它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST"。

HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。 比如: DELETE
http://www. forum. com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉ID为4231的帖子。因此,调用者可以多次调用或刷新页面而不必担心引起错误。

HTTP POST方法用于创建资源,所对应的URI并非创建的资源本身,而是去执行创建动作的操作者,有副作用,不满足幂等性。 比如: POST http:/ /www. forum. com/ articles的语义是在http: //www. forum. com/articles下创建一篇帖子, HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。

HTTP PUT方法用于创建或更新操作,所对应的URI是要创建或更新的资源本身,有副作用,它应该满足幂等性。 比如: PUT http: / /www. forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。

所以,对于POST的方式,很可能会出现多次提交的问题,就好比,我们在论坛中发贴时,有时候因为网络有问题,可能会对同一篇贴子出现多次提交的情况。对此的一-般的幂等性的设计如下。

  • 首先,在表单中需要隐藏一个token,这个token可以是前端生成的一个唯一的ID。用 于防止用户多次点击了表单提交按钮,而导致后端收到了多次请求,却不能分辨是否是重复的提交。这个token是表单的唯一标识。 (这种情况其实是 通过前端生成ID把POST变成了PUT。)
  • 然后,当用户点击提交后,后端会把用户提示的数据和这个token保存在数据库中。如果有重复提交,那么数据库中的token会做排它限制,从而做到幂等性。
  • 当然,更为稳妥的做法是,后端成功后向前端返回302跳转,把用户的前端页跳转到GET请求,把刚刚POST的数据给展示出来。如果是Web.上的最好还把之前的表单设置成过期,这样用户不能通过浏览器后退按钮来重新提交。这个模式又叫做OPRG模式(Post/Redirect/Get)。
<think> 首先,用户的问题是如何使用分布式锁实现幂等性。这基于提供的引用内容。 回顾引用内容: - 引用[1]:描述了使用分布式锁实现接口幂等性的步骤:生成锁key、获取锁、设置过期时间、执行业务逻辑。 - 引用[2]:展示了Redisson实现分布式锁的代码示例,用于处理重复上报数据的场景。 - 引用[3]:提到分布式锁的互斥性和Redis的实现方式。 - 引用[4]:讨论了数据库锁和分布式锁实现幂等性的流程图,使用唯一标识作为Redis key。 - 引用[5]:提供了一个Spring Boot配置示例,用于添加拦截器实现幂等性。 用户的问题是:如何实现幂等性?所以我的回答应该聚焦在分布式锁的具体实现上,参考这些引用。 系统级指令: - 所有行内数学表达式用$...$。 - 独立公式用$$...$$单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 用户可见层指令: - 回答结构清晰,逐步解决问题。 - 保证真实可靠。 我的回答结构: 1. **解释幂等性**:简要说明什么是幂等性及其重要性。 2. **分布式锁的概念**:解释分布式锁如何帮助实现幂等性。 3. **实现步骤**:基于引用内容,详细描述步骤: - 生成锁key(基于唯一标识)。 - 获取分布式锁(使用Redis等)。 - 设置锁的过期时间。 - 执行业务逻辑。 - 释放锁。 4. **代码示例**:提供代码片段,参考引用中的例子。 5. **注意事项**:讨论设置过期时间、锁的释放等关键点。 6. **生成相关问题**:在最后添加相关问题。 在回答中,引用相关的引用内容,并在末尾添加标识。 生成相关问题: - 基于主题,如:如何选择唯一标识?分布式锁的性能问题?其他实现幂等性的方法? 确保使用中文。 大纲回答: 1. **引言**:定义幂等性,解释为什么分布式锁有效。 2. **核心步骤**: - 步骤1:确定唯一标识(如请求ID、订单号)。 - 步骤2:基于唯一标识生成分布式锁key。 - 步骤3:尝试获取锁(使用Redis set命令或Redisson)。 - 步骤4:如果获取成功,设置过期时间。 - 步骤5:执行业务逻辑。 - 步骤6:释放锁。 3. **代码示例**:提供一个简单的Redisson或Redis实现。 4. **注意事项**:避免死锁、设置合理的过期时间。 5. **总结**。 6. **相关问题**。 在回答中,自然地添加引用标识。 </think> ### 如何使用分布式锁实现接口幂等性 接口幂等性是指多次重复执行同一操作时,结果与执行一次相同,这在分布式系统中尤为重要,例如防止重复支付或数据重复提交。分布式锁通过确保同一时刻只有一个请求能执行业务逻辑来实现幂等性,核心是利用锁的互斥性(即同一时间只有一个客户端能获取锁)[^3]。下面我将基于引用内容,逐步解释实现方法,并提供代码示例。 #### 1. **理解幂等性与分布式锁的关系** - 幂等性的本质:无论请求执行多少次,系统状态保持一致。例如,重复提交订单时,只创建一次订单。 - 分布式锁的作用:在分布式环境中,使用 Redis、Zookeeper 等中间件实现锁的互斥。当请求到达时,先尝试获取锁;获取成功则执行业务逻辑;获取失败则视为重复请求,直接返回成功或错误提示[^1][^4]。 - 关键优势:减少无效请求对服务端的压力,尤其适用于高并发场景(如物联网设备重复上报数据)[^2]。 #### 2. **实现分布式锁幂等性的步骤** 以下是核心步骤,需结合唯一标识(如订单号、请求ID)作为锁的 key: | 步骤 | 描述 | 关键点 | |------|------|--------| | **步骤1:生成唯一锁 key** | 基于请求参数(如订单号、用户ID)生成分布式锁的 key。例如,订单支付请求使用订单号作为 key。 | 确保 key 全局唯一,避免锁冲突[^1][^4]。 | | **步骤2:尝试获取分布式锁** | 使用 Redis 或 Redisson 的 API 尝试加锁。设置等待时间为 0(非阻塞模式),避免请求堆积。 | 如果锁获取成功,继续下一步;如果失败,直接返回幂等响应(如“操作成功”或错误提示)[^2][^3]。 | | **步骤3:设置锁的过期时间** | 在获取锁后,立即设置一个合理的过期时间(如 10-30 秒)。这是防止死锁的关键:如果业务逻辑异常崩溃,锁能自动释放[^1][^4]。 | | **步骤4:执行业务逻辑** | 在锁保护下执行核心操作(如数据库写入)。此时,系统状态仅被修改一次。 | 业务逻辑应设计为幂等(如使用数据库唯一索引)以增强可靠性[^4]。 | | **步骤5:释放锁** | 业务完成后,主动释放锁。即使设置了过期时间,也应确保在 finally 块中释放,避免资源泄漏[^2][^4]。 | #### 3. **代码实现示例** 以下是一个基于 Redisson 的 Java 示例(参考引用[2]和[5]),适用于 Spring Boot 项目。Redisson 简化了 Redis 分布式锁的 API。 ```java import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class IdempotentService { private RedissonClient redisson; // 注入 RedissonClient 实例 public void processRequest(String requestId) { // 步骤1: 生成锁 key,基于唯一标识如请求ID String lockKey = "lock:" + requestId; // 步骤2: 获取分布式锁 RLock lock = redisson.getLock(lockKey); boolean isLocked = false; try { // 尝试加锁,等待0秒(非阻塞),设置10秒自动过期 isLocked = lock.tryLock(0, 10, TimeUnit.SECONDS); if (isLocked) { // 步骤3: 锁获取成功,执行业务逻辑 executeBusinessLogic(requestId); } else { // 锁获取失败,代表重复请求 System.out.println("重复请求,直接返回成功"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 步骤5: 确保锁被释放 if (isLocked) { lock.unlock(); } } } private void executeBusinessLogic(String requestId) { // 实际业务逻辑,如数据库操作 System.out.println("处理业务: " + requestId); } } ``` - **解释**: - `tryLock(0, 10, TimeUnit.SECONDS)`:第一个参数为等待时间(0 表示不等待),第二个为锁过期时间。这确保了高并发下只有第一个请求能执行业务[^2]。 - **Spring Boot 集成**:通过拦截器自动处理幂等性(参考引用[5])。示例配置: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private IdempotentInterceptor idempotentInterceptor; // 自定义拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(idempotentInterceptor); } } ``` 拦截器在请求前检查锁,实现全局幂等控制。 #### 4. **关键注意事项** - **设置合理的过期时间**:过期时间太短可能导致业务未完成锁已释放,引发重复执行;太长则占用 Redis 资源。建议根据业务平均耗时设置(如业务耗时 + 缓冲时间)[^4]。 - **避免死锁**:务必在 `finally` 块中释放锁,即使业务逻辑抛出异常。 - **唯一标识的选择**:使用请求参数(如订单号)或生成唯一 ID(如 UUID),确保不同请求的 key 不冲突[^1]。 - **性能优化**:对于高频请求,Redis 单线程模型可能成为瓶颈;可考虑分片或使用更轻量级锁(如 Lua 脚本)[^3]。 - **适用场景**:适用于重复请求无害的场景(如数据上报),不适用于需保证绝对一致性的操作(需结合事务)[^2]。 #### 5. **总结** 分布式锁是实现接口幂等性的高效方法,通过互斥机制确保同一请求只执行一次核心逻辑。核心步骤包括生成唯一 key、非阻塞加锁、设置过期时间、执行业务和释放锁。结合 Redis 或 Redisson,实现简单可靠。在实际应用中,需测试锁的获取率和业务耗时,以优化过期时间和并发性能[^1][^4]。 ### 相关问题 1. **如何选择分布式锁的唯一标识(key)来避免冲突?** 2. **分布式锁在高并发场景下可能有哪些性能瓶颈?如何优化?** 3. **除了分布式锁,还有哪些实现接口幂等性的方法?** 4. **Redis 分布式锁在集群环境下如何保证可靠性?** 5. **如何处理分布式锁获取失败后的错误反馈机制?** [^1]: 关键步骤包括生成锁 key、获取锁、设置过期时间,确保业务逻辑只执行一次。 [^2]: 使用 Redisson 的 `tryLock(0, timeout)` 实现非阻塞加锁,适用于重复上报场景。 [^3]: 分布式锁的互斥性基于 Redis 单线程模型,适合处理并发请求。 [^4]: 分布式锁实现幂等性的流程图强调唯一标识作为 key 和设置合理过期时间的重要性。 [^5]: Spring Boot 集成示例通过拦截器全局管理幂等性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘彦青-Yannis

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值