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.


Yapı


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

Uygulama yapısı


├── docker-compose.yaml
└── internal
├── domain
│   ├── account
│   │   ├── account.go
│   │   ├── cache.go
│   │   └── holder.go
│   └── repository
│   └── account.go
└── storage
├── account.go
└── account_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/account/cache.go


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

internal/domain/account/holder.go


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
}

internal/domain/account/account.go


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
}

internal/domain/repository/account.go


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
}

internal/storage/account.go


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
}

internal/storage/account_test.go


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

Redis CLI


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