mybatis 缓存你学会了吗

我们先不开启二级缓存,然后使用两个SqlSession 来执行相同的操作。测试代码如下:

@org.junit.Test

public void Test2() throws Exception{

InputStream inputStream= Resources.getResourceAsStream(“SqlMapConfig.xml”);

SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession sqlSession = build.openSession();

SqlSession sqlSession1 = build.openSession();

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

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

System.out.println(“第一次:”);

List users = mapper.selectAll();

System.out.println(users.get(0));

sqlSession.close();

System.out.println(“第二次:”);

List users2 = mapper1.selectAll();

System.out.println(users2.get(0));

System.out.println(“两对象是否一致”);

System.out.println(users.equals(users2));

}

这里我们注意一下,在第二次操作前,要先将第一次的SqlSession 关闭掉。知道为什么吗?因为事物的特性,大家可以试验下嘿嘿。

image-20200528204042366

开启二级缓存


可以看到两个SqlSession 分别从数据库读取数据,并且两个对象是不一样的。我们现在开启二级缓存

在mapper.xml 文件中开启

在实体类对象中需要序列化对象,因为二级缓存不一定是放在内存中,也可能放在磁盘中。当然我们这里是否序列化都没有关系。

接下来就可以测试了,还是上面的代码,但是执行结果却不同了。

image-20200528205640111

可以看到第一次是从数据库读取的,但是第二次就没有从数据库读取,而是从缓存中读取的数据这个0.5 表示命中率。说明不同的 SqlSession 是可以共享数据的。但是我们也会发现虽然第二次事从缓存中读取的,但是两个对象却不相等,这是为什么呢?因为二级缓存,存储的仅仅是数据,而不是对象,在其他Sqlsession 从缓存中取数据的时候,创建一个新的对象返回。而一级缓存是保存的对象,所以是相同的。

手动实现一级缓存。

========================================================================

我们就不用考虑太复杂,手动实现以下只是为了我们更熟悉核心思想。每个SqlSession 一个缓存,缓存是使用的存储格式,以及失效生效场景。

引入cache 的相关类


我们首先借用 mybatis 中这几个类。CacheKey 主要是存储key的。而PerpetualCache 是实现缓存的实现类。

这里面主要就是重写 equals 方法和 hashCode 方法。所以这四个部分代码就不贴了。

SimpleExecutor 的具体实现


我们直接来看Executor 类中我们怎么实现。

1、首先初始化 localCache 对象,这样不同的SqlSession 就是不同的缓存了。限制作用域。

image-20200529140901276

2、query 方法,创建一个cacheKey ,判断 localCache 中是否存在这个 cacheKey ,存在直接从缓存中取出返回,不存在就从数据库中查询。

image-20200529141651729

3、createCacheKey 方法,这里我们就把mapper.id ,boundSql,以及参数作为key 的关键信息。这里如果对代码有些看不明白的可以先参考我的另一篇文章,自己定义一个持久层框架。我们这里增加缓存的功能就是在这个框架上修改的。

public CacheKey createCacheKey(Mapper mapper, BoundSql boundSql, Object… parameter) throws Exception{

// 创建 CacheKey 对象

CacheKey cacheKey = new CacheKey();

// 设置 id

cacheKey.update(mapper.getId());

// 设置 ParameterMapping 数组的元素对应的每个 value 到 CacheKey 对象中

List parameterMappingList = boundSql.getParameterMappingList();

Class<?> parmType = mapper.getParmType();

for (int i = 0; i < parameterMappingList.size(); i++) {

Object value=getValue(parmType,parameterMappingList.get(i),parameter);

cacheKey.update(value);

}

return cacheKey;

}

private Object getValue(Class<?> parmType,ParameterMapping parameterMapping, Object… parameter) throws Exception {

Object value=null;

String content = parameterMapping.getContent();

if (isJavaType(parmType)) {

value=parameter[0];

}else {

Field declaredField = parmType.getDeclaredField(content);

declaredField.setAccessible(true);

value = declaredField.get(parameter[0]);

}

return value;

}

4、queryFromDatabase 方法,从数据库查询操作,会将结果保存到缓存中。

// 从数据库中读取操作

private List queryFromDatabase(Configuration configuration,Mapper mapper,CacheKey key, BoundSql boundSql,Object parameter) throws Exception {

List list;

try {

// 执行读操作

list = doQuery(configuration,mapper, boundSql,parameter);

} finally {

localCache.removeObject(key);

}

// 添加到缓存中

localCache.putObject(key, list);

return list;

}

5、update ()方法,需要增加清除缓存操作。

image-20200529142944480

到这里我们就基本上实现了一级缓存的效果。这里我贴出 SimpleExecutor 类完整代码吧。都是在这个类中实现的。感兴趣的可以看下,不感兴趣的直接跳过吧。

public class SimpleExecuter implements Executer{

private Connection connection;

protected PerpetualCache localCache;

public SimpleExecuter(){

this.localCache = new PerpetualCache(“LocalCache”);

}

public List query(Configuration configuration, Mapper mapper, Object… parameter) throws Exception {

BoundSql boundSql=getBoundSql(mapper.getSql());

CacheKey key = createCacheKey(mapper,boundSql,parameter);

// 先从缓存中获取

List list=(List) localCache.getObject(key);

if (list != null) {

return list;

} else {

// 获得不到,则从数据库中查询

list = queryFromDatabase(configuration,mapper, key, boundSql,parameter);

}

return list;

}

// 从数据库中读取操作

private List queryFromDatabase(Configuration configuration,Mapper mapper,CacheKey key, BoundSql boundSql,Object parameter) throws Exception {

List list;

try {

// 执行读操作

list = doQuery(configuration,mapper, boundSql,parameter);

} finally {

localCache.removeObject(key);

}

// 添加到缓存中

localCache.putObject(key, list);

return list;

}

public List doQuery(Configuration configuration, Mapper mapper,BoundSql boundSql, Object… parameter) throws Exception{

PreparedStatement preparedStatement=preHandle(configuration,mapper,boundSql,parameter);

//执行sql

ResultSet resultSet = preparedStatement.executeQuery();

return resultHandle(mapper,resultSet);

}

@Override

public int update(Configuration configuration, Mapper mapper, Object… parameter) throws Exception {

// 清空本地缓存

clearLocalCache();

BoundSql boundSql=getBoundSql(mapper.getSql());

// 执行写操作

PreparedStatement preparedStatement=preHandle(configuration,mapper,boundSql,parameter);

return preparedStatement.executeUpdate();

}

public void clearLocalCache() {

// 清理 localCache

localCache.clear();

}

/**

  • 创建cacheKey

  • @param mapper

  • @param boundSql

  • @param parameter

  • @return

  • @throws Exception

*/

public CacheKey createCacheKey(Mapper mapper, BoundSql boundSql, Object… parameter) throws Exception{

// 创建 CacheKey 对象

CacheKey cacheKey = new CacheKey();

// 设置 id

cacheKey.update(mapper.getId());

// 设置 ParameterMapping 数组的元素对应的每个 value 到 CacheKey 对象中

List parameterMappingList = boundSql.getParameterMappingList();

Class<?> parmType = mapper.getParmType();

for (int i = 0; i < parameterMappingList.size(); i++) {

Object value=getValue(parmType,parameterMappingList.get(i),parameter);

cacheKey.update(value);

}

return cacheKey;

}

/**

  • 准备工作

  • 连接数数据库

  • 解析sql

  • 替换占位符

  • @param configuration

  • @param mapper

  • @param parameter

  • @return

  • @throws Exception

*/

private PreparedStatement preHandle(Configuration configuration, Mapper mapper,BoundSql boundSql, Object… parameter)throws Exception{

String sql=boundSql.getSqlText();

List parameterMappingList = boundSql.getParameterMappingList();

//获取连接

connection=configuration.getDataSource().getConnection();

//获取preparedStatement,并传递参数值

PreparedStatement preparedStatement=connection.prepareStatement(sql);

Class<?> parmType = mapper.getParmType();

for (int i = 0; i < parameterMappingList.size(); i++) {

Object value=getValue(parmType,parameterMappingList.get(i),parameter);

preparedStatement.setObject(i+1,value);

}

System.out.println(sql);

return preparedStatement;

}

/**

  • 获取参数对应的值

  • @param parmType

  • @param parameterMapping

  • @param parameter

  • @return

  • @throws Exception

*/

private Object getValue(Class<?> parmType,ParameterMapping parameterMapping, Object… parameter) throws Exception {

Object value=null;

String content = parameterMapping.getContent();

if (isJavaType(parmType)) {

value=parameter[0];

}else {

Field declaredField = parmType.getDeclaredField(content);

declaredField.setAccessible(true);

value = declaredField.get(parameter[0]);

}

return value;

}

private boolean isJavaType(Class<?> parmType){

String s = parmType.getSimpleName().toUpperCase();

return s.equals(“INTEGER”)|| s.equals(“STRING”)|| s.equals(“LONG”);

}

/**

  • 封装结果集

  • @param mapper

  • @param resultSet

  • @param

  • @return

  • @throws Exception

*/

private List resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{

ArrayList list=new ArrayList<>();

//封装结果集

Class<?> resultType = mapper.getResultType();

while (resultSet.next()) {

ResultSetMetaData metaData = resultSet.getMetaData();

Object o = resultType.newInstance();

int columnCount = metaData.getColumnCount();

for (int i = 1; i <= columnCount; i++) {

//属性名

String columnName = metaData.getColumnName(i);

//属性值

Object value = resultSet.getObject(columnName);

//创建属性描述器,为属性生成读写方法

PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);

Method writeMethod = propertyDescriptor.getWriteMethod();

writeMethod.invoke(o,value);

}

list.add((E) o);

}

return list;

}

/**

  • 解析自定义占位符

  • @param sql

  • @return

*/

private BoundSql getBoundSql(String sql){

ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();

GenericTokenParser genericTokenParser = new GenericTokenParser(“#{”,“}”,parameterMappingTokenHandler);

String parse = genericTokenParser.parse(sql);

return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());

}

public void close() throws Exception {

connection.close();

}

@Override

public void commit() throws Exception{

connection.commit();

}

}

测试


我们一样的来写一个测试类

@org.junit.Test

public void Test11() throws Exception{

InputStream inputStream= Resources.getResources(“SqlMapperConfig.xml”);

SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();

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

System.out.println(“第一次:”);

List users = mapper.selectAll();

System.out.println(users.get(0));

System.out.println(“第二次:”);

List users2 = mapper.selectAll();

System.out.println(users2.get(0));

System.out.println(“两对象是否一致”);

System.out.println(users.equals(users2));

}

下面是执行结果

image-20200529143706328

可以明显的看到,第二次是走的缓存,并且返回的对象是相同的。

我们接下来,在两次查询中间,增加一次修改操作。

@org.junit.Test

public void Test11() throws Exception{

InputStream inputStream= Resources.getResources(“SqlMapperConfig.xml”);

SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();

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

System.out.println(“第一次:”);

List users = mapper.selectAll();

System.out.println(users.get(0));

System.out.println(“修改操作”);

User user=new User();

user.setUsername(“张三”);

user.setId(1);

mapper.update(user);

System.out.println(“第二次:”);

List users2 = mapper.selectAll();

System.out.println(users2.get(0));

System.out.println(“两对象是否一致”);

System.out.println(users.equals(users2));

}

结果如下:

image-20200529144056103

可以看到这个时候第二次就没有从缓存中读取了,而是从数据库中读取的。

我们再来看看两个SqlSession 的能公用一级缓存不能。

@org.junit.Test

public void Test12() throws Exception{

InputStream inputStream= Resources.getResources(“SqlMapperConfig.xml”);

SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession sqlSession = build.openSqlSession();

SqlSession sqlSession1 = build.openSqlSession();

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

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

System.out.println(“第一次:”);

List users = mapper.selectAll();

System.out.println(users.get(0));

System.out.println(“第二次:”);

List users2 = mapper1.selectAll();

System.out.println(users2.get(0));

System.out.println(“两对象是否一致”);

System.out.println(users.equals(users2));

}

结果如下:

image-20200529144316838

说明是没有公用了,那么到这里我们就验证了我们自定义的一级缓存,和 mybatis 的一级缓存的效果是一样的。这也是 mybatis 一级缓存的核心实现了。有兴趣的小伙伴可以是试试,并且可以试试怎么实现二级缓存啊哈哈。

二级缓存整合 redis

===========================================================================

上面我们说二级缓存在分布式环境下存在问题。比喻说一个用户请求了这台服务,在这台服务上有缓存,但是另外一个用户请求了另一台服务,导致没有从缓存中获取,而是从数据库获取的。这就存在问题啦。

所以解决上述问题,我们很容易就想到使用 redis 做二级缓存,不管那台服务都会先从 redis 中查询,没有查到才会到数据库中查询,并包结果保存到缓存中。

配置


你想到的事情,人家也想到了哈哈,所以有现成的实现工具,我们直接来使用试试。

1、增加依赖

org.mybatis.caches

mybatis-redis

1.0.0-beta2

2、mapper.xml 中type 指向我们的缓存

3、增加redis.properties 配置

redis.host=localhost

redis.port=6379

redis.connectionTimeout=5000

redis.password=

redis.database=0

上面这样就配置好了一个基于redis 的二级缓存啦。

测试


我们这里简单测试一下,只要测试redis 缓存生效没有就可以了。

测试方法还是和上面一样:

@org.junit.Test

public void Test3() throws Exception{

InputStream inputStream= Resources.getResourceAsStream(“SqlMapConfig.xml”);

SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession sqlSession = build.openSession();

SqlSession sqlSession1 = build.openSession();

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

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

System.out.println(“第一次:”);

List users = mapper.selectAll();

System.out.println(users.get(0));

sqlSession.close();

System.out.println(“第二次:”);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-Deb8TjVi-1713475815771)]

[外链图片转存中…(img-pouX0ffD-1713475815773)]

[外链图片转存中…(img-dKZm9IAY-1713475815774)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

[外链图片转存中…(img-EHFte063-1713475815775)]

[外链图片转存中…(img-BhMFgsmR-1713475815776)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值