一、背景

云端使用Spring Cloud实现,A服务有一些数据,B和C服务也需要A服务的这些数据,但是系统上面只有A服务有数据操作的入口,B和C服务只能从A服务处同步数据到自己的表里面。

解决方案是A服务对数据进行增删改操作之后,将数据操作发送给消息服务,B和C服务从消息服务拉取数据同步消息,然后修改自己的数据。

本文以kafka作为消息服务组件。

二、需求和目标

开发一个组件,A服务引用此组件,配置kafka连接和topic信息,注入生产者,即可向指定的topic发送数据同步消息。

B和C服务引用此组件,配置kafka连接和topic信息,编写核心的数据同步业务代码,即可实现同步数据的消费和入库。

有以下几点要求:

  • 对使用者透明,使用者不需要了解具体的消息服务组件,只需要使用数据同步组件的API即可实现数据的发送和消费;

  • 扩展方便,如果云端的消息服务不使用kafka而是其余的比如rabbitmq或rocketmq之类的消息服务,使用者不需要修改代码,组件库添加支持rabbitmq或rocketmq的生产者和消费者监听,使用者修改一下配置即可。

三、组件概述

将具体的消息组件(如kafka、rabbitmq或rocketmq等)封装在底层,提供给数据同步的生产者和消费者相对友好、与底层消息组件无关的API和接口。

在生产者这边,在业务层中注入生产者即可使用producer来发送需要同步的数据消息。

在消费者这边,只要编写类实现业务消费者接口,并且将其放入spring中管理即可消费到同步数据消息做业务处理。

四、组件开发 - 基础

1. 消息实体类

生产者接口的实现类需要将数据同步消息封装成DataSyncMessage的对象,序列化之后进行发送。

消费者监听会将消息反序列化为DataSyncMessage对象,从里面获取到操作类型和数据类型,以及同步的真实数据,然后调用具体的业务消费者实现的相应的方法来做业务处理。

 1 /**
2 * 数据同步消息封装
3 */
4 @Data
5 public class DataSyncMessage {
6
7 private Integer operationType;
8
9 private Integer dataType;
10
11 private Object data;
12 }

2. 操作类型枚举

OperationType枚举封装的操作类型,增、删、改等。

 1 /**
2 * 操作类型枚举
3 */
4 public enum OperationType {
5
6 /**
7 * 新增
8 */
9 INSERT(1, "insert"),
10
11 /**
12 * 修改
13 */
14 UPDATE(2, "update"),
15
16 /**
17 * 删除
18 */
19 DELETE(3, "delete");
20
21 private Integer value;
22 private String key;
23
24 OperationType(Integer value, String key) {
25 this.value = value;
26 this.key = key;
27 }
28
29 public String getKey() {
30 return key;
31 }
32
33 public Integer getValue() {
34 return value;
35 }
36
37 public static OperationType getEnum(Integer value) {
38 return Stream.of(OperationType.values())
39 .filter(e -> e.getValue().equals(value))
40 .findFirst()
41 .orElse(null);
42 }
43 }

3. 同步数据类型枚举

 1 /**
2 * 数据类型枚举
3 */
4 public enum DataType {
5
6 /**
7 * 用户
8 */
9 USER(1, "USER"),
10
11 /**
12 * 订单
13 */
14 ORDER(2, "ORDER");
15
16 private Integer value;
17 private String key;
18
19 DataType(Integer value, String key) {
20 this.value = value;
21 this.key = key;
22 }
23
24 public String getKey() {
25 return key;
26 }
27
28 public Integer getValue() {
29 return value;
30 }
31
32 public static DataType getEnum(Integer value) {
33 return Stream.of(DataType.values())
34 .filter(e -> e.getValue().equals(value))
35 .findFirst()
36 .orElse(null);
37 }
38 }

五、组件开发 - 生产者

1. 生产者接口

提供给数据同步的生产者使用,对于生产者来说,具体的底层消息组件是透明的。

 1 /**
2 * 数据同步生产者接口
3 */
4 public interface DataSyncProducer {
5
6 /**
7 * 发送同步消息
8 *
9 * @param operationType 操作类型,增删改等
10 * @param dataType 数据类型,用户、订单等
11 * @param data 同步的数据
12 */
13 void sendSyncData(OperationType operationType, DataType dataType, Object data);
14 }

组件当前内置了kafka的生产者实现和自动注入配置类,如果将来的技术选型是rocketmq或者其他消息中间件,编写一个相对应的生产者实现类和配置类即可使用,数据同步的生产者不需要修改业务代码。

2. 生产者Kafka实现

内置的kafka生产者实现,应该根据不同的数据同步消息中间件选型编写不同的生产者实现类和配置类。

 1 /**
2 * 数据同步生产者 - kafka实现
3 */
4 @Slf4j
5 public class KafkaDataSyncProducer implements DataSyncProducer {
6
7 private final KafkaTemplate<String, String> kafkaTemplate;
8
9 private final String topic;
10
11 public KafkaDataSyncProducer(KafkaTemplate<String, String> kafkaTemplate, String topic) {
12 this.kafkaTemplate = kafkaTemplate;
13 this.topic = topic;
14 }
15
16 @Override
17 public void sendSyncData(OperationType operationType, DataType dataType, Object data) {
18
19 DataSyncMessage message = new DataSyncMessage();
20 message.setOperationType(operationType.getValue());
21 message.setDataType(dataType.getValue());
22 message.setData(data);
23
24 String jsonMessage = JSON.toJSONString(message);
25
26 kafkaTemplate.send(topic, jsonMessage);
27 }
28 }

3. 生产者kafka配置类

kafka数据同步生产者配置类,只有在data-sync.producer.data-sync-type配置的值为kafka时才自动装配kafka生产者。

 1 @Configuration
2 @ConditionalOnProperty(prefix = "data-sync.producer", name = "data-sync-type", havingValue = "kafka")
3 public class KafkaDataSyncConfig {
4
5 @Value("${data-sync.producer.data-sync-topic}")
6 private String dataSyncTopic;
7
8 @Resource
9 private KafkaTemplate<String, String> kafkaTemplate;
10
11 @Bean
12 public DataSyncProducer dataSyncProducer() {
13 return new KafkaDataSyncProducer(kafkaTemplate, dataSyncTopic);
14 }
15 }

@ConditionalOnProperty注解表示,当data-sync.producer.data-sync-type配置的值为kafka时才会装配该configuration类。

六、组件开发 - 消费者

1. 业务层需实现的接口

数据同步消费者需要编写DataSyncConsumer接口的实现类并将其注入到spring中,另外实现类需要使用@DataSyncListener标注,并指定数据类型。

 1 /**
2 * 数据同步监听器接口
3 */
4 public interface DataSyncConsumer<T> {
5
6 /**
7 * 当发生插入操作时触发
8 */
9 void onInsert(T data);
10
11 /**
12 * 当发生修改操作时触发
13 */
14 void onUpdate(T data);
15
16 /**
17 * 当发生删除操作时触发
18 */
19 void onDelete(T data);
20 }

2. 消费扫描处理器

DataSyncListenerAnnotationBeanPostProcessor处理器会扫描到容器中被@DataSyncListener标注、实现了DataSyncConsumer接口的所有bean对象,从@DataSyncListener注解获取数据类型,然后将此bean对象交给DataSyncConsumerHolder管理。

DataSyncConsumerHolder管理着所有的数据同步消费者,内部使用Map结构,key是同步消费者所处理的数据类型,value是消费者对象本身。

BeanPostProcessor接口的实现会在bean对象被初始化之后调用,此处不做过多解释。

 1 @Slf4j
