Flutter中利用MapCache加sqflite实现一个伪LRU三级缓存

在做flutter应用的时候,遇到了一个问题,纯粹属于自己给自己加戏,问题是什么呢?我的app首页是一个列表,目前每次进应用,都是通过网络拿到新的列表,所以,如果没有网络了,就看到了一个菊花,这样的用户体验可能并不怎么好吧,因此,这块的化,想给自己挖一个坑,让自己填一下,本来以为是一个非常简单的问题,因为如果是在Android平台上,用DiskLruCache,很容易就实现了这个需求啦。然而不信的是,经过我的调研,flutter仓库中的库不太符合要求。

首先,我列一下自己的需求

1、网络请求,我使用的是dio框架,在其上面稍微封装了一下,我的想法是需要在onSuccess回调中把get请求缓存下来,就像下面这样:

缓存

2、然后,在需要的地方,我需要判断缓存是否可用,如果可用,我就直接返回了,不发起网络请求,或者说,返回,并且发起网络请求,这依赖于业务需求,先不说这么多,大概方式是:

load缓存

其中红框中的就是我通过key去缓存中查。

3、假如说,我们把接口定义成这样的,那么背后的实现,我们准备如何去做,首先,我是这么考虑的,写缓存,要先写到内存缓存,在写到磁盘缓存,在写的过程中,要使用新的替换旧的,磁盘缓存,和内存缓存都也要有大小的显示,所谓的lru就体现在这里了。

4、好,说来说去,只要有lru_cache就够了,但是,flutter官方仓库中似乎是没有的。自己写一个,似乎代价太大。那么简单模拟实现有没有,我想到了一个思路。

5、MapCache作为内存缓存,sqflite作为磁盘缓存,那么好,LRU怎么实现呢?我的思路是给value加上一个时间戳,当,数据操作一定范围是,将时间戳交旧的删掉,然后重新load内存缓存就ok啦,你一定看出来了,这个太暴力了。

具体的实现代码

1、CacheManger作为cache管理工具,我把它做成了单例,初始化的时候,把磁盘缓存加到了内存中。

import 'package:app/model/cache_object.dart';
import 'package:quiver/cache.dart';

class CacheManger {
  static final CacheManger _singleton = CacheManger._internal();

  CacheDataProvider _cacheDataProvider;

  MapCache<String, String> _cacheMap = MapCache();

  bool _avaiable = false;

  factory CacheManger() {
    return _singleton;
  }

  CacheManger._internal() {
    _cacheDataProvider = CacheDataProvider();
    _initMemoryCache();
  }

  Future<String> get(String key) async {
    if (!_avaiable) {
      await _initMemoryCache();
    }
    return _cacheMap.get(key);
  }

  Future set(String key, String value) async {
    _cacheMap.set(key, value); //写到内存
    //写到磁盘
    return _cacheDataProvider.set(CacheObject(key: key, value: value));
  }

  ///哈哈,假装在lru,偷懒实现
  Future lru() async {
    await _cacheDataProvider.lru();
    _initMemoryCache();
  }

  ///整个清理
  Future clear() async {
    await _cacheDataProvider.clear();
//    _initMemoryCache();
  }

  ///将磁盘缓存load到内存中来
  ///
  Future _initMemoryCache() async {
    List<CacheObject> cacheObjects = await _cacheDataProvider.getAll();
    for (var value in cacheObjects) {
      _cacheMap.set(value.key, value.value);
    }
    _avaiable = true;
  }
}

2、CacheDataProvider作为磁盘缓存操作的具体实现类,主要是一些数据库的操作,以及偷懒的LRU实现:

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

///缓存数据库名字
const String dbName = "data.db";

///缓存表名字
final String tableCache = "table_cache";

///字段
final String columnId = "id";
final String columnKey = "key";
final String columnValue = "value";
final String columnTime = "time";

class CacheObject {
  int id;
  String key;
  String value;
  int time;

  CacheObject({this.id, this.key, this.value, this.time});

  CacheObject.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    key = json['key'];
    value = json['value'] ?? "";
    time = json['time'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['key'] = this.key;
    data['value'] = this.value;
    data['time'] = this.time;
    return data;
  }
}

class CacheDataProvider {
  Database _db;

  ///操作db之前必须保证db是打开的
  Future _open({String name = dbName}) async {
    if (_db == null || !_db.isOpen) {
      var databasesPath = await getDatabasesPath();
      String path = join(databasesPath, name);
      _db = await openDatabase(path, version: 1,
          onCreate: (Database db, int version) async {
        await db.execute('''
create table $tableCache ( 
  $columnId integer primary key autoincrement, 
  $columnKey text not null,
  $columnValue text DEFAULT '{}',
  $columnTime integer not null,
  UNIQUE($columnKey)
  )
''');
      });
    }
  }

