727 Stimmen

Wie kann ich Gleichzeitigkeitsprobleme bei der Verwendung von SQLite unter Android vermeiden?

Was sind die besten Praktiken bei der Ausführung von Abfragen auf einer SQLite-Datenbank in einer Android-App?

Ist es sicher, Einfüge-, Lösch- und Auswahlabfragen aus dem doInBackground einer AsyncTask auszuführen? Oder sollte ich den UI-Thread verwenden? Ich nehme an, dass Datenbankabfragen "schwer" sein können und nicht den UI-Thread verwenden sollten, da dies die Anwendung blockieren kann - was zu einem Anwendung antwortet nicht (ANR).

Wenn ich mehrere AsyncTasks habe, sollten sie sich eine Verbindung teilen oder sollten sie jeweils eine Verbindung öffnen?

Gibt es bewährte Verfahren für diese Szenarien?

648voto

Kevin Galligan Punkte 15346

Einfügungen, Aktualisierungen, Löschungen und Lesevorgänge sind im Allgemeinen von mehreren Threads aus möglich, aber Brads Antwort ist nicht korrekt. Sie müssen sorgfältig darauf achten, wie Sie Ihre Verbindungen erstellen und nutzen. Es gibt Situationen, in denen Ihre Aktualisierungsaufrufe fehlschlagen, selbst wenn Ihre Datenbank nicht beschädigt wird.

Die grundlegende Antwort.

Das SqliteOpenHelper-Objekt hält eine Datenbankverbindung aufrecht. Es scheint Ihnen eine Lese- und Schreibverbindung anzubieten, aber in Wirklichkeit ist das nicht der Fall. Rufen Sie die Nur-Lese-Verbindung auf, erhalten Sie trotzdem die Schreibverbindung zur Datenbank.

Also, eine Helferinstanz, eine Datenbankverbindung. Selbst wenn Sie es von mehreren Threads aus verwenden, wird immer nur eine Verbindung hergestellt. Das SqliteDatabase-Objekt verwendet Java-Locks, um den Zugriff zu serialisieren. Wenn also 100 Threads eine Datenbankinstanz haben, werden die Aufrufe an die eigentliche Datenbank auf der Festplatte serialisiert.

Also, ein Hilfsmittel, eine Datenbankverbindung, die in Java-Code serialisiert wird. Ein Thread, 1000 Threads, wenn Sie eine Helper-Instanz verwenden, die zwischen ihnen geteilt wird, ist Ihr gesamter Datenbankzugriffscode seriell. Und das Leben ist gut (ish).

Wenn Sie versuchen, von verschiedenen Verbindungen aus gleichzeitig in die Datenbank zu schreiben, wird eine fehlschlagen. Es wird nicht gewartet, bis die erste Verbindung fertig ist und dann geschrieben. Es wird Ihre Änderung einfach nicht geschrieben. Schlimmer noch, wenn Sie nicht die richtige Version von insert/update auf der SQLiteDatabase aufrufen, erhalten Sie keine Ausnahme. Sie erhalten lediglich eine Meldung in Ihrem LogCat, und das war's.

Also, mehrere Threads? Verwenden Sie einen Helfer. Punkt. Wenn Sie WISSEN, dass nur ein Thread schreiben wird, können Sie möglicherweise mehrere Verbindungen verwenden, und Ihre Lesevorgänge werden schneller sein, aber seien Sie als Käufer vorsichtig. Ich habe das nicht oft getestet.

Hier finden Sie einen Blogbeitrag mit weitaus mehr Details und einer Beispielanwendung.

Gray und ich sind gerade dabei, ein ORM-Tool zu entwickeln, das auf seinem Ormlite basiert, das nativ mit Android-Datenbankimplementierungen arbeitet und der sicheren Erstellungs-/Aufrufstruktur folgt, die ich in dem Blogpost beschreibe. Das sollte sehr bald herauskommen. Werfen Sie einen Blick darauf.


In der Zwischenzeit gibt es einen weiteren Blog-Beitrag:

Probieren Sie auch die Gabel von 2punkt0 des zuvor erwähnten Beispiels für ein Schloss:

197voto

Dmytro Danylyk Punkte 19482

Gleichzeitiger Datenbankzugriff

Derselbe Artikel in meinem Blog (ich mag die Formatierung mehr)

Ich habe einen kleinen Artikel geschrieben, der beschreibt, wie man den Zugriff auf die Android-Datenbank thread-sicher macht.


Vorausgesetzt, Sie haben Ihr eigenes SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Jetzt wollen Sie die Daten in getrennten Threads in die Datenbank schreiben.

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

Sie werden folgende Meldung in Ihrem Logcat erhalten und eine Ihrer Änderungen wird nicht geschrieben.

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

Dies geschieht, weil jedes Mal, wenn Sie eine neue SQLiteOpenHelper Objekt stellen Sie tatsächlich eine neue Datenbankverbindung her. Wenn Sie versuchen, über verschiedene Verbindungen gleichzeitig in die Datenbank zu schreiben, wird eine Verbindung fehlschlagen. (aus der obigen Antwort)

Um eine Datenbank mit mehreren Threads zu verwenden, müssen wir sicherstellen, dass wir eine Datenbankverbindung verwenden.

Erstellen wir eine Singleton-Klasse Datenbank-Manager die eine einzige Datei enthält und zurückgibt SQLiteOpenHelper objeto.

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

}

Der aktualisierte Code, der die Daten in separaten Threads in die Datenbank schreibt, wird wie folgt aussehen.

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

Dies wird zu einem weiteren Absturz führen.

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

Da wir nur eine Datenbankverbindung verwenden, ist die Methode getDatabase() geben dieselbe Instanz von SQLiteDatabase Objekt für Thema1 y Thema2 . Was geschieht? Thema1 kann die Datenbank schließen, während Thema2 immer noch verwendet. Deshalb haben wir IllegalStateException Absturz.

Wir müssen sicherstellen, dass niemand die Datenbank benutzt und sie erst dann schließen. Einige Leute auf Stackoveflow haben empfohlen, die Datenbank nie zu schließen. SQLiteDatabase . Dies führt zu der folgenden logcat-Meldung.

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

Arbeitsprobe

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

        }
    }

}

Verwenden Sie es wie folgt.

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

Jedes Mal, wenn Sie eine Datenbank benötigen, sollten Sie anrufen openDatabase() Methode der DatabaseManager Klasse. Innerhalb dieser Methode haben wir einen Zähler, der anzeigt, wie oft die Datenbank geöffnet wurde. Wenn er gleich eins ist, bedeutet dies, dass wir eine neue Datenbankverbindung erstellen müssen, wenn nicht, ist die Datenbankverbindung bereits erstellt.

Das Gleiche geschieht in closeDatabase() Methode. Jedes Mal, wenn wir diese Methode aufrufen, wird der Zähler verringert, und wenn er auf Null geht, wird die Datenbankverbindung geschlossen.


Jetzt sollten Sie in der Lage sein, Ihre Datenbank zu verwenden und sicher sein, dass sie thread-sicher ist.

18voto

Reed Punkte 14297
  • Verwenden Sie eine Thread o AsyncTask für lang andauernde Vorgänge (50ms+). Testen Sie Ihre Anwendung, um zu sehen, wo das der Fall ist. Die meisten Operationen benötigen (wahrscheinlich) keinen Thread, da die meisten Operationen (wahrscheinlich) nur ein paar Zeilen umfassen. Verwenden Sie einen Thread für Massenoperationen.
  • Eins teilen SQLiteDatabase Instanz für jede DB auf der Festplatte zwischen den Threads und implementieren ein Zählsystem, um die offenen Verbindungen zu verfolgen.

Gibt es bewährte Verfahren für diese Szenarien?

Geben Sie ein statisches Feld für alle Ihre Klassen frei. Dafür und für andere Dinge, die gemeinsam genutzt werden müssen, habe ich immer ein Singleton behalten. Ein Zählschema (in der Regel mit AtomicInteger) sollte auch verwendet werden, um sicherzustellen, dass Sie die Datenbank nie zu früh schließen oder offen lassen.

Meine Lösung:

Die alte Version, die ich geschrieben habe, ist verfügbar unter https://github.com/Taeluf/dev/tree/main/archived/databasemanager und wird nicht gewartet. Wenn Sie meine Lösung verstehen wollen, sehen Sie sich den Code an und lesen Sie meine Notizen. Meine Notizen sind normalerweise sehr hilfreich.

  1. Kopieren Sie den Code und fügen Sie ihn in eine neue Datei mit dem Namen DatabaseManager . (oder laden Sie es von github herunter)
  2. erweitern. DatabaseManager und umsetzen onCreate y onUpgrade wie Sie es normalerweise tun würden. Sie können mehrere Unterklassen der einen erstellen DatabaseManager Klasse, um verschiedene Datenbanken auf der Festplatte zu haben.
  3. Instanzieren Sie Ihre Unterklasse und rufen Sie getDb() zur Nutzung der SQLiteDatabase Klasse.
  4. Rufen Sie an. close() für jede Unterklasse, die Sie instanziiert haben

Der Code für Kopieren/Einfügen :

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

14voto

Brad Hein Punkte 10909

Die Datenbank ist sehr flexibel mit Multithreading. Meine Anwendungen greifen auf ihre DBs von vielen verschiedenen Threads aus gleichzeitig zu, und es funktioniert einwandfrei. In einigen Fällen habe ich mehrere Prozesse, die gleichzeitig auf die DB zugreifen, und das funktioniert auch gut.

Ihre asynchronen Aufgaben - verwenden Sie die gleiche Verbindung, wenn Sie können, aber wenn Sie müssen, seine OK, um die DB von verschiedenen Aufgaben zugreifen.

9voto

dell116 Punkte 5705

Nachdem ich mich einige Stunden damit herumgeschlagen habe, habe ich herausgefunden, dass man nur ein db helper-Objekt pro db-Ausführung verwenden kann. Zum Beispiel,

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

im Gegensatz zu:

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

Erstellen eines neuen DBAdapters jedes Mal, wenn die Schleife iteriert war die einzige Möglichkeit, die ich meine Zeichenfolgen in eine Datenbank durch meine Helper-Klasse erhalten konnte.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X