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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容