[ASP.NET] 使用Lock鎖定 在多使用者或多執行緒下新增編號確保編號不重複

摘要:[ASP.NET] 使用Lock鎖定 在多使用者或多執行緒下新增編號確保編號不重複

前言


執行專案中常常出現的一個功能就是使用者要新增流水編號,而建立流水編號的方法大部分都是從資料表中取得最大編號加1再新增回去,但撰寫新增流水編號時常常可能一不小心而忘記考慮當有眾多使用者同時在建立資料時會同時取得同樣的編號後新增入資料庫,而造成編號重複的情況發生。

 

為了防止這種情況的發生我們可以使用 Lock 陳述式、交易隔離等級 等方法來處理,

接下來就來看看使用Lock 陳述式該如何處理。

 

範例


Lock 陳述式

lock 關鍵字可將陳述式區塊標記為關鍵區段 (Critical Section),其做法是為指定的物件取得互斥鎖定、執行陳述式,接著釋出該鎖定。

用法如下:

Object thisLock = new Object();
lock (thisLock)
{
    // Critical code section
}

thisLock 是用來判斷物件是否處於獨佔狀態,必須是參考型別的物件

於 Lock 區段內稱之為關鍵區段,如有程式運行到Lock區段內後,當期他的程式想要存取此區段程式時將被要求等待Lock區段內的程式運行完成後才可進入,

接下來我實際建立一個網站測式,

 

Step 1

  1. 建立一個空白網站
  2. 於網站中新增一個精簡的SQL Server資料庫名為db.mdf
  3. 建立一新網頁頁面名為Default.aspx
  4. Default.aspx 輸入以下HTML碼:
    <asp:Label ID="Label1" runat="server" Text="測試Lock陳述式"></asp:Label>
    <br /> <br />
    <asp:Button ID="btnNonLock" runat="server" Text="無Lock新增資料" 
        onclick="btnNonLock_Click" />
    <asp:Button ID="btnLock" runat="server" Text="有Lock新增資料" 
        onclick="btnLock_Click" />
    <br />
    <asp:Button ID="btnQry" runat="server" Text="查詢資料表" onclick="btnQry_Click" />
    <asp:Button ID="btnClearDB" runat="server" Text="清空資料表" 
        onclick="btnClearDB_Click" />
    <br />
    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
        DataKeyNames="ProNo" DataSourceID="SqlDataSource1">
        <Columns>
            <asp:BoundField DataField="ProNo" HeaderText="ProNo" ReadOnly="True" 
                SortExpression="ProNo" />
            <asp:BoundField DataField="ProName" HeaderText="ProName" 
                SortExpression="ProName" />
        </Columns>
    </asp:GridView>
    <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
        ConnectionString="<%$ ConnectionStrings:ConnectionString %>" 
        SelectCommand="SELECT * FROM [Product]"></asp:SqlDataSource>

Step 2

由於我需要模擬多使用者的情況,所以在這邊我始用多執行緒去模擬,

於 Default.aspx.cs 中加入以下程式碼:

建立一個提供給 Lock 的物件 

    // 提供Lock鎖定的物件
    private static object thisLock = new object();

    protected void Page_Load(object sender, EventArgs e)
    {
    }

在我的測試中提供了兩種測試:  1. 無Lock新增   2.有Lock新增

    /// 
    /// 未鎖定新增按鈕事件
    /// 
    protected void btnNonLock_Click(object sender, EventArgs e)
    {
        for (int i = 0; i < 20; i++)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(NonLockInsert));
            thread.Start(i);
        }
    }

    /// 
    /// 鎖定新增按鈕事件
    /// 
    protected void btnLock_Click(object sender, EventArgs e)
    {
        for (int i = 0; i < 20; i++)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(LockInsert));
            thread.Start(i);
        }
    }

    /// 
    /// 查詢資料按鈕事件
    /// 
    protected void btnQry_Click(object sender, EventArgs e)
    {
        GridView1.DataBind();
    }

    /// 
    /// 清除資料按鈕事件
    /// 
    protected void btnClearDB_Click(object sender, EventArgs e)
    {
        using (SqlConnection myConnection = new SqlConnection(SqlDataSource1.ConnectionString))
        {
            SqlCommand myCommand = new SqlCommand();
            myCommand.Connection = myConnection;
            myCommand.CommandText = "Delete From Product";
            myConnection.Open();
            myCommand.ExecuteNonQuery();
        }
        GridView1.DataBind();
    }

    /// 
    /// 未鎖定新增方法
    /// 
    private void NonLockInsert(object i)
    {
        using (SqlConnection myConnection = new SqlConnection(SqlDataSource1.ConnectionString))
        {
            SqlCommand myCommand = new SqlCommand();
            myCommand.Connection = myConnection;
            myCommand.CommandText = "Insert Product (ProNo,ProName) values (@ProNo,@ProName)";
            myCommand.Parameters.AddWithValue("@ProNo", GetProductSerialNumber());
            myCommand.Parameters.AddWithValue("@ProName", "Product_" + i);
            myConnection.Open();
            myCommand.ExecuteNonQuery();
        }
    }

    /// 
    /// 鎖定新增方法
    /// 
    private void LockInsert(object i)
    {
        
        lock (thisLock)
        {
            using (SqlConnection myConnection = new SqlConnection(SqlDataSource1.ConnectionString))
            {
                SqlCommand myCommand = new SqlCommand();
                myCommand.Connection = myConnection;
                myCommand.CommandText = "Insert Product (ProNo,ProName) values (@ProNo,@ProName)";
                myCommand.Parameters.AddWithValue("@ProNo", GetProductSerialNumber());
                myCommand.Parameters.AddWithValue("@ProName", "Product_" + i);
                myConnection.Open();
                myCommand.ExecuteNonQuery();
            }
        }
    }

    /// 
    /// 取得產品最大編號
    /// 
    private string GetProductSerialNumber()
    {
        using (SqlConnection myConnection = new SqlConnection(SqlDataSource1.ConnectionString))
        {
            SqlCommand myCommand = new SqlCommand();
            myCommand.Connection = myConnection;
            myCommand.CommandText = "Select ISNULL(Max(ProNo),0) From Product";
            myConnection.Open();
            string MaxNumber = myCommand.ExecuteScalar().ToString().Trim();
            if (MaxNumber == "0")
                return DateTime.Now.ToString("yyyyMMdd") + "01";
            else
            {
                int sn = int.Parse(MaxNumber.Substring(8, 2));
                sn++;
                string fsn = "0" + sn;
                return DateTime.Now.ToString("yyyyMMdd") + fsn.Substring(fsn.Length - 2);
            }
        }
    }

在上面的程式理簡單撰寫了兩種不同的新增方法,實際在測試後就可以發現其中的分別,

先測試無Lock時,當多人同時操作新增資料表時的情況,先點選「無Lock新增資料」後在點選「查詢資料」。

 

會發現 ProNo 的編號出現了很多重複的編號,因為同時間大量的程式同時去取得最大編號,所以造成部分取的的編號是一樣的。

在來看一下始用Lock的新增,點選「清除資料」後點選「有Lock新增資料」點選「查詢資料」。

 

現在則發現 ProNo 很乖的依照順序建立,因為當大量程式存取時每個執行緒都要乖乖排隊等候。

以上就是防止編號重複的做法。

 

補充:

VB的話要使用SyncLock 陳述式,請參考 http://msdn.microsoft.com/zh-tw/library/3a86s51t(v=vs.80).aspx

 

參考資料


lock 陳述式 (C# 參考)

執行緒同步處理 (C# 程式設計手冊)

 

範例檔案


WebLock.rar

 

 


以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)