Mybatis缓存(1)--------系统缓存及简单配置介绍
前言
Mybatis 的缓存主要有两种:
- 系统缓存,也就是我们一级缓存与二级缓存;
- 自定义的缓存,比如 Redis、Enhance 等,需要额外的单独配置与实现,具体日后主要学习介绍。
在这里主要记录系统缓存的一些简单概念, 并没有涉及原理。其中会涉及 Mybatis 的相关配置以及生命周期等。
主要参考资料:《深入浅出 Mybatis 基础原理与实战》,http://www.mybatis.org/mybatis-3/zh/index.html
1、Mybatis 简单配置介绍
本文介绍的是基于 XML 的配置,并不是关于注解的 Mybatis 配置。当然复杂 SQL 情况下都建议使用 XML 配置。
(1)配置步骤
这里记录的只是 Myabtis 的简单配置,并没有证整合 Spring 等框架,所以相对简单。我开始学的时候也是反复记不住,不知道为什么要这么配置,这么配置的作用是什么。之后经过研读《深入浅出 Mybatis 基础原理与实战》(我这里只有 PDF 电子版本,有需要的朋友可以评论或者私信我),总结并画图让我对整个配置过程有了全新的认识。
简单来说,Mybatis 的配置主要分为以下几步(整合 Spring 之后有些就不需要了,但是一开始学习不建议直接整合 Spring):
- 编写 POJO 即 JavaBean,最终的目的是将数据库中的查询结果映射到 JavaBean 上;
- 配置与 POJO 对应的 Mapper 接口:里面有各种方法,对应 mapper.xml 中的查询语句;
- 配置与 POJO 对应的 XML 映射:编写缓存,SQL 查询等;
- 配置 mybatis-config.xml 主要的 Mybatis 配置文件:配置数据源、扫描 mapper.xml 等。
注意:以上的配置并没有严格的前后顺序;
(2)配置流程图
(3)配置总结
可以这么总结 Mybatis 或者帮助理解 Mybatis 的配置,我总结了以下三点提供参考:
- 一切Mybatis 配置都是为了创建 SqlSession 进行 SQL 查询;
- 归根结底程序代码中我们屏蔽了各种配置映射,只显式调用使用 Mapper 接口,那么接口实现类的获得是通过 SqlSession.getMapper() 获得;
- 那么 mapper 接口实现类的获得是通过mybatis-config.xml->SqlSessionFactoryBuilder->SqlSessionFacotry->SqlSession->mapper;
2、Mybatis 生命周期
正确理解 SqlSessionFactory、SqlSessionFactoryBuilder、SqlSession和Mapper的生命周期对于优化Mybatis尤为重要,这样可以使Mybatis高效正确完成;同为重要时 Mybatis 的生命周期对于理解 Myabtis 缓存的配置也尤为重要,我这里只做简单的文字介绍(其实也好理解):
(1)SqlSessionFactoryBuilder:作用就是创建一个构建器,一旦创建了 SqlSessionFactory,它的任务就算完成了,可以回收。
(2)SqlSessionFactory:作用是创建SqlSession,而SqlSession相当于JDBC的一个Connection对象,每次应用程序需要访问数据库,我们就要通过SqlSessionFactory创建一个SqlSession,所以SqlSessionFactory 在整Mybatis整个生命周期中(每个数据库对应一个 SqlSessionFactory,是单例产生的)。
(3)SqlSession:生命周期是存在于请求数据库处理事务的过程中,是一个线程不安全的对象(在多线程的情况下,需要特别注意),即存活于一个应用的请求和申请,可以执行多条 SQL 保证事务的一致性。
(4)Mapper:是一个接口,并没有实现类它的作用是发送 SQL,返回我们需要的结果,或者发送SQL修改数据库表,所以它存活于一个 SqlSession 内,是一个方法级别的东西。当SqlSession销毁的时候,Mapper也会销毁。
3、Myabtis 缓存介绍
(1)系统缓存:包括一级缓存与二级缓存
一级缓存:默认情况下 Myabtis 对于同一个 SqlSession 开启一级缓存
- 在默认没有配置的情况下,只会开启一级缓存(只针对同一个 SqlSession 而言);
- 在参数与 SQL 完全一样的情况下并且不声明刷新缓存没超时的,使用同一个 SqlSession 对象调用同一个 Mapper 方法时(SqlSession 对象生命周期为方法级别),SqlSession 只会取出当前缓存数据,不会再到数据库中进行查询;
- 如果不同的 SqlSession,即使同一个 Mapper 也会进行到数据库中进行不同的查询,即不同的 SqlSession 一级缓存是无效的。
二级缓存:这里可以结合SqlSessionFactory 等的生命周期能加深理解
- 不同的 SqlSession 是隔离的,为了解决这个问题,我们可以在SqlSessionFactory 层面上设置二级缓存提供各个对象 SqlSession
- 二级缓存默认是不开启的,需要进行配置,Mybatis 要求返回的 POJO 必须是可序列化的,即POJO 实现 Serializable 接口。
缓存的配置只需要在 XML 配置 <cache/> 即可,或者指定算法,刷新时间间隔,缓存状态,大小等
<cache eviction="LRU" readOnly="true" flushInterval="100000" size="1024"></cache>
A. 映射语句文件中所有 select 语句将会被缓存;
B. 映射语句文件中所有 insert、update 和 delete 语句会被刷新缓存;
C. 缓存使用默认的 LRU 最近最少使用算法回收;
D. 根据时间表,缓存不会任何时间顺序刷新;
E. 缓存会存储列表集合或对象的 1024 个引用
F. 缓存被视为可 read/write 的缓存,意味着是不可以被共享的,而可以被安全地修改。
(2)自定义缓存:结合 Redis 等主流缓存配置
我们可以使用比如现在比较火的 Redis 缓存,需要实现Myabtis为我们提供的接口org.apache.ibatis.cache.Cache。虽然现在主流 Mybatis 用的都是自定义缓存,但是这里先不过多介绍,我一步一步来学习记录!
4、Mybatis 系统缓存代码实现
包结构图:
数据库 user 表:要与 User 对应,Mybatis 会根据驼峰命名进行自动映射,即 user 表中 id 字段映射为 User POJO 中的 id,如果使用的是插件生成,POJO 就会自动对应。
mysql> use mybatis; Database changed mysql> select*from user; +----+----------+ | id | name | +----+----------+ | 1 | Zhangsan | | 2 | Lisi | +----+----------+ 2 rows in set
(1)User.java: POJO
import java.io.Serializable;/**
- POJO:User
- @author Lijian
*/
public class User implements Serializable{</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> id; </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String name; </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> getId() { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> id; } </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> setId(<span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> id) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id =<span style="color: rgba(0, 0, 0, 1)"> id; } </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getName() { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> name; } </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setName(String name) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.name =<span style="color: rgba(0, 0, 0, 1)"> name; }
}
(2)UserMapper.xml:
<?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.lijian.dao.UserMapper"> <resultMap id="userMap" type="com.lijian.model.User"> <id property="id" column="id"/> <result column="name" property="name" /> </resultMap> <!-- 使用 POJO 映射结果集 --> <select id="findByUserId" parameterType="int" resultType="user"> select * from user where id = #{id} </select> <!-- 使用 resultMap 映射结果集 --> <select id="findByUserName" parameterType="string" resultMap="userMap"> select * from user where name = #{name} </select> </mapper>
(3)UserMapper.java:
public interface UserMapper { User findByUserId(int id); User findByUserName(String name); }
(4)mybatis-config.xml:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- mybatis-config.xml 常用的配置及顺序: (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?) -->
<!-- jdbc 数据库属性配置文件 db.properties --> <properties resource="db.properties"></properties> <settings> <setting name="logImpl" value="LOG4J"/> </settings> <typeAliases> <typeAlias alias="user" type="com.lijian.model.User"/> </typeAliases> <!-- default 默认数数据源 --> <!-- environments 配置:可以注册多个数据源 DataSource,每个数据源分为两部分:一个是数据源的配置,另外一个是数据库事务配置。 --> <environments default="development"> <!-- dataSource 1--> <environment id="development"> <transactionManager type="JDBC"> <!-- 关闭自动提交 --> <property name="autoCommit" value="false"/> </transactionManager> <!-- POOLED 连接池数据库 --> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <!-- 可以通过包名导入映射器 :但 mapper interface 与 mapper.xml 必须在同一个包下--> <!-- <package name="com.lijian.mapper"/> --> <!-- 文件路径引入 --> <mapper resource="com/lijian/mapper/UserMapper.xml"/> </mappers> </configuration>
(5)SqlSessionFactoryUtils:
/** * 创建 SqlSession * @author Lijian * 创建顺序:mybatis-config.xml->SqlSessionFactoryBuilder->SqlSessionFactory(Singleton)->SqlSession * */ import java.io.IOException; import java.io.InputStream;import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;public class SqlSessionFactoryUtils {
private static final Logger logger = LogManager.getLogger(SqlSessionFactoryUtils.class);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">synchronized lock</span> <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> Class CLASS_LOCK = SqlSessionFactoryUtils.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> SqlSessionFactory sqlSessionFactory = <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">private constructors</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> SqlSessionFactoryUtils(){}; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 因为一个数据库对应一个SqlSessionFactory,所有采用单例模式生成SqlSessionFactory * </span><span style="color: rgba(128, 128, 128, 1)">@return</span><span style="color: rgba(0, 128, 0, 1)"> SqlSessionFactory </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span><span style="color: rgba(0, 0, 0, 1)"> SqlSessionFactory initSqlSessionFactory() { String config </span>= "mybatis-config.xml"<span style="color: rgba(0, 0, 0, 1)">; InputStream inputStream </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { inputStream </span>=<span style="color: rgba(0, 0, 0, 1)"> Resources.getResourceAsStream(config); }</span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException e) { logger.info(</span>"SqlSessionFactoryUtils"<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 0, 255, 1)">synchronized</span><span style="color: rgba(0, 0, 0, 1)"> (CLASS_LOCK) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (sqlSessionFactory == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { sqlSessionFactory </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SqlSessionFactoryBuilder().build(inputStream); } } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> sqlSessionFactory; } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * openSession进行SQL查询 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span><span style="color: rgba(0, 128, 0, 1)"> SqlSession </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span><span style="color: rgba(0, 0, 0, 1)"> SqlSession openSession() { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (sqlSessionFactory == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { initSqlSessionFactory(); } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> sqlSessionFactory.openSession(); }
}
(6)MybatisMain.java:测试类
import org.apache.ibatis.session.SqlSession;import com.lijian.dao.UserMapper;
import com.lijian.utils.SqlSessionFactoryUtils;public class MybatisMain {
public static void main(String[] args) {
SqlSession sqlSession = null;
SqlSession sqlSession2 = null;
try {
//获得 SqlSession
sqlSession = SqlSessionFactoryUtils.openSession();
sqlSession2 = SqlSessionFactoryUtils.openSession();
//获得 Mapper: 动态代理生成 UserMapper 实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//默认一级缓存:相同 SELECT 与 param,只查询一次
System.out.println("======================= 默认使用系统一级缓存 =======================");
userMapper.findByUserId(1);
userMapper.findByUserId(1);
//二级缓存 commit 才会有效
sqlSession.commit();
System.out.println("======================= 重新创建 SqlSession=======================");
sqlSession2 = SqlSessionFactoryUtils.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
userMapper2.findByUserId(1);
//二级缓存 commit 才会有效
sqlSession2.commit();
} catch (Exception e) {
System.err.println(e.getMessage());
}
finally {
if (sqlSession != null) {
//sqlSession 生命周期是随着 SQL 查询而结束的
sqlSession.close();
}
if (sqlSession2 != null) {
sqlSession2.close();
}
}
}
}
注意 UserMapper.xml 当前是没有开启二级缓存的,故默认为一级缓存,如何得到证实呢?在 MybatisMain 中,我们创建了:
- sqlSession:开启两个一模一样的 SELECT SQL 查询,即userMapper.findByUserId(1),那么如果一级缓存有效且开启的话,只会进行一次查询,之后有一次 SQL 语句日志输出;
- sqlSession2:开启与 sqlSession 中一模一样的 SELECT 查询,如果二级缓存没有开启,一级缓存默认开启的话,是会进行查询的,会有一次 SQL 语句日志输出。
======================= 默认使用系统一级缓存 ======================= [DEBUG][main][2018-07-29 21:45:41][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Preparing: select * from user where id = ? [DEBUG][main][2018-07-29 21:45:41][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Parameters: 1(Integer) [TRACE][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Columns: id, name [TRACE][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 1, Zhangsan [DEBUG][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Total: 1 ======================= 重新创建 SqlSession======================= [DEBUG][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Preparing: select * from user where id = ? [DEBUG][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Parameters: 1(Integer) [TRACE][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Columns: id, name [TRACE][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 1, Zhangsan [DEBUG][main][2018-07-29 21:45:42][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Total: 1
接下来我们开启二级缓存:在不同 SqlSession 中所有相同的 SELECT 语句将会被缓存,只会有一次 SQL 语句日志输出,并且会有 Cache Hit Ratio 缓存命中率
<?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.lijian.dao.UserMapper"> <!-- 开启二级缓存 :针对 SqlSessionFactory,同时 POJO 必须实现 Serializable 接口 (1)所有 select 会被缓存 (2)insert、update、delete 会刷新缓存 (3)默认使用 LRU (4)缓存是可 read/write,不是共享的是可以被安全地修改 --> <cache eviction="LRU" readOnly="true" flushInterval="100000" size="1024"></cache> <resultMap id="userMap" type="com.lijian.model.User"> <id property="id" column="id"/> <result column="name" property="name" /> </resultMap> <!-- 使用 POJO 映射结果集 --> <select id="findByUserId" parameterType="int" resultType="user"> select * from user where id = #{id} </select> <!-- 使用 resultMap 映射结果集 --> <select id="findByUserName" parameterType="string" resultMap="userMap"> select * from user where name = #{name} </select> </mapper>
日志如下:
======================= 默认使用系统一级缓存 ======================= [DEBUG][main][2018-07-29 21:49:26][org.apache.ibatis.cache.decorators.LoggingCache] - Cache Hit Ratio [com.lijian.dao.UserMapper]: 0.0 [DEBUG][main][2018-07-29 21:49:27][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Preparing: select * from user where id = ? [DEBUG][main][2018-07-29 21:49:27][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Parameters: 1(Integer) [TRACE][main][2018-07-29 21:49:27][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Columns: id, name [TRACE][main][2018-07-29 21:49:27][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Row: 1, Zhangsan [DEBUG][main][2018-07-29 21:49:27][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <== Total: 1 [DEBUG][main][2018-07-29 21:49:27][org.apache.ibatis.cache.decorators.LoggingCache] - Cache Hit Ratio [com.lijian.dao.UserMapper]: 0.0 ======================= 重新创建 SqlSession======================= [DEBUG][main][2018-07-29 21:49:27][org.apache.ibatis.cache.decorators.LoggingCache] - Cache Hit Ratio [com.lijian.dao.UserMapper]: 0.3333333333333333
总结与补充
(1)作为新手很大可能在配置过程中会遇到很多坑(我就是其中之一),比如 Mybatis 的日志配置。这里可以主要参考:http://www.mybatis.org/mybatis-3/zh/configuration.html
(2)mybatis 的相关配置文件(没有 myabtis-config.xml)可以使用 eclipse 中的插件生成(具体网上有很多教程),但还要进行适当修改!