13/08/2020 - GO, REDIS
Bu örnekte, Client.Watch özelliğini kullanarak Redis transaction işlemlerini kullanacağız. Veritabanı işlemlerinin Atomicity ilkesi ile aynı mantıktır - "either all occur, or nothing occurs" (ya hepsi olur ya da hiçbir şey olmaz). Bizim durumumuzda, bir komut başarısız olursa diğerleri de başarısız olur.
Bir kişinin birden fazla hesaba sahip olabileceği bir banka sistemimiz var. Bir kişi oluşturduğumuzda, aynı zamanda hesap oluşturmaya da çalışacağız. Ancak, bu işlem sırasında bir şeyler ters giderse, hiçbir şey yaratmayacağız. 3 farklı cache hash oluşturacağız. Bir tanesi ana öğe olan Holder
(kişi) için. Diğer ikiside Account
(hesap) öğeleri için.
{
"ID": "144c8dcb89dc293f55c68cc74adda88b",
"FirstName": "Al",
"MiddleName": "",
"LastName": "Pacino",
"Accounts": [
{
"Type": "Savings",
"Number": "20202020",
"SortCode": "20-20-20",
"Active": false
},
{
"Type": "Current",
"Number": "10101010",
"SortCode": "10-10-10",
"Active": true
}
]
}
├── docker-compose.yaml
└── internal
├── domain
│ ├── account
│ │ ├── account.go
│ │ ├── cache.go
│ │ └── holder.go
│ └── repository
│ └── account.go
└── storage
├── account.go
└── account_test.go
version: "3"
services:
redis:
image: redis:6.0.6-alpine
command: redis-server --requirepass pass
ports:
- 6379:6379
package account
import "fmt"
func CacheHashRootKey(holderID string) string {
return "app:holder:" + holderID
}
func CacheHashHolderField() string {
return "holder"
}
func CacheHashAccountField(accountType Type) string {
return fmt.Sprintf("account:%s", accountType)
}
package account
import "encoding/json"
type Holder struct {
ID string
FirstName string
MiddleName string
LastName string
Accounts []*Account
}
func (h *Holder) MarshalBinary() ([]byte, error) {
return json.Marshal(h)
}
func (h *Holder) UnmarshalBinary(data []byte) error {
if err := json.Unmarshal(data, &h); err != nil {
return err
}
return nil
}
package account
import "encoding/json"
type Type string
const (
TypeCurrent Type = "Current"
TypeSavings Type = "Savings"
)
type Account struct {
Type Type
Number string
SortCode string
Active bool
}
func (a *Account) MarshalBinary() ([]byte, error) {
return json.Marshal(a)
}
func (a *Account) UnmarshalBinary(data []byte) error {
if err := json.Unmarshal(data, &a); err != nil {
return err
}
return nil
}
package repository
import (
"context"
"github.com/inanzzz/cache/internal/domain/account"
)
type Account interface {
// Create creates a new account holder with/without actual accounts.
Create(ctx context.Context, holder account.Holder) error
}
Eğer işlemi geri almayı tetiklemek (roolback) isterseniz MarshalBinary
ve UnmarshalBinary
methodlarını yukarıdaki yapılardan silebilirsiniz.
package storage
import (
"context"
"fmt"
"github.com/inanzzz/cache/internal/domain/account"
"github.com/go-redis/redis/v8"
)
type Account struct {
rds *redis.Client
}
func NewAccount(rds *redis.Client) Account {
return Account{rds: rds}
}
func (a Account) Create(ctx context.Context, holder account.Holder) error {
accounts := holder.Accounts
// We do not want the inner structures within the parent because we will store them separately.
holder.Accounts = nil
err := a.rds.Watch(ctx, func(tx *redis.Tx) error {
// You can run more commands here. You will use `tx` though.
// e.g. tx.HGET()
// Note: `tx` is not part of transactional `pipe` below so any "SET" operation will be independent.
_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
// 1- Set holder
if _, err := pipe.HSetNX(
ctx,
account.CacheHashRootKey(holder.ID),
account.CacheHashHolderField(),
&holder,
).Result(); err != nil {
return fmt.Errorf("create: holder: %w", err)
}
//tx.Expire()
//
// 2- Set accounts within holder
for _, acc := range accounts {
if _, err := pipe.HSetNX(
ctx,
account.CacheHashRootKey(holder.ID),
account.CacheHashAccountField(acc.Type),
acc,
).Result(); err != nil {
return fmt.Errorf("create: account: %w", err)
}
//pipe.Expire()
}
//
return nil
})
return err
})
if err != nil {
return fmt.Errorf("create: transaction: %w", err)
}
return nil
}
package storage
import (
"context"
"testing"
"github.com/inanzzz/cache/internal/domain/account"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
)
func TestAccount_Create(t *testing.T) {
rds := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "pass",
})
storage := NewAccount(rds)
holder := account.Holder{
ID: "144c8dcb89dc293f55c68cc74adda88b",
FirstName: "Al",
MiddleName: "",
LastName: "Pacino",
Accounts: []*account.Account{
{
Type: account.TypeCurrent,
Number: "10101010",
SortCode: "10-10-10",
Active: true,
},
{
Type: account.TypeSavings,
Number: "20202020",
SortCode: "20-20-20",
Active: false,
},
},
}
assert.NoError(t, storage.Create(context.Background(), holder))
}
localhost:6379> KEYS *
1) "app:holder:144c8dcb89dc293f55c68cc74adda88b"
localhost:6379> HGETALL app:holder:144c8dcb89dc293f55c68cc74adda88b
1) "account:Savings"
2) "{\"Type\":\"Savings\",\"Number\":\"20202020\",\"SortCode\":\"20-20-20\",\"Active\":false}"
3) "holder"
4) "{\"ID\":\"144c8dcb89dc293f55c68cc74adda88b\",\"FirstName\":\"Al\",\"MiddleName\":\"\",\"LastName\":\"Pacino\",\"Accounts\":null}"
5) "account:Current"
6) "{\"Type\":\"Current\",\"Number\":\"10101010\",\"SortCode\":\"10-10-10\",\"Active\":true}"