美文网首页
2021-06-25

2021-06-25

作者: youn_ger | 来源:发表于2021-06-25 14:02 被阅读0次

    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
    

    相关文章

      网友评论

          本文标题:2021-06-25

          本文链接:https://www.haomeiwen.com/subject/tiqoyltx.html