Transaction 那點事兒(一)

這篇博文已經「難產」好幾天了,壓力仍是有些大的,由於 Transaction(事務管理)的問題,爭論一直就沒有中止過。因爲我的能力真的很是有限,花了好多功夫去學習,總算基本上解決了問題,因此這才第一時間就拿出來與網友們共享,也聽聽你們的想法。java

提示:對 Transaction 不太理解的朋友們,可閱讀這篇博文《Transaction 那點事兒》。sql

如今就開始吧!設計模式

請看下面這一段代碼:安全

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public class ProductServiceImpl extends BaseService implements ProductService {
 
     ...
 
     @Override
     public boolean createProduct(Map<String, Object> productFieldMap) {
         String sql = SQLHelper.getSQL( "insert.product" );
         Object[] params = {
             productFieldMap.get( "productTypeId" ),
             productFieldMap.get( "productName" ),
             productFieldMap.get( "productCode" ),
             productFieldMap.get( "price" ),
             productFieldMap.get( "description" )
         };
         int rows = DBHelper.update(sql, params);
         return rows == 1 ;
     }
}

咱們先不去考慮 createProduct() 方法中那段不夠優雅的代碼,總之這一坨 shi 就是爲了完成一個 insert 語句的,後續我會將其簡化。框架

除此之外,你們可能已經看出一些問題。沒有事務管理!ide

若是執行過程當中拋出了一個異常,事務沒法回滾。這個案例僅僅是一條 SQL 語句,若是是多條呢?前面的執行成功了,就最後一條執行失敗,那應該是整個事務都要回滾,前面作的都不算數纔對。性能

爲了實現這個目標,我山寨了 Spring 的作法,它有一個 @Transactional 註解,能夠標註在方法上,那麼被標註的方法就是具有事務特性了,還能夠設置事務傳播方式與隔離級別等功能,確實夠強大的,徹底取代了之前的 XML 配置方式。單元測試

因而我也作了一個 @Transaction 註解(注意:我這裏是事務的名詞,Spring 用的是形容詞),代碼以下:學習

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Bean
public class ProductServiceImpl extends BaseService implements ProductService {
 
    ...
 
     @Override
     @Transaction
     public boolean createProduct(Map<String, Object> productFieldMap) {
         String sql = SQLHelper.getSQL( "insert.product" );
         Object[] params = {
             productFieldMap.get( "productTypeId" ),
             productFieldMap.get( "productName" ),
             productFieldMap.get( "productCode" ),
             productFieldMap.get( "price" ),
             productFieldMap.get( "description" )
         };
         int rows = DBHelper.update(sql, params);
         if ( true ) {
             throw new RuntimeException( "Insert log failure!" ); // 故意拋出異常,讓事務回滾
         }
         return rows == 1 ;
     }
}

在執行 DBHelper.update() 方法之後,我故意拋出了一個 RuntimeException,我想看看事務可否回滾,也就是那條 insert 語句沒有生效。測試

作了一個單元測試,測了一把,果真報錯了,product 表裏也沒有插入任何數據。

看來事務管理功能的確生效了,那麼,我是如何實現 @Transaction 這個註解所具備的功能?請接着往下看,下面的纔是精華所在。

一開始我修改了 DBHelper 的代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class DBHelper {
 
     private static final BasicDataSource ds = new BasicDataSource();
     private static final QueryRunner runner = new QueryRunner(ds);
 
     // 定義一個局部線程變量(使每一個線程都擁有本身的鏈接)
     private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();
 
     static {
         System.out.println( "Init DBHelper..." );
 
         // 初始化數據源
         ds.setDriverClassName(ConfigHelper.getStringProperty( "jdbc.driver" ));
         ds.setUrl(ConfigHelper.getStringProperty( "jdbc.url" ));
         ds.setUsername(ConfigHelper.getStringProperty( "jdbc.username" ));
         ds.setPassword(ConfigHelper.getStringProperty( "jdbc.password" ));
         ds.setMaxActive(ConfigHelper.getNumberProperty( "jdbc.max.active" ));
         ds.setMaxIdle(ConfigHelper.getNumberProperty( "jdbc.max.idle" ));
     }
 
     // 獲取數據源
     public static DataSource getDataSource() {
         return ds;
     }
 
     // 開啓事務
     public static void beginTransaction() {
         Connection conn = connContainer.get();
         if (conn == null ) {
             try {
                 conn = ds.getConnection();
                 conn.setAutoCommit( false );
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
                 connContainer.set(conn);
             }
         }
     }
 
     // 提交事務
     public static void commitTransaction() {
         Connection conn = connContainer.get();
         if (conn != null ) {
             try {
                 conn.commit();
                 conn.close();
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
                 connContainer.remove();
             }
         }
     }
 
     // 回滾事務
     public static void rollbackTransaction() {
         Connection conn = connContainer.get();
         if (conn != null ) {
             try {
                 conn.rollback();
                 conn.close();
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
                 connContainer.remove();
             }
         }
     }
 
     ...
 
     // 執行更新(包括 UPDATE、INSERT、DELETE)
     public static int update(String sql, Object... params) {
         // 若當前線程中存在鏈接,則傳入(用於事務處理),不然將從數據源中獲取鏈接
         Connection conn = connContainer.get();
         return DBUtil.update(runner, conn, sql, params);
     }
}

