MyBatis----05----缓存&&配置进阶

1. MyBatis中的缓存机制

1.1 MyBatis中的一级缓存

1.1.1 什么是一级缓存

一级缓存用于提高查询效率。
我们使用MyBatis执行查询时,当我们的多次查询内容完全一致,如果MyBatis不采取一些措施的话,将会导致每次查询都查询一次数据库,如果我们在极短的时间内做了完全相同的查询,并且查询结果也完全相同,将会导致浪费大量的数据库资源。
为了解决这个问题,MyBatis会在SqlSession对象中创建一个本地缓(local catch)存对象,每次查询都会先从本地缓存中查询,如果本地缓存命中,直接返回本地缓存中的结果,如果缓存没有命中,那么从数据库中搜寻,将数据库的查询结果放入本地缓存并返回。
一级缓存是默认打开,强制使用,无法关闭。

1.1.2 一级缓存的组织架构


查看PerpetualCache源码

public class PerpetualCache implements Cache { private final String id; //实现一级缓存使用HashMap private Map<Object, Object> cache = new HashMap(); public PerpetualCache(String id) { this.id = id; } public String getId() { return this.id; } //获得缓存数量 public int getSize() { return this.cache.size(); } //存入缓存 public void putObject(Object key, Object value) { this.cache.put(key, value); } //从缓存中取值 public Object getObject(Object key) { return this.cache.get(key); } //移除某个缓存 public Object removeObject(Object key) { return this.cache.remove(key); } //清楚所有缓存 public void clear() { this.cache.clear(); } public ReadWriteLock getReadWriteLock() { return null; } 

1.1.3 一级缓存的生命周期

演示代码1:

@Test public void testFindById(){ UserMapper userMapper = session.getMapper(UserMapper.class); //第一次查询,缓存为命中 => 查询数据库,并放入缓存 User user1 = userMapper.findById(1); //第二次查询,缓存命中 => 返回缓存中的数据 User user2 = userMapper.findById(1); System.out.println(user1); System.out.println(user2); } 

输出结果(控制台):

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, tom, 1234 <== Total: 1 User{id=1, name='tom', password='1234', accounts=null, roles=null} User{id=1, name='tom', password='1234', accounts=null, roles=null} 

演示代码2:

@Test public void testFindById(){ UserMapper userMapper = session.getMapper(UserMapper.class); //第一次查询,缓存为命中 => 查询数据库,并放入缓存 User user1 = userMapper.findById(1); //关闭sqlsession session.close(); //开启一个新的sqlsession session = SqlSessionUtils.openSession(); userMapper = session.getMapper(UserMapper.class); //第二次查询,缓存仍然未命中 => 返回缓存中的数据 User user2 = userMapper.findById(1); System.out.println(user1); System.out.println(user2); } 

输出结果(控制台)

Created connection 1944978632. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, tom, 1234 <== Total: 1 Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] Returned connection 1944978632 to pool. Opening JDBC Connection
Checked out connection 1944978632 from pool. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, tom, 1234 <== Total: 1 User{id=1, name='tom', password='1234', accounts=null, roles=null} User{id=1, name='tom', password='1234', accounts=null, roles=null} 

演示代码3:

@Test public void testFindById(){ UserMapper userMapper = session.getMapper(UserMapper.class); //第一次查询,缓存为命中 => 查询数据库,并放入缓存 User user1 = userMapper.findById(1); //清空一级缓存 session.clearCache(); //第二次查询,缓存仍然为命中 => 返回缓存中的数据 User user2 = userMapper.findById(1); System.out.println(user1); System.out.println(user2); } 

输出结果(控制台)

Created connection 1944978632. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, tom, 1234 <== Total: 1 ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, tom, 1234 <== Total: 1 User{id=1, name='tom', password='1234', accounts=null, roles=null} User{id=1, name='tom', password='1234', accounts=null, roles=null} 

演示代码4:

@Test public void testFindById(){ UserMapper userMapper = session.getMapper(UserMapper.class); //第一次查询,缓存为命中 => 查询数据库,并放入缓存 User user1 = userMapper.findById(1); //修改操作(update|delete|insert) => 导致缓存被清空 userMapper.update(1,"汤姆"); //第二次查询,缓存仍然为命中 => 返回缓存中的数据 User user2 = userMapper.findById(1); System.out.println(user1); System.out.println(user2); } 

输出结果(控制台):

Created connection 1944978632. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8] ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, tom, 1234 <== Total: 1 ==> Preparing: update t_user set name = ? where id =? ==> Parameters: 汤姆(String), 1(Integer) <== Updates: 1 ==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, 汤姆, 1234 <== Total: 1 User{id=1, name='tom', password='1234', accounts=null, roles=null} User{id=1, name='汤姆', password='1234', accounts=null, roles=null} 

结论:

  • 如果SqlSession调用了Close方***释放掉一级缓存,并且整个SqlSession以及一级缓存不再可用;
  • 如果SqlSession调用了cleraCache方***清空一级缓存,缓存仍然可用;
  • 如果SqlSession执行了任何一个更新操作(update | delete | insert)都会导致清空以及缓存,缓存仍然可用。

1.1.4 一级缓存中的CacheKey设计

一级缓存本质就是一个Map,第一次执行查询时会使用本次查询的特征值作为key,查询结果作为value存入一级缓存Map中。
我们接下来要研究,查询的特征值,也就是key时如何定义的?

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (this.closed) { throw new ExecutorException("Executor was closed."); } else { //创建CacheKer对象 -> 存入HashMap中 CacheKey cacheKey = new CacheKey(); //1.StatementId cacheKey.update(ms.getId()); //2.rowBounds -> getOffset cacheKey.update(rowBounds.getOffset()); //3.rowBounds -> getLimit cacheKey.update(rowBounds.getLimit()); //4.sql语句 cacheKey.update(boundSql.getSql()); //5.sql语句的参数 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); Iterator var8 = parameterMappings.iterator(); while(var8.hasNext()) { ParameterMapping parameterMapping = (ParameterMapping)var8.next(); if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = this.configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } if (this.configuration.getEnvironment() != null) { cacheKey.update(this.configuration.getEnvironment().getId()); } return cacheKey; } } 

CacheKey设计和如下4点相关:

  • 传入的StatementId;
  • 查询时要求的结果集范围(RowBounds对象);
  • Sql语句;
  • Sql参数。

解释上述4点:

  • StatementId:就是我们为查询指定的唯一id,如下"findById"就是StatementId:
<select id="findById" resultType="User"> select * from t_user where id = #{id} </select> 
  • RowBounds:MyBatis中的软分页对象,实际开发中并不推荐使用该对象;
  • Sql语句:MyBatis底层实现就是JDBC,传给PrepareStatement对象的Sql语句;
  • Sql参数:MyBatis底层实现就是JDBC,传给PrepareStatement对象的参数;

结论:
调用方法时,是同一个方法,参数也相同的情况下,会使用一级缓存。

1.2 MyBatis中的二级缓存

二级缓存是进程级别的缓存,属于可选类型缓存,从性质上来说可用可不用,而且不建议使用

1.2.1 什么是二级缓存

1.2.2 如何让打开二级缓存

  • 在主配置文件中打开二级缓存;
<!-- 开启二级缓存 --> <setting name="CacheEnable" value="true"/> 
  • 在具体要使用二级缓存的Mapper中开启;
<mapper namespace="com.leo.mapper.UserMapper"> <!-- 表示再UserMapper中使用二级缓存 --> <cache/> 
  • 要使用二级缓存的实体需要实现Serializable接口。
public class User implements Serializable 

1.2.3 演示二级缓存

演示代码:

@Test public void testFindById(){ UserMapper userMapper = session.getMapper(UserMapper.class); //第一次查询,二级缓存为命中 => 查询数据库,并放入二级缓存,或发送sql User user1 = userMapper.findById(1); //执行commit的操作才会进入二级缓存 session.commit(); //关闭sqlsession,对应的一级缓存失效,但是二级缓存仍然有效 session.close(); //开启一个新的sqlsession session = SqlSessionUtils.openSession(); userMapper = session.getMapper(UserMapper.class); //第二次查询,二级缓存仍存在查询结果 => 返回二级缓存中的数据,不会发送sql User user2 = userMapper.findById(1); System.out.println(user1); System.out.println(user2); } 

输出结果(控制台):

==> Preparing: select * from t_user where id = ? ==> Parameters: 1(Integer) <== Columns: id, name, password <== Row: 1, 汤姆, 1234 <== Total: 1 Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@272ed83b] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@272ed83b] Returned connection 657381435 to pool. Cache Hit Ratio [com.leo.mapper.UserMapper]: 0.5 User{id=1, name='汤姆', password='1234', accounts=null, roles=null} User{id=1, name='汤姆', password='1234', accounts=null, roles=null} 

1.2.4 结论

二级缓存当涉及多表操作时,如果其他Mapper中的操作影响到了使用二级缓存的Mapper中的数据,将会导致缓存中的数据与数据库中的数据不一致。

结论:

  • 缓存是以namespace为单位,不同的namespace下的操作不互相影响;
  • 出现update操作(insert | update | delete)会清空namespace下的二级缓存;
  • 多表操作一般不建议使用二级缓存,因为会产生脏数据,可以使用cache-ref缓解;
  • 不建议使用二级缓存,更推荐其他的专业缓存产品,例如:Redis。

2. 配置进阶

2.1 使用Properties引入外部文件

例如:在项目中,我们希望使用单独的配置文件配置(db.properties)我们的数据库连接信息.
准备resource/db.properties:

driverClass = com.mysql.cj.jdbc.Driver
jdbcUrl = jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
user = root
password = hanjuechen

在mybatis-config.xml配置中引入db.properties:

<!-- 引入 db.properties --> <properties resource="db.properties" /> 

在mybatis-config.xml配置中使用db.properties配置的键值对:

<dataSource type="POOLED"> <!-- 四大连接信息配置--> <property name="driver" value="${driverClass}"/> <property name="url" value="${jdbcUrl}"/> <property name="username" value="com.wenyibi.futuremail.model.User@4fe26589"/> <property name="password" value="${password}"/> </dataSource> 

2.2 注册Mapper配置文件的三种方式

方式1:

<mappers> <!-- 方式1:分别指定XXXMapper.xml配置文件路径 --> <mapper resource="com/leo/mapper/UserMapper.xml"/> </mappers> 

方式2:

<!-- 方式2:通过Mapper类指定Mapper.xml配置文件 该方式MyBatis会自动从指定类所在包下查询与接口相同的XML --> <mapper class="com.leo.mapper.UserMapper" /> 

方式3:

<!-- 方式3:直接指定Mapper所在的包名,自动扫描包中的所有Mapper配置文件 该方式MyBatis会先找包中的类,再找类对应的同名XML --> <package name="com.leo.mapper"/> 

2.3 事务管理

事务管理方式配置:

<!-- 指定事务管理方式 --> <transactionManager type="JDBC"/> 

我们使用的是JDBC方式进行管理:
JDBC事务操作与MyBatis事务操作对应关系:
开启事务:

//JDBC,关闭自动提交事务 conn.setAutoCommit(false); //MyBatis //参数为true,开启自动提交事务 SqlSessionFactory.openSession(true); //不传参数,关闭自动提交事务 SqlSessionFactory.openSession(); 

提交 | 回滚事务:

//JDBC //提交事务 conn.commit(); //回滚事务 conn.rollBack(); //MyBatis //提交事务 SqlSession.commit(); //回滚事务 SqlSession.rollBck();