在这个例子中,我们将通过利用Client.Watch功能来使用Redis事务。这与数据库事务的原子性原则是一样的--"要么全部发生,要么什么都不发生"。在我们的例子中,如果一个命令失败了,其他的也会失败。
我们有一个银行系统,一个账户持有人可能有多个账户。当我们创建一个账户持有人时,我们也会尝试创建账户。然而,如果在创建过程中出现问题,我们就根本不会创建任何东西。我们将创建3个不同的哈希值。一个用于根元素Holder 。另外两个为Account 元素。
结构
{
"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
文件
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
你可以特意删除结构中的MarshalBinary 和UnmarshalBinary 方法,使交易失败,以便重现回滚的情况。
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}"