随着计算机在银行业中的深入应用,许多银行业务,尤其核心业务的开展都以数据库为依托。数据库技术与银行业发展的联系越来越密切。目前大多数银行采用C/S系统架构,后端数据库服务器响应前端发起的各种交易,前端交易的信息最终以数据形式集中存放在后台数据库中。后台数据库服务器是银行日常联机交易的核心,其稳定可靠的运行、迅速的响应速度、较高的吞吐量以及24小时不间断运行,是为客户提供优质服务的前提,是银行业务正常发展的保证,也是提高自身竞争力的基础。由于业务需要,经常要对数据库关键表进行全表更新处理,如银行年度结息、批量扣收卡年费等,还要在效率、可靠性、并发及硬件资源之间权衡。在保证可靠性的前提下,充分利用硬件资源,尽可能不影响其他业务的正常运行,即最大化并发、高效率地完成更新操作。这就需要软件开发人员熟悉银行业务,充分应用数据库技术及编程技巧,开发出优质高效的应用软件,保证业务稳定持续发展。
  本文结合常见实例,分析探讨在银行联机事务中批量业务的不同实现方法。比较不同方法的利弊,从而确定适合联机事务环境批量处理的最佳方案,并对重点实施给出代码。
一、实例及要求
  某银行批量扣收银行卡年费业务,银行卡信息约有1000万条记录(下称“card_info)。
 1.card_info表记录的处理
  (1)对符合扣收条件且余额充足的记录进行扣收,将扣收成功的信息写入扣收清单card_succ表中,扣收失败的信息写入欠收清单card_fail表中,用于以后统计,同时更新card_info表信息。
  (2)不符合扣收条件的不做处理。
 3)符合扣收条件但余额不足的写入欠收清单card_fail表中,同时更新card_info表。
  (4)符合扣收条件的记录占80%以上。
  假设实例所用的应用服务器为HP-V260小型机,该机配置有8CPU8G内存,操作系统为HP Unix,数据库采用目前许多大行业广泛应用的Informix Dynamic Server Version 9.30.FC1
  2.要求
  在扣收处理过程中不停业,即系统不停止对其他联机交易业务的处理。
二、实例分析
  1.扣收时间的确定
  首先,要确定批量扣收处理是在白天还是在夜间进行。
  随着银行业的竞争日益激烈,各银行为吸引并方便客户,提供了各种服务渠道以满足客户
的不同需求,有正常营业时间的网点柜面交易,还有ATMPOS、网上银行、电话银行、手机银行等24小时不间断营业的自助服务。
  由于白天为银行营业高峰时间,计算机数据库系统为典型的联机事务处理,要求系统有较高的吞吐率及快速的响应时间。银行卡表为关键表,日间访问频繁,如对其进行大量的更新操作,势必影响其他业务的正常运行。毫无疑问,应当选择交易量较少的夜间进行批量扣收处理。
  2.单进程与多进程的确定
  要确定启用单进程还是多进程进行批量扣收处理。为了提高运行性能,应采用多进程并发处理,其原因有以下几点。
  (1)时间因素。银行后台系统在夜间一项重要的工作是日终结账。为保证账务的完整一致,其他批处理一般在日结前或日结后进行,即与日终结账串行处理。一般情况下,日终结账会耗时数小时,在保证日结正常处理结束后,用于其他批处理的时间将会有限。所以有限的扣收处理时间是一个重要因素。
  (2)系统资源因素。夜间客户自助发起的交易较少,在正常批处理完成后,CPU的空闲率通常在98%以上,内存也大量闲置。如果采用单进程对实例进行批量处理,必然存在着主机
资源闲置和应用程序执行缓慢的状况。为此应采用多进程并发模式进行批量扣收处理,充分利用系统资源,提高程序的执行速度,减少运行时间。
  (3)应用处理对象因素。如果应用需要数据库系统进行大量的数据库操作,如批量数据装载、海量数据的查询统计等,其主要瓶颈为I/O,启用多进程无助于性能改善。相反,如果应用存在大量计算处理,则采用多个进程并行处理可提高性能。本例中对每条记录的处理,均存在条件判断、余额积数计算、校验码运算等大量计算,故应采用多进程并行处理,以提高性能。
 4)开发环境因素。本实例采用HP Unix操作系统。该系统是一个多用户、多任务、交互式的分时操作系统,其开发环境支持子进程的创建,而且实现较为容易。
  3.进程个数的确定
  确定同时启动的子进程数目。启动多少个子进程,依赖于所运行主机的物理CPU个数、内存大小、系统已有负载及进程做什么类型的操作等因素。
  如果存在主机系统负载已很重、物理CPU少、内存小等情况,启动过多的进程将会造成进程间较多的上下文切换、内存的换出换进,无助于性能的改善,反而降低性能。反之,可启用相对多的进程以提高性能。
  对于本例,由于系统有数个物理CPU,同时夜间业务较少,系统的负载小,闲置内存多,故确定同时启动子进程数为物理CPU数减1
  4.启动进程次数的确定
  即确定进程的生命周期。进程由操作系统负责管理和调度,创建一个子进程较为耗时,代价较大,如果频繁创建、释放子进程会造成系统性能的下降。在本实例中,由于采用小事务,事务的数目远远大于子进程个数,如果采用每个子进程处理单个事务,势必造成频繁创建、释放子进程。为解决这一问题,我们采用创建子进程个数不变,每个子进程循环处理分配给其上的多个小事务,即最大化每个子进程的生命周期,以提高性能。
  5.并发控制
  开发数据库应用软件,首先应该保证数据的可靠性、容错性及一致性,其他因素,诸如性能等,在与其产生冲突时,必须要保证前者。
  采用多进程,事务间必定存在着并发,为保证数据的可靠性与一致性,就要使用有故障恢复功能的数据库及事务。事务的原子性保证了每个事务中的更新操作要么全部成功,要么全部回滚,避免在更新操作中因锁碰撞或其他异常情况,造成数据的不一致。Informix使用锁来控制并发,实现事务之间的隔离。采用锁进行并发控制,应考虑并确定锁的以下几个特
性。
  (1)锁粒度的确定
  锁粒度是指被封锁的对象范围大小,从小到大分为五级:键级、行级、页级、表级、数据库级,并发性依次降低,其中对整个数据库上锁使得并发性降为零。提高并发性是开发数据库应用软件应遵循的原则,尤其是银行应用软件。保证客户业务的顺畅运行,是银行业向客户的基本承诺。
  针对本实例,为提高各进程间并发操作,最大化降低扣收处理对其他业务的影响,应选择页锁或行锁。页锁封锁了整个页,该级锁定提供了一次封锁若干条记录的有效手段,但其他用户就不能访问已封锁页面上的所有数据了。页级锁降低了并发或数据对其他用户的可用性,但按表的物理顺序处理时,页级锁能用很少的锁完成对大量记录的变更。行锁在任意时刻只封锁一条记录,它提供了最高的并发,但当封锁记录数目很大时,有可能耗尽锁资源,且锁管理的额外开销也会变得十分显著。
  权衡页锁与行锁的利弊,考虑锁定要持续到事务结束才能解除,为尽量减少扣收处理期间对客户交易的影响,即保证最大并发性,本实例使用行级锁。同时为有效减少锁管理的额外开销,采用小事务(建议将数据库锁参数的配置尽量调大,Informix每个锁仅占44字节的内
存空间,锁的物理资源开销很小,由于锁资源不够而引起事务回滚是得不偿失的)。
  (2)事务大小的确定
  事务的大小是很重要的,它直接影响性能的优劣,同时也是较难精确确定的,依赖于用户的不同需求及所运行的环境。一般来说,太大的事务性能较差,原因如下。
  事务越大执行时间越长,被其阻塞的事务等待时间可能越长,并发事务锁碰撞错误返回的可能性越大;需要等待其他事务释放某个锁的可能性也越大,造成锁溢出的可能性也越大。
  Informix的数据库日志为所有事务所共享,如果多个事务存在大量更新操作,日志将被迅速填充,大的事务在结束前将长时间地占有使用过的日志,不能释放,从而引起长事务的可能性增大。所以太大的事务性能较差,一般情况下,需要将大的事务划分为较小的事务,以提高性能。
  当然也不能把事务划分得太小,太小的事务无助于性能的提高。当事务太小,如将实例的每个事务按单条或很少的记录处理,会造成频繁的I/O及其他重复开销。这是因为启动成本高,运行成本低。数据库系统中最昂贵的操作是磁盘I/O,数据库服务器进行大块磁盘操作比只处理数条记录要快,因为磁盘开始一个读操作是很耗时的,但是一旦开始,磁盘就能高速
地传输数据。
  因此,本实例划分事务大少的原则是,在与夜间交易发生碰撞冲突概率很低的情况下使用适当小的事务。
  6.表的访问方式
  即采用顺序扫描还是索引扫描方式访问表。使用索引,根本目的就是为了提高查询效率。索引扫描在以下情况下将会提高查询效率:大表的查询,且满足查询条件的记录数占全表的比例不超过30%,查询的数据仅为索引数据等。相反,对记录数很小的表,或从一个表选择几乎所有的行且包含有非索引字段,使用索引反而会降低效率。这是因为数据库服务器将不得不反复地参照索引来读取数据。尤其对于大表而言是致命的,因为I/O是很昂贵的操作。从一个磁道连续读64KB的数据,很可能比从该磁道两次读取512字节数据所花费的时间还短。因此,对读取同等数据量的数据,顺序扫描是最经济的。
  在本实例中,由于扣收处理的表为大表,且满足处理条件的记录占绝大多数,采用顺序扫描,将最小化I/O操作,提高性能。各子进程均采用顺序扫描的方式,每个子进程处理的数据按物理存储空间严格区分开,这样保证了各子进程间事务的完全隔离。子进程间碰撞的概率降为零,极大地方便了各子进程间的并发控制,提高了整体性能。同时采用顺序扫描,还可
用到数据库服务器的预读功能,即在顺序扫描期间提前将数据页从磁盘读入内存,可进一步提高性能。子进程、事务及相应记录如图1所示。图中记录顺序为物理顺序;k为进程个数,n为每个进程处理的事务个数,m为每个事务处理的记录数。
  从图1中可清楚地看到每个事务处理的记录从物理上严格分开,虽然每个进程的第一个事务与其前一个进程的最后一个事务所处理的记录可能存在于同一个磁盘页面上,但由于每个进程顺序处理其上的多个事务,从而使每个进程的第一个事务与前一进程的最后一个事务存在着时间差,故此实现了每个事务间的完全隔离。
  7.其他方面
  (1)建立日志跟踪机制
  事务的并发控制是本例的难点,也是保证数据一致性、可靠性的关键。本例扣收批处理的各事务由于从数据物理空间上完全隔离,从而保证了批处理事务间无碰撞,简化了事务的并发控制,但仍存在着扣收批处理子进程的事务与夜间客户交易事务的并发,虽然碰撞概率很小,但仍存在可能。为了对子进程处理结果进行有效跟踪及控制,我们将每个子进程的所有事务处理结果记录下来,为操作人员提供了实时监控处理结果的有效手段,实现友好的人机接口,同时方便开发人员对出错信息进行分析及优化,也为出错(如锁碰撞)后的断点再继
提供依据。
  实现这一功能,需要用到多进程间的通信,其方法有多种,如共享内存、共享文件、信号量、消息队列、数据库表等方法。本例中我们采用数据库表的方法,该方法容易实现,安全可靠,同时以数据库表的形式可长期存放,方便日后查询、分析。
  (2)父进程对子进程的控制
  由于进程是由操作系统管理及调度,当子进程出现异常,如僵死时,父进程需要迅速捕获该信息,同时中止并清理重置子进程。为此,采用父进程创建子进程后,将子进程号记录下来,父进程在等待子进程正常结束的时候,定时扫描子进程是否正常。
