Redis儲存Session

來源:互聯網
上載者:User

標籤:

前言

  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實現

這裡並非我實現,而是借用https://github.com/aspnet/Caching/tree/dev/src/Microsoft.Extensions.Caching.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就成功實現了。

對於上面的執行個體我把源碼放在了:https://github.com/hantianwei/Microsoft.Extensions.Caching.Redis

且也在Nuget上上傳了一份,方便直接使用,Tianwei.Microsoft.Extensions.Caching.Redis ,只是ID加了Tianwei 空間名還是Microsoft.Extensions.Caching.Redis

從上面的執行個體我們發現微軟這次是真的開放了,這也意味著如果我們使用某些類不順手或不合適時可以自已寫自已擴充

 

Redis儲存Session

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.