Spring实现数据库读写分离

借助于Spring框架在2.0.1之后提供的AbstractRoutingDataSource可以实现动态的选择数据源datasource,下面先举一个最简单的例子(可以参考这个链接):
1. 首先新建一个Catalog VO对象的DAO(见代码1),它继承了SimpleJdbcDaoSupport,JdbcDaoSupport需要注入一个DataSource,同时也提供了操作模板JdbcTemplate。添加一个方法用于获取所有的“货物Item”。货物Iteam是一个POJO类(见代码2)。
代码1:
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport;
 
public class Catalog extends SimpleJdbcDaoSupport {
 
   public List<Item> getItems() {
      String query = "select name, price from item";
      return getSimpleJdbcTemplate().query(query, new ParameterizedRowMapper<Item>() {
            public Item mapRow(ResultSet rs, int row) throws SQLException {
               String name = rs.getString(1);
               double price = rs.getDouble(2);
               return new Item(name, price);
            }
      });
   }
}
代码2:
public class Item {
 
   private String name;
   private double price;
 
   public Item(String name, double price) {
      this.name = name;
      this.price = price;
   }
 
   public String getName() {
      return name;
   }
 
   public double getPrice() {
      return price;
   }
 
   public String toString() {
      return name + " (" + price + ")";
   }
 
}
2. 配置Spring多数据源,这里配置了一个主库和一个从库,他们可以共同继承一个父的数据源。
代码3:
<bean id="parentDataSource"
         class="org.springframework.jdbc.datasource.DriverManagerDataSource"
         abstract="true">
   <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
   <property name="username" value="test"/>
</bean>
 
<!--  主库 -->
<bean id="masterDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:mysql://localhost:${db.port.master}/blog"/>
</bean>
 
<!--  从库 -->
<bean id="slaveDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:mysql://localhost:${db.port.slave}/blog"/>
</bean>
 
<!-- 用PropertyPlaceholderConfigurer来读取properties配置,此处省略配置 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="location" value="classpath:/blog/datasource/db.properties"/>
</bean>
3. 新建一个datasource继承自AbstractRoutingDataSource,并且覆盖determineCurrentLookupKey()方法,每次用这个datasource获取数据库连接的时候都会回调这个方法获得key,根据返回的字符串key(也可以是枚举值,数字类型),动态地通过datasource配置的id来在Spring的配置文件中找到相应的datasource来获取connection(见代码4)。那么如果每次访问都需要根据key来决定如何选择数据源,那么这个key必须要保证线程安全,并发情况下每个线程都会去寻找本应该属于自己的key获取数据源,所以CustomerContextHolder类中就用到了ThreadLocal来保证(见代码5)。
代码4:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 
public class CustomerRoutingDataSource extends AbstractRoutingDataSource {
 
   @Override
   protected Object determineCurrentLookupKey() {
      return CustomerContextHolder.getCustomerType();
   }
}
代码5:
public class CustomerContextHolder {
 
   private static final ThreadLocal<String> contextHolder =
            new ThreadLocal<String>();
 
   public static void setCustomerType(String customerType) {
      Assert.notNull(customerType, "customerType cannot be null");
      contextHolder.set(customerType);
   }
 
   public static String getCustomerType() {
      return (String) contextHolder.get();
   }
 
   public static void clearCustomerType() {
      contextHolder.remove();
   }
}
在Spring中的配置如下:
代码6:
<bean id="catalog" class="blog.datasource.Catalog">
   <property name="dataSource" ref="dataSource"/>
</bean>
 
<bean id="dataSource" class="blog.datasource.CustomerRoutingDataSource">
   <!-- 这个targetDataSource是必须要注入的 -->
<property name="targetDataSources">
      <map key-type="java.lang.String">
         <entry key="MASTER" value-ref="masterDataSource"/>
         <entry key="SLAVE" value-ref="slaveDataSource"/>
      </map>
   </property>
   <!-- 默认的数据源 -->
   <property name="defaultTargetDataSource" ref="masterDataSource"/>
</bean>
4. 测试用例
代码7:
AbstractDependencyInjectionSpringContextTests {
 
   private Catalog catalog;
 
   public void setCatalog(Catalog catalog) {
      this.catalog = catalog;
   }
 
   public void testDataSourceRouting() {
      CustomerContextHolder.setCustomerType(“MASTER”);
      List<Item> goldItems = catalog.getItems();
      assertEquals(3, goldItems.size());
      System.out.println("gold items: " + goldItems);
 
      CustomerContextHolder.setCustomerType(“SLAVE”);
      List<Item> silverItems = catalog.getItems();
      assertEquals(2, silverItems.size());
      System.out.println("silver items: " + silverItems);
 
      CustomerContextHolder.clearCustomerType();
      List<Item> bronzeItems = catalog.getItems();
      assertEquals(1, bronzeItems.size());
      System.out.println("bronze items: " + bronzeItems);
   }
 
   protected String[] getConfigLocations() {
      return new String[] {"/blog/datasource/beans.xml"};
   }
}