FreeRedis
ClientSideCaching.cs
using FreeRedis.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace FreeRedis
{
public clast ClientSideCachingOptions
{
public int Capacity { get; set; }
///
/// true: cache
///
public Func KeyFilter { get; set; }
///
/// true: expired
///
public Func CheckExpired { get; set; }
}
public static clast ClientSideCachingExtensions
{
public static void UseClientSideCaching(this RedisClient cli, ClientSideCachingOptions options)
{
new ClientSideCachingContext(cli, options)
.Start();
}
clast ClientSideCachingContext
{
readonly RedisClient _cli;
readonly ClientSideCachingOptions _options;
IPubSubSubscriber _sub;
Dictionary _clusterTrackings = new Dictionary();
object _clusterTrackingsLock = new object();
clast ClusterTrackingInfo
{
public RedisClient Client;
public IPubSubSubscriber PubSub;
}
public ClientSideCachingContext(RedisClient cli, ClientSideCachingOptions options)
{
_cli = cli;
_options = options ?? new ClientSideCachingOptions();
}
public void Start()
{
_sub = _cli.Subscribe("__redis__:invalidate", InValidate) as IPubSubSubscriber;
_cli.Interceptors.Add(() => new MemoryCacheAop(this));
_cli.Unavailable += (_, e) =>
{
lock (_dictLock) _dictSort.Clear();
_dict.Clear();
lock (_clusterTrackingsLock)
{
if (_clusterTrackings.TryGetValue(e.Pool.Key, out var localTracking))
{
_clusterTrackings.Remove(e.Pool.Key);
localTracking.Client.Dispose();
}
}
};
_cli.Connected += (_, e) =>
{
var redirectId = GetOrAddClusterTrackingRedirectId(e.Host, e.Pool);
e.Client.ClientTracking(true, redirectId, null, false, false, false, false);
};
}
long GetOrAddClusterTrackingRedirectId(string host, RedisClientPool pool)
{
var poolkey = pool.Key;
//return _sub.RedisSocket.ClientId;
if (_cli.Adapter.UseType != RedisClient.UseType.Cluster) return _sub.RedisSocket.ClientId;
ClusterTrackingInfo tracking = null;
lock (_clusterTrackingsLock)
{
if (_clusterTrackings.TryGetValue(poolkey, out tracking) == false)
{
tracking = new ClusterTrackingInfo
{
Client = new RedisClient(new ConnectionStringBuilder
{
Host = host,
MaxPoolSize = 1,
Pastword = pool._policy._connectionStringBuilder.Pastword,
ClientName = "client_tracking_redirect",
ConnectTimeout = pool._policy._connectionStringBuilder.ConnectTimeout,
IdleTimeout = pool._policy._connectionStringBuilder.IdleTimeout,
ReceiveTimeout = pool._policy._connectionStringBuilder.ReceiveTimeout,
SendTimeout = pool._policy._connectionStringBuilder.SendTimeout,
Ssl = pool._policy._connectionStringBuilder.Ssl,
User = pool._policy._connectionStringBuilder.User,
})
};
tracking.Client.Unavailable += (_, e) =>
{
lock (_dictLock) _dictSort.Clear();
_dict.Clear();
lock (_clusterTrackingsLock)
{
if (_clusterTrackings.TryGetValue(e.Pool.Key, out var localTracking))
{
_clusterTrackings.Remove(e.Pool.Key);
localTracking.Client.Dispose();
}
}
};
tracking.PubSub = tracking.Client.Subscribe("__redis__:invalidate", InValidate) as IPubSubSubscriber;
_clusterTrackings.Add(poolkey, tracking);
}
}
return tracking.PubSub.RedisSocket.ClientId;
}
void InValidate(string chan, object msg)
{
if (msg == null)
{
//flushall
lock (_dictLock) _dictSort.Clear();
_dict.Clear();
return;
}
var keys = msg as object[];
if (keys != null)
{
foreach (var key in keys)
RemoveCache(string.Concat(key));
}
}
static readonly DateTime _dt2020 = new DateTime(2020, 1, 1);
static long GetTime() => (long)DateTime.Now.Subtract(_dt2020).TotalSeconds;
///
/// key -> Type(string|byte[]|clast) -> value
///
readonly ConcurrentDictionary _dict = new ConcurrentDictionary();
readonly SortedSet _dictSort = new SortedSet();
readonly object _dictLock = new object();
bool TryGetCacheValue(string key, Type valueType, out object value)
{
if (_dict.TryGetValue(key, out var trydictval) && trydictval.Values.TryGetValue(valueType, out var tryval)
//&& DateTime.Now.Subtract(_dt2020.AddSeconds(tryval.SetTime)) < TimeSpan.FromMinutes(5)
)
{
if (_options.CheckExpired?.Invoke(key, _dt2020.AddSeconds(tryval.SetTime)) == true)
{
RemoveCache(key);
value = null;
return false;
}
var time = GetTime();
if (_options.Capacity > 0)
{
lock (_dictLock)
{
_dictSort.Remove($"{trydictval.GetTime.ToString("X").PadLeft(16, '0')}{key}");
_dictSort.Add($"{time.ToString("X").PadLeft(16, '0')}{key}");
}
}
Interlocked.Exchange(ref trydictval.GetTime, time);
value = tryval.Value;
return true;
}
value = null;
return false;
}
void SetCacheValue(string command, string key, Type valueType, object value)
{
_dict.GetOrAdd(key, keyTmp =>
{
var time = GetTime();
if (_options.Capacity > 0)
{
string removeKey = null;
lock (_dictLock)
{
if (_dictSort.Count >= _options.Capacity) removeKey = _dictSort.First().Substring(16);
_dictSort.Add($"{time.ToString("X").PadLeft(16, '0')}{key}");
}
if (removeKey != null)
RemoveCache(removeKey);
}
return new DictValue(command, time);
}).Values
.AddOrUpdate(valueType, new DictValue.ObjectValue(value), (oldkey, oldval) => new DictValue.ObjectValue(value));
}
void RemoveCache(params string[] keys)
{
if (keys?.Any() != true) return;
foreach (var key in keys)
{
if (_dict.TryRemove(key, out var old))
{
if (_options.Capacity > 0)
{
lock (_dictLock)
{
_dictSort.Remove($"{old.GetTime.ToString("X").PadLeft(16, '0')}{key}");
}
}
}
}
}
clast DictValue
{
public readonly ConcurrentDictionary Values = new ConcurrentDictionary();
public readonly string Command;
public long GetTime;
public DictValue(string command, long gettime)
{
this.Command = command;
this.GetTime = gettime;
}
public clast ObjectValue
{
public readonly object Value;
public readonly long SetTime = (long)DateTime.Now.Subtract(_dt2020).TotalSeconds;
public ObjectValue(object value) => this.Value = value;
}
}
clast MemoryCacheAop : IInterceptor
{
ClientSideCachingContext _cscc;
public MemoryCacheAop(ClientSideCachingContext cscc)
{
_cscc = cscc;
}
bool _iscached = false;
public void Before(InterceptorBeforeEventArgs args)
{
switch (args.Command._command)
{
case "GET":
if (_cscc.TryGetCacheValue(args.Command.GetKey(0), args.ValueType, out var getval))
{
args.Value = getval;
_iscached = true;
}
break;
case "MGET":
var mgetValType = args.ValueType.GetElementType();
var mgetKeys = args.Command._keyIndexes.Select((item, index) => args.Command.GetKey(index)).ToArray();
var mgetVals = mgetKeys.Select(a => _cscc.TryGetCacheValue(a, mgetValType, out var mgetval) ?
new DictGetResult { Value = mgetval, Exists = true } : new DictGetResult { Value = null, Exists = false })
.Where(a => a.Exists).Select(a => a.Value).ToArray();
if (mgetVals.Length == mgetKeys.Length)
{
args.Value = args.ValueType.FromObject(mgetVals);
_iscached = true;
}
break;
}
}
public void After(InterceptorAfterEventArgs args)
{
switch (args.Command._command)
{
case "GET":
if (_iscached == false && args.Exception == null)
{
var getkey = args.Command.GetKey(0);
if (_cscc._options.KeyFilter?.Invoke(getkey) != false)
_cscc.SetCacheValue(args.Command._command, getkey, args.ValueType, args.Value);
}
break;
case "MGET":
if (_iscached == false && args.Exception == null)
{
if (args.Value is Array valueArr)
{
var valueArrElementType = args.ValueType.GetElementType();
var sourceArrLen = valueArr.Length;
for (var a = 0; a < sourceArrLen; a++)
{
var getkey = args.Command.GetKey(a);
if (_cscc._options.KeyFilter?.Invoke(getkey) != false)
_cscc.SetCacheValue("GET", getkey, valueArrElementType, valueArr.GetValue(a));
}
}
}
break;
default:
if (args.Command._keyIndexes.Any())
{
var cmdset = CommandSets.Get(args.Command._command);
if (cmdset != null &&
(cmdset.Flag & CommandSets.ServerFlag.write) == CommandSets.ServerFlag.write &&
(cmdset.Tag & CommandSets.ServerTag.write) == CommandSets.ServerTag.write &&
(cmdset.Tag & CommandSets.ServerTag.@string) == CommandSets.ServerTag.@string)
{
_cscc.RemoveCache(args.Command._keyIndexes.Select((item, index) => args.Command.GetKey(index)).ToArray());
}
}
break;
}
}
clast DictGetResult
{
public object Value;
public bool Exists;
}
}
}
}
}