天天看點

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

文章目錄

  • 1. Spring 事務簡介
  • 2. Spring 事務角色
  • 3. Spring 事務屬性
    • 3.1 事務配置
    • 3.2 案例:轉賬業務追加日志
    • 3.3 事務傳播行為

1. Spring 事務簡介

Spring 事務作用:在資料層或業務層保障一系列的資料庫操作同成功、同失敗。

資料層有事務我們可以了解,為什麼業務層也需要處理事務呢?

舉個簡單的例子:

轉賬業務會有兩次資料層的調用,一次是轉入者加錢,一次是轉出者減錢;

把事務放在資料層,加錢和減錢就有兩個事務;

如果先加錢後減錢,且加錢之後其他地方出現異常,就會出現加錢成功減錢失敗的結果。即沒辦法保證加錢和減錢同時成功或者同時失敗;

這個時候就需要将事務放在業務層進行處理。

不用事務時,我們是這樣處理的:

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. 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。

這是正常情況下的運作結果,但是如果在轉賬的過程中出現了異常,如:

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

這個時候就模拟了轉賬過程中出現異常的情況,正确的操作應該是 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事務之前:

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

AccountDao 的 outMoney 因為是修改操作,會開啟一個事務 T1;

AccountDao 的 inMoney 因為是修改操作,會開啟一個事務 T2;

AccountService 的 transfer 沒有事務。

運作過程中如果沒有抛出異常,則 T1 和 T2 都正常送出,資料正确;

如果在兩個方法中間抛出異常,T1 因為執行成功送出事務,T2 因為抛異常不會被執行,就會導緻資料出現錯誤。

(2) 開啟 Spring 事務管理後

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. 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 事務配置

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

上面這些屬性都可以在

@Transactional

注解的參數上進行設定。

  • readOnly:true 隻讀事務,false 讀寫事務,增删改要設為 false,查詢設為 true。
  • timeout:設定逾時時間(機關:秒),在多長時間之内事務沒有送出成功就自動復原,-1 表示不設定逾時時間。
  • rollbackFor:當出現指定異常時,進行事務復原。
  • noRollbackFor:當出現指定異常時,不進行事務復原。
  • rollbackForClassName 等同于 rollbackFor,隻不過屬性為異常的類全名字元串。
  • noRollbackForClassName 等同于 noRollbackFor,隻不過屬性為異常的類全名字元串。
  • isolation 設定事務的隔離級别

    DEFAULT:預設隔離級别, 會采用資料庫的隔離級别

    READ_UNCOMMITTED:讀未送出

    READ_COMMITTED:讀已送出

    REPEATABLE_READ:重複讀取

    SERIALIZABLE:串行化

rollbackFor 是當出現指定異常時,進行事務復原;對于異常事務不應該都復原麼,為什麼還要指定?

這塊需要更正一個知識點,并不是所有的異常都會復原事務,比如下面的代碼就不會復原,導緻轉出賬戶減錢成功,轉入賬戶加錢失敗。

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

出現這個問題的原因是:Spring 事務隻對 Error 異常和 RuntimeException 異常及其子類進行事務復原,其他的異常類型不復原。IOException 就是其他的異常類型,是以不復原。

此時就可以使用 rollbackFor 屬性來設定 IOException 異常復原:

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

3.2 案例:轉賬業務追加日志

需求:實作任意兩個賬戶間轉賬操作,并對每次轉賬操作在資料庫進行留痕

需求微縮:A賬戶減錢,B賬戶加錢,資料庫記錄日志

分析:

①:基于轉賬操作案例添加日志子產品,實作資料庫中記錄日志

②:業務層轉賬操作(transfer),調用減錢、加錢與記錄日志功能

實作效果預期:

無論轉賬操作是否成功,均進行轉賬操作的日志留痕

該環境是基于轉賬環境來完成的,下面在其基礎上繼續往下寫:

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

(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);//添加日志相關
        }
    }
}
           
5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

當程式正常運作,tbl_account 表中轉賬成功,tbl_log 表中日志記錄成功;

當轉賬業務之間出現異常(int i =1/0),轉賬失敗,tbl_account 成功復原,但 tbl_log 表未添加資料。

這個結果和我們想要的不一樣,什麼原因?該如何解決?

失敗原因:日志的記錄與轉賬操作隸屬同一個事務,同成功同失敗。

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

理想效果:無論轉賬操作是否成功,日志必須保留

3.3 事務傳播行為

前面講過 Spring 事務會把 T1、T2、T3 都加入到事務 T 中。

是以當轉賬失敗後,所有的事務都復原,導緻日志沒有記錄下來。

這與需求不符,此時就想能不能讓 log 方法單獨是一個事務呢。

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

要想解決這個問題,就需要用到事務傳播行為。

事務傳播行為:事務協調員對事務管理者所攜帶事務的處理态度。

具體如何解決,就需要用到 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()

讓事務復原。

事務傳播行為的可選值:

5. Spring 事務1. Spring 事務簡介2. Spring 事務角色3. Spring 事務屬性

繼續閱讀