问题:SQLite是一种文件级别的数据库,因此他的锁机制也是文件级别的,只支持并发读访问数据库,不支持并发写,也不支持并发读写。只要有一个线程进行写操作,其他线程都将等待,如果同时执行读写操作,会返回SQLite_Busy。因此对于写操作是需要串行执行的。
解决方案:
- 1.最初是想通过循环执行,如果query.exec执行失败,那么会循环直到执行成功为止,但是我看了下Qt的文档,以及网上查了资料,
并没有找到Qt对SQLite_Busy错误返回的相关资料,Qt文档仅提供下列的error type。
QSqlError::NoError 0 没有错误发生
QSqlError::ConnectionError 1 (数据库)连接错误
QSqlError::StatementError 2 SQL语句语法错误
QSqlError::TransactionError 3 事务失败错误
QSqlError::UnknownError 4 未知错误
重新翻了翻qt assistant,在QSqlDataBase页的void QSqlDatabase::setConnectOptions(const QString &options = QString())
让我找到了QSQLITE_BUSY_TIMEOUT
文档中关于这些option的使用也提供了一个例子:
// MySQL connection
db.setConnectOptions("SSL_KEY=client-key.pem;SSL_CERT=client-cert.pem;SSL_CA=ca-cert.pem;CLIENT_IGNORE_SPACE=1"); // use an SSL connection to the server
if (!db.open()) {
db.setConnectOptions(); // clears the connect option string
...
}
...
// PostgreSQL connection
db.setConnectOptions("requiressl=1"); // enable PostgreSQL SSL connections
if (!db.open()) {
db.setConnectOptions(); // clear options
...
}
...
// ODBC connection
db.setConnectOptions("SQL_ATTR_ACCESS_MODE=SQL_MODE_READ_ONLY;SQL_ATTR_TRACE=SQL_OPT_TRACE_ON"); // set ODBC options
if (!db.open()) {
db.setConnectOptions(); // don't try to set this option
...
}
- 2.新增一条专门执行写操作的任务队列,其由一条专属线程维护,只要task任务线程有写操作就将他push到任务队列中(push应该push一个包含写操作命令类型cmdtype和data的封装,然后任务队列会根据cmdtype来调用不同的数据库操作函数,并将data作为实参传入)
- 3.信号量或锁。只要执行写操作,都要先加锁或者先查看信号量,如果信号量为1,那么获取信号量,并将他置0,执行完后解锁或归还信号量 。
- 3.1在第3个方案的基础上,要降低数据库的压力,尽量减少对数据库的访问,特别是并发争夺一个资源的写操作时,可以在应用程序一启动时,将待抢夺资源全部导入到应用程序的缓存当中,比如放入Qset(针对每种资源只有一个,第一个线程夺取后就从set中remove)或者QHash<QString,int>容器(可针对一种资源多个数量),当有新增资源时,会同时更新数据库和缓存。当要争夺资源的时候,使用锁来处理其并发情况。当接到用户请求资源的情况下,先将资源数量递减(加锁/解锁)后再进行其他方面的处理,处理失败在将数量递增1(加锁/解锁),否则表示处理成功。当资源数量递减到0时,表示资源抢夺完毕,拒绝其他用户的请求。
总结:
实际应用中,并不是让数据库去直面大并发读写,会借助“外力”,比如缓存、利用主从库实现读写分离、分表、使用队列写入等方法来降低并发读写。特别是对于sqlite来说,相较于mysql等数据库毕竟缺乏了许多功能(例如乐观锁、悲观锁),所以更多还是要在应用层上下功夫来处理并发。