有句话叫做“如无必要,勿增实体"。

在一些小型项目当中,没有引入消息中间件,也不想引入,但有一些业务逻辑想要解耦异步,那怎么办呢?

我们的web项目,单独内网部署,由于大数据背景,公司消息中间件统一使用的kafka,在一些小项目上kafka就显得很笨重。

引入rocketmq或rabittmq也没必要。

事件或多线程也不适合。

具体一点的,之前对接的一个系统,一张记录表有10+以上的类型状态,新的需求是,针对每种状态做出对应的不同的操作。

之前写入这张记录表的时候,方式也是五花八门,有的是单条记录写入,有的是批量写入,有的调用了统一的service,有的呢直接调用了DAO层mapper直接写入。

所以想找到一个统一入口进行切入处理,就不行了。

这个时候就算引入消息队列,也需要在不同的业务方法里进行写入消息的操作。业务方也不太愿意配合改。

可以使用触发器,但它是属于上个时代的产物,槽点太多。(这里并不是完全不主张使用触发器,技术永远是为业务服务的,只要评估觉得可行,就可以使用)

那么这个时候,CDC技术就可以粉墨登场了。

CDC(change data capture)数据更改捕获。

常见的数据更改捕获都是通过数据库比如mysql的binlog来达到目的。

我们可以监控mysql binlog日志,当写入一条数据的时候,接收到数据变更日志,做出相应的操作。

这样的好处是,只需导入依赖,不额外引入组件,同时无需改动之前的代码。

两边完全解耦,互不干扰。

