并发导致重复添加问题-单机

基础环境与技术

  1. mysql 5.7
  2. springboot 2.4.2
  3. mybatis-plus

需求

概述

学生管理功能,具备增删改查学生信息的逻辑。支持批量新增;

要求

  1. 逻辑删除
  2. 重复加入的学生信息记录覆盖已存在的记录(学号作为学生唯一标识);
  3. 导入时间限制 5s之内
  4. 学生基本信息通过学号从三方系统提供接口获取(i/o操作)

分析

  1. 建表,由于逻辑删除,无法为学号建立唯一索引1
  2. 考虑查询学号和姓名居多,分别为学号和姓名建立普通索引
  3. 无法使用 mysql on duplicate update 语法,原因:无法为学号建立唯一键索引
  4. 无法使用 not exists 来保证,因为要对存量的数据进行修改,sql语义不好实现
  5. 数据库增加版本号字段(乐观锁),比较适用于修改,对于并发新增的情况,锁不住(没想到合适的)
  6. 只能使用先查询,再修改或者插入。
    单机下:加锁
    分布式系统: 增加分布式锁

数据准备

准备5条学生信息数据用例,至表student_exp,其中id=1 和 id=2 的stu_no重复。
目标:当将学生信息使用多线程插入student_info表中后,预期stu_no应该无重复记录。

  1. [[#^b04c6d|建表语句student_exp]]
    ![[Pasted image 20240114225356.png]]
  2. [[#^cfecf9|建表语句student_info]]

编码

通用编码

// step1: 从student_exp获取5个用例
List<StudentExp> studentExps = studentExpService.studentExps();

// step2: 转换成student_info表实体
List<StudentInfo> createStudentsInfo(List<StudentExp> studentExps)

// step3: 提交到线程池,让线程池处理
for (StudentInfo info : infos) {  
    completionService.submit(() -> {  
        try {  
            this.saveInfo(info);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }, null);  
}

// step4 : 不存在则保存,存在则更新
void saveInfo(StudentInfo studentInfo) {  
    String stuNo = studentInfo.getStuNo();  
    StudentInfo one = this.getOne(new LambdaQueryWrapper<StudentInfo>().eq(StudentInfo::getStuNo, stuNo), false);  
    if (one != null) {  
        studentInfo.setId(one.getId());  
        this.updateById(studentInfo);  
        return;    }  
    try {  
        Thread.sleep(2000);  
    } catch (InterruptedException interruptedException) {  
        interruptedException.printStackTrace();  
    }  
    this.save(studentInfo);  
}

在这里插入图片描述

图一: 先查再更新或保存 ^756709

situation1: 使用单线程

执行结果正常

//替换通用编码step3
for (StudentInfo info : infos) {  
    this.saveInfo(info);  
}

在这里插入图片描述

耗时:8975

situation1-1: 使用单线程异步执行

//核心线程,以及最大线程数均设置成1
return getExecutor("批量插入-", 1, 1, 10, 60, new ThreadPoolExecutor.CallerRunsPolicy());

在这里插入图片描述

耗时:8880 ms

situation1-2: 使用单线程异步并发执行

页面一直点击刷新即可复现,我快速点了4下,产生如下的数据。很明显在并发条件下,重复值很容易出现,当然还有一些其它情况-可以参考(接口幂等性——防止并发重复插入数据),对照[[并发导致重复添加问题-单机#^756709|图一]],并发情况下,执行到①时,数据库中还未入库,此时通过stu_no查询,结果均为null,因此都会执行插入操作。
在这里插入图片描述

//测试代码
for (StudentInfo info : infos) {  
    completionService.submit(() -> {  
        try {  
            this.saveInfo(info);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }, null);  
}

situation2: 使用5个线程同时插入

对照[[并发导致重复添加问题-单机#^756709|图一]],若5个线程同时通过stuNo执行查询,到达②处时,由于此时库中没有与插入数据stuNo相同的数据,因此one均为null,程序将会保存5条记录,其中2条存在重复。实验结果如下id = 1 和 id=5的stu_no重复
在这里插入图片描述

耗时:2344 ms

核心sql

insert into student_info(stu_no, name,age,sex) values('12223333','张三',23,'男');

建表语句 ^b04c6d

CREATE TABLE `student_exp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `stu_no` varchar(20) COLLATE utf8mb4_german2_ci NOT NULL,
  `name` varchar(20) COLLATE utf8mb4_german2_ci NOT NULL,
  `age` int(3) NOT NULL,
  `sex` char(1) COLLATE utf8mb4_german2_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci;

INSERT INTO `experiment`.`student_exp`(`id`, `stu_no`, `name`, `age`, `sex`) VALUES (1, '12223333', '张三', 23, '男');
INSERT INTO `experiment`.`student_exp`(`id`, `stu_no`, `name`, `age`, `sex`) VALUES (2, '12223333', '张三2', 24, '女');
INSERT INTO `experiment`.`student_exp`(`id`, `stu_no`, `name`, `age`, `sex`) VALUES (3, '12223334', '李四', 24, '男');
INSERT INTO `experiment`.`student_exp`(`id`, `stu_no`, `name`, `age`, `sex`) VALUES (4, '12223335', '王五', 25, '男');
INSERT INTO `experiment`.`student_exp`(`id`, `stu_no`, `name`, `age`, `sex`) VALUES (5, '12223336', '胡六', 26, '女');

建表语句^cfecf9

CREATE TABLE `student_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `stu_no` varchar(8) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '学号',
  `name` varchar(255) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` char(2) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '性别',
  `deleted` tinyint(1) DEFAULT '0' COMMENT '0:正常  1: 删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci COMMENT='名单列表';



  1. 因为逻辑删除之后,库中有多条被逻辑删除之后的记录 ;更普适的看这个博客先查询后插入在高并发下重复插入问题解决 ??