Browse Source

Refactored

pull/4500/head
liangshiwei 6 years ago
parent
commit
018e7cbd08
  1. 496
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs
  2. 70
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs
  3. 116
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs

496
framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs

@ -1,496 +0,0 @@
// This software is part of the DOTNET extensions
// Copyright (c) .NET Foundation and Contributors
// https://dotnet.microsoft.com/
//
// All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
namespace Microsoft.Extensions.Caching.StackExchangeRedis
{
public class RedisCache : IDistributedCache, IDisposable
{
// KEYS[1] = = key
// ARGV[1] = absolute-expiration - ticks as long (-1 for none)
// ARGV[2] = sliding-expiration - ticks as long (-1 for none)
// ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
// ARGV[4] = data - byte[]
// this order should not change LUA script depends on it
protected const string SetScript = (@"
redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
if ARGV[3] ~= '-1' then
redis.call('EXPIRE', KEYS[1], ARGV[3])
end
return 1");
protected const string AbsoluteExpirationKey = "absexp";
protected const string SlidingExpirationKey = "sldexp";
protected const string DataKey = "data";
protected const long NotPresent = -1;
protected volatile ConnectionMultiplexer Connection;
protected IDatabase Cache;
protected readonly RedisCacheOptions Options;
protected readonly string Instance;
protected readonly SemaphoreSlim ConnectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
public RedisCache(IOptions<RedisCacheOptions> optionsAccessor)
{
if (optionsAccessor == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
}
Options = optionsAccessor.Value;
// This allows partitioning a single backend cache for use with multiple apps/services.
Instance = Options.InstanceName ?? string.Empty;
}
public virtual byte[] Get(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
return GetAndRefresh(key, getData: true);
}
public virtual async Task<byte[]> GetAsync(
string key,
CancellationToken token = default)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
return await GetAndRefreshAsync(key, getData: true, token: token);
}
public virtual 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));
}
Connect();
var creationTime = DateTimeOffset.UtcNow;
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);
Cache.ScriptEvaluate(SetScript, new RedisKey[] {Instance + key},
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
value
});
}
public virtual async Task SetAsync(
string key,
byte[] value,
DistributedCacheEntryOptions options,
CancellationToken token = default)
{
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();
await ConnectAsync(token);
var creationTime = DateTimeOffset.UtcNow;
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);
await Cache.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + key},
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
value
});
}
public virtual void Refresh(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
GetAndRefresh(key, getData: false);
}
public virtual async Task RefreshAsync(
string key,
CancellationToken token = default)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
await GetAndRefreshAsync(key, getData: false, token: token);
}
protected virtual void Connect()
{
if (Cache != null)
{
return;
}
ConnectionLock.Wait();
try
{
if (Cache == null)
{
if (Options.ConfigurationOptions != null)
{
Connection = ConnectionMultiplexer.Connect(Options.ConfigurationOptions);
}
else
{
Connection = ConnectionMultiplexer.Connect(Options.Configuration);
}
Cache = Connection.GetDatabase();
}
}
finally
{
ConnectionLock.Release();
}
}
protected virtual async Task ConnectAsync(CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
if (Cache != null)
{
return;
}
await ConnectionLock.WaitAsync(token);
try
{
if (Cache == null)
{
if (Options.ConfigurationOptions != null)
{
Connection = await ConnectionMultiplexer.ConnectAsync(Options.ConfigurationOptions);
}
else
{
Connection = await ConnectionMultiplexer.ConnectAsync(Options.Configuration);
}
Cache = Connection.GetDatabase();
}
}
finally
{
ConnectionLock.Release();
}
}
protected virtual byte[] GetAndRefresh(string key, bool getData)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
Connect();
// This also resets the LRU status as desired.
// TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math.
RedisValue[] results;
if (getData)
{
results = Cache.HashMemberGet(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey, DataKey);
}
else
{
results = Cache.HashMemberGet(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey);
}
// TODO: Error handling
if (results.Length >= 2)
{
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr);
Refresh(key, absExpr, sldExpr);
}
if (results.Length >= 3 && results[2].HasValue)
{
return results[2];
}
return null;
}
protected virtual async Task<byte[]> GetAndRefreshAsync(
string key,
bool getData,
CancellationToken token = default)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
await ConnectAsync(token);
// This also resets the LRU status as desired.
// TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math.
RedisValue[] results;
if (getData)
{
results = await Cache.HashMemberGetAsync(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey,
DataKey);
}
else
{
results = await Cache.HashMemberGetAsync(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey);
}
// TODO: Error handling
if (results.Length >= 2)
{
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr);
await RefreshAsync(key, absExpr, sldExpr, token);
}
if (results.Length >= 3 && results[2].HasValue)
{
return results[2];
}
return null;
}
public virtual void Remove(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
Connect();
Cache.KeyDelete(Instance + key);
// TODO: Error handling
}
public virtual async Task RemoveAsync(
string key,
CancellationToken token = default)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
await ConnectAsync(token);
await Cache.KeyDeleteAsync(Instance + key);
// TODO: Error handling
}
protected virtual void MapMetadata(
RedisValue[] results,
out DateTimeOffset? absoluteExpiration,
out TimeSpan? slidingExpiration)
{
absoluteExpiration = null;
slidingExpiration = null;
var absoluteExpirationTicks = (long?) results[0];
if (absoluteExpirationTicks.HasValue && absoluteExpirationTicks.Value != NotPresent)
{
absoluteExpiration = new DateTimeOffset(absoluteExpirationTicks.Value, TimeSpan.Zero);
}
var slidingExpirationTicks = (long?) results[1];
if (slidingExpirationTicks.HasValue && slidingExpirationTicks.Value != NotPresent)
{
slidingExpiration = new TimeSpan(slidingExpirationTicks.Value);
}
}
protected virtual void Refresh(
string key,
DateTimeOffset? absExpr,
TimeSpan? sldExpr)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
// Note Refresh has no effect if there is just an absolute expiration (or neither).
TimeSpan? expr = null;
if (sldExpr.HasValue)
{
if (absExpr.HasValue)
{
var relExpr = absExpr.Value - DateTimeOffset.Now;
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
}
else
{
expr = sldExpr;
}
Cache.KeyExpire(Instance + key, expr);
// TODO: Error handling
}
}
protected virtual async Task RefreshAsync(
string key,
DateTimeOffset? absExpr,
TimeSpan? sldExpr,
CancellationToken token = default)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
token.ThrowIfCancellationRequested();
// Note Refresh has no effect if there is just an absolute expiration (or neither).
TimeSpan? expr = null;
if (sldExpr.HasValue)
{
if (absExpr.HasValue)
{
var relExpr = absExpr.Value - DateTimeOffset.Now;
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
}
else
{
expr = sldExpr;
}
await Cache.KeyExpireAsync(Instance + key, expr);
// TODO: Error handling
}
}
protected static long? GetExpirationInSeconds(
DateTimeOffset creationTime,
DateTimeOffset? absoluteExpiration,
DistributedCacheEntryOptions options)
{
if (absoluteExpiration.HasValue && options.SlidingExpiration.HasValue)
{
return (long) Math.Min(
(absoluteExpiration.Value - creationTime).TotalSeconds,
options.SlidingExpiration.Value.TotalSeconds);
}
else if (absoluteExpiration.HasValue)
{
return (long) (absoluteExpiration.Value - creationTime).TotalSeconds;
}
else if (options.SlidingExpiration.HasValue)
{
return (long) options.SlidingExpiration.Value.TotalSeconds;
}
return null;
}
protected static DateTimeOffset? GetAbsoluteExpiration(
DateTimeOffset creationTime,
DistributedCacheEntryOptions options)
{
if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration <= creationTime)
{
throw new ArgumentOutOfRangeException(
nameof(DistributedCacheEntryOptions.AbsoluteExpiration),
options.AbsoluteExpiration.Value,
"The absolute expiration value must be in the future.");
}
var absoluteExpiration = options.AbsoluteExpiration;
if (options.AbsoluteExpirationRelativeToNow.HasValue)
{
absoluteExpiration = creationTime + options.AbsoluteExpirationRelativeToNow;
}
return absoluteExpiration;
}
public virtual void Dispose()
{
Connection?.Close();
}
}
}

70
framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs

@ -1,70 +0,0 @@
// This software is part of the DOTNET extensions
// Copyright (c) .NET Foundation and Contributors
// https://dotnet.microsoft.com/
//
// All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System.Threading.Tasks;
using StackExchange.Redis;
namespace Microsoft.Extensions.Caching.StackExchangeRedis
{
internal static class RedisExtensions
{
private const string HmGetScript = (@"return redis.call('HMGET', KEYS[1], unpack(ARGV))");
internal static RedisValue[] HashMemberGet(this IDatabase cache, string key, params string[] members)
{
var result = cache.ScriptEvaluate(
HmGetScript,
new RedisKey[] { key },
GetRedisMembers(members));
// TODO: Error checking?
return (RedisValue[])result;
}
internal static async Task<RedisValue[]> HashMemberGetAsync(
this IDatabase cache,
string key,
params string[] members)
{
var result = await cache.ScriptEvaluateAsync(
HmGetScript,
new RedisKey[] { key },
GetRedisMembers(members)).ConfigureAwait(false);
// TODO: Error checking?
return (RedisValue[])result;
}
private static RedisValue[] GetRedisMembers(params string[] members)
{
var redisMembers = new RedisValue[members.Length];
for (int i = 0; i < members.Length; i++)
{
redisMembers[i] = (RedisValue)members[i];
}
return redisMembers;
}
}
}

116
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
@ -14,9 +15,75 @@ namespace Volo.Abp.Caching.StackExchangeRedis
[DisableConventionalRegistration]
public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems
{
protected static readonly string SetScript;
protected static readonly string AbsoluteExpirationKey;
protected static readonly string SlidingExpirationKey;
protected static readonly string DataKey;
protected static readonly long NotPresent;
private static readonly FieldInfo RedisDatabaseField;
private static readonly MethodInfo ConnectMethod;
private static readonly MethodInfo ConnectAsyncMethod;
private static readonly MethodInfo MapMetadataMethod;
private static readonly MethodInfo GetAbsoluteExpirationMethod;
private static readonly MethodInfo GetExpirationInSecondsMethod;
protected IDatabase RedisDatabase => GetRedisDatabase();
private IDatabase _redisDatabase;
protected string Instance { get; }
static AbpRedisCache()
{
var type = typeof(RedisCache);
RedisDatabaseField = type.GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic);
ConnectMethod = type.GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic);
ConnectAsyncMethod = type.GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic);
MapMetadataMethod = type.GetMethod("MapMetadata", BindingFlags.Instance | BindingFlags.NonPublic);
GetAbsoluteExpirationMethod = type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic);
GetExpirationInSecondsMethod = type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic);
SetScript = type.GetField("SetScript", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).To<int>();
}
public AbpRedisCache(IOptions<RedisCacheOptions> optionsAccessor)
: base(optionsAccessor)
{
Instance = optionsAccessor.Value.InstanceName ?? string.Empty;
}
protected virtual void Connect()
{
if (GetRedisDatabase() != null)
{
return;
}
ConnectMethod.Invoke(this, Array.Empty<object>());
}
protected virtual Task ConnectAsync(CancellationToken token = default)
{
if (GetRedisDatabase() != null)
{
return Task.CompletedTask;
}
return (Task) ConnectAsyncMethod.Invoke(this, new object[] {token});
}
public byte[][] GetMany(
@ -68,12 +135,12 @@ namespace Volo.Abp.Caching.StackExchangeRedis
if (getData)
{
results = Cache.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey, DataKey);
}
else
{
results = Cache.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey);
}
@ -96,12 +163,12 @@ namespace Volo.Abp.Caching.StackExchangeRedis
if (getData)
{
results = await Cache.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey, DataKey);
}
else
{
results = await Cache.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey);
}
@ -110,13 +177,14 @@ namespace Volo.Abp.Caching.StackExchangeRedis
return bytes;
}
private Task[] PipelineRefreshManyAndOutData(
protected virtual Task[] PipelineRefreshManyAndOutData(
string[] keys,
RedisValue[][] results,
out byte[][] bytes)
{
bytes = new byte[keys.Length][];
var tasks = new Task[keys.Length];
for (var i = 0; i < keys.Length; i++)
{
if (results[i].Length >= 2)
@ -137,7 +205,7 @@ namespace Volo.Abp.Caching.StackExchangeRedis
expr = sldExpr;
}
tasks[i] = Cache.KeyExpireAsync(keys[i], expr);
tasks[i] = RedisDatabase.KeyExpireAsync(keys[i], expr);
}
else
{
@ -158,7 +226,7 @@ namespace Volo.Abp.Caching.StackExchangeRedis
return tasks;
}
private Task[] PipelineSetMany(
protected virtual Task[] PipelineSetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options)
{
@ -172,7 +240,7 @@ namespace Volo.Abp.Caching.StackExchangeRedis
for (var i = 0; i < itemArray.Length; i++)
{
tasks[i] = Cache.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + itemArray[i].Key},
tasks[i] = RedisDatabase.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + itemArray[i].Key},
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
@ -184,12 +252,40 @@ namespace Volo.Abp.Caching.StackExchangeRedis
return tasks;
}
protected virtual void MapMetadata(
RedisValue[] results,
out DateTimeOffset? absoluteExpiration,
out TimeSpan? slidingExpiration)
{
var parameters = new object[] {results, null, null};
MapMetadataMethod.Invoke(this, parameters);
absoluteExpiration = (DateTimeOffset?) parameters[1];
slidingExpiration = (TimeSpan?) parameters[2];
}
protected virtual long? GetExpirationInSeconds(
DateTimeOffset creationTime,
DateTimeOffset? absoluteExpiration,
DistributedCacheEntryOptions options)
{
return (long?) GetExpirationInSecondsMethod.Invoke(null,
new object[] {creationTime, absoluteExpiration, options});
}
protected virtual DateTimeOffset? GetAbsoluteExpiration(
DateTimeOffset creationTime,
DistributedCacheEntryOptions options)
{
return (DateTimeOffset?) GetAbsoluteExpirationMethod.Invoke(null, new object[] {creationTime, options});
}
private IDatabase GetRedisDatabase()
{
if (_redisDatabase == null)
{
_redisDatabase = RedisDatabaseField.GetValue(this) as IDatabase;
_redisDatabase = RedisDatabaseField.GetValue(this) as IDatabase;
}
return _redisDatabase;

Loading…
Cancel
Save