先說兩句:
我們都知道Mybatis緩存分兩類: 一級緩存(同一個Session會話內) & 二級緩存(基於HashMap實現的以 namespace為范圍的緩存)
今天呢, 我們不談一級緩存, 我們來談一談 二級緩存, 通過查看Mybatis源碼發現, 他的二級緩存實現真的十分簡單, 默認的實現類是 org.apache.ibatis.cache.impl.PerpetualCache 這里貼一下他的源碼吧:

/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.cache.impl; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import org.apache.ibatis.cache.Cache; import org.apache.ibatis.cache.CacheException; /** * @author Clinton Begin */ public class PerpetualCache implements Cache { private String id; private Map<Object, Object> cache = new HashMap<Object, Object>(); public PerpetualCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public int getSize() { return cache.size(); } @Override public void putObject(Object key, Object value) { cache.put(key, value); } @Override public Object getObject(Object key) { return cache.get(key); } @Override public Object removeObject(Object key) { return cache.remove(key); } @Override public void clear() { cache.clear(); } @Override public ReadWriteLock getReadWriteLock() { return null; } @Override public boolean equals(Object o) { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } if (this == o) { return true; } if (!(o instanceof Cache)) { return false; } Cache otherCache = (Cache) o; return getId().equals(otherCache.getId()); } @Override public int hashCode() { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } return getId().hashCode(); } }
那么既然他都已經有一個實現了, 我們為什么還要自定義實現呢?
原因很簡單, 默認實現是(HashMap)本地緩存, 不支持分布式緩存, 而我們現在大多數項目都是以集群的方式部署, 這種情況下, 使用本地緩存會出現很嚴重的臟讀問題, 特定情況下更新可直接導致數據不一致的問題.
如果讓緩存實現支持分布式呢? 方案有很多, 基本圍繞着NoSQL數據庫實現, 最常用的就是Redis了, 接下來我們就來用Redis實現自定義緩存類
說干就干吧, 開始我們的自定義緩存實現之路
實現過程:
首先: Mybatis自定義實現緩存類的配置方式十分簡單, 我們只需要開啟二級緩存, 並在 mapper.xml 中修改如下配置:
其中 type 屬性值就是我們自定義的緩存實現辣!
接下來我們來看一看實現類內部是怎么寫的:

package com.cardgame.demo.game.component.db; import com.cardgame.demo.component.redis.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.cache.Cache; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME; /** * @author yjy * 2018-08-06 15:46 */ @Slf4j public class MybatisRedisCache implements Cache { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id; public MybatisRedisCache(final String id) { if (id == null) { throw new IllegalArgumentException("id can not be null"); } log.debug("MyBatisRedisCache:id=" + id); this.id = id; } @Override public String getId() { return this.id; } @Override public int getSize() { int size = (int) RedisUtils.getInstance().getHashSizeObj(getKey()); log.debug("MybatisCache getSize > {}", size); return size; } @Override public void putObject(Object key, Object value) { RedisUtils.getInstance().setHashObj(getKey(), key, value); log.debug("MybatisCache put > key: {}, val: {}", key, value); } @Override public Object getObject(Object key) { Object val = RedisUtils.getInstance().getHashObj(getKey(), key); log.debug("MybatisCache get > key: {}, val: {}", key, val); return val; } @Override public Object removeObject(Object key) { // 移除指定緩存 Object obj = RedisUtils.getInstance().getHashObj(getKey(), key); RedisUtils.getInstance().delHashObj(getKey(), key); log.debug("MybatisCache remove > key: {}, val: {}", key, obj); return obj; } @Override public void clear() { // 清除所有緩存 RedisUtils.getInstance().delObj(getKey()); log.debug("MybatisCache clear > hashKey : {}", getKey()); } @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } protected String getKey() { return REDIS_KEY_GAME + "mybatis_cache_" + id; } }
自定義緩存實現的要求很低, 只需要 實現 org.apache.ibatis.cache.Cache 就可以了. 這里我們用到了 RedisUtils 工具類, 還請同學們 對應到自己的項目中的工具類.
至此: 我們的Mybatis二級緩存就支持分布式啦!
問題來臨:
今天我偶然想起, 是否需要給二級緩存 設置一個過期時間?
我們來想一下不設置過期時間會有什么問題:
當有一天我們不得不手動修改數據庫的數據時(別問為啥要動數據庫, 因為我在本地測試需要修改), 如果相應的 namespace 沒有 插入和更新操作, 那么他的緩存將一直有有效, 然后查出來的數據一直是修改前的數據, 而且我們使用的Redis做的緩存, 即使重啟了系統緩存依然還是在, 只能從redis中找到指定緩存並清除, 實在是頭疼
有沒有辦法解決呢?
你可能已經想到了, cache標簽不是支持 flushInterval 屬性的嗎? 設置一個 flushInterval = "10000", 這樣不就行了嗎?
我開始也是這么認為的, 直到有一天我發現這個設置完全沒有起作用, 緩存一直是有效的, 那問題就來了, 為什么呢?
百思不得其解的我決定去瞄一瞄Mybatis的源碼, 最終讓我發現了其中的奧秘, 我們來看一下下面的代碼:
這是Mybatis初始化二級緩存中的一段代碼, 我們可以看到, flushInterval 屬性對於自定義實現類是不起作用的, 而 Mybatis 實現的緩存過期時間的原理則是利用 設計模式(裝飾器模式) 在默認的 緩存實現類上封裝了一層 ScheduleCache, 也正是此類實現了緩存的有效期設置.
完了完了, 那不能設置咋辦啊, 這不是坑爹嗎?
冷靜下來, 先別忙. 既然 不能通過設置解決, 那我們就自己想辦法解決吧
既然你Mybatis不幫我封裝 ScheduleCache, 那我就自己封裝一個 ScheduleRedisCache, 原有的MybatisRedisCache不變. 我們看一下 ScheduleRedisCache的代碼:

package com.cardgame.demo.game.component.db; import com.cardgame.demo.component.redis.RedisUtils; import org.apache.ibatis.cache.Cache; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME; /** * * 二級緩存 過期時間 實現 * * @author yjy * 2018-08-13 13:21 */ public class ScheduleRedisCache implements Cache { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id; private long flushInterval = 10000; // 緩存刷新時間, 單位毫秒 private Cache delegate; // 委派緩存類 public ScheduleRedisCache(String id) { if (id == null) { throw new IllegalArgumentException("id can not be null"); } this.id = id; this.delegate = new MybatisRedisCache(id); } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { // 記錄過期時間 long timeout = System.currentTimeMillis() + flushInterval; RedisUtils.getInstance().setHashObj(getKey(), key, timeout); delegate.putObject(key, value); } @Override public Object getObject(Object key) { Object timeout = RedisUtils.getInstance().getHashObj(getKey(), key); // if 未過期 if (timeout != null && (long) timeout > System.currentTimeMillis()) { // 更新過期時間 RedisUtils.getInstance().setHashObj(getKey(), key, System.currentTimeMillis() + flushInterval); return delegate.getObject(key); } return null; } @Override public Object removeObject(Object key) { RedisUtils.getInstance().delHashObj(getKey(), key); return delegate.removeObject(key); } @Override public void clear() { RedisUtils.getInstance().delObj(getKey()); delegate.clear(); } @Override public int getSize() { return (int) RedisUtils.getInstance().getHashSizeObj(getKey()); } @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } protected String getKey() { return REDIS_KEY_GAME + "schedule_cache_" + id; } }
代碼簡單易懂, 說白了就是增加一個 緩存管理, 專門 緩存二級緩存的 過期時間. 在適當的時候 更新過期時間 / 清除過期時間
需要注意的是, 既然我們封裝了 MybatisRedisCache, 那么 mapper.xml 中就需要 改一改了, 如下:
這樣, 我們就實現了 二級緩存的超時自動過期功能了!!!
結論: 謝謝大家!666