2021-06-25
title: YYCache多线程访问导致数据库locked
date: 2021-6-25 14:00:00
id: yycache-dblocked-cn
tags: ['YYCache', '多线程', '数据库']
categories: app
author: younger
简介
YYCache在多线程访问下的异常
YYCache与数据库
1.YYCache虽然年久失修,但是里面的很多设计思想仍然可以供我们参考;
2.不知道大家有没有遇见过下面的情况,那么此问题是什么问题引起的? 又是什么问题导致的?
-[YYKVStorage _dbExecute:] line:182 sqlite exec error (5): database is locked
unable to close due to unfinalized statements or unfinished backups
3.开始之前我们思考一个问题YYCache是线程安全的吗?
https://github.com/ibireme/YYCache 里面明确说了兼容性: API 基本和 NSCache 保持一致, 所有方法都是线程安全的。
大佬写的轮子线程安全问题肯定考虑进去了嘛,咱们去看源码也可以看到锁相关的东西就是来保证线程安全的(安不安全需要看怎么使用,后面我将演示如何不安全即大家常用的操作);
1.多线程操作YYCache
Person对象实现copying协议并且有age和name属性,这里就不贴出源码了
- (void)yyCacheMethod_1 {
YYCache *cache = [[YYCache alloc] initWithName:@"com.yycache.demo"];
self.cache = cache;
for (NSInteger index = 0; index < 20; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
- (void)yyCacheMethod_2 {
for (NSInteger index = 0; index < 20; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
YYCache *cache = [[YYCache alloc] initWithName:@"com.yycache.demo"];
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
- (void)yyCacheMethod_3 {
for (NSInteger index = 0; index < 20; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
YYCache *cache = [[YYCache alloc] initWithName:[NSString stringWithFormat:@"com.yycache.demo-%@", @(index)]];
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
-
代码1和代码2主要区别在于YYCache对象是否被创建多次
-
代码2和代码3的主要区别在于YYCache每次创建时name是否相同
-
遇行结果如下
代码1只创建了一次YYCache,后面的异步线程操作都是基于同一个YYCache对象来操作缓存;
RUN>>运行结果没有警告和异常✅
代码2每次创建Person对象时也创建了一个临时的YYCache,所以每次Person对象存入缓存时使用的都是临时的YYCache;
RUN>>运行结果控制台出现了警告(下面贴出部分警告)❌
YYCacheDemo[34498:4602634] -[YYKVStorage _dbExecute:] line:182 sqlite exec error (5): database is locked
[logging] invalidated open fd: 12 (0x11)
YYKVStorage init error: fail to open sqlite db.
代码3在每次创建YYCache传入的name是不同的,
**RUN>>**运行结果正常✅
看到此log你还认为YYCache是线程安全的吗? 安不安全得看我们的代码是如何写的,而且代码2是我们经常使用到的方式,因为我们不能像代码1那样整个APP搞一个YYCache对象,所有缓存操作都是基于同一个YYCache实例;
但是我们可以像代码3那样使用不同的name来做到线程安全访问🤔,是的没毛病,那你就没有代码2方式的需求吗🤔
- YYKVStorage 是什么时候创建的
在YYDiskCache可以看到关键性的几行代码,大概逻辑:看一下传进来的路径是否有对应缓存YYKVStorage,如果有就不创建新的实例对象,直接返回缓存的即可,如果没有则新建并缓存起来;所以YYCache创建时如果传入相同的name那么返回的就是同一个YYKVStorage,最终操作缓存肯定就没有问题了;
// static NSMapTable *_globalInstances; // 静态变量
// 1.如何路径传入一直则返回之前已经创建好的
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;
// 2.创建YYKVStorage
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
// 3.存储YYKVStorage
_YYDiskCacheSetGlobal(self);
2.问题是如何产生的(基于代码2)
我们可以看到log几个关键的信息[YYKVStorage _dbExecute:]
invalidated open fd
YYKVStorage init error: fail to open sqlite db.
那么大致可以猜测是由于数据库打开失败导致的;
数据库的open和close是需要成对出现的,尤其在服务器开发中一旦open db那么必须要保证db及时close,不管是否发生异常;
YYKVStorage 是什么时候打开数据库? 什么时候关闭数据库?(去源码一探究竟)
1.YYKVStorage.m导入的是 sqlite3.h(使用的是sqlite数据库)
2.可以看到- (BOOL)_dbOpen里面调用了sqlite3_open
3.- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type 时调用了_dbOpen方法
也就是初始化的时候就会open db
4.然后我们在看一下什么时候close db即什么时候调用- (BOOL)_dbClose(里面会调用sqlite3_close)
- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type
- (void)dealloc
- (BOOL)removeAllItems
上面三个方法都有调用_dbClose方法
接下来重点分析initWithPath:type:
和dealloc
关闭数据库的情况,removeAllItems
我们目前还没有调用,因此不分析;
3.继续试验
- 既然我们初始化YYCache传入的name是相同,所以访问的应该是同一个YYKVStorage,这样操作缓存应该没有问题
再写个demo测试一下
RUN>>运行结果正常✅(是不是异步操作导致哪里出了问题呢?)
NSString *key = @"com.yycache.demo.test3";
YYCache *cache1 = [[YYCache alloc] initWithName:key];
Person *person1 = [[Person alloc] init];
person1.age = 18;
person1.name = @"zhangsan";
[cache1 setObject:person1 forKey:@"key-person1"];
YYCache *cache2 = [[YYCache alloc] initWithName:key];
Person *person2 = [[Person alloc] init];
person2.age = 18;
person2.name = @"zhangsan";
[cache2 setObject:person2 forKey:@"key-person2"];
继续改造Demo为异步访问,同时在YYDiskCache的initWithPath:inlineThreshold:
初始化YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
下面加上NSLog(@"🍎 key: %@", kv);
- (void)yyCacheMethod_4 {
NSString *key = @"com.yycache.demo.test3";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
YYCache *cache1 = [[YYCache alloc] initWithName:key];
Person *person1 = [[Person alloc] init];
person1.age = 18;
person1.name = @"zhangsan";
[cache1 setObject:person1 forKey:@"key-person1"];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
YYCache *cache2 = [[YYCache alloc] initWithName:key];
Person *person2 = [[Person alloc] init];
person2.age = 18;
person2.name = @"zhangsan";
[cache2 setObject:person2 forKey:@"key-person2"];
});
}
打印结果如下:
YYCacheDemo[35646:4681865] 🍎 key: <YYKVStorage: 0x600002d3c7e0>
YYCacheDemo[35646:4681866] 🍎 key: <YYKVStorage: 0x600002d2c2a0>
-[YYKVStorage _dbSaveWithKey:value:fileName:extendedData:] line:243 sqlite insert error (5): database is locked
YYCacheDemo[35646:4681865] unable to close due to unfinalized statements or unfinished backups
YYCacheDemo[35646:4681866] unable to close due to unfinalized statements or unfinished backups
现在大概能猜到表层原因什么导致的了,YYCache初始化传入的name虽然相同但是由于异步导致YYKVStorage返回的不是同一个,那么问题来了
- 如何在YYDiskCache层解决掉此问题
- 深层原因又是什么导致的即:YYKVStorage里面的SQLite为什么会出现此问题
4.YYDiskCache层处理
上面我们发现问题的所在了就是因为多线程访问导致YYKVStorage创建的不是同一个,这里先解决返回不一致的问题,深层原因待下面继续深挖;
解决方案肯定是加锁,保证多线程情况下返回的是同一个YYKVStorage对象即可,那么问题来了锁加载哪里?
首先明确一点,产生问题的原因是YYKVStorage返回不一致导致,同时创建链条: 使用者创建YYCache->创建YYDiskCache->创建YYKVStorage
方案一:在我们使用YYCache的地方由使用者来加锁
问题:我们每次创建的YYCache都是不同的,貌似无法加锁,那问题就抛给了YYDiskCache,这也是我们想看到的结果
方案二:在YYCache内部加锁
上面分析代码得知需要保证name(也就是path)相同时YYKVStorage也要返回同一个实例;
YYDiskCache对YYKVStorage进行初始化,并持有YYKVStorage,我们要保证返回的YYDiskCache相同就能保证YYKVStorage返回的也是同一个,代码参考如下:
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;
锁就要在YYDiskCache初始化时添加,那么问题又来了,YYDiskCache在多次alloc时怎么加锁?也就是怎么保证这些YYDiskCache在alloc时按顺序进行? 如果是单个实例对象内部还好解决,这里要解决多个实例对象直接的同步访问问题,那就给类加个锁
尝试加锁... ... (看了一下代码无从下手,这里加锁不合适,还得往上层走)
尝试加锁... ... 最终修改效果如下:
// YYCache.m
- (instancetype)initWithPath:(NSString *)path {
if (path.length == 0) return nil;
@synchronized([self class]) {
YYDiskCache *diskCache = [[YYDiskCache alloc] initWithPath:path];
NSLog(@"🍎 diskCache: %@", diskCache);
if (!diskCache) return nil;
NSString *name = [path lastPathComponent];
YYMemoryCache *memoryCache = [YYMemoryCache new];
memoryCache.name = name;
self = [super init];
_name = name;
_diskCache = diskCache;
_memoryCache = memoryCache;
}
return self;
}
1.这里就不考虑锁的性能和加锁代码是否严谨的问题了,先把问题解决
2.拿yyCacheMethod_4测试一下可以看到diskCache返回的是同一个对象,而且也没有报警告
YYCacheDemo[36375:4716483] 🍎 diskCache: <YYDiskCache: 0x600000f15b80>
🍎 diskCache: <YYDiskCache: 0x600000f15b80>
YYCacheDemo[36375:4716482] 🍎 diskCache: <YYDiskCache: 0x600000f15b80>
3.再拿yyCacheMethod_2测试一下,彻底没有问题了
4.毕竟是开源框架咱们也没法改代码,是不是可以自己继承YYCache,然后在初始化时候做点文章,这里只提供思路就不给代码了
5.或者哪位大佬通知一下作者改一下源码
5.YYKVStorage与SQLite产生此问题根本原因
-
上层问题解决了,那么产生问题的根本原因还没有查清(此时需要把所有代码还原同时到YYKVStorage进行排查)
-
那么先大致猜测一下问题产生的原因:
- 应该是sqlite层产生的原因
- 难道是在多线程下path相同导致sqlite打开的是同一个数据库?
-
实验A继续.. ...
YYDiskCache.m里面的_YYDiskCacheSetGlobal(**self**);
注释掉,同时用下面代码进行测试
- (void)yyCacheMethod_5 {
NSString *key = @"com.yycache.demo.test5";
YYCache *cache1 = [[YYCache alloc] initWithName:key];
Person *person1 = [[Person alloc] init];
person1.age = 18;
person1.name = @"zhangsan";
[cache1 setObject:person1 forKey:@"key-person1"];
YYCache *cache2 = [[YYCache alloc] initWithName:key];
Person *person2 = [[Person alloc] init];
person2.age = 18;
person2.name = @"zhangsan";
[cache2 setObject:person2 forKey:@"key-person2"];
}
没有看到-[YYKVStorage _dbExecute:]警告,但是出现了别的警告unable to close.. ... (小问题自己可以Google看一下)
YYCacheDemo[36952:4732466] 🍎 key: <YYKVStorage: 0x600003e1f300>
YYCacheDemo[36952:4732466] 🍎 key: <YYKVStorage: 0x600003e088a0>
YYCacheDemo[36952:4732466] 🍎 _dbClose 0x7f9abbd04340
YYCacheDemo[36952:4732466] unable to close due to unfinalized statements or unfinished backups
YYCacheDemo[36952:4732466] 🍎 _dbClose 0x7f9abbc09110
YYCacheDemo[36952:4732466] unable to close due to unfinalized statements or unfinished backups
- 实验B继续.. ...
- (void)yyCacheMethod_6 {
for (NSInteger index = 0; index < 4; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
YYCache *cache = [[YYCache alloc] initWithName:@"com.yycache.demo"];
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
多运行几次可能会遇到下面的log
YYCacheDemo[37714:4758713] -[YYKVStorage _dbExecute:] line:183 sqlite exec error (5): database is locked
-
问题是复现了,那是什么问题产生的呢?
-
基于目前我对数据库的理解也只能大胆的猜测一下
- 开头我们也说了对数据库open->读/写数据完后要及时close
- sqlite在path一致肯定打开的是同一个数据库,sqlite在当前db open下是不允许在此open的,所以就报了database is locked警告
-
不光是此警告,也可能会出现别的警告
-
目前还有疑问待查正
- 难道sqlite的锁目前使用的是表锁吗? 目前还没有查正
- sqlite是否支持行锁,也就是多个线程同时读写把锁控制在行界别?因为MySQL是支持行锁的,MySQL如果多线程访问时是如何操作的?
- sqlite如果支持行锁,那么ACID问题也会出现,又如何配置?
后续待更新:
1.MySQL简单介绍
2.服务器与数据库
3.Android与SQLite
4.PINCache为什么没有此问题
所有代码贴于此处,不在上传git
// ViewController.m
@interface ViewController ()
@property(nonatomic, strong) YYCache *cache;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self yyCacheMethod_6];
// [self yyCacheMethod_2];
}
- (void)yyCacheMethod_6 {
for (NSInteger index = 0; index < 4; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
YYCache *cache = [[YYCache alloc] initWithName:@"com.yycache.demo"];
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
- (void)yyCacheMethod_5 {
NSString *key = @"com.yycache.demo.test5";
YYCache *cache1 = [[YYCache alloc] initWithName:key];
Person *person1 = [[Person alloc] init];
person1.age = 18;
person1.name = @"zhangsan";
[cache1 setObject:person1 forKey:@"key-person1"];
YYCache *cache2 = [[YYCache alloc] initWithName:key];
Person *person2 = [[Person alloc] init];
person2.age = 18;
person2.name = @"zhangsan";
[cache2 setObject:person2 forKey:@"key-person2"];
}
- (void)yyCacheMethod_4 {
NSString *key = @"com.yycache.demo.test3";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
YYCache *cache1 = [[YYCache alloc] initWithName:key];
Person *person1 = [[Person alloc] init];
person1.age = 18;
person1.name = @"zhangsan";
[cache1 setObject:person1 forKey:@"key-person1"];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
YYCache *cache2 = [[YYCache alloc] initWithName:key];
Person *person2 = [[Person alloc] init];
person2.age = 18;
person2.name = @"zhangsan";
[cache2 setObject:person2 forKey:@"key-person2"];
});
}
- (void)yyCacheMethod_3 {
for (NSInteger index = 0; index < 20; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
YYCache *cache = [[YYCache alloc] initWithName:[NSString stringWithFormat:@"com.yycache.demo-%@", @(index)]];
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
- (void)yyCacheMethod_2 {
for (NSInteger index = 0; index < 20; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
YYCache *cache = [[YYCache alloc] initWithName:@"com.yycache.demo"];
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
- (void)yyCacheMethod_1 {
YYCache *cache = [[YYCache alloc] initWithName:@"com.yycache.demo"];
self.cache = cache;
for (NSInteger index = 0; index < 20; index++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", @(index));
Person *person = [[Person alloc] init];
person.age = index;
person.name = @"zhangsan";
[cache setObject:person forKey:[NSString stringWithFormat:@"key-%@", @(index)]];
});
}
}
- (void)printCache {
for (NSInteger index = 0; index < 20; index++) {
Person *person = (Person *)[self.cache objectForKey:[NSString stringWithFormat:@"key-%@", @(index)]];
NSLog(@"name:%@ age:%@", person.name, @(person.age));
}
}
// Person.h
@interface Person : NSObject<NSCopying>
@property(nonatomic, assign) NSInteger age;
@property(nonatomic, copy) NSString *name;
@end
// Person.m
@implementation Person
- (id)copyWithZone:(NSZone *)zone {
Person *person = [[Person allocWithZone:zone] init];
person.age = self.age;
person.name = self.name;
return person;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:@(self.age) forKey:@"age"];
[aCoder encodeObject:self.name forKey:@"name"];
}
@end