SqlSession原理中介绍了在SQL查询时一级缓存和二级缓存的调用过程。这里介绍一下缓存的场景和失效。

image-20220405134607618

一、一级缓存

一级缓存与SqlSession相关,是一个会话级别的缓存,会话关闭清空缓存。在会话中一级缓存与运行时参数和操作配置相关

命中场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FirstCacheTest {
private Configuration configuration;
private SqlSession sqlSession;

@Before
public void init() throws IOException, SQLException {
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory build = factoryBuilder.build(inputStream);
configuration = build.getConfiguration();
sqlSession = build.openSession();
}

@Test
public void test(){
AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
List<Account> accounts1 = mapper.selectById(1);
List<Account> accounts2 = mapper.selectById(1);
System.out.println(accounts1 == accounts2);
}
}

缓存命中有几个运行时参数要求

  1. sql和参数必须相同
  2. 必须是相同的statementID,sql相同也不行
  3. sqlSession必须相同
  4. RowBounds 返回行返回必须相同

清理缓存之后就无法命中,默认采用的是simple执行器,编译2次,执行2次。建议采用的reuse执行器。只有有一条执行语句加入了flushCache=true等同于 sqlSession.clearCache()。共有4种操作配置导致缓存无法命中的场景:

  1. 手动sqlSession.clearCache(),sqlSession.commit(), sqlSession.rollback()
  2. 执行的方法上加入flushCache=true
  3. 中间执行Update语句
  4. 缓存作用域不是SESSION 改为STATEMENT这里是嵌套查询(子查询)
1
2
3
4
5
6
7
8
@Test
public void test2(){
AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
List<Account> accounts1 = mapper.selectById(1);
sqlSession.clearCache();
List<Account> accounts2 = mapper.selectById(1);
System.out.println(accounts1 == accounts2);
}

一级缓存源码解析

sqlSession查询时的流程:

image-20220405140905640

命中缓存

其中的BaseExecutor中的localCache的流程:

image-20220405142235268

BaseExecutor中的query源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@SuppressWarnings("unchecked")
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.");
//query实现嵌套查询,查询中有子查询,这里就会变大
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();
}
deferredLoads.clear(); // issue 601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue 482
}
}
return list;
}

//查询数据库
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;
}

一级缓存的key,只有都相同才能命中:

image-20220405143753736
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CacheKey implements Cloneable, Serializable {

private static final long serialVersionUID = 1146682552656046210L;

public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

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<Object> updateList;
}

这5个参数分别为,statementID、分页上下限、sql语句、参数

清空缓存

清空缓存的操作

1
2
3
4
5
6
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}

一共有4个地方调用了这个方法query、update、commit、rollback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//查询
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
//如果第一次查询并且配置了查询前刷新缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}

...
if (queryStack == 0) { //清空缓存不能发生在子查询,子查询依赖了一级缓存
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); // issue 601
//如果缓存是作用域是 LocalCacheScope.STATEMENT也会去清空缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue 482
}
}
}
//更新
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);
}

//提交
public void commit(boolean required) throws SQLException {
if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}

//回滚
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}

Spring一级缓存失效

会话不相同,如果不配置事务,每次都会新建一个session

1
2
3
4
5
6
7
public void testBySpring(){
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
AccountMapper mapper = context.getBean(AccountMapper.class);
List<Account> accounts1 = mapper.selectById(1);
List<Account> accounts2 = mapper.selectById(1);
System.out.println(accounts1 == accounts2); //false 无法命中
}

这里的UserMapper与从SqlSession取出的UserMapper不同,每次构造都会构造一个新会话,两次查询都是一个新会话。SqlSessionExecutor是一对一的关系。每次查询的Executor不同。

开启事务,后就会有效

1
2
3
4
5
6
7
8
9
10
11
public void testBySpring(){
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
AccountMapper mapper = context.getBean(AccountMapper.class);

DataSourceTransactionManager transactionManager =(DataSourceTransactionManager) context.getBean("txManager");
//手动开启事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());// 获得事务状态
List<Account> accounts1 = mapper.selectById(1);
List<Account> accounts2 = mapper.selectById(1);
System.out.println(accounts1 == accounts2); //false 无法命中
}

mapper -> SqlSessionTemplate -> SqlSessionInterceptor -> SqlSessionFactory

image-20220414222558730

动态代理嵌入动态代理最终调用MybatisSqlSessionFactory

image-20220414222932796

这里的sqlSessionSqlSessionTemplate,sqlSessionProxy就是SqlSessionInterceptor

SqlSessionTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SqlSessionTemplate implements SqlSession {

private final SqlSessionFactory sqlSessionFactory;

private final ExecutorType executorType;

private final SqlSession sqlSessionProxy; //这里还是一个SqlSession

private final PersistenceExceptionTranslator exceptionTranslator;

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {

notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");

this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//JDK的动态代理,所有的都在SqlSessionInterceptor,这个就是动态代理
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
}

通过2层的动态代理,实现Spring的事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//从这里获取SqlSession
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

获取会话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

notNull(sessionFactory, "No SqlSessionFactory specified");
notNull(executorType, "No ExecutorType specified");
//SqlSessionHolder就是一个ThreadLocal变量,如果当前的事务是打开的,就可以获得事务
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

if (holder != null && holder.isSynchronizedWithTransaction()) {
if (holder.getExecutorType() != executorType) {
throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
}

holder.requested();

if (logger.isDebugEnabled()) {
logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
}
//获得事务
return holder.getSqlSession();
}

if (logger.isDebugEnabled()) {
logger.debug("Creating a new SqlSession");
}
//获得新事务
SqlSession session = sessionFactory.openSession(executorType);
...
}

将断点打在holder,通过堆栈查看

image-20220417141343151

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

//调用SqlSessionTemplate的方法
public <E> List<E> selectList(String statement, Object parameter) {
return this.sqlSessionProxy.<E> selectList(statement, parameter);
}

invoke的动态代理是Mybatis的动态代理,这里的sqlSessionSqlSessionTemplate,原本的SqlSessionDefaultSqlSession。发起会话的调用是会调用到SqlSessionTemplate的逻辑。但是SqlSessionTemplate没有能力发起对Mybatis代码的代用,所以最终还是会调用DefaultSqlSession中的逻辑,SqlSessionTemplate中的每个方法都会打开会话构造SqlSession,所以将这些方法中构造DefaultSqlSession的逻辑使用动态代理来实现。

getSqlSession方法获取SqlSession,不使用事务时,每次从获取一个SqlSession都是null,都会使用工厂方法创建一个SqlSession,然后注册到事务上,当方法中不使用事务时,就会注册失败。每次的SqlSession都是新构建的,所以无法命中一级缓存。

二、二级缓存

定义和需求

image-20220417143217651

Mybatis中是先走会话,再走二级缓存,再走一级缓存。

二级缓存定义:

二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而宜可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据

二级缓存扩展性要求

因为二级缓存是应用级别的缓存,必然对缓存有所要求。

  1. 存储:需要多种方式的缓存,分为内存、硬盘、第三方集成。
  2. 缓存策略:当缓存到容量上限时,就需要有一些策略淘汰其中的缓存。FIFO先进先出、LRU最近最少使用。
  3. 过期清理:缓存设置过期清理。
  4. 线程安全:多线程场景下,必须保证数据一致。
  5. 命中率统计:对缓存中数据进行命中统计。
  6. 序列化:跨线程使用,2个对象必须不一样。

二级缓存组件结构

MyBatis的缓存接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Cache {

String getId();

int getSize();

void putObject(Object key, Object value);

Object getObject(Object key);

Object removeObject(Object key);

void clear();

ReadWriteLock getReadWriteLock();

}

Mybait的二级缓存采用了装饰器加责任链的设计模式。每一层都实现自己的功能,并通过代理的方式实现Cache的所有功能。

image-20220417145818712

开启mybatis二级缓存

配置 解释
cacheEnabled 全局缓存开关默认true
useCache statement 缓存开关默认true
flushCache 清除默认:修改true、查询false

@CacheNamespace
声明缓存空间

@CacheNamespaceRef
引用缓存空间

配置文件中加入

1
2
3
<settings>
<setting name="cacheEnabled" value="true" />
</settings>

mapper文件中加入

1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

就开启了二级缓存

对应的sql语句加上useCache,flushCache就会开启响应策略

和@CacheNamespace不是一个命名空间

测试二级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void cacheTest1(){
//cache就包含了若干个缓存节点。
Cache cache = configuration.getCache("com.lq.mybatis.mapper.AccountMapper");
Account account = new Account();
account.setId(1);
account.setMoney(1000.0);
account.setName("test");
cache.putObject("lq", account);
cache.getObject("lq");
}
}
@Test
public void cacheTest5(){
SqlSession session1 = factory.openSession(true);
AccountMapper mapper1 = session1.getMapper(AccountMapper.class);
mapper1.selectById(1);
session1.commit();

SqlSession session2 = factory.openSession(true);
AccountMapper mapper2 = session2.getMapper(AccountMapper.class);
mapper2.selectById(1);
}

这样设计Cache就可以屏蔽内部复杂性

image-20220417163913111

二级缓存的命中条件

  1. 会话提交之后
  2. Sql语句、参数相同
  3. 相同的statementID
  4. RowBounds相同

二级缓存源码分析

二级缓存命中

为什么要提交后才能命中缓存?

image-20220417170340680

如果不提交就命中会产生脏数据。

会话中访问缓存空间

image-20220417170801574

每次执行mapper的方法时都会把代码提交到暂存区,只有当commit时才会提交到缓冲区。

这里的缓冲区就是SynchronizedCache

二级缓存执行流程

image-20220417215007525
CachingExecutor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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, parameterObject, boundSql);
@SuppressWarnings("unchecked")
//通过缓存管理器获取
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. Query must be not synchronized to prevent deadlocks
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
TransactionalCacheManager缓存管理器,管理暂存区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TransactionalCacheManager {

private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}

//获取缓冲区作为key,暂存区作为value
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}

public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}

public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}

public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}

private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}

}
TransactionalCache暂存区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TransactionalCache implements Cache {
@Override
public Object getObject(Object key) {
// issue #116
//防止缓存穿透
Object object = delegate.getObject(key);
if (object == null) { //填充一个值
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) { //二级缓存有值,但是已经清空了,还是返回一个null,在同一个会话中,当先执行修改时,会清空二级缓存,但是因为没有commit不会真正清空,而在这个会话中执行查询,查到的是脏数据,所有要返回一个null
return null;
} else {
return object;
}
}
//清空,并没有清空,只是加了一个标记
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear(); //只是清空了暂存区
}

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

}