2 @Component
3 public class DataSyncListenerAnnotationBeanPostProcessor implements BeanPostProcessor {
4
5 @Override
6 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
7
8 DataSyncListener annotation = bean.getClass().getAnnotation(DataSyncListener.class);
9
10 if (annotation != null) {
11
12 Class<?>[] interfaces = bean.getClass().getInterfaces();
13
14 boolean isDataSyncConsumer = false;
15
16 for (Class<?> i : interfaces) {
17 if (i == DataSyncConsumer.class) {
18 isDataSyncConsumer = true;
19 break;
20 }
21 }
22
23 if (isDataSyncConsumer) {
24
25 DataSyncConsumer dataSyncConsumer = (DataSyncConsumer) bean;
26
27 DataType dataType = annotation.dataType();
28
29 Class clazz = annotation.dataClass();
30
31 DataSyncDataTypeAndConsumer typeAndConsumer = new DataSyncDataTypeAndConsumer();
32 typeAndConsumer.setDataClass(clazz);
33 typeAndConsumer.setConsumer(dataSyncConsumer);
34
35 DataSyncConsumerHolder.add(dataType, typeAndConsumer);
36
37 DataSyncConsumerHolder.print();
38 }
39 }
40
41 return bean;
42 }
43 }

@DataSyncListener注解:

 1 /**
2 * 标注在数据同步实现类上
3 */
4 @Retention(RetentionPolicy.RUNTIME)
5 @Target({ElementType.TYPE})
6 @Documented
7 public @interface DataSyncListener {
8
9 DataType dataType();
10
11 Class dataClass();
12 }

3. Kafka消费者监听

kafka数据同步消费者监听器,只有在data-sync.consumer.data-sync-type配置的值为kafka时才自动装配kafka监听。

 1 @Slf4j
2 @Service
3 @ConditionalOnProperty(prefix = "data-sync.consumer", name = "data-sync-type", havingValue = "kafka")
4 public class KafkaDataSyncService {
5
6 @KafkaListener(topics = {"${data-sync.consumer.data-sync-topic}"})
7 public void handleDataSyncMessage(ConsumerRecord<?, ?> record) {
8 Optional<?> kafkaMessage = Optional.ofNullable(record.value());
9 if (!kafkaMessage.isPresent()) {
10 return;
11 }
12 String message = kafkaMessage.get().toString();
13
14 log.info("topic:{},message:{}", record.topic(), message);
15
16 // 业务处理
17 dataSync(message);
18 }
19
20 private void dataSync(String message) {
21
22 try {
23
24 DataSyncMessage dataSyncMessage = JSON.parseObject(message, DataSyncMessage.class);
25
26 Integer dataType = dataSyncMessage.getDataType();
27 OperationType operationType = OperationType.getEnum(dataSyncMessage.getOperationType());
28
29 DataType type = DataType.getEnum(dataType);
30
31 // debug
32 DataSyncConsumerHolder.print();
33
34 DataSyncDataTypeAndConsumer consumer = DataSyncConsumerHolder.get(type);
35
36 if (consumer == null) {
37 log.warn("Consumer not found for data type: {}", type);
38 return;
39 }
40
41 Class clazz = consumer.getDataClass();
42
43 DataSyncConsumer realConsumer = consumer.getConsumer();
44
45 Object data = dataSyncMessage.getData();
46
47 // debug
48 log.info("data = " + data);
49
50 Object o;
51
52 if (clazz == Map.class) {
53 o = data;
54 } else {
55 o = clazz.newInstance();
56 BeanUtils.populate(o, (Map<String, ? extends Object>) data);
57 }
58
59 switch (operationType) {
60 case INSERT:
61 realConsumer.onInsert(o);
62 break;
63 case DELETE:
64 realConsumer.onDelete(o);
65 break;
66 case UPDATE:
67 realConsumer.onUpdate(o);
68 break;
69 default:
70 log.warn("Error operation: " + operationType);
71 }
72
73 } catch (Exception e) {
74 log.error(e.getLocalizedMessage(), e);
75 log.error("An exception occurred during data synchronization: ", message);
76 }
77 }
78 }

七、生产者配置及使用

1. 配置

1 data-sync:
2 producer:
3 data-sync-topic: data-sync-topic
4 data-sync-type: kafka

2. 注入生产者

@Resource
private DataSyncProducer dataSyncProducer;

3. 发送消息

1 User user = new User();
2 user.setUsername("admin");
3
4 dataSyncProducer.sendSyncData(OperationType.INSERT, DataType.USER, user);

八、消费者配置及使用

1. 配置

1 data-sync:
2 consumer:
3 data-sync-topic: data-sync-topic
4 data-sync-type: kafka

2. 实现业务层消费者接口

实现该接口的onInsert、onUpdate、onDelete等方法,如下:

 1 @DataSyncListener(dataType = DataType.USER, dataClass = User.class)
2 @Service("UserDataSyncConsumer")
3 public class UserDataSyncConsumer implements DataSyncConsumer<User> {
4
5 @Override
6 public void onInsert(User data) {}
7
8 @Override
9 public void onUpdate(User data) {}
10
11 @Override
12 public void onDelete(User data) {}
13 }

接口泛型是同步数据的数据类型,如果写Map<String, Object>,组件不会对同步数据做解析和封装,直接将原始Map传递给接口方法。

@DataSyncListener注解的dataType参数指定同步数据的数据类型,具体见上面的DataType枚举介绍。dataClass 参数作用同接口泛型。

@Service注解尽量传一个唯一的bean名称。

另一个实现类的例子:

 1 @DataSyncListener(dataType = DataType.USER, dataClass = Map.class)
2 @Service("UserDataSyncConsumer")
3 public class UserDataSyncConsumer implements DataSyncConsumer<Map<String, Object>> {
4
5 @Override
6 public void onInsert(Map<String, Object> data) {}
7
8 @Override
9 public void onUpdate(Map<String, Object> data) {}
10
11 @Override
12 public void onDelete(Map<String, Object> data) {}
13 }

九、扩展说明

1. 扩展同步数据类型

为DataType枚举添加新的类型即可。

暂时这样扩展,后续可以优化为更加友好的扩展方式。

2. 扩展支持新的消息服务

比如使用rabbitmq作为消息服务:

  • 编写支持rabbitmq的DataSyncProducer实现类;
  • 编写rabbitmq生产者配置类,data-sync.producer.data-sync-type配置的值为rabbitmq时才自动装配rabbitmq生产者;
  • 编写支持rabbitmq的消息监听器,data-sync.consumer.data-sync-type配置的值为rabbitmq时才自动装配监听器;
  • 数据同步生产者和消费者引入组件库,做相应的配置。

十、 参考源码

https://gitee.com/xuguofeng2020/net5ijy-mall/tree/master/component-datasync

