• 多线程操作SQLite注意事项


    • 多线程读写

    SQLite实质上是将数据写入一个文件,通常情况下,在应用的包名下面都能找到xxx.db的文件,拥有root权限的手机,可以通过adb shell,看到data/data/packagename/databases/xxx.db这样的文件。
    我们可以得知SQLite是文件级别的锁:多个线程可以同时读,但是同时只能有一个线程写。Android提供了SqliteOpenHelper类,加入Java的锁机制以便调用。
    如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper实例),后面的就会遇到android.database.sqlite.SQLiteException: database is locked这样的异常。
    对于这样的问题,解决的办法就是keep single sqlite connection保持单个SqliteOpenHelper实例,同时对所有数据库操作的方法添加synchronized关键字。
    如下所示:

    public class DatabaseHelper extends SQLiteOpenHelper {
            public static final String TAG = "DatabaseHelper";
            private static final String DB_NAME = "practice.db";
            private static final int DB_VERSION = 1;
    
            private Context mContext;
            private static DatabaseHelper mInstance;
    
            private DatabaseHelper(Context context) {
                    super(context, DB_NAME, null, DB_VERSION);
            }
    
            public synchronized static DatabaseHelper getInstance(Context context) {
                    if (mInstance == null) {
                            mInstance = new DatabaseHelper(context);
                    }
                    return mInstance;
            }
    
            @Override
            public void onCreate(SQLiteDatabase db) {
                    // TODO Auto-generated method stub
    
            }
    
            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                    // TODO Auto-generated method stub
    
            }
    public synchronized void queryMethod() {
                    SQLiteDatabase readableDatabase = getReadableDatabase();
                    //read operation
            }
            
            public void updateMethod() {
                    SQLiteDatabase writableDatabase = getWritableDatabase();
                    //update operation
            }
    }
    View Code

    Android为我们提供了SqliteOpenHelper类,我们可以通过getWritableDatabase或者getReadableDatabase拿到SQLiteDatabase对象,然后执行相关方法。这2个方法名称容易给人误解,我也在很长的一段时间内想当然的认为getReadabeDatabase就是获取一个只读的数据库,可以获取很多次,多个线程同时读,用完就关闭,实际上getReadableDatabase先以读写方式打开数据库,如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库。

    public synchronized SQLiteDatabase getReadableDatabase() {
            if (mDatabase != null && mDatabase.isOpen()) {
                return mDatabase;  // The database is already open for business
            }
    
            if (mIsInitializing) {
                throw new IllegalStateException("getReadableDatabase called recursively");
            }
    
            try {
                return getWritableDatabase();
            } catch (SQLiteException e) {
                if (mName == null) throw e;  // Can't open a temp database read-only!
                Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
            }
    
            SQLiteDatabase db = null;
            try {
                mIsInitializing = true;
                String path = mContext.getDatabasePath(mName).getPath();
                db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
                if (db.getVersion() != mNewVersion) {
                    throw new SQLiteException("Can't upgrade read-only database from version " +
                            db.getVersion() + " to " + mNewVersion + ": " + path);
                }
    
                onOpen(db);
                Log.w(TAG, "Opened " + mName + " in read-only mode");
                mDatabase = db;
                return mDatabase;
            } finally {
                mIsInitializing = false;
                if (db != null && db != mDatabase) db.close();
            }
        }
    View Code

    在多线程中,如果第一个线程先调用getWritableDatabase,后面线程再次调用,或者第一个线程先调用 getReadableDatabase,后面的线程调用getWritableDatabase,那么后面的这个方法是会失败的,因为数据库文件打开后 会加锁,必须等前面的关闭后后面的调用才能正常执行,正是因为这个原因,可以1 Write+Many Read(有可能产生冲突,因为第一个getReadableDatabase有可能先于getWritableDatabase执行,导致后面的失 败),也可以Many Read,但是不可能Many Write。所以使用单例加上同步的数据库操作方法,就不会出现死锁的问题,这部分例子请参照附件,多线程可以运行的很好,另外关于Sqlite database locking collisions example,网上有很不错的一个例子,可以这里去下载。

    其实我觉得理论上可以修改getReadableDatabase方法,打开的数据库都是Read Only的,这样就能同时1 Write+Many Read,只不过要保证打开之前,数据库要创建或者升级好,这样读操作就不会互斥写操作,效率相对更高。
    关于数据库关闭的问题,在下面好的习惯中会专门说明。

    • 事务

    接触过数据库的人,对事务这个概念一定不陌生,它是原子性的,要么执行成功,执行一半失败后会回滚,这样就能保证数据的完整性。SQLiteDatabase也提供了Transaction的相关方法,常见用法:

    db.beginTransaction();
       try {
         ...
         db.setTransactionSuccessful();
       } finally {
         db.endTransaction();
       }

    使用事务对于批量更新有极大的好处,因为单次更新会频繁的调用数据库

    • 除此之外要有几点要注意

    1.关闭Cursor
    Cursor如果不关闭,虽然不会导致出错,但是Log中会有错误提示,还是严谨点,Activity中有startManagingCursor的方法,Activity会在生命周期结束时关闭这些Cursor,其他地方,我们则需要用完关闭,以前需要CursorAdapter则需要在changeCursor时判断关闭old cursor,在ActivityonDestory方法中关闭cursor
    2.关闭DatabaseHelper
    在上述单例Helper例子中,其实一直没有关闭数据库,但是我们阅读getReadabeDatabasegetWritableDatabas的方法,他们会关闭Old SQLiteDatabase的,我们只需要在ApplicationonTerminal方法中关闭即可,这样也能避免多线程中,一个线程关闭了数据库,导致其他线程使用的时候失败的问题。
    实质上,数据库是一个文件引用,单例模式下,不关闭也不会出现问题,让它保持随单例的生命周期关闭就好了。
    3.在循环外面获取ColumnIndex,如果表中列不是很多,每次查询又返回所有列的话,可以将列的index定义到TABLE_COLUMNS中去,这样每次获取指定列数据的话,就不用去查找index了。
    4.数据库存放的数据类型
    Android提供了多种数据存储的方法,文件,数据库,SharePreference,网络等,要根据情况选择合适的方式,不要把什么东西都往数据库中塞。
    下面的几种情况就不适合放到数据库中:
    1)图片等二进制数据:如果是图片的话,可以将文件名称或者路径保存到数据库中,真正的文件可以作为缓存文件保存在文件系统中。
    2)临时数据:定位获取到的Location,登录的Session等。
    3)日志数据:可以写入文件中,通常是log_xxxx.txt

  • 相关阅读:
    Flutter Web预览时白屏解决方法
    提高IIS并发数量设置
    学习如何看懂SQL Server执行计划(一)——数据查询篇
    Sql Server 死锁相关sql语句
    IIS共享文件站点配置
    Newtonsoft.Json保留小数Convert
    git克隆代码出现Authentication failed for “http://xxxxxx“ 解决方案
    Sql Server无用索引查询
    Sql Server索引基本知识篇
    SQL Server 锁机制 悲观锁 乐观锁 实测解析
  • 原文地址:https://www.cnblogs.com/kkong/p/3555282.html
Copyright © 2020-2023  润新知