前言

在一个分布式缓存遍地都是的环境下,还讲本地缓存,感觉有点out了啊!可能大家看到标题,就没有想继续看下去的欲望了吧。但是,本地缓存的重要性也是有的!

本地缓存相比分布式缓存确实是比较out和比较low,这个我也是同意的。但是嘛,总有它存在的意义,存在即合理。

先来看看下面的图,它基本解释了缓存最基本的使用。

关于缓存的考虑是多方面,但是大部分情况下的设计至少应该要有两级才算是比较合适的,一级是关于应用服务器的(本地缓存),一级是关于缓存服务器的。

所以上面的图在应用服务器内还可以进一步细化,从而得到下面的一张图:

这里也就是本文要讲述的重点了。

注:本文涉及到的缓存没有特别说明都是指的数据缓存

常见的本地缓存

在介绍自己瞎折腾的方案之前,先来看一下目前用的比较多,也是比较常见的本地缓存有那些。

在.NET Framework 时代,我们最为熟悉的本地缓存应该就是HttpRuntime.CacheMemoryCache这两个了吧。

一个依赖于System.Web,一个需要手动添加System.Runtime.Caching的引用。

第一个很明显不能在.NET Core 2.0的环境下使用,第二个貌似要在2.1才会有,具体的不是很清楚。

在.NET Core时代,目前可能就是Microsoft.Extensions.Caching.Memory

当然这里是没有说明涉及到其他第三方的组件!现在应该也会有不少。

本文主要是基于SQLite做了一个本地缓存的实现,也就是我瞎折腾搞的。

为什么会考虑SQLite呢?主要是基于下面原因:

  1. In-Memory Database
  2. 并发量不会太高(中小型应该都hold的住)
  3. 小巧,操作简单
  4. 在嵌入式数据库名列前茅

简单设计

为什么说是简单的设计呢,因为本文的实现是比较简单的,还有许多缓存应有的细节并没有考虑进去,但应该也可以满足大多数中小型应用的需求了。

先来建立存储缓存数据的表。

  1. CREATE TABLE "main"."caching" (
  2. "cachekey" text NOT NULL,
  3. "cachevalue" text NOT NULL,
  4. "expiration" integer NOT NULL,
  5. PRIMARY KEY("cachekey")
  6. );

这里只需要简单的三个字段即可。

字段名 描述
cachekey 缓存的键
cachevalue 缓存的值,序列化之后的字符串
expiration 缓存的绝对过期时间

由于SQLite的列并不能直接存储完整的一个对象,需要将这个对象进行序列化之后 再进行存储,由于多了一些额外的操作,相比MemoryCache就消耗了多一点的时间,

比如现在有一个Product类(有id,name两个字段)的实例obj,要存储这个实例,需要先对其进行序列化,转成一个JSON字符串后再进行存储。当然在读取的时候也就需要进行反序列化的操作才可以。

为了方便缓存的接入,统一了一下缓存的入口,便于后面的使用。

  1. /// <summary>
  2. /// Cache entry.
  3. /// </summary>
  4. public class CacheEntry
  5. {
  6. /// <summary>
  7. /// Initializes a new instance of the <see cref="T:SQLiteCachingDemo.Caching.CacheEntry"/> class.
  8. /// </summary>
  9. /// <param name="cacheKey">Cache key.</param>
  10. /// <param name="cacheValue">Cache value.</param>
  11. /// <param name="absoluteExpirationRelativeToNow">Absolute expiration relative to now.</param>
  12. /// <param name="isRemoveExpiratedAfterSetNewCachingItem">If set to <c>true</c> is remove expirated after set new caching item.</param>
  13. public CacheEntry(string cacheKey,
  14. object cacheValue,
  15. TimeSpan absoluteExpirationRelativeToNow,
  16. bool isRemoveExpiratedAfterSetNewCachingItem = true)
  17. {
  18. if (string.IsNullOrWhiteSpace(cacheKey))
  19. {
  20. throw new ArgumentNullException(nameof(cacheKey));
  21. }
  22. if (cacheValue == null)
  23. {
  24. throw new ArgumentNullException(nameof(cacheValue));
  25. }
  26. if (absoluteExpirationRelativeToNow <= TimeSpan.Zero)
  27. {
  28. throw new ArgumentOutOfRangeException(
  29. nameof(AbsoluteExpirationRelativeToNow),
  30. absoluteExpirationRelativeToNow,
  31. "The relative expiration value must be positive.");
  32. }
  33. this.CacheKey = cacheKey;
  34. this.CacheValue = cacheValue;
  35. this.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
  36. this.IsRemoveExpiratedAfterSetNewCachingItem = isRemoveExpiratedAfterSetNewCachingItem;
  37. }
  38. /// <summary>
  39. /// Gets the cache key.
  40. /// </summary>
  41. /// <value>The cache key.</value>
  42. public string CacheKey { get; private set; }
  43. /// <summary>
  44. /// Gets the cache value.
  45. /// </summary>
  46. /// <value>The cache value.</value>
  47. public object CacheValue { get; private set; }
  48. /// <summary>
  49. /// Gets the absolute expiration relative to now.
  50. /// </summary>
  51. /// <value>The absolute expiration relative to now.</value>
  52. public TimeSpan AbsoluteExpirationRelativeToNow { get; private set; }
  53. /// <summary>
  54. /// Gets a value indicating whether this <see cref="T:SQLiteCachingDemo.Caching.CacheEntry"/> is remove
  55. /// expirated after set new caching item.
  56. /// </summary>
  57. /// <value><c>true</c> if is remove expirated after set new caching item; otherwise, <c>false</c>.</value>
  58. public bool IsRemoveExpiratedAfterSetNewCachingItem { get; private set; }
  59. /// <summary>
  60. /// Gets the serialize cache value.
  61. /// </summary>
  62. /// <value>The serialize cache value.</value>
  63. public string SerializeCacheValue
  64. {
  65. get
  66. {
  67. if (this.CacheValue == null)
  68. {
  69. throw new ArgumentNullException(nameof(this.CacheValue));
  70. }
  71. else
  72. {
  73. return JsonConvert.SerializeObject(this.CacheValue);
  74. }
  75. }
  76. }
  77. }

在缓存入口中,需要注意的是:

  • AbsoluteExpirationRelativeToNow , 缓存的过期时间是相对于当前时间(格林威治时间)的绝对过期时间。
  • IsRemoveExpiratedAfterSetNewCachingItem , 这个属性是用于处理是否在插入新缓存时移除掉所有过期的缓存项,这个在默认情况下是开启的,预防有些操作要比较快的响应,所以要可以将这个选项关闭掉,让其他缓存插入操作去触发。
  • SerializeCacheValue , 序列化后的缓存对象,主要是用在插入缓存项中,统一存储方式,也减少要插入时需要进行多一步的有些序列化操作。
  • 缓存入口的属性都是通过构造函数来进行初始化的。

然后是缓存接口的设计,这个都是比较常见的一些做法。

  1. /// <summary>
  2. /// Caching Interface.
  3. /// </summary>
  4. public interface ICaching
  5. {
  6. /// <summary>
  7. /// Sets the async.
  8. /// </summary>
  9. /// <returns>The async.</returns>
  10. /// <param name="cacheEntry">Cache entry.</param>
  11. Task SetAsync(CacheEntry cacheEntry);
  12. /// <summary>
  13. /// Gets the async.
  14. /// </summary>
  15. /// <returns>The async.</returns>
  16. /// <param name="cacheKey">Cache key.</param>
  17. Task<object> GetAsync(string cacheKey);
  18. /// <summary>
  19. /// Removes the async.
  20. /// </summary>
  21. /// <returns>The async.</returns>
  22. /// <param name="cacheKey">Cache key.</param>
  23. Task RemoveAsync(string cacheKey);
  24. /// <summary>
  25. /// Flushs all expiration async.
  26. /// </summary>
  27. /// <returns>The all expiration async.</returns>
  28. Task FlushAllExpirationAsync();
  29. }

由于都是数据库的操作,避免不必要的资源浪费,就把接口都设计成异步的了。这里只有增删查的操作,没有更新的操作。

最后就是如何实现的问题了。实现上借助了Dapper来完成相应的数据库操作,平时是Dapper混搭其他ORM来用的。

想想不弄那么复杂,就只用Dapper来处理就OK了。

  1. /// <summary>
  2. /// SQLite caching.
  3. /// </summary>
  4. public class SQLiteCaching : ICaching
  5. {
  6. /// <summary>
  7. /// The connection string of SQLite database.
  8. /// </summary>
  9. private readonly string connStr = $"Data Source ={Path.Combine(Directory.GetCurrentDirectory(), "localcaching.sqlite")}";
  10. /// <summary>
  11. /// The tick to time stamp.
  12. /// </summary>
  13. private readonly int TickToTimeStamp = 10000000;
  14. /// <summary>
  15. /// Flush all expirated caching items.
  16. /// </summary>
  17. /// <returns></returns>
  18. public async Task FlushAllExpirationAsync()
  19. {
  20. using (var conn = new SqliteConnection(connStr))
  21. {
  22. var sql = "DELETE FROM [caching] WHERE [expiration] < STRFTIME('%s','now')";
  23. await conn.ExecuteAsync(sql);
  24. }
  25. }
  26. /// <summary>
  27. /// Get caching item by cache key.
  28. /// </summary>
  29. /// <returns></returns>
  30. /// <param name="cacheKey">Cache key.</param>
  31. public async Task<object> GetAsync(string cacheKey)
  32. {
  33. using (var conn = new SqliteConnection(connStr))
  34. {
  35. var sql = @"SELECT [cachevalue]
  36. FROM [caching]
  37. WHERE [cachekey] = @cachekey AND [expiration] > STRFTIME('%s','now')";
  38. var res = await conn.ExecuteScalarAsync(sql, new
  39. {
  40. cachekey = cacheKey
  41. });
  42. // deserialize object .
  43. return res == null ? null : JsonConvert.DeserializeObject(res.ToString());
  44. }
  45. }
  46. /// <summary>
  47. /// Remove caching item by cache key.
  48. /// </summary>
  49. /// <returns></returns>
  50. /// <param name="cacheKey">Cache key.</param>
  51. public async Task RemoveAsync(string cacheKey)
  52. {
  53. using (var conn = new SqliteConnection(connStr))
  54. {
  55. var sql = "DELETE FROM [caching] WHERE [cachekey] = @cachekey";
  56. await conn.ExecuteAsync(sql , new
  57. {
  58. cachekey = cacheKey
  59. });
  60. }
  61. }
  62. /// <summary>
  63. /// Set caching item.
  64. /// </summary>
  65. /// <returns></returns>
  66. /// <param name="cacheEntry">Cache entry.</param>
  67. public async Task SetAsync(CacheEntry cacheEntry)
  68. {
  69. using (var conn = new SqliteConnection(connStr))
  70. {
  71. //1. Delete the old caching item at first .
  72. var deleteSql = "DELETE FROM [caching] WHERE [cachekey] = @cachekey";
  73. await conn.ExecuteAsync(deleteSql, new
  74. {
  75. cachekey = cacheEntry.CacheKey
  76. });
  77. //2. Insert a new caching item with specify cache key.
  78. var insertSql = @"INSERT INTO [caching](cachekey,cachevalue,expiration)
  79. VALUES(@cachekey,@cachevalue,@expiration)";
  80. await conn.ExecuteAsync(insertSql, new
  81. {
  82. cachekey = cacheEntry.CacheKey,
  83. cachevalue = cacheEntry.SerializeCacheValue,
  84. expiration = await GetCurrentUnixTimestamp(cacheEntry.AbsoluteExpirationRelativeToNow)
  85. });
  86. }
  87. if(cacheEntry.IsRemoveExpiratedAfterSetNewCachingItem)
  88. {
  89. // remove all expirated caching item when new caching item was set .
  90. await FlushAllExpirationAsync();
  91. }
  92. }
  93. /// <summary>
  94. /// Get the current unix timestamp.
  95. /// </summary>
  96. /// <returns>The current unix timestamp.</returns>
  97. /// <param name="absoluteExpiration">Absolute expiration.</param>
  98. private async Task<long> GetCurrentUnixTimestamp(TimeSpan absoluteExpiration)
  99. {
  100. using (var conn = new SqliteConnection(connStr))
  101. {
  102. var sql = "SELECT STRFTIME('%s','now')";
  103. var res = await conn.ExecuteScalarAsync(sql);
  104. //get current utc timestamp and plus absolute expiration
  105. return long.Parse(res.ToString()) + (absoluteExpiration.Ticks / TickToTimeStamp);
  106. }
  107. }
  108. }

