一级缓存用于提高查询效率。
我们使用MyBatis执行查询时,当我们的多次查询内容完全一致,如果MyBatis不采取一些措施的话,将会导致每次查询都查询一次数据库,如果我们在极短的时间内做了完全相同的查询,并且查询结果也完全相同,将会导致浪费大量的数据库资源。
为了解决这个问题,MyBatis会在SqlSession对象中创建一个本地缓(local catch)存对象,每次查询都会先从本地缓存中查询,如果本地缓存命中,直接返回本地缓存中的结果,如果缓存没有命中,那么从数据库中搜寻,将数据库的查询结果放入本地缓存并返回。
一级缓存是默认打开,强制使用,无法关闭。
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:
@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}
结论:
一级缓存本质就是一个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点相关:
解释上述4点:
<select id="findById" resultType="User">
select * from t_user where id = #{id}
select>
结论:
调用方法时,是同一个方法,参数也相同的情况下,会使用一级缓存。
二级缓存是进程级别的缓存,属于可选类型缓存,从性质上来说可用可不用,而且不建议使用。
<setting name="CacheEnable" value="true"/>
<mapper namespace="com.leo.mapper.UserMapper">
<cache/>
public class User implements Serializable
演示代码:
@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}
二级缓存当涉及多表操作时,如果其他Mapper中的操作影响到了使用二级缓存的Mapper中的数据,将会导致缓存中的数据与数据库中的数据不一致。
结论:
例如:在项目中,我们希望使用单独的配置文件配置(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:
<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="${user}"/>
<property name="password" value="${password}"/>
dataSource>
方式1:
<mappers>
<mapper resource="com/leo/mapper/UserMapper.xml"/>
mappers>
方式2:
<mapper class="com.leo.mapper.UserMapper" />
方式3:
<package name="com.leo.mapper"/>
事务管理方式配置:
<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();