Buka konsol

Penghitung Terdistribusi

Banyak aplikasi realtime memiliki dokumen yang berfungsi sebagai penghitung. Misalnya, Anda mungkin menghitung 'suka' di postingan, atau 'favorit' untuk item tertentu.

Di Cloud Firestore, Anda hanya dapat mengupdate 1 dokumen sebanyak 1 kali per detik. Hal ini mungkin terlalu sedikit bagi beberapa aplikasi dengan traffic yang tinggi.

Solusi: Penghitung terdistribusi

Untuk mendukung update penghitung yang lebih sering, buat penghitung terdistribusi. Setiap penghitung adalah dokumen dengan subkoleksi "shard", dan nilai penghitung adalah jumlah dari nilai shard itu.

Throughput penulisan meningkat secara linear sesuai jumlah shard, sehingga penghitung terdistribusi dengan 10 shard dapat menangani operasi tulis 10x lebih banyak dibanding penghitung tradisional.

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

Kode berikut menginisialisasi penghitung terdistribusi:

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

Untuk menambahkan penghitung, pilih sebuah shard acak dan tingkatkan jumlah dalam transaksi:

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

Untuk mendapatkan jumlah total, proses kueri untuk semua shard dan jumlahkan kolom count-nya:

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

Batasan

Solusi yang ditunjukkan di atas adalah cara skalabel untuk membuat penghitung bersama di Cloud Firestore, namun Anda perlu memahami batasan berikut:

  • Jumlah sharding - Jumlah sharding akan menentukan performa penghitung terdistribusi. Jika jumlah sharding terlalu sedikit, beberapa transaksi mungkin harus diulang hingga berhasil, sehingga akan memperlambat penulisan. Jika jumlah sharding terlalu banyak, operasi baca menjadi lebih lambat dan lebih mahal.
  • Biaya - Biaya pembacaan nilai penghitung akan meningkat secara linear seiring jumlah sharding, karena seluruh subkoleksi sharding harus dimuat.