首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis一级缓存的脏数据——MyBatis迷信者,清醒点之二

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

作者头像
疯狂软件李刚
发布2020-06-24 11:35:54
2.8K0
发布2020-06-24 11:35:54
举报

导读

本文详细分析了MyBatis中“一级缓存”在实际项目中如何产生脏数据,并并给出了具体的实施建议,本文适合对MyBatis有1年以上使用经验的开发者阅读,对MyBatis小白不适合。

另外,本文无意引发口水之争,请就技术谈技术!MyBatis盲目迷信、不愿看清事实的技术拳民请出门左转

如果将localCacheScope设为SESSION(默认值),通过SqlSession(或Mapper)查询得到的返回值都会被缓存在一级缓存中,这样就带来了一个风险:如果程序对这些返回值所引用的对象进行修改——实际上就是修改了一级缓存里的对象(关于对象与引用的关系请参考《疯狂Java讲义》第5章),这样就会影响整个SqlSession生命周期内通过缓存所返回的值,从而造成了一级缓存的脏数据。因此,永远不要对MyBatis返回的对象进行修改,这样才能避免一级缓存产生脏数据。

注意

永远不要对MyBatis返回到对象进行修改。

为了避免一级缓存产生脏数据,MyBatis还进行了另一个预防:SqlSession执行DML语句时会自动flush缓存,这样可以初步避免一级缓存中的脏数据。

假设MyBatis执行DML语句时不会自动flush缓存,让我们“推演”一下会发生什么?

(1)sqlSession执行select语句加载id为1的News对象。

(2)sqlSession执行update语句更新id为1的News对象——注意此时id为1的News对象应该已经发生了改变。

(3)当sqlSession想再次获取id为1的News对象时,如果第2步没有flush缓存,MyBatis将直接返回缓存中id为1的News对象,缓存中id为1的News对象依然是修改之前的脏数据——与数据表中实际记录不一致的数据就是脏数据。

正是由于SqlSession执行DML语句都会flush缓存,因此上面第3步sqlSession想再次获取id为1的News对象时,MyBatis会让它重新查询数据,这样就避免了脏数据。

但请记住:一级缓存的生命周期与SqlSession一致,这意味着一级缓存不可能跨SqlSession产生作用。实际应用通常存在多个并发线程使用不同SqlSession访问数据:一条线程的SqlSession读取数据,一条线程的SqlSession修改数据,这样就可能在一级缓存中产生脏数据。

如下示例示范了一级缓存产生脏数据的场景,本实例的数据脚本非常简单,下面是本例的数据库脚本。

create database mybatis;
use mybatis;
-- 创建数据表
create table news_inf
(
 news_id integer primary key auto_increment,
 news_title varchar(255),
 news_content varchar(255)
);


insert into news_inf
values (null, '李刚的公众号', '大家可关注李刚老师的公众号:fkbooks');
insert into news_inf
values (null, 'Java 13来了', 'Java 13新增了块字符串,用起来更爽了');

本示例的NewsMapper组件,它只是定义了getNews和updateNews两个方法。下面是NewsMapper组件的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.NewsMapper">
    <!-- 为getNews方法定义SQL语句 -->
    <select id="getNews" resultType="news">
        select news_id id, news_title title,
        news_content content from news_inf
        where news_id=#{id}
    </select>
    <!-- 为updateNews方法定义SQL语句 -->
    <update id="updateNews">
        update news_inf
        set news_title=#{arg0}
        where news_id=#{arg1}
    </update>
</mapper>

接下来使用如下测试程序来模拟多线成并发,从而看到并发状态下一级缓存的脏数据。

public class NewsManager
{
    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);
        new Thread(NewsManager::update).start();   // ①
        cacheTest();   // ②
    }
    public static void cacheTest()
{
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 获取Mapper对象
        NewsMapper newsMapper = sqlSession.getMapper(NewsMapper.class);
        // 查询id为1的News对象,会执行select语句
        News news = newsMapper.getNews(1);
        System.out.println(news.getTitle());
        try
        {
            // 线程暂停10ms,是为了模拟让另外一条线程在此处获得CPU的情形
            Thread.sleep(10);
        }
        catch (Exception ex){}
        News news2 = newsMapper.getNews(1);
        System.out.println(news2.getTitle());
        // 提交事务
        sqlSession.commit();
        // 关闭资源
        sqlSession.close();
    }
    public static void update()
{
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 获取Mapper对象
        NewsMapper newsMapper = sqlSession.getMapper(NewsMapper.class);
        // 更新id为1的News对象
        newsMapper.updateNews("测试", 1);
        // 提交事务
        sqlSession.commit();
        // 关闭资源
        sqlSession.close();
    }
}

