分布式中使用Redis实现Session共享
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