一文講清數(shù)據(jù)庫(kù)的分庫(kù)分表
想必大家在面試的時(shí)候都被問(wèn)到過(guò)數(shù)據(jù)庫(kù)的分庫(kù)分表應(yīng)該怎么做。
分庫(kù)分表指的是是將大型數(shù)據(jù)庫(kù)分割成多個(gè)小型數(shù)據(jù)庫(kù)或表格的技術(shù),旨在通過(guò)分散數(shù)據(jù)來(lái)提升性能、增加可擴(kuò)展性和簡(jiǎn)化管理。隨著數(shù)據(jù)量的增長(zhǎng),傳統(tǒng)的單體數(shù)據(jù)庫(kù)可能會(huì)遭遇性能瓶頸,而分庫(kù)分表能有效解決這些問(wèn)題,支持系統(tǒng)線性擴(kuò)展,確保高效的數(shù)據(jù)處理和響應(yīng)速度,同時(shí)降低運(yùn)維復(fù)雜度和成本。
今天我就分享一下我對(duì)此的一些見(jiàn)解。(如有錯(cuò)誤,歡迎指正)
一、選擇合適的數(shù)據(jù)庫(kù)驅(qū)動(dòng)和ORM框架(如果使用)
- 數(shù)據(jù)庫(kù)驅(qū)動(dòng)
Golang支持多種數(shù)據(jù)庫(kù)驅(qū)動(dòng),如database/sql包提供了與數(shù)據(jù)庫(kù)交互的標(biāo)準(zhǔn)接口。對(duì)于MySQL,常用的驅(qū)動(dòng)是github.com/go - sql - driver/mysql。確保在項(xiàng)目中正確導(dǎo)入和初始化驅(qū)動(dòng),例如:
import (
"database/sql"
_ "github.com/go - sql - driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name")
if err!= nil {
// 處理錯(cuò)誤
}
defer db.Close()
}- ORM框架(可選)
如果項(xiàng)目使用ORM框架,如GORM,它可以簡(jiǎn)化數(shù)據(jù)庫(kù)操作,包括分庫(kù)分表的實(shí)現(xiàn)。GORM提供了方便的API來(lái)定義模型和執(zhí)行數(shù)據(jù)庫(kù)操作。導(dǎo)入GORM和相關(guān)的數(shù)據(jù)庫(kù)驅(qū)動(dòng)(以MySQL為例):
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err!= nil {
// 處理錯(cuò)誤
}
}二、確定分庫(kù)分表策略
- 水平分表策略
對(duì)于用戶表,按照用戶ID進(jìn)行哈希取模分表。假設(shè)要將用戶數(shù)據(jù)分散到10張表中,可以計(jì)算user_id % 10,根據(jù)結(jié)果將用戶數(shù)據(jù)存儲(chǔ)到user_0、user_1等對(duì)應(yīng)的表中。在查詢用戶數(shù)據(jù)時(shí),同樣先計(jì)算哈希值,然后確定要查詢的表。
例如,對(duì)于抽獎(jiǎng)記錄,按照時(shí)間范圍進(jìn)行分表。可以每月創(chuàng)建一張新表,表名可以采用lottery_records_202401(表示2024年1月的抽獎(jiǎng)記錄)這樣的格式。在代碼中,需要根據(jù)抽獎(jiǎng)時(shí)間來(lái)確定操作哪一張表。
按范圍劃分
按哈希劃分
- 垂直分庫(kù)策略
按照業(yè)務(wù)模塊劃分?jǐn)?shù)據(jù)庫(kù)。例如,將用戶信息存儲(chǔ)在一個(gè)數(shù)據(jù)庫(kù)(user_db)中,抽獎(jiǎng)規(guī)則存儲(chǔ)在另一個(gè)數(shù)據(jù)庫(kù)(lottery_rule_db)中,抽獎(jiǎng)結(jié)果存儲(chǔ)在第三個(gè)數(shù)據(jù)庫(kù)(lottery_result_db)等。在代碼中,需要根據(jù)操作的業(yè)務(wù)模塊來(lái)選擇不同的數(shù)據(jù)庫(kù)連接。
三、實(shí)現(xiàn)分庫(kù)分表邏輯
- 基于SQL操作實(shí)現(xiàn)(不使用ORM)
在查詢用戶數(shù)據(jù)時(shí):
水平分表操作示例(按哈希劃分用戶表)
func QueryUser(db *sql.DB, userID int) (*User, error) {
tableName := fmt.Sprintf("user_%d", userID%10)
querySQL := fmt.Sprintf("SELECT * FROM %s WHERE user_id =? ", tableName)
row := db.QueryRow(querySQL, userID)
user := &User{}
err := row.Scan(&user.ID, &user.Name, &user.Age)
if err!= nil {
return nil, err
}
return user, nil
}- 在插入用戶數(shù)據(jù)時(shí):func InsertUser(db *sql.DB, user *User) error {
tableName := fmt.Sprintf("user_%d", user.ID%10)
insertSQL := fmt.Sprintf("INSERT INTO %s (user_id, name, age) VALUES (?,?,?)", tableName)
stmt, err := db.Prepare(insertSQL)
if err!= nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(user.ID, user.Name, user.Age)
return err
}- 垂直分庫(kù)操作示例(選擇不同數(shù)據(jù)庫(kù)連接)
假設(shè)已經(jīng)有兩個(gè)數(shù)據(jù)庫(kù)連接userDB和lotteryRuleDB:
func QueryUserInfo(userDB *sql.DB, userID int) (*UserInfo, error) {
querySQL := "SELECT * FROM user_info WHERE user_id =?"
row := userDB.QueryRow(querySQL, userID)
userInfo := &UserInfo{}
err := row.Scan(&userInfo.ID, &userInfo.Email, &userInfo.Address)
if err!= nil {
return nil, err
}
return userInfo, nil
}
func QueryLotteryRule(lotteryRuleDB *sql.DB, ruleID int) (*LotteryRule, error) {
querySQL := "SELECT * FROM lottery_rule WHERE rule_id =?"
row := lotteryRuleDB.QueryRow(querySQL, ruleID)
lotteryRule := &LotteryRule{}
err := row.Scan(&lotteryRule.ID, &lotteryRule.Probability, &lotteryRule.PrizeType)
if err!= nil {
return nil, err
}
return lotteryRule, nil
}- 基于ORM框架(如GORM)實(shí)現(xiàn)
可以通過(guò)自定義GORM插件來(lái)實(shí)現(xiàn)分表邏輯。首先定義插件結(jié)構(gòu)體:
水平分表操作示例(按哈希劃分用戶表)
type ShardingPlugin struct{}
```
- 實(shí)現(xiàn)GORM的Plugin接口方法,在`Name`方法中返回插件名稱(chēng),在`Initialize`方法中實(shí)現(xiàn)分表邏輯:
- ```Go
func (p ShardingPlugin) Name() string {
return "ShardingPlugin"
}
func (p ShardingPlugin) Initialize(db *gorm.DB) error {
// 根據(jù)用戶ID計(jì)算表名
db.Callback().Query().Before("gorm:query").Register("sharding:query", func(db *gorm.DB) {
userID, ok := db.Statement.Vars["user_id"].(int)
if ok {
tableName := fmt.Sprintf("user_%d", userID%10)
db.Statement.Table(tableName)
}
})
db.Callback().Create().Before("gorm:create").Register("sharding:create", func(db *gorm.DB) {
userID, ok := db.Statement.Vars["user_id"].(int)
if ok {
tableName := fmt.Sprintf("user_%d", userID%10)
db.Statement.Table(tableName)
}
})
return nil
}在初始化GORM時(shí)注冊(cè)這個(gè)插件:
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Plugins: []gorm.Plugin{ShardingPlugin{}},
})
if err!= nil {
// 處理錯(cuò)誤
}
}- 垂直分庫(kù)操作示例(選擇不同數(shù)據(jù)庫(kù)連接)
在GORM中,可以通過(guò)定義不同的數(shù)據(jù)庫(kù)連接實(shí)例來(lái)操作不同的數(shù)據(jù)庫(kù)。假設(shè)已經(jīng)定義了userDB和lotteryRuleDB兩個(gè)GORM數(shù)據(jù)庫(kù)實(shí)例:
func QueryUserInfo(userDB *gorm.DB, userID int) (*UserInfo, error) {
userInfo := &UserInfo{}
err := userDB.Where("user_id =?", userID).First(userInfo).Error
if err!= nil {
return nil, err
}
return userInfo, nil
}
func QueryLotteryRule(lotteryRuleDB *gorm.DB, ruleID int) (*LotteryRule, error) {
lotteryRule := &LotteryRule{}
err := lotteryRuleDB.Where("rule_id =?", ruleID).First(lotteryRule).Error
if err!= nil {
return nil, err
}
return lotteryRule, nil
}四、數(shù)據(jù)遷移和同步
- 初始數(shù)據(jù)遷移
當(dāng)實(shí)施分庫(kù)分表策略時(shí),需要將原有數(shù)據(jù)遷移到新的數(shù)據(jù)庫(kù)結(jié)構(gòu)中。如果是水平分表,可以編寫(xiě)數(shù)據(jù)遷移腳本,按照分表策略將數(shù)據(jù)從舊表復(fù)制到新表。例如,對(duì)于按時(shí)間范圍分表的抽獎(jiǎng)記錄:
func MigrateLotteryRecords() error {
oldDB, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/old_database_name")
if err!= nil {
return err
}
defer oldDB.Close()
newDB, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/new_database_name")
if err!= nil {
return err
}
defer newDB.Close()
rows, err := oldDB.Query("SELECT * FROM old_lottery_records")
if err!= nil {
return err
}
defer rows.Close()
for rows.Next() {
record := &LotteryRecord{}
err := rows.Scan(&record.ID, &record.UserID, &record.LotteryDate)
if err!= nil {
return err
}
// 根據(jù)抽獎(jiǎng)日期確定新表名
newTableName := fmt.Sprintf("lottery_records_%d", record.LotteryDate.Year()*100 + int(record.LotteryDate.Month()))
insertSQL := fmt.Sprintf("INSERT INTO %s (id, user_id, lottery_date) VALUES (?,?,?)", newTableName)
stmt, err := newDB.Prepare(insertSQL)
if err!= nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(record.ID, record.UserID, record.LotteryDate)
if err!= nil {
return err
}
}
return nil
}- 數(shù)據(jù)同步機(jī)制
在分庫(kù)分表后,可能需要建立數(shù)據(jù)同步機(jī)制,以確保數(shù)據(jù)的一致性。例如,在分布式系統(tǒng)中,當(dāng)一個(gè)服務(wù)更新了用戶表的數(shù)據(jù),可能需要通過(guò)消息隊(duì)列(如Kafka)將更新事件發(fā)送到其他相關(guān)服務(wù),其他服務(wù)收到消息后對(duì)相應(yīng)的分表進(jìn)行更新操作。以下是一個(gè)簡(jiǎn)單的示例,使用Kafka進(jìn)行數(shù)據(jù)同步:
import (
"github.com/Shopify/sarama"
)
func UpdateUserAndSync(userDB *sql.DB, kafkaProducer sarama.SyncProducer, user *User) error {
// 更新用戶數(shù)據(jù)
err := UpdateUser(userDB, user)
if err!= nil {
return err
}
// 發(fā)送數(shù)據(jù)更新消息到Kafka
message := &sarama.ProducerMessage{
Topic: "user_update_topic",
Value: sarama.StringEncoder(fmt.Sprintf("user_id:%d", user.ID)),
}
_, _, err = kafkaProducer.SendMessage(message)
return err
}
func KafkaConsumerLoop(kafkaConsumer sarama.Consumer, userDB *sql.DB) {
consumer, err := kafkaConsumer.ConsumePartition("user_update_topic", 0, sarama.OffsetNewest)
if err!= nil {
// 處理錯(cuò)誤
}
defer consumer.Close()
for message := range consumer.Messages() {
// 解析消息,獲取用戶ID
userIDStr := string(message.Value)
userID, err := strconv.Atoi(userIDStr[len("user_id:"):])
if err!= nil {
// 處理錯(cuò)誤
}
// 根據(jù)用戶ID更新其他分表中的用戶數(shù)據(jù)
user, err := QueryUser(userDB, userID)
if err!= nil {
// 處理錯(cuò)誤
}
// 更新其他分表...
}
}五、性能測(cè)試和優(yōu)化
- 性能測(cè)試
在實(shí)施分庫(kù)分表后,需要對(duì)系統(tǒng)進(jìn)行性能測(cè)試,以驗(yàn)證是否達(dá)到了預(yù)期的性能提升效果??梢允褂眯阅軠y(cè)試工具,如go - bench來(lái)測(cè)試數(shù)據(jù)庫(kù)操作的性能。例如,測(cè)試查詢用戶數(shù)據(jù)的性能:
func BenchmarkQueryUser(b *testing.B) {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name")
if err!= nil {
b.Fatal(err)
}
defer db.Close()
for i := 0; i < b.N; i++ {
userID := i
QueryUser(db, userID)
}
}- 優(yōu)化調(diào)整
根據(jù)性能測(cè)試結(jié)果,對(duì)分庫(kù)分表策略和代碼進(jìn)行優(yōu)化調(diào)整。例如,如果發(fā)現(xiàn)某些查詢操作仍然較慢,可以考慮優(yōu)化索引策略、調(diào)整分片規(guī)則或者增加緩存機(jī)制等。如果是使用ORM框架,還可以優(yōu)化ORM的配置,如調(diào)整GORM的Preload和Joins策略來(lái)減少不必要的數(shù)據(jù)庫(kù)查詢。
本文轉(zhuǎn)載自微信公眾號(hào)「王中陽(yáng)」,作者「王中陽(yáng)」,可以通過(guò)以下二維碼關(guān)注。

轉(zhuǎn)載本文請(qǐng)聯(lián)系「王中陽(yáng)」公眾號(hào)。

































