AppBoxFuture(七): 分散式外來鍵約束

白菜園發表於2019-05-22

  關聯式資料庫與NoSql其中的一個主要區別是具備完整的外來鍵約束,雖說現在一些大廠在設計資料儲存結構時禁止使用外來鍵約束,靠業務邏輯來保證資料完整性,但考慮到是人就會犯錯,為了保證關鍵業務資料的完整性,所以作者還是決定在儲存引擎層面實現外來鍵約束功能。

一、實現思路

  由於儲存引擎是分散式的,所以引用者與被引用者可能存在不同的節點上(如訂單資料在節點1上,訂單引用的產品資料在節點2上),這樣實現外來鍵約束的方式就會與傳統關聯式資料庫有些不一樣,作者設計瞭如下圖所示的儲存結構,在RocksDB劃分一個ColumnFamily儲存引用索引(記錄誰的某個成員引用了哪個目標),以及儲存被引用者的計數器(記錄哪個分割槽引用了我,被引用了多少次),通過分散式事務保證資料與引用索引及計數器的一致性。
AppBoxFuture(七): 分散式外來鍵約束

  根據上述設計,以下描述的邏輯可以得到保證(為了方便以下訂單指引用者,產品指被引用者):

1.Insert訂單

  Insert時儲存引擎根據實體模型後設資料是否存在EntityRef成員,是則在同一事務內會向被引用者的分割槽自動傳送AddRefCommand,該命令會鎖定並判斷是否存在相應的記錄,如不存在則通知事務回滾。如果是同一事務內Insert產品再Insert訂單,AddRefCommand會檢測同一事務內是否存在被引用者記錄。事務遞交時原子儲存引用索引與引用計數。

2.Delete產品

  Delete時儲存引擎先判斷當前記錄所有分割槽的引用計數值是否等於0,不等於0則通知事務回滾。

3.Update or Delete訂單

  如果引用的產品變更,則刪除舊引用索引然後新增新引用索引;如果引用的產品設為Null或刪除訂單,則刪除引用索引,同時通知產品分割槽更新引用計數。

二、併發優化

  由於儲存引擎的分散式事務是基於2PL實現的,如果大量不同的事務Insert訂單且引用同一產品,會造成這些事務排隊執行,從而導致併發效能不理想。作者做了個簡單優化,允許不同事務的AddRefCommand共享鎖定被引用者以提高併發效能。就上述場景作者簡單測試了併發Insert帶EntityRef的效能,單節點Debug模式約14000tps(I74C8G虛擬機器),不帶外來鍵引用的併發Insert約28000tps。

三、簡單測試

  暫利用初始化時的實體Emploee及OrgUnit來做測試,OrgUnit.CreateById引用Emploee.Id。通過IDE新建一個服務模型,然後依次實現以下服務方法儲存釋出後將輸入游標定位在需要測試的方法名稱內,點選主選單->Service->Invoke進行服務方法呼叫測試。

1.測試引用至不存在的目標

public async Task<string> Test1()
{
    var ou = new Entities.OrgUnit();
    ou.Name = "Name";
    ou.CreateById = Guid.Empty; //指向不存在的目標
    await EntityStore.SaveAsync(ou);
    return "Done.";
}

呼叫此方法顯示"Insert error: ForeignKeyConstraint", 即違反外來鍵約束。

2.測試同一事務插入

public async Task<string> Test2()
{
    var txn = await Transaction.BeginAsync();
    try
    {
        //先新建並儲存被引用者
        var emp = new Entities.Emploee();
        emp.Name = "Batch name";
        emp.Account = emp.Name;
        emp.Birthday = new DateTime(1977, 3, 16);
        await EntityStore.SaveAsync(emp, txn);
        //再新建並儲存引用者
        var ou = new Entities.OrgUnit();
        ou.Name = "Batch ou";
        ou.CreateById = emp.Id;
        await EntityStore.SaveAsync(ou, txn);

        await txn.CommitAsync();
    }
    catch (Exception ex)
    {
        txn.Rollback();
        return $"Failed: {ex.Message}";
    }
    return "Done.";
}

呼叫此方法返回"Done.",此時可開啟Emploee及OrgUnit的模型設計器內的"Data"欄驗證插入的資料。

3.測試同一事務刪除

public async Task<string> Delete()
{
    var q1 = new TableScan<Entities.OrgUnit>();
    q1.Filter(t => t.Name == "Batch ou");
    var ous = await q1.ToListAsync();

    var q2 = new TableScan<Entities.Emploee>();
    q2.Filter(t => t.Name == "Batch name");
    var emps = await q2.ToListAsync();

    var txn = await Transaction.BeginAsync();
    try
    {
        //先刪除引用者, 如果註釋這一行則存在外來鍵約束導致下一行執行失敗
        await EntityStore.DeleteAsync(ous[0], txn);
        //再刪除被引用者
        await EntityStore.DeleteAsync(emps[0], txn);
        await txn.CommitAsync();
    }
    catch(Exception ex)
    {
        txn.Rollback();
        return $"Failed: {ex.Message}";
    }
    return "Done.";
}

呼叫此方法返回"Done.",此時可開啟Emploee及OrgUnit的模型設計器內的"Data"欄驗證資料已被刪除。

三、本篇小結

  本篇主要介紹了框架整合的儲存引擎如何用另類的方式實現外來鍵約束,Github上的執行時已經更新可測試。如果您有問題或Bug報告,請留言或在Github提交Issue。

相關文章