社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

面试官:MySQL 乐观锁与悲观锁怎么实现?(修订版)

小哈学Java • 4 月前 • 163 次点击  
在线 Java 面试刷题(已更新239题)https://www.quanxiaoha.com/java-interview

面试考察点

  1. 概念理解度:面试官不仅仅是想知道你听过这两种锁的名字,更是想知道你是否理解它们的核心思想差异—— "先加锁再访问" vs "先访问再加锁"。
  2. 实践应用能力:考察你是否在实际项目中使用过这两种锁,能否根据业务场景(读多写少 vs 写多读少)选择合适的锁策略。
  3. 问题诊断能力:是否了解乐观锁的 ABA 问题、悲观锁的死锁风险,以及各自的解决方案。

核心答案

对比维度
乐观锁
悲观锁
核心思想
假设不会冲突,更新时检查
假设会冲突,操作前先加锁
实现方式CAS
 + 版本号/时间戳
SELECT ... FOR UPDATE
加锁时机
提交更新时才检测
读取数据时就加锁
适用场景
读多写少、冲突较少
写多读少、冲突频繁
性能特点
并发性高,但冲突时重试成本高
并发性低,但数据一致性保障强
ABA 问题
存在,需额外处理
不存在

一句话总结:乐观锁适合 "冲突少、读多写少" 的场景,悲观锁适合 "冲突多、写频繁" 的场景。

深度解析

一、乐观锁实现方案

乐观锁在 MySQL 中主要通过 版本号机制 或 时间戳机制  实现,核心是 "比较再交换"(Compare And Swap)的思想。

1. 版本号机制

实现步骤

img
img

上图展示了乐观锁版本号机制的完整流程:

  • 第一步(查询):先从数据库读取数据,同时获取当前版本号 version
  • 第二步(计算):在业务层进行逻辑计算,准备新值(此时不加锁,其他事务可以同时读取)
  • 第三步(更新):执行更新语句,WHERE 条件中携带之前查到的版本号,同时将版本号 +1
  • 第四步(判断):检查 affected_rows(影响行数),如果大于 0 说明更新成功;如果等于 0 说明版本号已被其他事务修改,需要重试或报错

表结构设计

-- 商品表,version 字段作为乐观锁版本号
CREATETABLE product (
    idBIGINT PRIMARY KEY AUTO_INCREMENT,
    nameVARCHAR(100NOTNULL,
    stock INTNOTNULLDEFAULT0,
    versionINTNOTNULLDEFAULT0,  -- 版本号字段
    created_at DATETIME DEFAULTCURRENT_TIMESTAMP
);

Java 代码示例

@Service
publicclass ProductService {

    @Autowired
    private ProductMapper productMapper;

    /**
     * 扣减库存(乐观锁实现)
     * @return true-扣减成功,false-库存不足或版本冲突需重试
     */

    public boolean deductStockWithOptimisticLock(Long productId,  int quantity) {
        // 1. 查询商品信息(包含版本号)
        Product product = productMapper.selectById(productId);
        if (product.getStock() < quantity) {
            returnfalse;  // 库存不足
        }

        // 2. 执行更新,携带版本号条件
        // UPDATE product SET stock = stock - #{quantity}, version = version + 1
        // WHERE id = #{id} AND version = #{version}
        int affectedRows = productMapper.deductStockOptimistic(
            productId,
            quantity,
            product.getVersion()  // 携带查询时的版本号
        );

        // 3. 判断是否更新成功
        return affectedRows > 0;
    }

    /**
     * 带重试机制的乐观锁扣减
     */

    @Retryable(value = OptimisticLockException.classmaxAttempts 3)
    public boolean deductStockWithRetry(Long productId, int quantity) {
        boolean success = deductStockWithOptimisticLock(productId, quantity);
        if (!success) {
            thrownew OptimisticLockException("版本冲突,请重试");
        }
        returntrue;
    }
}

Mapper XML

<select id="selectById" resultType="com.example.Product">
    SELECT id, name, stock, version FROM product WHERE id = #{id}
select>

<update id="deductStockOptimistic">
    UPDATE product
    SET stock = stock - #{quantity},
        version = version + 1
    WHERE id = #{id} AND version = #{version}
update>

2. 时间戳机制

与版本号类似,用  update_time 字段替代 version 字段:

-- 更新时比较时间戳
UPDATE product
SET stock = stock - 1,
    update_time = NOW()
WHERE id = 1 AND update_time = '2024-01-01 10:30:00';

⚠️ 注意:时间戳方案存在精度问题(毫秒级并发可能冲突),生产环境推荐使用版本号

3. 条件更新(简化版乐观锁)

对于库存扣减这种"数值增减"场景,可以简化为:

-- 直接在 SQL 中判断库存是否充足
UPDATE product
SET stock = stock - 1
WHERE id = 1 AND stock > 0;

这种方式不需要额外的版本号字段,本质上是利用数据库的原子性保证一致性。

二、悲观锁实现方案

悲观锁在 MySQL 中通过  SELECT ... FOR UPDATE 实现,利用数据库的排他锁(X 锁)机制。

1. FOR UPDATE 语法

img
img

上图展示了悲观锁的执行流程:

  • 事务 A 首先执行 SELECT ... FOR UPDATE,数据库会对查询到的记录加排他锁(X 锁)
  • 事务 B 同时也想对同一行执行 FOR UPDATE,由于锁被事务 A 持有,事务 B 会阻塞等待
  • 事务 A 完成更新并  COMMIT 后,锁被释放
  • 事务 B 此时才能获取到锁,继续执行

Java 代码示例

@Service
publicclass ProductService {

    @Autowired
    private ProductMapper productMapper;

    /**
     * 扣减库存(悲观锁实现)
     * 注意:必须在事务中执行
     */

    @Transactional
    public boolean deductStockWithPessimisticLock(Long productId, int quantity) {
        // 1. 加锁查询(FOR UPDATE)
        // SELECT id, name, stock FROM product WHERE id = #{id} FOR UPDATE
        Product product = productMapper.selectByIdForUpdate(productId);

        // 此时其他事务如果想操作这条记录,必须等待当前事务提交

        if (product.getStock() < quantity) {
            returnfalse;  // 库存不足
        }

        // 2. 执行更新
        productMapper.updateStock(productId, quantity);

        // 3. 事务提交时自动释放锁
        returntrue;
    }
}

Mapper XML




    

<select id="selectByIdForUpdate" resultType="com.example.Product">
    SELECT id, name, stock FROM product WHERE id = #{id} FOR UPDATE
select>

<update id="updateStock">
    UPDATE product SET stock = stock - #{quantity} WHERE id = #{id}
update>

2. 锁的范围与索引

⚠️ 重要FOR UPDATE 的锁范围与索引密切相关:

场景
锁范围
风险
通过主键/唯一索引查询
只锁匹配的行
✅ 推荐
通过普通索引查询
锁索引匹配的所有行 + 间隙
⚠️ 可能扩大锁范围
无索引
查询
锁整张表
❌ 严重性能问题
-- ✅ 推荐:通过主键查询,只锁一行
SELECT * FROM product WHERE id = 1 FOR UPDATE;

-- ⚠️ 注意:无索引会锁表
SELECT * FROM product WHERE name = 'iPhone' FOR UPDATE;  -- name 无索引

3. 死锁预防

悲观锁可能导致死锁,需要遵循以下原则:

/**
 * 死锁预防原则:
 * 1. 按固定顺序加锁(如按 ID 升序)
 * 2. 避免长事务
 * 3. 设置合理的锁等待超时
 */

@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // 按ID升序加锁,避免循环等待
    Long first = Math.min(fromId, toId);
    Long second = Math.max(fromId, toId);

    Account acc1 = accountMapper.selectByIdForUpdate(first);
    Account acc2 = accountMapper.selectByIdForUpdate(second);

    // 执行转账逻辑...
}

三、乐观锁 vs 悲观锁对比

img
img

选择建议

业务场景
推荐锁类型
原因
博客点赞、评论数
乐观锁
读远多于写,冲突概率极低
商品库存扣减(低并发)
乐观锁
冲突较少,避免锁开销
秒杀抢购(高并发)
悲观锁
冲突频繁,乐观锁重试成本高
账户余额操作
悲观锁
资金安全第一,强一致性
订单状态流转
悲观锁
状态一致性要求高

四、常见问题与解决方案

1. 乐观锁的 ABA 问题

问题描述:线程 1 读取版本 A,线程 2 将 A→B→A,线程 1 更新时误以为没有变化。

解决方案:使用更细粒度的版本号(如雪花算法 ID)或额外状态字段。

// 使用时间戳 + 随机数组合作为版本,避免 ABA
String newVersion = System.currentTimeMillis() + "_" + UUID.randomUUID();

2. 悲观锁的死锁问题

检测死锁

-- 查看当前锁等待情况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 查看当前运行的事务
SELECT * FROM information_schema.INNODB_TRX;

解决方案

  • 设置锁等待超时: innodb_lock_wait_timeout = 50(默认 50 秒)
  • 按固定顺序加锁
  • 避免长事务
  • 使用小事务

面试高频追问

  1. 追问一:秒杀场景用乐观锁还是悲观锁?

    高并发秒杀建议用悲观锁分布式锁。乐观锁在冲突率极高时,大量请求需要重试,反而降低吞吐量。实际生产中常用 Redis 预扣库存 + 消息队列异步落库。

  2. 追问二:乐观锁更新失败后怎么处理?

    两种策略:

  • 重试机制:使用 Spring Retry 或自定义重试逻辑,设置最大重试次数
  • 直接失败:返回错误提示让用户重新操作(如"数据已被修改,请刷新后重试")
  • 追问三:FOR UPDATE 和 LOCK IN SHARE MODE 的区别?

    • FOR UPDATE:加排他锁(X 锁),其他事务不能读也不能写
    • LOCK IN SHARE MODE:加 共享锁(S 锁),其他事务可以读但不能写

    常见面试变体

    • "如何解决并发更新数据的一致性问题?"
    • "数据库行锁和表锁的区别?什么时候会锁表?"
    • "什么场景下乐观锁比悲观锁性能更好?"
    • "MySQL 的 MVCC 机制是什么?和乐观锁有什么关系?"

    记忆口诀

    乐观锁:先查后比再更新,版本不对就重试,适合读多写少场景

    悲观锁:先锁后改再提交,别人只能干等着,适合写多冲突场景

    总结

    乐观锁通过版本号机制在更新时检测冲突,适合读多写少、冲突率低的场景;悲观锁通过 FOR UPDATE 加排他锁强制串行化,适合写多冲突、一致性要求高的场景。生产环境需根据业务特点选择,高并发场景可结合 Redis 分布式锁优化。

    👉 欢迎加入小哈的星球,你将获得:  专属的项目实战(多个项目) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

    • 新项目:《Spring AI 项目实战》正在更新中..., 基于 Spring AI + Spring Boot 3.x + JDK 21;
    • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍演示地址:http://116.62.199.48:7070/
    • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接: http://116.62.199.48/;
    • 专栏阅读地址:https://www.quanxiaoha.com/column

    截止目前,累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有4500+小伙伴加入

    图片


    图片
    图片

    1. 我的私密学习小圈子,从0到1手撸企业实战项目~

    2. 京东二面:加索引时,会锁表吗?(修订版)

    3. Java中的5大AI框架!

    4.  同事一个比喻,让我搞懂了 Docker 和 k8s 的核心概念

    图片


    最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

    获取方式:点“在看”,关注公众号并回复  Java 领取,更多内容陆续奉上。

    PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

    “在看”支持小哈呀,谢谢啦

    Python社区是高质量的Python/Django开发社区
    本文地址:http://www.python88.com/topic/194596