Go 無侵入實現讀寫分離

在高併發的現代應用中,數據庫往往成爲系統的瓶頸。讀寫分離作爲一種有效的數據庫優化策略,能夠顯著提升系統的性能和可用性。本文將深入講解讀寫分離的核心概念、實現原理,並通過 go-zero 框架提供詳細的實戰示例。

  1. 讀寫分離的使用場景和必要性

1.1 什麼是讀寫分離

讀寫分離是一種數據庫架構模式,它將數據庫操作分爲兩類:

1.2 核心使用場景

高讀寫比例的應用

大多數 Web 應用的 DB 操作都是讀多寫少,典型場景包括:

數據庫負載分擔需求

1.3 讀寫分離的必要性

性能提升

可用性增強

擴展性提升

  1. 讀寫分離的實現原理

2.1 整體架構

2.2 核心組件

連接路由器 (Connection Router)

負責根據 SQL 操作類型決定使用哪個數據庫連接:

負載均衡器 (Load Balancer)

在多個從庫之間分配讀請求:

上下文管理器 (Context Manager)

通過上下文傳遞讀寫模式信息:

2.3 數據一致性處理

最終一致性

強一致性需求處理

// 寫入後立即讀取,使用主庫
ctx := sqlx.WithReadPrimary(context.Background())
result, err := db.QueryRowCtx(ctx, &user, "SELECT * FROM users WHERE id = ?", userID)
  1. 使用 go-zero 讀寫分離的示例

3.1 配置讀寫分離

配置文件設置

# config.yaml
DB:
DataSource:"user:password@tcp(master:3306)/database"
DriverName:mysql# 默認值,可不寫
Policy:"round-robin"# 負載均衡策略:round-robin 或 random,默認 round-robin
Replicas:
    -"user:password@tcp(replica1:3306)/database"
    -"user:password@tcp(replica2:3306)/database"
    - "user:password@tcp(replica3:3306)/database"

配置結構體定義

package config

import "github.com/zeromicro/go-zero/core/stores/sqlx"

type Config struct {
    DB sqlx.SqlConf
}

3.2 初始化數據庫連接

package main

import (
    "context"
    "log"

    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type UserModel struct {
    conn sqlx.SqlConn
}

func NewUserModel(conn sqlx.SqlConn) *UserModel {
    return &UserModel{
        conn: conn,
    }
}

func main() {
    var c Config
    conf.MustLoad("config.yaml", &c)

    // 創建支持讀寫分離的數據庫連接
    conn := sqlx.MustNewConn(c.DB)
    userModel := NewUserModel(conn)

    // 示例1:普通讀操作(路由到從庫)
    user, err := userModel.FindUserFromReplica(ctx, 1)
    if err != nil {
        log.Fatal(err)
    }

    // 示例2:寫操作(自動路由到主庫)
    err = userModel.CreateUser(context.Background(), &User{Name: "張三", Email: "zhangsan@example.com"})
    if err != nil {
        log.Fatal(err)
    }

    // 示例3:寫入後立即讀取(強制使用主庫)
    user, err = userModel.FindUserFromPrimary(ctx, 1)
    if err != nil {
        log.Fatal(err)
    }
}

3.3 模型層實現

package model

import (
    "context"
    "database/sql"
    "fmt"

    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type User struct {
    ID       int64`db:"id"`
    Name     string`db:"name"`
    Email    string`db:"email"`
    CreateAt int64`db:"create_at"`
    UpdateAt int64`db:"update_at"`
}

type UserModel struct {
    conn sqlx.SqlConn
}

func NewUserModel(conn sqlx.SqlConn) *UserModel {
    return &UserModel{
        conn: conn,
    }
}

// findUser 查詢用戶
func (m *UserModel) FindUser(ctx context.Context, id int64) (*User, error) {
    var user User
    query := "SELECT id, name, email, create_at, update_at FROM users WHERE id = ?"

    err := m.conn.QueryRowCtx(ctx, &user, query, id)
    if err != nil {
        if err == sql.ErrNoRows {
            returnnil, fmt.Errorf("user not found")
        }
        returnnil, err
    }

    return &user, nil
}

// FindUserFromMaster 強制從主庫查詢用戶
func (m *UserModel) FindUserFromMaster(ctx context.Context, id int64) (*User, error) {
    // 強制使用主庫
    masterCtx := sqlx.WithReadPrimary(ctx)
    return m.FindUser(masterCtx, id)
}

// FindUserFromReplica 強制從從庫查詢用戶
func (m *UserModel) FindUserFromReplica(ctx context.Context, id int64) (*User, error) {
    // 強制使用從庫
    replicaCtx := sqlx.WithReadReplica(ctx)
    return m.FindUser(replicaCtx, id)
}

// CreateUser 創建用戶(自動使用主庫)
func (m *UserModel) CreateUser(ctx context.Context, user *User) error {
    query := "INSERT INTO users (name, email, create_at, update_at) VALUES (?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"

    result, err := m.conn.ExecCtx(sqlx.WithWrite(ctx), query, user.Name, user.Email)
    if err != nil {
        return err
    }

    id, err := result.LastInsertId()
    if err != nil {
        return err
    }

    user.ID = id
    returnnil
}

// UpdateUser 更新用戶(自動使用主庫)
func (m *UserModel) UpdateUser(ctx context.Context, user *User) error {
    query := "UPDATE users SET name = ?, email = ?, update_at = UNIX_TIMESTAMP() WHERE id = ?"

    _, err := m.conn.ExecCtx(ctsqlx.WithWrite(ctx), query, user.Name, user.Email, user.ID)
    return err
}

// DeleteUser 刪除用戶(自動使用主庫)
func (m *UserModel) DeleteUser(ctx context.Context, id int64) error {
    query := "DELETE FROM users WHERE id = ?"

    _, err := m.conn.ExecCtx(sqlx.WithWrite(ctx), query, id)
    return err
}

// ListUsers 查詢用戶列表(使用從庫)
func (m *UserModel) ListUsers(ctx context.Context, limit, offset int) ([]*User, error) {
    var users []*User
    query := "SELECT id, name, email, create_at, update_at FROM users LIMIT ? OFFSET ?"

    err := m.conn.QueryRowsCtx(sqlx.WithReadReplica(ctx), &users, query, limit, offset)
    if err != nil {
        returnnil, err
    }

    return users, nil
}

3.4 服務層最佳實踐

package service

import (
    "context"
    "time"

    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type UserService struct {
    userModel *UserModel
}

func NewUserService(userModel *UserModel) *UserService {
    return &UserService{
        userModel: userModel,
    }
}

// 場景1:用戶註冊後立即返回用戶信息
func (s *UserService) RegisterUser(ctx context.Context, name, email string) (*User, error) {
    user := &User{
        Name:  name,
        Email: email,
    }

    // 1. 創建用戶(寫操作,使用主庫)
    err := s.userModel.CreateUser(ctx, user)
    if err != nil {
        returnnil, err
    }

    // 2. 立即返回用戶信息(讀操作,但需要最新數據,使用主庫)
    masterCtx := sqlx.WithReadPrimary(ctx)
    return s.userModel.FindUser(masterCtx, user.ID)
}

// 場景2:用戶更新後需要驗證更新結果
func (s *UserService) UpdateUserProfile(ctx context.Context, userID int64, name, email string) (*User, error) {
    // 1. 更新用戶信息(寫操作,使用主庫)
    user := &User{
        ID:    userID,
        Name:  name,
        Email: email,
    }

    err := s.userModel.UpdateUser(ctx, user)
    if err != nil {
        returnnil, err
    }

    // 2. 返回更新後的用戶信息(讀操作,需要最新數據,使用主庫)
    masterCtx := sqlx.WithReadPrimary(ctx)
    return s.userModel.FindUser(masterCtx, userID)
}

// 場景3:用戶列表查詢(可以接受從庫的延遲數據)
func (s *UserService) GetUserList(ctx context.Context, page, pageSize int) ([]*User, error) {
    offset := (page - 1) * pageSize

    // 使用從庫查詢,可以接受輕微的數據延遲
    replicaCtx := sqlx.WithReadReplica(ctx)
    return s.userModel.ListUsers(replicaCtx, pageSize, offset)
}

// 場景4:事務處理(讀寫操作都在主庫)
func (s *UserService) TransferUserData(ctx context.Context, fromUserID, toUserID int64) error {
    // 事務中的所有操作都在主庫執行
    ctx = sqlx.WithWrite(ctx)

    return s.userModel.conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session)error {
        // 查詢源用戶
        var fromUser User
        err := session.QueryRowCtx(ctx, &fromUser, "SELECT * FROM users WHERE id = ?", fromUserID)
        if err != nil {
            return err
        }

        // 查詢目標用戶
        var toUser User
        err = session.QueryRowCtx(ctx, &toUser, "SELECT * FROM users WHERE id = ?", toUserID)
        if err != nil {
            return err
        }

        // 執行業務邏輯...
        // 更新操作
        _, err = session.ExecCtx(ctx, "UPDATE users SET update_at = UNIX_TIMESTAMP() WHERE id IN (?, ?)", fromUserID, toUserID)
        return err
    })
}

3.6 監控和調試

package main

import (
    "context"
    "log"
    "time"

    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

// 監控讀寫分離效果
func MonitorReadWriteSeparation(conn sqlx.SqlConn) {
    ctx := context.Background()

    // 測試讀操作路由
    log.Println("=== 測試讀操作路由 ===")

    // 普通讀操作(應該路由到從庫)
    replicaCtx := sqlx.WithReadReplica(ctx)
    start := time.Now()
    var count int
    err := conn.QueryRowCtx(replicaCtx, &count, "SELECT COUNT(*) FROM users")
    log.Printf("從庫查詢耗時: %v, 錯誤: %v", time.Since(start), err)

    // 強制主庫讀操作
    masterCtx := sqlx.WithReadPrimary(ctx)
    start = time.Now()
    err = conn.QueryRowCtx(masterCtx, &count, "SELECT COUNT(*) FROM users")
    log.Printf("主庫查詢耗時: %v, 錯誤: %v", time.Since(start), err)

    // 測試寫操作路由
    log.Println("=== 測試寫操作路由 ===")

    // 寫操作(應該自動路由到主庫)
    writeCtx := sqlx.WithWrite(ctx)
    start = time.Now()
    _, err = conn.ExecCtx(writeCtx, "UPDATE users SET update_at = UNIX_TIMESTAMP() WHERE id = 1")
    log.Printf("寫操作耗時: %v, 錯誤: %v", time.Since(start), err)
}
  1. 故障轉移

// 實現主從切換的故障轉移機制
func (m *UserModel) FindUserWithFailover(ctx context.Context, id int64) (*User, error) {
    // 優先嚐試從庫
    replicaCtx := sqlx.WithReadReplica(ctx)
    user, err := m.FindUser(replicaCtx, id)
    if err == nil {
        return user, nil
    }

    // 從庫失敗,回退到主庫
    log.Printf("從庫查詢失敗,回退到主庫: %v", err)
    masterCtx := sqlx.WithReadPrimary(ctx)
    return m.FindUser(masterCtx, id)
}
  1. 總結

讀寫分離是提升數據庫性能的重要手段,go-zero 框架提供了優雅的讀寫分離實現:

5.1 核心優勢

5.2 使用建議

  1. 1. 合理配置從庫數量:根據讀寫比例確定從庫數量

  2. 2. 監控主從延遲:確保業務可接受的數據延遲

  3. 3. 選擇合適的負載均衡策略:根據從庫性能選擇輪詢或隨機

  4. 4. 處理數據一致性:在需要強一致性的場景使用主庫讀取

通過合理的讀寫分離配置和使用,可以顯著提升系統的併發處理能力和整體性能。

項目地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zero 並 star 支持我們!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/WIp-YElgggfyA4BSMZ8blQ