背景
一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,通常会做一个数据库集群,开启主从备份,一主多从或多主多从,以提高读取性能。
主从同步
正常情况下读写分离的实现,首先要做一个一主多从的数据库集群,同时还需要进行数据同步。
MySQL主从同步配置方法见另一篇文章:MySQL主从同步配置
读写分离
代码层面实现读写分离
代码层面读写分离有两种方式判断是读还是写,根据方法名称、利用注解。在实际项目中,往往会存在这样的service,需要根据从数据库中读到的值进行不同的写操作,这处于一个事务中,那么这个service就只能走主库,不适合使用读写分离,因此,下面将按照注解的方式,让只读的service走从库。
通过注解的方式实现读写分离,无需修改业务代码,只要在只读的 service 方法上加一个注解即可。
1. mybatis和数据源配置
mybatis:
typeAliasesPackage: com.test.demo.entity
mapperLocations: classpath:mapper/*.xml
spring:
datasource:
masters:
- url: jdbc:mysql://master1_IP:port/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&nullCatalogMeansCurrent=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slaves:
- url: jdbc:mysql://slave1_IP:port/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&nullCatalogMeansCurrent=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://slave2_IP:port/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&nullCatalogMeansCurrent=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
2. 数据源切换
/**
* 多数据源配置项
*/
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
private List<Map<String,String>> masters;
private List<Map<String,String>> slaves;
public List<Map<String, String>> getMasters() {
return masters;
}
public List<Map<String, String>> getSlaves() {
return slaves;
}
}
/**
* 数据源切换(读、写)
* 利用ThreadLocal保存当前线程是否处于读模式
* 通过设置READ_ONLY注解在开始操作前设置模式为读模式,操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
*/
public class DataSourceContextHolder {
private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
public static final String WRITE = "write";
public static final String READ = "read";
private static ThreadLocal<String> contextHolder= new ThreadLocal<>();
public static void setDataSOurceType(String dataSourceType) {
if (dataSourceType == null) {
logger.error("dataSource type为空");
throw new NullPointerException();
}
logger.info("设置dataSource type为:{}", dataSourceType);
contextHolder.set(dataSourceType);
}
/**
* 默认写模式
* @return
*/
public static String getDataSourceType() {
return contextHolder.get() == null ? WRITE : contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
/**
* 重写 determineCurrentLookupKey
*/
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Autowired
private DataSourceProperties dataSourceProperties;
private final Logger logger = LoggerFactory.getLogger(MyRoutingDataSource.class);
protected Object determineCurrentLookupKey() {
final String dataSourceType = DataSourceContextHolder.getDataSourceType();
final int masterCount = dataSourceProperties.getMasters().size();
final int slaveCount = dataSourceProperties.getSlaves().size();
int index;
if ( DataSourceContextHolder.WRITE.equalsIgnoreCase(dataSourceType)) {
//使用随机数决定使用哪个写库
index = ThreadLocalRandom.current().nextInt(masterCount) % masterCount;
logger.info("使用了写库 {}", index);
} else {
//使用随机数决定使用哪个读库
index = ThreadLocalRandom.current().nextInt(slaveCount) % slaveCount;
logger.info("使用了读库 {}", index);
}
return dataSourceType + index;
}
}
/**
* 多数据源配置
*/
@Configuration
public class DataSourceConfig {
@Autowired
private DataSourceProperties dataSourceProperties;
@Value("${mybatis.typeAliasesPackage}")
private String typeAliasesPackage;
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
public List<DataSource> masterDatasources () throws Exception {
final List<Map<String, String>> masters = dataSourceProperties.getMasters();
if (CollectionUtils.isEmpty(masters)) {
throw new IllegalArgumentException("至少需要一个主数据源");
}
final List<DataSource> dataSources = new ArrayList<>();
for (Map map : masters) {
dataSources.add(DruidDataSourceFactory.createDataSource(map));
}
return dataSources;
}
@Bean
public List<DataSource> slaveDatasources () throws Exception {
final List<Map<String, String>> slaves = dataSourceProperties.getSlaves();
if (CollectionUtils.isEmpty(slaves)) {
throw new IllegalArgumentException("至少需要一个从数据源");
}
final List<DataSource> dataSources = new ArrayList<>();
for (Map map : slaves) {
dataSources.add(DruidDataSourceFactory.createDataSource(map));
}
return dataSources;
}
@Bean
@Primary
@DependsOn({"masterDatasources", "slaveDatasources"})
public MyRoutingDataSource routingDataSource () {
final Map<Object, Object> targetDataSources = new HashMap<>();
for (int i = 0; i < dataSourceProperties.getMasters().size(); i++) {
targetDataSources.put(DataSourceContextHolder.WRITE + i, dataSourceProperties.getMasters().get(i));
}
for (int i = 0; i < dataSourceProperties.getSlaves().size(); i++) {
targetDataSources.put(DataSourceContextHolder.READ + i, dataSourceProperties.getSlaves().get(i));
}
final MyRoutingDataSource routingDataSource = new MyRoutingDataSource();
// 该方法是AbstractRoutingDataSource的方法
routingDataSource.setTargetDataSources(targetDataSources);
// 默认的datasource设置为写库
routingDataSource.setDefaultTargetDataSource(dataSourceProperties.getMasters().get(0));
return routingDataSource;
}
/**
* 多数据源需要自己设置sqlSessionFactory
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory (final MyRoutingDataSource dataSource) throws Exception {
final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);// 指定数据源
sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);// 指定基包
final ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocations));
return sqlSessionFactoryBean.getObject();
}
/**
* 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
* @return
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
}
3. 自定义只读注解
/**
* 被这个注解方法使用读库
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
4. 拦截注解,切换数据源
/**
* 写一个切面来切换数据使用哪种数据源,重写 getOrder 保证本切面优先级高于事务切面优先级
*/
@Aspect
public class ReadOnlyInterceptor implements Ordered {
private final Logger logger = LoggerFactory.getLogger(ReadOnlyInterceptor.class);
@Around("@annotation(readOnly)")
public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable{
try{
DataSourceContextHolder.setDataSOurceType(DataSourceContextHolder.READ);
return joinPoint.proceed();
}finally {
//清除 dataSourceType 一方面为了避免内存泄漏,更重要的是避免对后续在本线程上执行的操作产生影响
DataSourceContextHolder.clearDataSourceType();
logger.info("清除threadLocal");
}
}
@Override
public int getOrder() {
return 0;
}
}