事務—【01】Spring事務管理介紹以及SpringBoot+Druid+MyBatis單數據源事務管理實現

前置知識

  • 簡單介紹 詳解自行google.

事務是什麼?html

  • 事務是一種可靠、一致的方式,訪問和操做數據庫中的程序單元

事務的特性java

  • 原子性:要麼作,要麼不作
  • 一致性:一致性是指事務必須使數據庫從一個一致性狀態變換到另外一個一致性狀態
  • 持久性:持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的。
  • 隔離性:不一樣的事務操做的互相不干擾

併發事務的問題mysql

  1. 髒讀:髒讀是指在一個事務處理過程裏讀取了另外一個未提交的事務中的數據,針對對某一項數據
  2. 不可重複讀:一個事務範圍內屢次查詢卻返回了不一樣的數據值,是因爲在查詢間隔,被另外一個事務修改並提交了。
  3. 幻讀:是指在事務執行過程當中屢次查詢出來的數據數,不一致,好比經過where篩選第二次比第一次多幾條別的事務新插入的數據,幻讀針對的是一批數據總體

事務的隔離級別git

  • READ_UNCOMMITTED 讀未提交:能夠讀到沒有沒有提交事務作出的數據修改內容(隔離級別最低,併發性能高)
  • READ_COMMITTED 讀已提交:只能讀到事務以及提交的數據(鎖定正在讀取的行)
  • REPEATABLE_READ 可重複讀:保證在一個事務中 屢次讀一樣的數據是一致的。不會被其餘事務所影響(鎖定所讀取的全部行)
  • SERIALIZABLE 可串行化:事務串行化順序執行,

Spring事務

  • Spring提供了統一的事務API來支持不一樣的資源類型
  • Spring也提供了聲明式事務管理的方式,與業務代碼解耦
  • 很容易和Spring生態框架整合
  • 支持多資源的事務管理與同步

1. 事務抽象

1.1 PlatformTransactionManager

  • 事務管理器:主要提供提交操做、回滾操做以及根據事務定義獲取事務狀態的統一接口
public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;

}
複製代碼

1.2 TransactionDefinition

  • 事務定義:事務的一些基礎信息,如超時時間、隔離級別、傳播屬性等

事務傳播機制,也就是在事務在多個方法的調用中是如何傳遞的;如:兩個service的方法都是事務操做,一個事務方法調用另外一個事務方法如何進行處理?github

public interface TransactionDefinition {

    // 事務的傳播機制
    // 若是調用方有事務則使用調用方的事務,若是不存在事務則建立一個事務
	int PROPAGATION_REQUIRED = 0;

    // 跟隨調用方,若是調用方有 那就用,調用方沒有那就不用
	int PROPAGATION_SUPPORTS = 1;

    // 調用方的方法必須運行在一個事務中,不存在事務則拋出異常
	int PROPAGATION_MANDATORY = 2;

    // 無論調用方是否有事務執行,本身都要起一個新事務。把原先的事務掛起,這個只在jta的事務管理器中起做用(事務是不支持嵌套的)
	int PROPAGATION_REQUIRES_NEW = 3;

    // 即便調用方有事務,我也要在非事務中執行,把原先的事務掛起
	int PROPAGATION_NOT_SUPPORTED = 4; 

    // 毫不在事務裏面執行
	int PROPAGATION_NEVER = 5;

    // 嵌套事務(實際是不支持的,是利用存盤點來實現,把調用方以前執行的存盤點,而後相似於再開啓一個事務執行,執行完畢後恢復存盤點,繼續執行。JDBC3.0以上支持)
	int PROPAGATION_NESTED = 6;

    // 如下定義的是事務的隔離機制
    // 根據數據庫的默認的隔離機制而定
	int ISOLATION_DEFAULT = -1;

    // 讀未提交
	int ISOLATION_READ_UNCOMMITTED = 1;  // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

    // 讀已提交
	int ISOLATION_READ_COMMITTED = 2;  // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;

    // 可重複讀
	int ISOLATION_REPEATABLE_READ = 4;  // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

    // 串行化
	int ISOLATION_SERIALIZABLE = 8;  // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


    // 默認超時時間,以數據庫設定爲準
	int TIMEOUT_DEFAULT = -1;

    // 獲取事務的傳播屬性
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}

    // 獲取事務的隔離級別
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}

    // 獲取事務超時時間
	default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

    // 事務是否只讀。
	default boolean isReadOnly() {
		return false;
	}

    // 獲取事務的名稱
	@Nullable
	default String getName() {
		return null;
	}

    // 返回一個默認事務定義
	static TransactionDefinition withDefaults() {
		return StaticTransactionDefinition.INSTANCE;
	}

}

複製代碼

1.3 TransactionStatus

  • 事務狀態:事務的一些狀態信息,如是不是一個新的事務、是否已被標記爲回滾
// Savepoiont就是在Nested這種傳播機制中提供保存點機制來實現嵌套事務,出錯的時候能夠選擇恢復到保存點
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    // 是否有保存點
	boolean hasSavepoint();

	@Override
	void flush();
}
複製代碼
public interface TransactionExecution {
    // 是不是一個新事務
	boolean isNewTransaction();

	// 設置事務回滾
	void setRollbackOnly();
    
    // 事務是否回滾
	boolean isRollbackOnly();
    
    // 獲取事務是否完成 
	boolean isCompleted();

}
複製代碼

2. PlatformTransactionManager常見的實現

  • DataSourceTransactionManager (用於JDBC Template、MyBatis等)
  • JpaTransactionManager (用於data-jpa、hibernate等)
  • JmsTransactionManager (用於消息中間件等)
  • JtaTransactionManager (主要用於分佈式事務)

3. Spring單數據源事務實際案例

3.1 環境說明:

3.2 實例代碼

  • 此案例額基於轉帳業務實現 PlatformTransactionManager則是DataSourceTransactionManager
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDAO accountDAO;
    @Autowired
    PlatformTransactionManager transactionManager;
    /** * 聲明式事務 * propagation = Propagation.REQUIRED (默認值就是REQUIRED) 若是調用方有事務就直接使用調用方的事務,若是沒有就新建一個事務 * transactionManager = "transactionManager" 也是默認值 * isolation= Isolation.DEFAULT 隔離級別 * 還有timeout等參數 可自行查看Transactional的源碼 裏面都有說明 * @param sourceAccountId 源帳戶 * @param targetAccountId 目標帳戶 * @param amount 金額 * @return 操做結果信息 */
    @Override
    @Transactional(transactionManager = "transactionManager",propagation = Propagation.REQUIRED, rollbackFor = Exception.class, isolation= Isolation.DEFAULT)
    public String transferAnnotation(Long sourceAccountId, Long targetAccountId, BigDecimal amount) {
        AccountDO sourceAccountDO = accountDAO.selectByPrimaryKey(sourceAccountId);
        AccountDO targetAccountDO = accountDAO.selectByPrimaryKey(targetAccountId);
        if (null == sourceAccountDO || null == targetAccountDO) {
            return "轉入或者轉出帳戶不存在";
        }
        if (sourceAccountDO.getBalance().compareTo(amount) < 0) {
            return "轉出帳戶餘額不足";
        }
        sourceAccountDO.setBalance(sourceAccountDO.getBalance().subtract(amount));
        accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
        // error("annotation error!");
        targetAccountDO.setBalance(targetAccountDO.getBalance().add(amount));
        accountDAO.updateByPrimaryKeySelective(targetAccountDO);
        return "轉帳成功!";
    }

    @Override
    public String transferCode(Long sourceAccountId, Long targetAccountId, BigDecimal amount) {
        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        // 獲取事務 開始業務執行
        TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
        try {
            AccountDO targetAccountDO = accountDAO.selectByPrimaryKey(targetAccountId);
            AccountDO sourceAccountDO = accountDAO.selectByPrimaryKey(sourceAccountId);
            if (null == sourceAccountDO || null == targetAccountDO) {
                return "轉入或者轉出帳戶不存在";
            }
            error("code error");
            if (sourceAccountDO.getBalance().compareTo(amount) < 0) {
                return "轉出帳戶餘額不足";
            }
            sourceAccountDO.setBalance(sourceAccountDO.getBalance().subtract(amount));
            targetAccountDO.setBalance(targetAccountDO.getBalance().add(amount));
            accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
            accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
            // 提交事務
            transactionManager.commit(transaction);
            return "轉帳成功!";
        } catch (Exception e) {
            log.error("轉帳發生錯誤,開始回滾,source: {}, target: {}, amount: {}, errMsg: {}",
                    sourceAccountId, targetAccountId, amount, e.getMessage());
            // 報錯回滾
            transactionManager.rollback(transaction);
        }
        return "轉帳失敗";
    }

    @Override
    public List<AccountDO> listAll() {
        return accountDAO.selectAll();
    }


    private static void error(String msg) {
        throw new RuntimeException(msg);
    }
}

複製代碼

3.3 Transactional註解實現事務

  • 使用註解式事務,Spring會利用代理實現一個代理類,web

  • 而後從上下文中獲得事務管理器,開啓一個事務後執行業務代碼,spring

  • 若是知足你設置的回滾異常條件,就執行rollbacksql

  • 咱們調用的時候是直接調用的Service的方法數據庫

  • 可是Spring在實現的時候,實際是經過AOP Proxy(AOP 代理服務)來調用Transaction Advisor(作事務管理的)而後來處理調用註解式事務的service方法apache


  • 咱們經過接口/api/account/transfer/annotation?source=1&target=2&amount=123 觸發帳戶1轉給帳戶2 123塊錢,轉帳操做
  • 日誌信息(開啓DEBUG日誌):
o.s.web.servlet.DispatcherServlet        : GET "/api/account/transfer/annotation?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.ilss.transaction.onedatasource.web.AccountController#transferAnnotation(Long, Long, String)
o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [io.ilss.transaction.onedatasource.service.impl.AccountServiceImpl.transferAnnotation]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'transactionManager',-java.lang.Exception
o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] for JDBC transaction
o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] to manual commit
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
o.m.s.t.SpringManagedTransaction         : JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] will be managed by Spring
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 1(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 2(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==>  Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==> Parameters: 小一(String), xiaoyi(String), 123456(String), 877.00(BigDecimal), 2020-01-09T17:04:28(LocalDateTime), 2020-01-09T17:44:33(LocalDateTime), 1(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective  : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==>  Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==> Parameters: 小二(String), xiaoer(String), 123456(String), 223.00(BigDecimal), 2020-01-09T17:04:40(LocalDateTime), 2020-01-09T17:04:40(LocalDateTime), 2(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective  : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@185f0a96]
o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] after transaction
m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
m.m.a.RequestResponseBodyMethodProcessor : Writing ["轉帳成功!"]
o.s.web.servlet.DispatcherServlet        : Completed 200 OK

複製代碼
  • 成功執行過程
  1. GET 請求到 /api/account/transfer/annotation接口 而後調用service方法
  2. 根據寫的Transactional註解的設置 建立一個新事務
  3. 選用JDBC鏈接 而後建立 SqlSession
  4. 爲SqlSession註冊事務同步
  5. SQL操做
  6. 而後把事務提交
  7. 最後釋放資源。完成方法調用
  8. 接口返回200
  • 經過日誌可看出咱們使用MyBatis+Druid 配置數據源 事務管理實際是使用的DataSourceTransactionManager.

  • 雖然代碼中指定了transactionManager,但實際卻沒有增長任何Bean註冊或者其餘有關DataSource的事務管理器代碼。這個地方是由於Spring幫你作了,我設置transactionManager是由於自動裝配的transactionManager名字就是這個。寫出來提醒一下有這個配置,這個配置顧名思義就是配置對這個事務的管理器實現。簡而言之就是PlatformTransactionManager的指定。後面多數據源的時候咱們會經過指定不一樣的transactionManager來實現事務管理。

  • 若是報錯會是怎麼樣呢?

  • 先看日誌(開啓DEBUG日誌):

o.s.web.servlet.DispatcherServlet        : GET "/api/account/transfer/annotation?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.ilss.transaction.onedatasource.web.AccountController#transferAnnotation(Long, Long, String)
o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [io.ilss.transaction.onedatasource.service.impl.AccountServiceImpl.transferAnnotation]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'transactionManager',-java.lang.Exception
o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] for JDBC transaction
o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] to manual commit
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
o.m.s.t.SpringManagedTransaction         : JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] will be managed by Spring
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 1(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58] from current transaction
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 2(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==>  Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==> Parameters: 小一(String), xiaoyi(String), 123456(String), 754.00(BigDecimal), 2020-01-09T17:04:28(LocalDateTime), 2020-01-09T17:44:33(LocalDateTime), 1(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective  : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec]
o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] after transaction
o.s.web.servlet.DispatcherServlet        : Failed to complete request: java.lang.RuntimeException: annotation error!
o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is 
java.lang.RuntimeException: annotation error!
                            .......
o.a.c.c.C.[Tomcat].[localhost]           : Processing ErrorPage[errorCode=0, location=/error]
o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, text/html;q=0.8]
o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500

複製代碼
  • 失敗執行過程
  1. GET 請求到 /api/account/transfer/annotation接口 而後調用service方法
  2. 根據寫的Transactional註解的設置 建立一個新事務
  3. 選用JDBC鏈接 而後建立 SqlSession
  4. 爲SqlSession註冊事務同步
  5. SQL操做:能夠從日誌看到,執行到第一個跟新操做發生錯誤
  6. 直接回滾Rolling back JDBC transaction on Connection後打印錯誤日誌。後面的第二個更新操做也就沒了
  7. 方法異常退出,接口500

3.4 編程實現事務管理

  • 編程式事務主要步驟
  1. 建立事務定義TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
  2. 設置事務屬性:如傳播屬性、隔離屬性等
  3. 經過TransactionManager開啓事務
  4. 執行業務代碼
  5. 提交事務 / 處理回滾
  • 此實例的編程式事務的執行基本和聲明式事務相同,有興趣能夠自行下載代碼本身實現。

// todo: 下一篇多數據源事務管理


關於我

  • 座標杭州,普通本科在讀,計算機科學與技術專業,20年6月畢業,瘋狂找工做中。。。。
  • 目前處於菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎你們和我交流鴨!!!
相關文章
相關標籤/搜索