李锋镝的博客

  • 首页
  • 时间轴
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

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

2025年12月26日 165点热度 0人点赞 2条评论

一、乐观锁核心原理

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

相关文章

  • JDK25模块级导入深度解析:Java导入机制的革命性进化
  • 使用内存数据库进行MyBatis单元测试
  • try...catch性能深度剖析:从JVM原理到实战优化,打破技术迷思
  • Spring WebFlux深度解析:异步非阻塞架构与实战落地指南
  • Java进阶实战:10个高效技巧+环境管理指南,让代码简洁又优雅
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA 数据库 锁
最后更新:2025年12月26日

李锋镝

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

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • 皮皮社长青铜友

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

    Windows
    Edge 114.0.1823.58 中国-湖南
    2025年12月28日
    回复
    • 李锋镝管理

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

      Windows
      Chrome 143.0.0.0 中国
      2025年12月28日
      回复
  • 1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
    取消回复

    秋天是倒放的春天,晚安是爱你的序篇。

    那年今日(02月10日)

    • 1953年:穆罕默德·纳吉布出任埃及总统
    • 1923年:德国物理学家、X射线发现者伦琴逝世
    • 1898年:德国戏剧家贝尔托·布莱希特出生
    • 1894年:英国政治家哈罗德·麦克米伦出生
    • 589年:杨坚灭陈朝,南北朝结束
    • 更多历史事件
    最新 热点 随机
    最新 热点 随机
    Apollo配置中心中的protalDB的作用是什么 org.apache.ibatis.plugin.Interceptor类详细介绍及使用 JDK25模块级导入深度解析:Java导入机制的革命性进化 AI时代,个人技术博客的出路在哪里? 什么是Meta Server? 千万级大表新增字段实战指南:告别锁表与业务中断
    玩博客的人是不是越来越少了?AI时代,个人技术博客的出路在哪里?准备入手个亚太的ECS,友友们有什么建议吗?使用WireGuard在Ubuntu 24.04系统搭建VPNWordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了
    JWT、Cookie、Session、Token 区别与实战选型指南 Spring Boot 2.5.0重新设计的spring.sql.init 配置有啥用? 微服务的数据库设计 MySQL数据库详解——执行SQL更新时,其底层经历了哪些操作? AI重构开发者工作范式:从Anthropic内部调研看Claude对研发领域的深层影响 使用Spring MVC的websocket配置时 Tomcat启动报错
    标签聚合
    Spring K8s docker JAVA JVM 分布式 数据库 SpringBoot AI IDEA Redis 日常 AI编程 MySQL 多线程 SQL 设计模式 WordPress ElasticSearch 架构
    友情链接
    • Blogs·CN
    • Honesty
    • Mr.Sun的博客
    • 临窗旋墨
    • 哥斯拉
    • 彬红茶日记
    • 志文工作室
    • 懋和道人
    • 搬砖日记
    • 旧时繁华
    • 林羽凡
    • 瓦匠个人小站
    • 皮皮社
    • 知向前端
    • 蜗牛工作室
    • 韩小韩博客
    • 风渡言

    COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

    域名年龄

    Theme Kratos Made By Dylan

    津ICP备2024022503号-3

    京公网安备11011502039375号