Cosmos DB peut être un bon candidat pour un magasin clé-valeur. Cosmos DB est une base de données multimodale dans Azure qui prend en charge le stockage sans schéma. Par défaut, les conteneurs Cosmos DB ont tendance à indexer tous les champs d’un document téléchargé. Nous pouvons limiter les propriétés d’index uniquement à id et partitionkey pour faire du conteneur un pur magasin clé-valeur. L’interdiction d’indexer d’autres champs augmente les performances et réduit les RU pour interroger les enregistrements de points. (Le coût de Cosmos DB est mesuré en RU.) Pour le stockage d’objets clés, le RU a tendance à être inférieur, mais cela dépend toujours de la taille de la charge utile. Les tests de performances effectués sur 100 partitions et 100 000 enregistrements ont donné un P95 entre 25 et 30 ms en lecture et en écriture.
Plongeons-nous et implémentons Cosmos DB en tant que magasin clé-valeur pur en C#.
Conditions préalables:
- Avoir un compte dans le portail Azure
- Provisionnez Cosmos DB avec un compte SQL.
Il existe plusieurs façons de connecter le cache à une application .NET. Définissons une interface générique de cache distribué avec quelques extensions.
interface IDistributedObjectCache
{
Task<bool> SetObjectAsync<T>(string key, T value, CancellationToken cancellationToken = default);
Task<bool> SetObjectAsync<T>(string key, string pk1, T value, CancellationToken cancellationToken = default);
Task<T> GetObjectAsync<T>(string key, CancellationToken cancellationToken = default);
Task<T> GetObjectAsync<T>(string key, string pk1, CancellationToken cancellationToken = default);
}
Par rapport à IDistributedCache du framework .NET, IDistributedObjectCache donne l’extensibilité d’ajouter une clé de partition selon les besoins de l’application.
Prédéfinissons tous les paramètres de connexion nécessaires à la connexion CosmosClient
public class DbConstants
{
public const string ConnectionString = "<CosmosDb connection string>";
public const string CacheDatabaseId = "ObjectCacheDb";
public const string CacheContainerId = "CacheContainer";
}
Définissons un CosmosClientManager qui nous aide à gérer l’objet de connexion CosmosClient. Vous pouvez modifier ApplicationRegion à l’emplacement souhaité.
public class CosmosClientManager : IDisposable
{
public CosmosClientManager()
{
if (!string.IsNullOrWhiteSpace(DbConstants.ConnectionString))
{
CosmosClient = new CosmosClient(DbConstants.ConnectionString, new CosmosClientOptions
{
ConnectionMode = ConnectionMode.Direct,
IdleTcpConnectionTimeout = TimeSpan.FromMinutes(30),
ApplicationRegion = Regions.WestUS2,
});
}
}
public CosmosClient? CosmosClient { get; } = null;
public void Dispose()
{
CosmosClient?.Dispose();
}
}
Définissons un modèle d’objet sur la façon dont les enregistrements dans un conteneur de cache ressembleront
public class ObjectContainer<TPayload>
{
[Required]
[JsonProperty("id")]
public string PrimaryKey { get; set; }
[Required]
public string PartitionKey { get; set; }
public TPayload Payload { get; set; }
[Required]
public string ContainerId => DbConstants.CacheContainerId;
public string PartitionKeyPath => $"/{nameof(PartitionKey)}";
[JsonProperty("_etag")]
public string ETag { get; }
[JsonProperty("_ts")]
public long DateTimestamp { get; set; }
}
J’ai créé un simple IWarmup pour initialiser les composants dans un thread d’arrière-plan.
internal interface IWarmUp
{
Task<bool> WarmUpAsync();
}
Définissons le noyau CosmosDistributedObjectStore qui implémente toutes les interfaces nécessaires.
public class CosmosDistributedObjectStore : IDistributedObjectCache, IWarmUp
{
private readonly CosmosClientManager cosmosClientManager;
public CosmosDistributedObjectStore(
CosmosClientManager cosmosClientManager
)
{
this.cosmosClientManager = cosmosClientManager;
}
public Task<T> GetObjectAsync<T>(string key, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return GetObjectAsync<T>(key, key, cancellationToken);
}
public async Task<T> GetObjectAsync<T>(string key, string pk1, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var container = GetCurrentContainer();
if (container == null) return default;
var record = await container.ReadItemAsync<ObjectContainer<T>>(key, new PartitionKey(pk1), cancellationToken: cancellationToken);
return record != null ? record.Resource.Payload : default;
}
public Task<bool> SetObjectAsync<T>(string key, T value, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return SetObjectAsync<T>(key, key, value, cancellationToken);
}
public async Task<bool> SetObjectAsync<T>(string key, string pk1, T value, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var container = GetCurrentContainer();
if (container == null) return false;
var record = new ObjectContainer<T>
{
PartitionKey = key,
PrimaryKey = key,
Payload = value,
};
await container.CreateItemAsync(record);
return true;
}
public async Task<bool> WarmUpAsync()
{
var entry = new ObjectContainer<dynamic>();
var containerProperties = new ContainerProperties(entry.ContainerId, entry.PartitionKeyPath);
// optimize container as a pure keyvalue store. Disable indexding other than id column
containerProperties.IndexingPolicy.ExcludedPaths.Add(new ExcludedPath() { Path = "/*" });
var db = await cosmosClientManager.CosmosClient?.CreateDatabaseIfNotExistsAsync(DbConstants.CacheDatabaseId, 1000);
var container = await db?.Database?.CreateContainerIfNotExistsAsync(containerProperties, 1000);
return true;
}
private Container? GetCurrentContainer()
{
return cosmosClientManager?.CosmosClient?.GetContainer(DbConstants.CacheDatabaseId, DbConstants.CacheContainerId);
}
}
Je voudrais mettre l’accent sur WarmUpAsync() et sur la manière d’affiner la création de conteneurs sur les champs d’ID d’index et de primaryKey uniquement afin que le conteneur fonctionne comme un pur magasin d’objets clés.
containerProperties.IndexingPolicy.ExcludedPaths.Add(new ExcludedPath() { Path = "/*" });
Écrivons un appelant simple pour tester notre nouveau magasin d’objets clés.
static async Task Main(string[] args)
{
var cosmosDistObjectStore = new CosmosDistributedObjectStore(new CosmosClientManager());
IWarmUp components = cosmosDistObjectStore;
IDistributedObjectCache objectCache = cosmosDistObjectStore;
await components.WarmUpAsync();
var studentKey = "alpha";
var studentObject = new { Name = "alpha", Age = 10, Class = "Grade-V" };
// set cache
await objectCache.SetObjectAsync(studentKey, studentObject);
// get student grade details
var alphaDetails = await objectCache.GetObjectAsync<dynamic>(studentKey, studentObject.Class);
// get all cache
var alpha = await objectCache.GetObjectAsync<dynamic>(studentKey);
}
Nous disposons désormais d’un magasin d’objets de clé NoSQL alternatif robuste avec Azure Cosmos DB qui peut partitionner et évoluer à la demande.