幂等设计的8种实现方式

【实战】聊聊幂等设计 前言 大家好,今天我们一起来聊聊幂等设计。 什么是幂等 为什么需要幂等 接口超时,如何处理呢? 如何设计幂等? 实现幂等的8种方案 HTTP的幂等 1. 什么是幂等? 幂等是一个数学与计算机科学概念。 在数学中,幂等用函数表达式就是:f(x) = f(f(x))。比如求绝对值的函数,就是幂等的,abs(x) = abs(abs(x))。 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。 2. 为什么需要幂等 阅读详情
AI摘要

本文深入解析了幂等性的概念及其应用场景,重点介绍了如何通过不同技术手段实现幂等性,包括唯一性约束、乐观锁、分布式锁等,并给出了具体的实现案例。

古语云:“一而再,再而三,其效不二”
俗语讲:被虐千百遍,依然如初恋
数学符号:f(f(f(x))) = f(x)
即无论操作执行一次还是多次,其效果始终如一,不会有差异。这就是幂等性。

文章导读

图片

什么是幂等性?

  接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。比如:公交车刷卡,用户上车后刷码支付扣款成功,如果用户再次点击按钮刷卡并扣款成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。因此,当你重复刷卡时,会提示:刷码重复。

注意:数据库可能产生幂等性问题,但是幂等性问题不只发生在数据库。

什么场景需要幂等设计?

一般对数据要求比较高的场景,如:金钱交易、数据一致性至关重要的业务场景:

  1. 在线支付:当用户发起支付请求时,避免重复扣款。

  2. 银行交易:确保同一笔交易不会因网络重试等原因被执行多次。

  3. 票务系统:在线购票平台在用户购票时,检查所选座位是否已被重复预订。

  4. 通信服务:如短信或通话服务,系统会检查是否已为相同内容的请求计费。

  5. 任务调度:在定时任务或批处理系统中,确保不会因为任务重启或重试而重复执行相同的操作。

  6. 用户注册:防止因重复提交表单而导致用户信息被创建多次。

......

如何产生幂等问题?

产生幂等性问题的原因主要有:

1.网络请求重试:网络波动或超时,客户端可能会重复发送相同的请求。

2.用户界面重复提交:用户在用户界面上可能会不小心重复点击按钮,导致相同的请求被发送多次。

3.消息队列重试机制:使用消息队列(如Kafka、RabbitMQ)时,消息可能会被重复消费。

4.数据库并发操作:数据库的插入、更新和删除操作多个事务同时修改同一条记录,而没有使用适当的锁机制或事务隔离级别。

5.外部系统API接口重试:对外提供的API接口可能由于调用方的重试逻辑,导致数据库操作被重复调用。

6.其它......

下面我们简单做些案例说明。

表设计(前期准备)

我们先来设计一张订单表并模拟一些数据:

1、表结构:

图片

2、字段说明:

  1. order_id:作为订单的唯一标识,通常是一个全局唯一的ID,如使用UUID或分布式ID生成器(如Snowflake算法)生成。

  2. user_id:标识下单的用户,用于关联用户信息。

  3. product_id:标识被购买的商品,用于关联商品信息。

  4. quantity:购买的商品数量。

  5. order_status:订单当前状态,用于控制订单的业务流程,确保幂等性。例如,只有当订单状态为“待支付”时,支付操作才会被执行。

  6. create_time:记录订单创建的时间戳。

  7. pay_time:记录订单支付的时间戳,如果订单被支付,这个字段会被更新。

  8. version:乐观锁的版本号,每次更新操作都会增加该字段的值,用于检测在业务处理期间订单是否被其他事务更新过。

3、业务规则:

  • 订单支付:在支付操作前,先检查order_status是否为“待支付”,若是,则执行支付逻辑,并更新order_status为“已支付”;如果不是,则拒绝支付,保持订单状态不变。

  • 订单取消:在取消操作前,同样检查order_status,只有订单在特定状态下才允许取消操作。

  • 插入订单:使用order_id作为唯一约束,防止重复插入相同订单。

  • 乐观锁:在更新订单状态时,使用version字段来确保在读取和更新之间没有其他事务更改了订单,如果读取的version和数据库中的version不一致,则拒绝更新。

4、数据状态

图片

幂等性解决方案

幂等性设计方案通常在分布式系统中,常见的幂等性设计方案如下:

