在Android上使用SQLite时如何避免并发问题?

在Android应用程序中对SQLite数据库执行查询时,什么被认为是最佳实践?

它是安全的运行插入,删除和选择查询从Async任务的doIn后台?或者我应该使用UI线程?我想数据库查询可以是“沉重的”,不应该使用UI线程,因为它可以锁定应用程序-导致应用无响应(ANR)。

如果我有多个Async任务,它们应该共享一个连接还是应该各自打开一个连接?

是否有针对这些场景的最佳实践?

236228 次浏览

数据库在多线程方面非常灵活。我的应用程序同时从许多不同的线程访问它们的数据库,它做得很好。在某些情况下,我有多个进程同时访问数据库,这也很好。

您的异步任务-尽可能使用相同的连接,但如果必须,可以从不同的任务访问数据库。

从多个线程中插入、更新、删除和读取通常是可以的,但是Brad的回答是不正确的。您必须小心如何创建和使用它们。即使您的数据库没有损坏,您的更新调用也会失败。

基本答案。

SqliteOpenHelper对象保留一个数据库连接。它似乎为您提供了读写连接,但实际上并没有。调用只读,您将获得写数据库连接。

因此,一个辅助实例,一个db连接。即使您从多个线程使用它,一次一个连接。SqliteDatabase对象使用Java锁来保持访问序列化。因此,如果100个线程有一个db实例,则对实际磁盘数据库的调用将被序列化。

所以,一个帮助器,一个数据库连接,它在java代码中序列化。一个线程,1000个线程,如果您使用它们之间共享的一个帮助器实例,那么您所有的数据库访问代码都是串行的。生活很好(ish)。

如果您尝试同时从实际不同的连接写入数据库,将会失败。它不会等到第一个完成后再写入。它根本不会写入您的更改。更糟糕的是,如果您没有在SQLiteDatabase上调用正确版本的插入/更新,您将不会得到异常。您只会在LogCat中收到一条消息,就是这样。

所以,多线程?使用一个助手。时期。如果你知道只有一个线程将写入,你可能能够使用多个连接,你的读取会更快,但买家要小心。我没有测试那么多。

这是一篇博客文章,其中包含更多细节和一个示例应用程序。

Gray和我实际上正在完成一个基于他的Ormlite的ORM工具,该工具在Android数据库实现中原生工作,并遵循我在博客文章中描述的安全创建/调用结构。应该很快就会出来。看一看。


与此同时,还有一篇后续的博客文章:

还可以通过前面提到的锁定示例的2点0检查分叉:

我对SQLiteDatabase API的理解是,如果您有一个多线程应用程序,您不能有超过1个SQLiteDatabase对象指向单个数据库。

对象肯定可以创建,但如果不同的线程/进程(也)开始使用不同的SQLiteDatabase对象(就像我们在JDBC Connection中使用的方式),则插入/更新会失败。

这里唯一的解决方案是坚持使用1个SQLiteDatabase对象,并且每当在多个线程中使用start Transaction()时,Android管理不同线程的锁定,并且一次只允许1个线程具有独占更新访问权限。

此外,您可以从数据库中“读取”并在不同的线程中使用相同的SQLiteDatabase对象(而另一个线程写入),并且永远不会发生数据库损坏,即“读取线程”不会从数据库中读取数据,直到“写入线程”提交数据,尽管两者都使用相同的SQLiteDatabase对象。

这与JDBC中的连接对象不同,如果您在读写线程之间传递(使用相同的)连接对象,那么我们也可能打印未提交的数据。

在我的企业应用程序中,我尝试使用条件检查,以便UI线程永远不必等待,而BG线程(专门)持有SQLiteDatabase对象。我尝试预测UI操作并推迟BG线程运行“x”秒。也可以维护PriorityQueue来管理分发SQLiteDatabase连接对象,以便UI线程首先获得它。

经过几个小时的努力,我发现每次db执行只能使用一个db helper对象。例如,

for(int x = 0; x < someMaxValue; x++)
{
db = new DBAdapter(this);
try
{


db.addRow
(
NamesStringArray[i].toString(),
StartTimeStringArray[i].toString(),
EndTimeStringArray[i].toString()
);


}
catch (Exception e)
{
Log.e("Add Error", e.toString());
e.printStackTrace();
}
db.close();
}

为:

db = new DBAdapter(this);
for(int x = 0; x < someMaxValue; x++)
{


try
{
// ask the database manager to add a row given the two strings
db.addRow
(
NamesStringArray[i].toString(),
StartTimeStringArray[i].toString(),
EndTimeStringArray[i].toString()
);


}
catch (Exception e)
{
Log.e("Add Error", e.toString());
e.printStackTrace();
}


}
db.close();

每次循环迭代时创建一个新的DBAdapter是我可以通过我的helper类将字符串放入数据库的唯一方法。

并发数据库访问

我博客上的同一篇文章(我更喜欢格式化)

我写了一篇小文章,描述了如何使访问您的android数据库线程安全。


假设你有自己的SQLiteOpenHelper相关文档

public class DatabaseHelper extends SQLiteOpenHelper { ... }

现在您想在单独的线程中将数据写入数据库。

 // Thread 1
Context context = getApplicationContext();
DatabaseHelper helper = new DatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();


// Thread 2
Context context = getApplicationContext();
DatabaseHelper helper = new DatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();

您将在logcat中收到以下消息,并且您的一项更改将不会被写入。

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

发生这种情况是因为每次您创建新的SQLiteOpenHelper相关文档对象时,您实际上都是在建立新的数据库连接。如果您尝试同时从实际不同的连接写入数据库,则会失败。(来自上面的答案)

要使用多个线程的数据库,我们需要确保我们使用的是一个数据库连接。

让我们创建单例类数据库管理器,它将保存并返回单个SQLiteOpenHelper相关文档对象。

public class DatabaseManager {


private static DatabaseManager instance;
private static SQLiteOpenHelper mDatabaseHelper;


public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DatabaseManager();
mDatabaseHelper = helper;
}
}


public static synchronized DatabaseManager getInstance() {
if (instance == null) {
throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
" is not initialized, call initialize(..) method first.");
}


return instance;
}


public SQLiteDatabase getDatabase() {
return new mDatabaseHelper.getWritableDatabase();
}


}

在单独的线程中将数据写入数据库的更新代码将如下所示。

 // In your application class
DatabaseManager.initializeInstance(new MySQLiteOpenHelper());
// Thread 1
DatabaseManager manager = DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();


// Thread 2
DatabaseManager manager = DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();

这会给你带来另一场灾难。

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

由于我们只使用一个数据库连接,方法获取数据库Thread1Thread2返回相同的SQLiteDatabase对象实例。发生了什么,Thread1可能会关闭数据库,而Thread2仍在使用它。这就是为什么我们有非法国家例外崩溃。

我们需要确保没有人在使用数据库,然后才关闭它。stackoveflow上的一些人建议永远不要关闭您的SQLiteDatabase。这将导致以下logcat消息。

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed

工作样本

public class DatabaseManager {


private int mOpenCounter;


private static DatabaseManager instance;
private static SQLiteOpenHelper mDatabaseHelper;
private SQLiteDatabase mDatabase;


public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DatabaseManager();
mDatabaseHelper = helper;
}
}


public static synchronized DatabaseManager getInstance() {
if (instance == null) {
throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
" is not initialized, call initializeInstance(..) method first.");
}


return instance;
}


public synchronized SQLiteDatabase openDatabase() {
mOpenCounter++;
if(mOpenCounter == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}


public synchronized void closeDatabase() {
mOpenCounter--;
if(mOpenCounter == 0) {
// Closing database
mDatabase.close();


}
}


}

使用它如下。

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way

每次需要数据库时,都应该调用数据库管理类类的openDatabase()方法。在这个方法中,我们有一个计数器,表示数据库被打开了多少次。如果等于1,则表示我们需要创建新的数据库连接,如果不是,则已经创建了数据库连接。

关闭数据库方法也是如此。每次我们调用此方法时,计数器都会减少,每当它变为零时,我们都会关闭数据库连接。


现在您应该能够使用您的数据库并确保它是线程安全的。

Dmytro的回答很适合我的情况。 至少对于我的情况,它会调用空指针异常,否则,例如getWritableDatabase尚未在一个线程中返回,而openDatabase se同时在另一个线程中调用。

public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
  • 对长时间运行的操作(50ms+)使用ThreadAsyncTask。测试你的应用程序以查看它在哪里。大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。使用线程进行批量操作。
  • 线程之间为磁盘上的每个DB共享一个SQLiteDatabase实例,并实现一个计数系统来跟踪打开的连接。

是否有针对这些场景的最佳实践?

在你所有的类之间共享一个静态字段。我曾经为它和其他需要共享的东西保留一个单例。还应该使用计数方案(通常使用原子整数)来确保你永远不会提前关闭数据库或将其打开。

我的解决方案:

我编写的旧版本可在https://github.com/Taeluf/dev/tree/main/archived/databasemanager获得,并且没有维护。如果您想了解我的解决方案,请查看代码并阅读我的笔记。我的笔记通常非常有用。

  1. 将代码复制/粘贴到名为DatabaseManager的新文件中。(或从github下载)
  2. 扩展DatabaseManager并像往常一样实现onCreateonUpgrade。您可以创建一个DatabaseManager类的多个子类,以便在磁盘上拥有不同的数据库。
  3. 实例化您的子类并调用getDb()以使用SQLiteDatabase类。
  4. 为您实例化的每个子类调用close()

复制/粘贴的代码:

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;


import java.util.concurrent.ConcurrentHashMap;


/** Extend this class and use it as an SQLiteOpenHelper class
*
* DO NOT distribute, sell, or present this code as your own.
* for any distributing/selling, or whatever, see the info at the link below
*
* Distribution, attribution, legal stuff,
* See https://github.com/JakarCo/databasemanager
*
* If you ever need help with this code, contact me at support@androidsqlitelibrary.com (or support@jakar.co )
*
* Do not sell this. but use it as much as you want. There are no implied or express warranties with this code.
*
* This is a simple database manager class which makes threading/synchronization super easy.
*
* Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
*  Instantiate this class once in each thread that uses the database.
*  Make sure to call {@link #close()} on every opened instance of this class
*  If it is closed, then call {@link #open()} before using again.
*
* Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
*
* I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
*
*
*/
abstract public class DatabaseManager {
    

/**See SQLiteOpenHelper documentation
*/
abstract public void onCreate(SQLiteDatabase db);
/**See SQLiteOpenHelper documentation
*/
abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
/**Optional.
* *
*/
public void onOpen(SQLiteDatabase db){}
/**Optional.
*
*/
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
/**Optional
*
*/
public void onConfigure(SQLiteDatabase db){}






/** The SQLiteOpenHelper class is not actually used by your application.
*
*/
static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {


DatabaseManager databaseManager;
private AtomicInteger counter = new AtomicInteger(0);


public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
super(context, name, null, version);
this.databaseManager = databaseManager;
}


public void addConnection(){
counter.incrementAndGet();
}
public void removeConnection(){
counter.decrementAndGet();
}
public int getCounter() {
return counter.get();
}
@Override
public void onCreate(SQLiteDatabase db) {
databaseManager.onCreate(db);
}


@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
databaseManager.onUpgrade(db, oldVersion, newVersion);
}


@Override
public void onOpen(SQLiteDatabase db) {
databaseManager.onOpen(db);
}


@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
databaseManager.onDowngrade(db, oldVersion, newVersion);
}


@Override
public void onConfigure(SQLiteDatabase db) {
databaseManager.onConfigure(db);
}
}


private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();


private static final Object lockObject = new Object();




private DBSQLiteOpenHelper sqLiteOpenHelper;
private SQLiteDatabase db;
private Context context;


/** Instantiate a new DB Helper.
* <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
*
* @param context Any {@link android.content.Context} belonging to your package.
* @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
* @param version the database version.
*/
public DatabaseManager(Context context, String name, int version) {
String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
synchronized (lockObject) {
sqLiteOpenHelper = dbMap.get(dbPath);
if (sqLiteOpenHelper==null) {
sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
dbMap.put(dbPath,sqLiteOpenHelper);
}
//SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
db = sqLiteOpenHelper.getWritableDatabase();
}
this.context = context.getApplicationContext();
}
/**Get the writable SQLiteDatabase
*/
public SQLiteDatabase getDb(){
return db;
}


/** Check if the underlying SQLiteDatabase is open
*
* @return whether the DB is open or not
*/
public boolean isOpen(){
return (db!=null&&db.isOpen());
}




/** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
*  <br />If the new counter is 0, then the database will be closed.
*  <br /><br />This needs to be called before application exit.
* <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
*
* @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
*/
public boolean close(){
sqLiteOpenHelper.removeConnection();
if (sqLiteOpenHelper.getCounter()==0){
synchronized (lockObject){
if (db.inTransaction())db.endTransaction();
if (db.isOpen())db.close();
db = null;
}
return true;
}
return false;
}
/** Increments the internal db counter by one and opens the db if needed
*
*/
public void open(){
sqLiteOpenHelper.addConnection();
if (db==null||!db.isOpen()){
synchronized (lockObject){
db = sqLiteOpenHelper.getWritableDatabase();
}
}
}
}

有了一些问题,我想我已经明白为什么我出错了。

我写了一个数据库包装类,其中包括一个close(),它将帮助器称为open()的镜像,它调用getWriteableDatabase,然后迁移到ContentProviderContentProvider的模型不使用SQLiteDatabase.close(),我认为这是一个很大的线索,因为代码确实使用了getWriteableDatabase在某些情况下,我仍然在进行直接访问(主屏幕验证查询,所以我迁移到getWriteableDatabase/rawQuery模型。

我使用单例,并且在关闭留档中有稍微不祥的评论

关闭任何打开数据库对象

(我的大胆)。

所以我有间歇性的崩溃,我使用后台线程访问数据库,它们与前台同时运行。

所以我认为close()强制数据库关闭,而不管任何其他线程持有引用——所以close()本身不是简单地撤消匹配的getWriteableDatabase,而是强制关闭任何打开请求。大多数时候,这不是问题,因为代码是单线程的,但在多线程的情况下,总是有打开和关闭不同步的机会。

阅读了其他地方的注释,解释了SqLiteDatabase aseHelper代码实例计数,那么你想要关闭的唯一时间就是你想要做备份副本的情况,你想强制关闭所有连接并强制SqLite写掉任何可能游荡的缓存内容-换句话说,停止所有应用程序数据库活动,关闭以防Helper丢失跟踪,执行任何文件级别的活动(备份/恢复),然后重新开始。

虽然这听起来像是一个好主意,尝试以受控的方式关闭,但现实情况是,Android保留垃圾虚拟机的权利,因此任何关闭都可以降低未写入缓存更新的风险,但如果设备受到压力,则无法保证,如果您正确释放了游标和对数据库的引用(不应该是静态成员),那么助手无论如何都会关闭数据库。

所以我的看法是,方法是:

使用getWriteableDatabase从单例包装器打开。(我使用派生应用程序类从静态提供应用程序上下文以解决对上下文的需求)。

永远不要直接叫关闭。

永远不要将生成的数据库存储在任何没有明显作用域的对象中,并且依赖引用计数来触发隐式的关闭()。

如果进行文件级处理,请将所有数据库活动停止,然后调用关闭,以防出现失控线程,假设您编写了正确的事务,因此失控线程将失败,并且关闭的数据库至少会有正确的事务,而不是部分事务的文件级副本。

您可以尝试在Google I/O 2017上应用新的架构方法宣布

它还包括名为房间的新ORM库

它包含三个主要组件:@Entity、@道和@数据库

User.java

@Entity
public class User {
@PrimaryKey
private int uid;


@ColumnInfo(name = "first_name")
private String firstName;


@ColumnInfo(name = "last_name")
private String lastName;


// Getters and setters are ignored for brevity,
// but they're required for Room to work.
}

UserDao.java

@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();


@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);


@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);


@Insert
void insertAll(User... users);


@Delete
void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

我知道响应晚了,但在android中执行sqlite查询的最佳方式是通过自定义内容提供程序。通过这种方式,UI与数据库类(扩展SQLiteOpenHelper类的类)解耦。查询也在后台线程(光标加载器)中执行。