Contadores distribuidos

Muchas aplicaciones en tiempo real tienen documentos que funcionan como contadores. Por ejemplo, puedes contar la cantidad de "me gusta" en una publicación o de "favoritos" de un elemento específico.

En Cloud Firestore, solo puedes actualizar el mismo documento cerca de una vez por segundo, lo que podría ser insuficiente para algunas aplicaciones con tráfico alto.

Solución: Contadores distribuidos

Para admitir actualizaciones más frecuentes de los contadores, crea 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 de manera lineal 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.

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
    }
}

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;
    }
}

El siguiente código inicializa un contador distribuido:

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++) {
        let 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) {
    ref.setData(["numShards": numShards]){ (err) in
        for i in 0...numShards {
            ref.collection("shards").document(String(i)).setData(["count": 0])
        }
    }
}

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);
                }
            });
}

Para aumentar el valor del contador, selecciona un fragmento aleatorio y aumenta la cantidad en una transacción:

Web

function incrementCounter(db, 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 in a transaction
    return db.runTransaction(t => {
        return t.get(shard_ref).then(doc => {
            const new_count = doc.data().count + 1;
            t.update(shard_ref, { count: new_count });
        });
    });
}

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))

    // Update count in a transaction
    db.runTransaction({ (transaction, errorPointer) -> Any? in
        do {
            let shardData = try transaction.getDocument(shardRef).data() ?? [:]
            let shardCount = shardData["count"] as! Int
            transaction.updateData(["count": shardCount + 1], forDocument: shardRef)
        } catch {
            // Error getting shard data
            // ...
        }

        return nil
    }) { (object, err) in
        // ...
    }
}

Android

public Task<Void> incrementCounter(final DocumentReference ref, final int numShards) {
    int shardId = (int) Math.floor(Math.random() * numShards);
    final DocumentReference shardRef = ref.collection("shards").document(String.valueOf(shardId));

    return db.runTransaction(new Transaction.Function<Void>() {
        @Override
        public Void apply(Transaction transaction) throws FirebaseFirestoreException {
            Shard shard = transaction.get(shardRef).toObject(Shard.class);
            shard.count += 1;

            transaction.set(shardRef, shard);
            return null;
        }
    });
}

Para obtener el total, consulta todos los fragmentos y suma sus campos count:

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) {
    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)")
    }
}

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;
                }
            });
}

Limitaciones

La solución anterior es una manera escalable de crear contadores compartidos en Cloud Firestore, pero debes tener en cuenta las siguientes limitaciones:

  • Recuento de fragmentos: La cantidad de fragmentos controla el rendimiento del contador distribuido. Si hay muy pocos fragmentos, es posible que algunas transacciones deban volver a intentarse antes de ejecutarse de manera correcta, lo que retrasaría las escrituras. Si hay demasiados fragmentos, las lecturas se vuelven más lentas y más caras.
  • Costo: El costo de lectura de un valor de contador aumenta de forma lineal con la cantidad de fragmentos, debido a que se debe cargar la subcolección de fragmentos completa.

Enviar comentarios sobre…

¿Necesitas ayuda? Visita nuestra página de asistencia.