三、实例实施
 总结上节分析,我们得出对本例采用:多进程、小事务、顺序扫描银行卡表。
 在具体实施中,为使得程序流程清楚,易于维护,我们采用一些编程方面的技巧。以下对该实例的重点及实施难点做详细的论述。
  1.采用公共变量定义参数
  为使程序易于调试、维护及调优,并具有通用性,我们将一些关键参数定义为全局变量,并为日后移植打下基础。关键参数包括子进程个数、事务个数、每个事务处理的记录条数等。

      #define  PROCESS_NUM    10
 /**********子进程个数*********/
      #define      TRAN_REC  100    
  /***每个子进程处理的事务个数***/
      #define      REC_NUM      10000   
 /*****每个事务处理的记录数****/
  2.子进程的创建及处理
 代码如下:
 for(i=0;i<PROCESS_NUM;i++)
  {
   if((pid=fork())==0)
   { 
   sqldetach();
   $database DBNAME;
   flag=(*ks_func[proflag])(i) ;
   if(flag!=0)
 printf("出错!进程号=[%d] flag=[%d]",i,flag);
 $close database;
 exit(13);
 }
 }
 while((ppid=wait(&status))!=-1)
 ;
  创建子进程:父进程使用fork( )函数创建子进程,子进程被创建后就进入就绪队列并和父进程分别独立地等待调度。
  父进程与子进程同步:采用wait( )函数来控制父进程与子进程的同步。父进程调用wait( )函数后被阻塞,进入等待队列,等待子进程的结束。当父进程接收到子进程终止的信号后,从wait( )函数返回继续执行原来的程序。
  子进程结束:子进程结束后,使用exit( )函数返回,同时产生一个终止状态字,系统向父进
程发出SIGCHILD信号。
  使用函数指针数组ks_func[ ]( )来定义要执行的函数,提高代码执行效率,也为程序维护与移植提供方便。
  需要特别说明的是,使用函数fork( )创建的子进程并不能继承父进程的数据库连接,需要在每个子进程中重新连接需要操作的数据库。
  3.顺序扫描及事务分段的实施
  本例确定了各事务顺序处理相应记录的方案,那么如何确定每个事务处理的记录起止位置,才能使各事务负载均衡及保证各事务间完全隔离呢?我们知道,Informix数据库对未分片的常规数据库表的每条记录都以一个不变化的rowid为唯一标识。rowid定义了一个数据行的位置。在进行顺序扫描时,数据库服务器就是通过rowid所包含的信息确定所要查的数据行的位置,这也是最快的定位方式。rowid是一个4字节整数,它由3字节逻辑页号和1字节槽表表项组成。由于数据页的槽表从1开始(即不包含0),存在删除操作、表结构字段大小等因素,造成rowid并不连续,从而不能简单地从rowid值确定每个事务的起止位置。
  为保持各事务大小的均衡及各子进程负载的平衡,以及保证程序与数据分布无关,我们处理前对全表进行顺序扫描,对每个事务预先分段,并将结果作为控制表保存在数据库中。这
也为我们对子进程处理结果进行跟踪、并对出错断点再续提供可能。
  我们构建数据库表,命名为proc_ctrl,表结构如下:
  {
  proc_id      int    /**子进程序号**/
  tran_id        int    /**子进程事务序号**/
  start_rowid    int    /***事务的起始rowid***/
    end_rowid    int      /***事务的终止rowid***/
  rec_num      int      /***事务记录数****/
  bz            char    /***处理状态****/
  }
  预处理代码如下:
  s_rowid=e_rowid=0;   
