分布式计数器

许多实时应用都有作为计数器的文档。例如,您可能会统计某个帖子的“顶”次数或某件商品的“收藏”数。

在 Cloud Firestore 中,每个文档只能以大约每秒一次的频率更新,这个频率对于某些高流量应用来说可能太低了。

解决方案:分布式计数器

为了能够更频繁地更新计数器,您可以创建分布式计数器。每个计数器都是一个包含“碎片”子集合的文档,计数器的值就是这些碎片值的总和。

写入吞吐量随碎片数量线性增长,因此具有 10 个碎片的分布式计数器可以处理的写入量是传统计数器的 10 倍。

网页

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

以下代码示范了分布式计数器的初始化:

网页

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

要增加计数器的值,请随机选择一个碎片并在事务中递增计数:

网页

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

要获得总计数,请查询所有碎片并对其 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;
    });
}

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

限制

上述解决方案是在 Cloud Firestore 中创建共享计数器的一种可扩展方法,但您应该了解以下限制:

  • 碎片数 - 碎片数量决定着分布式计数器的性能。如果碎片太少,某些事务可能需要重试才能成功,这会减慢写入速度。如果碎片太多,读取速度会减慢,费用也会增加。
  • 费用 - 读取计数器值的费用随碎片数量线性增加,这是因为必须加载整个碎片子集合。

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面