背景
- 我們的項(xiàng)目中使用的是ormlite的加密框架sqlcipher來(lái)進(jìn)行數(shù)據(jù)庫(kù)操作的
多進(jìn)程操作同一個(gè)數(shù)據(jù)庫(kù)文件出現(xiàn)了問(wèn)題
net.sqlcipher.database.SQLiteException: error code 5: database is locked
at net.sqlcipher.database.SQLiteStatement.native_execute(Native Method)
at net.sqlcipher.database.SQLiteStatement.executeInsert(SQLiteStatement.java:84)
at com.j256.ormlite.sqlcipher.android.AndroidDatabaseConnection.insert(AndroidDatabaseConnection.java:158)
at com.j256.ormlite.stmt.mapped.MappedCreate.insert(MappedCreate.java:91)
at com.j256.ormlite.stmt.StatementExecutor.create(StatementExecutor.java:450)
at com.j256.ormlite.dao.BaseDaoImpl.create(BaseDaoImpl.java:310)
at com.xtc.database.encrypt.OrmLiteDao.insert(OrmLiteDao.java:99)
at com.xtc.bigdata.report.db.ReportDao.insert(ReportDao.java:46)
at com.xtc.bigdata.report.db.UserBehaviorProvider.insert(UserBehaviorProvider.java:113)
at android.content.ContentProvider$Transport.insert(ContentProvider.java:264)
at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:163)
at android.os.Binder.execTransact(Binder.java:565)
如上異常堆棧中的錯(cuò)誤信息error code 5: database is locked,經(jīng)過(guò)查找發(fā)現(xiàn)code為5代表sqlite中的SQLITE_BUSY異常,詳見:https://www.sqlite.org/rescode.html#busy,這里面說(shuō),SQLITE_BUSY(5)異常是一個(gè)數(shù)據(jù)庫(kù)文件在被其他不同的數(shù)據(jù)庫(kù)連接進(jìn)行并發(fā)操作的時(shí)候?qū)懖僮鲗⒀a(bǔ)發(fā)繼續(xù),通常是多個(gè)進(jìn)程的不同數(shù)據(jù)庫(kù)連接對(duì)同一個(gè)數(shù)據(jù)庫(kù)進(jìn)行并發(fā)操作,例如進(jìn)程A在進(jìn)行耗時(shí)的數(shù)據(jù)庫(kù)事務(wù),而于此同時(shí)進(jìn)程B也要進(jìn)行一個(gè)數(shù)據(jù)庫(kù)事務(wù),這時(shí)候進(jìn)程B就會(huì)直接返回SQLITE_BUSY的錯(cuò)誤碼,因?yàn)閟qlite只能支持同一個(gè)時(shí)刻只能有一個(gè)寫操作,所以解決這個(gè)問(wèn)題的方法就是避免不同進(jìn)程分別對(duì)同一個(gè)數(shù)據(jù)庫(kù)各自開啟一個(gè)database connection,并且對(duì)相同的數(shù)據(jù)庫(kù)進(jìn)行并發(fā)操作,如果有這種需求,那么應(yīng)該全部都交給一個(gè)進(jìn)程來(lái)對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作,其他的進(jìn)程想操作這個(gè)數(shù)據(jù)庫(kù)就通過(guò)contentprovider的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)共享,使用contentprovider的方式是最安全的,如果是通過(guò)shareUserId的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)共享也是不安全的,因?yàn)椋?/p>
Context thdContext = null;
try {
thdContext = createPackageContext(
"com.example.testdatabase",
Context.CONTEXT_IGNORE_SECURITY);
String dbPath = thdContext.getDatabasePath("BookStore.db")
.getAbsolutePath();
SQLiteDatabase db = SQLiteDatabase.openDatabase(dbPath,
null, SQLiteDatabase.OPEN_READWRITE);
Cursor cursor = db.query("Book", null, null, null, null,
null, null);
if (cursor.moveToFirst()) {
do {
String name = cursor.getString(cursor
.getColumnIndex("name"));
Log.d(TAG, "name: " + name);
} while (cursor.moveToNext());
}
} catch (NameNotFoundException e) {
e.printStackTrace();
}
SQLiteDatabase.openDatabase會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)實(shí)例SQLiteDatabase,如果在不同的進(jìn)程如果通過(guò)shareuserid來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)共享,那么會(huì)造成每一個(gè)進(jìn)程都有SQLiteDatabase對(duì)象,在并發(fā)操作的時(shí)候也有可能會(huì)出現(xiàn)如上問(wèn)題,所以還是推薦使用contentprovider的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)共享,必須注意contentprovider必須只有宿主app進(jìn)程來(lái)維護(hù),其他的進(jìn)程就通過(guò)調(diào)用宿主app進(jìn)程的contentprovider暴露出去的接口來(lái)實(shí)現(xiàn)對(duì)宿主app進(jìn)程的數(shù)據(jù)庫(kù)的操作,實(shí)際上這時(shí)候的數(shù)據(jù)庫(kù)操作就都是由宿主app進(jìn)程來(lái)操作的了,就不會(huì)出現(xiàn)如上的異常
拓展
上面提及的數(shù)據(jù)庫(kù)操作異常的code是5,對(duì)應(yīng)的是SQLITE_BUSY,這里還有一個(gè)相似的數(shù)據(jù)庫(kù)操作異常,code為6,對(duì)應(yīng)的是SQLITE_LOCKED,詳見:https://www.sqlite.org/rescode.html#busy,具體意思就是說(shuō),SQLITE_LOCKED錯(cuò)誤碼是在同一個(gè)數(shù)據(jù)庫(kù)連接存在沖突,或者不同的數(shù)據(jù)庫(kù)連接共享相同的數(shù)據(jù)庫(kù)緩存存在沖突的時(shí)候,寫操作將無(wú)法繼續(xù),這里的沖突是什么意思呢?比如,有一個(gè)刪除表的操作發(fā)生在其他的線程在對(duì)這個(gè)表進(jìn)行讀操作的過(guò)程中,那么就會(huì)報(bào)SQLITE_LOCKED異常,也就是說(shuō)一個(gè)線程的刪除表操作和另一個(gè)線程對(duì)相同表的讀取操作存在沖突,前提是這兩個(gè)操作都是使用同一個(gè)數(shù)據(jù)庫(kù)連接
java.lang.IllegalStateException: get field slot from row 0 col 0 failed異常,這個(gè)異常是數(shù)據(jù)庫(kù)在執(zhí)行查詢操作的時(shí)候,如果數(shù)據(jù)庫(kù)中的一條記錄所占用的內(nèi)存大于1MB的話,這時(shí)候查詢操作就會(huì)報(bào)錯(cuò),解決方法就是讓每一條的數(shù)據(jù)庫(kù)記錄的大小都不要超過(guò)1MB,這里是單條記錄的大小不能超過(guò)1MB,如果是每條數(shù)據(jù)庫(kù)記錄大小都不超過(guò)1MB,但是10條加起來(lái)超過(guò)1MB,那這是沒有問(wèn)題的,此問(wèn)題在舊版的sqlcipher會(huì)出現(xiàn),但是在新版的sqlcipher貌似已經(jīng)修復(fù)了這個(gè)bug,但是只是提高了1MB的閥值,至于怎么提高的?看下面這個(gè)issue:
We are glad to hear 3.5.7 is working well for you. We've adjusted the library to allow it to dynamically resize the backing memory for the cursor, so you would be limited by the device.
詳見:https://github.com/sqlcipher/android-database-sqlcipher/issues/341#issuecomment-310289295,現(xiàn)在是改成動(dòng)態(tài)來(lái)分配大小的,所以限制的上限就會(huì)由機(jī)器來(lái)決定,也就是說(shuō),仍然存在這個(gè)問(wèn)題,如果存入數(shù)據(jù)庫(kù)的記錄太大,還是有可能發(fā)生此異常,我們不建議讓sqlite數(shù)據(jù)庫(kù)中去存儲(chǔ)blog這種大的數(shù)據(jù)記錄,應(yīng)該大的數(shù)據(jù)記錄存成文件,然后把文件路徑存到數(shù)據(jù)庫(kù)中會(huì)更加合適