MassTransit | 基于MassTransit Courier 实现 Saga 编排式分布式事务

Saga 模式

Saga 最初出现在1987年Hector Garcaa-Molrna & Kenneth Salem发表的一篇名为《Sagas》的论文里。其核心思想是将长事务拆分为多个短事务,借助Saga事务协调器的协调,来保证要么所有操作都成功完成,要么运行相应的补偿事务以撤消先前完成的工作,从而维护多个服务之间的数据一致性。举例而言,假设有个在线购物网站,其后端服务划分为订单服务、支付服务和库存服务。那么一次下订单的Saga流程如下图所示:

b1d04cf3619c81e6f066e1d226f7f1a5.png

在Saga模式中本地事务是Saga 参与者执行的工作单元,每个本地事务都会更新数据库并发布消息或事件以触发 Saga 中的下一个本地事务。如果本地事务失败,Saga 会执行一系列补偿事务,以撤消先前本地事务所做的更改。 对于Saga模式的实现又分为两种形式:

  1. 协同式:把Saga 的决策和执行顺序逻辑分布在Saga的每个参与方中,通过交换事件的方式进行流转。示例图如下所示:

144f95960d0906a9189e3f49d580fdf0.png
  1. 编排式:把Saga的决策和执行顺序逻辑集中定义在一个Saga 编排器中。Saga 编排器发出命令式消息给各个Saga 参与方,指示这些参与方执行怎样的操作。

37dd9a4a66be4de07cc2a219c59fdc9c.png

从上图可以看出,对于协同式Saga 存在一个致命的弊端,那就是存在循环依赖的问题,每个Saga参与方都需要订阅所有影响它们的事件,耦合性较高,且由于Saga 逻辑分散在各参与方,不便维护。相对而言,编排式Saga 则实现了关注点分离,协调逻辑集中在编排器中定义,Saga 参与者仅需实现供编排器调用的API 即可。 在.NET 中也有开箱即用的开源框架实现了编排式的Saga事务模型,也就是MassTransit Courier,接下来就来实际探索一番。

MassTransit Courier 简介

MassTransit Courier 是对Routing Slip(路由单) 模式的实现。该模式用于运行时动态指定消息处理步骤,解决不同消息可能有不同消息处理步骤的问题。实现机制是消息处理流程的开始,创建一个路由单,这个路由单定义消息的处理步骤,并附加到消息中,消息按路由单进行传输,每个处理步骤都会查看_路由单_并将消息传递到路由单中指定的下一个处理步骤。 在MassTransit Courier中是通过抽象IActivityRoutingSlip来实现了Routing Slip模式。通过按需有序组合一系列的Activity,得到一个用来限定消息处理顺序的Routing Slip。而每个Activity的具体抽象就是IActivityIExecuteActivity。二者的差别在于IActivity定义了ExecuteCompensate两个方法,而IExecuteActivitiy仅定义了Execute方法。其中Execute代表正向操作,Compensate代表反向补偿操作。用一个简单的下单流程:创建订单->扣减库存->支付订单举例而言,使用Courier的实现示意图如下所示:7a0f45b32acb69ef4baf12fd154ad170.jpeg

基于Courier 实现编排式Saga事务

那具体如何使用MassTransit Courier来应用编排式Saga 模式呢,接下来就来创建解决方案来实现以上下单流程示例。

创建解决方案

依次创建以下项目,除共享类库项目外,均安装MassTransitMassTransit.RabbitMQNuGet包。

项目项目名项目类型
订单服务MassTransit.CourierDemo.OrderServiceASP.NET Core Web API
库存服务MassTransit.CourierDemo.InventoryServiceWorker Service
支付服务MassTransit.CourierDemo.PaymentServiceWorker Service
共享类库MassTransit.CourierDemo.SharedClass Library

三个服务都添加扩展类MassTransitServiceExtensions,并在Program.cs类中调用services.AddMassTransitWithRabbitMq();注册服务。

using System.Reflection;
using MassTransit.CourierDemo.Shared.Models;namespace MassTransit.CourierDemo.InventoryService;public static class MassTransitServiceExtensions
{public static IServiceCollection AddMassTransitWithRabbitMq(this IServiceCollection services){return services.AddMassTransit(x =>{x.SetKebabCaseEndpointNameFormatter();// By default, sagas are in-memory, but should be changed to a durable// saga repository.x.SetInMemorySagaRepositoryProvider();var entryAssembly = Assembly.GetEntryAssembly();x.AddConsumers(entryAssembly);x.AddSagaStateMachines(entryAssembly);x.AddSagas(entryAssembly);x.AddActivities(entryAssembly);x.UsingRabbitMq((context, busConfig) =>{busConfig.Host(host: "localhost",port: 5672,virtualHost: "masstransit",configure: hostConfig =>{hostConfig.Username("guest");hostConfig.Password("guest");});busConfig.ConfigureEndpoints(context);});});}
}

订单服务

订单服务作为下单流程的起点,需要承担构建RoutingSlip的职责,因此可以创建一个OrderRoutingSlipBuilder来构建RoutingSlip,代码如下:

using MassTransit.Courier.Contracts;
using MassTransit.CourierDemo.Shared.Models;namespace MassTransit.CourierDemo.OrderService;
public static class OrderRoutingSlipBuilder
{public static RoutingSlip BuildOrderRoutingSlip(CreateOrderDto createOrderDto){var createOrderAddress = new Uri("queue:create-order_execute");var deduceStockAddress = new Uri("queue:deduce-stock_execute");var payAddress = new Uri("queue:pay-order_execute");        var routingSlipBuilder = new RoutingSlipBuilder(Guid.NewGuid());routingSlipBuilder.AddActivity(name: "order-activity",executeAddress: createOrderAddress,arguments: createOrderDto);routingSlipBuilder.AddActivity(name: "deduce-stock-activity", executeAddress: deduceStockAddress);routingSlipBuilder.AddActivity(name: "pay-activity", executeAddress: payAddress);var routingSlip = routingSlipBuilder.Build();return routingSlip;}
}

从以上代码可知,构建一个路由单需要以下几步:

  1. 明确业务用例涉及的具体用例,本例中为:

    1. 创建订单:CreateOrder

    2. 扣减库存:DeduceStock

    3. 支付订单:PayOrder

  2. 根据用例名,按短横线隔开命名法(kebab-case)定义用例执行地址,格式为queue:<usecase>_execute,本例中为:

    1. 创建订单执行地址:queue:create-order_execute

    2. 创建订单执行地址:queue:deduce-stock_execute

    3. 创建订单执行地址:queue:pay-order_execute

  3. 创建路由单:

    1. 通过RoutingSlipBuilder(Guid.NewGuid())创建路由单构建器实例

    2. 根据业务用例流转顺序,调用AddActivity()方法依次添加Activity用来执行用例,因为第一个创建订单用例需要入口参数,因此传入了一个CreateOrderDtoDTO(Data Transfer Object)对象

    3. 调用Build()方法创建路由单

对于本例而言,由于下单流程是固定流程,因此以上路由单的构建也是按业务用例进行定义的。而路由单的强大之处在于,可以按需动态组装。在实际电商场景中,有些订单是无需执行库存扣减的,比如充值订单,对于这种情况,仅需在创建路由单时判断若为充值订单则不添加扣减库存的Activity即可。 对于订单服务必然要承担创建订单的职责,定义CreateOrderActivity(Activity的命名要与上面定义的用例对应)如下,其中OrderRepository为一个静态订单仓储类:

public class CreateOrderActivity : IActivity<CreateOrderDto, CreateOrderLog>
{private readonly ILogger<CreateOrderActivity> _logger;public CreateOrderActivity(ILogger<CreateOrderActivity> logger){_logger = logger;}// 订单创建public async Task<ExecutionResult> Execute(ExecuteContext<CreateOrderDto> context){var order = await CreateOrder(context.Arguments);var log = new CreateOrderLog(order.OrderId, order.CreatedTime);_logger.LogInformation($"Order [{order.OrderId}] created successfully!");return context.CompletedWithVariables(log, new {order.OrderId});}private async Task<Order> CreateOrder(CreateOrderDto orderDto){var shoppingItems =orderDto.ShoppingCartItems.Select(item => new ShoppingCartItem(item.SkuId, item.Price, item.Qty));var order = new Order(orderDto.CustomerId).NewOrder(shoppingItems.ToArray());await OrderRepository.Insert(order);return order;}// 订单补偿(取消订单)public async Task<CompensationResult> Compensate(CompensateContext<CreateOrderLog> context){var order = await OrderRepository.Get(context.Log.OrderId);order.CancelOrder();var exception = context.Message.ActivityExceptions.FirstOrDefault();_logger.LogWarning($"Order [{order.OrderId} has been canceled duo to {exception.ExceptionInfo.Message}!");return context.Compensated();}
}

从以上代码可知,实现一个Activity,需要以下步骤:

  1. 定义实现IActivity<in TArguments, in TLog>需要的参数类:

    1. TArguments对应正向执行入口参数,会在Execute方法中使用,本例中为CreateOrderDto,用于订单创建。

    2. TLog对应反向补偿参数,会在Compensate方法中使用,本例中为CreateOrderLog,用于订单取消。

  2. 实现IActivity<in TArguments, in TLog>接口中的Execute方法:

    1. 具体用例的实现,本例中对应订单创建逻辑

    2. 创建TLog反向补偿参数实例,以便业务异常时能够按需补偿

    3. 返回Activity执行结果,并按需传递参数至下一个Activity,本例仅传递订单Id至下一流程。

  3. 实现IActivity<in TArguments, in TLog>接口中的Compensate方法:

    1. 具体反向补偿逻辑的实现,本例中对应取消订单

    2. 返回反向补偿执行结果

订单服务的最后一步就是定义WebApi来接收创建订单请求,为简要起便创建OrderController如下:

using MassTransit.CourierDemo.Shared.Models;
using Microsoft.AspNetCore.Mvc;namespace MassTransit.CourierDemo.OrderService.Controllers;[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{private readonly IBus _bus;public OrderController(IBus bus){_bus = bus;}[HttpPost]public async Task<IActionResult> CreateOrder(CreateOrderDto createOrderDto){// 创建订单路由单var orderRoutingSlip = OrderRoutingSlipBuilder.BuildOrderRoutingSlip(createOrderDto);// 执行订单流程await _bus.Execute(orderRoutingSlip);return Ok();}
}

库存服务

库存服务在整个下单流程的职责主要是库存的扣减和返还,但由于从上游用例仅传递了OrderId参数到库存扣减Activity,因此在库存服务需要根据OrderId 去请求订单服务获取要扣减的库存项才能执行扣减逻辑。而这可以通过使用MassTransit的Reqeust/Response 模式来实现,具体步骤如下:

  1. 在共享类库MassTransit.CourierDemo.Shared中定义IOrderItemsRequestIOrderItemsResponse

namespace MassTransit.CourierDemo.Shared.Models;public interface IOrderItemsRequest
{public string OrderId { get; }
}
public interface IOrderItemsResponse
{public List<DeduceStockItem> DeduceStockItems { get; set; }public string OrderId { get; set; }
}
  1. 在订单服务中实现IConsumer<IOrderItemsRequest:

using MassTransit.CourierDemo.OrderService.Repositories;
using MassTransit.CourierDemo.Shared.Models;namespace MassTransit.CourierDemo.OrderService.Consumers;public class OrderItemsRequestConsumer : IConsumer<IOrderItemsRequest>
{public async Task Consume(ConsumeContext<IOrderItemsRequest> context){var order = await OrderRepository.Get(context.Message.OrderId);await context.RespondAsync<IOrderItemsResponse>(new{order.OrderId, DeduceStockItems = order.OrderItems.Select(item => new DeduceStockItem(item.SkuId, item.Qty)).ToList()});}
}
  1. 在库存服务注册service.AddMassTransit()中注册x.AddRequestClient<IOrderItemsRequest>();

using System.Reflection;
using MassTransit.CourierDemo.Shared.Models;namespace MassTransit.CourierDemo.InventoryService;public static class MassTransitServiceExtensions
{public static IServiceCollection AddMassTransitWithRabbitMq(this IServiceCollection services){return services.AddMassTransit(x =>{//...            x.AddRequestClient<IOrderItemsRequest>();//...});}
}
  1. 在需要的类中注册IRequestClient<OrderItemsRequest>服务即可。

最终扣减库存的Activity实现如下:

public class DeduceStockActivity : IActivity<DeduceOrderStockDto, DeduceStockLog>
{private readonly IRequestClient<IOrderItemsRequest> _orderItemsRequestClient;private readonly ILogger<DeduceStockActivity> _logger;public DeduceStockActivity(IRequestClient<IOrderItemsRequest> orderItemsRequestClient,ILogger<DeduceStockActivity> logger){_orderItemsRequestClient = orderItemsRequestClient;_logger = logger;}// 库存扣减public async Task<ExecutionResult> Execute(ExecuteContext<DeduceOrderStockDto> context){var deduceStockDto = context.Arguments;var orderResponse =await _orderItemsRequestClient.GetResponse<IOrderItemsResponse>(new { deduceStockDto.OrderId });if (!CheckStock(orderResponse.Message.DeduceStockItems))return context.Faulted(new Exception("insufficient stock"));DeduceStocks(orderResponse.Message.DeduceStockItems);var log = new DeduceStockLog(deduceStockDto.OrderId, orderResponse.Message.DeduceStockItems);_logger.LogInformation($"Inventory has been deducted for order [{deduceStockDto.OrderId}]!");return context.CompletedWithVariables(log, new { log.OrderId });}// 库存检查private bool CheckStock(List<DeduceStockItem> deduceItems){foreach (var stockItem in deduceItems){if (InventoryRepository.GetStock(stockItem.SkuId) < stockItem.Qty) return false;}return true;}private void DeduceStocks(List<DeduceStockItem> deduceItems){foreach (var stockItem in deduceItems){InventoryRepository.TryDeduceStock(stockItem.SkuId, stockItem.Qty);}}//库存补偿public Task<CompensationResult> Compensate(CompensateContext<DeduceStockLog> context){foreach (var deduceStockItem in context.Log.DeduceStockItems){InventoryRepository.ReturnStock(deduceStockItem.SkuId, deduceStockItem.Qty);}_logger.LogWarning($"Inventory has been returned for order [{context.Log.OrderId}]!");return Task.FromResult(context.Compensated());}
}

支付服务

对于下单流程的支付用例来说,要么成功要么失败,并不需要像以上两个服务一样定义补偿逻辑,因此仅需要实现IExecuteActivity<in TArguments>接口即可,该接口仅定义了Execute接口方法,具体PayOrderActivity实现如下:

using MassTransit.CourierDemo.Shared;
using MassTransit.CourierDemo.Shared.Models;namespace MassTransit.CourierDemo.PaymentService.Activities;public class PayOrderActivity : IExecuteActivity<PayDto>
{private readonly IBus _bus;private readonly IRequestClient<IOrderAmountRequest> _client;private readonly ILogger<PayOrderActivity> _logger;public PayOrderActivity(IBus bus,IRequestClient<IOrderAmountRequest> client,ILogger<PayOrderActivity> logger){_bus = bus;_client = client;_logger = logger;}public async Task<ExecutionResult> Execute(ExecuteContext<PayDto> context){var response = await _client.GetResponse<IOrderAmountResponse>(new { context.Arguments.OrderId });        // do payment...if (response.Message.Amount % 2 == 0){_logger.LogInformation($"Order [{context.Arguments.OrderId}] paid successfully!");return context.Completed();}_logger.LogWarning($"Order [{context.Arguments.OrderId}] payment failed!");return context.Faulted(new Exception("Order payment failed due to insufficient account balance."));}
}

以上代码中也使用了MassTransit的Reqeust/Response 模式来获取订单要支付的余额,并根据订单金额是否为偶数来模拟支付失败。

运行结果

启动三个项目,并在Swagger中发起订单创建请求,如下图所示:

982ff587ab6c4357a65f0e56b3eb3a88.png

由于订单总额为奇数,因此支付会失败,最终控制台输出如下图所示:

3592827c6b17d513c589f31fdabb2956.png打开RabbitMQ后台,可以看见MassTransit按照约定创建了以下队列用于服务间的消息传递:

c3f587e989e32b5a5b7cea36d2d0c0dd.png

但你肯定好奇本文中使用的路由单具体是怎样实现的?简单,停掉库存服务,再发送一个订单创建请求,然后从队列获取未消费的消息即可解开谜底。以下是抓取的一条消息示例:

{"messageId": "ac5d0000-e330-482a-b7bc-08dada7915ab","requestId": null,"correlationId": "ce8af31b-a65c-4dfa-915c-4ae5174820f9","conversationId": "ac5d0000-e330-482a-28a5-08dada7915ad","initiatorId": null,"sourceAddress": "rabbitmq://localhost/masstransit/THINKPAD_MassTransitCourierDemoOrderService_bus_itqoyy8dgbrniyeobdppw6engn?temporary=true","destinationAddress": "rabbitmq://localhost/masstransit/deduce-stock_execute?bind=true","responseAddress": null,"faultAddress": null,"messageType": ["urn:message:MassTransit.Courier.Contracts:RoutingSlip"],"message": {"trackingNumber": "ce8af31b-a65c-4dfa-915c-4ae5174820f9","createTimestamp": "2022-12-10T06:38:01.5452768Z","itinerary": [{"name": "deduce-stock-activity","address": "queue:deduce-stock_execute","arguments": {}},{"name": "pay-activity","address": "queue:pay-order_execute","arguments": {}}],"activityLogs": [{"executionId": "ac5d0000-e330-482a-7cb2-08dada7915bf","name": "order-activity","timestamp": "2022-12-10T06:38:01.7115314Z","duration": "00:00:00.0183136","host": {"machineName": "THINKPAD","processName": "MassTransit.CourierDemo.OrderService","processId": 23980,"assembly": "MassTransit.CourierDemo.OrderService","assemblyVersion": "1.0.0.0","frameworkVersion": "6.0.9","massTransitVersion": "8.0.7.0","operatingSystemVersion": "Microsoft Windows NT 10.0.19044.0"}}],"compensateLogs": [{"executionId": "ac5d0000-e330-482a-7cb2-08dada7915bf","address": "rabbitmq://localhost/masstransit/create-order_compensate","data": {"orderId": "8c47a1db-cde3-43bb-a809-644f36e7ca99","createdTime": "2022-12-10T14:38:01.7272895+08:00"}}],"variables": {"orderId": "8c47a1db-cde3-43bb-a809-644f36e7ca99"},"activityExceptions": [],"subscriptions": []},"expirationTime": null,"sentTime": "2022-12-10T06:38:01.774618Z","headers": {"MT-Forwarder-Address": "rabbitmq://localhost/masstransit/create-order_execute"}
}

从中可以看到信封中的message.itinerary定义了消息的行程,从而确保消息按照定义的流程进行流转。同时通过message.compensateLogs来指引若失败将如何回滚。

总结

通过以上示例的讲解,相信了解到MassTransit Courier的强大之处。Courier中的RoutingSlip充当着事务编排器的角色,将Saga的决策和执行顺序逻辑封装在消息体内随着消息进行流转,从而确保各服务仅需关注自己的业务逻辑,而无需关心事务的流转,真正实现了关注点分离。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280402.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ccleaner无法更新_CCleaner正在静默更新关闭自动更新的用户

ccleaner无法更新CCleaner is forcing updates on users who specifically opt out of automatic updates. Users will only find out about these unwanted updates when they check the version number. CCleaner强制对专门选择退出自动更新的用户进行更新。 用户只有在检查版…

查找域内所有的Windows Server 2012 R2的服务器,并区分出哪些是物理机,那些是虚拟机...

通过使用Get-Adcomputer和Get-Wmiobject 组合来实现。思路是这样的&#xff0c;先看一台服务器的属性值有什么可用利用的。[12r2-dc]: PS C:\> Get-ADComputer -Identity 12r2-dc -Properties *AccountExpirationDate :accountExpires …

rest_framework12:多登陆方式与自动签发token/配置过期时间

多登陆方式与自动签发token views.py 1.继承Viewset&#xff0c;方法里可以使用自定义login&#xff0c;更直观。需要路由直接配置请方式 2. 序列化是直接对request数据处理&#xff0c;并从对象中获取token 3.context可以储存自定义数据 # 多登陆方式&#xff0c;自动签发…

20165310_获奖感想与Java阶段性学习总结

获奖感想与Java阶段性学习总结 一、Learning By Doing ​ 在此之前&#xff0c;其实我并没有想到能够成为小黄杉的第一批成员之一&#xff0c;喜悦之余&#xff0c;也感受到了许多的压力。小黄杉一方面代表了老师对于我这一阶段学习成果的肯定&#xff0c;但同时也是对我的督促…

chrome浏览器崩溃_不只是您:Chrome浏览器在Windows 10的2018年4月更新中崩溃

chrome浏览器崩溃If your computer is hanging or freezing after installing the Windows 10 April 2018 Update you’re not alone, and Microsoft is aware of the problem. 如果在安装Windows 10 April 2018 Update之后计算机挂起或死机&#xff0c;您并不孤单&#xff0c;…

读名老中医之路笔记(二)

任应秋&#xff1a;我的治学门径和方法 任应秋先生从幼读经&#xff0c;十三经皆能成诵&#xff0c;属于带童子功的医学家&#xff0c;他的医学经验&#xff1a; 一、读经宜读全本&#xff0c;解经宜先识字&#xff0c;读经宜正音读&#xff0c;强调对经典著作的朗读和背诵&…

致敬青春岁月

昨天发生的一件神奇的事情。我们公司工会组织了一次小型的户外团建&#xff0c;有机会认识一些其他部门同事&#xff0c;没想到有一个同事小心地认出了我&#xff0c;然后还谈起了关于.NET技术和社区的一些发展的历史和故事。他在微软工作的时间比我久&#xff0c;但时空交错&a…

谈谈- declare-styleable属性

在Android开发中&#xff0c;往往要用到自定义的控件来实现我们的需求或效果。在使用自定义 控件时&#xff0c;难免要用到自定义属性&#xff0c;那怎么使用自定义属性呢&#xff1f; 一、简单使用&#xff1a; 1.在文件res/values/下新建attrs.xml属性文件&#xff0c;中定义…

docker:自定义ubuntu/制作镜像引用/ubuntu换源更新

一、需求 1. 制作一个图像辨识的api&#xff0c;用到相同设置的ubuntu镜像&#xff0c;但是每次制作都要更新ubuntu和下载tesseract浪费半个到一个小时下载&#xff0c;所以制作一个自定义ubuntu几次镜像大大提高开发效率。 2. 制作ubuntu过程时&#xff0c;可以调试tesserac…

jQuery 属性和CSS

HTML代码&#xff1a; <div id"div1">div1<p>1</p><p>2</p><p>3</p> </div> <div id"div2">div2</div> <div id"div3">div3</div>attr()设置节点的属性 $("#div1…

facebook人脸照片_为什么您的Facebook照片看起来如此糟糕(以及您可以如何做)...

facebook人脸照片Facebook is a popular platform for sharing photos, even though it’s not a very good one. They prioritize fast loading images over high quality ones. You can’t stop it from happening, but you can minimize the quality loss. Facebook是一个受…

用C#自己动手写个操作系统,爽!

自从C#的AOT编译机制发布以来&#xff0c;有趣的项目越来越多&#xff0c;今天给大家推荐一个开源项目&#xff0c;用C#开发的64位操作系统。项目简介这是一个使用.NET Native AOT技术编译的C# 64位操作系统&#xff0c;系统的基础功能基本都已经支持&#xff1a;网卡、多处理、…

JavaFX 学习笔记——窗口与控件

前言 如今比较流行的桌面gui框架有WPF、WinForm、Qt、javafx等。其中WPF和WinForm目前还只能在运行Winsows上。Qt(widget)是一个很强大的跨平台C框架(不只是UI)&#xff0c;但用C写界面实在有点蛋疼&#xff0c;且编译出来的体积很大。 JavaFX是基于JAVA的开源桌面框架&#xf…

Linux 用户名、主机添加背景色

文章参考&#xff1a;PS1应用之——修改linux终端命令行各字体颜色 Linux 用户名、主机添加背景色&#xff0c;用于生产环境&#xff0c;这样可以减少人为的误操作。 1 [rootzhang ~]# tail /etc/bashrc 2 ……………… 3 export PS1"\[\e[37;40m\][\[\e[37;41m\]\u\[\e[3…

python 调用文件上传图片简单例子

使用方法&#xff1a; python.exe .\test.py "fileD:\img\mark_1080.png" "matchWordListRUN" "urlhttp://192.168.0.37:8081/templateMatch" test.py import requests import sysif __name__ "__main__":print(参数个数为:, len(s…

解决网站在负载均衡环境下SESSION丢失的问题

在WEB场中,动态网页往往会因为几台主机做了负载而产生SESSION丢失的问题&#xff0c;网上也有很多的介绍&#xff0c;我这里只将我经历的过程给大家分享一下&#xff1a; 系统要运行在负载平衡的 Web 场环境中&#xff0c;而系统配置文件web.config中的Session状态却设置为InPr…

如何从手机或PC将游戏下载到PlayStation 4

PlayStation 4 games can be huge, and take hours to download. Thankfully, you can start downloading games even when you’re away from home. All you need is Sony’s official smartphone app, or a web browser on any PC. PlayStation 4游戏可能非常庞大&#xff0c…

CML更新 | 新增百度小程序、支付宝小程序

祝所有工程师小伙伴开工大吉&#xff0c;Beatles 团队已经开始忙碌起来了。 几个事情要向诸位汇报一下&#xff1a; 一、新增百度小程序、支付宝小程序 发布alpha版本支持百度小程序、支付宝小程序&#xff0c;已有项目可以无缝直接运行在新增平台&#xff0c;欢迎安装试用&…

C#中4种深拷贝方法介绍

概述为什么要用到深拷贝呢&#xff1f;比如我们建了某个类Person&#xff0c;并且实例化出一个对象&#xff0c;然后&#xff0c;突然需要把这个对象复制一遍&#xff0c;并且复制出来的对象要跟之前的一模一样&#xff0c;来看下我们一般会怎么做。1、利用反射实现public stat…

kaggle入门项目:Titanic存亡预测(三)数据可视化与统计分析

---恢复内容开始--- 原kaggle比赛地址&#xff1a;https://www.kaggle.com/c/titanic 原kernel地址&#xff1a;A Data Science Framework: To Achieve 99% Accuracy Step 4: Perform Exploratory Analysis with Statistics 使用描述性与图表分析数据&#xff0c;重点在于数据可…