From 013782436101d7c5a7feee866e900b9819831256 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 26 Jun 2020 18:03:25 +0800 Subject: [PATCH 1/4] Implement the ICacheSupportsMultipleItems for the AbpRedisCache --- .../Caching/StackExchangeRedis/RedisCache.cs | 496 ++++++++++++++++++ .../StackExchangeRedis/RedisExtensions.cs | 70 +++ .../StackExchangeRedis/AbpRedisCache.cs | 187 ++++++- .../StackExchangeRedis/AbpRedisExtensions.cs | 42 ++ 4 files changed, 772 insertions(+), 23 deletions(-) create mode 100644 framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs create mode 100644 framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs create mode 100644 framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs new file mode 100644 index 0000000000..32cc85011b --- /dev/null +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs @@ -0,0 +1,496 @@ +// 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 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 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 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(); + } + } +} diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs new file mode 100644 index 0000000000..c83c8df7ff --- /dev/null +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs @@ -0,0 +1,70 @@ +// 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 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; + } + } +} diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs index a5236022f3..e02619bc6e 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs @@ -1,7 +1,9 @@ using System; -using System.Reflection; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Options; using StackExchange.Redis; @@ -10,40 +12,179 @@ using Volo.Abp.DependencyInjection; namespace Volo.Abp.Caching.StackExchangeRedis { [DisableConventionalRegistration] - public class AbpRedisCache : RedisCache + public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems { - private static readonly FieldInfo RedisDatabaseField; - private static readonly MethodInfo ConnectMethod; - private static readonly MethodInfo ConnectAsyncMethod; + public AbpRedisCache(IOptions optionsAccessor) + : base(optionsAccessor) + { + } - protected IDatabase RedisDatabase => RedisDatabaseField.GetValue(this) as IDatabase; + public byte[][] GetMany( + IEnumerable keys) + { + keys = Check.NotNull(keys, nameof(keys)); - static AbpRedisCache() + return GetAndRefreshMany(keys, true); + } + + public async Task GetManyAsync( + IEnumerable keys, + CancellationToken token = default) { - RedisDatabaseField = typeof(RedisCache) - .GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic); - - ConnectMethod = typeof(RedisCache) - .GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic); - - ConnectAsyncMethod = typeof(RedisCache) - .GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic); + keys = Check.NotNull(keys, nameof(keys)); + + return await GetAndRefreshManyAsync(keys, true, token); } - public AbpRedisCache(IOptions optionsAccessor) - : base(optionsAccessor) + public void SetMany( + IEnumerable> items, + DistributedCacheEntryOptions options) + { + Connect(); + + Task.WaitAll(PipelineSetMany(items, options)); + } + + public async Task SetManyAsync( + IEnumerable> items, + DistributedCacheEntryOptions options, + CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + await ConnectAsync(token); + + await Task.WhenAll(PipelineSetMany(items, options)); + } + + protected virtual byte[][] GetAndRefreshMany( + IEnumerable keys, + bool getData) + { + Connect(); + + var keyArray = keys.Select(key => Instance + key).ToArray(); + RedisValue[][] results; + + if (getData) + { + results = Cache.HashMemberGetMany(keyArray, AbsoluteExpirationKey, + SlidingExpirationKey, DataKey); + } + else + { + results = Cache.HashMemberGetMany(keyArray, AbsoluteExpirationKey, + SlidingExpirationKey); + } + + Task.WaitAll(GetAndRefreshMany(keyArray, results, out var bytes)); + + return bytes; + } + + protected virtual async Task GetAndRefreshManyAsync( + IEnumerable keys, + bool getData, + CancellationToken token = default) { - + token.ThrowIfCancellationRequested(); + + await ConnectAsync(token); + + var keyArray = keys.Select(key => Instance + key).ToArray(); + RedisValue[][] results; + + if (getData) + { + results = await Cache.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey, + SlidingExpirationKey, DataKey); + } + else + { + results = await Cache.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey, + SlidingExpirationKey); + } + + await Task.WhenAll(GetAndRefreshMany(keyArray, results, out var bytes)); + + return bytes; } - protected virtual void Connect() + private Task[] GetAndRefreshMany(string[] keys, RedisValue[][] results, out byte[][] bytes) { - ConnectMethod.Invoke(this, Array.Empty()); + bytes = new byte[keys.Length][]; + var tasks = new Task[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + if (results[i].Length >= 2) + { + MapMetadata(results[i], out DateTimeOffset? absExpr, out TimeSpan? sldExpr); + tasks[i] = PipelineRefresh(keys[i], absExpr, sldExpr); + } + + if (results[i].Length >= 3 && results[i][2].HasValue) + { + bytes[i] = results[i][2]; + } + else + { + bytes[i] = null; + } + } + + return tasks; + } + + private Task PipelineRefresh( + string key, + DateTimeOffset? absExpr, + TimeSpan? sldExpr) + { + if (sldExpr.HasValue) + { + TimeSpan? expr; + + if (absExpr.HasValue) + { + var relExpr = absExpr.Value - DateTimeOffset.Now; + expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; + } + else + { + expr = sldExpr; + } + + return Cache.KeyExpireAsync(key, expr); + } + + return Task.CompletedTask; } - protected virtual Task ConnectAsync(CancellationToken token = default) + private Task[] PipelineSetMany( + IEnumerable> items, + DistributedCacheEntryOptions options) { - return (Task) ConnectAsyncMethod.Invoke(this, new object[] {token}); + items = Check.NotNull(items, nameof(items)); + options = Check.NotNull(options, nameof(options)); + + var itemArray = items.ToArray(); + var tasks = new Task[itemArray.Length]; + var creationTime = DateTimeOffset.UtcNow; + var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); + + for (var i = 0; i < itemArray.Length; i++) + { + tasks[i] = Cache.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + itemArray[i].Key}, + new RedisValue[] + { + absoluteExpiration?.Ticks ?? NotPresent, + options.SlidingExpiration?.Ticks ?? NotPresent, + GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, + itemArray[i].Value + }); + } + + return tasks; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs new file mode 100644 index 0000000000..d3466d2222 --- /dev/null +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace Volo.Abp.Caching.StackExchangeRedis +{ + public static class AbpRedisExtensions + { + private const string HmGetManyScript = (@" + local res = {}; + for i , key in ipairs(KEYS) do + res[#res+1] = redis.call('HMGET', key,unpack(ARGV)) + end + return res"); + + public static RedisValue[][] HashMemberGetMany( + this IDatabase cache, + string[] keys, + params string[] members) + { + var results = cache.ScriptEvaluate( + HmGetManyScript, + keys.Select(key => (RedisKey) key).ToArray(), + members.Select(member => (RedisValue) member).ToArray()); + + return ((RedisResult[]) results).Select(result => (RedisValue[]) result).ToArray(); + } + + public static async Task HashMemberGetManyAsync( + this IDatabase cache, + string[] keys, + params string[] members) + { + var results = await cache.ScriptEvaluateAsync( + HmGetManyScript, + keys.Select(key => (RedisKey) key).ToArray(), + members.Select(member => (RedisValue) member).ToArray()); + + return ((RedisResult[]) results).Select(result => (RedisValue[]) result).ToArray(); + } + } +} From 58c17ceae04f4edad87cf0d5cad4da65ca8c8eab Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 26 Jun 2020 19:29:24 +0800 Subject: [PATCH 2/4] Rename GetAndRefreshMany to PipelineRefreshManyAndOutData --- .../StackExchangeRedis/AbpRedisCache.cs | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs index e02619bc6e..6d59b6fc64 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs @@ -77,7 +77,7 @@ namespace Volo.Abp.Caching.StackExchangeRedis SlidingExpirationKey); } - Task.WaitAll(GetAndRefreshMany(keyArray, results, out var bytes)); + Task.WaitAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes)); return bytes; } @@ -105,12 +105,15 @@ namespace Volo.Abp.Caching.StackExchangeRedis SlidingExpirationKey); } - await Task.WhenAll(GetAndRefreshMany(keyArray, results, out var bytes)); + await Task.WhenAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes)); return bytes; } - private Task[] GetAndRefreshMany(string[] keys, RedisValue[][] results, out byte[][] bytes) + private Task[] PipelineRefreshManyAndOutData( + string[] keys, + RedisValue[][] results, + out byte[][] bytes) { bytes = new byte[keys.Length][]; var tasks = new Task[keys.Length]; @@ -119,7 +122,27 @@ namespace Volo.Abp.Caching.StackExchangeRedis if (results[i].Length >= 2) { MapMetadata(results[i], out DateTimeOffset? absExpr, out TimeSpan? sldExpr); - tasks[i] = PipelineRefresh(keys[i], absExpr, sldExpr); + + if (sldExpr.HasValue) + { + TimeSpan? expr; + + if (absExpr.HasValue) + { + var relExpr = absExpr.Value - DateTimeOffset.Now; + expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; + } + else + { + expr = sldExpr; + } + + tasks[i] = Cache.KeyExpireAsync(keys[i], expr); + } + else + { + tasks[i] = Task.CompletedTask; + } } if (results[i].Length >= 3 && results[i][2].HasValue) @@ -135,31 +158,6 @@ namespace Volo.Abp.Caching.StackExchangeRedis return tasks; } - private Task PipelineRefresh( - string key, - DateTimeOffset? absExpr, - TimeSpan? sldExpr) - { - if (sldExpr.HasValue) - { - TimeSpan? expr; - - if (absExpr.HasValue) - { - var relExpr = absExpr.Value - DateTimeOffset.Now; - expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; - } - else - { - expr = sldExpr; - } - - return Cache.KeyExpireAsync(key, expr); - } - - return Task.CompletedTask; - } - private Task[] PipelineSetMany( IEnumerable> items, DistributedCacheEntryOptions options) From 096f8f075034945963c004a73572edb9d8257ba4 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 26 Jun 2020 20:19:02 +0800 Subject: [PATCH 3/4] Use pipeline instead of lua script --- .../StackExchangeRedis/AbpRedisExtensions.cs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs index d3466d2222..ad077a7440 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs @@ -6,24 +6,26 @@ namespace Volo.Abp.Caching.StackExchangeRedis { public static class AbpRedisExtensions { - private const string HmGetManyScript = (@" - local res = {}; - for i , key in ipairs(KEYS) do - res[#res+1] = redis.call('HMGET', key,unpack(ARGV)) - end - return res"); - public static RedisValue[][] HashMemberGetMany( this IDatabase cache, string[] keys, params string[] members) { - var results = cache.ScriptEvaluate( - HmGetManyScript, - keys.Select(key => (RedisKey) key).ToArray(), - members.Select(member => (RedisValue) member).ToArray()); + var tasks = new Task[keys.Length]; + var fields = members.Select(member => (RedisValue) member).ToArray(); + var results = new RedisValue[keys.Length][]; + + for (var i = 0; i < keys.Length; i++) + { + tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields); + } - return ((RedisResult[]) results).Select(result => (RedisValue[]) result).ToArray(); + for (var i = 0; i < tasks.Length; i++) + { + results[i] = cache.Wait(tasks[i]); + } + + return results; } public static async Task HashMemberGetManyAsync( @@ -31,12 +33,15 @@ namespace Volo.Abp.Caching.StackExchangeRedis string[] keys, params string[] members) { - var results = await cache.ScriptEvaluateAsync( - HmGetManyScript, - keys.Select(key => (RedisKey) key).ToArray(), - members.Select(member => (RedisValue) member).ToArray()); + var tasks = new Task[keys.Length]; + var fields = members.Select(member => (RedisValue) member).ToArray(); + + for (var i = 0; i < keys.Length; i++) + { + tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields); + } - return ((RedisResult[]) results).Select(result => (RedisValue[]) result).ToArray(); + return await Task.WhenAll(tasks); } } } From 018e7cbd0807b5bda5b23654de2a7ab7d3d8a707 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Sat, 27 Jun 2020 10:23:56 +0800 Subject: [PATCH 4/4] Refactored --- .../Caching/StackExchangeRedis/RedisCache.cs | 496 ------------------ .../StackExchangeRedis/RedisExtensions.cs | 70 --- .../StackExchangeRedis/AbpRedisCache.cs | 116 +++- 3 files changed, 106 insertions(+), 576 deletions(-) delete mode 100644 framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs delete mode 100644 framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs deleted file mode 100644 index 32cc85011b..0000000000 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisCache.cs +++ /dev/null @@ -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 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 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 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(); - } - } -} diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs deleted file mode 100644 index c83c8df7ff..0000000000 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Microsoft/Extensions/Caching/StackExchangeRedis/RedisExtensions.cs +++ /dev/null @@ -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 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; - } - } -} diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs index 86fd3441ab..c3cb34f30c 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs +++ b/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(); + } + public AbpRedisCache(IOptions optionsAccessor) : base(optionsAccessor) { + Instance = optionsAccessor.Value.InstanceName ?? string.Empty; + } + + protected virtual void Connect() + { + if (GetRedisDatabase() != null) + { + return; + } + + ConnectMethod.Invoke(this, Array.Empty()); + } + + 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> 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;