惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
云风的 BLOG
云风的 BLOG
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
J
Java Code Geeks
博客园 - 聂微东
B
Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
WordPress大学
WordPress大学
腾讯CDC
L
LangChain Blog
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Azure Blog
Microsoft Azure Blog
D
DataBreaches.Net
The GitHub Blog
The GitHub Blog
美团技术团队
博客园 - Franky
Google DeepMind News
Google DeepMind News
V
V2EX
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
The Cloudflare Blog

李锋镝的博客

LiteLLM 本地代理搭建 Claude-HUD 使用文档 Kratos+ —— Kratos 主题二次开发记录 译文:如何将单体应用拆解为微服务 codebase-memory-mcp 极简完整使用指南 Claude Haiku 4.5、Claude Sonnet 4.6、Claude Opus 4.7 区别以及各自的新特性 SchedulingConfigurer详解 踩坑60+次后,我终于搞懂 Claude Skill 怎么写才会真的触发 Everything Claude Code 详细使用文档 配置Jackson使用字段而不是getter/setter来序列化和反序列化 这个域名注册整整十年了,十年时间,真快啊 Claude Code全维度实战指南:从入门到精通,解锁AI编程新范式 Apollo配置中心中的protalDB的作用是什么 org.apache.ibatis.plugin.Interceptor类详细介绍及使用 岁末 Excel2016右键新建工作表,打开时提示“因为文件格式或文件扩展名无效。请确定文件未损坏,并且文件扩展名与文件的格式匹配。”的解决办法 wordpress增加说说功能 Java 为什么有这么多 “O”? 别再背线程池的七大参数了,现在面试官都这么问 2024年11月1号 农历十月初一 我的第一个WordPress插件:Dylan Custom Plugin上线了 推荐一款比较养眼的Xshell配色方案 hnswlib installation failed 开工啦~ 阳了... MybatisCodeHelperPro激活 @Async注解的坑 新买的笔记本发货啦…… 这个中秋节感觉过的好累啊 IDEA下载源码报:Cannot connect to the Maven process. Try again later.
数据库更新如何实现乐观锁
李锋镝 · 2025-12-28 · via 李锋镝的博客

一、乐观锁核心原理

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