侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

详解Asp.net Core 使用Redis存储Session

Redis  /  管理员 发布于 5年前   321

前言

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就成功实现了。

对于上面的实例我把源码放在了:demo下载

Tianwei.Microsoft.Extensions.Caching.Redis ,只是ID加了Tianwei 空间名还是Microsoft.Extensions.Caching.Redis

从上面的实例我们发现微软这次是真的开放了,这也意味着如果我们使用某些类不顺手或不合适时可以自已写自已扩展

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


  • 上一条:
    详解.NET中使用Redis数据库
    下一条:
    详解如何在ASP.NET Core中使用Redis
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在Redis中能实现的功能、常见应用介绍(0个评论)
    • 2024年Redis面试题之一(0个评论)
    • 在redis缓存常见出错及解决方案(0个评论)
    • 在redis中三种特殊数据类型:地理位置、基数(cardinality)估计、位图(Bitmap)使用场景介绍浅析(2个评论)
    • Redis 删除 key用 del 和 unlink 有啥区别?(1个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2017-12
    • 2020-03
    • 2020-05
    • 2021-04
    • 2022-03
    • 2022-05
    • 2022-08
    • 2023-02
    • 2023-04
    • 2023-07
    • 2024-01
    • 2024-02
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客