14

Session-Per-Requestパターンを使用している場合、トランザクション失敗の再試行をサポートする必要があるNHibernateを使用する3層アプリケーションでどのパターン/アーキテクチャを使用しますか?(これがデッドロック、タイムアウト、またはライブロックの例外であっても、ISessionは例外の後に無効になるため)。

4

1 に答える 1

37

注2最近は、書き込みトランザクションをWebプロジェクト内に配置することはありませんが、代わりにメッセージング+キューを使用し、トランザクション作業を実行することを目的としたメッセージを処理するワーカーをバックグラウンドで使用します。

ただし、一貫性のあるデータを取得するために、読み取りには引き続きトランザクションを使用します。WebプロジェクトからのMVCC/スナップショットアイソレーションと一緒に。その場合、トランザクションごとのリクエストごとのセッションは完全に問題ないことがわかります。

注1この投稿のアイデアは、 CastleTransactionsフレームワークと私の新しいNHibernateファシリティに配置されています。

OK、これが一般的な考え方です。顧客の未確定注文を作成するとします。関連情報を使用して新しいデータ構造を作成する(またはネットワークからこのデータ構造を取得する)、ある種の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; }
    }
}

あなたはそれを処理するために何かが必要です。おそらく、これはある種のサービスバスのコマンドハンドラーでしょう。「コマンドハンドラー」という言葉は多くの単語の1つであり、「サービス」または「ドメインサービス」または「メッセージハンドラー」と呼んでもかまいません。関数型プログラミングを行っている場合はメッセージボックスの実装になり、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の使用法を示しています。

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ステートメント/ブロックの外側にあります。関数を持つことは、オブジェクトインスタンスに対してメソッドを呼び出すのではなく、オブジェクトインスタンスに対して直接呼び出すことを除いて、ファクトリインスタンスを持つことと同じです。それはより良い発信者になります-APIimho。

再試行の実行方法をかなりスムーズに処理する必要があるため、IRetryHandlerと呼ばれるさまざまなハンドラーで実装できるインターフェイスがあります。制御フローを適用したいすべての側面(はい、AOPに非常に近い)に対してこれらを連鎖させることが可能であるはずです。AOPの動作と同様に、戻り値は制御フローを制御するために使用されますが、これは私たちの要件である真/偽の方法でのみ使用されます。

interface IRetryPolicy
{
    bool PerformRetry(SqlException ex);
}

AggregateRoot、PotentialCustomerは、存続期間を持つエンティティです。これは、*。hbm.xmlファイル/FluentNHibernateを使用してマッピングするものです。

送信されたコマンドと1:1で対応するメソッドがあります。これにより、コマンドハンドラーが完全に読みやすくなります。

さらに、ダックタイピングを使用する動的言語では、Ruby / Smalltalkと同様に、コマンドのタイプ名をメソッドにマップできます。

イベントソーシングを行っている場合、トランザクションがNHibernateのようなものとインターフェイスしないことを除いて、トランザクション処理は同様になります。当然の結果として、CreateOrder(decimal)を呼び出して作成されたイベントを保存し、保存されたイベントをストアから再読み取りするためのメカニズムをエンティティに提供します。

最後に注意すべき点は、作成した3つのメソッドをオーバーライドしていることです。これは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});
    }
}

流暢なインターフェースへの部分的に完全な呼び出しのためのインターフェースが必要です。これにより、型安全性が得られます。したがって、ポリシーの構成を完了する前に、静的型から離れた2つの間接参照演算子(つまり、「終止符」-(。))が必要です。

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を使用して読み取りを行っていて、書き込みの前に常にすべてになるように読み取りを順序付けていない場合は特にそうです。

于 2011-02-10T12:30:55.220 に答える