《ASP.NET Core 微服务实战》-- 读书笔记(第6章)

第 6 章 事件溯源与 CQRS

在本章,我们来了解一下随着云平台一同出现的设计模式

我们先探讨事件溯源和命令查询职责分离(CQRS)背后的动机与哲学

事件溯源简介

事实由事件溯源而来

我们大脑就是一种事件溯源系统,接收感官多种形式刺激,大脑负责对这些刺激进行合适排序,大约每隔几百毫秒,对刺激构成的流进行运算,而运算的结果,就是我们所说的事实

事件溯源的定义

传统应用中,状态由一系列零散的数据所管理,如果客户端向我们发送 PUT 或 POST 请求,状态就会改变

这种方式很好地给出了系统当前状态,却不能指示在当前状态之前,系统是如何变化的

事件溯源可以解决这个问题,因为它把状态管理的职责与接收导致状态变更的刺激的职责区分开来

基于事件溯源的系统需要满足一系列要求

  • 有序:有序事件流

  • 幂等:等价多个有序事件流的操作结果相同

  • 独立:不依赖外部信息

  • 过去式:事件发生在过去

流行的区块链技术的基础就是发生在特定私有资源上的安全、可信的事件序列

拥抱最终一致性

一种我们每天都在用的最终一致性的应用,就是社区网络应用

有时你从一个设备发出的评论要花几分钟才能展示在朋友的浏览器或者其他设备上

这是因为,应用的架构人员做了妥协:通过放弃同步操作的即时一致性,在可接受的范围内增加一定的反馈延迟,就能让应用支持巨大的规模与流量

CQRS 模式

如果把我们讨论的模式直接套用到系统中,很快会发现系统必须对输入命令和查询加以区分,这也被称为命令查询职责分离(CQRS)

我们用一个例子来说明这种模式的实际应用

租户通过一个门户网站查看用电情况,每当用户刷新门户页面时,就调用某种数据服务并请求,汇总一段时间内所有度量事件

但这种对于云规模的现代软件开发来说是不可接受的,如果将计算职责推卸给数据库,很快会造成数据库瓶颈

掌握了大多数客户的使用模式,让我们能够利用事件溯源来构建一个合理的 CQRS 实现。

事件处理器每次收到新事件时重新计算已缓存的度量总和

利用这种机制,在查询时,门户上的用户所期望的结果已经存在于数据库或者缓存中

不需要复制的计算,也没有临时的聚合与繁杂的汇总,只需要一个简单的查询

事件溯源于 CQRS 实战--附件的团队成员

接下来要开发的新版实例中,我们将检测成员彼此相距一个较小距离的时刻

系统将支持对这些接近的结果予以响应

例如我们可能希望向附近的团队成员的移动设备发送推送通知,以提醒他们可以约见对方

为了实现这一功能,我们把系统职责划分为以下四个组件:

  • 位置报送服务(命令)

  • 事件处理器(对事件进行溯源)

  • 事实服务(查询)

  • 位置接近监控器(对事件进行溯源)

位置报送服务

收到新报送的位置后,执行下列操作:

  • 验证上报数据

  • 将命令转换为事件

  • 生成事件,并用消息队列发送出去

GitHub 链接:https://github.com/microservices-aspnetcore/es-locationreporter

创建位置报送控制器

using System;
using Microsoft.AspNetCore.Mvc;
using StatlerWaldorfCorp.LocationReporter.Events;
using StatlerWaldorfCorp.LocationReporter.Models;
using StatlerWaldorfCorp.LocationReporter.Services;namespace StatlerWaldorfCorp.LocationReporter.Controllers
{[Route("/api/members/{memberId}/locationreports")]public class LocationReportsController : Controller{private ICommandEventConverter converter;private IEventEmitter eventEmitter;private ITeamServiceClient teamServiceClient;public LocationReportsController(ICommandEventConverter converter,IEventEmitter eventEmitter,ITeamServiceClient teamServiceClient) {this.converter = converter;this.eventEmitter = eventEmitter;this.teamServiceClient = teamServiceClient;}[HttpPost]public ActionResult PostLocationReport(Guid memberId, [FromBody]LocationReport locationReport){MemberLocationRecordedEvent locationRecordedEvent = converter.CommandToEvent(locationReport);locationRecordedEvent.TeamID = teamServiceClient.GetTeamForMember(locationReport.MemberID);eventEmitter.EmitLocationRecordedEvent(locationRecordedEvent);return this.Created($"/api/members/{memberId}/locationreports/{locationReport.ReportID}", locationReport);}}
}

创建 AMQP 事件生成器

using System;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using StatlerWaldorfCorp.LocationReporter.Models;namespace StatlerWaldorfCorp.LocationReporter.Events
{public class AMQPEventEmitter : IEventEmitter{private readonly ILogger logger;private AMQPOptions rabbitOptions;private ConnectionFactory connectionFactory;public AMQPEventEmitter(ILogger<AMQPEventEmitter> logger,IOptions<AMQPOptions> amqpOptions){this.logger = logger;this.rabbitOptions = amqpOptions.Value;connectionFactory = new ConnectionFactory();connectionFactory.UserName = rabbitOptions.Username;connectionFactory.Password = rabbitOptions.Password;connectionFactory.VirtualHost = rabbitOptions.VirtualHost;connectionFactory.HostName = rabbitOptions.HostName;connectionFactory.Uri = rabbitOptions.Uri;logger.LogInformation("AMQP Event Emitter configured with URI {0}", rabbitOptions.Uri);}public const string QUEUE_LOCATIONRECORDED = "memberlocationrecorded";public void EmitLocationRecordedEvent(MemberLocationRecordedEvent locationRecordedEvent){using (IConnection conn = connectionFactory.CreateConnection()) {using (IModel channel = conn.CreateModel()) {channel.QueueDeclare(queue: QUEUE_LOCATIONRECORDED,durable: false,exclusive: false,autoDelete: false,arguments: null);string jsonPayload = locationRecordedEvent.toJson();var body = Encoding.UTF8.GetBytes(jsonPayload);channel.BasicPublish(exchange: "",routingKey: QUEUE_LOCATIONRECORDED,basicProperties: null,body: body);}}}}
}

配置并启动服务

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using Microsoft.Extensions.Logging;
using System.Linq;
using StatlerWaldorfCorp.LocationReporter.Models;
using StatlerWaldorfCorp.LocationReporter.Events;
using StatlerWaldorfCorp.LocationReporter.Services;namespace StatlerWaldorfCorp.LocationReporter
{public class Startup{public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory){loggerFactory.AddConsole();loggerFactory.AddDebug();var builder = new ConfigurationBuilder().SetBasePath(env.ContentRootPath).AddJsonFile("appsettings.json", optional: false, reloadOnChange: false).AddEnvironmentVariables();Configuration = builder.Build();}public IConfigurationRoot Configuration { get; }public void ConfigureServices(IServiceCollection services){services.AddMvc();services.AddOptions();services.Configure<AMQPOptions>(Configuration.GetSection("amqp"));services.Configure<TeamServiceOptions>(Configuration.GetSection("teamservice"));services.AddSingleton(typeof(IEventEmitter), typeof(AMQPEventEmitter));services.AddSingleton(typeof(ICommandEventConverter), typeof(CommandEventConverter));services.AddSingleton(typeof(ITeamServiceClient), typeof(HttpTeamServiceClient));}public void Configure(IApplicationBuilder app,IHostingEnvironment env,ILoggerFactory loggerFactory,ITeamServiceClient teamServiceClient,IEventEmitter eventEmitter){// Asked for instances of singletons during Startup// to force initialization early.app.UseMvc();}}
}

对 Configure 的两次调用让配置子系统把分别从 amqp 和 teamservice 节加载的配置选项以依赖注入的方式提供出来

这些配置可以由 appsettings.json 文件提供,也可以用环境变量覆盖

{"amqp": {"username": "guest","password": "guest","hostname": "localhost","uri": "amqp://localhost:5672/","virtualhost": "/"},"teamservice": {"url": "http://localhost:5001"}
}

消费团队服务

using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using StatlerWaldorfCorp.LocationReporter.Models;namespace StatlerWaldorfCorp.LocationReporter.Services
{public class HttpTeamServiceClient : ITeamServiceClient{private readonly ILogger logger;private HttpClient httpClient;public HttpTeamServiceClient(IOptions<TeamServiceOptions> serviceOptions,ILogger<HttpTeamServiceClient> logger){this.logger = logger;var url = serviceOptions.Value.Url;logger.LogInformation("Team Service HTTP client using URL {0}", url);httpClient = new HttpClient();httpClient.BaseAddress = new Uri(url);}public Guid GetTeamForMember(Guid memberId){httpClient.DefaultRequestHeaders.Accept.Clear();httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));HttpResponseMessage response = httpClient.GetAsync(String.Format("/members/{0}/team", memberId)).Result;TeamIDResponse teamIdResponse;if (response.IsSuccessStatusCode) {string json = response.Content.ReadAsStringAsync().Result;teamIdResponse = JsonConvert.DeserializeObject<TeamIDResponse>(json);return teamIdResponse.TeamID;}else {return Guid.Empty;}}}public class TeamIDResponse{public Guid TeamID { get; set; }}
}

这个例子中,我们使用 .Result 属性在等待异步方法响应期间强行阻塞了线程

在生产级质量的代码里,很可能对此进行重构,确保在服务边界之内整个调用链都传递异步结果

运行位置报送服务

RabbitMQ 已经启动运行,默认的配置也指向了本地的 RabbitMQ 实例

此时可以使用以下方式启动位置报送服务

(确保位于 src/StatlerWaldorfCorp.LocationReporter 子目录中)

$ dotnet restore
$ dotnet build
$ dotnet run --server.urls=http://0.0.0.0:9090

服务运行后,只要向服务提交请求,就可以体验其功能了

$ curl -X POST -d \
'{"reportID":"...", \
"origin":"...", "latitude":10, "longtitude":20, \
"memberID":"..."}' \
http://...le2 \
/locationreports

提交完成后,应该能从服务获得一个 HTTP 201 响应

事件处理器

它的职责是消费来自流的事件,并执行合适的操作

为确保代码整洁、可测试,我们把事件处理的职责划分为如下部分:

  • 订阅队列并从事件流中获取新的消息

  • 将消息写入事件存储

  • 处理事件流(检测附近的队友)

  • 作为流的处理结果,生成新的消息并发送到队列

  • 作为流的处理结果,向事实服务的服务器 / 缓存提交状态变更情况

GitHub 链接:https://github.com/microservices-aspnetcore/es-eventprocessor

检测附近队友的基于 GPS 工具类的检测器

using System.Collections.Generic;
using StatlerWaldorfCorp.EventProcessor.Location;
using System.Linq;
using System;namespace StatlerWaldorfCorp.EventProcessor.Events
{public class ProximityDetector{/** This method assumes that the memberLocations collection only* applies to members applicable for proximity detection. In other words,* non-team-mates must be filtered out before using this method.* distance threshold is in Kilometers.*/public ICollection<ProximityDetectedEvent> DetectProximityEvents(MemberLocationRecordedEvent memberLocationEvent,ICollection<MemberLocation> memberLocations,double distanceThreshold){GpsUtility gpsUtility = new GpsUtility();GpsCoordinate sourceCoordinate = new GpsCoordinate() {Latitude = memberLocationEvent.Latitude,Longitude = memberLocationEvent.Longitude};return memberLocations.Where(ml => ml.MemberID != memberLocationEvent.MemberID &&gpsUtility.DistanceBetweenPoints(sourceCoordinate, ml.Location) < distanceThreshold).Select( ml => {return new ProximityDetectedEvent() {SourceMemberID = memberLocationEvent.MemberID,TargetMemberID = ml.MemberID,TeamID = memberLocationEvent.TeamID,DetectionTime = DateTime.UtcNow.Ticks,SourceMemberLocation = sourceCoordinate,TargetMemberLocation = ml.Location,MemberDistance = gpsUtility.DistanceBetweenPoints(sourceCoordinate, ml.Location)};}).ToList();}}
}

接着我们就可以用这个方法的结果来产生对应的额外效果,例如可能需要发出一个 ProximityDetectorEvent 事件,并将事件写入事件存储

作为主体的事件处理器代码

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using StatlerWaldorfCorp.EventProcessor.Location;
using StatlerWaldorfCorp.EventProcessor.Queues;namespace StatlerWaldorfCorp.EventProcessor.Events
{public class MemberLocationEventProcessor : IEventProcessor{private ILogger logger;private IEventSubscriber subscriber;private IEventEmitter eventEmitter;private ProximityDetector proximityDetector;private ILocationCache locationCache;public MemberLocationEventProcessor(ILogger<MemberLocationEventProcessor> logger,IEventSubscriber eventSubscriber,IEventEmitter eventEmitter,ILocationCache locationCache){this.logger = logger;this.subscriber = eventSubscriber;this.eventEmitter = eventEmitter;this.proximityDetector = new ProximityDetector();this.locationCache = locationCache;this.subscriber.MemberLocationRecordedEventReceived += (mlre) => {var memberLocations = locationCache.GetMemberLocations(mlre.TeamID);ICollection<ProximityDetectedEvent> proximityEvents =proximityDetector.DetectProximityEvents(mlre, memberLocations, 30.0f);foreach (var proximityEvent in proximityEvents) {eventEmitter.EmitProximityDetectedEvent(proximityEvent);}locationCache.Put(mlre.TeamID, new MemberLocation { MemberID = mlre.MemberID, Location = new GpsCoordinate {Latitude = mlre.Latitude, Longitude = mlre.Longitude} });};}public void Start(){this.subscriber.Subscribe();}public void Stop(){this.subscriber.Unsubscribe();}}
}

事件处理服务唯一的额外职责是需要将收到的每个事件都写入事件存储

这样做到原因有很多,包括向其他服务提供可供搜索的历史记录

如果缓存崩溃、数据丢失、事件存储也可用于重建事实缓存

请记住,缓存在架构里仅提供便利性,我们不应该在缓存中存储任何无法从其他位置重建的数据

我们要给服务里每一个团队创建一个 Redis 哈希(hash)

在哈希中,把团队成员的位置经序列化得到的 JSON 正文存储为字段(团队成员的 ID 用作键)

这样就能轻松地并发更新多个团队成员地位置而不会覆盖数据,同时也很容易查询给定的任意团队的位置列表,因为团队就是一个个哈希

事实服务

事实服务负责维护每个团队成员的位置,不过这些位置只代表最近从一些应用那里收到的位置

关于事实服务的这类服务,有两条重要的提醒需要记住:

  • 事实服务并不是事件存储

  • 事实服务是不可依赖服务

位置接近监控器

位置接近监控器的代码包括

  • 基本的微服务结构

  • 一个队列消费端,订阅 ProximityDetectedEvent 事件到达的消息

  • 调用一些第三方或云上的服务来发送推动通知

运行示例项目

下面列出运行本章示例的依赖项:

  • RabbitMQ 服务器

  • Redis 服务器

所有依赖项都启动运行后,可从 GitHub 拉取 es-locationreporter 和 es-eventprocessor 两个服务的代码

此外需要一份 teamservice 服务

请确保获取的是 master 分支,因为在测试期间只需要用到内存存储

要启动团队服务,在命令行中转到 src/StatlerWaldorfCorp.TeamService 目录并运行以下命令

$ dotnet run --server.urls=http://0.0.0.:5001

要启动位置报送服务,在命令行中转到 src/StatlerWaldorfCorp.LocationReporter 目录下并运行以下命令

$ dotnet run --server.urls=http://0.0.0:5002

启动事件处理器(从 src/StatlerWaldorfCorp.EventProcessor 目录运行)

$ dotnet run --server.urls=http://0.0.0.:5003

可用下列步骤端到端地检验整个事件溯源/CQRS系统:

  • (1)向 http://localhost:5001/teams 发送一个 POST 请求,创建一个新团队

  • (2)向 http://localhost:5001/teams/

    /members 发送一个 POST 请求,往团队中添加一个成员
  • (3)向 http://localhost:5002/api/members/

    /locationreports 发送一个 POST 请求,报送团队成员位置
  • (4)观察由报送的位置转换而成、被放到对应队列中的 MemberLocationReportedEvent 事件

  • (5)再重复几次第 3 步,添加一些相距较远的位置,确保不会触发并被检测到位置接近事件

  • (6)重复第 2 步,往第一名测试成员所在团队添加一名新成员

  • (7)为第二名成员再次重复第 3 步,添加一个于第一名成员最近的位置相距几公里以内的位置

  • (8)现在应该能够在 proximitydetected 队列中看到一条新消息

  • (9)可用直接查询 Redis 缓存,也可以利用事实服务来查看各团队成员最新的位置状态

手动操作几次后,大多数团队会花些时间把这一过程自动化

借助 docker compose 之类的工具,或者创建 Kubernetes 部署,或者其他容器编排环境,可自动将所有服务部署到集成测试环境

接着用脚本发送 REST 请求

待测试运行完成后,断言出现了正确的接近检测的次数,值也是正确的

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

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

相关文章

数据结构----快速排序

数据结构----快速排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> int quickSort(int a[], int l, int h) {//快速排序int i l, j h, p a[l];while (i < j) {while (i<j&&a[j]>p) {//从右往左…

编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理

很久没有写过 .NET Core 相关的文章了&#xff0c;目前关店在家休息所以有些时间写一篇新的????。这次的文章主要介绍如何在 Linux 上编译调试最新的 .NET Core 5.0 Preview 与简单分析 Span 的实现原理。微软从 .NET Core 5.0 开始把 GIT 仓库 coreclr 与 corefx 合并移动…

数据结构----归并排序

数据结构----归并排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> #define N 100 void guiBingSort(int a[], int l, int h,int length) {//归并排序int mid (l h) / 2;int* b (int *)malloc(N*sizeof(int));i…

利用obfuscar对.NET应用进行混淆

背景发布客户端程序产品时&#xff0c;免不了会遇到一些怀有恶意或有强烈学习欲望的用户尝试对程序进行反编译。对于一些编译成本地指令的程序&#xff08;如C、C&#xff09;&#xff0c;编译后可读性低&#xff0c;反编译和破解成本较高&#xff0c;不需要对代码进行太高强度…

数据结构---基数排序

数据结构—基数排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> int getNumberBit(int number) {//获取数字的位数int x number,count0;if (x 0)return 1;while (x ! 0) {count;x / 10;}return count; } int g…

C# 版本 疫情传播仿真程序

前言前一阵子看到有人制作了《疫情传播仿真程序》&#xff0c;是用 Java做的。里面根据多种实际情况&#xff0c;如居民移动意愿、医护能力、病毒传播能力&#xff0c;来模拟疫情的发展。看完之后&#xff0c;我暗暗称奇&#xff0c;特别是结合一些视频和照片&#xff0c;确实做…

jmeter 加密解密_使用Jmeter对SHA1加密接口进行性能测试

机会只留给那些有准备的人改变能改变的&#xff0c;接受不能改变的&#xff0c;就是进步性能测试过程中&#xff0c;有时候会遇到需要对信息头进行加密鉴权&#xff0c;下面我就来介绍如何针对SHA1加密鉴权开发性能测试脚本 1、首先了解原理&#xff0c;就是需要对如下三个参数…

word List 06

word List 06 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

面对疫情,在家办公的程序员如何突围

作者&#xff1a;陌北有棵树&#xff0c;架构师社区合伙人很多程序员朋友都已经开始了在家办公的生活&#xff0c;第一天办公&#xff0c;你的远程工具还流畅吗&#xff0c;视频会议换了几个软件&#xff1f;当然这些都是外在因素&#xff0c;尤其对于程序员来说&#xff0c;解…

[蓝桥杯2018初赛]全球变暖-dfs,bfs,连通块

解题思路: bfs:遍历所有未遍历过的陆地&#xff0c;通过bfs计算出当前位置连通陆地的数量cnt&#xff0c;以及被淹没陆地的数量bound,若cnt bound表示完整淹没的一个岛屿 dfs:将连通块全部标记&#xff0c;如果这个连通块全部都会淹没&#xff0c;则答案1&#xff0c;如果这个…

latex 参考文献显示问号_回「LaTeX 的罪与罚」

原文链接&#xff1a;LaTeX 的罪与罚 - 朴素的贝叶斯的文章 - 知乎作为 LaTeX 开发者&#xff0c;看到这种嘲讽自然是非常 angry 的。本来并不想趟这个混水&#xff0c;然而眼见着赞数一天天涨上去&#xff0c;还居然进了精华区&#xff0c;实在忍不住只好注册了贵乎来说几句。…

疫情之下,使用FRP实现内网穿透,远程连接公司电脑进行办公

当前情况下&#xff0c;经常会有需要到公司电脑进行一些操作&#xff0c;比如连接内网OA&#xff0c;数据库或者提交文档。为了减少外出&#xff0c;将使用frp进行内网穿透的方法进行一个说明。前提条件1. 一台拥有公网 IP 的设备(如果没有&#xff0c;服务器可以使用https://d…

ad中电容用什么封装_二极管在电路中到底做什么用的

所有的电子电路中基本上都会用到二极管&#xff0c;它的特性也是非常之多&#xff0c;最主要就是单方向导电性&#xff0c;(单向导电性的两根引脚之间的电阻分为正向电阻和反向电阻两种)。人们利用这些不同特性构成各种具体的应用电路&#xff0c;分析不同电路中的二极管工作原…

数据结构---邻接矩阵的DFS

数据结构—邻接矩阵的DFS 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> #define N 100 #define elemType int //const int MAX_INT (1 << 31) - 1; //const int MAX_INT 0X7fffffff; #define INF (((uns…

.NET Core 如何判断程序是否在远程桌面(RDP)下运行

点击上方蓝字关注“汪宇杰博客”导语由于疫情的关系&#xff0c;很久没发文章了。今天终于稳定下来在家办公&#xff0c;抽空分享一个刚学会的技巧。最近在家办公的程序员可能避免不了要用远程桌面&#xff0c;那么问题来了&#xff0c;你的 .NET Core 程序有没有办法知道自己是…

[蓝桥杯2018初赛]方格计数-巧妙枚举,找规,数论

解题思路&#xff1a; 枚举第一象限的所有点&#xff0c;判断是否在圆内&#xff0c;最后结果*4 我们用下面的程序&#xff0c;来算一个半径为2的圆&#xff0c;其实我们第一象限算的就是那个绿点&#xff0c;然后类比到半径5000. 代码如下&#xff1a; #include <iostre…

ipa在线安装搭建_三种越狱工具安装方法

从 iOS 9.2 开始&#xff0c;苹果越狱进入了半越狱时代&#xff0c;也就是重启手机之后需要重新进入越狱工具激活越狱环境&#xff0c;以下是三种常用的越狱工具安装方法&#xff1a;方法一&#xff1a;自签名下载大胡子签名工具&#xff1a;Cydia Impactor下载地址&#xff1a…

《ASP.NET Core 微服务实战》-- 读书笔记(第7章)

第 7 章 开发 ASP.NET Core Web 应用ASP.NET Core 基础在本章&#xff0c;我们将从一个命令行应用开始&#xff0c;并且在不借助任何模板&#xff0c;脚手架和向导的情况下&#xff0c;最终得到一个功能完整的 Web 应用GitHub链接&#xff1a;https://github.com/microservices…

git为私有仓库设置密码_dnf仓库密码设置不跳出 dnf仓库密码设置流程

部分玩家想要设置游戏中仓库密码但是自己不小心点击了不再提醒后不再弹出了&#xff0c;那么怎么办呢&#xff0c;其实不用着急&#xff0c;下面带大家了解一下如何设置设置仓库锁密码&#xff0c;感兴趣的玩家可以玩下看看哦。dnf仓库密码设置不跳出在游戏中玩家每天第一次登陆…

word List 07

word List 07 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;