7.Mybatis缓存

第七部分:Mybatis缓存

缓存就是内存中的数据,常常来自于对数据库查询结果的保存,使用缓存,我们可以避免频繁地与数据库进行交互,

进而提高响应速度。

mybatis也提供对缓存的支持,分为一级缓存和二级缓存,可以通过以下的图进行解释

1.一级缓存是SqlSession级别的缓存,在操作时需要构造SqlSession对象,对象中有一个数据结构

HashMap用户存储数据,不同SqlSession的存储区域HashMap互不影响。

2.二级缓存是mapper级别的缓存,多个SqlSession去操作同一个mapper的sql语句,多个SqlSession可以共用一个

mapper缓存,二级缓存是跨SqlSession的

7.1 ⼀级缓存

①、在⼀个 sqlSession 中,对 User 表根据 id 进⾏两次查询,查看他们发出 sql 语句的情况
 
@Test
public void test1 (){
// 根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession = sessionFactory . openSession ();
UserMapper userMapper = sqlSession . getMapper ( UserMapper . class );
// 第⼀次查询,发出 sql 语句,并将查询出来的结果放进缓存中
User u1 = userMapper . selectUserByUserId ( 1 );
System . out . println ( u1 );
// 第⼆次查询,由于是同⼀个 sqlSession, 会在缓存中查询结果
// 如果有,则直接从缓存中取出来,不和数据库进⾏交互
User u2 = userMapper . selectUserByUserId ( 1 );
System . out . println ( u2 );
sqlSession . close ();
}

一级缓存模型

、同样是对 user 表进⾏两次查询,只不过两次查询之间进⾏了⼀次 update 操作。
 @Test
public void firstLevelCache()  {
     //第一次查询id为1的用户
     User user1=iUserMapper.findUserById(1);

     User user = new User();
     user.setId(1);
     user.setUsername("zhangsan");
     iUserMapper.updateUser(user);

     //第二次查询
     User user2=iUserMapper.findUserById(1);
     System.out.println(user1==user2);


 }

控制台结果

缓存模型

③、 总结
1 、第⼀次发起查询⽤户 id 1 的⽤户信息,先去找缓存中是否有 id 1 的⽤户信息,如果没有,从 数据
库查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。
2 、 如果中间 sqlSession 去执⾏ commit 操作(执⾏插⼊、更新、删除),则会清空 SqlSession 中的 ⼀
级缓存,这样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。
3 、 第⼆次发起查询⽤户 id 1 的⽤户信息,先去找缓存中是否有 id 1 的⽤户信息,缓存中有,直 接从
缓存中获取⽤户信息
 
 
⼀级缓存原理探究与源码分析
⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?相信你现在应该会有
 
这⼏个疑问,那么我们本节就来研究⼀下⼀级缓存的本质
 
⼤家可以这样想,上⾯我们⼀直提到⼀级缓存,那么提到⼀级缓存就绕不开 SqlSession, 所以索性我们 
 
就直接从 SqlSession ,看看有没有创建缓存或者与缓存有关的属性或者⽅法
 
 
调研了⼀圈,发现上述所有⽅法中,好像只有 clearCache() 和缓存沾点关系,那么就直接从这个⽅ 法⼊
⼿吧,分析源码时 ,我们要看它 ( 此类 ) 是谁,它的⽗类和⼦类分别⼜是谁 ,对如上关系了解了,你才 会
对这个类有更深的认识,分析了⼀圈,你可能会得到如下这个流程图
 
 
再深⼊分析,流程⾛到 Perpetualcache 中的 clear() ⽅法之后,会调⽤其 cache.clear() ⽅法,那 么这个
cache 是什么东⻄呢?点进去发现, cache 其实就是 private Map cache = new
HashMap() ;也就是⼀个 Map ,所以说 cache.clear() 其实就是 map.clear() ,也就是说,缓存其实就是
本地存放的⼀个 map 对象,每⼀个 SqISession 都会存放⼀个 map 对象的引⽤,那么这个 cache 是何 时创
建的呢?
你觉得最有可能创建缓存的地⽅是哪⾥呢?我觉得是 Executor ,为什么这么认为?因为 Executor 是 执
⾏器,⽤来执⾏ SQL 请求,⽽且清除缓存的⽅法也在 Executor 中执⾏,所以很可能缓存的创建也很 有可
能在 Executor 中,看了⼀圈发现 Executor 中有⼀个 createCacheKey ⽅法,这个⽅法很像是创 建缓存的
⽅法啊,跟进去看看,你发现 createCacheKey ⽅法是由 BaseExecutor 执⾏的,代码如
 
