oracle java 分布式事务

来源:互联网 发布:淘宝流量劫持 编辑:程序博客网 时间:2024/05/29 08:16

背景

应用项目组每个小时会定时的run一个存储过程进行结算,每次执行的时间也许会超过一个小时,而且需要绝对保证存储过程的串行执行。因为使用内存锁不能绝对保证两个存储过程的串行执行,因为应用服务器down掉重启后可能会出现并发执行的情况,因为先前的存储过程还在db中运行。我们是使用LTS,对quartz进行了封装来做任务调度的。我们决定锁的管理操作由framework来实现。原因是:

l         锁管理器可以做成通用的模块

l         申请锁,释放锁是比较危险的操作,担心业务开发人员由于遗忘导致死锁或者并发问题

l         可以很好的集成到我们现有的framework中,方便地开放给业务开发人员使用

注意:我们极其不推荐使用悲观离线锁,如果冲突出现的概率比较少,可以用其他方法比如乐观离线锁,DB Constraint再通过补偿操作能解决的问题,请不要使用悲观离线锁。

 

原理

PLSQL UL LOCKoracle提供出来给开发人员使用的锁资源,功能和DML锁是类似的,当然我们可以通过DML锁来完成并发控制,select…for update或者自己维护一张锁表,考虑到实现代价,我们打算使用PLSQL UL LOCK。而且Oracle保证了session释放时,UL lock都会被释放。

但是用它时,需要注意到它的DBMS_LOCK.Unique函数它每次都会commit数据。如果是在分布式事务当中,会抛出事务已提交的异常。因为我们使用的是XA resource并且transaction levelglobal的,也就是JTA。为了使得锁的申请和释放不影响分布式业务事务,或者我们是使用非xaresourcelocaltransaction来完成锁操作,或者也可以暂停已有事务,等锁操作完成后resume暂停的分布式事务。考虑到重用已有的xa resource我们打算使用后一种方法,其实这种方法我们也会经常使用,暂停分布式事务做DDL操作,再释放事务。

 

实现方法:

l         封装DBMS_LOCK包中的几个存储过程为我们所用

l         Java端提供一个基于PLSQL UL锁的管理器

l         Java端定义好申请锁,业务操作,释放锁的使用流程,作为一个模板

 

DB存储过程:

DBMS_LOCK做了简单的封装,避免直接调用DBMS_LOCK。这样做的好处是:

l         Oracle解耦,如果其他数据库可以提供类似的功能,我们也可以用同名的存储过程实现

l         方便以后对存储过程重构,升级

l         我们需要对DBMS_LOCK进行简单的封装,因为DBMS_LOCK.Unique获取lockhandle oracle中锁的唯一标识,输入是lockname,逻辑名,输出是锁的唯一标识,对java端应该是透明的,java端应该只关心锁的逻辑名。

create or replace package body frm_lts_processor_lock_pkg is
  
/* table to store lockhandles by name */
   TYPE handle_tbltype IS TABLE OF varchar2(
128)
      INDEX BY varchar2(
128);

   v_lockhandle_tbl handle_tbltype;
  
 procedure frm_lts_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) as
      v_result     number;
      v_lockhandle varchar2(
128);
   begin
   if v_lockhandle_tbl.count =
0 then
      sys.dbms_lock.allocate_unique(i_lock_name, v_lockhandle, i_expiration_time);
      v_lockhandle_tbl(i_lock_name) := v_lockhandle;
   elsif v_lockhandle_tbl.exists(i_lock_name) then
      dbms_output.put_line(
'atttacked');
      v_lockhandle := v_lockhandle_tbl(i_lock_name);
   else
     dbms_output.put_line(
'new acquire');
     
--acquire a unique lock id
     sys.dbms_lock.allocate_unique(i_lock_name, v_lockhandle, i_expiration_time);
     v_lockhandle_tbl(i_lock_name) := v_lockhandle;
       
   end if;
   
--acquire a lock
    v_result := sys.dbms_lock.request(v_lockhandle, dbms_lock.x_mode, i_wait_time, false);
 
   
--set return values
     o_result := v_result;
 end frm_lts_lock_acquire;
 
 function frm_lts_lock_release(i_lock_name in varchar2) return number as
       v_result number :=
6;
       v_lockhandle varchar2(
128);
 begin
     
