Databases 17 min read

Robustdb: A Client‑Side Read‑Write Splitting Solution for MySQL

This article introduces Robustdb, a lightweight client‑side read‑write splitting framework built with only a dozen classes, explains its architecture, routing logic, method‑level transaction handling, dynamic data‑source management, and presents performance comparisons with the legacy Atlas proxy.

Architect's Guide
Architect's Guide
Architect's Guide
Robustdb: A Client‑Side Read‑Write Splitting Solution for MySQL

Company DBAs complained that the existing Atlas proxy was hard to use, prompting the development of a new client‑side read‑write splitting solution called Robustdb. The implementation uses roughly ten classes and about two thousand lines of Java code.

Background: As business traffic grows, many companies adopt vertical database sharding and partitioned tables, and use read‑write splitting to alleviate read pressure. The typical architecture includes a VIP layer for IP abstraction and a read‑write proxy (Atlas) that routes DML to the master and DQL to slaves based on configured ratios.

Problems with Atlas include lack of maintenance, missing request‑IP mapping, coarse‑grained SQL‑level routing, stale connections after automatic closure, and difficulty extending functionality.

Robustdb addresses these issues by performing routing on the client side. Its core ideas are:

SQL type detection (DML vs DQL) to set thread‑local routing flags.

Method‑level routing using an AspectJ interceptor that sets a multi‑SQL thread‑local variable before method execution and clears it afterward.

BackendConnection that caches connections per data source and determines the target (master or slave) based on the thread‑local flags.

DynamicDataSource extending Spring's AbstractRoutingDataSource to select the current lookup key, supporting weighted round‑robin slave selection.

Key code snippets are shown below.

@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<>();
    // ... other methods omitted for brevity ...
    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();
        Optional
connectionOptional = fetchCachedConnection(dataSourceKey.toString());
        if (connectionOptional.isPresent()) {
            return connectionOptional.get();
        }
        Connection connection = abstractRoutingDataSource.getTargetDataSource(dataSourceKey).getConnection();
        connectionMap.put(dataSourceKey.toString(), connection);
        return connection;
    }
}

Thread‑local variables are implemented with Alibaba's TransmittableThreadLocal (TTL) to preserve context across thread‑pool executions. The JVM must be started with the TTL agent:

-javaagent:/{Path}/transmittable-thread-local-2.6.0-SNAPSHOT.jar

DynamicDataSource overrides determineCurrentLookupKey() to return the master key or a weighted slave key, and provides a round‑robin algorithm for slave selection.

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;
    return slaveDataSources.get(index);
}

Configuration changes are pushed via an internal gconfig center (or alternatives like Diamond/Apollo). When a new configuration arrives, the data source beans are refreshed dynamically.

public void refreshDataSource(String properties) {
    YamlDynamicDataSource dataSource = new YamlDynamicDataSource(properties);
    // validation omitted
    DynamicDataSource dynamicDataSource = (DynamicDataSource) ((DefaultListableBeanFactory) beanFactory).getBean(dataSourceName);
    dynamicDataSource.setResolvedDefaultDataSource(dataSource.getResolvedDefaultDataSource());
    dynamicDataSource.setResolvedDataSources(new HashMap<>());
    for (Entry
e : dataSource.getResolvedDataSources().entrySet()) {
        dynamicDataSource.putNewDataSource(e.getKey(), e.getValue());
    }
    dynamicDataSource.setSlaveDataSourcesWeight(dataSource.getSlaveDataSourcesWeight());
    dynamicDataSource.afterPropertiesSet();
}

Performance tests show Robustdb outperforms Atlas under certain loads, with benchmark charts illustrating lower latency and higher throughput.

References:

https://tech.meituan.com/mtddl.html

https://tech.meituan.com/数据库高可用架构的演进与设计.html

JavaperformanceSpringRead-Write Splittingdatabase routingDynamic DataSource
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.