Discuss / Java / 切换DataSource的实现我没看懂

切换DataSource的实现我没看懂

Topic source

通过如下的代码就可以切换RoutingDataSource底层使用的真正的DataSource

RoutingDataSourceContext.setDataSourceRoutingKey("slaveDataSource");
jdbcTemplate.query(...);

RoutingDataSourceContext 这个类只是在 ThreadLocal 储存了指定DataSource的Bean名字,@Bean("slaveDataSource") DataSource dataSource(...){...},而不是一个 DataSource 对象

public class RoutingDataSourceContext implements AutoCloseable {

    public static final String MASTER_DATASOURCE = "masterDataSource";
    public static final String SLAVE_DATASOURCE = "slaveDataSource";

    // holds data source key in thread local:
    static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();

    public static String getDataSourceRoutingKey() {
        String key = threadLocalDataSourceKey.get();
        return key == null ? MASTER_DATASOURCE : key;
    }

    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }

    public void close() {
        threadLocalDataSourceKey.remove();
    }
}

客户端代码

@Controller
public class UserController {
	@RoutingWithSlave // <-- 指示在此方法中使用slave数据库,就是他,让我看懵了的注解,都不知道怎么做到的切换数据源
	@GetMapping("/profile")
	public ModelAndView profile(HttpSession session) {
        ...
    }
}

自定义的注解 @RoutingWithSlave 的作用也只是调用 RoutingDataSourceContext 的构造方法向 ThreadLocal 储存了字符串 slaveDataSource

@Aspect
@Component
public class RoutingAspect {
    @Around("@annotation(routingWithSlave)")
    public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWithSlave routingWithSlave)
            throws Throwable {
        try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(RoutingDataSourceContext.SLAVE_DATASOURCE)) {
            return joinPoint.proceed();
        }
    }
}

当我访问 http://localhost:8080/profile 后,可以看到数据源已经切换为 HikariPool-2 不再是 HikariPool-1

2020-11-17 15:24:09.701  INFO 7056 --- [nio-8080-exec-7] com.zaxxer.hikari.HikariDataSource       : HikariPool-2 - Starting...
2020-11-17 15:24:09.703  INFO 7056 --- [nio-8080-exec-7] com.zaxxer.hikari.pool.PoolBase          : HikariPool-2 - Driver does not support get/set network timeout for connections. (feature not supported)
2020-11-17 15:24:09.706  INFO 7056 --- [nio-8080-exec-7] com.zaxxer.hikari.HikariDataSource       : HikariPool-2 - Start completed.

看源码弄懂了一点,但对执行过程还是有些不理解;

笔记区:

首先,ThreadLocal 存储的只是DataSource的Bean名字

public class RoutingDataSourceContext implements AutoCloseable {

    public static final String MASTER_DATASOURCE = "masterDataSource";
    public static final String SLAVE_DATASOURCE = "slaveDataSource";

    // holds data source key in thread local:
    static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();

    public static String getDataSourceRoutingKey() {
        String key = threadLocalDataSourceKey.get();
        return key == null ? MASTER_DATASOURCE : key;
    }

    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }

    public void close() {
        threadLocalDataSourceKey.remove();
    }
}

注入的是标记为@Primary的RoutingDataSource,数据源默认为 masterDataSource,RoutingDataSource它通过Map关联一组DataSource

public class RoutingDataSourceConfiguration {

    @Primary
    @Bean
    DataSource dataSource(@Autowired @Qualifier(RoutingDataSourceContext.MASTER_DATASOURCE) DataSource masterDataSource,
            @Autowired @Qualifier(RoutingDataSourceContext.SLAVE_DATASOURCE) DataSource slaveDataSource) {
        var ds = new RoutingDataSource();
        ds.setTargetDataSources(Map.of(RoutingDataSourceContext.MASTER_DATASOURCE, masterDataSource,
                RoutingDataSourceContext.SLAVE_DATASOURCE, slaveDataSource));  //通过Map关联一组DataSource
        ds.setDefaultTargetDataSource(masterDataSource);
        System.err.println("RoutingDataSourceConfiguration:1");  //观察执行顺序
        return ds;
    }
}

