2022-01-06

前言

    对绝大数Java开发者而言mybatis并不陌生,从经典的SSM(Spring,spring-mvc,mybatis)框架,到现在流行的Springboot,随处可见mybatis的身影。mybatis作为比较主流的orm框架,支持用户定制sql,灵活又方便,颇受开发者喜爱。我们在使用mybatis难免会遇到各种坑,其中SqlSession的线程安全性问题也总会遇到。

  SqlSession作为一个接口,其并没有线程安全性的问题,我们常说的线程安全问题是SqlSession的一个实现类DefaultSqlSession,mybatis的作者也对此类加以"Note that this class is not Thread-Safe"的注释。此外SqlSession还有两个实现类SqlSessionManager和SqlSessionTemplate,这两个实现类是线程安全的。

线程不安全的DefaultSqlSession

    我们都知道DefaultSqlSession是线程不安全的,也会有很多博主讲解"SqlSessionTemplate是如何保证DefaultSqlSession线程安全的",但是DefaultSqlSession不安全的体现是什么?不安全产生的原因在哪?今天作者通过一个例子给读者演示下并发情况下DefaultSqlSession线程不安全的表现,以及源码追踪产生线程安全问题的源头。

样例代码如下:

配置文件mybatis-config.xml,简单配置保证能正常运行

<configuration>

    <!-- 自行配置db.properties -->

    <properties resource="db.properties"/>

    <environments default="test">

        <environment id="test">

            <transactionManager type="JDBC"></transactionManager>

            <dataSource type="POOLED">

                <property name="driver" value="${driverClassName}"/>

                <property name="url" value="${url}"/>

                <property name="username" value="${username}"/>

                <property name="password" value="${password}"/>

            </dataSource>

        </environment>

    </environments>

    <mappers>

        <!-- mapper映射文件和mapper接口在同一个路径 -->

    <mapper class="idin.sun.study.mapper.StudentMapper"/>

    </mappers>

</configuration>

mapper映射文件

<?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="idin.sun.study.mapper.StudentMapper">

<resultMap id="baseResultMap" type="idin.sun.study.model.Student">

    <id property="stdId" column="std_id"/>

    <result property="stdName" column="std_name"/>

    <result property="stdBirth" column="std_birth"/>

    <result property="stdGrade" column="std_grade"/>

    <result property="stdSex" column="std_sex"/>

</resultMap>

<select id="getStudents" resultMap="baseResultMap">

    SELECT *

    FROM sm_student

</select>

</mapper>

mapper接口

public interface StudentMapper {

List<Student> getStudents();

}

实体类

@Getter

@Setter

@ToString

public class Student {

private Integer stdId;

private String stdName;

private Date stdBirth;

private int stdGrade;

private int stdSex;

}

测试类

public class MybatisApp {

private static final int COUNT = 10;

// 使用CountDownLatch 来模拟并发,并发量10个

private static CountDownLatch cdl = new CountDownLatch(COUNT);

private SqlSession sqlSession;

//初始工作,用于初始化sqlSession此处获得是DefaultSqlSession的实例

@Before

public void init() {

    ClassLoader loader = Thread.currentThread().getContextClassLoader();

    InputStream inputStream = loader.getResourceAsStream("mybatis-config.xml");

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()

                                                .build(inputStream);

    sqlSession = sqlSessionFactory.openSession();

}

@Test

public void testThreadSafe() throws InterruptedException {

    for (int i = 0; i < COUNT; i++) {

        new Thread(() -> {

            try {

                cdl.await();

            } catch (Exception e) {

                e.printStackTrace();

            }

            // 调用查询方法

            getStudents();

        }).start();

        cdl.countDown();

    }

    Thread.sleep(5000);

}



public void getStudents() {

    // 使用statementId方法调用

    String statementId = "idin.sun.study.mapper.StudentMapper.getStudents";

    List<Student> students = sqlSession.selectList(statementId);

}

}

运行testThreadSafe()方法,控制台输出如下内容

Exception in thread "Thread-8" Exception in thread "Thread-4" org.apache.ibatis.exceptions.PersistenceException:

Error querying database. Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

The error may exist in idin/sun/study/mapper/StudentMapper.xml

The error may involve idin.sun.study.mapper.StudentMapper.getStudents

The error occurred while executing a query

Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:136)

at app.MybatisApp.getStudents1(MybatisApp.java:133)

at app.MybatisApp.lambdatestThreadSafe0(MybatisApp.java:99)

at java.lang.Thread.run(Thread.java:748)

Caused by: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:152)

at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)

at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)

... 5 more

org.apache.ibatis.exceptions.PersistenceException:

Error querying database. Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

The error may exist in idin/sun/study/mapper/StudentMapper.xml

The error may involve idin.sun.study.mapper.StudentMapper.getStudents

The error occurred while executing a query

Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:136)

at app.MybatisApp.getStudents1(MybatisApp.java:133)

at app.MybatisApp.lambdatestThreadSafe0(MybatisApp.java:99)

at java.lang.Thread.run(Thread.java:748)

Caused by: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:152)

at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)

at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)

... 5 more

    通过控制台的输出报错,说明在并发情况下使用同一个DefaultSqlSession的实例做查询是有问题的(读者可以尝试编写增加、删除、修改方法的并发),抛出的异常是类型转换异常(java.lang.ClassCastException:org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List),从上述报错信息来看报错发生在BaseExecutor类中第152行,作者将该涉及这行的方法全粘贴过来,一起研究下:

///方法来自<BaseExecutor>类

@SuppressWarnings("unchecked")

@Override

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());

if (closed) {

    throw new ExecutorException("Executor was closed.");

}

if (queryStack == 0 && ms.isFlushCacheRequired()) {

    clearLocalCache();

}

List<E> list;

try {

    queryStack++;

    // 下面这行代码即是源码中的第152行

    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

    if (list != null) {

        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

    } else {

        // 调用queryFromDatabase,queryFromDatabase方法见下文

        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

    }

} finally {

    queryStack--;

}

if (queryStack == 0) {

    for (DeferredLoad deferredLoad : deferredLoads) {

        deferredLoad.load();

    }

    // issue #601

    deferredLoads.clear();

    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

        // issue #482

        clearLocalCache();

    }

}

return list;

}

// queryFromDatabase方法

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

List<E> list;

// 缓存值

localCache.putObject(key, EXECUTION_PLACEHOLDER);

try {

    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

} finally {

    localCache.removeObject(key);

}

localCache.putObject(key, list);

if (ms.getStatementType() == StatementType.CALLABLE) {

    localOutputParameterCache.putObject(key, parameter);

}

return list;

}

BaseExecutor类中第152行,是一个三元表达式,用于判断resultHandler 是否是null,这里给出结论:此处的resultHandler =null(作者的测试用例中使用selectList方法,而selectList方法传入的resultHandler就是个null,读者可翻阅mybatis源码验证 ),所以三元表单式会成为:list = (List<E>) localCache.getObject(key) ,这里出现了强制类型装换,说明问题出在localCache取得值。

这里的localCache是我们常说的mybatis一级缓存,其原理作者在此不讲,也是给出结论:

localCache的key值生成策略,与查询方法、参数等有关,完全一样的查询,生成的key是一样的。读者可以查看源码

BaseExecutor#createCacheKey

    查询时,由于第一次查询是不存在缓存的,此时"list = (List<E>) localCache.getObject(key) "中"list=null",代码会进入" list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);",在queryFromDatabase方法中第二句代码"localCache.putObject(key, EXECUTION_PLACEHOLDER)",先给localCache存了一个EXECUTION_PLACEHOLDER,而EXECUTION_PLACEHOLDER是枚举类ExecutionPlaceholder的一个枚举项。由于BaseExecutor中的方法都不是同步方法,在并发的情况下,就会出现这样的场景:

    Thread1进入query方法,用key取缓存localCache数据不存在,则进入了queryFromDatabase,并执行了"localCache.putObject(key, EXECUTION_PLACEHOLDER)",而此时Thread2进入了query方法,用key取缓存localCache数据,此时取出来的是Thread1刚缓存的EXECUTION_PLACEHOLDER,然后执行类型转换,由于EXECUTION_PLACEHOLDER不是list类型,所以转换抛出异常(java.lang.ClassCastException:org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List)。

  通过上述场景,读者应该明白了吧,是BaseExecutor中缓存机制(mybatis的一级缓存)导致了并发问题。这种并发问题,产生原因:并发操作使用了同一个DefaultSqlSession的实例,而同一个DefaultSqlSession的实例使用的是同一个Executor对象,当缓存命中时就会出现异常或者数据不完整的情况。

总结

    关于SqlSessionTemplate如何保证线程安全性的博文太多,而且很多博主讲的都特别详细,作者不再老生常谈。我们已经知道DefaultSqlSession线程安全问题的产生原因,故避免线程安全问题就得避免多个线程并发使用同一个DefaultSqlSession的实例。SqlSessionTemplate中也是这么做的,这也是SqlSessionTemplate中一级缓存失效的原因,因为一级缓存是基于同一个DefaultSqlSession实例实现的。

————————————————

版权声明:本文为CSDN博主「程序员4J」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/Zhs901026/article/details/103997750

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,337评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,560评论 3 406
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,632评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,219评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,219评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,670评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,018评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,000评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,552评论 1 326
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,565评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,692评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,280评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,009评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,435评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,587评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,276评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,752评论 2 367

推荐阅读更多精彩内容