上面程序中①号代码执行了update()方法——但不是直接调用,而是以多线程并发的方式调用。程序中②号粗体字代码正常调用了cacheTest()方法,那么该方法将处于主线程内。

为了模拟实际项目中多线程并发的一种如下场景:

(1)A线程内的SqlSession先获取id为1的News对象。

(2)CPU切换给B线程,B线程内的SqlSession修改id为1的News对象。

(3)A线程内的SqlSession再次获取id为1的News对象。

上面程序在cacheTest()方法中增加了一条Thread.sleep(10)的代码,这行代码是为了让线程调度在此处切换。

如果发生上面所示的场景,A线程内的SqlSession第二次获取id为1的News对象时,MyBatis不应该使用缓存——因为B线程已经更新了底层数据表中的数据,A线程内SqlSession内缓存的News对象是脏数据。

但实际呢?实际上A线程内的SqlSession依然会使用缓存中id为1的News对象——记住:MyBatis的一级缓存的最大范围是SqlSession内部,因此它不可能跨SqlSession执行缓存flush。

实际运行看看吧,控制台生成如下日志输出:

[java] DEBUG [main] org.crazyit.app.dao.NewsMapper.getNews ==>  Preparing: select news_id id, news_title title, news_content content from news_inf where news_id=?
[java] DEBUG [main] org.crazyit.app.dao.NewsMapper.getNews ==> Parameters: 1(Integer)
[java] 李刚的公众号
[java] DEBUG [Thread-0] org.crazyit.app.dao.NewsMapper.updateNews ==>  Preparing: update news_inf set news_title=? where news_id=?
[java] DEBUG [Thread-0] org.crazyit.app.dao.NewsMapper.updateNews ==> Parameters: 测试(String), 1(Integer)
[java] DEBUG [Thread-0] org.crazyit.app.dao.NewsMapper.updateNews <==    Updates: 1
[java] DEBUG [main] org.crazyit.app.dao.NewsMapper.getNews <==      Total: 1
[java] 李刚的公众号

从上面运行日志可以清楚地看到:A线程(此处用main线程模拟)第二次读取id为1的News对象时,MyBatis并未重新读取数据表中最新的数据,而是依然使用缓存中的id为1的News对象——但请记住:此时B线程(此处用Thread-0线程模拟)已经修改了数据表中id为1的记录,这意味着A线程第二次读取的id为1的News对象是脏数据。

现在问题来了:如何避免MyBatis一级缓存产生这种脏数据呢?MyBatis的一级缓存可以关闭吗?

首先需要说明的是,本书已经多次强调:尽量缩短SqlSession的生命周期!其中一个理由就是为了避免SqlSession一级缓存产生脏数据。

为了避免MyBatis一级缓存产生这种脏数据,最佳实践有两个要点:

  • 尽量使用短生命周期的SqlSession!
  • 避免使用SqlSession的一级缓存。

先说说第一种实践方式适合的场景:对于数据实时性要求没那么高(允许有一定的脏数据)的应用,只要项目避免使用长生命周期的SqlSession,即使MyBatis的一级缓存产生了脏数据,但由于SqlSession的生命周期特别短暂,这种脏数据也许处于可控范围之内。

再说说第二种实践方式适合的场景:对于数据实时性要求非常高的引用,项目基本不允许使用脏数据,此时就应该避免使用MyBatis的一级缓存!但请记住:MyBatis并不允许关闭一级缓存,因为它需要一级缓存来处理循环引用等问题。

为了避免使用MyBatis一级缓存,程序有两种方式:

  • 每个SqlSession永远只执行单次查询。如果要执行第二次查询,请重新打开另一个SqlSession!上面示例之所以产生脏数据,关键就因为程序用同一个SqlSession两次查询了id为1的News对象。如果每个SqlSession只执行单次查询,那么一级缓存几乎就不会产生作用了,这样可避免一级缓存产生脏数据。
  • 将localCacheScope设为STATEMENT级,这样可避免在SqlSession范围内使用一级缓存,但这种方式依然有产生脏数据的风险。

总之,MyBatis一级缓存是一个“食之无味,弃之可惜”的机制,如果你打算使用它,就要接受它可能产生脏数据的风险,而且无法避免。通常建议避免在数据实时性要求较高的应用中使用MyBatis的一级缓存。

注意

避免在数据实时性要求较高的应用中使用MyBatis的一级缓存。

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

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

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

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

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