--release lock according to lockhandle
      if v_lockhandle_tbl.exists(i_lock_name) then
          v_lockhandle :=  v_lockhandle_tbl(i_lock_name);
          v_result := sys.dbms_lock.release(v_lockhandle);
          v_lockhandle_tbl.delete(i_lock_name);       
      end if;
      return v_result;
 end frm_lts_lock_release;

end frm_lts_processor_lock_pkg;
/

 

 

锁管理器:

其实应用项目组有多个这样的存储过程,而这些存储过程之间的串行执行可以有多个business key来决定的,比如job order numberdelivery order等。所以我们需要给他们提供多锁管理机制。我们会对这多个锁进行排序,以避免死锁,并强烈推荐应用项目设置超时时间。这些business key是由String对象构成的,为了防止大量的业务操作被锁在null或者空string这样没有意义的business key上面,我们对application提供的锁集合还需要进行过滤。

原理还是很简单的,就是在本地事务中调用db端的申请锁,释放锁的存储过程,然后对返回的结果进行一系列处理。

在使用多锁机制的时候要保证,如果只申请到了部分锁,在申请其中另外一个锁时发生了错误或者超时,要能够安全地将已申请的锁释放掉,所以多锁申请需要记录已申请到的锁,并且记录发生的错误,区分timeout和异常。Timeout返回false,如果出现异常记录下来,最后抛出。释放多锁时,不能被中断,记录释放每个锁后的结果,最后判定如果其中一些锁释放时发生了错误,抛出。

 

handleLock定义暂停jta事务,执行锁操作,释放jta事务流程

 

Java代码 复制代码
  1. private Object handleLock(Connection connection,   
  2.             LocalTransactionCallback localTransactionCallback)   
  3.             throws LockException {   
  4.         TransactionManager tm = null;   
  5.         Transaction currentTx = null;   
  6.         Object result = null;   
  7.         try {   
  8.             Context initialContext = new InitialContext();   
  9.             UserTransaction userTrx = (javax.transaction.UserTransaction) initialContext   
  10.                     .lookup("java:comp/UserTransaction");   
  11.             if (!(userTrx.getStatus() == Status.STATUS_NO_TRANSACTION)) {   
  12.                 tm = TransactionUtils.getTransactionManager(userTrx);   
  13.                 if (tm != null) {   
  14.                     currentTx = tm.suspend();   
  15.                 }   
  16.             }   
  17.             result = localTransactionCallback   
  18.                     .executeInLocalTransaction(connection);   
  19.   
  20.             if (null != currentTx) {   
  21.                 tm.resume(currentTx);   
  22.             }   
  23.         } catch (NamingException e) {   
  24.         } catch (SystemException e) {   
  25.         } catch (InvalidTransactionException e) {   
  26.         } catch (IllegalStateException e) {   
  27.         }   
  28.         return result;   
  29.     }  

 多锁申请操作是上面流程的一个回调

 

