likes
comments
collection
share

跟我来!MyBatis缓存实战

作者站长头像
站长
· 阅读数 15

前言

今天突然看到有关MyBatis缓存的讨论引发思考,发现这部分内容有些模糊了,所以来梳理一下MyBatis一级缓存和二级缓存的内容,根据代码来加深理解一下MyBatis的缓存。

简介

MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。2013年11月迁移到Github。

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。

MyBatis提供了两种缓存,分别是一级缓存二级缓存

  • 一级缓存是SqlSession级别的缓存。 缓存是存在一个HashMap中的,HashMap是会话对象私有的。一级缓存默认是开启状态。
  • 二级缓存是namespace级别的缓存。 二级缓存默认实现也是基于本地缓存的,也可以整合到一些缓存中间件中。二级缓存默认不开启。

实战

一、 SpringBoot中使用MyBatis缓存

代码环境

  • SpringBoot: 2.6.3
  • MyBatis: 2.3.0

一级缓存

一级缓存是默认开启的,但是项目中大多是用不到的,因为MyBatis不在事务中执行Sql时,每次都会创建新的SqlSession,即使在事务中执行Sql也要在事务中执行两次相同Sql第二条Sql才会用到一级缓存,两次Sql之间还不能有更新等清除缓存的操作,因此缓存几乎不会命中。

一级缓存验证

准备一个简单的sql语句,查询返回UUID。

<select id="handleSql" resultType="java.lang.String">
    SELECT UUID()
</select>

准备一个不加事务的方法。

@Service
public class IMyBatisCacheTestServiceImpl implements IMyBatisCacheTestService {
    private final IMyBatisCacheTestMapper iMyBatisCacheTestMapper;

    @Autowired
    public IMyBatisCacheTestServiceImpl(IMyBatisCacheTestMapper iMyBatisCacheTestMapper) {
        this.iMyBatisCacheTestMapper = iMyBatisCacheTestMapper;
    }
    @Override
    public void testCache() {
        System.out.println("第一次返回结果" + iMyBatisCacheTestMapper.handleSql());
        System.out.println("第二次返回结果" + iMyBatisCacheTestMapper.handleSql());
    }
}

看一下执行结果。

这里开启了Mybatis的日志。

开启方法: mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@17dad32f] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1886186662 wrapping com.mysql.cj.jdbc.ConnectionImpl@7f6329cb] will not be managed by Spring
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: 8f2cbcd5-8da1-11ed-93cf-fa163e2220b5
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@17dad32f]
第一次返回结果8f2cbcd5-8da1-11ed-93cf-fa163e2220b5
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e3df614] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1702481339 wrapping com.mysql.cj.jdbc.ConnectionImpl@7f6329cb] will not be managed by Spring
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: 8f35e5b9-8da1-11ed-93cf-fa163e2220b5
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e3df614]
第二次返回结果8f35e5b9-8da1-11ed-93cf-fa163e2220b5

根据执行结果看出每次执行时都创建了一个新的SqlSession,每次都执行了Sql语句,返回了不同的结果。

接下来,将我们的方法加上事务。

@Service
@Transactional
public class IMyBatisCacheTestServiceImpl implements IMyBatisCacheTestService {
    private final IMyBatisCacheTestMapper iMyBatisCacheTestMapper;

    @Autowired
    public IMyBatisCacheTestServiceImpl(IMyBatisCacheTestMapper iMyBatisCacheTestMapper) {
        this.iMyBatisCacheTestMapper = iMyBatisCacheTestMapper;
    }
    @Override
    public void testCache() {
        System.out.println("第一次返回结果" + iMyBatisCacheTestMapper.handleSql());
        System.out.println("第二次返回结果" + iMyBatisCacheTestMapper.handleSql());
    }
}

然后我们来看一下执行结果。

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d]
JDBC Connection [HikariProxyConnection@857732012 wrapping com.mysql.cj.jdbc.ConnectionImpl@2e5e6fc4] will be managed by Spring
==>  Preparing: SELECT UUID()
==> Parameters: 
<==    Columns: UUID()
<==        Row: fdc5b34c-8da2-11ed-93cf-fa163e2220b5
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d]
第一次返回结果fdc5b34c-8da2-11ed-93cf-fa163e2220b5
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d]
第二次返回结果fdc5b34c-8da2-11ed-93cf-fa163e2220b5
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bb1e4d]

根据执行结果看出创建了一次SqlSession且只执行了一次Sql,第二次查询时一级缓存生效,两次返回了相同的结果。

二级缓存

二级缓存默认不开启,需要我们自己来启用。

开启二级缓存:mybatis.configuration.cache-enabled=true

开启二级缓存后,还要在XML中标签<cache/>标签,这个标签有多个参数来设置二级缓存

二级缓存验证

准备上文中的简单Sql语句。 使用上文中不加事务的方法。 执行方法查看执行结果。

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3520963d] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.shuaijie.dao.mybatisCache.IMyBatisCacheTestMapper]: 0.0
JDBC Connection [HikariProxyConnection@137685382 wrapping com.mysql.cj.jdbc.ConnectionImpl@6cae2e4d] will not be managed by Spring
==>  Preparing: SELECT UUID()
==> Parameters: 
<==    Columns: UUID()
<==        Row: 45c584eb-8da5-11ed-93cf-fa163e2220b5
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3520963d]
第一次返回结果45c584eb-8da5-11ed-93cf-fa163e2220b5
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5f0bab7e] was not registered for synchronization because synchronization is not active
As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
Cache Hit Ratio [com.shuaijie.dao.mybatisCache.IMyBatisCacheTestMapper]: 0.5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5f0bab7e]
第二次返回结果45c584eb-8da5-11ed-93cf-fa163e2220b5

可以看到创建了两个SqlSession,但是第二个语句并没有去数据库执行,二级缓存生效。

二 使用SqlSession对象来理解MyBatis缓存

一级缓存

还是用那个简单Sql语句。

1、使用同一个SqlSession执行
@Service
public class IMyBatisCacheTestServiceImpl implements IMyBatisCacheTestService {
    private final SqlSessionFactory sqlSessionFactory;

    @Autowired
    public IMyBatisCacheTestServiceImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public void testCache() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IMyBatisCacheTestMapper mapper = sqlSession.getMapper(IMyBatisCacheTestMapper.class);
        System.out.println("第一次返回结果" + mapper.handleSql());
        System.out.println("第二次返回结果" + mapper.handleSql());
    }
}

查看返回结果


JDBC Connection [HikariProxyConnection@1904652802 wrapping com.mysql.cj.jdbc.ConnectionImpl@6b649efa] will not be managed by Spring
==>  Preparing: SELECT UUID()
==> Parameters: 
<==    Columns: UUID()
<==        Row: e7981e9a-8e2e-11ed-93cf-fa163e2220b5
<==      Total: 1
第一次返回结果e7981e9a-8e2e-11ed-93cf-fa163e2220b5
第一次返回结果e7981e9a-8e2e-11ed-93cf-fa163e2220b5

根据返回结果可以看出一级缓存生效。

2、使用同一个SqlSession,两个查询之间清除缓存
@Service
public class IMyBatisCacheTestServiceImpl implements IMyBatisCacheTestService {
    private final SqlSessionFactory sqlSessionFactory;

    @Autowired
    public IMyBatisCacheTestServiceImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public void testCache() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IMyBatisCacheTestMapper mapper = sqlSession.getMapper(IMyBatisCacheTestMapper.class);
        System.out.println("第一次返回结果" + mapper.handleSql());
        // 清除sqlSession本地缓存
        sqlSession.clearCache();
        System.out.println("第二次返回结果" + mapper.handleSql());
    }
}

查看返回结果

JDBC Connection [HikariProxyConnection@2139431292 wrapping com.mysql.cj.jdbc.ConnectionImpl@1fd7a37] will not be managed by Spring
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: e59cdfd0-8e2f-11ed-93cf-fa163e2220b5
<== Total: 1
第一次返回结果e59cdfd0-8e2f-11ed-93cf-fa163e2220b5
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: e5a80513-8e2f-11ed-93cf-fa163e2220b5
<== Total: 1
第一次返回结果e5a80513-8e2f-11ed-93cf-fa163e2220b5

根据返回结果可以看出执行了两次Sql,原因是一级缓存被清除。

3、使用两个SqlSession
@Service
public class IMyBatisCacheTestServiceImpl implements IMyBatisCacheTestService {
    private final SqlSessionFactory sqlSessionFactory;

    @Autowired
    public IMyBatisCacheTestServiceImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public void testCache() {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        IMyBatisCacheTestMapper mapper1 = sqlSession1.getMapper(IMyBatisCacheTestMapper.class);
        IMyBatisCacheTestMapper mapper2 = sqlSession2.getMapper(IMyBatisCacheTestMapper.class);
        System.out.println("第一次返回结果" + mapper1.handleSql());
        System.out.println("第二次返回结果" + mapper2.handleSql());
    }
}

查看返回结果

JDBC Connection [HikariProxyConnection@95552255 wrapping com.mysql.cj.jdbc.ConnectionImpl@58a84a12] will not be managed by Spring
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: 098cfeeb-8e31-11ed-93cf-fa163e2220b5
<== Total: 1
第一次会话返回结果098cfeeb-8e31-11ed-93cf-fa163e2220b5
JDBC Connection [HikariProxyConnection@1229143192 wrapping com.mysql.cj.jdbc.ConnectionImpl@b5c6a30] will not be managed by Spring
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: 09acdd70-8e31-11ed-93cf-fa163e2220b5
<== Total: 1
第一次会话返回结果09acdd70-8e31-11ed-93cf-fa163e2220b5

根据结果可以看出跨SqlSession一级缓存不生效。

二级缓存

如果想禁用某个查询的二级缓存的时候,可以在这个查询的<Select>标签加上 userCache="false"属性, <Select userCache="false"/>

1、使用两个SqlSession

按照上文方法开启二级缓存。 同样是用那个简单Sql语句。

@Service
public class IMyBatisCacheTestServiceImpl implements IMyBatisCacheTestService {

    private final SqlSessionFactory sqlSessionFactory;

    @Autowired
    public IMyBatisCacheTestServiceImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public void testCache() {
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        IMyBatisCacheTestMapper mapper = sqlSession.getMapper(IMyBatisCacheTestMapper.class);
        System.out.println("第一次返回结果" + mapper.handleSql());
        sqlSession.close();
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
        IMyBatisCacheTestMapper mapper2 = sqlSession2.getMapper(IMyBatisCacheTestMapper.class);
        System.out.println("第二次返回结果" + mapper2.handleSql());
    }
}

这里强调一下sqlSession.close()这一行代码。它的作用是关闭SqlSession,同时会将二级缓存添加到Map。如果没有这一行代码,示例代码的二级缓存是不会生效的。

查看返回结果

JDBC Connection [HikariProxyConnection@876945112 wrapping com.mysql.cj.jdbc.ConnectionImpl@c1050f2] will not be managed by Spring
==> Preparing: SELECT UUID()
==> Parameters:
<== Columns: UUID()
<== Row: b66f4fe2-8e3c-11ed-93cf-fa163e2220b5
<== Total: 1
第一次会话返回结果b66f4fe2-8e3c-11ed-93cf-fa163e2220b5
As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
Cache Hit Ratio [com.shuaijie.dao.mybatisCache.IMyBatisCacheTestMapper]: 0.5
第一次会话返回结果b66f4fe2-8e3c-11ed-93cf-fa163e2220b5

根据结果可以看出,MyBatis二级缓存生效。

总结

  • MyBatis一级缓存是SqlSession级别的缓存,二级缓存是namespace级别的缓存。
  • 一级缓存是默认开启的,二级缓存需要手动配置开启。
  • 一级缓存不能命中场景:sqlSession不同;查询条件不同;两次查询之间有一级缓存清除操作等等。
  • 二级缓存不能命中场景:sqlSession未关闭(二级缓存还未添加);查询条件不同;namespace不同;两次查询之间有二级缓存清除操作等等。
  • 缓存会出问题场景:单表产生缓存,数据被另一个地方修改(跨JVM,跨namespace,或者连接工具修改等等);多表操作产生缓存某个表被另一个地方修改等等。
  • 尽量避免使用二级缓存;一级缓存某些特殊场景下也要关闭。