最近项目中需要做【高可用】数据库读写分离相关的需求,特地整理了下关于读写分离的相关知识。项目中采用4台数据库:1个master,2个slave,1个readOnly,其中master数据库会自动定时同步到readOnly节点。可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …), 但是我们公司没有专门的中间件团队搭建读写分离基础设施,因此需要开发人员自行实现读写分离(有点离谱~)。
一、实现原理
Spring框架中,Spring-JDBC模块提供了AbstractRoutingDataSource,其内部可以包含了多个DataSource,通过继承该类并覆盖determineCurrentLookupKey方法,可以根据业务需求动态选择数据源。
二、具体实现
1、application.yml配置读和写数据源
server:port: 8080spring:datasource:druid:ds1:url: jdbc:mysql://localhost:3306/db_ds1?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=falseusername: rootpassword: root1234driver-class-name: com.mysql.jdbc.Driverds2:url: jdbc:mysql://localhost:3306/db_ds2?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=falseusername: rootpassword: root1234driver-class-name: com.mysql.jdbc.Driver
2、DynamicDataSource动态数据源,继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法
/*** 动态数据源*/
public class DynamicDataSource extends AbstractRoutingDataSource {/*** ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();/*** 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好** @param targetDataSources 目标数据源* @param defaultTargetDataSource 默认数据*/public DynamicDataSource(Map<Object, Object> targetDataSources, DataSource defaultTargetDataSource) {super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return CONTEXT_HOLDER.get();}public static void setDataSource(String dataSource) {CONTEXT_HOLDER.set(dataSource);}public static void clearDataSource() {CONTEXT_HOLDER.remove();}
}
3、DynamicDataSourceAspect多数据源切面
/*** 多数据源切面*/
@Slf4j
@Component
@Aspect
public class DynamicDataSourceAspect {/*** @annotation:这个表达式的含义是匹配所有带有特定注解的方法。 例如,@annotation(com.xxx.MyAnnotation)将匹配所有带有@MyAnnotation注解的方法。* @within:这个表达式的含义是匹配所有在特定注解的类中的方法,不管这个方法本身有没有这个注解。 例如,@within(com.xxx.MyAnnotation)将匹配所有在带有@MyAnnotation注解的类中的方法。*/@Pointcut("@annotation(com.ds.datasource.DS) " +"|| @within(com.ds.datasource.DS)")public void dataSourcePointCut() {}@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Class targetClass = point.getTarget().getClass();Method method = signature.getMethod();DS targetDataSource = (DS) targetClass.getAnnotation(DS.class);DS methodDataSource = method.getAnnotation(DS.class);if (targetDataSource != null || methodDataSource != null) {String value;if (methodDataSource != null) {//优先用方法上的value = methodDataSource.value();} else {//类上的value = targetDataSource.value();}DynamicDataSource.setDataSource(value);log.debug("set datasource is {}", value);}try {return point.proceed();} finally {DynamicDataSource.clearDataSource();log.debug("clean datasource");}}
}
4、DataSourceConfig数据源配置
/*** 数据源配置*/
@Configuration
public class DataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.druid.ds1")public DataSource dataSource1() {// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSourcereturn DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.druid.ds2")public DataSource dataSource2() {// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSourcereturn DruidDataSourceBuilder.create().build();}/*** 设设置动态数据源(设置目标数据源)** @param dataSource1 数据源1* @param dataSource2 数据源2* @return 动态数据源*/@Beanpublic DynamicDataSource dynamicDataSource(DataSource dataSource1, DataSource dataSource2) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DsConstant.DS1, dataSource1);targetDataSources.put(DsConstant.DS2, dataSource2);return new DynamicDataSource(targetDataSources, dataSource1);}/*** 当你自定义SqlSessionFactory Bean时,你需要在自定义的SqlSessionFactory中明确地设置别名包和mapper文件的位置。* 否则,application.yml中的配置可能不会生效。* 当你使用MyBatis的Spring Boot Starter自动配置时,它会根据application.yml中的配置来创建SqlSessionFactory。但当你自定义SqlSessionFactory时,Spring Boot Starter的自动配置就不会生效了,因此,你需要在自定义的SqlSessionFactory中设置这些属性。* 所以,虽然你在application.yml中配置了别名包和mapper文件的位置,但是你还是需要在自定义的SqlSessionFactory中设置这些属性,以确保它们被正确地使用。** @param dataSource* @return* @throws Exception*/@Beanpublic SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {// 使用我们的动态数据源来构建SqlSessionFactorySqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dataSource);// 设置别名包sessionFactory.setTypeAliasesPackage("com.ds.entity");// 设置mapper文件的位置PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));return sessionFactory.getObject();}@Beanpublic SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}/*** 注意:虽然Spring会自动管理事务,但是你还需要确保你的DataSourceTransactionManager已经正确配置了你的AbstractRoutingDataSource。* 如果没有正确配置,你可能会遇到无法找到合适的事务管理器的错误。** @param routingDataSource* @return*/@Beanpublic PlatformTransactionManager transactionManager(AbstractRoutingDataSource routingDataSource) {return new DataSourceTransactionManager(routingDataSource);}
}
5、StudentServiceImpl读取数据源1和UserServiceImpl读取数据源2
/*** 学生实现类*/
@Service
public class StudentServiceImpl implements IStudentService {@AutowiredStudentMapper studentMapper;@DS(DsConstant.DS2)@Overridepublic List<Student> findAll() {return studentMapper.selectAll();}@DS(DsConstant.DS2)@Overridepublic void insert() {Student student = new Student();student.setStudentCode("Code-" + RandomUtil.randomNumber());student.setStudentCode("学员-" + RandomUtil.randomNumber());studentMapper.insertStudent(student);// 模拟业务异常,验证事务回滚// int k = 1 / 0;}
}
/*** 用户实现类*/
@Service
public class UserServiceImpl implements IUserService {@AutowiredUserMapper userMapper;@DS(DsConstant.DS1)@Overridepublic List<User> findAll() {return userMapper.selectAll();}@DS(DsConstant.DS1)@Overridepublic void add() {User user = new User();user.setName("张三001-" + RandomUtil.randomInt());user.setAge(RandomUtil.randomInt());user.setSex("男");userMapper.insertUser2(user);}
}
三、测试接口,发现在自定义的业务逻辑上,能够区分数据源,实现读写分离。
四、项目结构
源码下载地址multi-datasource,欢迎Star!