forked from ebhomengo/niki
refactor cache
This commit is contained in:
parent
dbe9cb7df8
commit
6ca9fd1645
|
|
@ -5,8 +5,9 @@ redis:
|
|||
db: 0
|
||||
|
||||
repo:
|
||||
kart_key_prefix: "shopping-basket-cart:"
|
||||
ttl: 3600s
|
||||
cache_kart_key_prefix: "shopping-basket-cart:user_id"
|
||||
cache_ttl: 864000s # 24h
|
||||
mysql_ttl: 604800s # 7d
|
||||
|
||||
http_server:
|
||||
host: "localhost"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -13,17 +13,11 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
FieldNumber = 5
|
||||
UserIDField = "user_id"
|
||||
CreatedAtField = "created_at"
|
||||
ExpireAtField = "expire_at"
|
||||
TotalPriceField = "total_price"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
KartKeyPrefix string `koanf:"kart_key_prefix"`
|
||||
TTL time.Duration `koanf:"ttl"`
|
||||
CacheKartKeyPrefix string `koanf:"cache_kart_key_prefix"`
|
||||
CacheTTL time.Duration `koanf:"cache_ttl"`
|
||||
|
||||
MysqlTTL time.Duration `koanf:"mysql_ttl"`
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
|
|
@ -39,14 +33,6 @@ func op(s string) string {
|
|||
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 {
|
||||
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),
|
||||
TotalPriceField: item.Price * types.Price(item.Quantity),
|
||||
CreatedAtField: now,
|
||||
ExpireAtField: now + r.config.TTL.Nanoseconds(),
|
||||
ExpireAtField: now + r.config.CacheTTL.Nanoseconds(),
|
||||
})
|
||||
} else {
|
||||
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, 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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.client.Expire(ctx, cartKey, r.config.TTL)
|
||||
r.client.Expire(ctx, cartKey, r.config.CacheTTL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,3 +16,13 @@ var (
|
|||
ErrNotExistsCart = errors.New("not exists 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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ func (d DB) addToBasket(ctx context.Context, userID types.ID, item entity.Item)
|
|||
TotalPrice: item.Price * float64(item.Quantity),
|
||||
Status: entity.CartStatusActive,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
package repository
|
||||
|
|
@ -15,6 +15,7 @@ type Repository interface {
|
|||
DeleteItem(ctx context.Context, cartID, itemID types.ID) error
|
||||
UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error
|
||||
DeleteCart(ctx context.Context, cartID types.ID) error
|
||||
UpdateStatus(ctx context.Context, cartID types.ID, status entity.CartStatus) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
|
|
|
|||
Loading…
Reference in New Issue