服务端接口幂等性设计的五种常用方案

56 阅读7分钟

服务端接口幂等性设计的五种常用方案 在服务端开发的世界里,接口幂等性是一个至关重要的概念。那么什么是接口幂等性呢?简单来说,就是对同一操作,无论执行多少次,所产生的影响都和执行一次是一样的。这就好比我们反复按电梯的关门按钮,门只会关一次,不会因为多按几次就反复开关。在实际的服务端系统中,接口幂等性设计可以避免因重复请求带来的各种问题,如数据重复插入、资金重复扣除等。接下来,就为大家详细介绍服务端接口幂等性设计的五种常用方案。

  1. 唯一索引 唯一索引是数据库层面保证幂等性的一种简单而有效的方法。我们可以把它想象成每个人的身份证号码,在全国范围内是唯一的,不会出现重复。在数据库中,为表的某个字段或多个字段组合创建唯一索引,当插入重复数据时,数据库会自动拒绝,从而保证数据的唯一性。 例如,在一个订单系统中,每个订单都有一个唯一的订单编号。我们可以在订单表的订单编号字段上创建唯一索引。当客户端发起创建订单的请求时,如果因为网络等原因导致请求重复发送,数据库会因为唯一索引的存在,拒绝插入重复的订单记录。 创建唯一索引的操作通常很简单,以 MySQL 为例,我们可以使用以下 SQL 语句来创建唯一索引: CREATE UNIQUE INDEX idx_order_no ON orders (order_no); 这里的 idx_order_no 是索引的名称,orders 是表名,order_no 是要创建唯一索引的字段。 唯一索引的优点是实现简单,数据库会自动处理重复数据的插入问题,不需要在代码中做额外的处理。但它也有一定的局限性,比如只能保证数据库层面的幂等性,对于更新操作等无法提供全面的幂等性支持。

  2. 悲观锁 悲观锁就像是一个严格的门卫,在它看守的范围内,一次只允许一个人进行操作。在数据库中,悲观锁通常是通过对数据加锁的方式来实现的,当一个事务对数据进行操作时,会将数据锁住,其他事务必须等待该事务释放锁后才能对数据进行操作。 以一个库存系统为例,当有用户下单时,需要对商品的库存进行扣减操作。为了保证幂等性,我们可以使用悲观锁。在查询库存数据时,使用 SELECT ... FOR UPDATE 语句对库存记录加锁。 示例代码如下: BEGIN; SELECT stock FROM products WHERE product_id = 1 FOR UPDATE; -- 检查库存是否足够 -- 如果足够,进行扣减操作 UPDATE products SET stock = stock - 1 WHERE product_id = 1; COMMIT; 在这个过程中,当一个事务执行 www.ysdslt.com/SELECT ... FOR UPDATE 语句时,会将该商品的库存记录锁住,其他事务无法对该记录进行操作,直到当前事务提交或回滚释放锁。这样就可以避免因重复请求导致库存被多次扣减的问题。 悲观锁的优点是可以保证数据的一致性和幂等性,适用于并发量不高的场景。但它也有明显的缺点,由于加锁会导致其他事务等待,会降低系统的并发性能,可能会出现死锁等问题。

  3. 乐观锁 乐观锁和悲观锁不同,它就像是一个开明的管理者,相信大多数情况下不会出现冲突,所以在操作数据时不会立即加锁。乐观锁通常是通过在数据库表中添加一个版本号字段来实现的。 还是以库存系统为例,我们在商品表中添加一个 version 字段。当客户端发起扣减库存的请求时,首先查询商品的库存和版本号,然后在更新库存时,会检查版本号是否和查询时的一致。如果一致,则更新库存并将版本号加 1;如果不一致,则说明数据已经被其他事务修改过,当前操作失败。 示例代码如下: -- 查询库存和版本号 SELECT stock, version FROM products WHERE product_id = 1; -- 假设查询到的库存为 10,版本号为 1 -- 进行扣减操作 UPDATE products SET stock = stock - 1, version = version + 1 WHERE product_id = 1 AND version = 1; 如果更新语句影响的行数为 0,则说明版本号不一致,操作失败,需要重新查询和尝试。 乐观锁的优点是不会像悲观锁那样影响系统的并发性能,适用于并发量较高的场景。但它也有一定的局限性,比如需要在代码中处理操作失败的情况,可能会导致业务逻辑复杂。

  4. 令牌机制 令牌机制就像是我们去游乐园时的门票,每张门票只能使用一次。在服务端接口幂等性设计中,令牌机制是通过生成唯一的令牌来实现的。客户端在发起请求前,先向服务端请求一个令牌,服务端生成一个唯一的令牌并返回给客户端。客户端在请求接口时,将该令牌作为参数传递给服务端。服务端在处理请求时,会检查该令牌是否已经使用过,如果使用过则拒绝处理该请求,否则处理请求并将该令牌标记为已使用。 具体实现步骤如下:

  5. 客户端请求令牌:客户端向服务端发送获取令牌的请求,服务端生成一个唯一的令牌,如 UUID,并将该令牌存储在缓存(如 Redis)中,设置一个过期时间。

  6. 客户端携带令牌请求接口:客户端在发起实际的业务请求时,将获取到的令牌作为参数传递给服务端。

  7. 服务端验证令牌:服务端接收到请求后,检查缓存中是否存在该令牌。如果存在,则处理请求并将该令牌从缓存中删除;如果不存在,则说明该令牌已经使用过,拒绝处理该请求。 示例代码如下(使用 Python 和 Redis 实现): import redis import uuid r = redis.Redis(host='localhost', port=6379, db=0)

生成令牌

token = str(uuid.uuid4()) r.set(token, 1, ex=60)

客户端请求接口时携带令牌

def process_request(token): if r.exists(token): r.delete(token) # 处理业务逻辑 return '处理成功' else: return '请求重复,拒绝处理' 令牌机制的优点是可以在服务端层面保证接口的幂等性,适用于各种类型的接口。但它也有一定的缺点,需要额外的缓存来存储令牌,增加了系统的复杂度和维护成本。

  1. 状态机 状态机就像是一个流程控制器,它会根据不同的状态来控制操作的执行。在服务端接口幂等性设计中,状态机是通过定义业务的状态和状态转换规则来实现的。 以订单系统为例,订单通常有多个状态,如创建、支付、完成等。我们可以定义订单状态的转换规则,只有在特定的状态下才能执行相应的操作。 例如,当订单处于创建状态时,客户端可以发起支付请求。服务端在处理支付请求时,会检查订单的状态是否为创建状态,如果是,则执行支付操作并将订单状态更新为支付中;如果订单状态不是创建状态,则拒绝处理该请求。 示例代码如下: class Order: def init(self, order_id, status='created'): self.order_id = order_id self.status = status def pay(self): if self.status == 'created': # 执行支付操作 self.status = 'paying' return '支付成功' else: return '订单状态不允许支付' 状态机的优点是可以根据业务的实际情况灵活定义状态和状态转换规则,保证业务的幂等性。但它也有一定的局限性,需要对业务逻辑有深入的理解,状态机的设计和维护比较复杂。 综上所述,服务端接口幂等性设计的这五种常用方案各有优缺点,在实际开发中,需要根据具体的业务场景和系统需求来选择合适的方案。有时候,也可以将多种方案结合使用,以达到更好的效果。