深入淺出Mybatis系列(五)Mybatis事務篇


在學習Mysql事務開始,分為兩步。一.先看下Mysql的事務級別都有什么,然后看Mysql的事務級別設置命令。及常見問題。二.JDK是如何處理數據庫操作的呢? Mybatis是如何實現JDK定義的事務級別操作。

一.Mysql的事務級別及常見概念

  • MySQL事務隔離級別
事務隔離級別 臟讀 不可重復讀 幻讀 解釋
讀未提交(read-uncommitted) 可以讀到未提交的事物
不可重復讀(read-committed) 只能讀提交的事物
可重復讀(repeatable-read) 事務提交前后都能讀【MySql默認】
串行化(serializable) serializable時會鎖表,是最安全的,也是日常開發基本不會用
  • Mysql事務設置命令

  • 讀未提交
    set session transaction isolation level read uncommitted;

  • 不可重復讀(讀已提交的)
    set session transaction isolation level read committed;

  • 可重復讀
    set session transaction isolation level repeatable read;

  • 串行化
    set session transaction isolation level serializable;

舉一個例子,我做了一個事務未提交,默認情況是看不到的,當設置了讀未提交sql,就可以看到了

以上這四個事務級別最不常用的是串行化,因為他是最嚴格的會加行鎖或者表鎖。當一個事務在執行查詢操作,其他的連接就不允許對表進行寫操作,只允許讀。顯然這是不滿足開發的,想想一下,支付寶當一個人A在查詢賬單時候,其他人B就不能消費了。因為A在對賬單表進行查詢,B要對賬單表進行修改。

最適合的是可重復讀,因為只會加行級鎖。當兩個事務同時進行時,其中一個事務修改數據對另一個事務不會造成影響,即使修改的事務已經提交也不會對另一個事務造成影響。
(當一個事務執行中他看到的數據是不會改變的,即在一個事務中不論查詢數據多少次都不會改變,即便其他人對這個數據進行了修改,也看不到,只有在當前事務提交之后,然后在查詢才能看到其他人的改變,這就解決了不可重復讀和臟讀的問題,但是會造成幻讀)

  • √: 可能出現
  • ×: 不會出現
臟讀 不可重復讀 幻讀
Read uncommitted
Read committed ×
Repeatable read × ×
Serializable × ×
  • 什么是不可重復讀?

就是說在一個事務中,查詢了多次數據,每次看到的都不一樣。因為別人對數據進行了修改。

  • 事務的並發問題

1、臟讀:事務A讀取了事務B更新的數據,然后B回滾操作,那么A讀取到的數據是臟數據

2、不可重復讀:事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果 不一致。

3、幻讀:系統管理員A將數據庫中所有學生的成績從具體分數改為ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束后發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。

小結:不可重復讀的和幻讀很容易混淆,不可重復讀側重於修改,幻讀側重於新增或刪除。解決不可重復讀的問題只需鎖住滿足條件的行(Repeatable read),解決幻讀需要鎖表

  • 事務的基本要素(ACID)

1、原子性(Atomicity):事務開始后所有操作,要么全部做完,要么全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。

2、一致性(Consistency):事務開始前和結束后,數據庫的完整性約束沒有被破壞 。比如A向B轉賬,不可能A扣了錢,B卻沒收到。

3、隔離性(Isolation):同一時間,只允許一個事務請求同一數據,不同的事務之間彼此沒有任何干擾。比如A正在從一張銀行卡中取錢,在A取錢的過程結束前,B不能向這張卡轉賬。

4、持久性(Durability):事務完成后,事務對數據庫的所有更新將被保存到數據庫,不能回滾。

參考地址

二.JDK是如何處理數據庫操作的呢? Mybatis是如何實現JDK定義的事務級別操作。

JDK是如何處理數據庫操作的呢? 其實更直接操作MySql數據庫命令一樣,只不過Java進行了命令的封裝,是你在調用Java方法時候就自動執行了命令,下面我們看看吧.

  1. 定義底層通用接口,實現交給框架開發者來實現。

Connection。事務級JDK已經定義好了

public interface Connection  extends Wrapper, AutoCloseable {
    /** * 表示不支持事務的常量。 */
    int TRANSACTION_NONE             = 0;
   /** * 可讀到未提交 */
    int TRANSACTION_READ_UNCOMMITTED = 1;
    /** * 只能讀已提交 */
    int TRANSACTION_READ_COMMITTED   = 2;
   /** * 可重復讀 */
    int TRANSACTION_REPEATABLE_READ  = 4;
   /** * 串行化操作 */
    int TRANSACTION_SERIALIZABLE     = 8;
   /** * 設置事務 */
    void setTransactionIsolation(int level) throws SQLException;
}

留給開發者自己實現,下圖Mybatis中ConnectionImpl實現。在進行數據庫操作前,會先執行設置事務。

JdbcTransaction如何設置事務呢?

在打開數據庫連接時候設置事務

根據事務級別,執行不同的sql命令。

以上就是事務的底層實現,那么我們在帶到項目中來看看,以一個數據庫操作的例子來看看Mybatis的調用過程吧。

@Test
  public void transactionTest(){
    //拿到mybatis的配置文件輸入流
    InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
    //SqlSessionFactoryBuilder通過XMLConfigBuilder解析器讀取配置信息生成Configuration信息
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
    TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
    TUser tUser = new TUser();
    tUser.setId(10233);
    tUser.setName("事務測試");
    mapper.insert(tUser);
//    sqlSession.commit();
    sqlSession.rollback(true);
  }

SqlSession就是數據庫的會話,要想創建數據庫會話,首先要打開數據庫連接。由此來判斷事務級別就是在SqlSessionFactory,在獲取SqlSession的時候來執行sql語句的。我們看流程圖

此時事務還並沒有設置,只有在
SimpleExecutor執行任意數據庫操作時候才會調用

@Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      //執行事務
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }
 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //JdbcTransaction打開連接。
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

最終調用
JdbcTransaction打開連接。

以上就是Mybatis中的事務級別控制。但是這里所說的除了在學習源碼時候會看到,在日常開發中不會經常看到。大部分Java的開發都是基於Spring框架之上,Spring對事務的處理有進一步的處理。Spring提出了事務傳播方式的概念,這個概念怎么理解呢?

Spring的事務傳播方式

前面所說的事務,是不涉及嵌套條調用的,即是不會再事務中嵌套事務調用。但是在開發中難免會遇到這種情況。
比如,一個事務在沒有執行commit之前,有調用了一個事務。
那這個時候,遇到兩個都有事務的方法怎么辦呢,因為Spring提出了事務傳播方式這個概念。

1、PROPAGATION_REQUIRED:如果當前沒有事務,就創建一個新事務,如果當前存在事務,就加入該事務,該設置是最常用的設置。

2、PROPAGATION_SUPPORTS:支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就以非事務執行。‘

3、PROPAGATION_MANDATORY:支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就拋出異常。

4、PROPAGATION_REQUIRES_NEW:創建新事務,無論當前存不存在事務,都創建新事務。

5、PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

6、PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則拋出異常。

7、PROPAGATION_NESTED:如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

eg:

當前是有事務的,如果調用方法中也有一個含有事務的接口,那么就放棄里面的事務,而加入到當前的事務

   @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
   public String doOpt() throws Exception {
	    ...
   }

Spring的事務傳播方式的實現類

AbstractPlatformTransactionManager使用的技術手段就是代理。

其實就是set autocommit=off; 當方法執行完成無指定的異常,在進行commit;

下面我們自己來寫偽代碼

MyTransactional

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTransactional {
  //傳播方式
  Propagation propagation() default Propagation.REQUIRED;
  //捕捉異常
  Class<? extends Throwable>[] rollbackFor() default {};
}

SqlSession保證每個線程一個連接實例


public class SqlSession {

  private static ThreadLocal<Connection> connections = new ThreadLocal();

  static {
    try {//注冊驅動,反射方式加載
      Class.forName("com.mysql.jdbc.Driver");
    } catch (Exception e) {
    }
  }

  public SqlSession(boolean autocommit, int level) {
    try {
      String url = "jdbc:mysql://127.0.0.1:3306/test?useSSL=false";
      //設置用戶名
      String username = "root";
      //設置密碼
      String password = "root";
      //獲得連接對象
      Connection con = DriverManager.getConnection(url, username, password);
      con.setAutoCommit(autocommit);
      setTransactionIsolation(level, con.createStatement());
      connections.set(con);
    } catch (Exception e) {
    }
  }


  public void execute(String sql) {
    try {
      Console.normal("執行Sql: " + sql);
// connections.get().createStatement().execute(sql)
    } catch (Exception e) {
    }
  }

  private static void setTransactionIsolation(int level, Statement statement) throws SQLException {
    String sql;
    switch (level) {
      case 0:
        throw SQLError.createSQLException(Messages.getString("Connection.24"), null);
      case 1:
        sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
        break;
      case 2:
        sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
        break;
      case 3:
      case 5:
      case 6:
      case 7:
      default:
        throw SQLError.createSQLException(Messages.getString("Connection.25", new Object[]{level}), "S1C00", null);
      case 4:
        sql = "SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ";
        break;
      case 8:
        sql = "SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE";
    }
    Console.customerNormal("設置事務級別Sql: ", sql);
// statement.execute(sql);
  }

  public void commit() throws Exception {
// Connection connection = connections.get();
// connection.commit();
    Console.customerNormal("執行提交Sql:", "commit;");
  }

  public void rollback() throws Exception {
// Connection connection = connections.get();
// connection.rollback();
    Console.customerNormal("設置事務級別Sql: ", "roolback");
  }

}

UserService

public interface UserService {
  void save();
}
public class UserServiceImpl implements UserService {
  SqlSession sqlSession = new SqlSession(false, 4);
  @MyTransactional(propagation = Propagation.REQUIRED, rollbackFor = ArithmeticException.class)
  public void save() {
    //執行sql1
    sqlSession.execute("select * from t");
    try {
      int i = 2 / 0;
    } catch (Exception e) {
      throw e;
    }
    //執行sql2
    sqlSession.execute("select * from t");
  }

測試代碼

 public static void main(String[] args) {
    
    UserService userService = new UserServiceImpl();
    //生成JDK代理
    UserService userServiceProxy = (UserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{UserService.class}, new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method declaredMethod = userService.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
        MyTransactional declaredAnnotation = declaredMethod.getDeclaredAnnotation(MyTransactional.class);
        Class<? extends Throwable>[] classes = declaredAnnotation.rollbackFor();
        //傳播方式處理邏輯
        //.....
        Object res = null;
        try {
          res = declaredMethod.invoke(userService);
        } catch (Exception e) {
          Throwable cause = e.getCause();
          if (cause.getClass().equals(classes[0])) {
            //執行回滾
            ((UserServiceImpl) userService).sqlSession.rollback();
            return res;
          }
        }
        //執行提交
        ((UserServiceImpl) userService).sqlSession.commit();
        return res;
      }
    });
    userServiceProxy.save();
  }

結果

  • 成功
[設置事務級別Sql: ]: SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
[正常]: 執行Sql: update T_USER set name = 'china' where id = 1;
[正常]: 執行Sql: update T_USER set name = '中國' where id = 1;
[執行提交Sql:]: commit;
  • 失敗
[設置事務級別Sql: ]: SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
[正常]: 執行Sql: update T_USER set name = 'china' where id = 1;
[設置事務級別Sql: ]: roolback

以上就是Mybatis關於事務小編想說的。有問題可以提出,我們一起學習,如果有寫錯的地方或者想討論的,希望能提出,再次感謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM