AppBoxFuture(七): 分布式外键约束

  关系数据库与NoSql其中的一个主要区别是具有完整的外键约束,虽然说如今一些大厂在设计数据存储结构时禁止使用外键约束,靠业务逻辑来保证数据完整性,但考虑到是人就会犯错,为了保证关键业务数据的完整性,因此做者仍是决定在存储引擎层面实现外键约束功能。git

1、实现思路

  因为存储引擎是分布式的,因此引用者与被引用者可能存在不一样的节点上(如订单数据在节点1上,订单引用的产品数据在节点2上),这样实现外键约束的方式就会与传统关系数据库有些不同,做者设计了以下图所示的存储结构,在RocksDB划分一个ColumnFamily存储引用索引(记录谁的某个成员引用了哪一个目标),以及存储被引用者的计数器(记录哪一个分区引用了我,被引用了多少次),经过分布式事务保证数据与引用索引及计数器的一致性。
github

  根据上述设计,如下描述的逻辑能够获得保证(为了方便如下订单指引用者,产品指被引用者):数据库

1.Insert订单

  Insert时存储引擎根据实体模型元数据是否存在EntityRef成员,是则在同一事务内会向被引用者的分区自动发送AddRefCommand,该命令会锁定并判断是否存在相应的记录,如不存在则通知事务回滚。若是是同一事务内Insert产品再Insert订单,AddRefCommand会检测同一事务内是否存在被引用者记录。事务递交时原子保存引用索引与引用计数。并发

2.Delete产品

  Delete时存储引擎先判断当前记录全部分区的引用计数值是否等于0,不等于0则通知事务回滚。app

3.Update or Delete订单

  若是引用的产品变动,则删除旧引用索引而后添加新引用索引;若是引用的产品设为Null或删除订单,则删除引用索引,同时通知产品分区更新引用计数。框架

2、并发优化

  因为存储引擎的分布式事务是基于2PL实现的,若是大量不一样的事务Insert订单且引用同一产品,会形成这些事务排队执行,从而致使并发性能不理想。做者作了个简单优化,容许不一样事务的AddRefCommand共享锁定被引用者以提升并发性能。就上述场景做者简单测试了并发Insert带EntityRef的性能,单节点Debug模式约14000tps(I74C8G虚拟机),不带外键引用的并发Insert约28000tps。async

3、简单测试

  暂利用初始化时的实体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"栏验证数据已被删除。

4、本篇小结

  本篇主要介绍了框架集成的存储引擎如何用另类的方式实现外键约束,Github上的运行时已经更新可测试。若是您有问题或Bug报告,请留言或在Github提交Issue。

相关文章
相关标签/搜索