• 1. Hibernate缓存深入详解
  • 2. 大纲 Hibernate缓存概述1Hibernate一级缓存(Session缓存)23 Hibernate二级缓存4二级缓存的高级应用(分布式缓存)5查询缓存
  • 3. 1-1:Hibernate缓存概述 Hibernate缓存机制对Hibernate的性能发挥一直处于一个极其重要的作用,它是持久层性能提升的关键。 hibernate缓存在应用系统中的位置 Hibernate缓存介于Hibernate应用和数据库之间,缓存中存放了数据库数据的拷贝。 其作用是减少访问数据库的频率,从而提高应用的运行性能。 Hibernate在进行读取数据的时候,根据缓存机制在相应的缓存中查询,如果在缓存中找到了需要的数据(我们把这称做“缓存命中”),则就直接把命中的数据作为结果加以利用,避免的了建立数据库查询的性能损耗。
  • 4. 1-2:Hibernate缓存分类 Hibernate提供了两级缓存: 一级缓存: Session级别的缓存 二级缓存: SessionFactory级别的全局缓存 Hibernate的这两级缓存都位于持久化层,存放的都是数据库数据的拷贝。 那么它们之间的区别是什么呢? 为了理解二者的区别,需要深入理解持久化层的缓存的一个特性:缓存的范围。
  • 5. 1-3:缓存的范围(1) 缓存的范围决定了缓存的生命周期以及可以被谁访问。缓存的范围分为三类。事务范围 缓存只能被当前事务访问。缓存的生命周期依赖于事务的生命周期,当事务结束时,缓存也就结束生命周期。缓存的介质是内存。事务可以是数据库事务或者应用事务,每个事务都有独自的缓存,缓存内的数据通常采用相互关联的的对象形式。一级缓存就属于事务范围。
  • 6. 1-3:缓存的范围(2)进程范围 缓存被进程范围内的所有事务共享。这些事务有可能并发访问缓存,因此必须对缓存采取必要的事务隔离机制。缓存的生命周期依赖于进程的生命周期,进程结束时,缓存也就结束了生命周期。它的物理介质可以是内存或硬盘。
  • 7. 1-3:缓存的范围(3)集群范围 在集群环境中,缓存被一个机器或者多个机器的进程共享。缓存中的数据被复制到集群环境中的每个进程节点,进程间通过远程通信来保证缓存中的数据的一致性,缓存中的数据通常采用对象的松散数据形式。 持久化层的第二级缓存就存在于进程范围或集群范围。
  • 8. 大纲 Hibernate缓存简介1Hibernate一级缓存(Session缓存)23 Hibernate二级缓存 查询缓存4 二级缓存的高级应用(分布式缓存)5
  • 9. 2-1:理解一级缓存(1) Session具有一个缓存,是一块内存空间,在这个内存空间存放了相互关联的java对象,这种位于Session缓存内的对象也被称为持久化对象,Session负责根据持久化对象的状态变化来同步更新数据库。 Session的缓存是内置的,不能被卸除的,也被称为Hibernate的第一级缓存。在正常的情况下一级缓存是由Hibernate自动维护的,无需人工干预。 session缓存中对象的生命周期依赖session实例
  • 10. 2-1:理解一级缓存(2) (1)当应用程序调用Session接口的save()、update()、saveOrUpdate()时,如果Session缓存中还不存在相应的对象,Hibernate就会把该对象加入到第一级缓存中。 (2)当调用Session接口的load()、get()以及Query查询接口的list()、iterator()方法时, 如果Session缓存中存在相 应的对象,就不需要到数据 库中检索。 (3)当调用Session的close()时, Session缓存就被清空。
  • 11. 2-2:Session接口的用法(1) Session接口是Hibernate向应用程序提供的操纵数据库的最主要的接口,它提供了基本的保存、更新、删除和加载等方法。 Java对象在Hibernate持久化层的状态: 临时状态:刚用new语句创建,还没有被持久化,并且不处于session缓存中(处于临时状态的对象成为临时对象) 持久化状态:已经被持久化,并且加入到session缓存中。处于持久化状态的对象称为持久化对象 删除状态:不再处于session缓存中,并且session已经计划将其从数据库中删除 游离状态:已经被持久化,但不再处于session缓存中。处于游离状态的对象称为游离对象
  • 12. 2-2:Session接口的用法(2) save()和persist() save()方法把一个临时对象加入到Session缓存中,并持久化该临时对象,计划执行一个insert语句。 persist()和save()方法类似,也能把把一个临时对象转变为持久化对象。 区别: (1)persist() 是在Hibernate3版本中才出现。在使用代理主键的情况下persist()方法不保证立即为持久化对象的ID赋值,而是有可能在Session清理缓存时才为ID赋值。 (2)如果在事务边界以外调用persist()方法,那么该方法不会计划执行insert语句,这可以提高负责长时间运行事务的程序的健壮性。 而save()方法,不管是在事务边界以外或以内调用它,都会计划执行insert语句。
  • 13. 2-2:Session接口的用法(3)get()和load() 试图从数据库加载一个实体对象时,Session先判断对象是否存在,如果存在就不到数据库中检索。返回的对象都位于Session缓存中,接下来修改了持久化对象的属性后,当Session清理缓存时,会根据持久化对象的属性变化来同步更新数据库。 区别: (1)当数据库中不存在与OID对应的记录时,load()方法抛出ObjectNotFoundException异常,而get()方法返回null. (2)两者采用不同的检索策略。 默认情况下,load()方法采用延迟检索策略(Hibernate不会执行select语句,仅返回实体类的代理类实例,占用内存很少);而get()采用立即检索策略(Hibernate会立即执行select语句)。 使用场合: (1)如果加载一个对象的目的是为了访问它的各个属性,可以用get(); (2)如果加载一个对象的目的是为了删除它,或者建立与别的对象的关联关系,可以用load() ;
  • 14. 2-2:Session接口的用法(3) update() 和merge() update()方法把游离对象加入当前Session缓存中,计划执行update语句。 当update()方法关联一个游离对象时,如果session缓存中已经有一个同类型且ID相同的持久化对象,那么update()方法会抛出NonUniqueException异常. 当update()方法关联一个持久化对象时,该方法不起作用。 merge() 方法能够把游离对象的属性复制到一个持久化对象中。
  • 15. 2-2:Session接口的用法(4) merge() 方法的处理流程
  • 16. 2-2:Session接口的用法(4) saveOrUpdate() 同时包含了save()和update()方法的功能。 如果传入的是临时对象,就调用save()方法; 如果传入的是游离对象,就调用update()方法 如果传入的是持久化对象,就直接返回。 delete() :计划执行一个delete语句,把对象从Session缓存中删除。 close() :清空session缓存。
  • 17. 2-3:Session清理缓存 清理缓存是指Session按照缓存中对象的属性变化来同步更新数据库。 Session在清理缓存的时候会自动进行脏检查(dirty-check),如果发现Session缓存中的对象与数据库中相应的记录不一致,就会同步数据库。 Session是如何进行脏检查的呢? 当一个对象被加入到Session缓存时,Session 会为该对象的值类型的属性复制 一份快照。当Session清理缓存的时候,会进行脏检查,即比较对象的当前属性与它的快照,来判断对象的属性是否发生变化,如果发生变化,就称这个对象是“脏对象”,Session会根据脏对象的最新属性来执行相关的SQL语句,从而同步更新数据库。
  • 18. 2-4:Session脏检查及同步数据库的过程
  • 19. 2-5:Session何时清理缓存(1) session缓存中对象的属性每次发生变化,Session不会立即清理缓存及执行相关的update语句,而是在特定的时间点才清理缓存,这使得Session能够把几条相关的sql语句合并为一条sql语句,以便减少访问数据库的次数。 以下代码对dept对象的deptName属性修改了两次:Transaction tx = session.biginTransaction(); Dept dept = (Dept)session.get(Dept.class,”001”); dept.setDeptName(“人事部”); dept.setDeptName(“人力资源部”); tx.commit();当Session清理缓存时,只会执行一条update语句
  • 20. 2-5:Session何时清理缓存(2) session会在下面的时间点清理缓存:   (1)当应用程序调用org.hibernate.Transaction的commit()方法的时候   commit()方法先清理缓存,然后再向数据库提交事务。 (2) 当应用程序执行一些查询操作时,如果缓存中持久化对象的属性已经发生变化,就会先清理缓存,使得Session缓存与数据库进行了同步,从而保证查询结果返回的是正确的数据。 (3) 当应用程序显式调用Session的flush()方法的时候
  • 21. 2-6:Sesion缓存的管理 第一级缓存在正常的情况下是由Hibernate自动维护的。 在特殊的情况下需要我们进行手动维护,Hibernate就提供了两个管理Session缓存的方法:   (1)Session.evict(Object o)   将某个特定的对象从缓存中清除, 使用此方法有两种适用情形,一是在特定的操作(如批量处理),需要及时释放对象占用的内存。二是不希望当前Session继续运用此对象的状态变化来同步更新数据库。 (2)Session.clear() 清除缓存中的所有持久化对象。1、在多数情况下并不提倡通过evit()和clear()来管理一级缓存。 2、管理一级缓存最有效的方法是采用合理的检索策略和检索方式来节省内存的开销。
  • 22. 2-7:Session缓存的作用(1)(1)减少访问数据库的频率。Transaction tx = session.biginTransaction(); //第一次执行session.get() Dept dept1 = (Dept)session.get(Dept.class,”001”); //第二次执行session.get() Dept dept2 = (Dept)session.get(Dept.class,”001”); tx.commit();第一次执行session.get()
  • 23. 2-7:Session缓存的作用(2)(1)减少访问数据库的频率。第二次执行session.get()
  • 24. 2-7:Session缓存的作用(3)(2)保证缓存中的对象与数据库中的相关记录保持同步。 当缓存中的持久化对象的属性发生变化时,Session并不会立即执行相关的update语句,
  • 25. 大纲 Hibernate缓存简介1Hibernate一级缓存(Session缓存)23 Hibernate二级缓存4 二级缓存的高级应用(分布式缓存)5 查询缓存
  • 26. 3-1:Hibernate二级缓存概述 因为Session的生命期往往很短,存在于Session内部的第一级缓存的生命期当然也很短,所以第一级缓存的命中率是很低的。其对系统性能的改善也是很有限的。当然,这个Session缓存的主要作用是保持Session内部数据状态同步,并非是hibernate为了大幅提高系统性能所提供的。 Hibernate二级缓存: SessionFactory级别的全局缓存 可配置可插拔的缓存插件 涵盖了进程范围与集群范围 物理介质:内存或硬盘
  • 27. 3-2:Hibernate两级缓存机制(1) Hibernate的两级缓存机制: 如果在一级缓存中没有查询到相应的数据,可以到二级缓存中查找,如果在二级缓存中也没有找到数据,那么就只好查询数据库了。
  • 28. Hibernate的二级缓存 (SessionFactory的缓存) 3-2:Hibernate两级缓存机制(2) Hibernate的一级缓存 (Session的缓存) 缓存并发访问策略查询缓存缓存插件(CacheProvider)缓存的实现Hibernate二级缓存机制
  • 29. 3-2: Cache Providers(1) Hibernate本身并不提供二级缓存的产品化实现,而是为众多支持Hibernate的第三方缓存插件提供整和接口。缓存插件用于把第三方的具体的缓存实现软件和Hibernate集成,可以在每个持久化类或每个集合的粒度上配置二级缓存。 通过在hibernate.cache.provider_class属性中指定org.hibernate.cache.CacheProvider的某个实现的类名,你可以选择让Hibernate使用哪个缓存实现。Hibernate打包一些开源缓存实现,提供对它们的内置支持。 除此之外,你也可以实现你自己的缓存,将它们插入到系统中。
  • 30. 3-2: Cache Providers(2)CacheProvider ClassCluster SupportedQuery Cache SupportedEhcacheorg.hibernate.cache.EhCacheProvideryes yesOSCacheorg.hibernate.cache.OSCacheProvideryesyesSwarm Cacheorg.hibernate.cache.SwarmCacheProvideryesnoJossCacheorg.hibernate.cache.TreeCacheProvideryesyes Hibernate支持以下类型的缓存插件: 为了把这些缓存插件集成到Hibernate中,Hibernate提供了org.hibernate.cache.CacheProvider接口,它是缓存插件和与Hibernate之间的适配器。Hibernate分别为这些缓存插件提供了内置的CacheProvider实现 。
  • 31. 3-2: Cache Providers(3) Hibernate、缓存适配器、缓存插件之间的关系 如果需要实现其它类型的缓存插件,只需要为这个插件提供实现org.hibernate.cache.CacheProvider接口的类。
  • 32. 3-3 缓存的并发访问策略(1) cache concurrency strategy 当两个并发的事务同时访问数据库相同数据的时候,有可能出现五类并发访问问题,因此必须采用必要的事务隔离措施。同样,两个并发的事务同时访问缓存的相同数据时,也有可能出现各类并发问题。 因此必须为Hibernate二级缓存设定适当的并发访问策略,每一种策略对应一种事务隔离级别。
  • 33. 3-3 缓存的并发访问策略(2) cache concurrency strategy transactional (事务型)仅在受管理的环境中适用 提供Repeatable Read事务隔离级别 适用经常被读,很少修改的数据 可以防止脏读和不可重复读的并发问题 缓存支持事务,发生异常的时候,缓存也能够回滚 read-write (读写型)提供Read Committed事务隔离级别 在非集群的环境中适用 适用经常被读,很少修改的数据 可以防止脏读 更新缓存的时候会锁定缓存中的数据 nonstrict-read-write (非严格读写型)适用极少被修改,偶尔允许脏读的数据(两个事务同时修改数据的情况很少见) 不保证缓存和数据库中数据的一致性 为缓存数据设置很短的过期时间,从而尽量避免脏读 不锁定缓存中的数据 read-only (只读型) 适用从来不会被修改的数据(如参考数据) 在此模式下,如果对数据进行更新操作,会有异常 事务隔离级别低,并发性能高 在集群环境中也能完美运作
  • 34. 3-4:Cache Concurrency Strategy Supported各种缓存插件对缓存并发访问策略的支持情况:cachetransactionalread-writenonstrict-read-writeread-onlyEhcachenoyesyesyesOSCachenoyesyesyesSwarm CachenonoyesyesJossCacheyesnonoyes
  • 35. 3-5:二级缓存的配置 配置二级缓存主要包含以下几个步骤: 启用二级缓存; 选择需要进行二级缓存的持久化类,设置它的缓存并发访问策略; 选择合适的缓存插件; 为命名缓存设置数据过期策略;
  • 36. 3-5:二级缓存的配置(1) 启用二级缓存:在Hibernate的配置文件中配置属性 注意:此属性对指定的类缺省为true 。 选择需要二级缓存的持久化类,设置它的缓存并发访问策略 transactional|read-write|nonstrict-read-write|read-only Hibernate允许在分散的各个映射文件中为持久化类设置二级缓存,也允许在Hibernate的配置文件中集中设置二级缓存,指定 元素,统一配置。后一种方式更有利于代码维护。 hibernate.cache.use_second_level_cache = true
  • 37. 3-5:二级缓存的配置(2)在映射文件中设置二级缓存×××.hbm.xml : 类或者集合映射的“元素”的详细用法: usage:(必须)说明了缓存的同步策略: transactional、 read-write、 nonstrict-read-write或 read-only。 region :(可选, 默认为类或者集合的名字) 指定二级缓存的区域名 include :(可选,默认为 all)如果取值为non-lazy,表示当缓存一个对象时,不会缓存它的映射为延迟加载的属性。
  • 38. 3-5:二级缓存的配置(3)例如: ….. …… 注意: 1、元素必须紧跟元素 2、 Hibernate只会缓存对象的简单属性的值, 如果要缓存集合属性,必须在集合元素中也加入子元素,而Hibernate仅仅是把与当前持久对象关联的对象的ID存放到缓存中。如果希望把整个关联的对象的所有数据都存入缓存,则要在相应关联的对象的映射文件中配置元素。
  • 39. 3-5:二级缓存的配置(4)注解方式:@Entity @Table(name="T_DEPT") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class Dept implements Serializable{ ……. //集合属性部分添加Cache注解 @OneToMany (mappedBy = "dept") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set userList = new HashSet(); }
  • 40. 3-5:二级缓存的配置(5)选择合适的缓存插件: 在配置文件指定CacheProvider 每一种缓存插件都有自带的配置文件,需要手工编辑配置文件,在配置文件中需要为每个命名缓存设置数据过期策略. 如:Ehcache 插件 ehcache.xml Jbosscache 插件 treecache.xml 下面我们以ehcache为例进行介绍。 org.hibernate.cache.EhCacheProvider
  • 41. 3-6:ehcache.xml样例 42. 3-7: 配置文件ehcache.xml 把ehcache.xml放在classpath下,自动加载。 元素,指定一个文件目录,当ehcache把缓存写入硬盘的时候,就把数据写到这个文件目录下。 元素,设置缓存的默认数据过期策略 元素,设置具体的命名缓存的数据过期策略 每个命名缓存代表一个缓存区域,每个缓存区域都有自己的数据过期策略。每个需要缓存的持久化类都要这样配置。如果没有配置,hibernate会在启动的时候警告你,然后使用defaultCache的配置。
  • 43. 3-8:查看二级缓存的活动1、查看二级缓存的操作过程 调试时候,可以设置log4j的log4j.logger.org.hibernate.cache=debug(记录第二级缓存的活动),更方便看到ehcache的操作过程。用于调试过程,实际应用发布时候,请注释掉,以免影响性能。在log4j.properties文件中添加如下内容: log4j.logger.org.hibernate.cache=debug 2、SessionFactory提供了查看二级缓存中的数据的方法Map cacheEntries= sessionFactory.getStatistics() .getSecondLevelCacheStatistics(“cacheRegionName”) .getEntries();
  • 44. 3-9 一级缓存和二级缓存的交互(1) 一级缓存与二级缓存的几种交互模式,分别用org.hibernate.CacheMode类的5个静态常量表示。 CacheMode.NORMAL :从二级缓存读写数据(默认模式) CacheMode.GET :读取模式,从二级缓存读数据 CacheMode.PUT : 写入模式,仅向二级缓存写数据,但不从二级缓存读数据 CacheMode.IGNORAL :忽略模式,不会从二级缓存中读取数据,也不会向其中写入数据。 CacheMode.REFRESH :刷新模式,Session不会从二级缓存中读取数据,但会向其中写入从数据库中读到的数据。
  • 45. 3-9 一级缓存和二级缓存的交互(2)Session接口设置交互模式的方法session.setCacheMode(); session = sessionFactory.openSession(); //设置了不把数据放入二级缓存,仅从二级缓存读数据 session.setCacheMode(CacheMode.GET); tx = session.beginTransaction(); Dept dept = (Dept)session.get(Dept.class,"001"); System.out.println(dept.getDeptName()); //查询出来后没有放入二级缓存 session.getTransaction().commit(); session.close();
  • 46. 大纲 Hibernate缓存简介1Hibernate一级缓存(Session缓存)23 Hibernate二级缓存4 二级缓存的高级应用(分布式缓存)5 查询缓存
  • 47. 4-1 查询缓存概述 Hibernate的二级缓存策略是针对ID查询的策略,和对象ID密切相关,那么对于条件查询就不怎么适用了。对于这种情况的存在,Hibernate引入了“查询缓存”,在一定程度上缓解这个问题。 启用查询缓存后,第一次执行查询时,Hibernate会把查询结果放入查询缓存中。以后再次执行该查询语句时,只需从缓存中获得查询结果,从而提高查询性能。 查询缓存依赖二级缓存。
  • 48. 4-2:启用查询缓存对查询语句启用查询缓存的步骤 1、启用二级缓存hibernate.cache.use_query_cache=true3、调用Query接口的setCacheable(true)2、在配置文件中设置查询缓存属性为true hibernate.cache.use_second_level_cache=true Query query = session.createQuery(hql); query.setCacheable(true); //启用查询缓存 query.setCacheRegion(“queryCacheRegion”); //设置查询缓存区域(数据过期策略) query.list();
  • 49. 4-3:查询缓存的运行过程Hibernate提供了三种查询缓存区域: 默认的查询缓存区域:StandQueryCache,存放查询结果 用户自定义的查询缓存区域:存放查询结果 时间戳缓存区域:UpdateTimestampCache,存放对与查询相关的表进行插入、更新、删除操作的时间戳 Hibernate通过时间戳来判断被查询的结果是否过期,它的运行过程如下: ()在t1时刻执行查询语句,把查询结果放进查询缓存区域,该区域的时间戳为t1 时刻; (2)在t2时刻对查询相关的表进行插入更新删除操作,hibernate把t2 时刻放入时间戳缓存区域; (3)在t3时刻执行查询语句前,先比较QueryCache区域的时间戳和时间戳缓存区域,如果t2〉t1,则查询缓存失效 1
  • 50. 4-4:查询缓存小结(1) (1) 对于查询缓存来说,缓存的 key 是根据 hql 生成的 sql ,再加上参数,分页等信息。 (2) 查询缓存只会存放实体的ID,对于以下HQL查询语句 FROM Dept ; 如果启用了查询缓存,Hibernate会把查询结果中Dept对象的ID属性存放到查询缓存中,把Dept对象放入Dept缓存中。 第二次执行查询的时候,如果查询缓存还没有过期,就会从查询缓存中获取一连串的ID值,然后根据每个ID到Dept缓存中查找相应的dept对象。 因此,class缓存的超时时间不能短于查询缓存设置的超时时间!
  • 51. 4-5:查询缓存小结(2) 我们从查询缓存的策略中可以看出,Query Cache只是在特定的条件下才会发挥作用,而且要求相当严格:   (1) 完全相同的HQL重复执行。   (2) 重复执行期间,Query Cache对应的数据表不能有数据变动(比如添、删、改操作) 绝大多数的查询并不能从查询缓存中受益,所以Hibernate默认是不进行查询缓存的。 查询缓存适用于以下场合: (1) 在应用程序运行时经常使用的查询语句(参数相同) (2)很少对与查询语句检索到的数据进行插入、删除或更新操作
  • 52. 大纲 Hibernate缓存简介1Hibernate一级缓存(Session缓存)23 Hibernate二级缓存 查询缓存45 二级缓存的高级应用(分布式缓存)
  • 53. 5-1:分布式缓存 Ehcache从1.2版本开始支持集群 提供分布式、全局缓存同步机制 同一台服务器多个服务实例间的缓存同步策略 不同服务器节点间缓存同步策略 常用的集群模式: RMI: 是一种点对点的基于 Java 对象的通讯方式。 JGroups :提供了一个非常灵活的协议栈、基于 TCP 的单播 ( Unicast ) 和基于 UDP 的多播 ( Multicast )消息传输。 EhCache Server
  • 54. 5-2:Jgroups集群模式引入JGroups JGroups是一个可靠的组间通讯工具,进程可以加入一个通讯组,给组内所有的成员或单独的成员发送消息,同样,也可以从组中的成员处接收消息。 系统会记录组的每一个成员,在新成员加入或是现有的成员离开或是崩溃时,会通知组内的其他成员。 当我们更新集群中一个节点上的cache的时候,利用JGroups进行广播,其他的节点接收到广播,根据接收到的信息来更新自己的cache,这样达到了每个节点的cache同步。
  • 55. 5-3:单播方式使用单播方式需要指定其它节点的主机地址和端口
  • 56. 5-4:多播方式使用多播方式配置如下:
  • 57. 5-5:命名缓存的配置JGroups 方式对应命名缓存的配置信息如下:需要指定cacheEventListenerFactory />
  • 58. 6-1:小结(1)二级缓存适用范围: 很少被修改的数据 不是很重要的数据,允许出现偶尔的并发访问问题 不会被并发访问的数据 参考数据 不适用范围: 经常被修改的数据 财务数据,绝对不允许出现并发问题 与其他应用共享的数据
  • 59. 6-2:小结(2) 在实际开发中,运用好、驾驭好缓存可以让你的系统性能得到良好的提升。可hibernate缓存也有它的局限性,什么情况用什么情况不用,都要结合实际情况来考量。如果违背了相关的缓存机制,可能会得到负面效果,所以充分考虑实际情况来灵活运用缓存并发挥它的优势,这是我们需要做得更多的。 如果受不了hibernate的诸多限制,那么还是自己在应用程序的层面上做缓存吧。 在越高的层面上做缓存,效果往往就会越好。就好像尽管磁盘有缓存,数据库还是要实现自己的缓存,尽管数据库有缓存,咱们的应用程序还是要做缓存。因为底层的缓存它并不知道高层要用这些数据干什么,只能做的比较通用,而高层可以有针对性的实现缓存,所以在更高的级别上做缓存,效果也要好些。
  • 60. 1、《精通Hibernate:Java对象持久化技术详解》孙卫琴 2、 Ehcahe官方网站 http://ehcache.org/documentation/distributed_caching.html 资源
  • 61. END! THANK YOU!