Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql的乐观锁和幂等性问题

mysql的乐观锁和幂等性问题以及解决方案

作者:找不到、了

这篇文章主要介绍了mysql的乐观锁和幂等性问题以及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

1、介绍

在分布式系统中,乐观锁幂等性设计数据插入失败处理是保障数据一致性和系统可靠性的三大核心机制,它们共同协作以解决并发冲突、重复请求和网络异常等问题。

1.乐观锁

2.幂等性设计

3.数据插入失败的处理

1、网络宕机

若插入操作未提交,数据库事务会自动回滚;若已提交部分数据,需通过补偿机制(如回滚或修复)修正。

2、重试机制

在网络恢复后,客户端可结合指数退避算法重试请求,但需确保重试操作是幂等的(如通过唯一约束或请求ID)。

3、异步队列

将请求放入消息队列(如 Kafka、RabbitMQ),确保网络中断时消息不丢失,恢复后继续处理。

典型场景示例

用户提交支付请求时,系统通过order_no的唯一约束防止重复订单,使用乐观锁避免并发修改价格,若网络中断则通过重试机制重新提交(但依赖幂等性设计避免重复扣款)。

核心目标

通过乐观锁保证数据一致性,幂等性防止重复操作,重试与补偿应对网络异常,三者结合构建高可用、可靠的分布式系统。

2、乐观锁

MySQL的乐观锁是一种并发控制机制,它假设数据冲突(多个事务同时修改同一数据)的概率较低,因此在读取数据时不加锁,而是在更新时检查数据是否被其他事务修改过。

如果冲突发生,事务会失败并重试。

2.1、核心思想

2.2、实现方式

在MySQL中,乐观锁通常通过以下方式实现:

1. 使用version字段(推荐)

示例表结构

CREATE TABLE product (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    price DECIMAL(10,2),
    version INT DEFAULT 0  -- 乐观锁版本号
);

示例操作

1.读取数据

SELECT id, name, price, version FROM product WHERE id = 1;
-- 假设返回: id=1, name='Apple', price=10.00, version=5

2.更新数据(带版本号检查):

UPDATE product 
SET price = 12.00, version = version + 1 
WHERE id = 1 AND version = 5;

3.判断是否更新成功

    如果version=5的记录还存在,则更新成功。

    如果version已经被其他事务修改为6,则更新失败(影响行数为0),此时需要抛出异常或重试。

    2. 使用timestamp字段

    1.读取数据

    SELECT id, name, price, update_time FROM product WHERE id = 1;
    -- 假设返回: id=1, name='Apple', price=10.00, update_time='2023-10-01 12:00:00'
    

    2.更新数据(带时间戳检查):

    UPDATE product 
    SET price = 12.00, update_time = NOW() 
    WHERE id = 1 AND update_time = '2023-10-01 12:00:00';
    

    3.判断是否更新成功

    如果update_time匹配,则更新成功。

    否则更新失败(影响行数为0)。

    2.3、如何处理冲突

    当乐观锁检测到冲突时(更新失败),应用程序需要:

    1. 抛出异常(如StaleObjectStateException)。
    2. 重试逻辑:重新读取数据,重新尝试更新(可能需要限制重试次数)。

    代码示例(Java)

    int retryCount = 0;
    while (retryCount < MAX_RETRIES) {
        Product product = getProductFromDatabase(productId); // 包含 version
        product.setPrice(newPrice);
        int rowsUpdated = updateProductInDatabase(product); // 使用 version 条件更新
        if (rowsUpdated == 1) {
            break; // 更新成功
        } else {
            retryCount++;
            // 可能需要等待一段时间再重试
        }
    }
    if (retryCount >= MAX_RETRIES) {
        throw new RuntimeException("乐观锁重试失败");
    }
    

    小结:

    它适用于读多写少冲突概率低的场景,能有效提高并发性能,但需要业务层配合实现冲突处理逻辑。

    如下图所示:

    对比于悲观锁

    2.4、乐观锁局限性

    3、幂等性

    通过上面对于乐观锁的介绍,感觉是不是可以作为幂等性的处理手段呢?

    乐观锁可以作为处理幂等性问题的一种手段,但它的作用和适用范围需要结合具体场景来看。

    3.1、什么是幂等性

    幂等性(Idempotency)是指同一个操作多次执行的结果与执行一次的结果相同

    例如:

    幂等性设计的核心目标

    防止因网络重传、用户重复点击、系统故障等原因导致的重复请求对业务逻辑产生副作用。

    3.2、乐观锁与幂等性的关系

    乐观锁(Optimistic Locking)主要用于解决并发更新时的数据一致性问题,而幂等性解决的是重复请求对业务逻辑的影响

    两者的结合可以增强系统的健壮性。

    1. 乐观锁如何辅助幂等性?

    乐观锁通过版本号(version)或时间戳(timestamp)保证数据更新的原子性,防止并发冲突。

    在某些场景下,它可以间接支持幂等性:

    2. 乐观锁的局限性

    乐观锁无法直接解决幂等性问题,因为它不处理“重复请求”的识别和过滤。

    例如:

    3.3、如何设计

    在实际开发中,通常需要将乐观锁与其他幂等性策略结合使用,例如:

    示例:支付接口的幂等性设计

    假设用户发起支付请求,接口需要确保同一笔订单不会被重复扣款:

    -- 表结构
    CREATE TABLE orders (
        id INT PRIMARY KEY,
        order_no VARCHAR(50) UNIQUE,  -- 唯一业务标识符
        amount DECIMAL(10,2),
        status VARCHAR(20),
        version INT DEFAULT 0  -- 乐观锁版本号
    );
    

    处理流程

    客户端发送请求,包含order_no和request_id(唯一请求ID)。

    服务端处理

    检查缓存或数据库,是否存在已处理的order_no或request_id。

    执行支付操作时,使用乐观锁更新订单状态:

    sql语句如下所示:

    UPDATE orders 
    SET status = 'PAID', version = version + 1 
    WHERE id = ? AND version = ?;
    

    如果更新失败(版本号不匹配),说明订单状态已被其他事务修改,需重试或报错。

    关键点

    3.4、order_no添加唯一约束

    1、防止重复创建订单

    假设用户点击“提交订单”按钮多次,或网络重传导致相同请求被多次发送。如果没有唯一约束,可能会导致以下问题:

    -- 假设没有唯一约束
    INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100);
    -- 用户重复提交相同订单号
    INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100); -- 会成功插入第二条数据!
    

    后果:系统会认为这是两个不同的订单,可能导致重复扣款、库存异常等问题。

    2、保证业务逻辑的正确性

    order_no是业务的核心标识符,如果允许重复,会导致:

    示例

    -- 有唯一约束后,第二次插入会失败
    INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100); -- 成功
    INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100); -- 报错:Duplicate entry
    

    3、支持幂等性设计

    唯一约束是实现幂等性的关键手段之一:

    示例

    -- 用户多次提交相同的订单号
    BEGIN TRANSACTION;
      -- 尝试插入订单
      INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100);
    COMMIT;
    
    -- 如果已经存在相同 order_no,会抛出异常,事务回滚,避免重复操作
    

    4、节点故障场景

    在插入数据的过程中如果发生网络宕机,处理方式取决于数据库的事务机制应用层的容错设计以及网络恢复后的重试策略

    以下是详细的分析和解决方案:

    4.1. 数据库层面

    1、事务的原子性

    如果插入操作被包裹在事务中(例如使用begin transaction和commit),且数据库支持事务(如 MySQL 的 InnoDB 引擎):

    BEGIN;
    INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100);
    -- 网络中断,事务未提交,数据不会写入数据库
    

    2、自动提交(Autocommit)

    如果数据库处于自动提交模式(默认开启),每次插入操作会立即提交:

    4.2. 应用层

    1、重试机制

    重试逻辑:在网络恢复后,客户端可以重试插入请求。

    重试策略

    2、幂等性设计

    唯一约束:通过数据库的unique约束(如订单号order_no)防止重复插入。

    请求 ID(Request ID):为每个请求生成唯一 ID,记录已处理的请求。

    3、异步消息队列

    可靠性队列:将插入操作放入消息队列(如 Kafka、RabbitMQ),确保网络中断时消息不丢失。

    4.3. 网络恢复后的处理

    1、客户端检测网络状态

    2、服务端日志与监控

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    您可能感兴趣的文章:
    阅读全文