前言

有时候为了减少数据库的压力,就要实现数据库的读写分离,这种情况往往是读多写少的情况,例如电商平台。既然数据库读写分离了,那么代码层也就需要读写不同的数据库了。实现方法应该有不少,我知道有插件实现,判断写请求还是读请求来请求不同的数据库,还有代码实现,不同的SQL访问不同的数据源,也就是接下来要说的多数据源。

一、基础介绍

代码层多数据源的实现方法也有很多,例如不同的包拥有不同的数据源、AOP实现动态切换数据源等等。目前我使用的方法为AOP动态切换数据源方法,这种方法可以自定义注解,注解在controller层,根据不同的方法访问不同的数据源。主要实现方法就是先实例化一批数据源实例Map集合,key为实例名称,value为示例,AOP以自定义的注解为切点,根据注解的value读取是哪个数据库的数据源实例key,来获取指定的数据源。

二、配置多个数据源

首先,定义一个枚举来说明一下当前数据源实例key有哪些。

public enum DataSourceKey {
	MASTER("master"),
	SLAVE("slave");
	
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	private DataSourceKey(String name) {
		this.name = name;
	}
}

其次,使用ThreadLocal存储当前使用数据源实例的key。ThreadLocal实例化的时候给一个master的默认值,也就是默认数据源是master数据源。

public class DynamicDataSourceContextHolder {
	
	private static ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceKey.MASTER.getName());
	
	public static List<Object> dataSourceKeys = new ArrayList<Object>();
	
	public static void setDataSourceKey(String key){
		CONTEXT_HOLDER.set(key);
	}
	
	public static Object getDataSourceKey(){
		return CONTEXT_HOLDER.get();
	}
	
	public static void clearDataSourceKey(){
		CONTEXT_HOLDER.remove();
	}
	
	public static Boolean containDataSourceKey(String key){
		return dataSourceKeys.contains(key);
	}
		
}

重写AbstractRoutingDataSource的determineCurrentLookupKey方法,在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key。

/**
 * 该类继承自 AbstractRoutingDataSource 类,
 * 在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

	private static final Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
	
	@Override
	protected Object determineCurrentLookupKey() {
		logger.info("current datasource is : {}",DynamicDataSourceContextHolder.getDataSourceKey());
		return DynamicDataSourceContextHolder.getDataSourceKey();
	}

}

然后,需要实例化多个DataSource的Bean放在ApplicationContext中,这里我们使用druid的实现,定义好每个Bean的名字,名字要和枚举的name一致,

@primary的意思就是默认使用这个Bean

dynamicDataSource需要一个存储数据库实例的Map集合

sqlSessionFactoryBean可以配置mybatis的相关配置

transactionManager是事务配置

/**
 * 数据源配置类
 * 在该类中生成多个数据源实例并将其注入到 ApplicationContext 中
 * 2019年6月4日 上午10:06:54
 */
@Configuration
public class DataSourceConfigurer {

    /**
     * 配置数据源
     * @return
     */
    @Bean(name = "master")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource master() {
    	return DruidDataSourceBuilder.create().build();
    }
    
    /**
     * 配置数据源
     * @return
     */
    @Bean(name = "slave")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource slave() {
    	return DruidDataSourceBuilder.create().build();
    }
    
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(){
    	DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
    	
    	Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(2);
    	dataSourceMap.put(DataSourceKey.MASTER.getName(),master());
    	dataSourceMap.put(DataSourceKey.SLAVE.getName(),slave());
    	
    	dynamicRoutingDataSource.setDefaultTargetDataSource(master());
    	dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
    	
    	DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
    
    	return dynamicRoutingDataSource;
    }
    
    @Bean
    @ConfigurationProperties(prefix = "mybatis")
    public SqlSessionFactoryBean sqlSessionFactoryBean(){
    	SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    	
    	org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
    	configuration.setMapUnderscoreToCamelCase(true); //下划线转骆驼
    	
    	sqlSessionFactoryBean.setDataSource(dynamicDataSource());
    	sqlSessionFactoryBean.setConfiguration(configuration);
    	
    	return sqlSessionFactoryBean;
    }
    
    /**
     * 事务
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(){
    	return new DataSourceTransactionManager(dynamicDataSource());
    }
	
}

三、自定义注解

为了使用AOP代理特定的方法,我们就可以自定义注解。这个注解很简单,也没啥太大含义。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
	
	DataSourceKey value() default DataSourceKey.MASTER;
}

四、AOP动态切换数据源

使用AOP,以自定义注解注解在的方法为切点,动态切换数据源

@Aspect
@Component
public class DynamicDataSourceAspect {

	private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
	
	@Before("@annotation(targetDataSource))")
	public void switchDataSource(JoinPoint joinPoint,TargetDataSource targetDataSource){
		if ( !DynamicDataSourceContextHolder.containDataSourceKey( targetDataSource.value().getName() ) ) {
			logger.error("DataSource [{}] doesn't exist, use default DataSource [{}]", targetDataSource.value());
        } else {
            DynamicDataSourceContextHolder.setDataSourceKey( targetDataSource.value().getName() );
            logger.info("Switch DataSource to [{}] in Method [{}]",
                    DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
        }
	}
	
	@After("@annotation(targetDataSource))")
	public void restoreDataSource(JoinPoint joinPoint,TargetDataSource targetDataSource){
        DynamicDataSourceContextHolder.clearDataSourceKey();
        logger.info("Restore DataSource to [{}] in Method [{}]",
                DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
	}
	
}

五、使用

在controller层使用,在不同的方法上添加自定义注解来使用不同的数据源。MASTER是默认数据源,可以不使用注解。

@RestController
public class UserController {

	@Autowired
	private UserService userService;
	
	@GetMapping("/user")
	@TargetDataSource(DataSourceKey.MASTER)
	public ResponseEntity<Object> listAll(){
		
		List<JSONObject> result = userService.listAll();
		
		return ResponseEntity.ok(result);
		
	}
	
	@PostMapping("/user")
	@TargetDataSource(DataSourceKey.SLAVE)
	public ResponseEntity<Object> listAlla(){
		
		List<JSONObject> result = userService.listAll();
		
		return ResponseEntity.ok(result);
		
	}
	
	
	
}

写在最后的话

数据库主从同步可以参考网上的实现,我暂时还没实现,这个后面再说。代码的多数据源我自己测试过了。我也是看了别人写的,然后自己敲了一遍,记录一下。如果我有幸被你搜到了这篇博客,可以留言讨论。

GitHub 加速计划 / druid / druid
27.83 K
8.56 K
下载
阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品,为监控而生的数据库连接池
最近提交(Master分支:2 个月前 )
f060c270 - 3 天前
1613a765 * Improve gaussdb ddl parser * fix temp table 5 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