1、引言
笔者最近在做一个互联网的“类SNS”应用,应用中用户数量巨大(约4000万)左右,因此,简单的使用传统单一数据库存储肯定是不行的。
参考了业内广泛使用的分库分表,以及使用DAL数据访问层等的做法,笔者决定使用一种最简单的数据源路由选择方式来解决问题。
严格的说,目前的实现不能算是一个解决方案,只能是一种思路的简易实现,笔者也仅花了2天时间来完成(其中1.5天是在看资料和Spring/ibatis的源码)。这里也只是为各位看官提供一个思路参考,顺便给自己留个笔记
2、系统的设计前提
我们的系统使用了16个数据库实例(目前分布在2台物理机器上,后期将根据系统负荷的增加,逐步移库到16台物理机器上)。16个库是根据用户的UserID进行简单的hash分配。这里值得一说的是,我们既然做了这样的横向切分设计,就已经考虑了系统需求的特性,
- 1.不会发生经常性的跨库访问。
- 2.主要的业务逻辑都是围绕UserID为核心的,在一个单库事务内即可完成。
在系统中,我们使用Spring和iBatis。Spring负责数据库的事务管理AOP,以及Bean间的IOC。选择iBatis的最大原因是对Sql的性能优化,以及后期如果有分表要求的时,可以很容易实现对sql表名替换。
3、设计思路
首先,要说明一下笔者的思路,其实很简单,即“在每次数据库操作前,确定当前要选择的数据库对象”而后就如同访问单库一样的访问当前选中的数据库即可。
其次,要在每次DB访问前选择数据库,需要明确几个问题,1.iBatis在什么时候从DataSource中取得具体的数据库Connection的,2.对取得的Connection,iBatis是否进行缓存,因为在多库情况下Connection被缓存就意味着无法及时改变数据库链接选择。3.由于我们使用了Spring来管理DB事务,因此必须搞清Spring对DB Connction的开关拦截过程是否会影响多DataSource的情况。
幸运的是,研究源码的结果发现,iBatis和Spring都是通过标准的DataSource接口来控制
Connection的,这就为我们省去了很多的麻烦,只需要实现一个能够支持多个数据库的DataSource,就能达到我们的目标。
4、代码与实现
多数据库的DataSource实现:MultiDataSource.class
Java代码
1、 import java.io.PrintWriter;
2、 import java.sql.Connection;
3、 import java.sql.SQLException;
4、 import java.util.ArrayList;
5、 import java.util.Collection;
6、 import java.util.HashMap;
7、 import java.util.Map;
8、
9、 import javax.sql.DataSource;
10、
11、 import org.apache.log4j.Logger;
12、
13、 import com.xxx.sql.DataSourceRouter.RouterStrategy;
14、
15、 /**
16、 * 复合多数据源(Alpha)
17、 * @author linliangyi2005@gmail.com
18、 * Jul 15, 2010
19、 */
20、 public class MultiDataSource implements DataSource {
21、
22、 static Logger logger = Logger.getLogger(MultiDataSource.class);
23、
24、 //当前线程对应的实际DataSource
25、 private ThreadLocal currentDataSourceHolder = new ThreadLocal();
26、 //使用Key-Value映射的DataSource
27、 private Map
28、 //使用横向切分的分布式DataSource
29、 private ArrayList clusterDataSources;
30、
31、 public MultiDataSource(){
32、 mappedDataSources = new HashMap
33、 clusterDataSources = new ArrayList(4);
34、 }
35、
36、 /**
37、 * 数据库连接池初始化
38、 * 该方法通常在web 应用启动时调用
39、 */
40、 public void initialMultiDataSource(){
41、 for(DataSource ds : clusterDataSources){
42、 if(ds != null){
43、 Connection conn = null;
44、 try {
45、 conn = ds.getConnection();
46、 } catch (SQLException e) {
47、 e.printStackTrace();
48、 } finally{
49、 if(conn != null){
50、 try {
51、 conn.close();
52、 } catch (SQLException e) {
53、 e.printStackTrace();
54、 }
55、 conn = null;
56、 }
57、 }
58、 }
59、 }
60、 Collection dsCollection = mappedDataSources.values();
61、 for(DataSource ds : dsCollection){
62、 if(ds != null){
63、 Connection conn = null;
64、 try {
65、 conn = ds.getConnection();
66、 } catch (SQLException e) {
67、 e.printStackTrace();
68、 } finally{
69、 if(conn != null){
70、 try {
71、 conn.close();
72、 } catch (SQLException e) {
73、 e.printStackTrace();
74、 }
75、 conn = null;
76、 }
77、 }
78、 }
79、 }
80、 }
81、 /**
82、 * 获取当前线程绑定的DataSource
83、 * @return
84、 */
85、 public DataSource getCurrentDataSource() {
86、 //如果路由策略存在,且更新过,则根据路由算法选择新的DataSource
87、 RouterStrategy strategy = DataSourceRouter.currentRouterStrategy.get();
88、 if(strategy == null){
89、 throw new IllegalArgumentException(“DataSource RouterStrategy No found.”);
90、 }
91、 if(strategy != null && strategy.isRefresh()){
92、 if(RouterStrategy.SRATEGY_TYPE_MAP.equals(strategy.getType())){
93、 this.choiceMappedDataSources(strategy.getKey());
94、
95、 }else if(RouterStrategy.SRATEGY_TYPE_CLUSTER.equals(strategy.getType())){
96、 this.routeClusterDataSources(strategy.getRouteFactor());
97、 }
98、 strategy.setRefresh(false);
99、 }
100、 return currentDataSourceHolder.get();
101、 }
102、
103、 public Map<String, DataSource> getMappedDataSources() {
104、 return mappedDataSources;
105、 }
106、
107、 public void setMappedDataSources(Map<String, DataSource> mappedDataSources) {
108、 this.mappedDataSources = mappedDataSources;
109、 }
110、
111、 public ArrayList getClusterDataSources() {
112、 return clusterDataSources;
113、 }
114、
115、 public void setClusterDataSources(ArrayList clusterDataSources) {
116、 this.clusterDataSources = clusterDataSources;
117、 }
118、
119、 /**
120、 * 使用Key选择当前的数据源
121、 * @param key
122、 */
123、 public void choiceMappedDataSources(String key){
124、 DataSource ds = this.mappedDataSources.get(key);
125、 if(ds == null){
126、 throw new IllegalStateException(“No Mapped DataSources Exist!”);
127、 }
128、 this.currentDataSourceHolder.set(ds);
129、 }
130、
131、 /**
132、 * 使用取模算法,在群集数据源中做路由选择
133、 * @param routeFactor
134、 */
135、 public void routeClusterDataSources(int routeFactor){
136、 int size = this.clusterDataSources.size();
137、 if(size == 0){
138、 throw new IllegalStateException(“No Cluster DataSources Exist!”);
139、 }
140、 int choosen = routeFactor % size;
141、 DataSource ds = this.clusterDataSources.get(choosen);
142、 if(ds == null){
143、 throw new IllegalStateException(“Choosen DataSources is null!”);
144、 }
145、 logger.debug(“Choosen DataSource No.” + choosen+ ” : ” + ds.toString());
146、 this.currentDataSourceHolder.set(ds);
147、 }
148、
149、 /* (non-Javadoc)
150、 * @see javax.sql.DataSource#getConnection()
151、 */
152、 public Connection getConnection() throws SQLException {
153、 if(getCurrentDataSource() != null){
154、 return getCurrentDataSource().getConnection();
155、 }
156、 return null;
157、 }
158、
159、 /* (non-Javadoc)
160、 * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
161、 */
162、 public Connection getConnection(String username, String password)
163、 throws SQLException {
164、 if(getCurrentDataSource() != null){
165、 return getCurrentDataSource().getConnection(username , password);
166、 }
167、 return null;
168、 }
169、
170、 /* (non-Javadoc)
171、 * @see javax.sql.CommonDataSource#getLogWriter()
172、 */
173、 public PrintWriter getLogWriter() throws SQLException {
174、 if(getCurrentDataSource() != null){
175、 return getCurrentDataSource().getLogWriter();
176、 }
177、 return null;
178、 }
179、
180、 /* (non-Javadoc)
181、 * @see javax.sql.CommonDataSource#getLoginTimeout()
182、 */
183、 public int getLoginTimeout() throws SQLException {
184、 if(getCurrentDataSource() != null){
185、 return getCurrentDataSource().getLoginTimeout();
186、 }
187、 return 0;
188、 }
189、
190、 /* (non-Javadoc)
191、 * @see javax.sql.CommonDataSource#setLogWriter(java.io.PrintWriter)
192、 */
193、 public void setLogWriter(PrintWriter out) throws SQLException {
194、 if(getCurrentDataSource() != null){
195、 getCurrentDataSource().setLogWriter(out);
196、 }
197、 }
198、
199、 /* (non-Javadoc)
200、 * @see javax.sql.CommonDataSource#setLoginTimeout(int)
201、 */
202、 public void setLoginTimeout(int seconds) throws SQLException {
203、 if(getCurrentDataSource() != null){
204、 getCurrentDataSource().setLoginTimeout(seconds);
205、 }
206、 }
207、
208、 /* (non-Javadoc)
209、 * 该接口方法since 1.6
210、 * 不是所有的DataSource都实现有这个方法
211、 * @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
212、 */
213、 public boolean isWrapperFor(Class<?> iface) throws SQLException {
214、
215、 // if(getCurrentDataSource() != null){
216、 // return getCurrentDataSource().isWrapperFor(iface);
217、 // }
218、 return false;
219、 }
220、
221、 /* (non-Javadoc)
222、 * 该接口方法since 1.6
223、 * 不是所有的DataSource都实现有这个方法
224、 * @see java.sql.Wrapper#unwrap(java.lang.Class)
225、 */
226、 public T unwrap(Class iface) throws SQLException {
227、 // if(getCurrentDataSource() != null){
228、 // return getCurrentDataSource().unwrap(iface);
229、 // }
230、 return null;
231、 }
232、 }
这个类实现了DataSource的标准接口,而最核心的部分是getConnection()方法的重载。下面具体阐述:
- 1.实例变量 clusterDataSources 是一个DataSource 的 ArrayList它存储了多个数据库的DataSource实例,我们使用Spring的IOC功能,将多个DataSource注入到这个list中。
- 2.实例变量 mappedDataSources 是一个DataSource 的Map,它与clusterDataSources 一样用来存储多个数据库的DataSource实例,不同的是,它可以使用key直接获取DataSource。我们一样会使用Spring的IOC功能,将多个DataSource注入到这个Map中。
- 3.实例变量currentDataSourceHolder ,他是一个ThreadLocal变量,保存与当前线程相关的且已经取得的DataSource实例。这是为了在同一线程中,多次访问同一数据库时,不需要再重新做路由选择。
- 4.当外部类调用getConnection()方法时,方法将根据上下文的路由规则,从clusterDataSources 或者 mappedDataSources 选择对应DataSource,并返回其中的Connection。
(PS:关于DataSource的路由选择规则,可以根据应用场景的不同,自行设计。笔者这里提供两种简单的思路,1.根据HashCode,在上述例子中可以是UserId,进行取模运算,来定位数据库。2.根据上下文设置的关键字key,从map中选择映射的DataSource)
DataSourceRouter.class:
1、 /**
2、 * @author linliangyi2005@gmail.com
3、 * Jul 15, 2010
4、 */
5、 public class DataSourceRouter {
6、
7、 public static ThreadLocal currentRouterStrategy =
8、 new ThreadLocal();
9、
10、 /**
11、 * 设置MultiDataSource的路由策略
12、 * @param type
13、 * @param key
14、 * @param routeFactor
15、 */
16、 public static void setRouterStrategy(String type , String key , int routeFactor){
17、 if(type == null){
18、 throw new IllegalArgumentException(“RouterStrategy Type must not be null”);
19、 }
20、 RouterStrategy rs = currentRouterStrategy.get();
21、 if(rs == null){
22、 rs = new RouterStrategy();
23、 currentRouterStrategy.set(rs);
24、 }
25、 rs.setType(type);
26、 rs.setKey(key);
27、 rs.setRouteFactor(routeFactor);
28、 }
29、
30、 /**
31、 * 数据源路由策略
32、 * @author linliangyi2005@gmail.com
33、 * Jul 15, 2010
34、 */
35、 public static class RouterStrategy{
36、
37、 public static final String SRATEGY_TYPE_MAP = “MAP”;
38、 public static final String SRATEGY_TYPE_CLUSTER = “CLUSTER”;
39、 /*
40、 * 可选值 “MAP” , “CLUSTER”
41、 * MAP : 根据key从DataSourceMap中选中DS
42、 * CLUSTER : 根据routeFactor参数,通过算法获取群集
43、 */
44、 private String type;
45、 /*
46、 * “MAP” ROUTE 中的key
47、 *
48、 */
49、 private String key;
50、 /*
51、 * “CLUSTER” ROUTE时的参数
52、 */
53、 private int routeFactor;
54、 /*
55、 * True表示RouterStrategy更新过
56、 * False表示没有更新
57、 */
58、 private boolean refresh;
59、
60、 public String getType() {
61、 return type;
62、 }
63、
64、 public void setType(String type) {
65、 if(this.type != null && !this.type.equals(type)){
66、 this.type = type;
67、 this.refresh = true;
68、 }else if(this.type == null && type != null){
69、 this.type = type;
70、 this.refresh = true;
71、 }
72、 }
73、
74、 public String getKey() {
75、 return key;
76、 }
77、
78、 public void setKey(String key) {
79、 if(this.key != null && !this.key.equals(key)){
80、 this.key = key;
81、 this.refresh = true;
82、 }else if(this.key == null && key != null){
83、 this.key = key;
84、 this.refresh = true;
85、 }
86、 }
87、
88、 public int getRouteFactor() {
89、 return routeFactor;
90、 }
91、
92、 public void setRouteFactor(int routeFactor) {
93、 if(this.routeFactor != routeFactor){
94、 this.routeFactor = routeFactor;
95、 this.refresh = true;
96、 }
97、 }
98、
99、 public boolean isRefresh() {
100、 return refresh;
101、 }
102、
103、 public void setRefresh(boolean refresh) {
104、 this.refresh = refresh;
105、 }
106、 }
107、
108、 }
5、将MultiDataSource与Spring,iBatis结合
在完成了上述的编码过程后,就是将这个MultiDataSource与现有Spring和iBatis结合起来配置。
STEP 1。配置多个数据源
笔者这里使用了C3P0作为数据库连接池,这一步和标准的Spring配置一样,唯一不同的是,以前只配置一个,现在要配置多个
Xml代码
1、
2、 <bean id=”c3p0_dataSource_1″ class=”com.mchange.v2.c3p0.ComboPooledDataSource” destroy-method=”close”>
3、 <property name=”driverClass”>
4、 ${jdbc.driverClass}
5、
6、 <property name=”jdbcUrl”>
7、 ${mysql.url_1}
8、
9、 <property name=”user”>
10、 ${jdbc.username}
11、
12、 <property name=”password”>
13、 ${jdbc.password}
14、
15、
16、 <property name=”minPoolSize”>
17、 ${c3p0.minPoolSize}
18、
19、
20、 <property name=”maxPoolSize”>
21、 ${c3p0.maxPoolSize}
22、
23、
24、 <property name=”initialPoolSize”>
25、 ${c3p0.initialPoolSize}
26、
27、
28、 <property name=”idleConnectionTestPeriod”>
29、 ${c3p0.idleConnectionTestPeriod}
30、
31、
32、
33、
34、 <bean id=”c3p0_dataSource_2″ class=”com.mchange.v2.c3p0.ComboPooledDataSource” destroy-method=”close”>
35、 <property name=”driverClass”>
36、 ${jdbc.driverClass}
37、
38、 <property name=”jdbcUrl”>
39、 ${mysql.url_2}
40、
41、 <property name=”user”>
42、 ${jdbc.username}
43、
44、 <property name=”password”>
45、 ${jdbc.password}
46、
47、
48、 <property name=”minPoolSize”>
49、 ${c3p0.minPoolSize}
50、
51、
52、 <property name=”maxPoolSize”>
53、 ${c3p0.maxPoolSize}
54、
55、
56、 <property name=”initialPoolSize”>
57、 ${c3p0.initialPoolSize}
58、
59、
60、 <property name=”idleConnectionTestPeriod”>
61、 ${c3p0.idleConnectionTestPeriod}
62、
63、
64、
65、
66、 ……
STEP 2。将多个数据源都注入到MultiDataSource中
Xml代码
1、 <bean id=”multiDataSource” class=”com.xxx.sql.MultiDataSource”>
2、 <property name=”clusterDataSources”>
3、
4、 <ref bean=”c3p0_dataSource_1″ />
5、 <ref bean=”c3p0_dataSource_2″ />
6、 <ref bean=”c3p0_dataSource_3″ />
7、 <ref bean=”c3p0_dataSource_4″ />
8、 <ref bean=”c3p0_dataSource_5″ />
9、 <ref bean=”c3p0_dataSource_6″ />
10、 <ref bean=”c3p0_dataSource_7″ />
11、 <ref bean=”c3p0_dataSource_8″ />
12、
13、
14、 <property name=”mappedDataSources”>
15、
16、 <entry key=”system” value-ref=”c3p0_dataSource_system” />
17、
18、
19、
STEP 3。像使用标准的DataSource一样,使用MultiDataSource
Xml代码
1、
2、 <bean id=”sqlMapClient” class=”org.springframework.orm.ibatis.SqlMapClientFactoryBean”>
3、 <property name=”configLocation” value=”classpath:SqlMapConfig.xml”/>
4、 <property name=”dataSource” ref=”multiDataSource”>
5、
6、
7、
8、 <bean id=”jdbc_TransactionManager” class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
9、 <property name=”dataSource” ref=”multiDataSource”>
10、
至此,我们的程序就可以让Spring来管理多库访问了,但请注意,数据库事务仍然限于单库范围(之前已经说过,这里的应用场景不存在跨库的事务)。
6、Java代码使用例子
首先要说明的是,这里我们只是提供了一个简单的使用范例,在范例中,我们还必须手动的调用API,以确定DataSource的路由规则,在实际的应用中,您可以针对自己的业务特点,对此进行封装,以实现相对透明的路由选择
Java代码
1、 public boolean addUserGameInfo(UserGameInfo userGameInfo){
2、 //1.根据UserGameInfo.uid 进行数据源路由选择
3、 DataSourceRouter.setRouterStrategy(
4、 RouterStrategy.SRATEGY_TYPE_CLUSTER ,
5、 null,
6、 userGameInfo.getUid());
7、
8、 //2.数据库存储
9、 try {
10、 userGameInfoDAO.insert(userGameInfo);
11、 return true;
12、 } catch (SQLException e) {
13、 e.printStackTrace();
14、 logger.debug(“Insert UserGameInfo failed. ” + userGameInfo.toString());
15、 }
16、 return false;
17、 }