多租户DB。文档ID和授权策略

时间:2017-06-13 02:46:41

标签: c# .net ravendb multi-tenant nosql

我正在权衡拥有单独的数据库(每家公司一个)与一个多租户数据库(与所有公司合作)。标准:

  • 用户只能属于一家公司,无法访问其他公司的文档。
  • 系统管理员需要为所有公司维护数据库。
  • 公司/租户数量 - 从数百到数万
  • 对于所有公司/租户,都有一个带身份验证的入口点(它将解析租户并将其发送到正确的数据库)。

问题#1 。有没有"良好做法"在RavenDB中设计多租户数据库?

有类似的post for MongoDB。 RavenDB会是一样的吗? 更多记录会影响indexes,但是否有可能使一些租户遭受其他租户积极使用索引的影响?

如果我要为 RavenDB 设计一个多租户数据库,那么我将实现视为

  • 每个公司/租户都有一个标记,因此一家公司的所有用户都拥有公司标记的权限,所有顶级文档都有标记(请参阅KB on Auth Bundle
  • 有一个租户ID标记作为每个文档ID的前缀(由于official recommendation使用顺序标识符,我很高兴在服务器上生成ID)

问题#2.1 。标记使用Authorization Bundle解析用户的最佳方式'权限和阻止访问其他租户的文件?

问题#2.2 。在顶级文档的ID前缀中使用租户ID有多重要? 我想,这里主要考虑的是一旦通过标签解决权限或我错过了某些内容后的性能?

2 个答案:

答案 0 :(得分:3)

如果您要拥有几百家公司,那么每家公司的数据库都可以。 如果你要成千上万,那么你想把它全部放在一个数据库中。

数据库可以消耗非常少量的资源,并且拥有大量资源可能比单个更大的数据库贵很多。

我建议不要使用授权捆绑包,它要求我们进行O(N)过滤。最好直接在查询中添加TenantId = XYZ,也许可以通过查询监听器。

不要过于担心顺序标识符。它们有影响,但它们并不重要,除非你每秒产生数万个。

请参阅处理多租户的侦听器示例。

一个查询侦听器,用于将当前租户ID添加到所有查询中(过滤掉其他租户的条目):

public class TenantedEntityQueryListener : IDocumentQueryListener
{
    private readonly ICurrentTenantIdResolver _resolver;

    public TenantedEntityQueryListener(ICurrentTenantIdResolver resolver) : base(resolver) 
    {
        _resolver = resolver;
    }

    public void BeforeQueryExecuted(IDocumentQueryCustomization customization)
    {
        var type = customization.GetType();
        var entityType = type.GetInterfaces()
                             .SingleOrDefault(i => i.IsClosedTypeOf(typeof(IDocumentQuery<>))
                                                || i.IsClosedTypeOf(typeof(IAsyncDocumentQuery<>)))
                             ?.GetGenericArguments()
                             .Single();
        if (entityType != null && entityType.IsAssignableTo<ITenantedEntity>())
        {
            // Add the "AND" to the the WHERE clause 
            // (the method has a check under the hood to prevent adding "AND" if the "WHERE" is empty)
            type.GetMethod("AndAlso").Invoke(customization, null);
            // Add "TenantId = 'Bla'" into the WHERE clause
            type.GetMethod( "WhereEquals", 
                            new[] { typeof(string), typeof(object) }
                          )
                .Invoke(customization,
                    new object[]
                    {
                        nameof(ITenantedEntity.TenantId),
                        _resolver.GetCurrentTenantId()
                    }
                );
        }
    }
}

商店监听器,用于将当前租户ID设置为所有租用实体:

public class TenantedEntityStoreListener : IDocumentStoreListener
{
    private readonly ICurrentTenantIdResolver _resolver;

    public TenantedEntityStoreListener(ICurrentTenantIdResolver resolver) : base(resolver)
    {
        _resolver = resolver;
    }

    public bool BeforeStore(string key, object entityInstance, RavenJObject metadata, RavenJObject original)
    {
        var tenantedEntity = entityInstance as ITenantedEntity;
        if (tenantedEntity != null)
        {
            tenantedEntity.TenantId = _resolver.GetCurrentTenantId();
            return true;
        }

        return false;
    }

    public void AfterStore(string key, object entityInstance, RavenJObject metadata) {}
}

界面,由支持多租户的顶级实体实现:

public interface ITenantedEntity
{
    string TenantId { get; set; }
}

答案 1 :(得分:0)

我试图通过编辑他的帖子来讨论技术实现的@AyendeRahien是不成功的:),所以下面我将从上面解决我的担忧:

<强> 1。多租户数据库与多个数据库

以下是一般的多租户This

在我看来,这个问题归结为

  • 预期租户数
  • 每个租户的数据库大小。

简单地说,在一些拥有大量记录的租户的情况下,将租户信息添加到索引中将不必要地增加索引大小并且处理租户ID将带来一些开销,而不是避免,那么就去两个DB吧。

<强> 2。多租户DB的设计

第1步。将/// <summary> /// Interface for top-level entities, which belong to a tenant /// </summary> public interface ITenantedEntity { /// <summary> /// ID of a tenant /// </summary> string TenantId { get; set; } } /// <summary> /// Contact information [Tenanted document] /// </summary> public class Contact : ITenantedEntity { public string Id { get; set; } public string TenantId { get; set; } public string Name { get; set; } } 属性添加到要支持多租户的所有持久性文档中。

