关于缓存的思考

在评价一个系统的时候,性能指标是很重要的,那么在当前J2EE的系统开发当中,如何来提高系统的性能呢?我觉得应该从对象管理入手,从对象的生命周期开始。虽然大家可能会说,Java有垃圾收集器,我们的对象的生命周期不需要我们自己管理,但是如果要是真的过分依赖java语言本身的特性,那么我相信,系统的性能肯定好不到哪去。

下面我主要说一下对于缓存的理解。在说 缓存之前,我不得不说一下面向对象的设计,可能有些人认为,为什么 缓存会与面向对象的设计扯上关系,其实这就是 缓存的关键。首先设想一下,如果开发系统的过程中,都是采用面向过程,面向数据库的思维编程,每一次业务操作,我们都是调用通过数据库操作来完成,这其实就是POEAA中的事务脚本,只适合一些简单的系统的开发,或者一个项目中,比较简单模块的开发,对于复杂的模块,更好的方式就是采用面向对象的方法来进行开发。

好了,说到了面向对象的设计问题,至于这个问题已经有很多书籍以及很多人讨论了很多年了,就我个人来说,我觉得采用DDD建模是目前比较适合的一种方式。DDD中涉及到得每种模式或者说是每一种模型元素对于 对象设计来说都是很重要的,而对象模型的设计又对缓存的设计非常重要,下面我说说我的想法:

首先我说一下关于聚合的问题,为什么说聚合对于 缓存非常重要呢?这其实涉及到了一种控制访问的问题,一个聚合根控制了对整个聚合的访问,要想访问聚合里的对象必须要通过聚合的根。 好了,我们以一个实例来说话,比如jdon中的Forum对象的设计,在jdon中有ForumState对象,Forum对象是聚合的根,是一个实体模型,而ForumState是一个值对象,并且是属于Forum这个聚合根的子对象,我们把ForumState对象从Forum对象分离出来,好处主要有两个,从事务的角度来说,当我们更新ForumState对象的时候,不用锁住Forum对象,从 缓存设计的角度来说,当我们更新ForumState对象的时候不用刷新Forum对象的 缓存,因为Forum不是经常改变的,所以不必要因为经常改变属性的改变而改变。那么具体怎么来设计呢?我们可以这样做,在ForumState对象中设置一个状态位,表示它的状态是否已经改变,当Forum状态发生改变,比如有人创建新的帖子或者回复了帖子后,我们可以设置这个状态位为true,表示状态已经改变,这样当再次从 缓存中取得Forum时,查看状态位,如果发现已经变化了,那么就重新从数据库加载ForumState。当然要想达到这种效果,我们一定要设计好聚合,所有对子对象的访问都要通过聚合的根,比如所有对ForumState对象的访问都要经过Forum对象,并且要保证所有的数据库操作,都首先从统一的缓存入口进行,这样保证了整个系统中用的是同一个 缓存,大家操作的所有对象都是同一个 缓存中的对象。所以这里也给出了一条对象设计的提示,将经常变化的熟悉和不经常变化的属性分开,并且将经常变化的属性独立出去,作为聚合根的 一个子对象,这样做到变和不变分离,不仅有利于高内聚,而且有利于事务的控制和 缓存的更新。

下面我说一下关于Jdon缓存的一些想法,在jdon的Forum和ForumState对象的 缓存设计中,我看到了Forum有个embedded位表示对象中嵌套的对象是否已经加载完整,当得到Forum对象后,如果发现ForumState已经加载了,那么就直接返回,当有论坛有回帖或者是创建帖子的时候,就重新加载ForumState.还有一种方法也可以这么实现。可以在ForumState对象中增加状态是否改变的标志,那么当我们创建一个主题或者回复帖子的时候,首先从系统 缓存的统一入口处拿到Forum,然后通过Forum得到ForumState对象,并设置ForumState状态改变标记为true,这样以来,当下次从 缓存取出Forum对象的时候,检查两个标记:embedded和 stateChanged,如果embeded为true,但是stateChange也为ture,那么我们重新加载ForumState对象,然后设置stateChanged为false.当然了这样需要对stateChanged的操作同步。这样设计的好处就是将对模型对象的操作都封装在模型对象内部,便于多线程环境下的并发访问控制。因为从把所有对对象的操作都封装起来,方便多线程下的并发同步问题。

