对Oracle数据库表加行锁控制并发时重复交易

前端之家收集整理的这篇文章主要介绍了对Oracle数据库表加行锁控制并发时重复交易前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。


原文链接:http://juliana-only.iteye.com/blog/1233247


最近遇到一个比较棘手的问题,交易时出现重复交易,并且这个问题是偶尔才出现,公司的产品主要是针对餐饮行业的CRM管理系统,类似于开卡,做消费奖励活动等 ,一天的交易量大,商户有几百家,门店数千个,至于为什么为出现重复交易,虽然在程序里面已经控制了是否重复提交的限制(也就是根据transId去查是否已经存在),但是仍然会出现重复交易的现象。在追究为什么在有重复提交限制还出现这种问题上,答案很模糊,连技术总监也直言,重复交易的原因很不确定,可能由于网络原因造成多次发出请求,操作失误等(比如多次点击鼠标)等 。

程序中判断是否是重复提交的代码

Java代码
  1. publicbooleancheckRepeatTrans(StringbizId,StringposId){
  2. Map<String,Object>parameter=newHashMap<String,Object>();
  3. parameter.put("bizId",bizId);
  4. parameter.put("posId",posId);
  5. TransRecordtransRecord=(TransRecord)getsqlMapClientTemplate().queryForObject("TransRecord_sqlMap.getInstanceByBizId",parameter);
  6. if(transRecord!=null)thrownewAppException(PosErrors.REPEAT_TRADE);
  7. returntrue;
  8. }

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)阻塞。这一点几乎与其它所有数据库都不一样。在其它数据库中,读往往会被写阻塞。尽管听上去这个特性似乎很不错(一般情况下确实如此),但是如果你没有充分理解这个思想,而且想通过应用逻辑对应用施加完整性约束,就极有可能做得不对。
  • 写入器想写某行数据,但另一个写入器已经锁定了这行数据,此时该写入器才会被阻塞。读取器绝对不会阻塞写入器。
上面一段来自 http://www.cnblogs.com/wlb/archive/2011/07/01/2095242.html,其实第三条说得有点抽象,我还没有完全理解,正如他所说的,没有理解这个思想~~ 对于其他的总结,我做了小小的测试,比如 ,在PLsql中去写一个加锁的语句:
  1. select*fromMEMBER_CREDIT_ACCOUNTwheremerchant_id='01058121106'
  2. andcustomer_id='0010511200000971'forupdate
,执行后明显看到,PLsql 左上角有提交或者回滚的键变成可点状态了。这个时候不做任何操作,不提交也不回滚,然后再打开另一个PLsql窗口,执行一个读的操作,也就是select 语句,这个语句能够马上查出来。也就是证明,在加了修改锁的时候,读是不会阻塞的。然后再写一个update语句测试写的操作,执行的时候发现右下角一直出现
  1. updateMEMBER_CREDIT_ACCOUNTsetstore_id='332'wheremerchant_id='01058121106'
  2. andcustomer_id='0010511200000971'

解决这次的问题,我采用的是行级锁。是用select for update 去给某一行加锁,并且,考虑给哪个表加锁,还要考虑具体的业务,因为加了行锁的话,也就是加了一个事务,在这个事务没有提交或者回滚之前,其他的事务都得排队等待,在没有提交事务或者回滚前,假如这一条数据影响的其他操作,比如,锁定了会员预存表中的某一条数据,

  1. select*fromMEMBER_ACCOUNTwheremerchant_id='01058121106'
  2. andcustomer_id='0010511200000971'forupdate

那么假如这个时候营业员从管理台手工调账,调整这个customer预存,那么这个操作就会等很久不会执行(一个极端的模拟方式,锁住这一条数据,项目在调试状态,断点还没有执行到事务提交或者回滚时,后台对这个用户手工调账的操作就会反应很慢,是因为还在等待这个锁定的事务提交)。因此,在考虑锁哪个表的某一行时,一定要找到对整个应用系统中影响最小的那个表。

首先结合我的程序代码来看:

Java代码
  1. publicMap<String,Object>creditConsume(Map<String,String>parameter){
  2. StringposId=parameter.get(ApiConstants.PARAM_POS_ID);
  3. StringposPwd=parameter.get(ApiConstants.PARAM_POS_PWD);
  4. StringstoreId=parameter.get(ApiConstants.PARAM_STORE_ID);
  5. StringcardNum=parameter.get(ApiConstants.PARAM_CARD_ID);
  6. StringtransMoney=parameter.get(ApiConstants.PARAM_TRANS_MONEY);
  7. StringbizId=parameter.get(ApiConstants.PARAM_BIZ_ID);
  8. StringbatchId=parameter.get(ApiConstants.PARAM_BATCH_ID);
  9. //判断是否为重复交易
  10. apiAuthenticate.checkRepeatTrans(bizId,posId);

其中判断是否为重复交易 调用方法如下:

Java代码
  1. publicbooleancheckRepeatTrans(StringbizId,parameter);
  2. if(transRecord!=null)thrownewAppException(PosErrors.REPEAT_TRADE);
  3. returntrue;
  4. }