IDocumentSession

第2步。为Raven的Ayende's thoughtsIAsyncDocumentSession/// <summary> /// Facade for the Raven's IAsyncDocumentSession interface to take care of multi-tenanted entities /// </summary> public class RavenTenantedSession : IAsyncDocumentSession { private readonly IAsyncDocumentSession _dbSession; private readonly string _currentTenantId; public IAsyncAdvancedSessionOperations Advanced => _dbSession.Advanced; public RavenTenantedSession(IAsyncDocumentSession dbSession, ICurrentTenantIdResolver tenantResolver) { _dbSession = dbSession; _currentTenantId = tenantResolver.GetCurrentTenantId(); } public void Delete<T>(T entity) { if (entity is ITenantedEntity tenantedEntity && tenantedEntity.TenantId != _currentTenantId) throw new ArgumentException("Attempt to delete a record for another tenant"); _dbSession.Delete(entity); } public void Delete(string id) { throw new NotImplementedException("Deleting by ID hasn't been implemented"); } #region SaveChanges & StoreAsync--------------------------------------- public Task SaveChangesAsync(CancellationToken token = new CancellationToken()) => _dbSession.SaveChangesAsync(token); public Task StoreAsync(object entity, CancellationToken token = new CancellationToken()) { SetTenantIdOnEntity(entity); return _dbSession.StoreAsync(entity, token); } public Task StoreAsync(object entity, string changeVector, string id, CancellationToken token = new CancellationToken()) { SetTenantIdOnEntity(entity); return _dbSession.StoreAsync(entity, changeVector, id, token); } public Task StoreAsync(object entity, string id, CancellationToken token = new CancellationToken()) { SetTenantIdOnEntity(entity); return _dbSession.StoreAsync(entity, id, token); } private void SetTenantIdOnEntity(object entity) { var tenantedEntity = entity as ITenantedEntity; if (tenantedEntity != null) tenantedEntity.TenantId = _currentTenantId; } #endregion SaveChanges & StoreAsync------------------------------------ public IAsyncLoaderWithInclude<object> Include(string path) { throw new NotImplementedException(); } public IAsyncLoaderWithInclude<T> Include<T>(Expression<Func<T, string>> path) { throw new NotImplementedException(); } public IAsyncLoaderWithInclude<T> Include<T, TInclude>(Expression<Func<T, string>> path) { throw new NotImplementedException(); } public IAsyncLoaderWithInclude<T> Include<T>(Expression<Func<T, IEnumerable<string>>> path) { throw new NotImplementedException(); } public IAsyncLoaderWithInclude<T> Include<T, TInclude>(Expression<Func<T, IEnumerable<string>>> path) { throw new NotImplementedException(); } #region LoadAsync ----------------------------------------------------- public async Task<T> LoadAsync<T>(string id, CancellationToken token = new CancellationToken()) { T entity = await _dbSession.LoadAsync<T>(id, token); if (entity == null || entity is ITenantedEntity tenantedEntity && tenantedEntity.TenantId == _currentTenantId) return entity; throw new ArgumentException("Incorrect ID"); } public async Task<Dictionary<string, T>> LoadAsync<T>(IEnumerable<string> ids, CancellationToken token = new CancellationToken()) { Dictionary<string, T> entities = await _dbSession.LoadAsync<T>(ids, token); if (typeof(T).GetInterfaces().Contains(typeof(ITenantedEntity))) return entities.Where(e => (e.Value as ITenantedEntity)?.TenantId == _currentTenantId).ToDictionary(i => i.Key, i => i.Value); return null; } #endregion LoadAsync -------------------------------------------------- #region Query --------------------------------------------------------- public IRavenQueryable<T> Query<T>(string indexName = null, string collectionName = null, bool isMapReduce = false) { var query = _dbSession.Query<T>(indexName, collectionName, isMapReduce); if (typeof(T).GetInterfaces().Contains(typeof(ITenantedEntity))) return query.Where(r => (r as ITenantedEntity).TenantId == _currentTenantId); return query; } public IRavenQueryable<T> Query<T, TIndexCreator>() where TIndexCreator : AbstractIndexCreationTask, new() { var query = _dbSession.Query<T, TIndexCreator>(); var lastArgType = typeof(TIndexCreator).BaseType?.GenericTypeArguments?.LastOrDefault(); if (lastArgType != null && lastArgType.GetInterfaces().Contains(typeof(ITenantedEntity))) return query.Where(r => (r as ITenantedEntity).TenantId == _currentTenantId); return query; } #endregion Query ------------------------------------------------------ public void Dispose() => _dbSession.Dispose(); } )实施facade以照顾多租户实体。

以下示例代码:

Include()

如果您还需要{{1}},上面的代码可能需要一些爱。

我的最终解决方案并未像我之前建议的那样使用session用于RavenDb v3.x(请参阅listeners为什么)或my comment用于RavenDb v4(因为它&#39} ;很难修改那里的查询。)

当然,如果您编写events JavaScript函数,则必须手动处理多租户。