为什么需要读写分离?
为了承载更多用户、提升响应速度或分摊数据库压力,这些回答都不算错。但「读写分离」并非万能药,性能提升往往有限。从单库到读写分离,理论上服务器压力减半,但应用层真的需要这一倍提升吗?在硬件性能廉价且缓存系统(如 Redis)能带来几十倍提升的今天,读写分离更多是为了数据安全备份。
更准确地说,这应被称为「主从分离」。读操作连接从库,写操作连接主库,这是基础原则。
基于 AOP 的动态数据源切换方案
直接在代码中硬编码切换逻辑虽然简单,但缺乏扩展性。有追求的开发者倾向于利用 Spring AOP 的特性,在方法执行前根据业务动态切换数据源。这种方式无需侵入业务代码,未来若取消读写分离,只需移除 AOP 配置即可,维护成本更低。
核心思路是利用 ThreadLocal 存储当前线程的数据源标识,配合 AbstractRoutingDataSource 进行路由分发,并通过自定义注解标记需要特定数据源的方法。
核心实现组件
JdbcContextHolder 上下文管理
使用 ThreadLocal 确保同一请求线程内数据源标识的一致性。
public class JdbcContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setJdbcType(String jdbcType) {
contextHolder.set(jdbcType);
}
public static void setSlave() {
setJdbcType("slave");
}
public static void setMaster() {
clearJdbcType();
}
public static String getJdbcType() {
return contextHolder.get();
}
public static void clearJdbcType() {
contextHolder.remove();
}
}
DynamicDataSource 路由策略
继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法以获取当前线程的数据源标识。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import cn.mayongfa.common.JdbcContextHolder;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return JdbcContextHolder.getJdbcType();
}
}
枚举与注解定义
通过枚举明确数据源类型,避免字符串硬编码。
public enum DataSourceType {
Master("master"),
Slave("slave");
private final String name;
DataSourceType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
自定义注解用于标记方法所需的数据源,默认值为 Master。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSource {
DataSourceType value() default DataSourceType.Master;
}
切面拦截逻辑
通过 AOP 拦截带有 @DataSource 注解的方法,在方法执行前设置数据源上下文。
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import cn.mayongfa.common.JdbcContextHolder;
public class DataSourceChoose {
public void before(JoinPoint point) {
Object target = point.getTarget();
String methodName = point.getSignature().getName();
Class<?>[] interfaces = target.getClass().getInterfaces();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Class<?>[] parameterTypes = methodSignature.getMethod().getParameterTypes();
try {
if (interfaces.length > 0) {
Method m = interfaces[0].getMethod(methodName, parameterTypes);
if (m != null && m.isAnnotationPresent(DataSource.class)) {
DataSource data = m.getAnnotation(DataSource.class);
JdbcContextHolder.clearJdbcType();
JdbcContextHolder.setJdbcType(data.value().getName());
}
}
} catch (Exception e) {
// 处理异常,建议记录日志
}
}
}
注意:上述反射逻辑假设目标对象实现了接口。若直接操作类,需调整 getInterfaces() 为 getMethods() 遍历查找。实际运行时请确保注解位置正确,并在切面配置中引用该拦截器。
