Sorted Set에서 요소를 임의로 가져오는 방법
개요
웹 서비스를 개발하면'어떤 조건을 충족하는 집합을 무작위로 얻고 싶다'는 조건이 간혹 있을 수 있다.
개인개발이라면 RDB가 두드리는 SQL에 무리하게 노력하면 방법이 있을 거예요.
"임의성을 유지하면서 성능을 해치지 않습니다"라는 필수 조건이 있을 때 NosQL 데이터베이스는 후보 데이터베이스로 상승합니다.
이번에는 NoSQL 데이터를 바탕으로 Redis를 통해 구현되는 방법에 대해 쓰겠습니다.
전제 조건
이번 예의 필요조건으로
모든 사용자에게는 Level이 있습니다.
자신의 Level에서 +-10까지 랜덤으로 10개의 UserID를 획득합니다.
Redis에 추가
Redis의 Sorted Set에 추가할 경우
score: {Level}랜덤 5비트}
member: userID
로그인
Sorted Set의 사양에서는 동일한 Score의 경우 ASCII 코드 순으로 진행되므로 마지막에 {랜덤 5비트}를 추가하여 동일한 Score도 랜덤으로 만들 수 있습니다.
add.go
const levelRandomRange = 100000
func Add(ctx context.Context, userID string,myLevel int32) error {
// DIした方が良いけど、ここでは簡略化の為適宜Newを行う
redisClient := redis.NewClient(&redis.Options{})
score := createSearchScore(myLevel)
zMember := &redis.Z{
Score: score,
Member: userID,
}
if cmd := redisClient.ZAdd(ctx, "search", zMember);cmd.Err() != nil {
return err
}
return nil
}
func createSearchScore(level int32) float64 {
return float64(int(level)*levelRandomRange + rand.Intn(levelRandomRange))
}
Redis에서 찾기
우선 자신의 수준 이하의 범위, 자신의 수준 이상의 범위에서 랜덤으로 기준이 되는 등급을 결정한다.
minLevel,maxLevel := getMinAndMaxLv(myLevel)
minTargetLevel := rand.Intn(myLevel - minLevel + 1) + minLevel
maxTargetLevel := rand.Intn(maxLevel - myLevel + 1) + myLevel
func getMinAndMaxLv(myLevel int32) (int32, int32) {
minLevel := myLevel - 10
maxLevel := myLevel + 10
// 下限と上限を超えてたらそれぞれ丸める
if minLevel < 1 {
minLevel = 1
}
if maxLevel < 1 {
maxLevel = 1
}
if minLevel > 100 {
minLevel = 100
}
if maxLevel > 100 {
maxLevel = 100
}
return minMlv, maxMlv
}
다음에 각 범위 내에서 승차순, 하강순으로 검색할지 여부를 확정한다.예:
(원래는 40~48 레벨이 될 정도였다)
따라서 랜덤으로 결정되는 기준 등급은 범위 내의 절반 이하 또는 이상으로 승차순과 강차를 구분한다.
이렇게 하면 빠뜨리는 상황을 줄일 수 있다.
lowerInfo := getLowerSearchScoreInfo(minLevel, minTargetLevel, myLevel)
higherInfo := getHigherSearchScoreInfo(maxLevel, maxTargetLevel, myLevel)
type searchScoreInfo struct {
minScore float64
maxScore float64
isDesc bool
}
func getLowerSearchScoreInfo(minLevel, targetLevel, myLevel int32) searchScoreInfo {
/*
自分のLevel: 50
targetLevelが45 ~ 50なら、minLevel ← targetLevelの降順
targetLevelが40 ~ 44なら、targetLevel → myLevelの昇順
*/
isDesc := (targetLevel - minLevel) >= 5
var min float64
var max float64
if isDesc {
min = createSearchScore(minLevel)
max = createSearchScore(targetLevel)
} else {
min = createSearchScore(targetLevel)
max = createSearchScore(myLevel)
}
return searchScoreInfo {
minScore: min
maxScore: max
isDesc: isDesc,
}
}
func getHigherSearchScoreInfo(maxLevel, targetLevel, myLevel int32) searchScoreInfo {
/*
自分のLevel: 50
targetLevelが55 ~ 60なら、myLevel ← targetLevelの降順
targetLevelが50 ~ 54なら、targetLevel → maxLevelの昇順
*/
isDesc := (maxLevel - targetLevel) <= 5
var min float64
var max float64
if isDesc {
min = createSearchScore(myLevel)
max = createSearchScore(targetLevel)
} else {
min = createSearchScore(targetLevel)
max = createSearchScore(maxLevel)
}
return searchScoreInfo {
minScore: min
maxScore: max
isDesc: isDesc,
}
}
준비가 되었으므로 Redis를 방문합니다.5대 5로 하다.
// Redisにアクセス
lowerIDs := rangeByScore(lowerInfo.minScore, lowerInfo.maxScore, 10,lowerInfo.isDesc)
higherIDs := rangeByScore(higherInfo.minScore, higherInfo.maxScore, 10,higherInfo.isDesc)
// 5対5になるように仕分けてappend
lowerTargetIDs, lowerSurplusIDs := assortIDs(lowerIDs)
higherTargetIDs, higherSurplusIDs := assortIDs(higherIDs)
targetUserIDs := make([]string, 0, 10)
targetUserIDs = append(targetUserIDs, lowerTargetIDs...)
targetUserIDs = append(targetUserIDs, higherTargetIDs...)
// 表示件数に足りなければ、下か上の範囲で余っている方で補う
if len(targetUserIDs) < 10 {
surplusUserIDs := make([]string, 0, len(lowerSurplusIDs)+len(higherSurplusIDs))
surplusUserIDs = append(surplusUserIDs, lowerTargetIDs...)
surplusUserIDs = append(surplusUserIDs, higherTargetIDs...)
for _, id := range surplusUserIDs {
targetUserIDs = append(targetUserIDs, id)
if len(targetUserIDs) < 10 {
break
}
}
}
func rangeByScore(min,max,count int64, isDesc bool) ([]string,error) {
// DIした方が良いけど、ここでは簡略化の為適宜Newを行う
redisClient := redis.NewClient(&redis.Options{})
rangeBy := &redis.ZRangeBy{
Min: strconv.FormatInt(min, 10)
Max: strconv.FormatInt(max, 10)
Offset: 0,
Count: count,
}
var cmd *redis.StringSliceCmd
if isDesc {
cmd = redisClient.ZRevRangeByScore(ctx, key, rangeBy)
} else {
cmd = s.redisClient.ZRangeByScore(ctx, key, rangeBy)
}
if cmd.Err() != nil {
return err
}
return cmd.Val()
}
func assortIDs(ids []string) ([]string, []string) {
targetUserIDs := make([]string, 0, 5)
surplusUserIDs := make([]string, 0, 5)
for _, id := range ids {
if len(targetUserIDs) < 5 {
targetUserIDs = append(targetUserIDs, id)
} else {
surplusUserIDs = append(surplusUserIDs, id)
}
}
return targetUserIDs, surplusUserIDs
}
의 마지막 총결은 다음과 같다.search.go
search.go
const levelRandomRange = 100000
func Add(ctx context.Context, userID string,myLevel int32) error {
// DIした方が良いけど、ここでは簡略化の為適宜Newを行う
redisClient := redis.NewClient(&redis.Options{})
score := createSearchScore(myLevel)
zMember := &redis.Z{
Score: score,
Member: userID,
}
if cmd := redisClient.ZAdd(ctx, "search", zMember);cmd.Err() != nil {
return err
}
return nil
}
func createSearchScore(level int32) float64 {
return float64(int(level)*levelRandomRange + rand.Intn(levelRandomRange))
}
type searchScoreInfo struct {
minScore float64
maxScore float64
isDesc bool
}
func Search(ctx context.Context, userID string, myLevel int32) error {
minLevel,maxLevel := getMinAndMaxLv(myLevel)
// 自分のレベルより下の範囲、自分のレベルより上の範囲で、それぞれ基準とするレベルを決める。
minTargetLevel := rand.Intn(myLevel - minLevel + 1) + minLevel
maxTargetLevel := rand.Intn(maxLevel - myLevel + 1) + myLevel
// それぞれの範囲で昇順、降順で検索するかを確定する。
lowerInfo := getLowerSearchScoreInfo(minLevel, minTargetLevel, myLevel)
higherInfo := getHigherSearchScoreInfo(maxLevel, maxTargetLevel, myLevel)
// Redisにアクセス
lowerIDs := rangeByScore(lowerInfo.minScore, lowerInfo.maxScore, 10,lowerInfo.isDesc)
higherIDs := rangeByScore(higherInfo.minScore, higherInfo.maxScore, 10,higherInfo.isDesc)
// 5対5になるように仕分けてappend
lowerTargetIDs, lowerSurplusIDs := assortIDs(lowerIDs)
higherTargetIDs, higherSurplusIDs := assortIDs(higherIDs)
targetUserIDs := make([]string, 0, 10)
targetUserIDs = append(targetUserIDs, lowerTargetIDs...)
targetUserIDs = append(targetUserIDs, higherTargetIDs...)
// 表示件数に足りなければ、下か上の範囲で余っている方で補う
if len(targetUserIDs) < 10 {
surplusUserIDs := make([]string, 0, len(lowerSurplusIDs)+len(higherSurplusIDs))
surplusUserIDs = append(surplusUserIDs, lowerTargetIDs...)
surplusUserIDs = append(surplusUserIDs, higherTargetIDs...)
for _, id := range surplusUserIDs {
targetUserIDs = append(targetUserIDs, id)
if len(targetUserIDs) < 10 {
break
}
}
}
return nil
}
func getMinAndMaxLv(myLevel int32) (int32, int32) {
minLevel := myLevel - 10
maxLevel := myLevel + 10
// 下限と上限を超えてたらそれぞれ丸める
if minLevel < 1 {
minLevel = 1
}
if maxLevel < 1 {
maxLevel = 1
}
if minLevel > 100 {
minLevel = 100
}
if maxLevel > 100 {
maxLevel = 100
}
return minMlv, maxMlv
}
func getLowerSearchScoreInfo(minLevel, targetLevel, myLevel int32) searchScoreInfo {
/*
自分のLevel: 50
targetLevelが45 ~ 50なら、minLevel ← targetLevelの降順
targetLevelが40 ~ 44なら、targetLevel → myLevelの昇順
*/
isDesc := (targetLevel - minLevel) >= 5
var min float64
var max float64
if isDesc {
min = createSearchScore(minLevel)
max = createSearchScore(targetLevel)
} else {
min = createSearchScore(targetLevel)
max = createSearchScore(myLevel)
}
return searchScoreInfo {
minScore: min
maxScore: max
isDesc: isDesc,
}
}
func getHigherSearchScoreInfo(maxLevel, targetLevel, myLevel int32) searchScoreInfo {
/*
自分のLevel: 50
targetLevelが55 ~ 60なら、myLevel ← targetLevelの降順
targetLevelが50 ~ 54なら、targetLevel → maxLevelの昇順
*/
isDesc := (maxLevel - targetLevel) <= 5
var min float64
var max float64
if isDesc {
min = createSearchScore(myLevel)
max = createSearchScore(targetLevel)
} else {
min = createSearchScore(targetLevel)
max = createSearchScore(maxLevel)
}
return searchScoreInfo {
minScore: min
maxScore: max
isDesc: isDesc,
}
}
func rangeByScore(min,max,count int64, isDesc bool) ([]string,error) {
// DIした方が良いけど、ここでは簡略化の為適宜Newを行う
redisClient := redis.NewClient(&redis.Options{})
rangeBy := &redis.ZRangeBy{
Min: strconv.FormatInt(min, 10)
Max: strconv.FormatInt(max, 10)
Offset: 0,
Count: count,
}
var cmd *redis.StringSliceCmd
if isDesc {
cmd = redisClient.ZRevRangeByScore(ctx, key, rangeBy)
} else {
cmd = s.redisClient.ZRangeByScore(ctx, key, rangeBy)
}
if cmd.Err() != nil {
return err
}
return cmd.Val()
}
func assortIDs(ids []string) ([]string, []string) {
targetUserIDs := make([]string, 0, 5)
surplusUserIDs := make([]string, 0, 5)
for _, id := range ids {
if len(targetUserIDs) < 5 {
targetUserIDs = append(targetUserIDs, id)
} else {
surplusUserIDs = append(surplusUserIDs, id)
}
}
return targetUserIDs, surplusUserIDs
}
Reference
이 문제에 관하여(Sorted Set에서 요소를 임의로 가져오는 방법), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/mitsugu/articles/276e038f785a81텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)