目标
横向水平分库+读写分离
方案
方案选择
老吕知道的方案有三种:
1、在应用层通过AOP实现,有少许代码耦合,但是可控性高,灵活
2、通过分库中间件代理实现
3、通过云厂商一键实现(和2同属代理模式,通过SQL分流实现)
我们今天手撕下第一种方案
整体方案
Spring 动态数据源+注解+AOP
数据源命名
方案示意图
实现
1、动态数据源
/**
* @Title 动态数据源
* @Description
* @Author lvaolin
* @Date 2022/2/19
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TraceUtil.getReadonly()?TraceUtil.getDbKeyReadonly():TraceUtil.getDbKeyMaster();
}
}
2、配置多数据源
/**
* 动态数据源与mybatis集成
*/
@Configuration
public class MyBatisConfig {
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dynamicDataSource.setDefaultTargetDataSource(getNewDataSourceInstance());
dataSourceMap.put("biz-ds1",getNewDataSourceInstance());
dataSourceMap.put("biz-ds1-readonly1",getNewDataSourceInstance());
dataSourceMap.put("biz-ds1-readonly2",getNewDataSourceInstance());
dataSourceMap.put("biz-ds2",getNewDataSourceInstance());
dataSourceMap.put("biz-ds2-readonly1",getNewDataSourceInstance());
dataSourceMap.put("biz-ds2-readonly2",getNewDataSourceInstance());
dynamicDataSource.setTargetDataSources(dataSourceMap);
dynamicDataSource.afterPropertiesSet();
return dynamicDataSource;
}
@Bean("dsReadOnlyKeyMapping")
public Map<String, String> dsReadOnlyKeyMapping(){
//主库名称与只读库名称的映射
HashMap<String, String> map = new HashMap<>();
map.put("biz-ds1","biz-ds1-readonly1,biz-ds1-readonly2");
map.put("biz-ds2","biz-ds2-readonly1,biz-ds2-readonly2");
return map;
}
@Bean("lazyDataSource")
public DataSource lazyDataSource() {
LazyConnectionDataSourceProxy proxy = new LazyConnectionDataSourceProxy();
proxy.setTargetDataSource(dynamicDataSource());
proxy.afterPropertiesSet();
return proxy;
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(lazyDataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
return sessionFactory.getObject();
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.dhy.demo.spring.mybatis.infrastructure.datebase.mybatis.mapper");
return mapperScannerConfigurer;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(lazyDataSource());
dataSourceTransactionManager.setNestedTransactionAllowed(false);
return dataSourceTransactionManager;
}
private DruidDataSource getNewDataSourceInstance(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&serverTimezone=UTC&useUnicode=true");
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
return druidDataSource;
}
}
3、注解定义
/**
* @Title 只读数据源注解
* @Description
* @Author lvaolin
* @Date 2022/2/19 18:19
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DsReadonly {
String value() default "";
}
4、aop实现与注解解析
/**
* @Title service层aop
* @Description
* @Author lvaolin
* @Date 2022/2/19 18:30
**/
@Component
@Aspect
public class ServiceAop {
@Resource(name = "dsReadOnlyKeyMapping")
private Map<String,String> dsReadOnlyKeyMapping;
//只拦截带 DsReadonly 注解的方法
@Pointcut("@annotation(DsReadonly)")
public void pc(){}
//上下文绑定与只读库负载均衡
@Before("pc()")
public void before(JoinPoint joinPoint){
String readonlyStr = dsReadOnlyKeyMapping.get("biz-ds1");
if (readonlyStr!=null) {
String[] readonlys = readonlyStr.split(",");
TraceUtil.setReadonly(true);
TraceUtil.setDbKeyReadonly(readonlys[ThreadLocalRandom.current().nextInt(readonlys.length)]);
}
}
//恢复上下文
@After("pc()")
public void after(){
TraceUtil.setReadonly(false);
TraceUtil.setDbKeyReadonly(null);
}
}
5、上下文
/**
* 上下文
*/
public class TraceUtil {
private static Logger log = LoggerFactory.getLogger(TraceUtil.class);
private static final InheritableThreadLocal<MyContext> myContext = new InheritableThreadLocal(){
@Override
protected Object initialValue() {
return new MyContext();
}
};
public static String getTokenStr() {
return myContext.get().token;
}
public static void setTokenStr(String tokenStr) {
myContext.get().token = tokenStr;
}
public static void setDbKeyMaster(String dbKey) {
myContext.get().dbKeyMaster = dbKey;
}
public static String getDbKeyMaster() {
return myContext.get().dbKeyMaster;
}
public static void setDbKeyReadonly(String dbKey) {
myContext.get().dbKeyReadonly = dbKey;
}
public static String getDbKeyReadonly() {
return myContext.get().dbKeyReadonly;
}
public static void setReadonly(boolean readonly) {
myContext.get().readonly = readonly;
}
public static boolean getReadonly() {
return myContext.get().readonly;
}
static class MyContext{
public String token;
public String dbKeyMaster;
public String dbKeyReadonly;
public boolean readonly;
}
}
测试
测试结果如下,能正确按注解切换数据源,只读库多个时能负载均衡
方法selectAll, 只读:true,只读库:biz-ds1-readonly1
方法insert,只读:false
方法insert,只读:false
方法selectCount, 只读:true,只读库:biz-ds1-readonly2
方法selectCount, 只读:true,只读库:biz-ds1-readonly1
方法selectCount, 只读:true,只读库:biz-ds1-readonly2
方法selectCount, 只读:true,只读库:biz-ds1-readonly1
方法selectCount, 只读:true,只读库:biz-ds1-readonly1
方法selectCount, 只读:true,只读库:biz-ds1-readonly2
方法selectCount, 只读:true,只读库:biz-ds1-readonly2
方法selectCount, 只读:true,只读库:biz-ds1-readonly2
总结
1、本文从服务层实现了 读写分离,把读写分离的权力交给了开发人员,哪些方法可以走只读库就加注解
2、读写分离,一主多从 是针对读多写少业务场景的常用优化手段,可以显著提高系统的查询性能
3、读写分离的同时也会引入 主从库数据一致性问题,这个数据同步的过程一定是有时间延迟的,需要考虑业务场景是否能接受,不能接受的如何解决
关注我,公众号“老吕架构”