浅谈企业软件架构(5)
第五章 并發和事務
?
并發和事務是企業開發中常遇到的棘手問題,尤其對于新人來說有的時候他們是一個難以琢磨的名詞,但是企業開發中總會跟它們打交道,它們如影隨形總會在某個時候成為開發者夢魔。本章我們通過一些簡單的例子來說明并發和事務的一些基本概念。
5.1 常見的并發情況
如果我們在多線程或多進程中操作同一數據,就會遇到并發問題。企業開發中系統常常訪問的是存儲在數據庫中的業務數據,我們最常見的例子就是兩個用戶在相隔很短的時間內先后從數據庫中獲取了一份相同的某個業務單據的數據拷貝,用戶都完成各自的修改后分別向系統提交數據,這樣一個簡單的場景就導致了更新數據的并發問題之一,更新丟失。
5.1.1. 更新丟失
在前面的例子基礎上來演示更新丟失的情況,在UnitTest項目中的CustomerBizTest.cs增加一個測試方法LostUpdateTest來測試更新丟失的并發情況,代碼如下:
?
Codepublic?void?LostUpdateTest()????????{
????????????CustomerBiz?customerBizA?=?new?CustomerBiz();?//新創建一個CustomerBiz?A對象
????????????Customer?customerA?=?customerBizA.Get(_id);
????????????//?新創建另一個CustomerBiz?B對象
????????????//?目前的CustomerDal實現會創建一個新的NHibernate會話,來模擬兩個用戶訪問同一個數據。
????????????CustomerBiz?customerBizB?=?new?CustomerBiz();
????????????Customer?customerB?=?customerBizB.Get(_id);
????????????//A用戶修改數據,并提交數據
????????????customerA.Lastname??=?"吳";
????????????customerBizA.Edit(customerA);?//提交customer?A的修改
????????????//B用戶修改數據,也隨后提交數據?
????????????customerB.Address?=?"China";
????????????customerBizB.Edit(customerB);?//提交customer?B的修改
????????????CustomerBiz?customerBizC?=?new?CustomerBiz();?//新創建一個CustomerBiz?C對象
????????????Customer?customerC?=?customerBizC.Get(_id);
????????????Console.WriteLine(customerC.Lastname);
????????????Console.WriteLine(customerC.Address);
????????????Assert.AreNotEqual(customerC.Lastname,?"吳");
????????????Assert.AreEqual(customerC.Address,?"China");
????????}
? 我們來看測試結果和測試的輸出:
?
測試結果和輸出都證明了我們的斷言,用戶A的修改customerA.Lastname?= "吳" 最后提交到數據庫時其更新丟失,修改值被后面用戶B的更新覆蓋掉了,用戶A的更新就永遠的丟失了。
?
5.1.2. 不一致的讀
不一致的讀的場景,企業開發中常見的多表記錄維護的業務數據(如:報賬單),報銷人員A在第一次錄入完報賬單后,子表總共有5條記錄,數據提交回系統。這時經理B啟動了系統并讀到了這張報賬單準備進行審核,數據裝載到了經理B的電腦終端上,隨后經理B還沒來得及仔細看報賬單詳細內容,就接到了一個電話,在電話里他跟對方聊了一會。報銷人員A保存完數據后,發現報賬單明細(子表)有一個金額錯誤于是他重新修改了這條記錄,把金額從1562.42元修改成15620.42元,隨后把修改提交回系統。經理B接完電話仔細看了完報賬單沒什么問題審核了該單據。通常我們的單據審核的狀態會放在主表記錄里面,經理B審核了一份單據金額相差1萬多元的報賬單!
不一致的讀導致了上面的這種局面,經理B審核的單據與系統最后報銷人員A提交的單據存在不一致的數據。
5.1.4. 隔離數據操作
上述兩種情況都會導致業務數據正確性的失敗,從而導致系統錯誤行為。通過數據隔離可以避免上述兩種并發的基本情況,一個用戶讀取數據后,別的用戶不能在讀取數據,或者只能只讀讀取數據。企業開發中常用單據狀態來對單據數據進行過濾,如:草稿狀態,上面的例子里如果使用草稿狀態,經理B是不能查看到報銷人員A未正式提交的狀態的報銷單。
通過隔離數據操作來避免正確性失敗。但是只考慮數據的正確性是不夠的,隔離操作也會導致了一個僅僅瀏覽數據的用戶鎖住了數據導致真正要修改業務數據的用戶要等他瀏覽完數據后才能更改業務單據。企業開中我們也要考慮數據使用的靈活性,即多少個并發活動可以同時發生。
5.1.4. 樂觀并發控制和悲觀并發控制
樂觀并發控制在上述場景下,會給經理B提示他提交的數據與系統當前的拷貝不一致,他需要重新加載數據來進行審核,避免造成系統正確性失敗的錯誤。樂觀并發控制是關于沖突檢測,并提示用戶接下來如何操作。悲觀并發控制如果使用在上述場景中就是報銷人員A在經理B讀取報賬單后不能再修改他的報賬單據了。如果他確實需要修改單據必須等到審核完單據后,去找經理B取消審核該單據,然后再來修改自己的報賬單。悲觀并發控制可以看成隔離數據的操作來避免并發操作的產生。樂觀并發控制需要導致提交沖突的用戶放棄自己的數據修改,犧牲自己前面的工作。
???
5.2 事務
企業開發中處理并發最主要的工具就是事務,通常使用ACID來描述軟件事務。
原子性:在一個事務里,所有的操作都必須全部完成。要么全部成功,要么回滾所有操作。常見的例子就算是企業開發中的入庫單單據,入庫單如果某物料A入庫數據量為100,當前物料A的庫存數據量為30,那么在入庫單提交回系統的同時,當前庫存物料A的紀錄也需要把庫存書更新為30+100=130,部分完成不是事務的概念。
一致性:事務開始和完成的過程中,系統的其它資源必須是一致的,沒有被改變的狀態,也就是事務中不能有其他事務改變系統的資源狀態。
隔離性:事務成功完成后,其提交的數據才能被其他事物操作讀取本次事務結果。
持久性: 已提交的事務必須是永久保存的。
5.2.1. 系統事務
系統事務常說的就是由關系數據庫系統一組SQL命令的組合。如下:?
Code=?980;????????INSERT?INTO?Customer?(Firstname,?Lastname,?Gender,?Address,?Remark,?Active,?CustomerId)?
????????VALUES?('Howard',?'Wu',?'男',?'中國',?'',?0,?100);
????UPDATE?Customer?SET?Firstname?=?'Howard',?Lastname?=?'吳',?Gender?=?'男',?Address?=?'中國',?
????????Remark?=?'',?Active?=?0?WHERE?CustomerId?=?100;?
????DELETE?FROM?Customer?WHERE?CustomerId?=?'101';
END?TRY
BEGIN?CATCH
????SELECT?
????????ERROR_NUMBER()?AS?ErrorNumber
????????,ERROR_SEVERITY()?AS?ErrorSeverity
????????,ERROR_STATE()?AS?ErrorState
????????,ERROR_PROCEDURE()?AS?ErrorProcedure
????????,ERROR_LINE()?AS?ErrorLine
????????,ERROR_MESSAGE()?AS?ErrorMessage;
????IF?@@TRANCOUNT?>?0
????????ROLLBACK?TRANSACTION;
END?CATCH;
IF?@@TRANCOUNT?>?0
????COMMIT?TRANSACTION;
GO
? 在事務里任何一個命令如果執行失敗了就必須回滾所有前面執行的SQL命令,只有都執行成功了才提交到數據庫,最終完成系統事務。
?
5.2.2. 業務事務
業務事務就是前面舉的入庫單的例子,我們須在系統事務執行業務事務才能把業務事務變成我們業務系統中的事務處理,來實現客戶的業務事務。現在我們回過頭來看我們的前面的例子,我們的Dal層都是針對一個Model來設計的,然后由Biz層來調用Dal層提交Model數據。我們把針對單個Model的系統事務實現在了Dal層。可是操作多個Model對象的業務事務是在Biz層實現的,這樣就給我們Dal層設計出了一個難題,至少現在我們的Dal層是不能實現這樣的支持,增、改、刪除方法都默認啟動了事務,我們需要重構我們得代碼來實現Biz層多Model提交的業務事務支持。
這樣的設計是基于業務事務是由業務層(Biz)來封裝的,也只有在Biz層最應該負責在哪兒啟動事務哪兒提交事務,以及什么情況下回滾事務。因為Nhibernate的系統事務是由它的Session來啟動的。我們需要增加一個專門管理事務的類封裝事務的啟動、提交和回滾。避免Biz層直接引用Nhibernate的Session來實現業務事務要求(數據訪問層的實現邏輯對于Biz層是不可見的)。?
?
5.2.3. 重構Dal層代碼
我們增加一個類來專門管理NHibernate Session中的系統事務,同時在單Model提交的Dal中仍然有自己默認系統事務調用,這樣的寫法的方便性在于對于常見的單Model提交的系統事務由Dal層默認實現,減少在Biz層的事務調用的繁瑣操作。
NHibSessionMgr.cs NHibernate事務管理類
?
Code?System;using?System.Collections.Generic;
using?System.Linq;
using?System.Text;
using?NHibernate;
namespace?Dal
{
????public?class?NHibSessionMgr
????{
????????#region?私有變量
????????private?static?NHibernate.ISessionFactory?_sessionFactory;
????????private?static?ISession?_session?;
????????#endregion
????????#region?構造函數
????????private?NHibSessionMgr(){}
????????#endregion
????????private?static?ISessionFactory?GetSessionFactory()
????????{
????????????if?(_sessionFactory?==?null)
????????????{
????????????????NHibernate.Cfg.Configuration?cfg?=?new?NHibernate.Cfg.Configuration().AddAssembly("Model")
????????????????????.Configure();
????????????????_sessionFactory?=?cfg.BuildSessionFactory();
????????????}
????????????return?_sessionFactory;
????????}??
????????public?static?NHibernate.ISession?GetSession(bool?otherSession)
????????{
????????????if?(_sessionFactory?==?null)
????????????{
????????????????_sessionFactory?=?GetSessionFactory();
????????????}
????????????if?(otherSession)
????????????{
????????????????_session?=?_sessionFactory.OpenSession();
????????????}
????????????else
????????????{
????????????????if?(_session?==?null)
????????????????{
????????????????????_session?=?_sessionFactory.OpenSession();
????????????????}
????????????????else?if?(!_session.IsOpen)
????????????????{
????????????????????_session.Reconnect();
????????????????}
????????????}
????????????return?_session;
????????}
????????///?<summary>
????????///?獲取NHibernate?Session實例
?????????///?通過加載當前工程配置文件生成的SessionFactory,并創建Session
????????///?</summary>
????????///?<returns></returns>
????????public?static?NHibernate.ISession?GetSession()
????????{
????????????return?GetSession(false);
????????}??
????}
}
? 本類負責獲得NHibernate的會話對象,代碼中使用了單件模式,但是為了在某些場合下仍可創建新的會話,我們使用了帶參數的GetSession(bool otherSession)函數來實現返回新的話。?
NHibernateSession.cs代碼如下:?
Code?System;using?System.Collections.Generic;
using?System.Linq;
using?System.Text;
using?NHibernate;
namespace?Dal
{
????public?class?NHibernateSession
????{
????????#region?私有變量
????????private?bool?_otherSession?;
????????private?ITransaction?_trans?;
????????private??ISession?_session;?
????????#endregion
????????#region?會話對象
????????protected?ISession?Session
????????{
????????????get
????????????{
????????????????return?_session;
????????????}
????????}
????????#endregion
????????#region?構造函數
????????public?NHibernateSession(bool?otherSession)
????????{
????????????_otherSession?=?otherSession;
????????????//獲得NHibernate會話對象
????????????_session?=?NHibSessionMgr.GetSession(otherSession);
????????}
????????public?NHibernateSession()
????????????:?this(false)
????????{
????????}
????????#endregion
?????public?void?CloseSession()
?????{
??????????_session.Close();
?????}
#region?事務處理(統一對事務進行管理)
????????///?<summary>
????????///?開始事務
????????///?</summary>
????????public?void?TransBegin()
????????{
????????????if?(_session.Transaction?!=?null?&&?_session.Transaction.IsActive)
????????????{
????????????????_trans?=?null;
????????????}
????????????else
????????????{
????????????????_trans?=?_session.BeginTransaction();
????????????}
????????}
????????///?<summary>
????????///?回滾事務
????????///?</summary>
????????public?void?TransRollBack()
????????{
????????????if?(_trans?!=?null)
????????????{
????????????????_trans.Rollback();
????????????????_trans?=?null;
????????????}
????????}
????????///?<summary>
????????///?提交事務
????????///?</summary>
????????public?void?TransCommit()
????????{
????????????if?(_trans?!=?null)
????????????{
????????????????_trans.Commit();
????????????????_trans?=?null;
????????????}
????????}
????????#endregion
????}
}
? CustomerDal.cs代碼如下:?
using?System.Collections.Generic;
using?System.Linq;
using?System.Text;
using?NHibernate;
using?NHibernate.Cfg;
using?NHibernate.Criterion;
using?Model;
namespace?Dal
{
????public?class?CustomerDal?:?NHibernateSession
????{
????????public?CustomerDal()
????????????:?base(false)?{?}????????
????????public?Customer?Get(Int32?customerId)
????????{
????????????Customer?customer?=?(Customer)Session.Get(typeof(Customer),?customerId);
????????????if?(customer?!=?null)
????????????{
????????????????return?customer;
????????????}
????????????else{?return?null;?}?
????????}
???????public?Boolean?Add(Customer?customer)
????????{
????????????TransBegin();
????????????try
????????????{
????????????????Session.Save(customer);
????????????????TransCommit();
????????????????return?true;
????????????}
????????????catch
????????????{
????????????????TransRollBack();
????????????????if?(Session.Contains(customer))
????????????????{
????????????????????Session.Evict(customer);
????????????????}
????????????????return?false;
????????????}
????????}
??????? public?Boolean?Edit(Customer?customer)
????????{
????????????TransBegin();
????????????try
????????????{
????????????????Session.SaveOrUpdate(customer);
????????????????TransCommit();
????????????????return?true;
????????????}
????????????catch
????????????{
????????????????TransRollBack();
????????????????if?(Session.Contains(customer))
????????????????{
????????????????????Session.Evict(customer);
????????????????}
????????????????return?false;
????????????}
????????}
????????public?Boolean?Delete(Customer?customer)
????????{
????????????TransBegin();
????????????try
????????????{
????????????????Session.Delete(customer);
????????????????TransCommit();
????????????????return?true;
????????????}
????????????catch
????????????{
????????????????TransRollBack();
????????????????if?(Session.Contains(customer))
????????????????{
????????????????????Session.Evict(customer);
????????????????}
????????????????return?false;
????????????}
????????}
????}
}
? 注意:上面重構代碼的變化,事務調用我們直接調用了基類的統一封裝事務方法。Dal層的類需要從基類HibernateSession繼承而來,事務函數使用的是基類統一封裝的事務函數,這是這次重構中最關鍵的調整。這樣事務就不再直接使用Hibernate Session的事務。同時,為了能在單元測試中模擬不同會話的需要,我們的NHibernateSession類是可以通過構造函數的otherSession參數來確定是否使用另個會話來進行測試,這個對于我們進行并發沖突測試很重要。
?
5.2.4. 重構Biz層代碼
CustomerBiz.cs 只調整構造函數,目的也是確保可以打開另一會話來進行我們需要的單元測試或業務邏輯。????
Codepublic?CustomerBiz(?Boolean?otherSession?)????????{
????????????_customerDal?=?new?CustomerDal(otherSession);
????????}
????????public?CustomerBiz()
????????????:this(false)
????????{
????????????
????????}
?
5.2.5. 重構更新丟失單元測試代碼?
Codepublic?void?LostUpdateTest()????????{
????????????CustomerBiz?customerBizA?=?new?CustomerBiz(true);?//新創建一個CustomerBiz?A對象
????????????Customer?customerA?=?customerBizA.Get(_id);
?????????//?新創建另一個CustomerBiz?B對象
?????????//?目前的CustomerDal實現會創建一個新的NHibernate會話,來模擬兩個用戶訪問同一個數據。
????????????CustomerBiz?customerBizB?=?new?CustomerBiz(true);
????????????Customer?customerB?=?customerBizB.Get(_id);
????????????//A用戶修改數據,并提交數據
????????????customerA.Lastname??=?"吳";
????????????customerBizA.Edit(customerA);?//提交customer?A的修改
????????????//B用戶修改數據,也隨后提交數據?
????????????customerB.Address?=?"China";
????????????customerBizB.Edit(customerB);?//提交customer?B的修改
????????????CustomerBiz?customerBizC?=?new?CustomerBiz(true);?//新創建一個CustomerBiz?C對象
????????????Customer?customerC?=?customerBizC.Get(_id);
????????????Console.WriteLine(customerC.Lastname);
????????????Console.WriteLine(customerC.Address);
????????????Assert.AreNotEqual(customerC.Lastname,?"吳");
????????????Assert.AreEqual(customerC.Address,?"China");
????????}
?
? 運行單元測試通過說明我們的重構符合了預期的要求。
?
5.2.6. 實現業務事務
目前為止我們還沒有實現本章說的業務事務,也就是在一次業務事務中涉及到對多個Model實例的操作,最后需要確保業務事務被系統事務正確的提交到中,避免出現數據丟失或者完整性缺失等情形。業務事務都是在Biz層產生的,我們通過重構Biz層代碼來實現多Model操作的系統事務調用。
我們增加一個BaseBiz基類來實現統一的事務調用。BaseBiz.cs代碼如下:
?
Code?System;using?System.Collections.Generic;
using?System.Linq;
using?System.Text;
using?Dal;
namespace?Biz
{
????public?class?BaseBiz
????{
????????private?NHibernateSession?_session?=?null;
????????private?bool?_otherSession;
????????#region?構造函數
????????///?<summary>
????????///?構造方法
????????///?</summary>????????
????????public?BaseBiz(bool?otherSession)
????????{
????????????_otherSession?=?otherSession;
????????????_session?=?new?NHibernateSession(otherSession);
????????}
????????public?BaseBiz()
????????????:?this(false)?{?}
????????#endregion
????????#region?事務處理(統一對事務進行管理)
????????///?<summary>
????????///?開始事務
????????///?</summary>
????????public?void?TransBegin()
????????{
????????????_session.TransBegin();
????????}
????????///?<summary>
????????///?回滾事務
????????///?</summary>
????????public?void?TransRollBack()
????????{
????????????_session.TransRollBack();
????????}
????????///?<summary>
????????///?提交事務
????????///?</summary>
????????public?void?TransCommit()
????????{
????????????_session.TransCommit();
????????}
????????#endregion
????}
}
?
現在我們假設有一個業務需要批量添加的用戶必須在一個事務里完成,也就是說我們必須保證批量添加的用戶數據要么都提交到系統中,要么全部回滾數據,不允許部分數據提交的情形出現。
?
單元測試代碼如下:
?
Codepublic?void?AddCustomersTest()????????{
????????????Customer?customerA?=?new?Customer();
????????????customerA.CustomerId?=?101;
????????????customerA.Firstname?=?"Howard?A";
????????????customerA.Lastname?=?"Wu";
????????????customerA.Gender?=?"男";
????????????customerA.Address?=?"中國";
????????????Customer?customerB?=?new?Customer();
????????????customerB.CustomerId?=?102;
????????????customerB.Firstname?=?"Howard?B";
????????????customerB.Lastname?=?"Wu";
????????????customerB.Gender?=?"男";
????????????customerB.Address?=?"中國";
????????????IList<Customer>?list?=?new?List<Customer>();
????????????list.Add(customerA);
????????????list.Add(customerB);
????????????CustomerBiz?customerBizA?=?new?CustomerBiz(true);
????????????customerBizA.Add(list);
????????????list.Remove(customerA);
????????????list.Remove(customerB);
????????????//提交后,驗證數據是否提交成功
????????????CustomerBiz?customerBizB?=?new?CustomerBiz(true);
????????????Customer?customerA1?=?customerBizB.Get(101);
????????????Assert.AreEqual(customerA1.Firstname,?customerA.Firstname);
????????????Customer?customerB1?=?customerBizB.Get(102);
????????????Assert.AreEqual(customerB1.Firstname,?customerB.Firstname);
????????????//刪除本次成功提交的測試數據
????????????customerBizA.Delete(customerA);
????????????customerBizA.Delete(customerB);
????????????Customer?customerC?=?new?Customer();
????????????customerC.CustomerId?=?103;
????????????customerC.Firstname?=?"Howard?C";
????????????customerC.Lastname?=?"Wu";
????????????customerC.Gender?=?"男";
????????????customerC.Address?=?"中國";
????????????Customer?customerD?=?new?Customer();
????????????customerD.CustomerId?=?104;
????????????customerD.Firstname?=?"Howard?D";
????????????customerD.Lastname?=?"Wu";
????????????customerD.Gender?=?"男abc";??//屬性值超長,導致提交失敗,驗證數據是否全部回滾。
????????????customerD.Address?=?"中國";
????????????list.Add(customerC);
????????????list.Add(customerD);
????????????customerBizA.Add(list);
????????????//提交后,驗證數據是否全部回滾
????????????CustomerBiz?customerBizC?=?new?CustomerBiz(true);
????????????Customer?customerC1?=?customerBizC.Get(103);
????????????Assert.IsNull(customerC1);
????????????Customer?customerD1?=?customerBizC.Get(104);
????????????Assert.IsNull(customerD1);
????????}
?
CustomerBiz代碼重構和增加批量添加函數如下:
?
Code?System;using?System.Collections.Generic;
using?System.Linq;
using?System.Text;
using?Model;
using?Dal;
namespace?Biz
{
????public?class?CustomerBiz?:?BaseBiz
????{
????????private?CustomerDal?_customerDal?;
????????public?CustomerBiz(?Boolean?otherSession?)
????????????:?base(otherSession)
????????{
????????????_customerDal?=?new?CustomerDal();
????????}
????????public?CustomerBiz()
????????????:?this(false)
????????{
????????}
????????public?Boolean?Add(Customer?customer)
????????{
????????????return?_customerDal.Add(customer);
????????}
????????public?Customer?Get(Int32?customerId)
????????{
????????????return?_customerDal.Get(customerId);????????????
????????}???????
????????public?Boolean?Edit(Customer?customer)
????????{
????????????return?_customerDal.Edit(customer);
????????}
????????public?Boolean?Delete(Customer?customer)
????????{
????????????return?_customerDal.Delete(customer);
????????}
????????public?void?Active(Customer?customer)
????????{
????????????customer.Active?=?1;
????????????_customerDal.Edit(customer);
????????}
????????public?bool?Add(IList<Customer>?customers)
????????{
????????????TransBegin();??//開始業務事務
????????????try
????????????{
????????????????foreach?(Customer?c?in?customers)
????????????????{
????????????????????_customerDal.Add(c);
????????????????}
????????????????TransCommit();?//提交業務事務
????????????????return?true;
????????????}
????????????catch
????????????{
????????????????TransRollBack();???//回滾業務事務????
????????????????_customerDal.CloseSession();?//提交失敗后關閉當前會話
????????????????return?false;
????????????}????
????????}
????}
}
?
運行單元測試通過,注意看單元測試代碼邏輯,我們假定了兩種情況一種是正常提交到系統后,我們使用新的會話來獲取數據驗證是否與新增的數據是否一致,還有另一段測試代碼我們設計了一種提交錯誤的場景,來檢驗提交的數據是否被全部回滾了,不存在部分提交的情況。我們的重構實現了我們預期的功能。
?
5.3 結語
本章我們簡要的描述了軟件開發中并發和事務,并發會產生“更新丟失”和“不一致的讀”問題,這兩種情況都會導致數據正確性的失敗,系統出現錯誤的業務邏輯操作行為。如果沒有兩個人同時操作系統中相同的數據,就不會有并發問題。通過隔離數據操作可以解決并發帶來的正確性問題,但是卻缺少了并發帶來的靈活性。樂觀并發和悲觀并發是兩種處理并發的機制,兩種機制各有優缺點。樂觀鎖策略可以看成是關于沖突的檢測,悲觀所則是關于沖突的避免。他們倆選擇使用是看場景來的,如果沖突的頻率和嚴重性很小,或者客戶可以接受沖突導致的數據更新丟失,就采用樂觀鎖策略,它可以帶來很好的并發性。如果并發沖突導致的結果對于用戶來說是不可接受的,就只能使用悲觀鎖策略。
事務是企業開發中處理并發最主要的工具,事務經常使用ACID來描述。系統事務主要是指由關系數據庫或事務系統所支持的事務,如:一組sql命令組合。系統事務對于業務系統用戶來說是沒有意義的,只有通過系統事務實現的業務事務對于用戶來說才有價值。如我們例子里的批量添加用戶是一個業務事務,它需要在一個系統事務中實現。
下一章我們將繼續描述幾個復雜的業務事務是如何通過系統事務來實現的。加深我們對業務事務與系統事務關系的理解。
轉載于:https://www.cnblogs.com/aaa6818162/archive/2009/07/31/1535808.html
總結
以上是生活随笔為你收集整理的浅谈企业软件架构(5)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Hashtable:仅有两列的表
- 下一篇: cacls 使用方法