Richie

Sometimes at night when I look up at the stars, and see the whole sky just laid out there, don't you think I ain't remembering it all. I still got dreams like anybody else, and ever so often, I am thinking about how things might of been. And then, all of a sudden, I'm forty, fifty, sixty years old, you know?

NHibernate的缓存管理机制 - NHibernate 2.1.2

缓存管理面临的主要问题
缓存作为一个数据中心,具备添加、更新、删除数据的操作,因此跟数据库类似,会存在事务性、并发情况下数据一致性等问题需要解决
使用缓存比较典型的方式如下面代码:
Database db = new Database();
Transaction tx 
= db.BeginTransaction();
try
{
    
//从缓存读取
    MyEntity1 entity1 = cache.Get<MyEntity1>("pk of entity1"); 
    
//缓存中没有时从数据库读取
    if (entity1 == null) entity1 = db.Get<MyEntity1>("pk of entity1");
    
    
//对entity1进行处理

    updated 
= db.Update(entity1); //entity1的更新保存到数据库中
    if (updated) cache.Put(entity1); //数据库更新成功,则更新缓存

    
//事务中的其他处理

    tx.Commit();
}
catch
{
    tx.Rollback();
    
throw;
}
上 面的示例代码,是在一个事务性环境中使用缓存,存在更新操作(非只读缓存),如果这是一个共享缓存,这样的使用方式存在很多问题,比如说: 如果事务中的其他处理导致异常,数据库中对entity1的更新可以被回滚掉,但是cache中的entity1已经被更新了,如果不处理这样的情况后续 从cache中读出的entity1就是一个不正确的数据

所以,要正确的使用缓存,必须有一个完善的方案,充分考虑事务、并发等状况,确保数据的正确性、一致性

NHibernate 2个级别的缓存机制
   
相对于session来说,一级缓存是私有缓存,二级缓存是共享缓存
session加载实体的搜索顺序为: 1. 从一级缓存中查找;2. 从二级缓存中查找;3. 从数据库查找
一级缓存在事务之间担当了一个隔离区域的作用,事务内对实体对象的所有新增、修改、删除,在事务提交之前对其他session是不可见的,事务提交成功之后批量的将这些更新应用到二级缓存中
这样的2级缓存机制能够在很大程度上确保数据的正确性(比如前面示例代码中事务失败的情况下,就不会将数据更新到二级缓存中,防止了二级缓存出现错误的数据),以及防止ReadUncommited等其他一些事务一致性问题

内部实现上,对一级缓存的管理很简单,所有已加载的实体(以及已经创建proxy但未加载的实体等)都被缓存在持久化上下文(NHibernate.Engine.StatefulPersistenceContext)中
待 新增、更新、删除的实体,使用3个列表缓存起来,事务提交的时候将他们应用到数据库和二级缓存中(Flush调用或者因为查询等导致的 NHibernate自动执行的Flush操作也会将他们应用到数据库,但不会应用到二级缓存中,二级缓存只在事务提交成功之后才更新)
NH1.2中这3个列表维护在SessionImpl中,NH2.0以后添加的新功能特性以及代码本身的重构动作相当多,这3个列表维护在NHibernate.Engine.ActionQueue中

二级缓存因为是共享缓存,存在并发更新冲突,但又必须保证二级缓存数据的正确性,因此处理机制就复杂得多。下面是详细的二级缓存处理机制

二级缓存的主要结构
主要接口:
   
接口职责:
ICache: 统一的缓存存取访问接口
ICacheProvider: 工厂类、初始化类,用于创建ICache对象,启动时对cache server或组件进行初始化,退出时对cache server或组件进行必要的退出处理等
处理过程:
1. 配置文件中指定ICacheProvider的实现类
2. SessionFactory启动时创建ICacheProvider对象,执行ICacheProvider.Start()方法,并为每一个cache region创建一个ICache对象
3. 整个运行过程中,NHibernate可以使用SessionFactory创建的ICache完成缓存的存取操作
4. SessionFactory关闭时调用ICacheProvider.Stop()方法

实体状态的转换:

