Tip: 此篇已加入.NET Core微服务基础系列文章索引

一、案例结构与说明

  在上一篇中,我们了解了MassTransit这个开源组件的基本用法,这一篇我们结合一个小案例来了解在ASP.NET Core中如何借助MassTransit+Quartz.Net来实现数据的最终一致性。当然,实现数据的最终一致性有很多方案,这里只是举一种我所学到的比较简单易于学习的实现方式而已。

  假设我们有一个XX保险微信商城(WechatShop,简称WS)系统,根据服务的划分,针对下订单的这个场景,我们划分了四个微服务,他们分别是订单服务(OrderService),库存服务(StorageService)、配送服务(DeliveryService)以及一个只用于后台监控事件消息的事件后台服务(EventService)。

  我们来看看这个场景下的一些基本业务逻辑流程(可能并不一定正确或者覆盖全面):

  (1)当终端User在微信商城中看中一个保单产品,并且下单之后,订单服务会接收订单信息并更新订单数据库(这里假设只更新一张订单表)。

  (2)然后事件后台服务会定期(比如每隔30秒)检查订单数据库的事件状态表(比如一张单独的Events表,里面有某个订单的2行记录,每行记录分别代表与订单相关的服务(这里就是库存和配送服务)的处理状态),如果发现相关服务的事件状态为未处理,则会向事件总线(假设这里基于RabbitMQ)发送消息告知对应订阅者要处理这个订单。

  (3)这里对应的订阅者就是库存服务和配送服务,他们收到消息之后,会进行各自的业务逻辑处理。比如,库存服务会根据订单信息去更新库存数据库并做一些逻辑处理比如更新保单促销活动的记录,配送服务会根据订单信息更新配送数据库并做一些逻辑处理比如打印纸质保单并进行物流预约登记,当他们各自处理完成之后便会向事件总线发送一个处理完毕的消息。

  (4)事件后台服务也会作为订阅者,接收库存和配送服务发送过来的消息,如果接收到某个服务的处理完毕消息,便会根据接收到的消息去更新前面事件状态表中的对应的事件记录记录行。比如:接收到库存服务发送的消息,会更新时间状态表中这个OrderID相关的库存事件状态的那一行记录的状态为已处理。

  (5)事件后台服务的定时任务中(这里假设每隔30秒一次),会Check事件是否还有未处理完毕的事件消息,如果没有则休眠,否则会检查其创建记录的时间与现在的系统时间的间隔是否超过了最大容忍值(这里假设1小时),如果没有超过则继续向事件总线发送消息,如果超过了则进行一些事务回滚逆操作和向管理员发送一些警告信息以便进行人工干预等操作。

  Talk is cheap, Show me the code。下面我们来看看如何实现,由于篇幅原因可能只会列出关键性代码,详细代码请自行去GitHub上下载或Clone。

  下面是这次实验的项目结构,需要准备如下五个项目(四个ASP.NET Core WebAPI和一个.NET Core类库)

  

  数据库这里实验采用的是MSSQL,只创建了一个Order数据库,两张表的数据如下:

  

  可以看到,在Events表的设计中,通过EventType来区分事件类型,这里是订单创建(CreateOrder)这个事件的两个具体消息(StorageService和DeliveryService),状态(StatusValue)为1代表未处理,为2则代表已处理。

二、OrderService的实现

2.1 准备工作

  

  其中,Controllers中主要用于与终端用户(比如WebForm、MVC、SPA等)交互,Models下主要用于定义DTO、EF DbContext与Entity,Repositories目录下主要用于定义Repository与数据库进行交互。

2.2 具体实现

  (1)OrderController

    [Route("api/Order")]
public class OrderController : Controller
{
public IOrderRepository OrderRepository { get; } public OrderController(IOrderRepository OrderRepository)
{
this.OrderRepository = OrderRepository;
} [HttpPost]
public string Post([FromBody]OrderDTO orderDTO)
{
var result = OrderRepository.CreateOrder(orderDTO).GetAwaiter().GetResult(); return result ? "Post Success" : "Post Failed";
}
}

  这里就是简单的调用OrderRepository进行订单的创建操作。

  (2)OrderRepository

    public class OrderRepository : IOrderRepository
{
public OrderDbContext OrderContext { get; } public OrderRepository(OrderDbContext OrderContext)
{
this.OrderContext = OrderContext;
} public async Task<bool> CreateOrder(IOrder order)
{
var orderEntity = new Order()
{
ID = GenerateOrderID(),
OrderUserID = order.OrderUserID,
OrderTime = order.OrderTime,
OrderItems = null,
}; OrderContext.Orders.Add(orderEntity); // patch data
order.ID = orderEntity.ID;
order.StatusKey = EventConstants.EVENT_STATUS_KEY_STORAGE; var eventEntityA = new Event()
{
ID = GenerateEventID(),
OrderID = orderEntity.ID,
Type = EventConstants.EVENT_TYPE_CREATE_ORDER,
StatusKey = EventConstants.EVENT_STATUS_KEY_STORAGE,
StatusValue = EventStatusEnum.UNHANDLE,
CreatedTime = DateTime.Now,
EntityJson = JsonHelper.SerializeObject(order)
}; order.StatusKey = EventConstants.EVENT_STATUS_KEY_DELIVERY; var eventEntityB = new Event()
{
ID = GenerateEventID(),
OrderID = orderEntity.ID,
Type = EventConstants.EVENT_TYPE_CREATE_ORDER,
StatusKey = EventConstants.EVENT_STATUS_KEY_DELIVERY,
StatusValue = EventStatusEnum.UNHANDLE,
CreatedTime = DateTime.Now,
EntityJson = JsonHelper.SerializeObject(order)
}; OrderContext.Events.Add(eventEntityA);
OrderContext.Events.Add(eventEntityB); int count = await OrderContext.SaveChangesAsync(); return count == ;
} private string GenerateOrderID()
{
// TODO: Some business logic to generate Order ID
return Guid.NewGuid().ToString();
} private string GenerateEventID()
{
// TODO: Some business logic to generate Order ID
return Guid.NewGuid().ToString();
}
}

  这里主要是通过EF Core向订单数据库中的Orders表和Events表添加数据。可以看到,这里向Events表中添加了两个记录,分别通过StatusKey进行区分。这里的StatusKey其实是一个冗余字段,只是为了后面在不同的服务之间区分是否是自己需要处理的消息。

三、StorageService与DeliveryService的实现

3.1 StorageService的实现

  

  (1)通过NuGet安装MassTransit、MassTransit.RabbitMQ、MassTransit.Extensions.DependencyInjection

  (2)在StartUp类中注入MassTransit的IBusControl实例,加入了熔断、重试与限流,具体看注释

    public class Startup
{
...... // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
...... // EFCore
services.AddDbContextPool<OrderDbContext>(
options => options.UseSqlServer(Configuration["DB:OrderDB"])); // Repository
services.AddScoped<IStorageRepository, StorageRepsitory>();
services.AddScoped<StoreageOrderEventHandler>(); // MassTransit
services.AddMassTransit(c =>
{
c.AddConsumer<StoreageOrderEventHandler>();
});
} public static IBusControl BusControl { get; private set; } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider, IHostingEnvironment env, IApplicationLifetime lifetime)
{
...... // Register Bus
BusControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
var host = cfg.Host(new Uri(Configuration["MQ:Host"]), hst =>
{
hst.Username(Configuration["MQ:UserName"]);
hst.Password(Configuration["MQ:Password"]);
}); cfg.ReceiveEndpoint(host, Configuration["MQ:Queues:Storage"], e =>
{
// Retry
e.UseRetry(ret =>
{
ret.Interval(, TimeSpan.FromSeconds()); // 每隔10s重试一次,最多重试3次
}); // RateLimit
e.UseRateLimit(, TimeSpan.FromSeconds()); // 100s内限1000次访问请求 // CircuitBreaker
e.UseCircuitBreaker(cb =>
{
cb.TrackingPeriod = TimeSpan.FromMinutes(); // 跟踪周期:1min
cb.TripThreshold = ; // 失败比例达到15%后才会打开熔断器
cb.ActiveThreshold = ; // 至少发生5次请求后才会打开熔断器
cb.ResetInterval = TimeSpan.FromMinutes(); // 熔断时间间隔:5mins
}); e.LoadFrom(serviceProvider);
});
}); // Register Start & Stop for Bus
lifetime.ApplicationStarted.Register(BusControl.Start);
lifetime.ApplicationStarted.Register(BusControl.Stop);
}
}

  这里需要注意的就是,在注入时我们指定了接收消息所要进行处理的类对象:StorageOrderEventHandler。

  (3)StorageOrderEventHandler

    public class StoreageOrderEventHandler : IConsumer<IOrder>
{
public IStorageRepository StorageRepository { get; }
public IBusControl EventBus { get; } public StoreageOrderEventHandler(IStorageRepository StorageRepository)
{
this.StorageRepository = StorageRepository;
this.EventBus = Startup.BusControl;
} public async Task Consume(ConsumeContext<IOrder> context)
{
var order = context.Message;
if (order.StatusKey != EventConstants.EVENT_STATUS_KEY_STORAGE)
{
// 如果不是StorageService要处理的Event则忽略该消息
return;
} var result = StorageRepository.CreateStorage(order).GetAwaiter().GetResult();
if (result)
{
IOrderEventEntity orderEventEntity = new OrderEventEntity
{
OrderID = order.ID,
EventType = EventConstants.EVENT_TYPE_CREATE_ORDER,
StatusKey = EventConstants.EVENT_STATUS_KEY_STORAGE,
StatusValue = EventStatusEnum.HANDLED
}; await EventBus.Publish(orderEventEntity);
}
}

  这里首先判断接收到的消息是否是需要自己处理的,不是则忽略,是则调用StorageRepository进行库存记录的更新及其他业务逻辑(与DB有关的)操作,处理完毕后向事件总线发送一个消息,这里主要是告知哪个OrderID的哪个事件类型(EventType)的哪个具体服务(StatusKey)要更新到哪个状态(StatusValue)。

  (4)StorageRepository:假装自己是一个真实的仓储

    public class StorageRepsitory : IStorageRepository
{
public OrderDbContext OrderContext{ get; } public StorageRepsitory(OrderDbContext OrderContext)
{
this.OrderContext = OrderContext;
} public async Task<bool> CreateStorage(IOrder order)
{
var eventList = await OrderContext.Events.Where(e => e.OrderID == order.ID
&& e.Type == EventConstants.EVENT_TYPE_CREATE_ORDER
&& e.StatusKey == EventConstants.EVENT_STATUS_KEY_STORAGE).ToListAsync(); if (eventList == null)
{
return false;
} foreach (var eventItem in eventList)
{
try
{
// TODO : Add record to Storage DB
Console.WriteLine($"Add one record to Storage DB : { JsonHelper.SerializeObject(order) }");
}
catch (Exception ex)
{
// TODO : Exception log
Console.WriteLine($"Exception: {ex.Message}");
return false;
}
} return true;
}
}

  这里假设会做一些业务处理,比如向库存数据库添加一条记录等。由于时间和精力,这里我只向控制台输出一条消息已进行验证。

3.2 DeliveryService的实现

  与StorageService高度类似,篇幅关系,不再赘述,请自行浏览示例源代码。这里之展示一下DeliveryOrderEventHandler类:

    public class DeliveryOrderEventHandler : IConsumer<IOrder>
{
public IDeliveryRepository DeliveryRepository { get; }
public IBusControl EventBus { get; } public DeliveryOrderEventHandler(IDeliveryRepository StorageRepository)
{
this.DeliveryRepository = StorageRepository;
this.EventBus = Startup.BusControl;
} public async Task Consume(ConsumeContext<IOrder> context)
{
var order = context.Message;
if (order.StatusKey != EventConstants.EVENT_STATUS_KEY_DELIVERY)
{
// 如果不是DeliveryService要处理的Event则忽略该消息
return;
} var result = DeliveryRepository.CreateDelivery(order).GetAwaiter().GetResult();
if (result)
{
IOrderEventEntity orderEventEntity = new OrderEventEntity
{
OrderID = order.ID,
EventType = EventConstants.EVENT_TYPE_CREATE_ORDER,
StatusKey = EventConstants.EVENT_STATUS_KEY_DELIVERY,
StatusValue = EventStatusEnum.HANDLED
}; await EventBus.Publish(orderEventEntity);
}
}
}

  可以看出,这里的业务逻辑和StorageService一样,只是StatusKey不同而已。

四、EventService的实现

4.1 项目结构

  

  在EventService中,除了安装MassTransit相关的package之外,还要安装Quartz.Net的package。

4.2 StartUp类

  (1)ConfigureService方法

    public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc(); // EFCore
services.AddDbContextPool<OrderDbContext>(
options => options.UseSqlServer(Configuration["DB:OrderDB"])); // Dapper-ConnString
services.AddSingleton(Configuration["DB:OrderDB"]); // Repository
services.AddSingleton<IEventRepository<IOrderEventEntity>, OrderEventDapperRepository>(); // Quartz
services.UseQuartz(typeof(OrderEventJob)); // MassTransit
services.AddMassTransit(c =>
{
c.AddConsumer<OrderEventHandler>();
});
}

  这里指定了Quartz.Net要处理的定时任务(通过自定义的扩展方法)以及MassTransit要处理的事件处理程序。有关Quartz.Net的内容不在本篇的重点,下面看看OrderEventHandler类,它主要就是根据收到的消息去更新某个类别某个项的事件状态记录。

    public class OrderEventHandler : IConsumer<IOrderEventEntity>
{
public IEventRepository<IOrderEventEntity> EventRepository { get; } public OrderEventHandler(IEventRepository<IOrderEventEntity> EventRepository)
{
this.EventRepository = EventRepository;
} public async Task Consume(ConsumeContext<IOrderEventEntity> context)
{
var eventResult = await EventRepository.UpdateEventStatus(EventConstants.EVENT_TYPE_CREATE_ORDER, context.Message);
}
}

  在EventRepository具体类中,这里主要是通过Dapper去操作数据库:

    public class OrderEventDapperRepository : IEventRepository<IOrderEventEntity>
{
private string connStr; public OrderEventDapperRepository(string connStr)
{
this.connStr = connStr;
} public async Task<IEnumerable<IOrderEventEntity>> GetEvents(string eventType)
{
using (var conn = new SqlConnection(connStr))
{
string sqlCommand = @"SELECT [EventID],
[EventType],
[OrderID],
[CreatedTime],
[StatusKey],
[StatusValue],
[EntityJson]
FROM [dbo].[Events]
WHERE EventType = @EventType
AND StatusValue = @StatusValue"; var result = await conn.QueryAsync<OrderEventEntity>(sqlCommand, param: new
{
EventType = EventConstants.EVENT_TYPE_CREATE_ORDER,
StatusValue = EventStatusEnum.UNHANDLE
}); return result;
}
} public async Task<bool> UpdateEventStatus(string eventType, IOrderEventEntity orderEvent)
{
using (var conn = new SqlConnection(connStr))
{
string sqlCommand = @"UPDATE [dbo].[Events]
SET StatusValue = @StatusValue
WHERE OrderID = @OrderID
AND EventType = @EventType
AND StatusKey = @StatusKey"; var result = await conn.ExecuteAsync(sqlCommand, param: new
{
OrderID = orderEvent.OrderID,
EventType = EventConstants.EVENT_TYPE_CREATE_ORDER,
StatusKey = orderEvent.StatusKey,
StatusValue = EventStatusEnum.HANDLED
}); return result > ;
}
}
}

  *Problem: 在coding过程中,本来想用EF的,结果发现DbContext默认注入的周期是Scoped,而我们的定时Job又是Singleton的,无法正常使用,所以就改用了Dapper。在这个类中,未完成的方法是进行事务回滚逆操作的一系列方法。

  (2)Configure方法

    public static IBusControl BusControl { get; private set; }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider, IHostingEnvironment env, IApplicationLifetime lifetime, IScheduler scheduler)
{
//app.UseMvc(); // Register EventBus
BusControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
var host = cfg.Host(new Uri(Configuration["MQ:Host"]), hst =>
{
hst.Username(Configuration["MQ:UserName"]);
hst.Password(Configuration["MQ:Password"]);
}); // Retry
cfg.UseRetry(ret =>
{
ret.Interval(, TimeSpan.FromSeconds()); // 每隔10s重试一次,最多重试3次
}); // RateLimit
cfg.UseRateLimit(, TimeSpan.FromSeconds()); // 100s内限1000次访问请求 // CircuitBreaker
cfg.UseCircuitBreaker(cb =>
{
cb.TrackingPeriod = TimeSpan.FromMinutes(); // 跟踪周期:1min
cb.TripThreshold = ; // 失败比例达到15%后才会打开熔断器
cb.ActiveThreshold = ; // 至少发生5次请求后才会打开熔断器
cb.ResetInterval = TimeSpan.FromMinutes(); // 熔断时间间隔:5mins
}); cfg.ReceiveEndpoint(host, Configuration["MQ:Queues:Order"], e =>
{
e.LoadFrom(serviceProvider);
});
}); // Register Start & Stop for Bus
lifetime.ApplicationStarted.Register(BusControl.Start);
lifetime.ApplicationStarted.Register(BusControl.Stop); // Scheduler Job
QuartzServiceUtil.StartJob<OrderEventJob>(scheduler, TimeSpan.FromSeconds());
}

  由于无法在ConfigureService方法中正常的注入MassTransit,所以采用了一个折中的方式:静态变量,还好我们只需要一个单例的BusControl即可。这里我们在启动时,开启了一个定时任务,这个定时任务的逻辑如下,它每隔30s执行一次。

    public class OrderEventJob: IJob
{
public IEventRepository<IOrderEventEntity> OrderEventRepository { get; }
public IBusControl EventBus { get; }
private int MaxHours; public OrderEventJob(IEventRepository<IOrderEventEntity> OrderEventRepository, IConfiguration configuration)
{
this.OrderEventRepository = OrderEventRepository;
this.EventBus = Startup.BusControl;
this.MaxHours = Convert.ToInt32(configuration["Service:MaxHours"]);
} public async Task Execute(IJobExecutionContext context)
{
var events = OrderEventRepository.GetEvents(EventConstants.EVENT_TYPE_CREATE_ORDER).GetAwaiter().GetResult(); if (events == null)
{
await Console.Out.WriteLineAsync($"[Tip] There's no pending to process Order events.");
return;
} foreach (var eventItem in events)
{
if (GetDifferenceInHours(eventItem.CreatedTime) >= MaxHours)
{
// TODO:
// 1.Rollback previous transaction by send rollback message
// 2.Send Email/Messages to administrator
// ...... break;
} IOrder order = JsonHelper.DeserializeJsonToObject<Order>(eventItem.EntityJson);
await EventBus.Publish(order);
}
} private double GetDifferenceInHours(DateTime createdTime)
{
DateTime currentTime = DateTime.Now;
TimeSpan ts = currentTime.Subtract(createdTime);
double differenceInDays = ts.TotalHours; return differenceInDays;
}
}

  这里的MaxHours(最大容忍小时)在配置文件中设置的是1,即1小时。在每个定时任务中,系统会去首先check未处理的事件消息的创建时间和现在系统时间的间隔时间是否超过了1小时,超过了则会进行一系列的回滚逆操作和发送邮件/短信等操作告知人工干预,这一部分由于时间和精力未实现,有兴趣的可以自己去实现。如果没超过,则会将事件状态表记录行中的EntityJson(这里主要是订单表的序列化后的JSON字符串)反序列化并作为消息进行发送给事件总线从而通知订阅者。

五、快速测试

5.1 向OrderService发送一个订单请求

  

  首先,清空测试的订单数据库表,此时无一条记录。

  然后,通过PostMan工具向OrderService发送一条订单请求(前提是你得同时把这四个服务一起启动起来):

  

5.2 Check此时的订单数据库

  

  此时已经有了对应的数据,可以看到DeliveryService和StorageService的两个事件状态记录行的状态是1(代表待处理)

5.3 一定时间后的订单数据库

  

  经过两个服务的处理之后,状态变为了2(代表已处理),再看看两个服务的控制台信息,分别在处理事件消息时输出了一行记录:

  

  

  在标准情况下,当所有相关的事件消息状态都变成已处理时,这时数据就达到了最终一致性。当然,还有一些重试的补偿和事务的回滚逆操作,没有做演示,有兴趣可以自行研究。

六、小结

  本篇主要基于一个小案例(订单业务处理场景),首先介绍了其业务场景与基本业务流程,然后通过介绍相关的每个服务的代码实现,最后通过一个快速的测试演示了数据如何达到最终一致性。当然,这个小案例并不完整,没有对重试的补偿机制以及失败后的回滚机制进行演示和测试,不过有兴趣的朋友可以自行改代码实现。最后,再次强调实现数据的最终一致性有很多方案,这里只是举一种我从桂素伟老师那里所学到的比较简单易于学习的实现方式而已。对MassTransit感兴趣想应用于生产环境的朋友,可以去了解了解saga,建议看看这篇文章:《MassTransit&Sage分布式服务开发PPT分享

示例代码

  Click Here => 点我下载

参考资料

(1)桂素伟,《基于.NET Core的微服务架构

(2)richieyangs(张阳),《如何优雅的使用RabbitMQ》,《使用Masstransit开发基于消息传递的分布式应用

(3)青客宝团队,《MassTransit&Sagas分布式服务开发ppt分享

(4)成天,《MassTransit实现应用程序间的交互

(5)娃娃都会打酱油了,《MassTransit学习记录

(6)MassTransit 官方文档,http://masstransit-project.com/MassTransit/

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

.NET Core微服务之基于MassTransit实现数据最终一致性(Part 2)的更多相关文章

  1. .NET Core微服务之基于MassTransit实现数据最终一致性(Part 1)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.预备知识:数据一致性 关于数据一致性的文章,园子里已经有很多了,如果你还不了解,那么可以通过以下的几篇文章去快速地了解了解,有个感性认 ...

  2. .NET Core微服务之基于Ocelot实现API网关服务

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.啥是API网关? API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口.这样就可以明显的简化客户端 ...

  3. .NET Core微服务之基于Ocelot实现API网关服务(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.负载均衡与请求缓存 1.1 负载均衡 为了验证负载均衡,这里我们配置了两个Consul Client节点,其中ClientServic ...

  4. .NET Core微服务之基于Consul实现服务治理

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Consul基础介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置.与其他分布式服务注册与发 ...

  5. .NET Core微服务之基于Consul实现服务治理(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 上一篇发布之后,很多人点赞和评论,不胜惶恐,这一篇把上一篇没有弄到的东西补一下,也算是给各位前来询问的朋友的一些回复吧. 一.Consul ...

  6. .NET Core微服务之基于IdentityServer建立授权与验证服务

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.IdentityServer的预备知识 要学习IdentityServer,事先得了解一下基于Token的验证体系,这是一个庞大的主题 ...

  7. .NET Core微服务之基于IdentityServer建立授权与验证服务(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 上一篇我们基于IdentityServer4建立了一个AuthorizationServer,并且继承了QuickStartUI,能够成功 ...

  8. .NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.案例结构总览 这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为 ...

  9. .NET Core微服务之基于Exceptionless实现分布式日志记录

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Exceptionless极简介绍 Exceptionless 是一个开源的实时的日志收集框架,它可以应用在基于 ASP.NET,AS ...

随机推荐

  1. Python定时任务

    在项目中,我们可能遇到有定时任务的需求.其一:定时执行任务.例如每天早上 8 点定时推送早报.其二:每隔一个时间段就执行任务.比如:每隔一个小时提醒自己起来走动走动,避免长时间坐着.今天,我跟大家分享 ...

  2. 测试网页时需要添加等待的情形 (Selenium)

    测试网页时需要添加等待的情形: 1. 网页跳转 2. DOM结构发生改变.

  3. 学习笔记1--响应式网页+Bootstrap起步+全局CSS样式

    一.学习之前要了解一些背景知识: 在2g时代,3g时代,4g时代,早期的网页浏览设备,功能机,智能机.(本人最喜欢的透明肌,和古典黑莓机) 1.什么是响应式网页? Responsive Web Pag ...

  4. BZOJ_3747_[POI2015]Kinoman_线段树

    BZOJ_3747_[POI2015]Kinoman_线段树 Description 共有m部电影,编号为1~m,第i部电影的好看值为w[i]. 在n天之中(从1~n编号)每天会放映一部电影,第i天放 ...

  5. Python笔记 in 机器学习

    Python3的函数实在太多了,在机器学习的过程中,总会一不留神就学到一个python的高效率用法,怕自己会忘记,所以更新在这篇随笔中. 更新至:2018.5.23 字符串str的前缀或者后缀识别 s ...

  6. HttpClient4 TIME_WAIT和CLOSE_WAIT

    最近,公司的接口服务器(客户端,向外发送数据)频繁出现了connect timeout 以及readtime out 的情况,经过运维平台检测,并没有网络延时的情况.于是,开始怀疑连接池出了问题. 使 ...

  7. java quartz 计算近20次执行时间

    /** * * @desc 计算表达式近20次时间 * @auth josnow * @date 2017年5月31日 下午12:16:25 * @param cron * @return */ pu ...

  8. Jmeter----创建第一个接口测试流程

    第一步.创建线程 第二步.添加一个HTTP请求 第三步.设置request的请求头信息 根据自己需要填写的请求头信息进行填写,如下是我需要接口测试时填写的请求头 第四步.设置相关的HTTP请求参数,完 ...

  9. Mtcnn进行人脸剪裁和对齐B

    Mtcnn进行人脸剪裁和对齐 from scipy import misc import tensorflow as tf import detect_face import cv2 # import ...

  10. 你真的了解String吗?(修正版)

    修正前:new出来的对象,会在堆中存放真正的值: 大错特错!!!! 修正后:new出来的对象,堆存放的并不是真正的值,而是常量池中字符串常量的地址. 一.抛砖引玉 ​ 不知道大家在做面试题时是否会遇到 ...