为了避免二级缓存产生脏数据,MyBatis已经做了预防:Mapper组件执行DML语句(这些语句会更新底层数据)时默认会flush二级缓存,因此在同一个Mapper内,只要该Mapper组件执行DML语句更新底层数据,MyBatis就会自动flush二级缓存,这样就避免了产生脏数据。
虽然二级缓存达到了Mapper级别,可实现SqlSession之间缓存数据的共享,但这也暗示了:不同Mapper并发访问时同样可能导致脏数据。
当应用中两个实体存在关联关系时,程序可能出现如下流程:
(1)A Mapper组件执行select语句加载id为1的A对象,并通过关联关系访问A的关联实体B对象(假设该B对象的id为2)。
(2)B Mapper组件执行update语句更新了id为2的B对象(被关联的B对象)——注意此时id为2的B对象应该已经发生了改变。
(3)当A Mapper想再次获取id为1的A对象时,如果第2步没有flush缓存,MyBatis将直接返回二级缓存中id为1的A对象及关联的B对象,这个B对象依然是修改之前的脏数据。
默认情况下,二级缓存的生命周期与Mapper一致,这意味着当多个并发线程使用不同Mapper访问数据时:一条线程的A Mapper读取数据,一条线程的B Mapper修改数据,这样就可能在二级缓存中产生脏数据。
如下示例示范了二级缓存产生脏数据的场景,本示例用到了两个关联实体:Person和Address,它们之间存在1—1关联关系。程序的PersonMapper组件只是定义了一个简单的getPerson()方法,该方法用于获取Person实体,当然也可通过Person实体来获取关联的Address实体。
本实例的数据脚本非常简单,下面是本例的数据库脚本。
drop database if exists mybatis;
create database mybatis;
use mybatis;
create table person_inf
(
person_id integer primary key auto_increment,
person_name varchar(255),
person_age int
);
insert into person_inf
values (null, '孙悟空', 500);
insert into person_inf
values (null, '猪八戒', 280);
create table address_inf
(
addr_id integer primary key auto_increment,
addr_detail varchar(255),
-- 外键增加唯一约束,意味着1—1关联
owner_id int unique,
foreign key(owner_id) references person_inf(person_id)
);
insert into address_inf
values (null, '福陵山云栈洞', 2);
insert into address_inf
values (null, '花果山水帘洞', 1);
下面是PersonMapper组件的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="org.crazyit.app.dao.PersonMapper">
<!-- 使用多表连接查询 -->
<select id="getPerson" resultMap="personMap">
select p.*,
a.addr_id addr_id, a.addr_detail addr_detail
from person_inf p
join address_inf a
on a.owner_id = p.person_id
where person_id = #{id}
</select>
<resultMap id="personMap" type="person">
<id column="person_id" property="id"/>
<result column="person_name" property="name"/>
<result column="person_age" property="age"/>
<!-- 映射关联实体 -->
<association property="address" javaType="address"
resultMap="org.crazyit.app.dao.AddressMapper.addressMap"/>
</resultMap>
<!-- 使用默认的二级缓存设置 -->
<cache />
</mapper>
上面代码只是为PersonMapper配置了一个简单的getPerson()方法,并通过<cache.../>元素配置了默认的二级缓存。
AddressMapper组件则只配置一个简单的updateAddress方法啊,下面是AddressMapper组件的XML映射文件。
<?xml version="1.0" encoding="UTF-8" ?>
<!-- MyBatis Mapper文件的DTD -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.crazyit.app.dao.AddressMapper">
<update id="updateAddress">
update address_inf
set addr_detail = #{arg0}
where addr_id = #{arg1}
</update>
<resultMap id="addressMap" type="address">
<id column="addr_id" property="id"/>
<result column="addr_detail" property="detail"/>
<!-- 映射关联实体 -->
<association property="person" javaType="person"
resultMap="org.crazyit.app.dao.PersonMapper.personMap"/>
</resultMap>
<cache/>
</mapper>
上面AddressMapper为updateAddress()方法定义了一条简单的update语句,同样使用<cache.../>元素配置了默认的二级缓存。
接下来使用如下测试程序来模拟多线程并发,从而看到并发状态下二级缓存的脏数据。
public class PersonManager
{
private static SqlSessionFactory sqlSessionFactory;
public static void main(String[] args) throws Exception
{
String resource = "mybatis-config.xml";
// 使用Resources工具从类加载路径下加载指定文件
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
cacheTest(); // ①
}
public static void cacheTest()
{
SqlSession sess1 = sqlSessionFactory.openSession(true);
// 获取Mapper对象
PersonMapper personMapper1 = sess1.getMapper(PersonMapper.class);
// 调用Mapper对象的方法执行持久化操作
Person person1 = personMapper1.getPerson(1);
// 访问id为1的Person对应的Address
System.out.println(person1.getAddress().getDetail());
sess1.close();
// 启动新线程执行updateAddress()方法
new Thread(PersonManager::updateAddress).start(); // ②
try
{
Thread.sleep(200);
}
catch (Exception ex){}
SqlSession sess2 = sqlSessionFactory.openSession(true);
// 获取Mapper对象
PersonMapper personMapper2 = sess2.getMapper(PersonMapper.class);
// 调用Mapper对象的方法执行持久化操作
Person person2 = personMapper2.getPerson(1);
// 访问id为1的Person对应的Address
System.out.println(person2.getAddress().getDetail());
sess2.close();
}
public static void updateAddress()
{
SqlSession sess = sqlSessionFactory.openSession();
// 获取Mapper对象
AddressMapper AddressMapper = sess.getMapper(AddressMapper.class);
// 调用Mapper对象的方法执行持久化操作
AddressMapper.updateAddress("测试", 2);
// 提交事务
sess.commit();
sess.close();
}
}
上面程序中②号代码执行了update()方法——但不是直接调用,而是以多线程并发的方式调用。程序中①号代码正常调用了cacheTest()方法,那么该方法将处于主线程内。
为了模拟实际项目中多线程并发的一种如下场景:
(1)A线程内的PersonMapper先获取id为1的Person对象,并访问它关联的Address对象(其id为2)。
(2)CPU切换给B线程,B线程内的AddressMapper修改id为2的Address对象。
(3)A线程内的PersonMapper再次获取id为1的Person对象、并访问它关联的Address对象。
上面程序在cacheTest()方法中增加了一条Thread.sleep(200)的代码,这行代码是为了让线程调度在此处切换。
如果发生上面所示的场景,A线程内的PersonMapper第二次获取id为1的Person对象及其关联对象时,MyBatis不应该使用缓存——因为B线程已经更新了底层数据表中的数据,A线程内PersonMapper对应的二级缓存所缓存的Address对象是脏数据。
但实际呢?实际上A线程内的PersonMapper依然会使用缓存中Person所关联的Address对象——记住:MyBatis的二级缓存的默认范围是Mapper级别。
实际运行看看吧,控制台生成如下日志输出:
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper Cache Hit Ratio [org.crazyit.app.dao.PersonMapper]: 0.0
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson ==> Preparing: select p.*, a.addr_id addr_id, a.addr_detail addr_detail from person_inf p join address_inf a on a.owner_id = p.person_id where person_id = ?
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson ==> Parameters: 1(Integer)
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson <== Total: 1
[java] 花果山水帘洞
[java] DEBUG [Thread-0] org.crazyit.app.dao.AddressMapper.updateAddress ==> Preparing: update address_inf set addr_detail = ? where addr_id = ?
[java] DEBUG [Thread-0] org.crazyit.app.dao.AddressMapper.updateAddress ==> Parameters: 测试(String), 2(Integer)
[java] DEBUG [Thread-0] org.crazyit.app.dao.AddressMapper.updateAddress <== Updates: 1
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper Cache Hit Ratio [org.crazyit.app.dao.PersonMapper]: 0.5
[java] 花果山水帘洞
上面行号为7的日志清楚地显示:AddressMapper的updateAddress方法已经成功地更新了一条记录,此时该Address对象的detail属性应该是“测试”,但程序通过PersonMapper访问Person关联的Address对象时,依然看到它的detail属性是“花果山水帘洞”,这就是脏数据!
为了避免这种情况下产生脏数据,可以让有关联关系的Mapper组件共享同一个二级缓存,MyBatis提供了<cache-ref.../>元素或@CacheNamespaceRef注解来引用另一个二级缓存。
使用<cache-ref.../>元素时,可指定一个namespace属性(对应于@CacheNamespaceRef注解的name属性),通过该属性指定要引用的哪个Mapper的二级缓存。
程序只要将AddressMapper.xml中<cache.../>代码改为如下形式:
<!-- 引用PersonMapper的二级缓存 -->
<cache-ref namespace="org.crazyit.app.dao.PersonMapper" />
上面修改使得AddressMapper组件直接使用PersonMapper的二级缓存,这样AddressMapper和PersonMapper将共享同一个二级缓存,因此AddressMapper执行DML语句时,也会flush它们通向的二级缓存,这样即可避免产生脏数据。
再次运行测试程序,会在控制台看到如下日志输出:
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper Cache Hit Ratio [org.crazyit.app.dao.PersonMapper]: 0.0
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson ==> Preparing: select p.*, a.addr_id addr_id, a.addr_detail addr_detail from person_inf p join address_inf a on a.owner_id = p.person_id where person_id = ?
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson ==> Parameters: 1(Integer)
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson <== Total: 1
[java] 花果山水帘洞
[java] DEBUG [Thread-0] org.crazyit.app.dao.AddressMapper.updateAddress ==> Preparing: update address_inf set addr_detail = ? where addr_id = ?
[java] DEBUG [Thread-0] org.crazyit.app.dao.AddressMapper.updateAddress ==> Parameters: 测试(String), 2(Integer)
[java] DEBUG [Thread-0] org.crazyit.app.dao.AddressMapper.updateAddress <== Updates: 1
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper Cache Hit Ratio [org.crazyit.app.dao.PersonMapper]: 0.0
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson ==> Preparing: select p.*, a.addr_id addr_id, a.addr_detail addr_detail from person_inf p join address_inf a on a.owner_id = p.person_id where person_id = ?
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson ==> Parameters: 1(Integer)
[java] DEBUG [main] org.crazyit.app.dao.PersonMapper.getPerson <== Total: 1
[java] 测试
从上面运行日志可以看到,当AddressMapper执行updateAddress更新Address对象之后,程序再次通过Person访问关联的Address对象时,MyBatis重新执行select语句从底层数据表获取记录,这样就避免了脏数据,因此最后一行输出的内容也是Address对象修改之后的detail属性。
但这样做,又带来了新的问题,聪明的你,能猜到吗?