但是这样做还不够,当这个bizId,posId不存在时,也就是这个交易是新的交易,表中还不存在时,如果有两个线程同时调用这个判断是否重复提交的方法,那么这个方法返回的transRecord都是null,那么就都会执行后面的代码,扣减余额,

插入新的交易等。这样就有了两条同样的数据。

类似以下情况:

交易时间相同,或者是只相差几秒,bizId,posId相同。

我处理的方式就是加行锁,本来在这里判断是否有重复提交,是查交易表,以posId和bizId为条件,本来考虑是将trans_record的某个记录加锁,但是后来发现有一个问题,如果是一笔新交易,那么在交易表中是不存在的,那么这一条记录就锁不住,加锁了也是没用的。所以我考虑了业务需求,找了影响最小的一个表,也就是挂账交易账户表,并且只锁这个用户。在判断重复交易前加行锁,然后处理后面的业务,等处理完业务后,再释放锁。并且,要考虑处理业务的阶段,如果任何一个地方出了错,就得抛出异常,这个时候需要rollback。

Java代码
  1. @Transactional(readOnly=false,propagation=Propagation.required)
  2. publicMap<String,String>parameter){
  3. StringposId=parameter.get(ApiConstants.PARAM_POS_ID);
  4. StringposPwd=parameter.get(ApiConstants.PARAM_POS_PWD);
  5. StringstoreId=parameter.get(ApiConstants.PARAM_STORE_ID);
  6. StringcardNum=parameter.get(ApiConstants.PARAM_CARD_ID);
  7. StringtransMoney=parameter.get(ApiConstants.PARAM_TRANS_MONEY);
  8. StringbizId=parameter.get(ApiConstants.PARAM_BIZ_ID);
  9. StringbatchId=parameter.get(ApiConstants.PARAM_BATCH_ID);
  10. StringtransId=null;
  11. longtotalMoney=0;
  12. LongcreditLimit=null;
  13. LongcreditBalance=null;
  14. Pospos=apiAuthenticate.posCheck(posId,posPwd);
  15. apiAuthenticate.isPosAvailable(posId);
  16. Storestore=apiAuthenticate.storeCheck(pos,storeId);
  17. StringmerchantId=store.getMerchantId();
  18. Cardcard=apiAuthenticate.cardCheck(cardNum,store,false);
  19. StringcustomerId=card.getCustomerId();
  20. StringcardId=card.getId();
  21. //加锁【锁住MEMBER_CREDIT_ACCOUNT,因为挂账消费,要修改挂账用户表,这里根据merchantId,customerId两个条件可以锁住这一条】
  22. Connectioncon=null;
  23. Statementstatement=null;
  24. try{
  25. con=this.getsqlMapClient().getDataSource().getConnection();
  26. con.setAutoCommit(false);
  27. statement=con.createStatement();
  28. statement.execute("selectcustomer_idfromMEMBER_CREDIT_ACCOUNTwheremerchant_id='"+merchantId+"'andcustomer_id='"+customerId+"'forupdate");
  29. }catch(sqlExceptione){
  30. e.printStackTrace();
  31. }
  32. try{
  33. //判断是否为重复交易
  34. apiAuthenticate.checkRepeatTrans(bizId,posId);
  35. MerchantMembermerchantMember=apiAuthenticate.memberCheck(customerId,card,false);
  36. //选择主卡帐户
  37. StringmasterCustomerId=null;
  38. StringmasterRecordId=null;
  39. booleanisTeamAccount=certification.isTeamAccount(cardId,storeId);
  40. if(isTeamAccount){
  41. masterCustomerId=certification.getMasterCustomerId(customerId,merchantId);
  42. apiAuthenticate.memberCheck(masterCustomerId,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,transId,
  53. cardId,posId,TransConstants.TRANS_TYPE_CREDIT_CONSUME,null,
  54. GlobalConstants.TRANS_WAY_MANUAL,bizId,batchId,null);
  55. MemberCreditAccountaccount=creditService.findMemberCreditAccount(masterCustomerId,storeId);
  56. operateRecord.addTransCreditRecord(transId,totalMoney,merchantMember.getStoreId(),
  57. merchantId,customerId,masterCustomerId,
  58. posId,cardId,"api-pos",GlobalConstants.TRANS_WAY_MANUAL,account.getBalance(),null);
  59. //挂帐信息
  60. MemberCreditAccountcreditAccount=creditService.findMemberCreditAccount(masterCustomerId,storeId);
  61. if(null!=creditAccount){
  62. creditLimit=creditAccount.getCreditLimit();
  63. creditBalance=creditAccount.getBalance();
  64. }
  65. }catch(Exceptione1){
  66. //TODO:handleexception
  67. e1.printStackTrace();
  68. if(con!=null){
  69. try{
  70. con.rollback();
  71. con.close();
  72. }catch(sqlExceptione){
  73. //TODOAuto-generatedcatchblock
  74. e.printStackTrace();
  75. }
  76. }
  77. if(statement!=null){
  78. try{
  79. statement.close();
  80. }catch(sqlExceptione){
  81. //TODOAuto-generatedcatchblock
  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(sqlExceptione){
  96. e.printStackTrace();
  97. }
  98. }
  99. //返回结果
  100. Map<String,Object>result=newHashMap<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,merchantId);
  108. returnresult;
  109. }


猜你在找的Oracle相关文章