class RoutingDataSource extends AbstractRoutingDataSource {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getDataSourceRoutingKey();  //将获取到 DataSource 的Bean名字给父类 
    }

    @Override
    protected DataSource determineTargetDataSource() {
        DataSource ds = super.determineTargetDataSource();
        logger.info("determin target datasource: {}", ds);
        System.err.println("RoutingDataSource:2");    //观察执行顺序
        return ds;
    }
}

之后一直执行到return RoutingDataSourceContext.getDataSourceRoutingKey();查看该方法的源码,他拿到了 DataSource 的Bean名字和之前提供的Map,就能调用get方法得到DataSource了

protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();        
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);  //之前通过Map关联的一组DataSource将会存储在private Map<Object, DataSource> resolvedDataSources
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
}

存储过程,Map关联的 DataSource 是先赋值给 targetDataSources,再通过这个方法赋值给 resolvedDataSources

@Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

问题区:

为什么 RoutingDataSource.determineTargetDataSource() 会执行了2次,var ds = new RoutingDataSource(); 这句代码只不过是执行完了构造方法就退出了,不应该是导致这个问题的原因

RoutingDataSourceConfiguration:1
2020-11-18 00:59:11.771  INFO 5124 --- [  restartedMain] c.i.learnjava.config.RoutingDataSource   : determin target datasource: HikariDataSource (null)
RoutingDataSource:2
2020-11-18 00:59:11.851  INFO 5124 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-3 - Starting...
2020-11-18 00:59:12.120  INFO 5124 --- [  restartedMain] com.zaxxer.hikari.pool.PoolBase          : HikariPool-3 - Driver does not support get/set network timeout for connections. (feature not supported)
2020-11-18 00:59:12.171  INFO 5124 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-3 - Start completed.
2020-11-18 00:59:12.706  INFO 5124 --- [  restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-11-18 00:59:12.987  INFO 5124 --- [  restartedMain] c.i.learnjava.config.RoutingDataSource   : determin target datasource: HikariDataSource (HikariPool-3)
RoutingDataSource:2
2020-11-18 00:59:12.989  INFO 5124 --- [  restartedMain] c.i.learnjava.config.RoutingDataSource   : determin target datasource: HikariDataSource (HikariPool-3)
RoutingDataSource:2

然后就是针对这个注解@RoutingWithSlave的AOP实现切换数据源,他确实改变了存储在threadLocalDataSourceKey的值,

但我不知道他怎么让RoutingDataSource类中的所有方法再执行一次,更新原来获取的结果,当/profile被访问的时候;key值由masterDataSource变为slaveDataSource后应该重新执行RoutingDataSource类中的所有方法才会更新结果,我是这么认为的

@Aspect
@Component
public class RoutingAspect {
    @Around("@annotation(routingWithSlave)")
    public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWithSlave routingWithSlave)
            throws Throwable {
        try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(RoutingDataSourceContext.SLAVE_DATASOURCE)) {
            System.err.println("RoutingAspect:3");
            return joinPoint.proceed();
        }
    }
}

@GetMapping("/profile")
    @RoutingWithSlave
    public ModelAndView profile(HttpSession session) {
        User user = (User) session.getAttribute(KEY_USER);
        if (user == null) {
            return new ModelAndView("redirect:/signin");
        }
        // 测试是否走slave数据库:
        user = userService.getUserByEmail(user.getEmail());
        return new ModelAndView("profile.html", Map.of("user", user));
    }

理解的关键在于ThreadLocal

在进行jdbctemplate时,肯定要获取数据源,当前线程会先调用RoutingDataSourceContext的getDataSourceRoutingKey()拿到查询的key来决定选用哪个数据源。

在这之前,由于AspectJ调用了RoutingDataSourceContext的构造方法,并且调用了向threadLocalDataSourceKey塞入了"slaveDataSource",因此查询的key是由此步确定的。


  • 1

Reply