繁体   English   中英

在多线程应用程序Android中访问数据库的最佳方法?

[英]Best way to access database in multi threaded application Android?

注意:请不要将此问题标记为重复。 我已经经历了几个类似的问题,但找不到满意的答案。

我一直在研究使用Sqlite数据库的应用程序。 我们遵循单例模式,确保我们只能在整个应用程序中创建一个辅助类实例。

public class CustomSqliteHelper extends SQLiteOpenHelper {

  public static CustomSqliteHelper getInstance(Context context) {
    if (instance == null) {
      synchronized (CustomSqliteHelper.class) {
        if (instance == null) {
          instance = new CustomSqliteHelper(context);
        }
      }
    }
    return instance;
  }
}

但有时应用程序崩溃与SQLiteDatabaseLockedException 我理解当多个线程/进程尝试一次写入数据库时​​会出现此异常。 即使一个线程/进程在写入操作仍在进行时尝试读取数据库,也会抛出此异常。

所以我一直在阅读很多关于这个以及防止这种情况发生的可能方法。 很多帖子建议使用ContentProvider而不是直接扩展SqliteOpenHelper类并对数据库对象执行操作。 在阅读其中一篇帖子时,这篇文章提到在使用Content Provider时,您不需要手动处理多线程环境。

尽管ContentProvider缺乏线程安全性,但通常您会发现在防止潜在的竞争条件方面您无需采取进一步行动。 规范示例是您的ContentProvider由SQLiteDatabase支持的时间; 当两个线程同时尝试写入数据库时​​,SQLiteDatabase将自行锁定,确保一个等待直到另一个完成。 每个线程都将获得对数据源的互斥访问,从而确保满足线程安全性。

以上引用似乎令人困惑,因为首先它提到ContentProvider不支持线程安全。 但他总结说,应用程序开发人员不需要做任何事情来实现并发。

另外,如果我选择使用SqliteOpenHelper,那么防止这些崩溃的最佳方法是什么? 我一直在考虑为每个db操作使用锁。

 public class CustomSqliteHelper extends SQLiteOpenHelper {

 private String lock = "lock";

 public void insert(){
    synchronized(lock){
       // Do the insert operation here.
    }
 }


 public void update(){
    synchronized(lock){
       // Do the update operation here.
    }
 }
 }

但我的一个团队成员建议我不要这样做,因为Java锁很贵。

在浏览了Github上最受欢迎的项目之一后,我发现开发人员建议将每个数据库操作包装在一个事务中。

public void insert(ContentValues values) {
    // Create and/or open the database for writing
    SQLiteDatabase db = getWritableDatabase();

    // It's a good idea to wrap our insert in a transaction. This helps with performance and ensures
    // consistency of the database.
    db.beginTransaction();
    try {
        // The user might already exist in the database (i.e. the same user created multiple posts).

        db.insertOrThrow(TABLE_POSTS, null, values);
        db.setTransactionSuccessful();
    } catch (Exception e) {
        Log.d(TAG, "Error while trying to add post to database");
    } finally {
        db.endTransaction();
    }
}

我真的不确定这是否可以阻止Lock异常? 这似乎更像是一个以绩效为导向的步骤。

所以在阅读完所有博客和教程之后,我仍然感到困惑。 我的主要问题是

  1. 鉴于我的应用程序不与其他应用程序共享数据,使用ContentProviders或扩展SqliteOpenHelper是更好的选择。
  2. 将Java锁定放在所有操作上是最好的方法还是有比这更好的其他方法?

更新:根据@Mustanar的回答,似乎SQLiteDatabase负责锁定机制。 这意味着如果您正在执行写操作,则数据库将被锁定。 但同时,如果某个其他线程尝试执行写操作,那么第二个操作是否会等待直到锁被释放或者是否会抛出android.sqlite.database.SQLiteDatabaseLockedException异常?

更新2 :给这个问题一个赏金,因为答案似乎仍然不清楚。 我只使用Helper类的一个实例。 但仍然得到这个错误。

PS:感谢您提出这么长的问题。

使用Singleton模式来实例化SQLiteOpenHelper ,因此在整个应用程序中应该存在一个单例实例。 这将确保不会发生泄漏,并且可以使您的生活更轻松,因为它消除了在编码时忘记关闭数据库的可能性。 它还将确保在整个应用程序中安全访问数据库。

而且,您不必实现自己的锁定机制。 SQLiteDatabase维护锁定机制。 因此,在特定时间只有一个事务发生,所有其他事务将使用Sigleton.getInstance()方法在Queue 确保数据库的单个访问点。

此外,在此方法中, 您不必关闭数据库连接,因为SQLiteDatabase将在事务完成后释放所有引用。 因此,只要您想执行CRUD操作并删除锁定mecansim,就应该使用CustomSQLiteHelper.getInstance()

更多信息,请访问博客http://www.androiddesignpatterns.com/2012/05/correctly-managing-your-sqlite-database.html并查看评论。

希望这可以帮助。

查询1(单例):

你能改变下面的双锁定代码来创建一个真正的Singleton对象吗?

public class CustomSqliteHelper extends SQLiteOpenHelper {
      private CustomSqliteHelper instance;
      public static CustomSqliteHelper getInstance(Context context) {
        if (instance == null) {
          synchronized (CustomSqliteHelper.class) {
            if (instance == null) {
              instance = new CustomSqliteHelper(context);
            }
          }
        }
        return instance;
      }
    }

看看这篇文章 ,了解双重锁定的问题。

看看相关的SE问题:

在Java中实现单例模式的有效方法是什么?

我更喜欢创建Singleton

 private static final CustomSqliteHelper instance = new CustomSqliteHelper ();

其他方法:

enum CustomSqliteHelper {
    INSTANCE;
}

如果你还需要懒惰的Singleton,还有一种方法。

在这个SE问题中看看@ Nathan Hughes回答:

单件工厂中的同步和锁定

查询2 :(锁定)

使用java锁定可以提高应用程序的一致性。 一致性胜过费用。

SQLiteDatabaseLockedException页面,

如果数据库引擎无法获取数据库锁,则需要执行其工作。 如果语句是[COMMIT]或发生在显式事务之外,则可以重试该语句。 如果该语句不是[COMMIT]并且在显式事务中发生,那么您应该在继续之前回滚事务。

我希望您应该遵循上面的建议以及您在问题(Github)项目中发布的代码片段。 我喜欢这个想法,上面的建议与Github示例代码一致。

我肯定会建议您将数据库包装在ContentProvider

它提供了开箱即用的好功能,并将许多潜在的棘手任务转移到框架中。 您不需要处理单例数据库帮助程序,因为您可以保证您的应用程序有一个ContentResolver实例,可以将命令转发给目标提供程序。

您可以使用整个Loader框架和漂亮的帮助程序类(如AsyncQueryHandler来处理一些最常见的数据库访问警告。

我建议不要同步对CRUD方法的访问。 正如文档和之前的答案中所提到的,这应该由数据库本身来处理。 更不用说在需要密集数据库使用的应用程序中潜在的性能损失。

旁注:作为第一步,如果您尝试找出导致这些崩溃的原因可能会有用 - 它们是否零星? 一个更具体的例子可能在故障排除时有用。

您可以使用内容提供商。

我认为在大多数情况下使用读写锁就足够了。 看到这个答案 该解决方案不使用ContentProvider 相反,它在java.util.concurrent.locks包中使用ReentrantReadWriteLock类。

  1. 鉴于我的应用程序不与其他应用程序共享数据,使用ContentProviders或扩展SqliteOpenHelper是更好的选择。

我想如果你不共享任何数据,扩展SqliteOpenHelper是更好的选择。 这主要是因为它比ContentProvider更加开发友好的API :-)还有官方文档中描述的其他内容

确定您是否需要内容提供商。 如果要提供以下一项或多项功能,则需要构建内容提供程序:

  • 您希望向其他应用程序提供复杂的数据或文件。
  • 您希望允许用户将应用中的复杂数据复制到其他应用中。
  • 您希望使用搜索框架提供自定义搜索建议。

所以在你的情况下 - 它是多余的


对于单例初始化 - 有时你不需要它进行惰性初始化:

public class CustomSqliteHelper {
    private static final CustomSqliteHelper instance = new CustomSqliteHelper(YourApplication.getContext());

    public static CustomSqliteHelper getInstance() {
        return instance;
    }

    // all the super staff below :)
}

但是您需要在应用程序类中公开应用程序上下文:

public class YourApplication extends Application {
    private static final YourApplication instance;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }

    public static Context getContext() {
        return instance.getApplicationContext();
    }
}

加上Manifest中的配置。 这样可以避免在创建过程中同步。


如果你真的需要这个单例进行延迟初始化 - 比我说双重检查锁定 (DCL)是非常糟糕的做法。 对于安全的延迟初始化,您应该使用以下模式:

public class CustomSqliteHelper {
    private static class InstanceHolder {
        public static CustomSqliteHelper instance = new CustomSqliteHelper(YourApplication.getContext());
    }

    public CustomSqliteHelper getInstance() {
        return InstanceHolder.instance;
    }

    // all the super staff below :)
}

  1. 将Java锁定放在所有操作上是最好的方法还是有比这更好的其他方法? 这提供安全的延迟初始化而不锁定任何东西。

没有必要进行任何锁定(直到您的DAO无状态)。 检查SQLiteDatabase的文档

除了确保SQLiteOpenHelper是单例之外,如果希望多个线程访问数据库,还需要启用预写日志记录:

https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#enableWriteAheadLogging()

这样做可以解决我的问题。

正如在注释中所讨论的,如果您使用一个数据库实例,则不会出现阻塞锁定。 所以我怀疑DeadLock是因为你正在使用交易。

一种DeadLock示例是:

  1. 在一次交易中:

    更新表 - >更新B表 - >提交

  2. 在其他交易中:

    更新B表 - >更新表 - >提交

所以在中间时间,1等待2和2同时等待1,所以死锁发生。

如果它确实是死锁,您可以修复查询以不发生这种情况,或者您可以使用像Actor这样的排队查询(或异步查询)。

例如,commons-dbutils库支持AsyncQueryRunner,它提供ExecutorService并返回Future。 (来自: 是否可以进行异步jdbc调用?

要防止锁定错误,请在每个连接中将忙碌超时设置为20秒左右。 然后,只有当您的应用程序挂起运行的事务超过该事务时,才会获得异常。

您还可以使用来自所有线程的单个SQLiteDatabase对象,但是您必须在所有事务周围手动锁定它。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM