代码改变世界

记:一次在正式环境下的大批量业务数据纠正

2010-07-19 18:12  stubman  阅读(435)  评论(0编辑  收藏  举报

  在一次与客户关于系统年工作量报表统计的讨论后,翻看系统相关模块的业务代码,理清思路后得到了最不愿意接受的结论该模块的数据流程的处理有误,过去一年多产生的相关数据在某种意义上说都是脏数据

  之所以说是脏数据,是因为这些数据本来是源于用于统计的出发点进行设计的,可是最后却事与愿违,客户要的统计数据走的不是这样的业务逻辑【回过头去想,当时的需求没有做的足够充分、准确,也是导致这个错误的关键】。

  事情已经发生了,唯一能做的只有补救了。花了一段时间,对相关模块的业务逻辑全面了解之后确定,解决方法是有的:删除过往一年的统计数据表相关数据,根据过往的业务数据,按照正确业务需求对数据进行处理,插入正确的统计相关的数据。

  挺简单的一句话,实现起来可不怎么轻松,需要考虑并且解决的问题也不少:

  1. 需要根据正确的业务需求以及原有的业务数据写出得到正确统计数据的业务逻辑
  2. 统计相关的数据表中,不只有这个模块相关数据,涉及多个模块的业务数据,数据量很大,进行大量的插入操作估计占用时间较长;
  3. 系统24小时处于使用状态,不可能脱机纠正数据,工作必须是在正式环境进行
  4. 系统不断有新的业务数据插入,这些数据不能与新插入的纠正数据重复
  5. 纠正数据的过程会对系统压力比较大,要做到对纠正过程及纠正结果(成功or失败)了然于胸,看到实时的纠正情况,否则心里没底。

  经过一段时间的思考和尝试之后,最终问题当然还是解决了,解决方案

  • 根据正确的业务逻辑,修改系统相应模块的程序,使之正确运行在正式环境中【这一步是必须最早进行的,否则后面的纠正做完了之后还有脏数据进入系统】;
  • 在纠正前对要纠正的数据表进行备份,以防不测。
  • Oracle中设置一个的Job,控制数据库服务器在系统使用比较少,压力较小的时段【晚上】,执行写好的纠正控制存储过程,在此存储过程中,以月为跨度,从09年1月开始,循环到当前月份,按顺序纠正各个月份的业务数据,以月为单位,每成功纠正一个月的数据,则往纠正日志表中插入成功纠正的相关日志,纠正失败当然也会插入失败记录。
  • 这其中是如何避免业务数据的重复的呢?实现过程中,以最小业务单位为基础,【此系统中是记录每个儿童的业务数据】,游标找出这个月所有儿童的数据集,然后遍历儿童数据集,第一步则删除此儿童相关所有业务数据,然后根据正确的业务逻辑进行重新插入,这样,就避免了系统新插入的业务数据与纠正产生的业务数据重复的问题【最后是以纠正程序产生的业务数据为准了】。

以下是纠正控制存储过程:

CorrectE_chkitemsCTL
 1 create or replace procedure CorrectE_chkitemsCTL
 2 /*
 3      author by zzs:2010-01-13
 4      纠正e_chkitems控制程序
 5 */
 6 is
 7 V_Timebegin date;
 8 V_Timeend date;
 9 begin
10    insert  into M_PROCEDUREEXECUTELOG(LOGTIME,MSG) values(sysdate,'纠正程序开始执行!');
11    commit;
12    for i in 0..12 loop
13       V_Timebegin:=ADD_Months(to_date('2009-01-01','yyyy-MM-dd'),i);
14       V_Timeend:=ADD_Months(to_date('2009-02-01','yyyy-MM-dd'),i);
15       
16       CorrectE_chkitemsByE_Feed(V_Timebegin,V_Timeend);
17    end loop;
18    
19    insert  into M_PROCEDUREEXECUTELOG(LOGTIME,MSG) values(sysdate,'纠正程序执行完毕!');
20    commit;
21 end CorrectE_chkitemsCTL;

 此处,按照需要纠正的月数写0..12,纠正过去一年的数据。并且在程序开始,结束时都往存储过程执行日志表M_PROCEDUREEXECUTELOG插入日志。  

 以下是纠正给定时间内业务数据的具体纠正存储过程:

CorrectE_chkitemsByE_Feed
 1 create or replace procedure CorrectE_chkitemsByE_Feed(P_StartTime in date,P_EndTime in date)
 2 /*
 3        author by zzs:2010-01-28
 4 
 5        纠正e_chkitems:根据e_feed信息往e_chkitems插入数据
 6   */
 7  is
 8 
 9   V_ChildCycle varchar2(2); --查体周期
10   V_Isroot     varchar(2);
11 
12   V_ManageAge  varchar(2);
13   V_ManageOrg  varchar(36);
14 
15   V_ChildID    varchar(36);
16 
17   timebegin             varchar2(30);
18   timeend               varchar2(30);
19   Insert_E_Chkitems_Num number default(0);
20 
21 begin
22   timebegin := to_char(sysdate, 'yyyy-MM-dd hh24:mi:ss');
23   for y in(select columnNames from table where operatedate>=P_StartTime and operatedate<P_EndTime )--此处表名,列名被省略
     loop

25       delete E_CHKITEMS where....--此处涉及相关业务,省略
26       V_Isroot     := y.ISROOT;
27       for x in (select a.* from e_feed a where ...)) loop
29         
30         V_ChildCycle := GetChildCycle(x.OPERATEDATE, y.DATEOFBIRTH); --某儿童某次体检时所属周期
31         V_ManageAge  := GetManageAge(x.OPERATEDATE, y.DATEOFBIRTH); --某儿童某次体检时所属查体年龄段
32         V_ManageOrg  := getManageorgBeforeChkdate(x.CHILD, x.ORG, x.OPERATEDATE); --工作量所属机构
33 
34         if x.AGE='3' and V_ManageAge='1' then
35            insert into E_CHKITEMS ....--此处涉及相关业务,省略
36            Insert_E_Chkitems_Num := Insert_E_Chkitems_Num + 1;
37         elsif x.age='6' and V_ManageAge='2' then
38            insert into E_CHKITEMS ....--此处涉及相关业务,省略
39            Insert_E_Chkitems_Num := Insert_E_Chkitems_Num + 1;
40          end if;
41         
42       end loop;
43   end loop;
44 
45     timeend := to_char(sysdate, 'yyyy-MM-dd hh24:mi:ss');
46     insert  into M_PROCEDUREEXECUTELOG(LOGTIME,SUCCESS,MSG) values(sysdate,'1','CorrectE_chkitemsByE_Feed执行成功,开始时间为'||timebegin||',结束时间为'||timeend||'。e_child.operatedate范围:'||to_char(P_StartTime,'yyyy-MM-dd')||'~'||to_char(P_EndTime,'yyyy-MM-dd')||'插入e_chkitems记录数目:'||to_char(Insert_E_Chkitems_Num));
47       commit;
48 
49 exception
50     when others then
51     begin
52     rollback;
53     insert into M_PROCEDUREEXECUTELOG(LOGTIME,SUCCESS,MSG) values(sysdate,'0','CorrectE_chkitemsByE_Feed执行失败。e_child.operatedate范围:'||to_char(P_StartTime,'yyyy-MM-dd')||'~'||to_char(P_EndTime,'yyyy-MM-dd'));
54     commit;
55     end;
56 end CorrectE_chkitemsByE_Feed;

 

M_PROCEDUREEXECUTELOG建表脚本【纠正日志表】
 1 -- Create table
 2 create table M_PROCEDUREEXECUTELOG
 3 (
 4   LOGTIME DATE,
 5   MSG     NVARCHAR2(1000),
 6   SUCCESS NVARCHAR2(1)
 7 )
 8 tablespace USERS
 9   pctfree 10
10   initrans 1
11   maxtrans 255
12   storage
13   (
14     initial 128K
15     minextents 1
16     maxextents unlimited
17   );
18 -- Add comments to the table 
19 comment on table M_PROCEDUREEXECUTELOG
20   is '记录存储过程执行情况 add by zzs 2010-01-13';

   在程序执行过程中,对插入数据及插程序执行的开始,结束时间进行了记载,程序运行到最后往日志表中插入成功执行的情况。在这个纠正存储过程中,设置了异常处理,如果插入发生异常【不是不可能的,还真出现过】则捕获之,在异常处理中进行回滚操作,记录插入失败的情况。其中记录业务数据的月份,可以对纠正成功失败的月份一目了然,以遍进一步进行纠正。

  【否对数据进行回滚】一直是让我比较左右为难的问题。一方面,回滚必然造成性能上的一部分缺失,并且如果真的在执行了大部分业务数据处理的后期发生错误,回滚也会消耗不少时间。回滚的好处自然不必多说,它实现了这个存储过程对指定时间内业务数据处理的原子性,权衡之后,我还是选择了进行回滚。

  在本地反复测试后,某个周五的晚上进行了纠正工作,这个纠正作业最后的执行情况在【存储过程执行日志表M_PROCEDUREEXECUTELOG】中可以完全查看的到:

  

  第二天打开看一下日志表记录,成功,而且执行的速度快的让我惊讶,感叹一下SUN的中型机性能就是不一般。。原本在本地测试,估算要大概2,3个小时的工作量,居然只用了17分钟。后来改了点逻辑,在周末白天执行【上次的17分钟给了我勇气:)】,也只用了40分钟。

  之后,用类似的方法基本上把这张统计相关的数据表业务上有问题的数据都纠正了一遍,没出过什么问题。当然,业务需求最好是一次做到位,出错后的补救及纠正是相当被动并且浪费人力物力的。

  希望我的处理方法能让读者产生一定的思考,对读者有所帮助,也希望觉得这样处理有不妥的兄弟姐妹提出更好的解决方案,共同进步 :)