How Robustdb Enables Client‑Side Read/Write Splitting Beyond Atlas
This article explains how a company built Robustdb, a lightweight client‑side read/write splitting solution to replace Atlas, detailing its background, routing core, method‑level control, dynamic data source management, performance gains, and providing code examples for implementation.
Company DBA complained about the difficulty of using Atlas for read‑write splitting, so the team built Robustdb, a lightweight client‑side solution using only about ten classes and ~2k lines of code.
Background
As traffic grows, many companies use vertical sharding and partitioned tables, combined with read‑write splitting to alleviate load. The typical architecture places a VIP layer and a read‑write proxy (Atlas) that routes DML to the master and DQL to slaves based on configured ratios.
Atlas has several drawbacks: it is no longer maintained, lacks mapping between application IP and database IP, only routes at the SQL level, does not refresh closed connections, and is hard to extend.
Robustdb Design
To address these issues and centralize DB account and connection management, Robustdb implements client‑side routing.
1. Routing Core
Each SQL is classified as DML or DQL; DML forces the thread‑local variable to master, DQL to slave. An AspectJ interceptor reads a @DataSourceType annotation on service methods to force all SQL in the method to use the master, ensuring transactional consistency.
Thread‑local variables are stored using Alibaba’s TransmittableThreadLocal to preserve context across thread pools.
Key Classes
@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<String, Connection> 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<Connection> 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;
}
// other overridden methods omitted for brevity
}DynamicDataSource extends AbstractRoutingDataSource and implements a weighted round‑robin algorithm to select a slave.
public class DynamicDataSource extends AbstractRoutingDataSource implements InitializingBean {
private List<Object> slaveDataSources = new ArrayList<>();
private AtomicInteger counter = new AtomicInteger(-1);
private Map<Object, Integer> slaveDataSourcesWeight;
@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);
}
}Read‑Write Traffic Allocation
All connections are centrally managed; the traffic distribution to read replicas can be configured dynamically (random or sequential) via an internal configuration center (gconfig) or alternatives like Diamond.
When a new configuration arrives, the bean factory is refreshed, rebuilding the datasource map.
public void refreshDataSource(String properties) {
YamlDynamicDataSource ds = new YamlDynamicDataSource(properties);
// validation omitted
DynamicDataSource dynamic = (DynamicDataSource) beanFactory.getBean(dataSourceName);
dynamic.setResolvedDefaultDataSource(ds.getResolvedDefaultDataSource());
dynamic.setResolvedDataSources(new HashMap<>());
ds.getResolvedDataSources().forEach(dynamic::putNewDataSource);
dynamic.setSlaveDataSourcesWeight(ds.getSlaveDataSourcesWeight());
dynamic.afterPropertiesSet();
}Performance
Load testing shows Robustdb outperforms Atlas under the same workload.
References
https://tech.meituan.com/mtddl.html
https://tech.meituan.com/数据库高可用架构的演进与设计.html
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
