缓存子系统如何设计(Cachable tag, Memcache/redis support, xml config support, LRU/LFU/本地缓存命中率)
大家对这段代码肯定很熟悉吧:
public List<UserInfo> SearchUsers(string userName)
{
string cacheKey=string.Format("SearchUsers_{0}", userName);
List<UserInfo> users = cache.Find(cacheKey) as List<UserInfo>;
if (users == null)
{
users = repository.GetUsersByUserName(userName);
cache.Set(cacheKey, users);
}
return users;
} class HttpRuntimeCache
{
public object Find(string key)
{
return HttpRuntime.Cache[key];
}
public void Set(string key, object value)
{
HttpRuntime.Cache[key] = value;
}
}
导致了如下这些问题:
- 业务逻辑函数中引入了很多无关的缓存代码,导致DDD模型不够纯
- 更换缓存Provider不方便
- 加入缓存冗余机制不方便
- 没办法同时使用多个缓存系统
- 缓存大对象出现异常,比如Memcache有1M的value限制
有诸多问题,因此我们需要引入缓存子系统来解决上述问题,带来的好处:
- DDD模型更加纯
- 具体的Cache实现机制可以很灵活,比如HttpRuntimeCache, Memcache, Redis可以同时使用
- 加入了Cache冗余机制,不会由于某一台Memcache或者Redis down机导致系统速度很慢,实际上,系统还是会保持飞快(除非backup也down了的情况)
- 开发人员更加致力于核心业务,不会分散注意力
- 缓存位置透明化,都会在xml配置文件中进行配置
解决方案,要用到这2篇文章的技术:C# 代理应用 - Cachable 和 聊聊Memcached的应用。
主要的思路分2个:
模型端:通过代理来嵌入AOP方法,来判断是否需要缓存,有缓存value则直接返回value;缓存value的写入是通过AOP的后置方法写入的,因此不需要在业务函数中写代码,当然也支持代码调用。
Cache核心对象:这个对象要解决一致性hash算法、cache value大对象分解功能、冗余机制
代理嵌入AOP的方法,已经在这篇文章中说明了 C# 代理应用 - Cachable,有兴趣的看看,这里就不说了,我们来主要看看CacheCoordinator对象的实现
结构图如下:

先来看看UML图:

CacheCore代码(算法核心):
public class CacheCore
{
private ICacheCoordinator cacheProvider = null;
public CacheCore(ICacheCoordinator cacheProvider)
{
this.cacheProvider = cacheProvider;
} public void Set(string location, string key, object value)
{
AssureSerializable(value);
string xml = Serializer2XMLConvert(value);
CacheParsedObject parsedObj = new CacheParsedObject(); string classType = string.Format("{0}", value.GetType().FullName);
if (xml.Length > CacheConfig.CacheConfiguration.MaxCacheEntitySize)
{
/*
key:1@3@ConcreteType
key_1:subvalue1
key_2:subvalue2
key_3:subvalue3
*/
//拆分成更小的单元
int splitCount = xml.Length / CacheConfig.CacheConfiguration.MaxCacheEntitySize;
if (CacheConfig.CacheConfiguration.MaxCacheEntitySize * splitCount < xml.Length)
splitCount++;
parsedObj.MainObject = new KeyValuePair<string, string>(key, string.Format("1@{0}@{1}", splitCount, classType));
for (int i = ; i < splitCount;i++ )
{
if (i == splitCount - ) //最后一段,直接截取到最后,不用给出长度
parsedObj.SplittedElements.Add(xml.Substring(i * CacheConfig.CacheConfiguration.MaxCacheEntitySize));
else //其他,要给出长度
parsedObj.SplittedElements.Add(xml.Substring(i * CacheConfig.CacheConfiguration.MaxCacheEntitySize, CacheConfig.CacheConfiguration.MaxCacheEntitySize));
}
}
else
{
/*
key:1@1@ConcreteType
key_1:value
*/
parsedObj.MainObject = new KeyValuePair<string, string>(key, string.Format("1@1@{0}", classType));
parsedObj.SplittedElements.Add(xml);
} //针对CacheParsedObject进行逐项保存
this.cacheProvider.Put(parsedObj.MainObject.Key, parsedObj.MainObject.Value);
int curIndex = ;
foreach(string xmlValue in parsedObj.SplittedElements)
{
curIndex++;
string tkey=string.Format("{0}_{1}", parsedObj.MainObject.Key, curIndex);
this.cacheProvider.Put(tkey, xmlValue);
}
} public object Get(string location, string key)
{
string mainObjKeySetting = (string)cacheProvider.Get(key);
if (mainObjKeySetting == null || mainObjKeySetting.Length == )
return null; string classType;
CacheParsedObject parsedObj;
GetParsedObject(key, mainObjKeySetting, out classType, out parsedObj); string xmlValue=string.Empty;
parsedObj.SplittedElements.ForEach(t=>xmlValue+=t); using (StringReader rdr = new StringReader(xmlValue))
{
//Assembly.Load("Core");
Type t = Type.GetType(classType);
XmlSerializer serializer = new XmlSerializer(t);
return serializer.Deserialize(rdr);
}
} public void Remove(string location, string key)
{
string mainObjKeySetting = (string)cacheProvider.Get(key);
if (mainObjKeySetting == null || mainObjKeySetting.Length == )
return; string classType;
CacheParsedObject parsedObj;
GetParsedObject(key, mainObjKeySetting, out classType, out parsedObj); int i = ;
parsedObj.SplittedElements.ForEach(t => this.cacheProvider.Remove(string.Format("{0}_{1}", parsedObj.MainObject.Key, i++)));
this.cacheProvider.Remove(parsedObj.MainObject.Key);
}
private void GetParsedObject(string key, string mainObjKeySetting, out string classType, out CacheParsedObject parsedObj)
{
int from = , end = ;
classType = string.Empty;
if (mainObjKeySetting.IndexOf('@') > )
{
end = int.Parse(mainObjKeySetting.Split('@')[]);
classType = mainObjKeySetting.Split('@')[];
} parsedObj = new CacheParsedObject();
parsedObj.MainObject = new KeyValuePair<string, string>(key, string.Format("1@{0}@{1}", end, classType));
for (int i = from; i <= end; i++)
parsedObj.SplittedElements.Add((string)this.cacheProvider.Get(string.Format("{0}_{1}", parsedObj.MainObject.Key, i)));
}
private string Serializer2XMLConvert(object value)
{
using (StringWriter sw = new StringWriter())
{
XmlSerializer xz = new XmlSerializer(value.GetType());
xz.Serialize(sw, value);
return sw.ToString();
}
}
private void AssureSerializable(object value)
{
if (value == null)
throw new Exception("cache object must be Serializable");
if (value.GetType().GetCustomAttributes(typeof(SerializableAttribute), true).Count()<=)
throw new Exception("cache object must be Serializable");
}
}
下面是CacheCoordinator的代码,这个类的加入目的是要加入缓存的冗余机制:
class CacheCoordinator : ICacheCoordinator
{
CacheServerWrapper backupCacheServer = new CacheServerWrapper(CacheConfig.CacheConfiguration.BackupCacheServer);
CacheServersWrapper peerCacheServer = new CacheServersWrapper(CacheConfig.CacheConfiguration.PeerCacheServers); public void Put(string key, object value)
{
peerCacheServer.Put(key, value);
backupCacheServer.Put(key, value); //缓存冗余
} public object Get(string key)
{
object o=peerCacheServer.Get(key);
if (o != null)
return o;
return backupCacheServer.Get(key);
} public void Remove(string key)
{
peerCacheServer.Remove(key);
backupCacheServer.Remove(key);
}
}
剩下的就是具体的CacheProvider和CacheProviderWrapper类了:
public class CacheServerWrapper : ICacheExecutor
{
ICacheExecutor executor = null;
private CacheServerInfo configInfo;
public CacheServerWrapper(CacheServerInfo configInfo)
{
this.configInfo = configInfo;
ICacheExecutor tmpExecutor = null;
switch(this.configInfo.ServerType)
{
case CacheServerType.HttpRuntime:
tmpExecutor = new CacheProvider.HttpRuntimeCacheProvider(configInfo);
break;
case CacheServerType.InMemory:
tmpExecutor = new CacheProvider.InMemoryCacheProvider(configInfo);
break;
case CacheServerType.Memcached:
tmpExecutor = new CacheProvider.MemcachedCacheProvider(configInfo);
break;
case CacheServerType.Redis:
tmpExecutor = new CacheProvider.RedisCacheProvider(configInfo);
break;
default:
tmpExecutor = new CacheProvider.HttpRuntimeCacheProvider(configInfo);
break;
}
executor = tmpExecutor;
} public string FullServerAddress
{
get
{
return this.configInfo.FullServerAddress;
}
} public void Put(string key, object value)
{
executor.Put(key, value);
} public object Get(string key)
{
return executor.Get(key);
} public void Remove(string key)
{
executor.Remove(key);
}
}
只贴出Memcache的操作类
class MemcachedCacheProvider : ICacheExecutor
{
private MemcachedClient mc = new MemcachedClient();
private CacheServerInfo configInfo;
public MemcachedCacheProvider(CacheServerInfo configInfo)
{
this.configInfo = configInfo; //初始化池
SockIOPool pool = SockIOPool.GetInstance();
pool.SetServers(new string[] { string.Format("{0}:{1}", configInfo.ServerAddress, configInfo.ServerPort) });//设置连接池可用的cache服务器列表,server的构成形式是IP:PORT(如:127.0.0.1:11211)
pool.InitConnections = ;//初始连接数
pool.MinConnections = ;//最小连接数
pool.MaxConnections = ;//最大连接数
pool.SocketConnectTimeout = ;//设置连接的套接字超时
pool.SocketTimeout = ;//设置套接字超时读取
pool.MaintenanceSleep = ;//设置维护线程运行的睡眠时间。如果设置为0,那么维护线程将不会启动,30就是每隔30秒醒来一次 //获取或设置池的故障标志。
//如果这个标志被设置为true则socket连接失败,将试图从另一台服务器返回一个套接字如果存在的话。
//如果设置为false,则得到一个套接字如果存在的话。否则返回NULL,如果它无法连接到请求的服务器。
pool.Failover = true; pool.Nagle = false;//如果为false,对所有创建的套接字关闭Nagle的算法
pool.Initialize();
}
public void Put(string key, object value)
{
mc.Set(key, value);
} public object Get(string key)
{
return mc.Get(key);
} public void Remove(string key)
{
mc.Delete(key);
}
}
不能忘了可配置性,xml定义及代码如下:
<?xml version="1.0" encoding="utf-8" ?>
<CacheConfig>
<MaxCacheEntitySize>1048576</MaxCacheEntitySize><!--1*1024*1024-->
<PeerCacheServers>
<CacheServer>
<ServerType>InMemory</ServerType>
<ServerAddress>127.0.0.1</ServerAddress>
<ServerPort>11211</ServerPort>
</CacheServer>
<CacheServer>
<ServerType>InMemory</ServerType>
<ServerAddress>127.0.0.1</ServerAddress>
<ServerPort>11212</ServerPort>
</CacheServer>
</PeerCacheServers>
<BackupCacheServer>
<CacheServer>
<ServerType>InMemory</ServerType>
<ServerAddress>127.0.0.1</ServerAddress>
<ServerPort>11213</ServerPort>
</CacheServer>
</BackupCacheServer>
</CacheConfig>
读取配置信息的代码:
public static class CacheConfiguration
{
static CacheConfiguration()
{
Load();
} private static void Load()
{
PeerCacheServers = new List<CacheServerInfo>();
BackupCacheServer = null; XElement root = XElement.Load(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "CacheConfig.xml")); MaxCacheEntitySize = int.Parse(root.Element("MaxCacheEntitySize").Value);
foreach (var elm in root.Element("PeerCacheServers").Elements("CacheServer"))
{
CacheServerInfo srv = new CacheServerInfo();
srv.ServerAddress = elm.Element("ServerAddress").Value;
srv.ServerPort = int.Parse(elm.Element("ServerPort").Value);
srv.ServerType = (CacheServerType)Enum.Parse(typeof(CacheServerType), elm.Element("ServerType").Value);
PeerCacheServers.Add(srv);
}
foreach (var elm in root.Element("BackupCacheServer").Elements("CacheServer"))
{
CacheServerInfo srv = new CacheServerInfo();
srv.ServerAddress = elm.Element("ServerAddress").Value;
srv.ServerPort = int.Parse(elm.Element("ServerPort").Value);
srv.ServerType = (CacheServerType)Enum.Parse(typeof(CacheServerType), elm.Element("ServerType").Value);
BackupCacheServer = srv;
break;
}
if (PeerCacheServers.Count <= )
throw new Exception("Peer cache servers not found.");
if (BackupCacheServer == null)
throw new Exception("Backup cache server not found.");
AssureDistinctFullServerAddress(PeerCacheServers);
} private static void AssureDistinctFullServerAddress(List<CacheServerInfo> css)
{
Dictionary<string, int> map = new Dictionary<string, int>();
foreach(CacheServerInfo csInfo in css)
{
if (map.ContainsKey(csInfo.FullServerAddress))
throw new Exception(string.Format("Duplicated server address found [{0}].", csInfo.FullServerAddress));
else
map[csInfo.FullServerAddress] = ;
}
}
public static int MaxCacheEntitySize { get; set; }
public static List<CacheServerInfo> PeerCacheServers { get; set; }
public static CacheServerInfo BackupCacheServer { get; set; }
}
Append New
其实,我们忽略了一些重要的东西:
- 如果Memcached, Redis服务器超过了5台以上,通信量上升很快,怎么办?
- 由于取数据牵涉到网络I/O操作,因此速度依然比较慢,怎么办?
让我们来解决吧。
把新的UML图贴上(下图中左边红框中的是新增的):

本地缓存替换策略:LFU/LRU,其他的有很多。
EventBus是分布式的,下面有讲为什么要分布式的。
当Domain层需要获取数据时的逻辑:
- 先查看本地缓存中是否存在数据副本,存在则立刻返回(也没有网络I/O了)
- 没有则去redis/memcached获取,有则返回;并且把数据放入本地cache中
- 最后,实在没有数据,就db里取
当Domain层需要更新数据时的逻辑:
- 在本地cache中进行更新操作
- 更新分布式缓存
- 发布分布式事件,通知其他app server的cache manager去主动拉数据到他们本地缓存
看得出来,加入这个新的角色后,能对下面2项有改善作用:
- 降低网络间的通信流量
- 增大本地缓存的命中率
缓存子系统如何设计(Cachable tag, Memcache/redis support, xml config support, LRU/LFU/本地缓存命中率)的更多相关文章
- 【缓存】介绍和使用场景 MEMCACHE REDIS
缓存缓存就是在内存中存储的数据备份,当数据没有发生本质改变的时候,我们就不让数据的查询去数据库进行操作,而去内存中取数据,这样就大大降低了数据库的读写次数,而且从内存中读数据的速度比去数据库查询要快一 ...
- redis订阅发布消息操作本地缓存
Redis 本地缓存+远程缓存方案 使用纯java的ehcache作为本地缓存 Reids 作为远程分布式缓存 解决redis缓存压力过大,提高缓存速度,以及缓存性能. Redis和ehcache缓存 ...
- 使用本地缓存快还是使用redis缓存好?
使用本地缓存快还是使用redis缓存好? Redis早已家喻户晓,其性能自不必多说. 但是总有些时候,我们想把性能再提升一点,想着redis是个远程服务,性能也许不够,于是想用本地缓存试试!想法是不错 ...
- 本地缓存和redis
项目中的传统架构在服务启动时 读取数据库的大部分数据到本地内存,在看到redis的作用时发出疑问,到底有什么样的区别以及怎么选择呢,下面是别人的回答 使用本地缓存快还是使用redis缓存好? Redi ...
- 本地缓存google.guava及分布式缓存redis 随笔
近期项目用到了缓存,我选用的是主流的google.guava作本地缓存,redis作分布式 缓存,先说说我对本地缓存和分布式缓存的理解吧,可能不太成熟的地方,大家指出,一起 学习.本地缓存的特点是速度 ...
- 实现 Java 本地缓存,该从这几点开始
缓存,我相信大家对它一定不陌生,在项目中,缓存肯定是必不可少的.市面上有非常多的缓存工具,比如 Redis.Guava Cache 或者 EHcache.对于这些工具,我想大家肯定都非常熟悉,所以今天 ...
- C#自由组合本地缓存、分布式缓存和数据库的数据
一.背景介绍: 我们在进行数据存储的时候,有时候会加入本地缓存.分布式缓存以及数据库存储三级的结构,当我们取值的时候经常是像下面这样的流程: 1.先取本地缓存,如果值存在直接返回 2.本地缓存不存在, ...
- c#本地缓存实现
用了一段时间java,java实现服务端程序很简单,有很多公共开源的组件或者软件.但是c#的很少. 现在准备自己写点东西,学习下新的东西,总结下c#的内容以及我们经常用的内容,抽离成类,组件,模型.方 ...
- 本地缓存下载文件,download的二次封装
来源:http://ask.dcloud.net.cn/article/524 源码下载链接 说明: (1)由于平时项目中大量用到了附件下载等功能,所以就花了一个时间,把plus的downlaod进行 ...
随机推荐
- .net工具基础
MSIL Disassembler(ildasm.exe) -- 将C#程序或类库反汇编处理,显示C#编译器生成的CIL代码 C:\Program Files\Microsoft SDKs\Windo ...
- ExtJS中layout的12种布局风格
总览 extjs的容器组件都可以设置它的显示风格,它的有效值有 1. absolute,2. accordion, 3. anchor, 4. border, 5. card, 6. column, ...
- 解决Unable to load native-hadoop library for your platform
使用hadoop fs相应命令时候总是出现 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your plat ...
- [20181015]为什么是3秒.txt
[20181015]为什么是3秒.txt --//以前测试:连接http://blog.itpub.net/267265/viewspace-2144765/=>为什么是12秒.txt.--// ...
- linux安装教程
一.安装准备工作 虚拟机VMWARE10 镜像文件CentOs6.5 二.安装过程 1.启动VMWARE10,点击创建新的虚拟机. 2.选择典型安装即可 3.这里我们选择稍后再进行安装操作系统,因为这 ...
- 前后端分离djangorestframework——restful规范
restful现在非常流行,所以很有必要提一下 web服务交互 在浏览器中能看到的每个网站,都是一个web服务.那么我们在提供每个web服务的时候,都需要前后端交互,前后端交互就一定有一些实现方案,我 ...
- SQL Server 锁实验(重建索引)
昨晚某现场报一个重建索引失败的问题,远程查看后发现是自动收缩的内部会话引发的锁申请超时,突然想起来自己的加锁实验还没完成索引重建部分,今天有空正好做一下: USE [数据库名] GO ALTER IN ...
- [福大软工] Z班 第1次成绩排行榜
作业地址 http://www.cnblogs.com/easteast/p/7403491.html 作业要求 (1)回想一下你初入大学时对计算机专业的畅想 当初你是如何做出选择计算机专业的决定的? ...
- keepalived脑裂问题查找
在自己环境做keepalived+redis实验时,当重启了备用redies机器后,发现两台redies主机都拿到了VIP [root@redis2 ~]# ip addr list 1: lo: & ...
- TCP长连接保持连接状态
对于TCP长连接保活是十分必要的,原因如下: 1.系统多在OA网和外网间有防火墙隔离,很多防火墙对一段时间内没有报文活动的socket会自动关闭. 2.对于非正常断开的连接系统并不能侦测到,比如防火墙 ...