如何解决分布式系统中的跨时区问题[实例篇]

关于如何解决分布式系统中的跨时区问题,上一篇详细介绍了解决方案的实现原理,在这一篇中我们通过一个完整的例子来对这个问题进行深入探讨。尽管《原理篇》中介绍了那么多,解决方案的本质就是:在进行服务调用过程中将客户端的时区信息作为上下文传入服务端,并以此作为时间转换的依据。我们首先定一个具体的类型来定义包含时区信息的上下文类型,我们将这个类型起名为ApplicationContext。

一、通过CallContext实现ApplicationContext

在《通过WCF扩展实现Context信息的传递》一文中,我通过HttpSessionState和CallContext实现了一个ApplicationContext类,为ASP.NET和其他类型的应用提供上下文信息的容器。在这里进行了简化,仅仅实现了基于CallContext的部分。这样一个ApplicationContext类型定义如下:

   1: [CollectionDataContract(Namespace="http://www.artech.com/")]
   2: public class ApplicationContext:Dictionary<string, object>
   3: {
   4:     internal const string contextHeaderName         = "ApplicationContext";
   5:     internal const string contextHeaderNamespace    = "http://www.artech.com/";
   6:  
   7:     private ApplicationContext() { }
   8:     public static ApplicationContext Current
   9:     {
  10:         get
  11:         {
  12:             if (null == CallContext.GetData(typeof(ApplicationContext).FullName)) 
  13:             {
  14:                 lock (typeof(ApplicationContext))
  15:                 {
  16:                     if (null == CallContext.GetData(typeof(ApplicationContext).FullName))
  17:                     {
  18:                         var context = new ApplicationContext();
  19:                         context.TimeZone = TimeZoneInfo.Local;
  20:                         CallContext.SetData(typeof(ApplicationContext).FullName, context);
  21:                     }
  22:                 }
  23:             }
  24:  
  25:             return (ApplicationContext)CallContext.GetData(typeof(ApplicationContext).FullName);
  26:         }
  27:         set
  28:         {
  29:             CallContext.SetData(typeof(ApplicationContext).FullName, value);
  30:         }
  31:     }
  32:     public TimeZoneInfo TimeZone
  33:     {
  34:         get
  35:         {
  36:             return TimeZoneInfo.FromSerializedString((string)this["__TimeZone"]);
  37:         }
  38:         set
  39:         {
  40:             this["__TimeZone"] = value.ToSerializedString();
  41:         }
  42:     }
  43:  
  44:     public static void Clear()
  45:     { 
  46:         CallContext.FreeNamedDataSlot(typeof(ApplicationContext).FullName);
  47:     }
  48: }

ApplicationContext继承自Dictionary<string,object>类型,并被定义成集合数据契约。我们采用Singleton的方式来定义ApplicationContext,当前上下文通过静态方法Current获取。而Current属性返回的是通过CallContext的GetData方法获取,并且Key为类型的全名。便是当前时区的TimeZone属性的类型为TimeZoneInfo,通过序列化和反序列对当前时区进行设置和获取。Clear则将整个ApplicationContext对象从CallContext中移除。

二、创建一个用于时间转化的DateTimeConverter

服务端需要进行两种方式的时间转化,其一是将可户端传入的时间转换成UTC时间,其二就是将从数据库获取的UTC时间转化成基于当前时区上下文的Local时间。为此我定义了如下一个静态的帮助类DateTimeConverter专门进行这两方面的时间转换,而时间转换依据的时区来源于当前ApplicationContext的TimeZone属性。

   1: public static class DateTimeConverter
   2: {
   3:     public static DateTime ConvertTimeToUtc(DateTime dateTime)
   4:     { 
   5:         if(dateTime.Kind == DateTimeKind.Utc)
   6:         {
   7:             return dateTime;
   8:         }
   9:         return TimeZoneInfo.ConvertTimeToUtc(dateTime, ApplicationContext.Current.TimeZone);
  10:     }
  11:  
  12:     public static DateTime ConvertTimeFromUtc(DateTime dateTime)
  13:     {
  14:         if (dateTime.Kind == DateTimeKind.Utc)
  15:         {
  16:             return dateTime;
  17:         }
  18:         return TimeZoneInfo.ConvertTimeFromUtc(dateTime, ApplicationContext.Current.TimeZone);
  19:     }
  20: }

三、通过WCF扩展实现ApplicationContext的传播

让当前的ApplicationContext在每次服务调用时自动传递到服务端,并作为服务端当前的ApplicationContext,整个过程通过两个步骤来实现:其一是客户端将当前ApplicationContext对象进行序列化,并置于出栈消息的报头(SOAP

Header);其二是服务在接收到请求消息时从入栈消息中提取该报头并进行反序列化,最终将生成的对象作为服务端当前的ApplicationContext。

客户端对当前ApplicationContext输出可以通过WCF的MessageInspector对象来完成。为此,我们实现了IClientMessageInspector接口定义了如下一个自定义的MessageInspector:ContextMessageInspector。在BeforeSendRquest方法中,基于当前ApplicationContext创建了一个MessageHeader,并将其插入出栈消息的报头集合中。该消息报头对应的命名空间和名称为定义在ApplicationContext中的两个常量。

   1: public class ContextMessageInspector:IClientMessageInspector
   2: {
   3:     public void AfterReceiveReply(ref Message reply, object correlationState) { }
   4:     public object BeforeSendRequest(ref Message request, IClientChannel channel)
   5:     {           
   6:         MessageHeader<ApplicationContext> header = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
   7:         request.Headers.Add(header.GetUntypedHeader(ApplicationContext.contextHeaderName, ApplicationContext.contextHeaderNamespace));
   8:         return null;
   9:     }
  10: }

相应地,服务端对ApplicationContext的接收和设置可以通过WCF的CallContextInitializer来实现。为此,我们实现了ICallContextInitializer接口定义了如下一个自定义的CallContextInitializer:ContextCallContextInitializer。在BeforeInvoke方法中,通过相同的命名空间和名称从入栈消息中提取ApplicationConntext作为当前的ApplicationContext。为了避免当前ApplicationContext用在下一次服务请求处理中

(ApplicationContext保存在当前线程的TLS中,而WCF采用线程池的机制处理客户请求),我们在AfterInvoke方法中调用Clear方法将当前ApplicationContext清除。

   1: public class ContextCallContextInitializer: ICallContextInitializer
   2: {
   3:     public void AfterInvoke(object correlationState)
   4:     {
   5:         ApplicationContext.Clear();
   6:     }
   7:     public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
   8:     {
   9:         var index = message.Headers.FindHeader(ApplicationContext.contextHeaderName, ApplicationContext.contextHeaderNamespace);
  10:         if (index >= 0)
  11:         {
  12:             ApplicationContext.Current = message.Headers.GetHeader<ApplicationContext>(index);
  13:         }
  14:         return null;
  15:     }
  16: }

用于ApplicationContext发送的ContextMessageInspector,和用于ApplicationContext接收的ContextCallContextInitializer,最终我们通过一个EndpointBehavior被应用到WCF运行时框架中。为此我们定义了如下一个自定义的EndpointBehavior:ContextBehavior。

   1: public class ContextBehavior : IEndpointBehavior
   2: {
   3:     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
   4:     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
   5:     {
   6:         clientRuntime.MessageInspectors.Add(new ContextMessageInspector());
   7:     }
   8:     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
   9:     {
  10:         foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
  11:         {
  12:             operation.CallContextInitializers.Add(new ContextCallContextInitializer());
  13:         }
  14:     }
  15:     public void Validate(ServiceEndpoint endpoint) { }
  16: }

由于ContextBehavior这个终结点行为需要通过培植的方式来使用,我们需要定义它的BehaviorExtensionElement(本质上是一个配置元素):

   1: public class ContextBehaviorElement : BehaviorExtensionElement
   2: {
   3:     public override Type BehaviorType
   4:     {
   5:         get { return typeof(ContextBehavior); }
   6:     }
   7:     protected override object CreateBehavior()
   8:     {
   9:         return new ContextBehavior();
  10:     }
  11: }

四、建立一个Alertor Service来模拟跨时区场景


到目前为止,所有基础性编程已经完成,我们现在创建一个具体的分布式应用来使用上面定义的类型。为此,我们模拟一个用户提醒服务(Alertor
Service):我们为某个人创建相应的通知或者提醒,比如什么时候开会,什么时候见客户之类的。首先,所有的Alert条目被最终保存在数据库中,对应的表的结构如右图所示。四个字段分别表示Alert的Id、被通知的人、消息和被触发的时间。这里的表示时间的类型就是我们常用的datetime(不具有时区偏移量信息)。

与这个数据表结构相对应,一个Alert类型被创建出来表示一个具体的Alert条目。Alert被定义成数据契约,下面的代码给出了该类的具体定义。

   1: [DataContract]
   2: public class Alert
   3: {
   4:     [DataMember]
   5:     public string Id { get; private set; }
   6:     [DataMember]
   7:     public string Person { get; private set; }
   8:     [DataMember]
   9:     public string Message { get; private set; }
  10:     [DataMember]
  11:     public DateTime Time { get; set; }
  12:     public Alert(string persone, string message, DateTime time)
  13:     {
  14:         this.Id = Guid.NewGuid().ToString();
  15:         this.Person = persone;
  16:         this.Message = message;
  17:         this.Time = time;
  18:     }
  19: }

然后我们定义服务契约:IAlert接口。该结构定义了两个操作成员,CreateNewAlert用于创建一个信息的Alert条目;而GetAlerts则用于获取某个人对应的所有Alert列表。

   1: [ServiceContract(Namespace = "http://www.artech.com/")]
   2: public interface IAlertor
   3: {
   4:     [OperationContract]
   5:     void CreateNewAlert(Alert alert);
   6:     [OperationContract]
   7:     IEnumerable<Alert> GetAlerts(string person);
   8: }

下面是实现上面这个服务契约的具体服务的实现:AlertorService。DbHelper是我创建的一个简单的进行数据操作的帮助类,AlertorService用它来执行一段参数化的SQL语句,以及执行一段SELECT语句返回一个DbDataReader。对此你无需过多关注没,你需要关注的是在CreateNewAlert方法中,在进行数据保存之前先调用了DateTimeConverter的ConvertTimeToUtc将基于客户端时区的本地时间转化成了UTC时间;而在GetAlerts方法中在将从数据库中返回的Alert列表返回给客户端的时候,调用了DateTimeConverter的ConvertTimeFromUtc将UTC时间转化成了基于客户端时区的本地时间。

   1: public class AlertorService:IAlertor
   2: {
   3:     private DbHelper helper = new DbHelper("TestDb");
   4:     public void CreateNewAlert(Alert alert)
   5:     {
   6:         alert.Time = DateTimeConverter.ConvertTimeToUtc(alert.Time);
   7:         var parameters = new Dictionary<string, object>();
   8:         parameters.Add("@id", alert.Id);
   9:         parameters.Add("@person", alert.Person);
  10:         parameters.Add("@message", alert.Message);
  11:         parameters.Add("@time", alert.Time);
  12:         helper.ExecuteNoQuery("INSERT INTO dbo.Alert(Id, Person, Message, Time) VALUES(@id,@person,@message,@time)", parameters);
  13:     }        
  14:     public IEnumerable<Alert> GetAlerts(string person)
  15:     {
  16:         var parameters = new Dictionary<string, object>();
  17:         parameters.Add("@person", person);
  18:         using (var reader = helper.ExecuteReader("SELECT Person, Message, Time FROM dbo.Alert WHERE Person = @person", parameters))
  19:         {
  20:             while (reader.Read())
  21:             { 
  22:                 yield return new Alert(reader[0].ToString(),reader[1].ToString(),DateTimeConverter.ConvertTimeFromUtc( (DateTime)reader[2]));
  23:             }
  24:         }
  25:     }
  26: }

在对上面的服务进行寄宿的时候,采用了如下的配置,将上面创建的ContextBehavior终结点行为应用到了相应的终结点上。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <behaviors>
   5:             <endpointBehaviors>
   6:                 <behavior name="contextBehavior">
   7:                     <contextPropagtion />
   8:                 </behavior>
   9:             </endpointBehaviors>
  10:         </behaviors>
  11:         <extensions>
  12:             <behaviorExtensions>
  13:                 <add name="contextPropagtion" type="Artech.TimeConversion.ContextBehaviorElement, Artech.TimeConversion.Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  14:             </behaviorExtensions>
  15:         </extensions>
  16:         <services>
  17:             <service name="Artech.TimeConversion.Service.AlertorService">
  18:                 <endpoint address="http://127.0.0.1:3721/alertservice" behaviorConfiguration="contextBehavior"
  19:                     binding="ws2007HttpBinding" bindingConfiguration="" contract="Artech.TimeConversion.Service.Interface.IAlertor" />
  20:             </service>
  21:         </services>
  22:     </system.serviceModel>
  23: </configuration>

客户端在通过如下的配置将ContextBehavior应用到用于服务调用的终结点上:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <behaviors>
   5:             <endpointBehaviors>
   6:                 <behavior name="contextBehavior">
   7:                     <contextPropagation />
   8:                 </behavior>
   9:             </endpointBehaviors>
  10:         </behaviors>
  11:         <client>
  12:             <endpoint address="http://127.0.0.1:3721/alertservice" behaviorConfiguration="contextBehavior"
  13:                 binding="ws2007HttpBinding" bindingConfiguration="" contract="Artech.TimeConversion.Service.Interface.IAlertor"
  14:                 name="alertservice" />
  15:         </client>
  16:         <extensions>
  17:             <behaviorExtensions>
  18:                 <add name="contextPropagation" type="Artech.TimeConversion.ContextBehaviorElement, Artech.TimeConversion.Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  19:             </behaviorExtensions>
  20:         </extensions>
  21:     </system.serviceModel>
  22: </configuration>

而下面的代码代表了客户端程序:我们为某个人(Foo)创建了三个Alert,主要这里指定的时间的DateTimeKind为默认的DateTimeKind.Unspecified。然后调用服务或者这三条Alert对象,并将消息的时间打印出来。

   1: public class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         CreateAlert("Foo", "Weekly Meeting with Testing Team", new DateTime(2010, 9, 1, 8, 0, 0));
   6:         CreateAlert("Foo", "Architecture and Design Training", new DateTime(2010, 9, 2, 8, 0, 0));
   7:         CreateAlert("Foo", "New Stuff Orientaion", new DateTime(2010, 9, 3, 8, 0, 0));
   8:  
   9:         foreach (var alert in GetAlerts("Foo"))
  10:         {
  11:             Console.WriteLine("Alert:\t{0}", alert.Message);
  12:             Console.WriteLine("Time:\t{0}\n", alert.Time);
  13:         }
  14:  
  15:        Console.Read();
  16:     }
  17:  
  18:     static IEnumerable<Alert> GetAlerts(string person)
  19:     {
  20:         using (ChannelFactory<IAlertor> channelFactory = new ChannelFactory<IAlertor>("alertservice"))
  21:         {
  22:             IAlertor alertor = channelFactory.CreateChannel();
  23:             using (alertor as IDisposable)
  24:             {
  25:                 return alertor.GetAlerts(person);
  26:             }
  27:         }
  28:     }
  29:     static void CreateAlert(string person, string message, DateTime time)
  30:     {
  31:         Alert alert = new Alert(person, message, time);
  32:         using (ChannelFactory<IAlertor> channelFactory = new ChannelFactory<IAlertor>("alertservice"))
  33:         {
  34:             IAlertor alertor = channelFactory.CreateChannel();
  35:             using (alert as IDisposable)
  36:             {
  37:                 alertor.CreateNewAlert(alert);
  38:             }
  39:         }
  40:     }
  41: }

运行上面的程序之后。服务端数据库中被添加的三条Alert纪录对应的时间,会以UTC形式存储。如左图所示,数据表中的时间比我们指定的的时间早8个小时。

下面是客户端的输出结果,可见Alert的提醒时间依然是基于本地时区的时间,这达到了我们在《原理篇》提出的要求:客户端应用根本不用考虑时区问题,就像是一个单纯的本地应用一样。客户端调用服务传入的时间是DateTimeKind.Local时间或者DateTimeKind.Unspecified时间,同理通过服务调用返回的时间也应该是基于客户端所在时区的时间。

   1: Alert:  New Stuff Orientaion
   2: Time:   9/3/2010 8:00:00 AM
   3:  
   4: Alert:  Weekly Meeting with Testing Team
   5: Time:   9/1/2010 8:00:00 AM
   6:  
   7: Alert:  Architecture and Design Training
   8: Time:   9/2/2010 8:00:00 AM
   9:  

 

[相关阅读]

[1] 谈谈你最熟悉的System.DateTime[上篇]

[2] 谈谈你最熟悉的System.DateTime[下篇]

[3] 如何解决分布式系统中的跨时区问题[原理篇]

[4] 如何解决分布式系统中的跨时区问题[实例篇]

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-09-22 18:09:47

如何解决分布式系统中的跨时区问题[实例篇]的相关文章

如何解决分布式系统中的跨时区问题[原理篇]

一.场景以及需求 为了让大家本文介绍的主题有一个比较直观的认识,我们给出一个具体的应用场景.一个跨国公司开发一套统一的办公系统,供遍布全球的所有分公司使用.客户端的UI采用Smart Client (Windows Forms应用),而主要的业务逻辑均通过WCF服务的形式提供.我们将承载业务服务的服务器成为应用服务器,如右图(点击看大图)所示,应用服务器部属于中国境内(东8区).主要的客户端(分公司)分布于三个主要的国家和地区:北美.欧州和澳洲. 不论客户端和服务器之间,还是不同的客户端之间所处

