前言
Asp.net Core 改变了之前的封闭,现在开源且开放,下面我们来用Redis存储Session来做一个简单的测试,或者叫做中间件(middleware)。
对于Session来说褒贬不一,很多人直接说不要用,也有很多人在用,这个也没有绝对的这义,个人认为只要不影什么且又可以方便实现的东西是可以用的,现在不对可不可用做表态,我们只关心实现。
类库引用
这个相对于之前的.net是方便了不少,需要在project.json中的dependencies节点中添加如下内容:
1
2
|
"StackExchange.Redis": "1.1.604-alpha",
"Microsoft.AspNetCore.Session": "1.1.0-alpha1-21694"
|
Redis实现
这里并非我实现,而是借用不知道为什么之前还有这个类库,而现在NUGET止没有了,为了不影响日后升级我的命名空间也用 Microsoft.Extensions.Caching.Redis
可以看到微软这里有四个类,其实我们只需要三个,第四个拿过来反而会出错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
|
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
namespace Microsoft.Extensions.Caching.Redis
{
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
private 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" );
private const string AbsoluteExpirationKey = "absexp" ;
private const string SlidingExpirationKey = "sldexp" ;
private const string DataKey = "data" ;
private const long NotPresent = -1;
private ConnectionMultiplexer _connection;
private IDatabase _cache;
private readonly RedisCacheOptions _options;
private readonly string _instance;
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 byte [] Get( string key)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
return GetAndRefresh(key, getData: true );
}
public async Task< byte []> GetAsync( string key)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
return await GetAndRefreshAsync(key, getData: true );
}
public 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);
var result = _cache.ScriptEvaluate(SetScript, new RedisKey[] { _instance + key },
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
value
});
}
public async Task SetAsync( 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));
}
await ConnectAsync();
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 void Refresh( string key)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
GetAndRefresh(key, getData: false );
}
public async Task RefreshAsync( string key)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
await GetAndRefreshAsync(key, getData: false );
}
private void Connect()
{
if (_connection == null )
{
_connection = ConnectionMultiplexer.Connect(_options.Configuration);
_cache = _connection.GetDatabase();
}
}
private async Task ConnectAsync()
{
if (_connection == null )
{
_connection = await ConnectionMultiplexer.ConnectAsync(_options.Configuration);
_cache = _connection.GetDatabase();
}
}
private 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)
{
// Note we always get back two results, even if they are all null.
// These operations will no-op in the null scenario.
DateTimeOffset? absExpr;
TimeSpan? sldExpr;
MapMetadata(results, out absExpr, out sldExpr);
Refresh(key, absExpr, sldExpr);
}
if (results.Length >= 3 && results[2].HasValue)
{
return results[2];
}
return null ;
}
private async Task< byte []> GetAndRefreshAsync( string key, bool getData)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
await ConnectAsync();
// 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)
{
// Note we always get back two results, even if they are all null.
// These operations will no-op in the null scenario.
DateTimeOffset? absExpr;
TimeSpan? sldExpr;
MapMetadata(results, out absExpr, out sldExpr);
await RefreshAsync(key, absExpr, sldExpr);
}
if (results.Length >= 3 && results[2].HasValue)
{
return results[2];
}
return null ;
}
public void Remove( string key)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
Connect();
_cache.KeyDelete(_instance + key);
// TODO: Error handling
}
public async Task RemoveAsync( string key)
{
if (key == null )
{
throw new ArgumentNullException(nameof(key));
}
await ConnectAsync();
await _cache.KeyDeleteAsync(_instance + key);
// TODO: Error handling
}
private 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);
}
}
private 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
}
}
private async Task RefreshAsync( 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;
}
await _cache.KeyExpireAsync(_instance + key, expr);
// TODO: Error handling
}
}
private 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 ;
}
private 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 void Dispose()
{
if (_connection != null )
{
_connection.Close();
}
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Caching.Redis
{
/// <summary>
/// Configuration options for <see cref="RedisCache"/>.
/// </summary>
public class RedisCacheOptions : IOptions<RedisCacheOptions>
{
/// <summary>
/// The configuration used to connect to Redis.
/// </summary>
public string Configuration { get ; set ; }
/// <summary>
/// The Redis instance name.
/// </summary>
public string InstanceName { get ; set ; }
RedisCacheOptions IOptions<RedisCacheOptions>.Value
{
get { return this ; }
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
using System.Threading.Tasks;
using StackExchange.Redis;
namespace Microsoft.Extensions.Caching.Redis
{
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));
// 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;
}
}
}
|
配置启用Session
我们在Startup中ConfigureServices增加
1
2
3
4
5
6
7
8
|
services.AddSingleton<IDistributedCache>(
serviceProvider =>
new RedisCache( new RedisCacheOptions
{
Configuration = "192.168.178.141:6379" ,
InstanceName = "Sample:"
}));
services.AddSession();
|
在Startup中Configure增加
1
|
app.UseSession( new SessionOptions() { IdleTimeout = TimeSpan.FromMinutes(30) });
|
到此我们的配置完毕,可以测试一下是否写到了Redis中
验证结果
在Mvc项目中,我们来实现如下代码
1
2
3
4
5
6
7
8
9
10
11
12
|
if ( string .IsNullOrEmpty(HttpContext.Session.GetString( "D" )))
{
var d = DateTime.Now.ToString();
HttpContext.Session.SetString( "D" , d);
HttpContext.Response.ContentType = "text/plain" ;
await HttpContext.Response.WriteAsync( "Hello First timer///" + d);
}
else
{
HttpContext.Response.ContentType = "text/plain" ;
await HttpContext.Response.WriteAsync( "Hello old timer///" + HttpContext.Session.GetString( "D" ));
}
|
运行我们发现第一次出现了Hello First timer字样,刷新后出现了Hello old timer字样,证明Session成功,再查看一下Redis看一下,有值了,这样一个分布式的Session就成功实现了。
对于上面的实例我把源码放在了:demo下载
Tianwei.Microsoft.Extensions.Caching.Redis ,只是ID加了Tianwei 空间名还是Microsoft.Extensions.Caching.Redis
从上面的实例我们发现微软这次是真的开放了,这也意味着如果我们使用某些类不顺手或不合适时可以自已写自已扩展
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:http://www.cnblogs.com/hantianwei/p/5723959.html