这里需要注意下面几个:

  • SQLite并没有严格意义上的时间类型,所以在这里用了时间戳来处理缓存过期的问题。
  • 使用SQLite内置函数 STRFTIME('%s','now') 来获取时间戳相关的数据,这个函数获取的是格林威治时间,所有的操作都是以这个时间为基准。
  • 在插入一条缓存数据的时候,会先执行一次删除操作,避免主键冲突的问题。
  • 读取的时候就做了一次反序列化操作,简化调用操作。
  • TickToTimeStamp , 这个是过期时间转化成时间戳的转换单位。

最后的话,自然就是如何使用的问题了。

首先是在IServiceCollection中注册一下

  1. service.AddSingleton<ICaching,SQLiteCaching>();

然后在控制器的构造函数中进行注入。

  1. private readonly ICaching _caching;
  2. public HomeController(ICaching caching)
  3. {
  4. this._caching = caching;
  5. }

插入缓存时,需要先实例化一个CacheEntry对象,根据这个对象来进行相应的处理。

  1. var obj = new Product()
  2. {
  3. Id = "123" ,
  4. Name = "Product123"
  5. };
  6. var cacheEntry = new CacheEntry("mykey", obj, TimeSpan.FromSeconds(3600));
  7. await _caching.SetAsync(cacheEntry);

从缓存中读取数据时,建议是用dynamic去接收,因为当时没有考虑泛型的处理。

  1. dynamic product = await _caching.GetAsync("mykey");
  2. var id = product.Id;
  3. var name = product.Name;

从缓存中移除缓存项的两个操作如下所示。

  1. //移除指定键的缓存项
  2. await _caching.RemoveAsync("mykey");
  3. //移除所有过期的缓存项
  4. await _caching.FlushAllExpirationAsync();

总结

经过在Mac book Pro上简单的测试,从几十万数据中并行读取1000条到10000条记录也都可以在零点几ms中完成。

这个在高读写比的系统中应该是比较有优势的。

但是并行的插入就相对要慢不少了,并行的插入一万条记录,直接就数据库死锁了。1000条还勉强能在20000ms搞定!

这个是由SQLite本身所支持的并发性导致的,另外插入缓存数据时都会开一个数据库的连接,这也是比较耗时的,所以这里可以考虑做一下后续的优化。

移除所有过期的缓存项可以在一两百ms内搞定。

当然,还应该在不同的机器上进行更多的模拟测试,这样得到的效果比较真实可信。

SQLite做本地缓存有它自己的优势,也有它的劣势。

优势:

  • 无需网络连接
  • 读取数据快

劣势:

  • 高一点并发的时候就有可能over了
  • 读写都需要进行序列化操作

虽说并发高的时候可以会有问题,但是在进入应用服务器的前已经是经过一层负载均衡的分流了,所以这里理论上对中小型应用影响不会太大。

另外对于缓存的滑动过期时间,文中并没有实现,可以在这个基础上进行补充修改,从而使其能支持滑动过期。

本文示例Demo

LocalDataCachingDemo

使用SQLite做本地数据缓存的思考的更多相关文章

  1. iOS - LocalCache 本地数据缓存

    1.自定义方式本地数据缓存 1.1 自定义缓存 1 沙盒路径下的 Library/Caches 用来存放缓存文件,保存从网络下载的请求数据,后续仍然需要继续使用的文件,例如网络下载的离线数据,图片,视 ...

  2. Android清除本地数据缓存代码案例

    Android清除本地数据缓存代码案例 直接上代码: /*  * 文 件 名:  DataCleanManager.java  * 描    述:  主要功能有清除内/外缓存,清除数据库,清除shar ...

  3. Xamarin android使用Sqlite做本地存储数据库

    android使用Sqlite做本地存储非常常见(打个比方就像是浏览器要做本地存储使用LocalStorage,貌似不是很恰当,大概就是这个意思). SQLite 是一个软件库,实现了自给自足的.无服 ...

  4. 微信小程序开发:学习笔记[9]——本地数据缓存

    微信小程序开发:学习笔记[9]——本地数据缓存 快速开始 说明 本地数据缓存是小程序存储在当前设备上硬盘上的数据,本地数据缓存有非常多的用途,我们可以利用本地数据缓存来存储用户在小程序上产生的操作,在 ...

  5. 【Android】Android清除本地数据缓存代码

    最近做软件的时候,遇到了缓存的问题,在网上看到了这个文章,感觉不错.分享给大家看看 文章出处:http://www.cnblogs.com/rayray/p/3413673.html /* * 文 件 ...

  6. Android清除本地数据缓存代码

    /*  * 文 件 名:  DataCleanManager.java  * 描    述:  主要功能有清除内/外缓存,清除数据库,清除sharedPreference,清除files和清除自定义目 ...

  7. [2]项目创建-使用C#.NET开发基于本地数据缓存的PC客户端

    1.新建项目->已安装->模板->Visual c#->Windows桌面->Windows窗体应用程序,截图如下: 图中1:输入项目名称-“MoneyNotes”,图中 ...

  8. [1]开发准备-使用C#.NET开发基于本地数据缓存的PC客户端

    小记:本人是PHPer,对C#.NET的开发只能说看得懂,也写得了功能略简单的PC客户端程序,下面的是本人开发一款名叫“理财速记”的PC客户端软件的全过程记录,期间包括比较繁琐的C#.NET资料查询等 ...

  9. SQLite做为本地缓存的应用需要注意的地方

    原文:SQLite做为本地缓存的应用需要注意的地方 今天看到了园友陆敏计的一篇文章<<C#数据本地存储方案之SQLite>>, 写到了SQLite的诸多优点,尤其适应于本地数据 ...

随机推荐

  1. Kosaraju算法详解

    Kosaraju算法是干什么的? Kosaraju算法可以计算出一个有向图的强连通分量 什么是强连通分量? 在一个有向图中如果两个结点(结点v与结点w)在同一个环中(等价于v可通过有向路径到达w,w也 ...

  2. Project 3:N级魔方阵

    魔方阵:由n*n个数字所组成的n阶方阵,具有各对角线,各横列与纵行的数字和都相等的性质,称为魔方阵.而这个相等的和称为魔术数字.若填入的数字是从1到n*n,称此种魔方阵为n阶正规魔方阵. 目标:输入一 ...

  3. JS解析JSON 注意事项总结

    0.必须先解析看看,不然看了白看   地址: http://www.bejson.com/ 1.返回的节点内是不是一个json. 如  {id:1,names:"[{name:A},{nam ...

  4. c# HttpWebRequest 模拟HTTP post 传递JSON参数

    //HTTP post   JSON 参数        private string HttpPost(string Url, Object ticket)        {            ...

  5. 四,ESP8266 TCP服务器

    我要赶时间赶紧写完所有的内容....朋友的东西答应的还没做完呢!!!!!!!没想到又来了新的事情,,....... 配置模块作为TCP服务器然后呢咱们连接服务器发指令控制继电器吸合和断开 控制的指令呢 ...

  6. ★RFC标准库_目录链接

    RFC(Request For Comments)是一个国际标准化的数据库,记录了从计算机到互联网的海量标准协议.它是一个免费公开的IT标准文件分享平台,其内容也在不断增长,与时俱进.它与ISO等组织 ...

  7. 201521123088《Java程序设计》第11周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 2. 书面作业 本次PTA作业题集多线程 1. 互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) ...

  8. 201521123009《Java程序设计》第14周学习总结

    1. 本周学习总结 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自己的学号.姓名) 在自己建立的数据库上执行常见SQL语句(截图) - ...

  9. 201521044091 《Java程序设计》第12周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. Answer: 2. 书面作业 将Student对象(属性:int id, String name,int a ...

  10. Java、javax、org、sun、Java.util等常用包的区别、详解、实例

    Java.javax.org.sun包都是jdk提供的类包,且都是在rt.jar中.rt.jar是JAVA基础类库(java核心框架中很重要的包),包含lang在内的大部分功能,而且rt.jar默认就 ...