Database Synchronization: איך שני בסיסי נתונים נשארים תואמים בלי לאבד נתונים

מאת צוות מדיה דיל · 07.08.2026 · Data Engineering · 13 דק׳

מדריך מעמיק לסנכרון דו-כיווני בין בסיסי נתונים: אסטרטגיות conflict resolution, vector clocks, CRDTs, ולמה sync engines הם אחד האתגרים הקשים באמת בהנדסת תוכנה.

אפליקציית mobile עם offline-first support, מערכת enterprise עם כמה מוקדי כתיבה, או שילוב בין SaaS חיצוני ל-DB פנימי — כולם מתמודדים עם אותה בעיה יסודית: איך שני (או יותר) בסיסי נתונים עצמאיים נשארים תואמים כשלכל אחד יש אפשרות כתיבה משלו. זו לא בעיה של "להעתיק נתונים" פשוט — זו בעיה של database synchronization דו-כיוונית, שנחשבת בצדק לאחת הבעיות הקשות ביותר בהנדסת מערכות מבוזרות, כי היא דורשת התמודדות ישירה עם קונפליקטים אמיתיים, לא רק propagation חד-כיווני.

למה sync דו-כיווני שונה מ-replication

replication קלאסי (שתואר במאמר קודם) מניח מקור אמת יחיד — leader. Database synchronization מניח שיש כמה מקורות כתיבה לגיטימיים, שכל אחד יכול לשנות את אותם נתונים באופן עצמאי, ולפעמים אפילו כשאין חיבור רשת ביניהם (offline-first). זה הופך את בעיית ה-conflict resolution מ-edge case נדיר לתרחיש מרכזי שהמערכת חייבת לטפל בו כחלק מהתכנון הבסיסי, לא כטלאי בדיעבד.

Last-Write-Wins: הפתרון הפשוט והמסוכן

הגישה הנפוצה ביותר, בגלל פשטותה, היא Last-Write-Wins (LWW): לכל רשומה יש timestamp, וכשמתגלה קונפליקט, הגרסה עם ה-timestamp המאוחר יותר מנצחת, והשנייה נמחקת בשקט. הבעיה: זה מניח שעונים לשעונים מסונכרנים בין כל הצמתים — הנחה שכמעט אף פעם לא נכונה לחלוטין ברשת מבוזרת אמיתית (clock skew). גרוע מכך, LWW פשוט מוחק שינויים לגיטימיים בלי לשאול אף אחד — משתמש שערך מסמך ב-offline במשך שעה יכול לגלות שהעריכה שלו נעלמה כי מישהו אחר כתב, בטעות שעון, "מאוחר יותר". LWW מתאים רק כשהמידע לא קריטי, או כשיש הסכמה עסקית ברורה שזה קביל.

Tombstones: כשמחיקה עצמה צריכה להסתנכרן

נקודה שקל לפספס בעיצוב ראשוני של מערכת sync: מחיקת רשומה היא בעצמה שינוי שצריך להתפשט, לא רק "העלמות" מקומית. אם client A מוחק רשומה בזמן שהוא offline, וclient B ממשיך לערוך אותה בו-זמנית, המערכת צריכה לדעת להבדיל בין "הרשומה מעולם לא הייתה קיימת" (ואז אין מה לסנכרן) לבין "הרשומה נמחקה בכוונה ויש להפיץ את המחיקה". הפתרון המקובל הוא tombstones — רשומת placeholder שמסמנת מפורשות שפריט נמחק, במקום למחוק אותו פיזית מיד. ה-tombstone מסונכרן כמו כל שינוי אחר, וכל client שרואה אותו יודע להסיר את הרשומה המקומית שלו. הבעיה המעשית: tombstones לא יכולים להישאר לנצח (אחרת מסד הנתונים מתמלא ב"קברים"), אז צריך מדיניות garbage collection שמנקה אותם רק אחרי שכל ה-clients הידועים אישרו שקיבלו את המחיקה — תזמון עדין שדורש תיאום נוסף בפרוטוקול הסנכרון עצמו.

Vector Clocks: לדעת מה באמת קרה לפני מה

הבעיה הבסיסית עם timestamps רגילים היא שהם לא מייצגים באמת סדר סיבתי (causality) — רק זמן שעון פיזי, שיכול להטעות. Vector clocks פותרים את זה: כל node שומר מונה עבור כל node אחר במערכת, ומעדכן אותו בכל אירוע. כשמשווים שתי גרסאות, vector clock מאפשר לקבוע בוודאות אם גרסה אחת "קדמה סיבתית" לשנייה (concurrent updates אמיתיים) או שהיא פשוט התקדמות ליניארית של אותה שרשרת שינויים. כשה-vector clocks מראים שהעדכונים היו concurrent (אף אחד לא "ידע" על השני) — זה סימן אמיתי לקונפליקט אמיתי שדורש טיפול, בניגוד ל-LWW שמניח קונפליקט בכל מקרה של timestamps קרובים בלי קשר לסיבתיות.

CRDTs: קונפליקטים שנפתרים מתמטית

Conflict-free Replicated Data Types הם מבני נתונים מיוחדים שמעוצבים כך שכל שתי גרסאות, בלי קשר לסדר שבו הן מגיעות, תמיד מתכנסות לאותה תוצאה סופית — ללא צורך בלוגיקת merge ידנית. הדוגמה הפשוטה ביותר: G-Counter (grow-only counter) — כל node שומר מונה נפרד לעצמו, והערך הכולל הוא סכום כל המונים; שני nodes שמגדילים במקביל פשוט מסתכמים בלי קונפליקט אפשרי מעצם המבנה המתמטי. מבנים מתקדמים יותר כוללים OR-Set (set שתומך בהוספה והסרה בו-זמנית בלי אובדן מידע) ו-RGA (Replicated Growable Array, בסיס לעריכת טקסט collaborative כמו ב-Google Docs). המחיר: CRDTs דורשים לחשוב מחדש על מבני הנתונים מהיסוד, ולא כל מבנה עסקי ניתן לביטוי כ-CRDT בקלות — במיוחד invariants עסקיים מורכבים (כמו "יתרת חשבון לא יכולה להיות שלילית") לא תמיד ניתנים לביטוי בצורה שמתכנסת אוטומטית.

Operational Transformation: הגישה החלופית מעולם העריכה השיתופית

לפני ש-CRDTs תפסו תאוצה, כלי collaborative editing (Google Docs המוקדם, Etherpad) השתמשו ב-Operational Transformation (OT): במקום לעצב מבני נתונים חסיני-קונפליקט מראש, OT "מתרגם" כל operation שמגיעה כדי שהיא תתאים למצב הנוכחי של המסמך, גם אם operations אחרות התרחשו בינתיים. OT מורכב יותר להטמעה נכונה (יש הוכחות מתמטיות ידועות-לשמצה קשות לניסוח נכון), אבל נותן שליטה עדינה יותר על סמנטיקת המיזוג במקרים מסוימים. ברוב הפרויקטים החדשים היום, CRDTs הפכו לבחירה המועדפת בזכות הפשטות היחסית של ההוכחה שהן נכונות, אבל OT עדיין נפוץ בקוד legacy ובמערכות עריכת טקסט ותיקות.

Three-way merge: הטכניקה שמאחורי Git שעובדת גם על נתונים

טכניקה נוספת ששווה להכיר, שמקורה בניהול קוד מקור אבל רלוונטית ישירות לסנכרון נתונים, היא three-way merge. במקום להשוות רק בין שתי גרסאות מתנגשות (ours ו-theirs), הטכניקה מסתכלת גם על ה-base — הגרסה המשותפת האחרונה לפני שהשתיים התפצלו. זה מאפשר להבחין בין שינוי אמיתי (עמודה שהשתנתה בשני הצדדים לערכים שונים — קונפליקט אמיתי) לבין מצב שבו רק צד אחד שינה שדה מסוים (לא קונפליקט בכלל — פשוט לוקחים את הערך שהשתנה). ברוב מקרי הסנכרון בעולם האמיתי, רוב "הקונפליקטים" שנראים כאלה במבט ראשון הם בעצם לא-קונפליקטים אמיתיים כשמסתכלים דרך הפריזמה של three-way merge — שני משתמשים ערכו שדות שונים לגמרי באותו מסמך, ואין שום סיבה אמיתית שהמיזוג יידרוש התערבות ידנית.

Sync engines מודרניים: מה קורה מתחת למכסה המנוע

כלים כמו PouchDB/CouchDB, Automerge, Yjs, ו-Replicache בנו שכבות sync כלליות שמפשטות משמעותית את הבעיה עבור מפתחי אפליקציות. הרעיון המשותף: שמירת log מקומי של שינויים (בדומה ל-event sourcing) בכל client, sync א-סינכרוני של ה-logs האלה כשיש חיבור רשת, ומיזוג אוטומטי (לרוב מבוסס CRDT) של שינויים מ-clients שונים. זה מאפשר offline-first אמיתי — משתמש יכול לעבוד לגמרי בלי רשת, וכשהחיבור חוזר, השינויים מתמזגים אוטומטית בלי צורך בלוגיקת conflict resolution ידנית בכל מקום באפליקציה. המורכבות עוברת מהאפליקציה לשכבת ה-sync עצמה, מה שהופך אותה לרכיב תשתית קריטי שדורש הבנה עמוקה לפני שסומכים עליו בפרודקשן.

Schema drift: כשלא רק הנתונים משתנים אלא גם המבנה

