同步A系统多张表(oracle数据库)中的明细数据,插入到本系统的数据库中(MySQL),并做一些汇总计算,最后展示成报表。
使用一个大事务,覆盖面是所有的同步和汇总计算逻辑。如果程序出错或异常,则事务回滚。
直接按顺序查询A系统的每张表中的所有数据,按条插入B系统指定表中。
interface Service{
void syncAndSummaryData();
}
class ServiceImpl{
@Override
@Transactional(rollbackFor = Exception.class)
public void syncAndSummaryData(){
syncTable1();
syncTable2();
syncTable3();
summaryData();
}
public void syncTable1(){
1、查询A系统的Oracle库table1表中所有数据到ArrayList中
2、for循环逐条插入到本系统的MySQL库中
}
public void syncTable2(){
1、查询A系统的Oracle库table2表中所有数据到ArrayList中
2、for循环逐条插入到本系统的MySQL库中
}
public void syncTable3(){
1、查询A系统的Oracle库table3表中所有数据到ArrayList中
2、for循环逐条插入到本系统的MySQL库中
}
public void summaryData(){
1、将本系统中同步的数据使用SQL的group by分类汇总后,放入ArrayList中
2、for循环逐条插入到本系统中
}
}
1、现有的事务机制不支持多个数据库操作,所以方法中事务其实是失效的。
2、存在JVM内存溢出的风险。原因:B系统的JVM最大堆内存是4G,如果A系统中存在一张表中数据量在几百万条(每条数据有45个左右字段),一次性查询出全部数据放入List中,则所有数据全部存放在JVM的堆内存中,可能会撑爆JVM内存。
1、原有事务不变保持。
2、查询A系统的oracle数据库每张表的逻辑,单独抽成另一个方法,方法上方的事务注解中添加传播机制控制,使用NOT_SUPPORTED,即在查询Oracle数据库方法中挂起当前事务,插入MySQL数据库方法中事务才生效,经过运行测试证明,这样可以起到所有插入操作还是在同一个事务中的效果。
3、查询数据采用分页查询,每次查询1万条。
interface Service{
void syncAndSummaryData();
}
class ServiceImpl{
private static final int pageSize = 10000;
@Override
@Transactional(rollbackFor = Exception.class)
public void syncAndSummaryData(){
syncTable1();
syncTable2();
syncTable3();
summaryData();
}
public void syncTable1(){
//循环分页查询A系统的Oracle库table1表中数据到ArrayList中,然后再插入到本系统MySQL数据库中
List list = null;
int pageNo = 1;
do{
Map result = queryTable1(pageNo, pageSize);
//由于查询oralce库的table1表方法会挂起当前事务,所以查询方法报错不会造成事务回滚。
//为了在查询报错时,事务也可回滚,采用判断返回标识的方式。
//如果返回map为空或者返回标识是失败,则抛异常,让事务回滚。
if(result == null || !(Boolean)result.get("result")){
throw new Business("查询table1表出错");
}
List list = (List)result.get("data");
if(list == null || list.isEmpty()){
break;
}
pageNo++;
for循环(逐条插入到本系统的MySQL库中)
list.clear();//主动清空list,减轻JVM压力
}while(list.size() == pageSize);
}
//查询其他系统的oracle数据库,使用传播机制中的NOT_SUPPORTED,在执行查询方法时挂起当前事务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public Map queryTable1(pageNo, pageSize){
Map result = new HashMap();
result.put("result", Boolean.TRUE);
try{
按分页条件查询A系统的Oracle库table1表中数据,返回List
result.put("data", 查询返回的List);
}catch(Exception e){
log.error(String.format("查询table的第%s页时报错,原因:", pageNo), e.getMessage);
result.put("result", Boolean.FALSE);//如果查询报错就返回false,作为供调用方法识别
}
return result;
}
public void syncTable2(){
逻辑同table1
}
public void syncTable3(){
逻辑同table1
}
public void summaryData(){
1、将本系统中同步的数据使用SQL的group by分类汇总后,放入ArrayList中
2、for循环逐条插入到本系统中
}
}
1、事务生效,报错时事务可回滚
2、一次只查1万条数据,避免了JVM内存溢出的风险。
性能不理想,经过线上验证,同步64万条数据,用时75分钟左右,不满足性能需求。
1、去掉事务,加入版本概念保证展示数据正确性(类似于悲观锁和乐观锁的理念),具体做法如下:
(1)存版本表(主要字段有版本号、版本状态,状态有生成中、有效、失效),每条数据中都加入版本号字段。
(2)同步或计算失败时程序中断,垃圾数据(保存版本号)还在,但置对应版本号状态为“失效”。
(3)前端页面的数据查询时只查询有效版本号的数据。
(4)对于失效版本,写一个定时任务,清理垃圾数据。
去掉事务的原因:事务对于SQL操作的性能有很大的影响,经过我们多次线上验证,单条SQL操作:有事务在几百毫秒左右,无事务在几十毫秒左右,速度相差几倍到十几倍。
2、插入操作采用batchInsert,一次插入250条(经过试验,一次插入太多速度反而会慢,250条1次是一个速度较快的值)。即在Mybatis层使用for循环动态生成INSERT语句。最终的SQL示例:
INSERT INTO table1(data1, data2, version) VALUES (‘zhangsan’, 21, 201901001), (‘lisi’, 20, 201901001),(‘wangwu’, 34, 201901001)…共250条;
使用batchInsert的原因:
(1)众所周知,内存操作的速度远大于数据库IO操作。
(2)for循环调用250次插入数据库方法,至少产生了250次数据库IO操作。而batch操作是在内存中循环250次组装成SQL语句,然后调用一次数据库IO操作插入250条数据。
(3)虽然一次插入250条数据的耗时比一次插入1条数据要多一点,但是这点耗时与250次数据库IO操作比起来就要小太多了。
3、查询条数一次扩大到5万条,经过验证一次查询1万条数据需要13秒以上,一次查询5万条数据需要30秒左右。采用5万条可以节省一半的查询时间。
interface Service{
void syncAndSummaryData();
List getData();
}
class ServiceImpl{
private static final int pageSize = 50000;
@Override
public void syncAndSummaryData(){
int newVersion = 获取新版本号,并插入版本表中,新版本号状态为“生成中”
try{
syncTable1(newVersion);
syncTable2(newVersion);
syncTable3(newVersion);
summaryData(newVersion);
程序走到这里,同步和计算成功,更新newVersion版本号状态为“有效”,其他版本号状态都为“无效”
}catch(Exception e){
出现异常,则更新newVersion版本号状态为“无效”
}
}
private void syncTable1(){
//循环分页查询A系统的Oracle库table1表中数据到ArrayList中,然后再插入到本系统MySQL数据库中
List list = null;
int pageNo = 1;
do{
List list = queryTable1(pageNo, pageSize);
if(list == null || list.isEmpty()){
break;
}
pageNo++;
for循环(每250条一次批量插入数据到本系统的MySQL库中)
list.clear();//主动清空list,减轻JVM压力
}while(list.size() == pageSize);
}
//有异常不catch,直接抛到上层方法
private List queryTable1(pageNo, pageSize){
按分页条件查询A系统的Oracle库table1表中数据,返回List
return 查询返回的List;
}
private void syncTable2(){
逻辑同table1
}
private void syncTable3(){
逻辑同table1
}
private void summaryData(){
1、将本系统中同步的数据使用SQL的group by分类汇总后,放入ArrayList中
2、for循环(每250条一次批量插入数据到本系统的MySQL库中)
}
@Override
public List getData(){
int version = getValidVersion();//从版本表中获取状态为有效的版本号
List list = getData(version);//查询版本号是version的数据
}
}
//定时任务删除垃圾数据不再赘述
1、再保证了展示数据正确性的前提下,采用了类似于乐观锁的逻辑去掉事务、使用批量插入、增大每次查询条数3种方式,提高了同步速度。
经过验证,平均数据处理速度(查询、计算、插入3种操作的共同平均速度。其中计算逻辑速度较快,占比较小。查询逻辑,每次查询1万条13秒左右,每次查询5万条30秒左右。)
1、在使用事务、逐条插入、每次查询1万条时,整个同步和计算过程中,平均数据处理速度是150条左右/秒,75分钟左右处理64万条数据。
2、不使用事务、逐条插入、每次查询1万条时,整个同步和计算过程中,平均数据处理速度是410条左右/秒,26~30分钟处理64万条数据。
3、不使用事务、250条每次批量插入数据、每次查询5万条时,整个同步和计算过程中,平均数据处理速度是853条左右/秒,12.5分钟左右处理64万条数据(该方案的纯数据库插入操作速度,一条数据50个字段前提下,已经达到了1800条以上/秒)。
此处要声明一下:我们的数据库服务器是固态硬盘。机械硬盘的入库速度肯定不会这么快。
1、因为没有事务保证,数据同步失败后更新“生成中”的版本号为“无效”状态时,也可能更新失败,概率极小,此时需要人工介入或者再用其他的补充逻辑处理(比如长时间处于“生成中”的版本号修改为“无效”状态)。但是不影响数据的正确展示。
但是这个缺点的代价很小,完全可以接受。