线程安全与并发
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 通过 TransactionSynchronizationManager 的 ThreadLocal 机制,确保每个线程获取独立的 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 核心数 + 1 | 9 |
| 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"/>
注意事项
- SqlSessionFactory 全局单例:整个应用只创建一个实例,不要在每次请求时重复创建
- SqlSession 用完即关:必须使用 try-with-resources 或 finally 块确保关闭,避免连接泄漏
- Mapper 代理可注入为单例:在 Spring 环境中 Mapper 接口注入为单例 Bean 是安全的,因为内部通过 SqlSessionTemplate 管理线程隔离
- 连接池大小合理:根据 CPU 核心数和 I/O 特性计算,不是越大越好
- 二级缓存非线程安全:默认 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
📝 发现内容有误?点击此处直接编辑