n8n 兩個工作流程寫入同一資料列並均顯示成功的競態條件
The Two Workflows That Wrote to the Same Row and Both Said Success: Race Conditions in n8n
作者在 n8n 生產環境中遇到兩個工作流程更新同一客戶記錄,兩次執行均顯示成功,聯絡資料更新卻被整筆記錄寫回覆蓋。文章建議把版本檢查納入同一更新操作,版本不符時重新讀取後再重試,並限制每個工作流程只寫入自己負責的欄位。
This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is a failure I actually watched happen, not a hypothetical.
Two workflows wrote to the same customer record on the same night. One updated the plan from a billing webhook. The other updated the contact details from a form submission. Both executions said success. The record that came out was neither update: the plan change was there, the contact change was gone, and nothing in either log suggested anything had been lost.
The mistake was assuming that two workflows touching the same row at different times is the same as two workflows touching it in sequence. It is not. Each workflow read the record, changed its own field, and wrote the whole record back. The second write overwrote the first, not because either workflow was wrong about its own field, but because neither workflow knew the other had been there in between. A read-modify-write cycle with no protection is a race, even when the two writers never run at the same second — they only have to overlap between the read and the write.
The fix is to stop writing whole records from workflows. A workflow should send a change, not a replacement: update this field, to this value, based on this version. If the version it read is no longer current, the update must fail instead of silently overwriting. That failure is not a bug in the workflow. It is the system catching a collision that would otherwise have passed as success.
Two details make this work, and both are easy to get wrong.
First, the version check has to live in the update itself, not in a node before it. "Read the version, check it is still the same, then write" is again two steps with a gap in between, and another workflow can slip into that gap. The check must be part of the same statement that writes: update where the version matches, and report how many rows changed. Zero rows changed means someone else moved first, and the workflow must re-read, re-apply its own change on top of the new state, and try again — or hand the conflict to a human if the two changes cannot both be true.
Second, retries must not replay a stale change. A workflow that failed on the version check and simply reruns from its first node will re-read, which is fine. A workflow that retries only the write step, with the old values still in memory, will fail the same way forever, or worse, succeed after a third workflow has moved the record again and land on top of work it never saw. The retry has to start from a fresh read, every time. Stale data in a retry is how a collision that was caught once gets lost a second time.
I also keep a rule for which fields a workflow is allowed to touch. Each workflow owns its fields, and it writes only those. The billing workflow never writes contact details, even if it read them. The form workflow never writes the plan, even if the form mentioned it. Narrow writes shrink the surface where two workflows can collide, and when a collision does happen, the version check tells me exactly which two changes fought over which field, instead of leaving me with a record that is quietly half right.
The rule I keep coming back to: a workflow that writes a whole record is claiming it knows the record's full state. It does not. It knows one field, at one moment, and the honest write says so.
Free workflows from this series (MIT): https://github.com/zhp910318/n8n-ai-automation-lab
The production templates in my store: https://payhip.com/haopengailab
來源:DEV Community · dev.to