在生产环境中,我们几乎从不手动管理SqlSession,而是由Spring框架来管理。让我详细解释这套机制。
一、Spring+MyBatis的整合模式
1.1 核心组件:SqlSessionTemplate
java
// Spring管理SqlSession的核心组件
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
// 关键:通过ThreadLocal为每个线程绑定SqlSession
private final ThreadLocal<SqlSession> sqlSessionHolder =
new ThreadLocal<>();
// 执行数据库操作
public <T> T selectOne(String statement) {
// 1. 获取当前线程的SqlSession
SqlSession sqlSession = getSqlSession();
try {
// 2. 执行查询
return sqlSession.selectOne(statement);
} finally {
// 3. 不关闭!由Spring管理生命周期
}
}
}
二、Spring如何管理SqlSession生命周期
2.1 三种主要模式
模式1:Mapper代理模式(最常用)
java
// 配置类
@Configuration
@MapperScan("com.example.mapper") // 自动扫描Mapper接口
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
}
// Service中使用
@Service
public class UserService {
// 直接注入Mapper接口,不需要知道SqlSession
@Autowired
private UserMapper userMapper;
public User getUser(Long id) {
// Spring自动管理SqlSession
return userMapper.selectById(id);
}
}
模式2:SqlSessionTemplate直接使用
java
@Service
public class UserService {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
public User getUser(Long id) {
// 手动获取Mapper(较少使用)
UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
return mapper.selectById(id);
}
}
2.2 核心问题解答:一次请求是否对应一个SqlSession?
答案是:取决于事务配置!
三、不同场景下的SqlSession管理
3.1 无事务方法:每次Mapper方法调用都是新的SqlSession
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 没有@Transactional注解
public void demoNoTransaction() {
System.out.println("=== 无事务方法演示 ===");
// 第一次查询:创建新的SqlSession1
User user1 = userMapper.selectById(1);
System.out.println("查询1:创建SqlSession1,执行SQL");
// 第二次查询:创建新的SqlSession2
User user2 = userMapper.selectById(1);
System.out.println("查询2:创建SqlSession2,执行SQL");
// 结论:两次查询在两个不同的SqlSession中
// 一级缓存不生效!会执行两次SQL
}
}
3.2 有事务方法:整个方法使用同一个SqlSession
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional // 关键注解!
public void demoWithTransaction() {
System.out.println("=== 有事务方法演示 ===");
// 第一次查询:创建SqlSession
User user1 = userMapper.selectById(1);
System.out.println("查询1:创建SqlSession,执行SQL");
// 第二次查询:复用同一个SqlSession
User user2 = userMapper.selectById(1);
System.out.println("查询2:复用SqlSession,从一级缓存读取,无SQL");
// 第三次查询不同参数
User user3 = userMapper.selectById(2);
System.out.println("查询3:参数不同,执行SQL");
// 第四次查询相同参数
User user4 = userMapper.selectById(2);
System.out.println("查询4:从一级缓存读取,无SQL");
// 方法结束:提交事务,关闭SqlSession
System.out.println("方法结束:提交事务,关闭SqlSession");
// 结论:整个方法在同一个SqlSession中
// 一级缓存生效!相同查询只执行一次SQL
}
}
3.3 Web请求场景:通常每个请求是一个事务
java
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
// @Transactional // Controller层一般不直接加事务
public User getUser(@PathVariable Long id) {
// 通常Service方法有@Transactional
return userService.getUserWithCache(id);
}
}
@Service
public class UserService {
@Transactional // Service层加事务,一个HTTP请求对应一个事务
public User getUserWithCache(Long id) {
// 这个Service方法中的所有数据库操作
// 都在同一个SqlSession中
// 一级缓存生效
return userMapper.selectById(id);
}
}
四、Spring事务管理机制
4.1 事务管理器与SqlSession绑定
java
// Spring的事务管理机制
public class SpringManagedTransaction implements Transaction {
private final DataSource dataSource;
private Connection connection;
private boolean isConnectionTransactional;
// 获取连接(关键:从事务同步管理器中获取)
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
// 从Spring的事务同步管理器中获取连接
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(
this.connection, this.dataSource);
}
}
4.2 事务传播行为的影响
java
@Service
public class ComplexService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
// 外层事务
@Transactional(propagation = Propagation.REQUIRED)
public void complexBusiness(Long userId) {
// 在事务中:创建/获取SqlSession1
User user = userMapper.selectById(userId); // SqlSession1
// 调用内层方法(默认REQUIRED,加入现有事务)
processOrders(userId); // 仍然使用SqlSession1
// 结论:整个方法使用同一个SqlSession
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processOrders(Long userId) {
// 创建新的事务,新的SqlSession2
List<Order> orders = orderMapper.selectByUserId(userId); // SqlSession2
// 与complexBusiness方法不在同一个SqlSession
// 一级缓存不共享
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void logOperation(Long userId) {
// 非事务执行,没有SqlSession绑定
// 每次数据库操作可能使用不同的SqlSession
}
}
五、如何判断当前是否在同一个SqlSession中
5.1 调试方法:查看SqlSession ID
java
@Service
public class DebugService {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
@Transactional
public void debugSqlSession() {
// 方法1:获取当前SqlSession(仅用于调试)
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
.openSession();
// 获取SqlSession的唯一标识
System.out.println("SqlSession hashCode: " + sqlSession.hashCode());
// 方法2:通过反射查看ThreadLocal中的SqlSession
try {
Field field = sqlSessionTemplate.getClass()
.getDeclaredField("sqlSessionHolder");
field.setAccessible(true);
ThreadLocal<SqlSession> holder = (ThreadLocal<SqlSession>) field.get(sqlSessionTemplate);
SqlSession currentSession = holder.get();
System.out.println("Current SqlSession: " + currentSession.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
5.2 实践判断方法
java
public class SqlSessionInspector {
/**
* 判断两个操作是否在同一个SqlSession中的实用方法
*
* 规则:
* 1. 在同一个@Transactional方法中 → 同一个SqlSession ✓
* 2. 在同一个Service方法中(无@Transactional)→ 可能不同 ✗
* 3. 跨Service方法调用 → 取决于事务传播行为
*/
public static boolean isSameSqlSession() {
// 实际判断逻辑:
// 1. 查看当前线程是否有活跃事务
TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
return status != null && !status.isCompleted();
}
}
六、生产环境配置示例
6.1 Spring Boot + MyBatis配置
yaml
# application.yml
mybatis:
configuration:
cache-enabled: true # 开启二级缓存
local-cache-scope: statement # 一级缓存作用域
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
hikari:
maximum-pool-size: 20
# 事务管理配置
transaction:
default-timeout: 30 # 事务超时时间30秒
6.2 事务配置类
java
@Configuration
@EnableTransactionManagement // 启用事务管理
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager manager) {
TransactionTemplate template = new TransactionTemplate(manager);
template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return template;
}
}
七、实际案例分析
7.1 案例1:Web应用中的典型流程
java
// HTTP请求:GET /users/1
// 1. 请求到达DispatcherServlet
// 2. 调用UserController.getUser(1)
// 3. 调用UserService.getUserWithCache(1)
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(readOnly = true)
public User getUserWithCache(Long id) {
log.info("开始查询用户 {}", id);
// 第一次查询
long start1 = System.currentTimeMillis();
User user1 = userMapper.selectById(id);
log.info("第一次查询耗时: {}ms", System.currentTimeMillis() - start1);
// 第二次查询(应该从一级缓存读取)
long start2 = System.currentTimeMillis();
User user2 = userMapper.selectById(id);
log.info("第二次查询耗时: {}ms", System.currentTimeMillis() - start2);
// 验证是同一个对象(一级缓存直接返回引用)
boolean sameReference = (user1 == user2);
log.info("两次查询返回同一对象: {}", sameReference);
return user2;
}
}
// 日志输出:
// 第一次查询耗时: 15ms (执行SQL)
// 第二次查询耗时: 0ms (一级缓存)
// 两次查询返回同一对象: true
7.2 案例2:批量操作优化
java
@Service
public class BatchService {
@Autowired
private UserMapper userMapper;
// 错误示例:非事务中批量查询
public List<User> getUsersWrong(List<Long> ids) {
List<User> users = new ArrayList<>();
for (Long id : ids) {
// 每次循环都创建新的SqlSession
User user = userMapper.selectById(id); // N次SQL查询
users.add(user);
}
return users;
}
// 正确示例1:使用事务包装
@Transactional(readOnly = true)
public List<User> getUsersRight1(List<Long> ids) {
List<User> users = new ArrayList<>();
for (Long id : ids) {
// 同一个SqlSession,但相同查询只执行一次
User user = userMapper.selectById(id);
users.add(user);
}
return users;
}
// 正确示例2:批量查询方法
public List<User> getUsersRight2(List<Long> ids) {
// 一次SQL查询,返回所有结果
return userMapper.selectByIds(ids);
}
// 正确示例3:使用IN查询
@Select("<script>" +
"SELECT * FROM user WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<User> selectByIds(@Param("ids") List<Long> ids);
}
八、最佳实践总结
8.1 判断规则总结
| 场景 | 是否同一个SqlSession | 一级缓存是否生效 |
|---|---|---|
| 同一个@Transactional方法内 | ✓ 是 | ✓ 生效 |
| 同一个非事务方法内 | ✗ 否(每次调用新建) | ✗ 不生效 |
| 跨@Transactional方法调用 | 取决于传播行为 | 可能不生效 |
| Web请求(Service有@Transactional) | ✓ 是(每个请求一个) | ✓ 生效 |
| 异步方法(@Async) | ✗ 否(不同线程) | ✗ 不生效 |
8.2 生产环境建议
-
Service层添加事务注解:
java@Service @Transactional(readOnly = true) // 类级别默认只读 public class UserService { @Transactional // 写操作单独标记 public User updateUser(User user) { // ... } } -
合理设置事务边界:
java// 合适的事务边界:一个完整的业务操作 @Transactional public Order createOrder(OrderRequest request) { // 验证库存 // 扣减库存 // 创建订单 // 记录日志 // 所有操作在同一个事务中 } -
监控SqlSession使用:
java// 添加监控点 @Aspect @Component public class SqlSessionMonitor { @Around("@within(org.springframework.stereotype.Service)") public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); long start = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long duration = System.currentTimeMillis() - start; if (duration > 1000) { // 超过1秒警告 log.warn("方法 {} 执行时间过长: {}ms", methodName, duration); } } } } -
一级缓存注意事项:
- 长事务可能导致一级缓存积累大量数据
- 考虑设置
localCacheScope=STATEMENT(每次查询后清空一级缓存) - 复杂对象修改可能影响缓存中的对象
九、常见问题解答
Q1:Spring Boot中默认的SqlSession管理策略是什么?
A:Spring Boot + MyBatis Starter默认配置:
- 使用
SqlSessionTemplate管理SqlSession - 默认不开启事务时,每个Mapper方法调用使用新的SqlSession
- 开启事务时,整个事务使用同一个SqlSession
Q2:如何查看当前是否有活跃的SqlSession?
java
// 方法1:通过TransactionSynchronizationManager
boolean hasActiveTransaction = TransactionSynchronizationManager
.isActualTransactionActive();
// 方法2:通过DataSourceUtils
Connection conn = DataSourceUtils.getConnection(dataSource);
boolean isTransactional = DataSourceUtils.isConnectionTransactional(conn, dataSource);
Q3:一级缓存在微服务架构中有什么问题?
A:在微服务中:
- 每个实例有自己的JVM,一级缓存不共享
- 可能导致不同实例数据不一致
- 建议:关闭一级缓存或设置很短的作用域,使用分布式缓存
Q4:如何强制清空一级缓存?
java
@Service
public class CacheService {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
public void clearLocalCache() {
// 获取当前SqlSession并清空缓存
sqlSessionTemplate.clearCache();
}
@Transactional
public void updateAndClear() {
// 更新操作会自动清空相关缓存
userMapper.updateUser(user);
// 如果需要手动清空
sqlSessionTemplate.clearCache();
}
}
总结
在生产环境中,Spring通过事务管理自动控制SqlSession的生命周期:
- 没有事务:每次Mapper方法调用都创建新的SqlSession
- 有事务:整个事务使用同一个SqlSession(通常是一个HTTP请求对应一个事务)
关键判断标准:
- 查看方法是否有
@Transactional注解 - 同一个
@Transactional方法内 → 同一个SqlSession → 一级缓存生效 - 不同方法或没有事务 → 不同SqlSession → 一级缓存不生效
实际开发中,我们通常:
- 在Service层方法添加
@Transactional - 让Spring自动管理SqlSession
- 通过事务边界控制一级缓存的作用范围
- 监控长事务避免一级缓存过大
这样既能享受一级缓存的性能优势,又避免了手动管理SqlSession的复杂性。