怎么在Asp.net Core中利用Redis存储Session?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
前言
Asp.net Core 改变了之前的封闭,现在开源且开放,下面我们来用Redis存储Session来做一个简单的测试,或者叫做中间件(middleware)。
对于Session来说褒贬不一,很多人直接说不要用,也有很多人在用,这个也没有绝对的这义,个人认为只要不影什么且又可以方便实现的东西是可以用的,现在不对可不可用做表态,我们只关心实现。
类库引用
这个相对于之前的.net是方便了不少,需要在project.json中的dependencies节点中添加如下内容:
"StackExchange.Redis": "1.1.604-alpha", "Microsoft.AspNetCore.Session": "1.1.0-alpha1-21694"
Redis实现
这里并非我实现,而是借用不知道为什么之前还有这个类库,而现在NUGET止没有了,为了不影响日后升级我的命名空间也用 Microsoft.Extensions.Caching.Redis
可以看到微软这里有四个类,其实我们只需要三个,第四个拿过来反而会出错:
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
我们在Startup中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) });
到此我们的配置完毕,可以测试一下是否写到了Redis中
验证结果
在Mvc项目中,我们来实现如下代码
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就成功实现了。
关于怎么在Asp.net Core中利用Redis存储Session问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。