golang多线程嵌入式账户事务数据库插件transaction的使用

Golang多线程嵌入式账户事务数据库插件transaction的使用

概述

transaction是一个嵌入式的事务性账户数据库,支持多线程模式运行。它只处理整数类型数据,如果要处理百分数(如美元中的美分),可以将所有数值乘以100。账户最大限制为2^63(9,223,372,036,854,775,807)。

核心概念

单元(Unit)

  • 单元可以是客户、公司等实体
  • 一个单元可以有多个账户(账户用字符串标识)
  • 如果单元的任何账户余额不为零,则不能删除该单元
  • 如果向不存在的账户存入金额,将自动创建该账户

账户(Account)

  • 账户用于记录资金、股票等
  • 账户必须属于某个单元
  • 一个账户只能属于一个单元
  • 每个账户只有一个余额
  • 余额仅以整数计算

使用示例

基本用法

package main

import (
	"fmt"
	tn "github.com/claygod/transaction"
)

func main() {
	tr := tn.New()
	tr.Start()

	// 添加单元
	switch res := tr.AddUnit(123); res {
	case tn.Ok:
		fmt.Println("Done! Unit created")
	case tn.ErrCodeCoreCatch:
		fmt.Println("Not obtained permission")
	case tn.ErrCodeUnitExist:
		fmt.Println("Such a unit already exists")
	default:
		fmt.Println("Unknown error")
	}

	// 事务操作 - 借记
	switch res := tr.Begin().Debit(123, "USD", 5).End(); res {
	case tn.Ok:
		fmt.Println("Done! Money added")
	case tn.ErrCodeUnitNotExist:
		fmt.Println("Unit not exist")
	case tn.ErrCodeTransactionCatch:
		fmt.Println("Account not catch")
	case tn.ErrCodeTransactionDebit:
		fmt.Println("Such a unit already exists")
	default:
		fmt.Println("Unknown error")
	}

	// 保存数据
	switch res := tr.Save("./test.tdb"); res {
	case tn.Ok:
		fmt.Println("Done! Data saved to file")
	case tn.ErrCodeCoreStop:
		fmt.Println("Unable to stop app")
	case tn.ErrCodeSaveCreateFile:
		fmt.Println("Could not create file")
	default:
		fmt.Println("Unknown error")
	}
}

创建/删除单元

tr := transaction.New()
tr.Start()
tr.AddUnit(123)  // 创建单元
tr.DelUnit(123)  // 删除单元

账户操作

// 贷记账户
t.Begin().Credit(id, "USD", 1).End()

// 借记账户
t.Begin().Debit(id, "USD", 1).End()

转账示例

// 从idFrom账户转账1美元到idTo账户
t.Begin().
	Credit(idFrom, "USD", 1).
	Debit(idTo, "USD", 1).
	End()

购买/销售示例

// 示例:用10美元购买2股"Apple"股票
tr.Begin().
	Credit(buyerId, "USD", 10).Debit(sellerId, "USD", 10).
	Credit(sellerId, "APPLE", 2).Debit(buyerId, "APPLE", 2).
	End()

保存/加载数据

// 保存
tr := New()
tr.Start()
tr.AddUnit(123)
tr.Begin().Debit(123, "USD", 7).End()
tr.Save(path)

// 加载
tr := New()
tr.Start()
tr.Load(path)
tr.Begin().Credit(123, "USD", 7).End()

API方法

  • New() - 创建新实例
  • Load(“path”) - 从文件加载数据
  • Start() - 启动数据库
  • AddUnit(ID) - 添加单元
  • Begin().Debit(ID, key, amount).End() - 借记操作
  • Begin().Credit(ID, key, amount).End() - 贷记操作
  • TotalUnit(ID) - 获取单元总额
  • TotalAccount(ID, key) - 获取账户余额
  • DelUnit(ID) - 删除单元
  • Stop() - 停止数据库
  • Save(“path”) - 保存数据到文件

性能基准(i7-6700T)

  • BenchmarkTotalUnitSequence-8: 419 ns/op
  • BenchmarkTotalUnitParallel-8: 185 ns/op
  • BenchmarkCreditSequence-8: 311 ns/op
  • BenchmarkCreditParallel-8: 175 ns/op
  • BenchmarkDebitSequence-8: 314 ns/op
  • BenchmarkDebitParallel-8: 178 ns/op
  • BenchmarkTransferSequence-8: 417 ns/op
  • BenchmarkTransferParallel-8: 277 ns/op
  • BenchmarkBuySequence-8: 644 ns/op
  • BenchmarkBuyParallel-8: 354 ns/op

注意事项

  1. 所有账户操作(借记/贷记)必须包含在事务中
  2. 并行请求到同一账户不会导致该账户余额的错误更改
  3. 单元不能删除,除非其所有账户余额为零

这个库非常适合需要高并发账户事务处理的场景,如金融系统、交易平台等。


更多关于golang多线程嵌入式账户事务数据库插件transaction的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang多线程嵌入式账户事务数据库插件transaction的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang多线程嵌入式账户事务数据库插件Transaction使用指南

在Golang中实现多线程嵌入式账户事务数据库,可以使用一些优秀的嵌入式数据库如BoltDB、Badger或SQLite,结合事务机制来保证数据一致性。下面我将详细介绍如何使用这些数据库的事务功能。

1. BoltDB事务使用示例

BoltDB是一个纯Go实现的键值存储数据库,支持完全可序列化的事务。

package main

import (
	"fmt"
	"log"
	"sync"

	"go.etcd.io/bbolt"
)

func main() {
	// 打开数据库
	db, err := bbolt.Open("accounts.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 初始化账户
	err = db.Update(func(tx *bbolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists([]byte("accounts"))
		return err
	})
	if err != nil {
		log.Fatal(err)
	}

	// 设置初始余额
	initAccounts(db)

	var wg sync.WaitGroup

	// 模拟多线程转账
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			transferMoney(db, "account1", "account2", 10)
			fmt.Printf("Goroutine %d completed transfer\n", id)
		}(i)
	}

	wg.Wait()

	// 查看最终余额
	checkBalances(db)
}

func initAccounts(db *bbolt.DB) {
	db.Update(func(tx *bbolt.Tx) error {
		b := tx.Bucket([]byte("accounts"))
		b.Put([]byte("account1"), []byte("1000")) // 账户1初始余额1000
		b.Put([]byte("account2"), []byte("500"))  // 账户2初始余额500
		return nil
	})
}

func transferMoney(db *bbolt.DB, from, to string, amount int) {
	for {
		err := db.Update(func(tx *bbolt.Tx) error {
			b := tx.Bucket([]byte("accounts"))
			
			// 读取账户余额
			fromBalance := parseInt(b.Get([]byte(from)))
			toBalance := parseInt(b.Get([]byte(to)))
			
			// 检查余额是否足够
			if fromBalance < amount {
				return fmt.Errorf("insufficient funds")
			}
			
			// 更新余额
			fromBalance -= amount
			toBalance += amount
			
			// 写回数据库
			err := b.Put([]byte(from), []byte(fmt.Sprintf("%d", fromBalance)))
			if err != nil {
				return err
			}
			err = b.Put([]byte(to), []byte(fmt.Sprintf("%d", toBalance)))
			if err != nil {
				return err
			}
			
			return nil
		})
		
		if err == nil {
			break // 事务成功完成
		}
		
		// 如果是冲突错误,可以重试
		if err == bbolt.ErrTxNotWritable {
			continue
		}
		
		log.Printf("Transfer failed: %v", err)
		break
	}
}

func checkBalances(db *bbolt.DB) {
	db.View(func(tx *bbolt.Tx) error {
		b := tx.Bucket([]byte("accounts"))
		fmt.Printf("Account1 balance: %s\n", b.Get([]byte("account1")))
		fmt.Printf("Account2 balance: %s\n", b.Get([]byte("account2")))
		return nil
	})
}

func parseInt(b []byte) int {
	var i int
	fmt.Sscanf(string(b), "%d", &i)
	return i
}

2. BadgerDB事务使用示例

BadgerDB是另一个高性能的键值存储数据库,适合需要快速读写和事务支持的场景。

package main

import (
	"fmt"
	"log"
	"sync"

	"github.com/dgraph-io/badger/v3"
)

func main() {
	// 打开数据库
	opts := badger.DefaultOptions("").WithInMemory(true)
	db, err := badger.Open(opts)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 初始化账户
	err = initAccounts(db)
	if err != nil {
		log.Fatal(err)
	}

	var wg sync.WaitGroup

	// 模拟多线程转账
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			err := transferMoney(db, "account1", "account2", 10)
			if err != nil {
				log.Printf("Goroutine %d error: %v", id, err)
			} else {
				fmt.Printf("Goroutine %d completed transfer\n", id)
			}
		}(i)
	}

	wg.Wait()

	// 查看最终余额
	checkBalances(db)
}

func initAccounts(db *badger.DB) error {
	txn := db.NewTransaction(true)
	defer txn.Discard()

	err := txn.Set([]byte("account1"), []byte("1000"))
	if err != nil {
		return err
	}
	err = txn.Set([]byte("account2"), []byte("500"))
	if err != nil {
		return err
	}

	return txn.Commit()
}

func transferMoney(db *badger.DB, from, to string, amount int) error {
	for {
		err := db.Update(func(txn *badger.Txn) error {
			// 读取账户余额
			fromItem, err := txn.Get([]byte(from))
			if err != nil {
				return err
			}
			fromBalance, err := fromItem.ValueCopy(nil)
			if err != nil {
				return err
			}

			toItem, err := txn.Get([]byte(to))
			if err != nil && err != badger.ErrKeyNotFound {
				return err
			}
			var toBalance []byte
			if err == badger.ErrKeyNotFound {
				toBalance = []byte("0")
			} else {
				toBalance, err = toItem.ValueCopy(nil)
				if err != nil {
					return err
				}
			}

			// 转换并检查余额
			fromInt := parseInt(fromBalance)
			toInt := parseInt(toBalance)

			if fromInt < amount {
				return fmt.Errorf("insufficient funds")
			}

			// 更新余额
			fromInt -= amount
			toInt += amount

			// 写回数据库
			err = txn.Set([]byte(from), []byte(fmt.Sprintf("%d", fromInt)))
			if err != nil {
				return err
			}
			err = txn.Set([]byte(to), []byte(fmt.Sprintf("%d", toInt)))
			if err != nil {
				return err
			}

			return nil
		})

		if err == nil {
			return nil // 事务成功完成
		}

		if err == badger.ErrConflict {
			continue // 冲突,重试
		}

		return err // 其他错误
	}
}

func checkBalances(db *badger.DB) {
	err := db.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("account1"))
		if err != nil {
			return err
		}
		val, err := item.ValueCopy(nil)
		if err != nil {
			return err
		}
		fmt.Printf("Account1 balance: %s\n", val)

		item, err = txn.Get([]byte("account2"))
		if err != nil {
			return err
		}
		val, err = item.ValueCopy(nil)
		if err != nil {
			return err
		}
		fmt.Printf("Account2 balance: %s\n", val)
		return nil
	})
	if err != nil {
		log.Printf("Error checking balances: %v", err)
	}
}

func parseInt(b []byte) int {
	var i int
	fmt.Sscanf(string(b), "%d", &i)
	return i
}

事务使用要点

  1. 原子性:事务中的所有操作要么全部成功,要么全部失败
  2. 一致性:数据库从一个一致状态转换到另一个一致状态
  3. 隔离性:并发事务之间互不干扰
  4. 持久性:事务完成后,结果永久保存

在多线程环境下使用事务数据库时,需要注意:

  1. 合理设计重试机制处理事务冲突
  2. 避免长事务,减少锁持有时间
  3. 考虑使用读写分离提高并发性能
  4. 对于高并发场景,可以适当使用批处理操作

以上示例展示了如何在Golang中使用嵌入式数据库的事务功能来实现账户转账操作,保证在多线程环境下的数据一致性。

回到顶部