Contadores distribuídos

Muitos apps em tempo real têm documentos que atuam como contadores. Por exemplo, você pode contar "curtidas" em uma postagem ou "favoritos" de um item específico.

No Cloud Firestore, você só pode atualizar um único documento cerca de uma vez por segundo, o que pode ser muito pouco para alguns aplicativos de alto tráfego.

Solução: contadores distribuídos

Para aceitar atualizações de contador mais frequentes, crie um contador distribuído. Cada contador é um documento com uma subcoleção de "fragmentos", e o valor do contador é a soma do valor dos fragmentos.

A capacidade de gravação aumenta linearmente com o número de fragmentos, de modo que um contador distribuído com 10 fragmentos pode lidar com 10 vezes mais gravações que um 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;
    }
}

O código a seguir inicializa um contador distribuído:

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 incrementar o contador, escolha um fragmento aleatório e incremente a contagem em uma transação:

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 conseguir a contagem total, consulte todos os fragmentos e some os 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;
                }
            });
}

Limitações

A solução mostrada acima é uma maneira escalável de criar contadores compartilhados no Cloud Firestore, mas você deve estar ciente das seguintes limitações:

  • Contagem de fragmentos: o número de fragmentos controla o desempenho do contador distribuído. Com poucos fragmentos, algumas transações podem ter que tentar novamente antes de serem bem-sucedidas, o que diminuirá as gravações. Com fragmentos demais, as leituras ficam mais lentas e caras.
  • Custo: o custo da leitura de um valor de contador aumenta linearmente com o número de fragmentos, porque toda a subcoleção de fragmentos precisa ser carregada.

Enviar comentários sobre…

Precisa de ajuda? Acesse nossa página de suporte.