当前位置: 首页 > java >正文

乐观锁与悲观锁

悲观锁(Pessimistic Lock)

✅ 核心思想:

始终假设最坏的情况:别人一定会修改数据,所以每次读写都会加锁,确保操作安全。

在读数据时就加锁,防止其他事务修改这条数据,确保当前事务后续的操作(尤其是写)是安全的。

悲观锁就是 “读时加锁,确保写时没有并发冲突”,必须配合事务使用,先查锁定,后更新提交,这是它的核心机制。

🛠 实现方式:

  • 数据库层面的锁(如行锁、表锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;

该语句会对 id=1 的行加行锁(InnoDB),其他事务只能等你释放。

✅ 解释 SELECT … FOR UPDATE是什么?

这是一个 悲观锁 的实现方式。它会:

  • 给返回的记录加上行级排他锁(exclusive lock)
  • 直到当前事务提交/回滚之前,其他事务不能对这行数据做 UPDATE 或 DELETE

📌 使用场景:

通常用于先读后改的业务逻辑,例如银行扣款:

本质是用于“查询 + 锁定

BEGIN; -- 开始事务(BEGIN);-- 查出余额并加锁
SELECT balance FROM account WHERE id = 123 FOR UPDATE;-- 应用层判断余额是否足够,然后扣款
UPDATE account SET balance = balance - 100 WHERE id = 123;COMMIT;
应用场景原因 / 说明
库存扣减(电商系统)多用户同时抢购同一个商品,必须保证库存一致性,防止“超卖”现象。
积分/余额变更如用户领取奖励、消费积分、修改余额等,必须防止并发重复扣减或增加。
用户账户状态变更某些状态是强一致的,例如实名认证状态不能同时被两个操作修改。
分布式事务关键数据保护如主业务成功后,写入其他关键表数据(如日志、状态表),必须确保某些步骤不被并发干扰。
任务领取 / 队列消费多个服务实例抢占任务,使用悲观锁防止任务被重复领取(抢单系统中也常见)。
订单状态流转比如订单状态从“待支付”到“已支付”,不能被重复更新或被非法修改。
排队/名额抢占操作比如课程报名、预约挂号、秒杀等,需要严格控制名额。

📌 特点:

  • 并发低、冲突高的系统下比较安全
  • 容易引发锁等待、死锁等问题
  • 对数据库压力大

悲观锁特别适合:

  • 数据写冲突概率高的场景;
  • 强一致性要求高,如金融、电商、任务调度等;
  • 可以接受一定程度的性能牺牲来保证数据正确性。

乐观锁

✅ 核心思想:

默认别人不会修改,只有在提交时检查是否有冲突(比如版本号是否变了),如果有冲突,就放弃本次操作重试

🛠 实现方式:

使用版本号或时间戳字段来检测冲突。

示例:

id | name  | version
1  | Tom   | 3
UPDATE user SET name = 'Jerry', version = version + 1
WHERE id = 1 AND version = 3;

表结构:

  1. 如果返回 0 行,说明版本冲突;你可以重试或提示失败。

📌 特点:

  • 无需加锁,适合并发量大、冲突少的场景
  • 实现复杂度略高,需业务控制
  • 很适合读多写少的业务(如订单系统、配置系统)

流程

前端/调用方应当在读取数据后,连同 version 一并提交回来,否则服务器无法判断是否有并发修改行为。

查询数据

{"id": 1,"name": "Tom","version": 3
}
  1. 前端修改 name,连同 version 提交
{"id": 1,"name": "Jerry","version": 3
}

后端执行 SQL 判断 version 是否一致

UPDATE user
SET name = 'Jerry', version = version + 1
WHERE id = 1 AND version = 3;
  1. 建议后端接口文档明确要求前端带上 version 字段
  2. 若前端实在无法改,可以:
    • 后端先查当前版本(增加一次 DB IO,失去乐观锁“无锁”的优势);
    • 或只能用悲观锁代替(使用 SELECT … FOR UPDATE 加锁)。

版本号字段

目前最主流的乐观锁实现,版本号字段通常就是 int 类型

每次更新时 version + 1,判断是否冲突即可。

❓ 为什么不用 UUID 或 时间戳呢?

类型优点缺点是否推荐
int(版本号)简单高效,支持自增,SQL 快易被猜测、需要前端传回✅ 常用方案
timestamp(更新时间)可读性强,也可用于判断冲突精度可能不足,容易误判✅ 也常见(如 updated_at)
uuid(版本标识)安全、不可猜更新逻辑复杂,不能自增❌ 一般不建议用来做版本号

可以将 version 包装为不可修改字段,仅供提交用,或传输时改名,比如:

{"id": 123,"new_value": "XXX","_v": 3   // version
}

应用场景

非常适合以下高并发、读多写少、允许一定冲突重试的场景

应用场景说明
配置管理系统大量人读取配置,偶尔修改,修改时需避免冲突(比如灰度配置中心)。
商品库存预读写多人查看商品详情,实际提交订单时使用乐观锁避免库存扣减冲突(低频冲突)。
用户信息修改比如修改昵称/头像等用户信息,避免并发提交导致数据覆盖。
内容编辑(博客、文章、文案)避免两个编辑者同时保存时,互相覆盖编辑内容。
订单更新操作(非支付流程)如用户备注、订单地址修改,避免覆盖修改内容。
版本控制系统 / 文档协作平台如 Google Docs、Notion 等,在保存文档前先比对版本。
后台运营系统操作控制多人后台同时维护资源时避免状态冲突,如运营审核、上下架。
非关键业务的数据采集 / 日志写入控制冲突可以接受失败或重试,适合乐观锁策略。
http://www.xdnf.cn/news/8827.html

相关文章:

  • Ansible配置文件常用选项详解
  • [c语言实战]C语言多线程编程:从零开发高并发任务调度器(五)
  • 浅谈ggplot2图表美化~
  • 8:OpenCV—仿射变换和坐标映射
  • 每日Prompt:龙虎斗
  • LangChain4j 项目实战——idea快捷键搜索
  • 力扣第157场双周赛
  • NISP和CISP有什么区别,哪个更好
  • 内容中台的核心价值是什么?
  • 决策引擎与规则引擎在交易所业务风控中的建设思路、架构设
  • 【开源项目】成本50元内的开源项目
  • 只能上百度b站打不开其他网页
  • 关于 java: 2. 面向对象编程(OOP)核心概念
  • lc hot 100之:回文链表
  • 探索容器技术:Docker与Kubernetes的实践指南
  • TiDB:从快速上手到核心原理与最佳实践
  • FreeRTOS--信号量
  • JavaEE 网络编程套接字详解与实战示例
  • QNAP NEXTCLOUD 域名访问
  • GO语言基础4 Errors 报错
  • Redis之金字塔模型分层架构
  • go实现钉钉三方登录
  • 开源 OIDC(OpenID Connect)身份提供方(IdP)、iam选型
  • 历年安徽大学保研上机真题
  • AWS EC2 使用Splunk DB connect 连接 RDS mysql
  • ​​C++ 中 protected/public/private 访问控制修饰符的区别​
  • 白皮精读:全国统一数据资产登记体系建设白皮书【附全文阅读】
  • 使用Vue3制作一款个性化上传组件
  • 刷leetcode hot100返航版--栈和队列5/24
  • java多态的学习笔记