1. 程式人生 > >Mybatis深入原始碼分析之SqlSessionFactory二級快取原理分析

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: