欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何不加事务也能做到同一个线程内使用同一个数据库连接?

程序员文章站 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

这里做几点说明:

  1. 为了表达方便,这里用了两个ThreadLocal,一个持有当前线程时候是否要维持一个数据库连接的标记,一个持有当前线程要使用的数据库连接。这里当然可以用一个ThreadLocal来做,持有一个对象,对象里同时放标记和连接即可。
  2. 必须要实现SmartDataSource接口的shouldClose方法来“通知”Spring不要关闭我们自己打开的连接,不然Spring可能会在org.springframework.jdbc.datasource.DataSourceUtils#doCloseConnection这个方法里关掉我们打开的数据库连接
  3. connectionThreadLocalDateSource做键是考虑到多数据源的场景需要区分数据库连接所属数据源
  4. 理论上目前是可以直接用了,在要保持同一个连接的方法最开始标记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加事务一样在方法上加一个注解就能轻松获得“保持同一个数据库连接”的能力。

相关标签: Java