באפליקציות רבות שפועלות בזמן אמת יש מסמכים שמשמשים כמונים. לדוגמה, אפשר לספור את הלייקים שניתנו לפוסט או את הפריטים שסומנו כמועדפים.
ב-Cloud Firestore, אי אפשר לעדכן מסמך יחיד בקצב בלתי מוגבל. אם יש לכם מונה שמבוסס על מסמך יחיד, והעדכונים שלו מתבצעים בתדירות גבוהה, בסופו של דבר תראו התנגשות בעדכונים של המסמך. עדכונים במסמך יחיד
פתרון: מוני תנועה מבוזרים
כדי לתמוך בעדכונים תכופים יותר של מונה, צריך ליצור מונה מבוזר. כל מונה הוא מסמך עם אוסף משנה של 'רסיסים', והערך של המונה הוא סכום הערכים של הרסיסים.
קצב העברת הנתונים גדל באופן לינארי עם מספר הרסיסים, כך שמונה מבוזר עם 10 רסיסים יכול לטפל בפי 10 יותר פעולות כתיבה מאשר מונה מסורתי.
אינטרנט
// counters/${ID}
{
"num_shards": NUM_SHARDS,
"shards": [subcollection]
}
// counters/${ID}/shards/${NUM}
{
"count": 123
}
Swift
// counters/${ID} struct Counter { let numShards: Int init(numShards: Int) { self.numShards = numShards } } // counters/${ID}/shards/${NUM} struct Shard { let count: Int init(count: Int) { self.count = count } }
Objective-C
// counters/${ID} @interface FIRCounter : NSObject @property (nonatomic, readonly) NSInteger shardCount; @end @implementation FIRCounter - (instancetype)initWithShardCount:(NSInteger)shardCount { self = [super init]; if (self != nil) { _shardCount = shardCount; } return self; } @end // counters/${ID}/shards/${NUM} @interface FIRShard : NSObject @property (nonatomic, readonly) NSInteger count; @end @implementation FIRShard - (instancetype)initWithCount:(NSInteger)count { self = [super init]; if (self != nil) { _count = count; } return self; } @end
Kotlin
// counters/${ID} data class Counter(var numShards: Int) // counters/${ID}/shards/${NUM} data class Shard(var count: Int)
Java
// counters/${ID} public class Counter { int numShards; public Counter(int numShards) { this.numShards = numShards; } } // counters/${ID}/shards/${NUM} public class Shard { int count; public Shard(int count) { this.count = count; } }
Python
Python
Node.js
לא רלוונטי, אפשר לעיין בקטע הקוד לדוגמה של הגדלת המונה שבהמשך.
Go
PHP
לא רלוונטי, אפשר לעיין בקטע הקוד לאתחול המונה שבהמשך.
C#
הקוד הבא מאתחל מונה מבוזר:
אינטרנט
function createCounter(ref, num_shards) { var batch = db.batch(); // Initialize the counter document batch.set(ref, { num_shards: num_shards }); // Initialize each shard with count=0 for (let i = 0; i < num_shards; i++) { const shardRef = ref.collection('shards').doc(i.toString()); batch.set(shardRef, { count: 0 }); } // Commit the write batch return batch.commit(); }
Swift
func createCounter(ref: DocumentReference, numShards: Int) async { do { try await ref.setData(["numShards": numShards]) for i in 0...numShards { try await ref.collection("shards").document(String(i)).setData(["count": 0]) } } catch { // ... } }
Objective-C
- (void)createCounterAtReference:(FIRDocumentReference *)reference shardCount:(NSInteger)shardCount { [reference setData:@{ @"numShards": @(shardCount) } completion:^(NSError * _Nullable error) { for (NSInteger i = 0; i < shardCount; i++) { NSString *shardName = [NSString stringWithFormat:@"%ld", (long)shardCount]; [[[reference collectionWithPath:@"shards"] documentWithPath:shardName] setData:@{ @"count": @(0) }]; } }]; }
Kotlin
fun createCounter(ref: DocumentReference, numShards: Int): Task<Void> { // Initialize the counter document, then initialize each shard. return ref.set(Counter(numShards)) .continueWithTask { task -> if (!task.isSuccessful) { throw task.exception!! } val tasks = arrayListOf<Task<Void>>() // Initialize each shard with count=0 for (i in 0 until numShards) { val makeShard = ref.collection("shards") .document(i.toString()) .set(Shard(0)) tasks.add(makeShard) } Tasks.whenAll(tasks) } }
Java
public Task<Void> createCounter(final DocumentReference ref, final int numShards) { // Initialize the counter document, then initialize each shard. return ref.set(new Counter(numShards)) .continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(@NonNull Task<Void> task) throws Exception { if (!task.isSuccessful()) { throw task.getException(); } List<Task<Void>> tasks = new ArrayList<>(); // Initialize each shard with count=0 for (int i = 0; i < numShards; i++) { Task<Void> makeShard = ref.collection("shards") .document(String.valueOf(i)) .set(new Shard(0)); tasks.add(makeShard); } return Tasks.whenAll(tasks); } }); }
Python
Python
Node.js
לא רלוונטי, אפשר לעיין בקטע הקוד לדוגמה של הגדלת המונה שבהמשך.
Go
PHP
C#
Ruby
כדי להגדיל את הערך של המונה, בוחרים רסיס אקראי ומגדילים את הערך:
אינטרנט
function incrementCounter(ref, num_shards) { // Select a shard of the counter at random const shard_id = Math.floor(Math.random() * num_shards).toString(); const shard_ref = ref.collection('shards').doc(shard_id); // Update count return shard_ref.update("count", firebase.firestore.FieldValue.increment(1)); }
Swift
func incrementCounter(ref: DocumentReference, numShards: Int) { // Select a shard of the counter at random let shardId = Int(arc4random_uniform(UInt32(numShards))) let shardRef = ref.collection("shards").document(String(shardId)) shardRef.updateData([ "count": FieldValue.increment(Int64(1)) ]) }
Objective-C
- (void)incrementCounterAtReference:(FIRDocumentReference *)reference shardCount:(NSInteger)shardCount { // Select a shard of the counter at random NSInteger shardID = (NSInteger)arc4random_uniform((uint32_t)shardCount); NSString *shardName = [NSString stringWithFormat:@"%ld", (long)shardID]; FIRDocumentReference *shardReference = [[reference collectionWithPath:@"shards"] documentWithPath:shardName]; [shardReference updateData:@{ @"count": [FIRFieldValue fieldValueForIntegerIncrement:1] }]; }
Kotlin
fun incrementCounter(ref: DocumentReference, numShards: Int): Task<Void> { val shardId = Math.floor(Math.random() * numShards).toInt() val shardRef = ref.collection("shards").document(shardId.toString()) return shardRef.update("count", FieldValue.increment(1)) }
Java
public Task<Void> incrementCounter(final DocumentReference ref, final int numShards) { int shardId = (int) Math.floor(Math.random() * numShards); DocumentReference shardRef = ref.collection("shards").document(String.valueOf(shardId)); return shardRef.update("count", FieldValue.increment(1)); }
Python
Python
Node.js
Go
PHP
C#
Ruby
כדי לקבל את המספר הכולל, שולחים שאילתה לכל הרסיסים ומסכמים את השדות count
שלהם:
אינטרנט
function getCount(ref) { // Sum the count of each shard in the subcollection return ref.collection('shards').get().then((snapshot) => { let total_count = 0; snapshot.forEach((doc) => { total_count += doc.data().count; }); return total_count; }); }
Swift
func getCount(ref: DocumentReference) async { do { let querySnapshot = try await ref.collection("shards").getDocuments() var totalCount = 0 for document in querySnapshot.documents { let count = document.data()["count"] as! Int totalCount += count } print("Total count is \(totalCount)") } catch { // handle error } }
Objective-C
- (void)getCountWithReference:(FIRDocumentReference *)reference { [[reference collectionWithPath:@"shards"] getDocumentsWithCompletion:^(FIRQuerySnapshot *snapshot, NSError *error) { NSInteger totalCount = 0; if (error != nil) { // Error getting shards // ... } else { for (FIRDocumentSnapshot *document in snapshot.documents) { NSInteger count = [document[@"count"] integerValue]; totalCount += count; } NSLog(@"Total count is %ld", (long)totalCount); } }]; }
Kotlin
fun getCount(ref: DocumentReference): Task<Int> { // Sum the count of each shard in the subcollection return ref.collection("shards").get() .continueWith { task -> var count = 0 for (snap in task.result!!) { val shard = snap.toObject<Shard>() count += shard.count } count } }
Java
public Task<Integer> getCount(final DocumentReference ref) { // Sum the count of each shard in the subcollection return ref.collection("shards").get() .continueWith(new Continuation<QuerySnapshot, Integer>() { @Override public Integer then(@NonNull Task<QuerySnapshot> task) throws Exception { int count = 0; for (DocumentSnapshot snap : task.getResult()) { Shard shard = snap.toObject(Shard.class); count += shard.count; } return count; } }); }
Python
Python
Node.js
Go
PHP
C#
Ruby
מגבלות
הפתרון שמוצג למעלה הוא דרך ניתנת להרחבה ליצירת מוניטורים משותפים ב-Cloud Firestore, אבל חשוב להיות מודעים למגבלות הבאות:
- מספר השברים – מספר השברים קובע את הביצועים של המונה המבוזר. אם יש מעט מדי רסיסים, יכול להיות שחלק מהעסקאות יצטרכו לנסות שוב לפני שיצליחו, מה שיאט את פעולות הכתיבה. אם יש יותר מדי רסיסים, פעולות הקריאה הופכות לאיטיות ויקרות יותר. כדי לקזז את עלות הקריאה, אפשר לשמור את סכום המונה במסמך סיכום נפרד שמתעדכן בתדירות נמוכה יותר, ולגרום ללקוחות לקרוא מהמסמך הזה כדי לקבל את הסכום הכולל. החיסרון הוא שהלקוחות יצטרכו לחכות לעדכון של מסמך הסיכום, במקום לחשב את הסכום הכולל על ידי קריאת כל השברים מיד אחרי כל עדכון.
- עלות – העלות של קריאת ערך מונה עולה באופן ליניארי עם מספר הרסיסים, כי צריך לטעון את כל אוסף המשנה של הרסיסים.