基于STSdb和fastJson的磁盘/内存缓存
2013-07-03 10:50 灵感之源 阅读(3982) 评论(7) 编辑 收藏 举报更新
1. 增加了对批量处理的支持,写操作速度提升5倍,读操作提升100倍
2. 增加了对并发的支持
需求
业务系统用的是数据库,数据量大,部分只读或相对稳定业务查询复杂,每次页面加载都要花耗不少时间(不讨论异步),觉得可以做一下高速缓存,譬如用nosql那种key/value快速存取结果
目的
这里不是要做一个大家都适用的磁盘/内存缓存库,这个做法,部分是展示STSdb的用法,部分是提供一个简单易用的解决方案。
磁盘/内存
为什么不用memcached或者AppFabric Cache这样的现成解决方案呢?因为业务要缓存的内存或大或小,小的几KB,大的几MB,如果用户一多,势必对内存有过度的需求。所以选择做一个基于磁盘的。
当然,这个解决方案是支持内存缓存的。构造的时候传递空字符串便可。
STSdb是什么
再来说明一下STSdb是什么:STSdb是C#写的开源嵌入式数据库和虚拟文件系统,支持实时索引,性能是同类产品的几倍到几十倍,访问官方网站。
我之前介绍过:STSdb,最强纯C#开源NoSQL和虚拟文件系统 和 STSdb,最强纯C#开源NoSQL和虚拟文件系统 4.0 RC2 支持C/S架构 ,大家可以先看看。
实现
存取
因为是基于磁盘,所以需要使用到高效的Key/Value存取方案,碰巧我们有STSdb :)
序列化
因为要求简便快速,用的是fastJson。
代码
代码比较简单,花了2个小时写的,很多情况没考虑,譬如磁盘空间不足、过期空间回收等,这些留给大家做家庭作业吧。另外,为了发布方便,STSdb和fastJson的代码都合并到一个项目里。
CahceEngine.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using STSdb4.Database; using fastJSON; using System.IO; namespace Com.SuperCache.Engine { public class CacheEngine { private const string KeyExpiration = "Expiration"; private string dataPath; private static IStorageEngine memoryInstance = null; private static object syncRoot = new object(); private bool isMemory = false; public CacheEngine(string DataPath) { dataPath = DataPath; if (!dataPath.EndsWith(Path.DirectorySeparatorChar.ToString())) dataPath += Path.DirectorySeparatorChar; isMemory = string.IsNullOrEmpty(DataPath); } public void Add<K>(string Category, K Key, object Data) { Add(Category, Key, Data, null); } private IStorageEngine Engine { get { if (isMemory) { lock (syncRoot) { if (memoryInstance == null) memoryInstance = STSdb.FromMemory(); } return memoryInstance; } else return STSdb.FromFile(GetFile(false), GetFile(true)); } } private string GetExpirationTable(string Category) { return KeyExpiration + "_" + Category; } public void Add<K, V>(string Category, IEnumerable<KeyValuePair<K, V>> Items, DateTime? ExpirationDate) { lock (syncRoot) { var engine = Engine; var table = engine.OpenXIndex<K, string>(Category); Items.ForEach(i => { var key = i.Key; var data = i.Value; //will only serialize object other than string var result = typeof(V) == typeof(string) ? data as string : JSON.Instance.ToJSON(data); table[key] = result; table.Flush(); //specify expiration var expiration = engine.OpenXIndex<K, DateTime>(GetExpirationTable(Category)); //default 30 mins to expire from now var expirationDate = ExpirationDate == null || ExpirationDate <= DateTime.Now ? DateTime.Now.AddMinutes(30) : (DateTime)ExpirationDate; expiration[key] = expirationDate; expiration.Flush(); }); engine.Commit(); //only dispose disk-based engine if (!isMemory) engine.Dispose(); } } public void Add<K>(string Category, K Key, object Data, DateTime? ExpirationDate) { Add<K, object>(Category, new List<KeyValuePair<K, object>> { new KeyValuePair<K, object>(Key, Data) }, ExpirationDate); } private string GetFile(bool IsData) { if (!Directory.Exists(dataPath)) Directory.CreateDirectory(dataPath); return dataPath + "SuperCache." + (IsData ? "dat" : "sys"); } public List<KeyValuePair<K, V>> Get<K, V>(string Category, IEnumerable<K> Keys) { var result = new List<KeyValuePair<K, V>>(); lock (syncRoot) { var engine = Engine; var table = engine.OpenXIndex<K, string>(Category); var expiration = engine.OpenXIndex<K, DateTime>(GetExpirationTable(Category)); var isCommitRequired = false; Keys.ForEach(key => { string buffer; V value; if (table.TryGet(key, out buffer)) { //will only deserialize object other than string value = typeof(V) == typeof(string) ? (V)(object)buffer : JSON.Instance.ToObject<V>(buffer); DateTime expirationDate; //get expiration date if (expiration.TryGet(key, out expirationDate)) { //expired if (expirationDate < DateTime.Now) { value = default(V); table.Delete(key); table.Flush(); expiration.Delete(key); expiration.Flush(); isCommitRequired = true; } } } else value = default(V); result.Add(new KeyValuePair<K, V>(key, value)); }); //only need to commit write actions if (isCommitRequired) engine.Commit(); //only dispose disk-based engine if (!isMemory) engine.Dispose(); } return result; } public V Get<K, V>(string Category, K Key) { var buffer = Get<K, V>(Category, new K[] { Key }); var result = buffer.FirstOrDefault(); return result.Value; } } }
新建
构造CacheEngine需要传递缓存保存到哪个文件夹。
基于内存
如果你不喜欢基于磁盘的缓存,可以使用基于内存,构造函数传递空字符串便可。
增加/更新
同一个方法:Add。用户可以指定类型(Category),譬如User,Employee等。键(Key)支持泛型,值(Data)是object。有一个overload是过期日期(ExpirationDate),默认当前时间30分钟后
获取
Get方法需要指定类型(Category)和键(Key)。
例子
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Diagnostics; using Com.SuperCache.Engine; namespace Com.SuperCache.Test { public class Foo { public string Name { get; set; } public int Age { get; set; } public double? Some { get; set; } public DateTime? Birthday { get; set; } } class Program { static void Main(string[] args) { //TestAddGet(); //Thread.Sleep(4000); //TestExpiration(); TestPerformance(); //TestConcurrent(); Console.Read(); } private static void TestConcurrent() { var w = new Stopwatch(); w.Start(); Parallel.For(1, 3, (a) => { var employees = Enumerable.Range((a - 1) * 1000, a * 1000).Select(i => new KeyValuePair<int, string>(i, "Wilson " + i + " Chen")); var engine = new CacheEngine(@"..\..\data"); engine.Add<int, string>("Employee", employees, DateTime.Now.AddMinutes(1)); }); w.Stop(); Console.WriteLine("add:" + w.Elapsed); var engine2 = new CacheEngine(@"..\..\data"); var o = engine2.Get<int, string>("Employee", 1005); Console.WriteLine(o); } private static void TestPerformance() { var engine = new CacheEngine(@"..\..\data"); var w = new Stopwatch(); w.Start(); var employees = Enumerable.Range(0, 1000).Select(i => new KeyValuePair<int, string>(i, "Wilson " + i + " Chen")); engine.Add<int, string>("Employee", employees, DateTime.Now.AddMinutes(1)); w.Stop(); Console.WriteLine("add:" + w.Elapsed); /*w.Restart(); employees.ForEach(key => { var o1 = engine.Get<int, string>("Employee", key.Key); }); w.Stop(); Console.WriteLine("individual get:" + w.Elapsed);*/ w.Restart(); var keys = employees.Select(i => i.Key); var o = engine.Get<int, string>("Employee", keys); w.Stop(); Debug.Assert(o.Count == keys.Count()); Console.WriteLine("get:" + w.Elapsed); } private static void TestExpiration() { var engine = new CacheEngine(@"..\..\data"); var o = engine.Get<string, Foo>("User", "wchen"); Console.WriteLine(o != null ? o.Name : "wchen does not exist or expired"); } private static void TestAddGet() { var engine = new CacheEngine(@"..\..\data"); var f = new Foo { Name = "Wilson Chen", Age = 30, Birthday = DateTime.Now, Some = 123.456 }; engine.Add("User", "wchen", f, DateTime.Now.AddSeconds(5)); var o = engine.Get<string, Foo>("User", "wchen"); Console.WriteLine(o.Name); var o4 = engine.Get<string, Foo>("User", "foo"); Console.WriteLine(o4 != null ? o4.Name : "foo does not exist"); var o3 = engine.Get<string, string>("PlainText", "A"); Console.WriteLine(o3 ?? "A does not exist"); } } }
说明
项目中引用了System.Management是因为STSdb支持内存数据库,需要判断最大物理内存。如果不喜欢,大家可以移除引用,并且去掉STSdb4.Database.STSdb.FromMemory方法便可。
下载
点击这里下载