Read‑Write Splitting in Database Applications: Implementation Methods, Data Lag, and Routing Strategies
This article explains the concept of read‑write splitting for databases, compares client‑side and proxy implementations, discusses data lag and forced routing techniques, and provides code examples and custom load‑balancing algorithms using Sharding‑JDBC and Spring.
Implementation Methods
Read‑write splitting can be implemented in two main ways: a client‑side approach and a proxy approach.
The client‑side method can be built using Spring's AbstractRoutingDataSource or third‑party frameworks such as Sharding‑JDBC.
The proxy method involves writing a proxy service that manages all database nodes, allowing the application to remain unaware of multiple data sources; this can be custom‑built, use open‑source frameworks, or rely on commercial cloud services.
Data Lag
Understanding the master‑slave architecture is essential: write operations occur on the master, while reads are directed to slaves. Immediately after a write, the data may not yet be synchronized to the slaves, leading to stale reads.
For example, after publishing an article, the newly posted entry might not appear on the list page until the page is refreshed, illustrating the effect of data lag in a read‑write split setup.
Forced Routing
If a business scenario requires real‑time data, the only reliable solution is to force reads to the master.
Forced routing can be achieved using hint syntax such as FORCE_MASTER or FORCE_SLAVE supported by many middleware components.
In Sharding‑JDBC, the HintManager provides this capability:
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();It is recommended to encapsulate this logic in a custom annotation and apply it to methods that require real‑time queries.
Annotation usage:
@MasterRoute
@Override
public UserBO getUser(Long id) {
log.info("查询用户 [{}]", id);
if (id == null) {
throw new BizException(ResponseCode.PARAM_ERROR_CODE, "id不能为空");
}
UserDO userDO = userDao.getById(id);
if (userDO == null) {
throw new BizException(ResponseCode.NOT_FOUND_CODE);
}
return userBoConvert.convert(userDO);
}Aspect setting:
@Aspect
public class MasterRouteAspect {
@Around("@annotation(masterRoute)")
public Object aroundGetConnection(final ProceedingJoinPoint pjp, MasterRoute masterRoute) throws Throwable {
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
try {
return pjp.proceed();
} finally {
hintManager.close();
}
}
}Transaction Operations
Within a transaction, the safest approach is to route all operations to the master to guarantee data consistency, especially when a write is followed by a read before the transaction commits.
Sharding‑JDBC also offers a more nuanced behavior: for the same thread and the same database connection, if a write occurs, subsequent reads are automatically routed to the master, while reads before any write can still use slaves, reducing master load.
Dynamic Forced Routing
Hard‑coding forced routing in annotations requires code changes and redeployment when new endpoints need master reads. A dynamic approach can leverage a configuration center to decide routing at runtime, setting hints in a filter without restarting the application.
Dynamic routing can also be achieved with aspect‑oriented programming to apply routing decisions at the business‑method level.
Traffic Distribution
Scenario 1: With one master and two slave nodes, read traffic may overload the slaves. Adding a third slave can relieve pressure, but if the master’s load is low, it might be unnecessary to add more slaves.
Scenario 2: When slaves have heterogeneous resources (e.g., an 8‑core/64 GB slave versus a 4‑core/32 GB slave), without custom traffic distribution, the weaker slave can become a bottleneck. Proper routing rules are needed to balance load according to each node’s capacity.
Sharding‑JDBC provides a read‑write split routing algorithm that can be customized to manage traffic distribution.
Implementation of a custom load‑balance algorithm:
public class KittyMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {
private RoundRobinMasterSlaveLoadBalanceAlgorithm roundRobin = new RoundRobinMasterSlaveLoadBalanceAlgorithm();
@Override
public String getDataSource(String name, String masterDataSourceName, List
slaveDataSourceNames) {
String dataSource = roundRobin.getDataSource(name, masterDataSourceName, slaveDataSourceNames);
// control logic, e.g., assign different ratios to different slaves
return dataSource;
}
@Override
public String getType() {
return "KITTY_ROUND_ROBIN";
}
@Override
public Properties getProperties() {
return roundRobin.getProperties();
}
@Override
public void setProperties(Properties properties) {
roundRobin.setProperties(properties);
}
}SPI configuration entries:
org.apache.shardingsphere.core.strategy.masterslave.RoundRobinMasterSlaveLoadBalanceAlgorithm
org.apache.shardingsphere.core.strategy.masterslave.RandomMasterSlaveLoadBalanceAlgorithm
com.cxytiandi.kitty.db.shardingjdbc.algorithm.KittyMasterSlaveLoadBalanceAlgorithmRead‑write splitting configuration in Spring:
spring.shardingsphere.masterslave.load-balance-algorithm-class-name=com.cxytiandi.kitty.db.shardingjdbc.algorithm.KittyMasterSlaveLoadBalanceAlgorithm
spring.shardingsphere.masterslave.load-balance-algorithm-type=KITTY_ROUND_ROBINAbout the author: Yin Jihuan, a technology enthusiast, author of "Spring Cloud Microservices – Full‑Stack Technology and Case Analysis" and "Spring Cloud Microservices: Beginner, Practice and Advanced", and founder of the public account "Yuan Tiandi".
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
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.