.Net Core 缓存方式(二)DistributedSqlServerCache实现(2)
DistributedSqlServerCache 是什么
DistributedSqlServerCache是使用 SQL Server database 实现分布式缓存
使用方式
- Startup.ConfigureServices
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString =
_config["DistCache_ConnectionString"];
options.SchemaName = "dbo";
options.TableName = "TestCache";
});
源码以及实现
- SqlServerCachingServicesExtensions
/// <summary>
/// Extension methods for setting up Microsoft SQL Server distributed cache services in an <see cref="IServiceCollection" />.
/// </summary>
public static class SqlServerCachingServicesExtensions
{
/// <summary>
/// Adds Microsoft SQL Server distributed caching services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="setupAction">An <see cref="Action{SqlServerCacheOptions}"/> to configure the provided <see cref="SqlServerCacheOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddDistributedSqlServerCache(this IServiceCollection services, Action<SqlServerCacheOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (setupAction == null)
{
throw new ArgumentNullException(nameof(setupAction));
}
services.AddOptions();
AddSqlServerCacheServices(services);
services.Configure(setupAction);
return services;
}
// to enable unit testing
internal static void AddSqlServerCacheServices(IServiceCollection services)
{
services.Add(ServiceDescriptor.Singleton<IDistributedCache, SqlServerCache>());
}
}
- SqlServerCache
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Caching.SqlServer
{
/// <summary>
/// Distributed cache implementation using Microsoft SQL Server database.
/// </summary>
public class SqlServerCache : IDistributedCache
{
private static readonly TimeSpan MinimumExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
private static readonly TimeSpan DefaultExpiredItemsDeletionInterval = TimeSpan.FromMinutes(30);
private readonly IDatabaseOperations _dbOperations;
private readonly ISystemClock _systemClock;
private readonly TimeSpan _expiredItemsDeletionInterval;
private DateTimeOffset _lastExpirationScan;
private readonly Action _deleteExpiredCachedItemsDelegate;
private readonly TimeSpan _defaultSlidingExpiration;
public SqlServerCache(IOptions<SqlServerCacheOptions> options)
{
var cacheOptions = options.Value;
if (string.IsNullOrEmpty(cacheOptions.ConnectionString))
{
throw new ArgumentException(
$"{nameof(SqlServerCacheOptions.ConnectionString)} cannot be empty or null.");
}
if (string.IsNullOrEmpty(cacheOptions.SchemaName))
{
throw new ArgumentException(
$"{nameof(SqlServerCacheOptions.SchemaName)} cannot be empty or null.");
}
if (string.IsNullOrEmpty(cacheOptions.TableName))
{
throw new ArgumentException(
$"{nameof(SqlServerCacheOptions.TableName)} cannot be empty or null.");
}
if (cacheOptions.ExpiredItemsDeletionInterval.HasValue &&
cacheOptions.ExpiredItemsDeletionInterval.Value < MinimumExpiredItemsDeletionInterval)
{
throw new ArgumentException(
$"{nameof(SqlServerCacheOptions.ExpiredItemsDeletionInterval)} cannot be less than the minimum " +
$"value of {MinimumExpiredItemsDeletionInterval.TotalMinutes} minutes.");
}
if (cacheOptions.DefaultSlidingExpiration <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(cacheOptions.DefaultSlidingExpiration),
cacheOptions.DefaultSlidingExpiration,
"The sliding expiration value must be positive.");
}
_systemClock = cacheOptions.SystemClock ?? new SystemClock();
_expiredItemsDeletionInterval =
cacheOptions.ExpiredItemsDeletionInterval ?? DefaultExpiredItemsDeletionInterval;
_deleteExpiredCachedItemsDelegate = DeleteExpiredCacheItems;
_defaultSlidingExpiration = cacheOptions.DefaultSlidingExpiration;
// SqlClient library on Mono doesn't have support for DateTimeOffset and also
// it doesn't have support for apis like GetFieldValue, GetFieldValueAsync etc.
// So we detect the platform to perform things differently for Mono vs. non-Mono platforms.
if (PlatformHelper.IsMono)
{
_dbOperations = new MonoDatabaseOperations(
cacheOptions.ConnectionString,
cacheOptions.SchemaName,
cacheOptions.TableName,
_systemClock);
}
else
{
_dbOperations = new DatabaseOperations(
cacheOptions.ConnectionString,
cacheOptions.SchemaName,
cacheOptions.TableName,
_systemClock);
}
}
public byte[] Get(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
var value = _dbOperations.GetCacheItem(key);
ScanForExpiredItemsIfRequired();
return value;
}
public async Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken))
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
var value = await _dbOperations.GetCacheItemAsync(key, token).ConfigureAwait(false);
ScanForExpiredItemsIfRequired();
return value;
}
public void Refresh(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
_dbOperations.RefreshCacheItem(key);
ScanForExpiredItemsIfRequired();
}
public async Task RefreshAsync(string key, CancellationToken token = default(CancellationToken))
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
await _dbOperations.RefreshCacheItemAsync(key, token).ConfigureAwait(false);
ScanForExpiredItemsIfRequired();
}
public void Remove(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
_dbOperations.DeleteCacheItem(key);
ScanForExpiredItemsIfRequired();
}
public async Task RemoveAsync(string key, CancellationToken token = default(CancellationToken))
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
await _dbOperations.DeleteCacheItemAsync(key, token).ConfigureAwait(false);
ScanForExpiredItemsIfRequired();
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
GetOptions(ref options);
_dbOperations.SetCacheItem(key, value, options);
ScanForExpiredItemsIfRequired();
}
public async Task SetAsync(
string key,
byte[] value,
DistributedCacheEntryOptions options,
CancellationToken token = default(CancellationToken))
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
token.ThrowIfCancellationRequested();
GetOptions(ref options);
await _dbOperations.SetCacheItemAsync(key, value, options, token).ConfigureAwait(false);
ScanForExpiredItemsIfRequired();
}
// Called by multiple actions to see how long it's been since we last checked for expired items.
// If sufficient time has elapsed then a scan is initiated on a background task.
private void ScanForExpiredItemsIfRequired()
{
var utcNow = _systemClock.UtcNow;
// TODO: Multiple threads could trigger this scan which leads to multiple calls to database.
if ((utcNow - _lastExpirationScan) > _expiredItemsDeletionInterval)
{
_lastExpirationScan = utcNow;
Task.Run(_deleteExpiredCachedItemsDelegate);
}
}
private void DeleteExpiredCacheItems()
{
_dbOperations.DeleteExpiredCacheItems();
}
private void GetOptions(ref DistributedCacheEntryOptions options)
{
if (!options.AbsoluteExpiration.HasValue
&& !options.AbsoluteExpirationRelativeToNow.HasValue
&& !options.SlidingExpiration.HasValue)
{
options = new DistributedCacheEntryOptions()
{
SlidingExpiration = _defaultSlidingExpiration
};
}
}
}
}
- IDatabaseOperations
internal interface IDatabaseOperations
{
byte[] GetCacheItem(string key);
Task<byte[]> GetCacheItemAsync(string key, CancellationToken token = default(CancellationToken));
void RefreshCacheItem(string key);
Task RefreshCacheItemAsync(string key, CancellationToken token = default(CancellationToken));
void DeleteCacheItem(string key);
Task DeleteCacheItemAsync(string key, CancellationToken token = default(CancellationToken));
void SetCacheItem(string key, byte[] value, DistributedCacheEntryOptions options);
Task SetCacheItemAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken));
void DeleteExpiredCacheItems();
}
- SqlQueries sql 语句
internal class SqlQueries
{
private const string TableInfoFormat =
"SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE " +
"FROM INFORMATION_SCHEMA.TABLES " +
"WHERE TABLE_SCHEMA = '{0}' " +
"AND TABLE_NAME = '{1}'";
private const string UpdateCacheItemFormat =
"UPDATE {0} " +
"SET ExpiresAtTime = " +
"(CASE " +
"WHEN DATEDIFF(SECOND, @UtcNow, AbsoluteExpiration) <= SlidingExpirationInSeconds " +
"THEN AbsoluteExpiration " +
"ELSE " +
"DATEADD(SECOND, SlidingExpirationInSeconds, @UtcNow) " +
"END) " +
"WHERE Id = @Id " +
"AND @UtcNow <= ExpiresAtTime " +
"AND SlidingExpirationInSeconds IS NOT NULL " +
"AND (AbsoluteExpiration IS NULL OR AbsoluteExpiration <> ExpiresAtTime) ;";
private const string GetCacheItemFormat =
"SELECT Id, ExpiresAtTime, SlidingExpirationInSeconds, AbsoluteExpiration, Value " +
"FROM {0} WHERE Id = @Id AND @UtcNow <= ExpiresAtTime;";
private const string SetCacheItemFormat =
"DECLARE @ExpiresAtTime DATETIMEOFFSET; " +
"SET @ExpiresAtTime = " +
"(CASE " +
"WHEN (@SlidingExpirationInSeconds IS NUll) " +
"THEN @AbsoluteExpiration " +
"ELSE " +
"DATEADD(SECOND, Convert(bigint, @SlidingExpirationInSeconds), @UtcNow) " +
"END);" +
"UPDATE {0} SET Value = @Value, ExpiresAtTime = @ExpiresAtTime," +
"SlidingExpirationInSeconds = @SlidingExpirationInSeconds, AbsoluteExpiration = @AbsoluteExpiration " +
"WHERE Id = @Id " +
"IF (@@ROWCOUNT = 0) " +
"BEGIN " +
"INSERT INTO {0} " +
"(Id, Value, ExpiresAtTime, SlidingExpirationInSeconds, AbsoluteExpiration) " +
"VALUES (@Id, @Value, @ExpiresAtTime, @SlidingExpirationInSeconds, @AbsoluteExpiration); " +
"END ";
private const string DeleteCacheItemFormat = "DELETE FROM {0} WHERE Id = @Id";
public const string DeleteExpiredCacheItemsFormat = "DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime";
public SqlQueries(string schemaName, string tableName)
{
var tableNameWithSchema = string.Format(
"{0}.{1}", DelimitIdentifier(schemaName), DelimitIdentifier(tableName));
// when retrieving an item, we do an UPDATE first and then a SELECT
GetCacheItem = string.Format(UpdateCacheItemFormat + GetCacheItemFormat, tableNameWithSchema);
GetCacheItemWithoutValue = string.Format(UpdateCacheItemFormat, tableNameWithSchema);
DeleteCacheItem = string.Format(DeleteCacheItemFormat, tableNameWithSchema);
DeleteExpiredCacheItems = string.Format(DeleteExpiredCacheItemsFormat, tableNameWithSchema);
SetCacheItem = string.Format(SetCacheItemFormat, tableNameWithSchema);
TableInfo = string.Format(TableInfoFormat, EscapeLiteral(schemaName), EscapeLiteral(tableName));
}
public string TableInfo { get; }
public string GetCacheItem { get; }
public string GetCacheItemWithoutValue { get; }
public string SetCacheItem { get; }
public string DeleteCacheItem { get; }
public string DeleteExpiredCacheItems { get; }
// From EF's SqlServerQuerySqlGenerator
private string DelimitIdentifier(string identifier)
{
return "[" + identifier.Replace("]", "]]") + "]";
}
private string EscapeLiteral(string literal)
{
return literal.Replace("'", "''");
}
}
- IDatabaseOperations 实现方式
https://github.com/dotnet/extensions/blob/master/src/Caching/SqlServer/src/SqlServerCache.cs
// SqlClient library on Mono doesn't have support for DateTimeOffset and also
// it doesn't have support for apis like GetFieldValue, GetFieldValueAsync etc.
// So we detect the platform to perform things differently for Mono vs. non-Mono platforms.
if (PlatformHelper.IsMono)
{
_dbOperations = new MonoDatabaseOperations(
cacheOptions.ConnectionString,
cacheOptions.SchemaName,
cacheOptions.TableName,
_systemClock);
}
else
{
_dbOperations = new DatabaseOperations(
cacheOptions.ConnectionString,
cacheOptions.SchemaName,
cacheOptions.TableName,
_systemClock);
}