Skip to content

Spring JDBC 批处理操作:让数据库操作飞起来 🚀

为什么需要批处理?解决的核心痛点

想象一下这样的场景:你需要更新 10,000 条用户记录。如果使用传统的单条执行方式,你的应用需要与数据库进行 10,000 次网络通信。这就像你要搬家,每次只搬一个杯子,来回跑 10,000 趟!

NOTE

批处理的核心价值在于减少网络往返次数。通过将多个操作打包成一个批次,我们可以显著提升数据库操作的性能。

传统方式 vs 批处理方式对比

kotlin
// ❌ 低效的单条执行方式
fun updateActorsOneByOne(actors: List<Actor>) {
    actors.forEach { actor ->
        jdbcTemplate.update(
            "UPDATE t_actor SET first_name = ?, last_name = ? WHERE id = ?",
            actor.firstName, actor.lastName, actor.id
        ) // 每次都要与数据库通信!
    }
}
kotlin
// ✅ 高效的批处理方式
fun batchUpdateActors(actors: List<Actor>): IntArray {
    return jdbcTemplate.batchUpdate(
        "UPDATE t_actor SET first_name = ?, last_name = ? WHERE id = ?",
        actors, 100 // 每100条为一个批次
    ) { ps, actor ->
        ps.setString(1, actor.firstName)
        ps.setString(2, actor.lastName)
        ps.setLong(3, actor.id)
    }
}

Spring JDBC 批处理的三种实现方式

1. 基础批处理:使用 BatchPreparedStatementSetter

这是最基础但也是最灵活的批处理方式。你需要实现 BatchPreparedStatementSetter 接口来控制批处理的细节。

kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
    
    private val jdbcTemplate = JdbcTemplate(dataSource)
    
    fun batchUpdate(actors: List<Actor>): IntArray {
        return jdbcTemplate.batchUpdate(
            "UPDATE t_actor SET first_name = ?, last_name = ? WHERE id = ?",
            object : BatchPreparedStatementSetter {
                // 为每一行设置参数值
                override fun setValues(ps: PreparedStatement, i: Int) {
                    val actor = actors[i] 
                    ps.setString(1, actor.firstName)
                    ps.setString(2, actor.lastName)
                    ps.setLong(3, actor.id)
                }
                
                // 告诉 Spring 批处理的大小
                override fun getBatchSize() = actors.size 
            }
        )
    }
}

TIP

当你需要对批处理过程进行精细控制时,使用这种方式。比如需要在处理过程中进行复杂的数据转换或验证。

2. 对象列表批处理:最简洁的方式

这是最常用的批处理方式,Spring 会自动处理对象到 SQL 参数的映射。

使用命名参数(推荐)

kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
    
    private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
    
    fun batchUpdate(actors: List<Actor>): IntArray {
        return namedParameterJdbcTemplate.batchUpdate(
            "UPDATE t_actor SET first_name = :firstName, last_name = :lastName WHERE id = :id", 
            SqlParameterSourceUtils.createBatch(actors) 
        )
    }
}

IMPORTANT

SqlParameterSourceUtils.createBatch(actors) 会自动将对象列表转换为 Spring 可以理解的参数源数组。这要求你的 Actor 类有对应的 getter 方法。

使用位置参数

kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
    
    private val jdbcTemplate = JdbcTemplate(dataSource)
    
    fun batchUpdate(actors: List<Actor>): IntArray {
        val batch = actors.map { actor ->
            arrayOf(actor.firstName, actor.lastName, actor.id) 
        }
        
        return jdbcTemplate.batchUpdate(
            "UPDATE t_actor SET first_name = ?, last_name = ? WHERE id = ?",
            batch
        )
    }
}

3. 多批次处理:处理大数据量的利器

当你需要处理非常大的数据集时,将其分成多个小批次是明智的选择。这样可以避免内存溢出,同时保持良好的性能。

kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
    
    private val jdbcTemplate = JdbcTemplate(dataSource)
    
    fun batchUpdate(actors: Collection<Actor>): Array<IntArray> {
        return jdbcTemplate.batchUpdate(
            "UPDATE t_actor SET first_name = ?, last_name = ? WHERE id = ?",
            actors,
            100 // 每个批次100条记录
        ) { ps, actor ->
            ps.setString(1, actor.firstName)
            ps.setString(2, actor.lastName)
            ps.setLong(3, actor.id)
        }
    }
}

WARNING

返回值是 Array<IntArray>,外层数组表示批次数量,内层数组表示每个批次中每条记录的影响行数。

批处理的执行流程

让我们通过时序图来理解批处理的执行过程:

实际业务场景示例

场景:用户数据导入系统

假设你正在开发一个用户管理系统,需要从 Excel 文件批量导入用户数据:

