mybatis的一级、二级、分布式缓存的应用以及源码分析

  • 一、mybatis缓存
  • 二、一级缓存
  • 三、一级缓存源码分析
  • 四、二级缓存
  • 五、二级缓存整合redis
  • 六、二级缓存整合redis源码分析

一、mybatis缓存

缓存就是内存中的数据,常常来自对数据库查询结果的保存。使用缓存,我们可以避免频繁与数据库进行交互,从而提高响应速度。

mybatis 也提供了对缓存的支持,分为一级缓存和二级缓存,来看下下面这张图:

1、一级缓存SqlSession级别的缓存。在操作数据库时需要构造SqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的是SqlSession之间的缓存数据区(HashMap)是互相不影响。

2、二级缓存Mapper级别的缓存,多个SqlSession去操作同一个Mappersql语句,多个SqlSession可以共用二级缓存,二级缓存是SqlSession跨的。

二、一级缓存

1、我们在一个SqlSession中,对User表根据id进行两次查询,查看它们发出sql语句的情况。

一级缓存默认是开启的,现在我们来验证下:

我们先把一些通用的操作先用@Before来操作下面各个测试方法之前要运行的方法。

private OrderMapper orderMapper;private SqlSession sqlSession;private SqlSessionFactory sqlSessionFactory;@Before
public void before() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);sqlSession = sqlSessionFactory.openSession(true);userMapper = sqlSession.getMapper(UserMapper.class);orderMapper = sqlSession.getMapper(OrderMapper.class);
}
@Test
public void firstLevelCache() {// 第一次查询id为1的用户User user1 = userMapper.findUserById(1);// 第二次查询id为1的用户User user2 = userMapper.findUserById(1);System.out.println(user1 == user2);
}

控制台日志输出:

2、我们继续对User表进行两次查询,只不过两次查询之间加入一次update操作。

@Test
public void firstLevelCacheOfUpdate() {// 第一次查询id为1的用户User user1 = userMapper.findUserById(1);// 更新用户User user = new User();user.setId(3);user.setUsername("tom");userMapper.updateUser(user);sqlSession.commit(); // 刷新一级缓存// sqlSession.clearCache(); 这个也可以刷新一级缓存// 第二次查询id为1的用户User user2 = userMapper.findUserById(1);System.out.println(user1 == user2);
}

控制台日志输出:

3、小结

  • 第一次发起查询用户id为1的用户信息,先去缓存中找是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
  • 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空sqlSession中的一级缓存,这样做的目的为了让缓存存储最新的信息,避免脏读。
  • 第二次查询id为1的用户信息,先去缓存中找是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。

三、一级缓存源码分析

1、一级缓存的工作流程

调研了一下,画出工作流程图:

跟踪到PerpetualCache中的clear()方法。

...
private Map<Object, Object> cache = new HashMap();public void clear() {this.cache.clear();
}
...

也就是说一级缓存的底层数据结构就是 HashMap。所以说cache.clear()其实就是map.clear(),也就是说,缓存其实是本地存放的一个map对象,每一个SqlSession都会存放一个map对象的引用。

那么这个cache是何时创建的呢?

你觉得最有可能创建缓存的地方是哪?我觉得是Executor,为啥这么认为?因为Executor是执行器,用来执行sql请求,而且清除缓存的方法也在Executor中执行,去查看源码,果真在里面。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (this.closed) {throw new ExecutorException("Executor was closed.");} else {CacheKey cacheKey = new CacheKey();// MappedStatement的id// id 就是sql语句的所在位置 包名+类名+sql名称cacheKey.update(ms.getId());// offset 就是0cacheKey.update(rowBounds.getOffset());// limit 就是Integer.MAX_VALUEcacheKey.update(rowBounds.getLimit());// 具体的sql语句cacheKey.update(boundSql.getSql());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);}// sql中带的参数cacheKey.update(value);}}if (this.configuration.getEnvironment() != null) {cacheKey.update(this.configuration.getEnvironment().getId());}return cacheKey;}
}

创建缓存key会经过一系列的update方法,update方法由一个cacheKey 这个对象来执行的。这个update方法最终由updateListlist来把六个值存进去,对照上面的代码,你应该能理解下面六个值分别是啥了吧。

这里需要关注最后一个值this.configuration.getEnvironment().getId(),这其实就是定义在mybatis-config.xml中的标签。如下:

那么问题来了。创建缓存了,那具体在哪里用呢?我们一级缓存探究后,我们发现一级缓存更多的用于查询操作,比较一级缓存也叫查询缓存嘛。我们跟踪到query方法:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);// 创建缓存CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (this.closed) {throw new ExecutorException("Executor was closed.");} else {if (this.queryStack == 0 && ms.isFlushCacheRequired()) {this.clearLocalCache();}List list;try {++this.queryStack;// resultHandler为null的话,走的this.localCache.getObject(key)缓存list = resultHandler == null ? (List)this.localCache.getObject(key) : null;if (list != null) {// 主要处理存储过程用的this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 为null的话则从数据库中查list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {--this.queryStack;}if (this.queryStack == 0) {Iterator var8 = this.deferredLoads.iterator();while(var8.hasNext()) {BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();deferredLoad.load();}this.deferredLoads.clear();if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {this.clearLocalCache();}}return list;}
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);List list;try {list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {this.localCache.removeObject(key);}this.localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {this.localOutputParameterCache.putObject(key, parameter);}return list;
}

如果查不到话,就从数据库查,在queryFromDatabase中,会对localCache进行写入。

localCache对象的put方法最终交给map进行存放。

private Map<Object, Object> cache = new HashMap();public void putObject(Object key, Object value) {this.cache.put(key, value);
}

ok了,一级缓存的应用与源码分析就先到这,我们下面来介绍二级缓存。

四、二级缓存

二级缓存的原理与一级缓存一样,底层数据结构也是HashMap,不同的是,一级缓存是基于SqlSession的;而二级缓存是基于mapper文件的namespace的,也就是说多个SqlSession可以共享一个mapper中的二级缓存区域,这里注意如果要用二级缓存的话需要把一级缓存关闭,这样才会走二级缓存。mybatis是默认关闭二级缓存的,因为对于增删改操作频繁的话,那么二级缓存形同虚设,每次都会被清空缓存。

二级缓存结构图如下:

1、如何开启二级缓存

  • 开启二级缓存

    和一级缓存默认开启不一样,二级缓存需要我们手动开启。
    (1)、首先在全局配置文件SqlMapConfig.xml文件中加入如下代码:

    <!--开启二级缓存-->
    <settings><setting name="cacheEnabled" value="true"/>
    </settings>
    

    (2)、其次在UserMapper.xml文件中开启二级缓存

    mapper代理模式:

    <!--开启二级缓存-->
    <cache></cache>
    

    注解开发模式:

    @CacheNamespace(implementation = PerpetualCache.class) // 开启二级缓存
    public interface UserMapper {}
    

    mapper代理模式开启的二级缓存是一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现的二级缓存功能的类,我们不写type就使用mybatis默认的二级缓存,也可以去实现Cache接口来自定义缓存。下面注解的也是一样的道理,用@CacheNamespace 也是直接默认PerpetualCache这个类。

  • 实体类实现Serializable序列化接口


    开启二级缓存后,还需要将要缓存的实体类去实现Serializable序列化接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们再取出这个缓存的话,就需要反序列化。所以mybatis的所有pojo类都要去实现Serializable序列化接口。

2、测试二级缓存

(1)、测试二级缓存和与SqlSession无关

@Testpublic void secondLevelCache() {SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);// 第一次查询id为1的用户User user1 = userMapper1.findUserById(1);sqlSession1.close(); // 清空一级缓存// 第二次查询id为1的用户User user2 = userMapper2.findUserById(1);sqlSession2.close();System.out.println(user1 == user2);
}

第一次查询时,将查询结果放入缓存中,第二次查询,即使sqlSession1.close(); 清空了一级缓存,第二次查询依然不发出sql语句。

(2)、测试执行commit(),二级缓存数据清空

@Test
public void secondLevelCacheOfUpdate() {SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();SqlSession sqlSession3 = sqlSessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);// 第一次查询id为1的用户User user1 = userMapper1.findUserById(1);sqlSession1.close(); // 清空一级缓存User user = new User();user.setId(3);user.setUsername("edgar");userMapper3.updateUser(user);sqlSession3.commit();// 第二次查询id为1的用户User user2 = userMapper2.findUserById(1);sqlSession2.close();System.out.println(user1 == user2);
}

3、useCacheflushCache

useCache 是用来设置是否禁用二级缓存的,在statement中设置useCache="false",可以禁用当前select语句的二级缓存,即每次都会去数据库查询。如下:

<select id="findAll" resultMap="userMap" useCache="false">select * from user u left join orders o on u.id = o.uid
</select>

设置statement配置中的flushCache=“true”属性,默认情况下为true,即刷新缓存,一般执行完commit操作都需要刷新缓存,flushCache=“true”表示刷新缓存,这样可以避免增删改操作而导致的脏读问题。默认不要配置,如下:

<select id="findAll" resultMap="userMap" useCache="false" flushCache="true">select * from user u left join orders o on u.id = o.uid
</select>

五、二级缓存整合redis

上面我们介绍了mybatis自带的二级缓存,但这个缓存是单服务工作,无法实现分布式缓存。那什么是分布式缓存呢?假如我们系统部署在两台服务器1和2上,用户访问服务器1,将查询后的缓存放在服务器1上,加入另一个用户访问服务器2,那它就在服务器2上无法获取刚刚那个缓存。如下图所示:

为了解决这个问题,分布式缓存就应运而生了,这样不同的服务器要缓存数据都往它那存,取缓存也从它那取。如下图所示:

如上图所示,我们使用第三方缓存框架,将缓存存放在这个第三方框架中,无论多少台服务器,我们都从分布式缓存中获取数据。


这里我们来介绍mybatisredis整合

1、pom文件

<!--redis开启分布式mybatis二级缓存-->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>

2、配置文件

UserMapper.xml中加入:

<!--开启二级缓存(分布式)-->
<cache type="org.mybatis.caches.redis.RedisCache" />

3、redis.properties

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

4、测试

@Test
public void secondLevelCacheOfUpdate() {SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();SqlSession sqlSession3 = sqlSessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);// 第一次查询id为1的用户User user1 = userMapper1.findUserById(1);sqlSession1.close(); // 清空一级缓存User user = new User();user.setId(3);user.setUsername("edgar");userMapper3.updateUser(user);sqlSession3.commit();// 第二次查询id为1的用户User user2 = userMapper2.findUserById(1);sqlSession2.close();System.out.println(user1 == user2);
}

六、二级缓存整合redis源码分析

RedisCache和大家普遍实现的mybatis的缓存方案大同小异,无非就是实现Cache接口,并使用jedis操作缓存;不过该项目在设计细节有一些区别。

public final class RedisCache implements Cache {private final ReadWriteLock readWriteLock = new DummyReadWriteLock();private String id;private static JedisPool pool;public RedisCache(String id) {if (id == null) {throw new IllegalArgumentException("Cache instances require an ID");} else {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());}}...
}

RedisCachemybatis启动的时候,由MyBatisCacheBuilder创建,创建的方式很简单,就是调用RedisCache的带有String参数的构造方法,即RedisCache(String id)

而在RedisCache构造方法中,调用RedisConfigurationBuilder来创建RedisConfig对象,并使用RedisConfig来创建JedisPool

RedisConfig提供了host、port等属性的包装。

public class RedisConfig extends JedisPoolConfig {private String host = "localhost";private int port = 6379;private int connectionTimeout = 2000;private int soTimeout = 2000;private String password;private int database = 0;private String clientName;...
}

RedisConfig是由RedisConfigurationBuilder创建的,我们主要来看下核心方法parseConfiguration,该方法从classpath中读取一个redis.properties文件:

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
final class RedisConfigurationBuilder {private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";private static final String REDIS_RESOURCE = "redis.properties";private final String redisPropertiesFilename = System.getProperty("redis.properties.filename", "redis.properties");private RedisConfigurationBuilder() {}public static RedisConfigurationBuilder getInstance() {return INSTANCE;}public RedisConfig parseConfiguration() {return this.parseConfiguration(this.getClass().getClassLoader());}public RedisConfig parseConfiguration(ClassLoader classLoader) {Properties config = new Properties();InputStream input = classLoader.getResourceAsStream(this.redisPropertiesFilename);if (input != null) {try {config.load(input);} catch (IOException var12) {throw new RuntimeException("An error occurred while reading classpath property '" + this.redisPropertiesFilename + "', see nested exceptions", var12);} finally {try {input.close();} catch (IOException var11) {}}}RedisConfig jedisConfig = new RedisConfig();this.setConfigProperties(config, jedisConfig);return jedisConfig;}...
}

并将该配置文件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用RedisConfig类创建完成JedisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis:

private Object execute(RedisCallback callback) {Jedis jedis = pool.getResource();Object var3;try {var3 = callback.doWithRedis(jedis);} finally {jedis.close();}return var3;
}

模板接口RedisCallback,这个接口只需要实现一个doWithRedis方法:

public interface RedisCallback {Object doWithRedis(Jedis var1);
}

接下来看RedisCache最重要的两个方法putObjectgetObject

public void putObject(final Object key, final Object value) {this.execute(new RedisCallback() {public Object doWithRedis(Jedis jedis) {jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));return null;}});
}public Object getObject(final Object key) {return this.execute(new RedisCallback() {public Object doWithRedis(Jedis jedis) {return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));}});
}

不难发现,mybatis-redis在存储数据的时候,使用的是hash结构,把cacheid作为这个hashkey(cacheidmybatis中就是mappernamespace);这个mapper中的查询缓存数据作为hashfield,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他序列化工具类差不多,负责将对象序列化和反序列化。

我的架构梦:(二)MyBatis的一级、二级、分布式缓存的应用以及源码分析相关推荐

  1. mybatis第十话 - mybaits整个事务流程的源码分析

    1.故事前因 在分析mybatis源码时一直带的疑问,一直以为事务是在SqlSessionTemplate#SqlSessionInterceptor#invoke完成的,直到断点才发现并不简单! 在 ...

  2. Window XP驱动开发(二) 环境搭建(VS2008+WDK+DDKWzard)及示例源码分析

    郁闷,做了WCE嵌入式驱动这么久还没热身够,又被调到做window xp下的驱动开发.没办法.只能受令了. 现在就开始自己的学习之旅吧. 转载请标明是引用于 http://blog.csdn.net/ ...

  3. 我的架构梦:(六十三) 分布式缓存 Redis 之持久化

    分布式缓存 Redis 之持久化 一.前言 1.学习目标 2.为什么要持久化 二.RDB 1.触发快照的方式 2.RDB执行流程(原理) 3.RDB文件结构 4.RDB的优缺点 三.AOF 1.AOF ...

  4. MyBatis骨骼惊奇,跟着腾讯大牛学源码分析,总结出这份pdf文档

    什么是MyBatis MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为 ...

  5. 【Minecraft java edition 模组开发】(二):通过对岩浆怪和雪傀儡的源码分析,自己制作一个雪球怪

    零.什么是实体   实体(Entity)包括在Minecraft中所有动态的.移动中的对象.例如游戏中的怪物僵尸骷髅等,船和矿车,受重力影响的方块如下落的沙子铁砧等.   我们今天要加入的东西就是一个 ...

  6. 我的架构梦:(三)MyBatis源码分析

    mybatis的源码分析 一.传统方式源码分析 二.Mapper代理方式源码分析 三.MyBatis源码中涉及到的设计模式 一.传统方式源码分析 分析之前我们来回顾下传统方式的写法: /*** 传统方 ...

  7. MyBatis源码分析(一)MyBatis整体架构分析

    文章目录 系列文章索引 一.为什么要用MyBatis 1.原始JDBC的痛点 2.Hibernate 和 JPA 3.MyBatis的特点 4.MyBatis整体架构 5.MyBatis主要组件及其相 ...

  8. 十年老架构师神级推荐,MyBatis源码分析,再也不用为源码担忧了

    十年老架构师神级推荐,MyBatis源码分析,再也不用为源码担忧了 前言 MyBatis是一个优秀的持久层ORM框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注SQL 本身,而不需要花 ...

  9. springboot集成mybatis源码分析-启动加载mybatis过程(二)

    springboot集成mybatis源码分析-启动加载mybatis过程(二) 1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplicati ...

最新文章

  1. 登录文档服务器,服务器登录login
  2. 这么详细的Python matplotlib底层原理浅析
  3. 打不死的小强机器人来了,向心加速度堪比猎豹,能抵抗自身数百倍重量碾压...
  4. 一、你要拥有自己的服务器
  5. 存储时间:从Symmetrix V-Max看高端存储的未来
  6. java中==和equals引发的思考
  7. 赛诺朗基智能安全保障平台——安全着你的安全!
  8. 使用Selenium进行Spring Boot集成测试
  9. js parseInt()与Number()区别
  10. 得到IOleInPlaceActiveObject接口,IOleInPlaceActiveObject::TranslateAccelerator(msg);
  11. Kafka : kafka查询某时间段内的消息
  12. 特斯拉中国工厂2020投产,还可能为完全自动驾驶更新硬件
  13. java使用elasticsearch进行模糊查询-已在项目中实际应用
  14. bat快捷方式启动局域网共享文件
  15. dubbo源代码编译打包错误解决
  16. JUnit 单元测试多线程测试解决方法
  17. navicat 10免费下载及破解
  18. 人世之厄人性之恶——陈应松《母亲》读后
  19. Python100例——第五章----不定方程的解
  20. 寒武纪重磅发布首款AI云芯片,陈天石要让端云结合占领10亿智能终端!

热门文章

  1. html的公共样式,HTML+CSS入门 CSS公共样式
  2. 【收藏夹】人工智能领域的一些博客/论文/资讯 2017
  3. 对抗海量表格数据,【华为2012实验室】没有选择复仇者联盟
  4. 全终端办公电子邮件集成方案
  5. C++ 如何加载lib
  6. ios程序员的创业之路
  7. 一级造价工程师(安装)- 计量笔记 - 第二章第三节吊装工程
  8. arduino计时器程序
  9. 一文读懂SpringBoot中的事件机制
  10. Springboot配置Nacos出现异常