首页 > 其他分享 >确保业务一致性:幂等性设计在分布式系统中的实现策略

确保业务一致性:幂等性设计在分布式系统中的实现策略

时间:2024-06-16 21:00:28浏览次数:37  
标签:请求 重复 支付 订单 token 确保 分布式系统 一致性 order

一、什么是幂等性?

幂等性(Idempotence)是计算机科学和数学中的一个概念,指的是一个操作或者函数,无论执行多少次,其效果和执行一次都是相同的。换句话说,重复执行这个操作不会对系统状态产生额外的影响。

在不同的领域,幂等性有不同的应用和表现:

在编程中
幂等性通常用于描述方法或者操作,例如,一个幂等的方法可以被多次调用,而每次调用的结果都是一样的,不会对系统状态产生副作用。

在分布式系统中
幂等性尤为重要,因为它确保了即使在网络波动、重试机制或者并发操作的情况下,操作也不会被重复处理,从而保证了数据的一致性和系统的稳定性。

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

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

二、幂等性的必要场景

幂等性在以下场景中尤为重要:

  • 在线支付:防止因重复请求导致的重复扣款。
  • 银行交易:确保交易不会因为网络重试而被执行多次。
  • 票务系统:避免座位被重复预订。
  • 通信服务:确保相同内容的请求只计费一次。
  • 任务调度:在定时任务或批处理中避免重复执行。
  • 用户注册:防止重复提交导致用户信息被创建多次。

三、幂等性问题的产生原因

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

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

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

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

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

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

四、案例说明

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

1、表结构:

图片

2、字段说明:

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

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

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

  • quantity:购买的商品数量。

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

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

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

  • 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. 状态机

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

状态机设计:

订单创建:订单初始化,状态为 PENDING(待支付)。
支付操作:当订单状态为 PENDING 时,允许执行支付操作,支付成功后状态变为 PAID(已支付)。
重复支付检查:如果再次尝试支付一个已经是 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;

标签:请求,重复,支付,订单,token,确保,分布式系统,一致性,order
From: https://blog.csdn.net/dazhong2012/article/details/139708233

相关文章

  • springboot的多线程事务能否保证事务的一致性
    在SpringBoot中,多线程事务的一致性取决于事务管理的配置和实现。SpringFramework本身提供了多种事务管理的方式,如基于注解的声明式事务、编程式事务等。在多线程环境下,要保证事务的一致性需要特别注意以下几点:事务传播属性(TransactionPropagation):在进行多线程事务处理时,需......
  • AI产品安全标准:只要意图和价值观与人类对齐,能力可以无限强于人类。如何确保超人的超能
    “人工智能系统正在成为日常生活的一部分。关键是要确保这些机器符合人类的意图和价值观。”AI标准:能力可以无限强悍,只要意图和价值观与人类对齐,就值得信赖随着人工智能(AI)技术的飞速发展,AI系统在各个领域展示出超凡的能力。这些技术不仅影响了我们的日常生活,还对社会的......
  • 如何确保数据跨域交换安全、合规、可追溯性?
    数据跨域交换是指在不同的组织、系统或网络之间进行数据的传输和共享。随着数字经济的发展,数据跨域交换在促进数据流通和创新融合方面发挥着重要作用。然而,这一过程也面临着诸多挑战和风险,例如数据安全、合规性、完整性以及责任不清晰等问题。在进行不同地域文件传输时,可能遇到......
  • Java面试:Redis如何保证数据一致性?
    Redis是一个内存数据结构存储系统,广泛用于缓存、会话管理等场景。尽管Redis本身不是传统的关系型数据库,它仍然提供了一些机制来保证数据一致性。以下是Redis保证数据一致性的一些方法和机制:1.事务机制(Transactions)Redis支持事务,通过MULTI、EXEC、DISCARD、WATCH等命令实......
  • 实时数据的处理一致性如何保证?
    实时数据一致性的定义以及面临的挑战数据一致性通常指的是数据在整个系统或多个系统中保持准确、可靠和同步的状态。在实时数据处理中,一致性包括但不限于数据的准确性、完整性、时效性和顺序性。下图是典型的实时/流式数据处理的流程:流式数据以各种方式推送到kafka中flink......
  • 脏读:数据一致性问题及解决方案
    目录前言一、脏读的定义二、脏读的原因三、解决脏读的方案四、Demo讲解前言        在多线程或分布式系统中,当多个线程或进程同时访问和修改共享资源时,可能会出现数据不一致的情况。其中一个经典的问题就是脏读。本文将详细介绍脏读的概念、原因和解决方案,帮......
  • 应用分发策略与渠道优化:确保应用成功触达用户的关键
    应用分发是指将安卓或iOS类型的应用下载到用户手机中的过程。这个过程可以细分为上架前的内测应用分发和上架后的正式应用分发。以下是关于应用分发的详细解释:上架前的内测应用分发:主要用于应用发布前的内部测试,确保应用在正式发布前能够达到预期的功能和性能要求。例如......
  • Redis-11-Redis与Mysql的数据一致性
    1.是什么数据一致性呢,讲的就是缓存中的数据和db中的数据是否能一致。2.为什么先看我这篇文章,了解下缓存策略:Redis-6-三种缓存读写策略在文章中,介绍了并发场景下的一致性问题,我们已经确定了一个基本思路:先更新db,再删除缓存。这个方案,奠定了我们在读写操作时的基本思路。2.1......
  • 思科配置基础代码内容针对网络构建中型局域网构建中型局域网需全面考虑企业网或校园网
    构建中型局域网需全面考虑企业网或校园网的需求。对于企业网,需确保高效数据传输、可靠设备连接及严格的安全防护;而校园网则需关注教学管理的便捷性、无线网络的覆盖以及未来扩展的灵活性。整体而言,构建与扩展需围绕用户需求、网络性能及安全性展开。一、配置三层交换机的端口......
  • 高并发下使用Redis分布式锁确保接口执行唯一性【重点】
    摘要:本文将介绍如何使用Redis分布式锁来确保在高并发环境下,确保某个接口只有一个线程能够执行。通过使用Redis的SETNX命令,我们可以实现一个简单的分布式锁,从而避免多个线程同时访问共享资源。一、背景在高并发的系统中,为了保证数据的一致性和完整性,我们经常需要对某些接口......