我们知道给资源上锁可以使我们串行化地访问资源,oracle
为plsql
开发人员提供了DBMS_SQL
包用来管理USERL LOCK
锁资源。这种锁可以使得多个session
串行的执行某个存储过程,还可以用来排他的访问某个外部设备或服务,甚至可以检测事务的提交或回滚(提交或回滚时锁的释放)。
有人说我在java端调用db的存储过程,可以使用synchronized lock来串行的调用存储过程。那就不需要db lock呢?因为当java端应用服务器down的时候,存储过程已经在执行了,但是可能oracle session(RAC的db)并没有立即释放掉。当我们重启应用服务器后,其实后台的以前的存储过程还在执行,如果再次调用存储过程,这就无法保证存储过程的串行执行了。所以说存储过程的同步锁是必须放在Oracle db端的。
DBMS_LOCK包具有下面几个API,主要说明以下几个,其他的可以参考oracle相应文档,我们这里只用X锁(排他锁也称写锁)。
 
 PROCEDURE DBMS_LOCK.ALLOCATE_UNIQUE
PROCEDURE DBMS_LOCK.ALLOCATE_UNIQUE

 (lockname IN VARCHAR2
    (lockname IN VARCHAR2

 ,lockhandle OUT VARCHAR2
    ,lockhandle OUT VARCHAR2

 ,expiration_secs IN INTEGER DEFAULT 864000);
    ,expiration_secs IN INTEGER DEFAULT 864000); 
    
        
            | 参数 | 描述 | 
    
    
        
            | lockname | 锁的名称 | 
        
            | lockhandle | 与该名称相对应的锁的唯一标识 | 
        
            | expiration_secs | 这种名称到锁的映射的保存时间 | 
    
当多个session用同样的名字lockname来获取唯一标识字符串时,不同的session用同样名字获取的lockhandle是相同的,lockname是最大128位的字符串,而且是大小写敏感的,锁的名字最好不要用"ORA$"打头,这种锁的名称是被oracle保留的名称。DBMS_LOCK.ALLOCATE_UNIQUE执行完后就会commit所以不能被trigger调用。所有获得的映射都为存放在SYS用户DBMS_LOCK_ALLOCATED视图中。
 FUNCTION DBMS_LOCK.REQUEST
FUNCTION DBMS_LOCK.REQUEST
 (id IN INTEGER
    (id IN INTEGER
 ,lockmode IN INTEGER DEFAULT X_MODE
    ,lockmode IN INTEGER DEFAULT X_MODE
 ,timeout IN INTEGER DEFAULT MAXWAIT
    ,timeout IN INTEGER DEFAULT MAXWAIT
 ,release_on_commit IN BOOLEAN DEFAULT FALSE)
    ,release_on_commit IN BOOLEAN DEFAULT FALSE)
 RETURN INTEGER;
RETURN INTEGER;
 
 FUNCTION DBMS_LOCK.REQUEST
FUNCTION DBMS_LOCK.REQUEST
 (lockhandle IN VARCHAR2
    (lockhandle IN VARCHAR2
 ,lockmode IN INTEGER DEFAULT X_MODE
    ,lockmode IN INTEGER DEFAULT X_MODE
 ,timeout IN INTEGER DEFAULT MAXWAIT
    ,timeout IN INTEGER DEFAULT MAXWAIT
 ,release_on_commit IN BOOLEAN DEFAULT FALSE)
    ,release_on_commit IN BOOLEAN DEFAULT FALSE)
 RETURN INTEGER;
RETURN INTEGER;

 
    
        
            | 参数 | 描述 | 
    
    
        
            | id | 锁的唯一标识 | 
        
            | lockhandle | 由DBMS_LOCK.ALLOCATE_UNIQUE返回的handle | 
        
            | lockmode | 锁的模式 | 
        
            | timeout | 等待时间 | 
        
            | release_on_commit | COMMIT or ROLLBACK事务时是否释放锁 | 
    
    
        
            | 返回值 | 描述 | 
    
    
        
            | 0 | 成功申请到锁 | 
        
            | 1 | 超时 | 
        
            | 2 | 死锁 | 
        
            | 3 | 参数错误 | 
        
            | 4 | 已经拥有特定id或handle的锁 | 
        
            | 5 | 不合法的lockhandle | 
    
用户定义的锁标识必须在 0 到 1073741823. 锁标识在范围2000000000 到 2147483647 被oracle公司预先保留。推荐用lockhandle的方法获得锁,因为锁的名称是比较容易辨别的,也是比较容易描述的。第一种方法不被oracle推荐。
在共享服务器模式和分布式事务时我们最好把release_on_commit设置为true。
 FUNCTION DBMS_LOCK.RELEASE
FUNCTION DBMS_LOCK.RELEASE
 (id IN INTEGER)
    (id IN INTEGER)
 RETURN INTEGER;
RETURN INTEGER;
 FUNCTION DBMS_LOCK.RELEASE
FUNCTION DBMS_LOCK.RELEASE
 (lockhandle IN VARCHAR2)
    (lockhandle IN VARCHAR2)
 RETURN INTEGER;
RETURN INTEGER;
    
        
            | 参数 | 描述 | 
    
    
        
            | id | 锁的数字标识 | 
        
            | lockhandle | ALLOCATE_UNIQUE返回的锁的handle | 
    
    
        
            | 返回值 | 描述 | 
    
    
        
            | 0 | 成功 | 
        
            | 3 | 参数错误 | 
        
            | 4 | 并没有拥有特定的锁 | 
        
            | 5 | 不合法的lockhandle | 
    
RELEASE 函数用来释放先前申请的锁。当锁不用时最好立即释放,这是很好的习惯。锁本身就是宝贵的资源,并且可以尽早释放被锁住的资源,而且可以有效地避免死锁。
如何使用这些api,很容易只要在我们的存储过程之前或者之后调用申请锁,释放锁(或者在事务提交或rollback的时候自动释放锁)就可以了,但这样也带来了存储过程代码的侵入性,每个存储过程都必须调用申请锁,释放锁。我们可以写一个wrapper把锁的申请和释放包裹起来。类似于模板模式。
 create or replace package FRM_TEST_TESTING is
create or replace package FRM_TEST_TESTING is
 PROCEDURE loop4_specific_round;
     PROCEDURE loop4_specific_round;

 end FRM_TEST_TESTING;
end FRM_TEST_TESTING;

 create or replace package body FRM_TEST_TESTING is
create or replace package body FRM_TEST_TESTING is

 -- Function and procedure implementations
  -- Function and procedure implementations
 --PROCEDURE loop4_specific_round(p_loop_count IN INTEGER) AS
  --PROCEDURE loop4_specific_round(p_loop_count IN INTEGER) AS
 PROCEDURE loop4_specific_round AS
  PROCEDURE loop4_specific_round AS
 
  
 PRAGMA AUTONOMOUS_TRANSACTION;
  PRAGMA AUTONOMOUS_TRANSACTION;
 
  
 BEGIN
  BEGIN   
 FOR r IN 1 .. 60 LOOP
    FOR r IN 1 .. 60 LOOP
 SYS.dbms_lock.sleep(20);
      SYS.dbms_lock.sleep(20);
 
      
 DBMS_OUTPUT.PUT_LINE('During testing SP executing. '|| to_char(sysdate,'MM/DD/YYYY HH24:MI:SS'));
      DBMS_OUTPUT.PUT_LINE('During testing SP executing. '|| to_char(sysdate,'MM/DD/YYYY HH24:MI:SS'));
 insert into TEST_TEST
      insert into TEST_TEST
 (name, Creationtime)
        (name, Creationtime)
 values
      values
 ('1111', sysdate);
        ('1111', sysdate);  
 
      
 commit;
    commit;
 END LOOP;
    END LOOP;
 
   
 
    
 END loop4_specific_round;
  END loop4_specific_round;
 end FRM_TEST_TESTING;
end FRM_TEST_TESTING;

 create or replace package frm_test_task_pkg is
create or replace package frm_test_task_pkg is
 
 
 function  frm_test_task_func(i_lock_name in varchar2, i_procname in varchar2, i_expiration_time Integer default 864000, i_wait_time Integer default DBMS_LOCK.maxwait) return number;
  function  frm_test_task_func(i_lock_name in varchar2, i_procname in varchar2, i_expiration_time Integer default 864000, i_wait_time Integer default DBMS_LOCK.maxwait) return number;

 end frm_test_task_pkg;
end frm_test_task_pkg;


 create or replace package body frm_test_task_pkg is
