Mongoose writeconcern實作筆記
2024-02-27
/分享在mongoose上加入writeconcern的實作心得
緣起
為提供客戶更好的使用體驗,最近公司將現有系統架構新增設備容錯備援機制-HA(High Availability)架構 ,在程式操作面上希望加入 mongoDB 的 writeconcern 機制避免多資料庫間資料不同步的狀況
MongoDB writeconcern 介紹
因為專案使用 node.js 寫入資料庫,因此此篇文章以熱門的 npm 套件 mongoose 實作 writeconcern 應用
Mongoose writeconcern 參數
writeconcern 主要有幾個參數可以設定
{
w: 'majority', // 指定確認寫入數量,設置為majority時會確認過半(1加投票成員數量的一半,無條件捨去)的資料庫寫入之後,才作回應
j: true, // boolean,設置為true會確認已寫入所使用的儲存引擎(EX:WiredTiger)的日誌當中,以供資料庫查找並資料恢復
wtimeout: 1000 // 單位為毫秒,設置writeconcern超時限制,超過會跳出writeconcern錯誤,避免無限期阻塞
}
Mongoose writeconcern 設置層級
在 mongoose 裡,可以在以下幾個地方設置 writeconcern,其優先順序依序為 Transaction level > Operator level > Schema level > global level
Transaction level
當使用 transaction 時,必須在 session.startTransaction()設置 writeconcern,並在 session.commitTransaction() catch 該錯誤
const session = await mongoDB.startSession()
session.startTransaction({
writeConcern: {
w: {
w: "majority",
j: true,
wtimeout: 1000,
},
},
})
// Do some operators...
await session.commitTransaction().catch(errorHandler)
await session.endSession()
Operator level
如果想針對對單一操作,可以在此設置
值得注意的是,
insertMany()無法套用 schema level 的設置,必須在此(Operator level) 設置
await Movies.insertMany(
[{ name: "Star Wars" }, { name: "The Empire Strikes Back" }],
{
writeConcern: {
w: "majority",
j: true,
wtimeout: 1000,
},
},
)
Schema level
可以在此設置各 collection 的 writeconcern,
const schema = new Schema(
{ name: String },
{
writeConcern: {
w: "majority",
j: true,
wtimeout: 1000,
},
},
)
實測當操作
updateOne()、save()、deleteMany()、deleteOne()等操作時皆會自動套用
Global level
可以在 mongosh 使用 db.adminCommand()設置 writeconcern,如果在其他層級沒有設定,則會預設使用此設置
db.adminCommand({
setDefaultRWConcern: 1,
defaultWriteConcern: {
w: "majority",
},
})
writeconcern 設置與錯誤處理實作
公司將 mongoDB 的副本機制設置為 Primary with a Secondary and an Arbiter (PSA)
當設置 w 為 majority 時,依規定必須要有超過 1 加投票成員數量的一半成功寫入,因此在此應用中,只要有一個資料庫節點掛掉導致延時,便會出現 writeconcern error 的狀況,但是該資料仍可以成功寫入 Primary 資料庫裡,因此為了使 user 操作不被中斷,我們必須接住 writeconcern error 狀況,讓程式能順利完成操作
writeconcern 設置
- 我們先在 schema 層設置 writeConcern
const schema = new Schema(
{ name: String },
{
writeConcern: {
w: "majority",
j: true,
wtimeout: 1000,
},
},
)
-
在 Operator 層設置 writeConcern,
insertMany()必須設置在 Operator 層- 部分操作為客戶端資料庫輪詢同步資料,無同步必要,考量回應速度,設置為 w: '1'
await Movies.insertMany(
[{ name: "Star Wars" }, { name: "The Empire Strikes Back" }],
{
writeConcern: {
w: "majority",
j: true,
wtimeout: 1000,
},
},
)
await Movies.updateOne(
{ _id },
{ name: "Star Wars" },
{
writeConcern: {
w: "1",
j: true,
wtimeout: 1000,
},
},
)
-
在所有 Operator 新增共用 function 做錯誤處理 * mongoDB 的 writeconcern error 代碼為 64(WriteConcernFailed),catch error 並確認錯誤代碼為 64 時 return 使程式可繼續運行,其他錯誤則 throw error,供外層錯誤處理
當 operator 是
insertMany()時,可以抓到 error.insertedDocs 並 return 供程式繼續使用
const errorHandler = error => {
const { result } = error
const writeError = result?.writeError
const writeErrors = result?.result?.writeErrors
const writeConcernError = result?.writeConcernError
const writeConcernErrors = result?.result?.writeConcernErrors
const hasOtherError = writeError && writeError.code !== 64
const hasOtherErrors = writeErrors?.some(err => err.code !== 64)
const isSingleWCE = writeConcernError?.code === 64
const isBulkWCE =
writeConcernErrors?.length > 0 &&
writeConcernErrors.every(err => err.code === 64)
// 若僅為 WriteConcern Error (64) 且無其他錯誤,視為成功
if (!hasOtherError && !hasOtherErrors && (isSingleWCE || isBulkWCE)) {
return error.insertedDocs
}
// 若需拋出錯誤,先過濾掉 code 64 的警告,避免混淆
if (writeConcernErrors) {
error.result.result.writeConcernErrors = writeConcernErrors.filter(
err => err.code !== 64,
)
}
if (writeConcernError?.code === 64) {
error.result.writeConcernError = {}
}
throw error
}
// catch the error
await Movies.updateOne(
{ _id },
{ name: "Star Wars" },
{
writeConcern: {
w: "1",
j: true,
wtimeout: 1000,
},
},
).catch(errorHandler)
// other operators...