我的架构梦:(二)MyBatis的一级、二级、分布式缓存的应用以及源码分析
mybatis的一级、二级、分布式缓存的应用以及源码分析
- 一、mybatis缓存
- 二、一级缓存
- 三、一级缓存源码分析
- 四、二级缓存
- 五、二级缓存整合redis
- 六、二级缓存整合redis源码分析
一、mybatis缓存
缓存就是内存中的数据,常常来自对数据库查询结果的保存。使用缓存,我们可以避免频繁与数据库进行交互,从而提高响应速度。
mybatis
也提供了对缓存的支持,分为一级缓存和二级缓存,来看下下面这张图:
1、一级缓存是SqlSession
级别的缓存。在操作数据库时需要构造SqlSession
对象,在对象中有一个数据结构(HashMap
)用于存储缓存数据。不同的是SqlSession
之间的缓存数据区(HashMap
)是互相不影响。
2、二级缓存 是Mapper
级别的缓存,多个SqlSession
去操作同一个Mapper
的sql
语句,多个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
方法最终由updateList
的list
来把六个值存进去,对照上面的代码,你应该能理解下面六个值分别是啥了吧。
这里需要关注最后一个值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、useCache
和 flushCache
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上无法获取刚刚那个缓存。如下图所示:
为了解决这个问题,分布式缓存就应运而生了,这样不同的服务器要缓存数据都往它那存,取缓存也从它那取。如下图所示:
如上图所示,我们使用第三方缓存框架,将缓存存放在这个第三方框架中,无论多少台服务器,我们都从分布式缓存中获取数据。
这里我们来介绍mybatis
与redis
整合
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());}}...
}
RedisCache
在mybatis
启动的时候,由MyBatis
的CacheBuilder
创建,创建的方式很简单,就是调用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
最重要的两个方法putObject
和getObject
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
结构,把cache
的id
作为这个hash
的key
(cache
的id
在mybatis
中就是mapper
的namespace
);这个mapper
中的查询缓存数据作为hash
的field
,需要缓存的内容直接使用SerializeUtil
存储,SerializeUtil
和其他序列化工具类差不多,负责将对象序列化和反序列化。
我的架构梦:(二)MyBatis的一级、二级、分布式缓存的应用以及源码分析相关推荐
- mybatis第十话 - mybaits整个事务流程的源码分析
1.故事前因 在分析mybatis源码时一直带的疑问,一直以为事务是在SqlSessionTemplate#SqlSessionInterceptor#invoke完成的,直到断点才发现并不简单! 在 ...
- Window XP驱动开发(二) 环境搭建(VS2008+WDK+DDKWzard)及示例源码分析
郁闷,做了WCE嵌入式驱动这么久还没热身够,又被调到做window xp下的驱动开发.没办法.只能受令了. 现在就开始自己的学习之旅吧. 转载请标明是引用于 http://blog.csdn.net/ ...
- 我的架构梦:(六十三) 分布式缓存 Redis 之持久化
分布式缓存 Redis 之持久化 一.前言 1.学习目标 2.为什么要持久化 二.RDB 1.触发快照的方式 2.RDB执行流程(原理) 3.RDB文件结构 4.RDB的优缺点 三.AOF 1.AOF ...
- MyBatis骨骼惊奇,跟着腾讯大牛学源码分析,总结出这份pdf文档
什么是MyBatis MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为 ...
- 【Minecraft java edition 模组开发】(二):通过对岩浆怪和雪傀儡的源码分析,自己制作一个雪球怪
零.什么是实体 实体(Entity)包括在Minecraft中所有动态的.移动中的对象.例如游戏中的怪物僵尸骷髅等,船和矿车,受重力影响的方块如下落的沙子铁砧等. 我们今天要加入的东西就是一个 ...
- 我的架构梦:(三)MyBatis源码分析
mybatis的源码分析 一.传统方式源码分析 二.Mapper代理方式源码分析 三.MyBatis源码中涉及到的设计模式 一.传统方式源码分析 分析之前我们来回顾下传统方式的写法: /*** 传统方 ...
- MyBatis源码分析(一)MyBatis整体架构分析
文章目录 系列文章索引 一.为什么要用MyBatis 1.原始JDBC的痛点 2.Hibernate 和 JPA 3.MyBatis的特点 4.MyBatis整体架构 5.MyBatis主要组件及其相 ...
- 十年老架构师神级推荐,MyBatis源码分析,再也不用为源码担忧了
十年老架构师神级推荐,MyBatis源码分析,再也不用为源码担忧了 前言 MyBatis是一个优秀的持久层ORM框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注SQL 本身,而不需要花 ...
- springboot集成mybatis源码分析-启动加载mybatis过程(二)
springboot集成mybatis源码分析-启动加载mybatis过程(二) 1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplicati ...
最新文章
- 登录文档服务器,服务器登录login
- 这么详细的Python matplotlib底层原理浅析
- 打不死的小强机器人来了,向心加速度堪比猎豹,能抵抗自身数百倍重量碾压...
- 一、你要拥有自己的服务器
- 存储时间:从Symmetrix V-Max看高端存储的未来
- java中==和equals引发的思考
- 赛诺朗基智能安全保障平台——安全着你的安全!
- 使用Selenium进行Spring Boot集成测试
- js parseInt()与Number()区别
- 得到IOleInPlaceActiveObject接口,IOleInPlaceActiveObject::TranslateAccelerator(msg);
- Kafka : kafka查询某时间段内的消息
- 特斯拉中国工厂2020投产,还可能为完全自动驾驶更新硬件
- java使用elasticsearch进行模糊查询-已在项目中实际应用
- bat快捷方式启动局域网共享文件
- dubbo源代码编译打包错误解决
- JUnit 单元测试多线程测试解决方法
- navicat 10免费下载及破解
- 人世之厄人性之恶——陈应松《母亲》读后
- Python100例——第五章----不定方程的解
- 寒武纪重磅发布首款AI云芯片,陈天石要让端云结合占领10亿智能终端!