MyBatis缓存
缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度
MyBatis 也提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解
- 一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构建 sqlSession 对象,在对象中有一个数据结构(HashMap)用于存储数据缓存。不同的 sqlSession 之间缓存数据区域(HashMap)是相互不影响的。
- 二级缓存是 mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的sql语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的
一级缓存
MyBatis 的一级缓存是默认开启的
-
我们在一个sqlSession 中,对 user 表根据id进行两次查询,查看他们发出sql语句的情况
public class CacheTest { private UserMapper userMapper; @Before public void before() throws IOException { // 1.Resources工具类,配置文件的加载,把配置文件加载成字节输入流 InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); // 2.解析了配置文件,并创建了sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); // 3.生成sqlSession (默认开启一个事务,但是该事务不会自动提交, 参数为true为自动提交) SqlSession sqlSession = sqlSessionFactory.openSession(true); userMapper = sqlSession.getMapper(UserMapper.class); } @Test public void fistLevelCache(){ // 第一次查询id为1的用户 User user1 = userMapper.findById(1); System.out.println("*******第一次查询的user对象*******"); System.out.println(user1); // System.out.println("*******修改第一次查询的user对象*******"); // user1.setUsername("xxxxxxxx"); // userMapper.updateUser(user1); System.out.println("*******第二次查询*******"); // 第二次查询id为1的用户 User user2 = userMapper.findById(1); System.out.println(user1 == user2); } }
-
控制台输出如下
==> Preparing: select * from user where id = ? ==> Parameters: 1(Integer) <== Columns: id, username <== Row: 1, xxxxxxxx <== Total: 1 *******第一次查询的user对象******* User{id=1, username='xxxxxxxx', orderList=[], roleList=[]} *******第二次查询******* true
我们发现,只有第一次查询执行了sql语句,第二次查询并没有执行sql语句,并且user1和user2的地址值是相等的,这时我们判断,user2查询时,使用了一级缓存
-
我们把上面的代码注释打开,控制台输出如下:
==> Preparing: select * from user where id = ? ==> Parameters: 1(Integer) <== Columns: id, username <== Row: 1, xxxxxxxx <== Total: 1 *******第一次查询的user对象******* User{id=1, username='xxxxxxxx', orderList=[], roleList=[]} *******修改第一次查询的user对象******* ==> Preparing: update user set username = ? where id = ? ==> Parameters: xxxxxxxx(String), 1(Integer) <== Updates: 1 *******第二次查询******* ==> Preparing: select * from user where id = ? ==> Parameters: 1(Integer) <== Columns: id, username <== Row: 1, xxxxxxxx <== Total: 1 false
我们发现,这时两次查询都执行了sql语句,没有用到缓存,user1和user2地址值也不相等
-
分布式部署情况下:
本地模拟下分布式会出现的情况
public class CacheTest { private UserMapper userMapper; @Before public void before() throws IOException { // 1.Resources工具类,配置文件的加载,把配置文件加载成字节输入流 InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); // 2.解析了配置文件,并创建了sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); // 3.生成sqlSession (默认开启一个事务,但是该事务不会自动提交, 参数为true为自动提交) SqlSession sqlSession = sqlSessionFactory.openSession(true); userMapper = sqlSession.getMapper(UserMapper.class); } @Test public void fistLevelCache() throws IOException { // 第一次查询id为1的用户 User user1 = userMapper.findById(1); System.out.println("*******第一次查询的user对象*******"); System.out.println(user1); // 模拟分布式下另一台服务器的sqlSession操作 test(); System.out.println("*******第二次查询*******"); // 第二次查询id为1的用户 User user2 = userMapper.findById(1); System.out.println(user2); System.out.println(user1 == user2); } @Test public void test() throws IOException { InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(true); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setId(1); user.setUsername("修改xxx"); mapper.updateUser(user); System.out.println("test方法修改user1,修改后为:"); User user1 = mapper.findById(1); System.out.println("*******修改后查询的user对象*******"); System.out.println(user1); } }
执行后发现:
==> Preparing: select * from user where id = ? ==> Parameters: 1(Integer) <== Columns: id, username <== Row: 1, 原始数据 <== Total: 1 *******第一次查询的user对象******* User{id=1, username='原始数据', orderList=[], roleList=[]} ==> Preparing: update user set username = ? where id = ? ==> Parameters: 修改后的数据(String), 1(Integer) <== Updates: 1 test方法修改user1,修改后为: ==> Preparing: select * from user where id = ? ==> Parameters: 1(Integer) <== Columns: id, username <== Row: 1, 修改后的数据 <== Total: 1 *******修改后查询的user对象******* User{id=1, username='修改后的数据', orderList=[], roleList=[]} *******第二次查询******* User{id=1, username='原始数据', orderList=[], roleList=[]} true
可以看到,当存在另一个sqlSession提交事务时,不会影响到其他的sqlSession,第二次查询时sqlSession还是取的一级缓存的数据,这在分布式的部署下,很容易会出现脏读
-
关闭一级缓存
在sqlMapperConfig.xml配置
<settings> <!-- 禁用一级缓存 --> <setting name="localCacheScope" value="STATEMENT"/> </settings>
-
手动清理一级缓存
sqlSession.clearCache();
-
总结:
- 第一次发起查询用户id为1的用户信息,MyBatis先去缓存中查找是否有id为1的用户信息,如果没有,就从数据库中查询,并将查询到的用户信息存入一级缓存
- 如果中介sqlSession执行了事务提交操作(执行插入、更新、删除),则会把sqlSession中的一级缓存清空,这样做的目的是为了让缓存中存储的是最新的信息,避免脏读
- 第二次发起查询用户id为1的用户信息,MyBatis先去缓存中查找是否有id为1的用户信息,缓存中有,直接从缓存中返回用户信息
一级缓存原理探究与源码分析
一级缓存到底是什么?一级缓存什么时候被创建?一级缓存的工作流程是怎样的?我们现在带着问题来查看MyBatis源码
一级缓存是SqlSession级别的,所以我们从SqlSession类入口,看看有没有创建缓存或者与缓存相关的属性或方法
查看SqlSession类,发现只有 clearCache()
与缓存是相关的,那我们就直接从这个方法入手吧。
分析源码时,我们要看它(此类)是谁,他的父类和子类又分别是谁,对如上关系了解了,才会对这个类有更深入的认识。
再深入分析,流程走到 Perpetualcache
中的 clear() 方法之后,会调用其 cache.clear()
方法,那么这个cache 是什么东西呢?在PerpetualCache发现,
private Map<Object, Object> cache = new HashMap<>();
也就是一个Map,所以 cache.clear()
就是 map.clear()
,也就是说,缓存其实就是本地存放的一个 Map 对象,每一个 SqlSession 都会存放一个 Map 对象的引用,那么这个 cache 是什么时候创建的呢?
因为 Executor 是执行器,用来执行SQL请求,而且清除缓存的方法也是在 Executor 中执行,所以缓存很有可能也是在这里创建的。
查看 Executor 接口实现类,发现有 query()
方法里面执行了 createCacheKey()
方法:
@Override
public <E> List<E> 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);
}
因为一级缓存是一个Map结构,所以这个方法是在创建缓存Map里的key值,由 MappedStatement
,parameterObject
查询参数,RowBounds
分页对象 ,BoundSql
sql语句 组成的一个 CacheKey
对象
也就是说,在查询前,会先生成一个 CacheKey
, 然后再查询
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
// 如果有缓存,就从缓存取数据
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);
}
...
return list;
}
如果在 localCache.getObject(key)
缓存中没有命中的话,就从 queryFromDatabase()
数据库查, 查到结果后再把结果放进缓存 Map 里面