小項目不想引入 MQ?試試 Debezium!

来源:https://www.cnblogs.com/javastack/p/18055863
-Advertisement-
Play Games

作者:是奉壹呀 鏈接:https://juejin.cn/post/7264791359839223823 奧卡姆剃刀原理,“如無必要,勿增實體"。 在一些小型項目當中,沒有引入消息中間件,也不想引入,但有一些業務邏輯想要解耦非同步,那怎麼辦呢? 我們的web項目,單獨內網部署,由於大數據背景,公司消 ...


作者:是奉壹呀
鏈接:https://juejin.cn/post/7264791359839223823

奧卡姆剃刀原理,“如無必要,勿增實體"。

在一些小型項目當中,沒有引入消息中間件,也不想引入,但有一些業務邏輯想要解耦非同步,那怎麼辦呢?

我們的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

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

依賴

<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: test
 * @package: com.test.config
 * @className: MysqlBinlogListener
 * @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);
               // 這裡拋出異常會導致後面的日誌監聽失敗
                try {
                    mysqlBinlogService.service(changeData);
                }catch (Exception e){
                    log.error("binlog處理異常,原數據: " + changeData, e);
                }

            }
        }
    }

    @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,第一想到的是大量數據同步的工具。 但其實也可以利用其數據變更捕獲的特性,來達到一部份消息隊列的作用。

但其畢竟不能完全替代消息隊列。大家理性看待與選擇。

本文的重點在介紹一種思路,具體的某項技術反而不那麼重要。

更多文章推薦:

1.Spring Boot 3.x 教程,太全了!

2.2,000+ 道 Java面試題及答案整理(2024最新版)

3.免費獲取 IDEA 激活碼的 7 種方式(2024最新版)

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 依賴管理有gradle和maven,在這裡選擇比較常用和方便的Maven作為工程項目和依賴管理工具來搭建SpringCloud實戰工程。主要用到的maven管理方式是多模塊和bom依賴管理。 ...
  • pandas中的cut函數可將一維數據按照給定的區間進行分組,併為每個值分配對應的標簽。其主要功能是將連續的數值數據轉化為離散的分組數據,方便進行分析和統計。 1. 數據準備 下麵的示例中使用的數據採集自王者榮耀比賽的統計數據。數據下載地址:https://databook.top/。 導入數據: ...
  • 題目描述 如果一個國家滿足下述兩個條件之一,則認為該國是 大國 : 面積至少為 300 萬平方公裡 人口至少為 2500 萬 編寫解決方案找出大國的國家名稱、人口和麵積 按任意順序返回結果表,如下例所示 測試用例 輸入: name continent area population gdp Afgh ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹如何運用`QThread`組件實現多線程功能。 ...
  • 前言 在電商、外賣、預約服務等場景中,訂單超時自動取消是一個常見的業務需求。這一功能不僅提高了系統的自動化程度,還為用戶提供了更好的體驗。需求如下: TODO 如果用戶在生成訂單後一定時間未支付,則系統自動取消訂單。 接下來就用 SpringBoot 實現訂單超時未支付自動取消的幾種方案,並提供相應 ...
  • 在如今的商業環境中,企業信息的準確性和可信度是非常重要的。尤其是在與其他公司進行合作或者與銀行等金融機構進行業務往來時,對企業的背景和信用度有著更高的要求。那麼如何有效地驗證企業的信息是否真實可信呢?挖數據平臺的獲取企業裁判文書介面 - GetJudicialDocuments將成為你的得力助手。 ...
  • 在Java編程中,Integer類作為基本類型int的包裝器,提供了對象化的操作和自動裝箱與拆箱的功能。從JDK5開始引入了一項特別的優化措施——Integer緩存機制,它對於提升程式性能和減少記憶體消耗具有重要意義。接下來我們由一段代碼去打開Integer緩存機制的秘密。 public static ...
  • 本文介紹在Visual Studio 2022中配置、編譯C++電腦視覺庫OpenCV的方法。 1 OpenCV庫配置 首先,我們進行OpenCV庫的下載與安裝。作為一個開源的庫,我們直接在其官方下載網站中進行下載即可;如下圖所示,我們首先選擇需要下載的操作系統。 隨後,即可在彈出的新界面中自動開 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...