文档章节

NHibernate的缓存管理机制

彩色铅笔
 彩色铅笔
发布于 2014/07/08 17:51
字数 3305
阅读 56
收藏 0
  1. 首先举一个比较经典的使用缓存的例子:

  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具有两个级别的缓存机制,一级缓存和二级缓存:

    相对于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()方法

    实体转换:

    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)

  5. 解决并发更新冲突

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

© 著作权归作者所有

彩色铅笔
粉丝 4
博文 7
码字总数 7969
作品 0
朝阳
程序员
私信 提问
加载中

评论(2)

彩色铅笔
彩色铅笔 博主

引用来自“龙马行空”的评论

理论太多了,有没有代码?
等我过两天再上代码
龙马行空
龙马行空
理论太多了,有没有代码?
NHibernate从入门到精通系列(1)——NHibernate概括

内容摘要 NHibernate简介 ORM简介 NHibernate优缺点 一、NHibernate简介 什么是?NHibernate?NHibernate是一个面向.NET环境的对象/关系数据库映射工具。对象/关系数据库映射(object/relati...

长平狐
2012/06/11
576
0
NHibernate从入门到精通系列(2)——NHibernate环境与结构体系

内容摘要 NHibernate的开发环境 NHibernate的结构体系 NHibernate的配置 一、NHibernate的开发环境 NHibernate的英文官方网站为:http://nhforge.org/ NHibernate目前最新的版本是3.0.0.GA,...

长平狐
2012/06/11
845
0
Spring.NET实用技巧1——基于Prevalence下的NHibernate二级缓存使用技巧

什么是二级缓存? NHibernate的Session提供了一级缓存。每个Session,对同一个id进行两次Load,不会发送两条SQL语句给数据库,但是Session一但关闭,一级缓存也就失效了。 与Session相对的是...

长平狐
2012/06/11
404
0
比较 NHibernate 和实体框架

葡萄牙的一位开发者 Ricardo Peres 最近发布了一篇文章,以看起来无偏见的形式对领先的两种 .NET ORM:NHibernate 和实体框架进行了比较。 我们建议考虑使用这两种框架的人都应该读下他的文章...

墙头草
2012/06/18
3.6K
4
NHibernate 5.0 发布,对象关系映射解决方案

NHibernate 是一个基于.Net 的针对关系型数据库的对象持久化类库。Nhibernate 来源于非常优秀的基于Java的Hibernate 关系型持久化工具。NHibernate 从数据库底层来持久化你的.Net 对象到关系...

周其
2017/10/16
1K
6

没有更多内容

加载失败,请刷新页面

加载更多

简单了解 mvc 、mvp 与 MVVM 区别

mvc - 划分三个角色: 用户操作 view 层与 controller 层 mvp view 与 model 不发生联系,用户直接操作 p 层 mvvm view 与 model 双向绑定 react 中 v = f(s) 状态 到 视图的映射。改变状态就...

lemos
14分钟前
2
0
Python爬虫新手教程:手机APP数据抓取 pyspider

1. 手机APP数据----写在前面 继续练习pyspider的使用,最近搜索了一些这个框架的一些使用技巧,发现文档竟然挺难理解的,不过使用起来暂时没有障碍,估摸着,要在写个5篇左右关于这个框架的教...

计算机编程
15分钟前
2
0
巨杉Talk | 拒绝数据碎片化,原生分布式数据库灵活应对数据管理需求

2019年7月19-20日,以“运筹帷幄,数揽未来”为主题的DAMS中国数据智能管理峰会在上海青浦区成功举办。在DAMS峰会上,巨杉数据库为大家带来了题为“云架构下的分布式数据库设计与实践”的主题...

巨杉数据库
22分钟前
1
0
北京课工场教育科技公司喜获第八届中国软件杯企业突出贡献奖

百舸争流,奋楫者先;千帆竞发,勇进者胜。7月18 日, 第八届“中国软件杯” 大学生软件设计大赛决赛(简称“大赛”) 及颁奖典礼在江苏南京软件谷科创城圆满落下帷幕。课工场作为赛事支持单...

IFTNews
35分钟前
2
0
C++ *与&

*获取地址的值 &获取变量地址

colin_86
42分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部