1.使用场景
分库分表的使用场景:对大数据量的表或者库进行分割,将数据依据逻辑规则分别放置不同的库,减轻单库单表的压力。具体又分为垂直分库,水平分库,垂直分表,水平分表。
读写分离的应用场景:应用程序在进行读取数据库数据和写入数据库数据时,分别操作不同的数据库,从数据库及时数据同主数据库保持一致,就可以提高数据库的访问能力。
2.实现方式
a. 可以使用mycat(对此不熟,仅仅听说过)
b. 使用sharding-jdbc中间件,使用demo 参考:https://segmentfault.com/a/11...:https://github.com/sharding-sphere/ShardingSphereDemo
c. 使用spring AbstractRoutingDataSource实现。
3.使用AbstractRoutingDataSource实现分库分表
创建自有的XiayuDataSource,继承AbtstractRoutingDataSource
package com.xiayu.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class XiayuDataSource extends AbstractRoutingDataSource {
//重写获取lookupkey方法
@Override
protected Object determineCurrentLookupKey() {
return RequestThreadLocalHolder.getDatabasIndex();
}
}
定义ThreadLocal变量存储类
package com.xiayu.config;
import com.xiayu.constants.DataSourceConstants;
import java.util.Locale;
public class RequestThreadLocalHolder {
private static ThreadLocal databaseIndex = new ThreadLocal(){
@Override
protected String initialValue() {
return DataSourceConstants.MASTER_DATA_SOURCE;
}
}; //数据库的线程局部变量,可以基于此实现读写分离和垂直分库,默认值为主库名
private static ThreadLocal tableIndex = new ThreadLocal(){
@Override
protected Integer initialValue() {
return 1;
}
};//表名的线程局部变量,可以基于此实现水平分表
private static ThreadLocal local = new ThreadLocal(){
@Override
protected Locale initialValue() {
return Locale.CHINA;
}
};//国家的线程局部变量,在国际化时可以使用该变量,返回对应国家的message,默认值为Locale.CHINA
public static void setDatabaseIndex(String databaseIndex){
RequestThreadLocalHolder.databaseIndex.set(databaseIndex);
} //设置
public static String getDatabasIndex(){
return RequestThreadLocalHolder.databaseIndex.get();
}//获取
public static void removeDatabasIndex(){
RequestThreadLocalHolder.databaseIndex.remove();
}//移除,下同
public static void setTableIndex(Integer tableIndex){
RequestThreadLocalHolder.tableIndex.set(tableIndex);
}
public static Integer getTableIndex(){
return RequestThreadLocalHolder.tableIndex.get();
}
public static void removeTableIndex(){
RequestThreadLocalHolder.tableIndex.remove();
}
public static void setLocal(Locale local){
RequestThreadLocalHolder.local.set(local);
}
public static Locale getLocale(){
return RequestThreadLocalHolder.local.get();
}
public static void removeLocale(){
RequestThreadLocalHolder.local.remove();
}
}
创建XiaYuDataSource
有三个数据源,分别为masterDataSource,slaveOneDataSource和slaveTwoDataSource,一主两从。
package com.xiayu.config;
import com.xiayu.constants.DataSourceConstants;
import lombok.Data;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@Data
@Configuration
@MapperScan(basePackages = { DataSourceConstants.MAPPER_BASEPACKAGE }, sqlSessionFactoryRef = "sqlSessionFactory")
public class DatasourceConfig {
// @Qualifier("masterDataSource")
private DataSource masterDataSource;
// @Qualifier("slaveOneDataSource")
private DataSource slaveOneDataSource;
// @Qualifier("slaveTwoDataSource")
private DataSource slaveTwoDataSource;
public DatasourceConfig() {
}
@Bean(name = "xiayuDataSource")
@Primary
public DataSource xiayuDataSource(){
//这一块做的不行,我通过配置文件new,调用方法的dataSource,也可以在该类中自定义方法获取DataSource,我本想通过@ConfigurationProperties注解方便url,username,password的映射
MasterDataSourceConfig masterDataSourceConfig = new MasterDataSourceConfig();
this.masterDataSource = masterDataSourceConfig.dataSource();
SlaveOneDataSourceConfig slaveOneDataSourceConfig = new SlaveOneDataSourceConfig();
this.slaveOneDataSource = slaveOneDataSourceConfig.dataSource();
SlaveTwoDataSourceConfig slaveTwoDataSourceConfig = new SlaveTwoDataSourceConfig();
this.slaveTwoDataSource = slaveTwoDataSourceConfig.dataSource();
XiayuDataSource xiayuDataSource = new XiayuDataSource();
Map
定义数据库路由注解
@Target(ElementType.METHOD) //注解作用的地方,此处定义为方法上
@Retention(RetentionPolicy.RUNTIME) //在运行时候,生效
public @interface DBRouting {
String isRead() default "false"; //java注解成员不支持布尔类型
}
定义切面,改变线程的局部变量
package com.xiayu.aspect;
import com.xiayu.annotation.DBRouting;
import com.xiayu.config.RequestThreadLocalHolder;
import com.xiayu.constants.DataSourceConstants;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component
@Aspect
@Slf4j
public class DBRoutingAspect {
@Pointcut("@annotation(com.xiayu.annotation.DBRouting)")
public void pointCut(){}; //连接点
@Before("pointCut()")
public void before(JoinPoint joinPoint) throws NoSuchMethodException { //前置通知
log.info("start request");
long startTime = System.currentTimeMillis();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod(); //获取到加入注解的方法
DBRouting dbRouting = method.getAnnotation(DBRouting.class); //获取到DBRouting注解
String isRead = dbRouting.isRead(); //获取到设置的值
boolean read = Boolean.parseBoolean(isRead);
if (read){
RequestThreadLocalHolder.setDatabaseIndex(DataSourceConstants.SLAVE_ONE_DATA_SOURCE);
//如果read 为true的话,就将线程局部变量设置为从库,然后在通过与lookupkey进行比较,获取到从库的数据源
//有两个从库,可以通过hash 取余2,分别访问两个从库
//在此也可以实现垂直分库,依据传入的参数,例如参数中有省份参数,需要路由到对应省份的数据库,得到省份参数,然后再依据省份参数获取到对应的主库lookupkey
}
long endTime = System.currentTimeMillis();
log.info("response end");
log.info("time:" + (endTime - startTime));
}
@After("pointCut()")
public void after(JoinPoint joinPoint){ //后置通知,将线程局部变量清除掉
Method method = getMethod(joinPoint);
DBRouting dbRouting = method.getAnnotation(DBRouting.class);
String isRead = dbRouting.isRead();
boolean read = Boolean.parseBoolean(isRead);
if (read){
RequestThreadLocalHolder.removeDatabasIndex(); //remove
}
}
private Method getMethod(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
return method;
}
}
@DBRouting注解的使用
package com.xiayu.service.impl;
import com.xiayu.annotation.DBRouting;
import com.xiayu.entity.OrderEntity;
import com.xiayu.mapper.OrderMapper;
import com.xiayu.service.IOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements IOrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public boolean insertOrder(OrderEntity orderEntity) {
return orderMapper.insertOrder(orderEntity);
}
@DBRouting(isRead = "true") //设置为在从库中读取数据
@Override
public OrderEntity getOrder(Integer orderId) {
return orderMapper.getOrderById(orderId);
}
}
下图为数据库的设计,仅仅定义了三个库,没有实现从库同步主库数据
OrderEntity和OrderMapping
OrderEntity
--------------------------
package com.xiayu.entity;
import lombok.Data;
@Data
public class OrderEntity {
private Integer id;
private String orderName;
private String orderType;
}
OrderMapping
-----------------------------------------------
package com.xiayu.mapper;
import com.xiayu.entity.OrderEntity;
import org.apache.ibatis.annotations.*;
@Mapper
public interface OrderMapper {
String COLUMN_LIST = " id,order_name,order_type ";
@Insert("insert into t_order( " + COLUMN_LIST + ") values(#{id},#{orderName},#{orderType})")
boolean insertOrder(OrderEntity orderEntity);
@Results(id = "orderResultMap",value = {
@Result(property = "id",column = "id"),
@Result(property = "orderName",column = "order_name"),
@Result(property = "orderType",column = "order_type"),
})
@Select("select " + COLUMN_LIST + " from t_order where id = #{id}")
OrderEntity getOrderById(Integer id);
}