李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 每日心情
  • Now
  • 系列文章
  • 论坛
  • 左邻右舍
    • 左邻右舍
    • 博友圈
  • 留言
    • 留言
    • 走心评论
  • 关于
    • 关于本站
    • 网站地图
    • 网站统计
    • 另一个网站
    • 我的导航站
    • 赞助
  • 🚇开往
!Destiny
惟坚韧者始能遂其志
  1. 首页
  2. 后端
  3. 正文

数据库更新如何实现乐观锁

2025年12月26日 约 1,738 字6 分钟 199 0 2
本文最后更新于 2025年12月26日,距今已 269 天,其中的信息可能已经发生变化,请注意甄别。

一、乐观锁核心原理

乐观锁的核心是“假设不会发生并发冲突,只在提交更新时检查数据是否被修改过”,而非像悲观锁(如SELECT ... FOR UPDATE)那样提前锁定数据。

  • 核心逻辑:更新数据时,先验证数据的“版本/时间戳”是否和自己读取时一致——一致则更新,不一致则说明数据已被其他线程修改,放弃更新(或重试)。
  • 适用场景:并发冲突概率低、读多写少的业务(比如商品库存查询、用户信息修改),避免悲观锁带来的性能损耗。

二、主流实现方案(2种核心方式)

1. 版本号法(最常用)

  • 原理:在数据表中新增一个version字段(整数,初始值0),每次更新数据时:
    1. 读取数据时,同时获取version值;
    2. 更新时,将version作为条件(WHERE version = 读取值),并把version自增1;
    3. 若更新返回行数为0,说明版本不匹配(数据已被修改),触发冲突处理。

2. 时间戳法

  • 原理:类似版本号,新增update_time字段(时间戳/DateTime),更新时校验“当前读取的时间戳”和“数据库中的时间戳”是否一致。
  • 缺点:时间戳精度问题(如毫秒级)可能导致并发判断失效,不如版本号稳定,实际使用较少。

三、实操示例(MySQL + Java + MyBatis)

以“商品库存扣减”为例(典型的并发场景),完整实现乐观锁:

1. 建表语句(添加version字段)

CREATE TABLE product (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
    name VARCHAR(255) NOT NULL COMMENT '商品名称',
    stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
    version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'
);

-- 插入测试数据
INSERT INTO product (name, stock, version) VALUES ('手机', 100, 0);

2. 实体类(对应数据表)

public class Product {
    private Long id;
    private String name;
    private Integer stock;
    private Integer version; // 乐观锁版本号

    // 省略getter/setter/toString
}

3. Mapper接口(MyBatis)

public interface ProductMapper {
    /**
     * 根据ID查询商品(获取version)
     */
    Product selectById(Long id);

    /**
     * 扣减库存(乐观锁核心:WHERE version = #{version})
     * @param product 商品对象(含id、扣减后的库存、读取时的version)
     * @return 影响行数:1=更新成功,0=版本冲突
     */
    int deductStock(Product product);
}

4. Mapper XML(核心更新逻辑)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ProductMapper">
    <!-- 查询商品 -->
    <select id="selectById" resultType="com.example.entity.Product">
        SELECT id, name, stock, version FROM product WHERE id = #{id}
    </select>

    <!-- 扣减库存(乐观锁) -->
    <update id="deductStock">
        UPDATE product
        SET stock = #{stock}, version = version + 1
        WHERE id = #{id} AND version = #{version}
    </update>
</mapper>

5. 业务层(处理冲突+重试机制)

乐观锁冲突后,通常有两种处理方式:① 返回失败(告知用户“操作失败,请重试”);② 自动重试(有限次数)。以下是带重试的实现:

@Service
public class ProductService {
    @Autowired
    private ProductMapper productMapper;

    // 最大重试次数
    private static final int MAX_RETRY = 3;

    /**
     * 扣减商品库存(带乐观锁重试)
     * @param productId 商品ID
     * @param num 扣减数量
     * @return true=成功,false=失败
     */
    public boolean deductStock(Long productId, int num) {
        int retryCount = 0;
        while (retryCount < MAX_RETRY) {
            // 1. 查询商品(获取当前version和库存)
            Product product = productMapper.selectById(productId);
            if (product == null) {
                throw new RuntimeException("商品不存在");
            }
            if (product.getStock() < num) {
                return false; // 库存不足,直接失败
            }

            // 2. 准备更新参数(扣减库存,携带读取的version)
            Product updateParam = new Product();
            updateParam.setId(productId);
            updateParam.setStock(product.getStock() - num);
            updateParam.setVersion(product.getVersion());

            // 3. 执行更新(乐观锁校验)
            int affectedRows = productMapper.deductStock(updateParam);
            if (affectedRows == 1) {
                return true; // 更新成功
            }

            // 4. 版本冲突,重试(休眠50ms避免高频重试)
            retryCount++;
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        // 重试次数耗尽,返回失败
        return false;
    }
}

6. 测试并发场景

用多线程测试乐观锁效果:

@Test
public void testConcurrentDeduct() throws InterruptedException {
    Long productId = 1L;
    int threadCount = 5; // 5个线程同时扣减
    CountDownLatch latch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
        new Thread(() -> {
            try {
                boolean success = productService.deductStock(productId, 1);
                System.out.println(Thread.currentThread().getName() + ":" + (success ? "扣减成功" : "扣减失败"));
            } finally {
                latch.countDown();
            }
        }).start();
    }

    latch.await();
    // 最终库存应为 100 - 5 = 95(无超卖)
    Product product = productMapper.selectById(productId);
    System.out.println("最终库存:" + product.getStock());
}

四、进阶:MyBatis-Plus自动实现乐观锁

如果使用MyBatis-Plus,可以通过注解简化乐观锁实现,无需手动写UPDATE语句:

1. 实体类添加注解

public class Product {
    private Long id;
    private String name;
    private Integer stock;
    @Version // MyBatis-Plus乐观锁注解
    private Integer version;
}

2. 配置乐观锁插件

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

3. 业务层简化

// MyBatis-Plus会自动拼接version条件,无需手动写SQL
public boolean deductStock(Long productId, int num) {
    int retryCount = 0;
    while (retryCount < MAX_RETRY) {
        Product product = productMapper.selectById(productId);
        if (product.getStock() < num) return false;

        product.setStock(product.getStock() - num);
        // 更新时,MyBatis-Plus自动校验version并自增
        int affectedRows = productMapper.updateById(product);
        if (affectedRows == 1) return true;

        retryCount++;
        Thread.sleep(50);
    }
    return false;
}

五、避坑要点

  1. 乐观锁不解决“脏读”:乐观锁仅保证“更新时的版本一致性”,若业务需要读取最新数据,需结合事务隔离级别(如READ COMMITTED)。
  2. 重试次数要限制:避免无限重试导致死循环,通常设置3-5次即可。
  3. 不要在批量更新中使用:乐观锁适合单条数据更新,批量更新(如UPDATE ... WHERE 条件)无法精准校验版本,容易失效。
  4. 版本号必须自增:不能手动修改version,否则会破坏校验逻辑;MyBatis-Plus会自动处理,手动写SQL需确保version = version + 1。
  5. 高并发场景需兜底:若并发冲突概率极高(如秒杀),乐观锁重试可能频繁失败,建议结合分布式锁(如Redisson)使用。

总结

  1. 乐观锁核心是版本校验,通过version字段在更新时判断数据是否被修改,避免提前加锁;
  2. 实操关键是“查询时获取版本 + 更新时校验版本 + 冲突后重试/返回失败”;
  3. MyBatis-Plus可通过@Version注解简化实现,无需手动编写版本校验SQL,提升开发效率。

乐观锁的核心价值是“无锁化提升并发性能”,但需结合业务场景选择——低冲突用乐观锁,高冲突用悲观锁/分布式锁。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/hou-duan/4668

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
分享到

数据库更新如何实现乐观锁

也可使用浏览器菜单中的「分享」功能

微信扫一扫分享

标签: JAVA 数据库 锁
最后更新:2025年12月26日

岁月同一天 9 月 22 日

