1. The introduction

The author is doing an Internet “SNS like” application recently, the number of users in the application is huge (about 40 million), therefore, simple use of traditional single database storage is definitely not good.

Referring to the widely used database and table, and the use of DAL data access layer, the author decided to use the simplest data source routing method to solve the problem.

Strictly speaking, the current implementation is not a solution, only a simple implementation of a way of thinking, the author only spent 2 days to complete (1.5 days of reading materials and Spring/ IBatis source code). Here also just provides a train of thought reference for everybody to see an officer, leave a note to oneself incidentally

2. System design premise

Our system uses 16 database instances (currently distributed on 2 physical machines, which will be gradually moved to 16 physical machines in the future according to the increase of system load). The 16 libraries are simple hash assignments based on the user’s UserID. It’s worth noting that since we’ve done this horizontal shard design, we’ve taken into account the characteristics of the system requirements,

  • 1. Frequent cross-library access does not occur.

  • 2. The main business logic is centered around UserID and can be completed in a single library transaction.

    On the system, we use Spring and iBatis. Spring is responsible for transaction management AOP for the database and IOC between beans. The biggest reason for choosing iBatis is the performance optimization for Sql and the ease with which Sql table names can be replaced later if there is a sub-table requirement.

3. Design ideas

First, I want to explain the idea of the author, actually very simple, that is, “before each database operation, determine the currently selected database object” and then access the currently selected database as if accessing a single library.

Second, to select the database before each DB access, we need to clarify several questions: 1. When iBatis obtains the specific database Connection from the DataSource, 2. Whether iBatis caches the Connection, because in the case of multiple libraries, the Connection is cached, which means that the database link selection cannot be changed in time. 3. Since we use Spring to manage DB transactions, we must know whether Spring’s interception of DB Connction on or off affects the multi-datasource situation.

Fortunately, a look at the source code shows that both iBatis and Spring are controlled via the standard DataSource interface

Connection, which saves us a lot of trouble, just need to implement a DataSource that can support multiple databases, to achieve our goal.

4. Code and implementation

MultiDataSource implementation: MultiDataSource. Class

Java code

  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. import javax.sql.DataSource;
  9. import org.apache.log4j.Logger;
  10. import com.xxx.sql.DataSourceRouter.RouterStrategy;
  11. / * *
  12. * Composite multiple Data Sources (Alpha)
  13. * @author [email protected]
  14. * Jul 15, 2010
  15. * /
  16. public class MultiDataSource implements DataSource {
  17. static Logger logger = Logger.getLogger(MultiDataSource.class);
  18. // The actual DataSource for the current thread
  19. private ThreadLocal currentDataSourceHolder = new ThreadLocal();
  20. // Use the DataSource of the key-value mapping
  21. private Map<String , DataSource> mappedDataSources;
  22. // Use a distributed DataSource that is horizontally shard
  23. private ArrayList clusterDataSources;
  24. public MultiDataSource(){
  25. ​ mappedDataSources = new HashMap<String , DataSource>(4);
  26. ​ clusterDataSources = new ArrayList(4);
  27. }
  28. / * *
  29. * Database connection pool initialization
  30. * This method is usually called when the Web application starts
  31. * /
  32. public void initialMultiDataSource(){
  33. for(DataSource ds : clusterDataSources){
  34. if(ds ! = null){
  35. ​ Connection conn = null;
  36. try {
  37. ​ conn = ds.getConnection();
  38. ​ } catch (SQLException e) {
  39. ​ e.printStackTrace();
  40. ​ } finally{
  41. if(conn ! = null){
  42. try {
  43. ​ conn.close();
  44. ​ } catch (SQLException e) {
  45. ​ e.printStackTrace();
  46. ​ }
  47. ​ conn = null;
  48. ​ }
  49. ​ }
  50. ​ }
  51. ​ }
  52. ​ Collection dsCollection = mappedDataSources.values();
  53. for(DataSource ds : dsCollection){
  54. if(ds ! = null){
  55. ​ Connection conn = null;
  56. try {
  57. ​ conn = ds.getConnection();
  58. ​ } catch (SQLException e) {
  59. ​ e.printStackTrace();
  60. ​ } finally{
  61. if(conn ! = null){
  62. try {
  63. ​ conn.close();
  64. ​ } catch (SQLException e) {
  65. ​ e.printStackTrace();
  66. ​ }
  67. ​ conn = null;
  68. ​ }
  69. ​ }
  70. ​ }
  71. ​ }
  72. }
  73. / * *
  74. * Gets the DataSource of the current thread binding
  75. * @return
  76. * /
  77. public DataSource getCurrentDataSource() {
  78. // If the routing policy exists and has been updated, the new DataSource is selected according to the routing algorithm
  79. ​ RouterStrategy strategy = DataSourceRouter.currentRouterStrategy.get();
  80. if(strategy == null){
  81. throw new IllegalArgumentException(“DataSource RouterStrategy No found.”);
  82. ​ }
  83. if(strategy ! = null && strategy.isRefresh()){
  84. if(RouterStrategy.SRATEGY_TYPE_MAP.equals(strategy.getType())){
  85. this.choiceMappedDataSources(strategy.getKey());
  86. ​ }else if(RouterStrategy.SRATEGY_TYPE_CLUSTER.equals(strategy.getType())){
  87. this.routeClusterDataSources(strategy.getRouteFactor());
  88. ​ }
  89. ​ strategy.setRefresh(false);
  90. ​ }
  91. return currentDataSourceHolder.get();
  92. }
  93. public Map<String, DataSource> getMappedDataSources() {
  94. return mappedDataSources;
  95. }
  96. public void setMappedDataSources(Map<String, DataSource> mappedDataSources) {
  97. this.mappedDataSources = mappedDataSources;
  98. }
  99. public ArrayList getClusterDataSources() {
  100. return clusterDataSources;
  101. }
  102. public void setClusterDataSources(ArrayList clusterDataSources) {
  103. this.clusterDataSources = clusterDataSources;
  104. }
  105. / * *
  106. * Select the current data source with Key
  107. * @param key
  108. * /
  109. public void choiceMappedDataSources(String key){
  110. ​ DataSource ds = this.mappedDataSources.get(key);
  111. if(ds == null){
  112. throw new IllegalStateException(“No Mapped DataSources Exist!” );
  113. ​ }
  114. this.currentDataSourceHolder.set(ds);
  115. }
  116. / * *
  117. * Route selection in the cluster data source using a modular algorithm
  118. * @param routeFactor
  119. * /
  120. public void routeClusterDataSources(int routeFactor){
  121. int size = this.clusterDataSources.size();
  122. if(size == 0){
  123. throw new IllegalStateException(“No Cluster DataSources Exist!” );
  124. ​ }
  125. int choosen = routeFactor % size;
  126. ​ DataSource ds = this.clusterDataSources.get(choosen);
  127. if(ds == null){
  128. throw new IllegalStateException(“Choosen DataSources is null!” );
  129. ​ }
  130. ​ logger.debug(“Choosen DataSource No.” + choosen+ ” : ” + ds.toString());
  131. this.currentDataSourceHolder.set(ds);
  132. }
  133. /* (non-Javadoc)
  134. * @see javax.sql.DataSource#getConnection()
  135. * /
  136. public Connection getConnection() throws SQLException {
  137. if(getCurrentDataSource() ! = null){
  138. return getCurrentDataSource().getConnection();
  139. ​ }
  140. return null;
  141. }
  142. /* (non-Javadoc)
  143. * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
  144. * /
  145. public Connection getConnection(String username, String password)
  146. throws SQLException {
  147. if(getCurrentDataSource() ! = null){
  148. return getCurrentDataSource().getConnection(username , password);
  149. ​ }
  150. return null;
  151. }
  152. /* (non-Javadoc)
  153. * @see javax.sql.CommonDataSource#getLogWriter()
  154. * /
  155. public PrintWriter getLogWriter() throws SQLException {
  156. if(getCurrentDataSource() ! = null){
  157. return getCurrentDataSource().getLogWriter();
  158. ​ }
  159. return null;
  160. }
  161. /* (non-Javadoc)
  162. * @see javax.sql.CommonDataSource#getLoginTimeout()
  163. * /
  164. public int getLoginTimeout() throws SQLException {
  165. if(getCurrentDataSource() ! = null){
  166. return getCurrentDataSource().getLoginTimeout();
  167. ​ }
  168. return 0;
  169. }
  170. /* (non-Javadoc)
  171. * @see javax.sql.CommonDataSource#setLogWriter(java.io.PrintWriter)
  172. * /
  173. public void setLogWriter(PrintWriter out) throws SQLException {
  174. if(getCurrentDataSource() ! = null){
  175. ​ getCurrentDataSource().setLogWriter(out);
  176. ​ }
  177. }
  178. /* (non-Javadoc)
  179. * @see javax.sql.CommonDataSource#setLoginTimeout(int)
  180. * /
  181. public void setLoginTimeout(int seconds) throws SQLException {
  182. if(getCurrentDataSource() ! = null){
  183. ​ getCurrentDataSource().setLoginTimeout(seconds);
  184. ​ }
  185. }
  186. /* (non-Javadoc)
  187. * The interface method since 1.6
  188. * Not all DataSource implementations have this method
  189. * @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
  190. * /
  191. public boolean isWrapperFor(Class
    iface) throws SQLException {
  192. // if(getCurrentDataSource() ! = null){
  193. // return getCurrentDataSource().isWrapperFor(iface);
  194. // }
  195. return false;
  196. }
  197. /* (non-Javadoc)
  198. * The interface method since 1.6
  199. * Not all DataSource implementations have this method
  200. * @see java.sql.Wrapper#unwrap(java.lang.Class)
  201. * /
  202. public T unwrap(Class iface) throws SQLException {
  203. // if(getCurrentDataSource() ! = null){
  204. // return getCurrentDataSource().unwrap(iface);
  205. // }
  206. return null;
  207. }
  208. }

This class implements the standard DataSource interface, with the core getConnection() method overloading. The following is specific:

  • 1. Instance variable clusterDataSources is an ArrayList of DataSource instances, which stores the DataSource instances of multiple databases. We use the IOC function of Spring to inject multiple DataSource instances into this list.
  • MappedDataSources is a Map of DataSource instances. Like clusterDataSources, mappedDataSources is used to store DataSource instances of multiple databases. The difference is that mappedDataSources can directly obtain DataSource instances using key. We will also use Spring’s IOC functionality to inject multiple DataSource into the Map.
  • CurrentDataSourceHolder is a ThreadLocal variable that holds the DataSource instances associated with the current thread. This is so that when accessing the same database multiple times in the same thread, there is no need to re-route.
  • 4. When an external class calls the getConnection() method, the method selects the corresponding DataSource from clusterDataSources or mappedDataSources based on the routing rules of the context and returns the Connection.

(PS: You can design routing rules for a DataSource based on application scenarios. The author provides two simple ideas here. 1. According to HashCode, which can be UserId in the above example, carry out modular operation to locate the database. 2. Select the mapping DataSource from the map according to the key set in the context.

DataSourceRouter. Class:

  1. / * *
  2. * @author [email protected]
  3. * Jul 15, 2010
  4. * /
  5. public class DataSourceRouter {
  6. public static ThreadLocal currentRouterStrategy =
  7. new ThreadLocal();
  8. / * *
  9. * Set the routing policy for the MultiDataSource
  10. * @param type
  11. * @param key
  12. * @param routeFactor
  13. * /
  14. public static void setRouterStrategy(String type , String key , int routeFactor){
  15. if(type == null){
  16. throw new IllegalArgumentException(“RouterStrategy Type must not be null”);
  17. ​ }
  18. ​ RouterStrategy rs = currentRouterStrategy.get();
  19. if(rs == null){
  20. ​ rs = new RouterStrategy();
  21. ​ currentRouterStrategy.set(rs);
  22. ​ }
  23. ​ rs.setType(type);
  24. ​ rs.setKey(key);
  25. ​ rs.setRouteFactor(routeFactor);
  26. }
  27. / * *
  28. * Data source routing policy
  29. * @author [email protected]
  30. * Jul 15, 2010
  31. * /
  32. public static class RouterStrategy{
  33. public static final String SRATEGY_TYPE_MAP = “MAP”;
  34. public static final String SRATEGY_TYPE_CLUSTER = “CLUSTER”;
  35. / *
  36. * Optional values “MAP”, “CLUSTER”
  37. * MAP: Select DS from DataSourceMap based on key
  38. * CLUSTER: CLUSTER is obtained by the algorithm based on the routeFactor parameter
  39. * /
  40. private String type;
  41. / *
  42. * Key in “MAP” ROUTE
  43. ​ *
  44. * /
  45. private String key;
  46. / *
  47. * Parameter for “CLUSTER” ROUTE
  48. * /
  49. private int routeFactor;
  50. / *
  51. * True indicates that the RouterStrategy has been updated
  52. * False indicates no update
  53. * /
  54. private boolean refresh;
  55. public String getType() {
  56. return type;
  57. ​ }
  58. public void setType(String type) {
  59. if(this.type ! = null && ! this.type.equals(type)){
  60. this.type = type;
  61. this.refresh = true;
  62. }else if(this.type == null && type ! = null){
  63. this.type = type;
  64. this.refresh = true;
  65. ​ }
  66. ​ }
  67. public String getKey() {
  68. return key;
  69. ​ }
  70. public void setKey(String key) {
  71. if(this.key ! = null && ! this.key.equals(key)){
  72. this.key = key;
  73. this.refresh = true;
  74. }else if(this.key == null && key ! = null){
  75. this.key = key;
  76. this.refresh = true;
  77. ​ }
  78. ​ }
  79. public int getRouteFactor() {
  80. return routeFactor;
  81. ​ }
  82. public void setRouteFactor(int routeFactor) {
  83. if(this.routeFactor ! = routeFactor){
  84. this.routeFactor = routeFactor;
  85. this.refresh = true;
  86. ​ }
  87. ​ }
  88. public boolean isRefresh() {
  89. return refresh;
  90. ​ }
  91. public void setRefresh(boolean refresh) {
  92. this.refresh = refresh;
  93. ​ }
  94. }
  95. }

5. Combine MultiDataSource with Spring and iBatis

After completing the above coding process, it is time to combine the MultiDataSource configuration with existing Spring and iBatis.

STEP 1. Configuring multiple data sources

Here I use C3P0 as the database connection pool, which is the same step as the standard Spring configuration, except that you now have multiple configurations instead of just one

Xml code

  1. <bean id=”c3p0_dataSource_1″ class=”com.mchange.v2.c3p0.ComboPooledDataSource” destroy-method=”close”>
  2. <property name=”driverClass”>
  3. ${jdbc.driverClass}
  4. <property name=”jdbcUrl”>
  5. ${mysql.url_1}
  6. <property name=”user”>
  7. ${jdbc.username}
  8. <property name=”password”>
  9. ${jdbc.password}
  10. <property name=”minPoolSize”>
  11. ${c3p0.minPoolSize}
  12. <property name=”maxPoolSize”>
  13. ${c3p0.maxPoolSize}
  14. <property name=”initialPoolSize”>
  15. ${c3p0.initialPoolSize}
  16. <property name=”idleConnectionTestPeriod”>
  17. ${c3p0.idleConnectionTestPeriod}
  18. <bean id=”c3p0_dataSource_2″ class=”com.mchange.v2.c3p0.ComboPooledDataSource” destroy-method=”close”>
  19. <property name=”driverClass”>
  20. ${jdbc.driverClass}
  21. <property name=”jdbcUrl”>
  22. ${mysql.url_2}
  23. <property name=”user”>
  24. ${jdbc.username}
  25. <property name=”password”>
  26. ${jdbc.password}
  27. <property name=”minPoolSize”>
  28. ${c3p0.minPoolSize}
  29. <property name=”maxPoolSize”>
  30. ${c3p0.maxPoolSize}
  31. <property name=”initialPoolSize”>
  32. ${c3p0.initialPoolSize}
  33. <property name=”idleConnectionTestPeriod”>
  34. ${c3p0.idleConnectionTestPeriod}
  35. .

STEP 2. Inject multiple data sources into the MultiDataSource

Xml code

  1. <bean id=”multiDataSource” class=”com.xxx.sql.MultiDataSource”>
  2. <property name=”clusterDataSources”>
  3. <ref bean=”c3p0_dataSource_1″ />
  4. <ref bean=”c3p0_dataSource_2″ />
  5. <ref bean=”c3p0_dataSource_3″ />
  6. <ref bean=”c3p0_dataSource_4″ />
  7. <ref bean=”c3p0_dataSource_5″ />
  8. <ref bean=”c3p0_dataSource_6″ />
  9. <ref bean=”c3p0_dataSource_7″ />
  10. <ref bean=”c3p0_dataSource_8″ />
  11. <property name=”mappedDataSources”>
  12. <entry key=”system” value-ref=”c3p0_dataSource_system” />

STEP 3. Use the MultiDataSource just as you would a standard DataSource

Xml code

  1. <bean id=”sqlMapClient” class=”org.springframework.orm.ibatis.SqlMapClientFactoryBean”>
  2. <property name=”configLocation” value=”classpath:SqlMapConfig.xml”/>
  3. <property name=”dataSource” ref=”multiDataSource”>
  4. <bean id=”jdbc_TransactionManager” class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
  5. <property name=”dataSource” ref=”multiDataSource”>

At this point, our program can let Spring manage multi-library access, but note that database transactions are still limited to single-library scope (as mentioned earlier, there are no cross-library transactions in this scenario).

6.Java code usage examples

First of all, we only provide a simple example. In the example, we must manually call the API to determine the routing rules of the DataSource. In the actual application, you can encapsulate the routing rules according to your own business characteristics to achieve relatively transparent routing

Java code

  1. public boolean addUserGameInfo(UserGameInfo userGameInfo){
  2. //1. Route the data source according to usergameInfo. uid
  3. DataSourceRouter.setRouterStrategy(
  4. ​ RouterStrategy.SRATEGY_TYPE_CLUSTER ,
  5. null,
  6. ​ userGameInfo.getUid());
  7. //2. Database storage
  8. try {
  9. ​ userGameInfoDAO.insert(userGameInfo);
  10. return true;
  11. } catch (SQLException e) {
  12. ​ e.printStackTrace();
  13. ​ logger.debug(“Insert UserGameInfo failed. ” + userGameInfo.toString());
  14. }
  15. return false;
  16. }