事務(wù)管理最佳實(shí)踐全面解析
前言
寫作這篇文章的起因,是前一段時(shí)間,我使用Jbpm工作流引擎開發(fā)工作流管理系統(tǒng)的過程中,使用編程方式管理事務(wù)時(shí)遇到的問題。
由于之前很長一段時(shí)間,我一直都在使用Spring和EJB容器的聲明式事務(wù)管理,因此,咋一遇到Jbpm這樣的編程方式管理事務(wù)的情況,一下子搞不定了!經(jīng)過幾天的研究,我重新思考了怎樣進(jìn)行事務(wù)管理這個(gè)問題,并且發(fā)明了一種非常好的編程范式,或者說是事務(wù)管理的最佳實(shí)踐。不敢獨(dú)享,拿出來與諸君共賞。請(qǐng)大家批評(píng)指正。
前幾個(gè)月,我對(duì)C++和Java編程方式進(jìn)行了比較和研究。并且總結(jié)了一些C++編程中管理對(duì)象的最佳實(shí)踐。
但由于那一段時(shí)間工作較忙,沒有及時(shí)把文章寫出來。后來想寫文章時(shí),卻找不到當(dāng)初為了說明寫的Java和C++演示代碼了。因此,該文一直未成,頗為遺憾!因此,這篇事務(wù)管理的文章,我不敢拖得太久。由于時(shí)間倉促,寫得不好,請(qǐng)大家見諒!
事務(wù)管理
企業(yè)級(jí)應(yīng)用,或者叫“信息管理系統(tǒng)”。這類軟件通過數(shù)據(jù)庫持久化保存、處理的信息。它們工作的核心,就是數(shù)據(jù)庫。這類應(yīng)用,是目前市場上最主流的商業(yè)應(yīng)用。
事務(wù)管理,這個(gè)概念出自于數(shù)據(jù)庫管理系統(tǒng)中。事務(wù)是一個(gè)單元的工作,要么全做,要么全不做。
事務(wù)管理對(duì)于維持?jǐn)?shù)據(jù)庫系統(tǒng)內(nèi)部保存的數(shù)據(jù)邏輯上的一致性、完整性,起著至關(guān)重要的作用。如:一個(gè)銀行應(yīng)用軟件中,轉(zhuǎn)帳的操作中,需要先在A用戶帳戶中減去資金,然后再在B用戶帳戶中增加相應(yīng)的資金。如果完成A帳戶操作后,由于系統(tǒng)故障或者網(wǎng)絡(luò)故障,沒有能夠完成接下來的操作,那么A帳戶中的資金就白白流失了。顯然,客戶是無法接受這樣的結(jié)果的!
如果我們把一個(gè)A和B帳戶的操作放在一個(gè)事務(wù)單元中,那么如果遇到上述異常情況,A帳戶減少資金的操作會(huì)回滾。A帳戶的資金不會(huì)減少。
事務(wù)管理和數(shù)據(jù)庫連接的關(guān)系
事務(wù)管理的工作,需要在數(shù)據(jù)庫連接上進(jìn)行。如果沒有數(shù)據(jù)庫連接,事務(wù)管理是無法實(shí)施的。
因此,一個(gè)事務(wù)單元,應(yīng)該小于或者等于一個(gè)數(shù)據(jù)庫連接的生命周期。
事務(wù)管理最佳模式
數(shù)據(jù)庫連接管理最佳模式
數(shù)據(jù)庫連接,是一種很寶貴也很昂貴的資源。一個(gè)數(shù)據(jù)庫可以提供的數(shù)據(jù)庫連接總數(shù)是有限的。而且,獲取一次數(shù)據(jù)庫連接也是非常昂貴的操作。需要建立網(wǎng)絡(luò)連接。因此,我們應(yīng)當(dāng)盡可能的重用數(shù)據(jù)庫連接,讓數(shù)據(jù)庫連接維持的時(shí)間盡可能的長。
但是,我們也不能把數(shù)據(jù)庫連接維持的太久。因?yàn)?,上文已?jīng)說過了,一個(gè)數(shù)據(jù)庫可以提供的數(shù)據(jù)庫連接總數(shù)是有限的。如果數(shù)據(jù)庫連接的時(shí)間很長,那么其他需要數(shù)據(jù)庫連接的工作就無法得到所需的數(shù)據(jù)庫連接。
因此,最佳的數(shù)據(jù)庫連接模式,是“每次請(qǐng)求,一次數(shù)據(jù)庫連接”這樣的使用模式。
因?yàn)椋啻握?qǐng)求之間的時(shí)間間隔是無法預(yù)料的,可能長達(dá)幾小時(shí)、甚至幾天。數(shù)據(jù)庫連接顯然不能白白的等待在那里。而應(yīng)該返回給數(shù)據(jù)庫,或者數(shù)據(jù)庫連接緩沖池,讓其他程序和組件有機(jī)會(huì)使用數(shù)據(jù)庫連接。
另外,如果一次數(shù)據(jù)庫連接,小于一次用戶請(qǐng)求,那么,數(shù)據(jù)庫連接的得到和關(guān)閉次數(shù)又太頻繁了。因?yàn)?,得到一次?shù)據(jù)庫連接是非常消耗資源的。一次用戶請(qǐng)求,是一個(gè)短時(shí)、瞬間的操作,完全沒有必要使用多個(gè)數(shù)據(jù)庫連接。
另外,上文中說過,事務(wù)是依托在數(shù)據(jù)庫連接之上的。多個(gè)數(shù)據(jù)庫連接之間,是無法使用同一個(gè)事務(wù)的。(實(shí)際上,JTA分布式事務(wù)是可以在一個(gè)事務(wù)中使用多個(gè)數(shù)據(jù)庫連接的)
因此,我們更應(yīng)該讓數(shù)據(jù)庫連接的生命周期盡可能的延長。
事務(wù)管理最佳模式
最佳的數(shù)據(jù)庫連接模式,是“每次請(qǐng)求,一次數(shù)據(jù)庫連接”這樣的使用模式。事務(wù),與之相仿。最佳的事務(wù)管理模式,也是“每次請(qǐng)求,一次數(shù)據(jù)庫連接,一次事務(wù)”。
一次用戶請(qǐng)求,是用戶對(duì)軟件系統(tǒng)功能的一次獨(dú)立調(diào)用。用戶當(dāng)然不希望他的一次操作,系統(tǒng)只執(zhí)行一部分這種情況的發(fā)生。因此,對(duì)一次用戶請(qǐng)求的響應(yīng),使用一次事務(wù),是非常和正確的。
對(duì)于一次單純的查詢操作,不更改持久化數(shù)據(jù)庫中記錄,那么我們不需要使用事務(wù)。在數(shù)據(jù)庫操作發(fā)生錯(cuò)誤時(shí),拋出異常,讓用戶界面顯示出問題即可。而對(duì)于更改數(shù)據(jù)庫記錄的操作,并且涉及到多次數(shù)據(jù)庫操作的,則必須使用事務(wù),以保證數(shù)據(jù)庫中記錄的完整性和真實(shí)性。
數(shù)據(jù)庫連接和事務(wù)管理的反模式
數(shù)據(jù)庫連接和事務(wù)管理,在應(yīng)用中有一些反模式。我們應(yīng)該避免這樣做,否則會(huì)死得很慘!
一、數(shù)據(jù)庫連接和事務(wù)管理跨越一個(gè)客戶的多次請(qǐng)求
這樣的數(shù)據(jù)庫連接和事務(wù),其持續(xù)時(shí)間是無法估量的。這樣嚴(yán)重影響軟件和數(shù)據(jù)庫的性能。這是絕對(duì)不可取的。
二、每個(gè)數(shù)據(jù)庫操作,一次數(shù)據(jù)庫連接和事務(wù)
這是一種非常常見的反模式。在采用DAO設(shè)計(jì)模式進(jìn)行O-R映射中,DAO接口的一個(gè)數(shù)據(jù)庫訪問方法,就執(zhí)行一次數(shù)據(jù)庫連接的獲取和釋放,并且執(zhí)行一次或者多次事務(wù)。
如,下面的代碼:
/*
4,刪除單條消息
*/
publicvoid deleteMessage(bbbbbb id){
Connection conn=DB.getConnection();
Statement stmt =null;
ResultSet rst=null;
try {
stmt = conn.createStatement();
//拼裝SQL
bbbbbb sql="delete from message where id='"+id+"'";
stmt.executeUpdate(sql);
}
catch (SQLException ex) {
ex.printStackTrace();
thrownew DataAccessException();
}finally{
DB.freeDbResource(conn,stmt,rst);
}
}
這是典型的反模式。
數(shù)據(jù)庫連接在Dao中得到和釋放。如果一次用戶請(qǐng)求需要用到多個(gè)Dao方法,那么就需要多次得到和釋放數(shù)據(jù)庫連接。造成了極大的浪費(fèi)。而且,也無法對(duì)多個(gè)Dao方法實(shí)施事務(wù)管理。
另外,JDBC中,默認(rèn)的事務(wù)管理方式是自動(dòng)提交。上面的代碼只有一個(gè)SQL執(zhí)行語句。所有只有一次事務(wù)。如果Dao方法中有多個(gè)SQL語句,那么就會(huì)在一個(gè)Dao方法中使用多個(gè)事務(wù),多次提交到數(shù)據(jù)庫中,這也是極端錯(cuò)誤的!
當(dāng)然,上面這個(gè)簡單的Dao方法,并不會(huì)造成任何實(shí)際的損害,這里僅僅說明這種使用方式是一種反模式。
事務(wù)管理的最佳設(shè)計(jì)模式
最佳的事務(wù)管理模式,是“每次請(qǐng)求,一次數(shù)據(jù)庫連接,一次事務(wù)”。那么,根據(jù)這個(gè)原則,具體我們應(yīng)該怎樣編寫程序呢?
一、事務(wù)管理的分層
企業(yè)級(jí)應(yīng)用軟件中的代碼部分,可以分為以下幾個(gè)層次:
(一)控制器Controller層
這是表現(xiàn)層的業(yè)務(wù)委派。它處理用戶的請(qǐng)求,完成用戶要求的功能。它接收用戶傳來的參數(shù),然后調(diào)用業(yè)務(wù)層的服務(wù)方法,完成所需的功能。
根據(jù)“每次請(qǐng)求,一次數(shù)據(jù)庫連接,一次事務(wù)”的原則。似乎,這里是最好的得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)的地方。因?yàn)?,Controller層中的每一個(gè)方法,對(duì)應(yīng)著用戶的一次請(qǐng)求。
但是,我認(rèn)為,這里決不應(yīng)該“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”。因?yàn)?,首先,控制器層,作為表現(xiàn)層技術(shù)的一部分,它的作用,僅僅是委派操作給業(yè)務(wù)層的服務(wù)方法,應(yīng)該盡可能的小。不應(yīng)該包括這些代碼。
其次,管理數(shù)據(jù)庫連接和事務(wù),這是業(yè)務(wù)層的邏輯,應(yīng)該在業(yè)務(wù)層處理,而不是在表現(xiàn)層處理。
更實(shí)際一點(diǎn)來說,Struts這種技術(shù)中,我們一般不使用Spring來管理事務(wù)。這樣,如果“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”放在Struts的控制器層—bbbbbb類中,那么Spring自動(dòng)管理數(shù)據(jù)庫連接和事務(wù)的聲明式事務(wù)管理機(jī)制就無法使用了!
因此,我們應(yīng)該堅(jiān)決地拒絕在控制器層中處理數(shù)據(jù)庫連接和事務(wù)的誘惑!
(二)業(yè)務(wù)服務(wù)Service層
業(yè)務(wù)服務(wù)層,是業(yè)務(wù)邏輯的實(shí)際存放地。它們提供的服務(wù)分為2種:
1,為控制器層提供服務(wù),處理用戶請(qǐng)求。
2,為其他類(不僅僅是控制器層,可能是其他Service方法等)提供服務(wù)。
傳統(tǒng)上,大家都不區(qū)分這兩類服務(wù)方法。統(tǒng)稱為Service。
而在我的方法中,我把它們區(qū)分開來。
1,直接為控制器層提供服務(wù),并且需要使用到數(shù)據(jù)庫操作,從而需要處理數(shù)據(jù)庫連接和事務(wù)的,我把它們成為Transbbbbbb方法。用*Transbbbbbb后綴標(biāo)識(shí)。
這樣的方法,我仍然把它們放在Service接口中。如果你需要實(shí)現(xiàn)這樣的方法??吹胶缶Y,你就知道,你需要在這里調(diào)用Dao方法,并且“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”。
如果你不在這里進(jìn)行“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的操作,那么系統(tǒng)一定會(huì)出現(xiàn)故障!
2,為其他類(可以是控制器層,也可能是其他Service方法等)提供服務(wù),并且不需要訪問數(shù)據(jù)庫的方法。我稱它們?yōu)镾ervice方法。使用*Service后綴,或者不使用后綴來標(biāo)識(shí)它們。
這樣的方法,你可以無所顧忌的使用,既可以在控制器層中調(diào)用,也可以在任何代碼中調(diào)用!
3,需要使用到數(shù)據(jù)庫操作,并且不可以直接被控制器層調(diào)用的方法。我稱它們?yōu)镈ao方法。使用*Dao后綴來標(biāo)識(shí)它們。
它們不是Dao接口中的方法,而是Service業(yè)務(wù)邏輯接口中的方法。我稱它們?yōu)镈ao方法,并不是說,它們是Dao接口的方法,而是表示它們是Service層中需要使用Dao接口操縱數(shù)據(jù)庫的服務(wù)方法。并且,它們本身不含有“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的代碼。因此,所有需要調(diào)用它們的方法,需要注意了,“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”這些任務(wù)還沒有做。如果直接在控制器層調(diào)用它們,那么一定會(huì)出現(xiàn)數(shù)據(jù)庫和事務(wù)的錯(cuò)誤!
(三)DAO數(shù)據(jù)訪問層
DAO數(shù)據(jù)訪問模式,是目前用的最到的模式。在DAO中,使用各類數(shù)據(jù)庫訪問技術(shù)(如,JDBC,iBatis,Hibernate等)操作數(shù)據(jù)庫,實(shí)現(xiàn)O-R映射。
其中的方法,大都滿足“需要使用到數(shù)據(jù)庫操作,并且不可以直接被控制器層調(diào)用的方法”這樣一種情況。我們可以使用*Dao后綴來標(biāo)識(shí)這些方法,也可以不使用后綴。因?yàn)镈ao接口的方法,大抵都是這類方法。
二、數(shù)據(jù)庫連接和事務(wù)管理最佳模式
在我們的編程范式中,是這樣工作的:
控制器層,接收用戶請(qǐng)求參數(shù),并委派給業(yè)務(wù)層的Service接口執(zhí)行業(yè)務(wù)邏輯。它可以直接調(diào)用Service接口的*Transbbbbbb方法和*Service方法或者沒有后綴的一般方法。
其中,*Transbbbbbb方法需要用到數(shù)據(jù)庫。其中必然調(diào)用了業(yè)務(wù)層的Dao方法,或者DAO層的數(shù)據(jù)庫訪問方法。其實(shí)現(xiàn)方法中必然有處理“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的代碼。
而*Service方法或者沒有后綴的一般方法,則沒有使用數(shù)據(jù)庫。
在DAO數(shù)據(jù)訪問層,執(zhí)行數(shù)據(jù)庫操作的DAO方法,并不需要?jiǎng)?chuàng)建和關(guān)閉數(shù)據(jù)庫連接,也不需要處理事務(wù)。它們之需要得到數(shù)據(jù)庫連接,然后使用這個(gè)連接即可。(數(shù)據(jù)庫連接,可以通過參數(shù)從外部得到,也可以從本地線程變量中得到。后者是目前主流的技術(shù))
這就是我提出的“事務(wù)管理最佳實(shí)踐”的工作情況。
在Service業(yè)務(wù)層和DAO數(shù)據(jù)訪問層中,我們都使用了“接口—實(shí)現(xiàn)類”相分離的設(shè)計(jì)模式。
一、編程方式的數(shù)據(jù)庫連接和事務(wù)管理
假設(shè),現(xiàn)在我們使用多種數(shù)據(jù)庫訪問技術(shù),來進(jìn)行O-R映射。看看我們這個(gè)架構(gòu)的適應(yīng)能力。
我們的系統(tǒng),分別使用JDBC,iBatis,Hibernate這三種數(shù)據(jù)庫訪問技術(shù),使用編程方式手工管理數(shù)據(jù)庫連接和事務(wù),不使用Spring這樣的IOC容器進(jìn)行管理??纯次覀冃枰鍪裁矗?BR>(一)JDBC編程方式管理數(shù)據(jù)庫連接和事務(wù)
首先,開發(fā)一個(gè)JDBCUtil類,得到數(shù)據(jù)庫連接,并且把它們放在一個(gè)線程變量中,以便一個(gè)線程重用一個(gè)數(shù)據(jù)庫連接。[NextPage]
然后,開發(fā)DAO接口的實(shí)現(xiàn)類。實(shí)現(xiàn)DAO方法。從本地線程變量中得到數(shù)據(jù)庫連接,使用它。不需要關(guān)閉這個(gè)連接,也不需要管理事務(wù)。
接著,開發(fā)SeriVCe層的*Dao后綴命名的方法。它們只需要調(diào)用DAO接口的方法即可。不需要和數(shù)據(jù)庫連接、事務(wù)打交道。
最后,開發(fā)Service層的*Transbbbbbb后綴命名的方法。它們調(diào)用JDBCUtil類的方法,創(chuàng)建一個(gè)數(shù)據(jù)庫連接,并把它放在JDBCUtil類的本地線程變量中,設(shè)置conn.setAutoCommit(false);等待DAO接口的方法去取這個(gè)已經(jīng)設(shè)為不自動(dòng)提交的數(shù)據(jù)庫連接。
然后,在Try塊中,調(diào)用Dao方法(Service接口或者DAO接口的Dao方法)。調(diào)用結(jié)束之后,提交事務(wù),并在異常處理模塊中,設(shè)置回滾。最后,在finally塊中關(guān)閉數(shù)據(jù)庫連接,清除本地線程變量的值。
(二)iBatis編程方式管理數(shù)據(jù)庫連接和事務(wù)
iBatis本身就是使用本地線程變量來管理數(shù)據(jù)庫連接的。
1,DAO接口的實(shí)現(xiàn)方法中,調(diào)用iBatis代碼,執(zhí)行數(shù)據(jù)庫操作。
2,Service層的Dao方法,不需要任何更改。
3,Service層的Transbbbbbb方法,需要使用iBatis的事務(wù)管理代碼。
private SqlMapClient sqlMap = XMLSqlMaPBuilder.buildSqlMap(reader);
public updateItemDebbbbbbionTransbbbbbb (bbbbbb itemId, bbbbbb newDebbbbbbion) throws SQLException {
try {
sqlMap.startTransbbbbbb ();
dao方法調(diào)用;
sqlMap.commitTransbbbbbb ();
} finally {
sqlMap.endTransbbbbbb ();
}
}
iBatis處理事務(wù)的代碼,也處理得數(shù)據(jù)庫連接。并且,事務(wù)的回滾也被iBatis搞定了。
也就是說,換了一種數(shù)據(jù)庫訪問技術(shù),只需要改變Service層中*Transbbbbbb方法的實(shí)現(xiàn)和DAO層的實(shí)現(xiàn)。
(三)Hibernate編程方式管理數(shù)據(jù)庫連接和事務(wù)
Hibernate也是如此。
下面是Hibernate的助手類:
publicclass HibernateSessionFactoryFromJbpm {
privatestaticfinal ThreadLocal threadLocal = new ThreadLocal();
privatestatic org.hibernate.SessionFactory sessionFactory;
/**
*ReturnstheThreadLocalSessioninstance. Lazyinitialize
*the<code>SessionFactory</code>ifneeded.
*
* @returnSession
* @throwsHibernateException
*/
publicstatic Session getSession() throws HibernateException {
Session session = (Session) threadLocal.get();
if (session == null || !session.isOpen()) {
if (sessionFactory == null) {
rebuildSessionFactory();
}
session = (sessionFactory != null) ? sessionFactory.openSession()
: null;
threadLocal.set(session);
}
return session;
}
/**
* Rebuildhibernatesessionfactory
*
*/
publicstaticvoid rebuildSessionFactory() {
try {
// configuration.configure(configFile);
//sessionFactory = configuration.buildSessionFactory();
sessionFactory =HibernateHelper.createSessionFactory();
} catch (Exception e) {
System.err
.println("%%%% Error Creating SessionFactory %%%%");
e.printStackTrace();
}
}
/**
* Closethesinglehibernatesessioninstance.
*
* @throwsHibernateException
*/
publicstaticvoid closeSession() throws HibernateException {
Session session = (Session) threadLocal.get();
threadLocal.set(null);
if (session != null) {
session.close();
}
}
}
Hibernate的Session,是對(duì)JDBC Connection的封裝。Hibernate不同于JDBC和iBatis。它默認(rèn)就把自動(dòng)提交設(shè)為false。也就是說,如果你不顯式的使用Hiberante事務(wù),那么根本不會(huì)操作數(shù)據(jù)庫!這點(diǎn)需要注意。
(四)Jbpm對(duì)Hiberante的封裝
另外,再說一下Jbpm對(duì)Hiberante所作的封裝。Jbpm使用的是Hiberante3的數(shù)據(jù)庫訪問技術(shù)。但是,它對(duì)Hibernate進(jìn)行了封裝。
使用Jbpm,事務(wù)管理更加簡單。
如:
public List getAllCanSeenTaskInstancesTransbbbbbb (PageModule view,bbbbbb userId) throws Exception {
JbpmContext jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
try {
returnthis.getAllCanSeenTaskInstances(view, userId);
}finally{
jbpmContext.close();
}
}
當(dāng) jbpmContext.close();方法執(zhí)行時(shí),自動(dòng)提交事務(wù)。如果發(fā)生異常,自動(dòng)回滾。并且,最后會(huì)關(guān)閉Hiberante本地線程中的Session,并清空該線程變量。
二、聲明方式的數(shù)據(jù)庫連接和事務(wù)管理
Spring容器管理業(yè)務(wù)代碼和DAO數(shù)據(jù)訪問代碼,是現(xiàn)在非常常用的一種方式。使用Spring時(shí),我們一般使用Spring聲明式事務(wù)來管理數(shù)據(jù)庫連接和事務(wù)。
另外,還有EJB容器也有聲明式事務(wù)管理的機(jī)制,兩者的使用方法大體相同,我就不再論述,這里只說Spring。
Spring管理下的JDBC,iBatis,Hibernate數(shù)據(jù)庫訪問方法。我們?cè)贒AO接口的實(shí)現(xiàn)類中,可以使用Spring提供的助手類的便利方法,進(jìn)行數(shù)據(jù)庫操作。也可以使用Spring提供的助手類,得到Connection,Session等進(jìn)行數(shù)據(jù)庫操作?;蛘呤褂肧pring助手類的execute()方法調(diào)用數(shù)據(jù)庫操作代碼。
如果你原先使用自己的助手類得到Connection,Session。那么你完全可以修改該助手類的實(shí)現(xiàn)方法,改為從Spring得到Connection,Session。這樣就不需要修改DAO接口的實(shí)現(xiàn)類!
Service層中的Dao方法,仍然無需修改。
對(duì)于Service層中的Transbbbbbb方法。我們需要去除“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的代碼。然后,在Spring的配置文件中,對(duì)其應(yīng)用聲明式事務(wù)管理。運(yùn)行時(shí),Spring會(huì)通過SpringAOP技術(shù),自動(dòng)得到數(shù)據(jù)庫連接,管理事務(wù)。
可見,使用聲明式事務(wù)管理,我們只需要修改得到數(shù)據(jù)庫連接或者會(huì)話的Util助手類,以及Transbbbbbb方法即可。
綜上所述,可以看到,我提出的這一套事務(wù)管理最佳實(shí)踐是一套非常靈活、強(qiáng)大、簡潔的管理事務(wù)的最佳實(shí)踐。具有極其強(qiáng)大的適應(yīng)能力。采用這套編程范式,你可以很容易地徹底擺脫事務(wù)管理帶來的困擾!
寫作這篇文章的起因,是前一段時(shí)間,我使用Jbpm工作流引擎開發(fā)工作流管理系統(tǒng)的過程中,使用編程方式管理事務(wù)時(shí)遇到的問題。
由于之前很長一段時(shí)間,我一直都在使用Spring和EJB容器的聲明式事務(wù)管理,因此,咋一遇到Jbpm這樣的編程方式管理事務(wù)的情況,一下子搞不定了!經(jīng)過幾天的研究,我重新思考了怎樣進(jìn)行事務(wù)管理這個(gè)問題,并且發(fā)明了一種非常好的編程范式,或者說是事務(wù)管理的最佳實(shí)踐。不敢獨(dú)享,拿出來與諸君共賞。請(qǐng)大家批評(píng)指正。
前幾個(gè)月,我對(duì)C++和Java編程方式進(jìn)行了比較和研究。并且總結(jié)了一些C++編程中管理對(duì)象的最佳實(shí)踐。
但由于那一段時(shí)間工作較忙,沒有及時(shí)把文章寫出來。后來想寫文章時(shí),卻找不到當(dāng)初為了說明寫的Java和C++演示代碼了。因此,該文一直未成,頗為遺憾!因此,這篇事務(wù)管理的文章,我不敢拖得太久。由于時(shí)間倉促,寫得不好,請(qǐng)大家見諒!
事務(wù)管理
企業(yè)級(jí)應(yīng)用,或者叫“信息管理系統(tǒng)”。這類軟件通過數(shù)據(jù)庫持久化保存、處理的信息。它們工作的核心,就是數(shù)據(jù)庫。這類應(yīng)用,是目前市場上最主流的商業(yè)應(yīng)用。
事務(wù)管理,這個(gè)概念出自于數(shù)據(jù)庫管理系統(tǒng)中。事務(wù)是一個(gè)單元的工作,要么全做,要么全不做。
事務(wù)管理對(duì)于維持?jǐn)?shù)據(jù)庫系統(tǒng)內(nèi)部保存的數(shù)據(jù)邏輯上的一致性、完整性,起著至關(guān)重要的作用。如:一個(gè)銀行應(yīng)用軟件中,轉(zhuǎn)帳的操作中,需要先在A用戶帳戶中減去資金,然后再在B用戶帳戶中增加相應(yīng)的資金。如果完成A帳戶操作后,由于系統(tǒng)故障或者網(wǎng)絡(luò)故障,沒有能夠完成接下來的操作,那么A帳戶中的資金就白白流失了。顯然,客戶是無法接受這樣的結(jié)果的!
如果我們把一個(gè)A和B帳戶的操作放在一個(gè)事務(wù)單元中,那么如果遇到上述異常情況,A帳戶減少資金的操作會(huì)回滾。A帳戶的資金不會(huì)減少。
事務(wù)管理和數(shù)據(jù)庫連接的關(guān)系
事務(wù)管理的工作,需要在數(shù)據(jù)庫連接上進(jìn)行。如果沒有數(shù)據(jù)庫連接,事務(wù)管理是無法實(shí)施的。
因此,一個(gè)事務(wù)單元,應(yīng)該小于或者等于一個(gè)數(shù)據(jù)庫連接的生命周期。
事務(wù)管理最佳模式
數(shù)據(jù)庫連接管理最佳模式
數(shù)據(jù)庫連接,是一種很寶貴也很昂貴的資源。一個(gè)數(shù)據(jù)庫可以提供的數(shù)據(jù)庫連接總數(shù)是有限的。而且,獲取一次數(shù)據(jù)庫連接也是非常昂貴的操作。需要建立網(wǎng)絡(luò)連接。因此,我們應(yīng)當(dāng)盡可能的重用數(shù)據(jù)庫連接,讓數(shù)據(jù)庫連接維持的時(shí)間盡可能的長。
但是,我們也不能把數(shù)據(jù)庫連接維持的太久。因?yàn)?,上文已?jīng)說過了,一個(gè)數(shù)據(jù)庫可以提供的數(shù)據(jù)庫連接總數(shù)是有限的。如果數(shù)據(jù)庫連接的時(shí)間很長,那么其他需要數(shù)據(jù)庫連接的工作就無法得到所需的數(shù)據(jù)庫連接。
因此,最佳的數(shù)據(jù)庫連接模式,是“每次請(qǐng)求,一次數(shù)據(jù)庫連接”這樣的使用模式。
因?yàn)椋啻握?qǐng)求之間的時(shí)間間隔是無法預(yù)料的,可能長達(dá)幾小時(shí)、甚至幾天。數(shù)據(jù)庫連接顯然不能白白的等待在那里。而應(yīng)該返回給數(shù)據(jù)庫,或者數(shù)據(jù)庫連接緩沖池,讓其他程序和組件有機(jī)會(huì)使用數(shù)據(jù)庫連接。
另外,如果一次數(shù)據(jù)庫連接,小于一次用戶請(qǐng)求,那么,數(shù)據(jù)庫連接的得到和關(guān)閉次數(shù)又太頻繁了。因?yàn)?,得到一次?shù)據(jù)庫連接是非常消耗資源的。一次用戶請(qǐng)求,是一個(gè)短時(shí)、瞬間的操作,完全沒有必要使用多個(gè)數(shù)據(jù)庫連接。
另外,上文中說過,事務(wù)是依托在數(shù)據(jù)庫連接之上的。多個(gè)數(shù)據(jù)庫連接之間,是無法使用同一個(gè)事務(wù)的。(實(shí)際上,JTA分布式事務(wù)是可以在一個(gè)事務(wù)中使用多個(gè)數(shù)據(jù)庫連接的)
因此,我們更應(yīng)該讓數(shù)據(jù)庫連接的生命周期盡可能的延長。
事務(wù)管理最佳模式
最佳的數(shù)據(jù)庫連接模式,是“每次請(qǐng)求,一次數(shù)據(jù)庫連接”這樣的使用模式。事務(wù),與之相仿。最佳的事務(wù)管理模式,也是“每次請(qǐng)求,一次數(shù)據(jù)庫連接,一次事務(wù)”。
一次用戶請(qǐng)求,是用戶對(duì)軟件系統(tǒng)功能的一次獨(dú)立調(diào)用。用戶當(dāng)然不希望他的一次操作,系統(tǒng)只執(zhí)行一部分這種情況的發(fā)生。因此,對(duì)一次用戶請(qǐng)求的響應(yīng),使用一次事務(wù),是非常和正確的。
對(duì)于一次單純的查詢操作,不更改持久化數(shù)據(jù)庫中記錄,那么我們不需要使用事務(wù)。在數(shù)據(jù)庫操作發(fā)生錯(cuò)誤時(shí),拋出異常,讓用戶界面顯示出問題即可。而對(duì)于更改數(shù)據(jù)庫記錄的操作,并且涉及到多次數(shù)據(jù)庫操作的,則必須使用事務(wù),以保證數(shù)據(jù)庫中記錄的完整性和真實(shí)性。
數(shù)據(jù)庫連接和事務(wù)管理的反模式
數(shù)據(jù)庫連接和事務(wù)管理,在應(yīng)用中有一些反模式。我們應(yīng)該避免這樣做,否則會(huì)死得很慘!
一、數(shù)據(jù)庫連接和事務(wù)管理跨越一個(gè)客戶的多次請(qǐng)求
這樣的數(shù)據(jù)庫連接和事務(wù),其持續(xù)時(shí)間是無法估量的。這樣嚴(yán)重影響軟件和數(shù)據(jù)庫的性能。這是絕對(duì)不可取的。
二、每個(gè)數(shù)據(jù)庫操作,一次數(shù)據(jù)庫連接和事務(wù)
這是一種非常常見的反模式。在采用DAO設(shè)計(jì)模式進(jìn)行O-R映射中,DAO接口的一個(gè)數(shù)據(jù)庫訪問方法,就執(zhí)行一次數(shù)據(jù)庫連接的獲取和釋放,并且執(zhí)行一次或者多次事務(wù)。
如,下面的代碼:
/*
4,刪除單條消息
*/
publicvoid deleteMessage(bbbbbb id){
Connection conn=DB.getConnection();
Statement stmt =null;
ResultSet rst=null;
try {
stmt = conn.createStatement();
//拼裝SQL
bbbbbb sql="delete from message where id='"+id+"'";
stmt.executeUpdate(sql);
}
catch (SQLException ex) {
ex.printStackTrace();
thrownew DataAccessException();
}finally{
DB.freeDbResource(conn,stmt,rst);
}
}
這是典型的反模式。
數(shù)據(jù)庫連接在Dao中得到和釋放。如果一次用戶請(qǐng)求需要用到多個(gè)Dao方法,那么就需要多次得到和釋放數(shù)據(jù)庫連接。造成了極大的浪費(fèi)。而且,也無法對(duì)多個(gè)Dao方法實(shí)施事務(wù)管理。
另外,JDBC中,默認(rèn)的事務(wù)管理方式是自動(dòng)提交。上面的代碼只有一個(gè)SQL執(zhí)行語句。所有只有一次事務(wù)。如果Dao方法中有多個(gè)SQL語句,那么就會(huì)在一個(gè)Dao方法中使用多個(gè)事務(wù),多次提交到數(shù)據(jù)庫中,這也是極端錯(cuò)誤的!
當(dāng)然,上面這個(gè)簡單的Dao方法,并不會(huì)造成任何實(shí)際的損害,這里僅僅說明這種使用方式是一種反模式。
事務(wù)管理的最佳設(shè)計(jì)模式
最佳的事務(wù)管理模式,是“每次請(qǐng)求,一次數(shù)據(jù)庫連接,一次事務(wù)”。那么,根據(jù)這個(gè)原則,具體我們應(yīng)該怎樣編寫程序呢?
一、事務(wù)管理的分層
企業(yè)級(jí)應(yīng)用軟件中的代碼部分,可以分為以下幾個(gè)層次:
(一)控制器Controller層
這是表現(xiàn)層的業(yè)務(wù)委派。它處理用戶的請(qǐng)求,完成用戶要求的功能。它接收用戶傳來的參數(shù),然后調(diào)用業(yè)務(wù)層的服務(wù)方法,完成所需的功能。
根據(jù)“每次請(qǐng)求,一次數(shù)據(jù)庫連接,一次事務(wù)”的原則。似乎,這里是最好的得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)的地方。因?yàn)?,Controller層中的每一個(gè)方法,對(duì)應(yīng)著用戶的一次請(qǐng)求。
但是,我認(rèn)為,這里決不應(yīng)該“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”。因?yàn)?,首先,控制器層,作為表現(xiàn)層技術(shù)的一部分,它的作用,僅僅是委派操作給業(yè)務(wù)層的服務(wù)方法,應(yīng)該盡可能的小。不應(yīng)該包括這些代碼。
其次,管理數(shù)據(jù)庫連接和事務(wù),這是業(yè)務(wù)層的邏輯,應(yīng)該在業(yè)務(wù)層處理,而不是在表現(xiàn)層處理。
更實(shí)際一點(diǎn)來說,Struts這種技術(shù)中,我們一般不使用Spring來管理事務(wù)。這樣,如果“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”放在Struts的控制器層—bbbbbb類中,那么Spring自動(dòng)管理數(shù)據(jù)庫連接和事務(wù)的聲明式事務(wù)管理機(jī)制就無法使用了!
因此,我們應(yīng)該堅(jiān)決地拒絕在控制器層中處理數(shù)據(jù)庫連接和事務(wù)的誘惑!
(二)業(yè)務(wù)服務(wù)Service層
業(yè)務(wù)服務(wù)層,是業(yè)務(wù)邏輯的實(shí)際存放地。它們提供的服務(wù)分為2種:
1,為控制器層提供服務(wù),處理用戶請(qǐng)求。
2,為其他類(不僅僅是控制器層,可能是其他Service方法等)提供服務(wù)。
傳統(tǒng)上,大家都不區(qū)分這兩類服務(wù)方法。統(tǒng)稱為Service。
而在我的方法中,我把它們區(qū)分開來。
1,直接為控制器層提供服務(wù),并且需要使用到數(shù)據(jù)庫操作,從而需要處理數(shù)據(jù)庫連接和事務(wù)的,我把它們成為Transbbbbbb方法。用*Transbbbbbb后綴標(biāo)識(shí)。
這樣的方法,我仍然把它們放在Service接口中。如果你需要實(shí)現(xiàn)這樣的方法??吹胶缶Y,你就知道,你需要在這里調(diào)用Dao方法,并且“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”。
如果你不在這里進(jìn)行“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的操作,那么系統(tǒng)一定會(huì)出現(xiàn)故障!
2,為其他類(可以是控制器層,也可能是其他Service方法等)提供服務(wù),并且不需要訪問數(shù)據(jù)庫的方法。我稱它們?yōu)镾ervice方法。使用*Service后綴,或者不使用后綴來標(biāo)識(shí)它們。
這樣的方法,你可以無所顧忌的使用,既可以在控制器層中調(diào)用,也可以在任何代碼中調(diào)用!
3,需要使用到數(shù)據(jù)庫操作,并且不可以直接被控制器層調(diào)用的方法。我稱它們?yōu)镈ao方法。使用*Dao后綴來標(biāo)識(shí)它們。
它們不是Dao接口中的方法,而是Service業(yè)務(wù)邏輯接口中的方法。我稱它們?yōu)镈ao方法,并不是說,它們是Dao接口的方法,而是表示它們是Service層中需要使用Dao接口操縱數(shù)據(jù)庫的服務(wù)方法。并且,它們本身不含有“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的代碼。因此,所有需要調(diào)用它們的方法,需要注意了,“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”這些任務(wù)還沒有做。如果直接在控制器層調(diào)用它們,那么一定會(huì)出現(xiàn)數(shù)據(jù)庫和事務(wù)的錯(cuò)誤!
(三)DAO數(shù)據(jù)訪問層
DAO數(shù)據(jù)訪問模式,是目前用的最到的模式。在DAO中,使用各類數(shù)據(jù)庫訪問技術(shù)(如,JDBC,iBatis,Hibernate等)操作數(shù)據(jù)庫,實(shí)現(xiàn)O-R映射。
其中的方法,大都滿足“需要使用到數(shù)據(jù)庫操作,并且不可以直接被控制器層調(diào)用的方法”這樣一種情況。我們可以使用*Dao后綴來標(biāo)識(shí)這些方法,也可以不使用后綴。因?yàn)镈ao接口的方法,大抵都是這類方法。
二、數(shù)據(jù)庫連接和事務(wù)管理最佳模式
在我們的編程范式中,是這樣工作的:
控制器層,接收用戶請(qǐng)求參數(shù),并委派給業(yè)務(wù)層的Service接口執(zhí)行業(yè)務(wù)邏輯。它可以直接調(diào)用Service接口的*Transbbbbbb方法和*Service方法或者沒有后綴的一般方法。
其中,*Transbbbbbb方法需要用到數(shù)據(jù)庫。其中必然調(diào)用了業(yè)務(wù)層的Dao方法,或者DAO層的數(shù)據(jù)庫訪問方法。其實(shí)現(xiàn)方法中必然有處理“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的代碼。
而*Service方法或者沒有后綴的一般方法,則沒有使用數(shù)據(jù)庫。
在DAO數(shù)據(jù)訪問層,執(zhí)行數(shù)據(jù)庫操作的DAO方法,并不需要?jiǎng)?chuàng)建和關(guān)閉數(shù)據(jù)庫連接,也不需要處理事務(wù)。它們之需要得到數(shù)據(jù)庫連接,然后使用這個(gè)連接即可。(數(shù)據(jù)庫連接,可以通過參數(shù)從外部得到,也可以從本地線程變量中得到。后者是目前主流的技術(shù))
這就是我提出的“事務(wù)管理最佳實(shí)踐”的工作情況。
在Service業(yè)務(wù)層和DAO數(shù)據(jù)訪問層中,我們都使用了“接口—實(shí)現(xiàn)類”相分離的設(shè)計(jì)模式。
一、編程方式的數(shù)據(jù)庫連接和事務(wù)管理
假設(shè),現(xiàn)在我們使用多種數(shù)據(jù)庫訪問技術(shù),來進(jìn)行O-R映射。看看我們這個(gè)架構(gòu)的適應(yīng)能力。
我們的系統(tǒng),分別使用JDBC,iBatis,Hibernate這三種數(shù)據(jù)庫訪問技術(shù),使用編程方式手工管理數(shù)據(jù)庫連接和事務(wù),不使用Spring這樣的IOC容器進(jìn)行管理??纯次覀冃枰鍪裁矗?BR>(一)JDBC編程方式管理數(shù)據(jù)庫連接和事務(wù)
首先,開發(fā)一個(gè)JDBCUtil類,得到數(shù)據(jù)庫連接,并且把它們放在一個(gè)線程變量中,以便一個(gè)線程重用一個(gè)數(shù)據(jù)庫連接。[NextPage]
然后,開發(fā)DAO接口的實(shí)現(xiàn)類。實(shí)現(xiàn)DAO方法。從本地線程變量中得到數(shù)據(jù)庫連接,使用它。不需要關(guān)閉這個(gè)連接,也不需要管理事務(wù)。
接著,開發(fā)SeriVCe層的*Dao后綴命名的方法。它們只需要調(diào)用DAO接口的方法即可。不需要和數(shù)據(jù)庫連接、事務(wù)打交道。
最后,開發(fā)Service層的*Transbbbbbb后綴命名的方法。它們調(diào)用JDBCUtil類的方法,創(chuàng)建一個(gè)數(shù)據(jù)庫連接,并把它放在JDBCUtil類的本地線程變量中,設(shè)置conn.setAutoCommit(false);等待DAO接口的方法去取這個(gè)已經(jīng)設(shè)為不自動(dòng)提交的數(shù)據(jù)庫連接。
然后,在Try塊中,調(diào)用Dao方法(Service接口或者DAO接口的Dao方法)。調(diào)用結(jié)束之后,提交事務(wù),并在異常處理模塊中,設(shè)置回滾。最后,在finally塊中關(guān)閉數(shù)據(jù)庫連接,清除本地線程變量的值。
(二)iBatis編程方式管理數(shù)據(jù)庫連接和事務(wù)
iBatis本身就是使用本地線程變量來管理數(shù)據(jù)庫連接的。
1,DAO接口的實(shí)現(xiàn)方法中,調(diào)用iBatis代碼,執(zhí)行數(shù)據(jù)庫操作。
2,Service層的Dao方法,不需要任何更改。
3,Service層的Transbbbbbb方法,需要使用iBatis的事務(wù)管理代碼。
private SqlMapClient sqlMap = XMLSqlMaPBuilder.buildSqlMap(reader);
public updateItemDebbbbbbionTransbbbbbb (bbbbbb itemId, bbbbbb newDebbbbbbion) throws SQLException {
try {
sqlMap.startTransbbbbbb ();
dao方法調(diào)用;
sqlMap.commitTransbbbbbb ();
} finally {
sqlMap.endTransbbbbbb ();
}
}
iBatis處理事務(wù)的代碼,也處理得數(shù)據(jù)庫連接。并且,事務(wù)的回滾也被iBatis搞定了。
也就是說,換了一種數(shù)據(jù)庫訪問技術(shù),只需要改變Service層中*Transbbbbbb方法的實(shí)現(xiàn)和DAO層的實(shí)現(xiàn)。
(三)Hibernate編程方式管理數(shù)據(jù)庫連接和事務(wù)
Hibernate也是如此。
下面是Hibernate的助手類:
publicclass HibernateSessionFactoryFromJbpm {
privatestaticfinal ThreadLocal threadLocal = new ThreadLocal();
privatestatic org.hibernate.SessionFactory sessionFactory;
/**
*ReturnstheThreadLocalSessioninstance. Lazyinitialize
*the<code>SessionFactory</code>ifneeded.
*
* @returnSession
* @throwsHibernateException
*/
publicstatic Session getSession() throws HibernateException {
Session session = (Session) threadLocal.get();
if (session == null || !session.isOpen()) {
if (sessionFactory == null) {
rebuildSessionFactory();
}
session = (sessionFactory != null) ? sessionFactory.openSession()
: null;
threadLocal.set(session);
}
return session;
}
/**
* Rebuildhibernatesessionfactory
*
*/
publicstaticvoid rebuildSessionFactory() {
try {
// configuration.configure(configFile);
//sessionFactory = configuration.buildSessionFactory();
sessionFactory =HibernateHelper.createSessionFactory();
} catch (Exception e) {
System.err
.println("%%%% Error Creating SessionFactory %%%%");
e.printStackTrace();
}
}
/**
* Closethesinglehibernatesessioninstance.
*
* @throwsHibernateException
*/
publicstaticvoid closeSession() throws HibernateException {
Session session = (Session) threadLocal.get();
threadLocal.set(null);
if (session != null) {
session.close();
}
}
}
Hibernate的Session,是對(duì)JDBC Connection的封裝。Hibernate不同于JDBC和iBatis。它默認(rèn)就把自動(dòng)提交設(shè)為false。也就是說,如果你不顯式的使用Hiberante事務(wù),那么根本不會(huì)操作數(shù)據(jù)庫!這點(diǎn)需要注意。
(四)Jbpm對(duì)Hiberante的封裝
另外,再說一下Jbpm對(duì)Hiberante所作的封裝。Jbpm使用的是Hiberante3的數(shù)據(jù)庫訪問技術(shù)。但是,它對(duì)Hibernate進(jìn)行了封裝。
使用Jbpm,事務(wù)管理更加簡單。
如:
public List getAllCanSeenTaskInstancesTransbbbbbb (PageModule view,bbbbbb userId) throws Exception {
JbpmContext jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
try {
returnthis.getAllCanSeenTaskInstances(view, userId);
}finally{
jbpmContext.close();
}
}
當(dāng) jbpmContext.close();方法執(zhí)行時(shí),自動(dòng)提交事務(wù)。如果發(fā)生異常,自動(dòng)回滾。并且,最后會(huì)關(guān)閉Hiberante本地線程中的Session,并清空該線程變量。
二、聲明方式的數(shù)據(jù)庫連接和事務(wù)管理
Spring容器管理業(yè)務(wù)代碼和DAO數(shù)據(jù)訪問代碼,是現(xiàn)在非常常用的一種方式。使用Spring時(shí),我們一般使用Spring聲明式事務(wù)來管理數(shù)據(jù)庫連接和事務(wù)。
另外,還有EJB容器也有聲明式事務(wù)管理的機(jī)制,兩者的使用方法大體相同,我就不再論述,這里只說Spring。
Spring管理下的JDBC,iBatis,Hibernate數(shù)據(jù)庫訪問方法。我們?cè)贒AO接口的實(shí)現(xiàn)類中,可以使用Spring提供的助手類的便利方法,進(jìn)行數(shù)據(jù)庫操作。也可以使用Spring提供的助手類,得到Connection,Session等進(jìn)行數(shù)據(jù)庫操作?;蛘呤褂肧pring助手類的execute()方法調(diào)用數(shù)據(jù)庫操作代碼。
如果你原先使用自己的助手類得到Connection,Session。那么你完全可以修改該助手類的實(shí)現(xiàn)方法,改為從Spring得到Connection,Session。這樣就不需要修改DAO接口的實(shí)現(xiàn)類!
Service層中的Dao方法,仍然無需修改。
對(duì)于Service層中的Transbbbbbb方法。我們需要去除“得到和關(guān)閉數(shù)據(jù)庫連接,管理事務(wù)”的代碼。然后,在Spring的配置文件中,對(duì)其應(yīng)用聲明式事務(wù)管理。運(yùn)行時(shí),Spring會(huì)通過SpringAOP技術(shù),自動(dòng)得到數(shù)據(jù)庫連接,管理事務(wù)。
可見,使用聲明式事務(wù)管理,我們只需要修改得到數(shù)據(jù)庫連接或者會(huì)話的Util助手類,以及Transbbbbbb方法即可。
綜上所述,可以看到,我提出的這一套事務(wù)管理最佳實(shí)踐是一套非常靈活、強(qiáng)大、簡潔的管理事務(wù)的最佳實(shí)踐。具有極其強(qiáng)大的適應(yīng)能力。采用這套編程范式,你可以很容易地徹底擺脫事務(wù)管理帶來的困擾!
本文標(biāo)簽:事務(wù)管理最佳實(shí)踐全面解析
* 由于無法獲得聯(lián)系方式等原因,本網(wǎng)使用的文字及圖片的作品報(bào)酬未能及時(shí)支付,在此深表歉意,請(qǐng)《事務(wù)管理最佳實(shí)踐全面解析》相關(guān)權(quán)利人與機(jī)電之家網(wǎng)取得聯(lián)系。










