javascript
Spring Boot之基于Redis实现MyBatis查询缓存解决方案
轉(zhuǎn)載自?Spring Boot之基于Redis實(shí)現(xiàn)MyBatis查詢緩存解決方案
1. 前言
MyBatis是Java中常用的數(shù)據(jù)層ORM框架,筆者目前在實(shí)際的開發(fā)中,也在使用MyBatis。本文主要介紹了MyBatis的緩存策略、以及基于SpringBoot和Redis實(shí)現(xiàn)MyBatis的二級(jí)緩存的過程。實(shí)現(xiàn)本文的demo,主要依賴以下軟件版本信息,但是由于數(shù)據(jù)層面的實(shí)現(xiàn),并不依賴具體的版本,你可以以自己主機(jī)當(dāng)前的環(huán)境創(chuàng)建。
| SpringBoot | 1.5.18 |
| Redis | 通用 |
| MyBatis | 3.4.+ |
2. MyBatis緩存策略
2.1 一級(jí)緩存
MyBatis默認(rèn)實(shí)現(xiàn)了一級(jí)緩存,實(shí)現(xiàn)過程可參考下圖:
默認(rèn)基礎(chǔ)接口有兩個(gè):
-
org.apache.ibatis.session.SqlSession: 提供了用戶和數(shù)據(jù)庫(kù)交互需要的所有方法,默認(rèn)實(shí)現(xiàn)類是DefaultSqlSession。
-
org.apache.ibatis.executor.Executor: 和數(shù)據(jù)庫(kù)的實(shí)際操作接口,基礎(chǔ)抽象類BaseExecutor。
我們從底層往上查看源代碼,首先打開BaseExecutor的源代碼,可以看到Executor實(shí)現(xiàn)一級(jí)緩存的成員變量是PerpetualCache對(duì)象。
/*** @author Clinton Begin*/ public abstract class BaseExecutor implements Executor {private static final Log log = LogFactory.getLog(BaseExecutor.class);protected Transaction transaction;protected Executor wrapper;protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;// 實(shí)現(xiàn)一級(jí)緩存的成員變量protected PerpetualCache localCache;protected PerpetualCache localOutputParameterCache;protected Configuration configuration;... }我們?cè)俅蜷_PerpetualCache類的代碼:
/*** @author Clinton Begin*/ public class PerpetualCache implements Cache {private final String id;private Map<Object, Object> cache = new HashMap<Object, Object>();public PerpetualCache(String id) {this.id = id;}... }可以看到PerpetualCache是對(duì)Cache的基本實(shí)現(xiàn),而且通過內(nèi)部持有一個(gè)簡(jiǎn)單的HashMap實(shí)現(xiàn)緩存。
了解了一級(jí)緩存的實(shí)現(xiàn)后,我們?cè)倩氐饺肟谔?#xff0c;為了你的sql語(yǔ)句和數(shù)據(jù)庫(kù)交互,MyBatis首先需要實(shí)現(xiàn)SqlSession,通過DefaultSqlSessionFactory實(shí)現(xiàn)SqlSession的初始化的過程可查看:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// Executor初始化final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();} }從代碼中可以看到,通過configuration創(chuàng)建一個(gè)Executor,實(shí)際創(chuàng)建Executor的過程如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor 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);}// 是否開啟二級(jí)緩存// 如果開啟,使用CahingExecutor裝飾BaseExecutor的子類if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor; }注意,cacheEnabled字段是二級(jí)緩存是否開啟的標(biāo)志位,如果開啟,會(huì)使用使用CahingExecutor裝飾BaseExecutor的子類。
創(chuàng)建完SqlSession,根據(jù)Statment的不同,會(huì)使用不同的SqlSession查詢方法:
@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}SqlSession把具體的查詢職責(zé)委托給了Executor,如果只開啟了一級(jí)緩存的話,首先會(huì)進(jìn)入BaseExecutor的query方法。代碼如下所示:
@SuppressWarnings("unchecked") @Override 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 (closed) {throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;// 使用緩存list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482// 清空緩存clearLocalCache();}}return list; }query方法實(shí)現(xiàn)了緩存的查詢過程,在query方法執(zhí)行的最后,會(huì)判斷一級(jí)緩存級(jí)別是否是STATEMENT級(jí)別,如果是的話,就清空緩存,這也就是STATEMENT級(jí)別的一級(jí)緩存無法共享localCache的原因。
SqlSession的insert方法和delete方法,都會(huì)統(tǒng)一走update的流程,在BaseExecutor實(shí)現(xiàn)的update方法中:
@Override public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}// 清空緩存clearLocalCache();return doUpdate(ms, parameter); }可以看到,每次執(zhí)行update方法都會(huì)執(zhí)行clearLocalCache清空緩存。至此,我們分析完了MyBatis的一級(jí)緩存從入口到實(shí)現(xiàn)的過程。
關(guān)于MyBatis一級(jí)緩存的總結(jié):
-
一級(jí)緩存的生命周期和SqlSession保持一致;
-
一級(jí)緩存的緩存通過HashMap實(shí)現(xiàn);
-
一級(jí)緩存的作用域是對(duì)應(yīng)的SqlSession,假如存在多個(gè)SqlSession,寫操作可能會(huì)引起臟數(shù)據(jù)。
2.2 二級(jí)緩存
在上一小節(jié)中,我們知道一級(jí)緩存的的作用域就是對(duì)應(yīng)的SqlSession。若開啟了二級(jí)緩存,會(huì)使用CachingExecutor裝飾Executor,進(jìn)入一級(jí)緩存的查詢流程前,先在CachingExecutor進(jìn)行二級(jí)緩存的查詢,二級(jí)緩存的查詢流程如圖所示:
二級(jí)緩存開啟后,同一個(gè)namespace下的所有數(shù)據(jù)庫(kù)操作語(yǔ)句,都使用同一個(gè)Cache,即二級(jí)緩存結(jié)果會(huì)被被多個(gè)SqlSession共享,是一個(gè)全局的變量。當(dāng)開啟二級(jí)緩存后,數(shù)據(jù)查詢的執(zhí)行流程就是二級(jí)緩存 -> 一級(jí)緩存 -> 數(shù)據(jù)庫(kù)。
二級(jí)緩的實(shí)現(xiàn)源碼,可以查看CachingExecutor類的query方法:
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 從MappedStatement中獲得在配置初始化時(shí)賦予的CacheCache cache = ms.getCache();if (cache != null) {// 判斷是否需要刷新緩存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {// 主要是用來處理存儲(chǔ)過程的ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")// 嘗試從tcm中獲取緩存的列表,會(huì)把獲取值的職責(zé)一路傳遞List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }在二級(jí)緩存查詢結(jié)束后,就會(huì)進(jìn)入一級(jí)緩存的執(zhí)行流程,可參考上一小節(jié)內(nèi)容。
關(guān)于二級(jí)緩存的總結(jié):
-
二級(jí)緩存是SqlSession之間共享,能夠做到mapper級(jí)別,并通過Cache實(shí)現(xiàn)緩存。
-
由于MyBatis的緩存都是內(nèi)存級(jí)的,在分布式環(huán)境下,有可能會(huì)產(chǎn)生臟數(shù)據(jù),因此可以考慮使用第三方存儲(chǔ)組件,如Redis實(shí)現(xiàn)二級(jí)緩存的存儲(chǔ),這樣的安全性和性能也會(huì)更高。
3. SpringBoot和Redis實(shí)現(xiàn)MyBatis二級(jí)緩存
MyBatis的默認(rèn)實(shí)現(xiàn)一級(jí)緩存的,二級(jí)緩存也是默認(rèn)保存在內(nèi)存中,因此當(dāng)分布式部署你的應(yīng)用時(shí),有可能會(huì)產(chǎn)生臟數(shù)據(jù)。通用的解決方案是找第三方存儲(chǔ)緩存結(jié)果,比如Ehcache、Redis、Memcached等。接下來,我們介紹下,使用Redis作為緩存組件,實(shí)現(xiàn)MyBatis二級(jí)緩存。
在實(shí)現(xiàn)二級(jí)緩存之前,我們假設(shè)你已經(jīng)實(shí)現(xiàn)了SpringBoot+MyBatis的構(gòu)建過程,如果還沒有,建議你先創(chuàng)建一個(gè)demo實(shí)現(xiàn)簡(jiǎn)單的CRUD過程,然后再查看本文解決二級(jí)緩存的問題。
3.1 增加Redis配置
首先在你的工程加入Redis依賴:
compile('org.springframework.boot:spring-boot-starter-data-redis')我使用的gradle,使用maven的同學(xué)可對(duì)應(yīng)查詢即可!
其次在配置文件中加入Redis的鏈接配置:
spring.redis.cluster.nodes=XXX:port,YYY:port這里我們使用的是Redis集群配置。
打開mybatis.xml配置文件,開啟二級(jí)緩存:
<setting name="cacheEnabled" value="true"/>增加Redis的配置類,開啟json的序列化:
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;/*** Created by zhaoyh on 2019-01-23** @author zhaoyh*/ @Configuration public class RedisConfig {/*** 重寫Redis序列化方式,使用Json方式:* 當(dāng)我們的數(shù)據(jù)存儲(chǔ)到Redis的時(shí)候,我們的鍵(key)和值(value)都是通過Spring提供的Serializer序列化到數(shù)據(jù)庫(kù)的。RedisTemplate默認(rèn)使用的是JdkSerializationRedisSerializer,StringRedisTemplate默認(rèn)使用的是StringRedisSerializer。* Spring Data JPA為我們提供了下面的Serializer:* GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。* 在此我們將自己配置RedisTemplate并定義Serializer。* @param redisConnectionFactory* @return*/@Bean(name = "redisTemplate")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 設(shè)置值(value)的序列化采用Jackson2JsonRedisSerializer。redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// 設(shè)置鍵(key)的序列化采用StringRedisSerializer。redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;} }3.2 實(shí)現(xiàn)MyBatis的Cache接口
org.apache.ibatis.cache.Cache接口是MyBatis通用的緩存實(shí)現(xiàn)接口,包括一級(jí)緩存和二級(jí)緩存都是基于Cache接口實(shí)現(xiàn)緩存機(jī)制。
創(chuàng)建MybatisRedisCache類,實(shí)現(xiàn)Cache接口:
import org.apache.ibatis.cache.Cache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.CollectionUtils; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;/*** Created by zhaoyh on 2019-01-22* MyBatis二級(jí)緩存配置* @author zhaoyh*/ public class MybatisRedisCache implements Cache {private static final Logger LOG = LoggerFactory.getLogger(MybatisRedisCache.class);/*** 默認(rèn)redis有效期* 單位分鐘*/private static final int DEFAULT_REDIS_EXPIRE = 10;/*** 注入redis*/private static RedisTemplate<String, Object> redisTemplate = null;/*** 讀寫鎖*/private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);/*** cache id*/private String id = null;/*** 構(gòu)造函數(shù)* @param id*/public MybatisRedisCache(final String id) {if (null == id) {throw new IllegalArgumentException("MybatisRedisCache Instance Require An Id...");}LOG.info("MybatisRedisCache: " + id);this.id = id;}/*** @return The identifier of this cache*/@Overridepublic String getId() {return this.id;}/*** @param key Can be any object but usually it is a {@link}* @param value The result of a select.*/@Overridepublic void putObject(Object key, Object value) {if (null != value) {LOG.info("putObject key: " + key.toString());// 向Redis中添加數(shù)據(jù),默認(rèn)有效時(shí)間是2小時(shí)redisTemplate.opsForValue().set(key.toString(), value, DEFAULT_REDIS_EXPIRE, TimeUnit.MINUTES);}}/*** @param key The key* @return The object stored in the cache.*/@Overridepublic Object getObject(Object key) {try {if (null != key) {LOG.info("getObject key: " + key.toString());return redisTemplate.opsForValue().get(key.toString());}} catch (Exception e) {LOG.error("getFromRedis: " + key.toString() + " failed!");}LOG.info("getObject null...");return null;}/*** As of 3.3.0 this method is only called during a rollback* for any previous value that was missing in the cache.* This lets any blocking cache to release the lock that* may have previously put on the key.* A blocking cache puts a lock when a value is null* and releases it when the value is back again.* This way other threads will wait for the value to be* available instead of hitting the database.** 刪除緩存中的對(duì)象** @param keyObject The key* @return Not used*/@Overridepublic Object removeObject(Object keyObject) {if (null != keyObject) {redisTemplate.delete(keyObject.toString());}return null;}/*** Clears this cache instance* 有delete、update、insert操作時(shí)執(zhí)行此函數(shù)*/@Overridepublic void clear() {LOG.info("clear...");try {Set<String> keys = redisTemplate.keys("*:" + this.id + "*");LOG.info("keys size: " + keys.size());for (String key : keys) {LOG.info("key : " + key);}if (!CollectionUtils.isEmpty(keys)) {redisTemplate.delete(keys);}} catch (Exception e) {LOG.error("clear failed!", e);}}/*** Optional. This method is not called by the core.** @return The number of elements stored in the cache (not its capacity).*/@Overridepublic int getSize() {Long size = (Long) redisTemplate.execute(new RedisCallback<Long>() {@Overridepublic Long doInRedis(RedisConnection connection) throws DataAccessException {return connection.dbSize();}});LOG.info("getSize: " + size.intValue());return size.intValue();}/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.** @return A ReadWriteLock*/@Overridepublic ReadWriteLock getReadWriteLock() {return this.readWriteLock;}public static void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {MybatisRedisCache.redisTemplate = redisTemplate;}由于redisTemplate是類變量,需要手動(dòng)注入,再創(chuàng)建一個(gè)配置類注入redisTemplate即可:
/*** Created by zhaoyh on 2019-01-22* @author zhaoyh*/ @Component public class MyBatisHelper {/*** 注入redis* @param redisTemplate*/@Autowired@Qualifier("redisTemplate")public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {MybatisRedisCache.setRedisTemplate(redisTemplate);} }3.3 mapper文件中加入二級(jí)緩存的聲明
在任意需要開啟二級(jí)緩存的mapper配置文件中,加入:
<!-- mapper開啟二級(jí)緩存 --> <cache type="XX.XX.MybatisRedisCache"><!-- 定義回收的策略 --><property name="eviction" value="LRU"/><!-- 配置一定時(shí)間自動(dòng)刷新緩存,單位是毫秒 --><property name="flushInterval" value="600000"/><!-- 最多緩存對(duì)象的個(gè)數(shù) --><property name="size" value="1024"/><!-- 是否只讀,若配置可讀寫,則需要對(duì)應(yīng)的實(shí)體類能夠序列化 --><property name="readOnly" value="false"/> </cache>至此,就完成了基于Redis的MyBatis二級(jí)緩存的配置。
4. FAQ
-
二級(jí)緩存相比較于一級(jí)緩存來說,粒度更細(xì),但是也會(huì)更不可控,安全使用二級(jí)緩存的條件很難。
-
二級(jí)緩存非常適合查詢熱度高且更新頻率低的數(shù)據(jù),請(qǐng)謹(jǐn)慎使用。
-
建議在生產(chǎn)環(huán)境下關(guān)閉二級(jí)緩存,使得MyBatis單純作為ORM框架即可,緩存使用其他更安全的策略。
總結(jié)
以上是生活随笔為你收集整理的Spring Boot之基于Redis实现MyBatis查询缓存解决方案的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2000元电脑配置推荐(电脑配置2000
- 下一篇: gradle idea java ssm