FORM的并发性问题与并发锁

来源:互联网 发布:少年包青天3知乎 编辑:程序博客网 时间:2024/05/18 03:47

开发人员开发FORM的过程中,自己在进行单元测试的时候,通常是打开会话窗口,按照流程一步步往下测试。测试无误后就交付给功能顾问。而功能顾问的测试也基本上一样。这样开发完成的FORM,看起来没有问题,但是却忽略了一个可能导致BUG的问题—并发。

       在实际的业务场景中,出现多个业务人员操作同一个FORM的情况是经常存在的。假设恰好两个人都在操作同一条数据,例如都在修改,或者一个人点审批,一个人点拒绝,如果我们不在FORM里面对这些情况进行考虑并加以控制,很可能就会出现问题。下面介绍几种比较容易出现的并发问题,供大家参考,在以后的开发过程中注意避免。

                    

并发问题

同一个数据块之间的并发

两个用户同时对一条记录进行修改,如果不对这条记录进行加锁的话,那么将会出现以下情况。如果用户A先输入并保存,用户B后输入并保存,系统会记录的是用户B的数据,用户A的数据就会丢失。如下面例子所示,这是在把ON-LOCK触发器里面的代码注释掉以后的情况。

1、同时打开两个物料成本定级,修改其中一条记录的厚度值,并保存,提示保存成功。

2、修改另外一个物料等级成本的厚度值字段,也保存成功。

                        

3、两个FORM都重新查询,最后保存的是后面修改的数据,前面修改的数据丢失。

解决方法:要避免出现这种情况,就要在FORM的ON-LOCK触发器里面添加代码控制。项目上一般都会有hand_plsql_autocreate这个包自动生成代码。它的作用是锁表以及验证界面状态是否与数据库状态一致。它的原理是如果另外一个用户在修改数据但未保存,就会触发FOR UPDATE NOWAIT语句把表锁住并提示无法保存,是否等待。如果另外一个用户已修改完毕并保存了,就会检验数据库状态与界面状态是否一致。大家可以参考一下其生成的代码,看看它是怎么防止并发问题发生的。

头行关系数据块的并发

我们先看一个例子,这是我现在项目同事开发的一个FORM—采购订单。现在的业务逻辑是已提交的采购订单不允许修改。当打开一个会话窗口测试,逻辑当然是没有问题的。但如果有多个用户同时操作,就会出现并发问题。

1、首先我们打开两个采购订单界面。

2、提交其中一个采购订单,提交成功以后,状态变成已提交,当前界面变灰,不允许修改,测试通过。

                         

       3、这个时候,再导航到后面已经打开了的FORM,状态是审批退回,界面还是白的,可以修改,把其实际单价修改成2000。

可以看到,修改已经成功了。这会导致的后果是,明明已经提交了的采购订单,还是可以任意修改,甚至可以增删改,造成业务上潜在的风险。

我们再看看标准功能的采购订单是怎么做的。

       步骤跟刚才一样,先打开两个FORM,审批其中一个采购订单,点击到另外一个采购订单,修改其行上的价格,报出如下错误信息:

      

       重新查询以后就会发现这个采购订单已经被审批过了,不能再修改。

       那标准功能是怎么防止并发问题发生的呢?通过查看其FORM里面的代码,发现它是在行BLOCK的PRE-INSERT,PRE-UPDATE,PRE-DELETE触发器里面添加了头块的界面数据与数据库数据是否一致的检查。当数据不一致的时候报错,不让继续做下去。

       我们在开发有头行关系的FORM的时候,一般是直接通过头上的界面上的状态去控制行的增删改。这样做的好处一是简单,二是直观,但是也有弊端。就像我们刚才客户化的那个FORM,如果不加验证,就会出现流程性的BUG,造成不可预知的影响。这个并发问题是比较经常出现的,造成的影响也是比较大的,开发的时候必须多加注意。

       解决方法:视情况在行块的PRE-INSERT,PRE-UPDATE,PRE-DELETE触发器里面加上头块界面数据与数据库数据是否一致的验证。具体来说,如果是根据状态控制的,就去验证界面上的状态与数据库的状态是否一致,不一致则让用户重新查询数据块,便可避免这种错误。

还有一种方法,就是在行的ON-LOCK触发器把头也一起锁定,用户在修改行的时候就会同时校验头是否被锁定,报出提示。要注意的是,不能贪方便直接用hand_plsql_autocreate包生成的头的ON-LOCK触发器,而应该另外写FOR UPDATE NOWAIT去锁表。因为自动生成的包里面还会有界面数据与数据库状态是否一致的检验,如果直接在行上的ON-LOCK触发器调用的话,就算只打开一个界面,先修改头数据再修改行数据,这时候就会提示数据块已更改,因为头上数据已经发生变化,相当于自己把自己给锁了。

按钮的并发

       控制业务流程与数据流向,我们一般通过FORM上的按钮去实现。例如一个装箱单,从创建到提交审批,到审批,到拒绝。都是通过一系列的按钮操作调用API去实现的。跟头行关系控制一样,很多时候我们都是通过界面上的状态去控制按钮的可用与否。例如对于一个箱头来说,已经提交过的箱头理论上就不应该再让其点击提交这些按钮了(如图)。由于按钮的可用与否是根据该行数据界面上的状态去控制的,于是并发问题又出现了。当某个用户看到了这个箱,感觉信息正确然后点击审批按钮并且审批通过了,状态变成了已审批。如果刚好这时候另外一个领导看到了这个箱,发现有错误点击拒绝审批,会出现什么情况呢?

       这时候,千万不能仅验证界面上的状态了。正确的做法应该是通过验证该箱头在数据库的状态,提示该箱头的状态不为已提交,不能拒绝审批。但是这样会有一个问题,就是用户明明看到界面上的状态是已提交,但是提示的消息却说的是状态不为已提交,会让用户感觉到疑惑。个人感觉比较合理的处理方法应该是这样的:

1、 打开两个装箱单,找到一个已提交的箱。

 

 

 

 

 

 

 

 

 

 


2、 勾选记录后点击审批,审批成功,状态变为已审批,另外一个界面上的状态还是已提交。

 

 

 

 

 

 

 

 

 

 


3、 跳到另外一个界面,点击拒绝,这时候报出提示状态已更改。

 

 

 

 

 

 

 

 

 


4、 重新查询后就可以发现其实数据已经被更改,状态变成了已审批,拒绝按钮也被灰掉了。

 

 

 

 

 

 

 

 

 

 


解决方法:在调用API处理按钮的时候,把界面上的状态也作为参数一起传进去。验证的时候首先验证界面状态与数据库状态是否一致,若不一致,报错提示用户数据已更改,重新查询。如果一致再验证该箱头的数据库状态是否符合要求。

上下游数据变动所引发的并发问题

所谓上下游数据变动,举例来说,就比如一个FORM的某个LOV取值取决于它的上游的数据的状态。例如一个数据基础设置的FORM,它一般会有效标志跟有效日期。当别的FORM想从这个基础数据表里面取值的时候,它会判断其是否有效,在LOV里面过滤,把无效值去掉。如果刚好用户A选完这个值以后,有其他用户把它失效掉了。但是由于值已经选上了,用户A如果一直输入其他值直到保存,是不会报错的。相当于一条无效的数据被保存了。

解决方法:从程序的严谨性角度来看在往下进行事务处理操作的时候应该增加对该条数据的有效性验证,也就是说,保证在做该事务处理的节点时间的该行数据是有效的。

总结

FORM的并发性问题大多是由于锁表不当或者是界面与数据库的数据不一致所导致的。我们在开发和测试的时候一定要多去考虑并发性问题。在用界面状态去控制流程的时候,一定要增加界面数据与数据库数据是否一致的判断。总之,眼见不一定为实。

                    

API控制并发锁

       并发是指多个人在操作同一条数据,并发锁则是限定只有一个人能操作这条数据。在API里面处理事务的时候,里面一般都会涉及到一些UPDATE,DELETE的操作。我们需要对被操作数据进行加锁,指定这条数据已被占有,不允许其他人对这条数据进行操作,由于数据锁定及时,可以避免后续的并发的冲突处理。我们一般惯用的做法是,在事务处理开始的时候为这条数据加一个行级的“悲观锁”,也就是假定它会出现并发,先把它锁定,等做完之后的验证,更新等操作以后再将锁释放。

