源本科技 | 码上会

乐观锁与悲观锁

2026/04/10
1
0

引言

悲观锁与乐观锁的入门指南,旨在帮助大家清晰地理解悲观锁与乐观锁的基本概念,掌握两种锁机制的核心原理、实现方式与实际应用,解决并发场景下的数据一致性问题

锁的基础概念

锁(Lock)

锁是保障系统安全与数据一致性的核心机制,在生活中用于保护私有财产,在计算机程序与数据库中,则用于防范多线程、多用户并发环境下的数据竞争冲突。通过限制并发线程 / 事务对共享资源的同时访问,锁能够让操作有序执行,最终维护数据的完整性与安全性。

在数据库并发控制领域,悲观锁与乐观锁是两种最常用的并发控制策略,二者通过不同的逻辑处理并发访问问题,保障数据的一致性与可靠性。

锁的核心作用:在并发场景下控制多个操作的顺序执行,以此保证数据安全地修改。

悲观锁

悲观是一种偏向防御的思维模式,对应到锁机制中,悲观锁默认认定被保护的数据时刻存在被篡改的风险,安全性极低。当一个事务获取悲观锁后,其他任何事务均无法对该数据执行修改操作,只能等待锁释放后才能继续执行

悲观锁是一种独占式锁,依赖数据库或编程语言层面的原生锁机制实现,属于被动防御的并发控制方案。

注意:数据库中的行锁、表锁、读锁、写锁,以及 Java 中的 synchronized、ReentrantLock 实现的锁,均属于悲观锁范畴。

表锁与行锁

在 MySQL 数据库中,不同存储引擎对锁的支持存在差异:MyISAM 仅支持表锁,而业务中最常用的 InnoDB 引擎,默认支持行锁与表锁,且行锁是基于索引实现的

  • 行锁:仅锁定操作的单行数据,锁定粒度小,并发性能高,加锁时需命中索引,否则会升级为表锁。

  • 表锁:锁定整张数据表,锁定粒度大,并发性能差,会阻塞整张表的写操作。

简单来说,对数据加锁时若命中索引,InnoDB 会使用行锁;若未命中索引,则会触发表锁。

乐观锁

与悲观锁相反,乐观锁是一种偏向积极的并发控制策略,乐观锁默认认为数据的并发修改冲突概率极低,不会频繁发生变动。因此它允许多个事务同时读取并修改数据,不会提前对数据加锁

乐观锁并非数据库原生提供的锁机制,而是通过业务逻辑人为控制的无锁并发控制方案,核心通过版本号机制时间戳机制实现,其中版本号是最常用的方式。

其核心逻辑为:事务读取数据时同步获取版本标识,修改数据后提交时,对比数据库中当前版本标识,一致则更新数据并升级版本;不一致则说明数据已被其他事务修改,本次更新失效。

乐观锁的实现通常需要在数据表中新增额外字段,分为版本号与时间戳两种方式:

  1. 版本号字段:数据表新增 version 字段,初始值默认为 1,数据每被修改一次,版本号自增 1。

  2. 时间戳字段:数据表新增 update_time 字段,记录数据最后修改时间,提交时对比时间戳判断是否冲突。

具体执行流程

  • 数据读取:事务读取数据时,同步获取该数据的 version 值(记为 v1)。

  • 数据修改:事务完成数据修改,准备写回数据库时,再次查询数据库中该数据的 version 值(记为 v2)。

  • 版本对比

    1. 若 v1 = v2:说明读取到修改期间无其他事务操作数据,可正常更新数据,并将 version 赋值为 v1 + 1。

    2. 若 v1 ≠ v2:说明数据已被其他事务修改,本次更新不执行,需提示用户重新操作或执行重试。

锁的实际实现

在并发场景中,最典型的问题就是商品超卖,我们以购物平台猪肉脯库存并发购买为例,先模拟无锁场景的问题,再分别通过悲观锁、乐观锁解决该问题。

业务场景

场景:用户 A 与用户 B 同时在购物平台购买同一款猪肉脯,商品库存仅剩余 1 件,无锁情况下并发下单会出现超卖问题。

商品表 goods 结构与初始数据:

id

name

num

1

猪肉脯

1

2

牛肉干

1

数据表创建 SQL:

CREATE TABLE `goods` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `num` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
) CHARSET=utf8mb4;

INSERT INTO `goods` VALUES (1, '猪肉脯', 1);
INSERT INTO `goods` VALUES (2, '牛肉干', 1);

