Mybatis深入原始碼分析之SqlSessionFactory二級快取原理分析
上篇內容回顧可以參考;Mybatis深入原始碼分析之SQLSession一級快取原理分析
這裡再概括下上篇原始碼分析內容:
一:Mybatis一級快取
mybatis的一級快取是SqlSession快取,在操作資料庫的時候需要先建立SQL會話物件,在物件中有個HashMap用於儲存快取資料,此HashMap是當前物件私有的,其它SqlSession會話物件無法訪問。
具體流程:
- 第一次執行select完畢會將查到的資料寫入SqlSession內的HashMap中快取起來
- 第二次執行select會從快取中查資料,如果select相同且傳引數一樣,那麼就能從快取中返回資料,不用去查詢資料庫了,從而提高程式執行效率。
注意事項:
- 如果SqlSession執行了DML操作(insert、update、delete),並commit了,那麼mybatis就會清空當前SqlSession快取中所有的快取資料,這樣可以保證快取中的快取資料永遠和資料庫中的一致,避免出現髒讀。
- 當一個SqlSession結束後,那麼它裡面的一級快取也就不存在了,Mybatis預設是開啟一級快取,不需要配置。
- mybatis快取是基於【namespzce:sql語句:引數】來進行快取的,意思就是:sqlSession的HashMap儲存快取資料時,是使用【namespace:sql:引數】作為key
- 注意伺服器叢集的時候,每個sqlSession都有自己獨立的快取且互不共享,所以在伺服器叢集的時候容易產生資料衝突問題。
如何禁止一級快取
- 方案1 在sql語句上 隨機生成 不同的引數 存在缺點:map集合可能爆 記憶體溢位的問題
- 方案2 開啟二級快取
- 方案3 使用sqlSession強制清除快取
- 方案4 建立新的sqlSession連線。
二:Mybatis二級快取SessionFactory
MyBatis使用Redis作為二級快取
二級快取預設是沒有開啟的,需要在setting全域性引數中配置開啟二級快取,還需要對實體類實現序列化介面
<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> <setting name="cacheEnabled" value="true"/> 開啟二級快取 </settings>
在mapper配置檔案中加入下面這段配置:表示快取淘汰策略,和指定快取型別
<cache eviction="LRU" type="org.mybatis.cache.MybatisRedisCache" />
在原始碼中,是如何解析配置的快取呢?下面我們找到這段原始碼:
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace != null && !namespace.equals("")) { this.builderAssistant.setCurrentNamespace(namespace); this.cacheRefElement(context.evalNode("cache-ref")); this.cacheElement(context.evalNode("cache")); //解析mapper配置檔案中配置的快取結點cache this.parameterMapElement(context.evalNodes("/mapper/parameterMap")); this.resultMapElements(context.evalNodes("/mapper/resultMap")); this.sqlElement(context.evalNodes("/mapper/sql")); this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } else { throw new BuilderException("Mapper's namespace cannot be empty"); } } catch (Exception var3) { throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3); } }
private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); //淘汰策略 Class<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);//使用Java的反射機制初始化 } }
this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = (new CacheBuilder(this.currentNamespace)).implementation((Class)this.valueOrDefault(typeClass, PerpetualCache.class)).addDecorator((Class)this.valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build(); this.configuration.addCache(cache); //將cache配置新增到configura中 this.currentCache = cache; return cache; }
測試程式碼
// 4.獲取Session SqlSession sqlSession = sqlSessionFactory.openSession(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); // 5.操作Mapper介面 UserMapper mapper = sqlSession.getMapper(UserMapper.class); System.out.println("第一次呼叫...."); UserEntity o = sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser", 1); //斷點① System.out.println(o.getName()); System.out.println("第二次呼叫...."); UserEntity o2 = sqlSession2.selectOne("com.mayikt.mapper.UserMapper.getUser", 1); //斷點② System.out.println(o2.getName());
public <T> T selectOne(String statement, Object parameter) { List<T> list = this.selectList(statement, parameter);//進入這裡 if (list.size() == 1) { return list.get(0); } else if (list.size() > 1) { throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; } }
public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT);//進入這裡 }
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { List var5; try { MappedStatement ms = this.configuration.getMappedStatement(statement);//拿到sql語句的物件 var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);//到這裡 } catch (Exception var9) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9); } finally { ErrorContext.instance().reset(); } return var5; }
因為我們配置了外接快取Redis快取所以先走CacheExecutor執行器(代表二級快取)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); //拿到SQL語句 CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql); //建立快取key return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);//建立快取key
執行query方法:
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { this.flushCacheIfRequired(ms); //快取不為空,重新整理快取配置,清除快取 if (ms.isUseCache() && resultHandler == null) { this.ensureNoOutParams(ms, parameterObject, boundSql); List<E> list = (List)this.tcm.getObject(cache, key); //查詢二級快取資料 if (list == null) { //第一次查詢肯定沒有。執行下面程式碼 list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); this.tcm.putObject(cache, key, list); } return list; } }
cache不為空:
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { this.tcm.clear(cache); //快取不為空,清空快取 } }
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
二級快取沒有資料,就執行BaseExecutor查詢一級快取資料
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (this.closed) { throw new ExecutorException("Executor was closed."); } else { if (this.queryStack == 0 && ms.isFlushCacheRequired()) { this.clearLocalCache(); } List list; try { ++this.queryStack; list = resultHandler == null ? (List)this.localCache.getObject(key) : null; //判斷一級快取是否有資料,這裡第一次查詢也是沒有的 if (list != null) { this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //進入這塊 list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); //先去查詢資料庫 } } finally { --this.queryStack; } if (this.queryStack == 0) { Iterator i$ = this.deferredLoads.iterator(); while(i$.hasNext()) { BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next(); deferredLoad.load(); } this.deferredLoads.clear(); if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { this.clearLocalCache(); } } return list; } }
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);//查詢資料庫
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER); //先使用佔位符去快取一個key,表示我現在要去查詢資料庫了 List list; try { list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql); //去查詢資料庫資料 } finally { this.localCache.removeObject(key); //將之前快取的佔位符key刪除了 } this.localCache.putObject(key, list); //再將查詢到的資料快取到一級快取中 if (ms.getStatementType() == StatementType.CALLABLE) { this.localOutputParameterCache.putObject(key, parameter); } return list; }
this.tcm.putObject(cache, key, list);再把一級快取資料放入二級快取中
發現第二次查詢的時候:二級快取還是沒有資料,這是為什麼?
我們地清除地知道,每次呼叫openSession地時候,開啟了二級快取,每次都會new CacheExecutor執行器(二級快取)
Executor executor = this.configuration.newExecutor(tx, execType);
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? this.defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Object executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (this.cacheEnabled) { executor = new CachingExecutor((Executor)executor); //每次開啟了二級快取,都會器new 一個CachingExecutor二級快取執行器 } Executor executor = (Executor)this.interceptorChain.pluginAll(executor); return executor; }
我們可以知道了:每次new一個CachingExecutor二級快取執行器,都會new TransactionalCacheManager()
所以:TransactionalCacheManager管理我們地TransactionalCache和SQLSession繫結
我們執行下程式碼:發現執行了兩次SQL查詢
第一次呼叫....
11:08:35.474 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>getObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=null
Cache Hit Ratio [com.mayikt.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 294184992.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1188e820]
==> Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, update_time
<== Row: 1, xuyu, 2019-03-13 14:27:49.0
<== Total: 1
xuyu
第二次呼叫....
11:08:35.704 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>getObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=null
Cache Hit Ratio [com.mayikt.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 1268959798.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4ba2ca36]
==> Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, update_time
<== Row: 1, xuyu, 2019-03-13 14:27:49.0
<== Total: 1
xuyu
假如我們在查詢中間新增:
sqlSession.close();
結果如下:我們發現,第一次查詢了資料庫,第二次直接走快取了,沒有再去查詢資料庫,快取生效了,這是為什麼呢?下面我們開始原始碼分析
第一次呼叫....
11:10:06.121 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>getObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=null
Cache Hit Ratio [com.mayikt.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 294184992.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1188e820]
==> Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, update_time
<== Row: 1, xuyu, 2019-03-13 14:27:49.0
<== Total: 1
xuyu
11:10:06.350 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>putObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=[com.mayikt.entity.UserEntity@1dd02175]
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1188e820]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1188e820]
Returned connection 294184992 to pool.
第二次呼叫....
11:10:06.404 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>getObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=[com.mayikt.entity.UserEntity@2357d90a]
Cache Hit Ratio [com.mayikt.mapper.UserMapper]: 0.5
xuyu
重點來看下這段程式碼:
先看下tcm指什麼?指的是TransactionalCacheManager,作為二級快取查詢
public Object getObject(Cache cache, CacheKey key) { return this.getTransactionalCache(cache).getObject(key); }
public Object getObject(Object key) { Object object = this.delegate.getObject(key); if (object == null) { this.entriesMissedInCache.add(key); } return this.clearOnCommit ? null : object; }
private Set<Object> entriesMissedInCache;//代表一級快取存放資料到二級快取資料
下面我們來debug原始碼分析下:
List<E> list = (List)this.tcm.getObject(cache, key);
public Object getObject(Cache cache, CacheKey key) { return this.getTransactionalCache(cache).getObject(key); }
private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = (TransactionalCache)this.transactionalCaches.get(cache); // if (txCache == null) { txCache = new TransactionalCache(cache); //二級快取沒有資料,就建立一個新的TransactionalCache this.transactionalCaches.put(cache, txCache); //把快取資料新增到TransactionalCache裡 } return txCache; }
//transactionalCaches為map集合
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap();
這個cache指的是我們自定義Redis二級快取
最後回到getObject方法:
public Object getObject(Object key) { Object object = this.delegate.getObject(key); if (object == null) { this.entriesMissedInCache.add(key); } return this.clearOnCommit ? null : object; }
public Object getObject(Object key) { ++this.requests; //記錄每次查詢次數 Object value = this.delegate.getObject(key); if (value != null) { ++this.hits; } if (this.log.isDebugEnabled()) { this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio()); } return value; }
由上圖可知,開始呼叫我們自定義的MybatisRedisCache外接快取方法
public Object getObject(Object key) { Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString()))); logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value); return value; }
我們知道了,我們第一次查詢,二級快取是沒有資料的,最後進入entriesMissedInCache去新增我們的快取資料
最後返回到
再去一級快取查詢資料
一級快取沒有資料,就查詢資料庫,查詢到資料,將結果返回給一級快取,一級快取再把資料快取給二級快取
public void putObject(Cache cache, CacheKey key, Object value) { this.getTransactionalCache(cache).putObject(key, value); }
就回去呼叫getTransactionalCache方法建立事務的快取
private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = (TransactionalCache)this.transactionalCaches.get(cache); if (txCache == null) { txCache = new TransactionalCache(cache); this.transactionalCaches.put(cache, txCache); } return txCache; }
呼叫
public void putObject(Object key, Object object) { this.entriesToAddOnCommit.put(key, object); }
private Map<Object, Object> entriesToAddOnCommit;
我們知道了entriesToAddOnCommit集合為臨時儲存的事務快取(一級快取資料新增到二級快取先新增到entriesToAddOnCommit集合臨時快取起來)
List<E> list = (List)this.tcm.getObject(cache, key);//會從redis中獲取快取資料
this.tcm.putObject(cache, key, list);//只是將資料存放到getTransactionalCache的entriesToAddOnCommit的map集合中
那什麼時候去取出getTransactionalCache中的快取資料呢?下面我們程式碼進入sqlSession.close()方法
public void close(boolean forceRollback) { try { if (forceRollback) { //是否需要強制回滾,我們是不需要的 this.tcm.rollback(); } else { this.tcm.commit(); //這塊重點了,表示要提交資料到redis了 } } finally { this.delegate.close(forceRollback); } }
public void commit() { Iterator i$ = this.transactionalCaches.values().iterator(); while(i$.hasNext()) { //迴圈迭代TransactionalCache TransactionalCache txCache = (TransactionalCache)i$.next(); txCache.commit(); } }
public void commit() { if (this.clearOnCommit) { this.delegate.clear(); } this.flushPendingEntries(); this.reset(); }
private void flushPendingEntries() { Iterator i$ = this.entriesToAddOnCommit.entrySet().iterator(); while(i$.hasNext()) { Entry<Object, Object> entry = (Entry)i$.next(); this.delegate.putObject(entry.getKey(), entry.getValue()); //遍歷entriesToAddOnCommit,把資料重新整理到redis中 } i$ = this.entriesMissedInCache.iterator(); while(i$.hasNext()) { Object entry = i$.next(); if (!this.entriesToAddOnCommit.containsKey(entry)) { this.delegate.putObject(entry, (Object)null); } } }
public void putObject(Object key, Object value) { logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value); redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value)); }
我們可知:最後將臨時快取資料提交到了redis快取中
再次查詢
就直接從redis快取中取出資料了。
結果:
第一次呼叫....
12:57:19.090 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>getObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=null
Cache Hit Ratio [com.mayikt.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 294184992.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1188e820]
==> Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, update_time
<== Row: 1, xuyu, 2019-03-13 14:27:49.0
<== Total: 1
xuyu
12:57:19.318 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>putObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=[com.mayikt.entity.UserEntity@1dd02175]
第二次呼叫....
12:57:19.360 [main] DEBUG com.mayikt.cache.MybatisRedisCache - >>>>>>>>>>>>>>>>>>>>>>>>getObject:-978696591:1452564226:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select * from user where id=?:1:development=[com.mayikt.entity.UserEntity@2357d90a]
Cache Hit Ratio [com.mayikt.mapper.UserMapper]: 0.5
xuyu
我們思考下,我們要使得快取生效,每次都要呼叫close()方法,這樣不是很麻煩?
如果使用相同的sqlsession快取,那麼也是查詢一次,但是使用的快取只是一級快取,那麼我們有沒有辦法呼叫同一個sqlsession,也使得二級快取生效。
sqlSession.close(); //sqlSession.commit();
我們先看下二級快取回收策略
- LRU:最近最少使用的策略,移除最長時間不被使用的物件。
- FIFO:先進先出策略,按物件進入快取的順序來移除它們。
- SOFT:軟引用策略,移除基於垃圾回收器狀態和軟引用規則的物件。
- WEAK:弱引用策略,更積極地移除基於垃圾收集器狀態和弱引用規則的物件。
軟引用與弱引用的區別:
- 軟引用: 軟引用是用來描述一些有用但並不是必需的物件, 對於軟引用關聯著的物件,只有在記憶體不足的時候JVM才會回收該物件
- 弱引用: 弱引用也是用來描述非必需物件的,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件
回到我們實際springboot專案中 整合redis快取
mapper層:
@CacheNamespace(implementation = MybatisRedisCache.class) public interface OrderMapper { @Insert("insert order_info values (null,#{orderName},#{orderDes})") public int addOrder(OrderEntity OrderEntity); @Select("SELECT * FROM order_info;") public List<OrderEntity> findByOrder(); }
redis
@Component public class RedisToken { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 獲取Token */ public String getToken() { //1. 使用uuid生成Token String token = UUID.randomUUID().toString().replace("-", ""); //2. 將Token存放到Redis中 setString(token, token, 7200l); return token; } public Boolean findByToken(String token) { if (StringUtils.isEmpty(token)) { return false; } String redisToken = getString(token); if(StringUtils.isEmpty(redisToken)){ return false; } delKey(redisToken); return true; } private void setString(String key, Object data, Long timeout) { if (data instanceof String) { String value = (String) data; stringRedisTemplate.opsForValue().set(key, value); } if (timeout != null) { stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } } private String getString(String key) { return stringRedisTemplate.opsForValue().get(key); } private void delKey(String key) { stringRedisTemplate.delete(key); } }
MybatisRedisCache
** * mybatis二級快取整合Redis */ public class MybatisRedisCache implements Cache { private static Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class); private Jedis redisClient = createReids(); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id; public MybatisRedisCache(final String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id); this.id = id; } public String getId() { return this.id; } public int getSize() { return Integer.valueOf(redisClient.dbSize().toString()); } public void putObject(Object key, Object value) { logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value); redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value)); } public Object getObject(Object key) { Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString()))); logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value); return value; } public Object removeObject(Object key) { return redisClient.expire(SerializeUtil.serialize(key.toString()), 0); } public void clear() { redisClient.flushDB(); } public ReadWriteLock getReadWriteLock() { return readWriteLock; } protected static Jedis createReids() { JedisPool pool = new JedisPool("127.0.0.1", 6379); return pool.getResource(); } }
啟動類
@SpringBootApplication @MapperScan("com.mayikt.api.mapper") @EnableCaching public class OrderApp { public static void main(String[] args) { SpringApplication.run(OrderApp.class); } }
訪問:http://127.0.0.1:8080/getOrderList
最終被SqlSessionInterceptor攔截器攔截了, sqlSession.commit(true);也被呼叫了
private class SqlSessionInterceptor implements InvocationHandler { private SqlSessionInterceptor() { } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); Object unwrapped; try { Object result = method.invoke(sqlSession, args); if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { sqlSession.commit(true); } unwrapped = result; } catch (Throwable var11) { unwrapped = ExceptionUtil.unwrapThrowable(var11); if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); sqlSession = null; Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped); if (translated != null) { unwrapped = translated; } } throw (Throwable)unwrapped; } finally { if (sqlSession != null) { SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); } } return unwrapped; } }
提交資料
public void commit(boolean force) { try { this.executor.commit(this.isCommitOrRollbackRequired(force)); this.dirty = false; } catch (Exception var6) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + var6, var6); } finally { ErrorContext.instance().reset(); } }
private boolean isCommitOrRollbackRequired(boolean force) { return !this.autoCommit && this.dirty || force; }
public void commit() { Iterator i$ = this.transactionalCaches.values().iterator(); while(i$.hasNext()) { TransactionalCache txCache = (TransactionalCache)i$.next(); txCache.commit(); } }
public void commit() { if (this.clearOnCommit) { this.delegate.clear(); } this.flushPendingEntries(); this.reset(); }
private void flushPendingEntries() { Iterator i$ = this.entriesToAddOnCommit.entrySet().iterator(); while(i$.hasNext()) { Entry<Object, Object> entry = (Entry)i$.next(); this.delegate.putObject(entry.getKey(), entry.getValue()); } i$ = this.entriesMissedInCache.iterator(); while(i$.hasNext()) { Object entry = i$.next(); if (!this.entriesToAddOnCommit.containsKey(entry)) { this.delegate.putObject(entry, (Object)null); } } }
public void putObject(Object key, Object value) { logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value); redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value)); }
看下redis是否有資料了。
這裡最終會把當前session關閉掉。
最後,我們來總結下上面的原始碼分析
總結:
TransactionalCache
繼承自Cache介面,主要作用是儲存SqlSession在事務中需要向某個二級快取提交的快取資料
因為在事務過程中的資料可能會回滾,所以不能直接把資料提交給二級快取,而是暫存於TransactionalCache中,在事務提交後再將存放在其中的資料提交給二級快取,如果事務回滾,則將資料清除掉。
TransactionalCacheManager
- 用於管理CacheExecutor使用的二級快取物件,只定義了一個transactionalCaches欄位
- private final Cache delegate; //對應的二級快取物件
- private boolean clearOnCommit; //是否在commit時清除二級快取的標記
- // 需要在commit時提交到二級快取的資料
- private final Map<Object, Object> entriesToAddOnCommit;
- // 快取未命中的資料,事務commit時,也會放入二級快取(key,null)
- private final Set<Object> entriesMissedInCache;
StatementHandler
StatementHandler介面的實現大致有四個,其中三個實現類都是和JDBC中的Statement響對應的:
- SimpleStatementHandler,這個很簡單了,就是對應我們JDBC中常用的Statement介面,用於簡單SQL的處理; 存在sql注入攻擊問題
- PreparedStatementHandler,這個對應JDBC中的PreparedStatement,預編譯SQL的介面;防止sql注入
- CallableStatementHandler,這個對應JDBC中CallableStatement,用於執行儲存過程相關的介面;
- RoutingStatementHandler,這個介面是以上三個介面的路由,沒有實際操作,只是負責上面三個StatementHandler的建立及呼叫。
ResultSetHandler
就是將Statement例項執行之後返回的ResultSet結果集轉換成我們需要的List結果集
一級快取與二級快取區別
①、一級快取是SqlSession級別的快取。在操作資料庫時需要構造sqlSession物件,在物件中有一個數據結構(HashMap)用於儲存快取資料。不同的sqlSession之間的快取資料區域(sqlHashMap)是互相不影響的。
②、二級快取是mapper級別的快取,多個SqlSession去操作同一個Mapper的語句,多個SqlSession可以共用二級快取,二級快取是跨SqlSession的。
注意:sqlSession快取底層存線上程安全問題。
本文參考:
螞蟻課堂:http: