一.前言

spring结合mybatis后mybaits一级缓存失效分为两种情况:

  • 如果没有开启事务,每一次sql都是用的新的SqlSession,这时mybatis的一级缓存是失效的。
  • 如果有事务,同一个事务中相同的查询使用的相同的SqlSessioon,此时一级缓存是生效的。

判断是否是同一个SqlSession 可以把日志级别降到debug级别查看相应的SqlSessionId是否为多个

二.一级缓存介绍

Mybatis提供了一级缓存的方案来优化在数据库会话间重复查询的问题。实现的方式是每一个SqlSession中都持有了自己的缓存,一种是SESSION级别,即在一个Mybatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个statement有效。如果用一张图来代表一级查询的查询过程的话,可以用下图表示。

spring开启mybatis缓存 spring整合mybatis一级缓存_spring


每一个SqlSession中持有了自己的Executor,每一个Executor中有一个Local Cache。当用户发起查询时,Mybatis会根据当前执行的MappedStatement生成一个key,去Local Cache中查询,如果缓存命中的话,返回。如果缓存没有命中的话,则写入Local Cache,最后返回结果给用户。工作流程

spring开启mybatis缓存 spring整合mybatis一级缓存_一级缓存失效_02


主要步骤如下:

  1. 对于某个Select Statement,根据该Statement生成key。
  2. 判断在Local Cache中,该key是否用对应的数据存在。
  3. 如果命中,则跳过查询数据库,继续往下走。
  4. 如果没命中,回去数据库中查询,然后写入到Local Cache中

三.示例分析

1.以controller层方法

新增controller方法为查询用户列表数据

@RestController
public class UserController {
    private Logger logger = LoggerFactory.getLogger(UserController.class);

    @Resource
    UserServiceInter userServiceInter;

    @GetMapping("/userList")
    public TResult getUserList(){
        Date date=new Date();
        TResult<User> result = new TResult<>();
        result.setList(userServiceInter.user_list());
        logger.info("花费时间:"+ (System.currentTimeMillis()-date.getTime()) +" ms");
        return result;
    }
}
2.dao层

dao层接口方法

public interface UserDaoMapper {
    List<User> userList();
}

mapper实现方法

<?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="com.example.mybatistest.dao.UserDaoMapper">
    <select id="userList"   resultType="com.example.mybatistest.model.User">
        select *  from `user`
    </select>
</mapper>
3.service层方法

用户列表user_list接口方法

public interface UserServiceInter {
    List<User> user_list();
}

(1)不带Transactional注解serviceImpl实现层方法

@Service
public class UserServiceImpl implements UserServiceInter {
    @Resource
    UserDaoMapper userDaoMapper;

    @Override
    public List<User> user_list() {
        userDaoMapper.userList();
        List<User> users = userDaoMapper.userList();
        return users;
    }
}

执行 http://localhost:8016/userList 查看dubug日志信息

