AppBoxFuture: 實現分佈式存儲的外鍵約束

AppBoxFuture: 實現分佈式存儲的外鍵約束

  關係數據庫與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.";
}

/<string>

調用此方法顯示"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.";
}

/<string>

調用此方法返回"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.";
}

/<entities.emploee>/<entities.orgunit>/<string>

調用此方法返回"Done.",此時可打開Emploee及OrgUnit的模型設計器內的"Data"欄驗證數據已被刪除。

# 四、本篇小結

  本篇主要介紹了框架集成的存儲引擎如何用另類的方式實現外鍵約束,如果您有問題或Bug報告,請留言或在[Github](https://github.com/enjoycode/appbox.deploy)提交Issue。


分享到:


相關文章: