文章目錄
- 1. Spring 事務簡介
- 2. Spring 事務角色
- 3. Spring 事務屬性
-
- 3.1 事務配置
- 3.2 案例:轉賬業務追加日志
- 3.3 事務傳播行為
1. Spring 事務簡介
Spring 事務作用:在資料層或業務層保障一系列的資料庫操作同成功、同失敗。
資料層有事務我們可以了解,為什麼業務層也需要處理事務呢?
舉個簡單的例子:
轉賬業務會有兩次資料層的調用,一次是轉入者加錢,一次是轉出者減錢;
把事務放在資料層,加錢和減錢就有兩個事務;
如果先加錢後減錢,且加錢之後其他地方出現異常,就會出現加錢成功減錢失敗的結果。即沒辦法保證加錢和減錢同時成功或者同時失敗;
這個時候就需要将事務放在業務層進行處理。
不用事務時,我們是這樣處理的:

Dao接口
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
Service 接口和實作類
public interface AccountService {
/**
* 轉賬操作
* @param out 轉出方
* @param in 轉入方
* @param money 餘額
*/
void transfer(String out, String in, Double money);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按類型注入
private AccountDao accountDao;
@Override
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
}
}
測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() {
accountService.transfer("Tom", "Jerry", 100D);
}
}
上述代碼運作時,會執行轉賬操作,Tom 的賬戶減少 100,Jerry 的賬戶增加 100。
這是正常情況下的運作結果,但是如果在轉賬的過程中出現了異常,如:
這個時候就模拟了轉賬過程中出現異常的情況,正确的操作應該是 Tom 和 Jerry 賬戶中的錢不變。但是真正運作後會發現,并沒有像我們想象的那樣,Tom賬戶中的錢變少了,而 Jerry 賬戶中的錢沒變。
當程式出問題後,我們需要讓事務復原,且這個事務應該加在業務層上。Spring 的事務管理就是用來解決這類問題的。
Spring事務管理具體的實作步驟為:
(1) 在需要被事務管理的方法上添加注解
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按類型注入
private AccountDao accountDao;
@Override
@Transactional//開啟事務
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
}
}
注意:
@Transactional 可以寫在接口類上、接口方法上、實作類上和實作類方法上
寫在接口類上,該接口的所有實作類的所有方法都會有事務
寫在接口方法上,該接口的所有實作類的該方法都會有事務
寫在實作類上,該類中的所有方法都會有事務
寫在實作類方法上,該方法上有事務
建議寫在實作類或實作類的方法上
(2) 在 JdbcConfig 類中配置事務管理器
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean//把方法的傳回值定義成一個bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
//配置事務管理器,mybatis使用的是jdbc事務
@Bean//把方法的傳回值定義成一個bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
注意:事務管理器要根據使用技術進行選擇,Mybatis 架構使用的是 JDBC 事務,可以直接使用 DataSourceTransactionManager。
(3) 開啟事務注解
@Configuration//該類是配置類
@ComponentScan("com.itheima")//掃描這個包下的類,找bean
@PropertySource("jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})//引入這些類中的bean
//開啟注解式事務驅動
@EnableTransactionManagement
public class SpringConfig {
}
(4) 測試
會發現在轉賬業務出現錯誤後,事務就可以控制復原,保證資料的正确性。
2. Spring 事務角色
本節重點了解兩個概念,分别是事務管理者和事務協調員。
(1) 未開啟Spring事務之前:
AccountDao 的 outMoney 因為是修改操作,會開啟一個事務 T1;
AccountDao 的 inMoney 因為是修改操作,會開啟一個事務 T2;
AccountService 的 transfer 沒有事務。
運作過程中如果沒有抛出異常,則 T1 和 T2 都正常送出,資料正确;
如果在兩個方法中間抛出異常,T1 因為執行成功送出事務,T2 因為抛異常不會被執行,就會導緻資料出現錯誤。
(2) 開啟 Spring 事務管理後
transfer上添加了 @Transactional 注解,在該方法上就會有一個事務 T。
AccountDao 的 outMoney 方法的事務 T1 加入到 transfer 的事務 T 中;
AccountDao 的 inMoney 方法的事務 T2 加入到 transfer 的事務 T 中。
這樣就保證他們在同一個事務中,當業務層中出現異常,整個事務就會復原,保證資料的準确性。
通過上面例子的分析,可以得到如下概念:
事務管理者:發起事務方,在 Spring 中通常指代業務層開啟事務的方法(如:transfer 方法)
事務協調員:加入事務方,在 Spring 中通常指代資料層方法(如:outMoney、inMoney 方法),也可以是業務層方法。
注意:
目前的事務管理的前提是 DataSourceTransactionManager 和 SqlSessionFactoryBean 使用的是同一個資料源。
3. Spring 事務屬性
3.1 事務配置
上面這些屬性都可以在
@Transactional
注解的參數上進行設定。
- readOnly:true 隻讀事務,false 讀寫事務,增删改要設為 false,查詢設為 true。
- timeout:設定逾時時間(機關:秒),在多長時間之内事務沒有送出成功就自動復原,-1 表示不設定逾時時間。
- rollbackFor:當出現指定異常時,進行事務復原。
- noRollbackFor:當出現指定異常時,不進行事務復原。
- rollbackForClassName 等同于 rollbackFor,隻不過屬性為異常的類全名字元串。
- noRollbackForClassName 等同于 noRollbackFor,隻不過屬性為異常的類全名字元串。
-
isolation 設定事務的隔離級别
DEFAULT:預設隔離級别, 會采用資料庫的隔離級别
READ_UNCOMMITTED:讀未送出
READ_COMMITTED:讀已送出
REPEATABLE_READ:重複讀取
SERIALIZABLE:串行化
rollbackFor 是當出現指定異常時,進行事務復原;對于異常事務不應該都復原麼,為什麼還要指定?
這塊需要更正一個知識點,并不是所有的異常都會復原事務,比如下面的代碼就不會復原,導緻轉出賬戶減錢成功,轉入賬戶加錢失敗。
出現這個問題的原因是:Spring 事務隻對 Error 異常和 RuntimeException 異常及其子類進行事務復原,其他的異常類型不復原。IOException 就是其他的異常類型,是以不復原。
此時就可以使用 rollbackFor 屬性來設定 IOException 異常復原:
3.2 案例:轉賬業務追加日志
需求:實作任意兩個賬戶間轉賬操作,并對每次轉賬操作在資料庫進行留痕
需求微縮:A賬戶減錢,B賬戶加錢,資料庫記錄日志
分析:
①:基于轉賬操作案例添加日志子產品,實作資料庫中記錄日志
②:業務層轉賬操作(transfer),調用減錢、加錢與記錄日志功能
實作效果預期:
無論轉賬操作是否成功,均進行轉賬操作的日志留痕
該環境是基于轉賬環境來完成的,下面在其基礎上繼續往下寫:
(1) 建立日志表
create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)
(2) 添加 LogDao 接口
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
(3) 添加 LogService 接口與實作類
public interface LogService {
void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Transactional
public void log(String out,String in,Double money ) {
logDao.log("轉賬操作由"+out+"到"+in+",金額:"+money);
}
}
(4) 在轉賬的業務中添加記錄日志
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按類型注入
private AccountDao accountDao;
@Autowired
private LogService logService;//添加日志相關
@Override
//開啟事務,出現IOException時復原
@Transactional
public void transfer(String out, String in, Double money){
try{
accountDao.outMoney(out,money);
//int i =1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);//添加日志相關
}
}
}
當程式正常運作,tbl_account 表中轉賬成功,tbl_log 表中日志記錄成功;
當轉賬業務之間出現異常(int i =1/0),轉賬失敗,tbl_account 成功復原,但 tbl_log 表未添加資料。
這個結果和我們想要的不一樣,什麼原因?該如何解決?
失敗原因:日志的記錄與轉賬操作隸屬同一個事務,同成功同失敗。
理想效果:無論轉賬操作是否成功,日志必須保留
3.3 事務傳播行為
前面講過 Spring 事務會把 T1、T2、T3 都加入到事務 T 中。
是以當轉賬失敗後,所有的事務都復原,導緻日志沒有記錄下來。
這與需求不符,此時就想能不能讓 log 方法單獨是一個事務呢。
要想解決這個問題,就需要用到事務傳播行為。
事務傳播行為:事務協調員對事務管理者所攜帶事務的處理态度。
具體如何解決,就需要用到 propagation 屬性。
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
//propagation設定事務屬性:傳播行為設定為目前操作需要新事務
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money) {
logDao.log("轉賬操作由"+out+"到"+in+",金額:"+money);
}
}
經過上面的修改,當轉賬失敗時,tbl_log 表中的日志記錄也會成功。但是記錄的内容也是 “轉賬操作由Tom到Jerry,金額:50.0”,這顯然不合理。于是 AccountServiceImpl 中做如下修改:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按類型注入
private AccountDao accountDao;
@Autowired
private LogService logService;//添加日志相關
@Override
@Transactional
public void transfer(String out, String in, Double money){
try{
accountDao.outMoney(out,money);
//int i = 1/0;
accountDao.inMoney(in,money);
logService.log(out,in,money);//添加日志相關
}catch (Exception e){
logService.log(out,in+"失敗",0D);//添加日志相關
throw new RuntimeException();//使事務復原
}
}
}
這樣,轉賬失敗時,記錄的日志就變為 “轉賬操作由Tom到Jerry失敗,金額:0.0“。
【注意】當 catch 捕獲了異常時,事務不會復原。如果非得寫 catch,需要 catch 後
throw new RuntimeException()
讓事務復原。
事務傳播行為的可選值: