015:SqlSessionFactory二级缓存原理分析
1 如何禁止mybatis一级缓存
课程内容:
1.如何禁止使用Mybatis一级缓存
2.SessionFactory二级缓存原理分析
3.Mybatis源码分析之TransactionalCache
4.Mybatis源码分析之EhCache二级缓存淘汰策略
5.SqlSession的Close方法源码分析
6.Mybatis源码分析之StatementHandler
7.Mybatis源码分析之SpringBoot整合Mybatis源码分析
一级缓存
mybatis的一级缓存是SqlSession级别的缓存,在操作数据库的时候需要先创建SqlSession会话对象,在对象中有一个HashMap用于存储缓存数据,此HashMap是当前会话对象私有的,别的SqlSession会话对象无法访问。
具体流程:
1.第一次执行select完毕会将查到的数据写入SqlSession内的HashMap中缓存起来;
2.第二次执行select会从缓存中查数据,如果sql语句相同且传参数一样,那么就能从缓存中返回数据,不用去查找数据库,从而提高了效率;
注意事项:
如果SqlSession执行了DML操作(insert、update、delete)并commit了,那么mybatis就会清空当前SqlSession缓存中的所有缓存数据,这样可以保证缓存中存的数据永远和数据库中一致,避免出现脏读。
当一个SqlSession结束后它里面的一级缓存也不存在了,mybatis默认开启一级缓存,不需要配置。mybatis的缓存是基于[namespace:sql:参数…]来进行缓存的,SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数…]作为key。
注意:服务器集群的时候,每个sqlSession有自己独立的缓存,相互之间不存在共享,所以在服务器集群的时候容易产生数据冲突问题。
如何禁止一级缓存
方案1 在sql语句上随机生成不同的参数 存在缺点:map集合可能爆 内存溢出的问题
方案2 开启二级缓存
方案3 使用sqlSession强制清除缓存 sqlSession.clearCache();
方案4 创建新的sqlSession连接。
2 mybatis一级缓存与二级缓存的区别
二级缓存SessionFactory
二级缓存是mapper级别的缓存,也就是同一个namespace的mapper.xml,当多个SqlSession使用同一个Mapper操作数据库的时候,得到的数据会缓存在同一个二级缓存区域。二级缓存默认是开启的,但是没有配置缓存介质不实际生效。
一级缓存与二级缓存区别
1、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(sqlHashMap)是互相不影响的。
2、二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
注意:sqlSession缓存底层存在线程安全问题。
3 mybatis使用Redis作为二级缓存
1.maven引包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
2.需要在setting全局参数中配置开启二级缓存 mybatis_config.xml
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
3.在UserMapper.xml添加配置
<cache eviction="LRU" type="com.mayikt.cache.MybatisRedisCache" />
4.redis相关类
/**
* mybatis二级缓存整合Redis
*/
public class MybatisRedisCache implements Cache {
private static Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
private Jedis redisClient = createReids();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public int getSize() {
return Integer.valueOf(redisClient.dbSize().toString());
}
@Override
public void putObject(Object key, Object value) {
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value);
redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value));
}
@Override
public Object getObject(Object key) {
Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value);
return value;
}
@Override
public Object removeObject(Object key) {
return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
}
@Override
public void clear() {
redisClient.flushDB();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
protected static Jedis createReids() {
JedisPool pool = new JedisPool("127.0.0.1", 6379);
return pool.getResource();
}
}
/**
* mybatis二级缓存整合Redis
*/
public class MybatisRedisCache implements Cache {
private static Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
private Jedis redisClient = createReids();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public int getSize() {
return Integer.valueOf(redisClient.dbSize().toString());
}
@Override
public void putObject(Object key, Object value) {
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject:" + key + "=" + value);
redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value));
}
@Override
public Object getObject(Object key) {
Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
logger.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject:" + key + "=" + value);
return value;
}
@Override
public Object removeObject(Object key) {
return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
}
@Override
public void clear() {
redisClient.flushDB();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
protected static Jedis createReids() {
JedisPool pool = new JedisPool("127.0.0.1", 6379);
return pool.getResource();
}
}
5.实体类实现序列化
public class UserEntity implements Serializable
4 mybatis二级缓存源码分析01
测试类
public class TestMyBatis06 {
public static void main(String[] args) {
try {
// 1.定义mybatis文件
String resource = "mybatis_config.xml";
// 2.使用java的api读取mybatis_config.xml文件,获取InputStreamReader
Reader resourceAsReader = Resources.getResourceAsReader(resource);
// 3.获取SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsReader);
// 4.获取Session
SqlSession sqlSession1 = sqlSessionFactory.openSession();
System.out.println("第一次查询...");
UserEntity userEntity1 = sqlSession1.selectOne("com.mayikt.mapper.UserMapper.getUser", 2);
System.out.println(userEntity1.getName());
SqlSession sqlSession2 = sqlSessionFactory.openSession();
System.out.println("第二次查询...");
// sqlSession1.close();
UserEntity userEntity2 = sqlSession2.selectOne("com.mayikt.mapper.UserMapper.getUser", 2);
System.out.println(userEntity2.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果:
此时第二次查询走缓存代码二级缓存仍为null,原因是中间执行了一次openSession()方法,该方法每次执行创建一个新的缓存执行器,二级缓存管理器(tcm)也会重新创建一个。TransactionalCacheManager管理TransactionalCache和sqlSession绑定。
中间插入执行sqlSession1.close(),流程正常,第二次查询从二级缓存中获取数据:
5 mybatis二级缓存源码分析02
1.通过cache对象创建transactionalCache
TransactionalCache:继承自Cache接口,主要作用是保存SqlSession在事务中需要向某个二级缓存提交的缓存数据(因为事务过程中的数据可能会回滚,所以不能直接把数据就提交二级缓存,而是暂存在TransactionalCache中,在事务提交后再将过程中存放在其中的数据提交到二级缓存,如果事务回滚,则将数据清除掉)
2.执行getObject方法,底层查询redis是否存在缓存数据
1)List list = (List)this.tcm.getObject(cache, key); 会从redis中获取缓存数据;
2)this.tcm.putObject(cache, key, list); 只是将数据存放到TransactionalCache的entriesToAddOnCommit;entriesToAddOnCommit存放一级到二级缓存的缓存数据;
3)sqlSession1.close()/commit(); 将entriesToAddOnCommit中的数据放入二级缓存;单个sqlSession使用二级缓存可以手动调用以上方法。
6 mybatis缓存淘汰策略
二级缓存回收策略
LRU:最近最少使用的策略,移除最长时间不被使用的对象。
FIFO:先进先出策略,按对象进入缓存的顺序来移除它们。
SOFT:软引用策略,移除基于垃圾回收器状态和软引用规则的对象。
WEAK:弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象。
软引用与弱引用的区别:
软引用: 软引用是用来描述一些有用但并不是必需的对象, 对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象;
弱引用: 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象;
7 实际SpringBoot项目sqlSession源码分析
实际项目中整合springboot sqlSession会创建多次吗?
会,每个线程创建自己独立的sqlSession。
sqlSession因为线程不安全,所以不会共享;而sqlSessionFactory会共享。
SpringBoot整合redis作为二级缓存:
1.Mapper类加上注解@CacheNamespace(implementation = MybatisRedisCache.class)
2.启动类加上注解@EnableCaching
Spring整合mybatis的时候,有一个SqlSessionInterceptor类,当执行完sql语句的时候,会判断sqlSession事务有没有提交。有需要缓存数据提交的时候,底层封装sqlSession.commit方法;当每次当前sqlSession执行结束时候会调用closeSqlSession方法关闭当前sqlSession。因此,每次访问请求,都会单独有一个独立的线程创建独立的sqlSession。