常见的CDC框架,比如,canal (非Camel)

  • canal [kə'næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

    早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。

    从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

  • 它是基于日志增量订阅和消费的业务,包括

数据库镜像

数据库实时备份

索引构建和实时维护(拆分异构索引、倒排索引等)

业务 cache 刷新

带业务逻辑的增量数据处理

它的原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

再比如,debezium(音同 dbzm 滴BZ姆)很多人可能不太了解.

包括databus,maxwell,flink cdc(大数据领域)等等,它们同属CDC捕获数据更改(change data capture)类的技术。

为什么是debezium

这么多技术框架,为什么选debezium?

看起来很多。但一一排除下来就debezium和canal。

sqoop,kettle,datax之类的工具,属于前大数据时代的产物,地位类似于web领域的structs2。而且,它们基于查询而非binlog日志,其实不属于CDC。首先排除。

flink cdc是大数据领域的框架,一般web项目的数据量属于大材小用了。

同时databus,maxwell相对比较冷门,用得比较少。

最后不用canal的原因有以下几点。

  1. canal需要安装,这违背了“如非必要,勿增实体”的原则。
  2. canal只能对MYSQL进行CDC监控。有很大的局限性。
  3. 大数据领域非常流行的flink cdc(阿里团队主导)底层使用的也是debezium,而非同是阿里出品的canal。
  4. debezium可借助kafka组件,将变动的数据发到kafka topic,后续的读取操作只需读取kafka,可有效减少数据库的读取压力。可保证一次语义,至少一次语义。

    同时,也可基于内嵌部署模式,无需我们手动部署kafka集群,可满足”如非必要,勿增实体“的原则。

Debezium是一个捕获数据更改(CDC)平台,并且利用Kafka和Kafka Connect实现了自己的持久性、可靠性和容错性。

每一个部署在Kafka Connect分布式的、可扩展的、容错性的服务中的connector监控一个上游数据库服务器,捕获所有的数据库更改,

然后记录到一个或者多个Kafka topic(通常一个数据库表对应一个kafka topic)。

Kafka确保所有这些数据更改事件都能够多副本并且总体上有序(Kafka只能保证一个topic的单个分区内有序),这样,

更多的客户端可以独立消费同样的数据更改事件而对上游数据库系统造成的影响降到很小(如果N个应用都直接去监控数据库更改,对数据库的压力为N,

而用debezium汇报数据库更改事件到kafka,所有的应用都去消费kafka中的消息,可以把对数据库的压力降到1)。另外,客户端可以随时停止消费,然后重启,

从上次停止消费的地方接着消费。每个客户端可以自行决定他们是否需要exactly-once或者at-least-once消息交付语义保证,

并且所有的数据库或者表的更改事件是按照上游数据库发生的顺序被交付的。

对于不需要或者不想要这种容错级别、性能、可扩展性、可靠性的应用,他们可以使用内嵌的Debezium connector引擎来直接在应用内部运行connector。

这种应用仍需要消费数据库更改事件,但更希望connector直接传递给它,而不是持久化到Kafka里。

简介

Debezium是一个开源项目,为捕获数据更改(change data capture,CDC)提供了一个低延迟的流式处理平台。你可以安装并且配置Debezium去监控你的数据库,然后你的应用就可以消费对数据库的每一个行级别(row-level)的更改。只有已提交的更改才是可见的,所以你的应用不用担心事务(transaction)或者更改被回滚(roll back)。Debezium为所有的数据库更改事件提供了一个统一的模型,所以你的应用不用担心每一种数据库管理系统的错综复杂性。另外,由于Debezium用持久化的、有副本备份的日志来记录数据库数据变化的历史,因此,你的应用可以随时停止再重启,而不会错过它停止运行时发生的事件,保证了所有的事件都能被正确地、完全地处理掉。

监控数据库,并且在数据变动的时候获得通知一直是很复杂的事情。关系型数据库的触发器可以做到,但是只对特定的数据库有效,而且通常只能更新数据库内的状态(无法和外部的进程通信)。一些数据库提供了监控数据变动的API或者框架,但是没有一个标准,每种数据库的实现方式都是不同的,并且需要大量特定的知识和理解特定的代码才能运用。确保以相同的顺序查看和处理所有更改,同时最小化影响数据库仍然非常具有挑战性。

Debezium提供了模块为你做这些复杂的工作。一些模块是通用的,并且能够适用多种数据库管理系统,但在功能和性能方面仍有一些限制。另一些模块是为特定的数据库管理系统定制的,所以他们通常可以更多地利用数据库系统本身的特性来提供更多功能。

github官网上罗列的一些

典型应用场景

  • 缓存失效(Cache invalidation)

    经典问题 Redis与MySQL双写一致性如何保证?Debezium利用kafka单分区的有序性(忽略mysql binlog本身可能的延迟和乱序),可完全解决此问题。

    在缓存中缓存的条目(entry)在源头被更改或者被删除的时候立即让缓存中的条目失效。

    如果缓存在一个独立的进程中运行(例如Redis,Memcache,Infinispan或者其他的),那么简单的缓存失效逻辑可以放在独立的进程或服务中,

    从而简化主应用的逻辑。在一些场景中,缓存失效逻辑可以更复杂一点,让它利用更改事件中的更新数据去更新缓存中受影响的条目。

  • 简化单体应用(Simplifying monolithic applications)

    许多应用更新数据库,然后在数据库中的更改被提交后,做一些额外的工作:更新搜索索引,更新缓存,发送通知,运行业务逻辑,等等。

    这种情况通常称为双写(dual-writes),因为应用没有在一个事务内写多个系统。这样不仅应用逻辑复杂难以维护,

    而且双写容易丢失数据或者在一些系统更新成功而另一些系统没有更新成功的时候造成不同系统之间的状态不一致。使用捕获更改数据技术(change data capture,CDC),

    在源数据库的数据更改提交后,这些额外的工作可以被放在独立的线程或者进程(服务)中完成。这种实现方式的容错性更好,不会丢失事件,容易扩展,并且更容易支持升级。

  • 共享数据库(Sharing databases)

    当多个应用共用同一个数据库的时候,一个应用提交的更改通常要被另一个应用感知到。一种实现方式是使用消息总线,

    尽管非事务性(non-transactional)的消息总线总会受上面提到的双写(dual-writes)影响。但是,另一种实现方式,即Debezium,变得很直接:每个应用可以直接监控数据库的更改,并且响应更改。

  • 数据集成(Data integration)

    数据通常被存储在多个地方,尤其是当数据被用于不同的目的的时候,会有不同的形式。保持多系统的同步是很有挑战性的,

    但是可以通过使用Debezium加上简单的事件处理逻辑来实现简单的ETL类型的解决方案。

  • 命令查询职责分离(CQRS)

    在命令查询职责分离 Command Query Responsibility Separation (CQRS) 架构模式中,更新数据使用了一种数据模型,

    读数据使用了一种或者多种数据模型。由于数据更改被记录在更新侧(update-side),这些更改将被处理以更新各种读展示。

    所以CQRS应用通常更复杂,尤其是他们需要保证可靠性和全序(totally-ordered)处理。Debezium和CDC可以使这种方式更可行:

    写操作被正常记录,但是Debezium捕获数据更改,并且持久化到全序流里,然后供那些需要异步更新只读视图的服务消费。

    写侧(write-side)表可以表示面向领域的实体(domain-oriented entities),或者当CQRS和 Event Sourcing 结合的时候,写侧表仅仅用做追加操作命令事件的日志。

springboot 整合 Debezium

依赖

<debezium.version>1.7.0.Final</debezium.version>
<mysql.connector.version>8.0.26</mysql.connector.version> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-api</artifactId>
<version>${debezium.version}</version>
</dependency>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-embedded</artifactId>
<version>${debezium.version}</version>
</dependency>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-connector-mysql</artifactId>
<version>${debezium.version}</version>
<exclusions>
<exclusion>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
</exclusions>
</dependency>

注意debezium版本为1.7.0.Final,对应mysql驱动为8.0.26,低于这个版本会报兼容错误。

配置

相应的配置

debezium.datasource.hostname = localhost
debezium.datasource.port = 3306
debezium.datasource.user = root
debezium.datasource.password = 123456
debezium.datasource.tableWhitelist = test.test
debezium.datasource.storageFile = E:/debezium/test/offsets/offset.dat
debezium.datasource.historyFile = E:/debezium/test/history/custom-file-db-history.dat
debezium.datasource.flushInterval = 10000
debezium.datasource.serverId = 1
debezium.datasource.serverName = name-1

然后进行配置初始化。

主要的配置项:

connector.class

  • 监控的数据库类型,这里选mysql。

offset.storage

  • 选择FileOffsetBackingStore时,意思把读取进度存到本地文件,因为我们不用kafka,当使用kafka时,选KafkaOffsetBackingStore

offset.storage.file.filename

  • 存放读取进度的本地文件地址。

offset.flush.interval.ms

  • 读取进度刷新保存频率,默认1分钟。如果不依赖kafka的话,应该就没有exactly once只读取一次语义,应该是至少读取一次。意味着可能重复读取。如果web容器挂了,最新的读取进度没有刷新到文件里,下次重启时,就会重复读取binlog。

table.whitelist

  • 监控的表名白名单,建议设置此值,只监控这些表的binlog。

database.whitelist

  • 监控的数据库白名单,如果选此值,会忽略table.whitelist,然后监控此db下所有表的binlog。
import io.debezium.connector.mysql.MySqlConnector;
import io.debezium.relational.history.FileDatabaseHistory;
import lombok.Data;
import org.apache.kafka.connect.storage.FileOffsetBackingStore;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import java.io.File;
import java.io.IOException; /**
* @className: MysqlConfig
* @author: nyp
* @description: TODO
* @date: 2023/8/7 13:53
* @version: 1.0
*/
@Configuration
@ConfigurationProperties(prefix ="debezium.datasource")
@Data
public class MysqlBinlogConfig { private String hostname;
private String port;
private String user;
private String password;
private String tableWhitelist;
private String storageFile;
private String historyFile;
private Long flushInterval;
private String serverId;
private String serverName; @Bean
public io.debezium.config.Configuration MysqlBinlogConfig () throws Exception {
checkFile();
io.debezium.config.Configuration configuration = io.debezium.config.Configuration.create()
.with("name", "mysql_connector")
.with("connector.class", MySqlConnector.class)
// .with("offset.storage", KafkaOffsetBackingStore.class)
.with("offset.storage", FileOffsetBackingStore.class)
.with("offset.storage.file.filename", storageFile)
.with("offset.flush.interval.ms", flushInterval)
.with("database.history", FileDatabaseHistory.class.getName())
.with("database.history.file.filename", historyFile)
.with("snapshot.mode", "Schema_only")
.with("database.server.id", serverId)
.with("database.server.name", serverName)
.with("database.hostname", hostname)
// .with("database.dbname", dbname)
.with("database.port", port)
.with("database.user", user)
.with("database.password", password)
// .with("database.whitelist", "test")
.with("table.whitelist", tableWhitelist)
.build();
return configuration; } private void checkFile() throws IOException {
String dir = storageFile.substring(0, storageFile.lastIndexOf("/"));
File dirFile = new File(dir);
if(!dirFile.exists()){
dirFile.mkdirs();
}
File file = new File(storageFile);
if(!file.exists()){
file.createNewFile();
}
}
}

snapshot.mode 快照模式,指定连接器启动时运行快照的条件。可能的设置有:

  • initial

    只有在没有为逻辑服务器名记录偏移量时,连接器才运行快照。

  • When_needed

    当连接器认为有必要时,它会在启动时运行快照。也就是说,当没有可用的偏移量时,或者当先前记录的偏移量指定了服务器中不可用的binlog位置或GTID时。

  • Never

    连接器从不使用快照。在第一次使用逻辑服务器名启动时,连接器从binlog的开头读取。谨慎配置此行为。只有当binlog保证包含数据库的整个历史记录时,它才有效。

  • Schema_only

    连接器运行模式而不是数据的快照。当您不需要主题包含数据的一致快照,而只需要主题包含自连接器启动以来的更改时,此设置非常有用。

  • Schema_only_recovery

    这是已经捕获更改的连接器的恢复设置。当您重新启动连接器时,此设置允许恢复损坏或丢失的数据库历史主题。您可以定期将其设置为“清理”意外增长的数据库历史主题。数据库历史主题需要无限保留。

database.server.id

  • 伪装成slave的Debezium服务的id,自定义,有多个Debezium服务不能重复,如果重复的话会报以下异常。
io.debezium.DebeziumException: A slave with the same server_uuid/server_id as this slave has connected to the master; the first event 'binlog.000013' at 46647257, the last event read from './binlog.000013' at 125, the last byte read from './binlog.000013' at 46647257. Error code: 1236; SQLSTATE: HY000.
at io.debezium.connector.mysql.MySqlStreamingChangeEventSource.wrap(MySqlStreamingChangeEventSource.java:1167)
at io.debezium.connector.mysql.MySqlStreamingChangeEventSource$ReaderThreadLifecycleListener.onCommunicationFailure(MySqlStreamingChangeEventSource.java:1212)
at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:980)
at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:599)
at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:857)
at java.lang.Thread.run(Thread.java:750)
Caused by: com.github.shyiko.mysql.binlog.network.ServerException: A slave with the same server_uuid/server_id as this slave has connected to the master; the first event 'binlog.000013' at 46647257, the last event read from './binlog.000013' at 125, the last byte read from './binlog.000013' at 46647257.
at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:944)
... 3 common frames omitted

监听

配置监听服务

import com.alibaba.fastjson.JSON;
import io.debezium.config.Configuration;
import io.debezium.data.Envelope;
import io.debezium.engine.ChangeEvent;
import io.debezium.engine.DebeziumEngine;
import io.debezium.engine.format.Json;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor; /**
* @projectName: legal-mediation
* @package: com.alpha.data.config
* @className: MysqlListener
* @author: nyp
* @description: TODO
* @date: 2023/8/7 13:56
* @version: 1.0
*/
@Component
@Slf4j
public class MysqlBinlogListener { @Resource
private Executor taskExecutor; private final List<DebeziumEngine<ChangeEvent<String, String>>> engineList = new ArrayList<>(); private MysqlBinlogListener (@Qualifier("mysqlConnector") Configuration configuration) {
this.engineList.add(DebeziumEngine.create(Json.class)
.using(configuration.asProperties())
.notifying(record -> receiveChangeEvent(record.value()))
.build());
} private void receiveChangeEvent(String value) {
if (Objects.nonNull(value)) {
Map<String, Object> payload = getPayload(value);
String op = JSON.parseObject(JSON.toJSONString(payload.get("op")), String.class);
if (!(StringUtils.isBlank(op) || Envelope.Operation.READ.equals(op))) {
ChangeData changeData = getChangeData(payload);
log.info("changeData = " + changeData);
}
}
} @PostConstruct
private void start() {
for (DebeziumEngine<ChangeEvent<String, String>> engine : engineList) {
taskExecutor.execute(engine);
}
} @PreDestroy
private void stop() {
for (DebeziumEngine<ChangeEvent<String, String>> engine : engineList) {
if (engine != null) {
try {
engine.close();
} catch (IOException e) {
log.error("", e);
}
}
}
} public static Map<String, Object> getPayload(String value) {
Map<String, Object> map = JSON.parseObject(value, Map.class);
Map<String, Object> payload = JSON.parseObject(JSON.toJSONString(map.get("payload")), Map.class);
return payload;
} public static ChangeData getChangeData(Map<String, Object> payload) {
Map<String, Object> source = JSON.parseObject(JSON.toJSONString(payload.get("source")), Map.class);
return ChangeData.builder()
.op(payload.get("op").toString())
.table(source.get("table").toString())
.after(JSON.parseObject(JSON.toJSONString(payload.get("after")), Map.class))
.source(JSON.parseObject(JSON.toJSONString(payload.get("source")), Map.class))
.before(JSON.parseObject(JSON.toJSONString(payload.get("before")), Map.class))
.build();
} @Data
@Builder
public static class ChangeData {
/**
* 更改前数据
*/
private Map<String, Object> after;
private Map<String, Object> source;
/**
* 更改后数据
*/
private Map<String, Object> before;
/**
* 更改的表名
*/
private String table;
/**
* 操作类型, 枚举 Envelope.Operation
*/
private String op;
} }