一起谈.NET技术,如何解决分布式系统中的跨时区问题[原理篇]

一.场景以及需求   为了让大家本文介绍的主题有一个比较直观的认识,我们给出一个具体的应用场景.一个跨国公司开发一套统一的办公系统,供遍布全球的所有分公司使用.客户端的UI采用Smart Client (Windows Forms应用),而主要的业务逻辑均通过WCF服务的形式提供.我们将承载业务服务的服务器成为应用服务器,如右图(点击看大图)所示,应用服务器部属于中国境内(东8区).主要的客户端(分公司)分布于三个主要的国家和地区:北美.欧州和澳洲. 不论客户端和服务器之间,还是不同的客户端之间

使用HTML5中postMessage知识点解决Ajax中POST跨域问题_AJAX相关

由于同源策略的限制,Javascript存在跨域通信的问题,典型的跨域问题有iframe与父级的通信等.常规的几种解决方法: (1) document.domain+iframe: (2) 动态创建script: (3) iframe+location.hash: (4) flash. postMessage是HTML5为解决js跨域问题而引入的新的API,允许多个iframe/window跨域通信. HTML5中提供了在网页文档之间相互接收与发送信息的功能.使用这个功能,只要获取到网页所在窗口

使用HTML5中postMessage知识点解决Ajax中POST跨域问题

由于同源策略的限制,Javascript存在跨域通信的问题,典型的跨域问题有iframe与父级的通信等.常规的几种解决方法: (1) document.domain+iframe: (2) 动态创建script: (3) iframe+location.hash: (4) flash. postMessage是HTML5为解决js跨域问题而引入的新的API,允许多个iframe/window跨域通信. HTML5中提供了在网页文档之间相互接收与发送信息的功能.使用这个功能,只要获取到网页所在窗口

asp.net中WebResponse 跨域访问实例代码

 一篇朋友很久前写的asp.net中WebResponse 跨域访问示例,下面我转过来与大家一起学习学习,希望文章对大家会有帮助 前两天,一个朋友让我帮他写这样一个程序:在asp.net里面访问asp的页面,把数据提交对方的数据库后,根据返回的值(返回值为:OK或ERROR),如果为OK再把填入本地数据库.当时,想当然,觉得很简单,用js的xmlhttp ,如果根据response 的值是"OK"就执行提交本地数据库.很快写完发过去,让朋友试试,一试发现不行,后来一问,原来是跨域访问,

jquery中ajax跨域方法实例分析_jquery

本文实例分析了jquery中ajax跨域.分享给大家供大家参考,具体如下: JSONP是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问 方法一: jsonp之 getJSON js var url = "http://localhost/mytest/jsonp_php.php?callback=?"; $.getJSON(url, { "age": 21, "name

ASP.NET中Cookie跨域的问题及解决代码

ASP.NET中Cookie跨域的问题及解决代码 http://www.liyumei.net.cn/post/share18.html Cookies揭秘  http://www.cnblogs.com/zhangziqiu/archive/2009/08/06/cookies-javascript-aspnet.html 最近在项目开发中遇到一个很棘手的问题,一个用户在顶级域名登录后,跳转到自己所拥有的二级域名下管理二级网站时,cookie丢失了,一直找解决办法找了整整两天,百度谷歌一大堆,

一篇文读懂缓存在大型分布式系统中的最佳应用

本文大纲: 缓存概述 CDN缓存 反向代理缓存 分布式缓存 本地缓存 缓存架构示例 缓存常见问题        一.缓存概述       缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题.提供高性能的数据快速访问.   1.缓存的原理   将数据写入/读取速度更快的存储(设备): 将数据缓存到离应用最近的位置: 将数据缓存到离用户最近的位置.   2.缓存分类   在分布式系统中,缓存的应用非常广泛,从部署角度有以下几个方面的缓存应用.   CDN缓存: 反向代

MVC使用Memcache+Cookie解决分布式系统共享登录状态学习笔记6_实用技巧

      为了解决单机处理的瓶颈,增强软件的可用性,我们需要将软件部署在多台服务器上启用多个二级子域名以频道化的方式,根据业务功能将网站分布部署在独立的服务器上,或通过负载均衡技术(如:DNS轮询.Radware.F5.LVS等)让多个频道共享一组服务器.当我们将网站程序分部到多台服务器上后,由于Session受实现原理的局限,无法跨服务器同步更新Session,使得登录状态难以通过Session共享.       我们使用MemCache+Cookie方案来解决分布式系统共享登录状态的问题.