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

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

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

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

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

(0)

相关推荐

  • 浅谈ASP.NET Core中间件实现分布式 Session

    1.1. 中间件原理 1.1.1. 什么是中间件 中间件是段代码用于处理请求和响应,通常多个中间件链接起来形成管道,由每个中间件自己来决定是否要调用下一个中间件. 1.1.2. 中间件执行过程 举一个示例来演示中间件的执行过程(分别有三个中间件:日志记录.权限验证和路由):当请求进入应用程序时,执行执行日志记录的中间件,它记录请求属性并调用链中的下一个中间件权限验证,如果权限验证通过则将控制权传递给下一个中间件,不通过则设置401 HTTP代码并返回响应,响应传递给日志中间件进行返回. 1.1.

  • 解析Asp.net Core中使用Session的方法

    前言 2017年就这么悄无声息的开始了,2017年对我来说又是特别重要的一年. 元旦放假在家写了个Asp.net Core验证码登录, 做demo的过程中遇到两个小问题,第一是在Asp.net Core中引用dll,以往我们引用DLL都是直接引用,在Core里这样是不行的,必须基于NuGet添加,或者基于project.json添加,然后保存VS会启动还原类库. 第二就是使用Session的问题,Core里使用Session需要添加Session类库. 添加Session 在你的项目上基于NuG

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

    前言 Asp.net Core 改变了之前的封闭,现在开源且开放,下面我们来用Redis存储Session来做一个简单的测试,或者叫做中间件(middleware). 对于Session来说褒贬不一,很多人直接说不要用,也有很多人在用,这个也没有绝对的这义,个人认为只要不影什么且又可以方便实现的东西是可以用的,现在不对可不可用做表态,我们只关心实现. 类库引用 这个相对于之前的.net是方便了不少,需要在project.json中的dependencies节点中添加如下内容: "StackExc

  • 详解Asp.Net Core 2.1+的视图缓存(响应缓存)

    响应缓存Razor 页与 ASP.NET 核心 2.0 中不支持. 此功能将支持ASP.NET 核心 2.1 版本. 在老的版本的MVC里面,有一种可以缓存视图的特性(OutputCache),可以保持同一个参数的请求,在N段时间内,直接从mvc的缓存中读取,不去走视图的逻辑. [OutputCache(Duration =20)]//设置过期时间为20秒 public ActionResult ExampleCacheAction() { var time=DateTime.Now.ToStr

  • 详解ASP.NET Core Web Api之JWT刷新Token

    前言 如题,本节我们进入JWT最后一节内容,JWT本质上就是从身份认证服务器获取访问令牌,继而对于用户后续可访问受保护资源,但是关键问题是:访问令牌的生命周期到底设置成多久呢?见过一些使用JWT的童鞋会将JWT过期时间设置成很长,有的几个小时,有的一天,有的甚至一个月,这么做当然存在问题,如果被恶意获得访问令牌,那么可在整个生命周期中使用访问令牌,也就是说存在冒充用户身份,此时身份认证服务器当然也就是始终信任该冒牌访问令牌,若要使得冒牌访问令牌无效,唯一的方案则是修改密钥,但是如果我们这么做了,

  • 详解ASP.NET Core中配置监听URLs的五种方式

    默认情况下,ASP. NET Core应用会监听一下2个Url: http://localhost:5000 https://localhost:5001 在本篇博文中,我将展示如何使用五种不同的方式改变应用监听的URLs. 在ASP.NET Core项目启动时,有多种配置监听Url的方式,在我之前的一篇博客中,已经展示了在ASP.NET Core 1.0中如何应用不同的方式配置,在ASP.NET Core 3.x中,大部分方式还是一样的. UseUrls() - 在Program.cs配置程序

  • 详解asp.net core 依赖注入

    前言 好久没有写微博了,因为前段时间由于家庭原因决定从工作了3年多的北京转移到上海去.依赖注入在学习net core的时候也有写过类似的东西,只是实践的较少,结果来到上海新公司系统框架涉及到了这块知识点,所以在了解完自己的项目之后决定做一些相关的总结.接下来就让我们先来了解hewi依赖注入. 什么是依赖注入 依赖注入,全称是"依赖注入到容器", 容器(IOC容器)是一个设计模式,它也是个对象,你把某个类(不管有多少依赖关系)放入这个容器中,可以"解析"出这个类的实例

  • 详解ASP.NET Core端点路由的作用原理

    端点路由(Endpoint Routing)最早出现在ASP.NET Core2.2,在ASP.NET Core3.0提升为一等公民. Endpoint Routing的动机 在端点路由出现之前,我们一般在请求处理管道的末尾,定义MVC中间件解析路由.这种方式意味着在处理管道中,MVC中间件之前的中间件将无法获得路由信息. 路由信息对于某些中间件非常有用,比如CORS.认证中间件(认证过程可能会用到路由信息). 同时端点路由提炼出端点概念,解耦路由匹配逻辑.请求分发. Endpoint Rout

  • 详解ASP.NET Core 反向代理部署知多少

    引言 最近在折腾统一认证中心,看到开源项目IdentityServer4.Admin集成了IdentityServer4和管理面板,就直接拿过来用了.在尝试Nginx部署时遇到了诸如虚拟目录映射,请求头超长.基础路径映射有误等问题,简单记录,以供后人参考. Nginx 配置路由转发 首先来看下IdentityServer4.Admin的项目结构: IdentityServer4.Admin / ├── Id4.Admin.Api # 用于提供访问Id4资源的WebApi项目 ├── Id4.Ad

  • 详解asp.net core重新加载应用配置

    asp.net core重新加载应用配置Intro 我把配置放在了数据库或者是Redis里,配置需要修改的时候我要直接修改数据库,然后调用一个接口去重新加载应用配置,于是就尝试写一个运行时重新加载配置的接口. Configuration 重新加载实现 重新加载配置的接口其实很简单,经过看 Configuration 的源码可以知道,如果想要重新加载应用配置,需要一个 IConfigurationRoot 对象,而 IConfigurationRoot 其实可以直接拿注入服务中的 IConfigu

  • 详解ASP.Net Core 中如何借助CSRedis实现一个安全高效的分布式锁

    引言:最近回头看了看开发的.Net Core 2.1项目的复盘总结,其中在多处用到Redis实现的分布式锁,虽然在OnResultExecuting方法中做了防止死锁的处理,但在某些场景下还是会发生死锁的问题,下面我只展示部分代码: 问题: (1)这里setnx设置的值"1",我想问,你最后del的这个值一定是你自己创建的吗? (2)图中标注的步骤1和步骤2不是原子操作,会有死锁的概率吗? 大家可以思考一下先,下面让我们带着这两个问题往下看,下面介绍一下使用Redis实现分布式锁常用的

  • 详解ASP.NET Core 中基于工厂的中间件激活的实现方法

    IMiddlewareFactory/IMiddleware是中间件激活的扩展点. UseMiddleware扩展方法检查中间件的已注册类型是否实现IMiddleware.如果是,则使用在容器中注册的IMiddlewareFactory实例来解析IMiddleware实现,而不使用基于约定的中间件激活逻辑.中间件在应用的服务容器中注册为作用域或瞬态服务. 优点: 按客户端请求(作用域服务的注入)激活 让中间件强类型化 IMiddleware按客户端请求(连接)激活,因此作用域服务可以注入到中间件

随机推荐