Подтвердить что ты не робот

Как позволить NHibernate повторить тупиковые транзакции при использовании сеанса за запрос?

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

4b9b3361

Ответ 1

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

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

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

Мы хотим довольно плавно обрабатывать то, как мы выполняем повторные попытки, поэтому у нас есть интерфейс, который может быть реализован различными обработчиками, называемыми IRetryHandler. Должно быть возможно связать их для каждого аспекта (да, он очень близок к 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-атакой в ​​базе данных (см., что произошло несколько месяцев назад, когда фреймпад разбился, потому что их кеш-серверы все запросили свои базы данных в то же самое время время).

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, с которым вы пишете, и вы не заказываете такие чтения, чтобы они были все, всегда, перед записью.