  ///设置待缓存对象,如果key重复,会直接替换
  Future<CacheObject> set(CacheObject cacheObject) async {
    await _open();
    List<Map> maps = await _db.query(tableCache,
        columns: [columnKey, columnValue],
        where: "$columnKey = ?",
        whereArgs: [cacheObject.key]);
    if (maps.length > 0) {
      int count = await _db.rawUpdate(
          'UPDATE $tableCache SET $columnValue = ?, $columnTime = ? WHERE $columnKey = ?',
          [
            cacheObject.value,
            new DateTime.now().millisecondsSinceEpoch ~/ 1000,
            cacheObject.key
          ]);
      print("updated: $count");
    } else {
      cacheObject.id = await _db.execute('''
    INSERT  INTO $tableCache($columnKey,$columnValue,$columnTime)
VALUES('${cacheObject.key}','${cacheObject.value}',strftime('%s','now'));
    ''');
    }
    return cacheObject;
  }

  ///取到缓存中的对象
  Future<CacheObject> get(String key) async {
    await _open();
    List<Map> maps = await _db.query(tableCache,
        columns: [columnKey, columnValue],
        where: "$columnKey = ?",
        whereArgs: [key]);
    if (maps.length > 0) {
      return new CacheObject.fromJson(maps.first);
    }
    return null;
  }

  ///取到缓存中的对象
  Future<List<CacheObject>> getAll() async {
    await _open();
    List<Map> maps = await _db.query(tableCache);
    if (maps.length > 0) {
      return maps.map((json) => CacheObject.fromJson(json)).toList();
    }
    return List();
  }

  ///简单的替换一下lru策略
  Future lru() async {
    await _open();
    List<Map> maps = await _db.query(tableCache);
    if (maps.length > 100) {
      var time = CacheObject.fromJson(maps[100]).time;
      return _db
          .delete(tableCache, where: "$columnTime <= ?", whereArgs: [time]);
    }
  }

  ///整个清理
  Future<int> clear() async {
    await _open();
    return await _db.delete(tableCache);
  }

  Future close() async => _db.close();
}

3、可以看出,非常简单,需求就这么实现了,跑起来没有任何问题,然而如果要考虑的更加全面的化,还是有不少问题的。

还存在的问题

1、LRU策略现在只是简单的做成了缓存100个数据,可以改为动态配置。

2、过期策略似乎还可以优化,比如让数据记录自己有效时间,这样一来,可以更加智能的清理数据,清理过期的,而不是简单除暴的按生成时间去移除。

蓦然回首

当然,我在实现的时候,也了解到有人做了disk_lru_cache了,不过我还是没有使用这个,如果要替换也是相当简单的一件事,不过因为现在这个库测试覆盖不全,评分不是太高,所以暂且还是使用自己的实现。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT可乐

mybatis 详解(八)------ 懒加载

  本章我们讲如何通过懒加载来提高mybatis的查询效率。   本章所有代码:http://pan.baidu.com/s/1o8p2Drs 密码:trd6 ...

4019
来自专栏微信公众号:Java团长

彻底解决MySQL中文乱码

mysql是我们项目中非常常用的数据型数据库。但是因为我们需要在数据库保存中文字符,所以经常遇到数据库乱码情况。下面就来介绍一下如何彻底解决数据库中文乱码情况。

2792
来自专栏我的小碗汤

听说你还没掌握Normalizer的使用方法?

在 Elasticsearch 中处理字符串类型的数据时,如果我们想把整个字符串作为一个完整的 term 存储,我们通常会将其类型 type 设定为 keywo...

1654
来自专栏小樱的经验随笔

BugkuCTF SQL注入1

1814
来自专栏web编程技术分享

【Java框架型项目从入门到装逼】第九节 - 数据库建表和CRUD操作

4545
来自专栏hbbliyong

ShellExecute 启动外部程序 参数详细介绍

ShellExecute的功能是运行一个外部程序(或者是打开一个已注册的文件、打开一个目录、打印一个文件等等),并对外部程序有一定的控制。 目录 1基本简介 2...

53310
来自专栏V站

SQL注入基础-基于Sqli-lab平台实战

有关SQL注入的各种定义阐述已经很多,大家可自行使用搜索引擎搜索即可,小东不再赘述。

2815
来自专栏Java技术分享

高并发分布式系统中生成全局唯一Id汇总

数据在分片时,典型的是分库分表,就有一个全局ID生成的问题。 单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:    1 不能有单...

3185
来自专栏木头编程 - moTzxx

PHP 学习筆記[1] —— ThinkPHP 公共函数整理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011415782/article/de...

1384
来自专栏蓝天

一个简单的支持MySQL和SQLite3的DB接口

simple_db.zip 相关联代码:https://github.com/eyjian/mooon/tree/master/common_library/...

1132

扫码关注云+社区

领取腾讯云代金券