上面是关于Repository在对象建模中的作用,下面我也说说关于工厂的作用,既然是工厂,那么它肯定要生产东西出来,但是它不能随便乱生产,它生产出来的应该是整个聚合的根,并且要保证这个聚合的不变量受到保护,这样通过工厂提供一个集中的模型创建的访问点,也方便了控制访问。要设计一个好的工厂,我们首先需要设计一个好的对象模型,分辨出合理的聚合,这样工厂才会发挥真正的效力。

最后既然说到了 缓存,还有一点需要注意,那就是这个 缓存的范围,我这里说的 缓存是全局的 缓存,是一个application级别的 缓存。我个人是比较反对在Httpsession中保存大量数据的,这样当用户增多的情况下,比较会浪费很多的内存,浪费性能。所以我们更应该需要的是全局的 缓存,这样底层的缓冲框架我们还可以采用分布式的 缓存系统,这样以来我们的系统在集群环境下也免去了一些session failover的开销。这其实也是一种SNA架构的思想。

[该贴被xmuzyu于2009-04-25 10:34修改过]
[该贴被admin于2009-04-26 20:03修改过]

写得很好,你已经明白了Evans DDD魅力重点:聚合根和不变性,这个不变性反应了领域需求的内聚性,而万事万物之所以成为那个事物,必然有其内聚性,算法无论再复杂强大,它也是一个有内聚性的事物(这就是哲学观点)。

所以,抓住聚合根和不变性,就是抓住需求的本质。这是Evans DDD巨大贡献,它指明了什么是需求本质,这是一个通用的原则,真的会让所有软件编制都很简单。

软件在具体实现时又和缓存有关,因为对象是内存中对象,这是我们默认的语义前提,而对象不但可以在一台机器内存中运行,而且通过网络在多台机器之间传输并运行,这种无边界的灵活思路无疑为我们为各种问题提供很大平台基础,没有边界约束,打破了那些所谓数据库概念定义的边界。

需求本质 ---> 聚合根 不变性 ----> 缓存和内存计算 ---->云计算,这是一个体系,一个新世界。

抓住聚合根,就可以很容易处理内存中对象,这些都是一通百通,一顺百顺的一个体系,没有遨游其中的人是很难体会到编程的乐趣。

编程从此是思维逻辑的自然延伸,所有人都会死,苏格拉底是人,所以,苏格拉底也会死,当我们的DDD水平上升到一定程度,我们可以程序语言很自然表达这个逻辑,简洁易懂,就象另外一种人类语言一样,抑或是外星人语言。

所以,大部分现在的人是无法懂外星人语言,这是情有可原,但是我们不能掩盖这样事实。

Declarative programming声明性编程
http://www.jdon.com/jivejdon/thread/36035.html

[该贴被admin于2009-04-26 20:39修改过]

>>需求本质 ---> 聚合根 不变性 ----> 缓存和内存计算 ---->云计算

非常精辟的一句话。我觉得:业务对象建模+业务对象缓存==优良的系统性能解决方案。

至于业务对象建模来说,DDD中的每一种模式都不能少,比如仓库,工厂,聚合,首先聚合使得对象之间的关系更加清晰,同时也使得对象的封装更加的严实,这样带来的好处就是容易控制其生命周期。仓库屏蔽了数据库,同时也屏蔽了缓存,我们在用对象的时候,从仓库中拿,至于这个对象原来是在缓存还是在数据库我们不管,反正仓库保证给我的对象是一个完整的,满足不变量约束的对象,由此以来仓库管理了对象在创建以后的生命周期,对象完全是由我们系统设计者自己控制,而不是用完了就扔掉,这样多浪费资源,呵呵,因为自己用完了,别人说不定还用得到嘿嘿(假如自己和别人都是不同的线程)。


