วิธีแคชข้อมูลในแอปพลิเคชัน MVC


252

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

ในสถานการณ์ของฉันฉันจะใช้ LINQ กับเอนทิตี (กรอบงานเอนทิตี) ในการเรียกใช้ GetNames ครั้งแรก (หรือวิธีการใด ๆ ก็ตาม) ฉันต้องการดึงข้อมูลจากฐานข้อมูล ฉันต้องการบันทึกผลลัพธ์ในแคชและในการโทรครั้งที่สองเพื่อใช้เวอร์ชันแคชหากมีอยู่

ทุกคนสามารถแสดงตัวอย่างของวิธีการนี้จะทำงานได้อย่างไรซึ่งควรจะนำไปใช้ (แบบจำลอง) และถ้ามันจะทำงาน

ฉันเคยเห็นสิ่งนี้ทำในแอป ASP.NET แบบดั้งเดิมซึ่งโดยทั่วไปจะเป็นข้อมูลที่นิ่งมาก


1
ในการทบทวนคำตอบด้านล่างอย่าลืมพิจารณาว่าคุณต้องการให้คอนโทรลเลอร์ของคุณมีความรู้ / ความรับผิดชอบสำหรับการเข้าถึงข้อมูลและการแคชข้อกังวล โดยทั่วไปคุณต้องการแยกสิ่งนี้ ดูรูปแบบ Repository สำหรับวิธีที่ดีในการทำเช่นนี้: deviq.com/repository-pattern
ssmith

คำตอบ:


75

อ้างอิง System.Web dll ในรุ่นของคุณและใช้ System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

ลดความซับซ้อนลงเล็กน้อย แต่ฉันเดาว่ามันจะใช้ได้ นี่ไม่ใช่ MVC เฉพาะและฉันมักจะใช้วิธีนี้ในการแคชข้อมูล


89
ฉันไม่แนะนำวิธีแก้ปัญหานี้: ในทางกลับกันคุณอาจได้รับวัตถุว่างเปล่าอีกครั้งเพราะมันถูกอ่านอีกครั้งในแคชและมันอาจจะถูกทิ้งจากแคชไปแล้ว ฉันอยากจะทำ: public string [] GetNames () {string [] noms = Cache ["names"]; if (noms == null) {noms = DB.GetNames (); แคช ["names"] = noms; } return (noms); }
Oli

ฉันเห็นด้วยกับ Oli .. การได้รับผลลัพธ์จากการโทรจริงไปยังฐานข้อมูลนั้นดีกว่าการเอาออกมาจากแคช
CodeClimber

1
สิ่งนี้ใช้งานได้กับDB.GetNames().AsQueryableวิธีการเลื่อนการสอบถามหรือไม่
Chase Florell

ไม่เว้นเสียแต่ว่าคุณจะเปลี่ยนค่าส่งคืนจากสตริง [] เป็น IEnumerable <string>
terjetyl

12
หากคุณไม่ได้ตั้งค่าการหมดอายุ .. เมื่อใดที่แคชหมดอายุตามค่าเริ่มต้น
Chaka

403

นี่เป็นคลาส / บริการแคชที่ดีและเรียบง่ายที่ฉันใช้:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

การใช้งาน:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

ผู้ให้บริการแคชจะตรวจสอบว่ามีชื่อ "แคช id" อยู่ในแคชหรือไม่หากไม่มีก็จะเรียกวิธีการมอบหมายเพื่อดึงข้อมูลและเก็บไว้ในแคช

ตัวอย่าง:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())

3
ฉันได้ปรับเปลี่ยนสิ่งนี้เพื่อให้กลไกการแคชถูกใช้ต่อเซสชันผู้ใช้โดยใช้ HttpContext.Current.Session แทน ฉันยังใส่คุณสมบัติ Cache ในคลาส BaseController ของฉันเพื่อให้เข้าถึงได้ง่ายและปรับปรุง Constructor อนุญาตให้ DI สำหรับการทดสอบหน่วย หวังว่านี่จะช่วยได้
WestDiscGolf

1
คุณยังสามารถทำให้คลาสนี้และวิธีการแบบคงที่เพื่อนำมาใช้ใหม่ในตัวควบคุมอื่น ๆ
Alex

5
คลาสนี้ไม่ควรขึ้นอยู่กับ HttpContext ฉันทำให้มันง่ายขึ้นเพื่อจุดประสงค์ตัวอย่างที่นี่ วัตถุแคชจะต้องแทรกผ่านตัวสร้าง - มันสามารถถูกแทนที่ด้วยกลไกการแคชอื่น ๆ ทั้งหมดนี้ทำได้ด้วย IoC / DI พร้อมกับวงจรชีวิตแบบคงที่ (ซิงเกิล)
Hrvoje Hudo

3
@Brendan - และที่แย่กว่านั้นคือมันมีสายอักขระเวทสำหรับแคชคีย์แทนที่จะอนุมานจากชื่อเมธอดและพารามิเตอร์
ช่างทอง

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

43

ฉันหมายถึงโพสต์ของ TT และแนะนำวิธีการดังต่อไปนี้:

อ้างอิง System.Web dll ในรุ่นของคุณและใช้ System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

คุณไม่ควรส่งคืนค่าที่อ่านซ้ำจากแคชเนื่องจากคุณจะไม่มีทางรู้ว่าในช่วงเวลานั้นยังคงอยู่ในแคช แม้ว่าคุณจะใส่ไว้ในคำสั่งก่อนหน้ามันอาจจะหายไปแล้วหรือไม่เคยถูกเพิ่มลงในแคช - คุณแค่ไม่รู้

ดังนั้นคุณเพิ่มข้อมูลที่อ่านจากฐานข้อมูลและส่งคืนโดยตรงไม่ใช่การอ่านซ้ำจากแคช


แต่บรรทัดไม่ได้Cache["names"] = noms;อยู่ในแคช?
โอมาร์

2
@Baddie ใช่มันเป็นเช่นนั้น แต่ตัวอย่างนี้แตกต่างจาก Oli ตัวแรกที่อ้างถึงเพราะเขาไม่ได้เข้าถึงแคชอีกครั้ง - ปัญหาคือว่าเพิ่งทำ: return (string []) Cache ["names"]; .. COULD ส่งผลให้ค่า Null ถูกส่งคืนเนื่องจาก COULD หมดอายุแล้ว มันไม่น่าเป็นไปได้ แต่สามารถเกิดขึ้นได้ ตัวอย่างนี้ดีกว่าเนื่องจากเราเก็บค่าจริงที่ส่งคืนจาก db ในหน่วยความจำแคชค่านั้นจากนั้นคืนค่านั้นไม่ใช่ค่าที่อ่านซ้ำจากแคช
jamiebarrow

หรือ ... ค่าอ่านซ้ำจากแคชหากยังคงมีอยู่ (! = null) ดังนั้นจุดทั้งหมดของแคช นี่เป็นการบอกว่ามันตรวจสอบค่า Null ซ้ำอีกครั้งและอ่านฐานข้อมูลเมื่อจำเป็น ฉลาดมากขอบคุณ Oli!
Sean Kendle

คุณช่วยแบ่งปันลิงค์ที่ฉันสามารถอ่านเกี่ยวกับการแคชแอปพลิเคชั่นตาม Key Value ได้ไหม ฉันไม่สามารถหาลิงก์ได้
ไม่สามารถแตกได้

@Oli, วิธีการใช้บันทึกแคชนี้จาก CSHTML หรือ HTML หน้า
Deepan Raj

37

สำหรับ. NET 4.5+ framework

เพิ่มการอ้างอิง: System.Runtime.Caching

เพิ่มการใช้คำสั่ง: using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

ใน. NET Framework 3.5 และรุ่นก่อนหน้า ASP.NET ให้การใช้งานแคชในหน่วยความจำใน System.Web.Caching namespace ใน. NET Framework รุ่นก่อนหน้าการแคชมีเฉพาะใน System.Web namespace เท่านั้นดังนั้นจึงจำเป็นต้องพึ่งพาคลาส ASP.NET ใน. NET Framework 4 เนมสเปซ System.Runtime.Caching ประกอบด้วย APIs ที่ออกแบบมาสำหรับเว็บและแอปพลิเคชันที่ไม่ใช่เว็บ

ข้อมูลเพิ่มเติม:


อยู่ที่ไหนDb.GerNames()มาจากไหน
จูเนียร์

DB.GetNames เป็นเพียงวิธีการจาก DAL ที่ดึงชื่อบางส่วนจากฐานข้อมูล นี่คือสิ่งที่คุณจะเรียกตามปกติ
juFo

นี่ควรจะอยู่ด้านบนเนื่องจากมีทางออกที่เกี่ยวข้องในปัจจุบัน
BYISHIMO Audace

2
ขอบคุณจำเป็นต้องเพิ่มแพ็กเกจ System.Runtime.Caching nuget ด้วย (v4.5)
Steve Greene

26

Steve Smith ได้โพสต์บล็อกยอดเยี่ยมสองรายการซึ่งสาธิตวิธีการใช้รูปแบบ CachedRepository ใน ASP.NET MVC มันใช้รูปแบบที่เก็บได้อย่างมีประสิทธิภาพและช่วยให้คุณได้รับแคชโดยไม่ต้องเปลี่ยนรหัสที่มีอยู่ของคุณ

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

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


1
โพสต์ยอดเยี่ยม! ขอบคุณสำหรับการแบ่งปัน!!
ทำเครื่องหมาย Good

ลิงค์เสียชีวิตตั้งแต่ 2013-08-31
CBono


คุณช่วยแบ่งปันลิงค์ที่ฉันสามารถอ่านเกี่ยวกับการแคชแอปพลิเคชั่นตาม Key Value ได้ไหม ฉันไม่สามารถหาลิงก์ได้
ไม่สามารถแตกได้

4

AppFabric Cachingถูกแจกจ่ายและเทคนิคการแคชในหน่วยความจำที่เก็บข้อมูลในคู่ของคีย์ - ค่าโดยใช้หน่วยความจำฟิสิคัลข้ามเซิร์ฟเวอร์หลายเครื่อง AppFabric ให้การปรับปรุงประสิทธิภาพและการปรับขยายได้สำหรับแอปพลิเคชัน. NET Framework แนวคิดและสถาปัตยกรรม


นี่เป็นเฉพาะ Azure ไม่ใช่ ASP.NET MVC โดยทั่วไป
Henry C

3

กำลังขยายคำตอบของ @Hrvoje Hudo ...

รหัส:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

ตัวอย่าง

การแคชรายการเดี่ยว (เมื่อแต่ละรายการถูกแคชตาม ID เนื่องจากการแคชแคตตาล็อกทั้งหมดสำหรับประเภทรายการจะเข้มข้นเกินไป)

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

แคชบางสิ่งบางอย่าง

IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

ทำไมต้อง TId

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


1
นิยามอินเทอร์เฟซของคุณไม่มีพารามิเตอร์ "DurationInMinutes" ;-)
Tech0

3

นี่คือการปรับปรุงคำตอบของ Hrvoje Hudo การใช้งานนี้มีการปรับปรุงที่สำคัญสองประการ:

  • คีย์แคชถูกสร้างขึ้นโดยอัตโนมัติตามฟังก์ชั่นเพื่ออัปเดตข้อมูลและวัตถุที่ส่งผ่านซึ่งระบุการอ้างอิง
  • ผ่านช่วงเวลาสำหรับช่วงเวลาแคชใด ๆ
  • ใช้ล็อคเพื่อความปลอดภัยด้าย