完整的用户批量导入示例
kotlin
@Service
class UserImportService(
    private val dataSource: DataSource
) {
    private val jdbcTemplate = JdbcTemplate(dataSource)
    private val logger = LoggerFactory.getLogger(UserImportService::class.java)
    
    data class User(
        val username: String,
        val email: String,
        val firstName: String,
        val lastName: String,
        val department: String
    )
    
    fun importUsers(users: List<User>): ImportResult {
        val startTime = System.currentTimeMillis()
        
        try {
            // 使用多批次处理,每批500条
            val results = jdbcTemplate.batchUpdate(
                """
                INSERT INTO users (username, email, first_name, last_name, department, created_at) 
                VALUES (?, ?, ?, ?, ?, NOW())
                """.trimIndent(),
                users,
                500 // 批次大小
            ) { ps, user ->
                ps.setString(1, user.username)
                ps.setString(2, user.email)
                ps.setString(3, user.firstName)
                ps.setString(4, user.lastName)
                ps.setString(5, user.department)
            }
            
            val totalInserted = results.sumOf { it.sum() }
            val duration = System.currentTimeMillis() - startTime
            
            logger.info("成功导入 $totalInserted 个用户,耗时 ${duration}ms")
            
            return ImportResult(
                success = true,
                totalRecords = users.size,
                insertedRecords = totalInserted,
                duration = duration
            )
            
        } catch (e: Exception) {
            logger.error("用户导入失败", e)
            return ImportResult(
                success = false,
                totalRecords = users.size,
                insertedRecords = 0,
                duration = System.currentTimeMillis() - startTime,
                error = e.message
            )
        }
    }
    
    data class ImportResult(
        val success: Boolean,
        val totalRecords: Int,
        val insertedRecords: Int,
        val duration: Long,
        val error: String? = null
    )
}

场景:订单状态批量更新

电商系统中经常需要批量更新订单状态:

kotlin
@Service
class OrderBatchService(dataSource: DataSource) {
    
    private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
    
    data class OrderStatusUpdate(
        val orderId: Long,
        val newStatus: String,
        val updateReason: String
    )
    
    fun batchUpdateOrderStatus(updates: List<OrderStatusUpdate>): IntArray {
        return namedParameterJdbcTemplate.batchUpdate(
            """
            UPDATE orders 
            SET status = :newStatus, 
                update_reason = :updateReason, 
                updated_at = NOW() 
            WHERE order_id = :orderId
            """.trimIndent(),
            SqlParameterSourceUtils.createBatch(updates) 
        )
    }
}

性能优化技巧与注意事项

1. 选择合适的批次大小

TIP

批次大小的选择需要平衡内存使用和网络效率:

  • 小批次 (50-100):适合内存受限的环境
  • 中批次 (500-1000):大多数场景的最佳选择
  • 大批次 (5000+):适合高性能服务器和简单操作

2. 处理参数类型推断问题

kotlin
// 可能遇到的性能问题
fun problematicBatchUpdate(data: List<Map<String, Any?>>) {
    namedParameterJdbcTemplate.batchUpdate(
        "INSERT INTO table (col1, col2) VALUES (:col1, :col2)",
        data.map { MapSqlParameterSource(it) }.toTypedArray()
    ) // [!code warning] // 可能触发昂贵的参数类型推断
}

WARNING

当使用 Map 或包含 null 值的参数时,Spring 可能需要调用 ParameterMetaData.getParameterType,这在某些数据库驱动中非常昂贵。

解决方案:

kotlin
// 明确指定参数类型
fun optimizedBatchUpdate(data: List<Map<String, Any?>>) {
    val parameterSources = data.map { row ->
        MapSqlParameterSource().apply {
            addValue("col1", row["col1"], Types.VARCHAR) 
            addValue("col2", row["col2"], Types.INTEGER) 
        }
    }.toTypedArray()
    
    namedParameterJdbcTemplate.batchUpdate(
        "INSERT INTO table (col1, col2) VALUES (:col1, :col2)",
        parameterSources
    )
}

3. 错误处理和事务管理

kotlin
@Service
@Transactional
class SafeBatchService(dataSource: DataSource) {
    
    private val jdbcTemplate = JdbcTemplate(dataSource)
    
    fun safeBatchUpdate(actors: List<Actor>): BatchResult {
        return try {
            val results = jdbcTemplate.batchUpdate(
                "UPDATE t_actor SET first_name = ?, last_name = ? WHERE id = ?",
                actors,
                100
            ) { ps, actor ->
                ps.setString(1, actor.firstName)
                ps.setString(2, actor.lastName)
                ps.setLong(3, actor.id)
            }
            
            BatchResult(success = true, results = results)
            
        } catch (e: DataAccessException) {
            // 批处理失败时,整个事务会回滚
            logger.error("批处理失败", e) 
            BatchResult(success = false, error = e.message)
        }
    }
    
    data class BatchResult(
        val success: Boolean,
        val results: Array<IntArray>? = null,
        val error: String? = null
    )
}

最佳实践总结

批处理最佳实践

  1. 选择合适的批次大小:通常 100-1000 条记录为一个批次
  2. 使用命名参数:提高 SQL 的可读性和维护性
  3. 明确指定参数类型:避免性能问题
  4. 合理使用事务:确保数据一致性
  5. 监控和日志:记录批处理的执行情况
  6. 错误处理:优雅处理批处理失败的情况

通过掌握 Spring JDBC 的批处理操作,你可以显著提升应用程序处理大量数据的性能。记住,批处理不仅仅是一个技术特性,更是一种解决大数据量操作性能问题的设计思维! 🎯