关于 Mybatis 的开启二级缓存返回对象不一致问题


关于 Mybatis 的开启二级缓存返回对象不一致问题

原创 已于 2024-07-01 17:09:29 修改 · 粉丝可见 · 1.1k 阅读 · 5 · 13 GEO检测 · 编辑
文章链接:https://blog.csdn.net/hacker_51/article/details/140103458

做实验报告的时候,跟着学习,发现我已经将 开启 二级缓存的 配置都配置好了,但是返回值地址不一致,说明对象不一致,二级缓存命中失败。

跟着流程配置:

mybatis-config

1
2
3
4
5
6
<settings>
<!-- 启用 mybatis 全局缓存 -->
<setting name="cacheEnabled" value="true"/>

<setting name="logImpl" value="LOG4J"/>
</settings>

Mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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="com.Angindem.mapper.DynamicMapper">

<cache/>

<select id="queryByEntities" resultType="com.Angindem.Entity.EmployeesEntity" useCache="true">
select * from employees
<where>
<foreach collection="emp" index="fld" item="val" separator="and">
${fld} = #{val}
</foreach>
</where>
</select>

</mapper>

TestCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Test
public void testGlobalCache() throws IOException
{
// 加载配置文件
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
// 创建 SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);


// 创建 sqlsession(得到 sql 语句,并执行)
SqlSession session1 = factory.openSession(true); // 如果参数为空 默认 false 手动提交事务
// 获取 mapper 对象(代理模式,可以囊组返回当前接口的实现类对象)
DynamicMapper dynamicMapper1 = session1.getMapper(DynamicMapper.class);
SqlSession session2 = factory.openSession(true); // 如果参数为空 默认 false 手动提交事务
DynamicMapper dynamicMapper2 = session2.getMapper(DynamicMapper.class);

Map<String, Object> emp = new HashMap<>();
emp.put("OfficeCode", 1);

// 调用 SqlSession 执行一次 Mybatis,保存到本地缓存(二级级缓存)
List<EmployeesEntity> list = dynamicMapper1.queryByEntities(emp);


// 重复 操作 list2 接受 该语句
List<EmployeesEntity> list2 = dynamicMapper2.queryByEntities(emp);


// 判断是否命中本地缓存,如果命中了,则返回的地址是相同的
System.out.println("所接受的两个链表地址一致? 结果:" + (list == list2));

System.out.println("===================================\n\n");
list.stream().forEach(s -> System.out.println(s));
}

最后运行效果:

上网查资料,说 需要将运行的SqlSession会话关闭:

根据我的理解,需要将 SqlSession 关闭,本地缓存 与 SqlSession 的绑定,解绑后,将本地缓存,传到 二级缓存那里才可以。

修改后的 TestCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void testGlobalCache() throws IOException 
{
// 加载配置文件
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
// 创建 SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);


// 创建 sqlsession(得到 sql 语句,并执行)
SqlSession session1 = factory.openSession(true); // 如果参数为空 默认 false 手动提交事务
// 获取 mapper 对象(代理模式,可以囊组返回当前接口的实现类对象)
DynamicMapper dynamicMapper1 = session1.getMapper(DynamicMapper.class);
SqlSession session2 = factory.openSession(true); // 如果参数为空 默认 false 手动提交事务
DynamicMapper dynamicMapper2 = session2.getMapper(DynamicMapper.class);

Map<String, Object> emp = new HashMap<>();
emp.put("OfficeCode", 1);

// 调用 SqlSession 执行一次 Mybatis,保存到本地缓存(一级缓存)
List<EmployeesEntity> list = dynamicMapper1.queryByEntities(emp);

// 关闭当前 SqlSession ,解绑 一级缓存,将语句 放到二级缓存
session1.close();

// 重复 操作 list2 接受 该语句
List<EmployeesEntity> list2 = dynamicMapper2.queryByEntities(emp);

// 关闭当前 SqlSession ,解绑 一级缓存,将语句 放到二级缓存
session2.close();

// 判断是否命中本地缓存,如果命中了,则返回的地址是相同的
System.out.println("所接受的两个链表地址一致? 结果:" + (list == list2));

System.out.println("===================================\n\n");
list.stream().forEach(s -> System.out.println(s));
}

最后运行效果:

报错了,信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: com.Angindem.Entity.EmployeesEntity
at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:95)
at org.apache.ibatis.cache.decorators.SerializedCache.putObject(SerializedCache.java:56)
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:45)
at org.apache.ibatis.executor.CachingExecutor.close(CachingExecutor.java:61)
at org.apache.ibatis.session.defaults.DefaultSqlSession.close(DefaultSqlSession.java:260)
at com.Angindem.test.TestOffice.testGlobalCache(TestOffice.java:241)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
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.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.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:93)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:757)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: java.io.NotSerializableException: com.Angindem.Entity.EmployeesEntity
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1197)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
at java.base/java.util.ArrayList.writeObject(ArrayList.java:866)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1074)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1526)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1448)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1191)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:91)
... 35 more

