gorm操作sqlite3,高併發讀寫如何避免鎖庫?

1. 場景

這兩天一直被這個sqlit3困擾,起因是項目中需要有這樣一箇中間,中間件承擔着API角色和流量轉發的角色,需要接收來自至少300個agent的請求數據,和健康檢測的請求。 所以當即想到用go來實現,因爲數據教訓,不考慮使用pg大型數據庫,所以就選擇了輕量化的sqlite數據庫。程序很快就開發完了。上線,運行幾個節點,數據讀寫都未發生異常,但是當測試數據到達一定量級後,會出現database is locked錯誤。 查了些資料,大意是sqlite併發讀支持不錯,但是併發寫就不太友好,所以有了此次的實踐。

ps: 部分代碼來自於chatGPT,不得不說chatGPT太香了。

在 Gorm 中操作 SQLite3數據庫時,由於 SQLite3 的寫鎖機制是針對整個數據庫而不是單個表或行,因此高併發讀寫可能會導致鎖庫的情況。


2. 如何避免

爲了避免鎖庫問題,可以採用以下幾種方法:

  1. 使用 WAL 模式

    使用 SQLite3 的 WAL(Write-Ahead Logging)模式可以顯著降低鎖庫的概率。在 WAL 模式下,讀操作不會阻塞寫操作,寫操作也不會阻塞讀操作,因此可以實現高併發的讀寫操作。

    可以在 Gorm 中使用以下代碼開啓 WAL 模式:

    import "gorm.io/driver/sqlite"
     db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
         DSN: "mode=wal",
     })
     
     // 上面這種參數設置方式已經不適用新的設置方法如下
     if Inst, err = gorm.Open(sqlite.Open(dsn), gormConfig); err == nil {
                 // 啓用 WAL 模式
                 _ = Inst.Exec("PRAGMA journal_mode=WAL;")
                 //_ = Inst.Exec("PRAGMA journal_size_limit=104857600;")
               //_ = Inst.Exec("PRAGMA busy_timeout=999999;")
     }
    
  2. 合理控制事務範圍

    在進行高併發讀寫操作時,需要注意事務範圍的控制,儘可能縮小事務的範圍,減少寫鎖的佔用時間。例如,在進行批量寫入操作時,可以將每次寫入拆分爲多個事務,以減少寫鎖的佔用時間。

  3. 使用緩存

    使用緩存可以減少對數據庫的讀操作,從而減少鎖庫的概率。可以使用第三方緩存庫(如 Redis)來實現緩存功能。

  4. 增加數據庫連接數

    增加數據庫連接數可以提高數據庫的併發處理能力,減少鎖庫的概率。可以在 Gorm 中使用以下代碼來增加數據庫連接數:

    import "gorm.io/driver/sqlite"
    
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    
    sqlDB, dbError := db.DB()
    if dbError != nil {
        return nil, fmt.Errorf("failed to create sqlDB")
    }
    
    // SetMaxIdleConns 設置空閒連接池中連接的最大數量
    sqlDB.SetMaxIdleConns(10)
    
    // SetMaxOpenConns 設置打開數據庫連接的最大數量。
    sqlDB.SetMaxOpenConns(100)'
    

    需要注意的是,增加連接數也會增加服務器的負載,因此需要根據實際情況進行調整。

    綜上所述,通過採用合適的鎖機制、事務控制、緩存和連接數設置等措施,可以有效避免 SQLite3 數據庫的鎖庫問題。


3. 完整的代碼示例

  1. 示例1:

    下面是一個完整的 Gorm 操作 SQLite3 數據庫的代碼示例,其中包括開啓 WAL 模式、控制事務範圍、使用緩存和增加數據庫連接數等措施,以避免鎖庫問題。

    import (
        "gorm.io/driver/sqlite"
        "gorm.io/gorm"
        "time"
    )
    
    // 定義模型結構體
    type User struct {
        ID        uint
        Name      string
        Age       uint8
        CreatedAt time.Time
        UpdatedAt time.Time
    }
    
    // 初始化數據庫連接
    func InitDB() (*gorm.DB, error) {
        db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
            // 開啓 WAL 模式
            DSN: "mode=wal",
            // 增加最大連接數爲 100
            MaxOpenConns: 100,
        })
        if err != nil {
            return nil, err
        }
        // 設置數據庫連接池參數
        sqlDB, err := db.DB()
        if err != nil {
            return nil, err
        }
        sqlDB.SetMaxIdleConns(10)
        sqlDB.SetMaxOpenConns(100)
        sqlDB.SetConnMaxLifetime(time.Hour)
    
        return db, nil
    }
    
    // 定義批量寫入函數
    func BatchInsertUsers(db *gorm.DB, users []User) error {
        // 每次寫入 1000 條數據
        batchSize := 1000
        batchCount := (len(users) + batchSize - 1) / batchSize
        for i := 0; i < batchCount; i++ {
            start := i * batchSize
            end := (i + 1) * batchSize
            if end > len(users) {
                end = len(users)
            }
            batch := users[start:end]
            // 啓用事務
            tx := db.Begin()
            if err := tx.Error; err != nil {
                return err
            }
            if err := tx.Create(&batch).Error; err != nil {
                tx.Rollback()
                return err
            }
            // 提交事務
            if err := tx.Commit().Error; err != nil {
                return err
            }
        }
        return nil
    }
    
    // 查詢用戶信息
    func GetUsers(db *gorm.DB) ([]User, error) {
        var users []User
        // 使用緩存,減少對數據庫的讀操作
        err := db.Cache(&users).Find(&users).Error
        if err != nil {
            return nil, err
        }
        return users, nil
    }
    
    // 示例代碼
    func main() {
        // 初始化數據庫連接
        db, err := InitDB()
        if err != nil {
            panic(err)
        }
        defer db.Close()
    
        // 批量插入數據
        users := []User{}
        for i := 0; i < 100000; i++ {
            user := User{
                Name:      "user_" + string(i),
                Age:       uint8(i % 100),
                CreatedAt: time.Now(),
                UpdatedAt: time.Now(),
            }
            users = append(users, user)
        }
        err = BatchInsertUsers(db, users)
        if err != nil {
            panic(err)
        }
    
        // 查詢數據
        users, err = GetUsers(db)
        if err != nil {
            panic(err)
        }
        for _, user := range users {
            fmt.Println(user)
        }
    }
    
  2. 示例2:使用 WAL 模式和事務控制來避免鎖庫問題:

    package main
    
    import (
        "fmt"
        "gorm.io/driver/sqlite"
        "gorm.io/gorm"
    )
    
    type User struct {
        ID   uint
        Name string
    }
    
    func main() {
        // 創建 SQLite3 數據庫連接
        db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
            // 開啓 WAL 模式
            DSN: "mode=wal",
        })
        if err != nil {
            panic("failed to connect database")
        }
        // 設置連接池大小
        sqlDB, err := db.DB()
        if err != nil {
            panic("failed to set database pool size")
        }
        sqlDB.SetMaxIdleConns(10)
        sqlDB.SetMaxOpenConns(100)
    
        // 自動遷移 User 模型對應的表
        err = db.AutoMigrate(&User{})
        if err != nil {
            panic("failed to migrate table")
        }
    
        // 併發寫入 1000 條數據
        for i := 0; i < 1000; i++ {
            go func(i int) {
                err := db.Transaction(func(tx *gorm.DB) error {
                    user := User{Name: fmt.Sprintf("user_%d", i)}
                    result := tx.Create(&user)
                    return result.Error
                })
                if err != nil {
                    fmt.Printf("failed to write data: %v\n", err)
                }
            }(i)
        }
    
        // 併發讀取數據
        for i := 0; i < 1000; i++ {
            go func() {
                var users []User
                err := db.Transaction(func(tx *gorm.DB) error {
                    result := tx.Find(&users)
                    return result.Error
                })
                if err != nil {
                    fmt.Printf("failed to read data: %v\n", err)
                } else {
                    fmt.Printf("read %d records\n", len(users))
                }
            }()
        }
    
        // 等待 10 秒鐘,以便所有的寫入和讀取操作都完成
        time.Sleep(10 * time.Second)
    }   
    

    在這個代碼示例中,我們首先使用 gorm.Open 函數創建了一個 SQLite3 數據庫連接,並設置了連接池大小和 WAL 模式。然後,我們使用 d b.AutoMigrate 函數自動遷移 User 模型對應的表。

    接着,我們在循環中併發地寫入 1000 條數據,並使用事務控制來控制事務的範圍。每個寫入操作都會創建一個 User 對象,並使用 tx.Create 函數將其寫入數據庫。

    然後,我們在另一個循環中併發地讀取數據,並使用事務控制來控制事務的範圍。每個讀取操作都會使用 tx.Find 函數從數據庫中讀取所有的 User 記錄,並打印出讀取的記錄數。

    最後,我們等待 10 秒鐘,以便所有的寫入和讀取操作都完成。在這個示例中,我們使用了併發的寫入和讀取

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章