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

推荐订阅源

Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 聂微东
IT之家
IT之家
GbyAI
GbyAI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
博客园 - 【当耐特】
The Cloudflare Blog
宝玉的分享
宝玉的分享
罗磊的独立博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Visual Studio Blog
小众软件
小众软件
博客园_首页
Last Week in AI
Last Week in AI
J
Java Code Geeks
V
V2EX
雷峰网
雷峰网
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
腾讯CDC
博客园 - 司徒正美
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
DataBreaches.Net
博客园 - 三生石上(FineUI控件)
MyScale Blog
MyScale Blog
云风的 BLOG
云风的 BLOG
The Register - Security
The Register - Security
M
MIT News - Artificial intelligence
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
N
Netflix TechBlog - Medium
F
Full Disclosure
B
Blog
H
Help Net Security
C
Check Point Blog
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Jina AI
Jina AI
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
L
LangChain Blog
P
Proofpoint News Feed
D
Docker
Microsoft Security Blog
Microsoft Security 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. RocketMQ的push消费方式实现详解 看病难~取药难~~ 笑死、腹肌……根本不可能有腹肌的~~ 居家办公了~ C# 11 的这个新特性,我愿称之最强! IntelliJ IDEA 2020.3.x永久白嫖(Windows/Mac) IDEA无限试用方法【2020.3最新亲测有效】 SpringBoot使用注解的方式构建Elasticsearch查询语句,实现多条件的复杂查询 UUID太长怎么办?快来试试NanoId 忽然发现,在校大学生可以免费领一年有道云笔记会员~ 醒醒~补个税了 使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章 来来来,用python画一个冰墩墩儿 SpringBoot整合GraphQL入门教程 办理居住证困难重重啊! 妈呀,昨天晚上睡觉做了一晚上的梦,可累死我了 感觉Typecho很简洁啊…… WordPress的自动更新好烦啊 BeanCopier工具类(性能优化工具类) 内存屏障浅析 居住证可算是申请通过了…… 试了下壁挂炉供暖 关于重阳节 居住证签注... Spring Boot 2.x使用PostgreSQL数据库 十一节后开工头一天,修了个耳机…… 玉楼春·尊前拟把归期说 nginx反向代理配置去除前缀 1931→2021! Navicat Premium数据库账号密码解密 百度的索引量数据现在有点儿太夸张了吧? 哇塞~这个小姐姐实在太惊艳了…… 关于8月29号下午博客故障的一些记录 如何形象的描述反应式编程中的背压(Backpressure)机制? 基于Java8的Either类 海琴烟~~~ JMX监控权限认证配置 IntelliJ IDEA 2019.3.3 永久激活 破解[Windows] SpringBoot整合Elasticsearch详细步骤以及代码示例(附源码) SpringBoot整合Elasticsearch游标查询(scroll) GitLab创建新项目,初次提交命令和流程 吐槽下Google浏览器~ 从零搭建Spring Cloud Gateway网关(一) 从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志 博客有logo啦 你的答案 数据库事务的一点简单总结 Spring Boot发展史(Spring Boot介绍) 【漫画】戏说外行对程序员的误会有多深 【收藏】从面试官角度观察到的程序员技能瓶颈,同时给出突破瓶颈的建议 Git中.gitignore文件不起作用的解决办法 背影 - 朱自清 jmap命令(jdk1.8) 彻底搞懂mysql日志系统binlog,redolog,undolog 出院了~~~ linux中ftp查看不到文件列表的问题 PHP版本怎么更新啊…… 成语接龙 StarUML4.0破解文件 Java SPI详解 WordPress评论框增加自定义表情 因在公司上不正经网站,我没过试用期… 优化了MYSQL大量写入问题,老板奖励了1000块给我 不慌不忙的坚强(林徽因39段最美文字!) ConcurrentHashMap常用方法源码解析(jdk1.8) 我是如何失去团队掌控的? HBASE填坑日志 关闭apache httpclient4.5 DEBUG日志 使用OpenShift搭建k8s集群 SonarQube Scanner的配置与使用简介
数据库更新如何实现乐观锁
李锋镝 · 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