如何不加事务也能做到同一个线程内使用同一个数据库连接?
程序员文章站
2022-03-15 19:25:49
...
如何不加事务也能做到同一个线程内使用同一个数据库连接?
这个问题一般在使用数据库的临时表的时候会碰到。mysql的临时表是直接“挂在”数据库连接(即connection
)上的,这就意味着如果不是同一个数据库connection
,对应的临时表就会不存在,强行使用会直接报错。
所以解决这个问题的唯一的出发点就是“要保证同一个线程内反复和数据库交互都是同一个数据库连接”。
既然涉及“同一线程”,那ThreadLocal
肯定跑不了(Spring的声明式事务也是用ThreaLocal
做的)。现在就是在哪里用ThreadLocal
的问题。
其实不用想的太复杂,直接在数据源
那里"做手脚"即可,这里假设你用的是druid
数据源(用什么数据源不是重点),对应的数据源类为com.alibaba.druid.pool.DruidDataSource
。所以你的配置大概会是这样:
spring:
application:
name: xxxxxx
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: ${jdbc.url}
username: ${jdbc.username}
password: ${jdbc.password}
type: com.alibaba.druid.pool.DruidDataSource
现在只需要写一个自己的数据源就能解决问题,如下:
public class OneConnectionDataSource extends DruidDataSource implements SmartDataSource {
/**
* 当前线程是否一个线程用一个连接
*/
public static ThreadLocal<Boolean> needOneConnectionThreadLocal = new ThreadLocal<>();
/**
* 持有当前线程的连接,键为数据源,值为对应的连接
*/
public static ThreadLocal<Map<DataSource, Connection>> connectionThreadLocal = new ThreadLocal<>();
@Override
public Connection getConnection() throws SQLException {
final Boolean needOneConnection = needOneConnectionThreadLocal.get();
if (needOneConnection != null && needOneConnection) {
// 如果当前线程需要保持一个连接,则从connectionThreadLocal取
final DataSource dataSource = determineTargetDataSource();
Map<DataSource, Connection> dataSourceConnectionMap = connectionThreadLocal.get();
if (dataSourceConnectionMap == null) {
dataSourceConnectionMap = new HashMap<>();
}
Connection connection = dataSourceConnectionMap.get(dataSource);
if (connection != null && !connection.isClosed()) {
return connection;
}
connection = super.getConnection();
dataSourceConnectionMap.put(dataSource, connection);
connectionThreadLocal.set(dataSourceConnectionMap);
return connection;
}
return super.getConnection();
}
@Override
public boolean shouldClose(Connection con) {
final Boolean needOneConnection = needOneConnectionThreadLocal.get();
// 如果需要一个线程用一个连接,通知spring不要关这个连接
return needOneConnection == null || !needOneConnection;
}
}
然后把配置改成自己写的数据源:
spring:
application:
name: xxxxxx
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: ${jdbc.url}
username: ${jdbc.username}
password: ${jdbc.password}
type: com.xxx.xxx.xxx.OneConnectionDataSource
这里做几点说明:
- 为了表达方便,这里用了两个ThreadLocal,一个持有当前线程时候是否要维持一个数据库连接的标记,一个持有当前线程要使用的数据库连接。这里当然可以用一个ThreadLocal来做,持有一个对象,对象里同时放标记和连接即可。
- 必须要实现
SmartDataSource
接口的shouldClose
方法来“通知”Spring不要关闭我们自己打开的连接,不然Spring可能会在org.springframework.jdbc.datasource.DataSourceUtils#doCloseConnection
这个方法里关掉我们打开的数据库连接 -
connectionThreadLocal
用DateSource
做键是考虑到多数据源的场景需要区分数据库连接所属数据源 - 理论上目前是可以直接用了,在要保持同一个连接的方法最开始标记
needOneConnectionThreadLocal
,然后再方法最后释放并关闭连接即可。但这样每个方法都要加一样的代码,显然很麻烦也不优雅。最方便的用法肯定是在要保持同一个数据库连接的方法上加个注解,其它任何代码都不加。所以我们需要一个注解和一个切面。
注解和切面
首先定义一个注解:
/**
* 使用这个注解可以保证方法内的所有数据库连接用的是同一个
**/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface OneConnection {
}
然后再写一个切面,切所有打了OneConnection
注解的方法:
@Component
@Aspect
public class OneConnectionAdvice {
@Around("@annotation(com.xxx.xxx.xxx.utils.OneConnection)")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
// 进入方法之前做好标记
OneConnectionDataSource.needOneConnectionThreadLocal.set(true);
final Object proceed = jp.proceed();
// 释放ThreadLocal并关闭连接
final Map<DataSource, Connection> dataSourceConnectionMap = OneConnectionDataSource.connectionThreadLocal.get();
for (Connection connection : dataSourceConnectionMap.values()) {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
OneConnectionDataSource.needOneConnectionThreadLocal.remove();
return proceed;
}
}
到这里就完美了,以后在遇到需要维持同一个数据库连接的需求时,只需要像Spring加事务一样在方法上加一个注解就能轻松获得“保持同一个数据库连接”的能力。