添加悲观锁

       为数据加“悲观锁”主要有两种方式,从SQL的角度来说,一种是FOR UPDATE,一种是FOR UPDATE NOWAIT,这两种方法都可以为数据加锁。不同的是FOR UPDATE如果检测到数据已被锁定,它会等待这条数据的事务做完,而且途中不报任何错误。而FOR UPDATE NOWAIT检测到数据已被锁定以后,会立刻返回错误信息“ORA-00054错误,资源正忙,但指定以NOWAIT方式去获取”。从我们API处理的角度来看,肯定是用FOR UPDATE NOWAIT去锁数据比较好,因为用户可以第一时间知道此记录已经被锁定,而不是在傻傻地等待。

   这种方法用的是行级锁,只是对想要锁定的的数据才进行加锁,所以在并发插数据时候,基本上不会有影响。但它也有缺点,从整个锁定到最后解锁的过程中,必须一直与数据库进行连接(个人感觉这也很正常)。

下面通过一个例子去介绍如何在API中控制并发锁。

实例

在程序包主体,先定义一个全局变量EXCEPTION e_row_is_locked代表ORA-00054例外。

e_row_is_lockedEXCEPTION;

PRAGMAEXCEPTION_INIT(e_row_is_locked,-54);

 

在事务处理的主程序里,定义一个游标,该游标就是将要被锁定的数据,游标的最后加上 FORUPDATE NOWAIT 去锁定该行数据。

 

CURSOR c_trxn(p_wip_job_idNUMBER,

                  p_md_trxn_id NUMBER)IS

     SELECT mdt.md_transaction_id,

             mdt.lot_number,

             mdt.primary_quantity,

             jms.reserved_flag,

             jms.issued_flag,

             msi.segment1 AS item_number,

             mdt.organization_id,

             mdt.inventory_item_id

       FROM cux_wip_material_demand_trxn mdt,

             mtl_system_items_b           msi,

             cux_wip_job_md_trxn_status   jms

      WHERE msi.organization_id =mdt.organization_id

        AND msi.inventory_item_id =mdt.inventory_item_id

        AND jms.md_transaction_id(+) =mdt.md_transaction_id

        AND mdt.transaction_source_type ='WJ'--来源类型:加工单

        AND mdt.transaction_action =1--事务活动类型:领料

            --parameters:

        AND mdt.trxn_source_header_id = p_wip_job_id

        AND mdt.md_transaction_id = p_md_trxn_id

        FORUPDATEOF mdt.md_transaction_idNOWAIT;

 

在做事务处理之前,首先锁定该记录。

 

BEGIN

        OPEN c_trxn(l_wip_job_id,

                    l_md_transaction_id);

      EXCEPTION

        WHEN e_row_is_lockedTHEN

          cux_api.set_message(p_app_name     => 'FND',

                              p_msg_name     => 'FND_LOCK_RECORD_ERROR',

--无法锁定此记录。原因:另一用户正在修改此记录。

                              p_token1       => 'TableName',

                          p_token1_value =>cux_public_utl.get_table_comment('CUX',                                                                           'CUX_WIP_MATERIAL_DEMAND_TRXN'));

RAISE fnd_api.g_exc_error;

END;

   

        FETCH c_trxn

            INTO rec_txn;

        IF c_trxn%NOTFOUNDTHEN

            CLOSE c_trxn;

            cux_api.set_message(p_app_name=> 'FND',

                            p_msg_name => 'FORM_RECORD_DELETED');--已更改记录或由另一用户删除。

            RAISE fnd_api.g_exc_error;

        ENDIF;

        CLOSE c_trxn;

 

    没有报出异常的话,就可以继续往下做验证,事务处理等操作了。不管成功与否,最后这条数据都会被自动解锁。这样就能保证在整个事务处理的过程中都只有自己在操作这条数据,或者说被人在操作的时候自己不会同时进行操作,保证了数据的安全性。

0 0
原创粉丝点击