Java代码 复制代码
  1. private class ObtainMutipleLocksLocalTransactionCallback implements  
  2.             LocalTransactionCallback {   
  3.         private Set<String> lockNames;   
  4.         private int waitTime;   
  5.   
  6.         ObtainMutipleLocksLocalTransactionCallback(Set<String> lockNames,   
  7.                 int waitTime) {   
  8.             this.lockNames = lockNames;   
  9.             this.waitTime = waitTime;   
  10.         }   
  11.         public Object executeInLocalTransaction(Connection conn) {   
  12.             CallableStatement lockAcquireStmt = null;   
  13.             Set<String> obtainedLockNames = new HashSet<String>();   
  14.             boolean timeOut = false;   
  15.             String timeOutLockName = null;   
  16.             Exception mifLockException = null;   
  17.             try {   
  18.                 lockAcquireStmt = conn.prepareCall(OBTAIN_LOCK_PROC_CALL);   
  19.                 for (String lockName : lockNames) {   
  20.                     lockAcquireStmt.setString(1, lockName);   
  21.                     lockAcquireStmt.setInt(2, LCOK_EXPIRE_TIME);   
  22.                     lockAcquireStmt.setInt(3, waitTime);   
  23.                     lockAcquireStmt.registerOutParameter(4,   
  24.                             java.sql.Types.INTEGER);   
  25.                     lockAcquireStmt.registerOutParameter(5,   
  26.                             java.sql.Types.VARCHAR);   
  27.                     lockAcquireStmt.executeUpdate();   
  28.                     int lockacquireResult = lockAcquireStmt.getInt(4);   
  29.                     if (lockacquireResult == ULLockResultType.SUCCESSFUL)    
  30.                         obtainedLockNames.add(lockName);   
  31.                     } else if (lockacquireResult == ULLockResultType.TIMEOUT) {   
  32.                         timeOut = true;   
  33.                         timeOutLockName = lockName;   
  34.                         break;   
  35.                     } else if (lockacquireResult != ULLockResultType.ALREADY_OWNED) {   
  36.                         String lockResultDesc = ULLockResultType   
  37.                                 .getAcquireTypeDesc(lockacquireResult);   
  38.                         LockException lockException = new LockException(   
  39.                                 "Obtain lock " + lockName   
  40.                                         + " fails, the reason is "  
  41.                                         + lockResultDesc + " .");   
  42.                         lockException.setLockName(lockName);   
  43.                         lockException.setLockHandlingResult(lockResultDesc);   
  44.                         throw lockException;   
  45.                     } else {   
  46.                     }   
  47.                 }   
  48.             } catch (Exception ex) {   
  49.                 mifLockException = ex;   
  50.             } finally {   
  51.                 if (null != lockAcquireStmt) {   
  52.                     try {   
  53.                         lockAcquireStmt.close();   
  54.                     } catch (SQLException e) {   
  55.                         // swallow   
  56.                     }   
  57.                 }   
  58.             }   
  59.             boolean success = true;   
  60.             if (timeOut || mifLockException != null) {   
  61.                 success = false;   
  62.             }   
  63.             return new ObtainMultipleLocksResult(success, obtainedLockNames,   
  64.                     timeOut, timeOutLockName, mifLockException);   
  65.         }   
  66.     }  

 

多锁释放操作也是事务暂停流程的一个回调

 

 

Java代码 复制代码
  1. private class ReleaseMultipleLocksLocalTransactionCallback implements  
  2.             LocalTransactionCallback {   
  3.         private Set<String> lockNames;   
  4.   
  5.         ReleaseMultipleLocksLocalTransactionCallback(Set<String> lockNames) {   
  6.             this.lockNames = lockNames;   
  7.         }   
  8.   
  9.         public Object executeInLocalTransaction(Connection conn) {   
  10.             CallableStatement lockReleaseStmt = null;   
  11.             Map<String, Exception> mifLockErrors = new HashMap<String, Exception>();   
  12.             Set<String> releasedLocks = new HashSet<String>();   
  13.             try {   
  14.                 try {   
  15.                     lockReleaseStmt = conn.prepareCall(RELEASE_LOCK_PROC_CALL);   
  16.                 } catch (Exception ex) {   
  17.                     for (String lockName : lockNames) {   
  18.                         mifLockErrors.put(lockName, ex);   
  19.                     }   
  20. return new ReleaseMutipleLocksResult(false, releasedLocks, mifLockErrors);   
  21.                 }   
  22.   
  23.                 for (String lockName : lockNames) {   
  24.                     try {   
  25.                         lockReleaseStmt.registerOutParameter(1,   
  26.                                 java.sql.Types.INTEGER);   
  27.                         lockReleaseStmt.setString(2, lockName);   
  28.                         lockReleaseStmt.executeUpdate();   
  29.                         int lockReleaseResult = lockReleaseStmt.getInt(1);   
  30.                         if (lockReleaseResult == ULLockResultType.SUCCESSFUL) {   
  31.                             releasedLocks.add(lockName);   
  32.                         } else {   
  33.                             String lockResultDesc = ULLockResultType   
  34.                                     .getReleaseTypeDesc(lockReleaseResult);   
  35.                             LockException lockException = new LockException(   
  36.                                     "Release lock " + lockName   
  37.                                             + " fails, the reason is "  
  38.                                             + lockResultDesc + " .");   
  39.                             lockException.setLockName(lockName);   
  40.                             lockException.setLockHandlingResult(lockResultDesc);   
  41.                             mifLockErrors.put(lockName, lockException);   
  42.                         }   
  43.                     } catch (Exception ex) {   
  44.                         mifLockErrors.put(lockName, ex);   
  45.                     }   
  46.                 }   
  47.             } finally {   
  48.                 if (null != lockReleaseStmt) {   
  49.                     try {   
  50.                         lockReleaseStmt.close();   
  51.                     } catch (SQLException e) {   
  52.                     }   
  53.                 }   
  54.             }   
  55.             boolean success = releasedLocks.size() == this.lockNames.size();   
  56.             return new ReleaseMutipleLocksResult(success, releasedLocks,   
  57.                     mifLockErrors);   
  58.         }   
  59.     }  

 

