如何保证SqlSession的线程安全?

DefaultSqlSession是线程不安全的

在Mybatis中SqlSession是提供给外部调用的顶层接口,实现类有:DefaultSqlSession、SqlSessionManager以及mybatis-spring提供的实现SqlSessionTemplate。默认实现类为DefaultSqlSession,是线程不完全的。类结构图如下:

file

对于Mybatis提供的原生实现类来说,用的最多就是DefaultSqlSession,但是我们知道DefaultSqlSession这个类不是线程安全的!如下:

file

SqlSessionTemplate是如何保证线程安全的

在我们平时的开发中通常会用到Spring,也会用到mybatis-spring框架,在Spring集成Mybatis的时候我们可以用到SqlSessionTemplate(Spring提供的SqlSession实现类),使用场景案例如下:

file

查看SqlSessionTemplate的源码注释如下:

file

通过源码注释可以看到SqlSessionTemplate是线程安全的类,并且实现了SqlSession接口,也就是说我们可以通过SqlSessionTemplate来代替以往的DefaultSqlSession完成对数据库CRUD操作,并且还保证单例线程安全,那么它是如何保证线程安全的呢?

首先,通过SqlSessionTemplate拥有的三个重载的构造方法分析,最终都会调用最后一个构造方法,会初始化一个SqlSessionProxy的代理对象,如果调用代理类实例中实现的SqlSession接口中定义的方法,该调用会被导向SqlSessionInterceptor的invoke方法触发代理逻辑

file

接下来查看SqlSessionInterceptor的invoke方法

  1. 通过getSqlSession方法获取SqlSession对象(如果使用了事务,从Spring事务上下文获取)
  2. 调用SqlSession的接口方法操作数据库获取结果
  3. 返回结果集
  4. 若发生异常则转换后抛出异常,并最终关闭SqlSession对象
private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          //获取SqlSession(这个sqlSession才是真正使用的,它不是线程安全的)
                //这个方法可以根据Spring的事务上下文来获取事务范围内的SqlSession
                SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
                //调用sqlSession对象的方法(select、update等)
        Object result = method.invoke(sqlSession, args);
                //判断是否为事务操作,如果未被Spring事务托管则自动提交commit
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
                //如果出现异常则根据情况转换后抛出
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
                //最终关闭sqlSession对象
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

重点分析getSqlSession方法如下:

  1. 若无法从当前线程的ThreadLocal中获取则通过SqlSessionFactory获取SqlSession
  2. 若开启了事务,则从当前线程的ThrealLocal上下文中获取SqlSessionHolder
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
        //若开启了事务支持,则从当前的ThreadLocal上下文中获取SqlSessionHolder
        //SqlSessionHolder是SqlSession的包装类
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
        //若无法从ThrealLocal上下文中获取则通过SqlSessionFactory获取SqlSession
    session = sessionFactory.openSession(executorType);
        //若为事务操作,则注册SqlSessionHolder到ThrealLocal中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

大致的分析到此为止,文中只对主要的过程进行了大致的说明,小伙伴若想要仔细分析,可以自己打开源码走一遍!

SqlSessionManger又是什么?

SqlSessionManager是Mybatis提供的线程安全的操作类,且看定义如下:

file

通过上图可以发现SqlSessionManager的构造方法竟然是private的,那我们怎么创建对象呢?其实SqlSessionManager创建对象是通过newInstance方法创建对象的,但需要注入它虽然是私有的构造方法,并且提供给我们一个公有的newInstance方法,但它并不是一个单例模式!

newInstance有很多重载方法,如下所示:

public static SqlSessionManager newInstance(Reader reader) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, null));
}

public static SqlSessionManager newInstance(Reader reader, String environment) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, environment, null));
}

public static SqlSessionManager newInstance(Reader reader, Properties properties) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, properties));
}

public static SqlSessionManager newInstance(InputStream inputStream) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, null));
}

public static SqlSessionManager newInstance(InputStream inputStream, String environment) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, environment, null));
}

public static SqlSessionManager newInstance(InputStream inputStream, Properties properties) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, properties));
}

public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
  return new SqlSessionManager(sqlSessionFactory);
}

SqlSessionManager的openSession方法及其重载方法是直接通过调用底层封装SqlSessionFactory对象的openSession方法来创建SqlSession对象的,如下所示:

@Override
public SqlSession openSession(boolean autoCommit) {
  return sqlSessionFactory.openSession(autoCommit);
}

@Override
public SqlSession openSession(Connection connection) {
  return sqlSessionFactory.openSession(connection);
}

@Override
public SqlSession openSession(TransactionIsolationLevel level) {
  return sqlSessionFactory.openSession(level);
}

@Override
public SqlSession openSession(ExecutorType execType) {
  return sqlSessionFactory.openSession(execType);
}

@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
  return sqlSessionFactory.openSession(execType, autoCommit);
}

@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
  return sqlSessionFactory.openSession(execType, level);
}

@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
  return sqlSessionFactory.openSession(execType, connection);
}

SqlSessionManager中实现SqlSession接口中的方法,例如:select、update等,都是直接调用SqlSessionProxy代理对象中相应的方法,在创建该代理对像的时候使用的InvocationHandler对象是SqlSessionInterceptor,他是定义在SqlSessionManager的一个内部类,其定义如下:

private class SqlSessionInterceptor implements InvocationHandler {
  public SqlSessionInterceptor() {
      // Prevent Synthetic Access
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //获取当前ThreadLocal上下文的SqlSession
        final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
    if (sqlSession != null) {
      try {
                //从上下文获取到SqlSession之后调用对应的方法
        return method.invoke(sqlSession, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    } else {
            //如果无法从ThreadLocal上下文中获取SqlSession则新建一个SqlSession
      try (SqlSession autoSqlSession = openSession()) {
        try {
          final Object result = method.invoke(autoSqlSession, args);
          autoSqlSession.commit();
          return result;
        } catch (Throwable t) {
          autoSqlSession.rollback();
          throw ExceptionUtil.unwrapThrowable(t);
        }
      }
    }
  }
}

此处我们在思考下ThreadLocal的localSqlSession对象在什么时候赋值对应的SqlSession,往上查找最终定位代码(若调用startManagerSession方法将设置ThreadLocal的localSqlSession上下文中的SqlSession对象),如下所示:

public void startManagedSession() {
  this.localSqlSession.set(openSession());
}

public void startManagedSession(boolean autoCommit) {
  this.localSqlSession.set(openSession(autoCommit));
}

public void startManagedSession(Connection connection) {
  this.localSqlSession.set(openSession(connection));
}

public void startManagedSession(TransactionIsolationLevel level) {
  this.localSqlSession.set(openSession(level));
}

public void startManagedSession(ExecutorType execType) {
  this.localSqlSession.set(openSession(execType));
}

public void startManagedSession(ExecutorType execType, boolean autoCommit) {
  this.localSqlSession.set(openSession(execType, autoCommit));
}

public void startManagedSession(ExecutorType execType, TransactionIsolationLevel level) {
  this.localSqlSession.set(openSession(execType, level));
}

public void startManagedSession(ExecutorType execType, Connection connection) {
  this.localSqlSession.set(openSession(execType, connection));
}

SqlSessionTemplate与SqlSessionManager的联系与区别

  • SqlSessionTemplate是Mybatis为了接入Spring提供的Bean。通过TransactionSynchronizationManager中的ThreadLocal<Map<Object, Object>>保存线程对应的SqlSession,实现session的线程安全。
  • SqlSessionManager是Mybatis不接入Spring时用于管理SqlSession的Bean。通过SqlSessionManagger的ThreadLocal<SqlSession>实现session的线程安全。

总结分析

通过上面的代码分析,我们可以看出Spring解决SqlSession线程安全问题的思路就是动态代理与ThreadLocal的运用,我们可以触类旁通:当遇到线程不安全的类,但是又想当作线程安全的类使用,则可以使用ThreadLocal进行线程上下文的隔离,此处的动态代理技术更好的解决了上层API调用的非侵入性,保证API接口调用的高内聚、低耦合原则

本文由博客一文多发平台 OpenWrite 发布!

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

推荐阅读更多精彩内容