Как разрешить NHibernate повторять заблокированные транзакции при использовании сеанса для каждого запроса? - PullRequest
12 голосов
/ 25 октября 2010

Какой шаблон / архитектуру вы используете в трехуровневом приложении, использующем NHibernate, который должен поддерживать повторные попытки при сбое транзакции, когда вы используете шаблон Session-Per-Request?(поскольку ISession становится недействительным после исключения, даже если это исключение взаимоблокировки, таймаута или livelock).

1 Ответ

34 голосов
/ 10 февраля 2011

Примечание 2 В настоящее время я бы никогда не помещал транзакции записи внутри веб-проекта - вместо этого использовал бы сообщения + очереди и имел бы работника в фоновом режиме, обрабатывающего сообщения с целью выполнения транзакционной работы.

Однако я бы все еще использовал транзакции для чтения, чтобы получить непротиворечивые данные; вместе с изоляцией MVCC / Snapshot, от веб-проектов. В этом случае вы обнаружите, что сессия на запрос на транзакцию в порядке.

Примечание 1 Идеи этого поста были помещены в структуру Операции с замком и мою новую NHibernate Facility .

Хорошо, вот общая идея. Предположим, вы хотите создать не завершенный заказ для клиента. У вас есть какой-то графический интерфейс, например приложение для браузера / 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, это был бы актер.

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, которое вы можете выбрать для данной проблемной области (состояние приложения / обработка транзакций).

Реализация С:

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 каждый раз, когда что-то идет не так Вот почему цикл находится за пределами использования операторов / блоков. Наличие функций эквивалентно наличию фабричных экземпляров, за исключением того, что мы вызываем непосредственно экземпляр объекта, а не вызываем метод для него. Это делает API вызывающего абонента приятнее.

Нам нужна довольно плавная обработка того, как мы выполняем повторные попытки, поэтому у нас есть интерфейс, который может быть реализован различными обработчиками, называемый IRetryHandler. Должна быть возможность объединить их в цепочку для каждого аспекта (да, он очень близок к AOP), который вы хотите применить к потоку управления. Подобно тому, как работает AOP, возвращаемое значение используется для управления потоком управления, но только в режиме истина / ложь, что является нашим требованием.

interface IRetryPolicy
{
    bool PerformRetry(SqlException ex);
}

AggregateRoot, PotentialCustomer - это объект с целым сроком службы. Это то, что вы будете отображать с помощью файлов * .hbm.xml / FluentNHibernate.

У него есть метод, который соответствует 1: 1 отправленной команде. Это делает обработчики команд совершенно очевидными для чтения.

Кроме того, с динамическим языком с утиной типизацией это позволит вам сопоставлять имена типов команд с методами, аналогично тому, как это делает Ruby / Smalltalk.

Если бы вы выполняли поиск событий, обработка транзакций была бы аналогичной, за исключением того, что транзакция не взаимодействовала бы с таковой в NHibernate. Следствием этого является то, что вы должны сохранить события, созданные с помощью вызова CreateOrder (десятичное число), и предоставить своей сущности механизм для повторного чтения сохраненных событий из хранилища.

И последнее замечание: я переопределяю три метода, которые я создал. Это требование со стороны NHibernate, так как ему нужен способ узнать, когда сущность равна другой, если они находятся в наборах / сумках. Подробнее о моей реализации здесь . В любом случае, это пример кода, и мне сейчас наплевать на моего клиента, поэтому я не реализую его:

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});
    }
}

Нам нужен интерфейс для частично полного вызова свободного интерфейса.Это дает нам безопасность типов.Следовательно, нам нужно два оператора разыменования (т. Е. «Полная остановка» - (.)) Вдали от нашего статического типа, прежде чем завершить настройку политики.

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: Session-per-request - это неправильный шаблон, если вы не только выполняете чтение.Особенно, если вы выполняете чтение с той же ISession, с которой пишете, и не упорядочиваете чтения так, чтобы они всегда были всегда перед записью.

...