CacheKey cacheKey = new CacheKey (); //MappedStatement id
// id 就是 Sql 语句的所在位置包名 + 类名 + SQL 名称
cacheKey . update ( ms . getId ());
// offset 就是 0 ,rowBounds是分页参数
cacheKey . update ( rowBounds . getOffset ());
// limit 就是 Integer.MAXVALUE
cacheKey . update ( rowBounds . getLimit ());
// 具体的 SQL 语句
cacheKey . update ( boundSql . getSql ());
// 后⾯是 update sql 中带的参数
cacheKey . update ( value );
...
if ( configuration . getEnvironment () != null ) {
// issue #176
cacheKey . update ( configuration . getEnvironment (). getId ());
}
创建缓存 key 会经过⼀系列的 update ⽅法, udate ⽅法由⼀个 CacheKey 这个对象来执⾏的,这个
update ⽅法最终由 updateList list 来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能 理解
这五个值都是什么了
 
 
这⾥需要注意⼀下最后⼀个值, configuration.getEnvironment().getId() 这是什么,这其实就是 定义在
mybatis-config.xml 中的标签,⻅如下。
 
<environments default = "development" >
<environment id = "development" >
<transactionManager type = "JDBC" />
<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>

 

那么我们回归正题,那么创建完缓存之后该⽤在何处呢?总不会凭空创建⼀个缓存不使⽤吧?绝对不会
的,经过我们对⼀级缓存的探究之后,我们发现⼀级缓存更多是⽤ 于查询操作,毕竟⼀级缓存也叫做查
询缓存吧,为什么叫查询缓存我们⼀会⼉说 。我们先来看⼀下这个缓存到底⽤在哪了,我们跟踪到
query ⽅法如下:
 
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 );
}
@SuppressWarnings ( "unchecked" )
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 );
}
...
}
// queryFromDatabase ⽅法
private < E > List < E > queryFromDatabase ( MappedStatement ms , Object parameter ,
RowBounds rowBounds , ResultHandler resultHandler , CacheKey key , BoundSql
boundSql ) throws SQLException {
List < E > list ;
localCache . putObject ( key , EXECUTION_PLACEHOLDER );
try {
list = doQuery ( ms , parameter , rowBounds , resultHandler , boundSql );
} finally {
localCache . removeObject ( key );
}
localCache . putObject ( key , list );
if ( ms . getStatementType () == StatementType . CALLABLE ) {
localOutputParameterCache . putObject ( key , parameter );
}
return list ;
}
如果查不到的话,就从数据库查,在 queryFromDatabase 中,会对 localcache 进⾏写⼊。 localcache
对象的 put ⽅法最终交给 Map 进⾏存放
private Map < Object , Object > cache = new HashMap < Object , Object > ();
@Override
public void putObject ( Object key , Object value ) { cache . put ( key , value );
}

7.2 ⼆级缓存

⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去
缓存中取。但是⼀级缓存是基于 sqlSession 的,⽽⼆级缓存是基于 mapper ⽂件的 namespace 的,也 就
是说多个 sqlSession 可以共享⼀个 mapper 中的⼆级缓存区域,并且如果两个 mapper namespace
同,即使是两个 mapper, 那么这两个 mapper 中执⾏ sql 查询到的数据也将存在相同的⼆级缓存区域 中
 
 
如何使⽤⼆级缓存
 
开启⼆级缓存
 
和⼀级缓存默认开启不⼀样,⼆级缓存需要我们⼿动开启
 
⾸先在全局配置⽂件 sqlMapConfig.xml ⽂件中加⼊如下代码 :
 
<!-- 开启⼆级缓存 -->
<settings>
<setting name = "cacheEnabled" value = "true" />
</settings>

 如果是使用mapper配置文件进行开发的就是用以下配置

<!-- 开启⼆级缓存 -->
<cache></cache>
如果使用注解开发,在接口中加上@CacheNamespace注解
@CacheNamespace
public interface IUserMapper {
我们可以看到 mapper.xml ⽂件中就这么⼀个空标签,其实这⾥可以配置 ,PerpetualCache 这个类是
mybatis 默认实现缓存功能的类。我们不写 type 就使⽤ mybatis 默认的缓存,也可以去实现 Cache 接⼝
来⾃定义缓存。
注解实现具体化
@CacheNamespace(implementation = PerpetualCache.class)
 
 
public class PerpetualCache implements Cache {
private final String id ;
private MapcObject , Object > cache = new HashMapC );
 
public PerpetualCache ( St ring id ) { this . id = id ;
}

我们可以看到⼆级缓存底层还是HashMap结构

public class User implements Serializable (
// ⽤户 ID
private int id ;
// ⽤户姓名
private String username ;
}

 

开启了⼆级缓存后,还需要将要缓存的 pojo 实现 Serializable 接⼝,为了将缓存数据取出执⾏反序列化操
作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取
这个缓存的话,就需要反序列化了。所以 mybatis 中的 pojo 都去实现 Serializable 接⼝
 
③、测试
⼀、测试⼆级缓存和 sqlSession ⽆关

 

@Test public void testTwoCache (){
// 根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession1 = sessionFactory . openSession ();
SqlSession sqlSession2 = sessionFactory . openSession ();
 
UserMapper userMapper1 = sqlSession1 . getMapper ( UserMapper . class );
UserMapper userMapper2 = sqlSession2 . getMapper ( UserMapper . class );
// 第⼀次查询,发出 sql 语句,并将查询的结果放⼊缓存中
User u1 = userMapper1 . selectUserByUserId ( 1 );
System . out . println ( u1 );
sqlSession1 . close (); // 第⼀次查询完后关闭 sqlSession
 
// 第⼆次查询,即使 sqlSession1 已经关闭了,这次查询依然不发出 sql 语句
User u2 = userMapper2 . selectUserByUserId ( 1 );
System . out . println ( u2 );
sqlSession2 . close ();

 可以看出上⾯两个不同的sqlSession,第⼀个关闭了,第⼆次查询依然不发出sql查询语句

⼆、测试执⾏ commit() 操作,⼆级缓存数据清空
 
@Test
public void testTwoCache (){
// 根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession1 = sessionFactory . openSession ();
SqlSession sqlSession2 = sessionFactory . openSession ();
SqlSession sqlSession3 = sessionFactory . openSession ();
String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ;
UserMapper userMapper1 = sqlSession1 . getMapper ( UserMapper . class );
UserMapper userMapper2 = sqlSession2 . getMapper ( UserMapper . class );
UserMapper userMapper3 = sqlSession2 . getMapper ( UserMapper . class );
// 第⼀次查询,发出 sql 语句,并将查询的结果放⼊缓存中
User u1 = userMapperl . selectUserByUserId ( 1 );
System . out . println ( u1 );
sqlSessionl . close (); // 第⼀次查询完后关闭 sqlSession
 
// 执⾏更新操作, commit()
u1 . setUsername ( "aaa" );
userMapper3 . updateUserByUserId ( u1 );
sqlSession3 . commit ();
 
// 第⼆次查询,由于上次更新操作,缓存数据已经清空 ( 防⽌数据脏读 ) ,这⾥必须再次发出 sql
User u2 = userMapper2 . selectUserByUserId ( 1 );
System . out . println ( u2 );
sqlSession2 . close ();

查看控制台情况:

④、 useCache flushCache
mybatis 中还可以配置 userCache flushCache 等配置项, userCache 是⽤来设置是否禁⽤⼆级缓 存
的,在 statement 中设置 useCache=false 可以禁⽤当前 select 语句的⼆级缓存,即每次查询都会发出 sql
去查询,默认情况是 true, 即该 sql 使⽤⼆级缓存
<select id = "selectUserByUserId" useCache = "false"
resultType = "com.lagou.pojo.User" parameterType = "int" >
select * from user where id=#{id}
</select>

注解开发方式,在对应的接口方法上加上注解:

@Options(useCache = true)//启用二级缓存
@Select("select * from user where id=#{id}")
public User findUserById(Integer id);
这种情况是针对每次查询都需要最新的数据 sql, 要设置成 useCache=false ,禁⽤⼆级缓存,直接从数 据
库中获取。
mapper 的同⼀个 namespace 中,如果有其它 insert update, delete 操作数据后需要刷新缓 存,如
果不执⾏刷新缓存会出现脏读。
设置 statement 配置中的 flushCache="true” 属性,默认情况下为 true, 即刷新缓存,如果改成 false 则 不
会刷新。使⽤缓存时如果⼿动修改数据库表中的查询数据会出现脏读。
 
<select id = "selectUserByUserId" flushCache = "true" useCache = "false"
resultType = "com.lagou.pojo.User" parameterType = "int" >
select * from user where id=#{id}
</select>

 ⼀般下执⾏完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏

读。所以我们不⽤设置,默认即可
 

7.3 ⼆级缓存整合redis

上⾯我们介绍了 mybatis⾃带的⼆级缓存,但是这个缓存是单服务器⼯作,⽆法实现分布式缓存。 那么

什么是分布式缓存呢?假设现在有两个服务器 1 2, ⽤户访问的时候访问了 1 服务器,查询后的缓 存就
会放在 1 服务器上,假设现在有个⽤户访问的是 2 服务器,那么他在 2 服务器上就⽆法获取刚刚那个 缓
存,如下图所示:
 
 
为了解决这个问题,就得找⼀个分布式的缓存,专⻔⽤来存储缓存数据的,这样不同的服务器要缓存数
据都往它那⾥存,取缓存数据也从它那⾥取,如下图所示:
 
 
如上图所示,在⼏个不同的服务器之间,我们使⽤第三⽅缓存框架,将缓存都放在这个第三⽅框架中 ,
然后⽆论有多少台服务器,我们都能从缓存中获取数据。
这⾥我们介绍 mybatis redis 的整合。
刚刚提到过, mybatis 提供了⼀个 eache 接⼝,如果要实现⾃⼰的缓存逻辑,实现 cache 接⼝开发即可。
mybati s 本身默认实现了⼀个,但是这个缓存的实现⽆法实现分布式缓存,所以我们要⾃⼰来实现。
redis 分布式缓存就可以, mybatis 提供了⼀个针对 cache 接⼝的 redis 实现类,该类存在 mybatis-redis
 
1. pom ⽂件
<dependency>
<groupId> org.mybatis.caches </groupId>
<artifactId> mybatis-redis </artifactId>
<version> 1.0.0-beta2 </version>
</dependency>
2. 配置⽂件
Mapper.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.lagou.mapper.IUserMapper" >
<cache type = "org.mybatis.caches.redis.RedisCache" />
<select id = "findAll" resultType = "com.lagou.pojo.User" useCache = "true" >
select * from user
</select>

 注解配置:

@CacheNamespace(implementation = RedisCache.class) //开启二级缓存
public interface IUserMapper {
3.redis.properties
 
redis.host = localhost
redis.port = 6379
redis.connectionTimeout = 5000
redis.password =
redis.database = 0

4.测试

@Test
public void SecondLevelCache (){
SqlSession sqlSession1 = sqlSessionFactory . openSession (); SqlSession sqlSession2 = sqlSessionFactory . openSession ();
SqlSession sqlSession3 = sqlSessionFactory . openSession ();
IUserMapper mapper1 = sqlSession1 . getMapper ( IUserMapper . class );
lUserMapper mapper2 = sqlSession2 . getMapper ( lUserMapper . class );
lUserMapper mapper3 = sqlSession3 . getMapper ( IUserMapper . class );
User user1 = mapper1 . findUserById ( 1 );
sqlSession1 . close (); // 清空⼀级缓存
 
User user = new User ();
user . setId ( 1 );
user . setUsername ( "lisi" );
mapper3 . updateUser ( user );
sqlSession3 . commit ();
User user2 = mapper2 . findUserById ( 1 );
System . out . println ( user1 == user2 );
}

 

源码分析:
RedisCache 和⼤家普遍实现 Mybatis 的缓存⽅案⼤同⼩异,⽆⾮是实现 Cache 接⼝,并使⽤ jedis 操作缓
存;不过该项⽬在设计细节上有⼀些区别;
 
public final class RedisCache implements Cache {
public RedisCache ( final String id ) {
if ( id == null ) {
throw new IllegalArgumentException ( "Cache instances require anID" );
}
this . id = id ;
RedisConfig redisConfig =
RedisConfigurationBuilder . getInstance (). parseConfiguration ();
pool = new JedisPool ( redisConfig , redisConfig . getHost (),
redisConfig . getPort (),
redisConfig . getConnectionTimeout (),
redisConfig . getSoTimeout (), redisConfig . getPassword (),
redisConfig . getDatabase (), redisConfig . getClientName ());
}
RedisCache mybatis 启动的时候,由 MyBatis CacheBuilder 创建,创建的⽅式很简单,就是调⽤
RedisCache 的带有 String 参数的构造⽅法,即 RedisCache(String id) ;⽽在 RedisCache 的构造⽅法中,
调⽤了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使⽤ RedisConfig 来创建 JedisPool
RedisConfig 类继承了 JedisPoolConfig ,并提供了 host,port 等属性的包装,简单看⼀下 RedisConfig
属性:
public class RedisConfig extends JedisPoolConfig {
private String host = Protocol . DEFAULT_HOST ;
private int port = Protocol . DEFAULT_PORT ;
private int connectionTimeout = Protocol . DEFAULT_TIMEOUT ;
private int soTimeout = Protocol . DEFAULT_TIMEOUT ;
private String password ;
private int database = Protocol . DEFAULT_DATABASE ;
private String clientName ;

 RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要⽅法:

public RedisConfig parseConfiguration ( ClassLoader classLoader ) {
Properties config = new Properties ();
InputStream input =
classLoader . getResourceAsStream ( redisPropertiesFilename );
if ( input != null ) {
try {
config . load ( input );
} catch ( IOException e ) {
throw new RuntimeException (
"An error occurred while reading classpath property '"
+ redisPropertiesFilename
+ "', see nested exceptions" , e );
} finally {
try {
input . close ();
} catch ( IOException e ) {
// close quietly
}
}
}
RedisConfig jedisConfig = new RedisConfig ();
setConfigProperties ( config , jedisConfig );
return jedisConfig ;
}

 核⼼的⽅法就是parseConfiguration⽅法,该⽅法从classpath中读取⼀个redis.properties⽂件

host = localhost
port = 6379
connectionTimeout = 5000
soTimeout = 5000
password = database = 0 clientName =
并将该配置⽂件中的内容设置到 RedisConfig 对象中,并返回;接下来,就是 RedisCache 使⽤
RedisConfig 类创建完成 edisPool ;在 RedisCache 中实现了⼀个简单的模板⽅法,⽤来操作 Redis
 
private Object execute ( RedisCallback callback ) {
Jedis jedis = pool . getResource ();
try {
return callback . doWithRedis ( jedis );
} finally {
jedis . close ();
}
}

 模板接⼝为RedisCallback,这个接⼝中就只需要实现了⼀个doWithRedis⽅法⽽已:

public interface RedisCallback {
Object doWithRedis ( Jedis jedis );
}
接下来看看 Cache 中最重要的两个⽅法: putObject getObject ,通过这两个⽅法来查看 mybatis-redis
 
储存数据的格式:
 
@Override
public void putObject ( final Object key , final Object value ) {
execute ( new RedisCallback () {
@Override
public Object doWithRedis ( Jedis jedis ) {
jedis . hset ( id . toString (). getBytes (), key . toString (). getBytes (),
SerializeUtil . serialize ( value ));
return null ;
}
});
}
@Override
public Object getObject ( final Object key ) {
return execute ( new RedisCallback () {
 
@Override
public Object doWithRedis ( Jedis jedis ) {
return SerializeUtil . unserialize ( jedis . hget ( id . toString (). getBytes (),
key . toString (). getBytes ()));
}
});
}

 可以很清楚的看到,mybatis-redis在存储数据的时候,是使⽤的hash结构,把cacheid作为这个hash

key (cache id mybatis 中就是 mapper namespace) ;这个 mapper 中的查询缓存数据作为 hash
 
field, 需要缓存的内容直接使⽤ SerializeUtil 存储, SerializeUtil 和其他的序列化类差不多,负责 对象
 
的序列化和反序列化;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值