Robustdb: A Lightweight Client‑Side Read‑Write Splitting Solution for MySQL
This article introduces Robustdb, a compact Java library that implements client‑side read‑write splitting and method‑level routing for MySQL databases, explains its architecture, core code components, dynamic read‑pool allocation strategy, and performance comparison with Atlas, while providing practical implementation details and configuration guidance.
Company DBAs complained about the difficulty of using Atlas, prompting the development of a client‑side read‑write splitting solution called Robustdb, built with only about ten classes and roughly two thousand lines of code.
Background : As traffic grows, many companies adopt vertical sharding, partitioned tables, and read‑write splitting to handle large data volumes and high access pressure. The typical architecture uses a VIP layer for IP abstraction and a proxy layer (e.g., 360 Atlas) that routes DML to the master and DQL to read replicas, but Atlas has several drawbacks such as lack of maintenance, missing request‑IP mapping, coarse‑grained SQL control, and connection‑refresh issues.
Robustdb addresses these problems by moving the routing logic to the client, allowing fine‑grained control over master/slave selection and supporting method‑level transaction routing.
1. Core Routing Design
SQL statements are parsed to determine whether they are DML or DQL. A thread‑local variable records the desired data source type (master or slave). For method‑level routing, an AspectJ interceptor sets the data source type before method execution and clears it afterward. The thread‑local variables use Alibaba's TransmittableThreadLocal to preserve context across thread pools.
@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; } }
public final class BackendConnection extends AbstractConnectionAdapter { private AbstractRoutingDataSource abstractRoutingDataSource; private final Map connectionMap = new HashMap<>(); public BackendConnection(AbstractRoutingDataSource ds) { this.abstractRoutingDataSource = ds; } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { return getConnectionInternal(sql).prepareStatement(sql); } 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 key = abstractRoutingDataSource.determineCurrentLookupKey(); Optional cached = fetchCachedConnection(key.toString()); if (cached.isPresent()) return cached.get(); Connection conn = abstractRoutingDataSource.getTargetDataSource(key).getConnection(); conn.setAutoCommit(super.getAutoCommit()); conn.setTransactionIsolation(super.getTransactionIsolation()); connectionMap.put(key.toString(), conn); return conn; } }
public abstract class AbstractRoutingDataSource extends AbstractDataSource { private Map resolvedDataSources = new HashMap<>(); private Object defaultTargetDataSource; public DataSource determineTargetDataSource() { Object lookupKey = determineCurrentLookupKey(); DataSource ds = resolvedDataSources.get(lookupKey); if (ds == null) ds = (DataSource) defaultTargetDataSource; if (ds == null) throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); return ds; } protected abstract Object determineCurrentLookupKey(); }
public class DataSourceContextHolder { private static final TransmittableThreadLocal singleSqlContextHolder = new TransmittableThreadLocal<>(); private static final TransmittableThreadLocal multiSqlContextHolder = new TransmittableThreadLocal<>(); public static void setSingleSqlDataSourceType(String type) { singleSqlContextHolder.set(type); } public static String getSingleSqlDataSourceType() { return singleSqlContextHolder.get(); } public static void clearSingleSqlDataSourceType() { singleSqlContextHolder.remove(); } public static void setMultiSqlDataSourceType(String type) { multiSqlContextHolder.set(type); } 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())); } }
2. Dynamic Read‑Pool Allocation
Robustdb uses a custom DynamicDataSource that extends AbstractRoutingDataSource . It maintains a weighted list of slave keys and selects a slave using a round‑robin counter. The weights and slave list are refreshed at runtime from a configuration center (gconfig, Diamond, Apollo, etc.).
public class DynamicDataSource extends AbstractRoutingDataSource implements InitializingBean { private List slaveDataSources = new ArrayList<>(); private Map slaveDataSourcesWeight; private AtomicInteger counter = new AtomicInteger(-1); @Override public Object determineCurrentLookupKey() { if (DataSourceContextHolder.isSlave()) { return getSlaveKey(); } return "master"; } public Object getSlaveKey() { if (slaveDataSources.isEmpty()) return null; int index = counter.incrementAndGet() % slaveDataSources.size(); if (counter.get() > 9999) counter.set(-1); return slaveDataSources.get(index); } // afterPropertiesSet rebuilds the weighted slave list }
The refreshDataSource method parses a YAML configuration, validates required fields, rebuilds the DynamicDataSource bean, and updates the slave weight map, enabling on‑the‑fly traffic rebalancing.
public void refreshDataSource(String properties) { YamlDynamicDataSource ds = new YamlDynamicDataSource(properties); // validation omitted for brevity DynamicDataSource dynamic = (DynamicDataSource) ((DefaultListableBeanFactory) beanFactory).getBean(dataSourceName); dynamic.setResolvedDefaultDataSource(ds.getResolvedDefaultDataSource()); dynamic.setResolvedDataSources(new HashMap<>()); for (Entry e : ds.getResolvedDataSources().entrySet()) { dynamic.putNewDataSource(e.getKey(), e.getValue()); } dynamic.setSlaveDataSourcesWeight(ds.getSlaveDataSourcesWeight()); dynamic.afterPropertiesSet(); }
3. Performance Evaluation
Load‑testing shows that Robustdb delivers better throughput and lower latency than Atlas under comparable conditions. The article includes a chart illustrating the performance gains.
The article concludes with a call‑to‑action for readers to follow the public account for more architecture insights.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.