Skip to content

Commit f57c555

Browse files
committed
finish client-side-caching
1 parent f2d2945 commit f57c555

File tree

7 files changed

+320
-141
lines changed

7 files changed

+320
-141
lines changed

examples/console_netcore31_client_side_caching/Program.cs

+32-73
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
using FreeRedis.Internal;
33
using Newtonsoft.Json;
44
using System;
5+
using System.Collections.Generic;
56
using System.Collections.Concurrent;
67
using System.Linq;
8+
using System.Threading;
79

810
namespace console_netcore31_client_side_caching
911
{
@@ -24,86 +26,43 @@ class Program
2426

2527
static void Main(string[] args)
2628
{
27-
cli.UseClientSideCaching();
28-
29-
cli.Set("Interceptor01", "123123");
30-
31-
var val1 = cli.Get("Interceptor01");
32-
var val2 = cli.Get("Interceptor01");
33-
var val3 = cli.Get("Interceptor01");
29+
cli.UseClientSideCaching(new ClientSideCachingOptions
30+
{
31+
//本地缓存的容量
32+
Capacity = 3,
33+
//过滤哪些键能被本地缓存
34+
KeyFilter = key => key.StartsWith("Interceptor"),
35+
//检查长期未使用的缓存
36+
CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(2)
37+
});
3438

35-
Console.ReadKey();
39+
cli.Set("Interceptor01", "123123"); //redis-server
3640

37-
var val4 = cli.Get("Interceptor01");
41+
var val1 = cli.Get("Interceptor01"); //redis-server
42+
var val2 = cli.Get("Interceptor01"); //本地
43+
var val3 = cli.Get("Interceptor01"); //断点等3秒,redis-server
3844

39-
Console.ReadKey();
40-
}
41-
}
42-
43-
static class MemoryCacheAopExtensions
44-
{
45-
public static void UseClientSideCaching(this RedisClient cli)
46-
{
47-
var sub = cli.Subscribe("__redis__:invalidate", (chan, msg) =>
48-
{
49-
var keys = msg as object[];
50-
foreach (var key in keys)
51-
{
52-
_dicStrings.TryRemove(string.Concat(key), out var old);
53-
}
54-
}) as IPubSubSubscriber;
45+
cli.Set("Interceptor01", "234567"); //redis-server
46+
var val4 = cli.Get("Interceptor01"); //redis-server
47+
var val5 = cli.Get("Interceptor01"); //本地
5548

56-
var context = new ClientSideCachingContext(cli, sub);
57-
cli.Interceptors.Add(() => new MemoryCacheAop());
58-
cli.Unavailable += (_, e) =>
59-
{
60-
_dicStrings.Clear();
61-
};
62-
cli.Connected += (_, e) =>
63-
{
64-
e.Client.ClientTracking(true, context._sub.RedisSocket.ClientId, null, false, false, false, false);
65-
};
66-
}
49+
var val6 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //redis-server
50+
var val7 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地
51+
var val8 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地
6752

68-
class ClientSideCachingContext
69-
{
70-
internal RedisClient _cli;
71-
internal IPubSubSubscriber _sub;
72-
public ClientSideCachingContext(RedisClient cli, IPubSubSubscriber sub)
73-
{
74-
_cli = cli;
75-
_sub = sub;
76-
}
77-
}
53+
cli.MSet("Interceptor01", "Interceptor01Value", "Interceptor02", "Interceptor02Value", "Interceptor03", "Interceptor03Value"); //redis-server
54+
var val9 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //redis-server
55+
var val10 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地
7856

79-
static ConcurrentDictionary<string, object> _dicStrings = new ConcurrentDictionary<string, object>();
80-
class MemoryCacheAop : IInterceptor
81-
{
82-
public void After(InterceptorAfterEventArgs args)
83-
{
84-
switch (args.Command._command)
85-
{
86-
case "GET":
87-
if (_iscached == false && args.Exception == null)
88-
_dicStrings.TryAdd(args.Command.GetKey(0), args.Value);
89-
break;
90-
}
91-
}
57+
//以下 KeyFilter 返回 false,从而不使用本地缓存
58+
cli.Set("123Interceptor01", "123123"); //redis-server
9259

93-
bool _iscached = false;
94-
public void Before(InterceptorBeforeEventArgs args)
95-
{
96-
switch (args.Command._command)
97-
{
98-
case "GET":
99-
if (_dicStrings.TryGetValue(args.Command.GetKey(0), out var tryval))
100-
{
101-
args.Value = tryval;
102-
_iscached = true;
103-
}
104-
break;
105-
}
106-
}
60+
var val11 = cli.Get("123Interceptor01"); //redis-server
61+
var val12 = cli.Get("123Interceptor01"); //redis-server
62+
var val23 = cli.Get("123Interceptor01"); //redis-server
63+
Console.ReadKey();
10764
}
10865
}
66+
67+
10968
}

readme.md

+16
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ public static RedisClient cli = new RedisClient(
9999

100100
-----
101101

102+
#### ⚡ Client-side-cahing (本地缓存)
103+
104+
> requires redis-server 6.0 and above
105+
106+
```csharp
107+
cli.UseClientSideCaching(new ClientSideCachingOptions
108+
{
109+
//本地缓存的容量
110+
Capacity = 3,
111+
//过滤哪些键能被本地缓存
112+
KeyFilter = key => key.StartsWith("Interceptor"),
113+
//检查长期未使用的缓存
114+
CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(2)
115+
});
116+
```
117+
102118
#### 📡 Subscribe (订阅)
103119

104120
```csharp

src/FreeRedis/ClientSideCaching.cs

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
using FreeRedis.Internal;
2+
using System;
3+
using System.Collections.Concurrent;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading;
8+
9+
namespace FreeRedis
10+
{
11+
public class ClientSideCachingOptions
12+
{
13+
public int Capacity { get; set; }
14+
15+
/// <summary>
16+
/// true: cache
17+
/// </summary>
18+
public Func<string, bool> KeyFilter { get; set; }
19+
20+
/// <summary>
21+
/// true: expired
22+
/// </summary>
23+
public Func<string, DateTime, bool> CheckExpired { get; set; }
24+
}
25+
26+
public static class ClientSideCachingExtensions
27+
{
28+
public static void UseClientSideCaching(this RedisClient cli, ClientSideCachingOptions options)
29+
{
30+
new ClientSideCachingContext(cli, options)
31+
.Start();
32+
}
33+
34+
class ClientSideCachingContext
35+
{
36+
readonly RedisClient _cli;
37+
readonly ClientSideCachingOptions _options;
38+
IPubSubSubscriber _sub;
39+
40+
public ClientSideCachingContext(RedisClient cli, ClientSideCachingOptions options)
41+
{
42+
_cli = cli;
43+
_options = options ?? new ClientSideCachingOptions();
44+
}
45+
46+
public void Start()
47+
{
48+
_sub = _cli.Subscribe("__redis__:invalidate", InValidate) as IPubSubSubscriber;
49+
_cli.Interceptors.Add(() => new MemoryCacheAop(this));
50+
_cli.Unavailable += (_, e) =>
51+
{
52+
lock (_dictLock) _dictSort.Clear();
53+
_dict.Clear();
54+
};
55+
_cli.Connected += (_, e) =>
56+
{
57+
e.Client.ClientTracking(true, _sub.RedisSocket.ClientId, null, false, false, false, false);
58+
};
59+
}
60+
61+
void InValidate(string chan, object msg)
62+
{
63+
var keys = msg as object[];
64+
foreach (var key in keys)
65+
RemoveCache(string.Concat(key));
66+
}
67+
68+
static readonly DateTime _dt2020 = new DateTime(2020, 1, 1);
69+
static long GetTime() => (long)DateTime.Now.Subtract(_dt2020).TotalSeconds;
70+
/// <summary>
71+
/// key -> Type(string|byte[]|class) -> value
72+
/// </summary>
73+
readonly ConcurrentDictionary<string, DictValue> _dict = new ConcurrentDictionary<string, DictValue>();
74+
readonly SortedSet<string> _dictSort = new SortedSet<string>();
75+
readonly object _dictLock = new object();
76+
bool TryGetCacheValue(string key, Type valueType, out object value)
77+
{
78+
if (_dict.TryGetValue(key, out var trydictval) && trydictval.Values.TryGetValue(valueType, out var tryval)
79+
//&& DateTime.Now.Subtract(_dt2020.AddSeconds(tryval.SetTime)) < TimeSpan.FromMinutes(5)
80+
)
81+
{
82+
if (_options.CheckExpired?.Invoke(key, _dt2020.AddSeconds(tryval.SetTime)) == true)
83+
{
84+
RemoveCache(key);
85+
value = null;
86+
return false;
87+
}
88+
var time = GetTime();
89+
if (_options.Capacity > 0)
90+
{
91+
lock (_dictLock)
92+
{
93+
_dictSort.Remove($"{trydictval.GetTime.ToString("X").PadLeft(16, '0')}{key}");
94+
_dictSort.Add($"{time.ToString("X").PadLeft(16, '0')}{key}");
95+
}
96+
}
97+
Interlocked.Exchange(ref trydictval.GetTime, time);
98+
value = tryval.Value;
99+
return true;
100+
}
101+
value = null;
102+
return false;
103+
}
104+
void SetCacheValue(string command, string key, Type valueType, object value)
105+
{
106+
_dict.GetOrAdd(key, keyTmp =>
107+
{
108+
var time = GetTime();
109+
if (_options.Capacity > 0)
110+
{
111+
string removeKey = null;
112+
lock (_dictLock)
113+
{
114+
if (_dictSort.Count >= _options.Capacity) removeKey = _dictSort.First().Substring(16);
115+
_dictSort.Add($"{time.ToString("X").PadLeft(16, '0')}{key}");
116+
}
117+
if (removeKey != null)
118+
RemoveCache(removeKey);
119+
}
120+
return new DictValue(command, time);
121+
}).Values
122+
.AddOrUpdate(valueType, new DictValue.ObjectValue(value), (oldkey, oldval) => new DictValue.ObjectValue(value));
123+
}
124+
void RemoveCache(params string[] keys)
125+
{
126+
if (keys?.Any() != true) return;
127+
foreach (var key in keys)
128+
{
129+
if (_dict.TryRemove(key, out var old))
130+
{
131+
if (_options.Capacity > 0)
132+
{
133+
lock (_dictLock)
134+
{
135+
_dictSort.Remove($"{old.GetTime.ToString("X").PadLeft(16, '0')}{key}");
136+
}
137+
}
138+
}
139+
}
140+
}
141+
class DictValue
142+
{
143+
public readonly ConcurrentDictionary<Type, ObjectValue> Values = new ConcurrentDictionary<Type, ObjectValue>();
144+
public readonly string Command;
145+
public long GetTime;
146+
public DictValue(string command, long gettime)
147+
{
148+
this.Command = command;
149+
this.GetTime = gettime;
150+
}
151+
public class ObjectValue
152+
{
153+
public readonly object Value;
154+
public readonly long SetTime = (long)DateTime.Now.Subtract(_dt2020).TotalSeconds;
155+
public ObjectValue(object value) => this.Value = value;
156+
}
157+
}
158+
159+
class MemoryCacheAop : IInterceptor
160+
{
161+
ClientSideCachingContext _cscc;
162+
public MemoryCacheAop(ClientSideCachingContext cscc)
163+
{
164+
_cscc = cscc;
165+
}
166+
167+
bool _iscached = false;
168+
public void Before(InterceptorBeforeEventArgs args)
169+
{
170+
switch (args.Command._command)
171+
{
172+
case "GET":
173+
if (_cscc.TryGetCacheValue(args.Command.GetKey(0), args.ValueType, out var getval))
174+
{
175+
args.Value = getval;
176+
_iscached = true;
177+
}
178+
break;
179+
case "MGET":
180+
var mgetValType = args.ValueType.GetElementType();
181+
var mgetKeys = args.Command._keyIndexes.Select((item, index) => args.Command.GetKey(index)).ToArray();
182+
var mgetVals = mgetKeys.Select(a => _cscc.TryGetCacheValue(a, mgetValType, out var mgetval) ?
183+
new DictGetResult { Value = mgetval, Exists = true } : new DictGetResult { Value = null, Exists = false })
184+
.Where(a => a.Exists).Select(a => a.Value).ToArray();
185+
if (mgetVals.Length == mgetKeys.Length)
186+
{
187+
args.Value = args.ValueType.FromObject(mgetVals);
188+
_iscached = true;
189+
}
190+
break;
191+
}
192+
}
193+
194+
public void After(InterceptorAfterEventArgs args)
195+
{
196+
switch (args.Command._command)
197+
{
198+
case "GET":
199+
if (_iscached == false && args.Exception == null)
200+
{
201+
var getkey = args.Command.GetKey(0);
202+
if (_cscc._options.KeyFilter?.Invoke(getkey) != false)
203+
_cscc.SetCacheValue(args.Command._command, getkey, args.ValueType, args.Value);
204+
}
205+
break;
206+
case "MGET":
207+
if (_iscached == false && args.Exception == null)
208+
{
209+
if (args.Value is Array valueArr)
210+
{
211+
var valueArrElementType = args.ValueType.GetElementType();
212+
var sourceArrLen = valueArr.Length;
213+
for (var a = 0; a < sourceArrLen; a++)
214+
_cscc.SetCacheValue("GET", args.Command.GetKey(a), valueArrElementType, valueArr.GetValue(a));
215+
}
216+
}
217+
break;
218+
default:
219+
if (args.Command._keyIndexes.Any())
220+
{
221+
var cmdset = CommandSets.Get(args.Command._command);
222+
if (cmdset != null &&
223+
(cmdset.Flag & CommandSets.ServerFlag.write) == CommandSets.ServerFlag.write &&
224+
(cmdset.Tag & CommandSets.ServerTag.write) == CommandSets.ServerTag.write &&
225+
(cmdset.Tag & CommandSets.ServerTag.@string) == CommandSets.ServerTag.@string)
226+
{
227+
_cscc.RemoveCache(args.Command._keyIndexes.Select((item, index) => args.Command.GetKey(index)).ToArray());
228+
}
229+
}
230+
break;
231+
}
232+
}
233+
234+
class DictGetResult
235+
{
236+
public object Value;
237+
public bool Exists;
238+
}
239+
}
240+
}
241+
}
242+
}

0 commit comments

Comments
 (0)