使用模板:注意锁的释放要写在finally语句块里面,保证锁的释放。

定义好模板,防止Application用户直接调用锁管理器或者滥用锁,忘记释放锁。我们决定定义一个模板,做到锁的申请和释放对application用户来说是透明的,把它做成了隐含锁。

 

  

Java代码 复制代码
  1. public void execute(JobExecutionContext context)   
  2.             throws JobExecutionException {   
  3.         Map jobDataMap = context   
  4.         .getJobDetail().getJobDataMap();   
  5.         Collection<String> lockKeys = (Collection<String>) jobDataMap.get(LOCK_NAME_KEY);   
  6.         Integer waitTimeInteger = (Integer) jobDataMap   
  7.         .get(LOCK_WAIT_TIME_SECONDS_KEY);   
  8.         int waitTime = MAX_WAITTIME;   
  9.         if (waitTimeInteger != null) {   
  10.             waitTime = waitTimeInteger.intValue();   
  11.         }   
  12.         Set<String> uniqueLockKeys = new HashSet<String>(lockKeys);   
  13.   
  14.         // filter empty keys   
  15.         Iterator<String> keyIterator = uniqueLockKeys.iterator();   
  16.         while (keyIterator.hasNext()) {   
  17.             String key = keyIterator.next();   
  18.             if (StringUtils.isEmptyNoOffset(key)) {   
  19.                 keyIterator.remove();   
  20.             }   
  21.         }   
  22.         if (CollectionUtils.isNotEmptyCollection(uniqueLockKeys)) {   
  23.             Set<String> obtainedLockNames = null;   
  24.             Connection connection = null;   
  25.             try {   
  26.                 connection = DataSource.getConnection();   
  27.                 ObtainMultipleLocksResult result = LOCK_MANAGER.obtainLock(   
  28.                         connection, uniqueLockKeys, waitTime);   
  29.                 obtainedLockNames = result.getObtainedLockNames();   
  30.                 if (!result.isSuccess()) {   
  31.                     if (result.isTimeout()) {   
  32.                       //do log   
  33.                         return;   
  34.                     } else {   
  35.                         JobExecutionException jobException = new JobExecutionException(   
  36.                                 "Obtain locks failed! "  
  37.                                         + result.getMifLockException()   
  38.                                                 .getMessage(), result   
  39.                                         .getMifLockException());   
  40.                         throw jobException;   
  41.                     }   
  42.                 }   
  43.                 this. executeInLock (context);   
  44.             } catch (Throwable e) {   
  45.                 throw new JobExecutionException(   
  46.                         "Get db connection failed!" + e.getMessage(), e);   
  47.             } finally {   
  48.                 if (null != connection) {   
  49.                     this.releaseLocks(connection, obtainedLockNames);   
  50.                     try {   
  51.                         connection.close();   
  52.                     } catch (SQLException e) {   
  53.                         throw new JobExecutionException(   
  54.                                 "close  db connection failed!" + e.getMessage(), e);   
  55.                     }   
  56.                 }   
  57.             }   
  58.         } else {   
  59.             this.executeInLock(context);   
  60.         }   
  61.     }  

 

executeInLockapplication的子类继承实现

 

缓存

l         缓存悲观离线锁

l         缓存lockhandle

因为使用的是悲观离线锁,每次申请锁都要跑一趟db,但如果当前线程已经是lock的所有者就不需要白跑一趟了。可以用ThreadLocal把当前线程已经拥有的锁缓存起来,释放锁时对应的需要清除缓存。

在申请锁时,需要获得UL Lock时的lockhandle,释放锁时也需要提供锁的lockhandle,我们需要将它缓存起来,主要是因为DBMS_LOCK.Unique每次都会commit,会影响性能,这样每次释放锁时就可以直接使用lockhandle了。有两种方法对lockhandle进行缓存,缓存在java端作为实例变量,缓存在plsql包的全局变量中。缓存在java端需要注意的是,lock manager不能作为单例或者享元来使用,否则lock handle的缓存在多jvm之间也存在着并发控制和同步的问题。

 

转自:http://ldd600.javaeye.com/blog/534032

原创粉丝点击