将监听到的binlog日志封装为ChangeData对象,包括表名,更改前后的数据,

以及操作类型

READ("r"),
CREATE("c"),
UPDATE("u"),
DELETE("d"),
TRUNCATE("t");

测试

update操作输出

MysqlListener.ChangeData(after = {
name = Suzuki Mio2,
id = 1
}, source = {
file = binlog .000013,
connector = mysql,
pos = 42587833,
name = test - 1,
row = 0,
server_id = 1,
version = 1.7 .0.Final,
ts_ms = 1691458956000,
snapshot = false,
db = test
table = test
}, before = {
name = Suzuki Mio,
id = 1
}, table = test, op = u)
data = {
name = Suzuki Mio2,
id = 1
}

新增操作输出

MysqlListener.ChangeData(after = {
name = 王五,
id = 0
}, source = {
file = binlog .000013,
connector = mysql,
pos = 42588175,
name = test - 1,
row = 0,
server_id = 1,
version = 1.7 .0.Final,
ts_ms = 1691459066000,
snapshot = false,
db = test,
table = test
}, before = null, table = test, op = c)

删除操作输出

MysqlListener.ChangeData(after = null, source = {
file = binlog .000013,
connector = mysql,
pos = 42588959,
name = test - 1,
row = 0,
server_id = 1,
version = 1.7 .0.Final,
ts_ms = 1691459104000,
snapshot = false,
db = test
table = test
}, before = {
name = 王五,
id = 0
}, table = test, op = d)