[该贴被xmuzyu于2009-04-26 21:44修改过]

>仓库屏蔽了数据库,同时也屏蔽了缓存,我们在用对象的时候,从仓库中拿,至于这个对象原来是在缓存还是在数据库我们不管,反正仓库保证给我的对象是一个完整的,满足不变量约束的对象

我的理解和你不同,仓储Repository只是屏蔽了数据库,而不是缓存,因为缓存就是domain对象的最本质的生存空间,如果没有缓存内存,那么对象object何以存在呢?(没有内存就没有地址空间,对象就无法加载,这是底层机制)

我们控制聚合根和聚合边界内对象的生命周期,就是控制它们在缓存这个空间中的一致性,DDD中只是谈到了一致性,但是这个隐含的场景是唯一性,整个内存空间只有一个聚合根和气边界,不能存在两个以上,否则每个请求都创建自己的聚合根,请求完就扔了,每次请求来再重新构建聚合根和边界很多对象,这会很慢哦,性能很差,这还是依赖数据库的思维哦。

聚合根和边界的子对象们一直存在内存中,进行业务计算,保存业务计算状态和结果,同时数据库中也有一份这些对象中数据和关系的备份,但只是数据备份,是压扁了的对象数据,就像我们将折叠椅子保存到仓库中,拿出来用时,必须把折叠椅展开才能用;聚合根和边界的子对象必须在内存中展开才能用哦。

具体可以看看JiveJdon3.6的代码,在messageKenerl中,数据库持久只是一个单独的动作,在某个时刻对于修改内容来说,是否持久保存到数据库,根本不影响我们业务,见updateMessage方法,你可以屏蔽了messageTransactionPersitence这个动作,当你修改帖子之后,大家还是能够看到帖子修改后的内容,因为我们修改了内存缓存中这个帖子,但是没有保存。

如果这个时候服务器当机,当然我们的修改结果,下次重启就没有了,那么我们还是需要messageTransactionPersitence持久的,但是我们的业务不再依赖它,它可以放在一个异步机制或抛新的线程来执行,与我们业务无关,只是一个save保存动作,就像我们编辑文本打字,经常按ctrl-s保存一样。

呵呵,可能我的表述有问题,我的意思是仓库控制了对于缓存的访问,系统中的所有的仓库都引用全局的缓存入口,而我们获得对象是从仓库里面获取,这样有一个统一的地方来管理缓存,不然我们系统中的其它地方还要引用全局的缓存入口去获得对象,这样有点散乱呵呵。

>>可以屏蔽了messageTransactionPersitence
呵呵,屏蔽这个可以,但是如果下一个请求访问的时候,缓存中的对象被清除了,那么这个请求就看不到最新的结果了呵呵。


呵呵,如果缓存中的对象在清除的时候能得到某种基于事件的通知,那么当对象被清除的时候,将其持久化,这样我们甚至不需要每次更新的时候都持久对象到数据库,直接操作缓存中的对象,当然这就要求有一个经过严格并发测试的可靠的缓存系统为我们的系统服务呵呵。

你的意思是仓库控制了缓存访问?应该是工厂吧,呵呵,我大概知道你一点意思,其实在service里也可以直接从缓存中获得某个聚合根对象。

我的意思是:Repository重点是将折叠的椅子还原成可以坐的椅子,而与缓存控制关系不大。

>那么当对象被清除的时候,将其持久化,这样我们甚至不需要每次更新的时候都持久对象到数据库,

你说对了,我们现在就需要这样的新式框架,通览当前Java开源领域,没有看到这样机制的持久层框架。如果有兴趣者,不妨做个这样框架,可以在Hibernate上加工,很先进哦。


[该贴被banq于2009-04-28 14:19修改过]

>>我的意思是:Repository重点是将折叠的椅子还原成可以坐的椅子,而与缓存控制关系不大。

恩,我理解banq老师的意思了,因为我在一个项目中采用了通过仓库控制了缓存的访问,service层获得聚合也要从仓库中取,banq 老师的意思是通过工厂来控制呵呵,只要DDD的精髓掌握了,至于控制方面,可以灵活一点呵呵。


ps:我这个用DDD开发的项目是一个和同学创业的项目,用了自己的想法来开发,用了ehcache做为缓存系统。并且我们系统session里面几乎没有数据,很多的数据我们都是放到了全局的缓存里。

至于公司,悲哀的是国内用的少啊,比如最近我公司的建行项目组,全部都是采用过去传统的开发方式,业务对象是由数据库表导出的,不过这也有原因,毕竟建行已经积累了很多年,要想把人家积累N年的平台换掉不切实际呵呵。再者来说,建行有钱啊,服务器,应用服务器,数据库都用几乎世界上最好的,没办法,性能不好,垂直伸缩。

>,建行有钱啊,服务器,应用服务器,数据库都用几乎世界上最好的,没办法,性能不好,垂直伸缩

其实软件不具有可伸缩性,就是花再多的钱也是无济于事,这个道理已经被国外很多年的发展证明,所以,才有Java在世界五百强大型企业中应用和普及。因为这些企业IT主管已经认识到:硬件可以用钱买的,如果软件架构设计做得不好,造成的损失将是金钱无法弥补的。

特别是业务模型,DDD谈到核心领域模型是一个公司的核心资产,甚至是核心竞争力,软件也是资产意思就是体现在核心领域模型上,这个是你公司通过时间迭代对业务深入认识升华后的结果,是别人无法通过钱买到的。

我们有的人分不清楚核心领域和通用子领域,以为所有软件都是资产,其实通用领域的东西可以通过购买 使用开源或者外包形式实现,但是核心领域是自己的。

所以,对于一个像建行这样的大公司大项目,性能不是重要的,就象你说可以垂直伸缩,甚至可以用银河计算机,国家的钱,但这些都不是重点,重点是软件核心领域。

如果我们建立了核心领域模型,核心领域的核心就是一个个聚合根和边界,那么就自然能够使用内存缓存计算,就自然有好的水平伸缩性,这是一个自成体系的,为什么大道至简的路不走,要走阳关小道呢?这其实就是知不知道的问题,知不知道软件世界有这么一个好体系,现在很多人都不知道,如果知道,他肯定会选择。

我看到有Repository中进行缓存和DB同步方法,如下,Repository应该更侧重DB,而缓存是无处不在的,这是想表达的意思,可能和你表达意思不是在讲同一件事情,角度不同:
public interface Repository
{
/** Syncronizes the database.
* <p> Can be used, for example, to save the current memory image of the DB. */
public void sync();

/** Returns the numbers of users in the database.
* @return the numbers of user entries */
public int size();

/** Returns an enumeration of the users in this database.
* @return the list of user names as an Enumeration of String */
public Enumeration getUsers();

/** Whether a user is present in the database and can be used as key.
* @param user the user name
* @return true if the user name is present as key */
public boolean hasUser(String user);

/** Adds a new user at the database.
* @param user the user name
* @return this object */
public Repository addUser(String user);

/** Removes the user from the database.
* @param user the user name
* @return this object */
public Repository removeUser(String user);

/** Removes all users from the database.
* @return this object */
public Repository removeAllUsers();

/** Gets the String value of this Object.
* @return the String value */
public String toString();

}


[该贴被banq于2009-04-28 20:37修改过]

