Step by Step:如何写出 sqlite:database is locked 的 BUG

写在前面

使用 SQLite 经常会遇到提示 database is locked,那这个 BUG 反映什么意义,又是如何产生呢?

官方文档

先看看 SQLite 官方文档的相关介绍。

SQLite到底在锁什么:SQLite 的锁机制

SQLite 的定位是简单而完整的提供一个支持并发的关系型数据库的解决方案。因此,为了支持并发,SQLite 巧妙的通过 OS 的文件锁来实现库级锁,为了最大限度的保证并发 SQLite 设计了多个不同层级的锁。

SQLite 有 5 个不同的锁状态:未加锁(UNLOCKED)、共享 (SHARED)、保留 (RESERVED)、未 决(PENDING) 和排它(EXCLUSIVE)。SQLite 使用锁逐步上升机制,为了获得写入数据库的权限,连接需要逐级地获得最终的排它锁进行写入。

每个数据库连接在同一时刻只能处于其中一个锁状态,那么对于某个特定的数据库连接而言,当其成功获取到特定的 SQLite 文件某个锁状态时,其意义如下:

锁层级锁名称数据库连接的“权限”数据库对数据库连接的“承诺”
1UNLOCKED-未加锁默认状态,即不能读也不能写
2SHARED-共享锁可读允许多个连接同时持有共享锁,且保证在持有共享锁期间不会发生数据库写入
3RESERVED-保留锁准备写入,可以在缓冲区进行修改整个数据库文件最多只有一把保留锁
4PENDING-未决锁等待缓冲区修改提交不允许其它连接获得新的共享锁,开始等待已经拥有共享锁的连接完成工作并释放其共享锁。 一旦所有其它共享锁都被释放,拥有未决锁的连接就可以将其锁提升至排它锁
5EXCLUSIVE-排它锁可以进行写入不允许任何其他类型的锁与独占锁共存

database is locked的含义:SQLITE_BUSY 与 SQLITE_LOCKED 的区别

官方解释如下:

错误号错误提示含义
SQLITE_BUSY(5)database is locked由于其他数据库连接未释放锁,准备写入(读取)数据库的连接等待超时,无法完成操作
SQLITE_LOCKED(6)database table is locked由于同一数据库连接中的冲突或与使用共享缓存的不同数据库连接的冲突,写操作无法继续

所以,database is locked 其实并非一定是死锁,它仅仅提示某个连接等待锁超时的一种错误提示。

如何写出 database is locked 的 BUG

作死方法一

  • 思路:前一个连接尚未释放锁,下一连接等待写入

  • BUG的写法:

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
	"log"
)

var (
	db     *sql.DB
	dbFile = "test.db"
)

func initDb() {
	var err error
	db, err = sql.Open("sqlite3", dbFile)
	if err != nil {
		log.Println("Sql Open:", err.Error())
	} else {
		_, err = db.Exec(`create table test(id integer primary key autoincrement,message text);`)
		if err != nil {
			log.Println("Create Table:", err.Error())
		} else {
			_, err = db.Exec(`insert into test(message) values("a")`)
			if err != nil {
				log.Println("Insert Into:", err.Error())
			}
		}
	}
}

func main() {
	var err error
	initDb()
	rows, err := db.Query("select * from test")
	if err != nil {
		log.Println("Query:", err.Error())
	} else {
		for rows.Next() {
			break
		}
		// rows.Close() /* 开启后会避免 SQLITE_BUSY(5) */
		_, err = db.Exec(`insert into test(message) values("a")`)
		if err != nil {
			log.Println("Insert Into After Query:", err.Error())
		}
		_, err = db.Exec(`select * from test`)
		if err != nil {
			log.Println("Select After Query:", err.Error())
		}
	}
}
  • 输出:
2019/07/14 12:55:44 Insert Into After Query: database is locked

Process finished with exit code 0

作死方法二

  • 思路:多连接并发写入

  • BUG的写法:

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
	"log"
	"sync"
)

var (
	db     *sql.DB
	dbFile = "test.db"
)

func initDb() {
	var err error
	db, err = sql.Open("sqlite3", dbFile)
	if err != nil {
		log.Println("Sql Open:", err.Error())
	} else {
		_, err = db.Exec(`create table test(id integer primary key autoincrement,message text);`)
		if err != nil {
			log.Println("Create Table:", err.Error())
		} else {
			_, err = db.Exec(`insert into test(message) values("a")`)
			if err != nil {
				log.Println("Insert Into:", err.Error())
			}
		}
	}
}

func main() {
	var err error
	var wg sync.WaitGroup
	flag := false
	for i := 1; i < 10; i++ {
		go func() {
			wg.Add(1)
			defer wg.Done()
			initDb()
			for {
				if flag {
					break
				}
				_, err = db.Exec(`update test set message = "b"`)
				if err != nil {
					log.Println("Update:", err.Error())
					flag = true
				}
			}
		}()
	}
	wg.Wait()
}

  • 输出:
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:34 Create Table: table test already exists
2019/07/14 14:14:41 Update: database is locked

Process finished with exit code 0
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值