在Golang中使用Redis事务的详细教程

828 阅读3分钟

在这个例子中,我们将通过利用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

你可以特意删除结构中的MarshalBinaryUnmarshalBinary 方法,使交易失败,以便重现回滚的情况。

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