全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

线程安全与并发

MyBatis 中不同组件的线程安全特性直接影响并发场景下的正确性和性能。理解 SqlSessionFactory、SqlSession 和 Mapper 代理的生命周期与线程安全边界,是构建高并发数据库应用的基础。

组件线程安全分析

各组件线程安全对比

组件线程安全生命周期创建开销推荐作用域
SqlSessionFactory应用级单例高(解析 XML、构建连接池)全局单例,应用启动创建一次
SqlSession请求级低(从连接池获取连接)方法内创建,用完立即关闭
Mapper 代理对象可复用极低(JDK 动态代理)可注入为 Spring Bean(单例)
Executor 执行器跟随 SqlSession由 SqlSessionFactory 创建

SqlSessionFactory:全局单例

SqlSessionFactory 是线程安全的,整个应用只需要一个实例:

Java
// 正确:全局单例
public class MyBatisUtil {
    private static volatile SqlSessionFactory sqlSessionFactory;

    public static SqlSessionFactory getInstance() {
        if (sqlSessionFactory == null) {
            synchronized (MyBatisUtil.class) {
                if (sqlSessionFactory == null) {
                    try (InputStream is = Resources.getResourceAsStream("mybatis-config.xml")) {
                        sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
                    } catch (IOException e) {
                        throw new RuntimeException("Failed to build SqlSessionFactory", e);
                    }
                }
            }
        }
        return sqlSessionFactory;
    }
}

// Spring Boot 中自动配置为单例 Bean
@Configuration
public class MyBatisConfig {
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }
}

SqlSessionFactory 的创建涉及 XML 解析、类型别名注册、插件链构建、连接池初始化等高开销操作,必须全局单例

SqlSession:非线程安全

SqlSession 不是线程安全的,每个线程必须使用独立的 SqlSession 实例:

Java
// 错误:多线程共享 SqlSession
public class BadExample {
    private SqlSession sqlSession; // 共享实例,线程不安全!

    public void badMethod1() {
        User user = sqlSession.selectOne("selectById", 1);
        // 线程 A 和 B 同时调用,可能抛出 ConcurrentModificationException
    }

    public void badMethod2() {
        sqlSession.insert("insertUser", new User());
        // 与 badMethod1 并发执行,事务状态混乱
    }
}
Java
// 正确:每次请求创建独立的 SqlSession
public class GoodExample {
    private final SqlSessionFactory sqlSessionFactory;

    public User getUserById(Long id) {
        // try-with-resources 自动关闭
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            return mapper.selectById(id);
        }
    }

    public void insertUser(User user) {
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            mapper.insert(user);
            session.commit(); // 显式提交事务
        }
    }
}

Mapper 代理对象:线程安全

Mapper 接口通过 JDK 动态代理生成的代理对象是线程安全的:

Java
// Mapper 代理对象的内部实现原理
public class MapperProxy<T> implements InvocationHandler {

    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache; // 线程安全的缓存

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 每次调用时从 methodCache 获取 MapperMethod(线程安全)
        MapperMethod mapperMethod = methodCache.computeIfAbsent(method,
            k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));

        // MapperMethod.execute 使用传入的 sqlSession 执行
        return mapperMethod.execute(sqlSession, args);
    }
}

Mapper 代理对象的线程安全性取决于传入的 SqlSession。如果 SqlSession 是线程安全的(如 Spring 管理的),则 Mapper 代理也是线程安全的。

Spring 环境中的线程安全

Spring 如何管理 SqlSession

Spring 通过 SqlSessionTemplate 提供线程安全的 SqlSession 封装:

Java
// SqlSessionTemplate 内部实现
public class SqlSessionTemplate implements SqlSession {

    private final SqlSessionFactory sqlSessionFactory;
    private final Executor executor;

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        // 每次方法调用都从当前线程获取 SqlSession
        return getSqlSession().selectOne(statement, parameter);
    }

    private SqlSession getSqlSession() {
        // 从 TransactionSynchronizationManager 获取当前线程的 SqlSession
        return SqlSessionUtils.getSqlSession(
            sqlSessionFactory, executorType, exceptionTranslator);
    }
}

Spring 通过 TransactionSynchronizationManagerThreadLocal 机制,确保每个线程获取独立的 SqlSession:

Java
// 简化版 ThreadLocal 管理
public abstract class SqlSessionUtils {

    private static final ThreadLocal<SqlSession> SESSION_HOLDER = new ThreadLocal<>();

    public static SqlSession getSqlSession(SqlSessionFactory factory, ...) {
        SqlSession session = SESSION_HOLDER.get();
        if (session == null) {
            session = factory.openSession();
            SESSION_HOLDER.set(session);
            // 注册事务同步回调,事务结束时自动关闭
            TransactionSynchronizationManager.registerSynchronization(
                new SqlSessionSynchronization(session, factory));
        }
        return session;
    }
}

Spring Bean 作用域配置

Java
@Configuration
public class MyBatisSpringConfig {

