分享一个处理读写分离延迟的方案

分享一个处理读写分离延迟的方案读写分离主要有两种实现:程序内的中间件支持,数据库代理层支持。如果使用云服务,现在很多平台的云数据库都有支持自动读写分离的版本,这无疑是更好的选

欢迎大家来到IT世界,在知识的湖畔探索吧!

读写分离主要有两种实现:程序内的中间件支持,数据库代理层支持。如果使用云服务,现在很多平台的云数据库都有支持自动读写分离的版本,这无疑是更好的选择,大大节省了开发成本。

读写分离的数据库构架一般都是一个主库和一到多个备库,备库负责读,主库负责写,由于读的操作远远多于写,多增加几个备库就可以大幅度提升性能,减轻主库的压力。但是主库的数据同步到备库是需要时间的,而这个时间其实是不确定的,也因此可能会导致我们的程序中出现数据不一致的情况,影响功能。

一般的做法是写完之后,等待一定的时间再做读取操作,但是正如上面所说的延迟的时间是不确定的,这种方案并不是很可靠。而且,程序故意延迟在也可能会导致阻塞,线程被长时间占用。

分享一个我用过的方案,设置两套连接,一个是主库的可读可写连接,一个是备库只读的连接,这样就有了两个数据源。然后,按照接口来自动切换数据源,对于一些实时性要求不高的读取数据的接口,就切换到只读数据源,实时性要求高的就切换到主库的数据源。正好对应于一个平台的前台和后台,后台多为管理员操作,需要看到实时数据,前台有很多列表和详情页提供给普通用户和游客浏览,是可以容忍延迟的。

具体的实现是自己做了个静态代理类 MultiDatasource,将数据源与线程绑定,每次切换后设置线程绑定的连接。

public class MultiDatasource implements DataSource {

    private DataSource primary;
    private DataSource secondary;
    private ThreadLocal<Mode> tl = new ThreadLocal<>();

    public MultiDatasource(final DataSource primary, final DataSource secondary) {
        this.primary = primary;
        this.secondary = secondary;
    }

    public void switchToPrimary() {
        tl.remove();
    }

    public void switchToSecondary() {
        tl.set(Mode.SECONDARY);
    }

    private boolean isUseSecondary() {
        final Mode mode = tl.get();
        return mode == Mode.SECONDARY;
    }

    @Override
    public Connection getConnection() throws SQLException {
        if (isUseSecondary()) {
            return secondary.getConnection();
        }
        return primary.getConnection();
    }

    @Override
    public Connection getConnection(final String username, final String password) throws SQLException {
        if (isUseSecondary()) {
            return secondary.getConnection(username, password);
        }
        return primary.getConnection(username, password);
    }

    @Override
    public <T> T unwrap(final Class<T> iface) throws SQLException {
        if (isUseSecondary()) {
            return secondary.unwrap(iface);
        }
        return primary.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(final Class<?> iface) throws SQLException {
        if (isUseSecondary()) {
            return secondary.isWrapperFor(iface);
        }
        return primary.isWrapperFor(iface);
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        if (isUseSecondary()) {
            return secondary.getLogWriter();
        }
        return primary.getLogWriter();
    }

    @Override
    public void setLogWriter(final PrintWriter out) throws SQLException {
        if (isUseSecondary()) {
            secondary.setLogWriter(out);
            return;
        }
        primary.setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(final int seconds) throws SQLException {
        if (isUseSecondary()) {
            secondary.setLoginTimeout(seconds);
            return;
        }
        primary.setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        if (isUseSecondary()) {
            return secondary.getLoginTimeout();
        }
        return primary.getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        if (isUseSecondary()) {
            return secondary.getParentLogger();
        }
        return primary.getParentLogger();
    }

    public enum Mode {
        PRIMARY, SECONDARY
    }
}

欢迎大家来到IT世界,在知识的湖畔探索吧!

配置 DataSource 覆盖默认。

欢迎大家来到IT世界,在知识的湖畔探索吧!@Configuration
public class DatasourceConfig {
    // 在 application.properties 根据下面的前缀配置两套数据源
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSourceProperties primaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSourceProperties secondaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    public DataSource multiDatasource() {
        final HikariDataSource primary = primaryDataSourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class).build();
        final HikariDataSource secondary = secondaryDataSourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class).build();
        return new MultiDatasource(primary, secondary);
    }
}

编写拦截器,用于自动切换数据源:

@Component
public class SecondaryDataSourceInterceptor implements HandlerInterceptor {
    /**
     * datasource.
     */
    @Autowired
    private MultiDatasource datasource;

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
            throws Exception {
        // 这里还可以考虑支持一下请求参数,允许通过参数来要求一定使用主库,从而获取最新的数据,此处省去
        datasource.switchToSecondary();
        return true;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
                                final Object handler, final Exception ex) throws Exception {
        datasource.switchToPrimary();
    }
}

最后在 mvc 的配置中设置要拦截的路径。

欢迎大家来到IT世界,在知识的湖畔探索吧!@Configuration
public class WebConfig implements WebMvcConfigurer {
 
 @Autowired
 private SecondaryDataSourceInterceptor secondaryDataSourceInterceptor;

  public void addInterceptors(final InterceptorRegistry registry) {
     // 设置要切换只读库的路径
     registry.addInterceptor(secondaryDataSourceInterceptor)
       .addPathPatterns("/product/**")
       .excludePathPatterns("/product/edit")
  }
}

这个方案的缺点是比较的繁琐,数据源要配置两套,还要精细配置要切换的路径。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/36545.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信