Mybatis缓存机制

MyBatis是常见的 Java数据库访问层框架。在日常工作中,多数情况下是使用 MyBatis的默认缓存配置减轻数据库压力,提高数据库性能,但是 MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。

一、一级缓存

LocalCache也被称为一级缓存,有如下特点:

  • 它的生命周期与 SqlSession一致。
  • 底层用 HashMap实现,没有缓存内容更新和过期。
  • 有多个 SqlSession时,且有数据库写,会出现脏读的情况,一级缓存慎用,或者将 Scope设置为 Statement

1.1 介绍

在应用运行过程中,有可能在一次数据库会话中,执行多次查询条件完全相同的 SQLMyBatis提供了一级缓存的方案优化这部分场景,如果是相同的 SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

Mybatis缓存机制

每个 SqlSession中持有了 Executor,每个 Executor中有一个 LocalCache。当用户发起查询时, MyBatis根据当前执行的语句生成 MappedStatement,在 LocalCache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 LocalCache,最后返回结果给用户。具体实现类的类关系图如下图所示。

Mybatis缓存机制

1.2 配置

只需在 MyBatis的配置文件中,添加以下语句,就可以使用一级缓存。


springboot配置如下

mybatis:
  configuration:
    cache-enabled: false  #禁用二级缓存
    local-cache-scope: session  #一级缓存指定为session级别

一级缓存无法关闭,但是 LocalCacheScope共有两个选项:

  • session(默认),在同一个 sqlSession内,对同样的查询将不再查询数据库,直接从缓存中获取。即在一个 MyBatis会话中执行的所有语句,都会共享这一个缓存。
  • statement,每次查询结束都会清掉一级缓存,实际效果就是禁用了一级缓存;可以理解为缓存只对当前执行的这一个 Statement有效。

1.3 案例

接下来通过案例,了解 MyBatis一级缓存的效果,每个单元测试后都请恢复被修改的数据。

首先是创建示例表 student,创建对应的 POJO类和增改的方法,具体可以在 entity包和 mapper包中查看。

CREATE TABLE student (
  id int(11) unsigned NOT NULL AUTO_INCREMENT,
  name varchar(200) COLLATE utf8_bin DEFAULT NULL,
  age tinyint(3) unsigned DEFAULT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

1.3.1 案例1

开启一级缓存,范围为 session级别,调用 getStudentById,代码如下所示:

public void getStudentById() throws Exception {
    SqlSession sqlSession = factory.openSession(true); // 自动提交事务
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    System.out.println(studentMapper.getStudentById(1));
    System.out.println(studentMapper.getStudentById(1));
}

执行结果:

StudentEntity{id=1,name='test',age=18}
StudentEntity{id=1,name='test',age=18}

我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。

1.3.2 案例2

增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。

@Test
public void addStudent() throws Exception {
    SqlSession sqlSession = factory.openSession(true); // 自动提交事务
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    System.out.println(studentMapper.getStudentById(1));
    System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
    System.out.println(studentMapper.getStudentById(1));
    sqlSession.close();
}

执行结果:

Mybatis缓存机制

我们可以看到,在修改操作后执行的相同查询,查询了数据库, 一级缓存失效

1.3.3 案例3

开启两个 SqlSession,在 sqlSession1中查询数据,使一级缓存生效,在 sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。

@Test
public void testLocalCacheScope() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true);
    SqlSession sqlSession2 = factory.openSession(true);

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1)
        + "个学生的数据");
    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

Mybatis缓存机制

sqlSession2更新了 id为1的学生的姓名,从凯伦改为了小岑,但 session1之后的查询中, id1的学生的名字还是凯伦,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话内部共享。

一级缓存失效的四种场景:

  • 场景一:SqlSeesion实例不同
  • 场景二:SqlSeesion实例相同,但查询条件不同
  • 场景三:SqlSeesion对象相同,查询条件也相同,但两次查询之间执行了增删改操作
  • 场景四:SqlSeesion对象相同,两次查询条件相同,中间无其它增删改操作,但使用了clearCache()方法

一级缓存失效的四种场景

1.4 工作流程&源码分析

1.4.1 工作流程

一级缓存执行的时序图,如下图所示。

Mybatis缓存机制

1.4.2 源码分析

接下来将对 MyBatis查询相关的核心类和一级缓存的源码进行解读。

SqlSession :对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是 DefaultSqlSession

Mybatis缓存机制

ExecutorSqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给 Executor

Mybatis缓存机制

如下图所示, Executor有若干个实现类,为 Executor赋予了不同的能力,大家可以根据类名,自行学习每个类的基本作用。

Mybatis缓存机制

在一级缓存的源码分析中,主要学习 BaseExecutor的内部实现。

BaseExecutorBaseExecutor是一个实现了 Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List doFlushStatements(boolean isRollback) throws SQLException;
protected abstract  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
        ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract  Cursor doQueryCursor(MappedStatement ms, Object parameter,
        RowBounds rowBounds, BoundSql boundSql) throws SQLException;

在一级缓存的介绍中提到对 LocalCache的查询和写入是在 Executor内部完成的。在阅读 BaseExecutor的代码后发现 LocalCacheBaseExecutor内部的一个成员变量,如下代码所示。

public abstract class BaseExecutor implements Executor {
    protected ConcurrentLinkedQueue deferredLoads;
    protected PerpetualCache localCache;
}

Cache:MyBatis中的Cache接口,提供了和缓存相关的最基本的操作,如下图所示:

Mybatis缓存机制

有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示:

Mybatis缓存机制

BaseExecutor成员变量之一的 PerpetualCache,是对 Cache接口最基本的实现,其实现非常简单,内部持有 HashMap,对一级缓存的操作实则是对 HashMap的操作。如下代码所示:

public class PerpetualCache implements Cache {
    private String id;
    private Map cache = new HashMap();
}

在阅读相关核心类代码后,从源代码层面对一级缓存工作中涉及到的相关代码,出于篇幅的考虑,对源码做适当删减,读者朋友可以结合本文,后续进行更详细的学习。

为执行和数据库的交互,首先需要初始化 SqlSession,通过 DefaultSqlSessionFactory开启 SqlSession

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
        boolean autoCommit) {
    ............

    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
}

在初始化 SqlSesion时,会使用 Configuration类创建一个全新的 Executor,作为 DefaultSqlSession构造函数的参数,创建 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);
    }
    // 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

SqlSession创建完毕后,根据Statment的不同类型,会进入 SqlSession的不同方法中,如果是 Select语句的话,最后会执行到 SqlSessionselectList,代码如下所示:

@Override
public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

SqlSession把具体的查询职责委托给了 Executor。如果只开启了一级缓存的话,首先会进入 BaseExecutorquery方法。代码如下所示:

@Override
public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds,
        ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在上述代码中,会先根据传入的参数生成 CacheKey,进入该方法查看 CacheKey是如何生成的,代码如下所示:

CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);

在上述的代码中,将 MappedStatementIdSQLoffsetSQLlimitSQL本身以及 SQL中的参数传入了 CacheKey这个类,最终构成 CacheKey。以下是这个类的内部结构:

private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;

private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List updateList;

public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList();
}

首先是成员变量和构造函数,有一个初始的 hachcode和乘数,同时维护了一个内部的 updatelist。在 CacheKeyupdate方法中,会进行一个 hashcodechecksum的计算,同时把传入的参数添加进 updatelist中。如下代码所示:

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
}

同时重写了 CacheKeyequals方法,代码如下所示:

@Override
public boolean equals(Object object) {
    .............

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
}

除去 hashcodechecksumcount的比较外,只要 updatelist中的元素一一对应相等,那么就可以认为是 CacheKey相等。只要两条 SQL的下列五个值相同,即可以认为是相同的 SQL

Statement Id + Offset + Limmit + Sql + Params

BaseExecutorquery方法继续往下走,代码如下所示:

list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
    // 这个主要是处理存储过程用的。
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

如果查不到的话,就从数据库查,在 queryFromDatabase中,会对 localcache进行写入。

query方法执行的最后,会判断一级缓存级别是否是 STATEMENT级别,如果是的话,就清空缓存,这也就是 STATEMENT级别的一级缓存无法共享 localCache的原因。代码如下所示:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    clearLocalCache();
}

在源码分析的最后,我们确认一下,如果是 insert/delete/update方法,缓存就会刷新的原因。

SqlSessioninsert方法和 delete方法,都会统一走 update的流程,代码如下所示:

@Override
public int insert(String statement, Object parameter) {
    return update(statement, parameter);
}
@Override
public int delete(String statement) {
    return update(statement, null);
}

update方法也是委托给了 Executor执行。 BaseExecutor的执行方法如下所示:

@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);
}

每次执行 update前都会清空 localCache。至此,一级缓存的工作流程讲解以及源码分析完毕。

1.5 小结

  1. MyBatis一级缓存的生命周期和 SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是 SqlSession内部,有多个 SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement

二、二级缓存

2.1 介绍

在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession内部,如果多个 SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。

Mybatis缓存机制

二级缓存开启后,同一个 namespace下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库

2.2 配置

要正确的使用二级缓存,需完成如下配置的。

  1. MyBatis的配置文件中开启二级缓存。

  1. MyBatis的映射 XML中配置 cache或者 cache-ref

cache标签用于声明这个 namespace使用二级缓存,并且可以自定义配置。


  • typecache使用的类型,默认是 PerpetualCache,这在一级缓存中提到过。
  • eviction:定义回收的策略,常见的有 FIFOLRU
  • flushInterval:配置一定时间自动刷新缓存,单位是毫秒。
  • size:最多缓存对象的个数。
  • readOnly:是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking:若缓存中找不到对应的 key,是否会一直 blocking,直到有对应的数据进入缓存。

cache-ref代表引用别的命名空间的 Cache配置,两个命名空间的操作使用的是同一个 Cache


2.3 案例

2.3.1 案例1

测试二级缓存效果,不提交事务, sqlSession1查询完数据后, sqlSession2相同的查询是否会从缓存中获取数据。

@Test
public void testCacheWithoutCommitOrClose() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true);
    SqlSession sqlSession2 = factory.openSession(true);

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

执行结果:

Mybatis缓存机制

我们可以看到,当 sqlsession没有调用 commit()方法时,二级缓存并没有起到作用。

2.3.2 案例2

测试二级缓存效果,当提交事务时, sqlSession1查询完数据后, sqlSession2相同的查询是否会从缓存中获取数据。

@Test
public void testCacheWithCommitOrClose() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true);
    SqlSession sqlSession2 = factory.openSession(true);

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    sqlSession1.commit();
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

Mybatis缓存机制

从图上可知, sqlsession2的查询,使用了缓存,缓存的命中率是0.5。

2.3.3 案例3

测试 update操作是否会刷新该 namespace下的二级缓存。

@Test
public void testCacheWithUpdate() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true);
    SqlSession sqlSession2 = factory.openSession(true);
    SqlSession sqlSession3 = factory.openSession(true);

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    sqlSession1.commit();
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

    studentMapper3.updateStudentName("方方",1);
    sqlSession3.commit();
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

Mybatis缓存机制

我们可以看到,在 sqlSession3更新数据库,并提交事务后, sqlsession2StudentMapper namespace下的查询走了数据库,没有走 Cache

2.3.4 案例4

验证 MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。

通常我们会为每个单表创建单独的映射文件,由于 MyBatis的二级缓存是基于 namespace的,多表查询语句所在的 namspace无法感应到其他 namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。

@Test
public void testCacheWithDiffererntNamespace() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true);
    SqlSession sqlSession2 = factory.openSession(true);
    SqlSession sqlSession3 = factory.openSession(true);

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentByIdWithClassInfo(1));
    sqlSession1.close();
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1));

    classMapper.updateClassName("特色一班",1);
    sqlSession3.commit();
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1));
}

执行结果:

Mybatis缓存机制

在这个案例中,我们引入了两张新的表,一张 class,一张 classroomclass中保存了班级的 id和班级名, classroom中保存了班级 id和学生 id。我们在 StudentMapper中增加了一个查询方法 getStudentByIdWithClassInfo,用于查询学生所在的班级,涉及到多表查询。在 ClassMapper中添加了 updateClassName,根据班级 id更新班级名的操作。

sqlsession1studentmapper查询数据后,二级缓存生效。保存在 StudentMappernamespace下的 cache中。当 sqlSession3classMapperupdateClassName方法对 class表进行更新时, updateClassName不属于 StudentMappernamespace,所以 StudentMapper下的 cache没有感应到变化,没有刷新缓存。当 StudentMapper中同样的查询再次发起时,从缓存中读取了脏数据。

2.3.5 案例5

为了解决案例4的问题呢,可以使用 Cache ref,让 ClassMapper引用 StudenMapper命名空间,这样两个映射文件对应的 SQL操作都使用的是同一块缓存了。

执行结果:

Mybatis缓存机制

不过这样做的后果是,缓存的粒度变粗了,多个 Mapper namespace下的所有操作都会对缓存使用造成影响。

