当前位置: 首页 > news >正文

ABP - 事件总线之分布式事件总线

ABP - 事件总线之分布式事件总线

  • 1. 分布式事件总线的集成
    • 1.2 基于 RabbitMQ 的分布式事件总线
  • 2. 分布式事件总线的使用
    • 2.1 发布
    • 2.2 订阅
    • 2.3 事务和异常处理
  • 3. 自己扩展的分布式事件总线实现

事件总线可以实现代码逻辑的解耦,使代码模块之间功能职责更清晰。而分布式事件总线的功能不止这些,它允许我们通过消息队列进行中转,发布和订阅跨应用/服务边界传输的事件,经常被用作微服务或不同应用程序之间异步发送和接收消息的手段,属于分布式应用通讯的方式之一。

分布式事件总线依赖于 消息队列 中间件,ABP 框架中提供了4种开箱即用的提供程序,我们也可以基于抽象的接口自行实现分布式事件总线提供程序,已有的四种分别适用于 RabbitMQ、Kafka、Rebus 等消息队列,还有一种是默认实现:进程内的分布式事件总线,它允许我们在没有接入消息队列时,也能够编写与分布式体系结构兼容的代码,方便日后可能的微服务拆分,这时它的工作方式与本地事件总线一样,整体的设计思想就和微软的分布式缓存一样。

1. 分布式事件总线的集成

以下的演示还是基于控制台程序,分布式事件总线不会默认集成在 ABP 启动模板之中,需要我们自行集成,Web 应用的集成方式也是一样的。

通过以下命令创建一个控制台程序启动模板:

abp new AbpDistributeEventBusSample  -t console

之后再打开解决方案,由于分布式事件总线是在不同应用程序之间进行通讯的,所以还需要再创建一个控制台项目进行演示,将解决方案中的项目复制一份即可。

在这里插入图片描述
本地分布式事件总线

首先讲一下进程内的事件总线的集成,这个还是有些必要的,如果有考虑后续进行微服务拆分的情况下,前期对于事件总线的使用可以基于这个进行开发。当然并不是说本地事件总线就不推荐使用,从我自己的日常工作经验中,很多时候还是分布式事件总线和本地事件总线搭配使用的。

首先,分布式事件总线的核心依赖包为 Volo.Abp.EventBus,和本地事件总线一样。我们在需要集成的项目的根目录下,通过以下命令进行集成:

abp add-package Volo.Abp.EventBus

由于是本地分布式事件总线,所以这种方式下是没办法跨进程通讯的,使用方式和上一篇讲的本地事件总线类似,不过使用的时候不再通过 ILocalEventBus接口,而是通过 IDistributedEventBus 接口,主要是用于业务逻辑的解耦,同时也为后续可能的分布式拆分做好准备。具体的使用方式下面细讲。

1.2 基于 RabbitMQ 的分布式事件总线

ABP 框架提供了三种开箱即用的分布式事件总线提供程序,分别对应 RabbitMQ、Kafka、Rebus,通过结合第三方消息队列实现真正基于消息跨进程通讯的分布式事件总线,这里主要讲一下基于 RabbitMQ 的方式,其他方式用法类似。

首先,基于 RabbitMQ 的分布式事件总线需要安装 Volo.Abp.EventBus.RabbitMQ 驱动程序包。可通过一下方式安装:

abp add-package Volo.Abp.EventBus.RabbitMQ

上面创建的两个工程都要安装,因为我们要演示两个进程间的通讯。

在这里插入图片描述
之后,需要部署 RabbitMQ 消息队列,这里我通过 docker 快速启动一个带有管理平台的 RabbitMQ,命令如下:

docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq rabbitmq:management

RabbitMQ 默认用户密码为:guest / guest,这里只是用于测试就直接使用了。生产环境中大家最好将默认用户禁用,另行创建自己的用户。RabbitMQ 相关的更详细的使用和配置这里就不细讲了,详细内容可见 [[2.1 RabbitMQ基本概念]] 系列文章。

在这里插入图片描述
然后,添加分布式事件总线相应的配置,我们可以在 appsettings.json 文件中添加以下配置节点:

"RabbitMQ": {"Connections": {// 这里的配置支持 RabbitMQ 官方 sdk 中的 ConnectionFactory 的任意属性的配置"Default": {"HostName": "localhost", // rabbitmq 地址,集群环境下多个ip用逗号分隔"Port": "5672", // rabbbitmq 端口,默认5672"UserName": "guest","Password": "guest"}// 允许配置多个 rabbitmq 连接,但只能有一个用于事件总线//"Second": {//  "HostName": "xxx.xxx.xxx.xxx", // rabbitmq 地址,集群环境下多个ip用逗号分隔//  "Port": "5672" // rabbbitmq 端口,默认5672//}},"EventBus": {"ClientName": "MyClientName", // 用于事件总线的队列名"ExchangeName": "MyExchangeName", // 用于事件总线的交换机名称// "ConnectionName": "Default" // 配置多个连接时,指定用于事件总线的 RabbitMQ 连接,默认是 Default}}

以上配置,最后都会被转换为 AbpRabbitMqOptions 和 AbpRabbitMqEventBusOptions,所以我们也可以直接在代码中对这两个选项进行配置:

Configure<AbpRabbitMqOptions>(options =>
{options.Connections.Default.UserName = "guest";options.Connections.Default.Password = "guest";options.Connections.Default.HostName = "localhost";options.Connections.Default.Port = 5672;
});Configure<AbpRabbitMqEventBusOptions>(options =>
{options.ClientName = "TestApp1";options.ExchangeName = "TestMessages";
});

两种方式选择一种即可,如果两种方式同时使用,代码配置优先于配置文件。解决方案的两个项目都需要进行配置,进行通讯的两个项目需要连接到同一个队列。

完成上面的配置之后,启动应用,即可看到 RabbitMQ 中创建了我们配置的交换机和队列:

在这里插入图片描述

2. 分布式事件总线的使用

2.1 发布

事件发布需要一个事件对象,官方将之称为 Eto(事件传输对象),这是一个普通类,用于保存和事件相关的数据,一般以 Eto 作为后缀。就算一个事件不需要传输任何数据,也必须创建一个空类,这和上一章的本地事件总线是一样的,由于在分布式事件触发之后,事件对象会被序列化传输到消息队列中,所以事件对象应避免循环引用、多态、私有setter,并提供默认(空)构造函数,如果你有其他的构造函数(虽然某些序列化器可能会正常工作)。 下面是一个用于测试的 Eto 对象的定义。

[EventName("helloEvent")]
public class HelloEto
{public string Who { get; set; }public DateTime When { get; set; }public string ToWho { get; set; }
}

默认情况下,事件名将事件名称将是事件类的全名,我们可以通过 EventNameattribute 特性指定事件名称。

分布式事件的发布通过 IDistributedEventBus 接口,只需将其注入到相应的类中使用即可,使用方式和本地事件总线一样。

public class HelloWorldService : ITransientDependency
{private readonly IDistributedEventBus _distributedEventBus;public HelloWorldService(IDistributedEventBus distributedEventBus){_distributedEventBus = distributedEventBus;}public async Task SayHelloAsync(){await _distributedEventBus.PublishAsync(new HelloEto{Who = "Jerry",When = DateTime.Now,ToWho = "Jack"});}
}

以上代码写在 AbpDistributeSample 项目中,这里是事件发布的进程。

2.2 订阅

事件的订阅也和本地事件总线类似,这里通过实现了 IDistributedEventHandler<TEvent> 接口的处理器来处理事件,当前像上一章本地事件总线中讲到的,我们也可以通过 IDistributedEventBus 来自己订阅事件。

public class HelloDistribuedEventHandler : IDistributedEventHandler<HelloEto>, ITransientDependency
{public Task HandleEventAsync(HelloEto eventData){Console.WriteLine($"{eventData.Who} Say Hello To {eventData.ToWho} at { eventData.When.ToString("yyyy-MM-dd HH:mm:ss") }");return Task.CompletedTask;}
}

一个事件处理程序可以同时处理多种事件,只需要实现多个针对不同 ETO 的 IDistributedEventHandler 泛型接口即可。

之后通过 vs 设置多项目启动:

在这里插入图片描述
应用启动之后,可以看到控制台的输出如下,一个简单的基于消息队列的跨进程通讯已经完成:

在这里插入图片描述
同时在 RabbitMQ 上可以看到已经连接上来了2个消费者,每个进程既作为生产者,也作为消费者:

在这里插入图片描述
通过源码可以看到,分布式事件总线中会进行初始化,其实就是根据配置连接了 RabbitMQ 队列,并创建了一个消费者自动订阅消息队列上的事件,当接受到消息之后会从消息中获取消息的类型和具体的数据,触发消息执行处理程序,如果事件处理程序成功执行(没有抛出任何异常),它将向消息代理发送确认(ACK)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里的 EventTypes 其实就是维护消息类型和它的类名的对应关系的一个字典,在 SubscribeHandlers 中初始化消息类型和执行器对应关系的时候维护的,这个就和前一篇的本地事件总线大同小异了。

而消息的发布过程就更简单了,也就是将消息序列化,发送到 RabbitMQ 队列中。

在这里插入图片描述
在这里插入图片描述
ABP 分布式事件总线从 5.0 版本开始,还加入了 Inbox 、Outbox 机制,我们可以通过 AbpDistributedEventBusOptions 选项进行配置,例如:

Configure<AbpDistributedEventBusOptions>(option =>
{option.Inboxes.Configure(config => config.UseDbContext<TDbContext>());option.Outboxes.Configure(config => config.UseDbContext<TDbContext>());
});

从配置方式也可以看出,这里是借助了数据库的,实际上 Inbox 就是从消息队列接收到消息之后,将消息先存入数据库中,没有直接执行。

在这里插入图片描述
在这里插入图片描述
而在应用启动的时候,消息队列模块会注册一个 InboxProcessManager,实际上这就是一个后台工作者,这里面又用到 IInboxProcessor

在这里插入图片描述
在这里插入图片描述
在 InboxProcessor 中再通过定时器,每个一段时间从数据库中读取消息真正地去执行,并且将数据库中的记录删除。

在这里插入图片描述
在这里插入图片描述
OutBox 的机制也是一样的,这样做大概就是起到缓冲的作用,避免短时间内大量的消息对消息队列或者我们的消费者造成冲击导致应用崩溃,而是将这些消息的发送、执行均匀地处理。

2.3 事务和异常处理

分布式事件总线默认实现是 LocalDistributedEventBus,在没有集成 RabbitMQ 等第三方消息队列中间件,并且没有使用发件箱/收件箱模式的情况,事件发布和事件订阅是在同一个进程的。如果当前事件发布的模块使用了工作单元,事件总线是在和发布事件的同一工作单元范围内执行事件处理程序,如果事件处理程序抛出异常,那么相关的工作单元(数据库事务)将被回滚。这样,我们的应用程序逻辑和事件处理逻辑就具有事务性(原子)。如果想忽略事件处理程序中的错误,则必须在处理程序中使用try-catch块,并且不应该重新抛出异常。

当我们切换到真正的分布式事件总线提供程序时,事件处理程序将在不同的进程/应用程序中执行。在这种情况下,实现事务性事件发布的唯一方法是使用发件箱/收件箱模式。这篇文章上面的内容简单地提了一下发件箱/收件箱,之后还会有文章详细梳理它的工作模式。

如果在未真正使用分布式事件总线的情况下,想在工作单元中立即发布事件,而不是等到工作单元中的逻辑执行完成之后再发布,可以在使用IDistributedEventBus.PublishAsync方法时将onUnitOfWorkComplete设置为false。如果接入 RabbitMQ 等第三方消息队列实现了分布式事件总线,想要立即发布事件,则还得将 useOutbox 设置为 false。

3. 自己扩展的分布式事件总线实现

要注意的一点是,ABP 框架的分布式事件总线只能配置使用一个队列,这就意味着如果一个进程需要和多个进程进行通讯时,是无法通过不同的队列进行区分的。当多个进程都连接到同一个队列中时,我们无法控制一个进程发送的消息会被哪个进程所消费,无法单独通知某一个消费者,这是需要注意的。

有一种折中的方式,那就是如果一个事件只想发送给某一个进程,那就只在这个进程中实现其处理程序,其他没有实现处理程序的进程接收到消息之后,因为没有处理会抛出异常,消息重新返回消息队列中。这种方式依旧是存在一些问题,只是将工作中的一点经验供大家参考一下。

public interface IWantDistributedEventBus
{Task PublishAsync(Type eventType, object eventData, string queueName);
}public class RabbitMqWantDistributedEventBus : IWantDistributedEventBus, ISingletonDependency
{protected ILocalEventBus LocalEventBus { get; }protected IConnectionPool ConnectionPool { get; }protected WantRabbitMqEventBusOptions WantRabbitMqEventBusOptions { get; }protected AbpLocalEventBusOptions AbpLocalEventBusOptions { get; }protected IRabbitMqSerializer Serializer { get; }protected IRabbitMqMessageConsumer Consumer { get; private set; }protected IRabbitMqMessageConsumerFactory MessageConsumerFactory { get; }protected ConcurrentDictionary<string, Type> EventTypes { get; }public RabbitMqWantDistributedEventBus(IConnectionPool connectionPool,IOptions<WantRabbitMqEventBusOptions> options,IOptions<AbpLocalEventBusOptions> localEventBusOptions,IRabbitMqSerializer serializer,IRabbitMqMessageConsumerFactory rabbitMqMessageConsumerFactory,ILocalEventBus localEventBus){ConnectionPool = connectionPool;WantRabbitMqEventBusOptions = options.Value;Serializer = serializer;MessageConsumerFactory = rabbitMqMessageConsumerFactory;LocalEventBus = localEventBus;AbpLocalEventBusOptions = localEventBusOptions.Value;EventTypes = new ConcurrentDictionary<string, Type>();}/// <summary>/// 队列消费者初始化/// </summary>public void Initialize(){foreach (var queueName in WantRabbitMqEventBusOptions.ConsumeQueueNames){Consumer = MessageConsumerFactory.Create(new ExchangeDeclareConfiguration(WantRabbitMqEventBusOptions.ExchangeName,type: "direct",durable: true),new QueueDeclareConfiguration(queueName,durable: true,exclusive: false,autoDelete: false),WantRabbitMqEventBusOptions.ConnectionName);Consumer.BindAsync(queueName);Consumer.OnMessageReceived(ProcessEventAsync);}LoadEventTypes(AbpLocalEventBusOptions.Handlers);}/// <summary>/// 消息消费逻辑/// </summary>/// <param name="message">从队列接收到的消息</param>/// <returns></returns>private async Task ProcessEventAsync(IModel channel, BasicDeliverEventArgs ea){var msgData = (MessageData)Serializer.Deserialize(ea.Body.ToArray(), typeof(MessageData));var eventName = msgData.Type;var eventType = EventTypes.GetOrDefault(eventName);// eventType为空, 即不存在类型对应的Handler,消息不应该被消费if (eventType == null){// return;throw new NotFoundHandlerException($"不存在{eventName}消息Handler!");}// 通过消息类型转发LocalHandler进行具体逻辑处理var eventData = Serializer.Deserialize(Serializer.Serialize(msgData.Data), eventType);await LocalEventBus.PublishAsync(eventType, eventData);}/// <summary>/// 根据现有注册的Handler构建消息类型集合/// </summary>/// <param name="handlers">当前应用中Handler类型集合</param>/// <returns></returns>public Task LoadEventTypes(ITypeList<IEventHandler> handlers){foreach (var handler in handlers){var interfaces = handler.GetInterfaces();foreach (var @interface in interfaces){if (!typeof(IEventHandler).GetTypeInfo().IsAssignableFrom(@interface)){continue;}var genericArgs = @interface.GetGenericArguments();if (genericArgs.Length == 1){var eventTypeName = genericArgs[0].FullName;if (!EventTypes.ContainsKey(eventTypeName)){EventTypes[eventTypeName] = genericArgs[0];}}}}return Task.CompletedTask;}/// <summary>/// 发布消息/// </summary>/// <param name="eventType">消息类型</param>/// <param name="eventData">消息内容</param>/// <param name="queueName">队列名称</param>/// <returns></returns>public Task PublishAsync(Type eventType, object eventData, string queueName){var msg = new MessageData { Type = eventType.FullName, Data = eventData };var body = Serializer.Serialize(msg);using (var channel = ConnectionPool.Get(WantRabbitMqEventBusOptions.ConnectionName).CreateModel()){channel.ExchangeDeclare(WantRabbitMqEventBusOptions.ExchangeName,"direct",durable: true);var properties = channel.CreateBasicProperties();properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent;var queue = channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false);channel.QueueBind(queueName, WantRabbitMqEventBusOptions.ExchangeName, queueName);channel.BasicPublish(exchange: WantRabbitMqEventBusOptions.ExchangeName,routingKey: queueName,mandatory: true,basicProperties: properties,body: body);}return Task.CompletedTask;}
}public class WantRabbitMqEventBusOptions
{public string ConnectionName { get; set; }public string ClientName { get; set; }public string ExchangeName { get; set; }public IList<string> ConsumeQueueNames { get; }public WantRabbitMqEventBusOptions(){ConsumeQueueNames = new List<string>();}
}public class MessageData
{public string Type { get; set; }public object Data { get; set; }
}[DependsOn(typeof(AbpRabbitMqModule))]
[DependsOn(typeof(AbpEventBusModule))]
public class WantAbpEventBusRabbitMqModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){var configuration = context.Services.GetConfiguration();Configure<WantRabbitMqEventBusOptions(configuration.GetSection("RabbitMQ:EventBus"));}public override void OnApplicationInitialization(ApplicationInitializationContext context){context.ServiceProvider.GetRequiredService<RabbitMqWantDistributedEventBus>().Initialize();}
}

以上就是 ABP 框架下分布式事件总线的基本知识点,其中也提到了 发件箱/收件箱 等新特性,ABP 框架经过长时间的发展,基本与 .NET 版本同步更新,到现在 9.0 版本,各种组件的特性也在逐步迭代更新。分布式事件总线除了这篇文章中提到的基本内容之外,还有一些新特性,后续会有一篇文章专门讲一下这些特性。

参考文档:
ABP 官方文档 - 分布式事件总线

相关文章:

ABP - 事件总线之分布式事件总线

ABP - 事件总线之分布式事件总线 1. 分布式事件总线的集成1.2 基于 RabbitMQ 的分布式事件总线 2. 分布式事件总线的使用2.1 发布2.2 订阅2.3 事务和异常处理 3. 自己扩展的分布式事件总线实现 事件总线可以实现代码逻辑的解耦&#xff0c;使代码模块之间功能职责更清晰。而分布…...

osgearth控件显示中文(八)

当前自己知道的方法大概有以下两种: (一)直接转成utf8 其实在前面的文章中已经有了。 osgEarth::Annotation::PlaceNode *pn = new osgEarth::Annotation::PlaceNode(GeoPoint(geoSRS, 110, 34), String2UTF8("中国"), style);std::wstring String2Wstring(con…...

基于opencv的 24色卡IQA评测算法源码-可完全替代Imatest

1.概要 利用24色卡可以很快的分析到曝光误差&#xff0c;白平衡误差&#xff0c;噪声&#xff0c;色差&#xff0c;饱和度&#xff0c;gamma值。IQA或tuning工程一般用Imatest来手动计算&#xff0c;不便于产测部署&#xff0c;现利用opencv实现了imatest的全部功能&#xff0c…...

webpack打包优化策略

1. 减少打包体积 减少打包文件的大小是为了提高加载速度&#xff0c;降低网络带宽消耗&#xff0c;提升用户体验。常见的减少打包体积的优化策略包括&#xff1a; 代码分割&#xff08;Code Splitting&#xff09;&#xff1a;将代码拆分成多个小文件&#xff0c;让浏览器按需…...

Kafka日志数据深度解析:从基础查看到高级操作全攻略

#作者&#xff1a;孙德新 文章目录 查看log日志文件(kafka-dump-log.sh)1、查看Log文件基本数据信息2、index文件健康性检查(--index-sanity-check)3、转储文件(--max-message-size)4、偏移量解码(--offsets-decoder)5、日志数据解析(--transaction-log-decoder)6、查询Log文件…...

DeepSeek-R1使用生存指南

文章目录 1.为什么普通人一定要使用DeepSeek2.DeepSeek的几种使用方式2.1网页端直接使用2.2手机端app使用2.3其他第三方平台 3.网页端按钮的说明4.正确的提问技巧4.1不要定义过程4.2明确受众4.3记忆时间有限4.4输出长度限制4.5如何清除上下文的记忆 5.几个避坑点5.1冗长提示词污…...

Code::Blocks 创建 C 项目 二

Code::Blocks 创建 C 项目 二 Code::Blocks 安装请看 Code::Blocks 安装 启动 Code Blocks 选择 Create a new project 弹出界面选择 Projects -> Console application -> Go 选择 C &#xff1a;表示创建的是 C 语言项目 点击 Next Project title&#xff1a;项目名 …...

pyqt写一个待办程序

ToDoApp 框架选择 一个简单的GUI程序&#xff0c;可以使用pyqt完成。pyqt是qt的python实现版本。 界面搭建 设计一个美观 简洁的界面 class ToDoApp(QWidget):def __init__(self):super().__init__()# 设置窗口属性self.setWindowTitle("Daily To Do List")self…...

总结前端常用数据结构 之 数组篇【JavaScript -包含常用数组方法】

【亲爱的读者&#xff0c;爱博主记得一键三连噢噢ooo~~ 啾咪】 创建数组&#xff1a; 以字面量的形式创建新数组&#xff1a;let arr1 [1, 2, 3];通过 Array 构造函数并传入一组元素&#xff08;4,5,6&#xff09;来创建一个新数组&#xff1a;let arr2 new Array(4, 5, 6);…...

利率掉期(Interest Rate Swap):运作原理、收益模式及市场角色解析(中英双语)

利率掉期&#xff08;Interest Rate Swap&#xff09;&#xff1a;运作原理、收益模式及市场角色解析 引言 利率掉期&#xff08;Interest Rate Swap, IRS&#xff09; 是金融市场中最常见的衍生品之一&#xff0c;它允许两方交换固定利率和浮动利率&#xff0c;以优化融资成…...

Mac 开发工具推荐

Homebrew 软件安装管理必备神器&#xff0c;相当于 Linux 上的 yum&#xff0c;安装了homebrew之后&#xff0c;以下软件都可以通过brew cask install 和 brew install进行直接安装 IntelliJ IDEA Java开发ide 相关插件&#xff1a; 1&#xff09;lombok 2&#xff09;Aliba…...

NCHAR_CS和CHAR_CS,导致UNION ALL 时,提示SQL 错误 [12704] [72000]: ORA-12704: 字符集不匹配

检查涉及的数据表和列的字符集设置 -- 查询表的字符集 SELECT parameter, value FROM nls_database_parameters WHERE parameter LIKE NLS_CHARACTERSET;-- 查询列的字符集&#xff08;对于特定表&#xff09; SELECT column_name, character_set_name FROM all_tab_columns W…...

使用 Python paramiko 自动备份设备配置实验

一、实验拓扑&#xff1a; 要求&#xff1a;交换机 SW1 做为 SSH 服务端&#xff0c;桥接本地虚拟虚拟网卡&#xff1b;本地主机通过 python paramiko 库功能登录到 SW1 上进行配置备份&#xff1b;AR1 做为测试 SW1 的 SSH 客户端 二、实验环境搭建&#xff1a; 1、SW1 配置…...

goland2022.3.3 安装过程

到csdn下载安装包 开始安装 安装完后&#xff0c;安装中文包...

工业级推荐系统冷启动解决方案:基于元迁移学习与动态知识图谱的混合架构设计与实践

技术原理与数学模型 1. 元学习冷启动适配器&#xff08;MAML改进&#xff09; 数学原理&#xff1a; \min_\theta \sum_{\mathcal{T}_i\sim p(\mathcal{T})} \mathcal{L}_{\mathcal{T}_i}(U_i(\theta - \alpha\nabla_\theta\mathcal{L}_{\mathcal{T}_i}^{sup}(\theta))))其中…...

小小小病毒(3)(~_~|)

一分耕耘一分收获 声明&#xff1a; 仅供损害电脑&#xff0c;不得用于非法。损坏电脑&#xff0c;作者一律不负责。此作为作者原创&#xff0c;转载请经过同意。 欢迎来到小小小病毒&#xff08;3&#xff09; 感谢大家的支持 还是那句话&#xff1a;上代码&#xff01; …...

在 WSL上的 Ubuntu 中通过 Docker 来运行 Redis,并在微服务项目中使用redis

通过在 WSL&#xff08;Windows Subsystem for Linux&#xff09;上的 Ubuntu 虚拟机中通过 Docker 来运行 Redis&#xff0c;然后再微服务项目中使用redis 以下是步骤&#xff1a; 1. 安装 Docker&#xff08;如果还未安装&#xff09; 首先&#xff0c;确保你已经在 WSL 的…...

深入解析SVG图片原理:从基础到高级应用

文章目录 引言一、SVG基础概念1.1 什么是SVG&#xff1f;1.2 SVG的优势 二、SVG的基本结构2.1 SVG文档结构2.2 常用SVG元素 三、SVG的工作原理3.1 坐标系与变换3.2 路径与曲线3.3 渐变与滤镜 四、SVG的高级应用4.1 动画与交互4.2 数据可视化4.3 响应式设计 五、SVG的优化与性能…...

Python 中的一种调试工具 assert

assert 是 Python 中的一种调试工具&#xff0c;用于在代码中设置断言&#xff08;assertion&#xff09;。断言是一种声明&#xff0c;用于确保某个条件为真。如果条件为假&#xff0c;assert 会触发一个 AssertionError 异常&#xff0c;并可选地输出错误信息。 语法 asser…...

面基Spring Boot项目中实用注解一

在Spring Boot项目中&#xff0c;实用注解根据功能可以分为多个类别。以下是常见的注解分类、示例说明及对比分析&#xff1a; 1. 核心配置注解 SpringBootApplication 作用&#xff1a;标记主启动类&#xff0c;组合了Configuration、EnableAutoConfiguration和ComponentScan…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中&#xff0c;结构体可以嵌套使用&#xff0c;形成更复杂的数据结构。例如&#xff0c;可以通过嵌套结构体描述多层级数据关系&#xff1a; struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

SkyWalking 10.2.0 SWCK 配置过程

SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外&#xff0c;K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案&#xff0c;全安装在K8S群集中。 具体可参…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

LeetCode - 394. 字符串解码

题目 394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; 思路 使用两个栈&#xff1a;一个存储重复次数&#xff0c;一个存储字符串 遍历输入字符串&#xff1a; 数字处理&#xff1a;遇到数字时&#xff0c;累积计算重复次数左括号处理&#xff1a;保存当前状态&a…...

HTML 列表、表格、表单

1 列表标签 作用&#xff1a;布局内容排列整齐的区域 列表分类&#xff1a;无序列表、有序列表、定义列表。 例如&#xff1a; 1.1 无序列表 标签&#xff1a;ul 嵌套 li&#xff0c;ul是无序列表&#xff0c;li是列表条目。 注意事项&#xff1a; ul 标签里面只能包裹 li…...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

Nuxt.js 中的路由配置详解

Nuxt.js 通过其内置的路由系统简化了应用的路由配置&#xff0c;使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

C++中string流知识详解和示例

一、概览与类体系 C 提供三种基于内存字符串的流&#xff0c;定义在 <sstream> 中&#xff1a; std::istringstream&#xff1a;输入流&#xff0c;从已有字符串中读取并解析。std::ostringstream&#xff1a;输出流&#xff0c;向内部缓冲区写入内容&#xff0c;最终取…...

Axios请求超时重发机制

Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式&#xff1a; 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...