รูปแบบการล็อกสำหรับการใช้. NET MemoryCache อย่างเหมาะสม


116

ฉันถือว่ารหัสนี้มีปัญหาการเกิดพร้อมกัน:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

สาเหตุของปัญหาการเกิดพร้อมกันคือหลายเธรดสามารถรับคีย์ว่างจากนั้นพยายามแทรกข้อมูลลงในแคช

วิธีใดเป็นวิธีที่สั้นและสะอาดที่สุดในการพิสูจน์รหัสพร้อมกันนี้ ฉันชอบทำตามรูปแบบที่ดีในโค้ดที่เกี่ยวข้องกับแคชของฉัน ลิงก์ไปยังบทความออนไลน์จะช่วยได้มาก

UPDATE:

ฉันคิดรหัสนี้ตามคำตอบของ @Scott Chamberlain ใครสามารถพบปัญหาเกี่ยวกับประสิทธิภาพหรือการทำงานพร้อมกันนี้ได้หรือไม่? หากได้ผลก็จะช่วยประหยัดโค้ดและข้อผิดพลาดจำนวนมาก

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}

3
ทำไมคุณไม่ใช้ReaderWriterLockSlim?
DarthVader

2
ฉันเห็นด้วยกับ DarthVader ... ฉันคิดว่าคุณผอมReaderWriterLockSlim... แต่ฉันก็จะใช้เทคนิคนี้เพื่อหลีกเลี่ยงtry-finallyคำพูดเช่นกัน
poy

1
สำหรับเวอร์ชันที่อัปเดตของคุณฉันจะไม่ล็อค cacheLock เดียวอีกต่อไปฉันจะล็อคต่อคีย์แทน ซึ่งสามารถทำได้อย่างง่ายดายโดยDictionary<string, object>ที่คีย์นั้นเป็นคีย์เดียวกับที่คุณใช้ในของคุณMemoryCacheและอ็อบเจ็กต์ในพจนานุกรมเป็นเพียงพื้นฐานที่Objectคุณล็อคไว้ อย่างไรก็ตามตามที่กล่าวไว้ฉันขอแนะนำให้คุณอ่านคำตอบของจอนฮันนา หากไม่มีการกำหนดโปรไฟล์ที่เหมาะสมคุณอาจทำให้โปรแกรมของคุณช้าลงด้วยการล็อกมากกว่าการปล่อยให้SomeHeavyAndExpensiveCalculation()รันสองอินสแตนซ์และมีผลลัพธ์เดียว
Scott Chamberlain

1
สำหรับฉันแล้วดูเหมือนว่าการสร้าง CacheItemPolicy หลังจากได้รับค่าแคชราคาแพงจะแม่นยำกว่า ในกรณีที่เลวร้ายที่สุดเช่นการสร้างรายงานสรุปที่ใช้เวลา 21 นาทีในการส่งคืน "สตริงราคาแพง" (อาจมีชื่อไฟล์ของรายงาน PDF) จะ "หมดอายุ" ก่อนที่จะส่งคืน
Wonderbird

1
@ วันเดอร์เบิร์ดจุดดีฉันอัปเดตคำตอบเพื่อทำเช่นนั้น
Scott Chamberlain

คำตอบ:


91

นี่เป็นการทำโค้ดซ้ำครั้งที่ 2 ของฉัน เนื่องจากMemoryCacheเธรดปลอดภัยคุณไม่จำเป็นต้องล็อกการอ่านครั้งแรกคุณสามารถอ่านได้และหากแคชส่งคืนค่าว่างจากนั้นให้ทำการตรวจสอบการล็อกเพื่อดูว่าคุณต้องการสร้างสตริงหรือไม่ ทำให้โค้ดง่ายขึ้นมาก

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

แก้ไข : รหัสด้านล่างไม่จำเป็น แต่ฉันต้องการปล่อยไว้เพื่อแสดงวิธีการดั้งเดิม อาจเป็นประโยชน์สำหรับผู้เยี่ยมชมในอนาคตที่ใช้คอลเล็กชันอื่นที่มีการอ่านเธรดที่ปลอดภัย แต่ไม่ใช่เธรดการเขียนที่ปลอดภัย (เกือบทั้งหมดของคลาสภายใต้System.Collectionsเนมสเปซเป็นเช่นนั้น)

นี่คือวิธีที่ฉันจะใช้ReaderWriterLockSlimเพื่อป้องกันการเข้าถึง คุณต้องทำการ " Double Checked Locking " เพื่อดูว่ามีใครสร้างรายการที่แคชไว้หรือไม่ในขณะที่เรารอที่จะทำการล็อก

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}

1
@DarthVader โค้ดด้านบนจะไม่ทำงานในลักษณะใด นอกจากนี้นี่ไม่ใช่ "การล็อกแบบตรวจสอบซ้ำ" อย่างเคร่งครัดฉันแค่ทำตามรูปแบบที่คล้ายกันและเป็นวิธีที่ดีที่สุดที่ฉันสามารถคิดอธิบายได้ นั่นคือเหตุผลที่ฉันบอกว่ามันเป็นการล็อกแบบสองครั้ง
Scott Chamberlain

ฉันไม่ได้แสดงความคิดเห็นเกี่ยวกับรหัสของคุณ ฉันแสดงความคิดเห็นว่า Double Check Locking ไม่ทำงาน รหัสของคุณเรียบร้อยดี
DarthVader

1
ฉันพบว่ายากที่จะเห็นว่าสถานการณ์การล็อกแบบนี้เป็นอย่างไรและพื้นที่จัดเก็บข้อมูลประเภทนี้จะสมเหตุสมผลอย่างไร: หากคุณกำลังล็อกการสร้างค่าทั้งหมดที่มีMemoryCacheโอกาสเกิดขึ้นอย่างน้อยหนึ่งในสองสิ่งนั้นผิด
จอนฮันนา

@ScottChamberlain เพียงแค่มองไปที่รหัสนี้และมันไม่ไวต่อข้อยกเว้นที่เกิดขึ้นระหว่างการได้มาของล็อคและบล็อกลอง ผู้เขียน C # ในช่วงสั้น ๆ พูดถึงเรื่องนี้ที่นี่albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity

9
ข้อเสียของรหัสนี้คือ CacheKey "A" จะบล็อกคำขอ CacheKey "B" หากทั้งสองยังไม่ได้แคช ในการแก้ปัญหานี้คุณสามารถใช้พจนานุกรม <string, object> พร้อมกันซึ่งคุณเก็บ cachekeys ไว้เพื่อล็อค
MichaelD

44

มีไลบรารีโอเพ่นซอร์ส [ข้อจำกัดความรับผิดชอบ: ที่ฉันเขียน]: LazyCacheที่ IMO ครอบคลุมความต้องการของคุณด้วยโค้ดสองบรรทัด:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

มันได้สร้างขึ้นในการล็อคโดยค่าเริ่มต้นดังนั้นวิธีการแคชจะดำเนินการเพียงครั้งเดียวต่อการพลาดแคชและใช้แลมด้าเพื่อให้คุณสามารถ "รับหรือเพิ่ม" ได้ในครั้งเดียว ค่าเริ่มต้นคือ 20 นาทีเลื่อนหมดอายุ

มีแม้แต่แพ็คเกจ NuGet ;)


4
Dapper ของการแคช
Charles Burns

3
สิ่งนี้ทำให้ฉันเป็นนักพัฒนาที่ขี้เกียจซึ่งทำให้คำตอบนี้ดีที่สุด!
jdnew18

ควรค่าแก่การกล่าวถึงบทความที่หน้า github สำหรับ LazyCache ชี้ให้เห็นว่าอ่านได้ดีสำหรับเหตุผลเบื้องหลัง alastaircrabtree.com/…
ราฟาเอลเมอร์ลิน

2
มันล็อคต่อคีย์หรือต่อแคช?
jjxtra

1
@DirkBoer ไม่มันจะไม่ถูกบล็อกเพราะวิธีการล็อคและความขี้เกียจใช้ใน lazycache
alastairtree

30

ฉันได้รับการแก้ไขปัญหานี้โดยการใช้ของAddOrGetExistingวิธีการใน MemoryCache และการใช้เริ่มต้นขี้เกียจ

โดยพื้นฐานแล้วรหัสของฉันมีลักษณะดังนี้:

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

สถานการณ์กรณีที่เลวร้ายที่สุดคือคุณสร้างLazyวัตถุเดียวกันสองครั้ง แต่นั่นเป็นเรื่องเล็กน้อย การใช้การAddOrGetExistingรับประกันว่าคุณจะได้รับอินสแตนซ์ของLazyวัตถุเพียงครั้งเดียวดังนั้นคุณจึงรับประกันได้ว่าจะเรียกใช้วิธีการเริ่มต้นที่มีราคาแพงเพียงครั้งเดียว


4
ปัญหาเกี่ยวกับวิธีการประเภทนี้คือคุณสามารถแทรกข้อมูลที่ไม่ถูกต้องได้ หากSomeHeavyAndExpensiveCalculationThatResultsAString()เกิดข้อยกเว้นแสดงว่าติดอยู่ในแคช แม้แต่ข้อยกเว้นชั่วคราวก็จะถูกแคชไว้ด้วยLazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
ในขณะที่ความจริง Lazy <T> สามารถส่งคืนข้อผิดพลาดหากข้อยกเว้นการเริ่มต้นล้มเหลวนั่นเป็นสิ่งที่ค่อนข้างง่ายในการตรวจจับ จากนั้นคุณสามารถขับไล่ Lazy <T> ใด ๆ ที่แก้ไขข้อผิดพลาดออกจากแคชสร้าง Lazy <T> ใหม่ใส่สิ่งนั้นในแคชและแก้ไขได้ ในรหัสของเราเองเราทำสิ่งที่คล้ายกัน เราลองซ้ำตามจำนวนครั้งที่กำหนดก่อนที่จะเกิดข้อผิดพลาด
Keith

12
AddOrGetExisting คืนค่าว่างหากไม่มีรายการดังนั้นคุณควรตรวจสอบและส่งคืน lazyObject ในกรณีนั้น
Gian Marco

1
การใช้ LazyThreadSafetyMode.PublicationOnly จะหลีกเลี่ยงการแคชข้อยกเว้น
Clement

2
ตามความคิดเห็นในบล็อกโพสต์นี้หากการเริ่มต้นรายการแคชมีราคาแพงมากควรขับไล่โดยมีข้อยกเว้น (ดังที่แสดงในตัวอย่างในบล็อกโพสต์) แทนที่จะใช้ PublicationOnly เนื่องจากมีความเป็นไปได้ที่ทั้งหมด เธรดสามารถเรียกตัวเริ่มต้นได้ในเวลาเดียวกัน
bcr

15

ฉันถือว่ารหัสนี้มีปัญหาการเกิดพร้อมกัน:

จริงๆแล้วมันก็ค่อนข้างดีแม้ว่าจะมีการปรับปรุงที่เป็นไปได้

โดยทั่วไปแล้วรูปแบบที่เรามีเธรดหลายเธรดตั้งค่าที่ใช้ร่วมกันในการใช้งานครั้งแรกเพื่อไม่ล็อคค่าที่ได้รับและตั้งค่าได้:

  1. หายนะ - โค้ดอื่น ๆ จะถือว่ามีเพียงอินสแตนซ์เดียวเท่านั้น
  2. หายนะ - รหัสที่ได้รับอินสแตนซ์ไม่สามารถทนต่อการดำเนินการพร้อมกันเพียงครั้งเดียว (หรืออาจเป็นจำนวนน้อย)
  3. หายนะ - วิธีการจัดเก็บไม่ปลอดภัยต่อเธรด (เช่นมีสองเธรดที่เพิ่มลงในพจนานุกรมและคุณจะได้รับข้อผิดพลาดที่น่ารังเกียจทุกประเภท)
  4. Sub-optimal - ประสิทธิภาพโดยรวมแย่กว่าถ้าการล็อคทำให้มั่นใจได้ว่ามีเธรดเพียงตัวเดียวเท่านั้นที่สามารถรับค่าได้
  5. เหมาะสมที่สุด - ค่าใช้จ่ายในการมีเธรดหลายเธรดทำงานซ้ำซ้อนน้อยกว่าค่าใช้จ่ายในการป้องกันโดยเฉพาะอย่างยิ่งเนื่องจากสามารถเกิดขึ้นได้ในช่วงเวลาสั้น ๆ เท่านั้น

อย่างไรก็ตามเมื่อพิจารณาจากที่นี่ซึ่งMemoryCacheอาจขับไล่รายการแล้ว:

  1. หากเป็นเรื่องหายนะที่จะมีมากกว่าหนึ่งอินสแตนซ์MemoryCacheนั่นเป็นแนวทางที่ผิด
  2. หากคุณต้องป้องกันการสร้างพร้อมกันคุณควรดำเนินการ ณ จุดที่สร้าง
  3. MemoryCache เธรดปลอดภัยในแง่ของการเข้าถึงอ็อบเจ็กต์นั้นดังนั้นจึงไม่ต้องกังวลที่นี่

แน่นอนว่าคุณต้องนึกถึงความเป็นไปได้ทั้งสองอย่างนี้แม้ว่าครั้งเดียวที่มีสองอินสแตนซ์ของสตริงเดียวกันที่มีอยู่อาจเป็นปัญหาได้ก็คือหากคุณทำการเพิ่มประสิทธิภาพโดยเฉพาะที่ไม่ได้ใช้ที่นี่ *

ดังนั้นเราจึงเหลือความเป็นไปได้:

  1. SomeHeavyAndExpensiveCalculation()มันมีราคาถูกเพื่อหลีกเลี่ยงค่าใช้จ่ายของการโทรซ้ำไป
  2. SomeHeavyAndExpensiveCalculation()มันถูกกว่าไม่ได้ที่จะหลีกเลี่ยงค่าใช้จ่ายของการโทรซ้ำไป

และการทำงานนั้นอาจเป็นเรื่องยาก (จริงๆแล้วสิ่งที่ควรค่าแก่การทำโปรไฟล์แทนที่จะคิดว่าคุณสามารถทำงานได้) ควรพิจารณาที่นี่แม้ว่าวิธีที่ชัดเจนที่สุดในการล็อคเม็ดมีดจะป้องกันการเพิ่มทั้งหมดในแคชรวมถึงวิธีที่ไม่เกี่ยวข้องด้วย

ซึ่งหมายความว่าหากเรามี 50 เธรดที่พยายามกำหนดค่าที่แตกต่างกัน 50 ค่าเราจะต้องทำให้เธรดทั้งหมด 50 เธรดรอซึ่งกันและกันแม้ว่าจะไม่ได้ทำการคำนวณแบบเดียวกันก็ตาม

ด้วยเหตุนี้คุณจึงควรใช้รหัสที่คุณมีมากกว่าการใช้รหัสที่หลีกเลี่ยงสภาพการแข่งขันและหากสภาพการแข่งขันเป็นปัญหาคุณก็ค่อนข้างจะต้องจัดการกับสิ่งนั้นที่อื่นหรือต้องการรหัสอื่น กลยุทธ์การแคชมากกว่าหนึ่งที่ขับไล่รายการเก่า†

สิ่งหนึ่งที่ฉันจะเปลี่ยนคือผมต้องการเปลี่ยนการเรียกร้องให้กับคนที่จะSet() AddOrGetExisting()จากที่กล่าวมาข้างต้นเป็นที่ชัดเจนว่าอาจไม่จำเป็น แต่จะช่วยให้สามารถรวบรวมรายการที่ได้รับใหม่ได้ลดการใช้หน่วยความจำโดยรวมและช่วยให้อัตราส่วนของคอลเลกชันรุ่นต่ำถึงรุ่นสูงสูงขึ้น

ใช่แล้วคุณสามารถใช้การล็อกสองครั้งเพื่อป้องกันการเกิดพร้อมกันได้ แต่การเกิดพร้อมกันนั้นไม่ได้เป็นปัญหาหรือการจัดเก็บค่าในทางที่ผิดหรือการล็อกสองครั้งในร้านค้าจะไม่ใช่วิธีที่ดีที่สุดในการแก้ปัญหา .

* หากคุณทราบว่ามีสตริงเพียงชุดเดียวคุณสามารถเพิ่มประสิทธิภาพการเปรียบเทียบความเท่าเทียมกันได้ซึ่งเป็นครั้งเดียวที่มีสำเนาของสตริงสองชุดอาจไม่ถูกต้องแทนที่จะเป็นเพียงแค่ย่อยที่เหมาะสมที่สุด แต่คุณต้องการทำ ประเภทของการแคชที่แตกต่างกันมากเพื่อให้เหมาะสม เช่นการจัดเรียงXmlReaderทำภายใน

†มีแนวโน้มว่าจะมีการจัดเก็บอย่างไม่มีกำหนดหรือผู้ที่ใช้การอ้างอิงที่ไม่เหมาะสมดังนั้นจึงจะขับไล่รายการได้ก็ต่อเมื่อไม่มีการใช้งานที่มีอยู่


1

เพื่อหลีกเลี่ยงการล็อกแบบโกลบอลคุณสามารถใช้ SingletonCache เพื่อใช้งานหนึ่งล็อคต่อคีย์โดยไม่ต้องใช้หน่วยความจำระเบิด (อ็อบเจ็กต์การล็อกจะถูกลบออกเมื่อไม่มีการอ้างอิงอีกต่อไปและการรับ / รีลีสเป็นเธรดที่ปลอดภัยซึ่งรับประกันได้ว่าจะใช้อินสแตนซ์เพียง 1 รายการผ่านการเปรียบเทียบ และแลกเปลี่ยน)

การใช้งานมีลักษณะดังนี้:

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }      
}

รหัสอยู่ที่ GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

นอกจากนี้ยังมีการใช้งาน LRU ที่มีน้ำหนักเบากว่า MemoryCache และมีข้อดีหลายประการ - อ่านและเขียนพร้อมกันได้เร็วขึ้นขนาดขอบเขตไม่มีเธรดพื้นหลังตัวนับ perf ภายในเป็นต้น (ข้อจำกัดความรับผิดชอบฉันเขียนไว้แล้ว)


0

ตัวอย่างคอนโซลของMemoryCache "วิธีบันทึก / รับคลาสออบเจ็กต์อย่างง่าย"

เอาต์พุตหลังจากเปิดและกด Any keyยกเว้นEsc:

บันทึกลงในแคช!
รับจากแคช!
Some1
บาง 2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }

0
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}

LazyCache เร็วมาก :) ฉันเขียนโค้ดนี้สำหรับที่เก็บ REST API
art24war

0

มันช้าไปหน่อย ... การใช้งานเต็มรูปแบบ:

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    {
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    }
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

นี่คือgetPageContentลายเซ็น:

async Task<string> getPageContent(RequestQuery requestQuery);

และนี่คือการMemoryCacheWithPolicyใช้งาน:

public class MemoryCacheWithPolicy<TParameter, TResult>
{
    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    {
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    });

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    {
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        {
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        }
        else
        {
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            {
                //exception: this case should not happened!
            }

            if (!cacheParam.Equals(param))
            {
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            }
            else
            {
                _nlogger.Trace(" cache is used.");
            }

        }
        return await Task.FromResult<TResult>(cacheEntry);
    }
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    {
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    }
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    {
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    }
    void checkCacheEntry<T>(object key, string name)
    {
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: {0}, Name: {1}, Value: {2}", key, name, value);
    }
}

nloggerเป็นเพียงnLogวัตถุในการติดตามMemoryCacheWithPolicyพฤติกรรม ฉันสร้างแคชหน่วยความจำอีกครั้งหากRequestQuery requestQueryมีการเปลี่ยนแปลงอ็อบเจ็กต์คำขอ ( ) ผ่านตัวแทน ( Func<TParameter, TResult> createCacheData) หรือสร้างใหม่เมื่อเลื่อนหรือเวลาสัมบูรณ์ถึงขีด จำกัด โปรดทราบว่าทุกอย่างไม่ตรงกันด้วย;)


บางทีคำตอบของคุณอาจเกี่ยวข้องกับคำถามนี้มากกว่า: Async threadsafe Get from MemoryCache
Theodor Zoulias

ฉันเดาอย่างนั้น แต่ก็ยังมีประโยชน์ในการแลกเปลี่ยนประสบการณ์;)
Sam Saarian

0

เป็นการยากที่จะเลือกว่าอันไหนดีกว่า ล็อคหรือ ReaderWriterLockSlim คุณต้องการสถิติการอ่านและเขียนตัวเลขและอัตราส่วนในโลกแห่งความเป็นจริงเป็นต้น

แต่ถ้าคุณเชื่อว่าการใช้ "ล็อค" เป็นวิธีที่ถูกต้อง นี่คือทางออกที่แตกต่างกันสำหรับความต้องการที่แตกต่างกัน ฉันยังรวมโซลูชันของ Allan Xu ไว้ในโค้ดด้วย เนื่องจากทั้งสองสามารถจำเป็นสำหรับความต้องการที่แตกต่างกัน

นี่คือข้อกำหนดที่นำฉันไปสู่โซลูชันนี้:

  1. คุณไม่ต้องการหรือไม่สามารถจัดหาฟังก์ชัน 'GetData' ได้ด้วยเหตุผลบางประการ บางทีฟังก์ชัน 'GetData' อาจอยู่ในคลาสอื่นที่มีคอนสตรัคเตอร์จำนวนมากและคุณไม่ต้องการสร้างอินสแตนซ์จนกว่าจะมั่นใจได้ว่าจะไม่สามารถหลีกเลี่ยงได้
  2. คุณจำเป็นต้องเข้าถึงข้อมูลแคชเดียวกันจากตำแหน่ง / ระดับต่างๆของแอปพลิเคชัน และสถานที่ต่างๆเหล่านั้นไม่สามารถเข้าถึงวัตถุตู้เก็บของเดียวกันได้
  3. คุณไม่มีคีย์แคชคงที่ ตัวอย่างเช่น; จำเป็นต้องแคชข้อมูลบางอย่างด้วยคีย์แคช sessionId

รหัส:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            {
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                {
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    {
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    }
                }
            }
        }


        private static string SomeHeavyAndExpensiveXYZCalculation() { return "Expensive"; }
        private static string SomeHeavyAndExpensiveABCCalculation() { return "Expensive"; }

        public static class MemoryCacheHelper
        {
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, Func<T> GetData) where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                    return cachedData;

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                }
            }

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            {
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            }

            public static T GetCachedData<T>(string cacheKey) where T : class
            {
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            }

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            {
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            }

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            {
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                {
                    lock (Lockers)//maybe a better locker is needed?
                    {
                        try//bypass exceptions
                        {
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            {
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            }

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        }
                        catch (Exception)
                        { }
                    }
                }

            }
            #endregion
        }
    }

    class DatabaseRepositoryWithHeavyConstructorOverHead
    {
        internal string GetDataExpensiveDataForSession(string sessionId)
        {
            return "Expensive data from database";
        }
    }

}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.