SpringCloud对使用者透明的数据同步组件的更多相关文章

  1. 测试 ClownFish、CYQ、Entity Framework、Moon、MySoft、NHibernate、PDF、XCode数据访问组件性能

    下期预告: 由于很多园友反馈,有的组件不应该缺席.测试复杂度不够.测试还缺乏一定的公平. 因此考虑在下一个版本中,确保在更加公平的前提下进行更高复杂度的测试 . 同时将分为2组测试,纯SQL组件及纯O ...

  2. 开源数据同步神器——canal

    前言 如今大型的IT系统中,都会使用分布式的方式,同时会有非常多的中间件,如redis.消息队列.大数据存储等,但是实际核心的数据存储依然是存储在数据库,作为使用最广泛的数据库,如何将mysql的数据 ...

  3. 【NIFI】 实现数据库到数据库之间数据同步

    本里需要基础知识:[NIFI] Apache NiFI 安装及简单的使用 数据同步 界面如下: 具体流程: 1.使用ExecuteSQL连接mysql数据库,通过写sql查询所需要的数据 2.nifi ...

  4. TiDB 部署及数据同步

    简介 TiDB 是 PingCAP 公司受 Google Spanner / F1 论文启发而设计的开源分布式 HTAP (Hybrid Transactional and Analytical Pr ...

  5. 微服务之数据同步Porter

    Porter是一款数据同步中间件,主要用于解决同构/异构数据库之间的表级别数据同步问题. 背景 在微服务架构模式下深刻的影响了应用和数据库之间的关系,不像传统多个服务共享一个数据库,微服务架构下每个服 ...

  6. WinCE数据通讯之SqlCE数据同步篇

    上一篇总结了WinCE通过WebService进行数据通讯的交互方式,今天整理个SqlCE数据同步方式的内容.先说下软件环境:终端平台使用WinCE5.0+SqlCE2.0,服务器使用Windows ...

  7. 基于ActiveMQ的Topic的数据同步——初步实现

    一.背景介绍 公司自成立以来,一直以做项目为主,算是经累经验吧,自去年以来,我们部门准备将以前的项目做成产品,大概细分了几个小的产品,部们下面又分了几个团队,分别负责产品的研发,而我们属于平台团队,负 ...

  8. 增量数据同步中间件DataLink分享(已开源)

    项目介绍 名称: DataLink['deitə liŋk]译意: 数据链路,数据(自动)传输器语言: 纯java开发(JDK1.8+)定位: 满足各种异构数据源之间的实时增量同步,一个分布式.可扩展 ...

  9. vue2.X props 数据传递 实现组件内数据与组件外的数据的双向绑定

    vue2.0 禁止 子组件修改父组件数据 在Vue2中组件的props的数据流动改为了只能单向流动,即只能由组件外(调用组件方)通过组件的DOM属性attribute传递props给组件内,组件内只能 ...

  10. SQLite与MySQL、SQLServer等异构数据库之间的数据同步

    SQLite DBSync是开源嵌入式数据库SQLite的数据同步引擎,实现了SQLite与SQLite数据库之间以及SQLite与异构数据库(Oracle.MySQL.SQLServer)之间的增量 ...

随机推荐

  1. 【笔记整理】request模块基本使用

    基本使用 发送get请求.获取响应各种请求.响应信息 def fun1(): url = "http://www.baidu.com" resp = requests.get(ur ...

  2. 从零玩转设计模式之外观模式-waiguanmos

    title: 从零玩转设计模式之外观模式 date: 2022-12-12 15:49:05.322 updated: 2022-12-23 15:34:40.394 url: https://www ...

  3. 屎山代码风格指南(避免被优化&&避免被接盘)

    欢迎补充!!! 序言 良好的代码结构:Bad 使用有意义的变量和函数名,遵循命名规范,使代码易于理解. 组织代码,使用适当的文件和文件夹结构,保持模块化. 避免全局变量的滥用,尽量使用局部作用域. 单 ...

  4. Liquid 常用语法记录

    一.什么是 Liquid Liquid 是一款专为特定需求而打造的模板引擎. Liquid 中有两种类型的标记:Output 和 Tag. Output 通常用来显示文本 {{ 两个花括号 }} Ta ...

  5. 分布式机器学习的故事:Docker改变世界

    分布式机器学习的故事:Docker改变世界 Docker最近很火.Docker实现了"集装箱"--一种介于"软件包"和"虚拟机"之间的概念- ...

  6. UE5: UpdateOverlap - 从源码深入探究UE的重叠触发

    前言 出于工作需要和个人好奇,本文对UE重叠事件更新的主要函数UpdateOverlaps从源码的角度进行了详细的分析,通过阅读源码,深入理解重叠事件是如何被触发和更新的. 解决问题 阅读本文,你将得 ...

  7. 我开源了一个 Go 学习仓库

    目录 前言 一.综述 1.1 Hello Word 1.2 命令行参数 1.3 查找重复行 1.4 GIF 动画 1.5 获取一个URL 1.6 并发获取多个URL 1.7 实现一个 Web 服务器 ...

  8. HDU 4641 K string 后缀自动机

    原题链接 题意 每个测试点,一开始给我们n,m,k然后是一个长度为n的字符串. 之后m次操作,1 c是往字符串后面添加一个字符c,2是查询字符串中出现k次以及以上的子串个数,m为2e5 思路 首先可以 ...

  9. Serverless 架构就不要服务器了?

    摘要:Serverless 架构不是不要服务器了,而是依托第三方云服务平台,服务端逻辑运行在无状态的计算容器中,其业务层面的状态则被开发者使用的数据库和存储资源所记录. Serverless 是什么 ...

  10. 对象存储只能按文件名搜索,你out了吧

    摘要:不少大公司的一个桶里都是几亿几十亿的对象,那他们都是怎么检索的呢? 本文分享自华为云社区<对象存储只能按文件名搜索? 用 DWR + ElasticSearch 实现文件名.文件内容.图片 ...