NHibernate的缓存管理机制

  1. 首先举一个比较经典的使用缓存的例子:
    html

  2. Database db = new Database();
    Transaction tx = db.BeginTransaction();
    try
    {
        //从缓存读取数据
        MyEntity entity = cache.Get<MyEntity>(entityId); 
        
        //缓存中没有时从数据库读取
        if (entity == null) entity = db.Get<MyEntity>(entityId);
        
        //对entity进行处理
    
        //entity的更新保存到数据库中
        updated = db.Update(entity); 
        
        //数据库更新成功,则更新缓存
        if (updated) cache.Put(entity); 
    
        //事务中的其余处理
    
        
        tx.Commit();
    }
    catch
    {
        tx.Rollback();
        throw;
    }

          上面的示例代码,是在一个事务性环境中使用缓存,存在更新操做(非只读缓存),若是这是一个共享缓存,这样的使用方式存在不少问题,好比说: 若是事务中的其余处理致使异常,数据库中对entity的更新能够被回滚掉,可是cache中的entity已经被更新了,若是不处理这样的状况后续从cache中读出的entity就是一个不正确的数据。因此,正确的使用缓存还须要考虑不少方面,确保数据的正确性、一致性。数据库

  3. nhibernate具备两个级别的缓存机制,一级缓存和二级缓存:c#

    相对于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中。
    二级缓存由于是共享缓存,存在并发更新冲突,但又必须保证二级缓存数据的正确性,所以处理机制就复杂得多。数组

  4. 二级缓存的主要结构:缓存

    接口职责:
    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()方法sass

    实体转换:服务器

    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)session

  5. 解决并发更新冲突并发

    NHibernate定义了3中缓存策略: 只读策略(useage="read-only")、非严格的读写策略(useage="nonstrict-read-write")和读写策略(useage="read-write")dom

    处理并发更新的结构:

          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方法还会比较实体的版本(使用乐观锁的状况)

相关文章
相关标签/搜索