|
| 1 | +--- |
| 2 | +title: go-redis - go-redis v9 에서 hash 타입을 다루는 방법 |
| 3 | +layout: single |
| 4 | +author_profile: false |
| 5 | +read_time: true |
| 6 | +share: true |
| 7 | +related: true |
| 8 | +categories: |
| 9 | + - Redis |
| 10 | +tags: |
| 11 | + - Redis |
| 12 | + - Go-redis |
| 13 | +toc: false |
| 14 | +toc_stiky: true |
| 15 | +description: "" |
| 16 | +article_section: "" |
| 17 | +meta_keywords: "" |
| 18 | +completed: false |
| 19 | +visible: |
| 20 | +status: |
| 21 | +created: 2024-06-30T19:08 |
| 22 | +last_modified_at: 2024-07-07T18:15:23+09:00 |
| 23 | +thumnail_url: |
| 24 | +priority: |
| 25 | +--- |
| 26 | +# TL;DR |
| 27 | +- go-reds v9 버전부터 |
| 28 | + - hashset을 저장할때 interface로 전달하지 않고 type 을 전달해도 저장됩니다. |
| 29 | + - 역직렬화 과정에서도 scan을 통해 HGetAll, MGet 등을 type으로 unmarshal 할 수 있습니다. |
| 30 | +# Overview |
| 31 | +go-redis/v9 버전 이상에서 사용 가능한 기능들을 발견하여, 용례를 정리하고 성능 등 문제가 없는지 비교해봅니다. |
| 32 | +## Usage |
| 33 | +### go-redis/v8 이하 |
| 34 | +- HashSet |
| 35 | + - map interface를 만들어서 저장해야 한다. |
| 36 | + - type을 만들어서 전달할 경우 아래의 에러 메세지를 받는다. |
| 37 | + - `redis: can't marshal main.User (implement encoding.BinaryMarshaler)` |
| 38 | +```go |
| 39 | + |
| 40 | +func populateDataV8(rdb *redisv8.Client, n int) { |
| 41 | + for i := 1; i <= n; i++ { |
| 42 | + user := User{ |
| 43 | + Name: fmt.Sprintf("User%d", i), |
| 44 | + Age: 20 + (i % 30), |
| 45 | + Email: fmt.Sprintf("user%d@example.com", i), |
| 46 | + Country: "Country" + strconv.Itoa(i%10), |
| 47 | + } |
| 48 | + |
| 49 | + userMap := map[string]interface{}{ |
| 50 | + "name": user.Name, |
| 51 | + "age": user.Age, |
| 52 | + "email": user.Email, |
| 53 | + "country": user.Country, |
| 54 | + } |
| 55 | + |
| 56 | + err := rdb.HSet(ctx, fmt.Sprintf("user:%d", i), userMap).Err() |
| 57 | + if err != nil { |
| 58 | + log.Fatalf("Failed to set user %d: %v", i, err) |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +``` |
| 64 | + |
| 65 | +- HGetAll |
| 66 | + - map interface로 전달받은 값을 map을 참조하여 결과를 구성 |
| 67 | +```go |
| 68 | +func fetchDataV8(rdb *redisv8.Client, n int) { |
| 69 | + for i := 1; i <= n; i++ { |
| 70 | + fields, err := rdb.HGetAll(ctx, fmt.Sprintf("user:%d", i)).Result() |
| 71 | + if err != nil { |
| 72 | + log.Fatalf("Failed to get user %d: %v", i, err) |
| 73 | + } |
| 74 | + |
| 75 | + var user User |
| 76 | + user.Name = fields["name"] |
| 77 | + user.Age, _ = strconv.Atoi(fields["age"]) |
| 78 | + user.Email = fields["email"] |
| 79 | + user.Country = fields["country"] |
| 80 | + // Print for debug purpose, comment out during benchmarking |
| 81 | + // fmt.Printf("user:%d - %+v\n", i, user) |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +``` |
| 86 | + |
| 87 | + |
| 88 | +### go-redis/v9 이상 |
| 89 | +- HashSet |
| 90 | + - type으로 생성된 결과를 그대로 전달해도 저장된다. |
| 91 | + - string, int 여러 타입이 섞여 전달 될 수 있다. |
| 92 | +```go |
| 93 | + for i := 1; i <= n; i++ { |
| 94 | + user := User{ |
| 95 | + Name: fmt.Sprintf("User%d", i), |
| 96 | + Age: 20 + (i % 30), |
| 97 | + Email: fmt.Sprintf("user%d@example.com", i), |
| 98 | + Country: "Country" + strconv.Itoa(i%10), |
| 99 | + } |
| 100 | + |
| 101 | + err := rdb.HSet(ctx, fmt.Sprintf("user:%d", i), user).Err() |
| 102 | + if err != nil { |
| 103 | + log.Fatalf("Failed to set user %d: %v", i, err) |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | +``` |
| 108 | +- HGetAll |
| 109 | + - Scan 을 통해 타입에 맞도록 결과를 unmarshal 한다. |
| 110 | +```go |
| 111 | + |
| 112 | +func fetchDataV9(rdb *redisv9.Client, n int) { |
| 113 | + for i := 1; i <= n; i++ { |
| 114 | + res := rdb.HGetAll(ctx, fmt.Sprintf("user:%d", i)) |
| 115 | + |
| 116 | + var user User |
| 117 | + if err := res.Scan(&user); err != nil { |
| 118 | + log.Fatalf("Failed to get user %d: %v", i, err) |
| 119 | + } |
| 120 | + // Print for debug purpose, comment out during benchmarking |
| 121 | + // fmt.Printf("user:%d - %+v\n", i, user) |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +``` |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | + |
| 130 | + |
| 131 | +### Benchmark |
| 132 | +- go-redis v8, v9의 hash저장, 조회 관련된 operation 을 수행하는 동작을 테스트한 결과는 아래와 같습니다. |
| 133 | +- v9이 v8에 비해 성능면에서는 다소 느려진 점이 있으나, 무시해도 될만한 수준의 차이입니다. |
| 134 | +```go |
| 135 | +=== RUN BenchmarkPopulateDataV8 |
| 136 | +BenchmarkPopulateDataV8 |
| 137 | +BenchmarkPopulateDataV8-10 20 57818098 ns/op 99482 B/op 2010 allocs/op |
| 138 | +=== RUN BenchmarkPopulateDataV9 |
| 139 | +BenchmarkPopulateDataV9 |
| 140 | +BenchmarkPopulateDataV9-10 21 67450760 ns/op 69820 B/op 2213 allocs/op |
| 141 | +=== RUN BenchmarkFetchDataV8 |
| 142 | +BenchmarkFetchDataV8 |
| 143 | +BenchmarkFetchDataV8-10 18 60724053 ns/op 67652 B/op 1716 allocs/op |
| 144 | +=== RUN BenchmarkFetchDataV9 |
| 145 | +BenchmarkFetchDataV9 |
| 146 | +BenchmarkFetchDataV9-10 18 62911891 ns/op 71903 B/op 2030 allocs/op |
| 147 | +``` |
| 148 | + |
| 149 | +## Conclusion |
| 150 | +redis에 데이터를 저장할때, type을 지정해두고 값을 저장, 조회한다면 가독성과 코드 구조가 많이 개선될 수 있을 것 같습니다. |
| 151 | +무조건 버전을 올리기보다, 필요한 기능으로 적용이 필요할때 올리면 좋을 것 같습니다. |
| 152 | +# References |
| 153 | +- [https://github.com/redis/go-redis/issues/672](https://github.com/redis/go-redis/issues/672) |
| 154 | +- https://github.com/redis/go-redis/blob/f8cbf483f4a193d441fac2cf14be3d84783848c6/example_test.go#L281 |
| 155 | +- https://github.com/redis/go-redis/discussions/2454 |
| 156 | +- https://github.com/redis/go-redis |
0 commit comments