首先,我將 Connection 放到 ThreadLocal 容器中了,這樣每一個線程之間對 Connection 的訪問就是隔離的了(不會共享),保證了線程安全。

而後,我增長了幾個關於事務的方法,例如:beginTransaction()、commitTransaction()、rollbackTransaction(),這三個方法中的代碼很是重要,必定要細看!我就不解釋了。 

最後,我修改了 update() 方法,先從 ThreadLocal 中拿出 Connection,而後傳入到 DBUtil.update() 方法中。注意:有可能從 ThreadLocal 中根本拿不到 Connection,由於此時的 Connection 是從 DataSource 中獲取的(這是非事務的狀況),只要執行了 beginTransaction() 方法,就會從 DataSource 中獲取一個 Connection,而後將事務自動提交功能關閉,最後往 ThreadLocal 中放入一個 Connection。

提示:對 ThreadLocal 不太理解的朋友們,可閱讀這篇博文《ThreadLocal 那點事兒》。

那問題來了,DBUtil 又是如何處理事務的呢?我對 DBUtil 是這樣修改的:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DBUtil {
 
     ...
 
     // 更新(包括 UPDATE、INSERT、DELETE,返回受影響的行數)
     public static int update(QueryRunner runner, Connection conn, String sql, Object... params) {
         int result = 0 ;
         try {
             if (conn != null ) {
                 result = runner.update(conn, sql, params);
             } else {
                 result = runner.update(sql, params);
             }
         } catch (SQLException e) {
             e.printStackTrace();
         }
         return result;
     }
}

這裏,我首先對傳入進來的 Connection 對象進行判斷:

若不爲空(事務狀況),調用 runner.update(conn, sql, params) 方法,將 conn 傳遞到 QueryRunner 中,也就是說,徹底交給 Apache Commons DbUtils 來處理事務了,由於此時的 conn 是動過手腳的(在 beginTransaction() 方法中,作了 conn.setAutoCommit(false) 操做)。

若爲空(非事務狀況),調用 runner.update(sql, params) 方法,此時沒有將 conn 傳遞到 QueryRunner 中,也就是說,Connection 由 Apache Commons DbUtils 從 DataSource 中獲取,無需考慮事務問題,或者說,事務是自動提交的。

我想到這裏,我已經解釋清楚了。但還有必要再作一下總結:

獲取 Connection 分兩種狀況,若自動從 DataSource 中獲取,則爲非事務狀況;反之,從關閉 Connection 自動提交功能後,強制傳入 Connection 時,則爲事務狀況。由於傳遞過去的是同一個 Connection,那麼 Apache Commons DbUtils 是不會自動從 DataSource 中獲取 Connection 了。 

好了,地基終於建設完畢,剩下的就是何時調用那些 xxxTransaction() 方法呢?又是在哪裏調用的呢?

最簡單又最直接的方式莫過於此:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Bean
public class ProductServiceImpl extends BaseService implements ProductService {
 
     ...
 
     public boolean createProduct(Map<String, Object> productFieldMap) {
         int rows = 0 ;
         try {
             // 開啓事務
             DBHelper.beginTransaction();
 
             String sql = SQLHelper.getSQL( "insert.product" );
             Object[] params = {
                 productFieldMap.get( "productTypeId" ),
                 productFieldMap.get( "productName" ),
                 productFieldMap.get( "productCode" ),
                 productFieldMap.get( "price" ),
                 productFieldMap.get( "description" )
             };
             rows = DBHelper.update(sql, params);
         } catch (Exception e) {
             // 回滾事務
             DBHelper.rollbackTransaction();
 
             e.printStackTrace();
             throw new RuntimeException();
         } finally {
             // 提交事務
             DBHelper.commitTransaction();
         }
         return rows == 1 ;
     }
}

但這樣寫,總感受太累贅,之後凡是須要考慮事務問題的,都要用一個 try...catch...finally 語句來處理,還要手工調用那些 DBHelper.xxxTransaction() 方法。對於開發人員而言,簡直這就像噩夢!

這裏就要用到一點設計模式了,我選擇了「Proxy 模式」,就是「代理模式」,說準確一點應該是「動態代理模式」。

提示:對 Proxy 不太理解的朋友,可閱讀這篇博文《Proxy 那點事兒》。

