缓存使用的十大错误

介绍

那些使用频率比较高,并且获取过程代价比较高(网络时间 cpu使用等 )的数据,通常使用缓存的方法提高应用在高负载情况下的性能。然而不正确的使用缓存会带来很多痛苦的问题。特别是使用独立缓存服务器或者缓存应用的分布式缓存技术时。此外,使用内存缓存可以很好运行的代码可能会在使用进程外的cache时失败( code that works fine using in-memory cache can fail when the cache is made out-of-process.),下面介绍几点使用分布式缓存时可能遇到的问题:

主要的10点:

1. 依赖.Net的默认序列化机制

2. 在一个cache中存放大的数据对象

3.使用cache在多线程之间共享对象

4.想当然认为数据被存储到cache中后立刻就会被缓存

5.缓存嵌套的对象集合(collection)

6.整体缓存父子对象和分开缓存父子对象

7.缓存配置Caching Configuration settings

8.Caching Live Objects that have open handle to stream, file, registry, or network

9.使用多个key缓存一个相同item

10.数据更新或者删除后没有再缓存中做对应的操作

下面看看如何避免:

    假设你暂时在使用asp.net cache或者 enterprise library cache,并对其性能感到满意。现在,你为了是系统更具扩展性, 因此使用了进程外cache或者如Velocity和Memcache这样的分布式缓存系统。之后,发现系统可能崩溃了,因此上述错误列表可能帮到你


依赖.Net 默认序列化

当你使用诸如Velocity或Memcached的进程外缓存集解决方案时,缓存数据存放在单独进程中而不是你的应用的进程中;每次你添加一个数据到缓存中,它将数据序列化为byte array,然后发送这些byte array 到缓存服务器进行缓存存储。类似的,当应用从缓存中获取数据时,缓存服务器发送byte array 给应用程序,然后由应用反序列化byte array 为合适的.Net 对象。然而,由于.Net 的默认序列化器是基于反射,属于cpu密集运算(不知咋说cpu intensive)。结果是,缓存数据或者获取缓存数据存在的序列化和反序列化会导致较高的CPU运算,特比是当你缓存比较复杂的数据类型时。较高的CPU占有率不是发生在缓存服务器,而是运行应用的机器上,所以,你最好使用这里的几种方法 shown in this article 来减小CPU进行序列化和反序列化操作时的消耗。如果你需要序列化和反序列化数据,我个人更倾向自己将其实现ISerializable接口,然后实现反序列化构造函数

    public class Customer : ISerializable
    {
        public string FirstName;
        public string LastName;
        public int Salary;
        public DateTime DateOfBirth;

        public Customer()
        {
        }

        public Customer(SerializationInfo info, StreamingContext context)
        {
            FirstName = info.GetString("FirstName");
            LastName = info.GetString("LastName");
            Salary = info.GetInt32("Salary");
            DateOfBirth = info.GetDateTime("DateOfBirth");
        }

        #region ISerializable Members

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("FirstName", FirstName);
            info.AddValue("LastName", LastName);
            info.AddValue("Salary", Salary);
            info.AddValue("DateOfBirth", DateOfBirth);
        }

        #endregion        
    }

这阻止了formatter使用反射,将有很大的性能提升,可能会是默认序列化的100倍。所以,强烈建议那些你要缓存的数据,自己实现其序列化和反序列化的接口,而不是让.net使用反射来指出那些地方需要序列化

在一个cache中存放大数据

      有时会想,一个大数据如果从数据源中获取,代价比较高,干脆缓存起来吧。如缓存一个1M的对象图表,如果每次一个请求,当然感觉会很不错,实际上也如此。然而,在并发请求时,频繁访问大数据将会撑爆你的服务器CPU,这是因为cache过程有很高的序列化和反序列化的代价,每次从进程外cache中获取1M的数据时,CPU不得不花费大量时间去在Memory中构建这个大数据对象
var largeObjectGraph = myCache.Get("LargeObjectGraph");
var anItem = 
    largeObjectGraph.FirstLevel.SecondLevel.ThirdLevel.FourthLevel.TheItemWeNeed;

解决方案是不要再一个cache 中使用一个key存放大数据,相反,把大数据变成小的数据并各自缓存,你只获取你需要的最小数据

// store smaller parts in cache as individual item
var largeObjectGraph = new VeryLargeObjectGraph();
myCache.Add("LargeObjectGraph.FirstLevel.SecondLevel.ThirdLevel", 
  largeObjectGraph.FirstLevel.SecondLevel.ThirdLevel);
...
...
// get the smaller parts from cache
var thirdLevel = myCache.Get("LargeObjectGraph.FirstLevel.SecondLevel.ThirdLevel");
var anItem = thirdLevel.FourthLevel.TheItemWeNeed;

这个方法的关注点是大数据中最经常使用的数据(例如配置对象中的connection strings)把他们单独存放在cache中。

记住,缓存的每个数据保持最小,最大最好不要超过8KB

使用缓存在多线程之间共享对象

    可以多线程访问cache,有时只是为了在多线程之间方便传送数据。但cache如同static 变量一样,会带来多线程资源竞争问题。这个问题在分布式cache系统中更为常见,因为存储和读取缓存数据需要和其他进程进行通讯保证资源竞争,下面的例子在内存缓存中很少出现但在外进程cache系统中经常出现的资源竞争代码

myCache["SomeItem"] = 0;
var thread1 = new Thread(new ThreadStart(() =>
{
    var item = myCache["SomeItem"]; // Most likely 0
    item ++;
    myCache["SomeItem"] = item;
});
var thread2 = new Thread(new ThreadStart(() =>
{
    var item = myCache["SomeItem"]; // Most likely 1
    item ++;
    myCache["SomeItem"] = item;
});
var thread3 = new Thread(new ThreadStart(() =>
{
    var item = myCache["SomeItem"];  // Most likely 2
    item ++;
    myCache["SomeItem"] = item;
});
thread1.Start();
thread2.Start();
thread3.Start();
.
.

The above code most of the time demonstrates the most likely behavior when you are using in-memory cache. But when you go out-of-process or distributed, it will always fail to demonstrate the most-likely behavior. You need to implement some kind of locking here. Some caching provider allows you to lock an item. For example, Velocity has locking feature, but memcache does not. In Velocity, you can lock an item:

// get an item and lock it
DataCacheLockHandle handle;
SomeClass someItem = _defaultCache.GetAndLock("SomeItem", 
   TimeSpan.FromSeconds(1), out handle, true) as SomeClass;
// update an item
someItem.FirstName = "Version2";
// put it back and get the new version
DataCacheItemVersion version2 = _defaultCache.PutAndUnlock("SomeItem", 
    someItem, handle);

多线程情况下,你可以使用lock机制去保证读写的可靠性

假设存储数据后会立刻缓存

有时你通过点击确定按钮来存储数据到缓存,假设页面已经Postback,就可以从缓存中获取这条数据。你错了:

private void SomeButton_Clicked(object sender, EventArgs e)
{
  myCache["SomeItem"] = someItem;
}

private void OnPreRender()
{
  var someItem = myCache["SomeItem"]; // It's gone dude!
  Render(someItem);
}

你不能这样假设,即便你在代码第一行存储了数据然后在第三行读取它。因为当你的应用处在高压服务并缺少物理内存是,缓存将会吧不经常使用的数据清除,所以,当代码运行到第三行时, 你刚缓存的数据可能被清除了。因此不要假设你总是能从缓存获取缓存过的数据,你应该检查一下获取数据是否为null,如果是,就从数据源中重新获取吧

var someItem = myCache["SomeItem"] as SomeClass ?? GetFromSource();

应遵照上述方式获取缓存数据

缓存整个嵌套的对象集合

 有时你在一个缓存中缓存了整个对象集合因为你需要频繁访问集合中的item,所以每次你要集合中的某个item是,总是要先获取整个collection,如下:
var products = myCache.Get("Products");
var product = products[1];

这是很没效率的,为了读取某个item没必要加载整个collection,如果是内存缓存,这没问题,因为缓存只是存储了那个collection的引用地址。但是在分布式缓存中,每次获取这个collection缓存,整个collection都会被反序列化,这会导致性能下降,因此不缓存整个collection,而是单独缓存collection中的每个item

// store individual items in cache
foreach (Product product in products)
  myCache.Add("Product." + product.Index, product);
...
...
// read the individual item from cache
var product = myCache.Get("Product.0");

想法很简单,使用容易理解的key,比如使用index

整体缓存父子对象和分开缓存

有时你缓存了一个具有子对象的父对象,然后又分别缓存了父对象和子对象,例如,customer对象具有有一个order的集合(collection)对象,所以当你缓存customer时,order的集合对象也被缓存了。但是你有单独缓存了集合中的每个order后,如果cache中的某个order被更新了,customer中的order集合中的那个order却没有被更新,因此获取customer时,其中order集合中的某个order是错误的数据。

再次说明,这个例子在进程内的内存缓存可以很好的工作,但是如果是进程外 /分布式缓存,将会失败:

var customer = SomeCustomer();
var recentOrders = SomeOrders();
customer.Orders = GetCustomerOrders();
myCache.Add("RecentOrders", recentOrders);
myCache.Add("Customer", customer);
...
...
var recentOrders = myCahce.Get("RecentOrders");
var order = recentOrders["ORDER10001"];
order.Status = CANCELLED; 
...
...
...
var customer = myCache.Get("Customer");
var order = customer.Orders["ORDER10001"];
order.Status = PROCESSING; // Inconsistent. The order has already been cancelled

这是个比较难解决的问题,要有一个聪明设计避免同一个数据缓存两次带来的问题。通常的做法是不要缓存父对象的子对象集合,而是存储子对象的key以便单独从cache中获取。因此,在上述场景中,你不应该缓存customers的order集合,你应该缓存customer和orderID集合,当你需要customer的orders数据时,可以通过使用orderid获取单独的order 

var recentOrders = SomeOrders();
foreach (Order order in recentOrders)
   myCache.Add("Order." + order.ID, order);
...
var customer = SomeCustomer();
customer.OrderKeys = GetCustomerOrders(); // Store keys only
myCache.Add("Customer", customer);
...
...
var order = myCache.Get["Order.10001"];
order.Status = CANCELLED; 
...
...
...
var customer = myCache.Get("Customer");
var customerOrders = customer.OrderKeys.ConvertAll<string, Order>
   (key => myCache.Get("Order." + key));
var order = customerOrders["10001"]; // Correct object from cache

这个方法确保每个entity只在cache存储一次,不管它在collections或者父对象中出现多少次

缓存配置设置

  有时你会缓存配置设置(configuration settings),你可以使用cache过期逻辑确保配置定期刷新或者当配置文件或数据库变化时刷新,既然配置项访问非常频繁,从缓存中读取会增加CPU负担,不如使用静态变量存储配置

var connectionString = myCache.Get("Configuration.ConnectionString");

你不应该采用上面代码中的方式,从cache获取数据代价不便宜, 可能不比从文件或注册表这姑娘读取更费资源,但也不是很划算,特别是如果数据是自定义类型会增加序列化的成本,所以应该用静态变量来存储配置信息。但你可能会问,如果使用静态变量存储配置数据,在不重新启动appdomain的情况下,怎么刷新配置?你可以使用一些其他过期策略,如file listener监视并重新加载配置当配置文件有变化时,或者使用database polling 去检查数据库更新

缓存诸如有打开操作的file,registry或者network的活动对象

我曾见过有些开发人员缓存了这样的一些对象,这些对象有打开了的文件、 注册表或者网络链接,这是很危险的事情,当数据从cache中删除是,它们并没有立即释放(disposed),除非你释放了这些对象,否则将导致系统资源泄漏。每次这种对象实例因为过期或者其他原因造成的未释放资源就从缓存中被删除,都会造成它所占的资源泄漏。

You should never cache such objects that hold open streams, file handles, registry handles or network connections just because you want to save opening the resource 每次你需要它们,你最好使用一些静态变量或者进程内内存缓存来确保你能得到过期回调以便释放它们, Out of process caches or session stores do not give you expiration callback consistently. So, never store live objects there.

使用多个key缓存同一个数据

Sometimes you store objects in cache using the key and also by index because you not only need to retrieve items by key but also need to iterate through items using index. For example,

var someItem = new SomeClass();
myCache["SomeKey"] = someItem;
.
.
myCache["SomeItem." + index] = someItem;
.
.

If you are using in-memory cache, the following code will work fine:

var someItem = myCache["SomeKey"];
someItem.SomeProperty = "Hello";
.
.
.
var someItem = myCache["SomeItem." + index];
var hello = someItem.SomeProperty; // Returns Hello, fine, when In-memory cache
/* But fails when out of process cache */

The above code works when you have in-memory cache. Both of the items in the cache are referring to the same instance of the object. So, no matter how you get the item from cache, it always returns the same instance of the object. But in an out-of-process cache, especially in a distributed cache, items are stored after serializing them. Items aren’t stored by reference. Thus you store copies of items in cache, you never store the item itself. So, if you retrieve an item using a key, you are getting a freshly made copy of that item as the item is deserialized and created fresh every time you get it from cache. As a result, changes made to the object never reflects back to the cache unless you overwrite the item in the cache after making the changes. So, in a distributed cache, you will have to do the following:

var someItem = myCache["SomeKey"];
someItem.SomeProperty = "Hello";
myCache["SomeKey"] = someItem; // Update cache
myCache["SomeItem." + index] = someItem; // Update all other entries
.
.
.
var someItem = myCache["SomeItem." + index];
var hello = someItem.SomeProperty; // Now it works in out-of-process cache

Once you update the cache entry using the modified item, it works as the items in the cache receive a new copy of the item.

数据源更新或者删除后缓存没有做对应的处理

如下,内存缓存可以工作,但是如果是进程外/分布式缓存时,就会失败
var someItem = myCache["SomeItem"];
someItem.SomeProperty = "Hello Changed";
database.Update(someItem);
.
.
.
var someItem = myCache["SomeItem"];
Console.WriteLine(someItem.SomeProperty); // "Hello Changed"? Nope.

内存缓存i情况下可以很好的运行,但是在进程外/分布式缓存时就会失败,原因就是你修改了object但是你没有使用最后的object修改cache,缓存的存储的数据是copy,而不是original instance。

另外一个错误例子是从数据库中删除了数据,但却没有将cache中的数据进行删除

var someItem = myCache["SomeItem"];
database.Delete(someItem);
.
.
.
var someItem = myCache["SomeItem"];
Console.WriteLine(someItem.SomeProperty); // Works fine. Oops!

当你从数据库或者文件或者其他存储媒介中删除数据时,不要忘了从cache中删除对应的那些通过各种渠道保存的缓存数据

结论

缓存需要小心设计并了解要缓存的数据,否则当缓存被设计为分布式时,就不是性能下降的问题了,记着这些问题点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值