تحتوي العديد من التطبيقات التي تعمل في الوقت الفعلي على مستندات تعمل كمعدّلات. على سبيل المثال، يمكنك احتساب عدد "المعجبين" بمشاركة معيّنة أو عدد "العناصر المفضّلة" لعنصر معيّن.
في 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 } }
Objective-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
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
Python
Node.js
لا ينطبق، راجِع المقتطف الخاص بزيادة العداد أدناه.
انتقال
PHP
لا ينطبق، راجِع المقتطف أدناه لإعداد العداد.
#C
تُعدّ القيمة التالية مُعدّة مسبقًا للعداد الموزّع:
الويب
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
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
- (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
Python
Node.js
لا ينطبق، راجِع المقتطف الخاص بزيادة العداد أدناه.
انتقال
PHP
#C
لغة Ruby
لزيادة المعداد، اختَر شريحة عشوائية وزِد العدد:
الويب
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
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
- (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
Python
Node.js
انتقال
PHP
#C
لغة Ruby
للحصول على العدد الإجمالي، ابحث عن جميع الأجزاء وأضِف حقول 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) 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
- (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
Python
Node.js
انتقال
PHP
#C
لغة Ruby
القيود
يُعدّ الحلّ المعروض أعلاه طريقة قابلة للتطوير لإنشاء عدادات مشترَكة في Cloud Firestore، ولكن عليك الانتباه إلى القيود التالية:
- عدد الأجزاء: يتحكّم عدد الأجزاء في أداء العداد الموزّع. في حال توفّر عدد قليل جدًا من الأجزاء، قد تحتاج بعض المعاملات إلى إعادة المحاولة قبل إتمامها، ما سيؤدي إلى إبطاء عمليات الكتابة. مع وجود عددٍ كبير جدًا من الشظايا، تصبح عمليات القراءة أبطأ وأكثر تكلفة. يمكنك تعويض نفقات القراءة من خلال الاحتفاظ بإجمالي العداد في مستند تلخيصي منفصل يتم تعديله بمعدّل أبطأ، ومطالبة العميل بالقراءة من هذا المستند للحصول على الإجمالي. في المقابل، على العملاء الانتظار إلى أن يتم تعديل مستند التجميع، بدلاً من احتساب المجموع من خلال قراءة كل الأجزاء بعد أي تعديل مباشرةً.
- التكلفة: تزيد تكلفة قراءة قيمة العداد بشكل خطي مع عدد الأجزاء، لأنّه يجب تحميل المجموعة الفرعية للأجزاء بالكامل.