缓存管理面临的主要问题

📅 2026/7/6 4:06:10
缓存管理面临的主要问题
缓存作为一个数据中心具备添加、更新、删除数据的操作因此跟数据库类似会存在事务性、并发情况下数据一致性等问题需要解决使用缓存比较典型的方式如下面代码:Database db new Database();Transaction tx db.BeginTransaction();try{//从缓存读取MyEntity1 entity1 cache.GetMyEntity1(pk of entity1);//缓存中没有时从数据库读取if (entity1 null) entity1 db.GetMyEntity1(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中包含拆解后的实体属性值(DisassembledStateobject[]类型数组中是每个属性的值)、实体的版本(乐观 锁时使用)、类型名称。采用这样的处理方式我们定义的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 keyNHibernate.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_regionC、D、E 3台服务器作为读写缓存region取名为readwrite_region4. 从DictionaryEntry到Memcached Server这段处理由Memcached.ClientLibrary完成关于Memcached.ClientLibrary的分析参 考memcached client - memcacheddotnet (Memcached.ClientLibrary)解决并发更新冲突NHibernate定义了3中缓存策略: 只读策略(useageread-only)、非严格的读写策略(useagenonstrict-read-write)和读写策略(useageread-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和ICacheConcurrencyStrat