De nombreuses applications en temps réel disposent de documents qui font office de compteurs. Par exemple, vous pouvez compter les « j'aime » sur une publication ou les « favoris » sur un élément spécifique.
Dans Cloud Firestore, vous ne pouvez pas mettre à jour un seul document à un rythme illimité. Si vous disposez d'un compteur basé sur un seul document et avec des incréments suffisamment fréquents, vous finirez par constater des conflits sur les mises à jour du document. Voir Mises à jour d'un seul document .
Solution : compteurs distribués
Pour prendre en charge des mises à jour de compteur plus fréquentes, créez un compteur distribué. Chaque compteur est un document avec une sous-collection de « fragments » et la valeur du compteur est la somme de la valeur des fragments.
Le débit d'écriture augmente linéairement avec le nombre de fragments, de sorte qu'un compteur distribué avec 10 fragments peut gérer 10 fois plus d'écritures qu'un compteur traditionnel.
la toile
// counters/${ID}
{
"num_shards": NUM_SHARDS,
"shards": [subcollection]
}
// counters/${ID}/shards/${NUM}
{
"count": 123
}
Rapide
// 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 } }
Objectif 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
// 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
Noeud.js
Non applicable, voir l'extrait d'incrément de compteur ci-dessous.
Aller
PHP
Non applicable, consultez l'extrait d'initialisation du compteur ci-dessous.
C#
Le code suivant initialise un compteur distribué :
la toile
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(); }
Rapide
func createCounter(ref: DocumentReference, numShards: Int) { ref.setData(["numShards": numShards]){ (err) in for i in 0...numShards { ref.collection("shards").document(String(i)).setData(["count": 0]) } } }
Objectif 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
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
Noeud.js
Non applicable, voir l'extrait d'incrément de compteur ci-dessous.
Aller
PHP
C#
Rubis
Pour incrémenter le compteur, choisissez un fragment aléatoire et incrémentez le compteur :
la toile
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)); }
Rapide
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)) ]) }
Objectif 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
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
Noeud.js
Aller
PHP
C#
Rubis
Pour obtenir le nombre total, recherchez toutes les partitions et additionnez leurs champs count
:
la toile
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; }); }
Rapide
func getCount(ref: DocumentReference) { ref.collection("shards").getDocuments() { (querySnapshot, err) in var totalCount = 0 if err != nil { // Error getting shards // ... } else { for document in querySnapshot!.documents { let count = document.data()["count"] as! Int totalCount += count } } print("Total count is \(totalCount)") } }
Objectif 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
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
Noeud.js
Aller
PHP
C#
Rubis
Limites
La solution présentée ci-dessus est un moyen évolutif de créer des compteurs partagés dans Cloud Firestore, mais vous devez être conscient des limitations suivantes :
- Nombre de fragments : le nombre de fragments contrôle les performances du compteur distribué. Avec trop peu de fragments, certaines transactions devront peut-être être réessayées avant de réussir, ce qui ralentira les écritures. Avec trop de fragments, les lectures deviennent plus lentes et plus coûteuses. Vous pouvez compenser les frais de lecture en conservant le total du compteur dans un document cumulatif distinct qui est mis à jour à une cadence plus lente et en demandant aux clients de lire ce document pour obtenir le total. Le compromis est que les clients devront attendre que le document de synthèse soit mis à jour, au lieu de calculer le total en lisant toutes les partitions immédiatement après toute mise à jour.
- Coût - Le coût de lecture d'une valeur de compteur augmente linéairement avec le nombre de fragments, car la sous-collection entière de fragments doit être chargée.