×
文章路径: Java

数据同步引发的一些思考

发表于3年前(Dec 24, 2014 10:39:16 AM)  阅读 454  评论 0

分类: Java 数据库 案例

标签: etl truncate 批量删除 数据同步

1、问题场景描述:
有Oracle数据库服务器A,有Oracle数据库服务器B,有第三方数据存储系统C,服务器A和C之间只能通过中间库B访问,现A定时产生大量数据,需要同步给C,C不只是简单的获取数据,还需做分析处理。这里没有采用第三方ETL工具,原因不赘叙,现在看如何自己编写代码完成同步过程。

2、初始方案:
在B服务器建立中间表,A服务器定时将数据写入B服务器,C系统定时从B服务器取数据(时间间隔根据数据要求的实时性来定),取完数据的同时要将数据进行删除。这里关键一个问题时,A服务器写数据的操作是虽然有一定时间规律,但每次数据量不同,写入花费的时间也不同,且大批量的数据写入必然是分批提交,所以,A服务器写数据的操作可以当成是随机的。首先C系统读取完数据,开始进行删除,在读取数据的过程中,很有可能有新的数据进来,这样删除数据就必须指定条件只能删已经处理过的数据,而不是简单的全表删除。所以我们对中间表做了一个要求,必须要有自增的序列,每次读取时,根据当前序列最大值,只处理小于这个最大值的数据,相当于给表建了个“快照”。
a、A服务器往B服务器写入数据,数据增加自增序列
b、C系统定时扫描B服务器中间表数据,找出当前序列字段最大值,读取小于等于该值的所有数据,最后删除小于等于该值的所有数据。

3、初始方案存在的问题:
初始方案暂时看不出什么问题,简单测试都能通过。但测试时数据量比较少,无法体现线上真实场景。实际上如果数据量很大(其实真实场景一定是大数据量的,数据量下的话,基本上就根本不用这么费事了),会发现同步过程会卡在最后一步,删除数据。测试时,一个表100多万记录,删除10多s,ok,还可以接受,但是如果记录达到几千万,发现sql根本无法响应(根据不同服务器有不同表现)。这很正常,oracle删除数据需要做大量日志操作,什么redo,undo啊,反正从一张oralce大表里面删除大量数据这个过程是很慢的,那可以不做日志吗,可以,直接truncate,但这里上节说了,由于A服务器写入数据时间的不确定性,不能直接删除整个表的数据,必须带条件,所以不能直接truncate,一个折中方案是分批删除,每次删除1000条或者10000条,这样虽然还是比较慢,但是至少能看到进程,能执行成功,看到结果。这里我们测试了几种分批删除的方式,首先确定每次删除的记录数,测试发现1k,1w,10w差别都不太大,其次使用jdbc删除与写存储过程删除,差别也不明显(测试删除3kw数据将近20分钟),使用jdbc删除在程序中可以看到执行进程,使用存储过程删除则不行。两种删除方式代码会贴在最后。

4、初始方案还能优化吗:
如果你能接受删除数据的耗时(实际上删除数据的操作比你写数据的操作还要长),上面的方案应该能满足你的要求了,我们可以缩小数据同步的间隔,使每次同步的数据量尽量小。但如果实时性要求高,性能瓶颈卡在删除数据上哪怎么办呢?删除数据只有truncate最快,真不能truncate吗?之前说了,不能truncate的原因是C系统不知道在truncate的过程中,A服务器有没有插入数据,插入了哪些数据,因为写数据和取数据是并行的,那如果是串行的呢。假如A服务器在写数据的时候,告诉C系统,我在写数据,这时候你不要取,C系统在取数据的时候,告诉服务器A,我在取数据,这时候你不要写,这时候我们是不是就可以直接truncate了?是的,这时两台服务器都可以放心的做自己的操作。

5、优化方案:
a、在B服务器建立一个辅助操作表,记录一个标示,标示有三个值,一个表示“空闲”,一个表示“正在写”,一个表示“正在读”。
b、A服务器在写数据之前,先获取状态标示,如果为“空闲”,则进入“准备写入数据”状态,并且把标示改为“正在写”。
c、如果此时标示为“正在读”,则等待,隔一段时间再检查标示。
d、A服务器在真正写入数据之前,再次检查状态标示,如果为“正在读”,进入c步骤,如果为“正在写”则开始写入数据(再次检查是防止“空闲”状态下,两者同时修改状态标示,所以再次检查跟第一次检查需要间隔几秒钟)。
e、A服务器写完数据后,将状态标示修改为“空闲”。
f、C系统取数据之前,同样先获取状态标示,如果为“空闲”,进入“准备取数据”状态,并且把标示改为“正在读”。
g、如果此时标示为“正在写”,则等待,隔一段时间再检查标示。
h、C系统再真正取数据之前,再次检查状态标示,如果为“正在写”,进去g步骤,如果为“正在读”,则开始取数据。
i、C系统取完数据,将整表truncate,然后将状态标示修改为“空闲”。

6、总结:
以上基本是这次问题的解决过程,还是那句老话,不一定适用每个场景,根据需要,根据问题进行具体优化。

附录:
1、循环删除数据的存储过程

create or replace procedure delete_table_data(tablename in varchar2,amax in number) is
sqlstr varchar2(4000);
begin
       sqlstr:='delete from '||tablename||' t where t.id<='||amax||' and rownum<=10000';
       loop
         execute immediate sqlstr;
         if SQL%NOTFOUND then
           exit;
         end if;
         commit; 
       end loop;    
end;
2、循环删除数据的jdbc代码

public void deleteData(String tableName,long count, long max) throws Exception {
		Connection con =     
	             OracleConnectionPool.getInstance().getConnection() ;
		Statement stmt = null;
		stmt = con.createStatement();
		String sql = "";
		int n = 0;
		long sum = 0;
		log.info("start delete source data:"+tableName+","+count+","+max);		
		n=-1;
		while(n!=0) {
			sql = "delete from "+tableName+" t where t.id<="+max+" and rownum<=10000";
			n = stmt.executeUpdate(sql);
			sum+=n;
			log.info("delete data "+sum+"/"+count+" from table "+tableName);
		}		
		log.info("end delete source data!");
}

发表评论