Многие приложения реального времени имеют документы, которые действуют как счетчики. Например, вы можете подсчитывать «лайки» для публикации или «избранное» для определенного элемента.
В Cloud Firestore вы можете обновлять один документ примерно раз в секунду, что может быть слишком мало для некоторых приложений с высоким трафиком.
Решение: распределенные счетчики
Для поддержки более частых обновлений счетчика создайте распределенный счетчик. Каждый счетчик представляет собой документ с вложенной коллекцией «осколков», а значение счетчика представляет собой сумму значений осколков.
Пропускная способность записи увеличивается линейно с количеством сегментов, поэтому распределенный счетчик с 10 сегментами может обрабатывать в 10 раз больше операций записи, чем традиционный счетчик.
Интернет
// counters/${ID}
{
"num_shards": NUM_SHARDS,
"shards": [subcollection]
}
// counters/${ID}/shards/${NUM}
{
"count": 123
}
Быстрый
// 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 } }
Цель-C
// 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
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; } }
Kotlin+KTX
// counters/${ID} data class Counter(var numShards: Int) // counters/${ID}/shards/${NUM} data class Shard(var count: Int)
Питон
Python
Node.js
Неприменимо, см. приведенный ниже фрагмент приращения счетчика.
Идти
PHP
Неприменимо, см. приведенный ниже фрагмент инициализации счетчика.
С#
/// <summary> /// Shard is a document that contains the count. /// </summary> [FirestoreData] public class Shard { [FirestoreProperty(name: "count")] public int Count { get; set; } }
Следующий код инициализирует распределенный счетчик:
Интернет
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(); }
Быстрый
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]) } } }
Цель-C
- (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) }]; } }]; }
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); } }); }
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) } }
Питон
Python
Node.js
Неприменимо, см. приведенный ниже фрагмент приращения счетчика.
Идти
PHP
$numShards = 10; $ref = $db->collection('samples/php/distributedCounters'); for ($i = 0; $i < $numShards; $i++) { $doc = $ref->document($i); $doc->set(['Cnt' => 0]); }
С#
/// <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); }
Рубин
Чтобы увеличить счетчик, выберите случайный фрагмент и увеличьте счетчик:
Интернет
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 return shard_ref.update("count", firebase.firestore.FieldValue.increment(1)); }
Быстрый
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)) ]) }
Цель-C
- (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] }]; }
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)); }
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)) }
питон
Python
Node.js
Идти
PHP
$ref = $db->collection('samples/php/distributedCounters'); $numShards = 0; $docCollection = $ref->documents(); foreach ($docCollection as $doc) { $numShards++; } $shardIdx = random_int(0, $numShards - 1); $doc = $ref->document($shardIdx); $doc->update([ ['path' => 'Cnt', 'value' => FieldValue::increment(1)] ]);
С#
/// <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)); }
Рубин
Чтобы получить общее количество, запросите все осколки и просуммируйте их поля 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; }); }
Быстрый
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)") } }
Цель-C
- (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); } }]; }
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; } }); }
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 } }
питон
Python
Node.js
Идти
PHP
$result = 0; $docCollection = $db->collection('samples/php/distributedCounters')->documents(); foreach ($docCollection as $doc) { $result += $doc->data()['Cnt']; }
С#
/// <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")); }
Рубин
Ограничения
Решение, показанное выше, представляет собой масштабируемый способ создания общих счетчиков в Cloud Firestore, но вы должны знать о следующих ограничениях:
- Количество сегментов — количество сегментов определяет производительность распределенного счетчика. При слишком малом количестве сегментов некоторым транзакциям, возможно, придется повторить попытку, прежде чем они будут успешными, что замедлит запись. При слишком большом количестве осколков чтение становится медленнее и дороже. Вы можете компенсировать затраты на чтение, сохранив общее значение счетчика в отдельном сводном документе, который обновляется медленнее (например, один раз в секунду), и заставив клиентов читать этот документ, чтобы получить общее значение. Компромисс заключается в том, что клиентам придется ждать обновления сводного документа вместо того, чтобы вычислять общее количество, читая все осколки сразу после любого обновления.
- Стоимость . Стоимость считывания значения счетчика увеличивается линейно с количеством осколков, так как должна быть загружена вся вложенная коллекция осколков.