はじめに
この記事では主に、キャッシュの使用経験と日常のプロジェクトで遭遇する問題について説明します。
ディレクトリ
1: 基本的な書き方
2: キャッシュなだれ
1: グローバルロック、インスタンスロック
2: 文字列ロック
3: キャッシュの貫通
4: キャッシュなだれについてもう一度話します
5:まとめ
1:基本的な書き方
デモの便宜上、キャッシュコンテナとしてRuntime.Cacheを使用し、簡単な操作クラスを定義します。以下の通り:
public class CacheHelper { public static object Get(string cacheKey) { return HttpRuntime.Cache[cacheKey]; } public static void Add(string cacheKey, object obj, int cacheMinute) { HttpRuntime.Cache.Insert(cacheKey, obj, null, DateTime.Now.AddMinutes(cacheMinute), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } }
簡単な読み方:
public object GetMemberSigninDays1() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数 CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; }
プロジェクトではこのような書き方が多くあります。このように書くことに問題はありませんが、同時実行の量が増加すると問題が発生します。続きを読む
2 つ: キャッシュ雪崩
キャッシュ雪崩はキャッシュの無効化 (期限切れ) が原因であり、新しいキャッシュはまだ期限切れになっていません。
この中間期間では、すべてのリクエストがデータベースにクエリを実行するため、データベースの CPU とメモリに大きな負荷がかかり、フロントエンド接続の数が十分ではなく、クエリがブロックされます。
この中間時間はそれほど短くはありません。たとえば、SQL クエリに 1 秒、送信と分析に 0.5 秒を加えます。 つまり、1.5 秒以内のすべてのユーザー クエリはデータベースに直接クエリを実行します。
この場合、最も考えられるのはロックとキューイングです。
1: グローバル ロック、インスタンス ロック
public static object obj1 = new object(); public object GetMemberSigninDays2() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; //lock (obj1) //全局锁 //{ // cacheValue = CacheHelper.Get(cacheKey); // if (cacheValue != null) // return cacheValue; // cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数 // CacheHelper.Add(cacheKey, cacheValue, cacheTime); //} lock (this) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数 CacheHelper.Add(cacheKey, cacheValue, cacheTime); } return cacheValue; }
最初のタイプ: ロック (obj1) は満たすことができるグローバル ロックですが、関数ごとに obj を宣言する必要があります。そうでない場合、A 関数と B 関数の両方が obj1 をロックします。 、必然的にそのうちの1つがブロックされます。
2 番目のタイプ: lock (this) は現在のインスタンスをロックし、他のインスタンスに対しては無効です。このロックは効果がありません。シングルトン モードを使用してロックできます。
しかし、現在のインスタンスでは、関数 A が現在のインスタンスをロックし、現在のインスタンスをロックする他の関数も読み取りと書き込みがブロックされます。望ましくない
2: 文字列ロック
オブジェクトのロックは不可能なので、文字列の特性を利用してキャッシュキーを直接ロックできます。見てみましょう
public object GetMemberSigninDays3() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; const string lockKey = cacheKey + "n(*≧▽≦*)n"; //lock (cacheKey) //{ // cacheValue = CacheHelper.Get(cacheKey); // if (cacheValue != null) // return cacheValue; // cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数 // CacheHelper.Add(cacheKey, cacheValue, cacheTime); //} lock (lockKey) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数 CacheHelper.Add(cacheKey, cacheValue, cacheTime); } return cacheValue; }
1 つ目: ロック (cacheName) に問題があります。これは、文字列も共有されており、この文字列を使用する他の操作がブロックされるためです。 詳細については、以前のブログ投稿「マルチスレッドにおける C# 言語ロック システム (1)」を参照してください。
2015-01-04 13:36 更新: 文字列は共通言語ランタイム (CLR) によって永続化されるため、プログラム全体には特定の文字列のインスタンスが 1 つだけ存在することになります。そのため、私は 2 つ目を使用します
2 つ目: ロック (lockKey) で十分です。実際、その目的は、ロックの最小粒度とグローバルな一意性を確保し、現在キャッシュされているクエリ動作のみをロックすることです。
3: キャッシュの浸透
簡単な例: 一般に、私たちはユーザーの検索結果をキャッシュします。データベースがクエリできない場合、データベースはキャッシュされません。ただし、このキーワードを頻繁にチェックすると、毎回データベースを直接チェックすることになります。
このようにキャッシュは意味がありませんが、これはよく指摘されるキャッシュ ヒット率の問題でもあります。
public object GetMemberSigninDays4() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; const string lockKey = cacheKey + "n(*≧▽≦*)n"; lock (lockKey) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = null; //数据库查询不到,为空。 //if (cacheValue2 == null) //{ // return null; //一般为空,不做缓存 //} if (cacheValue == null) { cacheValue = string.Empty; //如果发现为空,我设置个默认值,也缓存起来。 } CacheHelper.Add(cacheKey, cacheValue, cacheTime); } return cacheValue; }
この例では、クエリできない結果もキャッシュします。これにより、クエリが空の場合のキャッシュの侵入を回避できます。
もちろん、第 1 レベルの制御検証を実行するために別のキャッシュ領域を設定することもできます。 通常のキャッシュと区別するため。
四:再谈缓存雪崩
额 不是用加锁排队方式就解决了吗?其实加锁排队只是为了减轻DB压力,并没有提高系统吞吐量。
在高并发下: 缓存重建期间,你是锁着的,1000个请求999个都在阻塞的。 用户体验不好,还浪费资源:阻塞的线程本可以处理后续请求的。
public object GetMemberSigninDays5() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; //缓存标记。 const string cacheSign = cacheKey + "_Sign"; var sign = CacheHelper.Get(cacheSign); //获取缓存值 var cacheValue = CacheHelper.Get(cacheKey); if (sign != null) return cacheValue; //未过期,直接返回。 lock (cacheSign) { sign = CacheHelper.Get(cacheSign); if (sign != null) return cacheValue; CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) => { cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数 CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。 }); } return cacheValue; }
代码中,我们多用个缓存标记key,双检锁校验。它设置为正常时间,过期后通知另外的线程去更新缓存数据。
而实际的缓存由于设置了2倍的时间,仍然可以能用脏数据给前端展现。
这样就能提高不少系统吞吐量了。
五:总结
补充下: 这里说的阻塞其他函数指的是,高并发下锁同一对象。
实际使用中,缓存层封装往往要复杂的多。 关于更新缓存,可以单开一个线程去专门跑这些,图方便就扔线程池吧。
具体使用场景,可根据实际用户量来平衡。