Robustdb: A Lightweight Read‑Write Splitting Solution for Java Applications
This article introduces Robustdb, a compact open‑source read‑write separation framework built with about ten classes and two thousand lines of code, explains its architecture, routing logic, method‑level transaction handling, dynamic data‑source management, and performance advantages over existing solutions like Atlas.
Background – As business traffic grows, many companies adopt read‑write splitting to alleviate database load, typically using an intermediate proxy layer such as 360 Atlas. However, Atlas has several drawbacks, including lack of maintenance, limited routing granularity, and difficulty extending functionality.
Robustdb Design – To overcome Atlas limitations, Robustdb implements client‑side read‑write splitting with a simple architecture consisting of a VIP layer, a routing proxy, and a set of read/write databases. The core idea is routing each SQL statement based on its type (DML or DQL) to the appropriate data source.
1. Routing Core
Every SQL is parsed; DML statements are directed to the master, while DQL statements are sent to a slave according to a configurable weight‑based algorithm. Method‑level routing is achieved via an AspectJ interceptor that sets a thread‑local data‑source type before method execution and clears it afterward.
@Aspect
@Component
public class DataSourceAspect {
@Around("execution(* *(..)) && @annotation(dataSourceType)")
public Object aroundMethod(ProceedingJoinPoint pjd, DataSourceType dataSourceType) throws Throwable {
DataSourceContextHolder.setMultiSqlDataSourceType(dataSourceType.name());
Object result = pjd.proceed();
DataSourceContextHolder.clearMultiSqlDataSourceType();
return result;
}
}BackendConnection – Provides a connection that determines the target data source at the moment a SQL is prepared, caching connections per data source.
public final class BackendConnection extends AbstractConnectionAdapter {
private AbstractRoutingDataSource abstractRoutingDataSource;
private final Map
connectionMap = new HashMap<>();
// ... constructor and overridden prepareStatement methods ...
private Connection getConnectionInternal(final String sql) throws SQLException {
if (ExecutionEventUtil.isDML(sql)) {
DataSourceContextHolder.setSingleSqlDataSourceType(DataSourceType.MASTER);
} else if (ExecutionEventUtil.isDQL(sql)) {
DataSourceContextHolder.setSingleSqlDataSourceType(DataSourceType.SLAVE);
}
Object dataSourceKey = abstractRoutingDataSource.determineCurrentLookupKey();
String dataSourceName = dataSourceKey.toString();
Optional
connectionOptional = fetchCachedConnection(dataSourceName);
if (connectionOptional.isPresent()) {
return connectionOptional.get();
}
Connection connection = abstractRoutingDataSource.getTargetDataSource(dataSourceKey).getConnection();
connection.setAutoCommit(super.getAutoCommit());
connection.setTransactionIsolation(super.getTransactionIsolation());
connectionMap.put(dataSourceKey.toString(), connection);
return connection;
}
// ... other methods ...
}AbstractRoutingDataSource – Extends Spring’s routing data source, adding support for dynamic slave selection and weight‑based distribution.
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
private boolean lenientFallback = true;
private Map
targetDataSources;
private Object defaultTargetDataSource;
private Map
resolvedDataSources = new HashMap<>();
// ... getConnection methods returning BackendConnection ...
public DataSource determineTargetDataSource() {
Object lookupKey = determineCurrentLookupKey();
DataSourceContextHolder.clearSingleSqlDataSourceType();
DataSource dataSource = resolvedDataSources.get(lookupKey);
if (dataSource == null && (lenientFallback || lookupKey == null)) {
dataSource = resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
public abstract Object determineCurrentLookupKey();
}DataSourceContextHolder – Uses TransmittableThreadLocal to store single‑SQL and multi‑SQL routing information, ensuring thread‑safety even when thread pools are used.
public class DataSourceContextHolder {
private static final TransmittableThreadLocal
singleSqlContextHolder = new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal
multiSqlContextHolder = new TransmittableThreadLocal<>();
public static void setSingleSqlDataSourceType(String dataSourceType) { singleSqlContextHolder.set(dataSourceType); }
public static String getSingleSqlDataSourceType() { return singleSqlContextHolder.get(); }
public static void clearSingleSqlDataSourceType() { singleSqlContextHolder.remove(); }
public static void setMultiSqlDataSourceType(String dataSourceType) { multiSqlContextHolder.set(dataSourceType); }
public static String getMultiSqlDataSourceType() { return multiSqlContextHolder.get(); }
public static void clearMultiSqlDataSourceType() { multiSqlContextHolder.remove(); }
public static boolean isSlave() {
return "slave".equals(multiSqlContextHolder.get()) || (multiSqlContextHolder.get() == null && "slave".equals(singleSqlContextHolder.get()));
}
}DynamicDataSource – Implements the actual routing logic, selecting a slave based on a round‑robin counter and the configured weights. It also supports runtime updates of data‑source configurations via a custom configuration center (gconfig).
public class DynamicDataSource extends AbstractRoutingDataSource implements InitializingBean {
private Integer slaveCount = 0;
private AtomicInteger counter = new AtomicInteger(-1);
private List
slaveDataSources = new ArrayList<>();
private Map
slaveDataSourcesWeight;
private Object currentSlaveKey;
@Override
public Object determineCurrentLookupKey() {
if (DataSourceContextHolder.isSlave()) {
currentSlaveKey = getSlaveKey();
return currentSlaveKey;
}
return "master";
}
public Object getSlaveKey() {
if (slaveCount <= 0) return null;
int index = counter.incrementAndGet() % slaveCount;
if (counter.get() > 9999) counter.set(-1);
return slaveDataSources.get(index);
}
// afterPropertiesSet populates slaveDataSources based on weights ...
}Dynamic Configuration Refresh – When the configuration center pushes new settings, Robustdb parses the YAML, validates required fields, rebuilds the data‑source map, and updates the Spring bean factory to apply the changes without restarting the application.
public void refreshDataSource(String properties) {
YamlDynamicDataSource dataSource = new YamlDynamicDataSource(properties);
// validation omitted for brevity
ConcurrentHashMap
newDataSource = new ConcurrentHashMap<>(dataSource.getResolvedDataSources());
DynamicDataSource dynamicDataSource = (DynamicDataSource) ((DefaultListableBeanFactory) beanFactory).getBean(dataSourceName);
dynamicDataSource.setResolvedDefaultDataSource(dataSource.getResolvedDefaultDataSource());
dynamicDataSource.setResolvedDataSources(new HashMap<>());
for (Entry
e : newDataSource.entrySet()) {
dynamicDataSource.putNewDataSource(e.getKey(), e.getValue());
}
dynamicDataSource.setSlaveDataSourcesWeight(dataSource.getSlaveDataSourcesWeight());
dynamicDataSource.afterPropertiesSet();
}Performance Evaluation – Benchmarks show that Robustdb achieves lower latency and higher throughput than Atlas under comparable workloads, confirming the efficiency of its lightweight routing and dynamic load‑balancing mechanisms.
Overall, Robustdb provides a concise, extensible, and high‑performance read‑write splitting framework suitable for Java/Spring ecosystems, addressing common limitations of existing proxy‑based solutions.
IT Xianyu
We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.