詳解Asp.net Core 使用Redis存儲(chǔ)Session
前言
Asp.net Core 改變了之前的封閉,現(xiàn)在開(kāi)源且開(kāi)放,下面我們來(lái)用Redis存儲(chǔ)Session來(lái)做一個(gè)簡(jiǎn)單的測(cè)試,或者叫做中間件(middleware)。
對(duì)于Session來(lái)說(shuō)褒貶不一,很多人直接說(shuō)不要用,也有很多人在用,這個(gè)也沒(méi)有絕對(duì)的這義,個(gè)人認(rèn)為只要不影什么且又可以方便實(shí)現(xiàn)的東西是可以用的,現(xiàn)在不對(duì)可不可用做表態(tài),我們只關(guān)心實(shí)現(xiàn)。
類庫(kù)引用
這個(gè)相對(duì)于之前的.net是方便了不少,需要在project.json中的dependencies節(jié)點(diǎn)中添加如下內(nèi)容:
"StackExchange.Redis": "1.1.604-alpha", "Microsoft.AspNetCore.Session": "1.1.0-alpha1-21694"
Redis實(shí)現(xiàn)
這里并非我實(shí)現(xiàn),而是借用不知道為什么之前還有這個(gè)類庫(kù),而現(xiàn)在NUGET止沒(méi)有了,為了不影響日后升級(jí)我的命名空間也用 Microsoft.Extensions.Caching.Redis
可以看到微軟這里有四個(gè)類,其實(shí)我們只需要三個(gè),第四個(gè)拿過(guò)來(lái)反而會(huì)出錯(cuò):
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();
}
}
}
}
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; }
}
}
}
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
我們?cè)赟tartup中ConfigureServices增加
services.AddSingleton<IDistributedCache>(
serviceProvider =>
new RedisCache(new RedisCacheOptions
{
Configuration = "192.168.178.141:6379",
InstanceName = "Sample:"
}));
services.AddSession();
在Startup中Configure增加
app.UseSession(new SessionOptions() { IdleTimeout = TimeSpan.FromMinutes(30) });
到此我們的配置完畢,可以測(cè)試一下是否寫(xiě)到了Redis中
驗(yàn)證結(jié)果
在Mvc項(xiàng)目中,我們來(lái)實(shí)現(xiàn)如下代碼
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"));
}
運(yùn)行我們發(fā)現(xiàn)第一次出現(xiàn)了Hello First timer字樣,刷新后出現(xiàn)了Hello old timer字樣,證明Session成功,再查看一下Redis看一下,有值了,這樣一個(gè)分布式的Session就成功實(shí)現(xiàn)了。
對(duì)于上面的實(shí)例我把源碼放在了:demo下載
Tianwei.Microsoft.Extensions.Caching.Redis ,只是ID加了Tianwei 空間名還是Microsoft.Extensions.Caching.Redis
從上面的實(shí)例我們發(fā)現(xiàn)微軟這次是真的開(kāi)放了,這也意味著如果我們使用某些類不順手或不合適時(shí)可以自已寫(xiě)自已擴(kuò)展
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
asp.net實(shí)現(xiàn)刪除DataGrid的記錄時(shí)彈出提示信息
這篇文章主要介紹了asp.net實(shí)現(xiàn)刪除DataGrid的記錄時(shí)彈出提示信息,非常實(shí)用的功能,需要的朋友可以參考下2014-08-08
http調(diào)用webservice操作httprequest、httpresponse示例
這篇文章主要介紹了http調(diào)用webservice操作httprequest、httpresponse示例,需要的朋友可以參考下2014-05-05
Windows Server 2012 R2 Standard搭建ASP.NET Core環(huán)境圖文教程
這篇文章主要介紹了Windows Server 2012 R2 Standard搭建ASP.NET Core環(huán)境圖文教程,需要的朋友可以參考下2016-07-07
ASP.NET網(wǎng)頁(yè)打印(只打印相關(guān)內(nèi)容/自寫(xiě)功能)
朋友要求在前段時(shí)間完成的新聞的網(wǎng)站上加上一個(gè)功能,就是在每篇新聞瀏覽的頁(yè)面, 加一個(gè)打印銨鈕。讓用戶一點(diǎn)打印,能把整篇文章打印2013-01-01
asp.net DataSet轉(zhuǎn)換成josn并輸出示例
如何將DataSet轉(zhuǎn)換成josn并輸出,這是很多新手朋友們遇到的問(wèn)題,下面有個(gè)不錯(cuò)的示例,希望對(duì)大家有所幫助2014-01-01
ASP.NET實(shí)現(xiàn)License Key輸入功能的小例子
當(dāng)我們安裝微軟的軟件,多數(shù)軟件是需要輸入license key。它有五個(gè)文本框,輸入完第一個(gè)文本框之后,光標(biāo)自動(dòng)跳至下一個(gè)文本框。 Insus.NET今天也使用asp.net來(lái)模仿一個(gè)。呵呵。2013-03-03
ASP.NET頁(yè)面請(qǐng)求超時(shí)時(shí)間設(shè)置多種方法
這篇文章主要為大家詳細(xì)介紹了ASP.NET頁(yè)面請(qǐng)求超時(shí)時(shí)間設(shè)置Server.ScriptTimeOut executionTimeout多種方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09

