一、spring+mybatis部分原理
mybatis中我们将数据源定义为dataSource,每次数据库操作都会去获取一个Connection来执行sql,实现数据库读写分离即让程序按照我们的需求从不同的数据源去获取Connection。
1.执行单个dao的sql时数据库操作
执行单个sql的时候的代码底层会判断当前线程对应的DataSource是否有开启事务。
有开启事务:优先从TransactionSynchronizationManager类的线程本地变量ThreadLocal中去获取Connection,如果从ThreadLocal中没有获取到会直接从DataSource中去获取一个新的Connection并保存到ThreadLocal。
没有开启事务:直接从DataSource中获取Connection并返回。
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = dataSource.getConnection();
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
}
2.Spring的@Translational声明式事务时的数据库操作
事务管理器配置:
当使用Spring的事务管理时,通过Aop切面,执行注解了@Translational的事务方法前会进入到我们配置的DataSourceTransactionManager类的doBegin(…)方法中,本方法会尝试从TransactionSynchronizationManager的ThreadLocal中去获取DataSource对应的Connection,如果为空则从DataSource中获取一个新的Connection并放到ThreadLocal中,同时也会设置connection的autoCommit为false。
部分源码如下:
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
if (txObject.getConnectionHolder() == null ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = this.dataSource.getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
}
}
总结:以上就是目前单库的connection获取流程,如果方法通过Transactional开启了事务,方法执行前会获取一个Connection,设置autoCommit为false并保存到ThreadLocal中,方法执行过程中的任何一个sql都会获取复用该Connection。对于不在事务的sql则会每次都获取一个Connection。
二、几种实现方案及优缺点
1.主库sql和从库sql放到不同的包名下配置不同的数据源
将更新操作和查询操作的sql分开写并分别放到queryDao、updateDao目录下, 不同的目录配置不同的dataSource来控制queryDao下的sql走从库,updateDao下的sql走主库。事务配置使用主库。
优点:逻辑简单易控制,不容易出问题。
缺点:1.对于旧系统,改动比较多, 2.如果需要强制查询主库,可能要写一个SelectFromMasterDao放在updateDao包下,并且可能会存在重复代码。
2.mybatis插件分析sql来决定数据源
通过mybatis插件拦截sql执行,通过对每条sql的xml配置解析来判断走主库还是从库。将主库或者从库的对应枚举设置到全局的ThreadLocal中,结合AbstractRoutingDataSource通过读取当前线程的数据源类型来控制执行sql时采用哪一个DataSource,sql完成之后清除ThreadLocal。
配置mybatis拦截器:
mybatis插件配置
mybatis拦截器
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) })
public class DynamicSourceInterceptor implements Interceptor {
protected static final Logger logger = LoggerFactory.getLogger(DynamicSourceInterceptor.class);
private static final String REGEX = "\\s*insert\\u0020.*|\\s*delete\\u0020.*|\\s*update\\u0020.*";
private static final Map cacheMap = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
DynamicDataSourceGlobal dynamicDataSourceGlobal = cacheMap.get(ms.getId());
if(dynamicDataSourceGlobal == null) {
//读方法
if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
//!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
} else {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
if(sql.matches(REGEX)) {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
} else {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
}
}
}else{
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
}
cacheMap.put(ms.getId(), dynamicDataSourceGlobal);
}
DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
}
try {
Object result = invocation.proceed();
DynamicDataSourceHolder.clearDataSource();
return result;
} catch (Exception e) {
logger.error("执行sql报错, invocation:{}, error:{}", invocation, e.getMessage());
throw e;
}
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
//
}
}
动态dataSource类 :
动态数据源配置
DynamicDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
private Object writeDataSource; //写数据源
private Object readDataSource; //读数据源
@Override
public void afterPropertiesSet() {
if (this.writeDataSource == null) {
throw new IllegalArgumentException("Property 'writeDataSource' is required");
}
setDefaultTargetDataSource(writeDataSource);
Map
AbstractRoutingDataSource
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map
数据类型枚举:
数据源类型枚举
public enum DynamicDataSourceGlobal {
/**
* 读库
*/
READ,
/**
* 写库
*/
WRITE;
}
全局数据源类型线程本地变量:
数据源类型容器
public class DynamicDataSourceHolder {
/**
* 当前执行的sql应该走主库还是从库的holder
*/
private static final ThreadLocal HOLDER = new ThreadLocal();
public static void putDataSource(DynamicDataSourceGlobal dataSource){
HOLDER.set(dataSource);
}
public static void clearDataSource() {
HOLDER.remove();
}
public static DynamicDataSourceGlobal getDataSource(){
return HOLDER.get();
}
}
优点:逻辑简单易控制,不容易出问题。系统改动少。
缺点:没有好的方案应对强制查询主库。(可以考虑通过manager层加事务注解的方式强制查询主库,不过有些牵强还会影响性能)
3.手动切换主从数据源
默认数据源类型采用主库,需要走从库的时候用代码显式切换数据源,结合AbstractRoutingDataSource通过读取当前线程的数据源类型来控制执行sql时采用哪一个DataSource。
Manager方法:
手动切换数据源
@Override
public HotelMapDO selectByPrimaryKey(Long hotelMapId) {
DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
HotelMapDO hotelMapDO = hotelMapDao.selectByPrimaryKey(hotelMapId);
DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
return hotelMapDO;
}
AbstractRoutingDataSource同方案2。
优点:控制灵活。
缺点:显式切换数据源易出错,对现有大量从库查询也需要代码改造。
4.通过注解及切面确定主从数据源
默认数据源类型采用主库,需要走从库的时候,通过Spring Aop + 自定义注解 ,对加了注解的方法进行拦截设置指定的数据库类型,结合AbstractRoutingDataSource控制数据源类型。
自定义注解控制数据源
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
DynamicDataSourceGlobal value() default DynamicDataSourceGlobal.WRITE;
}
Manager方法:
自定义枚举的使用
@Override
@DataSource(DynamicDataSourceGlobal.READ)
public HotelMapDO selectByPrimaryKey(Long hotelMapId) {
return hotelMapDao.selectByPrimaryKey(hotelMapId);
}
切面类:
DynamicDataSourceAspect
@Aspect
public class DynamicDataSourceAspect {
protected org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* *.*(..)) && @annotation(com.jd.trip.hotel.mapping.common.framework.datasource.DataSource)")
public void pointCut() {
}
@Before("pointCut()")
public void before(JoinPoint point) {
Object target = point.getTarget();
String methodName = point.getSignature().getName();
Class> targetClass = target.getClass();
Class>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
try {
Method method = targetClass.getMethod(methodName, parameterTypes);
if (method != null && method.isAnnotationPresent(DataSource.class)) {
DataSource data = method.getAnnotation(DataSource.class);
DynamicDataSourceHolder.putDataSource(data.value());
}
} catch (Exception e) {
logger.error("Choose DataSource error, method:{}, msg:{}", methodName, e.getMessage(), e);
}
}
@After("pointCut()")
public void after(JoinPoint point) {
DynamicDataSourceHolder.clearDataSource();
}
}
AbstractRoutingDataSource同方案2。
优点:不需要手动切换,只需要加注解
缺点:同3,有大量从库查询需要代码改造
5.基于jdbc driver实现
配置mysql的Driver的时候使用ReplicationDriver,并配置主库从库:
driver配置
jdbc.driverClassName=com.mysql.jdbc.ReplicationDriver
jdbc.url=jdbc:mysql:replication://{master}:3307,{slave}:3308/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNul
通过配置“readOnly = true”,driver获取Connection的时候走从库
事务控制
@Override
@Transactional(readOnly = true)
public GeoMapDO selectById(Long geoMapId) {
return geoMapDao.selectByPrimaryKey(geoMapId);
}
优点:配置、使用简单
缺点:走从库的查询都需要改成事务控制,另外因为开启了事务是否会性能差一些?
三、我们的系统采用的方案
- 通过mybatis插件拦截sql执行,分析sql类型是update还是query来设置对应的数据源到全局的ThreadLocal中。
同上面方案2
- 基于aop和注解方式,在方法执行前根据注解参数类型设置对应的数据源到全局的ThreadLocal中。
同上面方案4
- 事务管理,原有的事务管理配置的全部都是主库,继承DataSourceTransactionManager类,重写doBegin()方法,对readOnly的事务走从库,非readOnly的都走主库。
事务管理器配置
自定义事务管理器
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
/**
* 只读事务到读库,读写事务到写库
* @param transaction
* @param definition
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
//设置数据源
boolean readOnly = definition.isReadOnly();
if(readOnly) {
DynamicDataSourceHolder.putSqlDataSource(DynamicDataSourceGlobal.READ);
} else {
DynamicDataSourceHolder.putSqlDataSource(DynamicDataSourceGlobal.WRITE);
}
super.doBegin(transaction, definition);
}
/**
* 清理本地线程的数据源
* @param transaction
*/
@Override
protected void doCleanupAfterCompletion(Object transaction) {
super.doCleanupAfterCompletion(transaction);
DynamicDataSourceHolder.clearSqlDataSource();
}
}
DataSourceTransactionManager部分源码:
DataSourceTransactionManager
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map targetDataSources;
private Object defaultTargetDataSource;
...
public void setTargetDataSources(Map targetDataSources) {
this.targetDataSources = targetDataSources;
}
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
...
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
...
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
protected abstract Object determineCurrentLookupKey();
}
**4. ****上面的方案是在原有方案2和方案4的组合上再加了对事务的优化。 **
问题1****:在实际使用过程中,我们可能要求某个方法的业务全部走主库,但是在这个方法的实现中为了提供性能我们可能会使用多线程。因为ThreadLocal本地线程变量保存的数据只能当前线程获取到,当我们使用并发流或者多线程的时候,子线程将获取不到主线程设置的数据源类型。
解决方案:TransmittableThreadLocal
将数据源容器中的ThreadLocal替换成TransmittableThreadLocal,这样子线程便可以获取到父线程的Thread
另外我们将数据源类型分为方法的数据源类型的sql的数据源类型,优先返回方法的数据源类型。
DynamicDataSourceHolder
public class DynamicDataSourceHolder {
/**
* 可传递给子线程的ThreadLocal,用来存放当前线程调用的方法,注解上是否有强制走主库或从库
*/
private static final TransmittableThreadLocal METHOD_FORCE_HOLDER = new TransmittableThreadLocal();
/**
* 当前执行的sql应该走主库还是从库的holder
*/
private static final ThreadLocal SQL_HOLDER = new ThreadLocal();
public static void putMethodDataSource(DynamicDataSourceGlobal dataSource){
METHOD_FORCE_HOLDER.set(dataSource);
}
public static void clearMethodDataSource() {
METHOD_FORCE_HOLDER.remove();
}
public static void putSqlDataSource(DynamicDataSourceGlobal dataSource){
SQL_HOLDER.set(dataSource);
}
public static void clearSqlDataSource() {
SQL_HOLDER.remove();
}
public static DynamicDataSourceGlobal getDataSource(){
return METHOD_FORCE_HOLDER.get() == null ? SQL_HOLDER.get() : METHOD_FORCE_HOLDER.get();
}
}
问题2: 使用TransmittableThreadLocal创建多线程时,子线程的ThreadLocal值是在“创建”时从父线程拷贝的。但是如果我们用到了线程池,那子线程是在一开始创建好,不销毁,后续只是传递任务给子线程执行。那么在执行任务时,不会从父线程拷贝ThreadLocal重新赋值,就出现了线程池复用时ThreadLocal值没更新的问题。
解决方案:扩展Ayncs和线程池
TtlExecutors会封装提交给它的任务Runable和Callable,以实现在任务执行前给ThreadLocal赋值在任务执行后清楚ThreadLocal。
线程池扩展配置
相关文档:https://github.com/alibaba/transmittable-thread-local