SQLite并发操作下的分析与处理,解决database is locked,以及多线程下执行事务等问题

转载:http://www.codexiu.cn/android/blog/37889/

最近公司的项目处于重构阶段,观察后台crash log的时候发现了一个发生很多的问题:

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: PRAGMA journal_mode

看了一下报错具体位置:

嗯,很简单,那就改成同步。

这边先说一下database is locked产生的原因:sqlite同一时间只能进行一个写操作,当同时有两个写操作的时候,后执行的只能先等待,如果等待时间超过5秒,就会产生这种错误.同样一个文件正在写入,重复打开数据库操作更容易导致这种问题的发生。

那首先,得避免重复打开数据库,首先引入单例方法与SQLiteOpenHelper类:

public class DBOpenHelper extends SQLiteOpenHelper{
	private DBOpenHelper(Context context,String dbPath, int version) {
		super(context, dbPath , null, version);
	}
	private volatile static DBOpenHelper uniqueInstance;
	public static DBOpenHelper getInstance(Context context) {
		if (uniqueInstance == null) {
			synchronized (DBOpenHelper.class) {
				if (uniqueInstance == null) {
					uniqueInstance = new DBOpenHelper(context,context.getFilesDir().getAbsolutePath()+"/foowwlite.db",1);
				}
			}
		}
		return uniqueInstance;
	}
	@Override
	public void onCreate(SQLiteDatabase db) {
	}
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
	}
}


通过getInstance()方法得到helper对象来得到数据库,保证helper类是单例的。

然后通过代理类SQLiteDataProxy来控制对数据库的访问:

private static SQLiteDataProxy proxy;
private static DBOpenHelper helper;

public static SQLiteDataProxy getSQLiteProxy(Context context) {
	helper = DBOpenHelper.getInstance(context);
	if (proxy == null) {
		synchronized (SQLiteDataProxy.class) {
			if (proxy == null) {
				proxy = new SQLiteDataProxy();
			}
		}
	}
	return proxy;
}


同样使用单例来保证对象的唯一性。然后我写了一个方法用于执行sql语句

@Override
public boolean execSQL(String sql) {
	boolean result = true;
	if (!db.isOpen()) {
		db = helper.getWritableDatabase();
	}
	try {
		db.execSQL(sql);
	} catch (Exception e) {
		Log.e("SQLERROR", "In SQLDA:" + e.getMessage() + sql);
		result = false;
	} finally {
		db.close();
	}
	return result;
}

 

每次获取db对象即SQLiteDataBase的时候先判断是否已经打开,如果已经打开则不需要重新获取,避免了db的重复开启。

接下来我们试验一下,这边我通过viewpager+fragment的方式测试,因为fragment会同时被加载。在两个fragment中各自新建一个子线程来执行大量insert语句:

new Thread(new Runnable() {
	@Override
	public void run() {
		for (String sql:getSQLList()){
			SQLiteDataProxy.getSQLiteProxy(getActivity()).execSQL(sql);
		}
	}
}).start();

 

先分析一下:虽然都是子线程,但是两个fragment都是通过单例获得db对象来执行sql语句,因此db应该是同一个的,这时候他们并发执行sql应该没有问题。

但是运行还是报错了:

attempt to re-open an already-closed object,意思是数据库已经关闭了,但是我却仍然用一个已经关闭的db去执行sql语句,可是为什么错呢,明明已经判断了db.isOpen,再执行sql的。

其实此时的原因如下:

线程一和线程二同时执行execSQL方法,线程一先获取到了db对象,开始执行sql语句,于此同时,线程二判断db.isOpen,发现是打开的于是不重新get,直接调用db开始执行sql语句。但现在问题来了,线程二准备执行sql的时候,线程一已经把sql执行完,并且关闭,由于线程一和线程二得到的db是同一个对象,线程二的db也关闭了,这时执行sql就导致了这个错误。

接下来如何是好。。。

这种情况,我们可以引入一个全局变量来计算打开关闭db的次数,而java刚好提供了这个方法AtomicInteger

AtomicInteger是一个线程安全的类,我们可以通过它来计数,无论什么线程AtomicInteger值+1后都会改变

我们讲判断db是否打开的方法改成以下:

private AtomicInteger mOpenCounter = new AtomicInteger();
private SQLiteDatabase getSQLiteDataBase() {
	if (mOpenCounter.incrementAndGet() == 1) {
		db = helper.getWritableDatabase();
	}
	return db;
}

关闭db的方法改为如下:

private void closeSQLiteDatabase(){
	if(mOpenCounter.decrementAndGet() == 0){
		db.close();
	}
}


而上面的意思就是:

每当想要获得db对象是计数器mOpenCounter会+1,第一次打开数据库mOpenCounter是0,mOpenCounter调用incrementAndGet()方法后+1等于1说明还没有被获得,此时有第二个线程想执行sql语句,它在执行getSQliteDataBase方法的时候mOpenCounter是1,然后mOpenCounter+1=2不等于1,说明db已经开启,直接return db即可。
在关闭db的时候,mOpenCounter会首先减1,如果mOpenCounter==0则说明此时没有其他操作,就可以关闭数据库,如果不等於则说明还有其他sql在执行,就不去关闭。
接着上面的说,两个线程各自执行想执行sql,此时mOpenCounter是2,当线程一的sql执行完后,线程一的db尝试关闭,会调用mOpenCounter.decrementAndGet()自减1,decrementAndGet-1后就等于1,说明还有一个正在执行的sql,即线程二正在执行。因此db不会去关闭,然后线程二正常执行,线程二执行完sql,尝试关闭db,此时decrementAndGet再自减1,就等于0,说明已经没有其他真正执行的sql,于是可以正常关闭。
这种判断方法保证了只有在所有sql都执行完后才去关闭,并且只会最后一次关闭,保证了不会出现re-open an already-closed这种问题。

这个方法再其他博客中也有说明,说是保证了觉得安全,但是,经过测试说明,那些博客都是抄袭的,并没去真正实验,接下来我就说明一下它为什么不行。

修改以后,我们再测试一下。

结果还是报错。

很显然db为null,这是一个空指针错误,但是为什么会导致这种错误呢?

分析一下AtomicInteger,并没有逻辑上的问题啊。

我们把代码改成如下,便于打印log日志:

private SQLiteDatabase getSQLiteDataBase() {
	Log.e("111", "Once start");
	if (mOpenCounter.incrementAndGet() == 1 || db == null) {
		db = helper.getWritableDatabase();
	}
	if (db == null) {
		Log.e("111", mOpenCounter.intValue() + "");
	} else {
		Log.e("111", mOpenCounter.intValue() + " NOT NULL");
	}
	return db;
}


运行后结果如下:

稍微想一下就知道了,线程一和二同时尝试获取db,线程一中mOpenCounter+1==1,但此时db还没有获取的情况下,线程二也执行了获取db的方法,mOpenCounter+1==2,单由于获取db的getWritableDatabase()需要一定的时间,而先执行的线程一db还没有被获取到,线程二却已经也经过判断并且return db,此时的db就是null了,导致了空指针错误。

原因已经找到了,那么解决就很简单,只需要多加一个非空判断就行,而getWriteableDataBase本身就是线程安全的,应该只需要这样就可以解决。

private SQLiteDatabase getSQLiteDataBase() {
	if (mOpenCounter.incrementAndGet() == 1 || db == null) {
		db = helper.getWritableDatabase();
	}
	return db;
}

这样修改好以后,经过测试没有问题。

接下来解决另一个问题:如果当前执行的是许多sql语句,要用到事务怎么办?

如果是事物,大家都知道,事物执行的时候调用beginTransaction(),完成后调用db.setTransactionSuccessful()、db.endTransaction()标志着事务的结束,但是如果多线程下调用了事务怎么办?尤其还是单例模式下,同时调用方法开启事务,这肯定会出问题。
如以下方法:

public boolean execSQLList(List<String> sqlList) {
	boolean result = true;
	db = getSQLiteDataBase();
	String currentSqlString = "";
	try {
		db.beginTransaction();
		for (String sql : sqlList) {
			currentSqlString = sql;
			db.execSQL(sql);
		}
		db.setTransactionSuccessful();
		result = true;
	} catch (Exception e) {
		result = false;
		Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
	} finally {
		db.endTransaction();
		closeSQLiteDatabase();
	}
	return result;
}


for循环中间执行sql,并且开始和结束分别打开关闭事务。

为了解决这个问题,只有保证执行事务时是同步的,但是多线程调用我们如何控制其同步呢。

这里要引入一个类java.util.concurrent.Semaphore,这个类可以用来协调多线程下的控制同步的问题。

首先初始化这个类,并且设置信号量为1

private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore(1);


这句话的意思是多线程下的调用许可数为1,当

semaphoreTransaction.acquire()

执行后,semaphore会检测是否有其他信号量已经执行,如果有,改线程就会停止,直到另一个semaphore释放资源之后,才会继续执行下去,即:

semaphoreTransaction.release();

我们只需要在开始事务前调用acquire方法,当其他事务想要执行,会先判断,如果有事务在执行,该线程就会等待,直到前一个事物结束并调用release之后,该线程的事务就会继续进行,这样解决事务并发产生的问题,也保证了事务都可以执行完毕。

改进后代码如下:

private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore(1);
public boolean execSQLList(List<String> sqlList) {
	boolean result = true;
	db = getSQLiteDataBase();
	String currentSqlString = "";
	try {
		semaphoreTransaction.acquire();
		db.beginTransaction();
		for (String sql : sqlList) {
			currentSqlString = sql;
			db.execSQL(sql);
		}
		db.setTransactionSuccessful();
		result = true;
	} catch (Exception e) {
		result = false;
		Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
	} finally {
		db.endTransaction();
		semaphoreTransaction.release();
		closeSQLiteDatabase();
	}
	return result;
}


经过上面的修改以后,经过测试,多线程下事务也可以正常插入,一些常见的由SQLite并发产生的问题也得以解决。

全部代码如下:

ISQLiteOperate:

package com.xiaoqi.sqlitedemo;
import android.database.Cursor;
import java.util.List;
/**
 * Created by xiaoqi on 2016/9/1.
 */
public interface ISQLiteOperate {
	boolean execSQL(String sql);
	boolean execSQLList(List<String> sqlList);
	boolean execSQLs(List<String[]> sqlList);
	boolean execSQLIgnoreError(List<String> sqlList);
	Cursor query(String sql);
	Cursor query(String sql, String[] params);
	void close();
}

DBOpenHelper:

package com.xiaoqi.sqlitedemo;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
/**
 * Created by xiaoqi on 2016/9/1.
 */
public class DBOpenHelper extends SQLiteOpenHelper{
	private DBOpenHelper(Context context,String dbPath, int version) {
		super(context, dbPath , null, version);
	}
	private volatile static DBOpenHelper uniqueInstance;
	public static DBOpenHelper getInstance(Context context) {
		if (uniqueInstance == null) {
			synchronized (DBOpenHelper.class) {
				if (uniqueInstance == null) {
					uniqueInstance = new DBOpenHelper(context,context.getFilesDir().getAbsolutePath()+"/foowwlite.db",1);
				}
			}
		}
		return uniqueInstance;
	}
	@Override
	public void onCreate(SQLiteDatabase db) {
	}
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
	}
}



SQLiteDataProxy:

package com.xiaoqi.sqlitedemo;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class SQLiteDataProxy implements ISQLiteOperate {
	private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore(1);
	private AtomicInteger mOpenCounter = new AtomicInteger();
	private SQLiteDatabase db;
	private Cursor cursor;
	private SQLiteDataProxy() {
	}
	private static SQLiteDataProxy proxy;
	private static DBOpenHelper helper;
	public static SQLiteDataProxy getSQLiteProxy(Context context) {
		helper = DBOpenHelper.getInstance(context);
		if (proxy == null) {
			synchronized (SQLiteDataProxy.class) {
				if (proxy == null) {
					proxy = new SQLiteDataProxy();
				}
			}
		}
		return proxy;
	}
	private SQLiteDatabase getSQLiteDataBase() {
		if (mOpenCounter.incrementAndGet() == 1) {
			db = helper.getWritableDatabase();
		}
		return db;
	}
	private void closeSQLiteDatabase(){
		if(mOpenCounter.decrementAndGet() == 0){
			db.close();
		}
	}
	@Override
	public boolean execSQL(String sql) {
		boolean result = true;
		db = getSQLiteDataBase();
		try {
			db.execSQL(sql);
		} catch (Exception e) {
			Log.e("SQLERROR", "In SQLDA:" + e.getMessage() + sql);
			result = false;
		} finally {
			closeSQLiteDatabase();
		}
		return result;
	}
	@Override
	public boolean execSQLList(List<String> sqlList) {
		boolean result = true;
		db = getSQLiteDataBase();
		String currentSqlString = "";
		try {
			semaphoreTransaction.acquire();
			db.beginTransaction();
			for (String sql : sqlList) {
				currentSqlString = sql;
				db.execSQL(sql);
			}
			db.setTransactionSuccessful();
			result = true;
		} catch (Exception e) {
			result = false;
			Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
		} finally {
			db.endTransaction();
			semaphoreTransaction.release();
			closeSQLiteDatabase();
		}
		return result;
	}
	@Override
	public boolean execSQLs(List<String[]> sqlList) {
		boolean result = true;
		db = getSQLiteDataBase();
		String currentSql = "";
		try {
			semaphoreTransaction.acquire();
			db.beginTransaction();
			for (String[] arr : sqlList) {
				currentSql = arr[0];
				Cursor curCount = db.rawQuery(arr[0], null);
				curCount.moveToFirst();
				int count = curCount.getInt(0);
				curCount.close();
				if (count == 0) {
					if (arr[1] != null && arr[1].length() > 0) {
						currentSql = arr[1];
						db.execSQL(arr[1]);
					}
				} else {
					if (arr.length > 2 && arr[2] != null && arr[2].length() > 0) {
						currentSql = arr[2];
						db.execSQL(arr[2]);
					}
				}
			}
			db.setTransactionSuccessful();
			result = true;
		} catch (Exception e) {
			Log.e("SQLERROR", "IN SQLDA: " + currentSql + e.getMessage());
			result = false;
		} finally {
			db.endTransaction();
			semaphoreTransaction.release();
			closeSQLiteDatabase();
		}
		return result;
	}
	@Override
	public boolean execSQLIgnoreError(List<String> sqlList) {
		db = getSQLiteDataBase();
		try {
			semaphoreTransaction.acquire();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		db.beginTransaction();
		for (String sql : sqlList) {
			try {
				db.execSQL(sql);
			} catch (Exception e) {
				Log.e("SQLERROR", "IN SQLDA: " + sql + e.getMessage());
			}
		}
		db.setTransactionSuccessful();
		db.endTransaction();
		semaphoreTransaction.release();
		closeSQLiteDatabase();
		return true;
	}
	@Override
	public Cursor query(String sql) {
		return query(sql, null);
	}
	@Override
	public Cursor query(String sql, String[] params) {
		db = getSQLiteDataBase();
		cursor = db.rawQuery(sql, params);
		return cursor;
	}
	@Override
	public void close() {
		if (cursor != null) {
			cursor.close();
		}
		if (db != null) {
			db.close();
		}
	}
}


DBManager:

package com.xiaoqi.sqlitedemo;
import android.content.Context;
import android.database.Cursor;
import java.util.List;
public class DBManager {
	public static void asyncExecSQL(final Context context, final String sql){
		new Thread(new Runnable() {
			@Override
			public void run() {
				SQLiteDataProxy.getSQLiteProxy(context).execSQL(sql);
			}
		}).start();
	}
	public static void asyncExecSQLList(final Context context,final List<String> sqlList){
		new Thread(new Runnable() {
			@Override
			public void run() {
				SQLiteDataProxy.getSQLiteProxy(context).execSQLList(sqlList);
			}
		}).start();
	}
	public static void asyncExecSQLs(final Context context,final List<String[]> sqlList){
		new Thread(new Runnable() {
			@Override
			public void run() {
				SQLiteDataProxy.getSQLiteProxy(context).execSQLs(sqlList);
			}
		}).start();
	}
	public static void asyncExecSQLIgnoreError(final Context context,final List<String> sqlList){
		new Thread(new Runnable() {
			@Override
			public void run() {
				SQLiteDataProxy.getSQLiteProxy(context).execSQLIgnoreError(sqlList);
			}
		}).start();
	}
	public static boolean execSQL( Context context, String sql){
		return SQLiteDataProxy.getSQLiteProxy(context).execSQL(sql);
	}
	public static boolean execSQLList( Context context, List<String> sqlList){
		return SQLiteDataProxy.getSQLiteProxy(context).execSQLList(sqlList);
	}
	public static boolean execSQLs( Context context, List<String[]> sqlList){
		return SQLiteDataProxy.getSQLiteProxy(context).execSQLs(sqlList);
	}
	public static boolean execSQL( Context context, List<String> sqlList){
		return SQLiteDataProxy.getSQLiteProxy(context).execSQLIgnoreError(sqlList);
	}
	public static Cursor query(Context context, String sql){
		return SQLiteDataProxy.getSQLiteProxy(context).query(sql);
	}
	public static Cursor query(Context context, String sql, String[] params){
		return SQLiteDataProxy.getSQLiteProxy(context).query(sql, params);
	}
	public static void close(Context context){
		SQLiteDataProxy.getSQLiteProxy(context).close();
	}
}



建议使用的时候不要直接调用SQLiteDataProxy的方法,而是通过DBManager来执行SQL操作。

使用方法很简单:

DBManager.asyncExecSQLList(context,getSQLList())
DBManager.execSQLList(context,getSQLList())

、、、

如有什么问题和建议欢迎大家提出。


 

行为真正伟大的人,别人会从他的善行感受出来。一天没有臆见善行,就是白过了。奖章和头衔不能让你上天堂,善行才能增加你的分量。建设性的行为才能服人,言语的吹嘘无益。不要说你想要什么,用行为表达。善行是赞美自己最好的办法。如果你比别人更具智慧,别人会从你的行为看出来。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章