回望过去的今天,你在写什么

  • 3 年前 2023年9月22日
    《人生海海》读后感

    前段时间读完了麦家的《人生海海》,起初对人生海海这个词不太理解,记得第一次听到这个词,应该是在《欢喜就好》这首歌里,里面…

  • 4 年前 2022年9月22日
    OHCache使用

    OHCache介绍 缓存框架OHC基于Java语言实现,并以类库的形式供其他Java程序调用,是一种以单机模式运行的堆外…

相关文章
  • 结合Apollo配置中心实现日志级别动态配置2021年12月29日
  • 从零搭建Spring Cloud Gateway网关(三)——报文结构转换2020年7月13日
  • 跨平台版本管理神器,开发者的环境配置救星:vfox2025年10月27日
  • MySQL语法总结2019年6月20日
  • CPU飙高,系统性能问题如何排查?2020年10月9日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

  • 皮皮社长Lv 2友

    2025年底来打打卡,学习学习,学不会也看看~ :42:

    WindowsWindowsEdge 114.0.1823.58Edge 114.0.1823.58中国中国-湖南-永州
    2025年12月28日
    00 回复
    • 李锋镝博主

      @皮皮社长 欢迎打卡 :17:

      WindowsWindowsChrome 143.0.0.0Chrome 143.0.0.0中国中国-北京
      2025年12月28日
      00 回复
  • 了却君王天下事,赢得生前身后名。

    听点儿音乐吧 朋友~
    文章目录
    最新 热点 随机
    最新 热点 随机
    让WordPress静态化之Rocket‑Nginx WordPress下一代默认主题Ipsum预览 C++之父重磅发声:AI编程正在毁掉一代程序员 支撑全网40%网站的WordPress正在重新拥抱PHP生态 Kratos-plus v1.1.24版本更新说明 关于主题加载速度优化的一点儿小演进
    关于主题加载速度优化的一点儿小演进给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能WordPress缓存插件WP Fastest Cache、WP Rocket 、FlyingPress对比关于使用AI的一些思考Kratos+ v1.1.16版本更新说明AI时代,个人技术博客的出路在哪里?
    9.9元解锁AI编程新体验:Doubao-Seed-Code 深度实测 关于使用AI的一些思考 蓝奏云地址怎么天天失效…… 每日早报 · 2026年8月20日 · 早上好 忽然发现,在校大学生可以免费领一年有道云笔记会员~ 还不懂Redis?看完这个故事就明白了!
    最近评论
    李锋镝 发布于 2 天前(09月20日) 静态博客我之前也用过,但是感觉不是很方便,后来就一直用的WordPress
    Sheep5 发布于 2 天前(09月20日) 我直接用静态博客,天然有速度优势。
    不凡 发布于 2 天前(09月20日) 主要是wordpress插件丰富,需要什么功能插件,插件市场应有尽有,typecho是性能更好、更轻...
    李锋镝 发布于 2 天前(09月20日) 忒极简了,而且这个布局我也搞不懂
    李锋镝 发布于 2 天前(09月20日) 哈哈哈~自定义的功能都是符合自己需求的
    标签聚合
    SpringBoot 数据库 MQ AI编程 Claude JVM K8s 多线程 日常 JAVA 分布式 Spring AI SQL IDEA MySQL WordPress Redis 架构 ElasticSearch
    友情链接
    • lijie blog
    • 懋和道人
    • 瓦匠个人小站
    • Honesty
    • 志文工作室
    • 九仞之行
    • Serendipity
    • Mr.Sun的博客
    • 韩情脉脉
    • 若梦博客
    • 搬砖日记
    • 哥斯拉
    • 临窗旋墨
    • 知向前端
    • 林羽凡
    • sssr7844的博客
    • 皮皮社
    • 韩小韩博客
    • 蜗牛工作室
    • 老张博客

    COPYRIGHT © 2016-2026 lifengdi.com. ALL RIGHTS RESERVED.

    lifengdi.com

    Domain age badge for lifengdi.com

    Theme Kratos-plus By Dylan Li

    津ICP备2024022503号-3

    京公网安备11011502039375号