refactor cache

This commit is contained in:
mzfarshad 2026-05-04 20:23:01 +03:30
parent dbe9cb7df8
commit 6ca9fd1645
7 changed files with 231 additions and 25 deletions

View File

@ -5,8 +5,9 @@ redis:
db: 0 db: 0
repo: repo:
kart_key_prefix: "shopping-basket-cart:" cache_kart_key_prefix: "shopping-basket-cart:user_id"
ttl: 3600s cache_ttl: 864000s # 24h
mysql_ttl: 604800s # 7d
http_server: http_server:
host: "localhost" host: "localhost"

View File

@ -0,0 +1,209 @@
package repository
import (
"context"
"encoding/json"
"fmt"
adapter "git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/redis/go-redis/v9"
"strconv"
"time"
)
type Cache struct {
client *adapter.Adapter
config Config
}
func NewCache(client *adapter.Adapter, cfg Config) Cache {
return Cache{client: client, config: cfg}
}
func (c Cache) cartKey(userID types.ID) string {
return fmt.Sprintf("%s:%d", c.config.CacheKartKeyPrefix, userID)
}
func (c Cache) itemKey(itemID types.ID) string {
return fmt.Sprintf("item:%d", itemID)
}
func (c Cache) kartIDsKey(userID types.ID) string {
return fmt.Sprintf("cart:%d:items", userID)
}
func (c Cache) upsertCart(ctx context.Context, cart entity.Cart) error {
cartKey := c.cartKey(cart.UserID)
cartIDsKey := c.kartIDsKey(cart.UserID)
pipe := c.client.Client().Pipeline()
pipe.HSet(ctx, cartKey, map[string]interface{}{
IDField: cart.ID,
UserIDField: cart.UserID,
StatusField: cart.Status,
TotalPriceField: cart.TotalPrice,
ExpireAtField: cart.ExpireAt,
CreatedAtField: cart.CreatedAt,
UpdatedAtField: cart.UpdatedAt,
})
pipe.Expire(ctx, cartKey, c.config.CacheTTL)
pipe.Del(ctx, cartIDsKey)
for _, i := range cart.Items {
pipe.SAdd(ctx, cartIDsKey, i.ID)
itemKey := c.itemKey(i.ID)
jsonItem, err := json.Marshal(i)
if err != nil {
return richerror.New(richerror.Op(op("cache.upsertCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
pipe.Set(ctx, itemKey, string(jsonItem), c.config.CacheTTL)
}
pipe.Expire(ctx, cartIDsKey, c.config.CacheTTL)
_, err := pipe.Exec(ctx)
if err != nil {
return richerror.New(richerror.Op(op("cache.upsertCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}
func (c Cache) getCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
cartKey := c.cartKey(userID)
cartIDsKey := c.kartIDsKey(userID)
pipe := c.client.Client().Pipeline()
resCart := pipe.HGetAll(ctx, cartKey)
resIDs := pipe.SMembers(ctx, cartIDsKey)
_, err := pipe.Exec(ctx)
if err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
result := resCart.Val()
if len(result) < 1 {
return entity.Cart{}, nil
}
cart, err := parseCartFromRedis(result)
if err != nil {
return entity.Cart{}, err
}
if cart.ID < 1 {
return entity.Cart{}, nil
}
ids := resIDs.Val()
if len(ids) < 1 {
return cart, nil
}
itemPipe := c.client.Client().Pipeline()
itemResults := make([]*redis.StringCmd, len(ids))
for i, idStr := range ids {
id, _ := strconv.Atoi(idStr)
itemKey := c.itemKey(types.ID(id))
cmd := itemPipe.Get(ctx, itemKey)
itemResults[i] = cmd
}
if _, err := itemPipe.Exec(ctx); err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
for _, cmd := range itemResults {
if err := cmd.Err(); err != nil {
if err == redis.Nil {
continue
}
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
var item entity.Item
if err := json.Unmarshal([]byte(cmd.Val()), &item); err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
cart.Items = append(cart.Items, item)
}
return cart, nil
}
func parseCartFromRedis(result map[string]string) (entity.Cart, error) {
if len(result) == 0 {
return entity.Cart{}, nil
}
var cart entity.Cart
cart.Items = []entity.Item{}
for key, val := range result {
switch key {
case IDField:
id, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
cart.ID = types.ID(id)
case UserIDField:
uid, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
cart.UserID = types.ID(uid)
case StatusField:
cart.Status = entity.CartStatus(val)
case TotalPriceField:
price, err := strconv.ParseFloat(val, 64)
if err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
cart.TotalPrice = price
case ExpireAtField, CreatedAtField, UpdatedAtField:
t, err := time.Parse(time.RFC3339, val)
if err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
switch key {
case ExpireAtField:
cart.ExpireAt = t
case CreatedAtField:
cart.CreatedAt = t
case UpdatedAtField:
cart.UpdatedAt = t
}
}
}
return cart, nil
}

View File

@ -13,17 +13,11 @@ import (
"time" "time"
) )
const (
FieldNumber = 5
UserIDField = "user_id"
CreatedAtField = "created_at"
ExpireAtField = "expire_at"
TotalPriceField = "total_price"
)
type Config struct { type Config struct {
KartKeyPrefix string `koanf:"kart_key_prefix"` CacheKartKeyPrefix string `koanf:"cache_kart_key_prefix"`
TTL time.Duration `koanf:"ttl"` CacheTTL time.Duration `koanf:"cache_ttl"`
MysqlTTL time.Duration `koanf:"mysql_ttl"`
} }
type Repo struct { type Repo struct {
@ -39,14 +33,6 @@ func op(s string) string {
return fmt.Sprintf("shoppingbasketapp-repository-", s) return fmt.Sprintf("shoppingbasketapp-repository-", s)
} }
func (r Repo) cartKey(userID types.ID) string {
return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID)
}
func (r Repo) itemKey(productID types.ID) string {
return fmt.Sprintf("item:%d", productID)
}
func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error { func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error {
const op = "shoppingbasketapp.repository.AddItem" const op = "shoppingbasketapp.repository.AddItem"
@ -64,7 +50,7 @@ func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) er
itemKey: string(itemJson), itemKey: string(itemJson),
TotalPriceField: item.Price * types.Price(item.Quantity), TotalPriceField: item.Price * types.Price(item.Quantity),
CreatedAtField: now, CreatedAtField: now,
ExpireAtField: now + r.config.TTL.Nanoseconds(), ExpireAtField: now + r.config.CacheTTL.Nanoseconds(),
}) })
} else { } else {
existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result() existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result()
@ -79,13 +65,13 @@ func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) er
} }
r.client.HSet(ctx, cartKey, itemKey, string(itemJson)) r.client.HSet(ctx, cartKey, itemKey, string(itemJson))
r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.TTL.Nanoseconds()) r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.CacheTTL.Nanoseconds())
if err := r.updateTotalPrice(ctx, cartKey); err != nil { if err := r.updateTotalPrice(ctx, cartKey); err != nil {
return err return err
} }
} }
r.client.Expire(ctx, cartKey, r.config.TTL) r.client.Expire(ctx, cartKey, r.config.CacheTTL)
return nil return nil
} }

