Bộ đếm được phân phối

Nhiều ứng dụng theo thời gian thực có tài liệu đóng vai trò là bộ đếm. Ví dụ: bạn có thể tính số lượt "thích" trên một bài đăng hoặc số lượt "yêu thích" của một mục cụ thể.

Trong Cloud Firestore, bạn không thể cập nhật một tài liệu ở tốc độ không giới hạn. Nếu có một bộ đếm dựa trên một tài liệu duy nhất và tăng đủ thường xuyên cho bộ đếm đó, cuối cùng bạn sẽ thấy sự tranh chấp về nội dung cập nhật cho tài liệu. Xem phần Cập nhật một tài liệu.

Giải pháp: Bộ đếm phân tán

Để hỗ trợ cập nhật bộ đếm thường xuyên hơn, hãy tạo một bộ đếm phân tán. Mỗi bộ đếm là một tài liệu có một tập hợp con "mảnh" và giá trị của bộ đếm là tổng giá trị của các mảnh.

Lưu lượng ghi tăng lên theo tỷ lệ tuyến tính với số lượng phân đoạn, vì vậy, bộ đếm phân tán với 10 phân đoạn có thể xử lý số lượt ghi nhiều gấp 10 lần so với bộ đếm truyền thống.

Web

// counters/${ID}
{
  "num_shards": NUM_SHARDS,
  "shards": [subcollection]
}

// counters/${ID}/shards/${NUM}
{
  "count": 123
}

Swift

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
// 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
  }
}

Objective-C

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
// 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

Kotlin+KTX

// counters/${ID}
data class Counter(var numShards: Int)

// counters/${ID}/shards/${NUM}
data class Shard(var count: Int)

Java

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

Python

import random

from google.cloud import firestore


class Shard:
    """
    A shard is a distributed counter. Each shard can support being incremented
    once per second. Multiple shards are needed within a Counter to allow
    more frequent incrementing.
    """

    def __init__(self):
        self._count = 0

    def to_dict(self):
        return {"count": self._count}


class Counter:
    """
    A counter stores a collection of shards which are
    summed to return a total count. This allows for more
    frequent incrementing than a single document.
    """

    def __init__(self, num_shards):
        self._num_shards = num_shards

Python

import random

from google.cloud import firestore


class Shard:
    """
    A shard is a distributed counter. Each shard can support being incremented
    once per second. Multiple shards are needed within a Counter to allow
    more frequent incrementing.
    """

    def __init__(self):
        self._count = 0

    def to_dict(self):
        return {"count": self._count}


class Counter:
    """
    A counter stores a collection of shards which are
    summed to return a total count. This allows for more
    frequent incrementing than a single document.
    """

    def __init__(self, num_shards):
        self._num_shards = num_shards

Node.js

Không áp dụng, hãy xem đoạn mã tăng bộ đếm bên dưới.

Tiến hành

import (
	"context"
	"fmt"
	"math/rand"
	"strconv"

	"cloud.google.com/go/firestore"
	"google.golang.org/api/iterator"
)

// Counter is a collection of documents (shards)
// to realize counter with high frequency.
type Counter struct {
	numShards int
}

// Shard is a single counter, which is used in a group
// of other shards within Counter.
type Shard struct {
	Count int
}

PHP

Không áp dụng, hãy xem đoạn mã khởi tạo bộ đếm bên dưới.

C#

/// <summary>
/// Shard is a document that contains the count.
/// </summary>
[FirestoreData]
public class Shard
{
    [FirestoreProperty(name: "count")]
    public int Count { get; set; }
}

Mã sau đây khởi chạy bộ đếm phân tán:

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++) {
        const shardRef = ref.collection('shards').doc(i.toString());
        batch.set(shardRef, { count: 0 });
    }

    // Commit the write batch
    return batch.commit();
}

Swift

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
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 {
    // ...
  }
}

Objective-C

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
- (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) }];
    }
  }];
}

Kotlin+KTX

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

Java

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

Python

def init_counter(self, doc_ref):
    """
    Create a given number of shards as
    subcollection of specified document.
    """
    col_ref = doc_ref.collection("shards")

    # Initialize each shard with count=0
    for num in range(self._num_shards):
        shard = Shard()
        col_ref.document(str(num)).set(shard.to_dict())

Python

async def init_counter(self, doc_ref):
    """
    Create a given number of shards as
    subcollection of specified document.
    """
    col_ref = doc_ref.collection("shards")

    # Initialize each shard with count=0
    for num in range(self._num_shards):
        shard = Shard()
        await col_ref.document(str(num)).set(shard.to_dict())

Node.js

Không áp dụng, hãy xem đoạn mã tăng bộ đếm bên dưới.

Tiến hành


// initCounter creates a given number of shards as
// subcollection of specified document.
func (c *Counter) initCounter(ctx context.Context, docRef *firestore.DocumentRef) error {
	colRef := docRef.Collection("shards")

	// Initialize each shard with count=0
	for num := 0; num < c.numShards; num++ {
		shard := Shard{0}

		if _, err := colRef.Doc(strconv.Itoa(num)).Set(ctx, shard); err != nil {
			return fmt.Errorf("Set: %w", err)
		}
	}
	return nil
}

PHP

$numShards = 10;
$ref = $db->collection('samples/php/distributedCounters');
for ($i = 0; $i < $numShards; $i++) {
    $doc = $ref->document((string) $i);
    $doc->set(['Cnt' => 0]);
}

C#

/// <summary>
/// Create a given number of shards as a
/// subcollection of specified document.
/// </summary>
/// <param name="docRef">The document reference <see cref="DocumentReference"/></param>
private static async Task CreateCounterAsync(DocumentReference docRef, int numOfShards)
{
    CollectionReference colRef = docRef.Collection("shards");
    var tasks = new List<Task>();
    // Initialize each shard with Count=0
    for (var i = 0; i < numOfShards; i++)
    {
        tasks.Add(colRef.Document(i.ToString()).SetAsync(new Shard() { Count = 0 }));
    }
    await Task.WhenAll(tasks);
}

Ruby

# project_id = "Your Google Cloud Project ID"
# num_shards = "Number of shards for distributed counter"
# collection_path = "shards"

require "google/cloud/firestore"

firestore = Google::Cloud::Firestore.new project_id: project_id

shards_ref = firestore.col collection_path

# Initialize each shard with count=0
num_shards.times do |i|
  shards_ref.doc(i).set({ count: 0 })
end

puts "Distributed counter shards collection created."

Để tăng bộ đếm, hãy chọn một mảnh ngẫu nhiên và tăng số lượng:

Web

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

Swift

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
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))
  ])
}

Objective-C

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
- (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]
  }];
}

Kotlin+KTX

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

Java

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

Python

def increment_counter(self, doc_ref):
    """Increment a randomly picked shard."""
    doc_id = random.randint(0, self._num_shards - 1)

    shard_ref = doc_ref.collection("shards").document(str(doc_id))
    return shard_ref.update({"count": firestore.Increment(1)})

Python

async def increment_counter(self, doc_ref):
    """Increment a randomly picked shard."""
    doc_id = random.randint(0, self._num_shards - 1)

    shard_ref = doc_ref.collection("shards").document(str(doc_id))
    return await shard_ref.update({"count": firestore.Increment(1)})

Node.js

function incrementCounter(docRef, numShards) {
  const shardId = Math.floor(Math.random() * numShards);
  const shardRef = docRef.collection('shards').doc(shardId.toString());
  return shardRef.set({count: FieldValue.increment(1)}, {merge: true});
}

Tiến hành


// incrementCounter increments a randomly picked shard.
func (c *Counter) incrementCounter(ctx context.Context, docRef *firestore.DocumentRef) (*firestore.WriteResult, error) {
	docID := strconv.Itoa(rand.Intn(c.numShards))

	shardRef := docRef.Collection("shards").Doc(docID)
	return shardRef.Update(ctx, []firestore.Update{
		{Path: "Count", Value: firestore.Increment(1)},
	})
}

PHP

$ref = $db->collection('samples/php/distributedCounters');
$numShards = 0;
$docCollection = $ref->documents();
foreach ($docCollection as $doc) {
    $numShards++;
}
$shardIdx = random_int(0, max(1, $numShards) - 1);
$doc = $ref->document((string) $shardIdx);
$doc->update([
    ['path' => 'Cnt', 'value' => FieldValue::increment(1)]
]);

C#

/// <summary>
/// Increment a randomly picked shard by 1.
/// </summary>
/// <param name="docRef">The document reference <see cref="DocumentReference"/></param>
/// <returns>The <see cref="Task"/></returns>
private static async Task IncrementCounterAsync(DocumentReference docRef, int numOfShards)
{
    int documentId;
    lock (s_randLock)
    {
        documentId = s_rand.Next(numOfShards);
    }
    var shardRef = docRef.Collection("shards").Document(documentId.ToString());
    await shardRef.UpdateAsync("count", FieldValue.Increment(1));
}