我们之前配置的保存读取进度的文件storageFile,类似于kafka的偏移量,记录的内容如下:

停止服务,对数据库进行操作,再次重启,会根据进度重新读取。

小结

本文介绍了debezium,更多的时候,我们一谈到CDC,第一想到的是大量数据同步的工具。

但其实也可以利用其数据变更捕获的特性,来达到一部份消息队列的作用。

但其毕竟不能完全替代消息队列。大家理性看待与选择。

本文的重点在介绍一种思路,具体的某项技术反而不那么重要。

参考:

https://debezium.io/documentation/reference/stable/connectors/mysql.html

https://debezium.io/documentation/reference/1.5/development/engine.html

https://github.com/alibaba/canal

https://github.com/debezium/debezium/blob/main/README_ZH.md

不想引入mq?试试debezium的更多相关文章

  1. 想了解MQ,读这篇就够了

    一.简介 MQ全称为Message Queue-消息队列,是一种应用程序对应用程序的消息通信,一端只管往队列不断发布信息,另一端只管往队列中读取消息,发布者不需要关心读取消息的谁,读取消息者不需要关心 ...

  2. AMQP协议与RabbitMQ、MQ消息队列的应用场景

    什么是AMQP? 在异步通讯中,消息不会立刻到达接收方,而是被存放到一个容器中,当满足一定的条件之后,消息会被容器发送给接收方,这个容器即消息队列,而完成这个功能需要双方和容器以及其中的各个组件遵守统 ...

  3. RabbitMQ引入

    引入MQ话题 可能很多人有疑惑:MQ到底是什么?哪些场景下要使用MQ? 前段时间安装了RabbitMQ,现在就记录下自己的学习心得吧.首先看段程序: class Program { static vo ...

  4. MQ相关面试题

    如果你的简历中有写到MQ,那么面试官一般会问到如下几个问题,至少我在面试中经常常被问到,所以今天总结一下,有不对的地方还望多多包涵: 首先第一个问题,为什么要用MQ? 如果这个问题你都没考虑过,那么说 ...

  5. MQ的消息丢失/重复/积压的问题解决

    在我们实际的开发过程中,我们肯定会用到MQ中间件,常见的MQ中间件有kafka,RabbitMQ,RocketMQ.在使用的过程中,我们必须要考虑这样一个问题,在使用MQ的时候,我们怎么确保消息100 ...

  6. HTML引入外部文件,解决统一管理导航栏问题。

    1.IFrame引入,看看下面的代码     <IFRAME NAME="content_frame" width=100% height=30 marginwidth=0 ...

  7. Nodejs Express下引入本地文件的方法

    Express的结构如下: |---node_modules------用于安装本地模块.     |---public------------用于存放用户可以下载到的文件,比如图片.脚本文件.样式表 ...

  8. 关于MQ,你必须知道的

    我走过最长的路是你的套路 女:二号男嘉宾,假如我们牵手成功后,你会买名牌包包给我吗? 男:那你会听话吗? 女:会 听话. 男:听话 咱不买! OK那么消息队列MQ有什么套路呢?(这个话题转换生硬度连我 ...

  9. 【MQ】消息队列及常见MQ比较

    一.什么是消息队列 我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用.消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰. ...

  10. vue项目全局引入vue-awesome-swiper插件做出轮播效果

    在安装了vue的前提下,打开命令行窗口,输入vue init webpack swiper-test,创建一个vue项目且名为swiper-test(创建速度可能会有点慢,耐心等),博文讲完后,源码托 ...

随机推荐

  1. Yolov5 根据自己的需要更改 预测框box和蒙版mask的颜色

    1.首先找到项目中 utils/plots.py 文件,打开该代码 将原来的 Colors类注释掉(或删掉),改成如下Colors类 class Colors: def __init__(self): ...

  2. 2021-07-13:恢复二叉搜索树。给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出

    2021-07-13:恢复二叉搜索树.给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换.请在不改变其结构的情况下,恢复这棵树.进阶:使用 O(n) 空间复杂度的解法很容易实现.你能想出 ...

  3. Selenium - 元素操作(5) - iframe切换

    Selenium - 元素操作 iframe切换 很多时候定位元素时候总是提示元素定位不到的问题,明明元素就在那里,这个时候就要关注你所 定位的元素是否在frame和iframe里面: frame标签 ...

  4. pycharm eslint 关闭

    pycharm 关闭eslint 文件->设置->语言和框架->JavaScript->代码质量工具->ESLint

  5. java通用xls导出设计

    背景 在后端日常开发中总会有各种各样的导出需求,实现这个需求必须要解决的两个问题: 1.表头不能直接使用字段名,需要显示为中文,甚至还需要考虑国际化 2.值需要翻译,比如性别.状态之类的字段 现状 现 ...

  6. L3-017 森森快递

    一.题目: 7-2 森森快递 (30 分) 森森开了一家快递公司,叫森森快递.因为公司刚刚开张,所以业务路线很简单,可以认为是一条直线上的N个城市,这些城市从左到右依次从0到(N−1)编号.由于道路限 ...

  7. Python基础 - 解释性语言和编译性语言

    什么是机器语言 计算机是不能理解高级语言,当然也就不能直接执行高级语言了.计算机只能直接理解机器语言,所以任何语言,都必须将其翻译成机器语言,计算机才能运行高级语言编写的程序.   如何把我们写的代码 ...

  8. ODOO之四Odoo 13 开发之模块继承

    Odoo 的一个强大功能是无需直接修改底层对象就可以添加功能.这是通过其继承机制来实现的,采取在已有对象之上修改层来完成.这种修改可以在不同层上进行-模型层.视图层和业务逻辑层.我们创建新的模块来做出 ...

  9. Java的运算符和表达式(基础语法学习)

    一.运算符 ​ 在Java中用于程序计算的操作符统称为运算符,运算符分为如下几类 1.算数运算符 运算符 说明 + 加号两边是数值,可以运算,如果一边存在字符串,则当作连接符 a+b - 两个数相加, ...

  10. JS异步解决方案及优缺点

    1. 回调函数 优点: 解决了同步的问题(只要有一个任务耗时长后面的任务都会等待,会拖延程序执行) 缺点: 回调地狱  不能用try  catch捕获  不能用 return setTimeout(( ...