بسیاری از برنامه های بلادرنگ اسنادی دارند که به عنوان شمارنده عمل می کنند. به عنوان مثال، ممکن است «لایک» در یک پست یا «موارد دلخواه» یک مورد خاص را بشمارید.
در 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 } }
// 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
// counters/${ID} data class Counter(var numShards: Int) // counters/${ID}/shards/${NUM} data class Shard(var count: Int)
// 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++) { 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) 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 { // ... } }
- (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) }]; } }]; }
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) } }
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(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)) ]) }
- (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] }]; }
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)) }
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)); }
برای بدست آوردن تعداد کل، همه خرده ها را پرس و جو کنید و فیلدهای 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) 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 } }
- (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); } }]; }
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 } }
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 است، اما باید از محدودیت های زیر آگاه باشید:
- تعداد خرده ها - تعداد خرده ها عملکرد شمارنده توزیع شده را کنترل می کند. با تعداد خردههای بسیار کم، ممکن است برخی از تراکنشها قبل از موفقیت دوباره تلاش کنند، که سرعت نوشتن را کاهش میدهد. با تعداد زیاد خردهها، خواندن کندتر و گرانتر میشود. میتوانید هزینه خواندن را با نگهداشتن مجموع شمارنده در یک سند جمعآوری جداگانه که با سرعت کمتری بهروزرسانی میشود، جبران کنید و از مشتریان بخواهید که از آن سند بخوانند تا کل را به دست آورند. مبادله این است که مشتریان باید منتظر بمانند تا سند جمع آوری شده به روز شود، به جای محاسبه کل با خواندن همه خرده ها بلافاصله پس از هر به روز رسانی.
- هزینه - هزینه خواندن یک مقدار شمارنده به صورت خطی با تعداد خرده ها افزایش می یابد، زیرا کل زیر مجموعه خرده ها باید بارگذاری شود.