Unterstützung für häufige und verteilte Zähler
Viele Echtzeitapps haben Dokumente, die als Zähler fungieren. Sie können z. B. "Gefällt mir"-Angaben für einen Beitrag zählen oder wie oft ein bestimmter Artikel als "Favorit" markiert wurde.
In Firestore können Sie ein einzelnes Dokument nicht unbegrenzt oft aktualisieren. Wenn Sie einen Zähler auf einem einzelnen Dokument basierend haben und dieser häufig genug erhöht wird, kommt es irgendwann zu Konflikten bei den Aktualisierungen des Dokuments. Weitere Informationen finden Sie unter Aktualisierungen für ein einzelnes Dokument.
Lösung: Verteilte Zähler
Wenn Sie möchten, dass häufigere Zähleraktualisierungen unterstützt werden, können Sie einen verteilten Zähler erstellen. Jeder Zähler ist ein Dokument mit einer untergeordneten Sammlung von "Shards". Der Wert des Zählers ist die Summe der Werte der Shards.
Der Schreibdurchsatz nimmt linear mit der Anzahl der Shards zu, sodass ein verteilter Zähler mit 10 Shards 10-mal so viele Schreibvorgänge verarbeiten kann wie ein herkömmlicher Zähler.
Web
// 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+KTX
Android
// counters/${ID} data class Counter(var numShards: Int) // counters/${ID}/shards/${NUM} data class Shard(var count: Int)
Java
Android
// 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
(Async)
Node.js
Nicht zutreffend, siehe Snippet zur Zählererhöhung unten.
Go
PHP
Nicht zutreffend, siehe Snippet zur Zählerinitialisierung unten.
C#
Mit dem folgenden Code wird ein verteilter Zähler initialisiert:
Web
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+KTX
Android
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
Android
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
(Async)
Node.js
Nicht zutreffend, siehe Snippet zur Zählererhöhung unten.
Go
PHP
C#
Ruby
Wenn Sie den Zähler erhöhen möchten, wählen Sie einen zufälligen Shard aus und erhöhen Sie dessen Zählerwert:
Web
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+KTX
Android
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
Android
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
(Async)
Node.js
Go
PHP
C#
Ruby
Um den Gesamtzählerwert zu ermitteln, führen Sie eine Abfrage für alle Shards durch und addieren Sie die count
-Felder:
Web
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+KTX
Android
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
Android
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
(Async)
Node.js
Go
PHP
C#
Ruby
Beschränkungen
Die oben gezeigte Lösung ist eine skalierbare Methode zum Erstellen gemeinsam genutzter Zähler in Firestore. Beachten Sie jedoch die folgenden Beschränkungen:
- Shard-Anzahl: Die Leistung des verteilten Zählers ergibt sich aus der Anzahl der Shards. Wenn zu wenige Shards vorhanden sind, müssen möglicherweise einige Transaktionen wiederholt werden, bevor sie erfolgreich ausgeführt werden. Dadurch werden Schreibvorgänge verlangsamt. Bei zu vielen Shards werden Lesevorgänge langsamer und teurer. Sie können die Lesekosten ausgleichen. Dazu belassen Sie die Zählersumme in einem separaten Rollup-Dokument, das mit einer langsameren Taktung aktualisiert wird, und lassen die Clients aus diesem Dokument lesen, um die Gesamtsumme zu erhalten. Dabei gehen Sie den Kompromiss ein, dass Clients auf die Aktualisierung des Rollup-Dokuments warten müssen, anstatt die Shards unmittelbar nach jeder Aktualisierung auslesen und daraus die Gesamtsumme berechnen zu können.
- Kosten: Die Kosten für das Lesen eines Zählerwerts steigen linear mit der Anzahl der Shards, da die gesamte untergeordnete Sammlung der Shards geladen werden muss.