關係數據庫與NoSql其中的一個主要區別是具有完整的外鍵約束,雖然說如今一些大廠在設計數據存儲結構時禁止使用外鍵約束,靠業務邏輯來保證數據完整性,但考慮到是人就會犯錯,爲了保證關鍵業務數據的完整性,因此做者仍是決定在存儲引擎層面實現外鍵約束功能。git
因爲存儲引擎是分佈式的,因此引用者與被引用者可能存在不一樣的節點上(如訂單數據在節點1上,訂單引用的產品數據在節點2上),這樣實現外鍵約束的方式就會與傳統關係數據庫有些不同,做者設計了以下圖所示的存儲結構,在RocksDB劃分一個ColumnFamily存儲引用索引(記錄誰的某個成員引用了哪一個目標),以及存儲被引用者的計數器(記錄哪一個分區引用了我,被引用了多少次),經過分佈式事務保證數據與引用索引及計數器的一致性。
github
根據上述設計,如下描述的邏輯能夠獲得保證(爲了方便如下訂單指引用者,產品指被引用者):數據庫
Insert時存儲引擎根據實體模型元數據是否存在EntityRef成員,是則在同一事務內會向被引用者的分區自動發送AddRefCommand,該命令會鎖定並判斷是否存在相應的記錄,如不存在則通知事務回滾。若是是同一事務內Insert產品再Insert訂單,AddRefCommand會檢測同一事務內是否存在被引用者記錄。事務遞交時原子保存引用索引與引用計數。併發
Delete時存儲引擎先判斷當前記錄全部分區的引用計數值是否等於0,不等於0則通知事務回滾。app
若是引用的產品變動,則刪除舊引用索引而後添加新引用索引;若是引用的產品設爲Null或刪除訂單,則刪除引用索引,同時通知產品分區更新引用計數。框架
因爲存儲引擎的分佈式事務是基於2PL實現的,若是大量不一樣的事務Insert訂單且引用同一產品,會形成這些事務排隊執行,從而致使併發性能不理想。做者作了個簡單優化,容許不一樣事務的AddRefCommand共享鎖定被引用者以提升併發性能。就上述場景做者簡單測試了併發Insert帶EntityRef的性能,單節點Debug模式約14000tps(I74C8G虛擬機),不帶外鍵引用的併發Insert約28000tps。async
暫利用初始化時的實體Emploee及OrgUnit來作測試,OrgUnit.CreateById引用Emploee.Id。經過IDE新建一個服務模型,而後依次實現如下服務方法保存發佈後將輸入光標定位在須要測試的方法名稱內,點擊主菜單->Service->Invoke進行服務方法調用測試。分佈式
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", 即違反外鍵約束。高併發
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"欄驗證插入的數據。性能
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。