通过这篇文章纪录hibernate二级缓存的一些使用经历,利用几个test case,从代码角度说明二级缓存在使用过程中一些需要注意的问题 

使用到的Model类有两个,Author, Book, 两者之间为一对多的关系 



Java代码


1. @Entity
2. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)    
3. public class
4.        
5. private
6. private
7.        
8. private Set<Book> books = new
9. // getter setter methods omitted 
10. }





Java代码


1. @Entity
2. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)    
3. public class
4.        
5. private
6. private
7.        
8. private
9. // getter setter methods omitted 
10. }



主要的测试类为TestHibernateSecondLevelCache.java 


Java代码


1. public class
2.        
3. protected
4.        
5. private static
6.        
7. @BeforeClass
8. public static void
9. new
10.     }   
11.        
12. @After
13. public void
14. "clear second level cache");    
15. class);    
16. class);    
17.                 sessionFactory.getStatistics().clear();   
18.     }   
19.   
20. private
21. return
22.     }   
23.        
24. private
25. return
26.     }   
27. }


方法setUpSessionFactory用于创建Hibernate SessionFactory,因为创建Session Factory是个相对比较耗时的操作,因此加上Junit4的@BeforeClass annotation,表示该Session Factory只会创建一次,被所有的test case共享.而clearSecondLevelCache方法会在每个test case结束时调用,用于清空二级缓存,防止前一个test case的結果影响后一个test case 

测试使用的hibernate-core版本为:3.3.2.GA, hibernate-annotations版本为:3.4.0.GA,测试的数据库为hsqldb内存数据库 

一. session.get() 

先来看一下session.get是否会查找二级缓存 



Java代码



    1.       
    2. @Test
    3. public void
    4.     Author author = createAuthor();   
    5.        
    6. class, author.getId());    
    7. class, author.getId());    
    8.        
    9.     updateAuthor(author);   
    10.        
    11. class, author.getId());    
    12. }   
    13.           
    14. private
    15.     Session session = openSession();   
    16. new
    17. "septem");    
    18.     session.save(author);   
    19.     session.close();   
    20. return
    21. }   
    22.   
    23. @SuppressWarnings("unchecked")    
    24. private void
    25.     Statistics stat = getStatistics();   
    26. long
    27.     Session session = openSession();   
    28.     session.get(clazz, id);   
    29.     session.close();   
    30. 1, stat.getSecondLevelCacheMissCount());    
    31. }   
    32.   
    33. @SuppressWarnings("unchecked")    
    34. private void
    35.     Statistics stat = getStatistics();   
    36. long
    37.     Session session = openSession();   
    38.     session.get(clazz, id);   
    39.     session.close();   
    40. 1, stat.getSecondLevelCacheHitCount());    
    41. }   
    42.   
    43. private void
    44. "new_name");    
    45.     Session session = openSession();   
    46.     session.update(author);   
    47.     session.flush();   
    48.     session.close();   
    49. }


    testSessionGetCache首先通过createAuthor创建一个author对象,然后在assertGetMissCache里面通过author.id使用get方法查出之前创建的author,因为这是每一次调用get方法,所以hibernate从数据库取回author对象,并将它存入二级缓存.测试結果通过hibernate statistics统计信息里的second level cache miss count来判断这次的get查询未命中缓存 

    接着assertGetHitCache用同一个id通过get方法获取author对象,因为这个id的对象之前已存入二级缓存,所以这次操作命中缓存 

    最后通过updateAuthor更新之前的author对象,hibernate会自动将该对象从二级缓存中清除,因此第三次调用get方法时没有命中缓存 

    总结 : session.get方法会先中二级缓存中通过id做为key查找相应的对象,如果不存在,再发送SQL语句到数据库中查询 

    二. session.load() 

    第二步试一下session.load方法 



    Java代码



      1. @Test
      2. public void
      3.         Author author = createAuthor();   
      4.            
      5. class, author.getId());    
      6. class, author.getId());    
      7.            
      8.         updateAuthor(author);   
      9.            
      10. class, author.getId());    
      11.     }   
      12.   
      13. @SuppressWarnings("unchecked")    
      14. private void
      15.         Statistics stat = getStatistics();   
      16. long
      17.         Session session = openSession();   
      18.         Author author = (Author) session.load(clazz, id);   
      19.         author.getName();   
      20.         session.close();   
      21. 1, stat.getSecondLevelCacheMissCount());    
      22.     }   
      23.        
      24. @SuppressWarnings("unchecked")    
      25. private void
      26.         Statistics stat = getStatistics();   
      27. long
      28.         Session session = openSession();   
      29.         session.load(clazz, id);   
      30.         Author author = (Author) session.load(clazz, id);   
      31.         author.getName();   
      32.         session.close();   
      33. 1, stat.getSecondLevelCacheHitCount());    
      34.     }


      同样的結果,每一次通过id load未命中缓存,第二次通过相同的id调用load方法命中缓存,而更新过author对象后,缓存失效,第三次查询通过数据库获取author 

      有一点跟get方法不同: 



      Java代码



        1. Author author = (Author) session.load(clazz, id);   
        2.         author.getName();



        总结: 调用load方法的时候,hibernate一开始并没有查询二级缓存或是数据库,而是先返回一个代理对象,该对象只包含id,只有显示调用对象的非id属性时,比如author.getName(),hibernate才会去二级缓存查找,如果没命中缓存再去数据库找,数据库还找不到则抛异常.load方法会尽量推迟对象的查找工作,这是它跟get方法最大的区别. 

        这两者的测试用例如下: 



        Java代码


        1. @Test(expected=ObjectNotFoundException.class)   
        2. public void
        3.         Session session = openSession();   
        4. class, -1L);    
        5. 1), author.getId());    
        6.         author.getName();   
        7.         session.close();   
        8.     }   
        9.        
        10. @Test
        11. public void
        12.         Session session = openSession();   
        13. class, -1L);    
        14.         session.close();   
        15.         assertNull(author);   
        16.     }


        三. session.createQuery().list() 


        Java代码



          1. @SuppressWarnings("unchecked")   
          2. @Test
          3. public void
          4.         Author author = createAuthor();   
          5.         createAuthor();   
          6.            
          7.         Session session = openSession();   
          8. //hit database to select authors and populate the cache 
          9. "from Author").list();    
          10.         session.close();   
          11.            
          12.         assertEquals(authors.size(), getStatistics().getSecondLevelCachePutCount());   
          13.            
          14.         Session session2 = openSession();   
          15. //hit database again to select authors 
          16. "from Author").list();    
          17.         session2.close();   
          18.            
          19.         assertEquals(authors.size(), getStatistics().getSecondLevelCachePutCount());   
          20.            
          21. class, author.getId());    
          22.     }



          首先创建2个author对象,使用HQL : "from Author"调用list方法,这时hibernate直接从数据库查询所有的author对象,并没有从缓存中查询,但是通过list方法查出的所有author对象会存入二级缓存,这点通过getStatistics().getSecondLevelCachePutCount()可以看出来 

          接着再调用list方法一次,因为此时还没找开查询缓存,list方法重新从数据查了一次.因为第一次查询已将所有的author存入缓存,所以再调用get方法时会命中缓存,assertGetHitCache通过 

          总结: list方法不会从二级缓存中查找,但它从数据库中查找出来的对象会被存入cache 

          四. session.createQuery().iterate() 



          Java代码


          1. @SuppressWarnings("unchecked")   
          2. @Test
          3. public void
          4.         Author author = createAuthor();   
          5.         createAuthor();   
          6.            
          7. int authorCount = 0;    
          8.            
          9.         Session session = openSession();   
          10. //hit database to get ids for all author 
          11. "from Author").iterate();    
          12. while(it.hasNext()){    
          13.             Author a = it.next();   
          14.             a.getName();   
          15.             authorCount++;   
          16.         }   
          17.         session.close();   
          18.         assertEquals(authorCount, getStatistics().getEntityLoadCount());   
          19. class, author.getId());    
          20.     }


          先创建2个author对象, 通过HQL: "from Author"调用iterate方法,此时hibernate并没有查author对象,而是先从数据库查出所有author的id,控制台会输入类似以下的SQL: 



          Sql代码



          1. select id from



          在对iterator里面遍历的时候,会根据id一个一个地从先中缓存中查找author,没找到再访问数据库 


          总结: iterate方法使用的是典型的N+1次查询,先从数据库查询出所有对象的ID,再根据ID一个一个地从二级缓存查找,二级缓存找不到再查询数据库 

          五. association cache 

          hibernate支持对关联进行缓存,先在Book.java加上books集合的缓存配置 



          Java代码



            1. @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)   
            2. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)    
            3. public
            4. return
            5.     }



            测试用例如下: 



            Java代码



              1. @Test
              2. public void
              3.         Author author = createAuthorWith3Books();   
              4. 1);    
              5. 4);    
              6.         updateOneBookForAuthor(author);   
              7. 1);    
              8.         addNewBookForAuthor(author);   
              9. 1);    
              10.     }   
              11.   
              12. private
              13.         Session session = openSession();   
              14.            
              15. new
              16. "septem");    
              17.            
              18. new
              19. "book1");    
              20.         book1.setAuthor(author);   
              21.            
              22. new
              23. "book2");    
              24.         book2.setAuthor(author);   
              25.            
              26. new
              27. "book3");    
              28.         book3.setAuthor(author);   
              29.            
              30.         author.getBooks().add(book1);   
              31.         author.getBooks().add(book2);   
              32.         author.getBooks().add(book3);   
              33.            
              34.         session.save(book1);   
              35.         session.save(book2);   
              36.         session.save(book3);   
              37.            
              38.         session.close();   
              39. return
              40.     }   
              41.   
              42. private void assertGetBooksForAuthorMissCache(Author author, long
              43.         Session session = openSession();   
              44. class, author.getId());    
              45. long
              46.         a.getBooks().size();   
              47.         session.close();   
              48.         assertEquals(missCount + miss, getStatistics().getSecondLevelCacheMissCount());   
              49.     }   
              50.        
              51. private void assertGetBooksForAuthorHitCache(Author author, long
              52.         Session session = openSession();   
              53. class, author.getId());    
              54. long
              55.         a.getBooks().size();   
              56.         session.close();   
              57.         assertEquals(hitCount + hit, getStatistics().getSecondLevelCacheHitCount());   
              58.     }   
              59.        
              60. private void
              61.         Session session = openSession();   
              62.            
              63. class, author.getId());    
              64. class, a.getBooks().iterator().next().getId());    
              65. "new_title");    
              66.         session.flush();   
              67.            
              68.         session.close();   
              69.     }   
              70.        
              71. private void
              72.         Session session = openSession();   
              73.            
              74. class, author.getId());    
              75. new
              76. "new_book");    
              77.         book.setAuthor(a);   
              78.         a.getBooks().add(book);   
              79.         session.save(book);   
              80.         session.update(a);   
              81.         session.flush();   
              82.         session.close();   
              83.     }



              先创建一个author,为该author添加3个book对象.在assertGetBooksForAuthorMissCache通过author.getBooks访问关联的book集合,因为延迟加载的关系,此时并没有查询缓存也没有查询数据库,在调用a.getBooks().size()也就是访问book集合的元素时,hibernate先中缓存中查找,没有发现关联缓存,重新数据库查询,生成的SQL类似如下: 



              Sql代码


              1.  * from book where author_id = ?


              1. 此时statistics的missCount只增加了1,因为调用author.getBooks没有命中缓存.hibernate从数据库查询出books后,将books关联以及三个book对象都存入二级缓存. 

                关联的缓存是以什么样的形式存在呢?注意关联缓存没有保存books集合本身,而是保存所有book的id,假设3个book对象的id分别为1, 2, 3,则author缓存的格式类似于如下: 



              Java代码


              1. *---------------------------------*   
              2. |        Author Data Cache        |   
              3. |---------------------------------|   
              4. 1 -> [ "septem" , [ 1, 2, 3
              5. *---------------------------------*


              第二步执行assertGetBooksForAuthorHitCache(author, 4)的时候,我们看到hitCount增加了4.因为第二次调用author.getBooks的时候,命中了关联缓存,从缓存中取回3个id,又分别用id一个一个地从二级缓存中取回3个book对象,一共命中缓存4次 

              接着通过updateOneBookForAuthor(author)更新了其中的一个book对象,假设更新的是id为1的book.接着的assertGetBooksForAuthorMissCache(author, 1)方法里面missCount又增加了1.book虽然更新了,但是author.getBooks还是能命中缓存,因为book id列表还是[ 1, 2, 3 ].从缓存中取回book id列表,通过book id查找book的时候,因为id为1的book已经更新过了,它的二级缓存失效了,重新去数据库取,此时missCount增加了1,而id为2,3的book还是从二级缓存中找到的.这个方法hibernate会生成类似如下的SQL: 



              Sql代码


              1. select * from book where



              更新其中的一个book对象不会造成关联缓存的失效,但如果更新了集合id列表的话,缓存就会失效.先通过addNewBookForAuthor为author增加一个books对象,此时books集合里面一共有4个book对象,最后的assertGetBooksForAuthorMissCache(author, 1)我们可以看到缓存失效,missCount增加了1.此时同第一次调用author.getBooks一样,hibernate生成类似如下的SQL 



              Sql代码




              1.  * from book where author_id = ?



              1. 总结: 关联缓存保存的是集合的id列表,而不是集合本身,关联命中缓存的时候,会根据id一个一个地先从二级缓存查找,找不到再查询数据库.更新集合中的某个对象不会造成关联缓存失效,只有改变集合的id列表才会造成缓存失效 

                五. 查询缓存query cache 

                在hibernate.cfg.xml里面加上以下配置开启查询缓存: 



              Xml代码



                1. <property name="hibernate.cache.use_query_cache">true</property>



                测试用例如下: 



                Java代码



                1. @Test
                2. public void
                3.         createAuthor();   
                4.         createAuthor();   
                5.            
                6.         assertQueryMissCache();   
                7.            
                8.         assertQueryHitCache();   
                9.            
                10.         createAuthor();   
                11.         assertQueryMissCache();   
                12.      }



                先做准备工作,创建两个author对象,假设它们的id分别为1,2.assertQueryMissCache里面第一次调用list方法,注意调用list前必须setCacheable(true)才会使用查询缓存,此时未命中查询缓存,hibernate从数据库查询Author对象,将此次查询存入查询缓存,同时会将查询到的author对象存入二级缓存 

                查询缓存并不保存查询结果集,而只是保存结果集的id,它的结构类似以下数据: 



                Java代码

                1. *---------------------------------------------------------------*   
                2. |                         Query Cache                           |   
                3. |---------------------------------------------------------------|   
                4. "from Author where name = ?", [ "septem"] ] -> [  1, 2
                5. *---------------------------------------------------------------*


                注意缓存的key与HQL,参数以及分页参数有关 

                再调用assertQueryHitCache()用同样的HQL与参数重新查询Author此时会命中查询缓存,并根据结果集id一个一个地查询author对象,因为author对象之前已存入二级缓存,所以这次查询也会命中二级缓存 

                查询缓存的失效比较特殊,只要查询涉及的任何一张表的数据发生变化,缓存就会失效.比如我们再创建一个Author对象,此时Author表发生了变化,原来的查询缓存就失效了 


                总结: 查询缓存的key与HQL,查询参数以及分布参数有关,而且一旦查询涉及到的任何一张表的数据发生了变化,缓存就失效了,所以在生产环境中命中率较低.查询缓存保存的是结果集的id列表,而不是结果集本身,命中缓存的时候,会根据id一个一个地先从二级缓存查找,找不到再查询数据库. 

                涉及到的所有代码保存在google code上 



                Java代码




                1. svn checkout http://hibernate-cache-testcase.googlecode.com/svn/trunk/ hibernate-cache-testcase