2021-06-25

2021-06-25  本文已影响0人  youn_ger

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)]];
        });
    }
}

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方式的需求吗🤔

在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.继续试验

再写个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返回的不是同一个,那么问题来了

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产生此问题根本原因

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

后续待更新:

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
上一篇下一篇

猜你喜欢

热点阅读