先分析以下需求
软件架构从单机到分布式遇到的问题(当然这是一个很深的问题,由于能力有限今天就说说如何实现用户信息的分布式存储的问题),也就是:走向分布式第一步:多台机器共享用户登录状态,该如何实现?例如:现在有三台机器组成了一个web的应用集群,其中一台机器用户登录,然后其他另外两台机器共享登录状态?具体请看下面的图示:
问题:如果有一个用户第一次登陆的时候,负载均衡把该用户分配到IIS1这台服务器上,该用户的信息就会被保留到IIS1这台服务器上,但是如果该用户再次访问其他的web资源的时候,被分配到IIS2上,这是IIS2中,没有改用户的信息,会发生什么?该怎么解决?该选用什么介质来保存状态比较合适?
从图中可以看出就是保存对应用户的信息,可能有人会用下面的几种方法:
(1)直接保存到进程内session中;
(2)使用ASP.Net进程外session;
(3)用数据库存储当前登录状态;
(4)微软的状态服务器
当然了,使用上面的三种方法不是不可以,但是从网站的整体性能上考虑,确实不太完美,影响系统性能。下面来一一分析这三种方法的缺点:
1)直接保存到进程内session中
缺点:IIS中由于有进程回收机制,系统繁忙的时候session回丢失,IIS重启也会造成session的丢失,这样用户就要重新登录或重新添加购物车、验证码等再放到session中。如果要是把重要敏感的数据放到session中,这是在作死的节奏~~~~
2)使用ASP.Net进程外session 3)用数据库存储当前登录状态
缺点:这两种方式效率会比较慢,性能也不是很好,而且无法捕获session的end事件。
4)微软的状态服务器
缺点:性能不好。
下面欢迎Memcached登场!
为什么要使用Memcached?
1)高并发访问数据库的痛:死锁
2)磁盘IO之痛
3)读写性能完美
4)超简单的集群搭建Cluster
5)开源
6)性能最佳
7)丰富的成功案例
Memcached介绍
Memcached是一个高性能的支持分布式的内存存储系统。你可以把他看成一个巨大的hash表。形式入:
Key(键) | Value(值) |
---|---|
唯一键值(String) | 基本数据(整型, 浮点型,字串,布尔) ,复合类型 (数组, 对象) ,特殊类型(NULL, 不能存放资源), 二进制数据(图片,视频,音频) |
注意:Redis在存入对象的时候,不能直接存入,而是要先序列化,然后再存入,使用的时候,再反序列化。
Memcached的安装和配置(在windows安装)
(1) 下载安装文件 memcached.exe
(2) 安装指令cmd>{%mem%}/memcached.exe -d install
(3) 使用cmd>{%mem%}/memcached -d start 【启动】
如果启动成功,我们可以使用
cmd>netstat -an
如果发现有一个 11211端口在监听则说明你的服务OK
补充:
我们也可以把memcached当做一个程序来使用,
cmd>{%mem%}/memcached.exe -p 端口号
(4) 使用telnet工具登录到Memcached 中进行操作
cmd>telnet 127.0.0.1 11211
注意:Memcached安装不成功的原因和解决
可能安装失败的原因分析
6.1 如果你是用win7,win8系统,他对安全性要求高,因此,需要大家使用管理员的身份来安装和启动. 具体是 程序开始===>所有程序==》附件==》cmd(单击右键,选择以管理员的身份来执行)
6.2 存放memcache.exe 目录不要有中文或者特殊字符
6.3 安装成功,但是启动会报告一个错误信息,提示缺少xx.dll ,你可以从别的机器拷贝该dll文件,然后放入到system32下即可,并执行【然后打开“开始-运行-输入regsvr32 /s MSVCR71.dll”,回车即可解决错误提示!】,这是因为有些电脑上装的操作系统是阉割版的。
6.4 如果上面三个方法都不可以,你可以直接这样启动mem
cmd>memcached.exe -p 端口 【这种方式不能关闭窗口】
使用Telnet操作Memcached
首先要登录到mem上
cmd>telnet 127.0.0.1 11211
(1 ) 添加
add key 0 有效时间 数据大小
举例
add key1 0 60 5
(2) 查询
get key
举例
get key1
(3) 修改
有两种
set key 0 效时间 数据大小
【说明这时,key如果存在,则是修改,否则就是添加】
举例
set key1 0 60 5
replace key 0 效时间 数据大小
【说明这时,key如果存在,则是修改,否则就失败】
(4) 删除
delete key
举例
delete key1
还有一种方式:
flush_all
(1) 查看mem的使用状态
为了大家可以详细了解,再附上一张图片:
(2) 其它指令
Memcached机制深入了解
1)基于c/s架构,协议简单:
c/s架构,此时memcached为服务器端,我们可以使用如PHP,c/c++等程序连接memcached服务器。
memcached的服务器客户端通信并不使用XML等格式,而使用简单的基于文本行的协议。因此,通过telnet也能在memcached上保存数据、取得数据。
2)内存处理的算法:
本质就是一个大的哈希表。key最大长度是255个字符。
内存模型:Memcache预先将可支配的内存空间进行分区(Slab),每个分区里再分成多个块(Chunk)最大1MB,但同一个分区里:块的长度(bytes)是固定的。插入数据时通过一致性哈希算法查找适合自己长度的块,然后插入,会有内存浪费。
为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used[最近最少使用算法])算法自动删除不使用的缓存。memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。
3)惰性删除:
它并没有提供监控数据过期的机制,而是惰性的,当查询到某个key数据时,如果过期那么直接抛弃。
4)集群搭建原理:
Memcache服务器端并没有提供集群功能,但是通过客户端的驱动程序实现了集群配置。
客户端实现集群的原理:首先客户端配置多台集群机器的ip和端口的列表。然后客户端驱动程序在写入之前,首先对key做哈希处理得到哈希值后对总的机器的个数进行取余然后就选择余数对应的机器。
5)基于客户端的分布式
6) 基于libevent的事件处理(这就是Memcached为什么这么吊的原因)
libevent是一套跨平台的事件处理接口的封装,能够兼容包括这些操作系统:Windows/Linux/BSD/Solaris 等操作系统的的事件处理。Memcached 使用libevent来进行网络并发连接的处理,能够保持在很大并发情况下,仍旧能够保持快速的响应能力。
part3:项目实战(demo版)
还记得上面提到的问题吗?如何把用户的状态信息保存起来,共享给这三台服务器?下面通过代码,给大家介绍ASP.Net MVC 4中如何使用Memcached,开始吧!
项目结构:
项目中需要引用Memcached的dll,如下:
1、首先准备好工具类:
MemcacheHelper:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Memcached.ClientLibrary;
namespace WebDemo.Models
{
public static class MemcacheHelper
{
private static MemcachedClient mc;
static MemcacheHelper()
{
//通过客户端来进行memcached的集群配置,在插入数据的时候,使用一致性哈希算法,将对应的value值存入Memcached
String[] serverlist = { "127.0.0.1:11211" };
// 初始化Memcached的服务池
SockIOPool pool = SockIOPool.GetInstance("test");
//设置服务器列表
pool.SetServers(serverlist);
//各服务器之间负载均衡的设置比例
pool.SetWeights(new int[] { 1 });
pool.Initialize();
//创建一个Memcached的客户端对象
mc = new MemcachedClient();
mc.PoolName = "test";
//是否启用压缩数据:如果启用了压缩,数据压缩长于门槛的数据将被储存在压缩的形式
mc.EnableCompression = false;
}
/// <summary>
/// 插入值
/// </summary>
/// <param name="key">建</param>
/// <param name="value">值</param>
/// <param name="expiry">过期时间</param>
/// <returns></returns>
public static bool Set(string key, object value,DateTime expiry){
return mc.Set(key, value, expiry);
}
/// <summary>
/// 获取值
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static object Get(string key)
{
return mc.Get(key);
}
}
}
BaseController:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using WebDemo.Models;
namespace WebDemo.Controllers
{
public class BaseController : Controller
{
//用来保存当前的用户信息
public UserInfo LoginUser { get; set; }
//通过过滤器来实现每个页面的检查
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
//从cookie中获取咱们的 登录的sessionId
string sessionId = Request["sessionId"];
//如果sessionid为空值,则跳转到登录页面
if (string.IsNullOrEmpty(sessionId))
{
//return RedirectToAction("Login", "Logon");
Response.Redirect("/Logon/Index");
}
object obj = MemcacheHelper.Get(sessionId);
UserInfo user = obj as UserInfo;
if (user == null)
{
Response.Redirect("/Logon/Index");
}
LoginUser = user;
//实现session的滑动机制
MemcacheHelper.Set(sessionId, user, DateTime.Now.AddMinutes(20));
}
}
}
MemcachedController.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Memcached.ClientLibrary;
namespace WebDemo.Controllers
{
public class MemcachedController : BaseController
{
//
// GET: /Memcached/
public ActionResult Index()
{
//初始化memcached 服务器端集群列表。
String[] serverlist = {"127.0.0.1:11211"};
//初始化Memcache服务器的池
SockIOPool pool = SockIOPool.GetInstance("test");
//设置怎么mem池连接点服务器端。
pool.SetServers(serverlist);
pool.Initialize();
//创建了一个mem客户端的代理类。
var mc = new MemcachedClient();
mc.PoolName = "test";
mc.EnableCompression = false;
mc.Set("gz2", "hahaha", DateTime.Now.AddSeconds(15));
pool.Shutdown(); //关闭连接池
return Content("ok");
}
}
}
LogonController.cs:
using System;
using System.Linq;
using System.Web.Mvc;
using WebDemo.Models;
namespace WebDemo.Controllers
{
public class LogonController : Controller
{
//
// GET: /Logon/
public ActionResult Index()
{
return View();
}
public ActionResult Login(UserInfo user)
{
//创建一个DbContext对象,这样写不是很合理,先留个问题。(使用EF的code-first时需要注意的点)
var dbContext = new SchoolDbContext();
UserInfo loginUser = dbContext.UserInfo.FirstOrDefault(u => u.UName.Equals(user.UName) && u.UPwd.Equals(user.UPwd));
if (loginUser == null)
{
return Content("用户名密码错误!");
}
Guid sessionId = Guid.NewGuid(); //申请了一个模拟的GUID:SessionId
//把sessionid写到客户端浏览器里面去了(一定要把sessionid写到客户端,这样用户在访问其他web资源的时候,就会把cookie中的信息传给服务器,然后通过sessionid的key到Memcached中去取对应的值)
Response.Cookies["sessionId"].Value = sessionId.ToString();
//再把用户的信息插入到Memcached中
MemcacheHelper.Set(sessionId.ToString(), loginUser, DateTime.Now.AddMinutes(20));
return Content("ok");
}
public ActionResult ValidateCode()
{
var helper = new ValidateCodeHelper();
string strCode = helper.CreateValidateCode(4);
Session["validateCode"] = strCode;
byte[] byteData = helper.CreateValidateGraphic(strCode);
return File(byteData, "image/jpeg");
}
}
}
2、Models文件夹:
SchoolDbContext.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace WebDemo.Models
{
public class SchoolDbContext :DbContext
{
//使用EF的code-first,如果数据库中没有数据名字为MemCachedDemo,则调用CreateIfNotExists方法会创建数据库
public SchoolDbContext()
: base("name=MemCachedDemo")
{
this.Database.CreateIfNotExists();
}
public virtual DbSet<Student> Student { get; set; }
public virtual DbSet<UserInfo> UserInfo { get; set; }
}
}
Student.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace WebDemo.Models
{
[Serializable]
public class Student
{
[StringLength(32)]
public virtual string SName { get; set; }
[StringLength(32)]
public virtual string Address { get; set; }
[Key]
public virtual int Id { get; set; }
}
}
UserInfo.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;
namespace WebDemo.Models
{
[Serializable]
public class UserInfo
{
public string UName { get; set; }
[Required]
[MaxLength(32)]
public string UPwd { get; set; }
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
}
}
ValidateCodeHelper.cs
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
namespace WebDemo
{
public class ValidateCodeHelper
{
/// <summary>
/// 验证码的最大长度
/// </summary>
public int MaxLength
{
get { return 10; }
}
/// <summary>
/// 验证码的最小长度
/// </summary>
public int MinLength
{
get { return 1; }
}
/// <summary>
/// 生成验证码
/// </summary>
/// <param name="length">指定验证码的长度</param>
/// <returns></returns>
public string CreateValidateCode(int length)
{
var randMembers = new int[length];
var validateNums = new int[length];
string validateNumberStr = "";
//生成起始序列值
var seekSeek = unchecked((int) DateTime.Now.Ticks);
var seekRand = new Random(seekSeek);
int beginSeek = seekRand.Next(0, Int32.MaxValue - length*10000);
var seeks = new int[length];
for (int i = 0; i < length; i++)
{
beginSeek += 10000;
seeks[i] = beginSeek;
}
//生成随机数字
for (int i = 0; i < length; i++)
{
var rand = new Random(seeks[i]);
int pownum = 1*(int) Math.Pow(10, length);
randMembers[i] = rand.Next(pownum, Int32.MaxValue);
}
//抽取随机数字
for (int i = 0; i < length; i++)
{
string numStr = randMembers[i].ToString();
int numLength = numStr.Length;
var rand = new Random();
int numPosition = rand.Next(0, numLength - 1);
validateNums[i] = Int32.Parse(numStr.Substring(numPosition, 1));
}
//生成验证码
for (int i = 0; i < length; i++)
{
validateNumberStr += validateNums[i].ToString();
}
return validateNumberStr;
}
/// <summary>
/// 创建验证码的图片
/// </summary>
/// <param name="containsPage">要输出到的page对象</param>
/// <param name="validateNum">验证码</param>
public void CreateValidateGraphic(string validateCode, HttpResponse Response)
{
var image = new Bitmap((int) Math.Ceiling(validateCode.Length*12.0), 22);
Graphics g = Graphics.FromImage(image);
try
{
//生成随机生成器
var random = new Random();
//清空图片背景色
g.Clear(Color.White);
//画图片的干扰线
for (int i = 0; i < 25; i++)
{
int x1 = random.Next(image.Width);
int x2 = random.Next(image.Width);
int y1 = random.Next(image.Height);
int y2 = random.Next(image.Height);
g.DrawLine(new Pen(Color.Silver), x1, y1, x2, y2);
}
var font = new Font("Arial", 12, (FontStyle.Bold | FontStyle.Italic));
var brush = new LinearGradientBrush(new Rectangle(0, 0, image.Width, image.Height),
Color.Blue, Color.DarkRed, 1.2f, true);
g.DrawString(validateCode, font, brush, 3, 2);
//画图片的前景干扰点
for (int i = 0; i < 100; i++)
{
int x = random.Next(image.Width);
int y = random.Next(image.Height);
image.SetPixel(x, y, Color.FromArgb(random.Next()));
}
//画图片的边框线
g.DrawRectangle(new Pen(Color.Silver), 0, 0, image.Width - 1, image.Height - 1);
//保存图片数据
var stream = new MemoryStream();
image.Save(stream, ImageFormat.Jpeg);
//输出图片流
Response.Clear();
Response.ContentType = "image/jpeg";
Response.BinaryWrite(stream.ToArray());
}
finally
{
g.Dispose();
image.Dispose();
}
}
/// <summary>
/// 得到验证码图片的长度
/// </summary>
/// <param name="validateNumLength">验证码的长度</param>
/// <returns></returns>
public static int GetImageWidth(int validateNumLength)
{
return (int) (validateNumLength*12.0);
}
/// <summary>
/// 得到验证码的高度
/// </summary>
/// <returns></returns>
public static double GetImageHeight()
{
return 22.5;
}
//C# MVC 升级版
/// <summary>
/// 创建验证码的图片
/// </summary>
/// <param name="containsPage">要输出到的page对象</param>
/// <param name="validateNum">验证码</param>
public byte[] CreateValidateGraphic(string validateCode)
{
var image = new Bitmap((int) Math.Ceiling(validateCode.Length*12.0), 22);
Graphics g = Graphics.FromImage(image);
try
{
//生成随机生成器
var random = new Random();
//清空图片背景色
g.Clear(Color.White);
//画图片的干扰线
for (int i = 0; i < 25; i++)
{
int x1 = random.Next(image.Width);
int x2 = random.Next(image.Width);
int y1 = random.Next(image.Height);
int y2 = random.Next(image.Height);
g.DrawLine(new Pen(Color.Silver), x1, y1, x2, y2);
}
var font = new Font("Arial", 12, (FontStyle.Bold | FontStyle.Italic));
var brush = new LinearGradientBrush(new Rectangle(0, 0, image.Width, image.Height),
Color.Blue, Color.DarkRed, 1.2f, true);
g.DrawString(validateCode, font, brush, 3, 2);
//画图片的前景干扰点
for (int i = 0; i < 100; i++)
{
int x = random.Next(image.Width);
int y = random.Next(image.Height);
image.SetPixel(x, y, Color.FromArgb(random.Next()));
}
//画图片的边框线
g.DrawRectangle(new Pen(Color.Silver), 0, 0, image.Width - 1, image.Height - 1);
//保存图片数据
var stream = new MemoryStream();
image.Save(stream, ImageFormat.Jpeg);
//输出图片流
return stream.ToArray();
}
finally
{
g.Dispose();
image.Dispose();
}
}
}
}
视图:
Home文件夹中Index.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<div>
</div>
</body>
</html>
Logon文件夹中Index.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="../../Scripts/jquery-1.7.1.js"></script>
<script src="../../Scripts/jquery.unobtrusive-ajax.min.js"></script>
<script type="text/javascript">
if (window.parent.window != window) {
window.top.location.href = "/Login/Index";
}
function changeCheckCode() {
var newUrl = $("#img").attr("src") + 1;
$("#img").attr("src", newUrl);
}
function afterLogin(data) {
alert(data);
if (data != "ok") {
alert(data);
changeCheckCode();
} else {
window.location.href = "/Home/Index";
}
}
</script>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
body {
background: #4974A4;
text-align: center;
}
#login {
font-size: 12px;
margin: 0 auto;
width: 740px;
}
#loginlogo {
background: url('/Content/Images/login/logo.png') no-repeat;
height: 100px;
margin-top: 50px;
overflow: hidden;
width: 700px;
}
#loginpanel {
height: 300px;
position: relative;
width: 729px;
}
.panel-h {
background: url('/Content/Images/login/panel-h.gif') no-repeat;
height: 20px;
left: 0px;
position: absolute;
top: 0px;
width: 729px;
z-index: 3;
}
.panel-f {
background: url('/Content/Images/login/panel-f.gif') no-repeat;
bottom: 0px;
height: 13px;
left: 0px;
position: absolute;
width: 729px;
z-index: 3;
}
.panel-c {
background: url('/Content/Images/login/panel-c.gif') repeat-y;
height: 300px;
width: 729px;
z-index: 2;
}
.panel-c-l {
left: 60px;
position: absolute;
top: 40px;
}
.panel-c-r {
line-height: 200%;
position: absolute;
right: 20px;
text-align: left;
top: 50px;
width: 222px;
}
.panel-c-l h3 {
color: #556A85;
margin-bottom: 10px;
}
.panel-c-l td { padding: 7px; }
.login-text {
background: #f9f9f9;
border: 1px solid #e9e9e9;
height: 24px;
left: 24px;
}
.login-text-focus { border: 1px solid #E6BF73; }
.login-btn {
background: url('/Content/Images/login/login-btn.gif') no-repeat;
border: none;
color: #E9FFFF;
cursor: pointer;
height: 29px;
line-height: 29px;
overflow: hidden;
width: 114px;
}
#txtUsername, #code, #txtPassword { width: 191px; }
#logincopyright {
color: White;
margin-top: 50px;
text-align: center;
}
a { color: Black; }
a:hover {
color: Red;
text-decoration: underline;
}
</style>
</head>
<body style="padding: 10px">
@using (Ajax.BeginForm("Login", "Logon", new AjaxOptions {OnSuccess = "afterLogin"}))
{
<div id="login">
<div id="loginlogo">
</div>
<div id="loginpanel">
<div class="panel-h">
</div>
<div class="panel-c">
<div class="panel-c-l">
<table cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td align="left" colspan="2">
<h3>
请用登录</h3>
</td>
</tr>
<tr>
<td align="right">
账号:
</td>
<td align="left">
<input type="text" name="UName" value="admin" id="UName" class="login-text" />
</td>
</tr>
<tr>
<td align="right">
密码:
</td>
<td align="left">
<input type="password" name="UPwd" id="UPwd" value="123" class="login-text" />
</td>
</tr>
<tr>
<td>
验证码:
</td>
<td align="left">
<input type="text" class="login-text" id="code" name="vCode" value="1" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<img id="img" onclick=" changeCheckCode() " src="/Logon/ValidateCode?id4=1" style="float: left; height: 24px;" />
<div style="float: left; margin-left: 5px; margin-top: 10px;">
<a href="javascript:void(0)" onclick=" changeCheckCode();return false; ">看不清,换一张</a>
</div>
</td>
</tr>
<tr>
<td align="center" colspan="2">
<input type="submit" id="btnLogin" value="登录" class="login-btn" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel-f">
</div>
</div>
<div id="logincopyright">
</div>
</div>
}
</body>
</html>
Sql脚本
USE [MemCachedDemo]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[UserInfoes](
[UserId] [int] IDENTITY(1,1) NOT NULL,
[UName] [nvarchar](max) NULL,
[UPwd] [nvarchar](32) NOT NULL,
CONSTRAINT [PK_dbo.UserInfoes] PRIMARY KEY CLUSTERED
(
[UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Students](
[Id] [int] IDENTITY(1,1) NOT NULL,
[SName] [nvarchar](32) NULL,
[Address] [nvarchar](32) NULL,
CONSTRAINT [PK_dbo.Students] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO