幂等性的实现方案

本文深入探讨幂等性在系统设计中的重要性,列举了多种实现幂等性的技术方案,包括查询操作、删除操作、建立唯一索引、Token机制、悲观锁、乐观锁、分布式锁、select+insert、状态机幂等,旨在帮助开发者理解和应用幂等性原则。

幂等性的实现方案

我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果的。

幂等性的概念

幂等(Idempotent)是一个数学与计算机学的概念,常见于抽象代数中。

f(n) = 1^n // 无论n等于多少,f(n)永远值等于1

在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数/方法。这些函数/方法不会影响系统状态,因此不用担心重复执行会对系统造成改变。例如:

1.前端重复提交选中的数据,后台也只会产生对应这个数据的一个反应结果。

2.用户发起一笔付款请求,就应该只扣用户一次钱,即使遇到网络重发或系统bug重发请求,也应该之扣一次钱。

3.发送验证短息也应该只发一次,同样的验证短信不应该发送多次。

4.创建业务订单,一个业务请求只能创建一个业务订单,创建多个就会出大问题。

这些等等很多的业务逻辑都需要幂等的特性来支持。

简单来理解就是,幂等就是一个操作,这个操作不管执行多少次,产生的效果和返回的结果都是一样的。比如说有一个getOne()函数,无论执行这个函数多少次,它返回的都是1,这时就可以说它是一个幂等函数。

幂等性的技术方案

1.查询操作

查询一次和查询多次,在数据不变的情况下,查询结果都是一样的,select是天然的幂等操作。

2.删除操作

删除操作也是幂等的,删除一次和删除多次都是把数据删除。

3.建立唯一索引,防止新增脏数据

当表存在唯一索引,并发时新增重复记录就会报错,那么这时候就查询已存在的记录并返回即可。

4.Token机制,防止页面重复提交

页面数据只能够提交一次,但是由于出现重复点击或者网络重发或Nginx重发等情况导致数据被重复提交的情况下,可以采用Token+Redis(Redis是单线程的,处理需要排队)的解决方案。处理的流程是,在数据提交前要向服务器申请带有有效时间的Token,然后Token放到Redis或JVM内存中,当数据正式提交到后台要校验Token并删除Token。

5.悲观锁

获取数据的时候加锁获取:

select * from table where id = 'xxx' for update;

要注意的是,id字段一定要是主键或者唯一索引,否则会导致锁表。

悲观锁的使用一般伴随事务一起使用,数据锁定事件可能会很长,要根据实际情况慎用。

6.乐观锁

乐观锁只是在更新数据的那一刻锁表,其他时间不锁表,所以相对于悲观锁效率更高。

乐观锁的实现方式多种多样,可以通过version或者其他状态条件。

7.分布式锁

还是拿插入数据的例子,如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段无法确定。那么这时候就可以引入分布式锁,通过第三方的系统(Redis或Zookeeper),在业务系统插入数据或更新数据,获取分布式锁,然后做操作,之后再释放锁。这样其实是把多线程并发锁的思路引入了多个系统,也就是分布式系统中的解决思路。

要注意的是,某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁需要第三方系统提供))。

8.select+insert

对于一些并发不高的后台系统,或者一些任务Job,为了支持幂等,支持重复执行,简单的处理方法是先查询下一些关键数据,判断是否已经执行过,然后再进行业务处理就可以了。但是要注意的是核心高并发流程不要用这种方法,因为效率较低。

9.状态机幂等

在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候如果状态机已经处于下一个状态,却来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。 

要注意的是,订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助。

总结

幂等性应该是一个合格程序员的基因,在设计系统的时候一定要考虑进去,尤其是像支付宝、银行、互联网金融公司等涉及的都是钱的系统,既要高效,也要准确,所以不能出现多扣款、多打款等问题,不然这样会很难处理,用户体验也不会好。

 

