鼎鼎知识库
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

2020.2.13领域驱动实战.md 35KB


typora-root-url: ./

1、什么是领域驱动

领域驱动,Domain Driven Design。

一张桌子有4条腿。程序员可能的第一反应是通过Table类和Leg类描述两者之间的关系。一个人戴着一顶帽子,程序员可能的第一反应是一个Person类有一个Hat属性。

这似乎是顺其自然的、合乎事理的。但是,大家是否会觉得这是一种Code Driven Implementation?的确,领域驱动最终会落实到代码层面,但领域驱动的意义在于:在写代码之前,我们先理解领域,理解业务概念、约束、行为、规则等等

领域驱动似乎传递着*问题第一代码第二*、*市场第一代码第二*、*业务第一代码第二*的理念。程序员不再是拿着锤子到处找钉子的人,而是让自己首先成为一个领域专家,或者成为一个领域专家的倾听者。程序员不仅对代码负责,还对利益相关方负责,对业务负责。程序员关注的焦点不仅在解决问题本身,而是是否真的解决了问题。

当我们谈领域驱动,其实与编程语言、代码、数据库、微服务都无关,而是一种更高层面的设计。

2、分层结构

2.1 领域抽象层

聚合根标记接口

public interface IAggregateRoot{}

什么是聚合根?理解聚它首先要理解聚合Aggregate。假设有一个银行转账场景,程序员A和领域专家B正在进行对话:

A:我们目前想解决的问题是什么?

B:做一个客户转账的功能。

A:能具体说一下吗?

B: 客户X给客户Y转账100元,X的账户上少了100元,Y的账户上增加100元。我们希望所有的转账记录都可以被追溯。

接着,A和B在白板上进行了一次事件风暴Event Storming。首先,在列出了影响系统状态的事件。

事件作用在哪里?谁触发事件?触发什么事件?

显然,事件围绕账户而进行。在DDD中适用聚合Aggregate这个概念对相关性事件进行逻辑划分。

有两种做法。一种是把聚合看作名词,比如在这里定义Account类。

public class Account : IAggregateRoot{}

以上的Account就是聚合,可以被持久化到数据库,我们目前的架构采用的就是这种方式。

另一种做法是把聚合看作动词。其中有两个关键的视角:组成和规则。

聚合组成:

  • 转账的唯一性:每次转账都有其唯一编号。创建TransferNumber用来生成唯一编号

  • 出账: 从一个账户中扣除转账金额。创建Debit类用来描述出账

  • 入账: 把转账金额存入某个账户。创建Credit类用来描述入账

  • 账户:出账和入账都用到了账户。创建AccountNumber类用来描述账户

聚合规则:

  • 出账:转账的金额必须大于零,出账账户不能为空
  • 入账:入账的金额必须大于零,入账账户不能为空
//每次转账唯一编号的封装
public class TranferNumber
{
	public string Value {get; private set;}
	public Guid EntityId {get; private set;}
	
	public TranferNumber(string value, Guid entityId)
	{
		Value = value;
		EntityId = entityId;
	}
}

//出账
public class Debit
{
	public decimal Value {get; private set; }
	public AccountNumber Account {get; private set; }
	
	public Debit(decimal value, AccountNumber account)
	{
		Value = value;
		Account = account;
		value.Must(v => v >=0);//business rule
		account.MustNotBeNull();//ensure value object is valid
	}
}

//入账
public class Credit
{
	public decimal Value {get; private set; }
	public AccountNumber Account {get; private set; }
	
	public Credit(decimal value, AccountNumber account)
	{
		value.Must(v => v >=0);//business rule
		account.MustNotBeNull();//ensure value object is valid
		Value = value;
		Account = account;
	}
}


//账号的封装
public class AccountNumber : IEquatable<AccountNumber>
{
	public string Number {get; private set;}
	public AccountNumber(string number)
	{
		Number = number;
	}
	
	public bool Equals(AccountNumber other) => other !=null && Number == other.Number;
}

public class TransferedRegistered
{
	public Guid EntityId {get; set;}
	public string TransferNumber {get; set;}
	public string DebitAccountNo {get;set;}
	public string CreditAccountNo {get;set;}
	public DateTimeOffset CreatedOn {get;set;} = DateTimeOffset.Now;
}

//Aggregate Root
public static class TransferAccount
{
	public static TransferRegistered Create(TransferNumber number, Debit debit, Credit credit)
	{
		number.MustNotBeNull();
		debit.MustNotBeNull();
		credit.MustNotBeNull();
		
		debit.Account.MustNotBeNull(credit.Account);
		
		var ev = new TransferedRegistered();
		ev.EntityId = number.EntityId;
		ev.TransferNumber = number.Value;
		en.Amount = debit.Value;
		ev.DebitAccountNo = debit.Account.Number;
		ev.CreditAccountNo = credit.Account.Number;
		return ev;
	}
}

以上TransferAccount就是聚合,聚合无需持久化到数据库,状态的改变需要持久化到数据库。

领域接口和抽象实现

public interface IEntity{}

public interface  IEntity<TKey> : IEntity{}

public abstract class Entity : IEntity
{
	   //保存所有的事件,针对领域的事件在领域内部调用
        private List<IDomainEvent> _domainEvents;
        public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly();

        public void AddDomainEvent(IDomainEvent eventItem)
        {
            _domainEvents = _domainEvents ?? new List<IDomainEvent>();
            _domainEvents.Add(eventItem);
        }

        public void RemoveDomainEvent(IDomainEvent eventItem)
        {
            _domainEvents?.Remove(eventItem);
        }

        public void ClearDomainEvents()
        {
            _domainEvents?.Clear();
        }
}

领域事件接口和领域事件处理接口

using MediatR;
public interface IDomainEvent : INotification{}

pubic interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> where TDomainEvent : IDomainEvent{}

值对象: 如果两个值对象的值都一样,这两个值对象可以相互替换

public abstract class ValueObject{}

2.2 基础设施抽象层

工作者单元接口

public interface IUnitOfWork : IDisposable
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
    Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default);
}

事务接口

public interface ITransaction
{
    IDbContextTransaction GetCurrentTransaction();
    bool HasActiveTransaction { get; }
    Task<IDbContextTransaction> BeginTransactionAsync();
    Task CommitTransactionAsync(IDbContextTransaction transaction);
    void RollbackTransaction();
}

自定义上下文

public class EFContext : DbContext, IUnitOfWork, ITransaction
{
	protected IMediator _mediator;
	ICapPublisher _capBus;
	
	public EFContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options)
	{
		_mediator = mediator;
		_capBus = capBus;
	}
	
	public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
	{
		var result = await base.SaveChangesAsync(cancellationToken);
		await _mediator.DispatchDomainEventsAsync(this);
		return true;
	}
	
	...
}

仓储接口

public interface IRepository<TEntity> where TEntity : Entity, IAggregateRoot{}

public interface IRepository<TEntity, TKey> : IRepository<TEntity> where TEntity : Entity<TKey>, IAggregateRoot{}

仓储抽象实现

public abstract class Repository<TEntity, TDbContext> : IRepository<TEntity> where TEntity : Entity, IAggregateRoot where TDbContext : EFContext
{
 
}

管道行为

在管道内提交事务

    //在MedatR的IPipelineBehavior基础上注入上下文
    public class TransactionBehavior<TDbContext, TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TDbContext : EFContext
    {
        ILogger _logger;
        TDbContext _dbContext;
        public TransactionBehavior(TDbContext dbContext, ILogger logger)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }


        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            var response = default(TResponse);
            var typeName = request.GetGenericTypeName();//object类型的TRequest的扩展方法,获取泛型类型名称

            try
            {
                if (_dbContext.HasActiveTransaction)
                {
                    return await next();
                }

                var strategy = _dbContext.Database.CreateExecutionStrategy();

                await strategy.ExecuteAsync(async () =>
                {
                    Guid transactionId;
                    using (var transaction = await _dbContext.BeginTransactionAsync())
                    using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId))
                    {
                        _logger.LogInformation("----- 开始事务 {TransactionId} ({@Command})", transaction.TransactionId, typeName, request);

                        response = await next();

                        _logger.LogInformation("----- 提交事务 {TransactionId} {CommandName}", transaction.TransactionId, typeName);


                        await _dbContext.CommitTransactionAsync(transaction);

                        transactionId = transaction.TransactionId;
                    }
                });

                return response;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "处理事务出错 {CommandName} ({@Command})", typeName, request);

                throw;
            }
        }
    }
    
    

