Comments (83)
@casatwy 老哥,我又回来了,数据库偶尔崩溃的问题找到了原因了。
使用场景我描述下:
1、所有的读写操作都在CTPersistanceAsyncExecutor读写锁的控制下
2、项目中有单例持有DataCenter->table->queryCommand->database->sqlite3 *database
问题出在操作使用table实例的时候,对于同一个table多个读操作并发的时候,用到的是同一个table实例(也就是同一个sqlite3 *database实例),如果第一个读操作结束时,另一个正在执行操作。这个时候第一个读操作的线程回收,CTPersistanceDatabasePool里面的didReceiveNSThreadWillExitNotification方法会找到table持有的这个database,调用close方法关掉并置空,这时另外一个正在执行的线程就崩溃了。
现在的模型是多个线程会共享一个table实例(sqlite3 *database),然后第一个创建的线程拥有释放的权利。而且不管其他线程有没有在用,这样就导致了偶尔的崩溃。
from ctpersistance.
我这边选择的解决方案还是基于现有的方案改进了一下,在关闭数据库之前数一数每个数据库有多少线程在操作。
如果某个数据库只有一个线程在操作,而且这个线程就是didReceiveNSThreadWillExitNotification的线程的话,我就认为这个线程打开的数据库是可以安全关闭的。
如果这个线程退出时操作的数据库,别的线程也在使用,那么ownershipCount就会大于1,这个数据库就不会被关闭。
如果ownershipCount等于1,但是记录的线程不是当前退出线程,那么这个数据库就也不会被关闭。
最新版已经改好发版了,版本号是200
from ctpersistance.
我今天看一下
from ctpersistance.
我复现了
from ctpersistance.
发好版了,最新版本号是201
from ctpersistance.
感觉这个像是多线程导致的问题,是不是在执行这个删除操作的时候,还有别的线程在处理相关的事情?
删除操作你是放在异步调用的那个方法里面做的吗?
from ctpersistance.
所有的数据库操作都是放到CTPersistanceAsyncExecutor类里面那三个方法的block里面做的
不过有些对table实例的创建是在block外面的,例如下面这种:
会不会创建table实例的是在fetchMyApplicationList方法调用的线程里面,table实例创建会做createTable的操作,这个跟其他地方提交的block里面的出现了并发的数据库写操作
from ctpersistance.
你可以试试在block里面做所有的事情,包括createTable。
还有,syncRead是会多线程同时操作的,syncWrite一次只允许一个线程操作。
所以是不是有代码可能在syncRead里面做了数据库写的操作?然后多个syncRead可能就同时写了。delete操作算是写操作。
from ctpersistance.
如果一套逻辑里面同时有读有写的,应该都放在syncWrite里,这样才能保证只有唯一线程操作数据库。
from ctpersistance.
嗯,我试下
有些读写是分开的,读放到了syncRead里面,写的操作在write里面,唯一线程能保证的,就是可能会有原子性问题,读写中间可能有其他操作插入;解决这个问题的时候我就把读写都放到一个write里面做了
from ctpersistance.
嗯,先试试看
from ctpersistance.
顺便问个其他的问题,我看insertRecordList里面的实现是for循环每个record单个插入的,意味着数组里面有多少个元素就会执行多少次写入操作,那insertRecordList是不是放到事务里面会快一点
from ctpersistance.
用事务的话就要考虑插入失败的回滚操作,我思考了一下一般客户端很少出现成百上千的操作的,所以就这么做了。
migration的时候倒是建议事务操作。
from ctpersistance.
然后,事务操作也是要再放到write里面做并发控制的吧
这种操作没问题吧
from ctpersistance.
全部操作,加上table实例的创建都在CTPersistanceAsyncExecutor的队列里面调度了,但是有个问题就是,table实例的创建现在是懒加载的,就是说第一次调用在那个block里面,就是跟随在那个block的线程里面做了table实例初始化的create table操作,也就是写操作。这个block可以能是syncRead的block,就出现了你说的写出现在了syncRead里面
例如下面这个:
如果这个syncRead执行的时候,另外又有一个syncRead里面做了createTable,就出现了并发写入的问题
我现在能想到的解决办法就是在把[[table alloc] init]放到一个同步写操作里面,确保这个create table的写操作完成之后再返回table实例
from ctpersistance.
@casatwy 所有的读写操作都放到CTPersistanceAsyncExecutor的队列里操作了,包括创建一个table实例时的create table也放到write里面了,还是会出现偶尔崩溃的情况
from ctpersistance.
一样的错误提示?
from ctpersistance.
是的,都是EXC_BAD_ACCESS
from ctpersistance.
这个问题我又研究了几天,限于知识水平和能力,目前还没啥进展😢
这个库项目里已经用了很久了,老版本用户的本地数据库已经生成了,切换其他数据库封装库的话要适配很多东西,也挺麻烦,中间我也提过PR,还是想能找到问题帮助这个库更好的发展,更完善一些。
目前的进展:
由于崩溃是偶尔崩溃,而且是由于EXC_BAD_ACCESS导致的崩溃,你之前也说过可能是多线程导致的,我现在正在排查对于sqlite3 *database的引用、关闭和置空,还有DatabasePool里面线程退出时对database关闭的处理。目前还没发现问题
@casatwy 能不能给点可能出问题的方向?我再继续排查
from ctpersistance.
现在的情况是我也没什么方向…
from ctpersistance.
@casatwy 有方向了,这个issue应该可以关闭了。之前由于找不到方向,又结合应用的前一个加密版本(SQLCipher4.0.1)没有崩溃问题的情况,然后对应SQLCipher4.1.0版本的发布时间,怀疑是SQLCipher版本的问题,然后就回退了SQLCipher的版本,果然,经过了一周时间多个同事的使用,没有发现崩溃问题。所以,目前可以基本断定是SQLCipher版本更新导致了崩溃的问题,所以,先把这个issue关掉了,后面有时间我再去跟进。
from ctpersistance.
可以的👍
from ctpersistance.
回复神速😄
from ctpersistance.
旷日持久的追踪啊
from ctpersistance.
看来我要针对多线程的table实例持有做计数了
from ctpersistance.
😄还在用这个库,所以问题早晚要解决的。之前以为是SQLCipher的问题。最近又升版本,问题又回来了,跟踪多了慢慢就发现了问题。
from ctpersistance.
有个问题,就是如果不用这个CTPersistanceDatabasePool呢,table自己管理sqlite3 *database呢?
不再针对线程维护database的创建和关闭,一个table实例对应一个sqlite3 *database,然后table销毁的时候关闭数据库、销毁databas实例,好像也可以。如果我的table需要一直用,database一直不关就好了。
而且table一般不会太多,也不太会频繁创建、销毁,所以也不会出现database的频繁创建、打开、关闭、销毁的开销。
from ctpersistance.
老哥神速👍
from ctpersistance.
还得仰仗你的业务场景呀,这次修改应该能够好很多。不知道你这边新版什么时候上线,观察观察数据这一类crash是不是就降下来了
from ctpersistance.
问题可能我这边搞错了,我们这边另外一个同事也在看这个问题。发现多线程环境下一个table对应的database实例会是多个,每个线程对应一个。
CTPersistanceTable类里面queryCommand的get方法走的都是initWithDatabaseName:swiftModuleName:swiftModuleName:,这个方法里面设置self.shouldKeepDatabas = NO
`
- (CTPersistanceQueryCommand *)queryCommand
{
if (_queryCommand == nil && self.isFromMigration == NO) {
NSString *swiftModuleName = nil;
if ([self.child respondsToSelector:@selector(swiftModuleName)]) {
swiftModuleName = [self.child swiftModuleName];
}
_queryCommand = [[CTPersistanceQueryCommand alloc] initWithDatabaseName:[self.child databaseName] swiftModuleName:swiftModuleName];
}
return _queryCommand;
}
`
`
- (instancetype)initWithDatabaseName:(NSString *)databaseName swiftModuleName:(NSString *)swiftModuleName
{
self = [super init];
if (self) {
self.shouldKeepDatabase = NO;
self.databaseName = databaseName;
self.swiftModuleName = swiftModuleName;
}
return self;
}
`
这样每次执行compileSqlString:bindValueList:error:方法时self.database拿到的是从pool中重新获取的线程对应的_database实例,新的线程会拿到新的实例
`
- (CTPersistanceSqlStatement *)compileSqlString:(NSString *)sqlString bindValueList:(NSMutableArray <NSInvocation *> *)bindValueList error:(NSError *__autoreleasing *)error
{
CTPersistanceSqlStatement *statement = [[CTPersistanceSqlStatement alloc] initWithSqlString:sqlString bindValueList:bindValueList database:self.database error:error];
return statement;
}
- (CTPersistanceDataBase *)database
{
if (self.shouldKeepDatabase) {
return _database;
}
_database = [[CTPersistanceDatabasePool sharedInstance] databaseWithName:self.databaseName swiftModuleName:self.swiftModuleName];
return _database;
}
`
所以,多线程共享database的情况可能是不存在的,当时漏掉了这段代码
from ctpersistance.
现在你比我都更了解这个库了~
这改动先放着好了,因为走gcd的话,任务被dispatch到哪个线程是不确定的,这段代码还有存在的价值
from ctpersistance.
😄 我是“久病成医”。嗯嗯,代码是有价值的,应该留着。
其他的还发现几个小问题:
1、重复的关闭动作
didReceiveNSThreadWillExitNotification方法里面 [databaseToClose makeObjectsPerformSelector:@selector(closeDatabase)];执行了关闭操作,紧接着CTPersistanceDataBase对象从databaseList中移除,不再被持有,销毁的时候dealloc方法里面又调用了一次。没发现什么问题,但是应该是不必要的
[keyToDelete enumerateObjectsUsingBlock:^(NSString * _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) { [self.databaseList removeObjectForKey:key]; }];
`
- (void)dealloc
{
[self closeDatabase];
}
`
2、sql语句中字符串变量值的单引号
库里面单引号有些用的是,我查了一下这个是重音符,现在删除操作都是ok的,不知道会不会引起什么其他的问题
- (CTPersistanceSqlStatement *)deleteTable:(NSString *)tableName whereString:(NSString *)whereString bindValueList:(NSMutableArray <NSInvocation *> *)bindValueList error:(NSError *__autoreleasing *)error
{
NSString *sqlString = [NSString stringWithFormat:@"DELETE FROM%@
WHERE %@", tableName, whereString];
return [self compileSqlString:sqlString bindValueList:bindValueList error:error];
}
`
from ctpersistance.
-
这个是冗余代码,我防御性地写一下的
-
`是SQL的一种标准吧,数据库名、表名、字段名这些,在标准的SQL里是用`的
from ctpersistance.
这个是冗余代码,我防御性地写一下的
`是SQL的一种标准吧,数据库名、表名、字段名这些,在标准的SQL里是用`的
好的,我再研究下
from ctpersistance.
老哥,我又来了,数据库偶尔崩溃的问题,这次估计是找到了最终的原因。
现象是这样的,由于使用的时间增加,用户本地数据库的数据量越来越大,几万条的样子,数据库的读写都会变慢,这样的情况下,崩溃变得更频繁了,由于崩溃的频繁,也更容易找出问题的特点了。
现在崩溃的特点是并发读也会崩溃,而且每次崩溃的几个并发读线程里,肯定有一个是在走数据库sqlite3_key的方法:
Sqlite数据库是支持并发读的,所以应该不是读的问题,所以我猜测是不是sqlite3_key是不能跟读同时出现的,我搜了没找到sqlite3_key明确的线程安全相关的数据,这个我目前只能确认到这一步了。
不过我做了反向验证,就是项目中特别容易出现并发读操作且容易引起崩溃的操作,我给改成了加到write锁里面执行,前后并发执行了几十次都没有崩溃,同样的测试平常一两次并发肯定会崩一次,所以这也侧面验证了并发读会崩的问题。
另外这个问题,直接在CTPersistance这个项目里面加测试代码也是能复现的,上面的截图就是我在CTPersistance的项目里加测试代码先插入几万条数据,然后并发读复现的崩溃,老哥,有时间一起看下这种并发读的崩溃问题吧
下面是我的测试代码,放到CTPersistance项目里跑几次基本就能复现崩溃:
CTPersistanceTestConcurrentRead.m.zip
from ctpersistance.
正好下周有空可以一起看看
from ctpersistance.
老哥,啥时候有空一起看下,这个问题不解决,只能考虑换库了,不过换库的成本太高了,问题能解决的话,还是希望继续用这个库的
from ctpersistance.
测试用例直接拖到CTPersistanceTests里面,模拟器跑也能复现
CTPersistanceTestConcurrentRead.m.zip
from ctpersistance.
我跑了几次,没复现出来?
我考虑把队列换成serial队列,这样的话就相当于是“读任务”一个一个执行
你这边的业务场景能够撑得住不?
或者我换成operation queue,最多同时执行10个读任务
这样会不会更好一些?
from ctpersistance.
再多试几次呢,本地多积累点数据呢,我这边跑的话是很容易复现的。
数据库加密的情况下,读写都会变慢,数据量再大一点,就更慢了,并发的读就更容易出现多个读同时执行的情况,这种情况就容易崩溃
from ctpersistance.
真机试下呢,我用模拟器第一次就复现了,但是后面确实没再复现,我用真机(iPhone XS)复现的概率很大,这个可能跟设备的性能也有关系
你再试下这个,我用这个用例,iPhone XS真机崩溃复现的概率会高一些
CTPersistanceTestConcurrentRead.m.zip
from ctpersistance.
好的,我试一下
from ctpersistance.
可以把真机上的crash日志导出贴上来么?
from ctpersistance.
我还在弄日志,发现有日志也都是没有符号化的地址信息,现在应该不需要日志了
from ctpersistance.
老哥,这个问题有解决的方向吗?
from ctpersistance.
你拉取synchronized分支,看看是不是就好了?
from ctpersistance.
测试用例我也已经放进去了
from ctpersistance.
好的,我试下
from ctpersistance.
好了的话跟我说一声,我发个版
from ctpersistance.
好的,在试了,我再多试几次
from ctpersistance.
跑了十几次,没崩,应该是可以了
from ctpersistance.
按照我的理解,改了之后是同一个table实例的操作变成串行了吧,但是多个table之间还是并发的,对吧
目前看着对性能的影响不是很大
from ctpersistance.
不是的,即使单个table也是并发的
这个问题的原因在于statement的创建和使用不是原子性的
然后如果一个statement刚创建,但另一个线程用了这个statement
就导致statement失效了
现在的做法就是让statement的创建和使用放到一个原子操作里面去了
多线程还是正常的
from ctpersistance.
话说这个问题跟踪了2年,终于是解决了啊~
from ctpersistance.
😄是的,还好没放弃
from ctpersistance.
不过我还是有些疑问:
1、代码中的同步锁用的是@synchronized (self),self对应的是table实例,那么所有加了@synchronized (self)锁的操作之间是不能并发的,因为用的是同一把锁“self”,所以只能一个@synchronized (self){}花括号中的代码执行完下一个@synchronized (self){}花括号中的代码才会执行
而@synchronized (self) {}也包住了所有sql执行的代码,所以其实是一个table实例间的sql执行都串行了,如下图:
from ctpersistance.
2、CTPersistanceSqlStatement *statement这个我看了下,是一个局部变量(它持有sqlite3_stmt *statement),没有对象持有它,每个实例都是临时生成的,所以应该是每个线程使用的都是新生成的statement对象,应该不存在原子性问题吧
from ctpersistance.
哦,事实上确实还是串行了,只是乱序了而已,我理解错了。
from ctpersistance.
嗯,开始时是并发的,但是后面都在等锁了
from ctpersistance.
是的,后面就在等锁了.
崩溃的时候,看到的情况是statement这个对象变成野指针了,所以bad access
是因为从sqlcommand出来的statement没有正确被execute或fetch导致的
我当初写的时候想的也是这些都是我创建的临时变量,应该不会有线程安全问题
但看起来事实并不是我想的这样
from ctpersistance.
你说的statement是CTPersistanceSqlStatement还是它持有的sqlite3_stmt *statement
from ctpersistance.
我用老的代码复现了崩溃,这个时候statement是能访问的,崩溃的点是在很底层了
from ctpersistance.
我说的是sqlite3_stmt *statement
在这个地方你po self.statement,就会发现这是个野指针
from ctpersistance.
崩溃的地方打印好像是正常的,提示的是unknown class
from ctpersistance.
按道理来说不应该是个unknown class吧?
from ctpersistance.
这里unknown class应该是因为它是个结构体,不是OC对象,这里正常断点打印也是unknown class
from ctpersistance.
会不会真的是我开始说的sqlite3_key的问题,sqlite3_key不是线程安全的或者是作为一个写操作,需要加写锁之类的,现象确实是每次崩溃都有一个sqlite3_key的调用在
from ctpersistance.
可能是,如果真是这样,那就还是只能靠加锁解决了😂
from ctpersistance.
synchronized分支加锁保证单个table的操作串行确实解决了崩溃的问题,还有一种情况还是会崩
synchronized分支里面,把table改成临时创建的,这个作用就没了,还是会崩
想要不崩溃,咋这么难😿
from ctpersistance.
你在master分支上试一下呢?因为之前我遗漏了两处没有加锁。现在新版本是202了
from ctpersistance.
OK
from ctpersistance.
上面读取改成table临时创建,使用master分支的代码,试了很多次都是好的,还是复现了一次崩溃,table初始化的时候的createTable操作跟其他的数据库操作是一样的,应该也要加锁,这样来看的话,需要加锁的范围应该更大一些,可能还有其他的操作需要加锁
from ctpersistance.
我又思考了下,上面说的可能是有问题的,table每次都是一个新的实例,就不会存在同一个table之前的并发.因为都在read:{}里面,是顺序执行的先createTable,然后是具体的读操作。
这个崩溃是源于不同的table实例之间的问题,也就是说tableA在读的同时,tableB执行createTable,触发了sqlite3_key,也是可能会崩溃的
如果这个崩溃是由于sqlite3_key不能与其他的读写操作并发,那么是不是应该加全局的锁,做到全局不并发。我们现在是针对table加的锁,不同的table之间还是有可能会崩溃的
from ctpersistance.
sqlcipher那边我也提了一个issue,作者给了一个回复,可以参考下
is it safe call sqlite3_key when other threads reading on the same db #381
from ctpersistance.
我大概明白了,我可能要去掉数据库链接池,因为如果池里有链接,那我就会复用这个链接,就会出现他说的情况。
from ctpersistance.
他第1条说“每个线程都有自己的数据库连接,是安全的”,我们这个数据库链接池,满足了这个条件
第2条是“在多个线程上重复调用一个数据库句柄上的sqlite3_key是不安全的。”这个现在的库应该也是满足的,而且如果线程尽量发现针对自己已经有了数据库连接,直接拿来用,是不会调用open和sqlite3_key的,也不会出问题
from ctpersistance.
他第1条说“每个线程都有自己的数据库连接,是安全的”,我们这个数据库连接池,满足了这个条件
第2条是“在多个线程上重复调用一个数据库句柄上的sqlite3_key是不安全的。”这个现在的库应该也是满足的,而且如果线程发现针对自己已经有了数据库连接,直接拿来用,是不会调用open和sqlite3_key的,也不会出问题,就是说我们存储了数据库连接,同一个数据库连接不会调用两次open和sqlite3_key。
from ctpersistance.
我看了一下,我这边数据库连接池是按照线程区分的,按道理确实是不会出现他说的情况。
之前我记错了,我以为复用链接的时候没有区分线程。
现在202先用着吧,等我有空了再好好研究这个问题。
from ctpersistance.
好的,sqlcipher那边我继续跟着,看是不是他们库的缺陷
from ctpersistance.
from ctpersistance.
好了,我推上来了
from ctpersistance.
老哥,项目中碰到两个table之间的崩溃了,一个table(messageTable)正在读,另一个线程另外一个table(conversationTable)打开了一个新的database,就崩了
sqlcipher那边我给他们回复了,他们的回复我没看太懂,他们可能觉得还是CTPersistance库的问题(最新回复),老哥啥时候有时间帮忙看下
from ctpersistance.
我大概看明白了,是因为我在open之后调用了很多次sqlite3_key,我回头看一下这个应该怎么解决一下
from ctpersistance.
Related Issues (20)
- update 操作可能出现问题 HOT 3
- 同样是数据库更新错误 HOT 19
- 更新还是有问题 HOT 1
- 空值对象的改进问题 HOT 1
- CTPersistanceDatabasePool 中的一些问题 HOT 2
- CTPersistanceTable没有Merge数据的API HOT 22
- CTPersistanceDataBase没有提供自己定义数据库路径的接口 HOT 3
- 关于bindValueList HOT 1
- 当Table中没有数据的时候,Upsert一个带有PrimaryKey的Record,失败 HOT 1
- 数据库模糊查询findAllWithWhereCondition:@"name LIKE :name",没有数据 HOT 6
- 已有未加密数据库,增加加密功能出错 HOT 8
- "Include of non-modular header inside framework module" HOT 1
- crash Thread 28: EXC_BAD_ACCESS (code=1, address=0x100000000) HOT 1
- [__NSDictionaryM setObject:forKey:]: object cannot be nil (key: jsonString) crash HOT 3
- 小白问下,表文件创建后能添加新字段吗 HOT 2
- 这个数据库迁移要结合CTMediator这个三方来做吗 HOT 1
- crash:setObjectForKey: object cannot be nil
- 你个博客http://casatwy.com无法打开。求增加字段升级数据库操作文档。 HOT 3
- podfile 文件开启 use_frameworks 后 sqlite3_key 调用系统方法
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from ctpersistance.