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
- import java.io.PrintWriter;
- import java.sql.Connection;
- import java.sql.SQLException;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.HashMap;
- import java.util.Map;
- import javax.sql.DataSource;
- import org.apache.log4j.Logger;
- import com.xxx.sql.DataSourceRouter.RouterStrategy;
- / * *
- * Composite multiple Data Sources (Alpha)
- * @author [email protected]
- * Jul 15, 2010
- * /
- public class MultiDataSource implements DataSource {
- static Logger logger = Logger.getLogger(MultiDataSource.class);
- // The actual DataSource for the current thread
- private ThreadLocal currentDataSourceHolder = new ThreadLocal();
- // Use the DataSource of the key-value mapping
- private Map<String , DataSource> mappedDataSources;
- // Use a distributed DataSource that is horizontally shard
- private ArrayList clusterDataSources;
- public MultiDataSource(){
- mappedDataSources = new HashMap<String , DataSource>(4);
- clusterDataSources = new ArrayList(4);
- }
- / * *
- * Database connection pool initialization
- * This method is usually called when the Web application starts
- * /
- public void initialMultiDataSource(){
- for(DataSource ds : clusterDataSources){
- if(ds ! = null){
- Connection conn = null;
- try {
- conn = ds.getConnection();
- } catch (SQLException e) {
- e.printStackTrace();
- } finally{
- if(conn ! = null){
- try {
- conn.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- conn = null;
- }
- }
- }
- }
- Collection dsCollection = mappedDataSources.values();
- for(DataSource ds : dsCollection){
- if(ds ! = null){
- Connection conn = null;
- try {
- conn = ds.getConnection();
- } catch (SQLException e) {
- e.printStackTrace();
- } finally{
- if(conn ! = null){
- try {
- conn.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- conn = null;
- }
- }
- }
- }
- }
- / * *
- * Gets the DataSource of the current thread binding
- * @return
- * /
- public DataSource getCurrentDataSource() {
- // If the routing policy exists and has been updated, the new DataSource is selected according to the routing algorithm
- RouterStrategy strategy = DataSourceRouter.currentRouterStrategy.get();
- if(strategy == null){
- throw new IllegalArgumentException(“DataSource RouterStrategy No found.”);
- }
- if(strategy ! = null && strategy.isRefresh()){
- if(RouterStrategy.SRATEGY_TYPE_MAP.equals(strategy.getType())){
- this.choiceMappedDataSources(strategy.getKey());
-
- }else if(RouterStrategy.SRATEGY_TYPE_CLUSTER.equals(strategy.getType())){
- this.routeClusterDataSources(strategy.getRouteFactor());
- }
- strategy.setRefresh(false);
- }
- return currentDataSourceHolder.get();
- }
- public Map<String, DataSource> getMappedDataSources() {
- return mappedDataSources;
- }
- public void setMappedDataSources(Map<String, DataSource> mappedDataSources) {
- this.mappedDataSources = mappedDataSources;
- }
- public ArrayList getClusterDataSources() {
- return clusterDataSources;
- }
- public void setClusterDataSources(ArrayList clusterDataSources) {
- this.clusterDataSources = clusterDataSources;
- }
- / * *
- * Select the current data source with Key
- * @param key
- * /
- public void choiceMappedDataSources(String key){
- DataSource ds = this.mappedDataSources.get(key);
- if(ds == null){
- throw new IllegalStateException(“No Mapped DataSources Exist!” );
- }
- this.currentDataSourceHolder.set(ds);
- }
- / * *
- * Route selection in the cluster data source using a modular algorithm
- * @param routeFactor
- * /
- public void routeClusterDataSources(int routeFactor){
- int size = this.clusterDataSources.size();
- if(size == 0){
- throw new IllegalStateException(“No Cluster DataSources Exist!” );
- }
- int choosen = routeFactor % size;
- DataSource ds = this.clusterDataSources.get(choosen);
- if(ds == null){
- throw new IllegalStateException(“Choosen DataSources is null!” );
- }
- logger.debug(“Choosen DataSource No.” + choosen+ ” : ” + ds.toString());
- this.currentDataSourceHolder.set(ds);
- }
- /* (non-Javadoc)
- * @see javax.sql.DataSource#getConnection()
- * /
- public Connection getConnection() throws SQLException {
- if(getCurrentDataSource() ! = null){
- return getCurrentDataSource().getConnection();
- }
- return null;
- }
- /* (non-Javadoc)
- * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
- * /
- public Connection getConnection(String username, String password)
- throws SQLException {
- if(getCurrentDataSource() ! = null){
- return getCurrentDataSource().getConnection(username , password);
- }
- return null;
- }
- /* (non-Javadoc)
- * @see javax.sql.CommonDataSource#getLogWriter()
- * /
- public PrintWriter getLogWriter() throws SQLException {
- if(getCurrentDataSource() ! = null){
- return getCurrentDataSource().getLogWriter();
- }
- return null;
- }
- /* (non-Javadoc)
- * @see javax.sql.CommonDataSource#getLoginTimeout()
- * /
- public int getLoginTimeout() throws SQLException {
- if(getCurrentDataSource() ! = null){
- return getCurrentDataSource().getLoginTimeout();
- }
- return 0;
- }
- /* (non-Javadoc)
- * @see javax.sql.CommonDataSource#setLogWriter(java.io.PrintWriter)
- * /
- public void setLogWriter(PrintWriter out) throws SQLException {
- if(getCurrentDataSource() ! = null){
- getCurrentDataSource().setLogWriter(out);
- }
- }
- /* (non-Javadoc)
- * @see javax.sql.CommonDataSource#setLoginTimeout(int)
- * /
- public void setLoginTimeout(int seconds) throws SQLException {
- if(getCurrentDataSource() ! = null){
- getCurrentDataSource().setLoginTimeout(seconds);
- }
- }
- /* (non-Javadoc)
- * The interface method since 1.6
- * Not all DataSource implementations have this method
- * @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
- * /
- public boolean isWrapperFor(Class
iface) throws SQLException { -
- // if(getCurrentDataSource() ! = null){
- // return getCurrentDataSource().isWrapperFor(iface);
- // }
- return false;
- }
- /* (non-Javadoc)
- * The interface method since 1.6
- * Not all DataSource implementations have this method
- * @see java.sql.Wrapper#unwrap(java.lang.Class)
- * /
- public T unwrap(Class iface) throws SQLException {
- // if(getCurrentDataSource() ! = null){
- // return getCurrentDataSource().unwrap(iface);
- // }
- return null;
- }
- }
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:
- / * *
- * @author [email protected]
- * Jul 15, 2010
- * /
- public class DataSourceRouter {
- public static ThreadLocal currentRouterStrategy =
- new ThreadLocal();
- / * *
- * Set the routing policy for the MultiDataSource
- * @param type
- * @param key
- * @param routeFactor
- * /
- public static void setRouterStrategy(String type , String key , int routeFactor){
- if(type == null){
- throw new IllegalArgumentException(“RouterStrategy Type must not be null”);
- }
- RouterStrategy rs = currentRouterStrategy.get();
- if(rs == null){
- rs = new RouterStrategy();
- currentRouterStrategy.set(rs);
- }
- rs.setType(type);
- rs.setKey(key);
- rs.setRouteFactor(routeFactor);
- }
- / * *
- * Data source routing policy
- * @author [email protected]
- * Jul 15, 2010
- * /
- public static class RouterStrategy{
-
- public static final String SRATEGY_TYPE_MAP = “MAP”;
- public static final String SRATEGY_TYPE_CLUSTER = “CLUSTER”;
- / *
- * Optional values “MAP”, “CLUSTER”
- * MAP: Select DS from DataSourceMap based on key
- * CLUSTER: CLUSTER is obtained by the algorithm based on the routeFactor parameter
- * /
- private String type;
- / *
- * Key in “MAP” ROUTE
- *
- * /
- private String key;
- / *
- * Parameter for “CLUSTER” ROUTE
- * /
- private int routeFactor;
- / *
- * True indicates that the RouterStrategy has been updated
- * False indicates no update
- * /
- private boolean refresh;
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- if(this.type ! = null && ! this.type.equals(type)){
- this.type = type;
- this.refresh = true;
- }else if(this.type == null && type ! = null){
- this.type = type;
- this.refresh = true;
- }
- }
-
- public String getKey() {
- return key;
- }
-
- public void setKey(String key) {
- if(this.key ! = null && ! this.key.equals(key)){
- this.key = key;
- this.refresh = true;
- }else if(this.key == null && key ! = null){
- this.key = key;
- this.refresh = true;
- }
- }
-
- public int getRouteFactor() {
- return routeFactor;
- }
-
- public void setRouteFactor(int routeFactor) {
- if(this.routeFactor ! = routeFactor){
- this.routeFactor = routeFactor;
- this.refresh = true;
- }
- }
-
- public boolean isRefresh() {
- return refresh;
- }
-
- public void setRefresh(boolean refresh) {
- this.refresh = refresh;
- }
- }
- }
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
- <bean id=”c3p0_dataSource_1″ class=”com.mchange.v2.c3p0.ComboPooledDataSource” destroy-method=”close”>
- <property name=”driverClass”>
- ${jdbc.driverClass}
- <property name=”jdbcUrl”>
- ${mysql.url_1}
-
- <property name=”user”>
- ${jdbc.username}
- <property name=”password”>
- ${jdbc.password}
- <property name=”minPoolSize”>
- ${c3p0.minPoolSize}
-
- <property name=”maxPoolSize”>
- ${c3p0.maxPoolSize}
- <property name=”initialPoolSize”>
- ${c3p0.initialPoolSize}
- <property name=”idleConnectionTestPeriod”>
- ${c3p0.idleConnectionTestPeriod}
-
- <bean id=”c3p0_dataSource_2″ class=”com.mchange.v2.c3p0.ComboPooledDataSource” destroy-method=”close”>
- <property name=”driverClass”>
- ${jdbc.driverClass}
- <property name=”jdbcUrl”>
- ${mysql.url_2}
-
- <property name=”user”>
- ${jdbc.username}
- <property name=”password”>
- ${jdbc.password}
- <property name=”minPoolSize”>
- ${c3p0.minPoolSize}
-
- <property name=”maxPoolSize”>
- ${c3p0.maxPoolSize}
- <property name=”initialPoolSize”>
- ${c3p0.initialPoolSize}
- <property name=”idleConnectionTestPeriod”>
- ${c3p0.idleConnectionTestPeriod}
-
- .
STEP 2. Inject multiple data sources into the MultiDataSource
Xml code
- <bean id=”multiDataSource” class=”com.xxx.sql.MultiDataSource”>
- <property name=”clusterDataSources”>
-
- <ref bean=”c3p0_dataSource_1″ />
- <ref bean=”c3p0_dataSource_2″ />
- <ref bean=”c3p0_dataSource_3″ />
- <ref bean=”c3p0_dataSource_4″ />
- <ref bean=”c3p0_dataSource_5″ />
- <ref bean=”c3p0_dataSource_6″ />
- <ref bean=”c3p0_dataSource_7″ />
- <ref bean=”c3p0_dataSource_8″ />
-
- <property name=”mappedDataSources”>
-
- <entry key=”system” value-ref=”c3p0_dataSource_system” />
-
STEP 3. Use the MultiDataSource just as you would a standard DataSource
Xml code
- <bean id=”sqlMapClient” class=”org.springframework.orm.ibatis.SqlMapClientFactoryBean”>
- <property name=”configLocation” value=”classpath:SqlMapConfig.xml”/>
- <property name=”dataSource” ref=”multiDataSource”>
- <bean id=”jdbc_TransactionManager” class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
- <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
- public boolean addUserGameInfo(UserGameInfo userGameInfo){
- //1. Route the data source according to usergameInfo. uid
- DataSourceRouter.setRouterStrategy(
- RouterStrategy.SRATEGY_TYPE_CLUSTER ,
- null,
- userGameInfo.getUid());
- //2. Database storage
- try {
- userGameInfoDAO.insert(userGameInfo);
- return true;
- } catch (SQLException e) {
- e.printStackTrace();
- logger.debug(“Insert UserGameInfo failed. ” + userGameInfo.toString());
- }
- return false;
- }