在开发中我们经常遇到多数据源的情况,比如读写分离、分库分表等情况,而且不仅仅是要配置多个数据源,还得在代码中动态的切换数据源。比如写操作访问主库数据源(master),读操作访问从库数据源(slave)。
Spring框架中提供了AbstractRoutingDataSource接口来帮助我们灵活动态的切换数据源。
假设有一个订单表tb_order,表结构如下:
CREATE TABLE `tb_order` (
`id` BIGINT(10) NOT NULL AUTO_INCREMENT COMMENT '自增id主键',
`customer_name` VARCHAR(40) NOT NULL COMMENT '客户名称',
`total_price` DOUBLE (8,2) NOT NULL COMMENT '商品总价',
`amount` INT(6) NOT NULL COMMENT '商品数量',
`address` VARCHAR(200) NOT NULL COMMENT '客户详细地址',
PRIMARY KEY (`ID`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
tb_order表对应的dao如下:
package com.ricky.codelab.spring.jdbc.dao.impl;
import com.ricky.codelab.spring.domain.Order;
import com.ricky.codelab.spring.jdbc.dao.IOrderDao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-10-18 23:06
*/
@Repository("orderDao")
public class OrderDaoImpl implements IOrderDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public long insert(Order order) {
String sql = "INSERT INTO tb_order(customer_name,total_price,amount,address) VALUES (?,?,?,?)";
return jdbcTemplate.update(sql, order.getCustomerName(),
order.getTotalPrice(), order.getAmount(), order.getAddress());
}
@Override
public List queryOrders(){
return jdbcTemplate.query("SELECT * FROM tb_order", new RowMapper() {
@Override
public Order mapRow(ResultSet rs, int i) throws SQLException {
Order order = new Order();
order.setId(rs.getLong("id"));
order.setCustomerName(rs.getString("customer_name"));
order.setTotalPrice(rs.getDouble("total_price"));
order.setAmount(rs.getInt("amount"));
order.setAddress(rs.getString("address"));
return order;
}
});
}
}
Order类如下:
package com.ricky.codelab.spring.domain;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-10-18 23:02
*/
public class Order {
private Long id;
private String customerName;
private double totalPrice;
private int amount;
private String address;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public double getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(double totalPrice) {
this.totalPrice = totalPrice;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
Spring数据源配置如下:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
default-lazy-init="false">
<context:annotation-config />
<context:component-scan base-package="com.ricky.codelab.spring" />
<bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close" abstract="true" init-method="init" >
<property name="initialSize" value="2" />
<property name="maxActive" value="10" />
<property name="minIdle" value="5" />
<property name="maxWait" value="30000" />
<property name="validationQuery" value="SELECT 1" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="25200000" />
<property name="removeAbandoned" value="true" />
<property name="removeAbandonedTimeout" value="1800" />
<property name="logAbandoned" value="true" />
<property name="filters" value="mergeStat" />
bean>
<bean id="masterDataSource" parent="parentDataSource">
<property name="url" value="#{jdbc['master.jdbc.url']}" />
<property name="username" value="#{jdbc['master.jdbc.username']}" />
<property name="password" value="#{jdbc['master.jdbc.password']}" />
<property name="driverClassName" value="#{jdbc['master.jdbc.driver']}" />
<property name="maxActive" value="15" />
bean>
<bean id="slave1DataSource" parent="parentDataSource">
<property name="url" value="#{jdbc['slave1.jdbc.url']}" />
<property name="username" value="#{jdbc['slave1.jdbc.username']}" />
<property name="password" value="#{jdbc['slave1.jdbc.password']}" />
<property name="driverClassName" value="#{jdbc['slave1.jdbc.driver']}" />
bean>
<bean id="slave2DataSource" parent="parentDataSource">
<property name="url" value="#{jdbc['slave2.jdbc.url']}" />
<property name="username" value="#{jdbc['slave2.jdbc.username']}" />
<property name="password" value="#{jdbc['slave2.jdbc.password']}" />
<property name="driverClassName" value="#{jdbc['slave2.jdbc.driver']}" />
bean>
<bean id="dataSource" class="com.ricky.codelab.spring.ds.DynamicRoutingDataSource">
<property name="targetDataSources">
<map key-type="com.ricky.codelab.spring.ds.RouteStrategy">
<entry key="slave1" value-ref="slave1DataSource"/>
<entry key="slave2" value-ref="slave2DataSource"/>
map>
property>
<property name="defaultTargetDataSource" ref="masterDataSource"/>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
bean>
beans>
jdbc.properties
master.jdbc.driver=com.mysql.jdbc.Driver
master.jdbc.url=jdbc:mysql://localhost:3306/process?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
master.jdbc.username=root
master.jdbc.password=root
slave1.jdbc.driver=com.mysql.jdbc.Driver
slave1.jdbc.url=jdbc:mysql://localhost:3308/process?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
slave1.jdbc.username=root
slave1.jdbc.password=root
slave2.jdbc.driver=com.mysql.jdbc.Driver
slave2.jdbc.url=jdbc:mysql://localhost:3309/process?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
slave2.jdbc.username=root
slave2.jdbc.password=root
DynamicRoutingDataSource.java
package com.ricky.codelab.spring.ds;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源切换
*
* @author Ricky Fung
* @create 2016-10-18 22:41
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicRoutingContextHolder.getRouteStrategy();
}
}
package com.ricky.codelab.spring.ds;
import org.springframework.util.Assert;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-10-18 22:44
*/
public class DynamicRoutingContextHolder {
private static final ThreadLocal contextHolder =
new ThreadLocal<>();
public static void setRouteStrategy(RoutingStrategy customerType) {
Assert.notNull(customerType, "customerType cannot be null");
contextHolder.set(customerType);
}
public static RoutingStrategy getRouteStrategy() {
return (RoutingStrategy) contextHolder.get();
}
public static void clearRouteStrategy() {
contextHolder.remove();
}
}
package com.ricky.codelab.spring.ds;
/**
* 路由策略
*
* @author Ricky Fung
* @create 2016-10-18 22:47
*/
public enum RoutingStrategy {
Master(true, "master"), Slave(false, "slave");
private boolean write;
private String key;
RoutingStrategy(boolean write, String key) {
this.write = write;
this.key = key;
}
public boolean isWrite() {
return write;
}
public String getKey() {
return key;
}
}
测试用例
package com.ricky.codelab.spring.dao;
import com.ricky.codelab.spring.domain.Order;
import com.ricky.codelab.spring.ds.DynamicRoutingContextHolder;
import com.ricky.codelab.spring.ds.RoutingStrategy;
import com.ricky.codelab.spring.jdbc.dao.IOrderDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-10-23 15:25
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext.xml"})
public class OrderDaoTest {
@Autowired
private IOrderDao orderDao;
@Test
public void testDynamic() {
//访问主库
DynamicRoutingContextHolder.setRouteStrategy(RoutingStrategy.Master);
long id = orderDao.insert(new Order());
//访问从库
DynamicRoutingContextHolder.setRouteStrategy(RoutingStrategy.Slave);
List list = orderDao.queryOrders();
}
}
参考:
https://spring.io/blog/2007/01/23/dynamic-datasource-routing/