`
2277259257
  • 浏览: 515674 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Redis--分布式中实现Session共享(二)

 
阅读更多

上一篇介绍了一些redis的安装及使用步骤,本篇开始将介绍redis的实际应用场景,先从最常见的session开始,刚好也重新学习一遍session的实现原理。在阅读之前假设你已经会使用nginx+iis实现负载均衡搭建负载均衡站点了,这里我们会搭建两个站点来验证redis实现的session是否能共享。

阅读目录

Session实现原理

  session和cookie是我们做web开发中常用到的两个对象,它们之间会不会有联系呢?

  Cookie是什么? Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息。(Cookie 会随每次HTTP请求一起被传递服务器端,排除js,css,image等静态文件,这个过程可以从fiddler或者ie自带的网络监控里面分析到,考虑性能的化可以从尽量减少cookie着手)

  Cookie写入浏览器的过程:我们可以使用如下代码在Asp.net项目中写一个Cookie 并发送到客户端的浏览器(为了简单我没有设置其它属性)。

HttpCookie cookie = new HttpCookie("RedisSessionId", "string value");Response.Cookies.Add(cookie);

我们可以看到在服务器写的cookie,会通过响应头Set-Cookie的方式写入到浏览器。

      Session是什么? Session我们可以使用它来方便地在服务端保存一些与会话相关的信息。比如常见的登录信息。

      Session实现原理? HTTP协议是无状态的,对于一个浏览器发出的多次请求,WEB服务器无法区分 是不是来源于同一个浏览器。所以服务器为了区分这个过程会通过一个sessionid来区分请求,而这个sessionid是怎么发送给服务端的呢?前面说了cookie会随每次请求发送到服务端,并且cookie相对用户是不可见的,用来保存这个sessionid是最好不过了,我们通过下面过程来验证一下。

Session["UserId"] = 123;

   通过上图再次验证了session和cookie的关系,服务器产生了一次设置cookie的操作,这里的sessionid就是用来区分浏览器的。为了实验是区分浏览器的,可以实验在IE下进行登录,然后在用chrome打开相同页面,你会发现在chrome还是需要你登录的,原因是chrome这时没有sessionid。httpOnly是表示这个cookie是不会在浏览器端通过js进行操作的,防止人为串改sessionid。

  asp.net默认的sessionid的键值是ASP.NET_SessionId,可以在web.config里面修改这个默认配置

<sessionState mode="InProc" cookieName="MySessionId"></sessionState>

 服务器端Session读取? 服务器端是怎么读取session的值呢 ,Session["键值"]。那么问题来了,为什么在Defaule.aspx.cs文件里可以获取到这个Session对象,这个Session对象又是什么时候被初始化的呢。

  为了弄清楚这个问题,我们可以通过转到定义的方式来查看。

   System.Web.UI.Page ->HttpSessionState(Session)

复制代码
protected internal override HttpContext Context {
[System.Runtime.TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
  get {
       if (_context == null) {
           _context = HttpContext.Current;
       }
       return _context;
    }
 }
 public virtual HttpSessionState Session {
        get {
            if (!_sessionRetrieved) {
                /* try just once to retrieve it */
                _sessionRetrieved = true;

                try {
                    _session = Context.Session;
                }
                catch {
                    //  Just ignore exceptions, return null.
                }
            }

            if (_session == null) {
                throw new HttpException(SR.GetString(SR.Session_not_enabled));
            }

            return _session;
        }
    }
复制代码

  上面这一段是Page对象初始化Session对象的,可以看到Session的值来源于HttpContext.Current,而HttpContext.Current又是什么时候被初始化的呢,我们接着往下看。

 View Code

     我这里只贴出了一部分源码,HttpContext包含了我们常用的Request,Response等对象。HttpContext得从ASP.NET管道说起,以IIS 6.0为例,在工作进程w3wp.exe中,利用Aspnet_ispai.dll加载.NET运行时(如果.NET运行时尚未加载)。IIS 6.0引入了应用程序池的概念,一个工作进程对应着一个应用程序池。一个应用程序池可以承载一个或多个Web应用,每个Web应用映射到一个IIS虚拟目录。与IIS 5.x一样,每一个Web应用运行在各自的应用程序域中。如果HTTP.SYS接收到的HTTP请求是对该Web应用的第一次访问,在成功加载了运行时后,会通过AppDomainFactory为该Web应用创建一个应用程序域(AppDomain)。随后,一个特殊的运行时IsapiRuntime被加载。IsapiRuntime定义在程序集System.Web中,对应的命名空间为System.Web.Hosting。IsapiRuntime会接管该HTTP请求。IsapiRuntime会首先创建一个IsapiWorkerRequest对象,用于封装当前的HTTP请求,并将该IsapiWorkerRequest对象传递给ASP.NET运行时:HttpRuntime,从此时起,HTTP请求正式进入了ASP.NET管道。根据IsapiWorkerRequest对象,HttpRuntime会创建用于表示当前HTTP请求的上下文(Context)对象:HttpContext。
     至此相信大家对Session初始化过程,session和cookie的关系已经很了解了吧,下面开始进行Session共享实现方案。 

Session共享实现方案

一.StateServer方式

  这种是asp.net提供的一种方式,还有一种是SQLServer方式(不一定程序使用的是SQLServer数据库,所以通用性不高,这里就不介绍了)。也就是将会话数据存储到单独的内存缓冲区中,再由单独一台机器上运行的Windows服务来控制这个缓冲区。状态服务全称是“ASP.NET State Service ”(aspnet_state.exe)。它由Web.config文件中的stateConnectionString属性来配置。该属性指定了服务所在的服务器,以及要监视的端口。

<sessionState mode="StateServer"      stateConnectionString="tcpip=127.0.0.1:42424"     cookieless="false" timeout="20" />

  在这个例子中,状态服务在当前机器的42424端口(默认端口)运行。要在服务器上改变端口和开启远程服务器的该功能,可编辑HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters注册表项中的Port值和AllowRemoteConnection修改成1。 显然,使用状态服务的优点在于进程隔离,并可在多站点中共享。 使用这种模式,会话状态的存储将不依赖于iis进程的失败或者重启,然而,一旦状态服务中止,所有会话数据都会丢失(这个问题redis不会存在,重新了数据不会丢失)。

  这里提供一段bat文件帮助修改注册表,可以复制保存为.bat文件执行

复制代码
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "AllowRemoteConnection" /t REG_DWORD  /d 1 /f

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "Port" /t REG_DWORD  /d 42424 /f

net stop aspnet_state
net start aspnet_state

pause
复制代码

 

   完成这些配置以后还是不能实现共享,虽然站点间的SessionId是一致的,但只有一个站点能够读取的到值,而其它站点读取不到。下面给出解决方案,在Global文件里面添加下面代码

复制代码
 public override void Init()
 {
      base.Init();

      foreach (string moduleName in this.Modules)
      {
           string appName = "APPNAME";
           IHttpModule module = this.Modules[moduleName];
           SessionStateModule ssm = module as SessionStateModule;
           if (ssm != null)
           {
                FieldInfo storeInfo = typeof(SessionStateModule).GetField("_store", BindingFlags.Instance | BindingFlags.NonPublic);
                FieldInfo configMode = typeof(SessionStateModule).GetField("s_configMode", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);

                SessionStateMode mode = (SessionStateMode)configMode.GetValue(ssm);
                if (mode == SessionStateMode.StateServer)
                {
                    SessionStateStoreProviderBase store = (SessionStateStoreProviderBase)storeInfo.GetValue(ssm);
                    if (store == null)//In IIS7 Integrated mode, module.Init() is called later
                    {
                        FieldInfo runtimeInfo = typeof(HttpRuntime).GetField("_theRuntime", BindingFlags.Static | BindingFlags.NonPublic);
                        HttpRuntime theRuntime = (HttpRuntime)runtimeInfo.GetValue(null);
                        FieldInfo appNameInfo = typeof(HttpRuntime).GetField("_appDomainAppId", BindingFlags.Instance | BindingFlags.NonPublic);
                        appNameInfo.SetValue(theRuntime, appName);
                    }
                    else
                    {
                       Type storeType = store.GetType();
                       if (storeType.Name.Equals("OutOfProcSessionStateStore"))
                       {
                           FieldInfo uribaseInfo = storeType.GetField("s_uribase", BindingFlags.Static | BindingFlags.NonPublic);
                           uribaseInfo.SetValue(storeType, appName);
                           object obj = null;
                           uribaseInfo.GetValue(obj);
                        }
                    }
                }
                break;
            }
        }
  }
复制代码

 

 

二.redis实现session共享

   下面我们将使用redis来实现共享,首先要弄清楚session的几个关键点,过期时间,SessionId,一个SessionId里面会存在多组key/value数据。基于这个特性我将采用Hash结构来存储,看看代码实现。用到了上一篇提供的RedisBase帮助类。

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.SessionState;
using ServiceStack.Redis;
using Com.Redis;

namespace ResidSessionDemo.RedisDemo
{
    public class RedisSession
    {
        private HttpContext context;

        public RedisSession(HttpContext context, bool IsReadOnly, int Timeout)
        {
            this.context = context;
            this.IsReadOnly = IsReadOnly;
            this.Timeout = Timeout;
            //更新缓存过期时间
            RedisBase.Hash_SetExpire(SessionID, DateTime.Now.AddMinutes(Timeout));
        }

        /// <summary>
        /// SessionId标识符
        /// </summary>
        public static string SessionName = "Redis_SessionId";

        //
        // 摘要:
        //     获取会话状态集合中的项数。
        //
        // 返回结果:
        //     集合中的项数。
        public int Count
        {
            get
            {
                return RedisBase.Hash_GetCount(SessionID);
            }
        }

        //
        // 摘要:
        //     获取一个值,该值指示会话是否为只读。
        //
        // 返回结果:
        //     如果会话为只读,则为 true;否则为 false。
        public bool IsReadOnly { get; set; }

        //
        // 摘要:
        //     获取会话的唯一标识符。
        //
        // 返回结果:
        //     唯一会话标识符。
        public string SessionID
        {
            get
            {
                return GetSessionID();
            }
        }

        //
        // 摘要:
        //     获取并设置在会话状态提供程序终止会话之前各请求之间所允许的时间(以分钟为单位)。
        //
        // 返回结果:
        //     超时期限(以分钟为单位)。
        public int Timeout { get; set; }

        /// <summary>
        /// 获取SessionID
        /// </summary>
        /// <param name="key">SessionId标识符</param>
        /// <returns>HttpCookie值</returns>
        private string GetSessionID()
        {
            HttpCookie cookie = context.Request.Cookies.Get(SessionName);
            if (cookie == null || string.IsNullOrEmpty(cookie.Value))
            {
                string newSessionID = Guid.NewGuid().ToString();
                HttpCookie newCookie = new HttpCookie(SessionName, newSessionID);
                newCookie.HttpOnly = IsReadOnly;
                newCookie.Expires = DateTime.Now.AddMinutes(Timeout);
                context.Response.Cookies.Add(newCookie);
                return "Session_"+newSessionID;
            }
            else
            {
                return "Session_"+cookie.Value;
            }
        }

        //
        // 摘要:
        //     按名称获取或设置会话值。
        //
        // 参数:
        //   name:
        //     会话值的键名。
        //
        // 返回结果:
        //     具有指定名称的会话状态值;如果该项不存在,则为 null。
        public object this[string name]
        {
            get
            {
                return RedisBase.Hash_Get<object>(SessionID, name);
            }
            set
            {
                RedisBase.Hash_Set<object>(SessionID, name, value);
            }
        }

        // 摘要:
        //     判断会话中是否存在指定key
        //
        // 参数:
        //   name:
        //     键值
        //
        public bool IsExistKey(string name)
        {
            return RedisBase.Hash_Exist<object>(SessionID, name);
        }

        //
        // 摘要:
        //     向会话状态集合添加一个新项。
        //
        // 参数:
        //   name:
        //     要添加到会话状态集合的项的名称。
        //
        //   value:
        //     要添加到会话状态集合的项的值。
        public void Add(string name, object value)
        {
            RedisBase.Hash_Set<object>(SessionID, name, value);
        }
        //
        // 摘要:
        //     从会话状态集合中移除所有的键和值。
        public void Clear()
        {
            RedisBase.Hash_Remove(SessionID);
        }

        //
        // 摘要:
        //     删除会话状态集合中的项。
        //
        // 参数:
        //   name:
        //     要从会话状态集合中删除的项的名称。
        public void Remove(string name)
        {
            RedisBase.Hash_Remove(SessionID,name);
        }
        //
        // 摘要:
        //     从会话状态集合中移除所有的键和值。
        public void RemoveAll()
        {
            Clear();
        }
    }
}
复制代码

   下面是实现类似在cs文件中能直接使用Session["UserId"]的方式,我的MyPage类继承Page实现了自己的逻辑主要做了两件事  1:初始化RedisSession  2:实现统一登录认证,OnPreInit方法里面判断用户是否登录,如果没有登录了则跳转到登陆界面

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;

namespace ResidSessionDemo.RedisDemo
{
    /// <summary>
    /// 自定义Page 实现以下功能
    /// 1.初始化RedisSession
    /// 2.实现页面登录验证,继承此类,则可以实现所有页面的登录验证
    /// </summary>
    public class MyPage:Page
    {
        private RedisSession redisSession;

        /// <summary>
        /// RedisSession
        /// </summary>
        public RedisSession RedisSession
        {
            get
            {
                if (redisSession == null)
                {
                    redisSession = new RedisSession(Context, true, 20);
                }
                return redisSession;
            }
        }

        protected override void OnPreInit(EventArgs e)
        {
            base.OnPreInit(e);
            //判断用户是否已经登录,如果未登录,则跳转到登录界面
            if (!RedisSession.IsExistKey("UserCode"))
            {
                Response.Redirect("Login.aspx");
            }
        }
    }
}
复制代码

 我们来看看Default.aspx.cs是如何使用RedisSession的,至此我们实现了和Asp.netSession一模一样的功能和使用方式。

RedisSession.Remove("UserCode");

   相比StateServer,RedisSession具有以下优点

   1.redis服务器重启不会丢失数据  2.可以使用redis的读写分离个集群功能更加高效读写数据  

   测试效果,使用nginx和iis部署两个站点做负载均衡,iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000  nginx代理服务地址127.0.0.1:8003,不懂如何配置的可以去阅读我的nginx+iis实现负载均衡这篇文章。我们来看一下测试结果。

  访问127.0.0.1:8003 需要进行登录   用户名为admin  密码为123

  

 登录成功以后,重点关注端口号信息

  

 刷新页面,重点关注端口号信息

可以尝试直接访问iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000 这两个站点,你会发现都不需要登录了。至此我们的redis实现session功能算是大功告成了。

问题拓展

  使用redis实现session告一段落,下面留个问题讨论一下方案。微信开发提供了很多接口,参考下面截图,可以看到获取access_token接口每日最多调用2000次,现在大公司提供的很多接口针对不对级别的用户接口访问次数限制都是不一样的,至于做这个限制的原因应该是防止恶意攻击和流量限制之类的。那么我的问题是怎么实现这个接口调用次数限制功能。大家可以发挥想象力参与讨论哦,或许你也会碰到这个问题。

  

  先说下我知道的两种方案:

     1.使用流量整形中的令牌桶算法,大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。

  说浅显点:比如上面的获取access_token接口,一天2000次的频率,即1次/分钟。我们令牌桶容量为2000,可以使用redis 最简单的key/value来存储 ,key为用户id,value为整形存储还可使用次数,然后使用一个定时器1分钟调用client.Incr(key) 实现次数自增;用户每访问一次该接口,相应的client.Decr(key)来减少使用次数。

  但是这里存在一个性能问题,这仅仅是针对一个用户来说,假设有10万个用户,怎么使用定时器来实现这个自增操作呢,难道是循环10万次分别调用client.Incr(key)吗?这一点没有考虑清楚。

      2.直接用户访问一次 先进行总次数判断,符合条件再就进行一次自增

      两种方案优缺点比较
  优点 缺点
令牌桶算法 流量控制精确  实现复杂,并且由于控制精确反而在实际应用中有麻烦,很可能用户在晚上到凌晨期间访问接口次数不多,白天访问次数多些。
简单算法 实现简单可行,效率高  流量控制不精确

总结

  本篇从实际应用讲解了redis,后面应该还会有几篇继续介绍redis实际应用,敬请期待!

      本篇文章用到的资源打包下载地址:redis_demo

  svn下载地址:http://code.taobao.org/svn/ResidSessionDemo/

分享到:
评论

相关推荐

    tomcat-redis-session-manager实现session共享 配置文件

    "tomcat-redis-session-manager"是一个解决方案,它将用户的Session信息存储在Redis缓存服务器中,从而实现跨服务器的Session共享。本篇文章将深入探讨这个话题,包括它的原理、配置以及实际应用。 **一、Session...

    tomcat-redis-session-manager

    `tomcat-redis-session-manager`就是这样一款解决方案,它将Tomcat的session管理与Redis相结合,实现了跨服务器的session共享。 首先,让我们理解`tomcat-redis-session-manager`的核心概念。这是一个开源项目,它...

    session 共享 tomcat-redis-session-manager 所需要的jar (绝对可用)

    这里提到的"session 共享 tomcat-redis-session-manager"就是一种解决方案,它利用Redis作为中央存储来实现Tomcat容器中的Session共享。 首先,我们来看看标题所提及的"session 共享 tomcat-redis-session-manager...

    tomcat-redis-session-manager的jar包-包含Tomcat7和Tomcat8

    总结起来,Tomcat-Redis-Session-Manager通过整合Redis和Tomcat,实现了高效、可靠的session管理,特别适用于需要处理高并发和分布式部署的Web应用。其提供的jar包简化了集成过程,让开发者能快速地在Tomcat7和...

    tomcat-redis-session-manager包集合下载(tomcat8)

    它的高速读写能力使其成为存储session的理想选择,尤其是在分布式环境中,可以轻松实现session共享。 **知识点三:tomcat-redis-session-manager** tomcat-redis-session-manager是专门为Tomcat设计的一个插件,它...

    tomcat-redis-session-manager-2.0.0.zip

    `tomcat-redis-session-manager-2.0.0`就是为了解决这个问题而诞生的一个开源项目,它将Tomcat容器中的Session数据存储到Redis缓存系统中,实现了跨服务器的Session共享。 Redis是一种高性能的键值数据库,特别适合...

    tomcat9+tomcat-cluster-redis-session-manager_4.0.zip

    Tomcat Cluster Redis Session Manager是一个插件,它允许Tomcat集群中的各个节点共享和同步Session信息,确保用户在集群中的任何服务器上都能保持登录状态。 集成Tomcat集群和Redis Session Manager的过程大致如下...

    tomcat-redis-session-manager jar包

    这个库的目的是将用户的会话信息存储在Redis这样的分布式内存数据库中,而不是传统的基于内存的Session管理,这样可以解决因服务器重启或负载均衡导致的Session丢失问题。 **Redis作为Session存储的优势** 1. **高...

    tomcat-redis-session-manager-1.2-tomcat-7-java-7

    1. "tomcat-redis-session-manager-1.2-tomcat-7-java-7.jar":这是主要的会话管理器实现,它提供了在Tomcat中使用Redis存储session数据的接口和逻辑。这个jar文件包含具体的类和方法,使得Tomcat能够与Redis通信,...

    分布式集群Session共享 简单多tomcat8+redis的session共享实现

    首先,让我们来看看如何在Tomcat 8中实现基于Redis的Session共享。`context.xml`文件是Tomcat配置的一部分,它定义了每个Web应用的上下文环境。在这个场景下,我们需要在`context.xml`中配置一个名为`Manager`的元素...

    tomcat共享session tomcat-redis-session-manager-2.0.0.jar包下载

    tomcat-redis-session-manager-2.0.0.jar包,不用自己打包了,tomcat共享session到redis中,解决分布式应用的状态问题。

    tomcat-redis-session-manager-1.2-tomcat-6&7

    这个组件使得Web应用程序可以利用Redis分布式缓存系统来存储和管理用户的会话数据,从而实现高可用性和可扩展性。 描述中提到的 "commons-pool-1.6.jar" 是Apache Commons Pool库的1.6版本,这是一个对象池服务,...

    redis-session-manager-tomcat6共享包

    总之,“redis-session-manager-tomcat6共享包”为Tomcat 6提供了一种高效、可靠的session共享方案,通过修复null值session问题,提高了系统的稳定性和兼容性,是现代Web应用分布式架构中的重要组件。

    tomcat8-redis-session共享

    通过这种方式,Tomcat 8和Redis结合,能够实现高效的Session共享,确保用户在分布式环境中的会话一致性。注意,使用Redis存储Session时,需要注意数据的持久化策略以及Redis的内存管理,避免内存溢出。同时,考虑到...

    redis-session-manager-redis-session-manager-2.0.0.tar.gz

    1. **分布式支持**:通过 Redis 实现跨服务器的 session 共享,适合分布式应用环境。 2. **高并发**:Redis 的高性能特性使得大量并发请求下的 session 管理变得高效。 3. **数据持久化**:Redis 提供的数据持久化...

    redis-session-manager-redis-session-manager-2.0.3.zip

    Redis Session Manager是一款基于Redis数据库实现的会话管理工具,它主要用在Web应用程序中,用于高效、安全地存储和管理用户的会话数据。这个压缩包文件"redis-session-manager-redis-session-manager-2.0.3.zip...

    redis-tomcat8以上集群所需要的jar :tomcat-redis-session-manager-master 等

    总结来说,使用`tomcat-redis-session-manager`可以在Tomcat 8及以上版本中实现Redis为基础的Session共享,通过引入必要的jar包,并正确配置Tomcat,可以确保在分布式环境中的用户会话一致性。这种方案在应对高并发...

    redis-session-manager-redis-session-manager-2.0.2.zip

    Redis Session Manager是一款基于Redis数据库实现的会话管理工具,主要用于在分布式系统中高效、安全地存储和管理Web应用的用户会话数据。标题中的"redis-session-manager-redis-session-manager-2.0.2.zip"表明这是...

    redis-session-manager-redis-session-manager-2.1.1.zip

    Redis Session Manager是一款基于Redis数据库实现的会话管理工具,它主要负责存储和管理Web应用程序中的用户会话数据。Redis是一个高性能、开源、内存中的键值数据存储系统,常被用作数据库、缓存和消息代理。这个...

    -redis-session-manager-master.zip

    标题中的“-redis-session-manager-master.zip”表明...总的来说,这个工具包是提高分布式系统session管理效率和一致性的有效手段,通过结合Tomcat和Redis的优势,解决了集群环境下session共享的难题,提升了用户体验。

Global site tag (gtag.js) - Google Analytics