2.4 源码分析

MyBatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用 CachingExecutor装饰了 BaseExecutor的子类,在委托具体职责给 delegate之前,实现了二级缓存的查询和写入功能,具体类关系图如下图所示。

Mybatis缓存机制

2.4.1 源码分析

源码分析从 CachingExecutorquery方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细讲解,读者朋友可以自行查询相关资料来学习。

CachingExecutorquery方法,首先会从 MappedStatement中获得在配置初始化时赋予的 Cache

Cache cache = ms.getCache();

本质上是装饰器模式的使用,具体的装饰链是:

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

Mybatis缓存机制

以下是具体这些 Cache实现类的介绍,他们的组合为 Cache赋予了不同的能力。

  • SynchronizedCache:同步 Cache,实现比较简单,直接使用 synchronized修饰方法。
  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了 DEBUG模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的 Copy,用于保存线程安全。
  • LruCache:采用了 Lru算法的 Cache实现,移除最近最少使用的 Key/Value
  • PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了 HashMap

然后是判断是否需要刷新缓存,代码如下所示:

flushCacheIfRequired(ms);

在默认的设置中 SELECT语句不会刷新缓存, insert/update/delte会刷新缓存。进入该方法。代码如下所示:

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
}

MyBatisCachingExecutor持有了 TransactionalCacheManager,即上述代码中的 tcm

TransactionalCacheManager中持有了一个 Map,代码如下所示:

private Map transactionalCaches = new HashMap();

这个 Map保存了 Cache和用 TransactionalCache包装后的 Cache的映射关系。

TransactionalCache实现了 Cache接口, CachingExecutor会默认使用他包装初始生成的 Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。

TransactionalCacheclear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:

@Override
public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
}

CachingExecutor继续往下走, ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。

if (ms.isUseCache() && resultHandler == null) {
    ensureNoOutParams(ms, parameterObject, boundSql);

之后会尝试从tcm中获取缓存的列表。

List list = (List) tcm.getObject(cache, key);

getObject方法中,会把获取值的职责一路传递,最终到 PerpetualCache。如果没有查到,会把 key加入 Miss集合,这个主要是为了统计命中率。

Object object = delegate.getObject(key);
if (object == null) {
    entriesMissedInCache.add(key);
}

CachingExecutor继续往下走,如果查询到数据,则调用 tcm.putObject方法,往缓存中放入值。

if (list == null) {
    list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    tcm.putObject(cache, key, list); // issue #578 and #116
}

tcm的 put方法也不是直接操作缓存,只是在把这次的数据和 key放入待提交的 Map中。

@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

从以上的代码分析中,我们可以明白,如果不调用 commit方法的话,由于 TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看 Sqlsessioncommit方法中做了什么。代码如下所示:

@Override
public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));

因为我们使用了 CachingExecutor,首先会进入 CachingExecutor实现的 commit方法。

@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
}

会把具体 commit的职责委托给包装的 Executor。主要是看下 tcm.commit()tcm最终又会调用到 TrancationalCache

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
}

看到这里的 clearOnCommit就想起刚才 TrancationalCacheclear方法设置的标志位,真正的清理 Cache是放到这里来进行的。具体清理的职责委托给了包装的 Cache类。之后进入 flushPendingEntries方法。代码如下所示:

private void flushPendingEntries() {
    for (Map.Entry entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    ................

}

flushPendingEntries中,将待提交的 Map进行循环处理,委托给包装的 Cache类,进行 putObject的操作。

后续的查询操作会重复执行这套流程。如果是 insert|update|delete的话,会统一进入 CachingExecutorupdate方法,其中调用了这个函数,代码如下所示:

private void flushCacheIfRequired(MappedStatement ms)

在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。

2.5 小结

  1. MyBatis的二级缓存相对于一级缓存来说,实现了 SqlSession之间缓存数据的共享,同时粒度更加的细,能够到 namespace级别,通过 Cache接口实现类不同的组合,对 Cache的可控性也更强。
  2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 在分布式环境下,由于默认的 MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatisCache接口实现,有一定的开发成本,直接使用 RedisMemcached等分布式缓存可能成本更低,安全性也更高。

三、总结

本文对介绍了 MyBatis一二级缓存的基本概念,并从应用及源码的角度对 MyBatis的缓存机制进行了分析。最后对 MyBatis缓存机制做了一定的总结,个人建议 MyBatis缓存特性在生产环境中进行关闭,单纯作为一个 ORM框架使用可能更为合适。

参考文章

Original: https://www.cnblogs.com/ciel717/p/16190611.html
Author: 夏尔_717
Title: Mybatis缓存机制

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/712246/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • java学习之springboot

    0x00前言 呀呀呀时隔好久我又来做笔记了,上个月去大型保密活动了,这里在网上看了一些教程如果说不是去做java开发我就不做ssm的手动整合了采用springboot去一并开发。S…

    技术杂谈 2023年6月21日
    0110
  • Hive中Hql关于行转列及列转行的综合应用

    建表语句 create table user_tag_merge (   uid int,    gender String,    agegroup String,    fav…

    技术杂谈 2023年7月24日
    070
  • ZOJ 3209 Treasure Map (Dancing Links)

    2 Seconds 32768 KB Your boss once had got many copies of a treasure map. Unfortunately, al…

    技术杂谈 2023年5月31日
    078
  • mysql 多表join优化

    多表join的sql EXPLAIN SELECT employee.name, department.name, project.name FROM employee left …

    技术杂谈 2023年7月11日
    095
  • Twitter系统架构参考

    Twitter系统架构参考 Push、Pull模式 每时每刻都有用户在Twitter上发表内容,Twitter工作是规划如何组织内容并把它发送用户的粉丝。 实时是真正的挑战,5秒内…

    技术杂谈 2023年6月1日
    0110
  • Maven中基于POM.xml的Profile来动态切换配置信息

    【转载:https://blog.csdn.net/blueheart20/article/details/52838093】 Maven中的profile设置 Maven是目前主…

    技术杂谈 2023年5月30日
    0101
  • Fortify 代码扫描安装使用教程

    前言 Fortify 能够提供静态和动态应用程序安全测试技术,以及运行时应用程序监控和保护功能。为实现高效安全监测,Fortify具有源代码安全分析,可精准定位漏洞产生的路径,以及…

    技术杂谈 2023年5月31日
    0106
  • Gtk调整widget部件大小size

    原型 gtkmm void set_size_request(int width = -1, int height = -1); gtk voidgtk_widget_set_si…

    技术杂谈 2023年7月24日
    073
  • Hadoop(三)通过C#/python实现HadoopMapReduce

    MapReduce Hadoop中将数据切分成块存在HDFS不同的DataNode中,如果想汇总,按照常规想法就是,移动数据到统计程序:先把数据读取到一个程序中,再进行汇总。 但是…

    技术杂谈 2023年7月24日
    080
  • 苏涛:对抗样本技术在互联网安全领域的应用

    导读: 验证码作为网络安全的第一道屏障,其重要程度不言而喻。当前,卷积神经网络的高速发展使得许多验证码的安全性大大降低,一些新型验证码甚至选择牺牲可用性从而保证安全性。针对对抗样本…

    技术杂谈 2023年7月25日
    089
  • dup和dup2用法小结

    今天和同学探讨了一下关于重定向输出到文件的问题,其中需要用到dup和dup2函数,因此来小小的总结一下。 首先来man一下: dup直接返回一个新的描述符和原来的描述符一样代表同一…

    技术杂谈 2023年6月21日
    094
  • Vue 网站首页加载优化

    Vue 网站首页加载优化 本篇主要讲解 Vue项目打包后 vendor.js 文件很大 如何对它进行优化 以及开启Vue的压缩 和 nginx gzip 压缩的使用,其他就是对接口…

    技术杂谈 2023年7月11日
    093
  • 树莓派远程连接工具SSH使用教程

    树莓派远程连接工具SSH使用教程 树莓派 背景故事 树莓派作为一款迷你小主机,大部分的使用场景都会用到远程调试,远程调试用到最多的方式一般就是VNC和SSH,SSH就是命令行型的远…

    技术杂谈 2023年7月23日
    088
  • 设计模式之二十一:中介者模式(Mediator)

    中介者模式:定义了一个对象。用来封装一系列对象的交互。中介者模式通过使对象之间不必显式引用减少了对象之间的耦合,而且同意你独立改变它们之间的交互。 中介者模式就是将对象之间的交互封…

    技术杂谈 2023年5月31日
    0121
  • 一文搞懂│php 中的 DI 依赖注入

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年7月11日
    079
  • Go基础2:数据结构(一)

    这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。 1.数组 数组是一段固定长度的连续内存区域。在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小…

    技术杂谈 2023年7月24日
    083
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球