數(shù)據(jù)庫(kù)“鎖”事一例:并發(fā)情景下重復(fù)主鍵問(wèn)題方案討論

在做的一個(gè)賬單計(jì)息功能,其中,賬單表的主鍵是BillId,varchar類型,BillId取值形如B0000001,生成規(guī)則是每次新增記錄是先從賬單表里計(jì)算出最大的BillId數(shù)字,然后+1再轉(zhuǎn)換后作為新增記錄的BillId。例如,B0000001、B0000002。

邏輯很簡(jiǎn)單,但考慮到并發(fā),技術(shù)上就要費(fèi)點(diǎn)心了。

為了簡(jiǎn)化場(chǎng)景,這里我寫了一些測(cè)試用例,涉及到一個(gè)包含兩個(gè)字段的表student(id int, sno varchar(32)),PK是id。數(shù)據(jù)庫(kù)是引擎為InnoDB的Mysql5。InnoDB支持事務(wù)操作。

并發(fā)處理之lock

這段邏輯用程序來(lái)實(shí)現(xiàn)的話,用lock關(guān)鍵字,就可以保證多線程情況下同時(shí)只能有一個(gè)線程來(lái)訪問(wèn)資源。

static object syncRoot = new object();
private void AddStudent_Lock(string name)
{
    lock (syncRoot)
    {
        object maxId = ExecuteScalar("select max(id) from student");
        if (maxId == null || maxId == DBNull.Value)
        {
            maxId = 0;
        }
        int newId = Convert.ToInt32(maxId) + 1;
        string sql = "INSERT INTO student VALUES(" + newId + ",'" + name + "');";
        ExecuteNonQuery(sql);
    }
}

測(cè)試用例:

[TestMethod]
public void Test3()
{
    ExecuteScalar("DROP TABLE IF Exists student;");
    ExecuteScalar("CREATE TABLE student(id INT NOT NULL,sno varchar(255),PRIMARY KEY (id));");
    ExecuteScalar("truncate table student;");

    //Stopwatch watch = new Stopwatch();
    //watch.Start();
    List<Thread> ths = new List<Thread>();
    for (int i = 0; i < 10; i++)
    {
        var thread = new Thread(() =>
          {
              try
              {
                  for (int j = 0; j < 500; j++)
                      AddStudent(Thread.CurrentThread.Name + "--" + j);
              }
              catch (Exception ex)
              {
                  Console.WriteLine(Thread.CurrentThread.Name + "--" + ex.Message);
              }
          });
        thread.Name = "thread" + i;
        ths.Add(thread);
    }
    ths.ForEach(t => t.Start());
    Thread.Sleep(15 * 1000);
}

運(yùn)行測(cè)試,ok。

這個(gè)方案解決了多線程下(同一進(jìn)程內(nèi))的并發(fā)問(wèn)題。但,在分布式系統(tǒng)的場(chǎng)景下,項(xiàng)目中的若干系統(tǒng)都涉及到生成賬單的邏輯,這個(gè)方案顯然就無(wú)能為力了。

數(shù)據(jù)庫(kù)鎖

看來(lái),如果多個(gè)系統(tǒng)都涉及到生成賬單的邏輯,其中一個(gè)方案是封裝這個(gè)生成賬單的邏輯,然后通過(guò)rpc來(lái)實(shí)現(xiàn)。另一個(gè)方案,假定這個(gè)邏輯在每個(gè)系統(tǒng)里都有,就要在數(shù)據(jù)庫(kù)層面來(lái)控制了。這里,我要介紹的是后者。

為了避免多個(gè)進(jìn)程同時(shí)訪問(wèn)這段邏輯出現(xiàn)重復(fù)主鍵沖突,所以,需要鎖表。mysql語(yǔ)句見(jiàn)下:

public void AddStudent(string name)
{
    string sql = @"
LOCK TABLES student WRITE;
SELECT @maxid:= MAX(id) FROM student for update;
SET @maxid:=IF(@maxid IS NULL,0,@maxid);
INSERT student VALUES(@maxid+1,@name);
UNLOCK TABLES;";
    ExecuteNonQuery(sql,new MySqlParameter("@name",name));

}

同樣用上面的測(cè)試用例來(lái)測(cè)試,ok。

以上是用mysql實(shí)現(xiàn)的。 在SqlServer里,因?yàn)閠-sql與pl/sql是兩大派系,其sql語(yǔ)句是這樣子的:

public void AddStudent(string name)
{
    string sql = @"
BEGIN Tran;
declare @maxid int;
SELECT @maxid= MAX(id) FROM student with(TABLOCKX);
SET @maxid=case when @maxid IS NULL then 0 else @maxid end;
INSERT student VALUES(@maxid+1,@name);
COMMIT;";
    ExecuteNonQuery(sql, new SqlParameter("@name", name));

}

為表加了TABLOCKX鎖后,其他事務(wù)將無(wú)法對(duì)表做任何讀寫操作。TABLOCKX與HOLDLOCK是有區(qū)別的,如果換成HOLDLOCK,運(yùn)行測(cè)試用例,會(huì)出現(xiàn)死鎖“事務(wù)(進(jìn)程id xx)與另一個(gè)進(jìn)程被死鎖在 鎖 資源上,并且已被選作死鎖犧牲品。請(qǐng)重新運(yùn)行該事務(wù)?!?,見(jiàn)下截圖:


一同學(xué)說(shuō),用存儲(chǔ)過(guò)程就可以解決這種并發(fā)沖突,不過(guò)經(jīng)過(guò)測(cè)試,這種說(shuō)法是不對(duì)的,即時(shí)是在存儲(chǔ)過(guò)程里,也要加上TABLOCKX和事務(wù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容