/****记录起始rowid和终止rowid****/
 j=0;                    
 /****计数器,用于记录每个事务处理的记录数****/
 proc_n= 0;              /****子进程号***/
 proc_sub_n=1;            /****子进程事务序号****/
 k=1;                    /****记录进程数****/
  $declare sele_jbzh cursor for
  select rowid into row_id
 from card_info;
 $open    sele_jbzh;
 $fetch  sele_jbzh;
 s_rowid=row_id; 
 while(!sqlca.sqlcode)
 {
 if(j==REC_NUM)
 {
 $insert into proc_ctrl values 
 ($proc_n,$proc_sub_n,$s_rowid,$row_id,$j, 0 );
 j=0;
 s_rowid=row_id+1;
 k++;
 }
 if(k==TRAN_REC)
 {
 proc_n++;
 k=1;
 }
 j++;
  $fetch  sele_jbzh;
 }
      $insert into proc_ctrl values 
 ($proc_n,$proc_sub_n,$s_rowid,$row_id,$j, 0 );
 通过以上代码,将卡表的记录平均分配给每个进程、每个事务,并且与数据物理分布无关,
也不会受到磁盘碎片的影响,保证了事务间的完全隔离。
  需要说明的是,由于夜间不存在开卡、销卡交易,只有查询、转账、消费等交易,也就是卡表没有插入、删除操作,所以记录数及rowid值保持不变。为避免在预处理时碰到卡交易而发生并发冲突,在预处理时将隔离级设置为脏读。
  4.其他性能方面的考虑
  (1)使用插入游标
  对card_succcard_fail表进行插入时,采用插入游标(insert cursor)以提高性能。插入游标中的put语句使用了insert buffer,可以将多条insert语句先写入共享内存缓冲区中,当缓冲区满或提交事务时,大块写入数据库中。因为减少了写缓冲区的I/O次数,故性能得到提高。
进程间通信最快的方式  插入游标语法: 
  $declare cursor_name cursor for insert into tabnem(...) values (...); 
  $open cursor_name; 
  $put cursor_name; 
  或者,
  $prepare insert_name from “insert into tabname(...) values(...); 
  $declare cursor_name cursor for insert_name; 
  $open cursor_name; 
  $put cursor_name;
  (2)使用with hold声明游标
  在使用游标的操作中,open游标的操作是最耗时的,因为SQL语句的合法性检查、解析、语<分析、优化等都是在该步进行的。游标如果以缺省定义的方式打开,在事务结束后将被自动关闭。对本例数量较多的小事务而言,就存在着游标的频繁打开、自动关闭状况,性能必定会下降。在此我们不采用缺省定义方式,而是使用with hold声明游标,这样在子进程循环体内的每个小事务处理完成后,游标将不关闭。即每个子进程的所有事务,只有第一个事务存在游标的open操作,不再进行游标打开操作。
  语法如:$declare sel_cur cursor with hold for sele_statment;
  (3select语句使用列表选项
  使用select语句时,采用select col1,col2,...,coln  from tabname,只把需要的列显式选择出来,而不要采用select * from tabname。采用列表方式,可减少前后台之间的数据通信量,从而减少磁盘I/O,同时当访问的表结构发生变化但列表中的字段不变时,无需重新调试、编
译程序。
  (4prepare语句的使用 
  在SQL中使用prepare语句后可以根据应用程序提供的不同的values来执行多遍,而语法分析却仅执行一遍。当一个SQL语句在同一应用程序中被重复执行多遍时,用prepare的方法可以大大提高性能和效率。
  prepare语法:$prepare p_id from “insert into tabname (...) values (...) 
  (5)多进程并发写数据库表
  在该实例中,对符合年费扣收条件的银行卡记录,都要将处理成功或失败的信息记录下来。这就存在着多个进程同时写同一张表的数据库操作。理论上,无索引的数据库表并发插入记录时,不会出现锁冲突。所以对于card_succ表、card_fail表及前所述的控制表,在进行多进程并发插入操作时,应取掉其索引,待所有子进程处理结束后,由父进程再创建索引。
 
  依此方案开发的批量扣收银行卡年费软件在上线后,运行稳定、性能良好,不仅完成业务部门提出的需求,且在性能上超过预期目标。同时,该方案具有通用性,可用于银行的储蓄年度结息、日终结账等其他联机批量处理。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。