后端笔记06 | 高并发下的接口幂等性解决方案总结

1. 参考资料

Hollis:解决幂等问题,只需要记住这个口诀!

苏三说技术:高并发下如何保证接口的幂等性?

分布式系统设计中的并发访问解决方案 | 得物技术

2. 内容设问

  1. 什么是幂等性?
  2. 常见幂等性场景(HTTP资源请求方式和支付、订单创建等)
  3. 幂等性类型(请求幂等和业务幂等)
  4. 没有实现幂等性可能存在的问题
  5. 实现幂等性可能问题:并发场景下发生幂等击穿
  6. 实现幂等性通用方案(一锁二判三更新)
  7. 实现幂等性具体方案(分为基于缓存和基于数据库两大类)

3. 正文

1. 🚀 什么是幂等性?

幂等性(Idempotency) 是一个操作的特性,指的是无论该操作执行多少次,其结果始终是相同的,不会因重复执行而产生不同的副作用或不一致的状态。

2. 🚀 常见的幂等性场景

① HTTP请求方式

  • GET:用于获取资源的请求。幂等安全,即无论执行多少次相同的 GET​ 请求,都会返回相同的资源内容。
  • PUT:用于更新资源的请求。如果请求内容相同,无论执行多少次,系统状态都保持一致,因此 PUT​ 请求是幂等的。
  • POST:通常用于创建资源的请求,且非幂等。每次发送相同的 POST​ 请求可能会导致不同的结果,比如每次都会创建新的资源或引发新的操作。因此,POST​ 请求通常不是幂等的。

② 支付系统应用:

支付业务

如果用户因网络问题点击多次支付按钮,只有第一次会成功扣款,后续的重复请求不会再重复扣款。

订单创建

创建订单时,用户因为重试点击导致多次请求,但系统只会创建一次订单,避免重复创建。

3. 🚀 幂等性的类型

一般分为请求幂等和业务幂等。

请求幂等:每次请求,如果参数一样,结果也要一样。

业务幂等:要求对业务流程的不同阶段,对重复请求的处理方式有所区分

同一次业务请求:

在拿到最终状态之后的每次请求,系统的响应结果应相同,业务状态不可变。

在没拿到最终状态之前,每次请求会继续执行相关的业务逻辑,直到业务最终达到预期状态。

4. 🚀 如果没有幂等可能带来的问题举例

① 前端重复提交表单:post请求,点击两次时会出现两条重复数据,只有id不同;

② 接口超时重试: 主要是第三方调用接口因网络原因调用失败时设计的失败重试机制,当第一次调用执行一半时发生网络异常,再次调用时就会因为脏数据的存在而出现调用异常;

③ 重复消费消息:在分布式系统中,消息队列(如 Kafka、RabbitMQ)用于解耦服务和提高系统的可靠性。但如果消费者在处理消息时宕机、断开连接或因超时未正确确认(ACK),消息队列会将该消息重新投递。如果业务逻辑没有幂等性保障,可能会导致同一条消息被消费多次,进而引发数据重复处理的问题。

5. 🚀 实现幂等性可能存在的问题:幂等击穿

  • 🎯 什么是幂等击穿?

实现幂等性时系统无法处理并发请求导致幂等性失效(幂等方案设计问题), 从而引发业务异常或者错误。重复操作仍然会产生副作用或者错误的结果。

  • 🔄 产生原因

① 并发处理问题: 没有正确控制共享资源,导致多个请求并行执行产生不同结果;

② 业务状态同步问题:系统无法在多个操作或服务之间正确同步状态,导致不同的服务或系统对于请求的处理状态出现不一致。

缺乏正确的幂等性标识:请求的唯一标识(如幂等性键)没有正确传递或识别,导致服务无法判断重复请求。

6. 🚀 解决并发场景下幂等问题的通用设计方案

💡 口诀: ”一锁、二判、三更新”

任何一个并发请求过来时,

一锁: 第一步,先加锁(互斥锁:分布式锁或者悲观锁);

二判: 第二步,进行幂等性判断,可以基于状态机、流水表、唯一性索引等,判断是否已经更新过对应状态/是否存在;

三更新: 第三步,如果之前数据没有更新,本次请求可以更新;如果之前数据有更新,本次请求不能更新。

7. 🚀 解决并发下的幂等问题的具体方案

💡 可以分成两大类,基于数据库和基于缓存的幂等性方案。

一类是基于MySQL实现(适合数据强一致性场景) :insert前先select、加锁(乐观锁、悲观锁),建立唯一索引、建立防重表,状态机(大部分依赖数据库)。

一类是基于Redis实现(适用于高并发和低延迟场景): Token机制,分布式锁。

  • ✅ insert前先select

插入前 先查看这个数据是否存在, 存在则改用update,不存在则insert。

示例:

-- 先查询数据
SELECT * FROM table_name WHERE id = ?;

-- 如果存在则更新数据
UPDATE table_name SET column1 = ?, column2 = ? WHERE id = ?;

-- 如果不存在则插入数据
INSERT INTO table_name (id, column1, column2) VALUES (?, ?, ?);

类比:乐观锁(根据version/timestamp来检查数据是否被其它事务修改)

-- 假设表中有 version 字段
SELECT * FROM table_name WHERE id = ? AND version = ?;

