Cachalot DB 是一个完全开源的项目,可以在GitHub上找到。它提供了一种高效的分布式缓存解决方案,适用于需要快速访问数据的应用程序。本文将详细介绍如何使用Cachalot DB来提高应用程序的性能。
Cachalot DB 的最新版本和完整文档可以在以下链接中找到:
分布式缓存最常见的用例是存储由一个或多个唯一键标识的对象。当访问一个对象时,首先尝试从缓存中获取它,如果不可用,则从数据库加载。通常,如果对象是从数据库加载的,它也会被存储在缓存中以供后续使用。
以下是一个简单的算法示例,展示了如何使用C#语言从缓存中获取对象:
Item = cache.TryGet(itemKey);
if (Item != null)
{
return Item;
}
else
{
Item = database.Load(itemKey);
cache.Put(Item);
return Item;
}
通过使用这种简单的算法,缓存会随着时间的推移逐渐填充数据,其“命中率”也会随之提高。
这种缓存使用通常与“逐出策略”相关联,以避免内存消耗过大。当达到某个阈值(无论是内存使用还是对象计数)时,缓存中的一些对象会被移除。
最常用的逐出策略是“最近最少使用”(Least Recently Used,简称LRU)。在这种情况下,每次在缓存中访问对象时,都会更新与其关联的时间戳。当触发逐出时,会移除具有最旧时间戳的对象。
使用Cachalot作为这种类型的分布式缓存非常简单。首先,禁用持久性(默认情况下是启用的)。在集群的每个节点上,都有一个名为node_config.json的小配置文件,它通常看起来像这样:
{
"IsPersistent": true,
"ClusterName": "test",
"TcpPort": 6666,
"DataPath": "root"
}
要将集群切换到纯缓存模式,只需在所有节点上将IsPersistent设置为false。在这种情况下,DataPath将被忽略。
以下是一个带有LRU逐出激活的客户端代码示例:
public class TradeProvider
{
private Connector _connector;
public void Startup(ClientConfig config)
{
_connector = new Connector(config);
var trades = _connector.DataSource<Trade>();
// 每次达到500,000的限制时,移除500个项目
trades.ConfigEviction(EvictionType.LessRecentlyUsed, 500_000, 500);
}
public Trade GetTrade(int id)
{
var trades = _connector.DataSource<Trade>();
var fromCache = trades[id];
if (fromCache != null)
{
return fromCache;
}
var trade = GetTradeFromDatabase(id);
trades.Put(trade);
return trade;
}
public void Shutdown()
{
_connector.Dispose();
}
}
单个对象访问模式在某些现实世界案例中很有用,例如存储网站的会话信息、部分填写的表单、博客文章等。但有时,需要使用类似SQL的查询从缓存中检索一组对象。
希望缓存仅在能够保证查询涉及的所有数据都在缓存中时才返回结果。这里的明显问题是:如何知道所有数据都在缓存中?
在最简单的(但不是最常见的)情况下,可以保证数据库中的所有数据也都在缓存中。这需要有足够的RAM来存储数据库中的所有数据。 缓存可以由外部组件预加载(例如,每天早上),或者在首次访问时懒加载。
DataSource类提供了两种新方法来管理这种用例。 一个LINQ扩展:OnlyIfComplete。当在LINQ命令管道中插入这个方法时,它会修改数据源的行为。它仅在所有数据都可用时返回IEnumerable,否则抛出异常。 一个新方法来声明给定数据类型的所有数据都可用:DeclareFullyLoaded(DataSource类的成员)。
以下是一个从单元测试中提取的代码示例:
var dataSource = connector.DataSource<ProductEvent>();
dataSource.PutMany(events);
// 这里会抛出异常
Assert.Throws<CacheException>(() =>
dataSource.Where(e => e.EventType == "FIXING").OnlyIfComplete().ToList()
);
// 声明所有数据都可用
dataSource.DeclareFullyLoaded();
// 这里工作正常
var fixings = dataSource.Where(e => e.EventType == "FIXING").
OnlyIfComplete().ToList();
Assert.Greater(fixings.Count, 0);
// 再次声明数据不可用
dataSource.DeclareFullyLoaded(false);
// 这里会再次抛出异常
Assert.Throws<CacheException>(() =>
dataSource.Where(e => e.EventType == "FIXING").
OnlyIfComplete().ToList()
);
对于这种用例,Cachalot提供了一个创新的解决方案:将预加载的数据描述为一个查询(以LINQ表达式表示)。 当从缓存中查询数据时,确定查询是否是预加载数据的一个子集。
DataSource类中涉及这个过程的两个方法是: 相同的OnlyIfComplete LINQ扩展 DeclareLoadedDomain方法。它的参数是一个LINQ表达式,定义了全局数据的一个子域。
示例1:在像Airbnb这样的租赁网站上,希望将访问量最大的城市的所有房产存储在缓存中。
homes.DeclareLoadedDomain(h => h.Town == "Paris" || h.Town == "Nice");
然后,这个查询将成功,因为它是指定域的一个子集。
var result = homes.Where(h => h.Town == "Paris" && h.Rooms >= 2)
.OnlyIfComplete().ToList();
但是这个会抛出异常。
result = homes.Where(h => h.CountryCode == "FR" && h.Rooms == 2)
.OnlyIfComplete().ToList();
如果省略对OnlyIfComplete的调用,它将简单地返回缓存中与查询匹配的元素。