基础环境与技术
- mysql 5.7
- springboot 2.4.2
- mybatis-plus
需求
概述
学生管理功能,具备增删改查学生信息的逻辑。支持批量新增;
要求
- 逻辑删除
- 重复加入的学生信息记录覆盖已存在的记录(学号作为学生唯一标识);
- 导入时间限制 5s之内
- 学生基本信息通过学号从三方系统提供接口获取(i/o操作)
分析
- 建表,由于逻辑删除,无法为学号建立唯一索引1
- 考虑查询学号和姓名居多,分别为学号和姓名建立普通索引
- 无法使用 mysql on duplicate update 语法,原因:无法为学号建立唯一键索引
- 无法使用 not exists 来保证,因为要对存量的数据进行修改,sql语义不好实现
- 数据库增加版本号字段(乐观锁),比较适用于修改,对于并发新增的情况,锁不住(没想到合适的)
- 只能使用先查询,再修改或者插入。
单机下:加锁
分布式系统: 增加分布式锁
数据准备
准备5条学生信息数据用例,至表student_exp,其中id=1 和 id=2 的stu_no重复。
目标:当将学生信息使用多线程插入student_info表中后,预期stu_no应该无重复记录。
- [[#^b04c6d|建表语句student_exp]]
- [[#^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='名单列表';
-
因为逻辑删除之后,库中有多条被逻辑删除之后的记录 ;更普适的看这个博客先查询后插入在高并发下重复插入问题解决 ??