Bu örnekte, Go yapılarını önbellekte saklamak için Redis Hashes'ı kullanacağız. Hash veri türleri esas olarak nesneleri temsil etse de, mümkün olduğunca Hash'ları kullanmalısınız. Ayrıca çok az yer kaplarlar.


Örneğimizin mükemmel bir anlam ifade etmesi amaçlanmamıştır. Önemli olan size, çok boyutlu bir yapının Redis'te nasıl saklanıp geri alınabileceğini göstermektir.


Yapı


{
"ID": "c4ca4238a0b923820dcc509a6f75849b",
"User": {
"ID": "user-1",
"Name": "Robert De Niro",
"Permissions": [
"Read",
"Write"
]
}
}

Uygulama yapısı


├── docker-compose.yaml
└── internal
├── domain
│   ├── auth
│   │   ├── cache.go
│   │   ├── permission.go
│   │   ├── token.go
│   │   └── user.go
│   └── repository
│   └── auth.go
└── storage
├── auth.go
└── auth_test.go

Dosyalar


docker-compose.yaml


version: "3"

services:
redis:
image: redis:6.0.6-alpine
command: redis-server --requirepass pass
ports:
- 6379:6379

internal/domain/auth/cache.go


package auth

func CacheHashKey(tokenID string) string {
return "app:auth:" + tokenID
}

func CacheHashField() string {
return "token"
}

internal/domain/auth/permission.go


package auth

type Permission string

const (
PermissionRead Permission = "Read"
PermissionWrite Permission = "Write"
)

internal/domain/auth/token.go


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
}

internal/domain/auth/user.go


package auth

type User struct {
ID string
Name string
Permissions []Permission
}

internal/domain/repository/auth.go


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
}

internal/storage/auth.go


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
}

internal/storage/auth_test.go


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)
}

Redis CLI


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\"]}}"