Cachalot DB 作为具有独特功能的分布式缓存





5.00/5 (10投票s)
允许描述和安全查询缓存数据的 LINQ 扩展。
引言
本文介绍如何使用 Cachalot DB 作为分布式缓存(无持久化)。
有关 Cachalot DB 的一般介绍,请参阅 第一部分、第二部分、第三部分。
Cachalot DB 是一个完全开源的项目,可在以下位置找到:
包含完整文档的最新版本
从缓存中提供单个对象
分布式缓存最常见的用例是存储由一个或多个唯一键标识的对象。
数据库包含持久化数据,当访问一个对象时,我们首先尝试从缓存中获取它,如果不存在,则从数据库加载。通常,如果对象是从数据库加载的,它也会被存储在缓存中以供以后使用。
Item = cache.TryGet(itemKey)
If Item found
return Item
Else
Item = database.Load(itemKey)
cache.Put(Item)
return Item
通过使用这个简单的算法,缓存会逐渐填充数据,其“命中率”会随着时间的推移而提高。
此缓存用法通常与“驱逐策略”相关联,以避免过多的内存消耗。当达到阈值时(无论是内存使用量还是对象计数),都会从缓存中删除一些对象。
最常用的驱逐策略是“最近最少使用”,简称为 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>();
// remove 500 items every time the limit of 500_000 is reached
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();
}
}
驱逐是按数据类型配置的。每种数据类型都可以有特定的驱逐策略(或无)。
市面上任何像样的分布式缓存都可以做到这一点。但 Cachalot 可以做到更多。
从缓存中提供复杂查询
单对象访问模式在某些实际场景中有用,例如存储网站的会话信息、部分填充的表单、博客文章等等。
但有时,我们需要从缓存中检索一组对象,并使用类似 SQL 的查询。
我们希望缓存仅在可以保证查询所涉及的所有数据都可用于缓存时才返回结果。
显而易见的问题是:我们如何知道所有数据都可用在缓存中?
第一种情况:数据库中的所有数据都已加载到缓存中
在最简单(但不是最常见)的情况下,我们可以保证数据库中的所有数据也在缓存中。这要求有足够的 RAM 来容纳数据库中的所有数据。
缓存要么由外部组件预先加载(例如,每天早上),要么在首次访问时惰性加载。
DataSource
类中有两个新方法可用于管理此用例。
- LINQ 扩展:
OnlyIfComplete
。当我们将此方法插入 LINQ 命令管道时,它将修改数据源的行为。它仅在所有数据都可用时才返回IEnumerable
,否则将抛出异常。 - 一个新方法,用于声明给定数据类型的所有数据都已可用:
DeclareFullyLoaded
(DataSource
类成员)
以下是摘自单元测试的代码示例
var dataSource = connector.DataSource<ProductEvent>();
dataSource.PutMany(events);
// here an exception will be thrown
Assert.Throws<CacheException>(() =>
dataSource.Where(e => e.EventType == "FIXING").OnlyIfComplete().ToList()
);
// declare that all data is available
dataSource.DeclareFullyLoaded();
// here it works fine
var fixings = dataSource.Where(e => e.EventType == "FIXING").OnlyIfComplete().ToList();
Assert.Greater(fixings.Count, 0);
// declare that data is not available again
dataSource.DeclareFullyLoaded(false);
// an exception will be thrown again
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
的调用,它将仅返回缓存中与查询匹配的元素。
示例 2:在交易系统中,我们想缓存所有有效的交易(到期日期 >= 今天)以及所有在过去一年内创建的交易(交易日期 > 一年前)。
var oneYearAgo = DateTime.Today.AddYears(-1);
var today = DateTime.Today;
trades.DeclareLoadedDomain(
t=>t.MaturityDate >= today || t.TradeDate > oneYearAgo
);
然后,这些查询将成功,因为它们是指定域的子集。
var res =trades.Where(
t=>t.IsDestroyed == false && t.TradeDate == DateTime.Today.AddDays(-1)
).OnlyIfComplete().ToList();
res = trades.Where(
t => t.IsDestroyed == false && t.MaturityDate == DateTime.Today
).OnlyIfComplete().ToList();
但这个查询将抛出异常。
trades.Where(
t => t.IsDestroyed == false && t.Portfolio == "SW-EUR"
).OnlyIfComplete().ToList()
域名声明和驱逐策略在数据类型上当然是互斥的。自动驱逐会导致数据不完整。