Muchas aplicaciones en tiempo real tienen documentos que actúan como contadores. Por ejemplo, puedes contar los "me gusta" de una publicación o los "favoritos" de un elemento específico.
En Cloud Firestore, no puedes actualizar un solo documento a una tarifa ilimitada. Si tiene un contador basado en un solo documento e incrementos lo suficientemente frecuentes, eventualmente verá una contienda en las actualizaciones del documento. Consulte Actualizaciones de un solo documento .
Solución: contadores distribuidos
Para admitir actualizaciones de contadores más frecuentes, cree un contador distribuido. Cada contador es un documento con una subcolección de "fragmentos" y el valor del contador es la suma del valor de los fragmentos.
El rendimiento de escritura aumenta linealmente con la cantidad de fragmentos, por lo que un contador distribuido con 10 fragmentos puede manejar 10 veces más escrituras que un contador tradicional.
// counters/${ID}
{
"num_shards": NUM_SHARDS,
"shards": [subcollection]
}
// counters/${ID}/shards/${NUM}
{
"count": 123
}
// 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
}
}
// 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
// counters/${ID}
data class Counter(var numShards: Int)
// counters/${ID}/shards/${NUM}
data class Shard(var count: Int)
// 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;
}
}
No aplicable; consulte el fragmento de incremento del contador a continuación.
No aplicable; consulte el fragmento de inicialización del contador a continuación.
El siguiente código inicializa un contador distribuido:
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();
}
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 {
// ...
}
}
- (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) }];
}
}];
}
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)
}
}
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);
}
});
}
No aplicable; consulte el fragmento de incremento del contador a continuación.
Para incrementar el contador, elija un fragmento aleatorio e incremente el recuento:
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));
}
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))
])
}
- (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]
}];
}
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))
}
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));
}
Para obtener el recuento total, consulte todos los fragmentos y sume sus campos 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;
});
}
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
}
}
- (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);
}
}];
}
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
}
}
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;
}
});
}
Limitaciones
La solución que se muestra arriba es una forma escalable de crear contadores compartidos en Cloud Firestore, pero debes tener en cuenta las siguientes limitaciones:
- Recuento de fragmentos : el número de fragmentos controla el rendimiento del contador distribuido. Con muy pocos fragmentos, es posible que algunas transacciones deban volver a intentarse antes de tener éxito, lo que ralentizará las escrituras. Con demasiados fragmentos, las lecturas se vuelven más lentas y costosas. Puede compensar el gasto de lectura manteniendo el total del contador en un documento acumulativo separado que se actualiza a una cadencia más lenta y haciendo que los clientes lean ese documento para obtener el total. La desventaja es que los clientes tendrán que esperar a que se actualice el documento acumulado, en lugar de calcular el total leyendo todos los fragmentos inmediatamente después de cualquier actualización.
- Costo : el costo de leer un valor de contador aumenta linealmente con la cantidad de fragmentos, porque se debe cargar toda la subcolección de fragmentos.