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

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

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

image   到目前为止,所有基础性编程已经完成,我们现在创建一个具体的分布式应用来使用上面定义的类型。为此,我们模拟一个用户提醒服务(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:  
    [相关阅读]
目录
相关文章
|
4月前
|
负载均衡 测试技术 调度
大模型分布式推理:张量并行与流水线并行技术
本文深入探讨大语言模型分布式推理的核心技术——张量并行与流水线并行。通过分析单GPU内存限制下的模型部署挑战,详细解析张量并行的矩阵分片策略、流水线并行的阶段划分机制,以及二者的混合并行架构。文章包含完整的分布式推理框架实现、通信优化策略和性能调优指南,为千亿参数大模型的分布式部署提供全面解决方案。
1087 4
|
4月前
|
监控 Cloud Native 测试技术
.NET技术深度解析:现代企业级开发指南
每日激励:“不要一直责怪过去的自己,他曾经站在雾里也很迷茫”。我是蒋星熠Jaxonic,一名在代码宇宙中探索的极客旅人。从.NET Framework到.NET 8,我深耕跨平台、高性能、云原生开发,践行领域驱动设计与微服务架构,用代码书写技术诗篇。分享架构演进、性能优化与AI融合前沿,助力开发者在二进制星河中逐光前行。关注我,共探技术无限可能!
.NET技术深度解析:现代企业级开发指南
|
11月前
|
Cloud Native 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
阿里云PolarDB云原生数据库在TPC-C基准测试中以20.55亿tpmC的成绩刷新世界纪录,展现卓越性能与性价比。其轻量版满足国产化需求,兼具高性能与低成本,适用于多种场景,推动数据库技术革新与发展。
|
5月前
|
消息中间件 监控 Java
Apache Kafka 分布式流处理平台技术详解与实践指南
本文档全面介绍 Apache Kafka 分布式流处理平台的核心概念、架构设计和实践应用。作为高吞吐量、低延迟的分布式消息系统,Kafka 已成为现代数据管道和流处理应用的事实标准。本文将深入探讨其生产者-消费者模型、主题分区机制、副本复制、流处理API等核心机制,帮助开发者构建可靠、可扩展的实时数据流处理系统。
539 4
|
4月前
|
机器学习/深度学习 监控 PyTorch
68_分布式训练技术:DDP与Horovod
随着大型语言模型(LLM)规模的不断扩大,从早期的BERT(数亿参数)到如今的GPT-4(万亿级参数),单卡训练已经成为不可能完成的任务。分布式训练技术应运而生,成为大模型开发的核心基础设施。2025年,分布式训练技术已经发展到相当成熟的阶段,各种优化策略和框架不断涌现,为大模型训练提供了强大的支持。
|
5月前
|
JSON 监控 Java
Elasticsearch 分布式搜索与分析引擎技术详解与实践指南
本文档全面介绍 Elasticsearch 分布式搜索与分析引擎的核心概念、架构设计和实践应用。作为基于 Lucene 的分布式搜索引擎,Elasticsearch 提供了近实时的搜索能力、强大的数据分析功能和可扩展的分布式架构。本文将深入探讨其索引机制、查询 DSL、集群管理、性能优化以及与各种应用场景的集成,帮助开发者构建高性能的搜索和分析系统。
408 0
|
9月前
|
安全 JavaScript 前端开发
HarmonyOS NEXT~HarmonyOS 语言仓颉:下一代分布式开发语言的技术解析与应用实践
HarmonyOS语言仓颉是华为专为HarmonyOS生态系统设计的新型编程语言,旨在解决分布式环境下的开发挑战。它以“编码创造”为理念,具备分布式原生、高性能与高效率、安全可靠三大核心特性。仓颉语言通过内置分布式能力简化跨设备开发,提供统一的编程模型和开发体验。文章从语言基础、关键特性、开发实践及未来展望四个方面剖析其技术优势,助力开发者掌握这一新兴工具,构建全场景分布式应用。
888 35
|
9月前
|
Go
在golang中发起http请求以获取访问域名的ip地址实例(使用net, httptrace库)
这只是追踪我们的行程的简单方法,不过希望你跟着探险家的脚步,即使是在互联网的隧道中,也可以找到你想去的地方。接下来就是你的探险之旅了,祝你好运!
505 26
|
10月前
|
Cloud Native 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
|
10月前
|
SQL 小程序 API
如何运用C#.NET技术快速开发一套掌上医院系统?
本方案基于C#.NET技术快速构建掌上医院系统,结合模块化开发理念与医院信息化需求。核心功能涵盖用户端的预约挂号、在线问诊、报告查询等,以及管理端的排班管理和数据统计。采用.NET Core Web API与uni-app实现前后端分离,支持跨平台小程序开发。数据库选用SQL Server 2012,并通过读写分离与索引优化提升性能。部署方案包括Windows Server与负载均衡设计,确保高可用性。同时针对API差异、数据库老化及高并发等问题制定应对措施,保障系统稳定运行。推荐使用Postman、Redgate等工具辅助开发,提升效率与质量。
424 0

热门文章

最新文章