1、唯一性约束

利用数据库的唯一性约束,如唯一索引或主键,来避免插入重复数据。

mysql> INSERT INTO `mydb`.`orders` (`order_id`, `user_id`, `product_id`, `quantity`, `order_status`, `create_time`, `pay_time`, `version`) VALUES ('ORD-20231023-0001', 'USR-A123456', 'PRD-X123', 2, 0, '2023-10-23 10:15:30', NULL, 1);
ERROR 1062 (23000): Duplicate entry 'ORD-20231023-0001' for key 'orders.PRIMARY'

注意:业务上要求生成全局唯一的主键。且不是自增策略,否则在分库分表的场景下,不同的表之间主键互不关联。

2. 乐观锁

  通过记录数据的版本号或时间戳,仅当数据未被其他事务修改时,才允许更新操作执行。每次更新数据时,版本号都会递增。

UPDATE orders
SET
  quantity = 1,
  order_status = 1,
  pay_time = '2024-04-30 10:20:00',
  version = version + 1
WHERE
  order_id = 'ORD-20231023-0001' AND
  version = 1;

效果演示:

图片

如果 Session-01 已经提交了事务,Session-02 的更新操作将不会影响任何行,因为 version 已经从 1 增加到了 2。

3. 悲观锁

  使用悲观锁,事务在读取数据时会锁定相应的数据行,直到事务结束(提交或回滚)。这可以防止其他事务在锁定期间修改这些数据,从而确保数据的一致性。

在执行读取操作时,使用 SELECT ... FOR UPDATE 语句来锁定相关记录。

-- 锁定记录
SELECT * FROM orders WHERE order_id = 'ORD-20231023-0001' FOR UPDATE;

-- 执行业务逻辑
 UPDATE orders SET quantity = 1, order_status = 1, pay_time = '2023-10-23 10:20:00' WHERE order_id = 'ORD-20231025-0003';

效果演示:

图片

  由此可见,悲观锁确保每个事务也能安全地执行,而不会导致数据不一致的问题。但是,悲观锁可能会因为锁定机制而导致 性能问题 ,尤其是在高并发的系统中,这可能会引起 锁争用和死锁 。

4. 分布式锁

在分布式系统中,使用分布式锁来保证同一时间只有一个实例处理特定消息或请求。

图片

当前使用redis分布式锁案例实现,

public class MyService {
    private final RedisDistributedLock lock;

    public MyService(Jedis jedis, String lockKey, int lockTimeout) {
        this.lock = new RedisDistributedLock(jedis, lockKey, lockTimeout);
    }

    public void executeInLock() {
        if (lock.tryLock()) {
            try {
                // 执行业务逻辑
            } finally {
                lock.unlock();
            }
        } else {
            // 处理无法获取锁的情况,例如重试或记录日志
        }
    }
}

这里顺便提一句,建议采用Lua脚本实现删除锁的逻辑,保证原子性。

public void unlock() {
        // 释放锁,使用Lua脚本来确保原子性
        String unlockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                              "return redis.call('del', KEYS[1]) " +
                              "else " +
                              "return 0 " +
                              "end";
        jedis.eval(unlockScript, 1, lockKey, "1");
}

5. Token令牌机制

  为每个请求生成一个唯一的Token,并在服务端进行校验,一旦处理了对应的请求,就丢弃该Token,避免重复处理。具体步骤:

图片

1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。

2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。

3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。

4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。

核心逻辑:

// 服务端接口,接收请求并处理token
void do(String token) {
    if (Redis.exists(token)) {
       // 删除token,确保不会重复处理
        Redis.del(token); 
        // 执行具体的业务操作
        doSometing(); 
    } else {
        log.info(token); 
    }
}

注意:最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。可以在 redis 使用 lua 脚本完成这个操作

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

6. 状态机

  使用状态机是判断业务流程,确保操作只执行一次。

状态机设计:

  1. 订单创建:订单初始化,状态为 PENDING(待支付)。

  2. 支付操作:当订单状态为 PENDING 时,允许执行支付操作,支付成功后状态变为 PAID(已支付)。

  3. 重复支付检查:如果再次尝试支付一个已经是 PAID 状态的订单,状态机将拒绝该操作,保持订单状态不变。

实现案例

public enum OrderStatus {
    PENDING, PAID, CANCELLED
}

public class Order {
    private OrderStatus status; // 订单当前状态
    // 其他订单属性...

    public Order() {
        this.status = OrderStatus.PENDING; // 初始化状态为待支付
    }

    // 执行支付操作
    public synchronized void pay() {
        if (this.status == OrderStatus.PENDING) {
            // 执行支付逻辑,如减少库存、扣款等
            this.status = OrderStatus.PAID; // 状态转变为已支付
        } else {
            // 如果订单不是在待支付状态,抛出异常或记录日志
            throw new IllegalStateException("Order can only be paid when status is PENDING");
        }
    }

    // 其他业务逻辑...
}

幂等性保证:

  • 支付操作 pay 在订单状态不是 PENDING 时不会被执行,从而保证了幂等性。

  • 如果有重复的支付请求,由于状态机的保护,第二次及后续的支付请求将不会改变订单状态,因此不会执行重复的支付逻辑。

7. 去重表

  记录已经处理过的请求标识,对于重复的请求直接返回结果,而不再次执行业务逻辑。

1、去重表结构设计

表字段至少包括:

  • 请求标识符:唯一标识一次请求。

  • 创建时间:记录请求的时间戳。

  • 处理状态:标识请求是否已处理,以及处理的结果。

2、设置过期策略

  为了防止去重表无限增长,表中的记录可以设置过期时间。使用定时任务定期清理旧的请求记录。

实现案例:

1、检查去重表

在执行业务逻辑之前,检查去重表确定该请求是否已经被处理过。

boolean isDuplicate = checkDuplicateInDatabase(requestId);

2、处理请求

if (isDuplicate) {
    // 返回之前的结果或拒绝处理
    return previousResult;
} else {
    // 执行业务逻辑
    doSomthing();
    // 记录去重表
    saveRecord(requestId);
    // 返回新的结果
    return newResult;
}

注意事项:

  • 数据一致性:确保去重表的更新与业务逻辑的执行保持一致性,避免出现数据不一致的情况。

8. 全局请求唯一ID

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。

可以使用 nginx 设置每一个请求的唯一 id;

proxy_set_header X-Request-Id $request_id;
探秘幂等性设计——唯一索引,乐观锁,悲观锁,分布式锁 本文深入探讨了幂等性设计在构建高可用、高并发分布式系统中的重要作用。文章首先介绍了幂等性的概念和常见实现方案,如唯一索引、乐观锁和悲观锁。接着重点分析了高并发场景下实现幂等性的难点,并提出引入分布式锁的解决方案,详细介绍了分布式锁的原理、实现和注意事项。 阅读详情

相关推荐

实战,实现幂等的8种方案!

幂等是一个数学与计算机科学概念。。比如求绝对值的函数,就是幂等的,。计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。

m0_71777195的博客 3526

幂等性设计

1. 什么是幕等   幂等是一个数学上的概念,它的含义是多次执行同一个操作和执行一次操作,最终得到的结果是相同的。 数学表达:f(x) = f(f(x))。 例子:比如,男生和女生吵架,女生抓住一个点不放,传递“你不在乎我了吗?”(生产消息)的信息。那么当多次抱怨“你不在乎我了吗?”的时候(多次生产相同消息),她不知道的是,男生的耳朵(消息处理)会自动把 N 多次的信息屏蔽,就像只听到一次一样,这就是幂等性。   可以这么理解“幂等”:一件事无论做多少次都和做一次产生的结果是一样的,那么这件事儿就具有幂等

行走 874

实战~聊聊幂等设计,一文解决

前言 大家好,今天我们一起来聊聊幂等设计。 什么是幂等 为什么需要幂等 接口超时,如何处理呢? 如何设计幂等? 实现幂等的8种方案 HTTP的幂等 1. 什么是幂等? 幂等是一个数学与计算机科学概念。 在数学中,幂等用函数表达式就是:f(x) = f(f(x))。比如求绝对值的函数,就是幂等的,abs(x) = abs(abs(x))。 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。 2. 为什么需要幂等..

SharingOfficer的博客 901

聊聊幂等设计

前言 今天我们一起来聊聊幂等设计。什么是幂等为什么需要幂等接口超时,如何处理呢?如何设计幂等?实现幂等的8种方案HTTP的幂等1. 什么是幂等? 幂等是一个数学与计算机科学概念。在数学中,...

