在每个请求使用会话时如何让 NHibernate 重试死锁事务?

Posted

技术标签:

【中文标题】在每个请求使用会话时如何让 NHibernate 重试死锁事务?【英文标题】:How to let NHibernate retry deadlocked transactions when using session per request? 【发布时间】:2011-04-29 22:39:19 【问题描述】:

当您使用 Session-Per-Request 模式时,您在需要支持事务失败重试的 NHibernate 的 3 层应用程序中使用什么模式/架构? (因为 ISession 在发生异常后变得无效,即使这是死锁或超时或活锁异常)。

【问题讨论】:

你是如何处理你的问题的? 【参考方案1】:

注意 2 如今,我永远不会将写事务放在 Web 项目中 - 而是使用消息传递 + 队列,并在后台有一个工作人员处理旨在完成事务工作的消息。

但是,我仍然会使用事务进行读取以获得一致的数据;与 MVCC/Snapshot 隔离一起,来自 Web 项目。在这种情况下,您会发现 session-per-request-per-transaction 非常好。

注1这篇文章的想法已经放在Castle Transactions framework和我的新NHibernate Facility。

好的,这是一般的想法。假设您要为客户创建一个未完成的订单。您有某种 GUI,例如一个浏览器/MVC 应用程序,它使用相关信息创建一个新的数据结构(或者您从网络获取此数据结构):

[Serializable]
class CreateOrder /*: IMessage*/

    // immutable
    private readonly string _CustomerName;
    private readonly decimal _Total;
    private readonly Guid _CustomerId;

    public CreateOrder(string customerName, decimal total, Guid customerId)
    
        _CustomerName = customerName;
        _Total = total;
        _CustomerId = customerId;
    

    // put ProtoBuf attribute
    public string CustomerName
    
        get  return _CustomerName; 
    

    // put ProtoBuf attribute
    public decimal Total
    
        get  return _Total; 
    

    // put ProtoBuf attribute
    public Guid CustomerId
    
        get  return _CustomerId; 
    

你需要一些东西来处理它。这可能是某种服务总线中的命令处理程序。 “命令处理程序”一词是其中之一,您不妨将其称为“服务”或“域服务”或“消息处理程序”。如果你在做函数式编程,那将是你的消息框实现,或者如果你在做 Erlang 或 Akka,那将是一个 Actor。

class CreateOrderHandler : IHandle<CreateOrder>

    public void Handle(CreateOrder command)
    
        With.Policy(IoC.Resolve<ISession>, s => s.BeginTransaction(), s =>
        
            var potentialCustomer = s.Get<PotentialCustomer>(command.CustomerId);
            potentialCustomer.CreateOrder(command.Total);
            return potentialCustomer;
        , RetryPolicies.ExponentialBackOff.RetryOnLivelockAndDeadlock(3));
    


interface IHandle<T> /* where T : IMessage */

    void Handle(T command);

上面显示了您可能为这个给定的问题域(应用程序状态/事务处理)选择的 API 用法。

With 的实现:

static class With

    internal static void Policy(Func<ISession> getSession,
                                       Func<ISession, ITransaction> getTransaction,
                                       Func<ISession, EntityBase /* abstract 'entity' base class */> executeAction,
                                       IRetryPolicy policy)
    
        //http://fabiomaulo.blogspot.com/2009/06/improving-ado-exception-management-in.html

        while (true)
        
            using (var session = getSession())
            using (var t = getTransaction(session))
            
                var entity = executeAction(session);
                try
                
                    // we might not always want to update; have another level of indirection if you wish
                    session.Update(entity);
                    t.Commit();
                    break; // we're done, stop looping
                
                catch (ADOException e)
                
                    // need to clear 2nd level cache, or we'll get 'entity associated with another ISession'-exception

                    // but the session is now broken in all other regards will will throw exceptions
                    // if you prod it in any other way
                    session.Evict(entity);

                    if (!t.WasRolledBack) t.Rollback(); // will back our transaction

                    // this would need to be through another level of indirection if you support more databases
                    var dbException = ADOExceptionHelper.ExtractDbException(e) as SqlException;

                    if (policy.PerformRetry(dbException)) continue;
                    throw; // otherwise, we stop by throwing the exception back up the layers
                
            
        
    

如您所见,我们需要一个新的工作单元;每次出现问题时的 ISession。这就是为什么循环位于 Using 语句/块之外的原因。拥有函数相当于拥有工厂实例,只是我们直接在对象实例上调用,而不是在其上调用方法。它提供了一个更好的调用者 API 恕我直言。

我们希望对如何执行重试进行相当流畅的处理,因此我们有一个可以由不同的处理程序实现的接口,称为 IRetryHandler。应该可以将这些链接到您想要强制执行控制流的每个方面(是的,它非常接近 AOP)。与 AOP 的工作方式类似,返回值用于控制控制流,但只能以真/假的方式进行,这是我们的要求。

interface IRetryPolicy

    bool PerformRetry(SqlException ex);

AggregateRoot、PotentialCustomer 是一个具有生命周期的实体。这就是您将使用 *.hbm.xml 文件/FluentNHibernate 映射的内容。

它有一个与发送命令1:1对应的方法。这使得命令处理程序完全易于阅读。

此外,使用带有鸭子类型的动态语言,它允许您将命令的类型名称映射到方法,类似于 Ruby/Smalltalk 的做法。

如果您在进行事件溯源,则事务处理将是相似的,只是事务不会与 NHibernate 的此类接口。推论是您将保存通过调用 CreateOrder(decimal) 创建的事件,并为您的实体提供从存储中重新读取已保存事件的机制。

最后要注意的一点是,我覆盖了我创建的三个方法。这是 NHibernate 方面的要求,因为它需要一种知道一个实体何时与另一个实体相等的方法,它们是否应该在集合/包中。更多关于我的实现here。无论如何,这是示例代码,我现在不关心我的客户,所以我没有实现它们:

sealed class PotentialCustomer : EntityBase

    public void CreateOrder(decimal total)
    
        // validate total
        // run business rules

        // create event, save into event sourced queue as transient event
        // update private state
    

    public override bool IsTransient()  throw new NotImplementedException(); 
    protected override int GetTransientHashCode()  throw new NotImplementedException(); 
    protected override int GetNonTransientHashCode()  throw new NotImplementedException(); 

我们需要一种方法来创建重试策略。当然,我们可以通过多种方式做到这一点。在这里,我将一个流畅的接口与一个与静态方法的类型相同的对象的实例相结合。我显式地实现了接口,因此在流畅的接口中没有其他方法可见。此接口仅使用下面我的“示例”实现。

internal class RetryPolicies : INonConfiguredPolicy

    private readonly IRetryPolicy _Policy;

    private RetryPolicies(IRetryPolicy policy)
    
        if (policy == null) throw new ArgumentNullException("policy");
        _Policy = policy;
    

    public static readonly INonConfiguredPolicy ExponentialBackOff =
        new RetryPolicies(new ExponentialBackOffPolicy(TimeSpan.FromMilliseconds(200)));

    IRetryPolicy INonConfiguredPolicy.RetryOnLivelockAndDeadlock(int retries)
    
        return new ChainingPolicy(new[] new SqlServerRetryPolicy(retries), _Policy);
    

我们需要一个接口来部分完整地调用 fluent 接口。这给了我们类型安全性。因此,在完成策略配置之前,我们需要远离我们的静态类型的两个取消引用运算符(即“句号”--(.))。

internal interface INonConfiguredPolicy

    IRetryPolicy RetryOnLivelockAndDeadlock(int retries);

可以解决链接策略。它的实现检查所有子节点是否继续返回,并在检查时执行它们中的逻辑。

internal class ChainingPolicy : IRetryPolicy

    private readonly IEnumerable<IRetryPolicy> _Policies;

    public ChainingPolicy(IEnumerable<IRetryPolicy> policies)
    
        if (policies == null) throw new ArgumentNullException("policies");
        _Policies = policies;
    

    public bool PerformRetry(SqlException ex)
    
        return _Policies.Aggregate(true, (val, policy) => val && policy.PerformRetry(ex));
    

这个策略让当前线程休眠一段时间;有时数据库过载,并且让多个读取器/写入器不断尝试读取将是对数据库的事实上的 DOS 攻击(请参阅几个月前 Facebook 崩溃时发生的事情,因为他们的缓存服务器都同时查询他们的数据库时间)。

internal class ExponentialBackOffPolicy : IRetryPolicy

    private readonly TimeSpan _MaxWait;
    private TimeSpan _CurrentWait = TimeSpan.Zero; // initially, don't wait

    public ExponentialBackOffPolicy(TimeSpan maxWait)
    
        _MaxWait = maxWait;
    

    public bool PerformRetry(SqlException ex)
    
        Thread.Sleep(_CurrentWait);
        _CurrentWait = _CurrentWait == TimeSpan.Zero ? TimeSpan.FromMilliseconds(20) : _CurrentWait + _CurrentWait;
        return _CurrentWait <= _MaxWait;
    

同样,在任何基于 SQL 的优秀系统中,我们都需要处理死锁。我们无法真正深入地计划这些,尤其是在使用 NHibernate 时,除了保持严格的事务策略——没有隐式事务;并小心Open-Session-In-View。如果您要获取大量数据,还需要记住笛卡尔积问题/N+1 选择问题。取而代之的是,您可能有 Multi-Query,或 HQL 的 'fetch' 关键字。

internal class SqlServerRetryPolicy : IRetryPolicy

    private int _Tries;
    private readonly int _CutOffPoint;

    public SqlServerRetryPolicy(int cutOffPoint)
    
        if (cutOffPoint < 1) throw new ArgumentOutOfRangeException("cutOffPoint");
        _CutOffPoint = cutOffPoint;
    

    public bool PerformRetry(SqlException ex)
    
        if (ex == null) throw new ArgumentNullException("ex");
        // checks the ErrorCode property on the SqlException
        return SqlServerExceptions.IsThisADeadlock(ex) && ++_Tries < _CutOffPoint;
    

一个帮助类使代码更好读。

internal static class SqlServerExceptions

    public static bool IsThisADeadlock(SqlException realException)
    
        return realException.ErrorCode == 1205;
    

不要忘记在 IConnectionFactory 中处理网络故障(可能通过实现 IConnection 进行委派)。


PS:如果您不只是在阅读,那么每个请求的会话是一种破坏模式。特别是如果您正在使用与您正在编写的相同 ISession 进行阅读,并且您没有对读取进行排序,因此它们总是在写入之前。

【讨论】:

以上是关于在每个请求使用会话时如何让 NHibernate 重试死锁事务?的主要内容,如果未能解决你的问题,请参考以下文章

Nhibernate in asp,net ISession 帮助

使用 IIS 7 的 NHibernate 会话

带有 Ninject 的 NHibernate 会话管理

如何阻止NHibernate阅读儿童收藏?

事务如何在 nhibernate 中工作?

nHibernate 不加载第三级属性(不可刷新缓存)