背景
使用mongo查询过期数据记录,由于业务量增加导致过期数据激增,之前运行正常的任务报游标超时错误。
2021-08-16 01:12:39.119 ERROR scheduling-1 [org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler:96]-Unexpected error occurred in scheduled task.
com.mongodb.MongoCursorNotFoundException: Query failed with error code -5 and error message 'Cursor 947173600818248061 not found on server 192.168.109.138:20000' on server 192.168.109.138:20000
at com.mongodb.operation.QueryHelper.translateCommandException(QueryHelper.java:27) ~[mongodb-driver-core-3.8.2.jar!/:?]
at com.mongodb.operation.QueryBatchCursor.getMore(QueryBatchCursor.java:229) ~[mongodb-driver-core-3.8.2.jar!/:?]
at com.mongodb.operation.QueryBatchCursor.hasNext(QueryBatchCursor.java:115) ~[mongodb-driver-core-3.8.2.jar!/:?]
at com.mongodb.client.internal.MongoBatchCursorAdapter.hasNext(MongoBatchCursorAdapter.java:54) ~[mongodb-driver-3.8.2.jar!/:?]
原因分析
MongoCursor<Document> mongoCursor = mongoClient.getCollection(fileRecordCollection).find(expireFileQuery).iterator()
用db.collection.find()的时候,它返回的不是所有的数据,而是一个cursor。
游标的默认行为是:第一次向数据库查询101个文档,或1MB的文档,取决于哪个条件先满足;之后每次cursor中的文档用尽后,查询4MB的文档。==另外,find()的默认行为是返回一个10分钟无操作后超时的cursor。==如果一个batch的文档十分钟内没有处理完,过后再处理完了,再用同一个cursor id向服务器去下一个batch,这时候cursor id已经过期了,这也就能解释为啥得到cursor id无效的错误了。
解决方案
批量读取固定数量的记录,保证10分钟内完成记录的处理(推荐)
MongoCursor<Document> mongoCursor = mongoClient.getCollection(fileRecordCollection).find(expireFileQuery)
.batchSize(SysConstant.TENTHOUSAND).iterator();
这种方案比较稳妥,但是会增加数据库的连接次数,从而增加I/O耗时。如果连接创建次数在接受范围内,是完全可以的。
设置游标永不超时(不推荐)
MongoCursor<Document> mongoCursor = mongoClient.getCollection(fileRecordCollection).find(expireFileQuery).noCursorTimeout(true).iterator();
让游标永不超时,然而这个操作非常危险,因为如果循环参数异常,甚至使用mongo的应用服务器断电或断网,都会导致MongoDB服务器资源永远无法被释放。这个游标就再也无法关闭了!除非重启MongoDB,否则这些游标会一直留在MongoDB上,占用资源。
可以使用try catch finally去做。
/ * try catch:自己处理异常
* try {
*可能出现异常的代码
*} catch(异常类名A e){
*如果出现了异常类A类型的异常,那么执行该代码
*} ...(catch可以有多个)
* finally {
*最终肯定必须要执行的代码(例如释放资源的代码)
*}
虽然这种方法会对代码美观性造成一定的破坏但是无疑解决了我们的问题。