简体   繁体   English

使用 firebase 的最低成本排行榜系统

[英]Lowest-Cost Leaderboard system using firebase

My goal is to effectively get a list of children (ordered* and indexed**) with the lowest number of data transfer.我的目标是有效地获取数据传输数量最少的子项列表(有序*和索引**)。

* ordered: ordered by points for each user / database child * ordered:按每个用户/数据库子项的点数排序

** indexed: 2 or less ranks behind/after the current user [A specific child] (further elaborated below) ** 索引:在当前用户 [A specific child] 后面/之后排名 2 或更少(下面进一步详细说明)

My database structure is as follows:-我的数据库结构如下:-

数据库结构

I basically want to get the first 3 users ordered by points (simple):-我基本上想获得按积分排序的前 3 个用户(简单):-

val usersRef = FirebaseDatabase.getInstance(DB_LINK).getReference("users").orderByChild("points")
usersRef.limitToFirst(3).addValueEventListener(
    object : ValueEventListener {
        override fun onDataChange(snapshot: DataSnapshot) {
            for (ds in snapshot.children) {
                val points: String = snapshot.child("points").getValue(String::class.java)!!
                val firstName: String = snapshot.child("firstName").getValue(String::class.java) ?: ""
                val uid: String = snapshot.key!!
                // Update View
            }
        }

        override fun onCancelled(error: DatabaseError) {}
    }
)

Then, provided that the currently logged in user isn't one of the first three, I want to get his rank (order according to points in the whole db), 2 users' before him, and 2 users' after him without querying the whole database (it's a user database that can get up to 50K unique users) because querying the whole database is a really expensive client-side task.然后,假设当前登录的用户不是前三个用户之一,我想获得他的排名(根据整个数据库中的点数排序),他之前的 2 个用户和他之后的 2 个用户,而不用查询整个数据库(它是一个用户数据库,最多可以有 5 万个唯一用户),因为查询整个数据库是一项非常昂贵的客户端任务。

I checked firebase data filtering page but found nothing useful about limiting results according to a certain child.我检查了firebase 数据过滤页面,但发现根据某个孩子限制结果没有任何用处。

This answer doesn't satisfy my needs, as it loops over the whole database (in my case, 50K records).这个答案不能满足我的需求,因为它遍历了整个数据库(在我的例子中是 50K 条记录)。 I need an effective method as I need to really save these firebase bills.我需要一种有效的方法,因为我需要真正节省这些 firebase 账单。

Moreover, I check this answer but it didn't meet my needs because it still queries the whole database, meaning it is not effective and will be billed for each node before the current user.而且,我检查了这个答案,但它不符合我的需求,因为它仍然查询整个数据库,这意味着它是无效的,并且会在当前用户之前为每个节点计费。 (Maybe he is number 40,000 in the db, so I shouldn't query the whole db each time to get his rank and get billed for 39,999 reads) (也许他在数据库中排名第 40,000,所以我不应该每次都查询整个数据库以获得他的排名并为 39,999 次读取付费)

I searched for a way to somehow use booleans to filter queries but again found nothing useful.我搜索了一种以某种方式使用布尔值来过滤查询的方法,但再次发现没有任何用处。 Here is my not-effective code:-这是我无效的代码:-

// Gets all children.
usersRef.addValueEventListener(
    object : ValueEventListener {
        override fun onDataChange(snapshot: DataSnapshot) {
            for (ds in snapshot.children) {
                val points: String = snapshot.child("points").getValue(String::class.java)!!
                val firstName: String = snapshot.child("firstName").getValue(String::class.java) ?: ""
                val uid: String = snapshot.key!!
                // Update View only if user is `2 <= usersRank - theirRank <= -2`
            }
        }

        override fun onCancelled(error: DatabaseError) {}
    }
)

I want to achieve something like this:- (Styling already done, logic remaining)我想实现这样的目标:-(样式已经完成,逻辑仍然存在)

排行榜样本

Is there a way to achieve this?有没有办法做到这一点? Any alternatives?还有其他选择吗?

EDIT: I found out that firestore offers aggregation queries that may help in this situation.编辑:我发现 firestore 提供的聚合查询可能有助于解决这种情况。 Doing more research to further narrow down the costs.做更多的研究以进一步缩小成本。

This operation is not available on a Firebase Realtime Database.此操作在 Firebase 实时数据库上不可用。 A better option would be Firestore.更好的选择是 Firestore。

Why?为什么?

Well, A fire-store database can give you the count of objects in a certain query.好吧,Fire-Store 数据库可以为您提供特定查询中的对象计数。 This is a new feature added by firebase.这是 firebase 添加的新功能。 You basically type the query you want, then add .count() before .get() ;您基本上键入所需的查询,然后在.get() .count() ) ; that way it'll return the count of objects only.这样它只会返回对象的数量。 This is called aggregation queries.这称为聚合查询。 Learn more about them here . 在这里了解更多关于它们的信息。

Cloud Functions - Why isn't it appropriate here? Cloud Functions - 为什么在这里不合适?

Using a Cloud Function for aggregations avoids some of the issues with client-side transactions, but comes with a different set of limitations:使用 Cloud Functions 进行聚合避免了客户端事务的一些问题,但有一组不同的限制:

  • Cost - Each rating added will cause a Cloud Function invocation, which may increase your costs.成本 - 添加的每个评级都会导致 Cloud Function 调用,这可能会增加您的成本。 For more information, see the Cloud Functions pricing page .有关更多信息,请参阅 Cloud Functions定价页面
  • Latency - By offloading the aggregation work to a Cloud Function, your app will not see updated data until the Cloud Function has finished executing and the client has been notified of the new data.延迟 - 通过将聚合工作卸载到 Cloud Functions,您的应用程序将不会看到更新的数据,直到 Cloud Functions 完成执行并且客户端已收到新数据的通知。 Depending on the speed of your Cloud Function, this could take longer than executing the transaction locally.根据您的 Cloud Functions 的速度,这可能比在本地执行交易花费更长的时间。
  • Write rates - this solution may not work for frequently updated aggregations because Cloud Firestore documents can only be updated at most once per second.写入速率 - 此解决方案可能不适用于频繁更新的聚合,因为 Cloud Firestore 文档每秒最多只能更新一次。 Additionally, If a transaction reads a document that was modified outside of the transaction, it retries a finite number of times and then fails.此外,如果事务读取在事务外修改的文档,它会重试有限次数然后失败。

Combining this with other methods结合其他方法

Now that you're using COUNT() for this system, there is one more method to help further narrow down the costs.现在您正在为这个系统使用COUNT() ,还有一种方法可以帮助进一步降低成本。 That is Periodic Updates.那就是定期更新。

Periodic Updates定期更新

Who would care about a live ranking of all users?谁会关心所有用户的实时排名? You can make the leaderboard update each minute, hour, or day.您可以让排行榜每分钟、每小时或每天更新一次。 For example, stack overflow's leaderboard is updated once a day!例如,stack overflow 的排行榜每天更新一次!

This approach would really work for any number of players and any write rate.这种方法确实适用于任意数量的播放器和任意写入速率。 However, you might need to adjust the frequency though as you grow depending on your willingness to pay.但是,随着您的成长,您可能需要根据您的支付意愿调整频率。

Costs Side成本端

For each normal read, you are charged for one read.对于每次正常阅读,您需要支付一次阅读费用。 Very simple.很简单的。 However, for one count, you're charged for 0.001 reads (meaning 1000 counts = 1 read).但是,对于一次计数,您需要为 0.001 次读取付费(意味着 1000 次计数 = 1 次读取)。 For more information about costs, check this article by firebase.有关成本的更多信息,请查看 firebase 的这篇文章

Final Thoughts最后的想法

To connect everything up, we shall now apply this on our problem.为了连接一切,我们现在将把它应用到我们的问题上。 Firstly, we'll need to keep the first portion of the code as it is.首先,我们需要保持代码的第一部分不变。 (The portion that grabs the first 3 users), though with some changes to port it to firebase. (抓住前 3 个用户的部分),尽管有一些更改以将其移植到 firebase。

NOTICE: Don't forget to setup a composite index because we're ordering by multiple fields at the same time.注意:不要忘记设置复合索引,因为我们同时按多个字段排序。

val top3 = HashMap<Int, HashMap<String, String>>()

Firebase.firestore.collection("users").orderBy("points", Query.Direction.DESCENDING)
    .orderBy("firstName", Query.Direction.ASCENDING)
    .get().addOnSuccessListener {
    for ((index, doc) in it.documents.withIndex()) {
        val firstName = doc.getString("firstName")!!
        val points = doc.getString("points")!!
        top3[index+1] = hashMapOf("first" to firstName, "points" to points, "uid" to doc.id)
    }
}
  • More about ordering and limiting here .有关订购和限制的更多信息,请点击此处

Then, we'll need to implement the COUNT() feature.然后,我们需要实现COUNT()功能。

Firebase.firestore.collection("users")
    .whereGreaterThan("points", UserProfile.getInstance()!!.getProfile().points)
    .count().get(AggregateSource.SERVER).addOnSuccessListener {
    println("Your rank is: ${it.count+1}")
}

Basically what I did here was:-基本上我在这里所做的是:-

  1. Selecting the Collection选择集合
  2. Ordered Ascending by first name so no duplicate ranks.按名字升序排列,因此没有重复的排名。
  3. Count them, and pass onto the function.计算它们,然后传递给函数。

The final step is just updating the hash map top3 and rank of user each hour/day/minute/...最后一步只是更新 hash map top3和用户每小时/天/分钟/排名。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM