《一块学习分布式》之接口幂等

本文探讨了并发场景下幂等性的概念与实现,包括乐观锁、悲观锁、分布式锁及状态机幂等,旨在解决系统交互中重复操作引起的数据不一致问题。

引子

前面介绍过乐观锁、悲观锁。传送门:Mysql乐观锁探究Mysql悲观锁探究

回头思考一个问题,为什么会有悲观锁、乐观锁?它究竟为了解决什么问题而产生?

记得读高中时候,有一个数学老师说过(该数学老师人不高,还有点微胖,大家都亲切的称之为"大叔",还很喜欢打篮球;一晃都好多年没回去过了。唉,估计人老了,一想往事就收不住了,眼里进了鳄鱼。言归正传,他说过一个名句):如果做题的时候,不知道怎么解,就回到定义、公式!

现在做技术的都感觉的到,各种新技术层出不穷,各种语言百花齐放,业务场景也是千差万别,但是要解决的问题本质其实变化不大。并发问题一直是程序员要解决的和碰到的问题。

看一下场景

  • 现在的很多电商网站,都是分布式服务甚至微服务,即进行了服务的拆分,比如用户系统、订单系统、支付系统、库存系统等等。系统间用RPC或webservice调用进行交互。一个用户购买商品,首先订单系统下单,进行了库存的扣减(假设库存系统事务提交,进行了真实的扣减),然后系统挂了或者网络抖动导致没有返回响应给订单系统,订单系统这时候怎么办?是直接当做失败还是进行返回用户成功然后重试?
  • 给用户发送通知短信,如果不做幂等控制,很有可能造成重复发送导致投诉

幂等的概念

这里为什么会提到幂等定义呢?因为很多系统交互都是通过接口调用,如果一个接口被反复调用,结果不变,就认为接口具有幂等性。比如上面的例子,如果订单系统进行重试,如果不保证幂等,可能会造成库存重复扣减导致错误。那怎么判断请求是否幂等呢,这就是涉及到幂等号!在请求时,指定一个字段用来判断请求是否幂等。

幂等概念(来源于网上):幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数,SQL语句中的select语句也是天然幂等操作,不管查询多少次,结果都不会变。更复杂的操作幂等保证是利用唯一交易号(流水号)实现

解决方案

幂等号(流水号)

在设计接口的时候,尤其是涉及到外部的交互,都是需要考虑到幂等控制的。这种一般是通过在接口层面增加幂等请求字段,让调用方可以手动指定。

/** 业务订单号,幂等使用*/
    private String                   bizNo;

整体的系统交互如下

  • 系统A调用第一次系统B的接口methodB,并传入一个幂等字段bizNo,值为bizNo1
  • 系统B受到该请求,先根据bizNo判断是否处理过该请求,发现没有处理过,执行业务处理,并返回结果
  • 系统再次调用系统B的接口methodB,并传入相同的幂等字段值bizNo1,系统B根据bizNo1判断依据处理过了,则不进行业务执行,直接返回上一次执行结果

上面就是一次幂等控制的流程:会发现有一步很关键,就是幂等判断。那么一般这种幂等怎么判断呢?可以把bizNo单独存储到数据库一个字段上,放到业务数据里面;每次请求过来,先用bizNo取出记录,然后判断记录处理过,如果成功处理过就直接返回,不再重复处理。这样就避免了重复处理造成的数据不一致问题。

消息重复

现在很多系统一般会用到mq,用消息进行系统解耦及提高系统吞吐量。但是mq有一个严重问题,就是消息重复投递的问题。因此,消息消费者必须保证消息消费的幂等性,否则就可能出现数据不一致的状态。

消息队列产品一般会明确其 “消息投递” 的服务质量保证 (QoS),常见的质量等级包括以下三种:

  • 至少投递一次 (At-least-once)
  • 最多投递一次 (At-most-once)
  • 严格只投递一次 (Exactly-once)

最常见的服务质量保证是 “至少投递一次 (At-least-once)”,一般消息流程时序图如下所示:

 

 

针对同一个消息对象,消息消费者 (Subscriber/ Consumer) 可能收到一次或者多次;收到多次的场景可能由以下几种原因导致:

  • 网络异常,导致消息消费者未收到消息对象。
  • 消息消费者进程宕机,导致未收到消息对象。
  • 消息消费者收到消息对象,但处理耗时过长,例如超过 10 秒。
  • 消息消费者收到消息对象并正常处理,但回执 ACK 丢失。
  • 消息消费者收到消息对象,处理过程抛出异常或者主动通知消息队列 broker 无法正常消费消息。

比如上面的例子,如果是mq消息交互,不做幂等控制,订单系统发起重试,可能就会造成库存重复扣减。对于一般的消息体,可能如下:

messageId = "1e4cf860680dad989dceec17f1adb837"
TOPIC = "TEST_TRADE"
eventcode = "TEST_ORDER"
payload = Account("200XXX", "800080001","60.00","2018-07-30 12:00:00,000", "operator")

那么针对这种消息可以这么做幂等控制呢?

  1. 一种直观的解决方案是把消息存储下来,根据messageId标识这个消息是否处理过。
  2. 或者根据业务主键比如订单号orderId确认该比订单是否处理过。

可以很容易判断,用业务主键是一种更优的方式:如果用messageId判断,需要额外的存在消息,数据库需要频繁的插入查询非业务数据,更重要的是messageId 相同的消息对象,业务数据对象一定是相同的;但 messageId 不同的消息对象,业务数据对象也可能是相同的,这取决于消息生产者的实现逻辑。

所以,对于 “至少投递一次 (At-least-once)” 的消息投递服务质量,保证消息消费幂等性是必须的,不是可选的。

其它场景

可以把上面的消息及api视为一种场景,即幂等处理。但是还有一些其它的场景,通过防止并发从而避免产生幂等性问题

  1. 悲观锁:通过获取数据的时候加锁获取,就不再具体介绍了,参考上面Mysql悲观锁探究
  2. 乐观锁:Mysql乐观锁探究
  3. 分布式锁:如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);
  4. 状态机幂等:上面的消息重复处理可以视为应用了状态机幂等,即一条业务数据是有状态的,比如初始化-》处理中-》成功/失败

幂等引发的血案

<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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值