呵呵,多谢板桥老师。我以前的思路是缓存管理器封装了缓存,仓库引用缓存管理器,然后service里面取数据还是要通过仓库去取数据,因为仓库要判断缓存中是否存在所请求的对象,如果存在就直接返回,如果不存在就从DB里面取出后,放回cache里,并且仓库还要保证放到cache里的对象是一个完整的聚合根。经过您提醒,我发现其实可以将判断对象是否在缓冲的职责从仓库剥离出来,分配给另外一个代理类,这个代理类判断对象是否在缓存中,如果在那么就直接返回,如果不在的话就通过仓库从DB取出来。此时仓库主要负责从DB拿出来的对象是完整的,而这个代理类要保证缓存中的对象是完整的,比如一个对象的局部状态发生改变,那么需要刷新状态的时候,就通过这个代理类来完成,这样仓库保证了从DB拿来的对象是完整的,而代理类保证了已经在缓存中的对象是完整的呵呵。

呵呵,关键以前我那个项目是用仓库代替了dao,所以将判断对象是否在缓存的功能也给了仓库,仓库还要保证从DB拿来的对象的完整性,而现在可以细分一下呵呵。
[该贴被xmuzyu于2009-04-28 22:38修改过]

缓存控制处理还是一个细节,我的经验过去是将缓存和数据库作为一个来源,就象另外一个帖子讨论,将数据库读写分开,分成两个数据库,只不过我们用缓存替代另外一个读数据库,保留写数据库事务,这样打破垂直伸缩的局限,将水平伸缩和垂直伸缩有机结合在一起。

原来是把缓存简单当作一个数据源,后来发现这个思路又受了数据库的影响,缓存不是数据库,没有一个定型边界的限制,缓存是对象形影不离的,是对象的空间 水和氧气。

对象生存在缓存内存中,就象我们当前时空一样,有时我们会将空间位置作为分辨一个实体的标识,比如两个一模一样的桌子在我们面前,但是我们可以分辨它们,因为我们已经将空间位置作为桌子的标识。

同样道理,两个对象在内存中,之所以不同,从技术角度讲它们的内存地址不一样,但从我上面时空角度来讲,就更加本质,就和实体标识这些知识融会贯通,自成一体。

所以,从将读写数据库分成两个数据库的性能优化概念出发,将读的数据库用缓存内存的对象来替代,不但解决性能问题,而且无缝和需求分析,领域建模对接,多么美妙的事情。

大数据性能优化:
http://www.jdon.com/jivejdon/thread/36068.html

哈哈,又见很精典的讨论贴,学习了,真的学习了!
不过小弟在此也有个问题和各位请教一下:
在楼主与banq老师的讨论中除了谈到仓库和工厂、缓存,但没谈到池这东西,想知道池应该是做为工厂呢还是做为仓库或者是其他的?

>>所以,从将读写数据库分成两个数据库的性能优化概念出发,将读的数据库用缓存内存的对象来替代,不但解决性能问题,而且无缝和需求分析,领域建模对接,多么美妙的事情。

同意。并且缓存还有一个优点就是:它可以更具系统运行的情况动态的来适应系统的要求,缓存主动迎合系统的要求。比如:缓存的置换算法一般情况下都采用LRU,这样以来,缓存中的对象其实是经常被用到得,而那些不经常使用的,缓存系统已经控制将其移除了,所以从这个角度来说,缓存就好像系统的母亲一样,能根据系统这个孩子的运行时状态,动态的满足孩子的要求。

不过根据我在缓存实践来说,我发现要想实现缓存,面向对象的设计是一个挑战,如果对象的状态没有封装好,这样系统中到处都充斥着改变对象状态的代码,根本没有办法控制缓存中对象状态的并发访问控制。所以要想用好缓存,我们首先要设计好对象模型,能用private的地方尽量用private,这样的目的是封装。能用final的地方尽量用final,因为final类型的变量,JVM可以保证初始化安全性,这样更加容易做到安全的发布。

ps:最近越来越发现<<java 并发编程实践>>经典了。

>>在楼主与banq老师的讨论中除了谈到仓库和工厂、缓存,但没谈到池这东西,想知道池应该是做为工厂呢还是做为仓库或者是其他的?


