前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis二级缓存的脏数据——MyBatis迷信者,清醒点之三

MyBatis二级缓存的脏数据——MyBatis迷信者,清醒点之三

作者头像
疯狂软件李刚
发布2020-06-24 11:34:28
1K0
发布2020-06-24 11:34:28
举报
文章被收录于专栏:疯狂软件李刚疯狂软件李刚

为了避免二级缓存产生脏数据,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属性。

但这样做,又带来了新的问题,聪明的你,能猜到吗?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疯狂软件李刚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档