-- 更新时检查 version 是否匹配
UPDATE table_name SET column1 = ?, version = version + 1 WHERE id = ? AND version = ?;

  • ✅ 用悲观锁

悲观锁是一种确保在同一时刻只有一个请求能够修改数据的锁机制。

加锁语法:通SELECT ... FOR UPDATE​ 语句可以为查询的记录加上悲观锁

如:使得只有获取到锁的事务能够修改数据,其他事务必须等待当前事务释放锁。

select * from user id=123 for update;

注意

如果此时用的是MySQL,需要确保其存储引擎是innodb,因为只有innodb才支持行级锁和事务;

且锁定的记录需要具备唯一标识(如主键或者唯一索引) ,如果不存在唯一索引,行锁会降级为表锁。

  • ✅ 用乐观锁

因为悲观锁会存在性能问题,乐观锁只在提交时检查数据是否被其它事务修改过,可以提升接口性能。适用于并发高、数据更新不频繁的场景。

方式:通过在表中加上version或者timestamp字段。在update操作时,在条件上对version或者timestamp检查。

举例: 订单创建实现幂等性流程

① 检查订单是否创建,不存在则插入新的订单

-- 查询是否已存在相同的订单
SELECT * FROM orders WHERE order_id = ?;
-- 插入订单记录
INSERT INTO orders (order_id, user_id, product_id, quantity, status, version)
VALUES (?, ?, ?, ?, 'CREATED', 1);

② 订单存在,用乐观锁更新

检查version是否匹配,并且更新version。

-- 使用乐观锁机制,检查 version 是否一致
UPDATE orders 
SET status = 'CREATED', version = version + 1 
WHERE order_id = ? 
AND version = ?;

  • ✅ 加唯一索引

添加一个仿重业务id或者业务唯一标识符,具备唯一索引。

加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求会有异常。

① 在数据库:插入数据时会报唯一索引冲突。

② 在Java程序:需要捕获DuplicateKeyException​异常;

③ 在Spring框架:还需要捕获MySQLIntegrityConstraintViolationException​异常

  • ✅ 建防重表

① 为什么需要防重表?

因为不是所有场景都不允许产生重复数据,只有特定场景不允许,此时直接表里加唯一索引不太合适,因此用防重表来解决,提高灵活性。

② 什么是防重表?

通过在数据库中创建一个专门的表(防重表/幂等性表) 来存储业务请求的唯一标识符,可以实现幂等性,确保同一个请求不会被重复处理。

防重表设计

id + 请求的唯一标识符,比如订单系统,id+order_id

防重表应用举例:

系统接收业务处理请求时 ,如果此时要求不能重复处理(要求幂等性),

则需要在防重表先检查其order_id是否已存在,存在则捕获唯一索引异常跳过执行,不存在插入数据并继续处理业务逻辑。

  • ✅ 状态机

为什么可以使用状态机?

业务数据存在不同状态: 因为业务表大多数情况下存在状态,比如订单的下单/已支付/完成;

业务数据状态用数值表示:在数据库表设计时,我们通常会将这些固定字符修改为数值表示,比如下单-1,已支付-2,完成-3,存在从小到大的数值规律。

② 应用场景

仅限于要更新的表有状态字段​,并且刚好要更新状态字段​的这种特殊情况,

③ 应用方式

类似如下:查询到已支付-2的状态,然后更新到完成-3的状态。

如果此时在其它事务中已经更新了已支付订单的状态,那么下面的语句执行结果影响行数为0,实现幂等性。

update `order` set status=3 where id=123 and status=2;

  • ✅ 分布式锁

唯一索引或者加防重表,本质是使用了数据库的分布式锁,也是分布式锁的一种。

① 为什么需要分布式锁?

上面数据库分布式锁性能不太好,改用redis或者zookeeper来实现分布式锁。

② redis分布式锁实现

setNX+Lua脚本和redisson框架(如Rlock,实现自动续期)。

注意误删别人的锁、锁过期、锁的可靠性问题,具体说明如下链接:

实现分布式锁(redis和zookeeper)

  • ✅ token机制

① 什么是token机制?

Token 机制是一种通过唯一标识符(Token) 来确保接口请求不会被重复处理的方法。

② token的作用是什么?

Token 作为该次请求的唯一标识,确保相同 Token 只会被处理一次。

② 实现步骤是什么?

  • 请求前生成Token

客户端在发起请求前,先向服务端请求一个唯一的 Token(通常存储在 Redis 中,设置过期时间)。

  • 请求时携带Token

客户端在调用接口时,需要携带该 Token。

在请求头中添加,类似如下:

POST /api/createOrder
Content-Type: application/json
{
  "token": "123e4567-e89b-12d3-a456-426614174000",
  "userId": 1001,
  "productId": 5001,
  "quantity": 1
}
  • 接口处理Token幂等

服务器端收到请求后,先检查该 Token 是否已经使用过(比如创建时存入redis,使用后从redis中删除,在redis中查找不到,说明被使用过):

未使用:先保存 Token 并执行业务逻辑。

已使用:直接返回成功,避免重复执行业务逻辑。

  • 请求完成后删除 Token

业务处理成功后,可以删除 Token,防止 Redis 过载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值