手撕读写分离

目标

横向水平分库+读写分离

方案

方案选择

老吕知道的方案有三种:

1、在应用层通过AOP实现,有少许代码耦合,但是可控性高,灵活

2、通过分库中间件代理实现

3、通过云厂商一键实现(和2同属代理模式,通过SQL分流实现)

我们今天手撕下第一种方案

整体方案

Spring 动态数据源+注解+AOP

数据源命名

d5f0db99e2d6c2f89406378a07624082.png

方案示意图

6bcb9bf572a37edbc41c2820fe3372d3.png

实现

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、读写分离的同时也会引入 主从库数据一致性问题,这个数据同步的过程一定是有时间延迟的,需要考虑业务场景是否能接受,不能接受的如何解决

关注我,公众号“老吕架构”

60281608ef211c08eaea1ac428426433.png

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕哥架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值