在學習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方法時候就自動執行了命令,下面我們看看吧.
- 定義底層通用接口,實現交給框架開發者來實現。
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關於事務小編想說的。有問題可以提出,我們一起學習,如果有寫錯的地方或者想討論的,希望能提出,再次感謝!