MyBatis 的 SqlSession 到底线程安全吗?SqlSessionFactory 有哪些潜在问题?

MyBatis 中 SqlSession 与 SqlSessionFactory 线程安全性深度剖析

在 MyBatis 开发实践中,SqlSession 的线程安全问题SqlSessionFactory 的潜在风险是导致生产环境事故的高发区。曾有团队因共享 SqlSession 导致用户数据错乱,也有系统因工厂配置错误引发数据库连接池雪崩。本文将用真实场景还原这两个核心组件的运作机制,帮助开发者避开 90% 的并发陷阱。

一、SqlSession 线程安全陷阱

1.1 为什么说 SqlSession 非线程安全?

通过源码分析发现:每个 SqlSession 实例都维护着独立的 JDBC Connection 和事务上下文。当多个线程共享同一个 SqlSession 时:

// 错误示例:全局共享 SqlSession
public class UserDao {
    private SqlSession sharedSession; // 定时炸弹
    
    public User getById(Long id) {
        return sharedSession.selectOne("selectUser", id);
    }
}

这种共享会导致:

  • 事务交叉污染:线程A的提交可能包含线程B的修改
  • 缓存混乱:一级缓存可能返回错误版本数据
  • 连接泄漏:异常场景下连接无法正常回收

1.2 正确使用姿势

黄金法则:每个请求单独创建,方法内立即关闭

// 正确用法:使用局部变量
public User getUser(Long id) {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        return mapper.selectById(id);
    }
}

推荐组合方案:

  1. try-with-resources 自动关闭(Java7+)
  2. ThreadLocal 绑定(需配合请求拦截器)
  3. Spring 事务管理(推荐企业级方案)

二、SqlSessionFactory 的隐秘风险

2.1 线程安全背后的隐患

虽然官方文档声明 SqlSessionFactory 是线程安全的,但在实际使用中仍存在三大雷区:

风险类型 典型案例 后果
配置错误 mybatis-config.xml 中 environment 配置缺失 无法获取 DataSource
资源泄漏 未正确关闭 SqlSessionFactory 数据库连接池耗尽
单例滥用 Web 应用中重复创建工厂实例 JVM 堆内存溢出

2.2 工厂创建最佳实践

// 推荐初始化方式
public class MyBatisUtil {
    private static final SqlSessionFactory factory;
    
    static {
        String resource = "mybatis-config.xml";
        try (InputStream is = Resources.getResourceAsStream(resource)) {
            factory = new SqlSessionFactoryBuilder().build(is);
        }
    }
    
    public static SqlSession getSession() {
        return factory.openSession();
    }
}

关键注意事项:

  • 配置验证:通过 factory.getConfiguration().validate() 检查配置完整性
  • 连接监控:集成 Druid 等连接池的监控功能
  • 版本兼容:MyBatis 3.5+ 要求 JDBC 4.2+ 驱动

三、性能优化进阶方案

3.1 二级缓存调优

当 SqlSessionFactory 启用二级缓存时:

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

需要特别注意:

  • 缓存策略:LRU vs FIFO 的选择依据
  • 序列化:分布式环境必须使用 Redis 等支持序列化的缓存
  • 失效控制:通过 flushCache 属性管理缓存更新

3.2 批处理优化

大数据量插入时的性能对比:

处理方式 10万条耗时 内存占用
逐条插入 120s 500MB
批处理模式 8s 150MB

正确使用方式:

try (SqlSession session = factory.openSession(ExecutorType.BATCH)) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    for (User user : userList) {
        mapper.insert(user);
    }
    session.commit();
}

四、避坑指南

根据线上事故总结的常见问题排查表:

  1. 连接泄漏检测:通过 SELECT FROM sys.dm_exec_sessions 监控数据库连接状态
  2. 线程竞争排查:使用 VisualVM 的线程分析功能定位共享 SqlSession
  3. 配置验证脚本:编写单元测试验证工厂初始化流程

互动讨论:你在项目中是否遇到过 MyBatis 的线程安全问题?当时是如何排查解决的?欢迎分享你的实战经验!