create or replace package body frm_test_task_pkg is

 procedure app_task_wrapper_proc(i_procname in varchar2) as
  procedure app_task_wrapper_proc(i_procname in varchar2) as
 
  
 PRAGMA AUTONOMOUS_TRANSACTION;
  PRAGMA AUTONOMOUS_TRANSACTION;  
 cur BINARY_INTEGER := DBMS_SQL.OPEN_CURSOR;
   cur BINARY_INTEGER := DBMS_SQL.OPEN_CURSOR;
 fdbk BINARY_INTEGER;
   fdbk BINARY_INTEGER; 
 begin
  begin
 DBMS_SQL.PARSE (cur,
     DBMS_SQL.PARSE (cur, 
 'begin ' || i_procname  || ';end;',
      'begin ' || i_procname  || ';end;',
 DBMS_SQL.NATIVE);
     DBMS_SQL.NATIVE);
 fdbk := DBMS_SQL.execute (cur);
     fdbk := DBMS_SQL.execute (cur);
 DBMS_OUTPUT.put_line('Fetch rows : ' || fdbk);
     DBMS_OUTPUT.put_line('Fetch rows : ' || fdbk);
 DBMS_SQL.close_cursor(cur);
     DBMS_SQL.close_cursor(cur);
 commit;
     commit;
 end app_task_wrapper_proc;
  end app_task_wrapper_proc;
 
  
 function  frm_test_task_func(i_lock_name in varchar2, i_procname in varchar2, i_expiration_time Integer default 864000, i_wait_time Integer default DBMS_LOCK.maxwait) return number is
  function  frm_test_task_func(i_lock_name in varchar2, i_procname in varchar2, i_expiration_time Integer default 864000, i_wait_time Integer default DBMS_LOCK.maxwait) return number is
 v_result     number;
      v_result     number;
 v_lockhandle varchar2(200);
      v_lockhandle varchar2(200);
 v_sid   pls_integer;
      v_sid   pls_integer;       
 begin
   begin
 dbms_lock.allocate_unique(i_lock_name, v_lockhandle, i_expiration_time);
    dbms_lock.allocate_unique(i_lock_name, v_lockhandle, i_expiration_time);

 v_result := dbms_lock.request(v_lockhandle, dbms_lock.x_mode, i_wait_time, true);
    v_result := dbms_lock.request(v_lockhandle, dbms_lock.x_mode, i_wait_time, true);
 
    
 select sys_context('USERENV','SID') into v_sid from dual;
    select sys_context('USERENV','SID') into v_sid from dual;
 
    
 dbms_output.put_line (to_char(systimestamp,'HH24:MI:SS.FF3')||': SID '||v_sid||' requests lock');
    dbms_output.put_line (to_char(systimestamp,'HH24:MI:SS.FF3')||': SID '||v_sid||' requests lock');

 if v_result <> 0 then
    if v_result <> 0 then
 dbms_output.put_line(
    dbms_output.put_line(
 case
           case 
 when v_result=1 then 'Timeout'
              when v_result=1 then 'Timeout'
 when v_result=2 then 'Deadlock'
              when v_result=2 then 'Deadlock'
 when v_result=3 then 'Parameter Error'
              when v_result=3 then 'Parameter Error'
 when v_result=4 then 'Already owned'
              when v_result=4 then 'Already owned'
 when v_result=5 then 'Illegal Lock Handle'
              when v_result=5 then 'Illegal Lock Handle'
 end);
            end);
 else
     else 
 app_task_wrapper_proc(i_procname);
          app_task_wrapper_proc(i_procname);
 end if;
  end if;
 commit;
     commit;
 return v_result;
     return v_result;
 EXCEPTION
  EXCEPTION
 WHEN OTHERS
   WHEN OTHERS
 THEN
   THEN

 /**//*
      /**//*
 || Anonymous block inside the exception handler lets me declare
      || Anonymous block inside the exception handler lets me declare
 || local variables to hold the error code information.
      || local variables to hold the error code information.
 */
      */
 DECLARE
      DECLARE
 error_code NUMBER := SQLCODE;
         error_code NUMBER := SQLCODE;
 error_msg  VARCHAR2 (300) := SQLERRM;
         error_msg  VARCHAR2 (300) := SQLERRM;
 BEGIN
      BEGIN
 DBMS_OUTPUT.put_line(error_code || ': ' ||error_msg);
           DBMS_OUTPUT.put_line(error_code || ': ' ||error_msg);
 --RE RAISE ERROR TO CLIENT
           --RE RAISE ERROR TO CLIENT
 raise;
           raise;
 END; -- End of anonymous block.
      END; -- End of anonymous block.

 end frm_test_task_func;
  end frm_test_task_func;

 end frm_test_task_pkg;
end frm_test_task_pkg;

在java端,应用程序只需要调用frm_test_task_func,把需要串行化的存储过程作为参数传入。Java端还需要提供db锁的逻辑名,这样同步在相同的逻辑名的锁上的存储过程会同步执行。在实际存储过程参数比较复杂的情况下,传参可能是个问题。
其实还有一种方法,对已有的存储过程代码没有侵入性,申请和释放db锁在java端完成,oracle保证了session断掉后,会释放session占有的锁资源。所以如果应用服务器down掉后,session在经过一段时间后会被释放,锁资源也会被释放。现在要考虑的是是否在事务结束的时候自动释放锁,考虑到现在申请锁是由java端完成的,所以释放锁也由java端显式的调用release释放。
 create or replace package body frm_test_task_pkg is
create or replace package body frm_test_task_pkg is

 procedure frm_test_lock_acquire(i_lock_name in varchar2, i_expiration_time in Integer default 864000, i_wait_time in Integer default DBMS_LOCK.maxwait, o_result out number, o_lockhandle out varchar2) as
 procedure frm_test_lock_acquire(i_lock_name in varchar2, i_expiration_time in Integer default 864000, i_wait_time in Integer default DBMS_LOCK.maxwait, o_result out number, o_lockhandle out varchar2) as
 v_result     number;
      v_result     number;
 v_lockhandle varchar2(200);
      v_lockhandle varchar2(200);
 begin
   begin
 --acquire a unique lock id
    --acquire a unique lock id
 sys.dbms_lock.allocate_unique(i_lock_name, v_lockhandle, i_expiration_time);
    sys.dbms_lock.allocate_unique(i_lock_name, v_lockhandle, i_expiration_time);
 
    
 --acquire a lock
    --acquire a lock
 v_result := sys.dbms_lock.request(v_lockhandle, dbms_lock.x_mode, i_wait_time, false);
    v_result := sys.dbms_lock.request(v_lockhandle, dbms_lock.x_mode, i_wait_time, false);
 
  
 --set return values
    --set return values
 o_result := v_result;
     o_result := v_result;
 o_lockhandle := v_lockhandle;
     o_lockhandle := v_lockhandle;
 end frm_test_lock_acquire;
 end frm_test_lock_acquire;
 
 
 function frm_test_lock_release(i_lockhandle in varchar2) return number as
 function frm_test_lock_release(i_lockhandle in varchar2) return number as
 v_result number;
       v_result number;
 begin
 begin
 --release lock according to lockhandle
      --release lock according to lockhandle
 v_result := sys.dbms_lock.release(i_lockhandle);
      v_result := sys.dbms_lock.release(i_lockhandle);
 
 
 return v_result;
      return v_result;
 end frm_test_lock_release;
 end frm_test_lock_release;

 end frm_test_task_pkg;
end frm_test_task_pkg;

  
这样java端需要先调用frm_test_lock_acquire申请锁,然后执行用户逻辑的存储过程,最后在显式的调用frm_test_lock_release释放锁。如果不想每次用到的时候都去申请,释放锁,在java端也可以使用模板模式,假设子类实现execute方法来完成需要串行化执行的存储过程,这时要注意我们可能会将connection传入到子类的execute方法中,但是子类却不能将connection关闭掉,因为我们还需要在execute方法执行完后会用它来释放锁。当然如果子类真的把物理的connection关闭掉也没有问题,但是现在我们大都使用connection pool,把connection返回给pool的时候,session的锁资源并没有清除。这样还需要将传入到子类execute方法的connection封装一下,或者叫装饰一下,我们有两种解决方法:
·         oonnection wrapper的close并不关闭连接或者返回给pool。
·         connection wrapper的close方法关闭连接,但是需要在关闭连接之前释放锁,然后在抽象父类的方法中只需判断connection是否close掉,connection.isClosed()方法,如果没有close就调用wrapeer的close方法,既释放锁又关闭连接。
 
总结一下:lock放在java端会出现意外的情况,锁就必须放在db端,为了避免对已有存储过程代码的侵入性,可以使用wrapper存储过程,由于动态执行存储过程传递参数是个难题,所以还是把锁的申请,释放放在了java端,也避免了对已有存储过程代码的侵入性,同时又懒得每次都去申请锁,释放锁,可以在java端使用模板模式,但是使用模板模式时又怕子类不小心关了connection,而没释放锁,就将connection封装了一下,将这个被封装后的connection传到了子类的execute方法中。