「JavaArt」をフォローして、一緒に成長しましょう!
サブスクリプションBinlog
の目的は、リアルタイムのキャッシュ更新、複雑なロジックデータの処理Elasticsearch
と他のデータベーステーブルおよび他のビジネスシナリオとのリアルタイム同期を実現することです。
この記事の内容は次のとおりです。
アプリケーション層で監視
SQL
を実装する方法予備知識:
Mysql
2段階のコミットとBinlog
予備知識:について
Kafka
アリババクラウドデータ送信サービス
DTS
-データサブスクリプション公式
DEMO
消費モデル:生産->消費モデル公式に
DEMO
提供されMetaStore
、Checkpoint
機能DEMO
注意を払うために公式の場所を使用してくださいavro
シリアル化と逆シリアル化について
アプリケーション層でSQLを監視する方法
著者は以前に、Mybatis
プラグインとSQL
解析ツールを使用してアプリケーションのSQL
データを監視および更新することについて記述し、この機能を個人のオープンソースプロジェクトに統合し(easymulti-datasource-spring-boot-starter
)、トランザクションの監視をサポートし、リアルタイムの消費をサポートしていますSQL
。トランザクションリスナーのコールバックインターフェイスメソッドを登録し、トランザクションがコミットされたときにリスナーの消費を開始しますSQL
。
easymulti-datasource-spring-boot-starter
アプリケーションレベルの簡単なSQL
サブスクリプションでリアルタイムに使用することもできますが、この方法ではSQL
、コンシューマーのリッスンは非同期ですが、傍受SQL
、分析SQL
、それ自体もパフォーマンスが少し低下するという欠点もあります。また、同じテーブルを変更するアプリケーションが複数ある場合は、各アプリケーションで消費コードを記述する必要があります。
より低いレベルで直接サブスクライブできる場合Mysql
、Binlog
効率はアプリケーション層で達成される効率よりもはるかに高くなります。
予備知識:Mysqlトランザクションに関する2フェーズコミットとBinlog
Binlog
データベースによって実行された書き込み操作情報を記録するために使用され、バイナリ形式でディスクに保存されます。
Binlog
はいMysql
、論理ログServer
はレイヤーによって記録されます。使用されているストレージエンジンに関係なく、Mysql
データベースはBinlog
ログを記録します。
Mysql
トランザクションがコミットされたときにのみ記録され、Biglog
トランザクションがコミットBiglog
されたときにのみメモリに記録され、構成されたフラッシュ戦略によってファイルに書き込まれます。
Mysql
sync_binlog
パラメータによって制御されるBiglog
ディスクのフラッシュのタイミング。値の範囲は0-N
次のとおりです。
0
:システムは、ディスクにいつ書き込むかを自分で決定します。1
:毎回ディスクcommit
にBinlog
書き込まれます;N
:各N
トランザクションcommit
はBinlog
ディスクに書き込まれることになっていた。
sync_binlog
最も安全な設定は1
、これがMySQL 5.7.7
後のバージョンのデフォルト値でもあることは間違いありません。
通常Mysql
、前述のトランザクションの2フェーズコミットは、InnoDB
ストレージエンジンに関連しています。
Mysql
トランザクションは2つの段階でコミットされます。最初の段階はストレージエンジンによって事前に書き込まれます。たとえば、InnoDB
ストレージエンジンが書き込む場合、Redolog
この段階Binlog
では操作は実行されません。2番目の段階は最初に書き込むことBinlog
であり、次にストレージエンジンはcommit
日記への書き込みなどのトランザクションコミット作業を完了します。 、ロックを解除するなど。
2番目のフェーズでの書き込みがBinlog
成功MySQL
すると、トランザクションがコミットされて永続化されたと見なされるため、このステップでBinlog
サブスクライバーに送信できます。書き込み後Binlog
、ストレージエンジンがコミットされたトランザクションを完了しておらず、この時点でデータベースがクラッシュした場合でも、再起動後もBinlog
トランザクションを正しく復元できます。Binlog
操作が失敗する前に書き込みステップが完了すると、トランザクションがロールバックされます。
したがって、直接サブスクリプションの場合Binlog
、トランザクションが最終的にコミットされるかロールバックされるかを気にする必要はありません。トランザクションがコミットされる前に、トランザクションで実行されるSQL
日記をサブスクライブすることはできません。
詳細を知りたい場合は、次の記事を読むことをお勧めします:「MySQL
・原則の紹介・再考されたMySQL
障害回復」http://mysql.taobao.org/monthly/2018/12/04/
予備知識:カフカについて
データストレージの問題
Kafka
クラスターは、消費されたかどうかに関係なく、公開されたすべてのレコードを保持します。メッセージの保持期間は、保持期間パラメーターを構成することで制御できます。保持ポリシーが2
日数に設定されている場合、レコードはリリースされてから2日以内であればいつでも消費できます。2日後、レコードは破棄され、ディスク領域が解放されます。
オフセット消費オフセット
オフセットは消費者によって制御されます。消費者commit
が新しいオフセットを記録した後、オフセットkafka
は消費者のために保存され、その後の消費を容易にします。オフセットkafka
はgroup + topic + partition
保存されます。もちろん、自分で保存することもできます。オフセットを自分で保存する際に注意が必要な問題については、後で説明します。
起因kafka
するgroup + topic + partition
オフセット記憶、これはまた別の問題に対応する:「同じグループで、topic
それぞれのそれぞれがpartition
唯一消費する一つの消費者を有することができるが、1つの消費者は、同時に複数の消費することができますpartition
」。
offset
消費者によって制御されるため、消費者は任意の順序でレコードを消費できます。つまりtopic
、消費者の消費者は古いオフセットにリセットして、過去のデータを再処理したり、スキップしたりできます。最新のレコードは、現在の場所から消費を開始します。
消費者
KafkaConsumer
インスタンスは、必ずしも消費者に等しくありません。
ではsubscribe
モデル、1つのKafkaConsumer
インスタンスは、1つのコンシューマに等しいです。パーティションが1つだけで、複数が開いていると仮定するとKafkaConsumer
、アイドル状態のコンシューマーが存在します。つまり、このスレッドがKafkaConsumer
インスタンスのpoll
メソッドを呼び出すたびに、常に空が返され、現在消費している永続的なKafkaConsumer
接続が切断されるまでメッセージはプルされません。リバランス後、アイドル状態の消費者はレコードをプルできるようになります。
これは、この文も確認します。Kafka
消費を実現する方法は、ログ内のパーティションを各コンシューマーインスタンスに分割することです。これにより、いつでも、各コンシューマーが特定のパーティションの唯一のコンシューマーになります。
subscribe
モデルKafkaConsumer
、(一つの接続Socket
)は、一人の消費者に等しくなく、1 。
ただし、このassign
モードでは、複数のKafkaConsumer
サブスクリプションが指定されtopic
てパーティション化されている場合(および同じグループ内)、これらのKafkaConsumer
プルレコードはすべて同じパーティションになります。これは単なる例です。このように使用しないでください。使用すると、レコードが繰り返し消費され、2つのスレッドがコミット(commit
)とオフセット(offset
)をクロスするときに問題が発生します。
消費者グループ
通常の状況では、それぞれtopic
にいくつかのコンシューマーグループがあり、コンシューマーグループは論理サブスクライバーです。
例えば:
topic:用户注册
group 1:短信推送服务订阅者
group 2:邮件推送服务订阅者
group1
合計group2
は論理サブスクライバーですが、各論理サブスクライバーは複数のコンシューマーを持つことができます。
同じグループ内の消費者の数はtopic
、そのpartition
数を超えてはpartition
なりませんpartition
。これは、その数を超える消費者は割り当てられない、つまりアイドル状態になるためです(「消費者」の説明を参照)。
消費者グループ内の消費者関係の維持は、Kafka
契約によって動的に処理されます。新しい消費者がグループに参加すると、新しく追加された消費者は、グループの他のメンバーから一部のpartition
パーティションを引き継ぎます。消費者が消えると、消費者は所有します。パーティションは、他の残りのコンシューマーに再割り当てされます。
もう1つのポイントは、同じグループで、topic
各地区で現在消費している消費者がいる場合、新しく追加された消費者が消費している消費者を置き換え、置き換えられた消費者が消費する地区を引き継ぐことです。
アリババクラウドデータ送信サービスDTS-データサブスクリプション
アリクラウドデータ伝送サービスのDTS
サポートMySQL
とリアルタイムのサブスクリプション。DRDS
Binlog
公式SDK
サブスクリプションを使用する必要はありませんが、サブスクリプションを実装Binlog
するためにKafka
クライアントKafka
を使用するだけで済みます。API
Binlog
公式ドキュメント:Kafkaクライアントを使用してサブスクリプションデータを使用するhttps://help.aliyun.com/document_detail/121239.html?spm=a2c4g.11186623.6.785.6d4d6d2aIOqQQm
公式に提供されたものDEMO
:[subscribe_example] https://github.com/LioRoger/subscribe_example
、これは上司DEMO
によって龙玄
提供されるべきです。
ホイールを繰り返すのではなく、公式のDEMO
[subscribe_example / javaimpl]Mysql Binlog
に基づいてリアルタイムサブスクリプションサービス(トライアルフェーズ)を構築することを選択しました。ただし、メッセージの逆シリアル化MetaStore
とCheckpoint
機能を維持したまま、ソースコードにいくつかの変更を加えました。どこMetaStore
でCheckpoint
、DEMO
最も勉強する価値があります。
公式デモの消費モデル:生産->消費モデル
DEMO
開かれるコンシューマーは1つだけです。このコンシューマーは、メッセージのサブスクライブを担当し、サブスクライブされたメッセージをブロッキングキュー(LinkedBlockingQueue
)に入れます。このブロッキングキューのデフォルトサイズはに設定されてい512
ます。
また、ブロックキューからメッセージを読み、呼び出し、実際にメッセージを消費することスレッドを開始し、後に消費する方法を 、メッセージを消費し、ラップメッセージを(チェックポイントに)、および最新のチェックにチェックポイントを設定しますポイントに加えて、送信された最新のチェックポイントが送信秒ごとに時間指定されたタスクがあります。RecordListener
consume
RecordListener
offset
Checkpoint
5
offset
kafka
消費者は毎回メッセージのバッチをプルすることができ、これらのメッセージはリリース順にソートされます。そのためtopic
、パーティションが一つだけのコンシューマによって消費することができ、パーティション内のメッセージは、それらが公開された順序でソートされています。
ではDEMO
、コンシューマーはサブスクライブされたメッセージを順番にブロックキューに入れ、キューがいっぱいになるとブロックして待機するため、ブロックキュー内のメッセージが順番に消費されて送信されることを確認するだけで済みますoffset
。
ブロッキングキュー内のメッセージが順不同で消費された場合はどうなりますか?
複数のスレッドがプルされたメッセージを並行して順序どおりに消費しないと仮定すると、それらoffset
が正しく送信されたことを確認することは不可能であり、その結果、一部のメッセージが繰り返し消費される可能性があります。
各メッセージを正しく消費する必要があるという厳密な要件がなく、例外なく、マルチスレッド消費を使用してメッセージ消費の速度を上げることができます。
たとえば、ブロッキングキュー内のメッセージを消費するスレッドは、ブロッキングキューからのメッセージの取得と解析のみを担当します。キャッシュの更新などの他のアクションは、実行のために非同期スレッドプールに配置されます。非同期スレッドプールに正常に配置されている限り、update Checkpoint
(offset
)、続行します。消費者ニュース。
公式デモが提供するメタストアとチェックポイントの機能
Checkpoint
これは、グループ内のtopic
特定のパーティションの現在の実際の消費位置を記録するために使用されます(オフセット:) offset
。
/**
* 安全检查点(即:记录消费位置)
*/
public class Checkpoint {
// 分区信息
private final TopicPartition topicPartition;
private final long timeStamp;
private final long offset;
public Checkpoint(TopicPartition topicPartition, long timeStamp, long offset) {
this.topicPartition = topicPartition;
this.timeStamp = timeStamp;
this.offset = offset;
}
}
MetaStore
ストレージCheckpoint
、または送信オフセットに使用されます。
public interface MetaStore<V> {
Future<V> serializeTo(TopicPartition topicPartition, String group, V value);
V deserializeFrom(TopicPartition topicPartition, String group);
}
DEMO
2つの実装クラスが提供されています:KafkaMetaStore
、LocalFileMetaStore
。何されてLocalFileMetaStore
達成することは、消費パーティションのオフセット、および保存するために、ローカルファイルを使用することですメソッドKafkaMetaStore
と呼ばれるがKafkaConsumer
しているcommitAsync
ことを、非同期的にオフセットされ手段提出kafka
オフセットが格納されているが。
このsubscribe
モードでは、を使用しないでくださいLocalFileMetaStore
。
コンシューマーがクラスターにデプロイされると、ノードの再起動後kafka
のリバランスにより、ノードによって消費されるパーティションが再起動前のパーティションと異なる場合があるため、ローカルファイルストレージの消費オフセットが使用されず、再起動が発生します(構成済み)消費場所の初期化)消費記録を開始します。
また、1つのコンシューマーサービスのみをデプロイする場合、または複数のコンシューマーがプロセス中または使用assign
モードにある場合は、それを使用できますがLocalFileMetaStore
、サービスを再起動するたびにオフセットファイルがあることを確認する必要があります。サーバーデプロイメントを切り替える場合は、オフセットファイルを新しいサーバーに同期する必要があります。
不要な手間を省くため、直接捨てLocalFileMetaStore
て使用していKafkaMetaStore
ます。
public class KafkaMetaStore implements MetaStore<Checkpoint> {
private volatile KafkaConsumer kafkaConsumer;
//.....
// 异步提交offset
@Override
public Future<Checkpoint> serializeTo(TopicPartition topicPartition, String group, Checkpoint value) {
KafkaFutureImpl ret = new KafkaFutureImpl();
if (null != kafkaConsumer) {
OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(value.getOffset(), String.valueOf(value.getTimeStamp()));
// 异步提交(不能同步提交,否则影响RecordGenerator#run())
// Notice: commitAsync is only put commit offset request to sending queue, the future result will be driven by KafkaConsumer.poll() function
// So if you only call this method but not poll, you may not wait offset commit call back
kafkaConsumer.commitAsync(Collections.singletonMap(topicPartition, offsetAndMetadata), new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (null != exception) {
log.warn("KafkaMetaStore: Commit offset for group[" + group + "] topicPartition[" + topicPartition.toString() + "] " +
value.toString() + " failed cause " + exception.getMessage(), exception);
ret.completeExceptionally(exception);
} else {
log.debug("KafkaMetaStore:Commit offset success for group[{}] topicPartition [{}] {}", group, topicPartition, value);
ret.complete(value);
}
}
});
} else {
log.warn("KafkaMetaStore: kafka consumer not set, ignore report");
ret.complete(value);
}
return ret;
}
// 从kafka获取当前分区的offset和时间戳
@Override
public Checkpoint deserializeFrom(TopicPartition topicPartition, String group) {
if (null != kafkaConsumer) {
OffsetAndMetadata offsetAndMetadata = kafkaConsumer.committed(topicPartition);
if (null != offsetAndMetadata) {
return new Checkpoint(topicPartition, Long.valueOf(offsetAndMetadata.metadata()), offsetAndMetadata.offset(), offsetAndMetadata.metadata());
} else {
return null;
}
} else {
log.warn("KafkaMetaStore: kafka consumer not set, ignore fetch offset");
throw new KafkaException("KafkaMetaStore: kafka consumer not set, ignore fetch offset for group[" + group + "] and tp [" + topicPartition + "]");
}
}
}
公式デモを使用する際の注意点
topic
特定のパーティションが一度も消費されていない場合は、コンシューマーを初めて起動するときに、最初の消費場所を構成する必要があります。タイムスタンプを使用するかoffset
、消費する場所を見つけることができます。
パーティションが消費されている場合、コンシューマーが開始/再起動すると、最後の消費の場所が最初に取得され、次に最後の消費の場所から消費が開始されます。ただし、offset
各5
送信のタイミングは1秒に1回であるためoffset
、実際の消費量のオフセットを表すものではなく、記録を再開するたびに再消費量が発生します。これは、独自の電力消費量やその他のメッセージを確認する必要があります。
DEMO
デフォルトLocalFileMetaStore
で公式に使用され、置換MetaStore
はRecordGenerator#getConsumerWrap
メソッドを変更するだけで済みます。コードは次のとおりです。
public class RecordGenerator{
private ConsumerWrap getConsumerWrap(String message) {
// KafkaConsumer包装器
ConsumerWrap kafkaConsumerWrap = getConsumerWrap();
// 不建议使用LocalFileMetaStore存储(特别是部署到k8s上),否则将消费者部署到其它服务器后,需要将localCheckpointStore文件也要同步过去才可以
// metaStoreCenter.registerStore(LOCAL_FILE_STORE_NAME, new LocalFileMetaStore(LOCAL_FILE_STORE_NAME));
// 使用KafkaMetaStore
metaStoreCenter.registerStore(KAFKA_STORE_NAME, new KafkaMetaStore(kafkaConsumerWrap.getRawConsumer()));
// 从检查点存储器获取检查点(由于是每5秒提交一次,所以每次重起都会有小部分记录被重新消费)
Checkpoint checkpoint = getCheckpoint();
// 没有找到检查点,则使用配置的初始化检查点
if (null == checkpoint || Checkpoint.INVALID_STREAM_CHECKPOINT == checkpoint) {
checkpoint = initialCheckpoint; // 在配置文件中配置
log.info("RecordGenerator: use initial checkpoint [{}] to start", checkpoint);
} else {
log.info("RecordGenerator: load checkpoint from checkpoint store success, current checkpoint [{}]", checkpoint);
}
//.......
}
}
最後に、Alibaba Cloud Data Transfer Service DTS
-Data Subscriptionは日記を1つのパーティションにのみ送信するため、つまりtopic
、パーティションは1つだけです。これは、各sql
日記を正しい順序で使用できるようにするためです。したがって、subscribe
パターンを使用する必要はなく、パターンを使用する必要がありますassign
。また、クラスターを展開する必要もありません。これは、公式のDEMOでも推奨されています。
DefaultConsumerWrap
カプセル化されKafkaConsumer
、使用assign
パターンassignTopic
はこのクラスのメソッドで表現され、コードは次のとおりです。
public class DefaultConsumerWrap extends ConsumerWrap {
private KafkaConsumer<byte[], byte[]> consumer;
@Override
public void assignTopic(TopicPartition topicPartition, Checkpoint checkpoint) {
// KafkaConsumer
consumer.assign(Collections.singletonList(topicPartition));
log.info("RecordGenerator: assigned for {} with checkpoint {}", topicPartition, checkpoint);
// 设置消费位置
setFetchOffsetByTimestamp(topicPartition, checkpoint);
}
}
その中で、assignTopic
メソッドの2番目のパラメーター(Checkpoint
)MetaStore
は、構成の初期位置から取得されるか、構成の初期位置です。KafkaConsumer#assign
メソッドを呼び出した後、メソッドを呼び出してsetFetchOffsetByTimestamp
消費場所を設定すると、KafkaConsumer#poll
メソッドを呼び出してメッセージをプルできます。
setFetchOffsetByTimestamp
メソッドは次のように実装されています。DEMO
ソースコードと比較して、いくつかの変更を加えました。
public class DefaultConsumerWrap extends ConsumerWrap {
@Override
public void setFetchOffsetByOffset(TopicPartition topicPartition, Checkpoint checkpoint) {
// 移动到指定位置继续消费
consumer.seek(topicPartition, checkpoint.getOffset());
}
// recommended
@Override
public void setFetchOffsetByTimestamp(TopicPartition topicPartition, Checkpoint checkpoint) {
// 优先使用偏移量
if (checkpoint.getOffset() > 0) {
setFetchOffsetByOffset(topicPartition, checkpoint);
return;
}
long timeStamp = checkpoint.getTimeStamp();
// 根据时间戳获取偏移量
Map<TopicPartition, OffsetAndTimestamp> remoteOffset = consumer.offsetsForTimes(Collections.singletonMap(topicPartition, timeStamp));
OffsetAndTimestamp toSet = remoteOffset.get(topicPartition);
if (null == toSet) {
throw new RuntimeException("RecordGenerator:seek timestamp for topic [" + topicPartition + "] with timestamp [" + timeStamp + "] failed");
}
// 移动到指定位置继续消费
consumer.seek(topicPartition, toSet.offset());
}
}
avroのシリアル化と逆シリアル化について
公式demo
、com.alibaba.dts.formats.avro
これがpackage
ある次のように来て、コンパイル、我々はあなた自身の特定の実装をコンパイルすることができますは、次のとおりです。avro
shcema
1.コマンドを実行してavsc
ファイルをコンパイルし、java
コードを生成します
java -jar avro/avro-tools-1.8.2.jar compile -string schema avro/Record.avsc .
2、現在のプロジェクトルートディレクトリの生成されたcom.alibaba.dts.formats.avro
このpackage
コピーは、もちろん、メインモジュールに組み込まれたモジュールにカプセル化することもできます。
[Javaアート] WeChat ID:javaskill
オリジナルの記事のみをプッシュし、Javaバックエンド関連のテクノロジーを共有するテクニカルパブリックアカウント。