12/08/2020 - GO, REDIS
In this example we are going to use Redis Hashes to store Go structs in cache. Although hash data types mainly represent objects, you should use hashes when possible. They also take very little space.
Our example is not meant to make perfect sense. The point is to show you, how a multidimensional struct could be stored and retrieved in Redis.
{
"ID": "c4ca4238a0b923820dcc509a6f75849b",
"User": {
"ID": "user-1",
"Name": "Robert De Niro",
"Permissions": [
"Read",
"Write"
]
}
}
├── docker-compose.yaml
└── internal
├── domain
│ ├── auth
│ │ ├── cache.go
│ │ ├── permission.go
│ │ ├── token.go
│ │ └── user.go
│ └── repository
│ └── auth.go
└── storage
├── auth.go
└── auth_test.go
version: "3"
services:
redis:
image: redis:6.0.6-alpine
command: redis-server --requirepass pass
ports:
- 6379:6379
package auth
func CacheHashKey(tokenID string) string {
return "app:auth:" + tokenID
}
func CacheHashField() string {
return "token"
}
package auth
type Permission string
const (
PermissionRead Permission = "Read"
PermissionWrite Permission = "Write"
)
package auth
import "encoding/json"
type Token struct {
ID string
User User
}
func (t *Token) MarshalBinary() ([]byte, error) {
return json.Marshal(t)
}
func (t *Token) UnmarshalBinary(data []byte) error {
if err := json.Unmarshal(data, &t); err != nil {
return err
}
return nil
}
package auth
type User struct {
ID string
Name string
Permissions []Permission
}
package repository
import (
"context"
"github.com/inanzzz/cache/internal/domain/auth"
)
type Auth interface {
Create(ctx context.Context, token auth.Token) error
Find(ctx context.Context, tokenID string) (*auth.Token, error)
Update(ctx context.Context, token auth.Token) error
Delete(ctx context.Context, tokenID string) error
}
package storage
import (
"context"
"fmt"
"time"
"github.com/inanzzz/cache/internal/domain/auth"
"github.com/go-redis/redis/v8"
)
type Auth struct {
rds *redis.Client
}
func NewAuth(rds *redis.Client) Auth {
return Auth{rds: rds}
}
func (a Auth) Create(ctx context.Context, token auth.Token) error {
if _, err := a.rds.HSetNX(ctx, auth.CacheHashKey(token.ID), auth.CacheHashField(), &token).Result(); err != nil {
return fmt.Errorf("create: redis error: %w", err)
}
a.rds.Expire(ctx, auth.CacheHashKey(token.ID), time.Minute)
return nil
}
func (a Auth) Find(ctx context.Context, tokenID string) (*auth.Token, error) {
result, err := a.rds.HGet(ctx, auth.CacheHashKey(tokenID), auth.CacheHashField()).Result()
if err != nil && err != redis.Nil {
return nil, fmt.Errorf("find: redis error: %w", err)
}
if result == "" {
return nil, fmt.Errorf("find: not found")
}
token := &auth.Token{}
if err := token.UnmarshalBinary([]byte(result)); err != nil {
return nil, fmt.Errorf("find: unmarshal error: %w", err)
}
return token, nil
}
func (a Auth) Update(ctx context.Context, token auth.Token) error {
// Find token: a.rds.HGet()
// Override token: a.rds.HSet()
return nil
}
func (a Auth) Delete(ctx context.Context, tokenID string) error {
// Find token: a.rds.HGet()
// Delete token: a.rds.Del()
return nil
}
package storage
import (
"context"
"fmt"
"testing"
"github.com/inanzzz/cache/internal/domain/auth"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
)
var rds *redis.Client
func init() {
rds = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "pass",
})
}
func newToken(tokenID string) auth.Token {
return auth.Token{
ID: tokenID,
User: auth.User{
ID: "user-1",
Name: "Robert De Niro",
Permissions: []auth.Permission{
auth.PermissionRead,
auth.PermissionWrite,
},
},
}
}
func TestAuth_Create(t *testing.T) {
storage := NewAuth(rds)
assert.NoError(t, storage.Create(context.Background(), newToken("d3d9446802a44259755d38e6d163e820")))
}
func TestAuth_Find_Error(t *testing.T) {
storage := NewAuth(rds)
assert.NoError(t, storage.Create(context.Background(), newToken("98f13708210194c475687be6106a3b84")))
token, err := storage.Find(context.Background(), "unknown-token-id")
assert.Nil(t, token)
assert.Error(t, fmt.Errorf("find: not found"), err)
}
func TestAuth_Find_Success(t *testing.T) {
storage := NewAuth(rds)
assert.NoError(t, storage.Create(context.Background(), newToken("34173cb38f07f89ddbebc2ac9128303f")))
token, err := storage.Find(context.Background(), "34173cb38f07f89ddbebc2ac9128303f")
assert.NoError(t, err)
assert.IsType(t, &auth.Token{}, token)
}
localhost:6379> KEYS *
1) "app:auth:34173cb38f07f89ddbebc2ac9128303f"
localhost:6379> HGETALL app:auth:34173cb38f07f89ddbebc2ac9128303f
1) "token"
2) "{\"ID\":\"34173cb38f07f89ddbebc2ac9128303f\",\"User\":{\"ID\":\"user-1\",\"Name\":\"Robert De Niro\",\"Permissions\":[\"Read\",\"Write\"]}}"
localhost:6379> HGET app:auth:34173cb38f07f89ddbebc2ac9128303f token
"{\"ID\":\"34173cb38f07f89ddbebc2ac9128303f\",\"User\":{\"ID\":\"user-1\",\"Name\":\"Robert De Niro\",\"Permissions\":[\"Read\",\"Write\"]}}"