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

、、、

如有什麼問題和建議歡迎大家提出。


 

行爲真正偉大的人,別人會從他的善行感受出來。一天沒有臆見善行,就是白過了。獎章和頭銜不能讓你上天堂,善行才能增加你的分量。建設性的行爲才能服人,言語的吹噓無益。不要說你想要什麼,用行爲表達。善行是讚美自己最好的辦法。如果你比別人更具智慧,別人會從你的行爲看出來。

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