在 Grails 批量處理業務中,單個 Action 處理大量數據記錄會導致 Hibernate Session 一級緩存累積過大,最終觸發 OOM (OutOfMemoryError)。即使使用 MongoDB Plugin 底層連接操作,同樣面臨 Session 緩存無法及時釋放的問題。
將大批量邏輯拆分為多個小批次,每個批次通過 內部 HTTP 調用 獨立 Action 執行,每個 Action 擁有獨立 Session,執行完畢自動釋放。
def updateData() {
println "classId:${params.classId} usersId:${params.usersId}"
// 單次處理邏輯:保存少量記錄、更新狀態等
// Session 自動提交並關閉
render "ok"
}
class DataService {
/**
* 獲取協議+主機+端口基礎 URL
* 例:https://localhost:9090/blog/bkoff/test → https://localhost:9090
*/
private String getHostUrl(request) {
String requestUrl = request.requestURL.toString()
String separator = "://"
int index = requestUrl.indexOf(separator)
index = requestUrl.indexOf("/", index + separator.length())
return requestUrl.substring(0, index)
}
/**
* 遠程調用指定 Action
* @param request 當前請求(用於構建基礎 URL)
* @param actionUrl createLink 生成的相對路徑
* @return 響應文本
*/
def String remoteCall(request, actionUrl) {
def hostUrl = getHostUrl(request)
def url = new URL(hostUrl + actionUrl)
return url.text // 同步阻塞獲取響應
}
}
class BatchController {
def dataService
def processLargeDataset() {
def classId = "test1"
def userIds = (1..10000).collect { "user_${it}" } // 模擬 1 萬條數據
// 分批處理:每批 100 條
userIds.collate(100).eachWithIndex { batch, index ->
def actionUrl = g.createLink(
controller: 'batch',
action: 'updateData',
params: [classId: classId, usersId: batch.join(',')]
)
def result = dataService.remoteCall(request, actionUrl)
println "Batch ${index + 1} completed: ${result}"
// 可選:避免過載,短暫休眠
Thread.sleep(100)
}
render "All batches processed"
}
}
// 在 Controller/GSP/Service 中均可使用
g.createLink(action: 'updateData', params: [classId: "test1", usersId: 'usersIdxxx'])
// 生成:/batch/updateData?classId=test1&usersId=usersIdxxx
包含協議、域名、端口、路徑,不含查詢參數。通過字符串截取獲取基礎 URL。
Groovy JDK 增強方法,等同於:
// Java 等價代碼
HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection();
conn.setRequestMethod("GET");
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
return in.lines().collect(Collectors.joining("\n"));
}
// 使用 GPars 或 CompletableFuture 並行調用
import groovyx.gpars.GParsPool
GParsPool.withPool(10) {
userIds.collate(100).collectParallel { batch ->
def actionUrl = g.createLink(action: 'updateData', params: [usersId: batch.join(',')])
dataService.remoteCallAsync(request, actionUrl) // 返回 Future
}.each { future ->
println "Batch result: ${future.get()}"
}
}
def String remoteCallWithRetry(request, actionUrl, int maxRetries = 3) {
def hostUrl = getHostUrl(request)
def url = new URL(hostUrl + actionUrl)
(1..maxRetries).each { attempt ->
try {
return url.text
} catch (Exception e) {
if (attempt == maxRetries) throw e
println "Attempt ${attempt} failed, retrying... ${e.message}"
Thread.sleep(1000 * attempt) // 指數退避
}
}
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 遠程 Action 調用 | Session 完全隔離、代碼改動小、邏輯清晰 | 網絡開銷、同步阻塞、需處理分佈式事務 | 中等批量、現有 Action 復用 |
| 手動 Session 管理 | 無網絡開銷、單進程、性能高 | 需深度理解 Hibernate、代碼侵入性強 | 大批量、高性能要求 |
| 無狀態 Session | 無緩存、內存恆定 | 失去一級緩存優勢、不支援級聯操作 | 純批量寫入、無關聯實體 |
| 分頁游標處理 | 標準 JPA/Hibernate 模式 | 需手動 clear/flush、代碼較複雜 | 讀密集型批量處理 |
遠程調用 Action 是解決 Grails 大批量處理 OOM 的低侵入性、快速見效方案。通過 HTTP 內部調用將大事務拆分為多個小事務,利用容器 Session 生命週期管理自動釋放記憶體。生產環境建議結合異步並行、重試機制、監控告警進一步增強穩健性。