De nombreuses applications en temps réel ont des documents qui agissent comme des compteurs. Par exemple, vous pouvez compter les "j'aime" sur une publication ou les "favoris" d'un élément spécifique.
Dans Cloud Firestore, vous ne pouvez pas mettre à jour un seul document à un rythme illimité. Si vous avez un compteur basé sur un seul document et des incréments assez fréquents, vous verrez éventuellement 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 de manière linéaire 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
Node.js
Non applicable, voir l'extrait d'incrémentation du compteur ci-dessous.
Aller
PHP
Non applicable, voir 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
Node.js
Non applicable, voir l'extrait d'incrémentation du compteur ci-dessous.
Aller
PHP
C#
Rubis
Pour incrémenter le compteur, choisissez un fragment aléatoire et incrémentez le nombre :
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
Node.js
Aller
PHP
C#
Rubis
Pour obtenir le nombre total, interrogez 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
Node.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 peuvent devoir réessayer 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 dépenses de lecture en conservant le total du compteur dans un document cumulatif séparé 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 cumulatif soit mis à jour, au lieu de calculer le total en lisant tous les fragments immédiatement après toute mise à jour.
- Coût - Le coût de lecture d'une valeur de compteur augmente de manière linéaire avec le nombre de fragments, car la sous-collection de fragments entière doit être chargée.