עד כה הדיון התמקד בסנכרון ערכי נתונים, אבל בפרודקשן אמיתי, לעיתים קרובות גם הסכמה עצמה משתנה בכל צד באופן עצמאי — במיוחד כשמדובר בסנכרון בין שתי אפליקציות נפרדות עם מחזורי deployment שונים. אם צד A מוסיף שדה חדש לרשומה בעוד צד B עדיין לא מכיר אותו, מה קורה בסנכרון הבא? הפתרון הנפוץ הוא schema versioning מפורש בתוך פרוטוקול הסנכרון עצמו — כל רשומה מסונכרנת נושאת גרסת סכמה, וצד שמקבל גרסה שהוא לא מכיר שומר את השדות הלא-מוכרים "כמו שהם" (preserve unknown fields) במקום לזרוק אותם, כך שכשהוא עצמו מתעדכן לגרסה החדשה, המידע לא אבד. התעלמות מהנקודה הזו היא מקור נפוץ לאובדן נתונים שקט בסנכרון בין מערכות multi-tenant עם קצב שדרוג שונה בכל צד.

טעויות נפוצות בפרודקשן

הטעות הראשונה: לבחור LWW כברירת מחדל בלי לשקול את המחיר של אובדן שינויים בשקט — לפחות לתעד ולנטר כמה קונפליקטים בפועל נפתרים בדרך הזו, ולבדוק שזה מקובל עסקית. הטעות השנייה: להתעלם מ-clock skew בין שרתים כשמשתמשים ב-timestamps — NTP sync לא תמיד מדויק מספיק, וסטייה של כמה שניות יכולה להפוך את סדר האירועים. הטעות השלישית: לבנות sync engine מותאם אישית מאפס במקום להשתמש בכלים בשלים קיימים — הבעיה נחקרה עמוקות באקדמיה ובתעשייה, וכמעט תמיד יש נטייה לפספס edge cases שהכלים המוכחים כבר טיפלו בהם. הטעות הרביעית: לא לתכנן conflict resolution UI — גם עם CRDTs, לפעמים תוצאת המיזוג האוטומטי לא הגיונית עסקית, וצריך אפשרות למשתמש לראות ולתקן ידנית.

Bandwidth ו-batching: שיקולים מעשיים בסנכרון מובייל

בסנכרון עם מכשירי mobile, לא רק latency אלא גם bandwidth ועלות סוללה הם אילוצים אמיתיים. שליחת כל שינוי בנפרד ברגע שהוא קורה יכולה להיות בזבזנית — במיוחד ברשת סלולרית לא יציבה. הפתרון המקובל הוא batching ו-debouncing: איסוף שינויים מקומיים למשך חלון זמן קצר (שניות עד דקות, תלוי בדחיפות) ושליחתם כ-batch יחיד כשיש הזדמנות רשת טובה, יחד עם דחיסה (compression) של ה-payload. בנוסף, פרוטוקולי sync טובים תומכים ב-delta sync — שליחת רק ההבדל בין הגרסה האחרונה הידועה לבין המצב הנוכחי, ולא את הרשומה המלאה בכל פעם, מה שמקטין דרמטית את נפח הנתונים המועבר במיוחד עבור מסמכים גדולים עם שינויים קטנים ותכופים.

מתי כן ומתי לא

Database synchronization מלא שווה את המורכבות כשיש דרישה אמיתית ל-offline-first, או כשיש כמה מקורות כתיבה עצמאיים לגיטימיים שלא ניתן לאחד למקור יחיד. הוא לא מוצדק כשאפשר פשוט להגדיר leader ברור ולוותר על כתיבה מרובת-מקורות — במקרים רבים, הפתרון הנכון הוא לא sync מורכב אלא ארכיטקטורה שמונעת את הצורך בו מלכתחילה.

Testing: איך בכלל בודקים sync engine

בדיקת מערכת סנכרון דורשת גישה שונה מבדיקת קוד רגיל, כי הבאגים המסוכנים ביותר מתגלים רק תחת תרחישי concurrency נדירים. הכלי המרכזי הוא property-based testing: במקום לכתוב תרחישים ספציפיים, מגדירים invariant שצריך להתקיים תמיד (למשל, "שני clients שסנכרנו עם אותם שינויים, בכל סדר, חייבים להגיע לאותו מצב סופי") ומריצים אלפי קומבינציות אקראיות של סדר פעולות ותזמון רשת כדי לחפש הפרות. גם simulated network partitioning — ניתוק מלאכותי בין clients למשך זמן משתנה ואז חיבור מחדש — הוא חלק בלתי נפרד מ-test suite רציני, כי בדיוק שם רוב הבאגים האמיתיים מתגלים בפרודקשן.

סיכום

סנכרון בסיסי נתונים דו-כיווני הוא אחד האתגרים העמוקים ביותר במערכות מבוזרות, כי הוא דורש התמודדות ישירה עם קונפליקטים אמיתיים ולא רק העתקה. הבחירה בין LWW הפשוט, CRDTs המתמטיים, או sync engine מוכן, צריכה להיגזר מהבנה כנה של כמה קונפליקטים באמת יקרו, ומה המחיר העסקי של פתרון לא-אידיאלי שלהם.

תגיות: database synchronization · conflict resolution · vector clocks · CRDT · Operational Transformation · offline-first · sync engine

← חזרה לבלוג · צור קשר