Ruby

# project_id = "Your Google Cloud Project ID"
# num_shards = "Number of shards for distributed counter"
# collection_path = "shards"

require "google/cloud/firestore"

firestore = Google::Cloud::Firestore.new project_id: project_id

# Select a shard of the counter at random
shard_id = rand 0...num_shards
shard_ref = firestore.doc "#{collection_path}/#{shard_id}"

# increment counter
shard_ref.update({ count: firestore.field_increment(1) })

puts "Counter incremented."

Để biết tổng số, hãy truy vấn tất cả các mảnh và cộng các trường count của các mảnh đó:

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

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
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
  }
}

Objective-C

Lưu ý: Sản phẩm này không dùng được trên các mục tiêu watchOS và App Clip.
- (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);
        }
  }];
}

Kotlin+KTX

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

Java

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

Python

def get_count(self, doc_ref):
    """Return a total count across all shards."""
    total = 0
    shards = doc_ref.collection("shards").list_documents()
    for shard in shards:
        total += shard.get().to_dict().get("count", 0)
    return total

Python

async def get_count(self, doc_ref):
    """Return a total count across all shards."""
    total = 0
    shards = doc_ref.collection("shards").list_documents()
    async for shard in shards:
        total += (await shard.get()).to_dict().get("count", 0)
    return total

Node.js

async function getCount(docRef) {
  const querySnapshot = await docRef.collection('shards').get();
  const documents = querySnapshot.docs;

  let count = 0;
  for (const doc of documents) {
    count += doc.get('count');
  }
  return count;
}

Tiến hành


// getCount returns a total count across all shards.
func (c *Counter) getCount(ctx context.Context, docRef *firestore.DocumentRef) (int64, error) {
	var total int64
	shards := docRef.Collection("shards").Documents(ctx)
	for {
		doc, err := shards.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return 0, fmt.Errorf("Next: %w", err)
		}

		vTotal := doc.Data()["Count"]
		shardCount, ok := vTotal.(int64)
		if !ok {
			return 0, fmt.Errorf("firestore: invalid dataType %T, want int64", vTotal)
		}
		total += shardCount
	}
	return total, nil
}

PHP

$result = 0;
$docCollection = $db->collection('samples/php/distributedCounters')->documents();
foreach ($docCollection as $doc) {
    $result += $doc->data()['Cnt'];
}

C#

/// <summary>
/// Get total count across all shards.
/// </summary>
/// <param name="docRef">The document reference <see cref="DocumentReference"/></param>
/// <returns>The <see cref="int"/></returns>
private static async Task<int> GetCountAsync(DocumentReference docRef)
{
    var snapshotList = await docRef.Collection("shards").GetSnapshotAsync();
    return snapshotList.Sum(shard => shard.GetValue<int>("count"));
}

Ruby

# project_id = "Your Google Cloud Project ID"
# collection_path = "shards"

require "google/cloud/firestore"

firestore = Google::Cloud::Firestore.new project_id: project_id

shards_ref = firestore.col_group collection_path

count = 0
shards_ref.get do |doc_ref|
  count += doc_ref[:count]
end

puts "Count value is #{count}."

Các điểm hạn chế

Giải pháp hiển thị ở trên là một cách có thể mở rộng để tạo bộ đếm dùng chung trong Cloud Firestore, nhưng bạn nên lưu ý những hạn chế sau:

  • Số lượng phân đoạn – Số lượng phân đoạn kiểm soát hiệu suất của bộ đếm phân tán. Khi có quá ít mảnh, một số giao dịch có thể phải thử lại trước khi thành công, điều này sẽ làm chậm quá trình ghi. Khi có quá nhiều phân mảnh, hoạt động đọc sẽ chậm hơn và tốn kém hơn. Bạn có thể bù đắp chi phí đọc bằng cách giữ tổng bộ đếm trong một tài liệu cuộn lên riêng biệt được cập nhật với tần suất chậm hơn và yêu cầu ứng dụng đọc từ tài liệu đó để lấy tổng số. Mức đánh đổi là ứng dụng sẽ phải đợi tài liệu cuộn lên được cập nhật, thay vì tính tổng bằng cách đọc tất cả các mảnh ngay sau khi cập nhật.
  • Chi phí – Chi phí đọc giá trị bộ đếm tăng lên theo hàm tuyến tính với số lượng phân mảnh, vì toàn bộ tập hợp con phân mảnh phải được tải.