Related Posts Plugin for WordPress, Blogger...

6-9 Beginning Transaction In NHibernate

接下來要來介紹如何在NHibernate操作Transaction。Transaction算是資料庫操作的基本概念之一,所以我就不針對理論上進行太多介紹。總之,Transaction大致上有四種特性:

1. Atomic:Transaction中必須執行所有的SQL,不能只有部分執行。
2. Consistent:Transaction結束時,全部的Table都必須更新成功,不能只有部份更新。
3. Isolated:Transaction所做的修改,必須與其他任何並行的Transaction所做的修改隔離。 資料庫的Concurrency control,常見的有兩階段鎖定法(Two-Phase Locking;2PL)、時間戳記法(Time-Stamp Order)、樂觀並行控制法(Optimistic Control),有興趣的人自己去查查看吧。
4. Durable:Transaction完成之後,其作用便永遠存在於系統之中,即使系統當機但修改仍會保存。

上述就是著名的ACID原則,盡量以較簡潔的方式進行解說了。如果不太了解的話,可以試著想看看Transaction適合的範例。比方說要在遊戲中實作一個交易所的系統,讓玩家之間可以互相買東西賣東西,這在線上遊戲中應該很常見。

玩家在買一樣東西的過程中,肯定會有查詢物品、點擊購買、付出金額、獲得物品這一系列的過程,賣家那邊也會有物品被查詢、獲得金額、失去物品這一系列的過程。這樣的過程就可以稱為Transaction了,一旦其中有一個過程失敗了,就必須還原到最初始的狀態。要不然發生了玩家付了錢卻沒有拿到東西,或是玩家拿到東西了,但是賣家那邊的東西沒有被扣掉之類的Bug就麻煩了。

還有在Transaction中我比較關注的是Concurrency Control的問題,如果有兩個玩家同時搶買同一樣東西的話,要注意是否會有同時都買到東西的問題,若沒用Transaction的話就有可能發生。MySQL提供Row-level的鎖定功能,可單獨針對某一筆資料進行鎖定,同一時間是不能有兩個人進行修改的。引用官方的介紹:

In the InnoDB transaction model, the goal is to combine the best properties of a multi-versioning database with traditional two-phase locking. InnoDB performs locking at the row level and runs queries as nonlocking consistent reads by default, in the style of Oracle. The lock information in InnoDB is stored space-efficiently so that lock escalation is not needed. Typically, several users are permitted to lock every row in InnoDB tables, or any random subset of the rows, without causing InnoDB memory exhaustion.

繁複的解說就到這邊,接下來,來實作看看吧。首先,我先將Table中的Account設為Unique,確保這個欄位的值不可以重複。

接著確定MySQL的儲存引擎使用的是InnoDB,才有支援Transaction功能唷。

接著如下圖撰寫程式,使用session.BeginTransaction發起交易,然後我新增了兩個Users類別,故意在Account的部分定義成相同的值。使用trasaction.Commit後,代表開始進行交易,會將上述的兩段Save都放進這次的交易過程中。

來到MySQL查詢看看結果,發現沒有插入任何的資料。 P.S. 上一章教學文中插入的7筆資料我已經預先刪除掉了。

然後來到Log文件中觀看,會看見ADOExceptionReporter - Duplicate entry 'abcd1' for key 'account',意即該欄位插入了相同的值。因為兩筆插入都在同一個Transaction下執行,只要有其中一筆失敗了,Table的狀態就會回復到使用Transaction之前。

接著再修改程式碼,這次將兩筆帳號的資料都設定為不相同。

執行程式以後,就成功插入兩筆資料嘍!

以下提供本次修改的NoliahFantasyServer.cs的原始碼:

using System;
using System.IO;
using Photon.SocketServer;
using ExitGames.Logging;
using ExitGames.Logging.Log4Net;
using log4net.Config;
using NHibernate;
using NHibernate.Cfg;
using NoliahFantasyServer.Model;

namespace NoliahFantasyServer
{
    public class NoliahFantasyServer : ApplicationBase
    {

        public static readonly ILogger logger = LogManager.GetCurrentClassLogger();
        public NoliahFantasyServer()
        {
        }

        // 當Client端發出Request的時候
        protected override PeerBase CreatePeer(InitRequest initRequest)
        {
            return new MyClientPeer(initRequest);
        }

        // Server端啟動的時候初始化
        protected override void Setup()
        {
            LoggerInit();
            ConnectToMySQL();
        }

        // Server端關閉的時候
        protected override void TearDown()
        {
        }

        void LoggerInit()
        {
            // 日誌初始化
            log4net.GlobalContext.Properties["Photon:ApplicationLogPath"] = 
                Path.Combine(this.ApplicationRootPath, "bin_Win64", "log");
            FileInfo loggerConfig = new FileInfo(Path.Combine(this.BinaryPath, "log4net.config"));
            if (loggerConfig.Exists)
            {
                // 設置使用log4net的Log功能
                LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                // 讓log4net讀取config
                XmlConfigurator.ConfigureAndWatch(loggerConfig);
            }
            logger.Info("Setup Log4Net Compeleted!");
        }

        void ConnectToMySQL(){
            Configuration conf = new Configuration();
            // 解析hibernate.cfg.xml
            conf.Configure();
            // 輸入Application的名稱,並自動解析Mappings文件
            conf.AddAssembly(typeof(NoliahFantasyServer).Assembly);

            // 發起Session才能使用CRUD功能
            ISessionFactory sessionFactory = null;
            ISession session = null;
            ITransaction transaction = null;
            try{
                sessionFactory = conf.BuildSessionFactory();
                // 發起Session
                session = sessionFactory.OpenSession();

                // 發起Transaction
                transaction = session.BeginTransaction();
                Users users1 = new Users()
                {
                    Account = "abcd1",
                    Pwd = "dsa"
                };
                Users users2 = new Users()
                {
                    Account = "abcd2",
                    Pwd = "dsa"
                };
                // 使用Save方法將Class傳給資料庫
                session.Save(users1);
                session.Save(users2);

                transaction.Commit();

            }catch(Exception e){
                logger.Error(e);
            }finally{
                if(transaction != null){
                    transaction.Dispose();
                }
                if(session != null){
                    session.Close();
                }
                if(sessionFactory != null){
                    sessionFactory.Close();
                }
            }
        }
    }
}

留言