1、背景
大多数系统都是读多写少,为了降低数据库的压力,可以对主库创建多个从库,从库自动从主库同步数据,程序中将写的操作发送到主库,将读的操作发送到从库去执行。
今天的主要目标:通过 spring 实现读写分离。
读写分离需实现下面 2 个功能:
1、读的方法,由调用者来控制具体是读从库还是主库
2、有事务的方法,内部的所有读写操作都走主库
本文分享给需要面试刷题的朋友,整理了面试资料这份资料主要包含了Java基础,数据结构,jvm,多线程等等,由于篇幅有限,以下只展示小部分面试题,
需要完整版的朋友可以点一点领取:戳这里即可领取下面资料,获取码:优快云
2、思考 3 个问题
1、读的方法,由调用者来控制具体是读从库还是主库,如何实现?
可以给所有读的方法添加一个参数,来控制读从库还是主库。
2、数据源如何路由?
spring-jdbc 包中提供了一个抽象类:AbstractRoutingDataSource,实现了 javax.sql.DataSource 接口,我们用这个类来作为数据源类,重点是这个类可以用来做数据源的路由,可以在其内部配置多个真实的数据源,最终用哪个数据源,由开发者来决定。
AbstractRoutingDataSource 中有个 map,用来存储多个目标数据源
private Map<Object, DataSource> resolvedDataSources;
比如主从库可以这么存储
resolvedDataSources.put(“master”,主库数据源);
resolvedDataSources.put(“salave”,从库数据源);
AbstractRoutingDataSource 中还有抽象方法determineCurrentLookupKey,将这个方法的返回值作为 key 到上面的 resolvedDataSources 中查找对应的数据源,作为当前操作 db 的数据源
protected abstract Object determineCurrentLookupKey();
3、读写分离在哪控制?
读写分离属于一个通用的功能,可以通过 spring 的 aop 来实现,添加一个拦截器,拦截目标方法的之前,在目标方法执行之前,获取一下当前需要走哪个库,将这个标志存储在 ThreadLocal 中,将这个标志作为 AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,拦截器中在目标方法执行完毕之后,将这个标志从 ThreadLocal 中清除。
3、代码实现
3.1、工程结构图
3.2、DsType
表示数据源类型,有 2 个值,用来区分是主库还是从库。
package com.javacode2018.readwritesplit.base;
public enum DsType {
MASTER, SLAVE;
}
3.3、DsTypeHolder
内部有个 ThreadLocal,用来记录当前走主库还是从库,将这个标志放在 dsTypeThreadLocal 中
package com.javacode2018.readwritesplit.base;
public class DsTypeHolder {
private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>();
public static void master() {
dsTypeThreadLocal.set(DsType.MASTER);
}
public static void slave() {
dsTypeThreadLocal.set(DsType.SLAVE);
}
public static DsType getDsType() {
return dsTypeThreadLocal.get();
}
public static void clearDsType() {
dsTypeThreadLocal.remove();
}
}
3.4、IService 接口
这个接口起到标志的作用,当某个类需要启用读写分离的时候,需要实现这个接口,实现这个接口的类都会被读写分离拦截器拦截。
package com.javacode2018.readwritesplit.base;
//需要实现读写分离的service需要实现该接口
public interface IService {
}
3.5、ReadWriteDataSource
读写分离数据源,继承 ReadWriteDataSource,注意其内部的 determineCurrentLookupKey 方法,从上面的 ThreadLocal 中获取当前需要走主库还是从库的标志。
package com.javacode2018.readwritesplit.base;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
public class ReadWriteDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DsTypeHolder.getDsType();
}
}
3.6、ReadWriteInterceptor
读写分离拦截器,需放在事务拦截器前面执行,通过@1 代码我们将此拦截器的顺序设置为 Integer.MAX_VALUE - 2,稍后我们将事务拦截器的顺序设置为 Integer.MAX_VALUE - 1,事务拦截器的执行顺序是从小到达的,所以,ReadWriteInterceptor 会在事务拦截器 org.springframework.transaction.interceptor.TransactionInterceptor 之前执行。
由于业务方法中存在相互调用的情况,比如 service1.m1 中调用 service2.m2,而 service2.m2 中调用了 service2.m3,我们只需要在 m1 方法执行之前,获取具体要用哪个数据源就可以了,所以下面代码中会在第一次进入这个拦截器的时候,记录一下走主库还是从库。
下面方法中会获取当前目标方法的最后一个参数,最后一个参数可以是 DsType 类型的,开发者可以通过这个参数来控制具体走主库还是从库。
package com.javacode2018.readwritesplit.base;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype