在高并发场景下,仅依赖数据库机制(如行锁、版本控制)无法完全避免数据异常的问题

在面试的过程中,会经常遇到面试官询问:

1、为什么要订单的新增\修改前要增加分布式锁?

不是可以通过联合唯一索引控制重复了嘛?可以通过版本控制并发修改嘛?或者直接行锁或者间隙锁?

我解释了一遍,他们也不信,觉得网上的解决方案就是对的,所以我这里用实际的代码运行结果,解释一遍原因,不信的人可以仿造代码自己执行试试

1、背景

在做ToB业务系统时,出现长事务时无可避免的,所以你会发现很多时候在针对某一个订单数据做新增和修改时,都会提前加一个分布式的锁(缓存锁,注册锁都可以),

这里你可能就有疑问了,新增为什么不靠数据库表的联合唯一索引做限制呢?修改为什么不用version字段做版本控制呢?

带着这些问题,往下看一个异常数据的代码示例

2、一个数据库出现异常问题的代码示例

java 复制代码
 @Test
    public void test() throws InterruptedException {
        // 创建两个线程对象
        MyThread thread1 = new MyThread(supplierInfoService,"1111");
        MyThread thread2 = new MyThread(supplierInfoService,"2222");

        // 启动线程
        thread1.start();
        Thread.sleep(500);
        thread2.start();
    }

下面是线程的内部实现

java 复制代码
 class MyThread extends Thread {
    private String creatName;
    private SupplierInfoService supplierInfoService;
    public MyThread(SupplierInfoService supplierInfoService,String creatName) {
        this.supplierInfoService = supplierInfoService;
        this.creatName = creatName;
    }

    @Override
    public void run() {
        try {
            supplierInfoService.test123(creatName);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}   

下面是supplierInfoService 这个接口的test123方法, 在方法也增加了事务注解 @Transactional

java 复制代码
volatile int num=0;
 @Transactional(rollbackFor = Exception.class)
    @Override
    public void test123(String createName) throws InterruptedException {
        SupplierInfoDto supplierInfoDto=supplierInfoDomain.getById(1901453933037268992L);
        log.error("3333"+createName+JSONObject.toJSONString(supplierInfoDto));
        num++;
        while(num<2){

        }
         if(createName.equals("2222")){
            Thread.sleep(500);
        }
        supplierInfoDomain.updateCreatUserName(1901453933037268992L,"admin",createName);

        SupplierInfoDto supplierInfoDto1=supplierInfoDomain.getById(1901453933037268992L);
        log.error("4444"+createName+JSONObject.toJSONString(supplierInfoDto1));
    }

SQL如下

sql 复制代码
 <update id="updateCreatUserName">
        UPDATE sup_supplier_info
        SET create_by=#{newCreatUserName,jdbcType=VARCHAR}
        WHERE id=#{id,jdbcType=BIGINT} and create_by=#{creatUserName};
    </update>

具体的流程:

  1. 创建两个线程,第一个线程先执行,第二个线程等待500毫秒
  2. 当第一个线程根据事务创建了readview(1)后原地自旋,等待第二个线程根据事务创建readview(2)
  3. 之后线程2休眠500ms,线程1执行update
  4. 之后线程2再执行update

这里有一个问题,最后create_by的最后结果是什么?

答案:如下

是不是很意外,以为通过where已经过滤了,那第二次修改应该不成功的,实际是成功的

3、出现这个数据异常问题的原因

原因也很简单,就是事务的第一次select 生成了readview,两个事务都有各自的readview,

虽然update默认有行锁,但是那是写,不是查,为了顺序写的,而在update语句中,where条件的作用域是在当前事务的readview上,只有set是作用在实际数据上

4、如何防止出现这种情况

通过上面的代码示例也清楚,通过version字段是无法解决问题这个问题的,加行锁也没有用(update默认行锁,看下面的锁的解释,只是为了其他事务的修改和删除,对于查询是不起作用的),
所以还是要提前加分布式锁,

行锁(Record Lock)​​

​​触发条件​​:当 UPDATE 语句的 WHERE 条件命中 ​​索引列​​(尤其是唯一索引或主键)时,InnoDB 会对符合条件的行加 ​​排他行锁(X Lock)​​,阻止其他事务修改或删除该行。

间隙锁(Gap Lock)​​
​​触发条件​​:当 WHERE 条件未命中索引或涉及范围查询时,InnoDB 会加 ​​间隙锁(Gap Lock)​​,锁定索引记录之间的间隙,防止其他事务插入新数据。

相关推荐
2603_965148115 分钟前
如何解析JSON数据?API返回的商品信息处理教程
开发语言·数据库·python·自动化·json·api
ltl9 分钟前
RocksDB 经典故障排查:L0、compaction 与 write stall
数据库
xfhuangfu1 小时前
Oracle中建立到CDB和PDB的连接
数据库·oracle·rpc
小小龙学IT1 小时前
DuckDB 深度实战:用 C++ 在进程内跑一个「分析型数据库」
数据库·c++
NineData2 小时前
DTCC 2026 预告|NineData CEO& 创始人叶正盛:面向 AI Agent 的数据库 DevOps 与数据复制实践
数据库·人工智能·数据库开发·devops·ninedata·数据库技术·dtcc
J_bean2 小时前
MySQL 事务是否必须手动开启?
数据库·mysql·数据库事务·自动提交·手动提交
J_bean2 小时前
MySQL InnoDB 如何检测死锁、判定死锁、处理死锁
数据库·mysql·死锁·死锁检测·处理死锁·死锁判定
浪子明X3 小时前
从 MongoDB 文档到关系模型:构建可重跑、可对账的数据迁移流水线
数据库·mongodb·oracle
杜子不疼.3 小时前
国产数据库撑起固井软件自主化:金仓 × 中海油服「海恒 Cemsol」落地解析
数据库
隔窗听雨眠3 小时前
GBase 8s并发控制深度解析:封锁机制、隔离级别与死锁处理全攻略
服务器·数据库·oracle