悲观锁实现方案

悲观锁通过对目标数据加排他锁,保证同一时间仅一个事务能操作数据,从根源避免并发冲突。在 MySQL 中,通过 SELECT ... FOR UPDATE 语句实现排他型悲观锁。

select num from goods where id = 1 for update;

通过 MySQL 两个会话模拟双事务并发执行:

  1. 事务 A 开启事务,对 id = 1 的猪肉脯数据加悲观锁,准备更新库存

MySQL 默认自动提交事务,需通过 begin 手动开启事务,否则锁会立即释放。

  1. 事务 B 尝试对 id = 1 的数据加悲观锁,执行更新操作

此时事务 B 会进入阻塞状态,等待事务 A 释放锁;若锁等待超时,事务 B 会直接报错。

  1. 事务 A 执行库存扣减,提交事务释放锁

-- 第一步:开启事务并加锁
begin;
select num from goods where id = 1 for update;

-- 第二步:扣减库存并查询
update goods set num = num - 1 where id = 1;
select num from goods where id = 1;
commit;

最终事务 A 成功扣减库存,库存变为 0;事务 A 提交释放锁后,事务 B 获取锁查询到库存为 0,放弃购买,彻底解决超卖问题。

乐观锁实现方案

使用乐观锁需先在 goods 表中新增 version 版本号字段,用于追踪数据修改状态。

CREATE TABLE `goods` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `num` bigint DEFAULT NULL,
  `version` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) CHARSET=utf8mb4;

操作前需先提交事务 B,否则表结构修改语句会阻塞执行

begin;
select num from goods where id = 1 for update;
commit;

依旧通过两个 MySQL 会话模拟并发操作:

  1. 事务 A 与事务 B 同时查询数据,获取相同的库存与版本号

select num, version from goods where id = 1;
  1. 事务 A 执行购买操作,更新库存与版本号

-- 第一步:查询数据
select num, version from goods where id = 1;

-- 第二步:扣减库存并升级版本号
update goods set num = num - 1, version = version + 1 where id = 1;

-- 第三步:查询更新后数据
select num, version from goods where id = 1;
  1. 事务 B 执行购买更新,基于版本号判断冲突

-- 第一步:查询数据
select num, version from goods where id = 1;

-- 第二步:带版本号条件更新数据
update goods set num = num - 1, version = version + 1 where id = 1 and version = 0;

-- 第三步:查询最终数据
select num, version from goods where id = 1;

最终事务 B 的更新语句影响行数为 0,数据未发生修改,需提示用户重新操作,避免超卖。

优缺点

悲观锁

  • 优点:强一致性保障,依赖数据库原生锁机制,严格控制并发操作顺序,能百分百避免数据冲突,适合对数据一致性要求极高的场景。

  • 缺点:并发性能差,锁阻塞会导致其他事务长时间等待;锁粒度控制不当易引发死锁;高并发场景下会大幅降低系统吞吐量。

乐观锁

  • 优点:无数据库锁阻塞,并发性能优异,减少线程等待时间,大幅提升系统吞吐量;实现灵活,不依赖数据库底层机制。

  • 缺点:存在冲突重试成本,高并发写场景下冲突率极高,需要手动实现重试逻辑;无法解决外部系统非法修改数据的问题;存在 ABA 问题(可通过版本号规避)。

应用场景

悲观锁场景

适用于写操作频繁、数据一致性要求极高的场景,即便牺牲部分并发性能,也要保证数据绝对安全。 典型场景:金融交易结算、电商订单库存强一致扣减、银行转账、敏感数据修改等。

乐观锁场景

适用于读多写少、并发冲突概率低的场景,追求高并发性能,允许少量冲突后重试。 典型场景:商品信息查询、用户资料更新、论坛帖子编辑、普通电商商品库存(读远多于写)等。

其他补充

  1. 悲观锁补充:MySQL 中除了 FOR UPDATE 排他锁,还有 LOCK IN SHARE MODE 共享锁,共享锁允许其他事务读,但禁止写操作。

  2. 乐观锁核心原理:底层基于 CAS(Compare And Swap)比较并交换算法实现,Java 中的 Atomic 原子类就是乐观锁的典型应用。

  3. 死锁问题:悲观锁若多个事务相互等待锁资源,会引发死锁,需通过合理索引、控制事务粒度避免。

  4. 重试机制:乐观锁冲突后,可通过自旋重试、固定次数重试等策略,提升更新成功率。