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

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

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)​​,锁定索引记录之间的间隙,防止其他事务插入新数据。

相关推荐
转身後 默落21 分钟前
03.一键编译安装Redis脚本
数据库·redis·缓存
blurblurblun1 小时前
Redis实战(7)-- 高级特性 Redis Stream数据结构与基础命令
数据库·redis·缓存
永卿0011 小时前
mysql 日志机制
数据库·mysql
wu~9702 小时前
Mysql深入学习:慢sql执行
mysql
先鱼鲨生2 小时前
etcd 的安装与使用
数据库·etcd
crossoverJie3 小时前
StarRocks 如何在本地搭建存算分离集群
数据库·后端
潇凝子潇4 小时前
如何在不停机的情况下,将MySQL单库的数据迁移到分库分表的架构上?
数据库·mysql·架构
Tapdata4 小时前
什么是 Operational Data Hub?它因何而生,又为何能够在当下成为技术共识?
数据库
Seven974 小时前
Mysql的索引数量是否越多越好?为什么?
mysql
这里有鱼汤4 小时前
普通人做量化,数据库该怎么选?
数据库·后端