    // SqlSessionFactory:全局单例
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }

    // SqlSessionTemplate:Spring 自动管理线程安全
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }

    // Mapper 接口:注入为单例 Bean,内部通过 SqlSessionTemplate 保证线程安全
    @MapperScan(basePackages = "com.example.mapper")
    static class MapperConfig {}
}
Java
// Mapper 接口可以直接注入为单例
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper; // 单例 Bean,线程安全

    public User getUser(Long id) {
        return userMapper.selectById(id); // 每次调用获取独立 SqlSession
    }
}

连接池并发配置

连接池大小计算

连接池大小直接影响并发性能和资源利用率:

应用类型公式示例(8 核 CPU)
CPU 密集型CPU 核心数 + 19
I/O 密集型(大多数 Web 应用)CPU 核心数 * 2 + 磁盘数16-20
高并发 Web 应用CPU 核心数 * (1 + 平均等待时间/平均执行时间)20-50
YAML
spring:
  datasource:
    hikari:
      # 推荐配置
      maximum-pool-size: 20       # 最大连接数
      minimum-idle: 5             # 最小空闲连接
      connection-timeout: 30000   # 获取连接超时 30s
      idle-timeout: 600000        # 空闲连接回收 10min
      max-lifetime: 1800000       # 连接最大生命周期 30min

连接池过小与过大的问题

问题表现根因解决方案
连接池过小大量线程等待获取连接,TPS 上不去连接数 < 并发请求数增大 maximum-pool-size
连接池过大CPU 使用率低,上下文切换频繁连接数 >> CPU 处理力减小 maximum-pool-size
连接泄漏连接数持续增长,最终耗尽SqlSession 未正确关闭检查 try-with-resources、开启 leak-detection

连接泄漏检测

YAML
spring:
  datasource:
    hikari:
      leak-detection-threshold: 60000  # 60s 未归还视为泄漏
Java
// 连接泄漏时 HikariCP 日志
// HikariPool-1 - Connection leak detection triggered (60000ms threshold exceeded)
// java.lang.Exception: Connection leak detection
//     at com.zaxxer.hikari.pool.ProxyLeakTask.run(ProxyLeakTask.java:104)
//     at java.util.concurrent.ScheduledThreadPoolExecutor...

并发场景常见问题

问题 1:事务传播导致的并发异常

Java
@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        // 嵌套调用另一个事务方法
        inventoryService.deductStock(order.getProductId());
    }
}

@Service
public class InventoryService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deductStock(Long productId) {
        // REQUIRES_NEW 会挂起当前事务,创建新事务
        // 如果并发执行,可能导致死锁
        stockMapper.deduct(productId, 1);
    }
}
传播行为说明并发风险
REQUIRED(默认)加入当前事务,不存在则创建
REQUIRES_NEW始终创建新事务,挂起当前事务中(可能死锁)
NESTED嵌套事务(SavePoint)
NOT_SUPPORTED以非事务方式执行

问题 2:二级缓存并发问题

MyBatis 二级缓存(PerpetualCache)默认使用 HashMap,不是线程安全的:

XML
<!-- 默认二级缓存 -->
<cache eviction="LRU" size="1024"/>
Java
// PerpetualCache 内部实现(非线程安全)
public class PerpetualCache implements Cache {
    private final Map<Object, Object> cache = new HashMap<>();

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value); // HashMap 非线程安全,并发 put 可能死循环
    }
}

多线程并发读写二级缓存的解决方案

XML
<!-- 方案 1:使用 synchronized 装饰器 -->
<cache eviction="LRU" size="1024" readOnly="true"/>
XML
<!-- 方案 2:使用分布式缓存(Redis)替代本地缓存 -->
<cache type="com.example.RedisCache"/>

注意事项

  1. SqlSessionFactory 全局单例:整个应用只创建一个实例,不要在每次请求时重复创建
  2. SqlSession 用完即关:必须使用 try-with-resources 或 finally 块确保关闭,避免连接泄漏
  3. Mapper 代理可注入为单例:在 Spring 环境中 Mapper 接口注入为单例 Bean 是安全的,因为内部通过 SqlSessionTemplate 管理线程隔离
  4. 连接池大小合理:根据 CPU 核心数和 I/O 特性计算,不是越大越好
  5. 二级缓存非线程安全:默认 PerpetualCache 使用 HashMap,多线程并发需使用同步装饰器或分布式缓存

要点总结

  • SqlSessionFactory 线程安全,应全局单例;SqlSession 非线程安全,每个请求独立创建;Mapper 代理对象线程安全,可注入为 Spring 单例 Bean
  • Spring 通过 SqlSessionTemplate + ThreadLocal 机制实现 SqlSession 的线程隔离,Mapper 注入为单例是安全的
  • 连接池大小推荐公式:I/O 密集型 = CPU 核心数 * 2 + 磁盘数;连接池过小导致等待,过大导致上下文切换频繁
  • 连接泄漏通过 HikariCP leak-detection-threshold 检测,常见原因是 SqlSession 未正确关闭
  • MyBatis 二级缓存(PerpetualCache)默认使用 HashMap 非线程安全,多线程场景需使用同步装饰器或 Redis 分布式缓存
  • 事务传播行为 REQUIRES_NEW 在并发场景可能引发死锁,需谨慎使用

存放路径:D:\git2\jwdev\articles\MYBATIS\专家\生产环境最佳实践\线程安全与并发.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 监控与告警
下一篇 → 动态表名与列名
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库