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);
    }
}
IServiceList
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);
                }
            }
        }
    }
}
ServiceList
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)
        {
            
        }
    }
}
Program

然后开启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)
        {
            
        }
    }
}

性能图:

 

 对的,一个秒杀。一个毫秒杀,所以越复杂越能展示出“内存索引”的强大之处。

源码下载:

源码下载

 

posted on 2020-09-08 16:41  yanfeifei  阅读(144)  评论(0)    收藏  举报

导航