使用Canal进行MySQL到MySQL数据库全量+增量同步以及踩坑指南

使用Canal进行MySQL到MySQL数据库全量+增量同步以及踩坑指南

背景

最近工作中遇到一个迁移数据库的需求,需要将数据库从A服务器迁移至B服务器,为了尽量减少迁移导致的停机时间,考虑使用全量迁移+增量同步的方式,最终选择使用Canal作为迁移工具

准备工作

1. 数据库

两台服务器的数据库都需要提前准备好数据库账号,用于Canal进行数据库连接,因为Canal是伪装成从库读取源数据的,所以需要对源库进行一些配置,比如账号需要授予从库权限(如果给的是最高权限账号,那基本不用担心这个)、保证Canal部署的程序与数据库网络互通、以及最重要的!开启ROW格式的Binlog,这个非常重要!

2.Canal安装

使用Canal进行全量和增量同步需要用到两个组件,Canal adapter、Canal deployer,可自行前往github仓库下载。

Canal配置启动增量同步和全量同步

1.deployer配置

将下载的两个组件解压,首先配置deployer

主要关注conf目录

进入example目录,编辑instance.properties文件:

重点关注这几个参数:canal.instance.master.addresscanal.instance.dbUsernamecanal.instance.dbPasswordcanal.instance.filter.regex(重要)

阅读更多

Spring Cache @Cacheable 缓存在部分Service中不生效的解决办法

1. 背景

最近开发的项目中,需要大量的使用到缓存以提升性能

其中,有个活动controller,需要查询所有的活动,代码如下:

1
2
3
4
5
@GetMapping("/list")
public RestResult<List<ActivityInfoDTO>> list() {
List<ActivityInfoDTO> list = activityService.getAllActivity();
return addRestResult(list);
}

对应的ActivityService方法如下:

1
2
3
4
5
6
@Override
@Cacheable(value = "ActivityInfoDTO", key = "'getAllActivity'")
public List<ActivityInfoDTO> getAllActivity() {
List<ActivityInfoDTO> allActivity = activityDefMapper.getAllActivity();
return allActivity;
}

结构十分简单,但是奇怪的是,ActivityService里的方法完全不会走缓存

而另一个service,BannerInfoService则可以正常缓存

调试时可以看到,正常的BannerInfoService是有被cglib代理的

而ActivityService是没有被代理的

2. 解决方法

漫长的搜索过后,没能在网上找到解决方案

最终将目光瞄向了ActivityService的其他引用,于是猜测是否是循环引用导致ActivityService没有被代理

结果发现,在另一个OpusService中,有引用该ActivityService

同时OpusService也引用了MessagePushService,而这个MessagePushService又引用了OpusService

于是选择将OpusService中的ActivityService改为懒加载,加上@Lazy注解:

发现成功触发了缓存机制,调试信息也可以看到被cglib代理

3. 总结

之前遇到的缓存不生效大多都是因为在类内部通过this进行调用

这次遇到的情况确实不太常见,循环依赖会导致cglib无法成功代理被依赖的对象,导致缓存失效。

Java在ElasticSearch中使用LocalDatetime类型

问题

最近在开发一个搜索功能的需求的时候,遇到了LocalDatetime类型不能保存到ElasticSearch中的问题,报错如下:

1
2
ElasticsearchStatusException[Elasticsearch exception [type=mapper_parsing_exception, reason=failed to parse field [createTime] of type [date] in document with id '3000']
]; nested: ElasticsearchException[Elasticsearch exception [type=illegal_state_exception, reason=Can't get text on a START_OBJECT at 1:125]];

从网上查找尝试第一个办法:

将以下注解加到时间字段上,依然无效

1
2
3
4
@Field(type = FieldType.Date, index = FieldIndex.not_analyzed, store = true,
format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd HH:mm:ss.SSS")
private LocalDateTime createTime;

解决办法

在项目中添加以下依赖:

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

在字段上加上以下注解

1
2
3
4
5
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Field(type = FieldType.Date, store = true, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS")
private LocalDateTime createTime;

完美解决

ajax跨域请求中设置header的一个坑

今天晚上有同事反馈,线上一个登记系统出现错误

提示的报错内容为:

1
2
3
4
5
6
{
"readyState": 0,
"responseText": "",
"status": 0,
"statusText": "error"
}

第一眼直觉就是ajax跨域请求失败了,触发了CORS限制。

印象中自己已经做好了跨域相关的配置呀,怎么还会触发呢?

由于是在微信浏览器上才会出现,使用vconsole也没有找到详细的错误原因,又粗略检查了下代码,java后端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 工具类中的统一返回
httpResponse.addHeader("Access-Control-Allow-Origin", "*");
httpResponse.addHeader("Access-Control-Allow-Headers", "*");
httpResponse.addHeader("Access-Control-Allow-Credentials", "true");
httpResponse.addHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
httpResponse.addHeader("Access-Control-Max-Age", "3600");

//继承WebMvcConfigurationSupport的configurer
registry.addMapping("/**")// 设置允许跨域的路径
.allowedOrigins("*")// 设置允许跨域请求的域名
.allowCredentials(true)// 是否允许证书 不再默认开启
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")// 设置允许的方法
.allowedHeaders("*")

origin和method都已经配置上了需要配置的header,可是微信浏览器的options预校验死活不通过!

最终,将目光锁定在了Access-Control-Allow-Headers参数上

查阅资料后发现,Access-Control-Allow-Headers请求头的值设置成 “*” 是不生效的,只能设置成具体的值,比如token等等
image.png
最终代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 工具类中的统一返回
httpResponse.addHeader("Access-Control-Allow-Origin", "*");
httpResponse.addHeader("Access-Control-Allow-Headers", "X-Token");
httpResponse.addHeader("Access-Control-Allow-Credentials", "true");
httpResponse.addHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
httpResponse.addHeader("Access-Control-Max-Age", "3600");

//继承WebMvcConfigurationSupport的configurer
registry.addMapping("/**")// 设置允许跨域的路径
.allowedOrigins("*")// 设置允许跨域请求的域名
.allowCredentials(true)// 是否允许证书 不再默认开启
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")// 设置允许的方法
.allowedHeaders("X-Token")

指定明确的Access-Control-Allow-Headers请求头的值即可

JAVA使用腾讯企业邮箱发送邮件时报错Could not connect to SMTP host

最近做一个邮件发送功能的时候,发现腾讯企业邮的邮箱,用java发送邮件的时候一直报错:
Mail server connection failed; nested exception is javax.mail.MessagingException: Could not connect to SMTP host: smtp.exmail.qq.com, port: 465, response: -1. Failed messages: javax.mail.MessagingException: Could not connect to SMTP host: smtp.exmail.qq.com, port: 465, response: -1

发邮件使用的spring-boot-starter-mail,配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 设置邮箱主机
spring.mail.host=smtp.exmail.qq.com
spring.mail.port=465
spring.mail.protocol=smtp

# 设置用户名
spring.mail.username=xxxxxx
# 设置密码,是客户端专用密码,非网页登录密码
spring.mail.password=xxxxxx

# true代表邮箱需要认证
spring.mail.properties.mail.smtp.auth=true

#启用SSL
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

查阅各种资料后发现,配置文件应当加入下面的配置:

1
2
3
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.smtp.port=465
spring.mail.properties.mail.smtp.socketFactory.port=465

将这三行加入到配置文件里就可以正常发送邮件了

Mybatis使用collection标签进行树形结构数据查询时如何携带外部参数查询

1. 背景

最近更新博客的评论功能,想实现这么一个需求:

评论使用树形结构展示,评论提交后,需要后台审核后才展示到前台,但是用户自己可以显示自己提交的未审核的评论

2. 实施

最初的实现方法是想使用collection进行树形结构查询

为了实现树形查询,需要两个resultMap,一个为最外层的查询结果,另一个是集合里的查询结果,也就是对象中的children对应的List,因为是树形结构,所以外层和里层的结构基本一样,下方代码为两个resultMap代码(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<resultMap type="com.stonewu.blog.core.entity.custom.ReplyTree" id="replyTree">
<id column="id" property="id"/>
<result column="content" property="content"/>
<result column="article_id" property="articleId"/>
<result column="author_id" property="authorId"/>
<result column="parent_reply_id" property="parentReplyId"/>
<result column="check_reply" property="checkReply"/>
<collection column="id=id,authorId=author_id" property="children" ofType="com.stonewu.blog.core.entity.custom.ReplyTree" javaType="java.util.ArrayList" select="getReplyChildren">

</collection>
</resultMap>

<resultMap type="com.stonewu.blog.core.entity.custom.ReplyTree" id="replyTreeChild">
<id column="id" property="id"/>
<result column="content" property="content"/>
<result column="article_id" property="articleId"/>
<result column="author_id" property="authorId"/>
<result column="parent_reply_id" property="parentReplyId"/>
<result column="check_reply" property="checkReply"/>
<collection column="id=id,authorId=author_id" property="children" ofType="com.stonewu.blog.core.entity.custom.ReplyTree" javaType="java.util.ArrayList" select="getReplyChildren">

</collection>
</resultMap>

因为父查询中,需要查出顶层的评论,所以parent_reply_id应该为null,同时为了查询出自己评论的,但是未审核的,就需要下方<if test="param.authorId != null">的代码

阅读更多

Spring中的ThreadPoolTaskExecutor的参数:corePoolSize、maxPoolSize、queueCapacity分别是什么意思

问题:

今天线上爆出了一个问题,很多的异步线程都跑的非常慢

多线程使用的是Spring的ThreadPoolTaskExecutor

配置如下

1
2
3
corePoolSize: 5
maxPoolSize: 500
queueCapacity: 100

错误逻辑

之前以为的逻辑(错误逻辑):
corePoolSize:默认创建线程数量:5个,

maxPoolSize:如果线程数量超过5个后,在maxPoolSize大小内继续增加线程,直到线程数量到达500个,

queueCapacity:线程数量到达maxPoolSize以后,再添加线程,将会进入queueCapacity进行等待,当等待队列超过queueCapacity设定的值,抛出异常。

可是。。。。现实狠狠的抽了我一巴掌

因为线上发现,线程执行的速度非常慢,很多线程创建了以后并没有执行,而且可以确定线程没有超过500个

百度了一堆,都没有找到这几个参数的正确解释,直到最终看了看源码,发现正确的逻辑是这样的:

正确逻辑

corePoolSize:当线程数小于corePoolSize个的时候,正常创建线程

queueCapacity:当线程大于corePoolSize个的时候,将线程放入queueCapacity大小的队列

maxPoolSize:当queueCapacity队列已满,将会继续创建线程,直到线程数超过maxPoolSize的大小,将抛出异常

于是线上的问题就找到了,实际上,线程在跑的一直只有5个,所以导致线程一直不执行

解决办法:

也简单,把corePoolSize的大小增大到500即可。

Spring Boot 使用 spring-boot-devtools 实现热加载时出现类型转换异常

热加载问题

网上有给出了许多解决热加载的方法,比如:

在resource目录下创建META-INF/spring-devtools.properties文件

里面的内容为类似下面的内容:

1
2
restart.exclude.companycommonlibs=/mycorp-common-[\\w-]+\.jar
restart.include.projectcommon=/mycorp-myproj-[\\w-]+\.jar

网上能给出的答案一般也就这些,可是作为观众我们自然是表示一脸懵逼啊,这两行啥玩意?

那么现在给出下我的解决办法:

现在大部分的项目在eclipse中基本都是多个项目的形式,在intellij idea中则是多个module的形式

其中,web项目引用了faced项目,没有引用service项目,因为service项目是个独立的dubbo provider(提供者),web项目则为consumer(消费者)

web项目中的controller中有这么一行代码:

1
UserInfo info = userService.findInfo();

其中UserInfo来自faced项目,userService则通过dubbo的reference注入

项目引入spring-boot-devtools了以后,如果改动了web项目中的代码,则下次web项目中运行到了上面这行代码,则会出现如下报错
java.lang.ClassCastException: com.xxx.UserInfo cannot be cast to com.xxx.UserInfo
为啥明明显示的是同一个类,却给我显示类型转换异常?

那么这就要从spring-boot-devtools的工作原理说起

工作原理

spring-boot-devtools的热加载其实是这么工作的

当你启动web项目的时候,你的web项目中的代码都会交给spring-boot-devtools的restart加载器去进行加载,而jar包,则基本会交给base加载器去加载

当项目中的代码有改动时,devtools检测到代码变动,restart加载器就会被扔掉重建,这个时候,所有restart加载器加载的代码都会重新部署,而base加载器则维持不变

因为jar包的内容基本都不会变,所以用base加载器不会有什么问题

但是由于web项目中引用了faced项目,于是你引用的faced的所有代码也会被restart加载器重新加载,此时,刚刚加载的UserInfo则跟项目刚开始启动的UserInfo就是两个类了,java中判断两个类是否为同一个类不仅仅是通过包名+类名去识别,还会去看两个类的加载器是否一致,如果不一致,则识别为两个类,所以就会出现我们之前看到的转换异常

那么解决办法呢?

解决方案

首先我们需要确定我们出现转换异常的类在哪个项目中,我的则是platform-faced项目

那么回到最初的方法,我们需要在resource目录下创建META-INF/spring-devtools.properties文件

里面的内容填写:
restart.exclude.faced=/platform-faced
如果你有多个face项目,且项目名开头都是这个,那么可以使用如下的配置
restart.exclude.faced=/platform-faced[\\w-]+
网上给的答案后面有带上.jar,但是由于我们开发的时候的引用方式是项目引用,后面是不带jar的,所以我们不需要带上.jar

restart.exclude.faced这个键中的faced是可以自己命名的,只需要保证在这个配置文件中唯一即可

文件配置好后,我们可以在项目已启动状态下修改web项目中的代码,可以看到控制台有显示项目重新加载,继续访问到之前触发转换异常的地方,发现已经不会异常了,完美!

使用Spring MVC时传递FlashAttribute无法在controller中接收的问题

为了实现一个授权登录的功能,需要在两个controller中跳转,可是中间传递的参数必须要隐密,于是便使用了RedirectAttributes类进行参数的传递。

可是使用的时候发现,无论使用request.getParameter还是getAttribute以及redirectAttributes.getFlashAttributes().get()的方式都不能获取到结果。

查了很多资料后找到了方法:
public Map<String, Object> acceptAuth(HttpServletRequest request, @ModelAttribute("param") String param)
如上,把需要传递的参数放在方法中,并且加上@ModelAttribute(“xxxx”)就能取到值了,完美!

maven在不同的环境获取不同配置文件的方法

最近准备把正在开发的项目给分为两个环境来部署,使用jenkins进行自动构建。

把maven部署多环境相关的资料看了下,都比较难理解,于是自己摸索,找到一个比较好的办法。

首先在 src/main/resource 下建两个文件夹,具体几个看你的环境有几个,我这里是分了两个,dev和product,开发环境和正式环境。

然后将配置文件分别放入两个目录中,然后把两个环境中的配置配好

接下来编辑项目的pom文件

进行如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
......
......
<profiles>
<!-- 开发环境 -->
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!-- 正式环境 -->
<profile>
<id>product</id>
<properties>
<env>product</env>
</properties>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
</profile>
</profiles>
......
......
</project>

中间的properties是用来作为变量能在下文中取到的

然后在build节点中进行如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
......
......
<build>
......
......
<resources>
<!-- 此处是因为我的java目录中有一些xml文件,
如果你的项目中的java目录下没有xml文件则可以不用配 -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<!-- 是否替换资源中的属性 -->
<filtering>false</filtering>
</resource>
<!-- 此处的用法就是${env}会替换成你的上面选择的具体环境 -->
<resource>
<directory>src/main/resources/${env}</directory>
<!-- 是否替换资源中的属性 -->
<filtering>true</filtering>
</resource>
</resources>
......
......
</build>
......
......
</project>

最后使用maven命令进行编译打包

1
mvn clean package -Pproduct

-P为指定某个profile,后面跟上具体的profile就行了,比如上面就指定的是product的profile,如果不加,则默认是dev的profile,可以倒回上面的配置看。

接下来就会根据你的命令进行编译打包啦~