Spring+Mybatis透明實現讀寫分離
阿新 • • 發佈:2019-02-10
背景
網上有好多讀寫分離的實踐,所應對的業務場景也不一樣,本方法參考了網上好多方法,最終實現為快速應對中小型網際網路產品的讀寫分離。
資料庫環境:
1臺master;多臺slaver
適用框架:
spring+mybatis
操作資料庫的簡單原理:
mybatis最終是要通過sqlsessionfactory獲取資料連線,建立sqlsession並提交到資料庫的。所以我們入手的地方有兩點:
1. 通過建立多種sqlsessionfactory比如masterFactory,slaverFactory來實現讀寫分離。
2. 讓sqlsessionfactory直接可以動態獲取到只讀或者寫的資料來源。
解決方案:
通過擴充套件spring的AbstractRoutingDataSource及DataSourceTransactionManager來實現透明的讀寫分離。該類充當了DataSource的路由中介, 能有在執行時, 根據某種key值來動態切換到真正的DataSource上。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
//我們可以看到,它繼承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子類,
//So我們可以分析下它的getConnection方法:
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
/**
上面這段原始碼的重點在於determineCurrentLookupKey()方法,這是AbstractRoutingDataSource類中的一個抽象方法,
它的返回值是你所要用的資料來源dataSource的key值,有了這個key值,
resolvedDataSource(這是個map,由配置檔案中設定好後存入的)就從中取出對應的DataSource,如果找不到,就用配置預設的資料來源。
**/
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;
}
}
實現
現在我們需要做的就是重寫determineTargetDataSource(),獲取我們需要的資料來源。
1 我們自己的動態資料來源
public class DynamicDataSource extends AbstractRoutingDataSource {
Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
private AtomicInteger counter = new AtomicInteger();
private DataSource master;
private List<DataSource> slaves;
@Override
protected Object determineCurrentLookupKey() {
//do nothing
return null;
}
@Override
public void afterPropertiesSet() {
//do nothing
}
/**
* 根據標識獲取資料來源
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
DataSource dataSource = null;
if (DateSourceHolder.isMaster())
dataSource = master;
else if (DateSourceHolder.isSlave()){
int count = counter.getAndIncrement();
if (count > 1000000)
counter.set(0);
//簡單輪循
int sequence = count%slaves.size();
dataSource = slaves.get(sequence);
}else
dataSource = master;
//純粹為了除錯列印,線上需要註釋掉
/*
if (dataSource instanceof org.apache.tomcat.jdbc.pool.DataSource){
org.apache.tomcat.jdbc.pool.DataSource ds = (org.apache.tomcat.jdbc.pool.DataSource) dataSource;
String jdbcUrl = ds.getUrl();
int maxWait = ds.getMaxWait();
logger.debug(">>>>>>>DataSource>>>>>>use jdbc maxWait :"+maxWait+"; url : "+jdbcUrl);
}
*/
return dataSource;
}
public DataSource getMaster() {
return master;
}
public void setMaster(DataSource master) {
this.master = master;
}
public List<DataSource> getSlaves() {
return slaves;
}
public void setSlaves(List<DataSource> slaves) {
this.slaves = slaves;
}
}
2 資料來源控制器
public class DateSourceHolder {
private static final String MASTER = "master";
private static final String SLAVE = "slave";
private static final ThreadLocal<String> dataSource = new ThreadLocal<>();
private static final ThreadLocal<DataSource> masterLocal = new ThreadLocal<>();
private static final ThreadLocal<DataSource> slaveLocal = new ThreadLocal<>();
private static void setDataSource(String dataSourceKey){
dataSource.set(dataSourceKey);
}
private static String getDataSource(){
return dataSource.get();
}
public static boolean isMaster(){
return getDataSource() == MASTER;
}
public static boolean isSlave(){
return getDataSource() == SLAVE;
}
public static void setSlave(DataSource dataSource){
slaveLocal.set(dataSource);
}
public static void setMaster(DataSource dataSource){
masterLocal.set(dataSource);
}
public static void setMaster(){
setDataSource(MASTER);
}
public static void setSlave(){
setDataSource(SLAVE);
}
public static void clearDataSource(){
dataSource.remove();
masterLocal.remove();
slaveLocal.remove();
}
}
3 擴充套件事務處理器
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
boolean readOnly = definition.isReadOnly();//獲取當前事務切點的方法的讀寫屬性(在spring的xml或者事務註解中的配置)
if (readOnly)
DateSourceHolder.setSlave();
else
DateSourceHolder.setMaster();
super.doBegin(transaction, definition);
}
@Override
protected void doCleanupAfterCompletion(Object transaction) {
super.doCleanupAfterCompletion(transaction);
DateSourceHolder.clearDataSource();
}
}
4 配置檔案(部分)
<!-- 讀取配置檔案等等掃描 略...-->
<!--abstract資料來源配置 -->
<bean id="abstractDataSource" abstract="true">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!--tomcat jdbc pool資料來源配置 -->
<bean id="dataSourceMaster" name="dataSourceMaster" class="org.apache.tomcat.jdbc.pool.DataSource"
destroy-method="close">
<property name="poolProperties">
<bean class="org.apache.tomcat.jdbc.pool.PoolProperties" parent="abstractDataSource">
<property name="url" value="${jdbc.url}" />
<property name="maxWait" value="${tomcat.jdbc.maxWait}" /> <!-- 除錯讀寫用,線上不需要-->
</bean>
</property>
</bean>
<bean id="dataSourceSlaver1" name="dataSourceSlaver1" class="org.apache.tomcat.jdbc.pool.DataSource"
destroy-method="close">
<property name="poolProperties">
<bean class="org.apache.tomcat.jdbc.pool.PoolProperties" parent="abstractDataSource">
<property name="url" value="${jdbc.slaver1.url}" />
<property name="maxWait" value="${tomcat.jdbc.slaver1.maxWait}" /><!-- 除錯讀寫用,線上不需要-->
</bean>
</property>
</bean>
<bean id="dataSourceSlaver2" name="dataSourceSlaver2" class="org.apache.tomcat.jdbc.pool.DataSource"
destroy-method="close">
<property name="poolProperties">
<bean class="org.apache.tomcat.jdbc.pool.PoolProperties" parent="abstractDataSource">
<property name="url" value="${jdbc.slaver2.url}" />
<property name="maxWait" value="${tomcat.jdbc.slaver2.maxWait}" />
</bean>
</property>
</bean>
<bean id="dataSource" class="cn.com.demo.dao.core.DynamicDataSource">
<property name="master" ref="dataSourceMaster"></property>
<property name="slaves">
<list>
<ref bean="dataSourceSlaver1"></ref>
<ref bean="dataSourceSlaver2"></ref>
</list>
</property>
</bean>
<!-- mybatis -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<property name="mapperLocations" >
<value>classpath*:cn/com/demo/dao/mapper/xml/**/*Mapper.xml</value>
</property>
<property name="typeAliasesPackage" value="cn.com.demo.dao.entity" />
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageHelper">
<property name="properties">
<value>
dialect=mysql
offsetAsPageNum=true
rowBoundsWithCount=true
pageSizeZero=true
reasonable=true
</value>
</property>
</bean>
</array>
</property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="cn.com.demo.dao.mapper" />
</bean>
<!-- 事務管理器 -->
<bean id="transactionManager"
class="cn.com.demo.dao.core.DynamicDataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 宣告式事務 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="query*" read-only="true" /> <!-- 在擴充套件的事務管理器中就可以獲取到readOnly屬性了 -->
<tx:method name="get*" read-only="true" />
<tx:method name="select*" read-only="true" />
<tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut expression="execution(* cn.com.demo.service..*(..))"
id="ops" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="ops" />
</aop:config>
5 應用
public interface ProductService {
//匹配到*事務切點,會用master資料來源
Long addProduct(ProductAddDTO productAddDTO,CompanyUser com);
/**
簡單的執行流程:
1.匹配到query*事務切點,我們擴充套件的事務管理器會獲取到read-only="true"屬性
2. 呼叫DateSourceHolder.setSlave();
3. SQLSessionFactory會呼叫determineTargetDataSource()方法獲取資料來源,
在方法中通過 if (DateSourceHolder.isSlave()) dataSource = slaves.get(sequence);
將datasource設定為只讀資料來源。
4. 最終service呼叫mapper操作資料庫,mapper執行的時候用到的sqlSession中的connection即動態獲取的只讀資料來源。
**/
Map<String,Object> queryUserGroups(Long id);
}
至此,讀寫分離就配置完了,還是開頭說的,這種配置只使用1+N的中小專案中。大型專案可能需要N+N的配置。有機會我們在一起研究。
本文側重配置,描述偏少,見諒。如有問題,及時反饋。