<think>我们正在讨论接口幂等性实现方案。根据引用,常见的方案包括: 1. 利用数据库唯一约束(如唯一索引)防止重复记录[^2][^3] 2. 使用Token机制(包括前端生成或服务端生成一次性Token)[^2] 3. 基于状态机机制(确保状态流转的幂等性,如订单状态从“未支付”到“已支付”只能发生一次)[^3] 4. 采用乐观锁(通过版本号或条件更新)[^2] 5. 分布式锁(如Redis或ZooKeeper实现)[^4] 6. 幂等表(单独建立一张表来记录已处理的请求,通常结合唯一业务标识)[^2] 7. 通过AOP方式实现(自定义注解+拦截器/过滤器,结合Redis等存储)[^1][^4] 下面详细展开说明: </think>### 常见的接口幂等性实现方案 根据引用资料,以下是分布式系统中广泛应用的接口幂等性解决方案及其原理: #### 1. **Token机制(前端防重)** - **原理**:服务端生成唯一Token(如UUID)并缓存至Redis,前端请求时携带该Token。服务端校验Token存在即执行操作并删除Token,重复请求因Token失效被拦截[^1][^4]。 - **适用场景**:用户交互请求(如表单提交、订单创建)。 - **示例代码**(SpringBoot + Redis): ```java // 生成Token接口 @GetMapping("/token") public String generateToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(token, "1", 10, TimeUnit.MINUTES); // 10分钟有效期 return token; } // 业务接口(带幂等注解) @Idempotent(fields = {"orderId"}) @PostMapping("/createOrder") public String createOrder(@RequestParam String token, String orderId) { // 业务逻辑 return "success"; } ``` #### 2. **数据库唯一约束** - **原理**:利用数据库唯一索引/主键约束(如订单号、支付流水号),重复插入会导致异常[^2][^3]。 - **适用场景**:新增类操作(如创建订单、支付记录)。 - **实现方式**: ```sql ALTER TABLE orders ADD UNIQUE INDEX idx_order_id (order_id); ``` 业务层捕获 `DuplicateKeyException` 异常并返回幂等响应。 #### 3. **幂等表方案** - **原理**:单独创建幂等表,以请求唯一标识(如 `request_id`)为主键,处理前先插入记录,重复请求因主键冲突失败[^2]。 - **优势**:与业务解耦,通用性强。 - **表结构示例**: | 字段 | 类型 | 说明 | |---------------|-------------|-------------------| | id | BIGINT | 自增主键 | | request_id | VARCHAR(64) | 唯一业务标识(UK) | | create_time | DATETIME | 记录创建时间 | #### 4. **状态机控制** - **原理**:基于业务状态流转(如订单状态:未支付→已支付),仅当当前状态符合预期才执行更新[^3]。 - **适用场景**:订单支付、状态变更。 - **SQL示例**: ```sql UPDATE orders SET status = 'paid' WHERE order_id = '123' AND status = 'unpaid'; -- 仅当状态为未支付时更新 ``` #### 5. **乐观锁机制** - **原理**:通过版本号(`version`)或时间戳控制更新,每次更新校验版本一致性[^2]。 - **实现示例**: ```java // 更新前先查询版本号 Order order = orderDao.selectById(orderId); // 更新时携带版本条件 int count = orderDao.update("UPDATE orders SET amount=100, version=version+1 WHERE id=#{id} AND version=#{currentVersion}"); if (count == 0) throw new IdempotentException("重复请求"); ``` #### 6. **分布式锁方案** - **原理**:使用Redis或Zookeeper实现互斥锁,确保同一请求同时只有一个线程处理[^4]。 - **Redis实现**: ```java String lockKey = "LOCK_ORDER_" + orderId; if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) { try { // 执行业务逻辑 } finally { redisTemplate.delete(lockKey); } } ``` #### 方案对比 | 方案 | 适用场景 | 复杂度 | 可靠性 | |--------------------|--------------------------|--------|--------| | Token机制 | 用户交互请求 | 中 | 高 | | 数据库唯一约束 | 数据新增操作 | 低 | 极高 | | 幂等表 | 所有写操作 | 中 | 高 | | 状态机/乐观锁 | 数据更新操作 | 中 | 高 | | 分布式锁 | 高并发场景 | 高 | 中 | > **最佳实践**:通常组合使用多种方案(如Token+数据库唯一约束),兼顾安全性与性能[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值