Skip to content

Spring JDBC 数据库连接控制完全指南 🚀

前言:为什么需要控制数据库连接?

在现代企业级应用开发中,数据库连接管理是一个至关重要的话题。想象一下,如果每次需要访问数据库时都要手动创建连接、管理事务、处理异常,那将是多么繁琐和容易出错的事情!

IMPORTANT

Spring Framework 通过其强大的数据访问抽象层,为我们提供了优雅的数据库连接控制方案,让开发者能够专注于业务逻辑而不是底层的连接管理细节。

让我们通过一个简单的对比来理解这个问题:

kotlin
// 传统方式:繁琐且容易出错
fun getUserById(id: Long): User? {
    var connection: Connection? = null
    var statement: PreparedStatement? = null
    var resultSet: ResultSet? = null
    
    try {
        // 手动管理连接
        connection = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/mydb", 
            "user", 
            "password"
        ) 
        
        statement = connection.prepareStatement("SELECT * FROM users WHERE id = ?")
        statement.setLong(1, id)
        resultSet = statement.executeQuery()
        
        if (resultSet.next()) {
            return User(
                id = resultSet.getLong("id"),
                name = resultSet.getString("name")
            )
        }
    } catch (e: SQLException) {
        // 手动处理异常
        throw RuntimeException("Database error", e)
    } finally {
        // 手动释放资源
        resultSet?.close()
        statement?.close()
        connection?.close()
    }
    return null
}
kotlin
// Spring方式:简洁且安全
@Repository
class UserRepository(private val jdbcTemplate: JdbcTemplate) {
    
    fun getUserById(id: Long): User? {
        return jdbcTemplate.queryForObject( 
            "SELECT * FROM users WHERE id = ?",
            arrayOf(id)
        ) { rs, _ ->
            User(
                id = rs.getLong("id"),
                name = rs.getString("name")
            )
        }
    }
}

核心概念:DataSource - 数据源的统一抽象

什么是 DataSource?

DataSource 是 JDBC 规范中定义的一个接口,它代表了数据库连接的工厂。Spring 通过 DataSource 抽象了数据库连接的获取过程,让应用程序无需关心连接池、事务管理等底层细节。

TIP

把 DataSource 想象成一个"数据库连接的自动售货机":你投入请求,它就给你一个可用的连接,而且还会帮你管理连接的生命周期!

Spring 提供的 DataSource 实现类型

1. DriverManagerDataSource - 简单但不推荐用于生产

DriverManagerDataSource 是最基础的实现,每次请求都会创建新的连接。

kotlin
@Configuration
class DatabaseConfig {
    
    @Bean
    fun dataSource(): DataSource {
        return DriverManagerDataSource().apply {
            setDriverClassName("com.mysql.cj.jdbc.Driver") 
            url = "jdbc:mysql://localhost:3306/myapp"
            username = "root"
            password = "password"
        }
    }
}

WARNING

DriverManagerDataSource 仅适用于测试环境!在生产环境中使用会导致性能问题,因为它不提供连接池功能。

2. 连接池 DataSource - 生产环境的最佳选择

HikariCP(推荐)

kotlin
@Configuration
class ProductionDatabaseConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    fun dataSource(): DataSource {
        return HikariDataSource().apply {
            jdbcUrl = "jdbc:mysql://localhost:3306/myapp"
            username = "root"
            password = "password"
            driverClassName = "com.mysql.cj.jdbc.Driver"
            
            // 连接池配置
            maximumPoolSize = 20
            minimumIdle = 5
            connectionTimeout = 30000
            idleTimeout = 600000
        }
    }
}

Apache DBCP2

kotlin
@Bean(destroyMethod = "close")
fun dbcpDataSource(): DataSource {
    return BasicDataSource().apply {
        driverClassName = "com.mysql.cj.jdbc.Driver"
        url = "jdbc:mysql://localhost:3306/myapp"
        username = "root"
        password = "password"
        
        // 连接池参数
        initialSize = 5
        maxTotal = 20
        maxIdle = 10
        minIdle = 5
    }
}