View File

@ -16,3 +16,13 @@ var (
ErrNotExistsCart = errors.New("not exists cart") ErrNotExistsCart = errors.New("not exists cart")
ErrExpiredCart = errors.New("expired cart") ErrExpiredCart = errors.New("expired cart")
) )
const (
IDField = "id"
UserIDField = "user_id"
StatusField = "status"
TotalPriceField = "total_price"
CreatedAtField = "created_at"
ExpireAtField = "expire_at"
UpdatedAtField = "updated_at"
)

View File

@ -129,7 +129,7 @@ func (d DB) addToBasket(ctx context.Context, userID types.ID, item entity.Item)
TotalPrice: item.Price * float64(item.Quantity), TotalPrice: item.Price * float64(item.Quantity),
Status: entity.CartStatusActive, Status: entity.CartStatusActive,
CreatedAt: time.Now().UTC(), CreatedAt: time.Now().UTC(),
ExpireAt: time.Now().Add(d.config.TTL).UTC(), ExpireAt: time.Now().Add(d.config.MysqlTTL).UTC(),
} }
result, err := tx.ExecContext(ctx, cartQuery, result, err := tx.ExecContext(ctx, cartQuery,

View File

@ -1 +0,0 @@
package repository

View File

@ -15,6 +15,7 @@ type Repository interface {
DeleteItem(ctx context.Context, cartID, itemID types.ID) error DeleteItem(ctx context.Context, cartID, itemID types.ID) error
UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error
DeleteCart(ctx context.Context, cartID types.ID) error DeleteCart(ctx context.Context, cartID types.ID) error
UpdateStatus(ctx context.Context, cartID types.ID, status entity.CartStatus) error
} }
type Service struct { type Service struct {