以springboot2.x+mybatis+mysql+HikariCP为例,记录下自己的开发记录。实现思路读写分离有四种实现方式。我这里使用的是AOP切面实现的。目前实现的是一个主(Master)
以springboot2.x+mybatis+mysql+HikariCP
为例,记录下自己的开发记录。
实现思路
读写分离有四种实现方式。我这里使用的是AOP
切面实现的。目前实现的是一个主(Master
),两个从(Slave
)。废话少说,讲下我的思路。
- 定义四个数据源,为什么是四个呢?
Master*1
,Slave*2
,统一的数据源*1(一下称为DynamicDatasource
,这个数据源是其他三个数据源的总和,可以执行它使用哪个数据源(Master
,Slave1
,Slave2
)进行操作). - 定义一个
DataSourceHolder
,存放的是三个数据源(Master
,Slave1
,Slave2
),可以按照条件去获取指定类型的数据源之一。 - 将
DynamicDataSource
替换掉SpringBoot
默认提供的数据源。 - 在方法上使用注解或者使用某一类方法调用前指定使用的数据源。
什么是读写分离
将数据库分为主从库,一个主库用于写数据,多个从库完成读数据的操作。主库之间通过某种机制进行数据的同步,是一种常见的数据库架构。
数据库读写分离架构解决什么问题?
读写分离是用来解决数据库的读写瓶颈的。
在互联网的应用场景中,常常数据量大,并发量高,高可用要求高,一致性要求高,需要使用读写分离架构。需要注意如下问题:
- 数据库连接池要机型区分,哪些是读连接池,哪些是写连接池,研发的难度会增加。
- 为了保证高可用,读连接池需能够实现故障自动转移。
- 主从的一致性需要考虑。
其他减轻数据库压力的方式
缓存。这里就不再过多介绍了。关于缓存,也是比较大的一方面,后面我会单独以一篇文章介绍自己的理解吧。
数据库水平切分。
数据库水平切分,也是一种常见的数据库架构,是一种通过算法,将数据库进行分割的架构。一个水平切分集群中的每个数据库,通常称为一个“分片”。每一个分片中的数据没有重合,所有分片中的数据并集组成全部数据。
水平切分架构解决什么问题呢?
大部分的互联网业务,数据量都非常大,单库容量最容易成为瓶颈,当单库的容量成为了瓶颈,我们希望提高数据库的写性能,降低单库容量的话,就可以采用水平切分了。
上代码
- 在
pom.xml
中加上如下内容。
<!--- mybatis --><!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter --><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency><!-- HikariCP 数据库连接池--><!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP --><dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>3.3.0</version></dependency>
- 我们首先定义好三个特定的数据源
Master*1,Slave*2
public enum DBTypeEnum { /** * 主数据源 */ Master, /** * 从数据源1 */ Slave1, /** * 从数据源2 */ Slave2}
- 然后,设置
DataSourceHolder
。这样我们就定义好了数据源的调用的模型了。
@Slf4jpublic class DBContextHolder { private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>(); private static final AtomicInteger counter = new AtomicInteger(-1); public static void set(DBTypeEnum dbType) { contextHolder.set(dbType); } public static DBTypeEnum get() { return contextHolder.get(); } public static void master() { set(DBTypeEnum.Master); log.debug("使用的数据源是: master"); } public static void slave() { int index = counter.getAndIncrement() % 2; if (counter.get() > 99) { counter.set(-1); } if (index == 0) { set(DBTypeEnum.Slave1); log.debug("使用的数据源是: slave1"); } else { set(DBTypeEnum.Slave2); log.debug("使用的数据源是: slave2"); } }}
- 设置动态数据源,可以指定上面的任何一种数据源去真正的操作数据库。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DBContextHolder.get(); }}
- 创建四种数据源的实例,
ConfigurationProperties
注解中的配置项是在application.propreties
中配置的。
@Configurationpublic class DynamicDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public HikariDataSource masterDataSource() { return new HikariDataSource(); } @Bean @ConfigurationProperties("spring.datasource.slave1") public HikariDataSource slave1DataSource() { return new HikariDataSource(); } @Bean @ConfigurationProperties("spring.datasource.slave2") public HikariDataSource slave2DataSource() { return new HikariDataSource(); } @Bean public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { Map<Object, Object> targetDataSources = new HashMap<>(3); // 设置所有的数据源 targetDataSources.put(DBTypeEnum.Master, masterDataSource); targetDataSources.put(DBTypeEnum.Slave1, slave1DataSource); targetDataSources.put(DBTypeEnum.Slave2, slave2DataSource); DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 将写库设置为默认的数据源 dynamicDataSource.setDefaultTargetDataSource(masterDataSource); dynamicDataSource.setTargetDataSources(targetDataSources); return dynamicDataSource; } }
- 指定我们使用的数据源配置,
url
,username
,password
等。
spring.datasource.master.jdbc-url=jdbc:mysql://10.1.14.177:3306/dor_human?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNullspring.datasource.master.username=rootspring.datasource.master.password=123456#spring.datasource.master.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slave1.jdbc-url=jdbc:mysql://10.1.14.177:3306/dor_human?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNullspring.datasource.slave1.username=rootspring.datasource.slave1.password=123456#spring.datasource.slave1.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slave2.jdbc-url=jdbc:mysql://10.1.14.177:3306/dor_human?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNullspring.datasource.slave2.username=rootspring.datasource.slave2.password=123456#spring.datasource.slave2.driver-class-name=com.mysql.jdbc.Driver
- 这是我们要替换掉
springboot
默认的数据源
@EnableTransactionManagement@Configuration@Slf4jpublic class MyBatisConfig { @Autowired private DataSource dynamicDataSource; /** * SqlSessionFactory * * @return SqlSessionFactory * @throws Exception */ @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource); sqlSessionFactoryBean.setTypeAliasesPackage("com.fxb.doraemon.human.entity"); sqlSessionFactoryBean.setConfigLocation( new PathMatchingResourcePatternResolver().getResource("classpath:mapper/mybatis-config.xml")); sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml")); log.debug("初始化SqlSessionFactory"); return sqlSessionFactoryBean.getObject(); } /** * 事务管理器 * * @return PlatformTransactionManager */ @Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(dynamicDataSource); }}
好了,这样我们的读写分离的准备工作就做好了,接下来要做的就是使用了,这里我们使用AOP
的方式,来规定什么时候使用什么数据源。
@Aspect@Component@Slf4jpublic class DataSourceAop { /** * 织入读库数据源 */ @Pointcut("execution(public * com.fxb.doraemon.human.service..*.get*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.select*(..))") public void readPointcut() { } /*** * @1:这里有点小问题. */ @Pointcut("@annotation(com.fxb.doraemon.human.annotation.Master) " + "|| execution(public * com.fxb.doraemon.human.service..*.save*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.insert*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.update*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.edit*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.delete*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.del*(..)) " + "|| execution(public * com.fxb.doraemon.human.service..*.remove*(..)) ") public void writePointcut() { } @Before("readPointcut()") public void read() { DBContextHolder.slave(); } @Before("writePointcut()") public void write() { DBContextHolder.master(); }}
- 最后,最重要的一步,不然你会发现,
aop
怎么都切入不了。那是因为没有启动织入代理方式。在你的启动类上加上下面的这个注解。
@EnableAspectJAutoProxy
验证一下我们的结果:
好了,这样我们的数据分离就配置好了。
最后
下一篇《数据库读写分离的坑,你已经在坑了!》文章,我们来解释一下这个@1
的这个问题。
如果想了解最新动态,欢迎关注公众号: 方家小白
. 欢迎一起交流学习。
.

- 0