🌺The Begin🌺点点关注,收藏不迷路🌺

前言

在 MySQL 数据库高并发场景下,数据一致性问题往往是系统设计的重中之重。当多个事务同时操作同一条数据时,如何避免“丢失更新”、“脏读”等问题?锁机制就是解决这类问题的核心手段。本文将通过图文并茂的方式,详细剖析 MySQL 中的乐观锁悲观锁,包括它们的原理、适用场景以及具体的代码实现。

1. 什么是悲观锁(Pessimistic Lock)

悲观锁是一种“先获取锁,再操作数据”的机制。它认为当前事务操作数据时,一定会有其他事务来并发修改,所以它在操作数据之前会主动加锁,直到当前事务提交或回滚后,锁才会释放。在此期间,其他任何想操作这条数据的事务都会被阻塞。

1.1 悲观锁的实现原理

事务开始

SELECT ... FOR UPDATE 加排他锁

加锁成功?

等待锁释放

读取数据并执行业务逻辑

UPDATE 更新数据

COMMIT 提交事务释放锁

事务结束

在 MySQL 的 InnoDB 引擎中,使用 SELECT ... FOR UPDATE 语句实现悲观锁。该语句会对查询出的数据行加上排他锁(X锁)。需要注意的是,使用悲观锁时要关闭自动提交SET autocommit=0)。

1.2 悲观锁的适用场景

  • 写操作频繁:数据冲突概率高,悲观锁可以避免大量无效的重试。
  • 强一致性要求:例如银行转账、扣减库存等对数据准确性要求极高的场景。

1.3 悲观锁代码示例

假设我们有一个商品库存表 product_stock

CREATE TABLE `product_stock` (
  `id` int NOT NULL AUTO_INCREMENT,
  `product_name` varchar(100) DEFAULT NULL,
  `stock` int DEFAULT '0',
  `version` int DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

使用悲观锁扣减库存:

-- 事务1
SET autocommit = 0;
BEGIN;

-- 加排他锁查询当前库存
SELECT stock FROM product_stock WHERE id = 1 FOR UPDATE;

-- 假设业务逻辑判断 stock >= 1
UPDATE product_stock SET stock = stock - 1 WHERE id = 1;

COMMIT;  -- 释放锁

如果另一个事务同时在执行相同的 FOR UPDATE,会被阻塞,直到第一个事务提交。

2. 什么是乐观锁(Optimistic Lock)

乐观锁是一种“先操作数据,提交时检查冲突”的机制。它认为数据一般情况下不会发生冲突,所以不加锁,只在更新时通过版本号或时间戳判断数据是否被修改过。如果发现数据已被其他事务修改,则放弃当前操作或重试。

2.1 乐观锁的实现原理

匹配成功

匹配失败

读取数据与版本号 version

执行业务逻辑计算新值

执行 UPDATE 更新

WHERE version = 旧版本号

更新成功 version+1

检测到冲突

重试或返回失败

乐观锁通常通过版本号(version)时间戳来实现。核心 SQL 如下:

UPDATE table_name 
SET stock = new_stock, version = version + 1 
WHERE id = ? AND version = old_version;

如果 affected rows = 1 表示更新成功;如果 = 0 表示数据已被其他事务修改,需要重试。

2.2 乐观锁的适用场景

  • 读操作远多于写操作:如文章阅读量统计、用户点赞等。
  • 冲突概率低:系统整体并发量可控,偶尔发生的冲突通过重试解决。

2.3 乐观锁代码示例(基于版本号)

业务层伪代码(Java 示例):

public boolean deductStock(int productId, int quantity) {
    int retryCount = 3;
    while (retryCount-- > 0) {
        // 1. 查询当前商品信息(包含版本号)
        ProductStock stock = dao.selectById(productId);
        int oldVersion = stock.getVersion();
        int oldStock = stock.getStock();
        
        if (oldStock < quantity) {
            return false; // 库存不足
        }
        
        int newStock = oldStock - quantity;
        int newVersion = oldVersion + 1;
        
        // 2. 使用版本号条件更新
        int rows = dao.updateStockWithVersion(productId, newStock, newVersion, oldVersion);
        if (rows == 1) {
            return true; // 更新成功
        }
        // 3. 更新失败,说明版本号已变化,进入重试
    }
    return false; // 重试耗尽,更新失败
}

对应的 SQL:

UPDATE product_stock 
SET stock = newStock, version = newVersion 
WHERE id = productId AND version = oldVersion;

3. 乐观锁 vs 悲观锁:全面对比

对比维度 乐观锁 悲观锁
核心思想 假设无冲突,提交时检查 假设有冲突,提前加锁
实现方式 版本号 / 时间戳 + CAS SELECT … FOR UPDATE
锁机制 无实际数据库锁,靠业务逻辑 数据库行锁 / 表锁
并发性能 高,适合读多写少 低,存在锁等待和死锁风险
冲突处理 应用层重试或回滚 事务排队等待
数据库依赖 低(任何数据库都支持) 依赖 InnoDB 等支持行锁的引擎
适用场景 读多写少、冲突概率低 写多读少、强一致性要求

4. 注意事项与常见误区

4.1 悲观锁的注意事项

  1. 一定要带索引FOR UPDATE 如果没走索引,可能会锁表,严重降低并发。
  2. 避免长事务:持有锁时间越长,阻塞越严重,建议尽快提交。
  3. 警惕死锁:多个事务以不同顺序获取锁时可能发生死锁,数据库会自动检测并回滚其中一个事务。

4.2 乐观锁的注意事项

  1. ABA 问题:版本号机制天然解决了 ABA 问题(因为版本号严格递增)。
  2. 重试次数:高冲突场景下大量重试会降低性能,应设置合理的重试上限。
  3. 批量更新:乐观锁不适合批量操作,因为无法保证原子性。

5. 如何选择?一张决策图帮你做决定

开始设计并发控制

写操作占比高?

强一致性要求?

悲观锁

乐观锁

冲突概率高?

使用 SELECT ... FOR UPDATE

使用 Version 版本号机制

简单总结:

  • 金融、库存扣减 → 悲观锁(或使用 select ... for update 后再更新)
  • 点赞、计数、配置表 → 乐观锁

6. 混合使用:乐观锁 + 悲观锁

在实际复杂的业务场景中,也可以两种锁混合使用。例如:

  • 外层使用乐观锁进行快速检查
  • 在真正更新时使用悲观锁确保一致性

但这种方案会增加代码复杂度,一般不建议初学者尝试。

结语

乐观锁和悲观锁没有绝对的优劣,核心在于根据业务场景选择最合适的方案。对于互联网项目,通常优先考虑乐观锁以提升吞吐量;对于金融类项目,悲观锁能带来更强的数据安全感。

希望通过本文的流程图解和代码示例,你能彻底掌握这两种 MySQL 并发控制利器。如果你在实际项目中遇到过锁相关的坑,欢迎评论区交流讨论!

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