Mybatis主从读写分离

参考自这篇文章:https://www.cnblogs.com/cjsblog/p/9712457.html

具体的代码,文章里面都有,这里就不贴出来了。因此本文主要记录下遇到的几个问题,以及一些关于代码的理解。

主从读写分离的实现思路

将主从设置为多个数据源

读写分离,实际上就是在需要读的时候,将数据库连接切换到读数据源;在需要写的时候,将数据库连接切换到写数据源,因此我们肯定要提前先设置多个数据源才行。

文章中设置了4个数据源,包括一个主库、2个从库以及一个用于切换主从的数据源。

通过ThreadLocal保证线程安全

普通的数据库读写在多线程、高并发下,有很大的安全隐患。

ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

参考文章:http://www.cnblogs.com/dolphin0520/p/3920407.html

通过AOP+注解,智能切换主从

数据库的操作调用,一般都是在Service层。我们可以通过定义一个针对Service层的AOP切面,然后通过Service的方法名来确定到底应该连主库还是从库。

这是切换的核心部分,因此这里贴下代码(注意该文章的AOP切点的设置有问题,会将读写操作都切换到主库,这个需要自行修改下;下面的示例代码是已经修正过的):

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.test.demo.config.mybatis;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
* @Description : 自动切换主从数据源的AOP
* @Author : 周昌炬<zhouchangju@test.com>
* @Create : 15:00 2018/10/20
*/
@Aspect
@Component
public class DataSourceAOP {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAOP.class);

@Pointcut("execution(* com.test.demo.service..*.select*(..)) " +
"|| execution(* com.test.demo.service..*.get*(..))"
)
public void readPointcut()
{
System.out.println("readPointcut");
}

@Pointcut("@annotation(com.test.demo.config.mybatis.annotation.Master) " +
"|| execution(* com.test.demo.service..*.insert*(..)) " +
"|| execution(* com.test.demo.service..*.add*(..)) " +
"|| execution(* com.test.demo.service..*.update*(..)) " +
"|| execution(* com.test.demo.service..*.edit*(..)) " +
"|| execution(* com.test.demo.service..*.delete*(..)) " +
"|| execution(* com.test.demo.service..*.remove*(..)) " +
"|| execution(* com.test.demo.service..*.save*(..)) " +
"|| execution(* com.test.demo.service..*.modify*(..))")
public void writePointcut()
{
System.out.println("writePointcut");
}

@Pointcut("@annotation(com.test.demo.config.mybatis.annotation.Slave)")
public void slavePointcut()
{
System.out.println("slavePointcut");
}


@Before("readPointcut()")
public void read()
{
DBContextHolder.slave();
}

@Before("writePointcut()")
public void write()
{
DBContextHolder.master();
}

@Before("slavePointcut()")
public void slave()
{
DBContextHolder.slave();
}

/**
* 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库
*/
/*
@Before("execution(* com.cjs.example.service.impl.*.*(..))")
public void before(JoinPoint jp) {
String methodName = jp.getSignature().getName();

if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
DBContextHolder.slave();
}else {
DBContextHolder.master();
}
}
*/
}

针对一些特殊场景,比如用户的浏览量、点赞等,需要写入后马上查询的,为了避免主从同步延时导致数据读取有误,一般都要求写入主库后,读取也在主库进行。这种情况就需要我们能够手动指定切换到主or从。

这可以通过注解来搭配AOP切点实现,上面的代码已经体现了,就不再赘述:

1
@annotation(com.test.demo.config.mybatis.annotation.Master)

注意事项

AOP切面必须涵盖所有的service或者mapper方法

在之前的项目中出现过这个问题:有个方法没有被AOP涵盖到,这是一个update数据库的方法(方法名没有包含update等AOP中定义好的关键词,而是叫做doPriority()),结果引用了从库的数据源;而从库是read-only模式,就导致了写入失败。

遇到的问题

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.test.poi.api.mapper.PoiFormulaMapper.getNamesByIds

我的程序里面有两个包,poi和pos,然后发现删除pos就正常了,因此确定问题出在pos中。

经过排查,最终发现是因为项目分为了多个包,我在poi里面设置了数据源的配置;而之前另外一个同事在pos这个包里面,也设置了数据源,导致二者冲突了。这个错误提示信息不够友好,很容易造成误导。
解决方案:注释掉之前同事在pos中设置的数据源类的@Configuration注解即可

java.lang.IllegalArgumentException: dataSource or dataSourceClassName or jdbcUrl is required.

这个报错造成的现象是:两个从数据源,一个可以正常使用(slave1),一个不行(slave2)。

通过排查,最终确定应该是配置文件有特殊字符,导致slave1的配置没有被正常读取到。

但是我用vim的set invlist对比了下,也没看出什么特殊字符,很奇怪;不过用正确的slave2的配置覆盖上去就正常了;说明肯定还是有不一样的字符的。

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.test.pos.mapper.ClassifyListMapper’ available

只加载这个路径就会报错:
@MapperScan(“com.test.poi.api.mapper”)

写成这个则可以成功加载:
@MapperScan({“com.test.poi.api.mapper”, “com.test.pos.mapper”})

原因是这个ClassifyListMapper是我们自己定义的,位于pos下,我不扫描pos的mapper,当然报错了。