NOTE

注意 destroyMethod = "close" 的使用,这确保了 Spring 容器关闭时能正确释放连接池资源。

DataSourceUtils - 连接管理的得力助手

DataSourceUtils 提供了一系列静态方法来安全地获取和释放数据库连接,特别是在事务环境中。

kotlin
@Service
class UserService(private val dataSource: DataSource) {
    
    fun executeCustomQuery(): List<String> {
        // 使用 DataSourceUtils 安全获取连接
        val connection = DataSourceUtils.getConnection(dataSource) 
        
        try {
            val statement = connection.prepareStatement("SELECT name FROM users")
            val resultSet = statement.executeQuery()
            
            val results = mutableListOf<String>()
            while (resultSet.next()) {
                results.add(resultSet.getString("name"))
            }
            return results
            
        } finally {
            // DataSourceUtils 会智能地决定是否真正关闭连接
            DataSourceUtils.releaseConnection(connection, dataSource) 
        }
    }
}

IMPORTANT

DataSourceUtils.getConnection() 会自动参与到当前的事务中(如果存在),而直接使用 DataSource.getConnection() 则不会。

高级 DataSource 实现

SingleConnectionDataSource - 单连接数据源

适用于测试环境,整个应用生命周期内只使用一个连接:

kotlin
@TestConfiguration
class TestDatabaseConfig {
    
    @Bean
    fun testDataSource(): DataSource {
        return SingleConnectionDataSource().apply {
            setDriverClassName("org.h2.Driver")
            url = "jdbc:h2:mem:testdb"
            username = "sa"
            password = ""
            suppressClose = true // [!code highlight] // 防止连接被意外关闭
        }
    }
}

TransactionAwareDataSourceProxy - 事务感知代理

当你需要让现有代码参与到 Spring 管理的事务中时:

kotlin
@Configuration
class TransactionConfig {
    
    @Bean
    fun transactionAwareDataSource(
        @Qualifier("actualDataSource") targetDataSource: DataSource
    ): DataSource {
        return TransactionAwareDataSourceProxy(targetDataSource) 
    }
}

事务管理器配置

DataSourceTransactionManager vs JdbcTransactionManager

Spring 5.3 引入了 JdbcTransactionManager,它是 DataSourceTransactionManager 的增强版本:

kotlin
@Configuration
@EnableTransactionManagement
class TransactionConfig {
    
    @Bean
    fun transactionManager(dataSource: DataSource): PlatformTransactionManager {
        return DataSourceTransactionManager(dataSource) 
    }
}
kotlin
@Configuration
@EnableTransactionManagement
class TransactionConfig {
    
    @Bean
    fun transactionManager(dataSource: DataSource): PlatformTransactionManager {
        return JdbcTransactionManager(dataSource) 
    }
}

TIP

JdbcTransactionManager 提供了更好的异常转换能力,能将数据库锁定失败等异常转换为相应的 DataAccessException 子类,推荐在新项目中使用。

实际应用场景示例

场景1:多数据源配置

kotlin
@Configuration
class MultiDataSourceConfig {
    
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.primary")
    fun primaryDataSource(): DataSource {
        return DataSourceBuilder.create().build()
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.secondary")
    fun secondaryDataSource(): DataSource {
        return DataSourceBuilder.create().build()
    }
    
    @Bean
    fun primaryJdbcTemplate(@Qualifier("primaryDataSource") dataSource: DataSource): JdbcTemplate {
        return JdbcTemplate(dataSource) 
    }
    
    @Bean
    fun secondaryJdbcTemplate(@Qualifier("secondaryDataSource") dataSource: DataSource): JdbcTemplate {
        return JdbcTemplate(dataSource) 
    }
}

场景2:读写分离配置

读写分离DataSource实现示例
kotlin
class ReadWriteDataSource : AbstractRoutingDataSource() {
    
    companion object {
        private val contextHolder = ThreadLocal<String>()
        
        fun setReadOnly() {
            contextHolder.set("read")
        }
        
        fun setReadWrite() {
            contextHolder.set("write")
        }
        
        fun clear() {
            contextHolder.remove()
        }
    }
    
    override fun determineCurrentLookupKey(): Any? {
        return contextHolder.get() ?: "write"
    }
}

@Configuration
class ReadWriteDataSourceConfig {
    
    @Bean
    fun readWriteDataSource(
        @Qualifier("writeDataSource") writeDataSource: DataSource,
        @Qualifier("readDataSource") readDataSource: DataSource
    ): DataSource {
        val routingDataSource = ReadWriteDataSource()
        
        val targetDataSources = mapOf<Any, Any>(
            "write" to writeDataSource, 
            "read" to readDataSource 
        )
        
        routingDataSource.setTargetDataSources(targetDataSources)
        routingDataSource.setDefaultTargetDataSource(writeDataSource)
        
        return routingDataSource
    }
}

@Service
@Transactional
class UserService(private val userRepository: UserRepository) {
    
    @Transactional(readOnly = true)
    fun getUser(id: Long): User? {
        ReadWriteDataSource.setReadOnly() 
        try {
            return userRepository.findById(id)
        } finally {
            ReadWriteDataSource.clear()
        }
    }
    
    fun saveUser(user: User): User {
        ReadWriteDataSource.setReadWrite() 
        try {
            return userRepository.save(user)
        } finally {
            ReadWriteDataSource.clear()
        }
    }
}

最佳实践与注意事项

1. 连接池配置建议

kotlin
@ConfigurationProperties("spring.datasource.hikari")
@Component
data class HikariProperties(
    var maximumPoolSize: Int = 20,
    var minimumIdle: Int = 5,
    var connectionTimeout: Long = 30000,
    var idleTimeout: Long = 600000,
    var maxLifetime: Long = 1800000,
    var leakDetectionThreshold: Long = 60000 // [!code highlight] // 连接泄漏检测
)

2. 监控和诊断

kotlin
@Component
class DataSourceHealthIndicator(
    private val dataSource: DataSource
) : HealthIndicator {
    
    override fun health(): Health {
        return try {
            val connection = DataSourceUtils.getConnection(dataSource)
            val valid = connection.isValid(1) 
            DataSourceUtils.releaseConnection(connection, dataSource)
            
            if (valid) {
                Health.up()
                    .withDetail("database", "Available")
                    .build()
            } else {
                Health.down()
                    .withDetail("database", "Connection validation failed")
                    .build()
            }
        } catch (e: Exception) {
            Health.down(e)
                .withDetail("database", "Connection failed")
                .build()
        }
    }
}

3. 常见问题排查

WARNING

连接泄漏问题

如果应用程序获取了数据库连接但没有正确释放,会导致连接池耗尽。始终使用 try-finally 块或者 Spring 的模板类来确保连接被正确释放。

CAUTION

事务边界问题

在使用 @Transactional 注解时,要注意事务的传播行为。错误的事务配置可能导致数据不一致或性能问题。

总结

Spring 的数据库连接控制机制为我们提供了强大而灵活的数据访问能力:

统一抽象:通过 DataSource 接口统一了各种数据源的访问方式

连接池支持:内置支持多种连接池实现,提升应用性能

事务集成:与 Spring 事务管理无缝集成

异常处理:提供统一的异常处理机制

测试友好:提供专门的测试用 DataSource 实现

通过合理配置和使用这些组件,我们可以构建出高性能、可维护的数据访问层,让开发者能够专注于业务逻辑的实现。

TIP

记住:选择合适的 DataSource 实现是构建高质量应用的第一步。在开发阶段使用简单的实现进行快速原型开发,在生产环境中使用成熟的连接池方案确保性能和稳定性!