1. 場景
這兩天一直被這個sqlit3困擾,起因是項目中需要有這樣一箇中間,中間件承擔着API角色和流量轉發的角色,需要接收來自至少300個agent的請求數據,和健康檢測的請求。 所以當即想到用go來實現,因爲數據教訓,不考慮使用pg大型數據庫,所以就選擇了輕量化的sqlite數據庫。程序很快就開發完了。上線,運行幾個節點,數據讀寫都未發生異常,但是當測試數據到達一定量級後,會出現database is locked
錯誤。 查了些資料,大意是sqlite併發讀支持不錯,但是併發寫就不太友好,所以有了此次的實踐。
ps: 部分代碼來自於chatGPT,不得不說chatGPT太香了。
在 Gorm 中操作 SQLite3
數據庫時,由於 SQLite3
的寫鎖機制是針對整個數據庫而不是單個表或行,因此高併發讀寫可能會導致鎖庫的情況。
2. 如何避免
爲了避免鎖庫問題,可以採用以下幾種方法:
-
使用 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;") }
-
合理控制事務範圍
在進行高併發讀寫操作時,需要注意事務範圍的控制,儘可能縮小事務的範圍,減少寫鎖的佔用時間。例如,在進行批量寫入操作時,可以將每次寫入拆分爲多個事務,以減少寫鎖的佔用時間。
-
使用緩存
使用緩存可以減少對數據庫的讀操作,從而減少鎖庫的概率。可以使用第三方緩存庫(如 Redis)來實現緩存功能。
-
增加數據庫連接數
增加數據庫連接數可以提高數據庫的併發處理能力,減少鎖庫的概率。可以在 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:
下面是一個完整的 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:使用 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 秒鐘,以便所有的寫入和讀取操作都完成。在這個示例中,我們使用了併發的寫入和讀取