wcf系列学习5天速成——第四天 wcf之分布式架构
今天是wcf系列的第四天,也该出手压轴戏了。嗯,现在的大型架构,都是神马的,
nginx鸡群,iis鸡群,wcf鸡群,DB鸡群,由一个人作战变成了群殴.......
今天我就分享下wcf鸡群,高性能架构中一种常用的手法就是在内存中维护一个叫做“索引”的内存数据库,
在实战中利用“索引”这个概念做出"海量数据“的秒杀。
好,先上图:

这个图明白人都能看的懂吧,因为我的系列偏重于wcf,所以我重点说下“心跳检测”的实战手法。
第一步:上一下项目的结构,才能做到心中有数。

第二步:“LoadDBService”这个是控制台程序,目的就是从数据库抽出关系模型加载到内存数据库中,因为这些东西会涉及一些算法的知识,
在这里就不写算法了,给简单的模拟一下。
1 using Common; 2 using System; 3 using System.Collections.Generic; 4 using System.IO; 5 using System.Linq; 6 using System.Text; 7 using System.Threading.Tasks; 8 using System.Xml.Serialization; 9 10 namespace LoadDbService 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 SerializableDictionary<int, List<int>> dic = new SerializableDictionary<int, List<int>>(); 17 List<int> shopIDList = new List<int>(); 18 for (int shopID = 300000; shopID < 300050; shopID++) 19 shopIDList.Add(shopID); 20 int UserID = 23; 21 //假设这里已经维护好了UserID与ShopID的关系 22 dic.Add(UserID, shopIDList); 23 XmlSerializer xml = new XmlSerializer(dic.GetType()); 24 var memoryStream = new MemoryStream(); 25 xml.Serialize(memoryStream, dic); 26 memoryStream.Seek(0, SeekOrigin.Begin); 27 //将Dictionary持久化,相当于模拟保存在Mencache里面 28 File.AppendAllText("E://微博//wcf//4//1.txt", Encoding.UTF8.GetString(memoryStream.ToArray())); 29 Console.WriteLine("数据加载成功!"); 30 Console.Read(); 31 } 32 } 33 }
因为Dictionary不支持序列化,所以我从网上拷贝了一份代码让其执行序列化
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Runtime.Serialization; 5 using System.Text; 6 using System.Threading.Tasks; 7 using System.Xml.Serialization; 8 9 namespace Common 10 { 11 /// <summary> 12 /// 标题:支持 XML 序列化得 Dictionary 13 /// </summary> 14 /// <typeparam name="Tkey"></typeparam> 15 /// <typeparam name="TValue"></typeparam> 16 /// 17 [XmlRoot("SerializableDictionary")] 18 public class SerializableDictionary<Tkey,TValue> : Dictionary<Tkey,TValue>,IXmlSerializable 19 { 20 public SerializableDictionary():base() 21 { 22 } 23 public SerializableDictionary(IDictionary<Tkey, TValue> dictionary) 24 : base(dictionary) 25 { 26 } 27 public SerializableDictionary(IEqualityComparer<Tkey> comparer) 28 : base(comparer) 29 { 30 } 31 public SerializableDictionary(int capacity) 32 : base(capacity) 33 { 34 } 35 public SerializableDictionary(int capacity, IEqualityComparer<Tkey> comparer) 36 : base(capacity, comparer) 37 { 38 } 39 public SerializableDictionary(SerializationInfo info,StreamingContext context) 40 : base(info, context) 41 { 42 } 43 public System.Xml.Schema.XmlSchema GetSchema() 44 { 45 return null; 46 } 47 /// <summary> 48 /// 从对象的xml表示形式生成该对象 49 /// </summary> 50 /// <param name="reader"></param> 51 public void ReadXml(System.Xml.XmlReader reader) 52 { 53 XmlSerializer keySerializer = new XmlSerializer(typeof(Tkey)); 54 XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue)); 55 bool wasEmpty = reader.IsEmptyElement; 56 reader.Read(); 57 if (wasEmpty) 58 { 59 return; 60 } 61 while(reader.NodeType!=System.Xml.XmlNodeType.EndElement) 62 { 63 reader.ReadStartElement("item"); 64 reader.ReadStartElement("key"); 65 Tkey key = (Tkey)keySerializer.Deserialize(reader); 66 reader.ReadEndElement(); 67 reader.ReadStartElement("value"); 68 TValue value = (TValue)valueSerializer.Deserialize(reader); 69 reader.ReadEndElement(); 70 this.Add(key,value); 71 reader.MoveToContent(); 72 } 73 reader.ReadEndElement(); 74 75 76 } 77 /// <summary> 78 /// 将对象转化为其XML表示形式 79 /// </summary> 80 /// <param name="writer"></param> 81 public void WriteXml(System.Xml.XmlWriter writer) 82 { 83 XmlSerializer keySerializer = new XmlSerializer(typeof(Tkey)); 84 XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue)); 85 foreach (Tkey key in this.Keys) 86 { 87 writer.WriteStartElement("item"); 88 writer.WriteStartElement("key"); 89 keySerializer.Serialize(writer,key); 90 writer.WriteEndElement(); 91 writer.WriteStartElement("value"); 92 TValue value = this[key]; 93 valueSerializer.Serialize(writer,value); 94 writer.WriteEndElement(); 95 writer.WriteEndElement(); 96 } 97 } 98 99 } 100 101 }
第三步:“HeartBeatService”也做成了一个控制台程序,为了图方便,把Contract和Host都放在一个控制程序中,
代码中加入了注释,看一下就会懂得。
IAddress.cs
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); } }
Address.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Runtime.Serialization; 5 using System.ServiceModel; 6 using System.Text; 7 using System.Timers; 8 using System.IO; 9 using System.Collections.Concurrent; 10 11 using ClientService; 12 using SearchService; 13 14 namespace HeartBeatService 15 { 16 //InstanceContextMode:主要是管理上下文的实例,此处是single,也就是单体 17 //ConcurrencyMode: 主要是用来控制实例中的线程数,此处是Multiple,也就是多线程 18 [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] 19 public class Address : IAddress 20 { 21 static List<string> search = new List<string>(); 22 23 static object obj = new object(); 24 25 /// <summary> 26 /// 此静态构造函数用来检测存活的Search个数 27 /// </summary> 28 static Address() 29 { 30 Timer timer = new Timer(); 31 timer.Interval = 6000; 32 timer.Elapsed += (sender, e) => 33 { 34 35 Console.WriteLine("\n***************************************************************************"); 36 Console.WriteLine("当前存活的Search为:"); 37 38 lock (obj) 39 { 40 //遍历当前存活的Search 41 foreach (var single in search) 42 { 43 ChannelFactory<IProduct> factory = null; 44 45 try 46 { 47 //当Search存在的话,心跳服务就要定时检测Search是否死掉,也就是定时的连接Search来检测。 48 factory = new ChannelFactory<IProduct>(new NetTcpBinding(SecurityMode.None), new EndpointAddress(single)); 49 factory.CreateChannel().TestSearch(); 50 factory.Close(); 51 52 Console.WriteLine(single); 53 54 } 55 catch (Exception ex) 56 { 57 Console.WriteLine(ex.Message); 58 59 //如果抛出异常,则说明此search已经挂掉 60 search.Remove(single); 61 factory.Abort(); 62 Console.WriteLine("\n当前时间:" + DateTime.Now + " ,存活的Search有:" + search.Count() + "个"); 63 } 64 } 65 } 66 67 //最后统计下存活的search有多少个 68 Console.WriteLine("\n当前时间:" + DateTime.Now + " ,存活的Search有:" + search.Count() + "个"); 69 }; 70 timer.Start(); 71 } 72 73 public void AddSearch(string address) 74 { 75 76 lock (obj) 77 { 78 //是否包含相同的Search地址 79 if (!search.Contains(address)) 80 { 81 search.Add(address); 82 83 //search添加成功后就要告诉来源处,此search已经被成功载入。 84 var client = OperationContext.Current.GetCallbackChannel<ILiveAddressCallback>(); 85 client.LiveAddress(address); 86 } 87 } 88 } 89 90 public void GetService(string address) 91 { 92 Timer timer = new Timer(); 93 timer.Interval = 1000; 94 timer.Elapsed += (obj, sender) => 95 { 96 try 97 { 98 //这个是定时的检测IIS是否挂掉 99 var factory = new ChannelFactory<IServiceList>(new NetTcpBinding(SecurityMode.None), 100 new EndpointAddress(address)); 101 102 factory.CreateChannel().AddSearchList(search); 103 104 factory.Close(); 105 106 timer.Interval = 10000; 107 } 108 catch (Exception ex) 109 { 110 Console.WriteLine(ex.Message); 111 } 112 }; 113 timer.Start(); 114 } 115 } 116 }
ILiveAddressCallback.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace HeartBeatService 8 { 9 /// <summary> 10 /// 等客户端实现后,让客户端约束一下,只能是这个LiveAddress方法 11 /// </summary> 12 public interface ILiveAddressCallback 13 { 14 [OperationContract(IsOneWay = true)] 15 void LiveAddress(string address); 16 } 17 }
第四步:我们开一下心跳 ,预览一下效果

是的,心跳现在正在检测是否有活着的Search。
第五步:“SearchService”这个Console程序就是WCF的search,主要用于从MenerCache里面读取索引。
记得要添加一下对“心跳服务”的服务引用。
IProduct.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.ServiceModel; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace SearchService 9 { 10 //注意:使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的接口名“IService1”。 11 [ServiceContract] 12 public interface IProduct 13 { 14 [OperationContract] 15 List<int> GetShopListByUserID(int userID); 16 17 [OperationContract] 18 void TestSearch(); 19 } 20 }
Product.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using Common; 7 using System.IO; 8 using System.Xml.Serialization; 9 namespace SearchService 10 { 11 public class Product : IProduct 12 { 13 public List<int> GetShopListByUserID(int userID) 14 { 15 //模拟从MemCache中读取索引 16 SerializableDictionary<int, List<int>> dic = new SerializableDictionary<int, List<int>>(); 17 18 byte[] bytes = Encoding.UTF8.GetBytes(File.ReadAllText("E://微博//wcf//4//1.txt", Encoding.UTF8)); 19 20 var memoryStream = new MemoryStream(); 21 22 memoryStream.Write(bytes, 0, bytes.Count()); 23 24 memoryStream.Seek(0, SeekOrigin.Begin); 25 26 XmlSerializer xml = new XmlSerializer(dic.GetType()); 27 28 var obj = xml.Deserialize(memoryStream) as Dictionary<int, List<int>>; 29 30 return obj[userID]; 31 } 32 33 public void TestSearch() 34 { 35 36 } 37 } 38 }
SearchHost.cs
1 using SearchService.HeartBeatService; 2 using System; 3 using System.Collections.Generic; 4 using System.Configuration; 5 using System.Linq; 6 using System.ServiceModel; 7 using System.Text; 8 using System.Threading.Tasks; 9 10 namespace SearchService 11 { 12 public class SearchHost: IAddressCallback 13 { 14 static DateTime startTime; 15 public static void Main() 16 { 17 18 ServiceHost host = new ServiceHost(typeof(Product)); 19 20 host.Open(); 21 22 AddSearch(); 23 24 Console.Read(); 25 } 26 static void AddSearch() 27 { 28 startTime = DateTime.Now; 29 Console.WriteLine("Search服务发送中......\n\n*******************************************************\n"); 30 try 31 { 32 var heartClient = new AddressClient(new InstanceContext(new SearchHost())); 33 string search = ConfigurationManager.AppSettings["search"]; 34 heartClient.AddSearch(search); 35 } 36 catch (Exception ex) 37 { 38 Console.WriteLine("Search服务发送失败:"+ex.Message); 39 } 40 } 41 42 public void LiveAddress(string address) 43 { 44 Console.WriteLine("恭喜你," +address+"已被心跳成功接收!\n"); 45 Console.WriteLine("发送时间:" +startTime+"\n接收时间:"+DateTime.Now); 46 } 47 } 48 }
第六步:此时Search服务已经建好,我们可以测试当Search开启获取关闭对心跳有什么影响:
Search开启时:

Search关闭时:

对的,当Search关闭时,心跳检测该Search已经死掉,然后只能从集群中剔除。
当然,我们可以将Search拷贝N份,部署在N台机器中,只要修改一下endpoint地址就OK了,这一点明白人都会。
第七步:"ClientService"这里也就指的是IIS,此时我们添加一下对心跳的服务引用。
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.Text; using System.Threading.Tasks; 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 { //如果心跳没及时返回地址,客户端就在等候 while (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 BaseClass; using ClientService.HeartBeatService; using SearchService; using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Linq; using System.ServiceModel; using System.Text; using System.Threading.Tasks; 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(23); //后续就是我们将shopIDList做数据库查询(做到秒杀) 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.UserID= u.UserID and s.ShopID IN(" + strsql + ")"); watch.Stop(); Console.WriteLine("通过wcf索引获取的ID>>>花费时间"+watch.ElapsedMilliseconds); 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 AS s LEFT JOIN [User] AS u ON u.UserID=s.UserID"); builder.Append(" WHERE s.UserID=23)"); builder.Append(" AS array 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) { } } }
然后开启Client,看看效果咋样:

当然,search集群后,client得到search的地址是随机 的,也就分担了search的负担,实现有福同享,有难同当的效果了。
最后:我们做下性能检测,看下“秒杀”和“毫秒杀”得效果。
首先在数据库user表和shop表插入了180万和20万的数据用于关联
ClientService改造后的代码:
using BaseClass; using ClientService.HeartBeatService; using SearchService; using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Linq; using System.ServiceModel; using System.Text; using System.Threading.Tasks; 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(23); //后续就是我们将shopIDList做数据库查询(做到秒杀) Console.WriteLine("获取shopID的个数为:" + shopIDList.Count()); 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.UserID= u.UserID and s.ShopID IN(" + strsql + ")"); watch.Stop(); Console.WriteLine("通过wcf索引获取的ID>>>花费时间"+watch.ElapsedMilliseconds); 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 AS s LEFT JOIN [User] AS u ON u.UserID=s.UserID"); builder.Append(" WHERE s.UserID=23)"); builder.Append(" AS array 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) { } } }
性能图:

对的,一个秒杀。一个毫秒杀,所以越复杂越能展示出“内存索引”的强大之处。
源码下载:
浙公网安备 33010602011771号