最近遇到一个比较棘手的问题,交易时出现重复交易,并且这个问题是偶尔才出现,公司的产品主要是针对餐饮行业的CRM管理系统,类似于开卡,做消费奖励活动等 ,一天的交易量大,商户有几百家,门店数千个,至于为什么为出现重复交易,虽然在程序里面已经控制了是否重复提交的限制(也就是根据transId去查是否已经存在),但是仍然会出现重复交易的现象。在追究为什么在有重复提交限制还出现这种问题上,答案很模糊,连技术总监也直言,重复交易的原因很不确定,可能由于网络原因造成多次发出请求,操作失误等(比如多次点击鼠标)等 。
程序中判断是否是重复提交的代码:
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)阻塞。这一点几乎与其它所有数据库都不一样。在其它数据库中,读往往会被写阻塞。尽管听上去这个特性似乎很不错(一般情况下确实如此),但是如果你没有充分理解这个思想,而且想通过应用逻辑对应用施加完整性约束,就极有可能做得不对。
- 写入器想写某行数据,但另一个写入器已经锁定了这行数据,此时该写入器才会被阻塞。读取器绝对不会阻塞写入器。
select * from MEMBER_CREDIT_ACCOUNT where merchant_id = '01058121106'
and customer_id='0010511200000971' for update
,执行后明显看到,PLSql 左上角有提交或者回滚的键变成可点状态了。这个时候不做任何操作,不提交也不回滚,然后再打开另一个PLSQL窗口,执行一个读的操作,也就是select 语句,这个语句能够马上查出来。也就是证明,在加了修改锁的时候,读是不会阻塞的。然后再写一个update语句测试写的操作, 执行的时候发现右下角一直出现
update MEMBER_CREDIT_ACCOUNT set store_id = '332' where merchant_id = '01058121106'
and customer_id='0010511200000971'
解决这次的问题,我采用的是行级锁。是用select for update 去给某一行加锁,并且,考虑给哪个表加锁,还要考虑具体的业务,因为加了行锁的话,也就是加了一个事务,在这个事务没有提交或者回滚之前,其他的事务都得排队等待,在没有提交事务或者回滚前,假如这一条数据影响的其他操作,比如,锁定了会员预存表中的某一条数据,
select * from MEMBER_ACCOUNT where merchant_id = '01058121106'
and customer_id='0010511200000971' for update
那么假如这个时候营业员从管理台手工调账,调整这个customer预存,那么这个操作就会等很久不会执行(一个极端的模拟方式,锁住这一条数据,项目在调试状态,断点还没有执行到事务提交或者回滚时,后台对这个用户手工调账的操作就会反应很慢,是因为还在等待这个锁定的事务提交)。因此,在考虑锁哪个表的某一行时,一定要找到对整个应用系统中影响最小的那个表。
首先结合我的程序代码来看:
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);
其中判断是否为重复交易 调用的方法如下:
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。
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
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);
String transId=null;
long totalMoney =0;
Long creditLimit = null;
Long creditBalance = null;
Pos pos = apiAuthenticate.posCheck(posId, posPwd);
apiAuthenticate.isPosAvailable(posId);
Store store = apiAuthenticate.storeCheck(pos, storeId);
String merchantId = store.getMerchantId();
Card card = apiAuthenticate.cardCheck(cardNum, store, false);
String customerId = card.getCustomerId();
String cardId = card.getId();
//加锁【锁住MEMBER_CREDIT_ACCOUNT,因为挂账消费,要修改挂账用户表,这里根据merchantId,customerId两个条件可以锁住这一条】
Connection con = null;
Statement statement = null;
try {
con = this.getSqlMapClient().getDataSource().getConnection();
con.setAutoCommit(false);
statement = con.createStatement();
statement.execute("select customer_id from MEMBER_CREDIT_ACCOUNT where merchant_id='"+merchantId+"' and customer_id='"+customerId+"' for update");
} catch (SQLException e) {
e.printStackTrace();
}
try {
//判断是否为重复交易
apiAuthenticate.checkRepeatTrans(bizId, posId);
MerchantMember merchantMember = apiAuthenticate.memberCheck(customerId, card, store, false);
//选择主卡帐户
String masterCustomerId = null;
String masterRecordId = null;
boolean isTeamAccount = certification.isTeamAccount(cardId, storeId);
if (isTeamAccount) {
masterCustomerId = certification.getMasterCustomerId(customerId, merchantId);
apiAuthenticate.memberCheck(masterCustomerId, card, store, false);
masterRecordId = masterCustomerId;
} else {
masterCustomerId = customerId;
}
// 修改账户交易值
totalMoney = RequestUtil.toSafeDigit(transMoney);
creditService.consumeAccount(masterCustomerId, merchantId, storeId, totalMoney);
// 增加交易记录
transId = StringUtils.generateTransId();
operateRecord.insertTransRecord(customerId, masterRecordId, merchantId, storeId, transId,
cardId, posId, TransConstants.TRANS_TYPE_CREDIT_CONSUME, null,
GlobalConstants.TRANS_WAY_MANUAL, bizId, batchId,null,null);
MemberCreditAccount account = creditService.findMemberCreditAccount(masterCustomerId, merchantId, storeId);
operateRecord.addTransCreditRecord(transId, totalMoney, null, merchantMember.getStoreId(), storeId,
merchantId, customerId, masterCustomerId, TransConstants.TRANS_TYPE_CREDIT_CONSUME,
posId, cardId, null, "api-pos", GlobalConstants.TRANS_WAY_MANUAL, bizId, account.getBalance(), null);
// 挂帐信息
MemberCreditAccount creditAccount = creditService.findMemberCreditAccount(masterCustomerId, merchantId, storeId);
if(null != creditAccount) {
creditLimit = creditAccount.getCreditLimit();
creditBalance = creditAccount.getBalance();
}
} catch (Exception e1) {
// TODO: handle exception
e1.printStackTrace();
if(con != null){
try {
con.rollback();
con.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(statement != null){
try {
statement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}finally{ //假如判断到中间某些地方有异常,则回滚当前对数据库的操作。
// 解锁
try {
if(con != null) {
con.commit();
con.close();
}
if(statement != null) {
statement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
//返回结果
Map<String, Object> result = new HashMap<String, Object>();
result.put(ApiConstants.RETURN_STATUS, PosErrors.SUCCESS);
result.put(ApiConstants.RETURN_CARD_ID, cardId);
result.put(ApiConstants.RETURN_TRANS_ID, transId);
result.put(ApiConstants.RETURN_TRANS_MONEY, totalMoney);
result.put(ApiConstants.RETURN_CREDIT_LIMIT, creditLimit);
result.put(ApiConstants.RETURN_CREDIT_BALANCE, creditBalance);
// apiOperationLog.addLog(ApiConstants.CREDITCONSUME, "卡号"+cardId, ApiConstants.API, posId, storeId, merchantId);
return result;
}
分享到:
相关推荐
Oracle数据库提供了多种机制来确保数据的并发控制,包括锁机制、事务机制、版本机制等。 锁机制是指在数据库中对数据加锁,以避免多个用户同时访问和修改数据。Oracle数据库提供了多种锁机制,包括行锁、表锁、排它...
Oracle数据库在处理并发操作时,行级锁定机制起到了至关重要的作用,特别是在涉及多表更新的场景下。本文主要探讨了Oracle游标中的多表UPDATE行锁及其解决方案。 Oracle的锁定机制主要包括共享封锁、独占封锁和共享...
Oracle数据库性能优化是确保数据库高效运行的关键环节,尤其是在处理大量数据和高并发访问的环境中。Oracle作为业界领先的数据库管理系统,其性能优化策略涵盖多个层面,包括系统配置、数据库设计、SQL语句优化、...
在Oracle数据库系统中,"ORA-00060: 等待资源时检测到死锁" 是一个常见的错误提示,它表明两个或多个事务在执行过程中陷入了无法继续进行的状态,因为彼此都在等待对方释放资源。这种情况通常发生在并发操作中,比如...
DB2和Oracle都是广泛应用的关系型数据库,它们都采用了锁作为主要的并发控制手段。本文将对比分析DB2和Oracle的并发控制,特别是锁的实现和差异。 首先,数据库事务的ACID特性是并发控制的基础,其中A(原子性)、C...
【并发控制】在数据库系统中,特别是在DB2和Oracle这样的大型企业级数据库中,是确保多用户环境下数据一致性的重要机制。并发控制通过引入【锁】技术来防止事务间的冲突,保证ACID特性得以实现,即原子性(Atomicity...
Oracle数据库系统是关系型数据库的一种,它在处理并发事务时采用了加锁机制来确保数据的一致性和完整性。本文主要探讨了Oracle数据库中的加锁问题,包括为何启用锁、Oracle数据库中的各类锁以及锁的工作原理。 ...
在数据库管理系统中,并发控制是确保多个用户同时访问数据库时数据完整性的重要机制。DB2和Oracle作为两大主流的关系型数据库,它们都采用了锁机制来实现并发控制,但具体实现方式有所不同。 1. 并发控制的基本概念...
在Oracle数据库系统中,行级锁(Row Locks)是一种重要的并发控制机制,它允许多个用户同时访问数据库,但限制了他们对同一行数据的并发修改。行级锁的使用可以提高多用户环境下的数据库性能,因为它只锁定将要修改...
Oracle数据库是业界领先的商用关系型数据库管理系统,广泛应用于企业级应用中。数据库性能优化是保证数据库稳定、高效运行的重要手段。随着网络应用和电子商务的快速发展,数据库规模不断扩大,性能优化问题日益凸显...
Oracle数据库性能优化是一个复杂而关键的任务,涉及到多个层面的调整和优化。首先,我们要了解数据库性能优化的目标,主要是为了减少用户响应时间,提高系统吞吐量,确保数据库系统的稳定性和可扩展性。以下是对标题...
6. 锁机制:Oracle 数据库的锁机制包括表锁、行锁、共享锁、排他锁等,可以用来控制并发访问数据库的多个用户。 7. 索引:Oracle 数据库支持多种索引,包括 B-Tree 索引、位图索引、散列索引等,可以用来提高查询...
《Oracle数据库9i/10g/11g编程艺术:深入数据库体系结构》是一部针对Oracle数据库技术的经典著作,尤其适合那些希望深入了解Oracle数据库内部机制的开发者和DBA。这部资料分为多个部分进行分发,这里提及的是第二...
行锁避免了全表锁定,提高了多用户环境下的并发性能。另外,读已提交(Read Committed)和可重复读(Repeatable Read)两种事务隔离级别提供了不同的并发策略。 六、安全性 Oracle 11g提供了一套完整的安全机制,...
在Oracle数据库管理中,了解如何查询表是否被锁定以及如何执行解锁操作是至关重要的技能,尤其是在处理并发事务、性能调优或解决死锁问题时。本文将深入探讨Oracle中的表锁机制,包括如何识别锁定的表,理解不同类型...
在Oracle数据库管理中,锁机制是一种重要的资源管理手段,用于控制并发事务对数据资源的访问,防止多个用户同时修改相同的数据,从而确保数据的一致性和完整性。然而,在高并发环境下,锁的争用可能导致性能瓶颈,...
MySQL中表锁和行锁的互斥规则是一门较为复杂但十分重要的知识,尤其在数据库并发控制的场景下,理解这些规则能够帮助开发者和数据库管理员更好地设计和管理数据库应用。本篇文章将详细地阐述表锁和行锁在MySQL中的...