悲观锁与乐观锁的入门指南,旨在帮助大家清晰地理解悲观锁与乐观锁的基本概念,掌握两种锁机制的核心原理、实现方式与实际应用,解决并发场景下的数据一致性问题
锁是保障系统安全与数据一致性的核心机制,在生活中用于保护私有财产,在计算机程序与数据库中,则用于防范多线程、多用户并发环境下的数据竞争冲突。通过限制并发线程 / 事务对共享资源的同时访问,锁能够让操作有序执行,最终维护数据的完整性与安全性。
在数据库并发控制领域,悲观锁与乐观锁是两种最常用的并发控制策略,二者通过不同的逻辑处理并发访问问题,保障数据的一致性与可靠性。
锁的核心作用:在并发场景下控制多个操作的顺序执行,以此保证数据安全地修改。

悲观是一种偏向防御的思维模式,对应到锁机制中,悲观锁默认认定被保护的数据时刻存在被篡改的风险,安全性极低。当一个事务获取悲观锁后,其他任何事务均无法对该数据执行修改操作,只能等待锁释放后才能继续执行。
悲观锁是一种独占式锁,依赖数据库或编程语言层面的原生锁机制实现,属于被动防御的并发控制方案。
注意:数据库中的行锁、表锁、读锁、写锁,以及 Java 中的 synchronized、ReentrantLock 实现的锁,均属于悲观锁范畴。

在 MySQL 数据库中,不同存储引擎对锁的支持存在差异:MyISAM 仅支持表锁,而业务中最常用的 InnoDB 引擎,默认支持行锁与表锁,且行锁是基于索引实现的。
行锁:仅锁定操作的单行数据,锁定粒度小,并发性能高,加锁时需命中索引,否则会升级为表锁。
表锁:锁定整张数据表,锁定粒度大,并发性能差,会阻塞整张表的写操作。
简单来说,对数据加锁时若命中索引,InnoDB 会使用行锁;若未命中索引,则会触发表锁。

与悲观锁相反,乐观锁是一种偏向积极的并发控制策略,乐观锁默认认为数据的并发修改冲突概率极低,不会频繁发生变动。因此它允许多个事务同时读取并修改数据,不会提前对数据加锁。
乐观锁并非数据库原生提供的锁机制,而是通过业务逻辑人为控制的无锁并发控制方案,核心通过版本号机制或时间戳机制实现,其中版本号是最常用的方式。
其核心逻辑为:事务读取数据时同步获取版本标识,修改数据后提交时,对比数据库中当前版本标识,一致则更新数据并升级版本;不一致则说明数据已被其他事务修改,本次更新失效。
乐观锁的实现通常需要在数据表中新增额外字段,分为版本号与时间戳两种方式:
版本号字段:数据表新增
version字段,初始值默认为 1,数据每被修改一次,版本号自增 1。时间戳字段:数据表新增
update_time字段,记录数据最后修改时间,提交时对比时间戳判断是否冲突。
具体执行流程:
数据读取:事务读取数据时,同步获取该数据的 version 值(记为 v1)。
数据修改:事务完成数据修改,准备写回数据库时,再次查询数据库中该数据的 version 值(记为 v2)。
版本对比:
若 v1 = v2:说明读取到修改期间无其他事务操作数据,可正常更新数据,并将 version 赋值为 v1 + 1。
若 v1 ≠ v2:说明数据已被其他事务修改,本次更新不执行,需提示用户重新操作或执行重试。

在并发场景中,最典型的问题就是商品超卖,我们以购物平台猪肉脯库存并发购买为例,先模拟无锁场景的问题,再分别通过悲观锁、乐观锁解决该问题。
场景:用户 A 与用户 B 同时在购物平台购买同一款猪肉脯,商品库存仅剩余 1 件,无锁情况下并发下单会出现超卖问题。
商品表 goods 结构与初始数据:
数据表创建 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 两个会话模拟双事务并发执行:
事务 A 开启事务,对 id = 1 的猪肉脯数据加悲观锁,准备更新库存
MySQL 默认自动提交事务,需通过
begin手动开启事务,否则锁会立即释放。


事务 B 尝试对 id = 1 的数据加悲观锁,执行更新操作
此时事务 B 会进入阻塞状态,等待事务 A 释放锁;若锁等待超时,事务 B 会直接报错。


事务 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 会话模拟并发操作:
事务 A 与事务 B 同时查询数据,获取相同的库存与版本号
select num, version from goods where id = 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;
事务 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 问题(可通过版本号规避)。
悲观锁场景
适用于写操作频繁、数据一致性要求极高的场景,即便牺牲部分并发性能,也要保证数据绝对安全。 典型场景:金融交易结算、电商订单库存强一致扣减、银行转账、敏感数据修改等。
乐观锁场景
适用于读多写少、并发冲突概率低的场景,追求高并发性能,允许少量冲突后重试。 典型场景:商品信息查询、用户资料更新、论坛帖子编辑、普通电商商品库存(读远多于写)等。
悲观锁补充:MySQL 中除了 FOR UPDATE 排他锁,还有 LOCK IN SHARE MODE 共享锁,共享锁允许其他事务读,但禁止写操作。
乐观锁核心原理:底层基于 CAS(Compare And Swap)比较并交换算法实现,Java 中的 Atomic 原子类就是乐观锁的典型应用。
死锁问题:悲观锁若多个事务相互等待锁资源,会引发死锁,需通过合理索引、控制事务粒度避免。
重试机制:乐观锁冲突后,可通过自旋重试、固定次数重试等策略,提升更新成功率。