如何在消费者类型应用程序中缓存 DataContext 实例?

kus*_*men 8 c# datacontext entity-framework

我们有一个应用程序使用我们的供应商提供的 SDK 来轻松地与他们集成。此 SDK 连接到 AMQP 端点,并简单地向我们的消费者分发、缓存和转换消息。以前,这种集成是通过 HTTP 使用 XML 作为数据源的,而旧的集成有两种缓存 DataContext 的方法 - 每个 Web 请求和每个托管线程 ID。(1)

但是,现在我们不通过 HTTP 集成,而是通过 AMQP 集成,这对我们来说是透明的,因为 SDK 正在执行所有连接逻辑,我们只剩下定义我们的使用者,因此没有选项可以“根据 Web 请求”缓存 DataContext,因此只剩下每个托管线程 id。我实现了责任链模式,所以当我们收到更新时,它被放在一个处理程序管道中,该管道使用 DataContext 根据新的更新更新数据库。这是管道的调用方法的样子:

public Task Invoke(TInput entity)
{
    object currentInputArgument = entity;

    for (var i = 0; i < _pipeline.Count; ++i)
    {
        var action = _pipeline[i];
        if (action.Method.ReturnType.IsSubclassOf(typeof(Task)))
        {
            if (action.Method.ReturnType.IsConstructedGenericType)
            {
                dynamic tmp = action.DynamicInvoke(currentInputArgument);
                currentInputArgument = tmp.GetAwaiter().GetResult();
            }
            else
            {
                (action.DynamicInvoke(currentInputArgument) as Task).GetAwaiter().GetResult();
            }
        }
        else
        {
            currentInputArgument = action.DynamicInvoke(currentInputArgument);
        }
    }

    return Task.CompletedTask;
}
Run Code Online (Sandbox Code Playgroud)

问题是(至少我认为是这样)这个责任链是返回/启动新任务的方法链,所以当实体 A 的更新到来时,它由托管线程 id = 1 处理,让我们说,然后只在某个时间之后再次相同的实体 A 到达仅由托管线程 id = 2 处理,例如。这将导致:

System.InvalidOperationException: '一个实体对象不能被 IEntityChangeTracker 的多个实例引用。'

因为来自托管线程 id = 1 的 DataContext 已经跟踪实体 A.(至少我认为是这样)

我的问题是如何在我的情况下缓存 DataContext?各位有没有遇到同样的问题?我阅读了这个这个答案,并且根据我的理解,使用一个静态 DataContext 也不是一种选择。(2)

  1. 免责声明:我应该说我们继承了该应用程序,我无法回答为什么它是这样实现的。
  2. 免责声明 2:我对 EF 几乎没有经验。

社区提问:

  1. 我们使用的是什么版本的 EF?5.0
  2. 为什么实体的寿命比上下文长?- 他们没有,但也许你在问为什么实体需要比上下文更长寿。我使用使用缓存 DataContext 的存储库从数据库中获取实体,以将它们存储在我用作缓存的内存中集合中。

这就是“提取”实体的方式,DatabaseDataContext我正在谈论的缓存 DataContext在哪里(BLOB 里面有整个数据库集)

protected IQueryable<T> Get<TProperty>(params Expression<Func<T, TProperty>>[] includes)
{
    var query = DatabaseDataContext.Set<T>().AsQueryable();

    if (includes != null && includes.Length > 0)
    {
        foreach (var item in includes)
        {
            query = query.Include(item);
        }
    }

    return query;
}
Run Code Online (Sandbox Code Playgroud)

然后,每当我的消费者应用程序收到 AMQP 消息时,我的责任链模式就会开始检查我是否已经处理了该消息及其数据。所以我有这样的方法:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
            where TEntity : ISportEvent
{
    ... some unimportant business logic

    //save the sport
    if (sport.SportID > 0) // <-- this here basically checks if so called 
                           // sport is found in cache or not
                           // if its found then we update the entity in the db
                           // and update the cache after that
    {
        _sportRepository.Update(sport); /* 
                                         * because message update for the same sport can come
                                         * and since DataContext is cached by threadId like I said
                                         * and Update can be executed from different threads
                                         * this is where aforementioned exception is thrown
                                        */

    }
    else                   // if not simply insert the entity in the db and the caches
    {
        _sportRepository.Insert(sport);
    }

    _sportRepository.SaveDbChanges();

    ... updating caches logic
}
Run Code Online (Sandbox Code Playgroud)

我认为AsNoTracking()每次“更新”或“插入”实体时使用方法从数据库中获取实体或分离实体都会解决这个问题,但事实并非如此。

小智 0

由于这是关于一些现有的业务应用程序,因此我将重点关注有助于解决问题的想法,而不是讲授最佳实践或提出架构更改。

我知道这是显而易见的,但有时改写错误消息可以帮助我们更好地理解发生了什么,所以请耐心等待。

该错误消息表明实体正在被多个数据上下文使用,这表明存在多个 dbcontext 实例,并且实体被多个此类实例引用。

然后问题指出每个线程都有一个数据上下文,该上下文曾经是每个 http 请求的,并且实体被缓存。

因此,似乎可以安全地假设实体在缓存未命中时从数据库上下文中读取,并在命中时从缓存中返回。尝试使用第二个数据库上下文实例更新从一个数据库上下文实例加载的实体会导致失败。我们可以得出结论,在这种情况下,两个操作中使用了完全相同的实体实例,并且没有序列化/反序列化来访问缓存。

DbContext 实例本身是通过其内部更改跟踪器机制的实体缓存,并且此错误是保护其完整性的保障措施。由于这个想法是让一个长时间运行的进程通过多个数据库上下文(每个线程一个)加上一个共享实体缓存来处理同时请求,因此在性能和内存方面都将非常有益(更改跟踪可能会及时增加内存消耗) )尝试将数据库上下文生命周期更改为每条消息,或者在处理每条消息后清空其更改跟踪器。

当然,为了处理实体更新,它们需要在从缓存中检索实体之后并在对其应用任何更改之前立即附加到当前数据库上下文。