如何在 MySQL 和 Java 中安全地更新数据以避免并发覆盖

来源:开发教程作者:马来西亚程序员头衔:程序员
导读:本期聚焦于小伙伴创作的《如何在 MySQL 和 Java 中安全地更新数据以避免并发覆盖》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《如何在 MySQL 和 Java 中安全地更新数据以避免并发覆盖》有用,将其分享出去将是对创作者最好的鼓励。

在业务系统中,当多个用户或线程同时对同一条数据进行修改时,如果没有做并发控制,就可能出现后提交的更新覆盖先提交的更新的情况,也就是并发覆盖问题。比如库存扣减、账户余额修改这类场景,一旦出现并发覆盖,会造成严重的业务损失。要解决这个问题,需要结合MySQL的数据库特性和Java的代码逻辑共同实现。

如何在 MySQL 和 Java 中安全地更新数据以避免并发覆盖

并发覆盖的产生原因

并发覆盖通常发生在两个及以上的更新操作同时读取同一条数据,然后基于读取到的旧值进行计算和更新,后提交的更新会直接覆盖先提交的更新的结果。举个简单的例子,假设某商品库存初始为10,线程A读取到库存10,准备扣减2,同时线程B也读取到库存10,准备扣减3,两个线程先后提交更新,最终库存可能是8或者7,而不是正确的5,这就是典型的并发覆盖问题。

MySQL层面的并发控制方案

悲观锁方案

悲观锁的核心思想是假设并发冲突一定会发生,所以在操作数据之前先加锁,阻止其他事务同时操作同一条数据。在MySQL中,可以使用SELECT ... FOR UPDATE语句对读取的数据加行级排他锁,只有当前事务提交后,锁才会释放,其他事务需要等待锁释放才能操作这条数据。

使用悲观锁需要注意,查询条件必须命中索引,否则会从行锁升级为表锁,影响并发性能。以下是使用悲观锁的更新示例:

-- 开启事务
START TRANSACTION;
-- 查询并加行锁,假设id是主键索引
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
-- 基于查询到的库存计算结果后更新
UPDATE product SET stock = 8 WHERE id = 1;
-- 提交事务,释放锁
COMMIT;

乐观锁方案

乐观锁的核心思想是假设并发冲突不会经常发生,所以不在读取数据时加锁,而是在更新数据时判断数据是否被其他事务修改过。常见的实现方式是给表增加一个版本号字段或者更新时间字段,更新时带上版本号条件,只有版本号匹配时才执行更新,同时版本号加1。

以下是使用版本号实现乐观锁的更新示例:

-- 更新时判断版本号是否匹配,匹配则更新并递增版本号
UPDATE product 
SET stock = 8, version = version + 1 
WHERE id = 1 AND version = 1;

如果执行后返回的影响行数是0,说明版本号不匹配,数据已经被其他事务修改过,当前更新失败,需要重试或者提示用户。

Java代码中配合实现安全更新

悲观锁对应的Java实现

在Java中,我们可以使用Spring的声明式事务来管理事务,结合MyBatis执行加锁查询和更新操作。首先定义Mapper接口:

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Map;

@Mapper
public interface ProductMapper {
    // 加锁查询商品库存
    Map<String, Object> selectStockForUpdate(@Param("id") Integer id);
    // 更新商品库存
    int updateStock(@Param("id") Integer id, @Param("stock") Integer stock);
}

对应的MyBatis 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="selectStockForUpdate" resultType="java.util.Map">
        SELECT stock FROM product WHERE id = #{id} FOR UPDATE
    </select>
    <update id="updateStock">
        UPDATE product SET stock = #{stock} WHERE id = #{id}
    </update>
</mapper>

然后编写Service层逻辑,在事务中执行操作:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;

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

    @Transactional(rollbackFor = Exception.class)
    public void deductStockWithPessimisticLock(Integer productId, Integer deductCount) {
        // 加锁查询库存
        Map<String, Object> productMap = productMapper.selectStockForUpdate(productId);
        Integer currentStock = (Integer) productMap.get("stock");
        // 判断库存是否充足
        if (currentStock < deductCount) {
            throw new RuntimeException("库存不足");
        }
        // 计算新库存并更新
        Integer newStock = currentStock - deductCount;
        productMapper.updateStock(productId, newStock);
    }
}

乐观锁对应的Java实现

乐观锁的Java实现需要处理更新失败的重试逻辑,同样先定义Mapper接口:

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface ProductOptimisticMapper {
    // 根据id和版本号更新库存,返回影响行数
    int updateStockWithVersion(@Param("id") Integer id, 
                               @Param("deductCount") Integer deductCount, 
                               @Param("oldVersion") Integer oldVersion);
}

对应的MyBatis 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.ProductOptimisticMapper">
    <update id="updateStockWithVersion">
        UPDATE product 
        SET stock = stock - #{deductCount}, version = version + 1 
        WHERE id = #{id} AND version = #{oldVersion}
    </update>
</mapper>

Service层实现重试逻辑,这里设置最多重试3次:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductOptimisticService {
    @Autowired
    private ProductOptimisticMapper productOptimisticMapper;

    public void deductStockWithOptimisticLock(Integer productId, Integer deductCount) {
        int retryCount = 0;
        int maxRetry = 3;
        while (retryCount < maxRetry) {
            // 先查询当前版本号和库存,这里可以单独写查询方法,省略具体实现
            Integer currentVersion = 1; // 假设查询到的版本号是1
            Integer currentStock = 10; // 假设查询到的库存是10
            if (currentStock < deductCount) {
                throw new RuntimeException("库存不足");
            }
            // 执行乐观锁更新
            int affectedRows = productOptimisticMapper.updateStockWithVersion(productId, deductCount, currentVersion);
            if (affectedRows > 0) {
                // 更新成功,退出重试
                return;
            }
            // 更新失败,重试次数加1
            retryCount++;
            if (retryCount == maxRetry) {
                throw new RuntimeException("更新失败,请稍后重试");
            }
        }
    }
}

两种方案的适用场景

悲观锁适合并发冲突比较频繁的场景,比如秒杀、库存高频扣减场景,因为加锁可以保证数据操作的串行化,避免冲突,但会增加锁等待的开销,降低并发性能。

乐观锁适合并发冲突比较少的场景,比如用户修改个人信息、低频的数据更新操作,因为没有加锁,并发性能更好,但需要处理重试逻辑,更新失败时需要给用户合理的提示。

注意事项

  • 使用悲观锁时,事务要尽量小,避免长时间持有锁,影响其他事务的执行。
  • 乐观锁的重试次数不要设置太多,避免重试过多导致接口响应时间过长。
  • 无论是哪种方案,更新操作都要尽量基于数据库层面的计算,比如直接使用stock = stock - 1,而不是先查询出来在Java代码中计算再更新,减少并发窗口。
  • 如果更新涉及多张表,要保证所有操作在同一个事务中,避免部分更新成功导致的数据不一致。

MySQLJava并发控制数据更新事务修改时间:2026-06-24 04:09:33

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。