一、网上好多地方都写了spring+mybatis读写分离的四种方案,本人亲自在项目中应用了方案4,不过网上说的方案4存在问题,查询也走了主库。本文中该问题已修正。
一、自定义动态数据源(继承AbstractRoutingDataSource)实现读写分离
import org.apache.commons.collections.CollectionUtils; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Created by IDEA * User: zyj * Date: 2018-06-26 10:56 * Desc: 动态数据源实现读写分离 */ public class DynamicDataSource extends AbstractRoutingDataSource { private Object writeDataSource; //写数据源 private List<Object> readDataSources; //多个读数据源 private int readDataSourcePollPattern = 0; //获取读数据源方式,0:随机,1:轮询 private AtomicLong counter = new AtomicLong(0); private static final Long MAX_POOL = Long.MAX_VALUE; private final Lock lock = new ReentrantLock(); @Override public void afterPropertiesSet() { if (this.writeDataSource == null) { throw new IllegalArgumentException("Property 'writeDataSource' is required"); } setDefaultTargetDataSource(writeDataSource); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource); if(CollectionUtils.isNotEmpty(readDataSources)) { for(int i=0; i<readDataSources.size(); i++){ targetDataSources.put(DynamicDataSourceGlobal.READ.name()+i, readDataSources.get(i)); } } setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource(); if(dynamicDataSourceGlobal == null || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) { return DynamicDataSourceGlobal.WRITE.name(); } int index = 0; int readDataSourceSize = readDataSources.size(); if(readDataSourcePollPattern == 1) { //轮询方式 long currValue = counter.incrementAndGet(); if((currValue + 1) >= MAX_POOL) { try { lock.lock(); if((currValue + 1) >= MAX_POOL) { counter.set(0); } } finally { lock.unlock(); } } index = (int) (currValue % readDataSourceSize); } else { //随机方式 index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize); } return DynamicDataSourceGlobal.READ.name() + index; } public void setWriteDataSource(Object writeDataSource) { this.writeDataSource = writeDataSource; } public Object getWriteDataSource() { return writeDataSource; } public List<Object> getReadDataSources() { return readDataSources; } public void setReadDataSources(List<Object> readDataSources) { this.readDataSources = readDataSources; } public int getReadDataSourcePollPattern() { return readDataSourcePollPattern; } public void setReadDataSourcePollPattern(int readDataSourcePollPattern) { this.readDataSourcePollPattern = readDataSourcePollPattern; } }
该类中引用到两个类,很简单,也贴出来
/** * Created by IDEA * User: zyj * Date: 2018-06-26 10:56 * Desc: */ public enum DynamicDataSourceGlobal { READ, WRITE; }
public final class DynamicDataSourceHolder { private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>(); private DynamicDataSourceHolder() { // } public static void putDataSource(DynamicDataSourceGlobal dataSource){ holder.set(dataSource); } public static DynamicDataSourceGlobal getDataSource(){ return holder.get(); } public static void clearDataSource() { holder.remove(); } }
动态数据源事务管理类
import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionDefinition; /** * Created by IDEA * User: zyj * Date: 2018-06-26 10:56 * Desc: */ public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager { /** * 只读事务到读库,读写事务到写库 * @param transaction * @param definition */ @Override protected void doBegin(Object transaction, TransactionDefinition definition) { //设置数据源 boolean readOnly = definition.isReadOnly(); if(readOnly) { DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ); } else { DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE); } super.doBegin(transaction, definition); } /** * 清理本地线程的数据源 * @param transaction */ @Override protected void doCleanupAfterCompletion(Object transaction) { super.doCleanupAfterCompletion(transaction); DynamicDataSourceHolder.clearDataSource(); } }
最后是mybatis的自定义拦截器
import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.keygen.SelectKeyGenerator; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; /** * Created by IDEA * User: zyj * Date: 2018-06-26 10:56 * Desc: */ @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 DynamicPlugin implements Interceptor { protected static final Logger logger = LoggerFactory.getLogger(DynamicPlugin.class); private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap<>(); @Override public Object intercept(Invocation invocation) throws Throwable { // boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); //不能用这个判断,网上的方案4都错在了这里。 boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); if(!actualTransactionActive) { Object[] objects = invocation.getArgs(); MappedStatement ms = (MappedStatement) objects[0]; DynamicDataSourceGlobal dynamicDataSourceGlobal = null; if((dynamicDataSourceGlobal = cacheMap.get(ms.getId())) == 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; } logger.info("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name()); cacheMap.put(ms.getId(), dynamicDataSourceGlobal); } DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) { // } }
至此,要引入的类都OK了。接下来说说如何在项目中引用这些类。
扫描二维码关注公众号,回复:
2253239 查看本文章
首先DynamicDataSource肯定是在配置数据源的地方。
<!-- dataSource 配置 --> <bean id="dataSource" class="cn.remotejob.readwrite.DynamicDataSource"> <property name="writeDataSource" ref="dataSourceWrite" /> <property name="readDataSources"> <list> <ref bean="dataSourceRead1" /> <ref bean="dataSourceRead2" /> </list> </property> <!--获取读数据源方式,0:随机,1:轮询--> <property name="readDataSourcePollPattern" value="1" /> </bean> <bean id="dataSourceWrite" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="validationQuery" value="SELECT 1" /> <property name="testOnBorrow" value="true"/> <!-- 超时等待时间以毫秒为单位 --> <property name="maxWait" value="3000"/> <property name="connectionInitSqls" value="set names utf8mb4;"/> </bean> <bean id="dataSourceRead1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="${jdbc.url_1}" /> <property name="username" value="${jdbc.username_1}" /> <property name="password" value="${jdbc.password_1}" /> <property name="validationQuery" value="SELECT 1" /> <property name="testOnBorrow" value="true"/> <!-- 超时等待时间以毫秒为单位 --> <property name="maxWait" value="3000"/> <property name="connectionInitSqls" value="set names utf8mb4;"/> </bean> <bean id="dataSourceRead2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="${jdbc.url_2}" /> <property name="username" value="${jdbc.username_2}" /> <property name="password" value="${jdbc.password_2}" /> <property name="validationQuery" value="SELECT 1" /> <property name="testOnBorrow" value="true"/> <!-- 超时等待时间以毫秒为单位 --> <property name="maxWait" value="3000"/> <property name="connectionInitSqls" value="set names utf8mb4;"/> </bean>
其次DynamicPlugin作为插件需要配置在下面的地方
<!-- mybatis文件配置,扫描所有mapper文件 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource" p:mapperLocations="classpath:cn/remotejob/dao/*.xml"> <property name="plugins"> <array> <bean class="cn.remotejob.readwrite.DynamicPlugin" /> </array> </property> </bean>
最后DynamicDataSourceTransactionManager当然是自定义事务管理器,代替org.springframework.jdbc.datasource.DataSourceTransactionManager,因为DynamicDataSourceTransactionManager继承了它。
<bean id="transactionManager" class="cn.remotejob.readwrite.DynamicDataSourceTransactionManager" p:dataSource-ref="dataSource" />
到此,读写分离已经实现了!
但如果我刚写的数据想要立刻查出来,就必须让这个查询也走主库。要解决这个问题,可以让这个写和读放到一个方法中,然后在方法上加上事务@Transactional,这样就可以走主库了。当然,配置文件中要启用对事务注解的支持,<tx:annotation-driven transaction-manager="transactionManager" />。