Appearance
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 实现是构建高质量应用的第一步。在开发阶段使用简单的实现进行快速原型开发,在生产环境中使用成熟的连接池方案确保性能和稳定性!