Skywalking 中 Agent 自动同步配置源码解析

文章目录

    • 前言
    • 正文
      • 实现架构
      • 实现模型
      • OAP 同步 Apollo
        • ConfigWatcherRegister
        • ConfigChangeWatcher
      • Agent 侧

前言

本文代码 OAP 基于 v9.7,Java Agent 基于 v9.1,配置中心使用 apollo。

看本文需要配合代码“食用”。

正文

Skywalking 中就使用这种模型实现了 Agent 同步Apollo 配置,本文介绍下提供的功能以及代码实现,一起学习下。

Skywalking 支持 agent 动态更新配置,使 agent 可以依据业务需求进行自定义配置;更重要的是建立起这一个通信机制,那么 agent 的可管理性、扩展性都大大提升。

目前 Skywalking 提供了以下配置项

在这里插入图片描述

按照文档描述,主要为以下内容:

  • 控制采样速率

  • 忽略指定后缀的请求,注意必须是 first span 的 opretationName 匹配到

    针对 web 服务,有些静态资源是放在服务端,那么可以过滤掉这些请求

  • 忽略某些 path 的 trace

  • 限定每个 segment 中的 span 最大数量

  • 是否收集执行 sql 的参数

样例配置

configurations:serviceA:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1serviceB:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1

注意:这个是按照服务来进行逐项配置,如果不需要变动,不要添加对应 key,会使用默认值。

实现架构

  • OAP 同步 Apollo 配置

  • Agent 同步 OAP 配置。

每阶段的操作无关联,都是作为 Client 的一端发起的请求来同步数据。

实现模型

配置动态变更实际上是一个订阅发布模型,简单描述就是有发布者和订阅者两种角色,之间交互一般是:有一个注册接口,方便订阅者注册自身,以及发布者可以获取到订阅者列表;一个通知接口,方便发布者发送消息给订阅者。

例如需要订水,只要给订水公司留下自己的电话、地址及数量(发布者知道如何找到你),之后就有人送水上门(有水时进行派送)。

这种模型理解起来很简单,实现上难度也不大,且使用场景很广泛。

OAP 同步 Apollo

首先看下 OAP 是如何同步 apollo 数据。

ConfigWatcherRegister

这是一个抽象类,代表的是配置中心的角色,实现上有 apollo、nacos、zk 等方式。

在这里插入图片描述

先看下 notifySingleValue 方法:

protected void notifySingleValue(final ConfigChangeWatcher watcher, ConfigTable.ConfigItem configItem) {String newItemValue = configItem.getValue();if (newItemValue == null) {if (watcher.value() != null) {// Notify watcher, the new value is null with delete event type.// 调用 watcher 的 notify 进行处理 watcher.notify(new ConfigChangeWatcher.ConfigChangeEvent(null, ConfigChangeWatcher.EventType.DELETE));} else {// Don't need to notify, stay in null.}} else {if (!newItemValue.equals(watcher.value())) {watcher.notify(new ConfigChangeWatcher.ConfigChangeEvent(newItemValue,ConfigChangeWatcher.EventType.MODIFY));} else {// Don't need to notify, stay in the same config value.}}
}

该方法的逻辑是:读取 configItem 中的值,并且与 watcher 中的值进行比较,不相等之后判定是 DELETE、还是 UPDATE 操作,并封装成一个 ConfigChangeEvent 发送给 ConfigChangeWatcher,那么可以看出 ConfigChangeWatcher 是个订阅者的角色。

继续看下调用 notifySingleValue 方法的地方:

FetchingConfigWatcherRegister#singleConfigsSync

private final Register singleConfigChangeWatcherRegister = new Register();public abstract Optional<ConfigTable> readConfig(Set<String> keys);private void singleConfigsSync() {// 1. 读取配置数据Optional<ConfigTable> configTable = readConfig(singleConfigChangeWatcherRegister.keys());// Config table would be null if no change detected from the implementation.configTable.ifPresent(config -> {config.getItems().forEach(item -> {// 2. 遍历获取配置中的 itemNameString itemName = item.getName();// 3. 依据 itemName 找到 WatcherHolderWatcherHolder holder = singleConfigChangeWatcherRegister.get(itemName);if (holder == null) {return;}ConfigChangeWatcher watcher = holder.getWatcher();// 从 WatcherHolder 得到 ConfigChangeWatcher,发送通知notifySingleValue(watcher, item);});});
}

该方法执行的逻辑就是:

  1. 依据 singleConfigChangeWatcherRegister.keys() 作为参数读取配置信息
  2. 遍历配置信息,依据配置中的 name(即 itemName)找到 WatcherHolder,进而获取 ConfigChangeWatcher
  3. 调用 notifySingleValue。

readConfig 是个抽象方法,由具体的配置中心插件实现,本例中使用的 apollo,具体实现就是 ApolloConfigWatcherRegister。

读取到的内容类型 ConfigTable,并且可以知道是存储的 k-v 集合,那么 ConfigItem 就是每个配置项,itemName 就是 apollo 中配置的 key。

再看看调用 singleConfigsSync 的逻辑:

// FetchingConfigWatcherRegister.javapublic void start() {isStarted = true;Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(new RunnableWithExceptionProtection(this::configSync, // 启动定时任务来执行t -> log.error("Sync config center error.", t)), 0, syncPeriod, TimeUnit.SECONDS);
}void configSync() {singleConfigsSync();groupConfigsSync();
}

再回到 singleConfigsSync 中,读取配置时需要先获取到配置项的 key 的集合:singleConfigChangeWatcherRegister.keys()

先看下 singleConfigChangeWatcherRegister 的具体实现:FetchingConfigWatcherRegister$Register 内部就是一个 Map<String, WatcherHolder> 来存储。

static class Register {private Map<String, WatcherHolder> register = new HashMap<>();private boolean containsKey(String key) {return register.containsKey(key);}private void put(String key, WatcherHolder holder) {register.put(key, holder);}public WatcherHolder get(String name) {return register.get(name);}public Set<String> keys() {return register.keySet();}
}

有读取就有存储,看看调用 put 逻辑:

// FetchingConfigWatcherRegister
synchronized public void registerConfigChangeWatcher(ConfigChangeWatcher watcher) {WatcherHolder holder = new WatcherHolder(watcher);if (singleConfigChangeWatcherRegister.containsKey(holder.getKey()) || groupConfigChangeWatcherRegister.containsKey(holder.getKey())) {}switch (holder.getWatcher().getWatchType()) {case SINGLE:// put 调用singleConfigChangeWatcherRegister.put(holder.getKey(), holder);break;case GROUP:groupConfigChangeWatcherRegister.put(holder.getKey(), holder);break;default:}
}

registerConfigChangeWatcher 方法,用于注册 ConfigChangeWatcher ,内部处理逻辑:先将 watcher 放入 watchHolder 中,再以 holder key 分开存储 holder(放入 FetchingConfigWatcherRegister$Register 中)。

WatcherHolder 是 ConfigWatcherRegister 一个内部类,代码如下,重点是 key 生成规则:String.join(".", watcher.getModule(), watcher.getProvider().name(), watcher.getItemName());,每个 itemName 对应一个 watcher。

@Getter
protected static class WatcherHolder {private ConfigChangeWatcher watcher;private final String key;public WatcherHolder(ConfigChangeWatcher watcher) {this.watcher = watcher;this.key = String.join(".", watcher.getModule(), watcher.getProvider().name(),watcher.getItemName());}
}

总结:OAP 启动定时任务,同步 apollo 的配置数据,遍历每个配置项(configItem),找到对应的 ConfigChangerWater,将 watcher 中的值与 configItem 中的值进行比较,不相等之后继续判定是 DELETE、还是 UPDATE 操作,封装成一个 ConfigChangeEvent 发送给对应的 ConfigChangeWatcher。

ConfigChangeWatcher

抽象类,依据命名,表示的是关注配置变化的 watcher,是 OAP 中定义的用于对不同配置的具体实现;对于 Apollo 上的每个 Key 都有对应的 ConfigChangeWatcher。

在这里插入图片描述

具体的 ConfigChangeWatcher 获取到 ConfigChangeEvent,处理逻辑各有不同,本次具体看下 AgentConfigurationsWatcher。

private volatile String settingsString;private volatile AgentConfigurationsTable agentConfigurationsTable;public void notify(ConfigChangeEvent value) {if (value.getEventType().equals(EventType.DELETE)) {settingsString = null;this.agentConfigurationsTable = new AgentConfigurationsTable();} else {settingsString = value.getNewValue();AgentConfigurationsReader agentConfigurationsReader =new AgentConfigurationsReader(new StringReader(value.getNewValue()));this.agentConfigurationsTable = agentConfigurationsReader.readAgentConfigurationsTable();}
}

方法逻辑为:config value 存储到了 agentConfigurationsTable。

apollo value 是什么样子呢?

configurations:serviceA:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1serviceB:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1

AgentConfigurationsTable 如下具体实现

public class AgentConfigurationsTable {private Map<String, AgentConfigurations> agentConfigurationsCache;public AgentConfigurationsTable() {this.agentConfigurationsCache = new HashMap<>();}
}public class AgentConfigurations {private String service;private Map<String, String> configuration;/*** The uuid is calculated by the dynamic configuration of the service.*/private volatile String uuid;public AgentConfigurations(final String service, final Map<String, String> configuration, final String uuid) {this.service = service;this.configuration = configuration;this.uuid = uuid;}
}

将 agentConfigurationsTable 转换成 json 展示更容里理解数据存储的结构:

{"serviceB": {"service": "serviceB","configuration": {"trace.sample_n_per_3_secs": "1000","trace.ignore_path": "/a/b/c,/a1/b1/c1"},"uuid": "92670f1ccbdee60e14ffc0"},"serviceA": {"service": "serviceA","configuration": {"trace.sample_n_per_3_secs": "1000","trace.ignore_path": "/a/b/c,/a1/b1/c1"},"uuid": "92670f1ccbdee60e14ffc0"}
}

查看读取 agentConfigurationsTable 值的逻辑:

// AgentConfigurationsWatcher#getAgentConfigurations
public AgentConfigurations getAgentConfigurations(String service) {// 依据 service 获取数据AgentConfigurations agentConfigurations = this.agentConfigurationsTable.getAgentConfigurationsCache().get(service);if (null == agentConfigurations) {return emptyAgentConfigurations;} else {return agentConfigurations;}
}

继续查看调用 getAgentConfigurations 的代码,并且将 value 包装成 ConfigurationDiscoveryCommand 返回。

// ConfigurationDiscoveryServiceHandler#fetchConfigurations
public void fetchConfigurations(final ConfigurationSyncRequest request,final StreamObserver<Commands> responseObserver) {Commands.Builder commandsBuilder = Commands.newBuilder();AgentConfigurations agentConfigurations = agentConfigurationsWatcher.getAgentConfigurations(request.getService());if (null != agentConfigurations) {// 请求时会带有 uuid,会跟现有配置的 uuid 进行比对,如果不同,则获取最新值 if (disableMessageDigest || !Objects.equals(agentConfigurations.getUuid(), request.getUuid())) {ConfigurationDiscoveryCommand configurationDiscoveryCommand =newAgentDynamicConfigCommand(agentConfigurations);commandsBuilder.addCommands(configurationDiscoveryCommand.serialize().build());}}responseObserver.onNext(commandsBuilder.build());responseObserver.onCompleted();
}

ConfigurationDiscoveryServiceHandler 属于 GRPCHandler,类似 SpringBoot 中 Controller,暴露接口,外部就可以获取数据。

ConfigurationDiscoveryCommand 这个方法中有个属性来标识 command 的具体类型,这个在 agent 端接收到 command 需要依据 command 类型找到真正的处理器。

public static final String NAME = "ConfigurationDiscoveryCommand";

总结:当 AgentConfigurationsWatcher 收到订阅的 ConfigChangeEvent 时,会将值存储至 AgentConfigurationsTable,之后通过 ConfigurationDiscoveryServiceHandler 暴露接口,以方便 agent 可以获取到相应服务的配置。

至此,OAP 与 Apollo 间的配置更新逻辑以及值的处理逻辑大致理清了。

接下来看看 agent 与 oap 间的交互。

Agent 侧

找到调用 ConfigurationDiscoveryServiceGrpc#fetchConfigurations 的代码,看到 ConfigurationDiscoveryService,查看具体调用逻辑:

// ConfigurationDiscoveryService
private void getAgentDynamicConfig() {if (GRPCChannelStatus.CONNECTED.equals(status)) {try {// 准备参数ConfigurationSyncRequest.Builder builder = ConfigurationSyncRequest.newBuilder();builder.setService(Config.Agent.SERVICE_NAME);if (configurationDiscoveryServiceBlockingStub != null) {final Commands commands = configurationDiscoveryServiceBlockingStub.withDeadlineAfter(GRPC_UPSTREAM_TIMEOUT, TimeUnit.SECONDS).fetchConfigurations(builder.build()); // 方法调用// 结果处理ServiceManager.INSTANCE.findService(CommandService.class).receiveCommand(commands);}} catch (Throwable t) {}}
}

而 getAgentDynamicConfig 是在 ConfigurationDiscoveryService#boot 执行时 init 了一个定时任务调用。

public void boot() throws Throwable {getDynamicConfigurationFuture = Executors.newSingleThreadScheduledExecutor(new DefaultNamedThreadFactory("ConfigurationDiscoveryService")).scheduleAtFixedRate(new RunnableWithExceptionProtection(this::getAgentDynamicConfig,t -> LOGGER.error("Sync config from OAP error.", t)),Config.Collector.GET_AGENT_DYNAMIC_CONFIG_INTERVAL,Config.Collector.GET_AGENT_DYNAMIC_CONFIG_INTERVAL,TimeUnit.SECONDS);
}

获取结果后的处理逻辑:CommandService 接收 Commands,先是放入到队列中,

private LinkedBlockingQueue<BaseCommand> commands = new LinkedBlockingQueue<>(64);public void receiveCommand(Commands commands) {for (Command command : commands.getCommandsList()) {try {BaseCommand baseCommand = CommandDeserializer.deserialize(command);// 将结果放入队列中boolean success = this.commands.offer(baseCommand);if (!success && LOGGER.isWarnEnable()) {}} catch (UnsupportedCommandException e) {}}
}

新开线程来消费队列,commandExecutorService 处理 Commands,通过代码调用链看到,最后依据 command 的类型找到真正指令执行器。

// CommandService#run
public void run() {final CommandExecutorService commandExecutorService = ServiceManager.INSTANCE.findService(CommandExecutorService.class);while (isRunning) {try {// 消费队列BaseCommand command = this.commands.take();// 判断是否已经执行过了if (isCommandExecuted(command)) {continue;}// 分发 commandcommandExecutorService.execute(command);serialNumberCache.add(command.getSerialNumber());} catch (CommandExecutionException e) {}}
}// CommandExecutorService#execute
public void execute(final BaseCommand command) throws CommandExecutionException {this.executorForCommand(command).execute(command);
}
// CommandExecutorService#executorForCommand
private CommandExecutor executorForCommand(final BaseCommand command) {final CommandExecutor executor = this.commandExecutorMap.get(command.getCommand());if (executor != null) {return executor;}return NoopCommandExecutor.INSTANCE;
}

依据指令类型获取具体的指令执行器,这里为 ConfigurationDiscoveryService,发现又调用了 ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand 处理。

// ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand
public void handleConfigurationDiscoveryCommand(ConfigurationDiscoveryCommand configurationDiscoveryCommand) {final String responseUuid = configurationDiscoveryCommand.getUuid();List<KeyStringValuePair> config = readConfig(configurationDiscoveryCommand);// 遍历配置项config.forEach(property -> {String propertyKey = property.getKey();List<WatcherHolder> holderList = register.get(propertyKey);for (WatcherHolder holder : holderList) {if (holder != null) {// 依据配置项找到对应的 AgentConfigChangeWatcher,封装成 ConfigChangeEvent AgentConfigChangeWatcher watcher = holder.getWatcher();String newPropertyValue = property.getValue();if (StringUtil.isBlank(newPropertyValue)) {if (watcher.value() != null) {// Notify watcher, the new value is null with delete event type.watcher.notify(new AgentConfigChangeWatcher.ConfigChangeEvent(null, AgentConfigChangeWatcher.EventType.DELETE));}} else {if (!newPropertyValue.equals(watcher.value())) {watcher.notify(new AgentConfigChangeWatcher.ConfigChangeEvent(newPropertyValue, AgentConfigChangeWatcher.EventType.MODIFY));}}}}});this.uuid = responseUuid;
}

ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand 进行处理,遍历配置项列表,依据 Key 找到对应的 AgentConfigChangeWatcher,进行 notify。

这个过程是不是很熟悉,跟 OAP 中处理逻辑不能说是完全一样,简直一模一样。

AgentConfigChangeWatcher 是个抽象类,查看其具体实现,关注其注册以及处理 value 的逻辑即可。
在这里插入图片描述

具体逻辑就不再展开细说了,需要自行了解下。

总之,agent 可以进行动态配置,能做的事情就多了,尤其是对 agent.config 中的配置大部分就可以实现动态管理了。

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

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

相关文章

华为5720设置静态路由不通_【干货分享】交换机与路由器在环路中的处理机制了解一下!...

点击蓝字关注我们-今天小盟带大家来讨论一下交换机与路由器在环路中的处理机制-01基础配置1---如图配置路由器各接口地址&#xff0c;AR-2为PC-1的网关路由器2---AR-1配置静态默认路由&#xff0c;下一跳地址指向AR-2&#xff1b;[AR-1]ip route-static 0.0.0.0 0 12.1.1.2AR-2…

VisualVM介绍使用

1 打开VisualVM&#xff08;这个工具放在JDK安装目录的bin目录下&#xff0c;双击jvisualvm.exe即可打开&#xff09;&#xff0c;如下图所示 以VisualVM自身为例&#xff0c;VisualVM本身也是一个java程序&#xff0c;当然也而已用VisualVM来分析 2 概述页面主要显示程序…

Java功能的适用性

Java语言和标准库功能强大&#xff0c;但功能强大&#xff0c; 责任重大 。 一方面看到很多用户代码滥用或滥用稀有的Java功能&#xff0c;另一方面却完全忘记了大多数基本功能之后&#xff0c;我决定撰写此摘要。 这不是每个Java开发人员都应该探索&#xff0c;了解和使用的要…

台达b3伺服modbus通讯_【数控系统】台达伺服压机控制灵活 精准压合满足各种工序需求...

引言压机是一种利用压力改变工件形状的机械设备。随着制造业少量多样与客制化的日趋发展&#xff0c;压机的的优势逐渐显现&#xff0c;在汽车、五金与电子制造等产业中的应用不断增多。传统压机在使用操作上耗费人力并需要诸多压机元件才能完整运作&#xff0c;维修成本高&…

yolov4Linux,基于Darknet的YOLOv4目标检测

目录一、Windows环境下的YOLOv4目标检测1、环境配置环境准备&#xff1a;Win10、CUDA10.1、cuDNN7.65、Visual Studio 2019、OpenCV 3.4(1)Visual Studio2019企业版安装(3)下载并安装CUDA10.1&#xff0c;下载安装cuDNN7.65对于cudnn直接将其解开压缩包&#xff0c;然后需要将b…

二元置信椭圆r语言_医学统计与R语言:圆形树状图(circular dendrogram)

微信公众号&#xff1a;医学统计与R语言如果你觉得对你有帮助&#xff0c;欢迎转发输入1&#xff1a; "ggraph")结果1&#xff1a; name 输入2&#xff1a; <- graph_from_data_frame(myedges1, verticesmyvertices,directed T)ggraph(mygraph, layout dend…

Java:检查器框架

我在JavaOne 2012上 了解的有趣的工具之一是Checker Framework 。 Checker Framework的网页之一 指出 &#xff0c;Checker Framework“增强了Java的类型系统&#xff0c;使其更强大&#xff0c;更有用”&#xff0c;从而使软件开发人员“能够检测并防止Java程序中的错误”。 查…

南岸焊接机器人厂_造船三部高效焊接工艺技术年鉴

为了提升公司高效焊自动化率&#xff0c;实现降本增效目标&#xff0c;造船事业三部积极响应公司领导号召&#xff0c;充分挖掘自身资源&#xff0c;2020年&#xff0c;在高效焊接技术、设备开发研究等方面&#xff0c;不断创新、敢于突破&#xff0c;获取了多项焊接新技术、新…

线性回归csv数据集_测试数据科学家线性回归的30个问题

你的目标是数据科学家吗&#xff1f;那你对线性回归了解有多深入呢&#xff0c;下面的30道题&#xff0c;可以帮助你或者测试别人是否真的达到的数据科学家的水平&#xff0c;关注回复&#xff0c;答案在评论区&#xff1a;1)是非题&#xff1a;线性回归是一种受监督的机器学习…

dw空心圆项目符号_如何懂建筑施工图?搞懂这些符号解析,耐下性子研究不会学不会...

施工图纸一个建筑方向&#xff0c;是房屋建筑的依据&#xff0c;更是一种工程语言&#xff0c;它能够明确的规定出我们建造出怎样的建筑&#xff0c;看懂它是入行基础。当然建筑图包含的因素比较多&#xff0c;有具体的建筑符号&#xff0c;尺寸、做法以及技术要求都在里面&…

canvas学习和滤镜实现

最近学习了 HTML5 中的重头戏--canvas。利用 canvas&#xff0c;前端人员可以很轻松地、进行图像处理。其 API 繁多&#xff0c;这次主要学习常用的 API&#xff0c;并且完成以下两个代码&#xff1a;实现去色滤镜实现负色(反色)滤镜 欢迎入群&#xff1a;857989948 。IT 技术深…

android代码混淆作用,Android分享:代码混淆那些事

1) 前言ProGuard是一个开源的Java代码混淆器。它可以混淆Android项目里面的java代码&#xff0c;对的&#xff0c;你没看错&#xff0c;仅仅是java代码。它是无法混淆Native代码&#xff0c;资源文件drawable、xml等。2) ProGuard作用压缩: 移除无效的类、属性、方法等优化: 优…

施工部署主要不包括_建筑工程施工方案及培训实例

[南京]大型土石方工程施工方案本资料为[南京]大型土石方工程施工方案格&#xff0c;共43页。内容简介&#xff1a;本工程位于南京化工园内&#xff0c;扬子玉带扬巴路附近&#xff0c;工程项目为江苏成品油管道配设施扬子玉带油库套油库工程&#xff0c;由中国石化股份有限公司…

java第六次作业

《Java技术》第六次作业 &#xff08;一&#xff09;学习总结 1.用思维导图对本周的学习内容进行总结。 2.当程序中出现异常时&#xff0c;JVM会依据方法调用顺序依次查找有关的错误处理程序。可使用printStackTrace 和getMessage方法了解异常发生的情况。阅读下面的程序&#…

华为鸿蒙不再孤,华为鸿蒙OS系统不再孤单!又一款国产系统启动内测:再掀国产替代化...

【5月10日讯】相信大家都知道&#xff0c;备受广大花粉们期待的鸿蒙OS系统终于开始推送公测版本了&#xff0c;并且适配机型也开始不断地增多&#xff0c;而根据华为官方最新消息&#xff0c;华为鸿蒙OS系统将会在6月份开始大规模推送正式版鸿蒙系统&#xff0c;这无疑将会成为…

CSS 水平垂直居中

方法一&#xff1a; 容器确定宽高&#xff1a;知识点&#xff1a;transform只能设置在display为block的元素上。 <head> <meta charset"UTF-8"> <title>Title</title> <style type"text/css"> #container{…

linux怎么进入文件夹_Linux基础命令《上》

上一节介绍了VMware中安装centos7以及克隆系统&#xff0c;之中用到的几个命名还都是开发不常用的&#xff0c;这节课就准备讲解一下入门的Linux命名&#xff0c;都是日常使用的。首先呢&#xff0c;我们进入系统后&#xff0c;得先知道我是谁&#xff0c;我在哪儿&#xff1f;…

UML学习(一)-----用例图

1、什么是用例图 用例图源于Jacobson的OOSE方法&#xff0c;用例图是需求分析的产物&#xff0c;描述了系统的参与者与系统进行交互的功能&#xff0c;是参与者所能观察和使用到的系统功能的模型图。它的主要目的就是帮助开发团队以一种可视化的方式理解系统的功能需求&#xf…

首款鸿蒙系统终端n,荣耀智慧屏正式发布,首款搭载鸿蒙系统终端,家庭C位新选择...

原标题&#xff1a;荣耀智慧屏正式发布&#xff0c;首款搭载鸿蒙系统终端&#xff0c;家庭C位新选择智能手机的普及率越来越高&#xff0c;其所能够承担的功能也越来越多&#xff0c;电视机对于很多中青年的用户来讲&#xff0c;更多的时候就是个摆设。在家庭中&#xff0c;看电…

oracle如何保证数据一致性和避免脏读

oracle通过undo保证一致性读和不发生脏读 1.不发生脏读2.一致性读3. 事务槽&#xff08;ITL&#xff09;小解1.不发生脏读 例如&#xff1a;用户A对表更新了&#xff0c;没有提交&#xff0c;用户B对进行查询&#xff0c;没有提交的更新不能出现在用户的查询结果中 举例并通个d…