开始之前吹吹水
还记得当年懵懂的你对百万年薪的渴望吗
还记得当年青涩的你对百万数据的追求吗
还记得当年迷茫的你对百元日薪的无奈吗
还记得当年疲惫的你对百万编程的执着吗
还记得意气风发的少年在网吧爆肝的时光吗
还记得初入社会的青年在社会挣扎的日子吗
还记得三十而立的中年在人间烟火的每天吗
还记得步入暮年的前辈在世间万物的回忆吗
伊始
对于某些需求来说,需要百万数据的支持更能接近实际场景,对日常开发和日常练习也有好处。于是老铁我网上查了产生大量测试数据的方法,大概有以下几种
1.filldb,这个web提供多种正则生成测试数据,但其正则没有具体说明,对于正则不熟悉的同学来说会有点难上手,并且啊铁我实际使用来看,一万数据量容易503
2.存储过程(过,我不会。囧!!!)
3.程序
因为铁子要生成的测试数据是用户表,username, email, phoneNu需要做唯一检查,并且password也需要加密处理,这样看来通过java逻辑才能实现需求,在保证逻辑正确的基础上,我们不要忘记对性能的追求,对效率的执着,下面跟着铁子开干!
表结构
- CREATE TABLE
member_0( idbigint(20) NOT NULL DEFAULT '0',usernamevarchar(24) NOT NULL COMMENT '用户名',nicknamevarchar(32) DEFAULT '' COMMENT '昵称',passwordchar(63) NOT NULL COMMENT '用户密码',phone_nochar(24) DEFAULT NULL COMMENT '手机号码',email_addvarchar(127) DEFAULT NULL COMMENT '邮箱',iconvarchar(255) DEFAULT '' COMMENT '头像',genderenum('SECRET','MALE','FEMALE') DEFAULT 'SECRET' COMMENT '用户性别',leveltinyint(1) DEFAULT '0' COMMENT '用户等级',reward_pointint(11) DEFAULT '0' COMMENT '积分',statusenum('INACTIVE','ACTIVE','LOCKED') DEFAULT 'INACTIVE' COMMENT '用户状态',birthdaydatetime DEFAULT '1970-01-01 00:00:00' COMMENT '用户生日',deletedbit(1) DEFAULT b'0',created_timedatetime DEFAULT CURRENT_TIMESTAMP,updated_timedatetime DEFAULT CURRENT_TIMESTAMP,created_byvarchar(32) DEFAULT '',updated_byvarchar(32) DEFAULT ''- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
如你所见,除了主键索引,没有唯一索引,意味着需要在代码逻辑保证username, email, phoneNu的唯一。
目的
随机生成五百万条测试数据,统计耗时。
开搞
内存优化
需要检查3个字段的唯一性,五百万条数据就是1500万个字符串需要做唯一检查,如果把1500万个字符串怼到内存,会导致电脑卡顿,CUP疯转,现在天气这么热,电脑吹的热风和空调的冷风形成冷热交替,热风膨胀向上,冷风收缩向下,到时候你的头吹的是热风,脚吹的是冷风,头发发热,手脚冰冷,女朋友肯定会嫌弃你(女生都喜欢手脚暖的男朋友)
无论基于什么理由,都不能直接让内存撑着。从两方面优化,加内存和加内存。
字段检查
足够的内存保证电脑的舒适,也能保证我们的舒服,三个字段的检查,分别创建三个hash结构,利用hash的特点(相同字符串hash值相同,相同hash值的字符串可能相同),实现O(1)完成数据唯一性检查。基本做法是,每生成一条数据,往hash结构插入。
创建一个validate方法,判断待检查的数据是否符合要求。HashSet底层用的是HashMap,只不过它的value是null,省点内存。
再提内存问题
内存是珍贵资源,五百万条数据可能不能在一起怼到内存,所以生成一批测试数据就得批量插入,并统计成功插入数量。老铁用的是mybatis,关于mybatis批量插入有3种方式,记得使用那个sql较短的方式,减少传输数据量。
优化插入速度
折腾完java,不能漏掉mysql,两兄弟一起揍!
仔细思考,单次插入mysql的数据量在一万条左右,传输一万条数据少说也有几M吧(听我瞎猜),mysql解析数据也需要时间。数据传输时,mysql的网络io忙着,工作线程闲着(这部分涉及到多路复用,但不确定mysql是不是多路复用),使用并行传输数据,充分利用mysql cup。
涉及多线程,得考虑线程安全问题。还记得需求要求统计成功插入数据总量吗,使用原子对象,保证线程安全。
并且把三个ColumnSet改成分段锁模型,提高锁效率
最后重点在并发模型,这里的executor是线程池,开了10个线程,每次随机生成一千条条数据,循环五百次,最后一条线程完成任务打印总耗时。(这里涉及到AQS,有兴趣看铁子刚入行写的AQS文章,随便看看当年铁子多傻逼!)
CompletableFuture牛逼!
到这里已经很ok了,再注意几个细节就更完美。
1.mysql的innodb_buffer_pool_size参数,写操作超过内存阈值,发生磁盘交换。限制速度,需要实际测试合适的值。
2.整个过程尽量避免打印日志
3.长时间耗时循环会延长GC时间,可以考虑safePoint,把count改成long类型就好
4.铁子电脑是m1 mac,战绩还行,如果重启电脑,啥都不开,150秒就搞定。当然线上环境肯定不止。
最后附上插入代码
public int batchInsert(Set<String> columnUsername, Set<String> columnPhoneNo,
Set<String> columnEmailAdd, List<MemberPo> memberPoList) {
if (memberPoList.isEmpty()) {
return 0;
}
logger.info("{} pieces of data filter remain {} pieces of data are going to insert", memberPoList.size(), memberPoList.size());
List<MemberPo> memberPoList2Db = new ArrayList<>(memberPoList.size());
for (MemberPo memberPo : memberPoList) {
if (this.validate(memberPo, columnUsername, columnPhoneNo, columnEmailAdd)) {
memberPoList2Db.add(memberPo);
}
}
return BatchInsertUtils.insertData(memberPoList2Db, memberRepository::batchInsert);
}
/**
* 插入方法
*
* @param list 插入数据集合
* @param function 消费型方法,直接使用 mapper::method 方法引用的方式
* @param <T> 插入的数据类型
*/
public static <T> int insertData(List<T> list, ToIntFunction<List<T>> function) {
if (list == null || list.isEmpty()) {
return 0;
}
int totalCount = 0;
for (int i = 0; i < list.size(); i += 1000) {
int j = Math.min((i + SIZE), list.size());
List<T> subList = list.subList(i, j);
totalCount += function.applyAsInt(subList);
}
return totalCount;
}