weixin_31351409的博客 211

用友U8采购订单增删改审接口开发:基于U8API的CO方式实践

企业ERP系统集成中,单据的自动化处理是提升供应链效率的关键。用友U8通过U8API的CO方式(基于COM组件的VoucherCO对象)提供了与界面操作一致的单据管理能力。围绕COM组件原理,讲解采购订单新增、修改、删除、审核等动作的接口调用方法,包括登录连接串配置、表头表体字段赋值、事务与幂等控制等工程实践。该方案具备完整的校验规则和权限控制,适合MES、WMS等内网系统与U8的采购订单对接。通过最小可运行示例和踩坑分析,开发者可快速构建稳定可靠的U8采购订单增删改审接口。

weixin_29758911的博客 346

Delphi中使用FireDAC与uniDAC高效连接PostgreSQL的实战指南

本文为Delphi开发者提供了使用FireDAC与uniDAC组件高效连接PostgreSQL数据库的实战指南。文章详细对比了两款组件的选择策略,重点解析了驱动依赖(libpq.dll)的配置、连接参数设置、常见错误排查及性能优化技巧,旨在帮助开发者快速构建稳定可靠的数据库连接。

tomato的博客 805

68.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--常用支付方式

本文介绍了支付方式管理功能的实现方案,主要包括以下内容: 支付方式类型设计:采用两级枚举结构,主类型区分现金/信用卡/电子支付,子类型专门处理电子支付的细分(支付宝/微信等) 数据模型构建: 实体类继承基础模型,包含名称、类型、默认标识等字段 电子支付子类型设为可空,仅在主类型为电子支付时使用 数据传输对象: 定义新增/编辑/响应三类DTO 包含必要的验证规则和字段约束 业务规则: 新增时自动处理默认支付方式 删除时保护默认方式不被误删 类型切换时联动清空子类型 接口设计:通过RESTful风格暴露五个核心

喵叔 470

VFIO框架源码分析(一)- 从初始化到设备访问

本文深入剖析了Linux内核中的VFIO(Virtual Function I/O)框架,详细解读了其从初始化到设备访问的全过程。VFIO通过Container->Group->Device的层次化设计,利用IOMMU提供安全隔离,允许用户空间直接访问物理设备。文章基于Linux v5.15版本,重点分析了vfio_init模块入口、Container创建与配置、Group关联以及最终设备访问等关键环节,揭示了VFIO如何通过逐层授权机制构建安全可靠的设备直通模型,为虚拟化技术提供核心支持。

qq_36071928的博客 380

什么是幂等设计

我们的接口,一般都是基于http的,所以我们再来聊聊Http的幂等吧。的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。把请求储存起来,收到请求时,先查下存储记录,记录存在就返回上次的结果,不存在就处理请求。HTTP PUT 方法用于创建或更新操作,所对应的URI是要创建或更新的资源本身,有副作用,它应该满足幂等性。,但是UUID的缺点比较明显,它字符串占用的空间比较大,生成的ID过于随机,可读性差,而且没有递增。

辣香牛肉面的博客 818

从零构建:如何用MATLAB和SDR硬件搭建你的第一个OFDM卫星通信原型系统

本文详细介绍了如何利用MATLAB和SDR硬件从零构建OFDM卫星通信原型系统。通过核心组件选型、开发环境配置、OFDM算法实现到系统集成与优化的完整流程,帮助通信工程学生和DIY爱好者快速掌握软件无线电技术在卫星通信中的应用。特别推荐使用HackRF One和MATLAB工具箱实现高性价比的OFDM系统验证。

pz890123456的博客 378

【物联网毕设】多模态智能门禁系统设计(STM32+人脸识别+蓝牙联动)

本文详细介绍了基于STM32的多模态智能门禁系统设计,整合了人脸识别、蓝牙联动、RFID和密码等多种开锁方式。文章从硬件选型、电路设计、嵌入式软件编程到手机APP开发,提供了完整的毕设实现方案与调试指南,帮助物联网与嵌入式方向的学生构建一个功能全面、可扩展的智能门禁系统。

rain6的博客 247
上一篇: 我用这11招,让接口性能提升了100倍
下一篇: 4 种 MySQL 同步 ES 方案,yyds!
Java_2017_csdn
博客等级 码龄10年 329粉丝 49原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值