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

摘要:
在阅读之前假设你已经会使用nginx+iis实现负载均衡搭建负载均衡站点了,这里我们会搭建两个站点来验证redis实现的session是否能共享。httpOnly是表示这个cookie是不会在浏览器端通过js进行操作的,防止人为串改sessionid。服务器端是怎么读取session的值呢,Session["键值"]。IsapiRuntime会接管该HTTP请求。至此相信大家对Session初始化过程,session和cookie的关系已经很了解了吧,下面开始进行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);

分布式中使用Redis实现Session共享(二)第1张

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

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

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

Session["UserId"] = 123;

分布式中使用Redis实现Session共享(二)第2张

通过上图再次验证了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 overrideHttpContext Context {
[System.Runtime.TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
get{
       if (_context == null) {
           _context =HttpContext.Current;
       }
       return_context;
    }
 }
 public virtualHttpSessionState 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 newHttpException(SR.GetString(SR.Session_not_enabled));
            }

            return_session;
        }
    }
复制代码

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

分布式中使用Redis实现Session共享(二)第5张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_MACHINESYSTEMCurrentControlSetservicesaspnet_stateParameters注册表项中的Port值和AllowRemoteConnection修改成1。显然,使用状态服务的优点在于进程隔离,并可在多站点中共享。 使用这种模式,会话状态的存储将不依赖于iis进程的失败或者重启,然而,一旦状态服务中止,所有会话数据都会丢失(这个问题redis不会存在,重新了数据不会丢失)。

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

复制代码
reg add "HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservicesaspnet_stateParameters" /v "AllowRemoteConnection" /t REG_DWORD  /d 1 /f

reg add "HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservicesaspnet_stateParameters" /v "Port" /t REG_DWORD  /d 42424 /f

net stop aspnet_state
net start aspnet_state

pause
复制代码

 分布式中使用Redis实现Session共享(二)第8张

二.redis实现session共享

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

复制代码
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Web;
usingSystem.Web.SessionState;
usingServiceStack.Redis;
usingCom.Redis;

namespaceResidSessionDemo.RedisDemo
{
    public classRedisSession
    {
        privateHttpContext context;

        public RedisSession(HttpContext context, bool IsReadOnly, intTimeout)
        {
            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 intCount
        {
            get{
                returnRedisBase.Hash_GetCount(SessionID);
            }
        }

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

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

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

        /// <summary>
        ///获取SessionID
        /// </summary>
        /// <param name="key">SessionId标识符</param>
        /// <returns>HttpCookie值</returns>
        private stringGetSessionID()
        {
            HttpCookie cookie =context.Request.Cookies.Get(SessionName);
            if (cookie == null || string.IsNullOrEmpty(cookie.Value))
            {
                string newSessionID =Guid.NewGuid().ToString();
                HttpCookie newCookie = newHttpCookie(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[stringname]
        {
            get{
                return RedisBase.Hash_Get<object>(SessionID, name);
            }
            set{
                RedisBase.Hash_Set<object>(SessionID, name, value);
            }
        }

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

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

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

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

复制代码
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Web;
usingSystem.Web.UI;

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

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

        protected override voidOnPreInit(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

分布式中使用Redis实现Session共享(二)第13张

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

分布式中使用Redis实现Session共享(二)第14张

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

分布式中使用Redis实现Session共享(二)第15张

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

回到顶部
问题拓展

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

分布式中使用Redis实现Session共享(二)第16张

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

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/

原文地址:http://www.cnblogs.com/yanweidie/p/4678095.html

免责声明:文章转载自《分布式中使用Redis实现Session共享(二)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇[Kotlin参考]一、总览-(4)Kotlin原生精讲RestTemplate第9篇-如何通过HTTP Basic Auth认证下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

解决 "OperationalError: (sqlite3.OperationalError) no such table: ..."问题

参考:flask/sqlalchemy - OperationalError: (sqlite3.OperationalError) no such table 在用Flask写一个简单的py文件,做一个表单时,发现在login界面登陆的时候,出现: OperationalError: (sqlite3.OperationalError) no such...

(转)django使用django-celery与celery

一.引言 Django是python语言下的一个比较热门的Web框架,越来越多的企业和开发者使用Django实现自己的Web服务器。在Web服务器开发过程中,有时候我们不仅仅是要实现Web服务器端和用户端的简单逻辑交互,还要实现一些定时任务。举出以下的例子:   定期删除或缓存Redis数据库的记录为了追求更高的数据库访问性能,我把Redis作为MySq...

Redis——redis使用redis-dump,redis-load导出导入数据——【三】

来源 https://www.cnblogs.com/dadonggg/p/8662455.html https://blog.csdn.net/chenxinchongcn/article/details/78666374 安装过程 安装ruby 安装redis-dump之前首先需要安装ruby $ yum install ruby rubygems...

六、redis AOF的持久化

1、AOF持久化的配置 AOF持久化,默认是关闭的,默认是打开RDB持久化 appendonly yes,可以打开AOF持久化机制,在生产环境里面,一般来说AOF都是要打开的,除非你说随便丢个几分钟的数据也无所谓 打开AOF持久化机制之后,redis每次接收到一条写命令,就会写入日志文件中,当然是先写入os cache的,然后每隔一定时间再fsync一下...

php的session

来源:http://blog.163.com/lgh_2002/blog/static/4401752620105246517509/ http协议是WEB服务器与客户 端(浏览器)相互通信的协议,它是一种无状态协议。所谓无状态,指的是不会维护http请求数据,http请求是独立的,非持久的。而越来越复杂的WEB 应用,需要保存一些用户状态信息。这时候,S...

整理分布式锁:业务场景&amp;amp;分布式锁家族&amp;amp;实现原理

1、引入业务场景 业务场景一出现: 因为小T刚接手项目,正在吭哧吭哧对熟悉着代码、部署架构。在看代码过程中发现,下单这块代码可能会出现问题,这可是分布式部署的,如果多个用户同时购买同一个商品,就可能导致商品出现库存超卖 (数据不一致)现象,对于这种情况代码中并没有做任何控制。 原来一问才知道,以前他们都是售卖的虚拟商品,没啥库存一说,所以当时没有考虑那么多...