抛出了关键的异常:java.io.NotSerializableException

继续查资料,解释了一下这个异常:

java.io.NotSerializableException 是一个在 Java 程序中抛出的异常,属于 java.io 包。这个异常表明尝试序列化一个对象时,该对象的类没有实现 java.io.Serializable 接口。序列化是 Java 中的一个机制,允许将对象的状态保存到文件或通过网络发送,以便之后可以重新创建该对象。

根据我自己的理解,Mybatis 中的二级缓存,就是需要将我们执行后的语句的本地缓存,保存并且上传到 mybatis 的二级缓存机制中,所以需要将 对应的 Entity 对象 进行序列化,所以要在相应的类,加个接口, Serializable

修改我对应的类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.Angindem.Entity;

import java.io.Serializable;

public class EmployeesEntity implements Serializable {
private Integer employeeNumber;
private String lastName;
private String firstName;
private String extension;
private String email;
private Integer officeCode;
private String jobTitle;

public EmployeesEntity(Integer employeeNumber, String lastName, String firstName, String jobTitle) {
super();
this.employeeNumber = employeeNumber;
this.lastName = lastName;
this.firstName = firstName;
this.jobTitle = jobTitle;
}

public Integer getEmployeeNumber() {
return this.employeeNumber;
}
public void setEmployeeNumber(Integer employeeNumber) {
this.employeeNumber = employeeNumber;
}
public String getlastName() {
return lastName;
}
public void setlastName(String lastName) {
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getExtension() {
return extension;
}
public void setExtension(String extension) {
this.extension = extension;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getOfficeCode() {
return officeCode;
}
public void setOfficeCode(Integer officeCode) {
this.officeCode = officeCode;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
@Override
public String toString() {
return "Employees [employeeNumber=" + employeeNumber + ", lastName=" + lastName + ", firstName=" + firstName
+ ", extension=" + extension + ", email=" + email + ", officeCode=" + officeCode + ", jobTitle="
+ jobTitle + "]";
}
}

最后运行效果:

运行成功了,发现还是没有命中二级缓存

继续查资料,原来标签有问题

修改后的 Mapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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="com.Angindem.mapper.DynamicMapper">

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

<select id="queryByEntities" resultType="com.Angindem.Entity.EmployeesEntity" useCache="true">
select * from employees
<where>
<foreach collection="emp" index="fld" item="val" separator="and">
${fld} = #{val}
</foreach>
</where>
</select>
</mapper>

这里解释一下:

详细配置的 <cache> 标签:

MyBatis 的 <cache> 标签用于配置 Mapper 级别的缓存行为。以下是两个 <cache> 标签配置的区别:

1
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
  • eviction="FIFO" : 指定了缓存的逐出策略为先进先出(FIFO)。这是当缓存达到其最大容量时用来决定哪些对象应该被移除的算法。

  • flushInterval="60000" : 指定了缓存刷新的时间间隔,单位为毫秒。这里设置为 60000 毫秒,即每 60 秒缓存会被清空一次。

  • size="512" : 指定了缓存中可以存储的对象数量上限。这里设置为最多 512 个对象。

  • readOnly="true" : 指定了缓存中的对象是只读的。这通常可以提高性能,因为 MyBatis 不需要在每次查询后都同步数据。

默认配置的 <cache> 标签:

1
<cache/>

<cache> 标签没有包含任何属性时,MyBatis 将使用默认的缓存配置。默认配置通常包括:

  • 使用 LRU(最近最少使用)逐出策略。

  • 没有设置缓存刷新的时间间隔,缓存会在每次会话结束时清空。

  • 默认的缓存大小没有明确限制,但实际大小可能会受到 JVM 内存限制。

  • 缓存中的对象不是只读的,这意味着它们可以被修改。

总结来说,第一个 <cache> 标签提供了详细的缓存行为配置,包括逐出策略、刷新间隔、缓存大小和只读属性。而第二个 <cache> 标签则使用 MyBatis 的默认缓存配置,没有显式设置这些属性。使用详细的配置可以帮助开发者根据应用的具体需求来优化缓存性能。

我的理解是,如果使用默认的 <cache> 标签,我们关闭了SqlSession会话后,其中的二级缓存也会在 每次的 会话结束时清空,所以我们没有命中到前一个缓存。

最后成功运行效果:


觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭

微信二维码

wechat pay

支付宝二维码

ali pay

关于 Mybatis 的开启二级缓存返回对象不一致问题
http://blog.angindem.cn/2024/07/01/Angindem-CSDN博客/158_158/
作者
Angindem
发布于
2024年7月1日
许可协议