תארו לעצמכם: שירות שליחויות משתמש באפליקציה ניידת כדי לקבל הזמנות. מאות הזמנות אובדות בכניסה למנהרה — הסנכרון לא מצליח לעמוד בקצב. לאחר שחזור הרשת, הנתונים מתפצלים, ולקוחות לא מקבלים חבילות. מצבים כאלה הם המומחיות שלנו. אנו מתכננים ארכיטקטורת סנכרון לא מקוון לאפליקציות ניידות שמבטיחה שלמות נתונים גם עם קישוריות לא יציבה. לצוות שלנו יש ניסיון של למעלה מ-5 שנים והוא השלים יותר מ-30 פרויקטי סנכרון עבור פינטק, מסחר אלקטרוני ולוגיסטיקה.
ללא סנכרון תקין, שינויים לא מקוונים אובדים, נוצרים קונפליקטים וחוויית המשתמש נפגעת. הגישה שלנו היא סנכרון דלתא דו-כיווני עם תור פעולות וטיפול בקונפליקטים. מקרה אמיתי: עבור אפליקציית מסחר אלקטרוני עם 500,000 מוצרים ו-10,000 הזמנות ביום, יישמנו סנכרון דלתא. קונפליקטים התרחשו ב-2% מההזמנות — נפתרו באמצעות LWW. זמן הסנכרון הממוצע ירד מ-30 ל-2 שניות (פי 15 מהר יותר), והתעבורה ירדה ב-90% (הפחתה של פי 10). סנכרון דלתא עולה על סנכרון מלא פי 10 במהירות ופי 50 בתעבורה.
כיצד ליישם סנכרון דלתא דו-כיווני?
סנכרון דלתא ממזער ביעילות את התעבורה. בניגוד לסנכרון מלא, הלקוח טוען רק שינויים מאז הסנכרון האחרון. זה מפחית את עומס השרת ב-90% ומאיץ את הסנכרון פי 5–10.
| שיטה | תעבורה | זמן | עומס שרת | מתאים ל |
|---|---|---|---|---|
| סנכרון מלא | גבוהה | איטי | גבוה | נתונים קטנים (<100 רשומות) |
| סנכרון דלתא | נמוכה | מהיר | נמוך | שינויים תכופים, רשומות רבות |
| סנכרון מצטבר | בינונית | בינוני | בינוני | נתונים עם מזהים מונוטוניים |
אנו משתמשים בסנכרון דלתא כמנגנון העיקרי. הלקוח שולח lastSyncTimestamp, והשרת מחזיר רק רשומות ששונו או נמחקו.
קוד SyncManager
data class SyncRequest(
val lastSyncTimestamp: Long,
val clientId: String
)
data class SyncResponse(
val serverTimestamp: Long, // время ответа сервера
val updated: List<ProductDto>, // изменённые или новые
val deletedIds: List<String> // удалённые на сервере
)בצד הלקוח, אנו מאחסנים data class SyncRequest( val lastSyncTimestamp: Long, val clientId: String ) data class SyncResponse( val serverTimestamp: Long, // время ответа сервера val updated: List<ProductDto>, // изменённые или новые val deletedIds: List<String> // удалённые на сервере ) ב-MMKV או SharedPreferences. הסנכרון הבא משתמש בו כמסנן.
מדוע חותמת הזמן של השרת חשובה?
הזמן חייב להיות מבוסס על השרת. אם הלקוח משתמש בזמן שלו, סטיית שעון גורמת לפספוסים או כפילויות. השרת מחזיר את חותמת הזמן שלו בתגובה — הלקוח שומר בדיוק אותה. זה מונע בעיות של אזורי זמן ואי-דיוק בשעון המכשיר.
ארכיטקטורת SyncManager
SyncManager הוא הרכיב המרכזי. הוא מתאם שליחת פעולות שנצברו וקבלת דלתאות. הקוד שלהלן הוא בסיס ל-iOS ואנדרואיד, עם התאמות ספציפיות לפלטפורמה.
קוד SyncManager (kotlin)
class SyncManager(
private val api: SyncApi,
private val dao: ProductDao,
private val pendingOpsDao: PendingOperationDao,
private val prefs: SyncPreferences
) {
suspend fun sync(): SyncResult {
// 1. Отправляем накопленные offline-операции
val pending = pendingOpsDao.getAll()
if (pending.isNotEmpty()) {
try {
val uploadResult = api.uploadOperations(pending.map { it.toRequest() })
pendingOpsDao.deleteByIds(uploadResult.processedIds)
} catch (e: NetworkException) {
return SyncResult.NetworkError
}
}
// 2. Скачиваем изменения с сервера
return try {
val response = api.sync(
SyncRequest(
lastSyncTimestamp = prefs.lastSyncTimestamp,
clientId = prefs.clientId
)
)
dao.applyDelta(
updated = response.updated.map { it.toEntity() },
deletedIds = response.deletedIds
)
prefs.lastSyncTimestamp = response.serverTimestamp
SyncResult.Success(
updatedCount = response.updated.size,
deletedCount = response.deletedIds.size
)
} catch (e: Exception) {
SyncResult.Error(e)
}
}
}lastSuccessfulSyncTimestamp מתבצע בעסקה — אטומית. או הכל או כלום:
@Transaction
suspend fun applyDelta(updated: List<ProductEntity>, deletedIds: List<String>) {
upsertAll(updated)
softDeleteByIds(deletedIds, System.currentTimeMillis())
}מחיקה רכה היא חובה: אנחנו לא מוחקים פיזית, אנו מגדירים דגל class SyncManager( private val api: SyncApi, private val dao: ProductDao, private val pendingOpsDao: PendingOperationDao, private val prefs: SyncPreferences ) { suspend fun sync(): SyncResult { // 1. Отправляем накопленные offline-операции val pending = pendingOpsDao.getAll() if (pending.isNotEmpty()) { try { val uploadResult = api.uploadOperations(pending.map { it.toRequest() }) pendingOpsDao.deleteByIds(uploadResult.processedIds) } catch (e: NetworkException) { return SyncResult.NetworkError } } // 2. Скачиваем изменения с сервера return try { val response = api.sync( SyncRequest( lastSyncTimestamp = prefs.lastSyncTimestamp, clientId = prefs.clientId ) ) dao.applyDelta( updated = response.updated.map { it.toEntity() }, deletedIds = response.deletedIds ) prefs.lastSyncTimestamp = response.serverTimestamp SyncResult.Success( updatedCount = response.updated.size, deletedCount = response.deletedIds.size ) } catch (e: Exception) { SyncResult.Error(e) } } } ושומרים חותמת זמן. אחרת, סנכרון הדלתא הבא "ישכח" את המחיקה הזו שוב.
טריגרים לסנכרון
הסנכרון מופעל במספר תרחישים:
class SyncScheduler(
private val workManager: WorkManager,
private val syncManager: SyncManager,
private val networkMonitor: NetworkMonitor
) {
init {
val periodicSync = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
.build()
workManager.enqueueUniquePeriodicWork(
"periodic-sync",
ExistingPeriodicWorkPolicy.KEEP,
periodicSync
)
}
fun observeNetworkAndSync() {
networkMonitor.isOnline
.filter { it }
.distinctUntilChanged()
.onEach { triggerImmediateSync() }
.launchIn(applicationScope)
}
fun onAppForeground() {
val lastSync = prefs.lastSyncTimestamp
val tooOld = System.currentTimeMillis() - lastSync > 5 * 60 * 1000L
if (tooOld) triggerImmediateSync()
}
}ב-iOS, המקבילה ל-WorkManager היא applyDelta ו-@Transaction suspend fun applyDelta(updated: List<ProductEntity>, deletedIds: List<String>) { upsertAll(updated) softDeleteByIds(deletedIds, System.currentTimeMillis()) } (ראו תיעוד Apple). לפי מדריך ביצוע רקע של Apple, משימות רקע ב-iOS מוגבלות ל-30 שניות. הפתרון שלנו מכבד את המגבלות הללו ומשתמש בטריגרים אופטימליים.
סנכרון תמונות וקבצים
נתונים בינאריים מטופלים בנפרד ממטא-דאטה. אנו מסנכרנים רשימת קבצים (שמות, כתובות URL, hashes) ומורידים קבצים באמצעות בקשות בודדות עם עדיפות:
class MediaSyncManager {
suspend fun syncMedia(mediaList: List<MediaMeta>) {
val toDownload = mediaList.filter { meta ->
!fileCache.exists(meta.localPath) || fileCache.getHash(meta.localPath) != meta.serverHash
}
toDownload.chunked(4).forEach { batch ->
batch.map { meta -> async { downloadFile(meta) } }.awaitAll()
}
}
}נתחים של 4 — אל תעמיסו על החיבור; במקרה של ניתוק, לכל היותר 4 קבצים מהאצווה הנוכחית אובדים.
השוואת אסטרטגיות לפתרון קונפליקטים
| אסטרטגיה | עיקרון | ביצועים | מורכבות |
|---|---|---|---|
| LWW (הכותב האחרון מנצח) | השינוי האחרון לפי זמן השרת מנצח | מהיר מאוד | נמוכה |
| CRDT | מיזוג אוטומטי ללא קונפליקטים | בינוני | גבוהה |
| מיזוג מותאם אישית | פתרון ידני דרך ממשק משתמש | איטי | גבוהה |
עבור רוב התרחישים, LWW מספיק. CRDT מוצדק לעורכים שיתופיים או נתונים פיננסיים שבהם אסור לאבד שום שינוי (ראו ויקיפדיה: סוג נתונים משוכפל ללא קונפליקטים).
סטטוס סנכרון בממשק המשתמש
המשתמש צריך לראות את טריות הנתונים. מינימום: חותמת זמן של הסנכרון האחרון. עדיף: אייקון סטטוס (מסונכרן / מסנכרן / שגיאת סנכרון) ליד נתונים שעלולים להיות מיושנים.
בשגיאת סנכרון — אל תחסמו את ממשק המשתמש. הציגו אזהרה, אפשרו עבודה עם נתונים מקומיים והציעו ניסיון חוזר.
מה כלול
- ביקורת ארכיטקטורת הנתונים הנוכחית והכנת מפרט
- עיצוב סכמת סנכרון (ישויות, מזהים, קונפליקטים)
- יישום SyncManager, תור פעולות ואינטגרציית API
- הגדרת משימות רקע (WorkManager / BGAppRefreshTask)
- פיתוח מטפל קונפליקטים (LWW או CRDT)
- כיסוי קוד בבדיקות יחידה ותרחישי בדיקות אינטגרציה
- תיעוד API ומדריך פריסה
- תמיכה ל-30 יום לאחר ההשקה
תהליך
- ניתוח — לימוד מודל הנתונים, עומס, דרישות עקביות
- עיצוב — בחירת אסטרטגיית קונפליקטים, הגדרת שדות סנכרון
- יישום — כתיבת קוד, אינטגרציה עם הקצה האחורי שלכם (REST, GraphQL, Firebase)
- בדיקות — הדמיית תרחישים לא מקוונים, בדיקות עומס, אימות מקרי קצה
- פריסה — הגדרת CI/CD, ניטור, רישום שגיאות סנכרון
לוח זמנים ועלות
יישום מלא של סנכרון דלתא דו-כיווני עם תור פעולות וטיפול בקונפליקטים: 4 עד 8 שבועות בהתאם לנפח הנתונים ומספר הישויות. העלות מתחילה מ-$10,000 וממוצעת ב-$25,000 עבור רוב הפרויקטים. יישום סנכרון דלתא יכול להפחית עלויות תשתית שרת בעד 80% ולהאיץ את זמן היציאה לשוק ב-2 חודשים. עבור לקוח דומה במסחר אלקטרוני, חסכנו $40,000 בשנה בעלויות שרת.
קבלו ייעוץ והערכת פרויקט. הזמינו פיתוח מפתח-ביד והבטיחו חוויית משתמש חלקה גם במצב לא מקוון.
הצוות שלנו של מפתחי iOS ואנדרואיד מוסמכים מבטיח עמידה בהנחיות App Store ו-Google Play. ניסיון בפרויקטים מ-50,000 עד 10 מיליון משתמשים. סנכרון דלתא הוא ההתמחות המרכזית שלנו, וביצענו אותו למעלה מ-30 פעמים — הוא מהיר פי 10 מסנכרון מלא ויעיל פי 50.