我想把一頭一尾的代碼都放在 Proxy 中,這裏僅保留最核心的邏輯。代理類會自動攔截到 Service 類中全部的方法,先判斷該方法是否帶有 @Transaction 註解,若是有的話,就開啓事務,而後調用方法,最後提交事務,遇到異常還要回滾事務。若沒有 @Transaction 註解呢?什麼都不作,直接調用目標方法便可。

這就是個人思路,下面看看這個動態代理類是如何實現的吧:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TransactionProxy implements MethodInterceptor {
 
     private static TransactionProxy instance = new TransactionProxy();
 
     private TransactionProxy() {
     }
 
     public static TransactionProxy getInstance() {
         return instance;
     }
 
     @SuppressWarnings ( "unchecked" )
     public <T> T getProxy(Class<T> cls) {
         return (T) Enhancer.create(cls, this );
     }
 
     @Override
     public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
         Object result;
         if (method.isAnnotationPresent(Transaction. class )) {
             try {
                 // 開啓事務
                 DBHelper.beginTransaction();
 
                 // 執行操做
                 method.setAccessible( true );
                 result = proxy.invokeSuper(obj, args);
 
                 // 提交事務
                 DBHelper.commitTransaction();
             } catch (Exception e) {
                 // 回滾事務
                 DBHelper.rollbackTransaction();
 
                 e.printStackTrace();
                 throw new RuntimeException();
             }
         } else {
             result = proxy.invokeSuper(obj, args);
         }
         return result;
     }
}

我選用的是 CGLib 類庫實現的動態代理,由於我認爲它比 JDK 提供的動態代理更爲強大一些,它能夠代理沒有接口的類,而 JDK 的動態代理是有限制的,目標類必須實現接口才能被代理。

在這個 TransactionProxy 類中還用到了「Singleton 模式」,做用是提升一些性能,同時也簡化了 API 調用方式。

下面是最重要的地方了,如何才能將這些具備事務的 Service 類加入 IoC 容器呢?這樣在 Action 中注入的 Service 就再也不是普通的實現類了,而是經過 CGLib 動態生成的實現類(能夠在 IDE 中打個斷點看看就知道)。

好了,看看負責 IoC 容器的 BeanHelper吧,我又是如何修改的呢?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class BeanHelper {
 
     // Bean 類 => Bean 實例
     private static final Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
 
     static {
         System.out.println( "Init BeanHelper..." );
 
         try {
             // 獲取並遍歷全部的 Bean(帶有 @Bean 註解的類)
             List<Class<?>> beanClassList = ClassHelper.getClassListByAnnotation(Bean. class );
             for (Class<?> beanClass : beanClassList) {
                 // 建立 Bean 實例
                 Object beanInstance;
                 if (BaseService. class .isAssignableFrom(beanClass)) {
                     // 若爲 Service 類,則獲取動態代理實例(可使用 CGLib 動態代理,不能使用 JDK 動態代理,由於初始化 Bean 字段時會報錯)
                     beanInstance = TransactionProxy.getInstance().getProxy(beanClass);
                 } else {
                     // 不然經過反射建立實例
                     beanInstance = beanClass.newInstance();
                 }
                 // 將 Bean 實例放入 Bean Map 中(鍵爲 Bean 類,值爲 Bean 實例)
                 beanMap.put(beanClass, beanInstance);
             }
 
             // 遍歷 Bean Map
             for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
                 ...
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 
     ...
}

在遍歷 beanClassList 時,判斷當前的 beanClass 是否繼承於 BaseService?若是是,那麼就建立動態代理實例給 beanInstance;不然,就像之前同樣,經過反射來建立 beanInstance。

改動量還不算太大,動態代理就會初始化到相應的 Bean 對象上了。

到此爲止,事務管理實現原理已所有結束。固然問題還有不少,好比:我沒有考慮事務隔離級別、事務傳播行爲、事務超時、只讀事務等問題,甚至還有更復雜的 JTA 事務。

但我我的認爲,事務管理功能實用就好了,標註了 @Transaction 註解的方法就有事務,沒有標註就沒有事務,很簡單。不必真的作得和 Spring 事務管理器那樣完備,好比:支持 7 種事務傳播行爲。那有人就會提到,爲何不提供「嵌套事務」和「JTA 事務」呢?我想說的是,追求是無止境的,即使是 Spring 也有它的不足之處。關鍵是對框架的定位要看準,該框架僅用於開發中、小規模的 Java Web 應用系統,那麼這類複雜的事務處理狀況又會有多少呢?因此我暫時就此打住了,個人直覺告訴我,深刻下去將必定是一個無底洞。

我想有必要先聽聽你們的想法,避免走彎路的最佳方式就是及時溝通。

相關文章
相關標籤/搜索