2019-01-25 14:58:32.461 [http-nio-8016-exec-5] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/userList", parameters={}
2019-01-25 14:58:32.462 [http-nio-8016-exec-5] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public com.example.mybatistest.model.TResult com.example.mybatistest.controller.UserController.getUserList()
2019-01-25 14:58:32.462 [http-nio-8016-exec-5] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
2019-01-25 14:58:32.462 [http-nio-8016-exec-5] DEBUG org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@de6f38] was not registered for synchronization because synchronization is not active
2019-01-25 14:58:32.463 [http-nio-8016-exec-5] DEBUG o.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
2019-01-25 14:58:32.464 [http-nio-8016-exec-5] DEBUG o.m.spring.transaction.SpringManagedTransaction - JDBC Connection [HikariProxyConnection@33487810 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ee1289] will not be managed by Spring
2019-01-25 14:58:32.464 [http-nio-8016-exec-5] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - ==>  Preparing: select * from `user` 
2019-01-25 14:58:32.464 [http-nio-8016-exec-5] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - ==> Parameters: 
2019-01-25 14:58:32.467 [http-nio-8016-exec-5] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - <==      Total: 5
2019-01-25 14:58:32.467 [http-nio-8016-exec-5] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@de6f38]
2019-01-25 14:58:32.467 [http-nio-8016-exec-5] DEBUG o.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
2019-01-25 14:58:32.467 [http-nio-8016-exec-5] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
2019-01-25 14:58:32.467 [http-nio-8016-exec-5] DEBUG org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b0229d] was not registered for synchronization because synchronization is not active
2019-01-25 14:58:32.467 [http-nio-8016-exec-5] DEBUG o.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
2019-01-25 14:58:32.468 [http-nio-8016-exec-5] DEBUG o.m.spring.transaction.SpringManagedTransaction - JDBC Connection [HikariProxyConnection@28800845 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ee1289] will not be managed by Spring
2019-01-25 14:58:32.468 [http-nio-8016-exec-5] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - ==>  Preparing: select * from `user` 
2019-01-25 14:58:32.468 [http-nio-8016-exec-5] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - ==> Parameters: 
2019-01-25 14:58:32.473 [http-nio-8016-exec-5] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - <==      Total: 5
2019-01-25 14:58:32.474 [http-nio-8016-exec-5] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b0229d]
2019-01-25 14:58:32.474 [http-nio-8016-exec-5] DEBUG o.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
2019-01-25 14:58:32.474 [http-nio-8016-exec-5] INFO  com.example.mybatistest.controller.UserController - 花费时间:12 ms
2019-01-25 14:58:32.475 [http-nio-8016-exec-5] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [application/json, application/*+json, application/json, application/*+json]
2019-01-25 14:58:32.475 [http-nio-8016-exec-5] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [TResult{errorCode=0, model=null, list=[User{id=1, username='11', password='dd', Salt='ee'}, User{id=2, username='dd', password='dd', Salt='dd'}, User{id=3, username='ccc', password='ccc', Salt='sd'}, User{id=4, username='d', password='d', Salt='d'}, User{id=5, username='1', password='1', Salt='11'}]}]
2019-01-25 14:58:32.476 [http-nio-8016-exec-5] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK
2019-01-25 14:58:32.498 [http-nio-8016-exec-6] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/favicon.ico", parameters={}
2019-01-25 14:58:32.498 [http-nio-8016-exec-6] DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler [class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []]
2019-01-25 14:58:32.503 [http-nio-8016-exec-6] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK

可以发现dubug信息里面有2次Creating a new SqlSession,创建2次sqlSession,2次执行查询语句都在数据库中查询,说明没有使用一级缓存.

(2)带Transactional注解UserServiceImpl实现层方法

@Service
public class UserServiceImpl implements UserServiceInter {
    @Resource
    UserDaoMapper userDaoMapper;

    @Override
    @Transactional
    public List<User> user_list() {
        userDaoMapper.userList();
        List<User> users = userDaoMapper.userList();
        return users;
    }
}

执行 http://localhost:8016/userList 查看dubug日志信息

2019-01-25 15:26:20.239 [http-nio-8016-exec-7] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/userList", parameters={}
2019-01-25 15:26:20.240 [http-nio-8016-exec-7] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public com.example.mybatistest.model.TResult com.example.mybatistest.controller.UserController.getUserList()
2019-01-25 15:26:20.241 [http-nio-8016-exec-7] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.example.mybatistest.service.impl.UserServiceImpl.user_list]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2019-01-25 15:26:20.242 [http-nio-8016-exec-7] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [HikariProxyConnection@6324143 wrapping com.mysql.cj.jdbc.ConnectionImpl@1744046] for JDBC transaction
2019-01-25 15:26:20.242 [http-nio-8016-exec-7] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [HikariProxyConnection@6324143 wrapping com.mysql.cj.jdbc.ConnectionImpl@1744046] to manual commit
2019-01-25 15:26:20.243 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
2019-01-25 15:26:20.243 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713]
2019-01-25 15:26:20.243 [http-nio-8016-exec-7] DEBUG o.m.spring.transaction.SpringManagedTransaction - JDBC Connection [HikariProxyConnection@6324143 wrapping com.mysql.cj.jdbc.ConnectionImpl@1744046] will be managed by Spring
2019-01-25 15:26:20.243 [http-nio-8016-exec-7] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - ==>  Preparing: select * from `user` 
2019-01-25 15:26:20.243 [http-nio-8016-exec-7] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - ==> Parameters: 
2019-01-25 15:26:20.246 [http-nio-8016-exec-7] DEBUG com.example.mybatistest.dao.UserDaoMapper.userList - <==      Total: 5
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713]
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713] from current transaction
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713]
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713]
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713]
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a78713]
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Initiating transaction commit
2019-01-25 15:26:20.247 [http-nio-8016-exec-7] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Committing JDBC transaction on Connection [HikariProxyConnection@6324143 wrapping com.mysql.cj.jdbc.ConnectionImpl@1744046]
2019-01-25 15:26:20.249 [http-nio-8016-exec-7] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [HikariProxyConnection@6324143 wrapping com.mysql.cj.jdbc.ConnectionImpl@1744046] after transaction
2019-01-25 15:26:20.249 [http-nio-8016-exec-7] DEBUG o.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
2019-01-25 15:26:20.249 [http-nio-8016-exec-7] INFO  com.example.mybatistest.controller.UserController - 花费时间:8 ms
2019-01-25 15:26:20.251 [http-nio-8016-exec-7] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [application/json, application/*+json, application/json, application/*+json]
2019-01-25 15:26:20.251 [http-nio-8016-exec-7] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [TResult{errorCode=0, model=null, list=[User{id=1, username='11', password='dd', Salt='ee'}, User{id=2, username='dd', password='dd', Salt='dd'}, User{id=3, username='ccc', password='ccc', Salt='sd'}, User{id=4, username='d', password='d', Salt='d'}, User{id=5, username='1', password='1', Salt='11'}]}]
2019-01-25 15:26:20.253 [http-nio-8016-exec-7] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK
2019-01-25 15:26:20.281 [http-nio-8016-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/favicon.ico", parameters={}
2019-01-25 15:26:20.281 [http-nio-8016-exec-4] DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler [class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []]
2019-01-25 15:26:20.285 [http-nio-8016-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK

发现执行2次查询语句,只有一次Creating a new SqlSession,说明只生成一次SqlSession,第一次走的是数据库,第二次走myabtis的一级缓存。

事务一级缓存存在的弊端
同一个事务中在查询中间如果有其他线程修改了这条数据,这两条两次查询的还是内容相同(事务使用的是Read Committed)

因为同一个事务中spring使用的是同一个SqlSession,此时走的是SqlSession的缓存,并没有从数据中查询。

解决方案
在mybatis的mapper xml里配置清空缓存flushCache设置为true

<select id="userList" flushCache="true"  resultType="com.example.mybatistest.model.User">
        select *  from `user`
    </select>

四.分析

mybatis-spring官方文档: http://www.mybatis.org/spring/sqlsession.html# 其中有这么二段话解释:
(1)Using an SqlSession

In MyBatis you use the SqlSessionFactory to create an SqlSession. Once you have a session, you use it to execute your mapped statements, commit or rollback connections and finally, when it is no longer needed, you close the session. With MyBatis-Spring you don’t need to use SqlSessionFactory directly because your beans can be injected with a thread safe SqlSession that automatically commits, rollbacks and closes the session based on Spring’s transaction configuration.

在MyBatis中,使用sqlsessionFactory创建一个sqlsession。一旦有了会话,就可以使用它来执行Mapper的映射语句、提交或回滚连接,最后,当不再需要时,关闭会话。使用mybatis-spring,您不需要直接使用sqlsessionFactory,因为可以向bean注入一个线程安全的sqlsession,它根据spring的事务配置自动提交、回滚和关闭会话。

(2)SqlSessionTemplate

SqlSessionTemplate is the heart of MyBatis-Spring. It implements SqlSession and is meant to be a drop-in replacement for any existing use of SqlSession in your code. SqlSessionTemplate is thread safe and can be shared by multiple DAOs or mappers.

SqlSessionTemplate 是mybatis-spring 的心脏。它实现了sqlsession,是为了作为代码中任何现有使用sqlsession的替代品。sqlsessionTemplate是线程安全的,可以由多个DAO或映射器共享。

When calling SQL methods, including any method from Mappers returned by getMapper(), SqlSessionTemplate will ensure that the SqlSession used is the one associated with the current Spring transaction. In addition, it manages the session life-cycle, including closing, committing or rolling back the session as necessary. It will also translate MyBatis exceptions into Spring DataAccessExceptions.

当调用SQL方法(包括getmapper()返回的映射器中的任何方法)时,sqlsessionTemplate将确保使用的sqlsession是与当前spring事务关联的。此外,它还管理会话生命周期,包括根据需要关闭、提交或回滚会话。它还将mybatis异常转换为spring DataAccessExceptions

SqlSessionTemplate should always be used instead of default MyBatis implementation DefaultSqlSession because the template can participate in Spring transactions and is thread safe for use by multiple injected mapper classes. Switching between the two classes in the same application can cause data integrity issues.

应始终使用sqlsessiontemplate而不是默认的mybatis实现默认的sqlsession,因为模板可以参与Spring事务,并且对于多个注入的映射器类来说是线程安全的。在同一应用程序中的两个类之间切换可能会导致数据完整性问题。

spring通过mybatis调用数据库的过程如下:
1、需要访问数据
2、spring检查到了这种需求,于是去申请一个mybatis的sqlsession,并将申请到的sqlsession与当前线程绑定,放入threadlocal里面
3、template从threadlocal获取到sqlsession,去执行查询
4、查询结束,清空threadlocal中与当前线程绑定的sqlsession,释放资源
5、又需要访问数据
6、返回到步骤2

五.总结

1、Mybatis一级缓存的生命周期和SqlSession一致。
2、Mybatis的缓存没有更新缓存和缓存过期的概念,同时只是使用了默认的hashmap,也没有做容量上的限定。
3、Mybatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,有操作数据库写的话,会引起脏数据,建议是把一级缓存的默认级别设定为Statement,即不使用一级缓存。