以memcached为例,实体缓存时的状态转换如上图
1. CacheEntry表示一个需要存储到缓存中或者从缓存中返回的对象
    CacheEntry中包含拆解后的实体属性值(DisassembledState,object[]类型,数组中是每个属性的值)、实体的版本(乐观 锁时使用)、类型名称。采用这样的处理方式,我们定义的domain对象就不需要实现Serializable接口,也可以被序列化存储到缓存 中
    对于primitive type的实体属性,拆解和组装过程没有特殊的处理;对于composite component、one-to-one、one-to-many的collection等实体属性,分解之后在DisassembledState中 存放的是owner(即当前被缓存的实体对象)的id值,组装过程中根据这个id值去取相关的对象设置到这个属性上(可能从一级缓存、二级缓存,或者数据 库加载,依赖于具体的设置和运行时的状态)
2. CacheItem用于解决并发更新二级缓存时的数据一致性问题(不考虑这个问题的话,直接将CacheEntry存到缓存中就可以了),主要是对soft lock机制的处理,后面详细介绍
3. 将CacheItem转换成DictionaryEntry的处理,是由NHibernate.Caches.Memcache进行的,完全是一个多余的处理
    NHibernate使用规则 [完整的类名#id值] 生成cache key,NHibernate.Caches.Memcache会在NHibernate生成的key前面再添加上 [region名称@](如果类的hbm文件中没有设置region名称,默认region为完整的类名,这样完整类名会在cache key中出现2次)
    memcached的key最长只能是250个字符,NHibernate.Caches.Memcache在cache key超过250字符时,取key的hash值作为新的memcached key值,因为这样会存在hash冲突,所以NHibernate.Caches.Memcache构造一个DictionaryEntry对象(原 key值的MD5作为DictionaryEntry的key值,被缓存的对象作为value),将 DictionaryEntry存到memcached中。从缓存get对象时,NHibernate.Caches.Memcache对返回的 DictionaryEntry的key值再做一次比较,排除掉hash冲突的情况
    这样的方式使用memcached,效率上太浪费了。一不留神,完整的类名就会在缓存数据中出现4次!
    基于NHibernate的机制和memcached的特点,可以考虑使用cache region来区分不同的memcached集群,比如说用A、B 2台服务器作为只读缓存,region取名为readonly_region;C、D、E 3台服务器作为读写缓存,region取名为readwrite_region
4. 从DictionaryEntry到Memcached Server这段处理由Memcached.ClientLibrary完成,关于Memcached.ClientLibrary的分析,参 考memcached client - memcacheddotnet (Memcached.ClientLibrary)

解决并发更新冲突
NHibernate定义了3中缓存策略: 只读策略(useage="read-only")、非严格的读写策略(useage="nonstrict-read-write")和读写策略(useage="read-write")

处理并发更新的结构
   
ICacheConcurrencyStrategy聚 合了一个ICache对象,NHibernate操作缓存时不是直接使用ICache对象,而是通过ICacheConcurrencyStrategy 完成,这样确保系统对二级缓存的操作,都是在特定的缓存策略下进行的
ICacheConcurrencyStrategy和ICache接口的语义有差别,ICache纯粹是缓存的操作接口,而ICacheConcurrencyStrategy则与实体的状态变化相关

ICacheConcurrencyStrategy的语义
Evict: 让缓存项失效
Get, Put, Remove, Clear: 与ICache的相关方法相同,纯粹的缓存读取、存储等操作
Insert, AfterInsert: 新增实体时的方法,实体新增到数据库之后会执行Insert方法,事务提交后会执行AfterInsert方法。这些方法中如何处理二级缓存,由具体的缓存策略确定
Update, AfterUpdate: 更新实体时的方法,实体修改update到数据库之后会执行Update方法,事务提交后会执行AfterUpdate方法。这些方法中如何处理二级缓存,由具体的缓存策略确定
Lock, Release: 这2个方法分别对缓存项进行加锁、解锁。语义上,事务中开始更新实体时对缓存项执行Lock方法,事务提交后对缓存项执行Release方法,在这些方法中如何处理二级缓存由具体的缓存策略确定

在 前面实体状态转换的图中,CacheEntry到CacheItem的转换由ICacheConcurrencyStrategy接口完 成,CacheItem只被ICacheConcurrencyStrategy使用,NHibernate内部其他需要与缓存交互的地方均使用 CacheEntry和ICacheConcurrencyStrategy接口

ReadOnly策略
运用场景为,数据不会被更新,NHibernate不更新二级缓存的数据。采用只读策略的实体不能执行update操作,否则会抛出异常,可以执行新增、删除操作。只读策略只在实体从数据库加载后写到缓存中

UnstrictReadWrite策略
运用场景为,数据会被更新,但频率不高,并发存储情况很少
采用该策略的实体,新增时不会操作二级缓存;更新时只是简单的将二级缓存的数据删除掉(Update, AfterUpdate方法中都会删除二级缓存数据),这样期间或者后续的请求将从数据库加载数据并重新缓存
因为更新过程没有对缓存数据使用lock,读取时也不会进行版本检查,因此并发存取时无法保证数据的一致性,下面是一个这样的示例场景:
   
1, 2: 请求1在事务中执行更新,NH更新数据库并从二级缓存删除该数据
3: 某些操作(例如ISession.Evict)导致请求1的一级缓存中该数据失效
4, 5: 请求2从数据库加载该数据,并放入二级缓存。因为请求2在另外的事务上下文中,因此加载的数据不包含请求1的更新
6: 请求1需要重新加载该数据,因为一级缓存中没有,因此从二级缓存读取,结果读到的将是一份错误的数据

ReadWrite策略
运用场景为,数据可能经常并发更新,NHibernate确保ReadCommitted的事务隔离级别,如果数据库的隔离级别为RepeatableRead,该策略也能基本保证二级缓存满足RepeatableRead的隔离级别
NHibernate通过使用版本、timestamp检查、soft lock等机制实现这一目标

soft lock的原理比较简单,假如事务中需要更新key为839的数据,首先创建一个soft lock对象,用839这个key存到cache中(如果cache中原来已经用839的key缓存了这个数据,也直接用soft lock覆盖他),然后更新数据库,完成事务的其他处理,事务提交之后将id为839的实体对象再重新存入cache中。事务期间其他所有从二级缓存读取 839的请求都将返回soft lock对象,表明二级缓存中这个数据已经被加锁了,因此转向数据库读取
   
ReadWriteCache.ILockable为soft lock接口,CacheItem和CacheLock两个类实现了这个接口

更新数据时的处理步骤
   
1: 更新操作前先锁定二级缓存的数据
2,3: 从二级缓存取数据,如果返回的是null或者CacheItem,则新建一个CacheLock并存入二级缓存;如果返回的是一个CacheLock,则表明有另外的事务已经锁定该值,将并发锁定计数器增1并更新回二级缓存中
4: 返回lock对象给EntityAction
5, 6, 7: 更新数据库,完成事务的其他处理,提交事务。ReadWriteCache的Update不做任何处理
8: 事务提交后执行ReadWriteCache的AfterUpdate方法
    先从二级缓存读取CacheLock对象,如果返回null说明锁已经过期(事务时间太长造成)
    如果锁已经过期,或者返回的CacheLock已经不是加锁时返回的那个(锁过期后又被其他线程重新加锁了),则新建一个CacheLock,设为 unlock状态放回二级缓存,结束整个更新处理
    如果CacheLock为并发锁状态,则将CacheLock并发锁计数器减一,更新回二级缓存,结束整个更新处理
    如果不是上面这些情况,则说明期间没有并发更新,将新的实体状态更新到二级缓存(锁自然被解除掉了)

一 旦发生并发更新,并发的最后一个事务提交之后,NHibernate也不会将实体重新存入二级缓存,此时在二级缓存中存储的是一个unlock状态的 CacheLock对象,在这个CacheLock过期以后,实体才可能被重新缓存到二级缓存中。采用这样的处理方式,是因为并发事务发生 时,NHibernate不知道数据库中哪一个事务先执行、哪一个后执行,为了确保ReadWrite策略的语义,强制这段时间内二级缓存失 效

ReadWriteCache的Get方法,除了在二级缓存的数据被锁定时将返回null之外,还会将缓存项的时间戳与请求线程的事务时间进行比较,也可能返回null,使得请求转向数据库查询,由数据库保证事务隔离级别
而put方法还会比较实体的版本(使用乐观锁的情况)

看源代码时,Timestamper类是一个时间戳与计数器结合的产物,在时间上精确到毫秒,每毫秒内采用1-4096的一个计数器,增量分配。NHibernate.Caches.MemCache将ReadWriteCache的二级缓存锁超时时间设置为0xea60000,换算过来就是1分钟

posted on 2009-12-28 00:03 riccc 阅读(...) 评论(...) 编辑 收藏

导航

News