1. 程式人生 > >MyBaties的二級快取

MyBaties的二級快取

二級快取介紹

在上文中提到的一級快取中,其最大的共享範圍就是一個SqlSession內部,那麼如何讓多個SqlSession之間也可以共享快取呢,答案是二級快取。
當開啟二級快取後,會使用CachingExecutor裝飾Executor,在進入後續執行前,先在CachingExecutor進行二級快取的查詢,具體的工作流程如下所示。

在二級快取的使用中,一個namespace下的所有操作語句,都影響著同一個Cache,即二級快取是被多個SqlSession共享著的,是一個全域性的變數。
當開啟快取後,資料的查詢執行的流程就是 二級快取 -> 一級快取 -> 資料庫。

二級快取配置

要正確的使用二級快取,需完成如下配置的。
1 在Mybatis的配置檔案中開啟二級快取。

<setting name="cacheEnabled" value="true"/>

2 在Mybatis的對映XML中配置cache或者 cache-ref 。

<cache/>

<cache-ref namespace="mapper.StudentMapper"/>

cache-ref代表引用別的名稱空間的Cache配置,兩個名稱空間的操作使用的是同一個Cache。

二級快取實驗

實驗1

測試二級快取效果,不提交事務,sqlSession1查詢完資料後,sqlSession2相同的查詢是否會從快取中獲取資料。

@Test
public void testCacheWithoutCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取資料: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentById(1));
}

執行結果:

 

我們可以看到,當sqlsession沒有呼叫commit()方法時,二級快取並沒有起到作用。

實驗2

測試二級快取效果,當提交事務時,sqlSession1查詢完資料後,sqlSession2相同的查詢是否會從快取中獲取資料。

@Test
public void testCacheWithCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取資料: " + studentMapper.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentById(1));
}

 

從圖上可知,sqlsession2的查詢,使用了快取,快取的命中率是0.5。

實驗3

測試update操作是否會重新整理該namespace下的二級快取。

@Test
public void testCacheWithUpdate() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        SqlSession sqlSession3 = factory.openSession(true); 
        
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
        
        System.out.println("studentMapper讀取資料: " + studentMapper.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentById(1));
        
        studentMapper3.updateStudentName("方方",1);
        sqlSession3.commit();
        System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentById(1));
}

 

我們可以看到,在sqlSession3更新資料庫,並提交事務後,sqlsession2的StudentMapper namespace下的查詢走了資料庫,沒有走Cache。

實驗4

驗證Mybatis的二級快取不適應用於對映檔案中存在多表查詢的情況。一般來說,我們會為每一個單表建立一個單獨的對映檔案,如果存在涉及多個表的查詢的話,由於Mybatis的二級快取是基於namespace的,多表查詢語句所在的namspace無法感應到其他namespace中的語句對多表查詢中涉及的表進行了修改,引發髒資料問題。

@Test
public void testCacheWithDiffererntNamespace() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        SqlSession sqlSession3 = factory.openSession(true); 
    
        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);
        
        System.out.println("studentMapper讀取資料: " + studentMapper.getStudentByIdWithClassInfo(1));
        sqlSession1.close();
        System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentByIdWithClassInfo(1));

        classMapper.updateClassName("特色一班",1);
        sqlSession3.commit();
        System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentByIdWithClassInfo(1));
}

執行結果:

 

在這個實驗中,我們引入了兩張新的表,一張class,一張classroom。class中儲存了班級的id和班級名,classroom中儲存了班級id和學生id。我們在StudentMapper中增加了一個查詢方法getStudentByIdWithClassInfo,用於查詢學生所在的班級,涉及到多表查詢。在ClassMapper中添加了updateClassName,根據班級id更新班級名的操作。當sqlsession1的studentmapper查詢資料後,二級快取生效。儲存在StudentMapper的namespace下的cache中。當sqlSession3的classMapper的updateClassName方法對class表進行更新時,updateClassName不屬於StudentMapper的namespace,所以StudentMapper下的cache沒有感應到變化,沒有重新整理快取。當StudentMapper中同樣的查詢再次發起時,從快取中讀取了髒資料。

實驗5

為了解決實驗4的問題呢,可以使用Cache ref,讓ClassMapper引用StudenMapper名稱空間,這樣兩個對映檔案對應的Sql操作都使用的是同一塊快取了。
執行結果:

 

不過這樣做的後果是,快取的粒度變粗了,多個Mapper namespace下的所有操作都會對快取使用造成影響,其實這個快取存在的意義已經不大了。

總結

  1. Mybatis的二級快取相對於一級快取來說,實現了SqlSession之間快取資料的共享,同時粒度更加的細,能夠到Mapper級別,通過Cache介面實現類不同的組合,對Cache的可控性也更強。
  2. Mybatis在多表查詢時,極大可能會出現髒資料,有設計上的缺陷,安全使用的條件比較苛刻。
  3. 在分散式環境下,由於預設的Mybatis Cache實現都是基於本地的,分散式環境下必然會出現讀取到髒資料,需要使用集中式快取將Mybatis的Cache介面實現,有一定的開發成本,不如直接用Redis,Memcache實現業務上的快取就好了
  4. 最終的結論是Mybatis的快取機制設計的不是很完善,在使用上容易引起髒資料問題,個人建議不要使用Mybatis快取,在業務層面上使用其他機制實現需要的快取功能,讓Mybatis老老實實做它的ORM框架就好了。