Лучший способ получить доступ к базе данных в многопоточном приложении Android?

Примечание. Не помещайте этот вопрос как дубликат. Я рассмотрел несколько подобных вопросов, но не смог найти удовлетворительного ответа.

Я работаю над приложением, которое использует Sqlite Database. Мы следуем одноэлементному шаблону, который гарантирует, что мы можем создать только один экземпляр нашего вспомогательного класса в нашем приложении.

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 (ie 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: Спасибо за подшипник за такой длинный вопрос.

Solutions Collecting From Web of "Лучший способ получить доступ к базе данных в многопоточном приложении Android?"

Используйте шаблон Singleton для создания экземпляра SQLiteOpenHelper , поэтому во всем приложении должен существовать один экземпляр singleton. Это обеспечит отсутствие утечек и сделает вашу жизнь намного легче, так как она устраняет возможность забыть закрыть вашу базу данных по мере ее кодирования. Он также обеспечит безопасный доступ к базе данных во всем приложении.

Кроме того, вам не нужно реализовывать свой собственный механизм блокировки. SQLiteDatabase поддерживает блокировку mecanism. Итак, в конкретный момент будет только одна транзакция, все остальные транзакции будут в Queue с использованием Sigleton.getInstance() . Обеспечьте единую точку доступа к базе данных.

Более того, в этом подходе вам не нужно закрывать соединения с базой данных, так как SQLiteDatabase будет выпускать все ссылки после выполнения транзакции. Поэтому CustomSQLiteHelper.getInstance() следует использовать всякий раз, когда вы хотите выполнять операции CRUD и удалять блокировку mecansim.

Дополнительная информация. Пройдите через блог http://www.androiddesignpatterns.com/2012/05/correctly-managing-your-sqlite-database.html и посмотрите комментарии.

Надеюсь это поможет.

Я определенно рекомендую вам обернуть вашу базу данных в ContentProvider .

Он обеспечивает приятные функции из коробки и переносит множество потенциально сложных задач в инфраструктуру. Вам не нужно будет обращаться к помощникам базы данных singleton, потому что вам гарантировано, что у вас есть один экземпляр ContentResolver для вашего приложения, который будет перенаправлять команды предполагаемому провайдеру.

Вы можете использовать всю инфраструктуру Loader и хорошие вспомогательные классы, такие как AsyncQueryHandler для решения некоторых из наиболее распространенных оговорок доступа к БД.

Я бы посоветовал не синхронизировать доступ к вашим методам CRUD. Как упоминалось в документации и предыдущих ответах, об этом следует заботиться сама база данных. Не говоря уже о потенциальном снижении производительности в приложении, которое требует интенсивного использования базы данных.

Боковое примечание. Как первый шаг, может быть полезно, если вы попытаетесь определить, что вызывает эти сбои – они спорадические? Более конкретный пример может оказаться полезным при устранении неполадок.

Запрос 1 (Singleton):

Можете ли вы изменить код двойной блокировки для создания истинного объекта 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?

Я предпочитаю создавать Синглтон

  private static final CustomSqliteHelper instance = new CustomSqliteHelper (); 

Другой подход:

 enum CustomSqliteHelper { INSTANCE; } 

Еще один подход, если вам все еще нужен ленивый Синглтон.

Взгляните на @ Nathan Hughes ответ в этом вопросе SE:

Синхронизация и блокировка в однопользовательской фабрике

Запрос 2: (Блокировка)

Использование java lock – неплохая идея для улучшения согласованности приложения. Консистенция выигрывает за счет.

На странице SQLiteDatabaseLockedException ,

Если броузер базы данных не смог получить блокировки базы данных, он должен выполнить свою работу. Если оператор является [COMMIT] или происходит за пределами явной транзакции, вы можете повторно выполнить оператор. Если оператор не является [COMMIT] и встречается в явной транзакции, вам следует отменить транзакцию, прежде чем продолжить.

Я надеюсь, что вы должны следовать рекомендациям выше, а также фрагмент кода, который вы опубликовали в своем проекте (Github). Мне нравится, что эта идея и выше рекомендации соответствуют образцу кода Github.

Вы можете использовать поставщиков контента.

Я думаю, что использование блокировки чтения-записи в большинстве случаев достаточно. См. Этот ответ . Решение не использует ContentProvider . Вместо этого он использует класс ReentrantReadWriteLock в пакете java.util.concurrent.locks .

  1. Это лучший выбор для использования ContentProviders или расширения SqliteOpenHelper, учитывая, что мое приложение не передает данные другим приложениям.

Я бы сказал, что расширение SqliteOpenHelper намного лучше, если вы не используете какие-либо данные. В основном это связано с тем, что он более дружественный API, чем ContentProvider 🙂 И есть другая вещь, описанная в официальной документации :

Решите, нужен ли вам поставщик контента. Вам необходимо создать поставщика контента, если вы хотите предоставить одну или несколько из следующих функций:

  • Вы хотите предлагать сложные данные или файлы другим приложениям.
  • Вы хотите разрешить пользователям копировать сложные данные из вашего приложения в другие приложения.
  • Вы хотите предоставить пользовательские поисковые предложения, используя структуру поиска.

Так что в вашем случае – это довольно избыточно


Для инициализации синглтона – иногда вам не нужно его инициализировать ленивым:

 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(); } } 

Плюс конфигурация в манифесте. Таким образом, вы избегаете синхронизации во время создания.


Если вам действительно нужен этот синглтон для ленивого инициализации, – я скажу, что двойная проверка блокировки (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

Чтобы предотвратить ошибки блокировки, установите время ожидания занятости в двадцать секунд или около того в каждом соединении. Тогда вы получите исключение, только если ваше приложение зависает, транзакция будет длиннее этого.

Вы также можете использовать один объект SQLiteDatabase из всех потоков, но тогда вам придется заблокировать его вручную для всех транзакций.

Как обсуждалось в комментариях, блокировка блокировки не будет использоваться, если вы используете один экземпляр DB. Поэтому я сомневаюсь, что DeadLock вы используете транзакцию.

Примером DeadLock является:

  1. В одной транзакции:

    Обновить таблицу -> обновить таблицу B -> зафиксировать

  2. В другой сделке:

    Обновить таблицу B -> обновить таблицу -> зафиксировать

Поэтому в середине времени 1 ждет 2 и 2 ждет 1 в одно и то же время, поэтому срабатывает Deadlock.

Если это действительно Deadlock, вы можете исправить запрос, чтобы это не произошло, или вы можете использовать запрос Queued (или async query), например, Actor.

Например, библиотека Commons-dbutils поддерживает AsyncQueryRunner, который вы предоставляете ExecutorService, и он возвращает будущее. ( From: возможен ли асинхронный вызов jdbc? )