呵呵,个人观点是:池不需要我们手的的实现,因为容器帮我实现了,这也是Java发展到今天的格局。比如目前的轻量级的IOC容器或者EJB容器其实底层都有池的概念,它的目的就是通过对象的复用来提升性能。并且池里面的对象有个特点:那就是没有标识,它不区分谁是谁,采用池的好处就是可以限制并发的线程数量,从而避免因为一时线程数的突然增加导致系统崩掉。

上面是有关的池的,至于池,它里面主要保存的是无状态的功能性的,不需要考虑线程安全问题的组件,而我们的业务模型,通过什么来提升性能呢?那就要靠缓存了。

所以池和缓存都是提升系统性能的手段,只不过池的工作有容器帮我做了,而我们只要集中精力做缓存就好了。

ps:更多关于池和缓存的讨论,请参考<<面向模式的软件体系结构>>卷3,资源管理。如果大家对服务器底层设计感兴趣,也可以看看卷2.

>池应该是做为工厂呢还是做为仓库或者是其他的
Pool是处理无状态的,我们现在讨论的是需求分析设计,聚合根实体代表一个现实时空的事物,都是有数据状态在其中。

讨论到这里,我们要明确什么是缓存?缓存=对象唯一性+内存。

缓存就是保持聚合根和边界内对象的唯一性,因为内存有限,而且也没有必要为每个类在不同的客户端事件下,创建一个对象。
这里要谈到经典架构SSH: Struts+Spring+Hibernate中有一个OSIV也就是Open Session In View模式,Hibernate缺省的缓存是Session内的,也就是每次请求处理中缓存唯一的,而不是application级别,全局唯一的,这其实就是Hibernate的一级缓存,是内置,要达到我们这个主题讨论的缓存目标,必须使用Hibernate的二级缓存。

换句话说:也就是使用SSH,就会发生我前面讲的,每次请求都重建一次聚合根和其边界内子对象,这个很耗费性能,也就是根本无法实现聚合根和缓存的唯一性,我们这里讲的美妙设计无法在缺省的SSH架构中实现。所以,很多人就是使用了SSH,还是走到了数据库为中心的编程模型,因为SSH没有缺省提供一个application级别的缓存,这样,如果要实现我前面讲的JiveJdon中updateMessage中功能,就必须自己有意识去做,没人点拨指导是想不到这条路的,这也是我们以前指责SSH容易导致数据库编程问题关键所在。(说句笑话:大家应该明白为什么Jdon框架推出虽然落后于Spring,但是早就达到5.0中级稳定版,而Spring正在向3.0买进呢)

所以,在SSH架构中必须使用二级缓存,手工显式配置缓存,然后,做好自己的工厂,保证缓存中聚合边界内的对象们都是引用指向的是这个边界内的对象,不能指错了,如果存在两个聚合体对象群,其实它们是反映需求中一类聚合设计,那么变成多个对象群,边界内对象互相引用,陷入失败地步。

有了以上前提保证唯一性以后,java 并发编程实践就能派上用场了,我们甚至可以利用Java并发锁在内存中来优雅高性能解决并发写问题,这样避免数据库锁的低性能和粗粒度的排他性。

相关并发锁帖子:
http://www.jdon.com/article/34773.html
[该贴被banq于2009-04-29 10:43修改过]

呵呵,看了banq老师与楼主的回答,明白了很多,谢谢!
>>至于池,它里面主要保存的是无状态的功能性的,不需要考虑线程安全问题的组件,而我们的业务模型,通过什么来提升性能呢?那就要靠缓存了。

茅塞顿开啊!
至于老师提的SSH中的hibernate的缓存(不管一级--session级还是二级--全局),我们公司目前都不用,本人一直推荐,可惜公司怕所谓的风险(本人觉得风险在于一开始的需求分析设计,而不是在于有没有用过),所以公司目前所谓的缓存基本是用web 容器的application--在容器启动,把一些基础数据加载进来的形式,还有就是死命往httpsession中加数据的形式!看来这样的风险更大!