高效解决「SQLite」数据库并发访问安全问题,只这一篇就够了

Concurrent database access



本文译自:https://dmytrodanylyk.com/articles/concurrent-database/

对于 Android Dev 而言,有关 SQLite 的操作再经常不过了,相比你一定经历过控制台一片爆红的情况,这不禁让我们疑问:SQLite 到底是线程安全的吗?

OK 废话不多说,我们 ??

直接开始


首先,假设你已经实现了一个 SQLiteHelper 类,如下所示:

public class DatabaseHelper extends SQLiteOpenHelper { ... }

现在你想要在两个子线程中,分别地向 SQLite 里写入一些数据:

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

对吧?看上去很 OK 没啥毛病。

那么这时,我们点一下 run ,gio~ 你将会在你的 logcat 里收到如下礼物「报错」:

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

到底是怎么回事呢?

我们分析一下报错终于发现:这是由于你每次创建 SQLiteHelper 时,都对数据库进行了一个链接操作。这时,如果你尝试着,同时从实际不同的链接中,对数据库进行写入操作,失败就是必然的了。

总结一下
如果我们想再不同的线程中,对数据库进行包括读写操作在内的任何使用,我们就必须得确保,我们使用的是同一个的连接

好,那现在问题就明了了。现在让我们创建一个单例模式类:DatabaseManager 用来创建和返回唯一的,单例 DatabaseManager 对象。

ps 有些同学问我什么是单例模式,我专门跑去写了这篇博客来解释下,单例模式-全局可用的 context 对象,这一篇就够了码字不易帮我点个赞谢谢 ??

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 synchronized SQLiteDatabase getDatabase() {
        return mDatabaseHelper.getWritableDatabase();
    }

}

现在,我们在回来修改下之前的代码,结果如下所示:

// In your application class
DatabaseManager.initializeInstance(new DatabaseHelper());

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

逻辑比之前更清晰,代码冗余也少了。现在我们在跑下代码,这时我们会收到,另一个 cache

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

不要慌,我们仔细分析下报错,我们发现:单例模式的使用保证了我们,在线程一、二「Thread 1、Thread 2 中」只会获得到唯一的 SQLiteHelper 对象,但这时问题就来了,当我们运行完线程一「Thread 1」时,我们的 database.close(); 已经替我们关闭了对数据库的连接,但与此同时我们的线程二「Thread 2」依然保持这对 SQLiteHelper 的引用。正是这个原因,我们收到了IllegalStateException的报错。

所以,这时我们就需要保证,当没有人使用 SQLiteHelper 时,再将其断开连接。

保证 SQLIiteHelper 在无人使用时才断开连接

关于这个问题的解决 stackoveflow 上很多人建议我们:永远不要断开 SQLiteHelper 的连接,但是这样以来你会在 logcat 上得到如下输出:

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

所以,我非常不建议你用这个方法。为了解决这个问题,我们引入计数器的概念

标准样例

通过如下方法,你将通过一个计数器来完美解决 打开/关闭 数据库连接的问题:

public class DatabaseManager {

    private AtomicInteger mOpenCounter = new AtomicInteger();

    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() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

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

        }
    }
}

我们在线程中可以这样使用它:

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

每当你需要使用数据库时,你只要调用 DatabaseManager 中的 openDatabase() 方法。在这个方法中,我们有一个,用来记录数据库被“打开”了几次的 mOpenCounter 对象。当它等于 1 时,这意味着你需要去创建新的数据库连接来使用数据库,否则的话,就说明数据库已经在使用中了。

同样的情况也发生在 closeDatabase() 方法中,当你每次调用该方法时,我们的 mOpenCounter 对象就会减一。当它减到 0 时,我们就去关闭这个数据库的连接。

完美,最后:

  1. 现在你就能随心所欲的使用你的数据库,而且你可以相信 -- 它是线程安全的了!
  2. 当然很多同学对数据库的使用,还有着很多的疑惑,我后期将会针对数据库的使用,作出一系列总结,有兴趣可以继续关注 _yuanhao 的编程世界

相关文章


每个人都要学的图片压缩终极奥义,有效解决 Android 程序 OOM
Android 让你的 Room 搭上 RxJava 的顺风车 从重复的代码中解脱出来
ViewModel 和 ViewModelProvider.Factory:ViewModel 的创建者
单例模式-全局可用的 context 对象,这一篇就够了
缩放手势 ScaleGestureDetector 源码解析,这一篇就够了
Android 属性动画框架 ObjectAnimator、ValueAnimator ,这一篇就够了
看完这篇再不会 View 的动画框架,我跪搓衣板
看完这篇还不会 GestureDetector 手势检测,我跪搓衣板!
android 自定义控件之-绘制钟表盘
Android 进阶自定义 ViewGroup 自定义布局

欢迎关注_yuanhao的博客园!



定期分享Android开发湿货,追求文章幽默与深度的完美统一。

关于源码 Demo 链接:Demo 码了好几天才整完,希望大家点个 star~ 谢谢!

请点赞!因为你的鼓励是我写作的最大动力!

原文地址:https://www.cnblogs.com/yuanhao-1999/p/11629609.html

时间: 2024-12-16 14:26:40

高效解决「SQLite」数据库并发访问安全问题,只这一篇就够了的相关文章

基于Oracle数据库锁机制,解决集群中的并发访问问题

1.需求 应用场景是这样的: 使用Oracle数据保存待办任务,使用状态字段区分任务是否已经被执行.多个Worker线程同时执行任务,执行成功或失败后,修改状态字段的值. 假设数据库表结构如下所示. create table Task( id varchar2(32), name varchar2(32), flag varchar2(1), worker varchar2(32) ); flag 可取的值包括:0-待办,1-已办,-1-失败待重试. 需要避免的问题: 多个Worker同时工作时

你还没掌握超表「视图」, 难怪觉得数据繁杂得要命!

视图是什么? 一张表格可以生成无数的视图 每张表格都由行与列构成,所有的行与所有的列构成了全部数据.用户可以对表格所有的行和列自由取舍与排列,构成一个张新的自定义的"行与列",我们把它称之为「视图」. 因此 我们可以只共享视图(部分数据) 无须共享整张表格(全部数据) 更深刻的了解视图共享功能 ■ ■ ■ ■ ■ 全部数据 所有的行与所有的列合在一起就构成了全部数据 (表格默认展示的数据) ■ ■ ■ ■ ■ 新建视图 点击「视图」右侧「三点」,新建视图 ■ ■ ■ ■ ■ 设置视图

java设计模式--解决单例设计模式中懒汉式线程安全问题

首先写个单例,懒汉模式: public class SingleDemo { private static SingleDemo s = null; private SingleDemo(){} public static SingleDemo getInstance(){ if(s == null){ s = new SingleDemo(); } return s; } } 写个测试类: public class ThreadDemo3 { public static void main(S

解决单例设计模式中懒汉式线程安全问题

首先写个单例: public class SingleDemo { private static SingleDemo s = null; private SingleDemo(){} public static SingleDemo getInstance(){ if(s == null){ s = new SingleDemo(); } return s; } } 写个测试类: public class ThreadDemo3 { public static void main(String

解决数据库高并发访问瓶颈问题

一.缓存式的Web应用程序架构: 在Web层和db层之间加一层cache层,主要目的:减少数据库读取负担,提高数据读取速度.cache存取的媒介是内存,可以考虑采用分布式的cache层,这样更容易破除内存容量的限制,同时增加了灵活性. 二.实现MySQL数据库异步查询实现: 通常情况下在PHP中MySQL查询是串行的,如果能实现MySQL查询的异步化,就能实现多条SQL语句同时执行,这样就能大大地缩短MySQL查询的耗时,提高数据库查询的效率.目前MySQL的异步查询只在MySQLi扩展提供,查

大数据量高并发访问的数据库优化方法

一.数据库结构的设计 如果不能设计一个合理的数据库模型,不仅会增加客户端和服务器段程序的编程和维护的难度,而且将会影响系统实际运行的性能.所以,在一个系统开始实施之前,完备的数据库模型的设计是必须的. 在一个系统分析.设计阶段,因为数据量较小,负荷较低.我们往往只注意到功能的实现,而很难注意到性能的薄弱之处,等到系统投入实际运行一段时间后,才发现系统的性能在降低,这时再来考虑提高系统性能则要花费更多的人力物力,而整个系统也不可避免的形成了一个打补丁工程. 所以在考虑整个系统的流程的时候,我们必须

Android 高效的SQLite型数据库greenDAO使用

使用:在你project项目的build.gradle配置如下:buildscript {repositories {mavenCentral()}dependencies {classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'}} // 使用数据库升级辅助GreenDaoUpgradeHelper时添加allprojects {repositories {maven { url "https://jitpack.io" }}}

SqlIte数据库并发性

把遇到的一些小问题都记下来,告诉自己,一些小细节会铸成打错的 今天没事复习以前的知识,用sqlite做数据库,发现修改数据的时候等好久才有反应,而且还失败,可是过一会之后又会好,好了以后又是一样,种以为是自己的语句有问题,测试了好多次,感觉没问题,在到网上查查错误才发现,原来sqlite不可以并发处理数据,我要说的不是这个问题,一个好的习惯可以避免所有的小问题,前面的解决方法就是把datareader等要释放的释放,关闭的关闭就可以,这本来就是要的,但是自己却没有这么做,这一个小问题纠结了我一个

Oracle 数据库中不同事务并发访问的问题

现象 以SQL/Helper为例,打开不同的SQL窗口,对同一个表格进行操作,如下所示. 窗口1:当执行更新任务.紧接着执行查询时获得一组查询结果.结果是对的. 窗口2:而在另外一个SQL查询窗口中执查询,却得到更新前的结果. 当关闭窗口1时,执行窗口2,发现出现正确的更新结果. 分析 初步分析是数据库的并发访问的问题.当在窗口1中执行更新操作,对数据表格的引用并未有消除,而是在数据库中产生了针对当前事务的副本结果.而窗口2所针对的查询还是旧的更新前的数据库原本.当关闭窗口1后,数据库中的表格被