|
|
|
@ -1,4 +1,5 @@ |
|
|
|
using System; |
|
|
|
using System.Buffers; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Linq; |
|
|
|
using System.Reflection; |
|
|
|
@ -16,21 +17,21 @@ namespace Volo.Abp.Caching.StackExchangeRedis; |
|
|
|
[DisableConventionalRegistration] |
|
|
|
public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
{ |
|
|
|
protected static readonly string AbsoluteExpirationKey; |
|
|
|
protected static readonly string SlidingExpirationKey; |
|
|
|
protected static readonly string DataKey; |
|
|
|
protected static readonly long NotPresent; |
|
|
|
protected static readonly RedisValue[] HashMembersAbsoluteExpirationSlidingExpirationData; |
|
|
|
protected static readonly RedisValue[] HashMembersAbsoluteExpirationSlidingExpiration; |
|
|
|
|
|
|
|
private readonly static FieldInfo SetScriptField; |
|
|
|
private readonly static FieldInfo RedisDatabaseField; |
|
|
|
private readonly static MethodInfo ConnectMethod; |
|
|
|
private readonly static MethodInfo ConnectAsyncMethod; |
|
|
|
private readonly static MethodInfo MapMetadataMethod; |
|
|
|
private readonly static MethodInfo GetAbsoluteExpirationMethod; |
|
|
|
private readonly static MethodInfo GetExpirationInSecondsMethod; |
|
|
|
private readonly static MethodInfo OnRedisErrorMethod; |
|
|
|
protected readonly static string AbsoluteExpirationKey; |
|
|
|
protected readonly static string SlidingExpirationKey; |
|
|
|
protected readonly static string DataKey; |
|
|
|
protected readonly static long NotPresent; |
|
|
|
protected readonly static RedisValue[] HashMembersAbsoluteExpirationSlidingExpirationData; |
|
|
|
protected readonly static RedisValue[] HashMembersAbsoluteExpirationSlidingExpiration; |
|
|
|
|
|
|
|
protected readonly static FieldInfo RedisDatabaseField; |
|
|
|
protected readonly static MethodInfo ConnectMethod; |
|
|
|
protected readonly static MethodInfo ConnectAsyncMethod; |
|
|
|
protected readonly static MethodInfo MapMetadataMethod; |
|
|
|
protected readonly static MethodInfo GetAbsoluteExpirationMethod; |
|
|
|
protected readonly static MethodInfo GetExpirationInSecondsMethod; |
|
|
|
protected readonly static MethodInfo OnRedisErrorMethod; |
|
|
|
protected readonly static MethodInfo RecycleMethodInfo; |
|
|
|
|
|
|
|
protected RedisKey InstancePrefix { get; } |
|
|
|
|
|
|
|
@ -40,8 +41,6 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
|
|
|
|
RedisDatabaseField = Check.NotNull(type.GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic), nameof(RedisDatabaseField)); |
|
|
|
|
|
|
|
SetScriptField = Check.NotNull(type.GetField("_setScript", BindingFlags.Instance | BindingFlags.NonPublic), nameof(SetScriptField)); |
|
|
|
|
|
|
|
ConnectMethod = Check.NotNull(type.GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic), nameof(ConnectMethod)); |
|
|
|
|
|
|
|
ConnectAsyncMethod = Check.NotNull(type.GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic), nameof(ConnectAsyncMethod)); |
|
|
|
@ -51,9 +50,11 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
GetAbsoluteExpirationMethod = Check.NotNull(type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic), nameof(GetAbsoluteExpirationMethod)); |
|
|
|
|
|
|
|
GetExpirationInSecondsMethod = Check.NotNull(type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic), nameof(GetExpirationInSecondsMethod)); |
|
|
|
|
|
|
|
|
|
|
|
OnRedisErrorMethod = Check.NotNull(type.GetMethod("OnRedisError", BindingFlags.Instance | BindingFlags.NonPublic), nameof(OnRedisErrorMethod)); |
|
|
|
|
|
|
|
RecycleMethodInfo = Check.NotNull(type.GetMethod("Recycle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static), nameof(RecycleMethodInfo)); |
|
|
|
|
|
|
|
AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!.ToString()!; |
|
|
|
|
|
|
|
SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!.ToString()!; |
|
|
|
@ -61,9 +62,9 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!.ToString()!; |
|
|
|
|
|
|
|
NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!.To<int>(); |
|
|
|
|
|
|
|
|
|
|
|
HashMembersAbsoluteExpirationSlidingExpirationData = [AbsoluteExpirationKey, SlidingExpirationKey, DataKey]; |
|
|
|
|
|
|
|
|
|
|
|
HashMembersAbsoluteExpirationSlidingExpiration = [AbsoluteExpirationKey, SlidingExpirationKey]; |
|
|
|
} |
|
|
|
|
|
|
|
@ -78,7 +79,7 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual IDatabase Connect() |
|
|
|
{ |
|
|
|
{ |
|
|
|
return (IDatabase)ConnectMethod.Invoke(this, Array.Empty<object>())!; |
|
|
|
} |
|
|
|
|
|
|
|
@ -87,32 +88,36 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
return await (ValueTask<IDatabase>)ConnectAsyncMethod.Invoke(this, new object[] { token })!; |
|
|
|
} |
|
|
|
|
|
|
|
public byte[]?[] GetMany( |
|
|
|
IEnumerable<string> keys) |
|
|
|
protected virtual void Recycle(byte[]? lease) |
|
|
|
{ |
|
|
|
RecycleMethodInfo.Invoke(this, new object[] { lease! }); |
|
|
|
} |
|
|
|
|
|
|
|
public byte[]?[] GetMany(IEnumerable<string> keys) |
|
|
|
{ |
|
|
|
keys = Check.NotNull(keys, nameof(keys)); |
|
|
|
|
|
|
|
return GetAndRefreshMany(keys, true); |
|
|
|
} |
|
|
|
|
|
|
|
public async Task<byte[]?[]> GetManyAsync( |
|
|
|
IEnumerable<string> keys, |
|
|
|
CancellationToken token = default) |
|
|
|
public async Task<byte[]?[]> GetManyAsync(IEnumerable<string> keys, CancellationToken token = default) |
|
|
|
{ |
|
|
|
keys = Check.NotNull(keys, nameof(keys)); |
|
|
|
|
|
|
|
return await GetAndRefreshManyAsync(keys, true, token); |
|
|
|
} |
|
|
|
|
|
|
|
public void SetMany( |
|
|
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
|
|
|
DistributedCacheEntryOptions options) |
|
|
|
public void SetMany(IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options) |
|
|
|
{ |
|
|
|
var cache = Connect(); |
|
|
|
|
|
|
|
try |
|
|
|
{ |
|
|
|
Task.WaitAll(PipelineSetMany(cache, items, options)); |
|
|
|
Task.WaitAll(PipelineSetMany(cache, items, options, out var leases)); |
|
|
|
foreach (var lease in leases) |
|
|
|
{ |
|
|
|
Recycle(lease); |
|
|
|
} |
|
|
|
} |
|
|
|
catch (Exception ex) |
|
|
|
{ |
|
|
|
@ -121,10 +126,7 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public async Task SetManyAsync( |
|
|
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
|
|
|
DistributedCacheEntryOptions options, |
|
|
|
CancellationToken token = default) |
|
|
|
public async Task SetManyAsync( IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options, CancellationToken token = default) |
|
|
|
{ |
|
|
|
token.ThrowIfCancellationRequested(); |
|
|
|
|
|
|
|
@ -132,7 +134,11 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
|
|
|
|
try |
|
|
|
{ |
|
|
|
await Task.WhenAll(PipelineSetMany(cache, items, options)); |
|
|
|
await Task.WhenAll(PipelineSetMany(cache, items, options, out var leases)); |
|
|
|
foreach (var lease in leases) |
|
|
|
{ |
|
|
|
Recycle(lease); |
|
|
|
} |
|
|
|
} |
|
|
|
catch (Exception ex) |
|
|
|
{ |
|
|
|
@ -141,17 +147,14 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public void RefreshMany( |
|
|
|
IEnumerable<string> keys) |
|
|
|
public void RefreshMany(IEnumerable<string> keys) |
|
|
|
{ |
|
|
|
keys = Check.NotNull(keys, nameof(keys)); |
|
|
|
|
|
|
|
GetAndRefreshMany(keys, false); |
|
|
|
} |
|
|
|
|
|
|
|
public async Task RefreshManyAsync( |
|
|
|
IEnumerable<string> keys, |
|
|
|
CancellationToken token = default) |
|
|
|
public async Task RefreshManyAsync(IEnumerable<string> keys, CancellationToken token = default) |
|
|
|
{ |
|
|
|
keys = Check.NotNull(keys, nameof(keys)); |
|
|
|
|
|
|
|
@ -193,9 +196,7 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual byte[]?[] GetAndRefreshMany( |
|
|
|
IEnumerable<string> keys, |
|
|
|
bool getData) |
|
|
|
protected virtual byte[]?[] GetAndRefreshMany(IEnumerable<string> keys, bool getData) |
|
|
|
{ |
|
|
|
var cache = Connect(); |
|
|
|
|
|
|
|
@ -217,10 +218,7 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
return bytes; |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual async Task<byte[]?[]> GetAndRefreshManyAsync( |
|
|
|
IEnumerable<string> keys, |
|
|
|
bool getData, |
|
|
|
CancellationToken token = default) |
|
|
|
protected virtual async Task<byte[]?[]> GetAndRefreshManyAsync(IEnumerable<string> keys, bool getData, CancellationToken token = default) |
|
|
|
{ |
|
|
|
token.ThrowIfCancellationRequested(); |
|
|
|
|
|
|
|
@ -239,15 +237,11 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
OnRedisError(ex, cache); |
|
|
|
throw; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return bytes; |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual Task[] PipelineRefreshManyAndOutData( |
|
|
|
IDatabase cache, |
|
|
|
RedisKey[] keys, |
|
|
|
RedisValue[][] results, |
|
|
|
out byte[]?[] bytes) |
|
|
|
protected virtual Task[] PipelineRefreshManyAndOutData(IDatabase cache, RedisKey[] keys, RedisValue[][] results, out byte[]?[] bytes) |
|
|
|
{ |
|
|
|
bytes = new byte[keys.Length][]; |
|
|
|
var tasks = new Task[keys.Length]; |
|
|
|
@ -293,37 +287,36 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
return tasks; |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual Task[] PipelineSetMany( |
|
|
|
IDatabase cache, |
|
|
|
IEnumerable<KeyValuePair<string, byte[]>> items, |
|
|
|
DistributedCacheEntryOptions options) |
|
|
|
protected virtual Task[] PipelineSetMany(IDatabase cache, IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options, out List<byte[]?> leases) |
|
|
|
{ |
|
|
|
items = Check.NotNull(items, nameof(items)); |
|
|
|
options = Check.NotNull(options, nameof(options)); |
|
|
|
var tasks = new List<Task>(); |
|
|
|
leases = new List<byte[]?>(); |
|
|
|
|
|
|
|
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++) |
|
|
|
foreach (var item in items) |
|
|
|
{ |
|
|
|
tasks[i] = cache.ScriptEvaluateAsync(GetSetScript(), new RedisKey[] { InstancePrefix.Append(itemArray[i].Key) }, |
|
|
|
[ |
|
|
|
absoluteExpiration?.Ticks ?? NotPresent, |
|
|
|
options.SlidingExpiration?.Ticks ?? NotPresent, |
|
|
|
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, |
|
|
|
itemArray[i].Value |
|
|
|
]); |
|
|
|
var prefixedKey = InstancePrefix.Append(item.Key); |
|
|
|
var ttl = GetExpirationInSeconds(creationTime, absoluteExpiration, options); |
|
|
|
var fields = GetHashFields(Linearize(new ReadOnlySequence<byte>(item.Value), out var lease), absoluteExpiration, options.SlidingExpiration); |
|
|
|
leases.Add(lease); |
|
|
|
if (ttl is null) |
|
|
|
{ |
|
|
|
tasks.Add(cache.HashSetAsync(prefixedKey, fields)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
tasks.Add(cache.HashSetAsync(prefixedKey, fields)); |
|
|
|
tasks.Add(cache.KeyExpireAsync(prefixedKey, TimeSpan.FromSeconds(ttl.GetValueOrDefault()))); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return tasks; |
|
|
|
return tasks.ToArray(); |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual void MapMetadata( |
|
|
|
RedisValue[] results, |
|
|
|
out DateTimeOffset? absoluteExpiration, |
|
|
|
out TimeSpan? slidingExpiration) |
|
|
|
protected virtual void MapMetadata(RedisValue[] results, out DateTimeOffset? absoluteExpiration, out TimeSpan? slidingExpiration) |
|
|
|
{ |
|
|
|
var parameters = new object?[] { results, null, null }; |
|
|
|
MapMetadataMethod.Invoke(this, parameters); |
|
|
|
@ -332,36 +325,50 @@ public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems |
|
|
|
slidingExpiration = (TimeSpan?)parameters[2]; |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual long? GetExpirationInSeconds( |
|
|
|
DateTimeOffset creationTime, |
|
|
|
DateTimeOffset? absoluteExpiration, |
|
|
|
DistributedCacheEntryOptions options) |
|
|
|
protected virtual long? GetExpirationInSeconds(DateTimeOffset creationTime, DateTimeOffset? absoluteExpiration, DistributedCacheEntryOptions options) |
|
|
|
{ |
|
|
|
return (long?)GetExpirationInSecondsMethod.Invoke(null, |
|
|
|
new object?[] { creationTime, absoluteExpiration, options }); |
|
|
|
return (long?)GetExpirationInSecondsMethod.Invoke(null, new object?[] { creationTime, absoluteExpiration, options }); |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual DateTimeOffset? GetAbsoluteExpiration( |
|
|
|
DateTimeOffset creationTime, |
|
|
|
DistributedCacheEntryOptions options) |
|
|
|
protected virtual DateTimeOffset? GetAbsoluteExpiration(DateTimeOffset creationTime, DistributedCacheEntryOptions options) |
|
|
|
{ |
|
|
|
return (DateTimeOffset?)GetAbsoluteExpirationMethod.Invoke(null, new object[] { creationTime, options }); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected virtual void OnRedisError(Exception ex, IDatabase cache) |
|
|
|
{ |
|
|
|
OnRedisErrorMethod.Invoke(this, [ex, cache]); |
|
|
|
} |
|
|
|
|
|
|
|
private string GetSetScript() |
|
|
|
|
|
|
|
private static ReadOnlyMemory<byte> Linearize(in ReadOnlySequence<byte> value, out byte[]? lease) |
|
|
|
{ |
|
|
|
return SetScriptField.GetValue(this)!.ToString()!; |
|
|
|
// RedisValue only supports single-segment chunks; this will almost never be an issue, but
|
|
|
|
// on those rare occasions: use a leased array to harmonize things
|
|
|
|
if (value.IsSingleSegment) |
|
|
|
{ |
|
|
|
lease = null; |
|
|
|
return value.First; |
|
|
|
} |
|
|
|
var length = checked((int)value.Length); |
|
|
|
lease = ArrayPool<byte>.Shared.Rent(length); |
|
|
|
value.CopyTo(lease); |
|
|
|
return new(lease, 0, length); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static RedisValue[] GetHashFields(bool getData) |
|
|
|
{ |
|
|
|
return getData |
|
|
|
? HashMembersAbsoluteExpirationSlidingExpirationData |
|
|
|
: HashMembersAbsoluteExpirationSlidingExpiration; |
|
|
|
} |
|
|
|
|
|
|
|
private static HashEntry[] GetHashFields(RedisValue value, DateTimeOffset? absoluteExpiration, TimeSpan? slidingExpiration) |
|
|
|
{ |
|
|
|
return |
|
|
|
[ |
|
|
|
new HashEntry(AbsoluteExpirationKey, absoluteExpiration?.Ticks ?? NotPresent), |
|
|
|
new HashEntry(SlidingExpirationKey, slidingExpiration?.Ticks ?? NotPresent), |
|
|
|
new HashEntry(DataKey, value) |
|
|
|
]; |
|
|
|
} |
|
|
|
} |
|
|
|
|