Skip to content

C#.NET 面试题 中高级篇

谈一谈对 DDD 的理解?

DDD,领域驱动设计。就是通过领域来指导软件设计,是一种十分抽象的软件设计思想,它主要分为战略设计和战术设计

战略方面,通过事件风暴进行领域模型的划分,划分出核心域,子域,支撑域,定义通用语言,划分出界限上下文。

在战术设计方面,ddd 将架构分层,“松耦合,高内聚”是架构设计的整体思想。按照 DDD 思想,可以分为领域层,基础设施层,应用层,接口层。

接口层为前端用户提供 api 接口。基础设施层可以放一些第三方的服务,数据库连接等内容。应用层是对领域服务的编排,是很薄的一层(目前我自己的架构,应用的是 cqrs,所有的相关逻辑都是放在了应用层,而领域层只是放了实体,因为暂时还不是特别理解领域层的服务和事件都应该写什么)。领域层包括实体,值对象,聚合根,领域服务,领域事件等内容。

谈一谈对 AOP 的理解?

官方解释:

AOP(Aspect-Oriented Programming,面向切面的编程),它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。它是一种新的方法论,它是对传统 OOP 编程的一种补充。OOP 是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系;AOP 是希望能够将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。AOP 是使用切面(aspect)将横切关注点模块化,OOP 是使用类将状态和行为模块化。在 OOP 的世界中,程序都是通过类和接口组织的,使用它们实现程序的核心业务逻辑是十分合适。但是对于实现横切关注点(跨越应用程序多个模块的功能需求)则十分吃力,比如日志记录,权限验证,异常拦截等。

个人理解:

AOP 就是将公用功能提取出来,如果以后公用功能的需求发生变化,只需要改动公用的模块的代码即可,多个调用的地方则不需要改动。所谓面向切面,就是只关注通用功能,而不关注业务逻辑。实现方式一般是通过拦截。比如,我们随便一个 Web 项目基本都有的权限验证功能,进入每个页面前都会校验当前登录用户是否有权限查看该界面,我们不可能说在每个页面的初始化方法里面都去写这段验证的代码,这个时候我们的 AOP 就派上用场了,AOP 的机制是预先定义一组特性,使它具有拦截方法的功能,可以让你在执行方法之前和之后做你想做的业务,而我们使用的时候只需要的对应的方法或者类定义上面加上某一个特性就好了。

使用 AOP 的优势:

1、将通用功能从业务逻辑中抽离出来,可以省略大量重复代码,有利于代码的操作和维护。

2、在软件设计时,抽出通用功能(切面),有利于软件设计的模块化,降低软件架构的复杂度。也就是说通用的功能都是一个单独的模块,在项目的主业务里面是看不到这些通用功能的设计代码的。

AOP 的简单应用:

1、静态拦截

2、动态代理

3、IL 编织

4、MVC 里面的 Filter

.NET 相关的 AOP 框架:

1、PostSharp(编译时静态植入)是最有名且使用率较高的一个,但是在 Nuget 上的版本是需要付费的(2.0)以上都要付费。

2、Spring.Net 用于解决企业应用开发复杂性的一种容器框架,它实现了控制反转 IOC 和依赖注入 DI,通俗解释就是通过 spring.net 框架的容器来创建对象实体,而不是通过程序员 new 出来,降低程序对服务类的依赖性,提高软件的可扩展性。只要在 spring.net 的相应 xml 中配置节点,创建容器上下文后再通过配置获取对象就可以。

3、Autofac 是一个.net 下非常优秀,性能非常好的 IOC 容器(.net 下效率最高的容器),加上 AOP 简直是如虎添翼。Autofac 的 AOP 是通过 Castle(也是一个容器)项目的核心部分实现的,名为 Autofac.Extras.DynamicProxy,顾名思义其实现方式为动态代理。

4、Castle.Core 本质是创建继承原来类的代理类,重写虚方法实现 AOP 功能。

5、KingAOP 开源框架 KingAOP 是基于动态类型进行操作和绑定的。

个人推荐使用 Castle.Core 或者 KingAOP

谈谈对分布式锁的理解?

分布式锁是一种用于协调分布式系统中多个进程或线程访问共享资源的同步机制,它可以确保在分布式环境下多个节点之间的互斥访问,避免出现竞态条件和数据不一致等并发问题。在分布式系统中,由于各个节点之间的通信受限制,传统的本地锁机制无法满足需求,因此需要分布式锁来协调各个节点的访问。

分布式锁的实现方式有多种,其中比较常见的有基于数据库、缓存和 ZooKeeper 等实现。不同的实现方式具有不同的优缺点,需要根据具体的场景和需求来选择合适的实现方式。例如,基于数据库实现分布式锁可以保证锁的正确性,但是需要保证数据库的可用性和性能;基于缓存实现分布式锁可以保证锁的高性能,但是需要考虑缓存的容量和数据一致性;基于 ZooKeeper 实现分布式锁可以保证锁的可靠性和正确性,但是需要额外的 ZooKeeper 集群维护,并且对于锁的持有时间比较长的情况,会对 ZooKeeper 的性能造成影响。

在使用分布式锁时,需要注意锁的持有时间、锁的粒度、锁的可重入性、锁的可靠性等问题。例如,锁的持有时间过长可能会导致死锁和性能问题;锁的粒度过大可能会降低并发性能;锁的可重入性可以避免死锁和性能问题;锁的可靠性可以保证锁的正确性和一致性。

总之,分布式锁是分布式系统中非常重要的同步机制,它可以确保多个节点之间的互斥访问,避免出现并发问题,同时需要根据具体的场景和需求选择合适的实现方式,并注意锁的持有时间、粒度、可重入性和可靠性等问题。

分布式事务的经典解决方案

1、两阶段提交(2PC)

二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为准备阶段和提交阶段两个阶段来进行处理,通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。

  • 事务协调者(事务管理器):事务的发起者

  • 事务参与者(资源管理器):事务的执行者

优点

尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能 100%保证强一致,如宕机)

缺点

  • 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  • 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
  • 数据一致性问题:二阶段无法解决的问题如协调者在发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的, 没人知道事务是否被已经提交。
  • 实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

2、三阶段提交(3PC)

三阶段提交协议是二阶段提交协议的改进版本,其有两个改动点。

  • 在协调者和参与者中都引入超时机制;
  • 在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

即除了引入超时机制之外,3PC 把 2PC 的准备阶段再次一分为二,这样三阶段提交就有 CanCommit、PreCommit 和 DoCommit 三个阶段。

优点

相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点

数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

3、事务补偿(TCC)

TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交,是目前最火的一种柔性事务方案。TCC 采用了补偿机制,其核心思想就是针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为 Try、Confirm 和 Cancel 三个阶段。

  • Try 阶段主要是对业务系统做检测及资源预留;

  • Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的,即:只要 Try 成功,Confirm 一定成功;

  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

转账例子:假入 Bob 要向 Smith 转账,思路大概是: 我们有一个本地方法,里面依次调用。

  • 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来;
  • 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
  • 如果第 2 步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

优点

跟 2PC 比起来,实现以及流程相对简单了一些,但数据的一致性比 2PC 差。

缺点

在 2 和 3 步中都有可能会失败。TCC 属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用 TCC 不太好定义及处理。

4、本地消息表(推荐)

本地消息表方案的核心思路是将分布式事务拆分成本地事务进行处理。通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

优点

避免了分布式事务,实现了最终一致性。

缺点

消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

目前 .NET 提供了一个比较成熟的解决方案正好可以解决此问题:

DotNetCore.CAP

个人比较推荐

5、可靠消息事务

MQ 事务方案是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

6、最大努力通知

最大努力通知方案是对 MQ 事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。

其适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。

7、Saga 事务

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga 事务基本协议如下:

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。

  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

Saga 的执行顺序有两种:

  • T1, T2, T3, ..., Tn

  • T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n

TCC 事务补偿机制有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;而 Saga 事务没有预留动作,直接提交。对于事务异常,Saga 提供了向后恢复和向前恢复两种恢复策略。

向后恢复(backward recovery)

backward recovery,即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction,这种做法的效果是撤销掉之前所有成功的 sub-transation,使得整个 Saga 的执行结果撤销。

向前恢复(forward recovery)

forward recovery,即适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中 j 是发生错误的 sub-transaction。该情况下不需要 Ci。

8、总结

各分布式事务方案的常见使用场景:

  • 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。

  • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。

  • 本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。

  • Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。

高并发解决方案

1、负载均衡

负载均衡,它的职责是将网络请求 “均摊”到不同的机器上。避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况。

通过负载均衡,可以让每台服务器获取到适合自己处理能力的负载。在为高负载服务器分流的同时,还可以避免资源浪费,一举两得。

常见的负载算法:

  • 随机算法

  • 轮询算法

  • 轮询权重算法

  • 一致性哈希算法

  • 最小连接

  • 自适应算法

常用负载均衡工具:

  • LVS

  • Nginx

  • HAProxy

对于一些大型系统,一般会采用 DNS+四层负载+七层负载的方式进行多层次负载均衡。

2、分布式微服务

过去是一个大而全的系统,面对复杂的业务规则,我们采用分而治之的思想,通过 SOA 架构,将一个大的系统拆分成若干个微服务,粒度越来越小,称之为微服务架构。

每个微服务独立部署,服务和服务间采用轻量级的通信机制,如:标准的 HTTP 协议、或者私有的 RPC 协议。

微服务特点:

  • 按照业务划分服务,单个服务代码量小,业务单一,容易维护。

  • 每个微服务都有自己独立的基础组件,例如数据库。

  • 微服务之间的通信是通过 HTTP 协议或者私有协议,且具有容错能力。

  • 微服务有一套服务治理的解决方案,服务之间不耦合,可以随时加入和剔除。

  • 单个微服务能够集群化部署,有负载均衡的能力。

  • 整个微服务系统应该有完整的安全机制,包括用户验证,权限验证,资源保护。

  • 整个微服务系统有链路追踪的能力。

  • 有一套完整的实时日志系统。

3、缓存机制

性能不够,缓存来凑。要想快速提升性能,缓存肯定少不了。

缓存能够带来性能的大幅提升,以 Memcache 为例,单台 Memcache 服务器简单的 key-value 查询能够达到 TPS 50000 以上;Redis 性能数据是 10W+ QPS。

常见的缓存分为本地缓存和分布式缓存,区别在与是否要走网络通讯。

本地缓存是部署在应用服务器中,而我们应用服务器通常会部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。

相反,分布式缓存采用集群化管理,支持水平扩容,并提供客户端路由数据,数据一致性维护更好。虽然有不到 1ms 的网络开销,但比起其优势,这点损耗微不足道。

4、分布式关系型数据库

如果遇到单机数据库性能瓶颈,我们可以考虑分表。

分表又可以细分为 垂直分表 和 水平分表 两种形式。

垂直分表

数据表垂直拆分就是纵向地把一张表中的列拆分到多个表,表由“宽”变“窄”,简单来讲,就是将大表拆成多张小表,一般会遵循以下几个原则:

  • 冷热分离,把常用的列放在一个表,不常用的放在一个表。

  • 字段更新、查询频次拆分。

  • 大字段列独立存放。

  • 关系紧密的列放在一起。

水平分表

表结构维持不变,对数据行进行切分,将表中的某些行切分到一张表中,而另外的某些行又切分到其他的表中,也就是说拆分后数据集的并集等于拆分前的数据集。

分库分表技术点:

  • SQl 组合。因为是逻辑表名,需要按分表键计算对应的物理表编号,根据逻辑重新组装动态的 SQL。

  • 数据库路由。如果采用分库,需要根据逻辑的分表编号计算数据库的编号。

  • 结果合并。如果查询没有传入指定的分表键,会全库执行,此时需要将结果合并再输出。

5、分布式消息队列

并不是所有的调用都要走同步形式,对于时间要求不高、或者非核心逻辑,我们可以采用异步处理机制。也就衍生出消息队列。

消息队列主要有三种角色:生产者、消息队列、消费者。

生产端核心的逻辑处理完后,会封装一个 MQ 消息,发送到消息队列。下游系统,如果关心这个事件,只需要订阅这个 topic ,便可以收到消息,进行后续的业务逻辑处理。

两者之间通过消息中间件完成了解耦,系统的扩展性非常高。

常用的消息框架:

ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaQ,RocketMQ、Pulsar 等。

6、CDN 加速

CDN 全称 (Content Delivery Network),内容分发网络。

目的是在现有的网络中增加一层网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。

CDN = 镜像(Mirror)+缓存(Cache)+整体负载均衡(GSLB)。

CDN 都以缓存网站中的静态数据为主,如:CSS、JS、图片和静态页面等数据。用户从主站服务器中请求到动态内容后,再从 CDN 下载静态数据,从而加速网页数据内容的下载速度。

主要特点:

  • 本地 Cache 加速

  • 镜像服务

  • 远程加速

  • 带宽优化

  • 集群抗攻击

应用场景:

  • 网站站点/应用加速

  • 视音频点播/大文件下载分发加速

  • 视频直播加速

  • 移动应用加速

7、其他

  • 对于被频繁调用,更新频率较低的页面,可以采用 HTML 静态化技术

  • 图片服务器分离

  • 搜索用单独的服务器,搜索框架

你觉得这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度