防重设计与接口幂等设计详解
防重设计与接口幂等设计
一、防重设计与接口幂等性区别
- 防重设计
防重设计主要是为了防止重复操作或重复请求导致数据的重复处理或存储。
其核心目的是:确保操作的唯一性和数据的准确性。
例如,在电商系统中,防止用户重复提交订单是典型的防重设计应用场景。
常见的防重设计方法有:
- 唯一性约束:数据库层面通过设置唯一键来防止重复数据的插入。
- 业务层防重:在业务逻辑层面通过各种手段(如分布式锁、唯一标识符等)来防止重复操作。
- 前端防重:通过前端页面按钮禁用等方式防止用户重复提交。
- 幂等性设计
接口幂等性指的是某个操作无论执行多少次,其结果都是相同的。
幂等性是分布式系统、微服务架构中设计接口时的重要原则,确保系统在多次重复调用同一个接口时不会产生副作用。
幂等性常见的实现方法:
- GET请求:通常是天然幂等的,因为它只是读取数据而不修改数据。
- PUT请求:更新操作,重复执行多次结果不变。
- DELETE请求:删除操作,删除某个资源多次结果一致。
- POST请求的幂等性:通过引入唯一请求ID(如UUID)来确保多次提交的结果一致。
二、防重设计
- 数据库唯一性约束
- 唯一键:通过在数据库表中设置唯一键来防止重复数据的插入。例如,可以在订单表中设置订单号(Order ID)为唯一键,这样在插入重复订单时会报错。
- 唯一索引:在数据库中设置唯一索引,确保特定字段组合的唯一性。
- 业务逻辑层防重
- 分布式锁:在分布式系统中,可以使用分布式锁(如Redis、Zookeeper等)来防止多个实例同时处理同一个请求。
- 唯一请求ID:在每次请求中生成唯一的请求ID(如UUID),在处理请求时首先检查该ID是否已经处理过,如果处理过则直接返回结果。
- 状态标识:在业务逻辑中设置状态标识,例如订单状态字段,确保同一订单在不同状态下不能重复处理。
- 前端防重
- 按钮禁用:在前端页面中,用户提交请求后立即禁用提交按钮,防止用户重复点击。
- 防重复提交机制:在前端和后端之间进行防重复提交的交互,例如通过Token机制,每次提交表单时附带一个唯一的Token,服务器验证Token的有效性。
三、接口幂等性设计
一、接口幂等性的必要性
\1. 接口幂等性的定义
一个接口在多次调用的结果和调用一次的结果相同。
一个幂等性的接口,无论重读调用多少次,系统的状态都保持一致,不会因为多次调用而导致不一样的结果。幂等性可以增加系统的可靠性。
在Web开发中,由于重试机制或者网络不稳定,经常导致对接口重复调度用
\2. 非幂等性接口的危害场景
在线支付场景,用户购买商品下单,跳转到支付页面,点击了支付按钮进行扣款,假设系统在返回支付结果的时候出现网络异常,此时后台已经完成扣款,但是用户没有看到支付成功的结果,再次点击支付按钮,会进行二次扣款,造成用户的钱多扣了。
二、常见的重复请求场景
3.1 前端的表单重复提交
类似上面的支付场景,前端表单在提交时遇到网络波动,没有及时对用户做出提交成功响应,导致用户认为没有提交成功,然后一直点提交按钮,这时就会发生重复提交表单请求。
3.2 接口超时重试
很多http、rpc请求在实现的时候都会添加超时重试机制,为了防止网络波动超时等造成请求失败,这样就可能出现一次请求变成多次请求。
3.3 消息重复消费
当使用MQ消息中间件时候,如果Consumer消费超时 或者 producer 发送了消息,但是由于网络原因没有收到ACK导致消息重发,都会出现重复请求
3.4 恶意攻击
比如网上投票,黑客会针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
三、哪些接口需要幂等
接口幂等性的验证和实施需要消耗一定的资源,因此并非每个接口都应该被赋予幂等性验证。
相反,这种决策应该基于实际业务需求和操作类型进行区分。
以查询和删除操作为例,这两种操作通常不需要进行幂等性验证。
- 在查询操作中,无论执行一次还是多次,结果都是一致的,因此无需进行幂等性验证。
- 对于删除操作,无论是执行一次还是多次,都是将相关数据进行删除(这里指的是有条件的删除而不是删除所有数据),因此也无需进行幂等性验证。
后台的业务接口无非就是增删改查四种接口,幂等性如下表:
| 接口类型 | 描述 | 是否幂等 |
|---|---|---|
| 新增操作 | 新增操作每次执行都会往db新增数据 | ✖ |
| 更新操作 | 修改在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子: 1. 把表中id为XXX的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的 2. 把表中id为XXX的记录的A字段值增加1,这种操作就不是冪等的 | 分情况 |
| 查询操作 | 查询用于根据条件获取资源,并不会对当前系统资源进行改变 | ✅ |
| 删除操作 | 删除一次和多次删除都是把数据删除,效果一致 | ✅ |
| 四、幂等性常见解决方案 |
\1. 乐观锁
通过新增一个 version 字段来记录当前记录的版本号,发起接口请求的时候要携带版本号。
比如当前有一个商品记录:
Id = 1; name = iphone; price = 9000; version = 2
现在我需要调整价格,更新前查询到 version = 2,调整完之后对 version+1:
Update t set price = 9999,version = version + 1 where id = 1 AND version = 10
多次请求只要verison被执行了一次,其他的sql就不会生效。
\2. 防重Token令牌
为了应对客户端连续点击或调用方的超时重试等情况,例如在提交订单时,可以通过 Token 机制来防止重复提交。简而言之,调用方在调用接口之前会首先向后端请求一个全局ID(Token),并在请求时将该全局ID与其他数据一同发送(最好将Token放置在Headers中)。
后端会将该Token作为键,用户信息作为值存储在Redis中进行键值内容校验如果该键存在且值匹配,就会执行删除命令(用lua脚本保证原子性),然后正常执行后续的业务逻辑。
如果找不到对应的键或值不匹配,则表明是重复请求,不会再执行业务逻辑,直接返回重复请求信息,从而确保幂等性操作。

2.1 注意事项
- 检查token是否在redis中 + 删除key 这两步建议用lua脚本实现,保证原子性
- 全局唯一ID可用 业界的唯一ID生成算法生成 美团Leaf、、、、
2.2 问题分析
通过redis + token的方式虽然绕开了db层面来进行幂等性的校验,总的效率来说会高很多,但是却存在着不够精准的场景,不能够做到完全幂等性保证。
假设某个客户端第一次发起请求,然后服务端收到后将token从Redis中删除,接着去执行业务逻辑,但是业务逻辑执行失败了,此时有两种可能:
- 此时服务端可能会向客户端返回执行失败,客户端收到该返回后自动重新请求一个token,然后再次发起请求重试,这种场景下是正常请求,不存在幂等性问题
- 如果此时服务端向客户端返回执行失败的过程中,由于网络或其他什么原因导致 客户端无法接收到 执行失败 响应。那么此时客户端会再次使用 第一次申请的token 再次向服务端发送请求,但是此时服务端返回的确却是 重复请求 或 执行成功(这个是业务去定义的)
但综合效率以及网络故障概率等因素总体来说,这种方案实用性较强没有明显的缺陷。
如果在使用这种方式的基础上想要保证严格意义上的幂等性,可以结合业务场景,在db层加上我们之前的三种方案进行兜底。
2.3 思考:防重token 和 分布式锁
token防重令牌的方式跟分布式锁的方式很像,都是维护一个全局资源,类似于一个全局锁,获取到了才有资格进行请求处理,那用分布式锁来处理幂等性可以吗,加锁成功执行请求处理,加锁失败说明请求已经在处理了,直接返回?
答案是不合适的
思考下面三种情况
- 客户端连续发起两次请求(比如用户快速点击按钮的情况),第一次请求先到达服务端,然后第二次请求由于某些原因过了一会儿才到达服务端。等第二次请求达到服务端的时候,第一次请求已经执行完毕并且释放了锁。此时第二次请求仍然能加锁成功,并且执行业务逻辑。这种情况下幂等性失效。
- 客户端发起第一次请求,服务端正常执行完毕并释放了分布式锁但由于网络原因客户端没有正常收到服务端的响应,此时客户端再次发起请求。由于第一次请求所加的分布式锁已经过期所以第二次请求仍然能够加锁成功,然后执行业务逻辑。此时幂等性失效
- 客户端连续发起多次请求,这多次请求同时到达服务端,此时开始争抢锁,谁抢到锁谁就执行,其他没有抢到锁的请求都统统不执行。这种情况能保证幂等性。