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


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


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

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

Java代码 复制代码  收藏代码
  1. public boolean 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 != nullthrow new AppException(PosErrors.REPEAT_TRADE);  
  7.         return true;  
  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)阻塞。这一点几乎与其它所有数据库都不一样。在其它数据库中,读往往会被写阻塞。尽管听上去这个特性似乎很不错(一般情况下确实如此),但是如果你没有充分理解这个思想,而且想通过应用逻辑对应用施加完整性约束,就极有可能做得不对。
  • 写入器想写某行数据,但另一个写入器已经锁定了这行数据,此时该写入器才会被阻塞。读取器绝对不会阻塞写入器。
    上面一段来自 http://www.cnblogs.com/wlb/archive/2011/07/01/2095242.html ,其实第三条说得有点抽象,我还没有完全理解,正如他所说的,没有理解这个思想~~  对于其他的总结,我做了小小的测试,比如 ,在PLSql中去写一个加锁的语句:
Sql代码 复制代码  收藏代码
  1. select * from MEMBER_CREDIT_ACCOUNT where merchant_id = '01058121106'  
  2.  and customer_id='0010511200000971'  for update   
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' for update  
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.   
  11. //判断是否为重复交易  
  12.         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. public boolean 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 != nullthrow new AppException(PosErrors.REPEAT_TRADE);  
  7.         return true;  
  8.     }  
  9.       
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.           
  11.         String transId=null;  
  12.         long totalMoney =0;  
  13.         Long creditLimit = null;  
  14.         Long creditBalance = null;  
  15.           
  16.           
  17.         Pos pos = apiAuthenticate.posCheck(posId, posPwd);  
  18.         apiAuthenticate.isPosAvailable(posId);  
  19.         Store store = apiAuthenticate.storeCheck(pos, storeId);  
  20.         String merchantId = store.getMerchantId();  
  21.           
  22.         Card card = apiAuthenticate.cardCheck(cardNum, store, false);  
  23.         String customerId = card.getCustomerId();  
  24.         String cardId = card.getId();  
  25.           
  26.         //加锁【锁住MEMBER_CREDIT_ACCOUNT,因为挂账消费,要修改挂账用户表,这里根据merchantId,customerId两个条件可以锁住这一条】  
  27.         Connection con = null;  
  28.         Statement statement = null;  
  29.         try {  
  30.             con = this.getSqlMapClient().getDataSource().getConnection();  
  31.             con.setAutoCommit(false);  
  32.             statement = con.createStatement();  
  33.             statement.execute("select customer_id from MEMBER_CREDIT_ACCOUNT where merchant_id='"+merchantId+"' and customer_id='"+customerId+"' for update");  
  34.         } catch (SQLException e) {  
  35.             e.printStackTrace();  
  36.         }  
  37.           
  38.         try {  
  39.               
  40.         //判断是否为重复交易  
  41.         apiAuthenticate.checkRepeatTrans(bizId, posId);  
  42.           
  43.         MerchantMember merchantMember = apiAuthenticate.memberCheck(customerId, card, store, false);  
  44.           
  45.         //选择主卡帐户  
  46.         String masterCustomerId = null;  
  47.         String masterRecordId = null;  
  48.         boolean isTeamAccount = certification.isTeamAccount(cardId, storeId);  
  49.         if (isTeamAccount) {  
  50.             masterCustomerId = certification.getMasterCustomerId(customerId, merchantId);  
  51.             apiAuthenticate.memberCheck(masterCustomerId, card, store, false);  
  52.             masterRecordId = masterCustomerId;  
  53.         } else {  
  54.             masterCustomerId = customerId;  
  55.         }  
  56.           
  57.         // 修改账户交易值  
  58.         totalMoney = RequestUtil.toSafeDigit(transMoney);  
  59.         creditService.consumeAccount(masterCustomerId, merchantId, storeId, totalMoney);  
  60.         // 增加交易记录  
  61.         transId = StringUtils.generateTransId();  
  62.         operateRecord.insertTransRecord(customerId, masterRecordId, merchantId, storeId, transId,  
  63.                 cardId, posId, TransConstants.TRANS_TYPE_CREDIT_CONSUME, null,   
  64.                 GlobalConstants.TRANS_WAY_MANUAL, bizId, batchId,null,null);  
  65.         MemberCreditAccount account = creditService.findMemberCreditAccount(masterCustomerId, merchantId, storeId);  
  66.         operateRecord.addTransCreditRecord(transId, totalMoney, null, merchantMember.getStoreId(), storeId,   
  67.                 merchantId, customerId, masterCustomerId, TransConstants.TRANS_TYPE_CREDIT_CONSUME,   
  68.                 posId, cardId, null"api-pos", GlobalConstants.TRANS_WAY_MANUAL, bizId, account.getBalance(), null);  
  69.           
  70.         // 挂帐信息  
  71.         MemberCreditAccount creditAccount = creditService.findMemberCreditAccount(masterCustomerId, merchantId, storeId);  
  72.           
  73.         if(null != creditAccount) {  
  74.             creditLimit = creditAccount.getCreditLimit();  
  75.             creditBalance = creditAccount.getBalance();  
  76.         }  
  77.           
  78.           
  79.         } catch (Exception e1) {  
  80.             // TODO: handle exception  
  81.             e1.printStackTrace();  
  82.             if(con != null){  
  83.                 try {  
  84.                     con.rollback();  
  85.                     con.close();  
  86.                 } catch (SQLException e) {  
  87.                     // TODO Auto-generated catch block  
  88.                     e.printStackTrace();  
  89.                 }  
  90.             }  
  91.               
  92.             if(statement != null){  
  93.                 try {  
  94.                     statement.close();  
  95.                 } catch (SQLException e) {  
  96.                     // TODO Auto-generated catch block  
  97.                     e.printStackTrace();  
  98.                 }  
  99.             }  
  100.               
  101.         }finally{  //假如判断到中间某些地方有异常,则回滚当前对数据库的操作。  
  102.               
  103.             // 解锁  
  104.             try {  
  105.                 if(con != null) {  
  106.                     con.commit();  
  107.                     con.close();  
  108.                 }  
  109.                 if(statement != null) {  
  110.                     statement.close();  
  111.                 }  
  112.             } catch (SQLException e) {  
  113.                 e.printStackTrace();  
  114.             }  
  115.         }  
  116.           
  117.         //返回结果  
  118.         Map<String, Object> result = new HashMap<String, Object>();   
  119.         result.put(ApiConstants.RETURN_STATUS, PosErrors.SUCCESS);  
  120.         result.put(ApiConstants.RETURN_CARD_ID, cardId);  
  121.         result.put(ApiConstants.RETURN_TRANS_ID, transId);  
  122.         result.put(ApiConstants.RETURN_TRANS_MONEY, totalMoney);  
  123.         result.put(ApiConstants.RETURN_CREDIT_LIMIT, creditLimit);  
  124.         result.put(ApiConstants.RETURN_CREDIT_BALANCE, creditBalance);  
  125. //      apiOperationLog.addLog(ApiConstants.CREDITCONSUME, "卡号"+cardId, ApiConstants.API, posId, storeId, merchantId);  
  126.   
  127.         return result;  
  128.     }  

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值