Java企业系统开发时,会面临很多事物的问题。今天聊一下关于事务的一些事情。
1. 什么是事务
数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
当事务被提交给了DBMS(数据库管理系统),则DBMS([数据库管理系统])需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被[回滚],回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
事务ACID特性
数据库事务拥有以下四个特性,习惯上被称之为ACID特性。
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态,一致状态的含义是数据库中的数据应满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
脏读、不可重复读、幻读 解释
脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。数据库事务和Spring事务是一般面试都会被提到,很多朋友写惯了代码,很少花时间去整理归纳这些东西,结果本来会的东西,居然吞吞吐吐答不上来。
幻读 : 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
2. 事务的7种传播级别和4种隔离级别
事务是逻辑处理原子性的保证手段,通过使用事务控制,可以极大的避免出现逻辑处理失败导致的脏数据等问题。
事务最重要的两个特性,是事务的传播级别和数据隔离级别。传播级别定义的是事务的控制范围,事务隔离级别定义的是事务在数据库读写方面的控制范围。
(1)事务的7种传播级别:
1) PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
2)PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
3)PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
4)PROPAGATION_REQUIRES_NEW ,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。
5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。
6)PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
7)PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:
如果子事务回滚,会发生什么?
父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。
如果父事务回滚,会发生什么?
父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。那么:
事务的提交,是什么情况?
是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。
以上是事务的7个传播级别,在日常应用中,通常可以满足各种业务需求,但是除了传播级别,在读取数据库的过程中,如果两个事务并发执行,那么彼此之间的数据是如何影响的呢?
这就需要了解一下事务的另一个特性:数据隔离级别
(2)数据隔离级别分为不同的四种:
- Serializable :最严格的级别,事务串行执行,资源消耗最大;
- REPEATABLE READ :保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
- READ COMMITTED :大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
- Read Uncommitted :保证了读取过程中不会读取到非法数据。
上面的解释其实每个定义都有一些拗口,其中涉及到几个术语:脏读、不可重复读、幻读。
这里解释一下:
脏读 :脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。
不可重复读字面含义已经很明了了,比如事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。
幻读 :是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。
小的时候数手指,第一次数十10个,第二次数是11个,怎么回事?产生幻觉了?
幻读也是这样子,事务A首先根据条件索引得到10条数据,然后事务B改变了数据库一条数据,导致也符合事务A当时的搜索条件,这样事务A再次搜索发现有11条数据了,就产生了幻读。
一个对照关系表:
Dirty reads(脏读) | non-repeatable reads不可重复读 | phantom reads(幻读) | |
---|---|---|---|
Serializable | 不会 | 不会 | 不会 |
REPEATABLE READ | 不会 | 不会 | 会 |
READ COMMITTED | 不会 | 会 | 会 |
Read Uncommitted | 会 | 会 | 会 |
所以最安全的,是Serializable,但是伴随而来也是高昂的性能开销。
另外,事务常用的两个属性:readonly和timeout
一个是设置事务为只读以提升性能。
另一个是设置事务的超时时间,一般用于防止大事务的发生。还是那句话,事务要尽可能的小!
3. Spring五个事务隔离级别和七个事务传播行为
基于元数据的 Spring 声明性事务 :
(1) Isolation(隔离级别) 属性一共支持五种事务设置,如下:
- DEFAULT 使用数据库设置的隔离级别 ( 默认 ) ,由 DBA 默认的设置来决定隔离级别 .
- READ_UNCOMMITTED 会出现脏读、不可重复读、幻读 ( 隔离级别最低,并发性能高 )
- READ_COMMITTED 会出现不可重复读、幻读问题(锁定正在读取的行)
- REPEATABLE_READ 会出幻读(锁定所读取的所有行)
- SERIALIZABLE 保证所有的情况不会发生(锁表)
不可重复读的重点是修改 :
同样的条件 , 你读取过的数据 , 再次读取出来发现值不一样了
幻读的重点在于新增或者删除
同样的条件 , 第 1 次和第 2 次读出来的记录数不一样
(2) Spring在TransactionDefinition接口中定义这些属性
在TransactionDefinition接口中定义了五个不同的事务隔离级别
- ISOLATION_DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应
- ISOLATION_READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读
- ISOLATION_READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
- ISOLATION_REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
- ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
(2) 在TransactionDefinition接口中定义了七个事务传播行为
PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。
PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常
PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行
4. Spring配置声明式事务:
- 配置DataSource
- 配置事务管理器
- 事务的传播特性
- 那些类那些方法使用事务
Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager和代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分。
DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为 HibernateTransactionManager。
根据代理机制的不同,Spring事务的配置又有几种不同的方式:
第一种方式:每个Bean都有一个代理
第二种方式:所有Bean共享一个代理基类
第三种方式:使用拦截器
第四种方式:使用tx标签配置的拦截器
第五种方式:全注解
5. 常见问题
1、spring事务控制放在service层,在service方法中一个方法调用service中的另一个方法,默认开启几个事务?
spring的事务传播方式默认是PROPAGATION_REQUIRED,判断当前是否已开启一个新事务,有则加入当前事务,否则新开一个事务(如果没有就开启一个新事务),所以答案是开启了一个事务。
2、spring 什么情况下进行事务回滚?
Spring、EJB的声明式事务默认情况下都是在抛出unchecked exception后才会触发事务的回滚
unchecked异常,即运行时异常runntimeException 回滚事务;
checked异常,即Exception可try{}捕获的不会回滚.当然也可配置spring参数让其回滚.
spring的事务边界是在调用业务方法之前开始的,业务方法执行完毕之后来执行commit or rollback(Spring默认取决于是否抛出runtime异常).
如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
一般不需要在业务方法中catch异常,如果非要catch,在做完你想做的工作后(比如关闭文件等)一定要抛出runtime exception,否则spring会将你的操作commit,这样就会产生脏数据.所以你的catch代码是画蛇添足。
6. 开发过程中典型问题
(1) 事务冲突,导致事务等待死锁
开发在代码时,对一条数据进行修改,在执行下一段业务逻辑是,对该数据新启事务再次进行修改,导致事务冲突相互等待。
如下代码:
该代码有两个问题:
事务冲突等待导致系统卡死
标注1处,进行交易订单更新,在标注2处进行交易推送,推送完成后,会继续更新订单状态为推送成功。
标注2是多线程推送交易订单,多线程中,每个子线程是一个新启独立事务,子线程执行完成进行事务提交,此时数据库检测到外部还有挂起的事务,等待外部事务提交,外部线程等待子推送方法执行完成提交事务,导致事务等待死锁。结果,交易无法处理,一直卡在那里。
事务不可见,标注2方法开启多线程后,在子线程中查询到的order对象还是为执行标注1更新之前的对象。
for (int strat = 1; ; strat++) {
setPageInfo(strat, pageSize, corpIpdpOrder);
corpIpdpOrder.setSendOrderState(-1);
corpIpdpOrder.setEntrustType(-1);
List<CorpIpdpOrder> corpIpdpOrders = corpIpdpOrderDao.getCorpIpdpOrderList(corpIpdpOrder);
if (corpIpdpOrders.isEmpty()) {
break;
}
// 如果是发行失败设置交易状态为订单确认失败,否则是手动起息修改订单状态为订单确认成功
for (CorpIpdpOrder order : corpIpdpOrders) {
if (state == Issue.STATE_FAILTURE) {
this.doIssueFailPushIpdpTrade(order);
} else {
this.doIssueIpdpTradeState(order);
}
/**标注1: 该处进行更新订单表交易状态。*/
corpIpdpOrderDao.updateCorpIpdpOrder(order);
}
/**标注2: 该方法为多线程推送,单个子线程不带事务,执行后还会更新订单表交易状态*/
this.batchPushCorpOrderToIpdp(corpIpdpOrders);
count += corpIpdpOrders.size();
}
@Override
public void batchPushCorpOrderToIpdp(List<CorpIpdpOrder> corpIpdpOrders) {
final CountDownLatch doSgnl = new CountDownLatch(threadSize);
SyncRunList<CorpIpdpOrder> syncLs = new SyncRunList<CorpIpdpOrder>(corpIpdpOrders) {
@Override
protected void afterRun(SyncRunnb runnb) {
doSgnl.countDown();
}
@Override
protected void runItem(CorpIpdpOrder corpIpdpOrder) {
IpdpServiceImpl ipdpServiceImpl = new IpdpServiceImpl();
ipdpServiceImpl.setFlag(CommBody.FLAG_CORP_TRADE);
ipdpServiceImpl.setCorpIpdpOrder(corpIpdpOrder);
// 单条推送方法
doNewTransactionTradeSendInfoMq(ipdpServiceImpl);
}
};
try {
syncLs.executeTasks(taskExecutor, threadSize);
doSgnl.await();
} catch (InterruptedException e) {
logger.error("对公持仓MQ推送IPDP主线程等待发生异常:", e);
}
}
(2) 事务不可见,导致业务数据没有更新
业务场景:
交易处理(PROPAGATION_REQUIRES事务)生成订单A之后写入数据库,后续进行扣款(PROPAGATION_REQUIRES_NEW),扣款成功后,将订单A更新扣账流水号,当交易成交后,查询数据库,订单A记录并没有扣账流水号。
if (redemptionAsk.getTrancode().equals(RedemptionAsk.TRADE_PURCH)) {
//设置本金交割日
tradelog.setPayAmtDate(tradelog.getTradeDate());
//生成申购交易记录corp_rich_tradelog表交易记录
Long serialno = (Long)tradelogDAO.insertTradelog(tradelog);
/**********反洗钱-增加代理人-xjl-START**************/
tradelog.setSerialno(serialno);
logger.info("反洗钱增加-RedemptionAskTranService.doTransRedemptionAsk-交易流水号"+serialno);
/**标注1:新增交易订单记录 */
boolean isRetrade = tradeAgentService.addReTrdeAgentInfo(tradelog);//如果存在原交易,则进行处理
if(!isRetrade && tradelog.getIsAgtTrade()==Tradelog.ISAGTTRADE_YES){//针对新交易进行处理
tradeAgentService.addTradeAgentInfo(tradelog);
}
/**********反洗钱-增加代理人-xjl-END**************/
//暂时设置交易流水号为申请交易流水号,用于客户解冻
tradelog.setSerialno(redemptionAsk.getSerialno());
// 解冻
boolean recSuccess = redemptionAskService.unFreezeAmount(tradelog, redemptionAsk);
//解冻成功进行扣账
if (recSuccess) {
tradelog.setSerialno(serialno);
// 扣账
recSuccess = custAmountService.deductCustFund(tradelog, true);
if (!recSuccess) {
//扣账失败抛出异常
logger.error(logger + "流水号" + redemptionAsk.getSerialno()
+ "申请交易解冻失败、交易撤销" + "_" + LogInfoUtil.STATE_ERROR);
throw new FinancialException("扣款失败");
}
} else {
//解冻失败抛出异常
logger.error(logger + "流水号" + tradelog.getSerialno()
+ "交易解冻失败、交易撤销" + "_" + LogInfoUtil.STATE_ERROR);
throw new FinancialException("解冻失败");
}
}
/**
*扣账方法,独立事务
*/
public boolean deductCustFund(Tradelog tradelog, boolean overNightFlag) {
CustProfitLog cpl = new CustProfitLog();
cpl.setTradeNo(tradelog.getSerialno());
cpl.setCustAccount(tradelog.getCustAccount());
cpl.setAccount(tradelog.getAccount());
cpl.setIssueId(tradelog.getIssueid());
cpl.setBankId(tradelog.getBankid());
cpl.setCurrencyId(tradelog.getCurrencyId());
cpl.setAccType(tradelog.getAccType());
cpl.setType(CustProfitLog.TYPE_AM_IN);
cpl.setValidCardState(!tradelog.isAutoTrade());
cpl.setFlag(CustProfitLog.FLAG_OUT);
cpl.setAmount(Maths.add(tradelog.getCashAmount(), tradelog.getBillAmount()));
cpl.setExecDate(getNowDateTime());
cpl.setCashRemit(getCashRemit(tradelog));
cpl.setState(CustProfitLog.STATE_SUCCESS);
String bankId = cpl.getBankId();
BankList ls = (BankList)finGlobalCache.get(FinancialContext.CONTEXT_BANKLIST);
Bank b= ls.getBank(cpl.getBankId());
if(b.getType() == 1){
bankId = accountService.getAccountByCustAcc(cpl.getCustAccount()).getBankId();
}
long ser = getAmountSeq();
cpl.setSerialNo(ser);
cpl.setSubjType(MainArea.SUBJ_BID);
//交易类型
cpl.setTranCode(tradelog.getTrancode());
CicsParam p = cpl.gtCicsParam();
p.setTradeChanel(tradelog.getTradeChannel());
p = cicsFactoryService.callCics(p, bankId,
CicsFactoryService.TYPE_DEDUCTACCT);
//p.setErrorMsg("客户资金扣账未明测试", new CICSException("客户资金扣账未明测试",1));
// p.setErrorMsg("客户资金扣账失败测试");
if(p.recSuccess()){
/**********反洗钱增加-START**************/
TradeAgent agt = new TradeAgent();
agt.setBancsNum(String.valueOf(p.getRst()));
agt.setBancsCode("BA001050");//扣账
agt.setSerialno(String.valueOf(tradelog.getSerialno()));
/**标注2:更新订单交易扣账流水号 */
tradeAgentService.updateTradeAgentInfo(agt);//更新交易信息
/**********反洗钱增加-END**************/
cpl.setState(CustProfitLog.STATE_SUCCESS);
cpl.setBancsNo((String)p.getRst());
cpl.setParam1("ser");
insertAmountHis(cpl);
return true;
} else {
cpl.setParam1("ser");
if (overNightFlag) {
tradelog.setMemo(p.getErrorMsg());
Issue issue = new Issue();
issue.setIssueId(tradelog.getIssueid());
issue = issueDao.getIssueInfo(issue);
// 晚间扣款失败、交易撤销、记录扣款失败记录。
IssueSerial issueSerial = ((IssueSerialList)finGlobalCache.get(
FinancialContext.CONTEXT_ISSUESERIALLIST)).get(issue.getSerialCode());
if(null != issueSerial && IssueSerial.ISLOCKPERIODISSUE_NO == issueSerial.getIsLockperiodissue()){
doCancelTrade(tradelog, issue);
}
if (p.recUnclear()) {
cpl.setState(CustProfitLog.STATE_UNCLEAR);
logger.error("流水号"+tradelog.getSerialno()+"交易扣账未明、交易撤销。");
} else {
cpl.setState(CustProfitLog.STATE_FAIL);
logger.error("流水号"+tradelog.getSerialno()+"交易扣账失败、交易撤销。");
}
cpl.setMemo(p.getErrorMsg());
custProfitDao.insertAmount(cpl);
} else {
// 交易重发扣款失败重发失败、扣款未明交易失败并记录未明联机记录
if (p.recUnclear()) {
cpl.setState(CustProfitLog.STATE_UNCLEAR);
cpl.setParam1("ser");
cpl.setMemo(p.getErrorMsg());
cashPoolService.tranInsertAmount(cpl);
}
throw new FinancialException(tradelog.getTradeChannel() == 0 ? p.getErrorMsg() : FinancialError.ERROR_DEDUCTA_ERROR);
}
return false;
}
}