浅析NHibernate缓存管理机制和相关问题

开发 后端
本文将讨论的是NHibernate缓存管理机制和相关问题,希望通过本文能对大家了解缓存机制有所帮助。

这里我们将讨论的是NHibernate缓存管理机制和相关问题,通过讲解数据库类似的情况,来让大家对NHibernate缓存有一个更深刻的理解。

缓存管理面临的主要问题

缓存作为一个数据中心,具备添加、更新、删除数据的操作,因此跟数据库类似,会存在事务性、并发情况下数据一致性等问题需要解决

使用缓存比较典型的方式如下面代码:

  1. Database db = new Database();  
  2. Transaction tx = db.BeginTransaction();  
  3. try 
  4. {  
  5.     //从缓存读取  
  6.     MyEntity1 entity1 = cache.Get("pk of entity1");   
  7.     //缓存中没有时从数据库读取  
  8.     if (entity1 == null) entity1 = db.Get("pk of entity1");  
  9.       
  10.     //对entity1进行处理  
  11.  
  12.     updated = db.Update(entity1); //entity1的更新保存到数据库中  
  13.     if (updated) cache.Put(entity1); //数据库更新成功,则更新缓存  
  14.  
  15.     //事务中的其他处理  
  16.  
  17.     tx.Commit();  
  18. }  
  19. catch 
  20. {  
  21.     tx.Rollback();  
  22.     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为例,实体缓存时的状态转换如上图

#T#

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分钟

原文标题:NHibernate的缓存管理机制

链接:http://www.cnblogs.com/RicCC/archive/2009/12/28/nhibernate-cache-internals.html

责任编辑:彭凡 来源: 博客园
相关推荐

2022-06-01 16:01:58

MySQL内存管理系统

2010-01-06 10:23:47

.NET Framew

2020-08-18 19:15:44

Redis内存管理

2013-09-29 15:11:46

Linux运维内存管理

2009-07-08 15:10:00

Servlet会话管理

2010-07-23 09:34:48

Python

2010-09-26 13:23:13

JVM内存管理机制

2010-12-10 15:40:58

JVM内存管理

2011-06-29 17:20:20

Qt 内存 QOBJECT

2009-09-23 17:48:00

Hibernate事务

2016-09-06 22:05:41

HttpCookieWeb

2020-11-08 14:32:01

JavaScript变量内存管理

2016-10-09 14:41:40

Swift开发ARC

2022-02-28 10:25:17

Python参数传递拷贝

2009-08-03 18:35:51

ASP.NET数据缓存

2019-01-23 17:08:52

Python内存管理RealPython

2009-09-25 12:59:53

Hibernate事务

2021-12-15 06:58:27

Go多版本管理

2011-08-18 13:28:35

Objective-C内存

2010-04-08 15:43:28

Oracle缓冲块
点赞
收藏

51CTO技术栈公众号