1. 程式人生 > >對Oracle資料庫表加行鎖控制併發時重複交易

對Oracle資料庫表加行鎖控制併發時重複交易

最近遇到一個比較棘手的問題,交易時出現重複交易,並且這個問題是偶爾才出現,公司的產品主要是針對餐飲行業的CRM管理系統,類似於開卡,做消費獎勵活動等 ,一天的交易量大,商戶有幾百家,門店數千個,至於為什麼為出現重複交易,雖然在程式裡面已經控制了是否重複提交的限制(也就是根據transId去查是否已經存在),但是仍然會出現重複交易的現象。在追究為什麼在有重複提交限制還出現這種問題上,答案很模糊,連技術總監也直言,重複交易的原因很不確定,可能由於網路原因造成多次發出請求,操作失誤等(比如多次點選滑鼠)等 。

     程式中判斷是否是重複提交的程式碼:

Java程式碼 複製程式碼 收藏程式碼
  1. publicboolean
     checkRepeatTrans(String bizId, String posId) {  
  2.         Map<String, Object> parameter = new HashMap<String, Object>();  
  3.         parameter.put("bizId", bizId);  
  4.         parameter.put("posId", posId);  
  5.         TransRecord transRecord = (TransRecord) getSqlMapClientTemplate().queryForObject("TransRecord_SqlMap.getInstanceByBizId"
    , parameter);  
  6.         if (transRecord != nullthrownew AppException(PosErrors.REPEAT_TRADE);  
  7.         returntrue;  
  8.     }  
public boolean checkRepeatTrans(String bizId, String posId) {
		Map<String, Object> parameter = new HashMap<String, Object>();
		parameter.put("bizId", bizId);
		parameter.put("posId", posId);
		TransRecord transRecord = (TransRecord) getSqlMapClientTemplate().queryForObject("TransRecord_SqlMap.getInstanceByBizId", parameter);
		if (transRecord != null) throw new AppException(PosErrors.REPEAT_TRADE);
		return true;
	}

 if (transRecord != null) throw new AppException(PosErrors.REPEAT_TRADE);
這一句,如果相同的bizId和posId,則表示此交易已經存在,就會丟擲重複交易的異常。看似這樣做已經沒有問題,但是還是出現了重複交易的問題,bizId和posId完全一樣。可以判斷是由於併發造成的重複提交,之前處理防重複交易,大概也就和這個層次一樣,沒有再深入到其他層次。所以問了專案經理解決策略。還是PM有經驗,一看bizId,PosId一樣,然後說了一句,“是重複交易,加個行鎖就能解決了”,之前有了解過Hibernate的悲觀鎖,樂觀鎖,對於鎖機制一知半解,之前所做的都是web網站,流量不高,所以都沒有考慮併發問題。這次算是理解鎖機制,通過蒐集一些有關鎖的機制,今天就來總結一下我自己的理解,分享與交流,經驗有限,總結的或許有不足或者錯誤之處,多提改進修正建議,在此感謝。

      先來一段有關鎖,事務的總結的概括吧:

    許多對Oracle不太瞭解的技術人員可能會以為每一個TX鎖代表一條被封鎖的資料行,其實不然。TX的本義是Transaction(事務)當一個事務第一次執行資料更改(Insert、Update、Delete)或使用SELECT… FOR UPDATE語句進行查詢時,它即獲得一個TX(事務)鎖,直至該事務結束(執行COMMIT或ROLLBACK操作)時,該鎖才被釋放。所以,一個TX鎖,可以對應多個被該事務鎖定的資料行(在我們用的時候多是啟動一個事務,然後SELECT… FOR UPDATE NOWAIT)。  
  • Oracle只在修改時對資料庫加行級鎖。正常情況下不會升級到塊級鎖或表級鎖(不過兩段提交期間的一段很短的時間內除外,這是一個不常見的操作)。
  • 如果只是讀資料,Oracle絕不會對資料鎖定。不會因為簡單的讀操作在資料行上鎖定。
  • 寫入器(writer)不會阻塞讀取器(reader)。換種說法:讀(read)不會被寫(write)阻塞。這一點幾乎與其它所有資料庫都不一樣。在其它資料庫中,讀往往會被寫阻塞。儘管聽上去這個特性似乎很不錯(一般情況下確實如此),但是如果你沒有充分理解這個思想,而且想通過應用邏輯對應用施加完整性約束,就極有可能做得不對。
  • 寫入器想寫某行資料,但另一個寫入器已經鎖定了這行資料,此時該寫入器才會被阻塞。讀取器絕對不會阻塞寫入器。
Sql程式碼 複製程式碼 收藏程式碼
  1. select * from MEMBER_CREDIT_ACCOUNT where merchant_id = '01058121106'
  2.  and customer_id='0010511200000971'forupdate
select * from MEMBER_CREDIT_ACCOUNT where merchant_id = '01058121106'
 and customer_id='0010511200000971'  for update 
 ,執行後明顯看到,PLSql 左上角有提交或者回滾的鍵變成可點狀態了。這個時候不做任何操作,不提交也不回滾,然後再開啟另一個PLSQL視窗,執行一個讀的操作,也就是select 語句,這個語句能夠馬上查出來。也就是證明,在加了修改鎖的時候,讀是不會阻塞的。然後再寫一個update語句測試寫的操作, 執行的時候發現右下角一直出現 Sql程式碼 複製程式碼 收藏程式碼
  1. update MEMBER_CREDIT_ACCOUNT set store_id = '332'where  merchant_id = '01058121106'
  2. and customer_id='0010511200000971'
 update MEMBER_CREDIT_ACCOUNT set store_id = '332' where  merchant_id = '01058121106'
 and customer_id='0010511200000971'
 

    解決這次的問題,我採用的是行級鎖。是用select  for update 去給某一行加鎖,並且,考慮給哪個表加鎖,還要考慮具體的業務,因為加了行鎖的話,也就是加了一個事務,在這個事務沒有提交或者回滾之前,其他的事務都得排隊等待,在沒有提交事務或者回滾前,假如這一條資料影響的其他操作,比如,鎖定了會員預存表中的某一條資料,

Sql程式碼 複製程式碼 收藏程式碼
  1. select * from MEMBER_ACCOUNT where merchant_id = '01058121106'
  2.  and customer_id='0010511200000971'forupdate
select * from MEMBER_ACCOUNT where merchant_id = '01058121106'
 and customer_id='0010511200000971' for update

 那麼假如這個時候營業員從管理臺手工調賬,調整這個customer預存,那麼這個操作就會等很久不會執行(一個極端的模擬方式,鎖住這一條資料,專案在除錯狀態,斷點還沒有執行到事務提交或者回滾時,後臺對這個使用者手工調賬的操作就會反應很慢,是因為還在等待這個鎖定的事務提交)。因此,在考慮鎖哪個表的某一行時,一定要找到對整個應用系統中影響最小的那個表。

      首先結合我的程式程式碼來看:

Java程式碼 複製程式碼 收藏程式碼
  1. public Map<String, Object> creditConsume(Map<String, String> parameter) {  
  2.         String posId = parameter.get(ApiConstants.PARAM_POS_ID);  
  3.         String posPwd = parameter.get(ApiConstants.PARAM_POS_PWD);  
  4.         String storeId = parameter.get(ApiConstants.PARAM_STORE_ID);  
  5.         String cardNum = parameter.get(ApiConstants.PARAM_CARD_ID);  
  6.         String transMoney = parameter.get(ApiConstants.PARAM_TRANS_MONEY);  
  7.         String bizId = parameter.get(ApiConstants.PARAM_BIZ_ID);  
  8.         String batchId = parameter.get(ApiConstants.PARAM_BATCH_ID);  
  9. //判斷是否為重複交易
  10.         apiAuthenticate.checkRepeatTrans(bizId, posId);  
public Map<String, Object> creditConsume(Map<String, String> parameter) {
		String posId = parameter.get(ApiConstants.PARAM_POS_ID);
		String posPwd = parameter.get(ApiConstants.PARAM_POS_PWD);
		String storeId = parameter.get(ApiConstants.PARAM_STORE_ID);
		String cardNum = parameter.get(ApiConstants.PARAM_CARD_ID);
		String transMoney = parameter.get(ApiConstants.PARAM_TRANS_MONEY);
		String bizId = parameter.get(ApiConstants.PARAM_BIZ_ID);
		String batchId = parameter.get(ApiConstants.PARAM_BATCH_ID);


//判斷是否為重複交易
		apiAuthenticate.checkRepeatTrans(bizId, posId);

其中判斷是否為重複交易 呼叫的方法如下:

Java程式碼 複製程式碼 收藏程式碼
  1. publicboolean checkRepeatTrans(String bizId, String posId) {  
  2.         Map<String, Object> parameter = new HashMap<String, Object>();  
  3.         parameter.put("bizId", bizId);  
  4.         parameter.put("posId", posId);  
  5.         TransRecord transRecord = (TransRecord) getSqlMapClientTemplate().queryForObject("TransRecord_SqlMap.getInstanceByBizId", parameter);  
  6.         if (transRecord != nullthrownew AppException(PosErrors.REPEAT_TRADE);  
  7.         returntrue;  
  8.     }  
public boolean checkRepeatTrans(String bizId, String posId) {
		Map<String, Object> parameter = new HashMap<String, Object>();
		parameter.put("bizId", bizId);
		parameter.put("posId", posId);
		TransRecord transRecord = (TransRecord) getSqlMapClientTemplate().queryForObject("TransRecord_SqlMap.getInstanceByBizId", parameter);
		if (transRecord != null) throw new AppException(PosErrors.REPEAT_TRADE);
		return true;
	}
	

但是這樣做還不夠,當這個bizId, posId不存在時,也就是這個交易是新的交易,表中還不存在時,如果有兩個執行緒同時呼叫這個判斷是否重複提交的方法,那麼這個方法返回的transRecord都是null,那麼就都會執行後面的程式碼,扣減餘額,

 插入新的交易等。這樣就有了兩條同樣的資料。

類似以下情況:

交易時間相同,或者是隻相差幾秒,bizId,posId相同。

  我處理的方式就是加行鎖,本來在這裡判斷是否有重複提交,是查交易表,以posId和bizId為條件,本來考慮是將trans_record的某個記錄加鎖,但是後來發現有一個問題,如果是一筆新交易,那麼在交易表中是不存在的,那麼這一條記錄就鎖不住,加鎖了也是沒用的。所以我考慮了業務需求,找了影響最小的一個表,也就是掛賬交易賬戶表,並且只鎖這個使用者。在判斷重複交易前加行鎖,然後處理後面的業務,等處理完業務後,再釋放鎖。並且,要考慮處理業務的階段,如果任何一個地方出了錯,就得丟擲異常,這個時候需要rollback。

Java程式碼 複製程式碼 收藏程式碼
  1. @Transactional(readOnly = false, propagation = Propagation.REQUIRED)  
  2.     public Map<String, Object> creditConsume(Map<String, String> parameter) {  
  3.         String posId = parameter.get(ApiConstants.PARAM_POS_ID);  
  4.         String posPwd = parameter.get(ApiConstants.PARAM_POS_PWD);  
  5.         String storeId = parameter.get(ApiConstants.PARAM_STORE_ID);  
  6.         String cardNum = parameter.get(ApiConstants.PARAM_CARD_ID);  
  7.         String transMoney = parameter.get(ApiConstants.PARAM_TRANS_MONEY);  
  8.         String bizId = parameter.get(ApiConstants.PARAM_BIZ_ID);  
  9.         String batchId = parameter.get(ApiConstants.PARAM_BATCH_ID);  
  10.         String transId=null;  
  11.         long totalMoney =0;  
  12.         Long creditLimit = null;  
  13.         Long creditBalance = null;  
  14.         Pos pos = apiAuthenticate.posCheck(posId, posPwd);  
  15.         apiAuthenticate.isPosAvailable(posId);  
  16.         Store store = apiAuthenticate.storeCheck(pos, storeId);  
  17.         String merchantId = store.getMerchantId();  
  18.         Card card = apiAuthenticate.cardCheck(cardNum, store, false);  
  19.         String customerId = card.getCustomerId();  
  20.         String cardId = card.getId();  
  21.         //加鎖【鎖住MEMBER_CREDIT_ACCOUNT,因為掛賬消費,要修改掛賬使用者表,這裡根據merchantId,customerId兩個條件可以鎖住這一條】
  22.         Connection con = null;  
  23.         Statement statement = null;  
  24.         try {  
  25.             con = this.getSqlMapClient().getDataSource().getConnection();  
  26.             con.setAutoCommit(false);  
  27.             statement = con.createStatement();  
  28.             statement.execute("select customer_id from MEMBER_CREDIT_ACCOUNT where merchant_id='"+merchantId+"' and customer_id='"+customerId+"' for update");  
  29.         } catch (SQLException e) {  
  30.             e.printStackTrace();  
  31.         }  
  32.         try {  
  33.         //判斷是否為重複交易
  34.         apiAuthenticate.checkRepeatTrans(bizId, posId);  
  35.         MerchantMember merchantMember = apiAuthenticate.memberCheck(customerId, card, store, false);  
  36.         //選擇主卡帳戶
  37.         String masterCustomerId = null;  
  38.         String masterRecordId = null;  
  39.         boolean isTeamAccount = certification.isTeamAccount(cardId, storeId);  
  40.         if (isTeamAccount) {  
  41.             masterCustomerId = certification.getMasterCustomerId(customerId, merchantId);  
  42.             apiAuthenticate.memberCheck(masterCustomerId, card, store, false);  
  43.             masterRecordId = masterCustomerId;  
  44.         } else {  
  45.             masterCustomerId = customerId;  
  46.         }  
  47.         // 修改賬戶交易值
  48.         totalMoney = RequestUtil.toSafeDigit(transMoney);  
  49.         creditService.consumeAccount(masterCustomerId, merchantId, storeId, totalMoney);  
  50.         // 增加交易記錄
  51.         transId = StringUtils.generateTransId();  
  52.         operateRecord.insertTransRecord(customerId, masterRecordId, merchantId, storeId, transId,  
  53.                 cardId, posId, TransConstants.TRANS_TYPE_CREDIT_CONSUME, null,   
  54.                 GlobalConstants.TRANS_WAY_MANUAL, bizId, batchId,null,null);  
  55.         MemberCreditAccount account = creditService.findMemberCreditAccount(masterCustomerId, merchantId, storeId);  
  56.         operateRecord.addTransCreditRecord(transId, totalMoney, null, merchantMember.getStoreId(), storeId,   
  57.                 merchantId, customerId, masterCustomerId, TransConstants.TRANS_TYPE_CREDIT_CONSUME,   
  58.                 posId, cardId, null"api-pos", GlobalConstants.TRANS_WAY_MANUAL, bizId, account.getBalance(), null);  
  59.         // 掛帳資訊
  60.         MemberCreditAccount creditAccount = creditService.findMemberCreditAccount(masterCustomerId, merchantId, storeId);  
  61.         if(null != creditAccount) {  
  62.             creditLimit = creditAccount.getCreditLimit();  
  63.             creditBalance = creditAccount.getBalance();  
  64.         }  
  65.         } catch (Exception e1) {  
  66.             // TODO: handle exception
  67.             e1.printStackTrace();  
  68.             if(con != null){  
  69.                 try {  
  70.                     con.rollback();  
  71.                     con.close();  
  72.                 } catch (SQLException e) {  
  73.                     // TODO Auto-generated catch block
  74.                     e.printStackTrace();  
  75.                 }  
  76.             }  
  77.             if(statement != null){  
  78.                 try {  
  79.                     statement.close();  
  80.                 } catch (SQLException e) {  
  81.                     // TODO Auto-generated catch block
  82.                     e.printStackTrace();  
  83.                 }  
  84.             }  
  85.         }finally{  //假如判斷到中間某些地方有異常,則回滾當前對資料庫的操作。
  86.             // 解鎖
  87.             try {  
  88.                 if(con != null) {  
  89.                     con.commit();  
  90.                     con.close();  
  91.                 }  
  92.                 if(statement != null) {  
  93.                     statement.close();  
  94.                 }  
  95.             } catch (SQLException e) {  
  96.                 e.printStackTrace();  
  97.             }  
  98.         }  
  99.         //返回結果
  100.         Map<String, Object> result = new HashMap<String, Object>();   
  101.         result.put(ApiConstants.RETURN_STATUS, PosErrors.SUCCESS);  
  102.         result.put(ApiConstants.RETURN_CARD_ID, cardId);  
  103.         result.put(ApiConstants.RETURN_TRANS_ID, transId);  
  104.         result.put(ApiConstants.RETURN_TRANS_MONEY, totalMoney);  
  105.         result.put(ApiConstants.RETURN_CREDIT_LIMIT, creditLimit);  
  106.         result.put(ApiConstants.RETURN_CREDIT_BALANCE, creditBalance);  
  107. //      apiOperationLog.addLog(ApiConstants.CREDITCONSUME, "卡號"+cardId, ApiConstants.API, posId, storeId, merchantId);
  108.         return result;  
  109.     }