十八子君 發表於 2023-12-7 00:00:00

实时监视同步数据库变更,这个框架真是神器

<p>
        <img title="实时监视同步数据库变更,这个框架真是神器" alt="实时监视同步数据库变更,这个框架真是神器" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/a596f172f39d1c7b3c330adf64299bd6.jpg" width="auto"></p>
<p>
        我们数据库中的数据一直在变化,有时候我们希望能监听数据库数据的变化并根据变化做出一些反应,比如更新对应变化数据的缓存、增量同步到其它数据源、对数据进行检测和审计等等。而这种技术就叫变更数据捕获(Change Data Capture)。对于这种技术我们可能知道一个国内比较知名的框架Canal,非常好用!但是Canal有一个局限性就是只能用于Mysql的变更数据捕获。今天来介绍另一种更加强大的分布式CDC框架Debezium。</p>
<h3>
        Debezium</h3>
<p>
         </p>
<p>
        提起Debezium这个框架,相信大多数普通开发者都比较陌生,但是提及它所属的公司大家一定不会陌生。</p>
<p>
        <img title="实时监视同步数据库变更,这个框架真是神器" alt="实时监视同步数据库变更,这个框架真是神器" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/7f0a6713c5578c5290b4c5efb3624812.jpg" width="auto"></p>
<p>
        红帽公司</p>
<p>
        没错就是开源界最成功的红帽公司。Debezium是为捕获数据更改的流式处理框架,开源免费。Debezium近乎实时地监控数据库行级别(row-level)的数据变更,并针对变更可以做出反应。而且只有已提交的变更才是可见的,所以不用担心事务问题或者更改被回滚的问题。Debezium为所有的数据库更改事件提供了一个统一的模型,所以不用担心每种数据库系统的复杂性。Debezium提供了对MongoDB、MySQL、PostgreSQL、SQL Server、Oracle、DB2等数据库的支持。</p>
<p>
        另外借助于Kafka Connector可以开发出一个基于事件流的变更捕获平台,具有高容错率和极强的扩展性。</p>
<p>
        <img title="实时监视同步数据库变更,这个框架真是神器" alt="实时监视同步数据库变更,这个框架真是神器" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/96e85e8ff58680ea6651e8170f945d8d.jpg" width="auto"></p>
<p>
        Debezium Kafka 架构</p>
<p>
        如图所示,部署了用于 MySQL 和 PostgresSQL 的 Debezium Kafka连接器以捕获对这两种类型数据库的更改事件,然后将这些更改通过下游的Kafka Connector将记录传输到其他系统或者数据库(例如 Elasticsearch、数据仓库、分析系统)或缓存。</p>
<p>
        另一种玩法就是将Debezium内置到应用程序中,来做一个类似消息总线的设施,将数据变更事件传递给订阅的下游系统中。</p>
<p>
        <img title="实时监视同步数据库变更,这个框架真是神器" alt="实时监视同步数据库变更,这个框架真是神器" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/d46369f619685c72687fcd21054bf22c.jpg" width="auto"></p>
<p>
        Debezium内置服务器架构</p>
<p>
        Debezium对数据的完整性和可用性也是做了不少的工作。Debezium用持久化的、有副本备份的日志来记录数据库数据变化的历史,因此,你的应用可以随时停止再重启,而不会错过它停止运行时发生的事件,保证了所有的事件都能被正确地、完全地处理掉。</p>
<p>
        稍后我会演示一个Spring Boot集成Debezium的数据捕获系统。</p>
<h3>
        Spring Boot集成Debezium</h3>
<p>
         </p>
<p>
        理论介绍并不能让你直观感受到Debezium的能力,所以接下来我将使用嵌入式Debezium引擎来演示一下。</p>
<p>
        <img title="实时监视同步数据库变更,这个框架真是神器" alt="实时监视同步数据库变更,这个框架真是神器" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/cf1fd2dcbf942898bbc80bb2a9ff00bd.jpg" width="auto"></p>
<p>
        流程图</p>
<p>
        如上图所示,当我们变更MySQL数据库中的某行数据时,通过Debezium实时监听到binlog日志的变化触发捕获变更事件,然后获取到变更事件模型,并做出响应(消费)。接下来我们来搭建环境。</p>
<h3>
        MySQL开启binlog日志</h3>
<p>
         </p>
<p>
        为了方便这里使用MySQL的Docker容器,对应的脚本为:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span># 运行mysql容器  </span></span>
</li>
        <li>
                <span>docker run <span class="comment">--name mysql-service -v d:/mysql/data:/var/lib/mysql -p 3306:3306 -e TZ=Asia/Shanghai -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-time_zone="+8:00"</span><span> </span></span>
</li>
        <li class="alt">
                <span># 设置binlog位置 </span>
</li>
        <li>
                <span>docker <span class="keyword">exec</span><span> mysql-service bash -c </span><span class="string">"echo 'log-bin=/var/lib/mysql/mysql-bin' &gt;&gt; /etc/mysql/mysql.conf.d/mysqld.cnf"</span><span> </span></span>
</li>
        <li class="alt">
                <span># 配置 mysql的server-id </span>
</li>
        <li>
                <span>docker <span class="keyword">exec</span><span> mysql-service bash -c </span><span class="string">"echo 'server-id=123454' &gt;&gt; /etc/mysql/mysql.conf.d/mysqld.cnf"</span><span> </span></span>
</li>
</ol>
<p>
        上面的脚本运行了一个用户名为root、密码为123456并且将数据挂载到本地路径d:/mysql/data的MySQL容器,同时开启了binlog日志,并设置server-id为123454,这些信息后面配置会用。</p>
<p>
        请注意如果不使用root用户的话,需要保证用户具有SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT四种权限。</p>
<h3>
        Spring Boot集成嵌入式Debezium</h3>
<p>
         </p>
<p>
        <strong>Debezium依赖</strong></p>
<p>
        Spring Boot的应用中加入下列依赖:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>&lt;dependency&gt; </span></span>
</li>
        <li>
                <span>     &lt;groupId&gt;io.debezium&lt;/groupId&gt; </span>
</li>
        <li class="alt">
                <span>     &lt;artifactId&gt;debezium-api&lt;/artifactId&gt; </span>
</li>
        <li>
                <span>     &lt;version&gt;${debezium.version}&lt;/version&gt; </span>
</li>
        <li class="alt">
                <span> &lt;/dependency&gt; </span>
</li>
        <li>
                <span> &lt;dependency&gt; </span>
</li>
        <li class="alt">
                <span>     &lt;groupId&gt;io.debezium&lt;/groupId&gt; </span>
</li>
        <li>
                <span>     &lt;artifactId&gt;debezium-embedded&lt;/artifactId&gt; </span>
</li>
        <li class="alt">
                <span>     &lt;version&gt;${debezium.version}&lt;/version&gt; </span>
</li>
        <li>
                <span> &lt;/dependency&gt; </span>
</li>
        <li class="alt">
                <span> &lt;dependency&gt; </span>
</li>
        <li>
                <span>     &lt;groupId&gt;io.debezium&lt;/groupId&gt; </span>
</li>
        <li class="alt">
                <span>     &lt;artifactId&gt;debezium-connector-mysql&lt;/artifactId&gt; </span>
</li>
        <li>
                <span>     &lt;version&gt;${debezium.version}&lt;/version&gt; </span>
</li>
        <li class="alt">
                <span> &lt;/dependency&gt; </span>
</li>
</ol>
<p>
        目前最新的版本号为1.5.2.Final。</p>
<p>
        <strong>声明配置</strong></p>
<p>
        然后声明需要的配置:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>/** </span></span>
</li>
        <li>
                <span>     * Debezium 配置. </span>
</li>
        <li class="alt">
                <span>     * </span>
</li>
        <li>
                <span>     * @<span class="keyword">return</span><span> configuration </span></span>
</li>
        <li class="alt">
                <span>     */ </span>
</li>
        <li>
                <span>    @Bean </span>
</li>
        <li class="alt">
                <span>    io.debezium.config.Configuration debeziumConfig() { </span>
</li>
        <li>
                <span>        <span class="keyword">return</span><span> io.debezium.config.Configuration.</span><span class="keyword">create</span><span>() </span></span>
</li>
        <li class="alt">
                <span>//            连接器的Java类名称 </span>
</li>
        <li>
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"connector.class"</span><span>, MySqlConnector.class.getName()) </span></span>
</li>
        <li class="alt">
                <span>//            偏移量持久化,用来容错 默认值 </span>
</li>
        <li>
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"offset.storage"</span><span>, </span><span class="string">"org.apache.kafka.connect.storage.FileOffsetBackingStore"</span><span>) </span></span>
</li>
        <li class="alt">
                <span>//                偏移量持久化文件路径 默认/tmp/offsets.dat  如果路径配置不正确可能导致无法存储偏移量 可能会导致重复消费变更 </span>
</li>
        <li>
                <span>//                如果连接器重新启动,它将使用最后记录的偏移量来知道它应该恢复读取源信息中的哪个位置。 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"offset.storage.file.filename"</span><span>, </span><span class="string">"C:/Users/n1/IdeaProjects/spring-boot-debezium/tmp/offsets.dat"</span><span>) </span></span>
</li>
        <li>
                <span>//                捕获偏移量的周期 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"offset.flush.interval.ms"</span><span>, </span><span class="string">"6000"</span><span>) </span></span>
</li>
        <li>
                <span>//               连接器的唯一名称 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"name"</span><span>, </span><span class="string">"mysql-connector"</span><span>) </span></span>
</li>
        <li>
                <span>//                数据库的hostname </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.hostname"</span><span>, </span><span class="string">"localhost"</span><span>) </span></span>
</li>
        <li>
                <span>//                端口 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.port"</span><span>, </span><span class="string">"3306"</span><span>) </span></span>
</li>
        <li>
                <span>//                用户名 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.user"</span><span>, </span><span class="string">"root"</span><span>) </span></span>
</li>
        <li>
                <span>//                密码 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.password"</span><span>, </span><span class="string">"123456"</span><span>) </span></span>
</li>
        <li>
                <span>//                 包含的数据库列表 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.include.list"</span><span>, </span><span class="string">"etl"</span><span>) </span></span>
</li>
        <li>
                <span>//                是否包含数据库表结构层面的变更,建议使用默认值<span class="keyword">true</span><span> </span></span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"include.schema.changes"</span><span>, </span><span class="string">"false"</span><span>) </span></span>
</li>
        <li>
                <span>//                mysql.cnf 配置的 server-id </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.server.id"</span><span>, </span><span class="string">"123454"</span><span>) </span></span>
</li>
        <li>
                <span>//                 MySQL 服务器或集群的逻辑名称 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.server.name"</span><span>, </span><span class="string">"customer-mysql-db-server"</span><span>) </span></span>
</li>
        <li>
                <span>//                历史变更记录 </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.history"</span><span>, </span><span class="string">"io.debezium.relational.history.FileDatabaseHistory"</span><span>) </span></span>
</li>
        <li>
                <span>//                历史变更记录存储位置  </span>
</li>
        <li class="alt">
                <span>                .<span class="keyword">with</span><span>(</span><span class="string">"database.history.file.filename"</span><span>, </span><span class="string">"C:/Users/n1/IdeaProjects/spring-boot-debezium/tmp/dbhistory.dat"</span><span>) </span></span>
</li>
        <li>
                <span>                .build(); </span>
</li>
        <li class="alt">
                <span>    } </span>
</li>
</ol>
<p>
        配置分为两部分:</p>
<ul>
<li>
                一部分是Debezium Engine的配置属性,参见Debezium Engine配置。</li>
        <li>
                一部分是Mysql Connector的配置属性,参见Mysql Connector配置。</li>
</ul>
<h3>
        实例化Debezium Engine</h3>
<p>
         </p>
<p>
        应用程序需要为运行的Mysql Connector启动一个Debezium引擎,这个引擎会以异步线程的形式运行,它包装了整个Mysql Connector连接器的生命周期。声明一个引擎需要以下几步:</p>
<p>
        声明收到数据变更捕获信息的格式,提供了JSON、Avro、Protobuf、Connect、CloudEvents等格式。</p>
<p>
        加载上面定义的配置。</p>
<p>
        声明消费数据更改事件的函数方法。</p>
<p>
        声明的伪代码:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>DebeziumEngine&lt;RecordChangeEvent&lt;SourceRecord&gt;&gt; debeziumEngine = DebeziumEngine.</span><span class="keyword">create</span><span>(ChangeEventFormat.</span><span class="keyword">of</span><span>(</span><span class="keyword">Connect</span><span>.class)) </span></span>
</li>
        <li>
                <span>        .using(configuration.asProperties()) </span>
</li>
        <li class="alt">
                <span>        .notifying(this::handlePayload) </span>
</li>
        <li>
                <span>        .build(); </span>
</li>
</ol>
<p>
        handlePayload方法为:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>private void handlePayload(List&lt;RecordChangeEvent&lt;SourceRecord&gt;&gt; recordChangeEvents, DebeziumEngine.RecordCommitter&lt;RecordChangeEvent&lt;SourceRecord&gt;&gt; recordCommitter) { </span></span>
</li>
        <li>
                <span>    recordChangeEvents.forEach(r -&gt; { </span>
</li>
        <li class="alt">
                <span>        SourceRecord sourceRecord = r.record(); </span>
</li>
        <li>
                <span>        Struct sourceRecordChangeValue = (Struct) sourceRecord.value(); </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        if (sourceRecordChangeValue != <span class="op">null</span><span>) { </span></span>
</li>
        <li class="alt">
                <span>            // 判断操作的类型 过滤掉读 只处理增删改   这个其实可以在配置中设置 </span>
</li>
        <li>
                <span>            Envelope.Operation operation = Envelope.Operation.forCode((String) sourceRecordChangeValue.get(OPERATION)); </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>            if (operation != Envelope.Operation.<span class="keyword">READ</span><span>) { </span></span>
</li>
        <li class="alt">
                <span>                String record = operation == Envelope.Operation.<span class="keyword">DELETE</span><span> ? BEFORE : </span><span class="keyword">AFTER</span><span>; </span></span>
</li>
        <li>
                <span>                // 获取增删改对应的结构体数据 </span>
</li>
        <li class="alt">
                <span>                Struct struct = (Struct) sourceRecordChangeValue.get(record); </span>
</li>
        <li>
                <span>                // 将变更的行封装为Map </span>
</li>
        <li class="alt">
                <span>                Map&lt;String, Object&gt; payload = struct.<span class="keyword">schema</span><span>().fields().stream() </span></span>
</li>
        <li>
                <span>                        .map(Field::<span class="keyword">name</span><span>) </span></span>
</li>
        <li class="alt">
                <span>                        .filter(fieldName -&gt; struct.get(fieldName) != <span class="op">null</span><span>) </span></span>
</li>
        <li>
                <span>                        .map(fieldName -&gt; Pair.<span class="keyword">of</span><span>(fieldName, struct.get(fieldName))) </span></span>
</li>
        <li class="alt">
                <span>                        .collect(toMap(Pair::getKey, Pair::getValue)); </span>
</li>
        <li>
                <span>                // 这里简单打印一下 </span>
</li>
        <li class="alt">
                <span>                System.<span class="keyword">out</span><span>.println(</span><span class="string">"payload = "</span><span> + payload); </span></span>
</li>
        <li>
                <span>            } </span>
</li>
        <li class="alt">
                <span>        } </span>
</li>
        <li>
                <span>    }); </span>
</li>
        <li class="alt">
                <span>} </span>
</li>
</ol>
<p>
        引擎的启动和关闭正好契合Spring Bean的生命周期:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>@Data </span></span>
</li>
        <li>
                <span><span class="keyword">public</span><span> class DebeziumServerBootstrap implements InitializingBean, SmartLifecycle { </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>    private final Executor executor = Executors.newSingleThreadExecutor(); </span>
</li>
        <li class="alt">
                <span>    private DebeziumEngine&lt;?&gt; debeziumEngine; </span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>    @Override </span>
</li>
        <li>
                <span>    <span class="keyword">public</span><span> void start() { </span></span>
</li>
        <li class="alt">
                <span>        executor.<span class="keyword">execute</span><span>(debeziumEngine); </span></span>
</li>
        <li>
                <span>    } </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>    @SneakyThrows </span>
</li>
        <li class="alt">
                <span>    @Override </span>
</li>
        <li>
                <span>    <span class="keyword">public</span><span> void stop() { </span></span>
</li>
        <li class="alt">
                <span>        debeziumEngine.<span class="keyword">close</span><span>(); </span></span>
</li>
        <li>
                <span>    } </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>    @Override </span>
</li>
        <li class="alt">
                <span>    <span class="keyword">public</span><span> boolean isRunning() { </span></span>
</li>
        <li>
                <span>        <span class="keyword">return</span><span> </span><span class="keyword">false</span><span>; </span></span>
</li>
        <li class="alt">
                <span>    } </span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>    @Override </span>
</li>
        <li>
                <span>    <span class="keyword">public</span><span> void afterPropertiesSet() throws Exception { </span></span>
</li>
        <li class="alt">
                <span>        Assert.notNull(debeziumEngine, <span class="string">"debeziumEngine must not be null"</span><span>); </span></span>
</li>
        <li>
                <span>    } </span>
</li>
        <li class="alt">
                <span>} </span>
</li>
</ol>
<h3>
        启动</h3>
<p>
         </p>
<p>
        启动该Spring Boot项目,你可以采用各种手段往数据库增删改数据,观察会有类似下面的打印:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>payload = {user_id=1123213, username=felord.cn, age=11 , gender=0, enabled=1} </span></span>
</li>
</ol>
<p>
        说明Debezium监听到了数据库的变更。你可以想想这种技术在哪些场景有用武之地。好了今天的分享就到这里,感谢大家的支持,我是:码农小胖哥。原创不易,请多多关注、点赞、转发、再看。</p>
<p>
        参考资料</p>
<p>
        Debezium Engine配置: https://debezium.io/documentation/reference/1.5/development/engine.html#engine-properties</p>
<p>
        Mysql Connector配置: https://debezium.io/documentation/reference/1.5/connectors/mysql.html#mysql-connector-properties</p>
<p>
        原文链接:https://mp.weixin.qq.com/s/_67XXbPAawegCP08W8FHIQ</p>
頁: [1]
查看完整版本: 实时监视同步数据库变更,这个框架真是神器