go-redis - go-redis v9 에서 hash 타입을 다루는 방법

4 분 소요

TL;DR

  • go-reds v9 버전부터
    • hashset을 저장할때 interface로 전달하지 않고 type 을 전달해도 저장됩니다.
    • 역직렬화 과정에서도 scan을 통해 HGetAll, MGet 등을 type으로 unmarshal 할 수 있습니다.

      Overview

      go-redis/v9 버전 이상에서 사용 가능한 기능들을 발견하여, 용례를 정리하고 성능 등 문제가 없는지 비교해봅니다.

      Usage

      go-redis/v8 이하

  • HashSet
    • map interface를 만들어서 저장해야 한다.
    • type을 만들어서 전달할 경우 아래의 에러 메세지를 받는다.
      • redis: can't marshal main.User (implement encoding.BinaryMarshaler) ```go

func populateDataV8(rdb *redisv8.Client, n int) { for i := 1; i <= n; i++ { user := User{ Name: fmt.Sprintf(“User%d”, i), Age: 20 + (i % 30), Email: fmt.Sprintf(“user%d@example.com”, i), Country: “Country” + strconv.Itoa(i%10), }

	userMap := map[string]interface{}{
		"name":    user.Name,
		"age":     user.Age,
		"email":   user.Email,
		"country": user.Country,
	}

	err := rdb.HSet(ctx, fmt.Sprintf("user:%d", i), userMap).Err()
	if err != nil {
		log.Fatalf("Failed to set user %d: %v", i, err)
	}
} }

- HGetAll
	- map interface로 전달받은 값을 map을 참조하여 결과를 구성
```go
func fetchDataV8(rdb *redisv8.Client, n int) {
	for i := 1; i <= n; i++ {
		fields, err := rdb.HGetAll(ctx, fmt.Sprintf("user:%d", i)).Result()
		if err != nil {
			log.Fatalf("Failed to get user %d: %v", i, err)
		}

		var user User
		user.Name = fields["name"]
		user.Age, _ = strconv.Atoi(fields["age"])
		user.Email = fields["email"]
		user.Country = fields["country"]
		// Print for debug purpose, comment out during benchmarking
		// fmt.Printf("user:%d - %+v\n", i, user)
	}
}

go-redis/v9 이상

  • HashSet
    • type으로 생성된 결과를 그대로 전달해도 저장된다.
    • string, int 여러 타입이 섞여 전달 될 수 있다. ```go for i := 1; i <= n; i++ { user := User{ Name: fmt.Sprintf(“User%d”, i), Age: 20 + (i % 30), Email: fmt.Sprintf(“user%d@example.com”, i), Country: “Country” + strconv.Itoa(i%10), }

      err := rdb.HSet(ctx, fmt.Sprintf(“user:%d”, i), user).Err() if err != nil { log.Fatalf(“Failed to set user %d: %v”, i, err) } }

- HGetAll
	- Scan 을 통해 타입에 맞도록 결과를 unmarshal 한다.
```go

func fetchDataV9(rdb *redisv9.Client, n int) {
	for i := 1; i <= n; i++ {
		res := rdb.HGetAll(ctx, fmt.Sprintf("user:%d", i))

		var user User
		if err := res.Scan(&user); err != nil {
			log.Fatalf("Failed to get user %d: %v", i, err)
		}
		// Print for debug purpose, comment out during benchmarking
		// fmt.Printf("user:%d - %+v\n", i, user)
	}
}

Benchmark

  • go-redis v8, v9의 hash저장, 조회 관련된 operation 을 수행하는 동작을 테스트한 결과는 아래와 같습니다.
  • v9이 v8에 비해 성능면에서는 다소 느려진 점이 있으나, 무시해도 될만한 수준의 차이입니다.
    === RUN   BenchmarkPopulateDataV8
    BenchmarkPopulateDataV8
    BenchmarkPopulateDataV8-10            20          57818098 ns/op           99482 B/op       2010 allocs/op
    === RUN   BenchmarkPopulateDataV9
    BenchmarkPopulateDataV9
    BenchmarkPopulateDataV9-10            21          67450760 ns/op           69820 B/op       2213 allocs/op
    === RUN   BenchmarkFetchDataV8
    BenchmarkFetchDataV8
    BenchmarkFetchDataV8-10               18          60724053 ns/op           67652 B/op       1716 allocs/op
    === RUN   BenchmarkFetchDataV9
    BenchmarkFetchDataV9
    BenchmarkFetchDataV9-10               18          62911891 ns/op           71903 B/op       2030 allocs/op
    

Conclusion

redis에 데이터를 저장할때, type을 지정해두고 값을 저장, 조회한다면 가독성과 코드 구조가 많이 개선될 수 있을 것 같습니다. 무조건 버전을 올리기보다, 필요한 기능으로 적용이 필요할때 올리면 좋을 것 같습니다.

References

  • https://github.com/redis/go-redis/issues/672
  • https://github.com/redis/go-redis/blob/f8cbf483f4a193d441fac2cf14be3d84783848c6/example_test.go#L281
  • https://github.com/redis/go-redis/discussions/2454
  • https://github.com/redis/go-redis