SQL并发更新丢数据怎么办 乐观锁实现方案解析

来源:个人站长网作者:马来西亚程序员头衔:程序员
导读:本期聚焦于小伙伴创作的《SQL并发更新丢数据怎么办 乐观锁实现方案解析》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《SQL并发更新丢数据怎么办 乐观锁实现方案解析》有用,将其分享出去将是对创作者最好的鼓励。

在业务系统的数据库操作中,当多个线程或请求同时修改同一条数据记录时,很容易出现后提交的更新覆盖先提交的更新内容,最终导致数据丢失的问题,这就是SQL并发更新丢数据的典型场景。这类问题在库存扣减、账户余额修改等高频更新场景中尤为常见,会直接影响业务数据的准确性。

SQL并发更新丢数据怎么办 乐观锁实现方案解析

并发更新丢数据的产生原因

并发更新丢数据的核心原因是多个更新操作没有感知到其他操作的存在,各自基于旧的数据版本执行更新。我们可以通过一个库存扣减的场景来理解:

  • 初始库存为10,请求A和请求B同时查询到当前库存为10
  • 请求A执行扣减1的操作,更新后库存为9,提交事务
  • 请求B同样执行扣减1的操作,基于自己查询到的库存10计算后更新为9,提交事务
  • 最终库存为9,而实际应该是8,出现了1个库存的丢失

这类问题的本质是更新操作没有做并发控制,多个操作基于相同的旧数据执行更新,最终互相覆盖。

乐观锁的核心原理

乐观锁是一种并发控制方案,它假设并发冲突的概率较低,因此在操作数据时不会立刻加锁,而是在提交更新时判断数据是否被其他操作修改过。如果数据没有被修改,就执行更新;如果数据已经被修改,就放弃当前操作或者重试。

在数据库场景中,乐观锁最常用的实现方式是版本号机制,具体逻辑如下:

  1. 在数据表中添加一个版本号字段,比如version,初始值为0
  2. 查询数据时,同时获取当前的版本号
  3. 更新数据时,在WHERE条件中加上版本号等于查询到的版本号,同时将版本号加1
  4. 如果更新语句影响的行数为1,说明更新成功;如果影响行数为0,说明数据已经被其他操作修改,当前更新失败

基于版本号的乐观锁SQL实现

首先我们需要在业务表中添加版本号字段,以MySQL为例,建表语句如下:

-- 创建商品库存表,添加version版本号字段
CREATE TABLE `product_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(20) NOT NULL COMMENT '商品ID',
  `stock_num` int(11) NOT NULL COMMENT '库存数量',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品库存表';

接下来是查询和更新操作的SQL示例,假设我们要扣减商品ID为1001的库存1个:

-- 1. 查询商品库存和当前版本号
SELECT stock_num, version FROM product_stock WHERE product_id = 1001;

-- 2. 假设查询到的stock_num为10,version为0,执行更新操作
UPDATE product_stock 
SET stock_num = stock_num - 1, version = version + 1 
WHERE product_id = 1001 AND version = 0;

-- 3. 判断更新结果,如果返回的影响行数为1,说明更新成功;如果为0,说明并发冲突,更新失败

当多个请求同时执行上述更新SQL时,只有一个请求能匹配到version=0的条件,更新成功;其他请求的version条件不匹配,更新影响行数为0,也就不会出现数据覆盖的问题。

Java业务层整合乐观锁实现

在实际的业务代码中,我们需要结合ORM框架来处理乐观锁逻辑,以下是基于MyBatis的实现示例:

实体类定义

public class ProductStock {
    private Long id;
    private Long productId;
    private Integer stockNum;
    private Integer version;
    // 省略getter、setter方法
}

Mapper接口定义

public interface ProductStockMapper {
    // 根据商品ID查询库存信息
    ProductStock selectByProductId(Long productId);
    
    // 乐观锁更新库存,返回影响行数
    int updateStockWithOptimisticLock(@Param("productId") Long productId, 
                                      @Param("deductNum") Integer deductNum, 
                                      @Param("oldVersion") Integer oldVersion);
}

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.ProductStockMapper">
    <select id="selectByProductId" resultType="com.example.entity.ProductStock">
        SELECT id, product_id, stock_num, version 
        FROM product_stock 
        WHERE product_id = #{productId}
    </select>
    
    <update id="updateStockWithOptimisticLock">
        UPDATE product_stock 
        SET stock_num = stock_num - #{deductNum}, 
            version = version + 1 
        WHERE product_id = #{productId} 
          AND version = #{oldVersion}
    </update>
</mapper>

业务层实现逻辑

@Service
public class ProductStockService {
    @Autowired
    private ProductStockMapper productStockMapper;
    
    /**
     * 扣减商品库存,使用乐观锁处理并发
     * @param productId 商品ID
     * @param deductNum 扣减数量
     * @return 是否扣减成功
     */
    public boolean deductStock(Long productId, Integer deductNum) {
        // 查询当前库存和版本号
        ProductStock stock = productStockMapper.selectByProductId(productId);
        if (stock == null) {
            throw new RuntimeException("商品库存不存在");
        }
        if (stock.getStockNum() < deductNum) {
            throw new RuntimeException("库存不足");
        }
        // 执行乐观锁更新
        int affectRows = productStockMapper.updateStockWithOptimisticLock(
            productId, deductNum, stock.getVersion()
        );
        // 更新成功返回true,失败说明并发冲突,可重试或返回失败
        if (affectRows == 1) {
            return true;
        } else {
            // 这里可以添加重试逻辑,比如重试3次后返回失败
            throw new RuntimeException("扣减库存失败,可能存在并发冲突");
        }
    }
}

乐观锁的适用场景和注意事项

乐观锁并不是所有场景都适用,它更适合并发冲突概率较低的场景,比如大部分时间是读取操作,更新操作较少的场景。如果并发冲突非常频繁,乐观锁会导致大量的更新失败和重试,反而会降低性能,这种场景更适合使用悲观锁。

使用乐观锁时需要注意以下几点:

  • 版本号字段建议设置为整型,每次更新加1即可,不需要使用时间戳等其他方式,避免时间戳精度问题导致的判断错误
  • 更新语句的WHERE条件必须包含版本号判断,否则乐观锁会失效
  • 如果更新失败,需要根据业务需求决定是重试还是直接返回失败,重试时要注意避免死循环
  • 乐观锁只适用于单条记录的更新场景,如果是多条记录的批量更新,需要额外设计并发控制逻辑

SQL乐观锁并发更新version数据一致性修改时间:2026-06-13 03:18:23

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