扩展方法,获取泛型类型名称:

    public static class GenericTypeExtensions
    {
        public static string GetGenericTypeName(this Type type)
        {
            var typeName = string.Empty;

            if (type.IsGenericType)
            {
                var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray());
                typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>";
            }
            else
            {
                typeName = type.Name;
            }

            return typeName;
        }

        public static string GetGenericTypeName(this object @object)
        {
            return @object.GetType().GetGenericTypeName();
        }
    }

2.3 共用层

  • 异常处理

异常处理接口:IknownException

 public interface IKnownException
 {
 	string Message { get; }
	int ErrorCode { get; }
	object[] ErrorData { get; }
}

异常处理接口实现:KnownException

public class KnownException : IKnownException
{
	public string Message { get; private set; }
	public int ErrorCode { get; private set; }
	public object[] ErrorData { get; private set; }

	public readonly static IKnownException Unknown = new KnownException { Message = "未知错误", ErrorCode = 9999 };

	public static IKnownException FromKnownException(IKnownException exception)
	{
		return new KnownException { Message = exception.Message, ErrorCode = exception.ErrorCode, ErrorData = exception.ErrorData };
	}
}

2.4 领域层

引用

引用领域抽象层。

聚合

public class Order : Entity<long>, IAggregateRoot
{
 	public string UserId {get; private set;}
	public string UserName {get; private set;}
	public int ItemCount {get; private set;}
	public Address Address {get; private set;}
	
	public Order (string userId, string userName, int itemCount, Address address)
	{
		this.UserName = userName;
		this.UserId = userId;
		this.ItemCount = itemCount;
		
		this.AddDomainEvent(new OrderCreatedDomainEvent(this));
	}
	
	public void ChangeAddress(Address address)
	{
		this.Address = address;
		
		//TODO:这里也可以添加事件
	}
}

领域事件

public class OrderCreatedDomainEvent : IDomainEvent
{
	public Order Order {get; private set;}
	
	public OrderCreatedDomainEvent(Order order)
	{
		this.Order = order;
	}
}

2.5 基础设施层

引用

  • 基础设施抽象层
  • 领域层

上下文

public class OrderingContext : EFContext
{
	 public OrderingContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options, mediator, capBus){}
	 
	 public DbSet<Order> Orders { get; set; }
	 
	  protected override void OnModelCreating(ModelBuilder modelBuilder){
	  	modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
	  	base.OnModelCreating(modelBuilder);
	  }
}

领域约束

class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
        public void Configure(EntityTypeBuilder<Order> builder)
        {
            builder.HasKey(p => p.Id);
            builder.ToTable("order");
            builder.Property(p => p.UserId).HasMaxLength(20);
            builder.Property(p => p.UserName).HasMaxLength(30);
            builder.OwnsOne(o => o.Address, a =>
                {
                    a.WithOwner();
                    a.Property(p => p.City).HasMaxLength(20);
                    a.Property(p => p.Street).HasMaxLength(50);
                    a.Property(p => p.ZipCode).HasMaxLength(10);
                });
        }
}

管道

    public class OrderingContextTransactionBehavior<TRequest, TResponse> : TransactionBehavior<OrderingContext, TRequest, TResponse>
    {
        public OrderingContextTransactionBehavior(OrderingContext dbContext, ILogger<OrderingContextTransactionBehavior<TRequest, TResponse>> logger) : base(dbContext, logger)
        {
        }
    }

2.6 应用层和接口层

引用

  • 基础设施层

IServiceCollection的扩展

领域事件的处理链路

  • 在接口层或应用层定义IRequest
  public class CreateOrderCommand : IRequest<long>
  {
  	public CreateOrderCommand(int itemCount)
  	{
  		ItemCount = itemCount;
  	}
  	
  	public long ItemCount {get; private set;}
  }
  • 在接口层把IRequest传入
  [HttpPost]
  public async Task<long> CreateOrder([FromBody]CreateorderCommand cmd)
  {
  	return await _medator.Send(cmd, HttpContext.RequestAborted);
  }
  • 在接口层或应用层的IRequestHandlerIRequest进行处理
      public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, long>
      {
          IOrderRepository _orderRepository;
          ICapPublisher _capPublisher;
          public CreateOrderCommandHandler(IOrderRepository orderRepository, ICapPublisher capPublisher)
          {
              _orderRepository = orderRepository;
              _capPublisher = capPublisher;
          }
  
  
          public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
          {
  
              var address = new Address("wen san lu", "hangzhou", "310000");
              var order = new Order("xiaohong1999", "xiaohong", 25, address);
  
              _orderRepository.Add(order);
              await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
              return order.Id;
          }
      }
  • 领域层的领域在创建Order的同时添加了领域事件
   public class Order : Entity<long>, IAggregateRoot
   {
   	public Order(string userId, string userName, int itemCount, Address address)
      {
          this.UserId = userId;
          this.UserName = userName;
          this.Address = address;
          this.ItemCount = itemCount;
  
          this.AddDomainEvent(new OrderCreatedDomainEvent(this));//添加领域事件
  	}
   }
  
  • 领域抽象层的Entity抽象基类定义了对事件的处理
  public abstract class Entity : IEntity
  {
          private List<IDomainEvent> _domainEvents;
          public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly();
  
          public void AddDomainEvent(IDomainEvent eventItem)
          {
              _domainEvents = _domainEvents ?? new List<IDomainEvent>();
              _domainEvents.Add(eventItem);
          }
  
          public void RemoveDomainEvent(IDomainEvent eventItem)
          {
              _domainEvents?.Remove(eventItem);
          }
  
          public void ClearDomainEvents()
          {
              _domainEvents?.Clear();
          }
  }
  • 基础设施抽象层的EFContext在把数据持久化到数据库的同时会发布领域事件

    public class EFContext : DbContext, IUnitOfWork, ITransaction
    {
            public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
            {
                var result = await base.SaveChangesAsync(cancellationToken);
                await _mediator.DispatchDomainEventsAsync(this);//把事件发布出去
                return true;
            }
    }
    
  • 基础设施抽象层定义了对MedatR的扩展方法,本质上是把上下文所有领域的事件发布出去。

          public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
          {
              var domainEntities = ctx.ChangeTracker
                  .Entries<Entity>()
                  .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
  
              var domainEvents = domainEntities
                  .SelectMany(x => x.Entity.DomainEvents)
                  .ToList();
  
              domainEntities.ToList()
                  .ForEach(entity => entity.Entity.ClearDomainEvents());
  
              foreach (var domainEvent in domainEvents)
                  await mediator.Publish(domainEvent);
          }
  • 应用层和接口中定义对领域事件的处理
      public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
      {
          ICapPublisher _capPublisher;
          public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
          {
              _capPublisher = capPublisher;
          }
  
          public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
          {
              await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
          }
      }

这样,领域事件就被发布到Event Bus上,与外界产生了交互,很有可能与外界的另外一个微服务产生交互。

3、集成事件

ddd3

在各个微服务的数据库中一般有2张表,一张用来记录发布事件,另一张用来记录订阅事件。这些事件会随着领域事件的发布持久化到数据库,CAP可以做到业务逻辑和发布订阅事件的一致性。

在应用层或接口层,定义一个集成事件。

    public class OrderCreatedIntegrationEvent
    {
        public OrderCreatedIntegrationEvent(long orderId) => OrderId = orderId;
        public long OrderId { get; }
    }
    
    public class OrderPaymentSucceededIntegrationEvent
    {
        public OrderPaymentSucceededIntegrationEvent(long orderId) => OrderId = orderId;
        public long OrderId { get; }
    }

在应用层或接口层,定义IDomainEventHandler<IDomainEvent>发布集成事件。

    public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
    {
    	//中国开源社区,用于消息的发布和订阅,可以发布到RabbitMQ或者Kafa等
        ICapPublisher _capPublisher;
        public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
        {
            _capPublisher = capPublisher;
        }

        public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
        {
            await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
        }
    }

在应用层或接口层,定义订阅接口。

    public interface ISubscriberService
    {
        void OrderPaymentSucceeded(OrderPaymentSucceededIntegrationEvent @event);
    }

订阅接口实现。

    public class SubscriberService : ISubscriberService, ICapSubscribe
    {
        IMediator _mediator;
        public SubscriberService(IMediator mediator)
        {
            _mediator = mediator;
        }


        [CapSubscribe("OrderPaymentSucceeded")]
        public void OrderPaymentSucceeded(OrderPaymentSucceededIntegrationEvent @event)
        {
            //Do SomeThing
        }

        [CapSubscribe("OrderCreated")]
        public void OrderCreated(OrderCreatedIntegrationEvent @event)
        {

            //Do SomeThing
        }
    }

4、使用RabbitMQ实现EventBus

安装RabbitMQ

CAP的事件如何与业务逻辑放在同一个事务呢?在基础设施抽象层的EFContext中定义如下:

 public class EFContext : DbContext, IUnitOfWork, ITransaction
 {
 	 protected IMediator _mediator;
     ICapPublisher _capBus;
     
      public EFContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options)
      {
      	_mediator = mediator;
      	_capBus = capBus;
      }
      
      ......
        public Task<IDbContextTransaction> BeginTransactionAsync()
        {
            if (_currentTransaction != null) return null;
            _currentTransaction = Database.BeginTransaction(_capBus, autoCommit: false);
            return Task.FromResult(_currentTransaction);
        }
      ......
 }

这样保证了CAP事件的提交回滚与业务事务的提交回滚保持一致。

在应用层或接口层,注册CAP使用RabbitMQ

        public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddTransient<ISubscriberService, SubscriberService>();//有关CAP的接口和实现
            services.AddCap(options =>
            {
                options.UseEntityFramework<OrderingContext>();

                options.UseRabbitMQ(options =>
                {
                    configuration.GetSection("RabbitMQ").Bind(options);
                });
                //options.UseDashboard();
            });

            return services;
        }

appsettings.json中:

  "Mysql": "server=localhost;port=3306;user id=root;password=123456;database=geektime;charset=utf8mb4;ConnectionReset=false;",
  "RabbitMQ": {
    "HostName": "localhost",
    "UserName": "guest",
    "Password": "guest",
    "VirtualHost": "/",//将RabbitMQ空间划分为不同的空间,每个空间可以被认为是一个租户。相同的VirtualHost可以被认为是一个集群
    "ExchangeName": "queue"
  }

Startup.cs中注册:

services.AddEventBus(Configuration);

5、gRPC内部服务间的通讯利器

gRPC是一个远程调用框架,由Google公司发起并开源。通过gRPC让我们可以像调用本地类一样调用远程的服务。提供几乎所有语言的实现。基于HTTP/2,开放协议,受到广泛支持,易于实现和集成。默认使用Protocol Buffers序列化,性能较于RESTful Json好很多。工具链成熟,代码生成便捷,开箱即用。支持双向流式的请求和响应,对批处理、低延时场景友好。gRPC使用自制证书,使用非加密的HTTP2

.NET提供基于HttpClient的原生框架实现。提供原生的ASP.NET Core集成库。提供完整的代码生成工具。Visual StudioVisual Studio Code提供proto文件的智能提示。可以通过命令行工具创建服务。

服务端引用包:

  • Grpc.AspNetCore

客户端核心包:

  • Google.Protobuf
  • Grpc.Net.Client
  • Grpc.Net.ClientFactory
  • Grpc.Tools:提供命令行工具使用的包,用来基于protocol文件生成服务代码。.proto文件定义包、库名,定义服务service,定义输入输出模型message。然后借助Grpc.Tools生成服务端和客户端代码。

gRPC异常处理引用包:

  • Grpc.Core.RpcException
  • Grpc.Core.Interceptors.Interceptor

服务端

order.proto

syntax = "proto3";//协议版本
option csharp_namespace="GrpcServices";//命名空间
package GrpcServices;

service OrderGrpc {//服务
	rpc CreateOrder(CreateOrderCommand) returns (CreateOrderResult);
}

message CreateOrderCommand { //输入,字段的顺序决定了序列化顺序
	string buyerId = 1;
	int32 productId = 2;
	double unitPrice = 3;
	double discount = 4;
	int32 units = 5;	
}

message CreateOrderResult {//响应
	int32 orderId = 1;
}

会自动生成OrderOrderGrpc两个类。

OrderService.cs

public class OrderService : OrderGrpc.OrderGrpcBase
{
	public override Task<CreateOrderResult> CreateOrder(CreateOrderCommand request, ServerCallContext context)
	{
		return Task.FromResult(new CreateOrderResult {OrderId=24;});
	}
}

Startup.cs

sevices.AddGrpc(options => {
	options.EnaleDetailErrors = false;
	options.Intercoptions.Add<ExceptionInterceptor>();
});

app.UseEndpoints(endpoints => {
	endpoints.MapGrpcService<OrderService>();
});

appSettings.json

"Kestrel":{
	"Endpoints":{
		"Http":{
			"Url":"http://+:5000"
		},
		"Https":{
			"Url":"http://+:5001"
		},
		"Http2":{
			"Url":"http://+:5002",
			"Protocols":"Http2"
		}
	}
}

客户端

项目文件中引用服务端的order.proto,这样可以自动生成客户端代码。

<ItemGroup>
	<Protobuf Include="order.proto" GrpService="Client" />
</ItemGroup>

Startup.cs

AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",true);//允许使用不加密的HTTP/2协议

services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(options => {
	//options.Address= new Uri("https://localhost:5000");//服务端地址
	options.Address= new Uri("https://localhost:5002");//不配置证书使用Grpc
}).ConfigurePrimaryHttpMessageHandler(provider => {
	var handler = new SocketHttpHandler();
	handler.SslOptions.RemoteCeretificateValidationCallback = (a, b, c, d) => true;
	return handler;
});

app.UseEndpoints(endpoints => {
	endpoints.MapGet("/", async context => {
		OrderGrpcClient service = context.RequestServices.GetService<OrderGrpcClient>();
		try
		{
			var r = service.CreateOrder(new CreateOrderCommand {BuyerId=""});
		
		}
		catch(Exception ex)
		{
		
		}
	});
});

6、Polly失败重试和熔断机制

组件包:

  • Polly
  • Polly.Extensions.Http
  • Microsoft.Extensions.Http.Polly

Polly的能力

  • 失败重试
  • 服务熔断:服务不可用时快速响应一个熔断结果,避免持续请求不可用的服务导致跪掉
  • 超时处理:当超时发生,比如说可以返回一个缓存结果
  • 舱壁隔离:为服务定义最大的流量和队列,避免请求过服务被压崩
  • 缓存策略
  • 失败降级:当服务不可用时,响应一个更友好的结果而不是报错
  • 组合策略

Polly的使用步骤

  • 定义要处理的异常类型和返回值
  • 定义要处理的动作(重试、熔断、降级响应)
  • 使用定义的策略来执行代码

适合失败重试的场景

  • 服务失败时短暂的,可自愈的

  • 服务时幂等的,重复调用不会有副作用

  • 网络闪断

  • 部分服务节点异常

最佳实践

  • 设置失败重试次数
  • 设置带有步长策略的失败等待间隔:否则会持续不断地重试,类似DDOS攻击
  • 设置降级响应:当失败重试次数达到上限,为服务提供一个降级响应,更好的响应结果
  • 设置短路器:当重试一定次数,可能服务还是不可用

7、网关与BFF

BFF是指Backend For Frontend,负责认证授权,负责服务聚合,目标是为前端提供服务。网关和BFF的职责可以是重叠的。

ddd7

打造网关

  • 添加Ocelot
  • 添加配置文件ocelot.json
  • 添加配置读取代码
  • 注册Ocelot服务
  • 注册Ocelot中间件

网关和负载均衡的区别

方面 网关 负载均衡
OSI模型 第7层 第4层
基于url的路由 可以 不可以
基于cookie的路由 可以 不可以
web防火墙 可以 不可以
地域性 任意ip 云服务商下的某个ip

项目介绍

  • Ordering.API
  "applicationUrl":"https://localhost.5001;http://localhost:5000"
  public class TestController : ControllerBase
  {
  	[HttpGet]
  	public IActionResult Abc()
  	{
  		return Content("Ordering.API");
  	}
  }
  • Mobile.ApiAggregator
  "applicationUrl":"https://localhost.5005;http://localhost:5004"
  public class TestController : ControllerBase
  {
  	[HttpGet]
  	public IActionResult Abc()
  	{
  		return Content("Mobile.ApiAggregator");
  	}
  }
  • Mobile.Gateway网关项目

appSettings.json

 "Apollo": {
    "AppId": "geektime-mobile-gateway",
    "Env": "DEV",
    "MetaServer": "http://192.168.67.76:8080",
    "ConfigServer": [ "http://192.168.67.76:8080" ]
  },
  "AllowedHosts": "*",
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5004
        }
      ],
      "UpstreamPathTemplate": "/mobileAgg/api/{everything}",
      "UpstreamHttpMethod": []
    },
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5000
        }
      ],
      "UpstreamPathTemplate": "/mobile/api/{everything}",
      "UpstreamHttpMethod": []
    }

  ],
  "GlobalConfiguration": {
    "RequestIdKey": "OcRequestId",
    "AdministrationPath": "/administration"
  },
  "SecurityKey": "aabbccddffskldjfklajskdlfjlas234234234"

Startup.cs

 services.AddOcelot(Configuration);
 app.UseOcelot().Wait();

TestController.cs

 public IActionResult Abc()
 {
 	return Content("GeekTime.Mobile.Gateway");
 }

8、防跨站请求伪造

ddd8

如何防御

  • 不适用Cookie存储或传输身份信息
  • 适用AntiforgeryToken机制
  • 避免适用GET作为业务操作的请求方法

两种选择

  • ValidateAntiForgeryToken
  • AutoValidateAntiforgeryToken

举例

[ValidateAntiForgeryToken]
public IActionResult CreateOrder(string itemId, int count)
{
	_logger.LogInformation("创建了订单itemId:{itemId},count:{count}", itemId, count);
	return Content("Order Created");
}

9、防开放重定向攻击

ddd9

防范错误

  • 使用LoalRedirect处理重定向:适合重定向仅限于本站的情况
  • 验证重定向的目标域名是否合法

举例

        public async Task<IActionResult> Login([FromServices]IAntiforgery antiforgery, string name, string password, string returnUrl)
        {
            HttpContext.Response.Cookies.Append("CSRF-TOKEN", antiforgery.GetTokens(HttpContext).RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme
            identity.AddClaim(new Claim("Name", "小王"));
            await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
            if (string.IsNullOrEmpty(returnUrl))
            {
                return Content("登录成功");
            }
            try
            {
                var uri = new Uri(returnUrl);
                ///uri.Host 根据配置表数据库表数据来验证
                return Redirect(returnUrl);
            }
            catch
            {
                return Redirect("/");
            }
            //return Redirect(returnUrl);
        }

10、防跨站脚本

ddd10

防范措施

  • 对用户内容进行验证,拒绝恶意脚本
  • 对用户提交的内容进行编码UrlEncoder,JavaScriptEncoder
  • 慎用HtmlStringHtmlHelper.Raw
  • 身份信息Cookie设置为HttpOnly
  • 避免使用Path传递带有不受信的字符,使用Query进行传递

举例

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                {
                    options.LoginPath = "/home/login";
                    options.Cookie.HttpOnly = true;//这里的设置生效
                });

11、跨域请求

同源

  • 方案相同(HTTP/HTTPS)
  • 主机或域名相同
  • 端口相同

CORS是什么

  • 浏览器允许跨域发起请求
  • 是浏览器的行为协议
  • 并不会让服务器拒绝其它路径发起的HTTP请求
  • 开启时需要考虑是否存在被恶意网站攻击的情形

CORS请求头

  • Origin请求源
  • Access-Control-Reqyest-Method
  • Access-Control-Request-Headers

CORS响应头

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials
  • Access-Control-Expose-Headers
  • Access-Control-Max-Age
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers

默认支持的Expose Headers

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

举例


services.AddCors(options =>
            {
                options.AddPolicy("api", builder =>
                 {
                     builder.WithOrigins("https://localhost:5001").AllowAnyHeader().AllowCredentials().WithExposedHeaders("abc");

                     builder.SetIsOriginAllowed(orgin => true).AllowCredentials().AllowAnyHeader();
                 });
            });
            
            
   app.UseCors();         
    

12、为不同场景设计合适的缓存策略

缓存的场景

  • 计算结果缓存:反射对象缓存
  • 请求结果缓存:DNS缓存
  • 临时共享数据缓存:会话缓存
  • 热点内容缓存:商品详情页
  • 热点变更逻辑数据:秒杀库存数

缓存策略

  • 越接近最终的输出结果效果越好
  • 命中率越高越好

缓存位置

  • 浏览器中
  • 反向代理服务器(负载均衡)
  • 应用进程内存中
  • 分布式存储系统中

要点

  • Key的生成策略,表示数据范围业务含义
  • 缓存失效策略,过期时间机制,主动刷新机制
  • 缓存更新策略,表示更新缓存数据的时机

几个问题

  • 缓存失效,导致数据不一致
  • 缓存穿透,查询无数据时,导致缓存不生效,查询都落在数据库
  • 缓存击穿,缓存失效瞬间,大量请求访问到数据库
  • 缓存雪崩,大量缓存同一时间失效,导致数据库压力

内存缓存和分布式缓存的区别

  • 内存缓存可以存储任意对象
  • 分布式缓存的对象需要支持序列化
  • 分布式缓存远程请求可能失败,内存缓存不会