一、从单机到分布式
现在三台机器组成一个Web的应用集群,其中一台机器用户登录,然后其他另外两台机器如何共享登录状态?
解决方案:
1、AspNet进程外的Session 。
2、用数据库存数等钱登录状态。
3、Memcache。
二、为什么用Memcache?
1、解决高并发访问数据库带来的死锁
2、多用户端共享缓存
三、Memcache原理
其实memcache是一种windows服务,客户端发来的请求,都会被Socket服务器端接受到。存数使用键值对存储的。客户端进行存储的时候,就是找最接近value的大小的存储空间来存储,当然就造成了内存浪费,不过利大于弊。
四、安装和配置Memcache
首先我们看没有安装Memcache之前的服务:
可以看到M开头的没有Memcache.exe
我们把下下来的Memcache放在一个目录下,然后在命令窗口下运行这个目录下的Memcache.exe。嫌麻烦的话,可以直接运行。
安装命令:将Memcache.exe安装为Windows服务:Memcache.exe-d install。
启动命令:启动Memcache服务:Memcache.exe-d start。
这样我们就可以在服务器端看到了。
启动Memcache服务(windows命令):netstart "Memcache Server"
停止Memcache服务(windows命令):netstop "Memcache Server"
测试memcache是否连接成功:
启动命令窗口输入:telnet 127.0.0.1 11211
[提示错误:'telnet' 不是内部或外部命令,也不是可运行的程序或批处理文件。]
注:windows7带有telnet,只是默认没有安装而已。
解决方法:
依次打开“开始”→“控制面板”→“打开或关闭Windows功能”,在打开的窗口处,寻找并勾选“Telnet客户端”,然后点击“确定”。顺利安装后,再在运行下输入此命令就OK了。
输入:stats,出现Memcache当前的状态。
添加新记录:addKeyName 0 0 ValueByteLength [回车] ValueContent。
删除记录: delete KeyName。
添加或更新记录: set KeyName 0 0 ValueByteLength [回车] ValueContent。
更新记录: replace KeyName 0 0 ValueByteLength [回车] ValueContent。
OK了。
五、MVC+EF(CodeFirst)+Memcache代码:
EF有三种生成数据库的模式:ModelFirst、EntityFirst、CodeFirst。这里我们用的是CodeFirst,连接Sqlserver的数据库。
我们先看Model中的数据库上下文SchoolDbContext:
<span style="font-size:18px;">using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace WebDemo.Models
{
public class SchoolDbContext :DbContext
{
public SchoolDbContext()
: base("name=MySqlDemo")
{
this.Database.CreateIfNotExists(); //这里是数据库是否存在,如果不存在,就会新建一个数据库
}
public virtual DbSet<Student> Student { get; set; } //数据库中的实体
public virtual DbSet<UserInfo> UserInfo { get; set; }
}
}</span>
Model中的UserInfo实体:
<span style="font-size:18px;">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; }
}
}</span>
Student表就不写了,一样的。我们就只用UserInfo表来测试。
Model中的MemcacheHelper:
<span style="font-size:18px;">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()
{
String[] serverlist = { "127.0.0.1:11211" }; //连接的主机和端口号
// initialize the pool for memcache servers
SockIOPool pool = SockIOPool.GetInstance("test");
pool.SetServers(serverlist);
pool.Initialize();
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">键值对中的key值</param>
/// <returns></returns>
public static object Get(string key)
{
return mc.Get(key);
}
}
}</span>
CodeFirst最主要的就是配置文件了,用asp.net与sqlserver的配置如下:
<span style="font-size:18px;"> <connectionStrings>
<add name="MySqlDemo" connectionString="Data Source=.;user id=sa;password=123456;persist security info=True;database=MySqlDemo" providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration></span>
这里一定要写对,如果出现这样”Data Source“或”Server“无法识别,就代表这个配置文件是错误的。一定有地方写错了。
Controller中的HomeController:
<span style="font-size:18px;">using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using WebDemo.Models;
namespace WebDemo.Controllers
{
public class HomeController : BaseController //进入任何非登录页面,都要集成BaseController这个Controller,继承方法</span><pre name="code" class="csharp"><pre name="code" class="csharp"><span style="font-size:18px;">OnActionExecuting。</span>
{ // // GET: /Home/ /// <summary> /// 返回home/index /// </summary> /// <returns></returns> public ActionResult Index() { return Content("home index"); } }}
对应的View中的Index:
<span style="font-size:18px;">@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<div>
欢迎登陆!!!!
</div>
</body>
</html></span>
Controller中的LogonController跟这个是一样的,就不写了。
<span style="font-size:18px;">using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using WebDemo.Models;
namespace WebDemo.Controllers
{
public class LogonController : Controller
{
//
// GET: /Logon/
public ActionResult Index()
{
return View();
}
/// <summary>
/// 登陆信息
/// </summary>
/// <param name="user">User实体</param>
/// <returns></returns>
public ActionResult Login(UserInfo user)
{
SchoolDbContext dbContext =new SchoolDbContext();
var loginUser = dbContext.UserInfo.Where(u => u.UName.Equals(user.UName) && u.UPwd.Equals(user.UPwd)).FirstOrDefault();
if (loginUser == null)
{
return Content("用户名密码错误!");
}
else
{
Guid sessionId = Guid.NewGuid();//申请了一个模拟的GUID:SessionId
//把sessionid写到客户端浏览器里面去累
Response.Cookies["sessionId"].Value = sessionId.ToString();
//写入缓存
MemcacheHelper.Set(sessionId.ToString(), loginUser, DateTime.Now.AddMinutes(20));
//用户登录成功之后要保存用户的登录的数据:
//Session["loginUser"] = loginUser;
return Content("ok");
}
}
}
}</span>
大家可以看到上面的Response.Cookies["sessionId"]是将sessionid写到客户端浏览器中去的。
看这张图,大家就很明白了吧,当浏览器客户端登陆请求发到服务器端的时候,就会查询是否存在MM分布缓存,如果有就会调用BaseController中的OnActionExecuting方法(这个方法其实就是要所有的Controller执行的时候,都要执行的方法)。其实就是上面图中的后续请求。
Controller中的BaseController(这个类是抽象出来的):
<span style="font-size:18px;">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"];
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;
MemcacheHelper.Set(sessionId, user, DateTime.Now.AddMinutes(20));
}
}
}
</span>
视图:Logon/Index:
<span style="font-size:18px;">@{
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) {
if (data != "ok") {
alert(data);
changeCheckCode();
} else {
window.location.href = "/Home/Index";
}
}
</script>
<style type="text/css">
*
{
padding: 0;
margin: 0;
}
body
{
text-align: center;
background: #4974A4;
}
#login
{
width: 740px;
margin: 0 auto;
font-size: 12px;
}
#loginlogo
{
width: 700px;
height: 100px;
overflow: hidden;
background: url('/Content/Images/login/logo.png') no-repeat;
margin-top: 50px;
}
#loginpanel
{
width: 729px;
position: relative;
height: 300px;
}
.panel-h
{
width: 729px;
height: 20px;
background: url('/Content/Images/login/panel-h.gif') no-repeat;
position: absolute;
top: 0px;
left: 0px;
z-index: 3;
}
.panel-f
{
width: 729px;
height: 13px;
background: url('/Content/Images/login/panel-f.gif') no-repeat;
position: absolute;
bottom: 0px;
left: 0px;
z-index: 3;
}
.panel-c
{
z-index: 2;
background: url('/Content/Images/login/panel-c.gif') repeat-y;
width: 729px;
height: 300px;
}
.panel-c-l
{
position: absolute;
left: 60px;
top: 40px;
}
.panel-c-r
{
position: absolute;
right: 20px;
top: 50px;
width: 222px;
line-height: 200%;
text-align: left;
}
.panel-c-l h3
{
color: #556A85;
margin-bottom: 10px;
}
.panel-c-l td
{
padding: 7px;
}
.login-text
{
height: 24px;
left: 24px;
border: 1px solid #e9e9e9;
background: #f9f9f9;
}
.login-text-focus
{
border: 1px solid #E6BF73;
}
.login-btn
{
width: 114px;
height: 29px;
color: #E9FFFF;
line-height: 29px;
background: url('/Content/Images/login/login-btn.gif') no-repeat;
border: none;
overflow: hidden;
cursor: pointer;
}
#txtUsername, #code, #txtPassword
{
width: 191px;
}
#logincopyright
{
text-align: center;
color: White;
margin-top: 50px;
}
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>
</tr>
<tr>
<td align="center" colspan="2">
<input type="submit" id="btnLogin" value="登录" class="login-btn" />
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-c-r">
<p>
请从左侧输入登录账号和密码登录</p>
<p>
如果遇到系统问题,请联系网络管理员。</p>
<p>
如果没有账号,请联系网站管理员。
</p>
<p>
......</p>
</div>
</div>
<div class="panel-f">
</div>
</div>
<div id="logincopyright">
Copyright ? 2013 itcast.com
</div>
</div>
}
</body>
</html>
</span>
这样整个系统就写完了。
参考:
传智播客--互联网架构快餐之分布式缓存。
总结:
单机实现分布式,是大数据时代的要求,解决了高并发访问数据库死锁,实现了多客户端共享缓存。