李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 走心评论
  • 系列文章
  • Now
  • 每日心情
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

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

2025年12月26日 约 1,738 字6 分钟 48点热度 0人点赞 2条评论
本文最后更新于 2025年12月26日,距今已 224 天,其中的信息可能已经发生变化,请注意甄别。

一、乐观锁核心原理

乐观锁的核心是“假设不会发生并发冲突,只在提交更新时检查数据是否被修改过”,而非像悲观锁(如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

推荐阅读

  • Spring Boot 配置加载优先级总结
  • 如何通过命令查看Java应用内存中对象数量
  • 记一次Apollo配置中心+Spring配置自动刷新导致的内存泄露问题
  • jasypt-spring-boot 使用说明
  • 配置Jackson使用字段而不是getter/setter来序列化和反序列化
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA 数据库 锁
最后更新:2025年12月26日

岁月同一天 8 月 8 日

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

  • 4 年前 2022年8月8日
    减肥四个月~

    今年四月初开始每天跳绳减肥,到了今天刚好四个月了。从一开始的每天跳绳十分钟到半个小时,再从半个小时到一个小时,最后又稳定…

相关文章
  • SpringBoot集成Redis,从Redis中获取数据为null,但实际上Redis中是存在对应的数据的,是什么原因导致的呢?2021年1月19日
  • Java 为什么有这么多 “O”?2025年5月16日
  • 从零开始入门 K8s | Kubernetes 网络概念及策略控制2019年10月17日
  • Java中ArrayList为什么比LinkedList查询速度快?2020年5月16日
  • CPU飙高,系统性能问题如何排查?2020年10月9日

李锋镝

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

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

文章评论

  • 皮皮社长Lv 2友

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

    WindowsEdge 114.0.1823.58 中国-永州市
    2025年12月28日
    00 回复
    • 李锋镝管理

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

      WindowsChrome 143.0.0.0 中国-北京市
      2025年12月28日
      00 回复
  • 九年没联系的女友突然问我借钱,我开口道:四十万行么?不够了再说话。”前女友很是高兴地回复说:“够用,还是我男朋友有办法啊,你放心,我日后一定还给你,九年多没见了你现在干啥呢,这么有钱?”我呵呵一笑:“没事,不够用您尽管说话,俺现在改做抵押贷款了。”

    听点儿音乐吧 朋友~
    文章目录
    最新 热点 随机
    最新 热点 随机
    Kratos+ v1.1.14版本更新说明 Spring Boot 指定外部配置文件的方式 Spring Boot 配置加载优先级总结 Claude Fable 5(claude-fable-5)深度详解 如何通过命令查看Java应用内存中对象数量 关于服务的探活端口和业务端口不一致有什么问题
    给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了
    MybatisCodeHelperPro激活 解决kubectl exec -it xxxx-service-bfbd45bb9-ktvzj bash -n bit error: exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead See 'kubectl exec -h' for help and examples Apollo配置中心中的protalDB的作用是什么 k8s + docker + Jenkins使用Pipeline部署SpringBoot项目时Jenkins错误集锦 彻底搞懂mysql日志系统binlog,redolog,undolog Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
    最近评论
    Huo 发布于 8 小时前(08月07日) 挺有特色的主题,还是感觉 WP 的确是强大
    李锋镝 发布于 21 小时前(08月07日) 没理解你想说啥
    aboss 发布于 21 小时前(08月07日) 你的后台web-login?
    李锋镝 发布于 22 小时前(08月07日) 这个专门的插件实现的功能更好更全,还能对接支付之类的
    李锋镝 发布于 22 小时前(08月07日) 自定义登录地址是为了防止大部分机器人通过WP固定登录页面暴力破解用户账号密码
    标签聚合
    K8s 日常 Claude docker JVM WordPress Redis AI编程 多线程 SQL 架构 MySQL IDEA JAVA SpringBoot Spring AI 数据库 ElasticSearch 分布式
    友情链接
    • Blogs·CN
    • 懋和道人
    • 旧时繁华
    • Honesty
    • 拾趣博客导航
    • 搬砖日记
    • 知向前端
    • 临窗旋墨
    • 皮皮社
    • 瓦匠个人小站
    • 志文工作室
    • 韩小韩博客
    • 风渡言
    • 彬红茶日记
    • 老张博客
    • Mr.Sun的博客
    • 韩情脉脉
    • 哥斯拉
    • 林羽凡

    COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

    正在博友圈履约中

    域名年龄

    Theme Kratos+ By Dylan Li

    津ICP备2024022503号-3

    京公网安备11011502039375号