1. 程式人生 > >Mybatis的二級快取、使用Redis做二級快取

Mybatis的二級快取、使用Redis做二級快取

[Toc] ## 什麼是二級快取? > 二級快取和一級快取的原理是一樣的,第一次查詢,會將資料放入快取中,然後第二次查詢則會直接去快取中取。但是一級快取是基於的**sqlSession**,而二級快取是基於**mapper檔案的namespace**的,也就是說**多個sqlSession**可以**共享**一個mapper中的二級快取區域,並且如何兩個mapper的namespace相同,即使兩個mapper,那這兩個mapper中執行sql查詢到的資料也將存在相同的二級快取區域中 ![image-20201111211828145](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111211828145.png) - 如上圖`sqlSession1`在查詢時會從`UserMapper的二級快取`中取,如果沒有則執行資料庫查詢操作。 - 然後寫入到二級快取中 - `sqlSession2`則執行同樣的`UserMapper`查詢時,會從`UserMapper的二級快取`中取,此時的二級快取中已經有內容了,所以就可以直接取到,不再與資料庫互動。 - `sqlSession3`在執行事務操作(插入、更新、刪除)時,會清空`UserMapper`的二級快取 ### 1. 開啟二級快取 #### 如何使用二級快取: mybatis中,一級快取是預設開啟的,但是二級快取需要配置才可以使用 1. 在全域性配置檔案`sqlMapConfig.xml`中加入如下程式碼: ``` xml
``` 2. 其次在哪個namespace中開啟二級就在哪裡配置,因為mybatis有註解和xml兩種方式所以: - 註解 ![image-20201111221707937](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111221707937.png) 註解擴充套件: ``` java //我們預設使用的是mybatis自帶的二級快取,它的實現在PerpetualCache類中,所以可以寫成 @CacheNamespace(implementation = PerpetualCache.class) //如果是使用redis作為二級快取的話,下面第二部分會講到 ``` - xml ![image-20201111221501041](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111221501041.png) 這樣就開啟了`UserMapper`的二級快取 3. 測試一: 我們要根據使用者id查詢使用者資訊: ![image-20201111222635859](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111222635859.png) **注意:**將快取的pojo實現`Serializable`介面,為了將快取資料取出執行反序列化操作,因為二級快取的儲存介質多種多樣,不一定只在記憶體中,也可能在硬碟中,如果我們要再取出這個快取的話,就需要反序列化了。所以mybatis的pojo都去實現`Serializable`介面 ![image-20201111222950505](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111222950505.png) 最後執行看到列印日誌: ![image-20201111223421674](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111223421674.png) **為什麼`System.out.println(user1==user2)`為false ?** >
**二級快取和一級快取不同,二級快取快取的不是物件**,而是資料,在第二次查詢時底層重新建立了一個User物件,並且把二級快取中的資料重新封裝成了物件並返回。所以user1和user2不是一個物件。 4. 測試二: 我們在測試二中進行一下事務操作,看看是否能清空二級快取: ![image-20201111224352601](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111224352601.png) ![image-20201111224330557](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111224330557.png) ​ 增加了一個修改操作,發現執行了兩個`select`,說明提交事務會重新整理二級快取 #### userCache和flushCache 還可以配置`userCache`和`flushCache` - userCache : 是用來設定是否禁用二級快取的,在statement中設定可以**禁用**當前**select語句**的二級快取,即每次查詢都會發出sql。預設情況為true. ![image-20201111225137076](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111225137076.png) ![image-20201111225544937](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111225544937.png) - flushCache : 在mapper的同一個namespace中,如果有其它的增刪改操作後需要重新整理快取,如果部執行重新整理快取會出現髒讀。 設定statement配置中的flushCache="true",即重新整理快取,如果改成false則不會重新整理,有可能出現髒讀。所以一般情況下沒必要改 ![image-20201111225901338](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201111225901338.png) >
Mybatis二級快取和一級快取一樣也是使用到了`org.apache.ibatis.cache.impl.PerpetualCache` > > 這個類是mybatis的預設快取類,同時,想要自定義快取必須實現`cache`介面 ![image-20201112005725676](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201112005725676.png) ### 2. 使用Redis實現二級快取 Mybatis自帶的二級快取是有缺點的,就是這個快取是單伺服器進行工作的,無法實現分散式快取。 ![image-20201112010039059](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201112010039059.png) 所以為了解決這個問題,必須找一個分散式快取專門存放快取資料。 ![image-20201112010027075](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201112010027075.png) #### 如何使用 mybatis提供了一個針對cache介面的redis實現類,在`mybatis-redis`包中 1. 首先我們引入jar包 ``` xml org.mybatis.caches mybatis-redis 1.0.0-beta2 ``` 2. 修改Mapper.xml檔案 ``` java //**********XML方式***********: //表示針對於當前的namespace開啟二級快取 ``` ```java //*******註解方式********** @CacheNamespace(implementation = RedisCache .class) public interface UserMapper { //根據id查詢使用者 註解使用 @Select("select * from user where id=#{id}") public User findById(Integer id); ``` 這個類同樣實現了`Cache`介面 ![image-20201112011613642](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201112011613642.png) 3. 配置redis的配置檔案 ``` redis.host=localhost redis.port=6379 redis.connectionTimeout=5000 redis.password= redis.database=0 ``` 測試方法同自帶的二級快取一樣。 ### 3. Redis二級快取原始碼分析 RedisCache和Mybatis二級快取的方案都差不多,無非是實現Cache介面,並使用jedis操作快取,不過在設計細節上有點區別。 我們帶著問題分析原始碼: - 在RedisCache類中如何向redis中進行快取值的存取 ? - 使用了哪種資料結構 ? ``` java package org.mybatis.caches.redis; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import org.apache.ibatis.cache.Cache; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; //首先其實現了Cache介面,被mybatis初始化的時候的CacheBuilder建立 //建立方式就是呼叫了下面的有參構造 public final class RedisCache implements Cache { private final ReadWriteLock readWriteLock = new DummyReadWriteLock(); private String id; private static JedisPool pool; //有參構造 public RedisCache(final String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } this.id = id; //RedisConfigurationBuilder呼叫parseConfiguration()方法建立RedisConfig物件 RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration(); //構建Jedis池 pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName()); } //模板方法,下面的putObject和getObject、removeObject都會用到這個方法 private Object execute(RedisCallback callback) { Jedis jedis = pool.getResource(); try { return callback.doWithRedis(jedis); } finally { jedis.close(); } } //。。。。。。。。省略部分程式碼 @Override public void putObject(final Object key, final Object value) { execute(new RedisCallback() { @Override public Object doWithRedis(Jedis jedis) { jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value)); return null; } }); } @Override public Object getObject(final Object key) { return execute(new RedisCallback() { @Override public Object doWithRedis(Jedis jedis) { return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes())); } }); } @Override public Object removeObject(final Object key) { return execute(new RedisCallback() { @Override public Object doWithRedis(Jedis jedis) { return jedis.hdel(id.toString(), key.toString()); } }); } } ``` 1. `RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();` ![image-20201112014625885](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201112014625885.png) `RedisConfig`中封裝了預設的Redis配置資訊 ![image-20201112014920980](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201112014920980.png) 這個方法讀取了我們配置在`/resource/redis.properties`這個檔案 RedisConfig後構建了Jedis池 2. put方法 ``` java private Object execute(RedisCallback callback) { Jedis jedis = pool.getResource(); try { return callback.doWithRedis(jedis); } finally { jedis.close(); } } public void putObject(final Object key, final Object value) { execute(new RedisCallback() { @Override public Object doWithRedis(Jedis jedis) { jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value)); return null; } }); } ``` 我們可以看到,put方法呼叫了模板方法得到 一個jedis連結,然後呼叫doWithRedis()方法 ``` java jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value)); ``` > 可以很清楚的看到,mybatis-redis在儲存資料的時候,是使用的hash結構,把cache的id作為這個hash的key (cache的id在mybatis中就是mapper的namespace);這個mapper中的查詢快取資料作為 hash的field,需要快取的內容直接使用SerializeUtil儲存,SerializeUtil和其他的序列化類差不多,負責物件的序列化和反序