3

gorm操作sqlite3,高并发读写如何避免锁库? - failymao

 1 year ago
source link: https://www.cnblogs.com/failymao/p/17197166.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

这两天一直被这个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. 下面是一个完整的 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 秒钟,以便所有的写入和读取操作都完成。在这个示例中,我们使用了并发的写入和读取


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK