今天是wcf系列的第四天,也該出手壓軸戲了。嗯,現在的大型架構,都是神馬的,
nginx雞羣,iis雞羣,wcf雞羣,DB雞羣,由一個人作戰變成了羣毆.......
今天我就分享下wcf雞羣,高性能架構中一種常用的手法就是在內存中維護一個叫做“索引”的內存數據庫,
在實戰中利用“索引”這個概念做出"海量數據“的秒殺。
好,先上圖:
這個圖明白人都能看得懂吧。因爲我的系列偏重於wcf,所以我重點說下”心跳檢測“的實戰手法。
第一步:上一下項目的結構,才能做到心中有數。
第二步:“LoadDBService”這個是控制檯程序,目的就是從數據庫抽出關係模型加載在內存數據庫中,因爲這些東西會涉及一些算法的知識,
在這裏就不寫算法了,就簡單的模擬一下。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Web.Script.Serialization;
using System.IO;
using System.Xml.Serialization;
using System.Xml;
using Common;
namespace LoadDBData
{
class Program
{
static void Main(string[] args)
{
//模擬從數據庫加載索引到內存中,形成內存中的數據庫
//這裏的 "Dictionary" 用來表達“一個用戶註冊過多少店鋪“,即UserID與ShopID的一對多關係
SerializableDictionary<int, List<int>> dic = new SerializableDictionary<int, List<int>>();
List<int> shopIDList = new List<int>();
for (int shopID = 300000; shopID < 300050; shopID++)
shopIDList.Add(shopID);
int UserID = 15;
//假設這裏已經維護好了UserID與ShopID的關係
dic.Add(UserID, shopIDList);
XmlSerializer xml = new XmlSerializer(dic.GetType());
var memoryStream = new MemoryStream();
xml.Serialize(memoryStream, dic);
memoryStream.Seek(0, SeekOrigin.Begin);
//將Dictionary持久化,相當於模擬保存在Mencache裏面
File.AppendAllText("F://1.txt", Encoding.UTF8.GetString(memoryStream.ToArray()));
Console.WriteLine("數據加載成功!");
Console.Read();
}
}
}
因爲Dictionary不支持序列化,所以我從網上拷貝了一份代碼讓其執行序列化
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Xml;
using System.Xml.Schema;
using System.Runtime.Serialization;
namespace Common
{
///<summary>
/// 標題:支持 XML 序列化的 Dictionary
///</summary>
///<typeparam name="TKey"></typeparam>
///<typeparam name="TValue"></typeparam>
[XmlRoot("SerializableDictionary")]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
public SerializableDictionary()
: base()
{
}
public SerializableDictionary(IDictionary<TKey, TValue> dictionary)
: base(dictionary)
{
}
public SerializableDictionary(IEqualityComparer<TKey> comparer)
: base(comparer)
{
}
public SerializableDictionary(int capacity)
: base(capacity)
{
}
public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer)
: base(capacity, comparer)
{
}
protected SerializableDictionary(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
///<summary>
/// 從對象的 XML 表示形式生成該對象
///</summary>
///<param name="reader"></param>
public void ReadXml(System.Xml.XmlReader reader)
{
XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
bool wasEmpty = reader.IsEmptyElement;
reader.Read();
if (wasEmpty)
return;
while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
{
reader.ReadStartElement("item");
reader.ReadStartElement("key");
TKey key = (TKey)keySerializer.Deserialize(reader);
reader.ReadEndElement();
reader.ReadStartElement("value");
TValue value = (TValue)valueSerializer.Deserialize(reader);
reader.ReadEndElement();
this.Add(key, value);
reader.ReadEndElement();
reader.MoveToContent();
}
reader.ReadEndElement();
}
/**/
///<summary>
/// 將對象轉換爲其 XML 表示形式
///</summary>
///<param name="writer"></param>
public void WriteXml(System.Xml.XmlWriter writer)
{
XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
foreach (TKey key in this.Keys)
{
writer.WriteStartElement("item");
writer.WriteStartElement("key");
keySerializer.Serialize(writer, key);
writer.WriteEndElement();
writer.WriteStartElement("value");
TValue value = this[key];
valueSerializer.Serialize(writer, value);
writer.WriteEndElement();
writer.WriteEndElement();
}
}
}
}
第三步: "HeartBeatService"也做成了一個控制檯程序,爲了圖方便,把Contract和Host都放在一個控制檯程序中,
代碼中加入了註釋,看一下就會懂的。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace HeartBeatService
{
//CallbackContract:這個就是Client實現此接口,方便服務器端通知客戶端
[ServiceContract(CallbackContract = typeof(ILiveAddressCallback))]
public interface IAddress
{
///<summary>
/// 此方法用於Search啓動後,將Search地址插入到此處
///</summary>
///<param name="address"></param>
[OperationContract(IsOneWay = true)]
void AddSearch(string address);
///<summary>
/// 此方法用於IIS端獲取search地址
///</summary>
///<param name="address"></param>
[OperationContract(IsOneWay = true)]
void GetService(string address);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Timers;
using System.IO;
using System.Collections.Concurrent;
using SearhService;
using ClientService;
namespace HeartBeatService
{
//InstanceContextMode:主要是管理上下文的實例,此處是single,也就是單體
//ConcurrencyMode: 主要是用來控制實例中的線程數,此處是Multiple,也就是多線程
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class Address : IAddress
{
static List<string> search = new List<string>();
static object obj = new object();
///<summary>
/// 此靜態構造函數用來檢測存活的Search個數
///</summary>
static Address()
{
Timer timer = new Timer();
timer.Interval = 6000;
timer.Elapsed += (sender, e) =>
{
Console.WriteLine("\n***************************************************************************");
Console.WriteLine("當前存活的Search爲:");
lock (obj)
{
//遍歷當前存活的Search
foreach (var single in search)
{
ChannelFactory<IProduct> factory = null;
try
{
//當Search存在的話,心跳服務就要定時檢測Search是否死掉,也就是定時的連接Search來檢測。
factory = new ChannelFactory<IProduct>(new NetTcpBinding(SecurityMode.None), new EndpointAddress(single));
factory.CreateChannel().TestSearch();
factory.Close();
Console.WriteLine(single);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
//如果拋出異常,則說明此search已經掛掉
search.Remove(single);
factory.Abort();
Console.WriteLine("\n當前時間:" + DateTime.Now + " ,存活的Search有:" + search.Count() + "個");
}
}
}
//最後統計下存活的search有多少個
Console.WriteLine("\n當前時間:" + DateTime.Now + " ,存活的Search有:" + search.Count() + "個");
};
timer.Start();
}
public void AddSearch(string address)
{
lock (obj)
{
//是否包含相同的Search地址
if (!search.Contains(address))
{
search.Add(address);
//search添加成功後就要告訴來源處,此search已經被成功載入。
var client = OperationContext.Current.GetCallbackChannel<ILiveAddressCallback>();
client.LiveAddress(address);
}
}
}
public void GetService(string address)
{
Timer timer = new Timer();
timer.Interval = 1000;
timer.Elapsed += (obj, sender) =>
{
try
{
//這個是定時的檢測IIS是否掛掉
var factory = new ChannelFactory<IServiceList>(new NetTcpBinding(SecurityMode.None),
new EndpointAddress(address));
factory.CreateChannel().AddSearchList(search);
factory.Close();
timer.Interval = 10000;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
};
timer.Start();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace HeartBeatService
{
///<summary>
/// 等客戶端實現後,讓客戶端約束一下,只能是這個LiveAddress方法
///</summary>
public interface ILiveAddressCallback
{
[OperationContract(IsOneWay = true)]
void LiveAddress(string address);
}
}
第四步: 我們開一下心跳,預覽下效果:
是的,心跳現在正在檢測是否有活着的Search。
第五步:"SearhService" 這個Console程序就是WCF的search,主要用於從MemerCache裏面讀取索引。
記得要添加一下對“心跳服務”的服務引用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace SearhService
{
// 注意: 使用“重構”菜單上的“重命名”命令,可以同時更改代碼和配置文件中的接口名“IService1”。
[ServiceContract]
public interface IProduct
{
[OperationContract]
List<int> GetShopListByUserID(int userID);
[OperationContract]
void TestSearch();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using Common;
using System.Xml;
using System.IO;
using System.Xml.Serialization;
namespace SearhService
{
public class Product : IProduct
{
public List<int> GetShopListByUserID(int userID)
{
//模擬從MemCache中讀取索引
SerializableDictionary<int, List<int>> dic = new SerializableDictionary<int, List<int>>();
byte[] bytes = Encoding.UTF8.GetBytes(File.ReadAllText("F://1.txt", Encoding.UTF8));
var memoryStream = new MemoryStream();
memoryStream.Write(bytes, 0, bytes.Count());
memoryStream.Seek(0, SeekOrigin.Begin);
XmlSerializer xml = new XmlSerializer(dic.GetType());
var obj = xml.Deserialize(memoryStream) as Dictionary<int, List<int>>;
return obj[userID];
}
public void TestSearch() { }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Configuration;
using System.Timers;
using SearhService.HeartBeatService;
namespace SearhService
{
public class SearchHost : IAddressCallback
{
static DateTime startTime;
public static void Main()
{
ServiceHost host = new ServiceHost(typeof(Product));
host.Open();
AddSearch();
Console.Read();
}
static void AddSearch()
{
startTime = DateTime.Now;
Console.WriteLine("Search服務發送中.....\n\n*************************************************\n");
try
{
var heartClient = new AddressClient(new InstanceContext(new SearchHost()));
string search = ConfigurationManager.AppSettings["search"];
heartClient.AddSearch(search);
}
catch (Exception ex)
{
Console.WriteLine("Search服務發送失敗:" + ex.Message);
}
}
public void LiveAddress(string address)
{
Console.WriteLine("恭喜你," + address + "已被心跳成功接收!\n");
Console.WriteLine("發送時間:" + startTime + "\n接收時間:" + DateTime.Now);
}
}
}
第六步:此時Search服務已經建好,我們可以測試當Search開啓獲取關閉對心跳有什麼影響:
Search開啓時:
Search關閉時:
對的,當Search關閉時,心跳檢測該Search已經死掉,然後只能從集羣中剔除。
當然,我們可以將Search拷貝N份,部署在N臺機器中,只要修改一下endpoint地址就OK了,這一點明白人都會。
第七步:"ClientService" 這裏也就指的是IIS,此時我們也要添加一下對心跳的服務引用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace ClientService
{
[ServiceContract]
public interface IServiceList
{
[OperationContract]
void AddSearchList(List<string> search);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Configuration;
using System.Timers;
using System.Threading;
namespace ClientService
{
public class ServiceList : IServiceList
{
public static List<string> searchList = new List<string>();
static object obj = new object();
public static string Search
{
get
{
lock (obj)
{
//如果心跳沒及時返回地址,客戶端就在等候
if (searchList.Count == 0)
Thread.Sleep(1000);
return searchList[new Random().Next(0, searchList.Count)];
}
}
set
{
}
}
public void AddSearchList(List<string> search)
{
lock (obj)
{
searchList = search;
Console.WriteLine("************************************");
Console.WriteLine("當前存活的Search爲:");
foreach (var single in searchList)
{
Console.WriteLine(single);
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Configuration;
using System.Threading;
using ClientService.HeartBeatService;
using SearhService;
using BaseClass;
using System.Data;
using System.Diagnostics;
namespace ClientService
{
class Program : IAddressCallback
{
static void Main(string[] args)
{
ServiceHost host = new ServiceHost(typeof(ServiceList));
host.Open();
var client = new AddressClient(new InstanceContext(new Program()));
//配置文件中獲取iis的地址
var iis = ConfigurationManager.AppSettings["iis"];
//將iis的地址告訴心跳
client.GetService(iis);
//從集羣中獲取search地址來對Search服務進行調用
var factory = new ChannelFactory<IProduct>(new NetTcpBinding(SecurityMode.None), new EndpointAddress(ServiceList.Search));
//根據userid獲取了shopID的集合
var shopIDList = factory.CreateChannel().GetShopListByUserID(15);
//.......................... 後續就是我們將shopIDList做連接數據庫查詢(做到秒殺)
Console.Read();
}
public void LiveAddress(string address)
{
}
}
}
然後我們開啓Client,看看效果咋樣:
當然,search集羣后,client得到search的地址是隨機的,也就分擔了search的負擔,實現有福同享,有難同當的效果了。
最後: 我們做下性能檢測,看下“秒殺”和“毫秒殺”的效果。
首先在數據庫的User表和Shop插入了180萬和20萬的數據用於關聯。
ClientService改造後的代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Timers;
using System.Diagnostics;
using BaseClass;
using ClientService;
using ClientService.HeartBeatService;
using System.Configuration;
using SearhService;
namespace ClientService
{
class Program : IAddressCallback
{
static void Main(string[] args)
{
ServiceHost host = new ServiceHost(typeof(ServiceList));
host.Open();
var client = new AddressClient(new InstanceContext(new Program()));
//配置文件中獲取iis的地址
var iis = ConfigurationManager.AppSettings["iis"];
//將iis的地址告訴心跳
client.GetService(iis);
//從集羣中獲取search地址來對Search服務進行調用
var factory = new ChannelFactory<IProduct>(new NetTcpBinding(SecurityMode.None), new EndpointAddress(ServiceList.Search));
//根據userid獲取了shopID的集合
//比如說這裏的ShopIDList是通過索引交併集獲取的分頁的一些shopID
var shopIDList = factory.CreateChannel().GetShopListByUserID(15);
var strSql = string.Join(",", shopIDList);
Stopwatch watch = new Stopwatch();
watch.Start();
SqlHelper.Query("select s.ShopID,u.UserName,s.ShopName from [User] as u ,Shop as s where s.ShopID in(" + strSql + ")");
watch.Stop();
Console.WriteLine("通過wcf索引獲取的ID >>>花費時間:" + watch.ElapsedMilliseconds);
//普通的sql查詢花費的時間
StringBuilder builder = new StringBuilder();
builder.Append("select * from ");
builder.Append("(select ROW_NUMBER() over(order by s.ShopID) as NumberID, ");
builder.Append(" s.ShopID, u.UserName, s.ShopName ");
builder.Append("from Shop s left join [User] as u on u.UserID=s.UserID ");
builder.Append("where s.UserID=15) as array ");
builder.Append("where NumberID>300000 and NumberID<300050");
watch.Start();
SqlHelper.Query(builder.ToString());
watch.Stop();
Console.WriteLine("普通的sql分頁 >>>花費時間:" + watch.ElapsedMilliseconds);
Console.Read();
}
public void LiveAddress(string address)
{
}
}
}
性能圖:
對的,一個秒殺,一個是毫秒殺,所以越複雜越能展示出“內存索引”的強大之處。
源碼下載:HeartBeat.rar