先週、新しいコレクション・データ型(CDT)について興奮したのを覚えているでしょうか?ディクショナリ、セット、リストです。次は何だと思いますか?またやってしまいました。
今回は4つ目のCDT、Sorted Setsを紹介します!Sorted Setsとはその名の通り、異なる要素を並べた配列のことです!
「何を注文するのかって?あなたが望むものなら何でも(あなたが望むものがdobule
型である限り)!
Sorted Setsは、score
として知られるものによって順序付けされます。スコアは数値でなければならず、小数点以下があっても問題ありません。要素が追加、更新、削除されると、順序は自動的に更新されます。
かなりクールですよね?
ソートされたセットで何ができるのか?
前回のブログ記事で紹介した架空のゲーム「Acron Hunt」を覚えている人は、このゲームが多人数参加型のゲームで、プレイヤーが協力して時間切れになる前にできるだけ多くのどんぐりを集めるゲームであることを思い出すでしょう。
Acorn Huntでは、プレイヤーのメタデータを保存するために辞書を使い、ゲーム内のチャットを保存するためにリストを使い、各ゲームのプレイヤーリストを保存するためにセットを使いました。今回、Sorted Setsを使ってリーダーボードを追加することができます!プレイヤーがドングリを集めると、スコアが上がり、リーダーボードが自動的に更新されます。
Sorted Setsでリーダーボードを構築できるだけでなく、レート制限も加えることができます!どんぐりハントの各ラウンドで、プレイヤーはスーパーアビリティであるツリースラムを3回使うことができます。ソートされたセットを使えば、アビリティが使われるたびに得点を減らし、限界に達したらプレーヤーを排除することができます。
リーダーボードの作成
リーダーボードはスコア順にプレーヤーを表示します。リーダーボードの中には、トップ10のように上位N人だけを表示するものもあります。他のリーダーボードはスコアを反転して表示し、ゴルフのように最低スコアが勝ちとなります。
ソートされたセットを使えば、データの完全性を損なうことなく、このようなシナリオを簡単に扱うことができます。Acorn Huntのエンドポイントで、特定のゲームのトップ5のスコアを取得することを考えてみましょう:
// GET /games/{gameId}/scores?top=5&sort=desc
func handler(w http.ResponseWriter, r * http.Request) {
client:= getClient()
ctx:= r.Context()
setupCache(client, ctx)
gameId:= mux.Vars(r)["gameId"]
top, _ := strconv.Atoi(r.URL.Query().Get("top"))
sort:= r.URL.Query().Get("sort")
numberOfResults:= 10
if top > 0 {
numberOfResults = top
}
order:= momento.ASCENDING
if sort == "desc" {
order = momento.DESCENDING
}
fetchResponse, err := client.SortedSetFetch(ctx, & momento.SortedSetFetchRequest{
CacheName: cacheName,
SetName: gameId,
Order: order,
NumberOfResults: & momento.FetchLimitedElements{
Limit: uint32(numberOfResults),
},
})
if err != nil {
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
switch r := fetchResponse.(type) {
case * momento.SortedSetFetchHit:
leaderboard:= make([]Player, 0, len(r.Elements))
for _, e := range r.Elements {
leaderboard = append(leaderboard, Player{
Name: string(e.Value),
Score: int(e.Score),
})
}
leaderboardResult, err := json.Marshal(leaderboard)
if err != nil {
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(leaderboardResult)
return
case * momento.SortedSetFetchMiss:
http.Error(w, "Game not found", http.StatusNotFound)
}
}
当社のGo SDKは、条件付きで結果の数を制限し、値が返される順序を設定する値を受け入れます。これは、リーダーボードを構築する簡単で拡張可能な方法を提供します!
超能力を制限するレート
ツリースラムのスーパーアビリティはかなり強力です。ワンクリックでドングリを木から直接たたき出すことができます。私たちは、みんながいつもそれをやっていることを望んでいるわけではないので、公平さと楽しさを保つために、1ゲームあたりの使用回数を3回に制限しています。
Sorted Setsでは、制限を追跡することができます。SDKのsortedSetGetScore
、sortedSetIncrement
、sortedSetRemove
コマンドを使用することで、スーパーアビリティの使用可能回数を更新し、0になったらソートされたセットからプレーヤーを削除することができます。 スーパーアビリティを使用するための次のエンドポイントを考えてみましょう。
// DELETE /games/{gameId}/super-ability
func handler(w http.ResponseWriter, r * http.Request) {
client:= getClient()
ctx:= r.Context()
setupCache(client, ctx)
gameId:= mux.Vars(r)["gameId"]
username:= mux.Vars(r)["username"]
// Find user in the rate limit cache for the game
fetchResponse, err := client.SortedSetGetScore(ctx, & momento.SortedSetGetScoreRequest{
CacheName: "super-abilities",
SetName: gameId,
ElementNames: []momento.Value{ momento.String(username) },
})
if err != nil {
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
switch set := fetchResponse.(type) {
case * momento.SortedSetGetScoreHit:
switch set.Elements[0].(type) {
case * momento.SortedSetScoreHit:
// Continue processing outside of switch
case * momento.SortedSetScoreMiss:
http.Error(w, "Out of super-ability uses", http.StatusConflict)
return
}
case * momento.SortedSetGetScoreMiss:
http.Error(w, "Game not found", http.StatusNotFound)
return
}
// Decrease the number of remaining super-ability usages
incrementResponse, err := client.SortedSetIncrementScore(ctx, & momento.SortedSetIncrementScoreRequest{
CacheName: "super-abilities",
SetName: gameId,
ElementName: momento.String(username),
Amount: -1,
})
if err != nil {
http.Error(w, "Game not found", http.StatusNotFound)
return
}
switch r := incrementResponse.(type) {
case * momento.SortedSetIncrementScoreSuccess:
// Remove the user from the sorted set if they have no more usages left
if r.Value <= 0 {
_, err := client.SortedSetRemove(ctx, & momento.SortedSetRemoveRequest{
CacheName: "super-abilities",
SetName: gameId,
ElementsToRemove: & momento.RemoveSomeElements{
Elements: []momento.Value{
momento.String(username),
},
},
})
if err != nil {
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
}
remainingSuperAbilities, err := json.Marshal(& SuperAbility{
Remaining: int(r.Value),
})
if err != nil {
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(remainingSuperAbilities)
return
default:
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
}
エンドポイントが呼ばれるたびに、選手のスコアがデクリメントされます。スコアが0になると、その選手はソートされたセットから削除されます。プレイヤーを調べる際にキャッシュミスが発生した場合、そのプレイヤーはスーパーアビリティから外れていると判断し、エラーを返します。各ラウンドの終了時にカウントをリセットし、ツリースラムの許容率を効果的に制限します。
その他のエキサイティングなこと
この2つはかなりクールな使用例でした。しかし、まだ終わってはいません。プレイヤーにリーダーボードでの現在の順位を、全体をフェッチすることなく表示する必要がありますか?sortedSetGetRank
を使えばいいのです!リーダーボード全体を0に戻したり、ラウンドごとにスーパーアビリティカウンターを元に戻したりするのはどうでしょう?sortedSetPut
コマンドを呼び出すだけで、すべてを上書きしてくれます。
もちろん、Sorted Sets全体をDELETE
することもできるます。しかし、これらはキャッシュされたアイテムであり、勝手に期限切れになることに注意してください!Sorted Sets、Momento Cache の他のデータ型と同様に、有効期限 (TTL) が必要です。Sorted Setsを作成または更新する際に TTL を指定しなかった場合は、 Momento Simple Cache Client のデフォルト値が使用されます。
もっと時間が必要ですか?Sorted Sets(または他のキャッシュ・アイテム)に変更が加えられたときに、有効期間をリセットするオプションがあります。これにより、アクティブでないアイテムを自動的に失効させ、まだ動いているアイテムを生かすことができます。
Sorted Setsを、あなたの特定のユースケースに自由に使ってください!これらの新しいキャッシュ可能なコレクションは、リーダーボードとレート制限だけに適用されるわけではありません!
利用可能なコマンドはすべて、Sorted Sets APIリファレンスに記載されています。
プレーする準備はできていますか?
あなたのことは知らないが、私は興奮しています。Sorted Sets は、あなたのキャッシュツールベルトに信じられないほど強力なツールを追加します。今日からGo SDKで利用できるようになりました。
別のプログラミング言語が必要ですか?Node.js、Python、PHP、.NET、Rust、Javaのサポートにご期待ください!
コレクション・データ・タイプは、皆様からのフィードバックの直接の結果として、開発者のエクスペリエンスを向上させるために作成されました!ぜひお試しいただき、ご意見をお聞かせください!