2

データベースに格納されているさまざまなテーブルからレコードを読み取る .Net 3.5 を使用してマルチスレッド アプリケーションを開発しています。読み取りは非常に頻繁に行われるため、遅延読み込みキャッシュの実装が必要です。すべてのテーブルは C# クラスにマップされ、キャッシュでキーとして使用できる文字列列を持ちます。さらに、キャッシュされたすべてのレコードを定期的に更新する必要があります。スレッドセーフな環境を確保するために、すべての読み取りでロックを使用してキャッシュを実装することもできましたが、考えられるすべてのキーのリストを取得するのは簡単であるという事実に依存する別のソリューションを考えました。

これが私が書いた最初のクラスで、二重チェック ロック パターンで遅延読み込みされるすべてのキーのリストを格納します。また、最後に要求された更新のタイムスタンプを静的変数に格納するメソッドもあります。

public class Globals
{
    private static object _KeysLock = new object();
    public static volatile List<string> Keys;
    public static void LoadKeys()
    {
        if (Keys == null)
        {
            lock (_KeysLock)
            {
                if (Keys == null)
                {
                    List<string> keys = new List<string>();
                    // Filling all possible keys from DB
                    // ...
                    Keys = keys;
                }
            }
        }
    }

    private static long refreshTimeStamp = DateTime.Now.ToBinary();
    public static DateTime RefreshTimeStamp
    {
        get { return DateTime.FromBinary(Interlocked.Read(ref refreshTimeStamp)); }
    }
    public static void NeedRefresh()
    {
        Interlocked.Exchange(ref refreshTimeStamp, DateTime.Now.ToBinary());
    }
}

次にCacheItem<T>、キーによってフィルター処理された指定されたテーブル T のキャッシュの単一アイテムの実装であるクラスを作成しました。Loadレコード リストの遅延読み込み用のメソッドとLoadingTimeStamp、最後のレコード読み込みのタイムスタンプを格納するプロパティがあります。レコードの静的リストは、ローカルに入力された新しいリストで上書きされ、その後 も上書きされることに注意してくださいLoadingTimeStamp

public class CacheItem<T>
{
    private List<T> _records;
    public List<T> Records
    {
        get { return _records; }
    }

    private long loadingTimestampTick;
    public DateTime LoadingTimestamp
    {
        get { return DateTime.FromBinary(Interlocked.Read(ref loadingTimestampTick)); }
        set { Interlocked.Exchange(ref loadingTimestampTick, value.ToBinary()); }
    }

    public void Load(string key)
    {
        List<T> records = new List<T>();
        // Filling records from DB filtered on key
        // ...
        _records = records;
        LoadingTimestamp = DateTime.Now;
    }
}

最後Cache<T>に、テーブル T のキャッシュを静的 Dictionary として格納するクラスを次に示します。ご覧のとおり、このGetメソッドは、まだ実行されていない場合は最初にキャッシュ内の可能なすべてのキーをロードし、次に更新のためにタイムスタンプをチェックします (どちらも二重チェック ロック パターンで実行されます)。呼び出しによって返されたインスタンス内のレコードのリストはGet、ロック内でリフレッシュを実行している別のスレッドが存在する場合でも、スレッドによって安全に読み取ることができます。これは、リフレッシュ スレッドがリスト自体を変更するのではなく、新しいリストを作成するためです。

public class Cache<T>
{
    private static object _CacheSynch = new object();
    private static Dictionary<string, CacheItem<T>> _Cache = new Dictionary<string, CacheItem<T>>();
    private static volatile bool _KeysLoaded = false;

    public static CacheItem<T> Get(string key)
    {
        bool checkRefresh = true;
        CacheItem<T> item = null;
        if (!_KeysLoaded)
        {
            lock (_CacheSynch)
            {
                if (!_KeysLoaded)
                {
                    Globals.LoadKeys(); // Checks the lazy loading of the common key list
                    foreach (var k in Globals.Keys)
                    {
                        item = new CacheItem<T>();
                        if (k == key)
                        {
                            // As long as the lock is acquired let's load records for the requested key
                            item.Load(key);
                            // then the refresh is no more needed by the current thread
                            checkRefresh = false;
                        }
                        _Cache.Add(k, item);
                    }
                    _KeysLoaded = true;
                }
            }
        }
        // here the key is certainly contained in the cache
        item = _Cache[key];
        if (checkRefresh)
        {
            // let's check the timestamps to know if refresh is needed
            DateTime rts = Globals.RefreshTimeStamp;
            if (item.LoadingTimestamp < rts)
            {
                lock (_CacheSynch)
                {
                    if (item.LoadingTimestamp < rts)
                    {
                        // refresh is needed
                        item.Load(key);
                    }
                }
            }
        }
        return item;
    }
}

が定期的にGlobals.NeedRefresh()呼び出され、レコードが確実に更新されます。このソリューションは、キャッシュが可能なすべてのキーで事前に埋められているため、キャッシュの読み取りごとにロックを回避できます。これは、可能なすべてのキーの数 (約 20) に等しい数のインスタンスがメモリに存在することを意味します。 ) 要求されたタイプ T ごとに (すべての T タイプは約 100 です)、要求されたキーについてのみ、レコード リストは空ではありません。この解決策にスレッドセーフの問題や問題がある場合はお知らせください。どうもうありがとう。

4

1 に答える 1

0

とすれば:

  • すべてのキーを一度ロードし、決して変更しない
  • 各ディクショナリを一度作成し、変更することはありません
  • CacheItem.Load は、privateList<T>フィールドのみを新しい完全に初期化されたリストに置き換えるため、スレッド セーフです。

ロックはまったく必要ないため、コードを簡素化できます。

ロックが必要になる可能性があるのは、同時実行の試行を防ぐことだけCacheItem.Loadです。個人的にはデータベースへの同時アクセスを許可するだけですが、それを防ぎたい場合は、ロックを実装するだけで済みますCacheItem.LoadLazy<T>または、.NET 4 からピンチして、前の質問に対する私の回答で提案されているように使用します。

もう 1 つのコメントは、DateTime.Now(a) クロックが夏時間の終わりに戻る期間中、および (b) システム クロックが更新された場合に、リフレッシュ ロジックが使用するので、期待どおりに動作しないことです。

NeedRefreshが呼び出されるたびにインクリメントされる静的な整数値を使用するだけです。

コメントから:

たとえば、2 つのスレッドが ... 共通の Globals.Keys を同時に読み込もうとするとどうなりますか?"

これは、アプリケーションの起動時に 1 回発生する可能性があるという小さなリスクがありますが、それではどうでしょうか。データベースから 20 個のキーが 2 回読み取られることになりますが、パフォーマンスへの影響はほとんどありません。これを本当に防ぎたい場合は、ロックを のようなクラスにカプセル化できますLazy<T>

DateTime.Now の使用に関するコメントは実際には興味深い点ですが、アプリケーションが使用されていないときにこれらのイベントが発生する可能性があると推測できると思います。

それを「仮定」することはできますが、それを保証するものではありません。マシンは、いつでもタイム サーバーと時刻を同期することを決定できます。

NeedRefresh で整数を使用するというアドバイスについて、DateTime で表される各レコード リストの状態と比較する方法がわかりません。

私が見る限り、 を使用するのは、DateTime最新の への呼び出しの前または後にデータがロードされたかどうかを確認するためだけNeedRefreshです。したがって、これを次のように置き換えることができます。

public static class Globals
{
    ...

    public static int Version { get {return _version; } }
    private static int _version;

    public static void NeedRefresh()
    {
        Interlocked.Increment(ref _version);
    }
}

public class CacheItem<T>
{
    public int Version {get; private set; }

    ...

    public void Load(string key)
    {
        Version = Globals.Version;

        List<T> records = new List<T>();
        // Filling records from DB filtered on key
        // ...
        _records = records;
    }
}

次に、キャッシュにアクセスするとき:

item = _Cache[key];
if (item.Version < Globals.Version) item.Load();

** 更新 2 **

コメントへの応答:

... あるスレッドが Dictionary を読み込もうとしているときに、別のスレッドがロック内に項目を追加している場合、整合性に実際のリスクが生じる可能性があります。

既存のコードは、グローバル キーをロードした直後に一度だけすべてのキーをディクショナリに追加し、その後ディクショナリを変更することはありません。したがって、辞書が完全に構築されるまで _Cache プロパティを割り当てない限り、これはスレッドセーフです。

var dictionary = new Dictionary<string, CacheItem<T>>(Global.Keys.Count);
foreach (var k in Globals.Keys)                       
{                           
    dictionary.Add(k, new CacheItem<T>());
}
_Cache = dictionary;
于 2012-09-18T15:45:55.617 に答える