在Web项目开发中,实际生产环境上的数据库通常都配置成主从环境,并且要求在业务系统中读写分离,因此在业务代码中需要配置至少两个数据源(读/写)。
主要思路
具体实现
本文结合实际项目开发经验(Spring+Mybatis)提出一种实用的数据读写分离方案。
主要思路
1.xml配置文件针对读/写分别配置不同的Bean,主要包括DataSource(数据源)、SqlSessionFactoryBean以及MapperScannerConfigurer(mapper接口扫描,指定bean生成策略);
2.根据读写Bean的生成策略,使用Mapper接口时,通过注解使用不同的bean。
具体实现
1.配置读写数据源,由于开发中采用Druid作为数据库连接池,因此本文以Druid为例配置DataSource
主库配置
<!-- 配置主库数据源 -->
<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${masterdb.url}"/>
<property name="username" value="${masterdb.user}"/>
<property name="password" value="${masterdb.password}"/>
<property name="initialSize" value="${initialSize}"/>
<property name="minIdle" value="${minIdle}"/>
<property name="maxActive" value="${maxActive}"/>
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}"/>
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="${testWhileIdle}"/>
<property name="testOnBorrow" value="${testOnBorrow}"/>
<property name="testOnReturn" value="${testOnReturn}"/>
<property name="removeAbandoned" value="${removeAbandoned}"/>
<property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}"/>
<property name="logAbandoned" value="${logAbandoned}"/>
<property name="filters" value="stat"/>
<property name="connectionProperties" value="druid.stat.slowSqlMillis=${slowSqlMillis}"/>
</bean>
<!-- 配置主库SqlSessionFactory-->
<bean id="masterSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 使用定义的主库DataSource -->
<property name="dataSource" ref="masterDataSource" />
<!-- 定义mapper xml文件路径-->
<property name="mapperLocations">
<list>
<value>classpath*:mapper/*.xml</value>
</list>
</property>
</bean>
<!--指定mapper接口扫描-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定mapper java接口所在的包路径-->
<property name="basePackage" value="com.dao.mapper" />
<property name="sqlSessionFactoryBeanName" value="masterSqlSessionFactory" />
<!-- 定义用于连接主库的 mapper接口bean生成策略-->
<property name="nameGenerator" >
<!-- 自定义实现了BeanNameGenerator接口的实现类,bean名称添加Master后缀 -->
<bean class="com.utils.MapperBeanNameGenerator">
<property name="postfix" value="Master"/>
</bean>
</property>
</bean>
从库配置,与主库配置基本一致,slaveDataSource需要将defaultReadOnly设置为true, 具体如下:
<bean id="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${slavedb.url}"/>
<property name="username" value="${slavedb.user}"/>
<property name="password" value="${slavedb.password}"/>
<property name="initialSize" value="${initialSize}"/>
<property name="minIdle" value="${minIdle}"/>
<property name="maxActive" value="${maxActive}"/>
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}"/>
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="${testWhileIdle}"/>
<property name="testOnBorrow" value="${testOnBorrow}"/>
<property name="testOnReturn" value="${testOnReturn}"/>
<property name="defaultReadOnly" value="true"/> <!-- 设置为只读 -->
<property name="removeAbandoned" value="${removeAbandoned}"/>
<property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}"/>
<property name="logAbandoned" value="${logAbandoned}"/>
<property name="filters" value="stat"/>
<property name="connectionProperties" value="druid.stat.slowSqlMillis=${slowSqlMillis}"/>
</bean>
<!-- 配置从库SqlSessionFactory-->
<bean id="slaveSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 使用定义的从库DataSource -->
<property name="dataSource" ref="slaveDataSource" />
<!-- 定义mapper xml文件路径,可与主库一致-->
<property name="mapperLocations">
<list>
<value>classpath*:mapper/*.xml</value>
</list>
</property>
</bean>
<!--指定mapper接口扫描-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定mapper java接口所在的包路径,与主库一致-->
<property name="basePackage" value="com.dao.mapper" />
<property name="sqlSessionFactoryBeanName" value="slaveSqlSessionFactory" />
<!-- 定义用于连接从库的 mapper接口bean生成策略,bean名称添加Slave后缀-->
<property name="nameGenerator" >
<bean class="com.utils.MapperBeanNameGenerator">
<property name="postfix" value="Slave"/>
</bean>
</property>
</bean>
MapperBeanNameGenerator源码
package com.utils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.util.StringUtils;
public class MapperBeanNameGenerator implements BeanNameGenerator {
private String postfix = "";
public MapperBeanNameGenerator() {
}
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
String beanClassName = definition.getBeanClassName();
String beanName = beanClassName;
int index = beanClassName.lastIndexOf(".");
if(index != -1) {
beanName = beanClassName.substring(index + 1);
}
return StringUtils.isEmpty(this.postfix) ? beanName : beanName + this.postfix;
}
private String uncapitalize(String str) {
int strLen;
return str != null && (strLen = str.length()) != 0?(new StringBuilder(strLen)).append(Character.toLowerCase(str.charAt(0))).append(str.substring(1)).toString():str;
}
public String getPostfix() {
return this.postfix;
}
public void setPostfix(String postfix) {
this.postfix = postfix;
}
}
这样配置完成后,启动后bean会扫描com.dao.mapper定义的java接口文件,并分别生成带master或slave后缀的bean。
2. 业务调用
@Resource(name = "userMapperMaster")
UserMapper userMapperMaster;
@Resource(name = "userMapperSlave")
UserMapper userMapperSlave;
使用userMapperMaster来调用insert、update相关接口,进行数据插入修改等,使用userMapperSlave调用查询相关接口。
总结
本文仅仅结合实际工作中进行读写分离的一种实现方案,使用简单,可供需要读写分离场景提供一种参考,更多方案可参考其他文章