欢迎大家来到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