spring+mybatis配置多資料來源總結,重點是動態載入資料來源,支援動態切換
最近在做一款遊戲的GM管理平臺,需要連線遊戲的資料庫去查詢資料;由於遊戲的每個服的資料是獨立的,所以就有了連線多個數據庫的問題;經過一番查詢,好在mybatis的學習資源還少,很快找到了配置多資料來源的方法;感謝以下大牛分享的學習資源:
http://lvdong5830.iteye.com/blog/1626286
http://blog.csdn.net/thc1987/article/details/8969655
http://fenshen6046.iteye.com/blog/1810159
Spring整合MyBatis有兩種方式,一種是配置MapperFactoryBean,另一種則是利用MapperScannerConfigurer進行掃描介面或包完成物件的自動建立。相對來說後者更方便些,多資料來源的配置方法,既配置多個datasource,對應多個sqlsessionfactory,分別把不同的sqlsessionfactory配置到對應的MapperScannerConfigurer中去,mybatis會根據掃描的包路徑,自動區分資料來源,如下:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="oneSqlSessionFactory"></property>
<property name="basePackage" value="aaa.bbb.one.*.dao" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="twoSqlSessionFactory"></property> <property name="basePackage" value="aaa.bbb.two.*.dao" /> </bean>
這樣即可實現一個工程中不同包路徑下的程式碼使用不同的資料來源,配置事務管理器的原理也是一樣的;於是欣喜地開始著手我的工程,剛動手就發現一個問題,通常情況下一款遊戲會有成百上千個服,多款遊戲需要連數以千萬計的資料庫,那麼我要建千萬個包嗎?況且,遊戲的開服和關服是非常頻繁的,如此以來,我要跟著遊戲一起更新程式碼?豈不累死!
於是又仔細分析,一款遊戲的資料庫結構都是一樣的,寫的sql也是一樣的,那麼能不能相同資料庫結構的資料來源,只配置一個源一個包,這樣sql就可以共用了,那麼問題來了,怎麼配置一個數據源呢?前臺查詢多個服的資料,怎麼做到切換資料來源呢?前面也說過,遊戲的開服和關服是很頻繁的,如果遊戲新開伺服器,我怎麼能做到不更新程式碼,就讓他能夠查詢到資料庫呢?就是說我需要做到動態載入資料來源,並且自動切換多個數據源!這下可愁人了,多資料來源的學習資料本來不是很多,又整這麼複雜的配置;於是乎又開始叮咣五四的找各種資料,最後總算找到了一些可以借鑑的資源,學習的過程總是痛苦的,踩著前人的腳步,然後又結合自己的情況做了一番修改,才算有了點眉目;問題一步一步得以解決。
首先來解決怎麼在一個數據源配置下,可以連線多個數據庫,得以執行相同的sql語句的問題:這個資料來源是必須要配的,但是不能寫死資料庫連線,所以我自己實現了一個DataSource類,sqlsessionfactory執行sql時,會呼叫對應datasource的getConnection方法,廢話不多說,亮出程式碼:
public class AAAGameDBMultiDataSource implements DataSource,
ApplicationContextAware {
private ThreadLocal<String> local = new ThreadLocal<String>();
// 資料來源的驅動,使用者名稱,密碼都是固定的,所以在這裡寫死了
@Value("${aaa.gamedb.driverClassName}")
private String driverClassName;
@Value("${aaa.gamedb.username}")
private String username;
@Value("${aaa.gamedb.password}")
private String password;
@Autowired
private DynamicLoadBean dynamicLoadBean;
private ApplicationContext applicationContext = null;
private DataSource dataSource = null;
public AAAGameDBMultiDataSource(DataSource dataSource) {
this.setDataSource(dataSource);
}
public AAAGameDBMultiDataSource() {
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return getDataSource().getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
getDataSource().setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
getDataSource().setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return getDataSource().getLoginTimeout();
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
public DataSource getDataSource() {
// 獲取本執行緒中存入的資料來源標識
String dataSourceName = this.local.get();
return getDataSource(dataSourceName);
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public DataSource getDataSource(String dataSourceName) {
try {
if (GMUtil.isEmpty(dataSourceName)) {
return null;//this.dataSource;
}
return (DataSource) this.applicationContext.getBean(dataSourceName);
} catch (NoSuchBeanDefinitionException ex) {
// throw new Exception("There is not the dataSource ");
ex.printStackTrace();
}
return dataSource;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
/**
* @Desc 識別請求需要連線的資料來源,動態切換資料來源
* @param reqPath
* @param projName
* @param user
* @return 0:不需要切換資料來源,或已切換成功; -1:使用者資料錯誤導致切換失敗
*/
public int switchDataSource(SmolArea serverInfo){
// 如果目標查詢的遊戲服為空,則返回-1
if(GMUtil.isEmpty(serverInfo))
return -1;
String dataSourceName = "ds" + "_" + serverInfo.getGamedb_lan_ip() + "_gamedb";
if(!dynamicLoadBean.hasBean(dataSourceName)){
/* ============== 動態裝配DataSource begin ==================*/
DataSourceDynamicBean dataSourceDynamicBean = new DataSourceDynamicBean(dataSourceName);
dataSourceDynamicBean.setDriverClassName(driverClassName);
dataSourceDynamicBean.setUrl(serverInfo.getGamedb_lan_ip(), 3306, "gamedb", driverClassName);
dataSourceDynamicBean.setUsername(username);
dataSourceDynamicBean.setPassword(password);
dynamicLoadBean.loadBean(dataSourceDynamicBean);//
/* ============== 動態裝配DataSource end ==================*/
}
// 切換資料來源
this.local.set(dataSourceName);
return 0;
}
}
大致流程是這樣,前臺查詢伺服器資料時,把serverId傳過來,過濾器中接收到後會根據serverId獲取serverInfo,然後呼叫switchDataSource方法切換資料來源;等執行sql的時候,getConnection方法就知道該返回哪個資料來源連線了;(切換中還有判斷資料來源是否存在等邏輯,這個是後話)
動態切換資料來源的功能就這樣實現了,再說下一個問題,新開一組遊戲伺服器後,我的伺服器列表裡面會加入這個伺服器的連線資料,那麼如何能動態的加入這個資料來源呢?對於spring而言datasource也是一個普通的bean,那就是如何動態註冊一個bean的問題了;這方面的學習資源有很多,我不多講,列出程式碼供參考:
/**
* 使用方法loadBean()向spring的beanFactory動態地裝載bean,該方法的引數configLocationString等同於
* spring配置中的contextConfigLocation,同樣支援諸如"/WEB-INF/ApplicationContext-*.xml"的寫法。
* @author FanGang
*
*/
public class DynamicLoadBean implements ApplicationContextAware{
private ConfigurableApplicationContext applicationContext = null;
private XmlBeanDefinitionReader beanDefinitionReader;
/* 初始化方法 */
public void init() {
beanDefinitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) applicationContext.getBeanFactory());
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(applicationContext));
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
}
public ConfigurableApplicationContext getApplicationContext() {
return applicationContext;
}
public boolean hasBean(String beanName){
return applicationContext.containsBean(beanName);
}
/**
* 向spring的beanFactory動態地裝載bean
* @param configLocationString 要裝載的bean所在的xml配置檔案位置。
*/
public void loadBean(String configLocationString){
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry)getApplicationContext().getBeanFactory());
beanDefinitionReader.setResourceLoader(getApplicationContext());
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(getApplicationContext()));
try {
String[] configLocations = new String[]{configLocationString};
for(int i=0;i<configLocations.length;i++)
beanDefinitionReader.loadBeanDefinitions(getApplicationContext().getResources(configLocations[i]));
} catch (BeansException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void loadBean(DynamicBean dynamicBean) {
String beanName = dynamicBean.getBeanName();
if (applicationContext.containsBean(beanName)) {
// LogUtil.clientLog("bean【" + beanName + "】已經載入!");
return;
}
beanDefinitionReader.loadBeanDefinitions(new DynamicResource(dynamicBean));
// LogUtil.clientLog("初始化bean【" + dynamicBean.getBeanName() + "】耗時" + (System.currentTimeMillis() - startTime) + "毫秒。");
}
public void removeBean(String beanName) {
if (applicationContext.containsBean(beanName)) {
BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
beanDefinitionRegistry.removeBeanDefinition(beanName);
return;
}
}
}
/**
* 動態bean描述物件
*/
public abstract class DynamicBean {
protected String beanName;
public DynamicBean(String beanName) {
this.beanName = beanName;
}
public String getBeanName() {
return beanName;
}
public void setBeanName(String beanName) {
this.beanName = beanName;
}
/**
* 獲取bean 的xml描述
*
* @return
*/
protected abstract String getBeanXml();
/**
* 生成完整的xml字串
*
* @return
*/
public String getXml() {
StringBuffer buf = new StringBuffer();
buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
.append("<beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"")
.append(" xmlns:p=\"http://www.springframework.org/schema/p\" xmlns:aop=\"http://www.springframework.org/schema/aop\"")
.append(" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:jee=\"http://www.springframework.org/schema/jee\"")
.append(" xmlns:tx=\"http://www.springframework.org/schema/tx\"")
.append(" xsi:schemaLocation=\"")
.append(" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd")
.append(" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd")
.append(" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd")
.append(" http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd")
.append(" http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd\">")
.append(getBeanXml()).append("</beans>");
return buf.toString();
}
}
public class DynamicResource implements Resource {
private DynamicBean dynamicBean;
public DynamicResource(DynamicBean dynamicBean) {
this.dynamicBean = dynamicBean;
}
/*
* (non-Javadoc)
*
* @see org.springframework.core.io.InputStreamSource#getInputStream()
*/
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(dynamicBean.getXml().getBytes("UTF-8"));
}
// 其他實現方法省略
@Override
public long contentLength() throws IOException {
return 0;
}
@Override
public Resource createRelative(String arg0) throws IOException {
return null;
}
@Override
public boolean exists() {
return false;
}
@Override
public String getDescription() {
return null;
}
@Override
public File getFile() throws IOException {
return null;
}
@Override
public String getFilename() {
return null;
}
@Override
public URI getURI() throws IOException {
return null;
}
@Override
public URL getURL() throws IOException {
return null;
}
@Override
public boolean isOpen() {
return false;
}
@Override
public boolean isReadable() {
return false;
}
@Override
public long lastModified() throws IOException {
return 0;
}
}
ok,核心程式碼都已亮出來,思路也已說明,有問題留言,歡迎討論!