Note: This series of source code analysis is based on mybatis 3.5.6, the source code gitee warehouse warehouse address: funcy/mybatis .
This article will analyze the cache of mybatis from the perspective of source code.
mybatis
In the sql execution process analyzed earlier , CachingExecutor
two caches will be encountered along the way:
CachingExecutor#query(...)
When executing, it will first judge whether it exists in the cache, and call the specificExecutor
query when it does not exist.BaseExecutor#query(...)
When executed, it will belocalCache
obtained from it first, and will be queried from the database when it does not exist
Let's take the sql execution process in the previous article as an example, and analyze these two caches step by step.
1. mybatis
Level 1 cache
Let's get into the BaseExecutor#query(...)
method:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
queryStack++;
// 1. 直接从缓存中获取数据
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 #601
deferredLoads.clear();
// 2. 缓存范围为STATEMENT时,会清除缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
// 清除数据库中的记录
clearLocalCache();
}
}
return list;
}
复制代码
This method is a bit long, but we only need to focus on two places:
localCache
Get data directly from the cache ( )- When the cache range
STATEMENT
is , the cache will be cleared after each query, that is, the data will not be cached
1.1 What is the first level cache
First let's see what it localCache
is:
public abstract class BaseExecutor implements Executor {
...
/**
* 这就是 localCache
*/
protected PerpetualCache localCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
...
// 在这里赋值
this.localCache = new PerpetualCache("LocalCache");
}
...
复制代码
This localCache
is PerpetualCache
, we continue:
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
/**
* 设置缓存的操作
*/
@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();
}
...
}
复制代码
PerpetualCache
It is the processing of packaging Map
, and operations such as adding, acquiring, and removing caches are actually correct Map
operations.
Here we understand that the so-called first-level cache ( localCache
) is one Map
, and records are stored in memory. Note: here Map
is HashMap
, is not thread safe.
1.2 Update timing of the first level cache
In the BaseExecutor#query(...)
method, if the cache is not hit, it will be queried from the database. Let's take a look at the query process of the database:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 处理查询操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除一级缓存
localCache.removeObject(key);
}
// 设置一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
复制代码
代码可以看的很清楚了,在处理查询(doQuery(...)
)后,会手动移除一级缓存(localCache.removeObject(...)
),并设置一级缓存(localCache.putObject(...)
).
当然了,在update
操作中也有类似的处理缓存的操作,这里就不一一看了。
1.3 一级缓存的作用范围及禁用
一级缓存中,缓存数据的Map
是HashMap
,并非线程安全的,因此一级缓存并非线程安全的。那么它的作用范围呢?
通过代码的追溯,localCache
是Executor
的成员变量,而Executor
又是DefaultSqlSession
的成员变量。在前面的分析中,DefaultSqlSession
并不是线程安全的,比较合理的方式是为每个线程新建一个DefaultSqlSession
,至此可以推断出一级缓存(localCache
)的作用范围为SqlSession
,保存在内存中。
由于一级缓存的作用范围为SqlSession
,因此使用时可能会导致数据库更新了,但缓存还没变。
举例说明下:
// 配置文件路径
String resource = "org/apache/ibatis/demo/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
// 使用 sqlSession01 获取 userMapper
SqlSession sqlSession01 = null;
SqlSession sqlSession02 = null;
try {
sqlSession01 = factory.openSession(true);
sqlSession02 = factory.openSession(true);
UserMapper userMapper01 = sqlSession01.getMapper(UserMapper.class);
// 使用 sqlSession02 获取 userMapper
UserMapper userMapper02 = sqlSession02.getMapper(UserMapper.class);
// 处理查询与更新操作,顺序很重要
// 使用 userMapper01 查询
List<User> users = userMapper01.selectList(3L, 10);
System.out.println("userMapper01 得到的users: " + users);
// 使用 sqlSession02 更新
int result = userMapper02.updateUser(3L, "HelloWorld");
System.out.println("更新的记录数: " + result);
// 使用 sqlSession02 查询
List<User> users2 = userMapper02.selectList(3L, 10);
System.out.println("userMapper02 得到的users: " + users2);
// 使用 sqlSession01 查询
List<User> users01 = userMapper01.selectList(3L, 10);
System.out.println("userMapper01 得到的users: " + users01);
} finally {
// 省略关闭操作
...
}
复制代码
以上代码先是获取了两个SqlSession
,然后分别从这两个SqlSession
得到UserMapper
的实例userMapper01
、userMapper02
,然后我们使用这两个UserMapper
实例进行操作:
- 使用
userMapper01
查询id为3的记录 - 使用
userMapper02
更新id为3的记录 - 使用
userMapper02
查询id为3的记录 - 使用
userMapper01
查询id为3的记录
运行,得到的结果如下:
userMapper01 得到的users: [User{id=3, loginName='test', nick='test'}]
更新的记录数: 1
userMapper02 得到的users: [User{id=3, loginName='test', nick='HelloWorld'}]
userMapper01 得到的users: [User{id=3, loginName='test', nick='test'}]
复制代码
从运行结果来看,userMapper02
更新成功了,userMapper02
也能查到更新后数据了,但userMapper01
第二次查到的依然是更新前的记录,这也就是说,使用userMapper01
第一次查询时,记录被缓存了,第二次查询时,并没有查询数据库,而是直接从缓存里获取数据了。
从以上示例可以看到,开启一级缓存后,并不能查到实时数据,并且一级缓存并没有提供对外操作的入口,启用后极有可能会产生严重后果,那么我们要怎么关闭它呢?
在mybatis文档的settings
节点介绍中,有一个属性可以解决这个问题:
我们在配置文件中这样设置:
<configuration>
<properties resource="org/apache/ibatis/demo/config.properties">
</properties>
<settings>
<!-- 设置一级缓存级别 -->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
...
</configuration>
复制代码
这样设置之后,一级缓存就不再缓存数据了。关于这样做的原理,就得回到BaseExecutor#query(...)
方法了:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
if (queryStack == 0) {
...
// 2. 缓存范围为STATEMENT时,会清除缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清除数据库中的记录
clearLocalCache();
}
}
return list;
}
/**
* 清除缓存的操作
*/
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
复制代码
从源码来看,将localCacheScope
设置为STATEMENT
后,每次查询完成后,都会调用localCache.clear()
来清除缓存了。
2. mybatis
二级缓存
接下来我们再来看看二级缓存。
2.1 开启二级缓存
在创建执行器时,mybatis
在创建完具体的执行器后,会再缓存执行器:
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);
}
// 缓存装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 处理 plugin(插件)
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
复制代码
在之后处理sql的查询/更新操作时,都是调用CachingExecutor
进行,比如查询操作:
@Override
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) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从缓存中获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 实际处理查询的操作
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 添加到缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 不使用缓存,直接处理查询的操作
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码
这里的Cache cache = ms.getCache()
就是用来操作二级缓存的。默认情况下,二级缓存并未开启:
可以看到,cache
为 null
,表示并未启用二级缓存。那么该如何启用呢?mybatis文档告诉了我们启用方式:
我们在UserMapper.xml
中添加<cache/>
标签:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.demo.mapper.UserMapper">
<cache/>
...
</mapper>
复制代码
再次执行查询时,就会发现cache
已经不为null了:
2.2 配置二级缓存
在mybatis
文档中,二级缓存还有其他的配置,这里也一并贴出来:
这些参数本文就不细讲了,我们来看下这些参数是在哪里解析的。
<cache/>
标签定义在mapper.xml
文件中,我们当然就想到它应该是在解析mapper.xml
文件时解析到的,我们进入<mapper>
标签的解析方法XMLMapperBuilder#configurationElement
:
private void configurationElement(XNode context) {
try {
...
// 解析二级缓存标签
cacheElement(context.evalNode("cache"));
...
} catch (Exception e) {
...
}
}
复制代码
继续进入XMLMapperBuilder#cacheElement
方法:
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = 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();
// 构建Cache对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size,
readWrite, blocking, props);
}
}
复制代码
- 缓存的默认实现类为
PerpetualCache
,我们也可以指定type
为我们自己的实现类,下节再说明 - 缓存过期的默认策略为
LruCache
readOnly
默认值为false
blocking
默认值为false
size
默认值为null
flushInterval
默认值为null
我们继续进入MapperBuilderAssistant#useNewCache
看看cache
的构建过程:
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(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加缓存
configuration.addCache(cache);
currentCache = cache;
return cache;
}
复制代码
这个方法主要是调用CacheBuilder
来构建Cache
,我们进入CacheBuilder#build
方法:
public Cache build() {
// 设置默认的实现
setDefaultImplementations();
// 实例化,反射实例化
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 使用的是默认缓存才需要装饰
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 设置标准的装饰器,用来处理eviction,flushInterval,size,readOnly等配置
cache = setStandardDecorators(cache);
}
// 如果不是 PerpetualCache,不会处理eviction,flushInterval,size,readOnly等配置
else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
/**
* 设置缓存的默认实现类
*/
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
decorators.add(LruCache.class);
}
}
}
/**
* 处理标准装饰器
* @param cache
* @return
*/
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// 定其清理
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// 序列化
cache = new SerializedCache(cache);
}
// 日志
cache = new LoggingCache(cache);
// 同步
cache = new SynchronizedCache(cache);
// 阻塞
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException(...);
}
}
复制代码
默认情况下,缓存的实现类为PerpetualCache
,对于默认的缓存,实例化后,会根据配置的参数进行相关的装饰操作,如配置了clearInterval
参数,就会使用ScheduledCache
进行装饰:
@Override
public void putObject(Object key, Object object) {
clearWhenStale();
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
return clearWhenStale() ? null : delegate.getObject(key);
}
private boolean clearWhenStale() {
// 判断是否超时
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
复制代码
ScheduledCache
的功能是定期清理缓存,在put
、get
操作时,都会判断是否超时,如果超时了就会触发清理操作。
mybatis
的cache
实现及装饰器如下:
可以看到,真正的实现只有PerpetualCache
,其余的均为装饰器。
来看看得到的Cache
:
由于层层装饰,cache
的层次有点多,最底层的cache
为PerpetualCache
,这是真正干活的类,它的id
为org.apache.ibatis.demo.mapper.UserMapper
,也就是Mapper
接口的包名.类名
。
我们继续看看configuration.addCache(cache)
操作:
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
复制代码
这一步的操作就是把cache
保存到caches
中了。注意到caches
类型为StrictMap
,稍微看下它的定义:
protected static class StrictMap<V> extends HashMap<String, V> {
...
}
复制代码
就是HashMap
的子类了。
这一步得到的caches
的结果如下:
这一步得到cache
后,在后面解析select|insert|update|delete
语句时,会把得到的当前得到的cache
一并放入到MappedStatement
对象中,相关操作为MapperBuilderAssistant#addMappedStatement(...)
方法,就不过多分析了。
2.3 TransactionalCacheManager
再回到CachingExecutor#query(...)
方法,处理操作的方法如下:
public class CachingExecutor implements Executor {
/**
* 缓存管理
*/
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 这个就是 PerpetualCache 装饰后的对象
Cache cache = ms.getCache();
// 操作缓存
if (cache != null) {
// 从缓存中获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
...
// 添加到缓存中
tcm.putObject(cache, key, list);
}
...
}
...
}
...
}
复制代码
可以看到,得到cache
后,使用TransactionalCacheManager
进行操作,我们进入TransactionalCacheManager
看看相关方法:
public class TransactionalCacheManager {
// 也是一个Map,保存的是 TransactionalCache
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
/**
* 从transactionalCaches 中获取 TransactionalCache,如果不存在则新建
*/
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
...
}
复制代码
关于TransactionalCache
,它也是Cache
的实现类:
public class TransactionalCache implements Cache {
/**
* 真正的缓存操作类
*/
private final Cache delegate;
private final Set<Object> entriesMissedInCache;
...
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.entriesToAddOnCommit = new HashMap<>();
...
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
// 先添加到临时缓存中
entriesToAddOnCommit.put(key, object);
}
/**
* 处理事务的提交操作
* 事务提交时,才把临时缓存中的缓存添加到缓存中
*/
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 更新缓存
flushPendingEntries();
reset();
}
/**
* 更新缓存
*/
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
...
}
复制代码
这个类实现了如下功能:
- 它也是个包装类,实现了缓存的事务功能
- 添加对象时,会行添加到临时缓存中,当事务提交后,才会真正添加到二级缓存中
- 对象的移除也是类似的操作,只有当事务提交后才会真正操作二级缓存
2.4 使用自定义二级缓存
从上面的分析来看,mybatis
为每个mapper.xml
(namespace
)都创建了一个缓存对象(默认实现类为PerpetualCache
),与MappedStatement
绑定在一起,因此二级缓存的作用范围为全局,而且对于每个namespace
都对应一个缓存对象。
二级缓存的默认实现为PerpetualCache
,我们来看看它的内容:
public class PerpetualCache implements Cache {
...
// 用hashmap来保存记录
private final Map<Object, Object> cache = new HashMap<>();
@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();
}
...
}
复制代码
从代码来看,PerpetualCache
其中维护了一个名为cache
的成员变量,它是HashMap
类型的,缓存的添加
/获取
/清除
等都是操作这个HashMap
,mybatis
的二级缓存同样也是保存在内存中的!
HashMap
并不是线程安全的,我们在多线程环境下能使用二级缓存的默认实现(PerpetualCache
)吗?答案是可以,虽然HashMap
不是线程安全的,但经过缓存装饰器之后,二级缓存的添加
/获取
/清除
等操作就是线程安全了,这就是SynchronizedCache
的功劳:
public class SynchronizedCache implements Cache {
// 被装饰的对象,也是真正干活的对象
private final Cache delegate;
...
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}
...
}
复制代码
只是简单地在添加
/获取
/清除
等操作方法上加了synchronized
关键字,至于性能方面就呵呵了。
为了更好地发挥二级缓存的功能,mybatis
可以让开发者自定义二级缓存的实现:
注意,如果使用了自定义缓存,那么eviction
,flushInterval
,size
,readOnly
等配置将不会生效,这点在CacheBuilder#build
方法中也能得到体现:
在自定义缓存时,我们可以使用redis等分布式缓存来存储数据,这里就不多分析了。
2.5 何时使用二级缓存
分析完了二级缓存,什么情况下适合使用二级缓存(mybits
的默认实现)呢?
从前面的分析来看,二级缓存的默认实现有以下特点:
- 作用范围为全局(整个jvm进程)
- 保存在内存中(不支持分布式)
- 并发性能低(操作方法添加了
synchronized
关键字)
结合以上所述,mybatis
提供的二级缓存只 适合在单机、对并发要求不高的情况下使用。
需要注意的是,以上条件是针对mybats
提供的二级缓存默认实现,我们还可以自定义二级缓存,以实现其在分布式、高并发环境的使用条件。
3. 一二级缓存特性汇总
3.1 缓存的配置汇总
cacheEnabled
在 mybatis 配置文件中,有一个settings
节点,下面有一配置为cacheEnabled
:
Which cache is this cacheEnabled
on or off?
Let's go back to the Configuration#newExecutor(...)
method again:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
// 缓存装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
...
}
复制代码
This cacheEnabled
is the value settings
under the node cacheEnabled
. From the code point of view, when it is cacheEnabled
timefalse
, CachingExecutor
it will not CachingExecutor
be used to deal with the second-level cache, so the second-level cache cannot be used, even if the tag mapper.xml
is added , the second-level cache cannot be used. <cache/>
.
Is that level 1 cache still usable? From the previous analysis, the operation of the second-level cache is in BaseExecutor
, so it will not be affected here.
3.2 mybatis cache comparison
cache type | Scope | Save location | Is it thread safe? | performance | Whether to support distributed |
---|---|---|---|---|---|
Level 1 | session | Memory | no | high | no |
Level 2 (default implementation) | global | Memory | Yes | Low | no |
3.3 Reasonable use of mybatis cache
How to use the cache reasonably mybatis
? My suggestion is:
- For L1 cache,
localCacheScope
set toSTATEMENT
; - For the second-level cache, if the
mybatis
default one is used, it can be used in the case of a single machine and low concurrency requirements, and must not be used in the case of multiple machines; if you use a custom cache, you can freely implement the cache according to the usage scenario.
The link to the original text of this article: my.oschina.net/funcy/blog/… , limited to the author's personal level, there are inevitably mistakes in the text, welcome to correct me! Originality is not easy. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.