Spring Boot + Elastic Job 定时比对数据使用钉钉报警

最近的工作中心是容易扩容,刚刚把其它一个业务使用 sharding jdbc 把容量规划完成。因为系统采用的数据方案是:单写老库 -> 双写老库分片库 -> 单写分片库,使用 apollo 配置中心来切换数据写入规则。当有异常时可以进行方案回滚这种平滑的数据过渡方案,在上线过程中不可避免的会有中心状态,双写老库和分片库

1、数据对比的方案变迁

在系统处于中心状态的过程中,老库数据和分片库数据的一致性尤为重要。所以需要对老库和分片库的数据进行对比。数据对比方案经历过以下几个阶段:

  • 系统设计的最初时候,我的方案是人工定时从老库和分片库的从库和下载某时间段的数据,然后通过程序对比(当前没有考虑到可以使用 sharding-proxy)
  • 项目总监提出没有必要通过数据对比,只需要通过工具 Beyong Compare,定时从老库和分片库下载时间段数据通过 Beyong Compare 进行数据对比
  • 上一个阶段里面有一个关键词,那就是 定时。所以就对上面一个方案进行了优化,通过使用定时任务框架 Elastic Job,定时从老库和分片库(Sharding-proxy) 拉取数据进行比对,然后把对比结果写入系统指定目录中。通过查看目录的日志判断中间状态是否有问题
  • 上一个阶段里面还是需要人工通过跳板机登陆到服务器上查看日志比对结果,这种方案还是比较麻烦。其实我们日常使用的办公软件是钉钉,可以通过钉钉的群机器人来进行业务报警。当我们通过 Elastic Job 中定时比对好结果的时候,通过可通过钉钉提供的 Webhook 来发放数据对比信息到业务群。

系统方案一步一步进行优化,最终通过定时任务加钉钉群报警方式来检测在系统处于中间状态时,老库和新库的数据是否一致。

2、创建钉钉群机器人

2.1 钉钉群机器人

群机器人是钉钉为用户提供的智能群助手,帮助群里沟通协同更加高效。在【群设置】 - 【智能群助手】可以找到。目前群里支持最多添加6个机器人。

可以使用钉钉官方机器人小钉,也可以添加Github等机器人,还可以添加企业机器人或者自定义机器人。机器人是高效安全的,您请放心使用。

群成员可以在【手机钉钉】-【群聊】-【群设置】-【智能群助手】-【添加更多【选择机器人添加】。

2.2 获取自定义机器人webhook

步骤一,打开机器人管理页面。以PC端为例,打开PC端钉钉,点击头像,选择“机器人管理”。

Spring Boot + Elastic Job 定时比对数据使用钉钉报警_第1张图片
步骤二,在机器人管理页面选择“自定义”机器人,输入机器人名字并选择要发送消息的群,同时可以为机器人设置机器人头像。

Spring Boot + Elastic Job 定时比对数据使用钉钉报警_第2张图片
步骤三,完成必要的安全设置(至少选择一种),勾选 我已阅读并同意《自定义机器人服务及免责条款》,点击“完成”

Spring Boot + Elastic Job 定时比对数据使用钉钉报警_第3张图片

步骤四,完成安全设置后,复制出机器人的Webhook地址,可用于向这个群发送消息,格式如下:

https://oapi.dingtalk.com/robot/send?access_token=XXXXXX

更多信息请查看 钉钉开发文档。

步骤五,在 Linux 服务器上进行测试。

curl 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "compare 测试数据,可忽略"}}'

Spring Boot + Elastic Job 定时比对数据使用钉钉报警_第4张图片
钉钉群收到该信息,说明地址和数据格式没有问题。

3、数据对比服务开发

数据对比服务开发,主要的工作就是开发一个 Elastic Job Simple 类型的定时任务,并配置完成。包含任务注册中心 zookeeper 以及 job 定义的配置。定时任务的逻辑主要是以下几个步骤:

  • 查询老库数据,如果数据为空直接返回
  • 查询分片库数据,并把分片库数据转换为以业务主键为 id 为 key,业务对象为 value 的 Map。
  • 遍历查询出老库的数据,通过老库的业务主键查询分片库查询结果转换的 Map。如果查询结果为空,就把老库的结果添加到 StringBuilder 当中,并追加分片库数据为空。如果查询结果不为空,对比两个对象的值,如果不相等就把新库的对象信息与新库的对象信息添加到 StringBuilder 当中。在业务比对的开始阶段添加一个标记位,判断是否全部数据是否一致。当分片库数据为空或者老库数据与分片库数据不一致时,这个标记位设置为 false。当标记位为 true,StringBuilder 追加新库与老库数据完全一致。
  • 发送 StringBuilder 信息到钉钉

发送钉钉信息的类:

3.1 DingMessageRequest

发送钉钉消息请求类。

DingMessageRequest

@Getter
@Setter
public class DingMessageRequest {

	private String msgtype;

	private MessageText text;

}

3.2 MessageText

具体消息包装类

MessageText

@Getter
@Setter
public class MessageText {

	private String content;

}

3.3 RestTemplateConfig

http 发送模板配置类,使用的是 okhttp。

@Configuration
public class RestTemplateConfig {

    @Bean("restTemplate")
    public RestTemplate restTemplate(){
        return new RestTemplate(requestFactory());
    }

    private OkHttp3ClientHttpRequestFactory requestFactory(){
        return new OkHttp3ClientHttpRequestFactory(okHttpClient());
    }

    private OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory(), x509TrustManager())
                .retryOnConnectionFailure(false)
                .connectionPool(connectionPoll())
                .connectTimeout(10L, TimeUnit.SECONDS)
                .readTimeout(10L, TimeUnit.SECONDS)
                .build();
    }

    private X509TrustManager x509TrustManager() {
        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
            }
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    }

    private SSLSocketFactory sslSocketFactory() {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        return null;
    }

    private ConnectionPool connectionPoll() {
        return new ConnectionPool(200, 5, TimeUnit.MINUTES);
    }

}

3.4 钉钉消息响应类

当发送消息到 webhook 地址时,如果响应的 errcode 为 0 表示消息发送成功

DingMessageResponse

@Getter
@Setter
public class DingMessageResponse {

	private int errcode;
	private String errmsg;

}

3.5 钉钉消息发送类

这个类主要是发送消息到群 webhook,并且处理响应结果

@Slf4j
@Service("dingDingMessageProcessor")
public class DingDingMessageProcessor implements MessageProcessor {

	@Resource
	private RestTemplate restTemplate;

	@Value("${sharding.dingding.group}")
	private String shardingDingDingGroup;

	@Override
	public void process(String fileType, String message) {
		log.info("DingDingServiceImpl#sendMessage message is {}", message);
		DingMessageRequest request = new DingMessageRequest();
		request.setMsgtype("text");
		MessageText text = new MessageText();
		text.setContent(message);
		request.setText(text);
		DingMessageResponse response = restTemplate.postForObject(shardingDingDingGroup, request, DingMessageResponse.class);
		if(response == null) {
			log.error("DingDingServiceImpl#sendMessage send message response is null");
		}
		if(response.getErrcode() == 0){
			log.info("DingDingServiceImpl#sendMessage send success");
		} else {
			log.error("DingDingServiceImpl#sendMessage send fail {}", response.getErrmsg());
		}
	}

}

钉钉对比结果显示:

  • 正常结果

Spring Boot + Elastic Job 定时比对数据使用钉钉报警_第5张图片

  • 异常结果

Spring Boot + Elastic Job 定时比对数据使用钉钉报警_第6张图片

参考文章:

  • 钉钉帮助中心
  • 钉钉开发文档

你可能感兴趣的:(Job,Architecture)