【高可用】利用AOP实现数据库读写分离

最近项目中需要做【高可用】数据库读写分离相关的需求,特地整理了下关于读写分离的相关知识。项目中采用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: 8080

spring:
  datasource:
    druid:
      ds1:
        url: jdbc:mysql://localhost:3306/db_ds1?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: root1234
        driver-class-name: com.mysql.jdbc.Driver
      ds2:
        url: jdbc:mysql://localhost:3306/db_ds2?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: root1234
        driver-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();
    }

    @Override
    protected 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中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.ds2")
    public DataSource dataSource2() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 设设置动态数据源(设置目标数据源)
     *
     * @param dataSource1 数据源1
     * @param dataSource2 数据源2
     * @return 动态数据源
     */
    @Bean
    public 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
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        // 使用我们的动态数据源来构建SqlSessionFactory
        SqlSessionFactoryBean 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();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    /**
     * 注意:虽然Spring会自动管理事务,但是你还需要确保你的DataSourceTransactionManager已经正确配置了你的AbstractRoutingDataSource。
     * 如果没有正确配置,你可能会遇到无法找到合适的事务管理器的错误。
     *
     * @param routingDataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(AbstractRoutingDataSource routingDataSource) {
        return new DataSourceTransactionManager(routingDataSource);
    }
}

5、StudentServiceImpl读取数据源1和UserServiceImpl读取数据源2

/**
 * 学生实现类
 */
@Service
public class StudentServiceImpl implements IStudentService {

    @Autowired
    StudentMapper studentMapper;

    @DS(DsConstant.DS2)
    @Override
    public List<Student> findAll() {
        return studentMapper.selectAll();
    }

    @DS(DsConstant.DS2)
    @Override
    public 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 {

    @Autowired
    UserMapper userMapper;

    @DS(DsConstant.DS1)
    @Override
    public List<User> findAll() {
        return userMapper.selectAll();
    }

    @DS(DsConstant.DS1)
    @Override
    public void add() {
        User user = new User();
        user.setName("张三001-" + RandomUtil.randomInt());
        user.setAge(RandomUtil.randomInt());
        user.setSex("男");
        userMapper.insertUser2(user);
    }
}

三、测试接口,发现在自定义的业务逻辑上,能够区分数据源,实现读写分离。

在这里插入图片描述
在这里插入图片描述

四、项目结构

在这里插入图片描述
源码下载地址multi-datasource,欢迎Star!

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值