โปรดทราบว่าสิ่งนี้มีการพึ่งพาบน Newtonsoft.Json เพื่อทำให้เป็นอันดับของวัตถุที่ขึ้นต่อกัน แต่ที่สามารถสลับออกมาได้อย่างง่ายดายสำหรับวิธีการทำให้เป็นอนุกรมอื่น ๆ

ICache.cs

public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

การใช้งาน:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);

2
if (item == null)ควรจะเป็นภายในล็อค ตอนนี้เมื่อifอยู่ก่อนการล็อกสภาพการแข่งขันสามารถเกิดขึ้นได้ หรือดียิ่งขึ้นคุณควรเก็บไว้ifก่อนการล็อก แต่ตรวจสอบอีกครั้งว่าแคชยังว่างเปล่าหรือเปล่าเป็นบรรทัดแรกที่อยู่ในล็อค เพราะถ้ามีสองเธรดมาพร้อมกันทั้งคู่ก็อัพเดตแคช ล็อคปัจจุบันของคุณไม่เป็นประโยชน์
อัล Kepp

3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}

3
ลองเพิ่มคำอธิบายบางอย่าง
Mike Debela

2

ฉันได้ใช้มันในวิธีนี้และได้ผลกับฉัน https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx ข้อมูลพารามิเตอร์สำหรับ system.web.caching.cache.add

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}

upvotes พิเศษสำหรับสิ่งที่มีคุณสมบัติครบถ้วนด้วยเนมสเปซเต็มรูปแบบ !!
Ninjanoel

1

ฉันใช้สองชั้น ก่อนอื่นวัตถุแคชหลัก:

public class Cacher<TValue>
    where TValue : class
{
    #region Properties
    private Func<TValue> _init;
    public string Key { get; private set; }
    public TValue Value
    {
        get
        {
            var item = HttpRuntime.Cache.Get(Key) as TValue;
            if (item == null)
            {
                item = _init();
                HttpContext.Current.Cache.Insert(Key, item);
            }
            return item;
        }
    }
    #endregion

    #region Constructor
    public Cacher(string key, Func<TValue> init)
    {
        Key = key;
        _init = init;
    }
    #endregion

    #region Methods
    public void Refresh()
    {
        HttpRuntime.Cache.Remove(Key);
    }
    #endregion
}

รายการที่สองคือวัตถุแคช:

public static class Caches
{
    static Caches()
    {
        Languages = new Cacher<IEnumerable<Language>>("Languages", () =>
                                                          {
                                                              using (var context = new WordsContext())
                                                              {
                                                                  return context.Languages.ToList();
                                                              }
                                                          });
    }
    public static Cacher<IEnumerable<Language>> Languages { get; private set; }
}

0

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

 public class GPDataDictionary
{
    private Dictionary<string, object> configDictionary = new Dictionary<string, object>();

    /// <summary>
    /// Configuration values dictionary
    /// </summary>
    public Dictionary<string, object> ConfigDictionary
    {
        get { return configDictionary; }
    }

    private static GPDataDictionary instance;
    public static GPDataDictionary Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GPDataDictionary();
            }
            return instance;
        }
    }

    // private constructor
    private GPDataDictionary() { }

}  // singleton

สิ่งนี้ใช้ได้กับฉันอย่างสมบูรณ์นั่นคือเหตุผลที่ฉันแนะนำสิ่งนี้ให้กับทุกคนที่อาจได้รับความช่วยเหลือ
GeraGamo


-8

คุณสามารถลองและใช้แคชที่สร้างขึ้นใน ASP MVC:

เพิ่มแอททริบิวต่อไปนี้ให้กับวิธีการควบคุมที่คุณต้องการแคช

[OutputCache(Duration=10)]

ในกรณีนี้ ActionResult ของสิ่งนี้จะถูกแคชเป็นเวลา 10 วินาที

เพิ่มเติมเกี่ยวกับเรื่องนี้ที่นี่


4
OutputCache สำหรับการเรนเดอร์ของ Action คำถามนั้นเกี่ยวกับการแคชข้อมูลไม่ใช่เพจ
Coolcoder

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