Mybatis缓存的使用和源码分析()

  本篇文章为你整理了Mybatis缓存的使用和源码分析()的详细内容,包含有 Mybatis缓存的使用和源码分析,希望能帮助你了解 Mybatis缓存的使用和源码分析。

  Mybatis 缓存使用

  在Mybatis中缓存分为一级缓存和二级缓存,二级缓存又称为全局缓存,默认一级缓存和二级缓存都是开启的,只是二级缓存的使用需要配置才能生效,在Mybatis中一级缓存是SqlSession级别也就是会话级别的,而二级缓存是Mapper级别的可以跨SqlSession会话。

  我们看看一级缓存的使用,查询用户信息:

  

 

 

  private SqlSessionFactory sqlSessionFactory;

  @Before

  public void before() {

   //第一步获取配置文件,并将其读取到流中

   String resource = "mybatis-config.xml";

   InputStream in = null;

   try {

   in = Resources.getResourceAsStream(resource);

   } catch (Exception e) {

   e.printStackTrace();

   //获取到sessionFactory

   sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);

  @Test

  public void firstCache() {

   //第二步,读取数据

   try (SqlSession sqlSession = sqlSessionFactory.openSession()) {

   UserMapper mapper = sqlSession.getMapper(UserMapper.class);

   // 第一次查询

   User user1 = mapper.selectById(1);

   // 第二次查询

   User user2 = mapper.selectById(1);

   System.out.println(user1 == user2);

   } catch (Exception e) {

   e.printStackTrace();

  

 

  输出:

  

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

 

  == Preparing: select * from user where id = ?

  == Parameters: 1(Integer)

   == Columns: id, age, name

   == Row: 1, 19, 李四

   == Total: 1

  Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

  Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

  Returned connection 8641687 to pool.

  

 

  可以看到,这里只进行了一次查询,并且结果值返回的true,说明在JVM内存中只创建了一个对象出来。

  Mybatis中一级缓存也会失效,什么时候失效呢?那就是在进行更新操作的时候就会导致一级缓存失效,比如:

  

 @Test

 

   public void firstCache() {

   //第二步,读取数据

   try (SqlSession sqlSession = sqlSessionFactory.openSession()) {

   UserMapper mapper = sqlSession.getMapper(UserMapper.class);

   /// 第一次查询

   User user1 = mapper.selectById(1);

   // 执行更新操作,破环一级缓存

   mapper.updateUser(new User().setId(1).setName("李四").setAge(19));

   sqlSession.commit();

   // 第二次查询

   User user2 = mapper.selectById(1);

   System.out.println(user1 == user2);

   } catch (Exception e) {

   e.printStackTrace();

  

 

  输出:

  

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

 

  == Preparing: select * from user where id = ?

  == Parameters: 1(Integer)

   == Columns: id, age, name

   == Row: 1, 19, 李四

   == Total: 1

  == Preparing: update user set name = ?, age = ? where id = ?

  == Parameters: 李四(String), 19(Integer), 1(Integer)

   == Updates: 1

  Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

  == Preparing: select * from user where id = ?

  == Parameters: 1(Integer)

   == Columns: id, age, name

   == Row: 1, 19, 李四

   == Total: 1

  false

  Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

  Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@83dc97]

  Returned connection 8641687 to pool.

  Process finished with exit code 0

  

 

  可以看到这里的执行了两次的查询,中间执行了一次更新的sql语句输出,并且这个对象不相等,返回结果为false,说明在JVM内存中创建了两个对象。

  接下来看看二级缓存的使用,二级缓存开启需要在mybatis-config.xml中开启,在settings标签中开启:

  

 settings 

 

   !-- 打印sql日志 --

   setting name="logImpl" value="STDOUT_LOGGING" /

   !--开启二级缓存--

   setting name="cacheEnabled" value="true"/

   /settings

  

 

  开启之后还需要在xxxMapper.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="mybatis.mapper.UserMapper"

   !--配置二级缓存--

   cache /

   select id="selectUserByName" resultType="mybatis.model.User"

   select *

   from user

   if test="name != null"

   where name = #{name}

   /if

   /select

   select id="selectById" resultType="mybatis.model.User"

   select *

   from user

   where id = #{id}

   /select

   update id="updateUser"

   update user

   set name = #{name},

   age = #{age}

   where id = #{id}

   /update

   select id="selectAll" resultType="mybatis.model.User"

   select * from user

   /select

   /mapper

  

 

  测试二级缓存:

  

 /**

 

   * 二级缓存测试

   @Test

   public void secondCache() {

   SqlSession sqlSession1 = sqlSessionFactory.openSession();

   SqlSession sqlSession2 = sqlSessionFactory.openSession();

   SqlSession sqlSession3 = sqlSessionFactory.openSession();

   UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);

   UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

   UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);

   // 测试二级缓存开始

   User user1 = mapper1.selectById(1);

   sqlSession1.close();

   System.out.println(user1);

   // 第二次查询

   User user2 = mapper2.selectById(1);

   System.out.println(user2);

   System.out.println(user1 == user2);

  
输出结果如下:

  

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a12036]

 

  == Preparing: select * from user where id = ?

  == Parameters: 1(Integer)

   == Columns: id, age, name

   == Row: 1, 18, 灵犀

   == Total: 1

  Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a12036]

  Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a12036]

  Returned connection 10559542 to pool.

  User(id=1, age=18, name=灵犀)

  Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.5

  User(id=1, age=18, name=灵犀)

  false

  Disconnected from the target VM, address: 127.0.0.1:4672, transport: socket

  Process finished with exit code 0

  

 

  第二次查询没有执行sql语句,并且日志打印了缓存命中率为0.5,并且这两个对象不相等,这是为啥呢?

  原因是二级缓存的使用必须要求缓存对象实现序列化接口,因为二级缓存的实现是通过将数据序列化在保存的,当第二次查询的时候,如果缓存中有那就将数据再反序列化出来,由于反序列化时每次都是重新创建的对象,因此即使是缓存命中也不相等,缓存命中为0.5的原因是第一次没有命中,第二次命中了,请求了2次因此 1/2 得到的就是0.5。

  所以在使用二级缓存的时候,使用的对象必须要实现序列化接口,否则就会报错:

  

 

 

  org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: mybatis.model.User

   at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:94)

   at org.apache.ibatis.cache.decorators.SerializedCache.putObject(SerializedCache.java:55)

   at org.apache.ibatis.cache.decorators.LoggingCache.putObject(LoggingCache.java:49)

   at org.apache.ibatis.cache.decorators.SynchronizedCache.putObject(SynchronizedCache.java:43)

   at org.apache.ibatis.cache.decorators.TransactionalCache.flushPendingEntries(TransactionalCache.java:116)

   at org.apache.ibatis.cache.decorators.TransactionalCache.commit(TransactionalCache.java:99)

   at org.apache.ibatis.cache.TransactionalCacheManager.commit(TransactionalCacheManager.java:44)

   at org.apache.ibatis.executor.CachingExecutor.close(CachingExecutor.java:61)

   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

   at java.lang.reflect.Method.invoke(Method.java:498)

   at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)

   at com.sun.proxy.$Proxy15.close(Unknown Source)

   at org.apache.ibatis.session.defaults.DefaultSqlSession.close(DefaultSqlSession.java:263)

   at mybatis.MybatisApplication.secondCache(MybatisApplication.java:73)

   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

   at java.lang.reflect.Method.invoke(Method.java:498)

   at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)

   at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)

   at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)

   at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)

   at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)

   at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)

   at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)

   at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)

   at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)

   at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)

   at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)

   at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)

   at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)

   at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)

   at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)

   at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)

   at org.junit.runners.ParentRunner.run(ParentRunner.java:413)

   at org.junit.runner.JUnitCore.run(JUnitCore.java:137)

   at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)

   at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)

   at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)

   at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)

   at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)

   at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

  Caused by: java.io.NotSerializableException: mybatis.model.User

   at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

   at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

   at java.util.ArrayList.writeObject(ArrayList.java:768)

   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

   at java.lang.reflect.Method.invoke(Method.java:498)

   at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1155)

   at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)

   at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)

   at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)

   at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

   at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:90)

   ... 43 more

  

 

  Mybatis中的缓存介绍

  在Mybatis中内置了很多的缓存,比如永久缓存、LRU缓存、FIFO缓存等,这些缓存都是用来为一级缓存和二级缓存使用的Mybatis中的缓存在实现时使用了装饰器模式进行实现,列举下Mybatis中的缓存常见的种类:

  BlockingCache 阻塞缓存,当获取不到数据时会将缓存key锁定,其他线程会一直等待直到缓存被写入

  LruCache 最近最少使用缓存,当缓存数量达到一定数量时,缓存就会进行淘汰,将最近最少是使用的缓存给淘汰掉

  FifoCache 先进先出缓存,缓存谁先进来,谁就先获取到

  LoggingCache 用于打印日志的缓存,记录缓存的命中率

  TranslationalCache 事务缓存,专用于二级缓存的,当事务提交时缓存就会保存,当事务回滚时就会将缓存个清除掉,所以二级缓存的生效前提是必须事务的提交

  SerializedCache 序列化缓存,二级缓存使用就会装饰这个缓存,用于对象的序列化,通过ObjectOutputStream和ObjectInputStream进行序列化和反序列化使用

  PerpetualCache 永久缓存,也就是缓存的最终实现,其他的缓存都会通过委派,最终交给永久缓存进行数据的保存,缓存会保存在Map中

  当然还有WeakCache弱引用缓存,SynchronizedCache同步缓存,SoftCache软引用缓存,ScheduledCache调度缓存

  默认情况下一级缓存和二级缓存都是使用的默认实现永久缓存进行保存数据的,这些数据都是保存在Map中,但是二级缓存可以指定缓存的类型,比如我们可以将数据缓存到redis中。

  二级缓存使用Redis保存数据

  在Mybatis中已经有这个实现,只需引入依赖即可使用:

  

 dependency 

 

   groupId org.mybatis.caches /groupId

   artifactId mybatis-redis /artifactId

   version 1.0.0-beta2 /version

   /dependency

  

 

  然后在 标签上配置:

  

 cache type="org.mybatis.caches.redis.RedisCache"/ 

 

  

 

  然后还需要配置redis.properties文件,里面指定redis的配置,注意这个文件名字不能修改:

  

host=192.168.64.129

 

  port=6379

  password=

  database=0

  

 

  执行方法得到输出:

  

== Preparing: select * from user where id = ? 

 

  == Parameters: 1(Integer)

   == Columns: id, age, name

   == Row: 1, 18, 灵犀

   == Total: 1

  Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]

  Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]

  Returned connection 10711284 to pool.

  User(id=1, age=18, name=灵犀)

  Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.5

  User(id=1, age=18, name=灵犀)

  false

  

 

  使用redis客户端连接工具查看是否缓存数据:

  发现key为:

  

677706599:5423390838:mybatis.mapper.UserMapper.selectById:0:2147483647:select *

 

   from user

   where id = ?:1:development

  

 

  value为:

  

\xAC\xED\x00\x05sr\x00\x13java.util.ArrayListx\x81\xD2\x1D\x99\xC7a\x9D\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x01w\x04\x00\x00\x00\x01sr\x00\x12mybatis.model.User\xF4\xBB\x11O\xFF\xA4\xDC \x02\x00\x03I\x00\x03ageI\x00\x02idL\x00\x04namet\x00\x12Ljava/lang/String;xp\x00\x00\x00\x12\x00\x00\x00\x01t\x00\x06\xE7\x81\xB5\xE7\x8A\x80x

 

  

 

  说明缓存是生效了明确保存到了redis中,再次查询:

  

Cache Hit Ratio [mybatis.mapper.UserMapper]: 1.0

 

  User(id=1, age=18, name=灵犀)

  Cache Hit Ratio [mybatis.mapper.UserMapper]: 1.0

  User(id=1, age=18, name=灵犀)

  false

  

 

  直接走缓存了,二级缓存失效的原理跟一级缓存一样,只有有更新操作并且提交了事务,那么都会将缓存给清空导致缓存失效。

  比如:

  

@Test

 

  public void secondCache() {

   SqlSession sqlSession1 = sqlSessionFactory.openSession();

   SqlSession sqlSession2 = sqlSessionFactory.openSession();

   SqlSession sqlSession3 = sqlSessionFactory.openSession();

   UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);

   UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

   UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);

   // 测试二级缓存开始

   User user1 = mapper1.selectById(1);

   sqlSession1.close();

   System.out.println(user1);

   // 调用更新操作,破环二级缓存

   mapper3.updateUser(new User().setId(1).setName("灵犀").setAge(18));

   sqlSession3.commit();

   // 第二次查询

   User user2 = mapper2.selectById(1);

   System.out.println(user2);

   System.out.println(user1 == user2);

  
== Total: 1

  Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]

  Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]

  Returned connection 10711284 to pool.

  User(id=1, age=18, name=灵犀)

  Opening JDBC Connection

  Checked out connection 10711284 from pool.

  Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]

  == Preparing: update user set name = ?, age = ? where id = ?

  == Parameters: 灵犀(String), 18(Integer), 1(Integer)

   == Updates: 1

  Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a370f4]

  Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0

  Opening JDBC Connection

  Created connection 28524404.

  Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1b33f74]

  == Preparing: select * from user where id = ?

  == Parameters: 1(Integer)

   == Columns: id, age, name

   == Row: 1, 18, 灵犀

   == Total: 1

  User(id=1, age=18, name=灵犀)

  false

  Process finished with exit code 0

  

 

  缓存源码解析

  由于一级缓存是不需要配置的,默认就是使用的永久缓存,而二级缓存需要配置并且可以指定使用不同的缓存实现,所以先看二级缓存配置加载过程,二级缓存的配置加载实际上就是解析 cache 标签或者是解析@CacheNamespace注解,从源码入口找到解析mapper.xml的地方,会找到Configuration#addMapper方法,最终会在MapperRegistry类中开始解析:

  

 public T void addMapper(Class T type) {

 

   if (type.isInterface()) {

   if (hasMapper(type)) {

   throw new BindingException("Type " + type + " is already known to the MapperRegistry.");

   boolean loadCompleted = false;

   try {

   knownMappers.put(type, new MapperProxyFactory (type));

   // Its important that the type is added before the parser is run

   // otherwise the binding may automatically be attempted by the

   // mapper parser. If the type is already known, it wont try.

   MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);

   // 解析dao接口上标注的注解比如@CacheNamespace、@Select、@Insert、@Update、@Delete

   parser.parse();

   loadCompleted = true;

   } finally {

   if (!loadCompleted) {

   knownMappers.remove(type);

  

 

  点进去会进入到MapperAnnotaionBuilder类中,这里会进行xml的解析和注解的解析

  

public void parse() {

 

   String resource = type.toString();

   if (!configuration.isResourceLoaded(resource)) {

   // 加载xml资源并解析,这里xml配置文件是放在dao接口包下的,并且名称也是跟dao接口名一致,否则无法解析,一般来说不会这样做

   loadXmlResource();

   configuration.addLoadedResource(resource);

   // 设置当前的namespace为类名称

   assistant.setCurrentNamespace(type.getName());

   // 解析缓存

   parseCache();

   parseCacheRef();

   // 遍历所有的方法,并解析注解

   for (Method method : type.getMethods()) {

   if (!canHaveStatement(method)) {

   continue;

   if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()

   method.getAnnotation(ResultMap.class) == null) {

   // 解析ResultMap注解

   parseResultMap(method);

   try {

   // 解析方法

   parseStatement(method);

   } catch (IncompleteElementException e) {

   configuration.addIncompleteMethod(new MethodResolver(this, method));

   parsePendingMethods();

  

 

  我们使用的xml配置,那么直接看解析xml的地方,点进去:

  

private void loadXmlResource() {

 

   // Spring may not know the real resource name so we check a flag

   // to prevent loading again a resource twice

   // this flag is set at XMLMapperBuilder#bindMapperForNamespace

   if (!configuration.isResourceLoaded("namespace:" + type.getName())) {

   String xmlResource = type.getName().replace(., /) + ".xml";

   // #1347

   InputStream inputStream = type.getResourceAsStream("/" + xmlResource);

   if (inputStream == null) {

   // Search XML mapper that is not in the module but in the classpath.

   try {

   inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);

   } catch (IOException e2) {

   // ignore, resource is not required

   if (inputStream != null) {

   // 创建一个xmlMapper的解析器

   XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());

   xmlParser.parse();

  

 

  再点进去,进入到XmlMapperBuilder类中:

  

 public void parse() {

 

   if (!configuration.isResourceLoaded(resource)) {

   // 解析mapper配置

   configurationElement(parser.evalNode("/mapper"));

   configuration.addLoadedResource(resource);

   bindMapperForNamespace();

   // 解析ResultMap

   parsePendingResultMaps();

   parsePendingCacheRefs();

   // 解析sql

   parsePendingStatements();

  

 

  由于 cache 标签是配置再mapper标签下,所以点击configurationElement方法:

  

 private void configurationElement(XNode context) {

 

   try {

   // 获取namespace

   String namespace = context.getStringAttribute("namespace");

   if (namespace == null namespace.isEmpty()) {

   throw new BuilderException("Mappers namespace cannot be empty");

   // 设置当前的namespace

   builderAssistant.setCurrentNamespace(namespace);

   cacheRefElement(context.evalNode("cache-ref"));

   // 解析缓存配置标签

   cacheElement(context.evalNode("cache"));

   // 解析参数map

   parameterMapElement(context.evalNodes("/mapper/parameterMap"));

   // 解析resultMap

   resultMapElements(context.evalNodes("/mapper/resultMap"));

   // 解析sql标签

   sqlElement(context.evalNodes("/mapper/sql"));

   // 解析curd标签

   buildStatementFromContext(context.evalNodes("selectinsertupdatedelete"));

   } catch (Exception e) {

   throw new BuilderException("Error parsing Mapper XML. The XML location is " + resource + ". Cause: " + e, e);

  

 

  我们只关心 cache 标签,点进去:

  

 private void cacheElement(XNode context) {

 

   if (context != null) {

   // 获取 缓存type类型,默认是 PerpetualCache 永久缓存

   String type = context.getStringAttribute("type", "PERPETUAL");

   Class ? extends Cache typeClass = typeAliasRegistry.resolveAlias(type);

   // 获取过期的缓存淘汰策略,默认是LRUCache 最近最少缓存

   String eviction = context.getStringAttribute("eviction", "LRU");

   Class ? extends Cache evictionClass = typeAliasRegistry.resolveAlias(eviction);

   // 缓存刷新间隔

   Long flushInterval = context.getLongAttribute("flushInterval");

   Integer size = context.getIntAttribute("size");

   // 默认可读可写

   boolean readWrite = !context.getBooleanAttribute("readOnly", false);

   // 默认非阻塞

   boolean blocking = context.getBooleanAttribute("blocking", false);

   Properties props = context.getChildrenAsProperties();

   // 创建缓存

   builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);

  

 

  可以看到默认使用的是永久缓存,如果没有配置type指定缓存的话,点击useNewCache方法:

  

public Cache useNewCache(Class ? extends Cache typeClass,

 

   Class ? extends Cache evictionClass,

   Long flushInterval,

   Integer size,

   boolean readWrite,

   boolean blocking,

   Properties props) {

   // 建造者模式创建缓存对象

   Cache cache = new CacheBuilder(currentNamespace)

   .implementation(valueOrDefault(typeClass, PerpetualCache.class))

   .addDecorator(valueOrDefault(evictionClass, LruCache.class))

   .clearInterval(flushInterval)

   .size(size)

   .readWrite(readWrite)

   .blocking(blocking)

   .properties(props)

   .build();

   configuration.addCache(cache);

   currentCache = cache;

   return cache;

  

 

  这里使用了建造者模式创建缓存,点击build方法进入到CacheBuilder中:

  

public Cache build() {

 

   setDefaultImplementations();

   // 创建缓存对象,默认创建的是PerpetualCache 永久缓存

   Cache cache = newBaseCacheInstance(implementation, id);

   setCacheProperties(cache);

   // issue #352, do not apply decorators to custom caches

   if (PerpetualCache.class.equals(cache.getClass())) {

   for (Class ? extends Cache decorator : decorators) {

   // 反射创建出缓存对象

   cache = newCacheDecoratorInstance(decorator, cache);

   // 设置默认的属性

   setCacheProperties(cache);

   // 设置标准的缓存装饰器

   cache = setStandardDecorators(cache);

   } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {

   cache = new LoggingCache(cache);

   return cache;

  

 

  这里通过反射创建出缓存的类型,最终设置了一个标准的缓存装饰,调用了setStandardDecorators方法:

  

private Cache setStandardDecorators(Cache cache) {

 

   try {

   MetaObject metaCache = SystemMetaObject.forObject(cache);

   if (size != null metaCache.hasSetter("size")) {

   metaCache.setValue("size", size);

   // 如果周期清除不为空,那么将缓存装饰为调度缓存对象

   if (clearInterval != null) {

   cache = new ScheduledCache(cache);

   ((ScheduledCache) cache).setClearInterval(clearInterval);

   if (readWrite) {

   // 读写,默认是支持读写的,创建一个序列化的缓存,所以在开启二级缓存的时候需要将查询的对象实体实现序列化接口,否则会报错

   cache = new SerializedCache(cache);

   // 日志缓存,在查询是会打印命中的缓存概率,可以在config.xml中配置日志,开启日志打印

   cache = new LoggingCache(cache);

   // 同步缓存

   cache = new SynchronizedCache(cache);

   if (blocking) {

   // 阻塞缓存,默认是没有开启,需要手动开启,当缓存没有命中,那么就会导致其他线程一直等待,直到缓存被填充进去

   cache = new BlockingCache(cache);

   return cache;

   } catch (Exception e) {

   throw new CacheException("Error building standard cache decorators. Cause: " + e, e);

  

 

  可以看到默认情况下readWrite这个属性是true,这里会添加一个序列化缓存进行装饰,然后日志缓存也会进行装饰,同步缓存也会进行装饰,所以在打印日志时看到如果对象没有进行序列化那么就会报错,并且金可以看到缓存命中率的日志会打印,并且获取缓存都是加了synchronized同步关键字的,不会存在线程安全问题。

  这是缓存的准备阶段,接下来看缓存在执行过程中是怎么处理的?

  以查询为例,还是以selectList为例,在DefaultSqlSession中:

  

private E List E selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {

 

   try {

   // 获取Mapper中解析的配置,这个类中存放了sql语句,返回类型,参数类型等

   MappedStatement ms = configuration.getMappedStatement(statement);

   // 调用查询,这里默认是CachingExecutor

   return executor.query(ms, wrapCollection(parameter), rowBounds, handler);

   } catch (Exception e) {

   throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);

   } finally {

   ErrorContext.instance().reset();

  

 

  这里的执行器默认是CachingExecutor,因为二级缓存默认是开启的,所以在解析构建Configuration时创建的执行器就是CachingExecutor,点进去:

  

 @Override

 

   public E List E query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

   BoundSql boundSql = ms.getBoundSql(parameterObject);

   // 创建缓存key

   CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);

   return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

  

 

  这里创建缓存key会委派到BaseExecutor类中去创建:

  

@Override

 

   public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {

   if (closed) {

   throw new ExecutorException("Executor was closed.");

   // 缓存key

   CacheKey cacheKey = new CacheKey();

   // crud的标签的id,statementId

   cacheKey.update(ms.getId());

   // 分页参数

   cacheKey.update(rowBounds.getOffset());

   cacheKey.update(rowBounds.getLimit());

   // sql语句

   cacheKey.update(boundSql.getSql());

   List ParameterMapping parameterMappings = boundSql.getParameterMappings();

   TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

   // mimic DefaultParameterHandler logic

   // 参数值

   for (ParameterMapping parameterMapping : parameterMappings) {

   if (parameterMapping.getMode() != ParameterMode.OUT) {

   Object value;

   String propertyName = parameterMapping.getProperty();

   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 = configuration.newMetaObject(parameterObject);

   value = metaObject.getValue(propertyName);

   cacheKey.update(value);

   if (configuration.getEnvironment() != null) {

   // issue #176

   // 环境信息

   cacheKey.update(configuration.getEnvironment().getId());

   return cacheKey;

  

 

  这就是创建的缓存key,可以看到由多个值组成,所以在redis看到key就是这样的:

  

677706599:5423390838:mybatis.mapper.UserMapper.selectById:0:2147483647:select *

 

   from user

   where id = ?:1:development

  

 

  完全是可以对应上的,进去query方法:

  

 @Override

 

   public E List E query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)

   throws SQLException {

   Cache cache = ms.getCache();

   if (cache != null) {

   // 如果有必要就清除(刷新)缓存

   flushCacheIfRequired(ms);

   if (ms.isUseCache() resultHandler == null) {

   ensureNoOutParams(ms, boundSql);

   // 从二级缓存中获取数据,这里使用的事务缓存

   @SuppressWarnings("unchecked")

   List E list = (List E ) tcm.getObject(cache, key);

   if (list == null) {

   list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

   tcm.putObject(cache, key, list); // issue #578 and #116

   return list;

   return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

  

 

  这里通过tcm去获取缓存,这个tcm就是事务缓存管理器TranslationalCacheManager,里面正是使用的TranslationalCache实现的,通过getObject方法调用就会经过装饰中的所有缓存组件,最终会调用到永久缓存或者是自定义缓存比如RedisCache,如果没有获取到,那么就会委派给BaseExecutor#query去查。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: