一、分库分表思路
1、垂直分表
拆分列。基于列进行。
根据业务耦合性,将关联度低的不同表存储在不同的数据库,与微服务类似,按照业务独立划分,每个微服务使用单独的一个数据库。也可将字段较多
的表拆分新建一张扩展表,将不常用或字段较大的字段拆分出去到扩展表中。
在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,
一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加
载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。
优点:业务解耦清晰;高并发下,提升一定程度的IO,数据库连接数、单机硬件资源瓶颈。
缺点:部分表无法join,只能通过接口聚合,提升了开发复杂度;分布式事务处理复杂。
2、水平分表
拆分行。基于行进行。
分为库内分表,分库分表,根据表内数据内在的逻辑关系,将同一个表按条件分散到多个数据库或多个表中,每张表中包含一部分数据,从而使单张表的数
据量变小。
库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同
一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。
优点:不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力;应用端改造较小,不需要拆分业务模块。
缺点:跨分片的事务一致性难以保证;跨库的join关联性能较差;数据不易维护
二、Spring动态数据源实现分库
在分库分表的情况下,在执行SQL时选择连接不同的数据源(库)的思路:配置多个数据源加到动态数据源对象中,根据实际的情况动态切换到相应的数据源中。
如存放订单信息的有10个库,每个库中有100张表,根据用户的ID做分库分表。可以选择ID的某一位的值作为选择的第几个库,某两位的值作为对应的表名。
实现:在service中使用AOP拦截具有某个注解的方法,根据方法中的参数的用户信息,按照上面的规则指定要连接的数据源。
环境2.1.0.RELEASE,MySQL
1、所有的数据源信息配置:application.properties
#dataSource sharding
tvbss.jdbc.username=${config.tvbss.jdbc.username}
tvbss.jdbc.password=${config.tvbss.jdbc.password}
tvbss.jdbc.driver=${config.tvbss.jdbc.driver}
tvbss.jdbc.server00=${config.tvbss.jdbc.server00}
tvbss.jdbc.server01=${config.tvbss.jdbc.server01}
tvbss.jdbc.server02=${config.tvbss.jdbc.server02}
tvbss.jdbc.server03=${config.tvbss.jdbc.server03}
tvbss.jdbc.server04=${config.tvbss.jdbc.server04}
tvbss.jdbc.server05=${config.tvbss.jdbc.server05}
tvbss.jdbc.server06=${config.tvbss.jdbc.server06}
tvbss.jdbc.server07=${config.tvbss.jdbc.server07}
tvbss.jdbc.server08=${config.tvbss.jdbc.server08}
tvbss.jdbc.server09=${config.tvbss.jdbc.server09}
2、读取配置信息到属性类中
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map; /**
* 数据源属性配置
*
* @author yangyongjie
* @date 2019/9/27
* @desc
*/
@Component
@ConfigurationProperties(prefix = "tvbss.jdbc")
public class ShardingProperties {
private String username;
private String password;
private String driver;
private String server00;
private String server01;
private String server02;
private String server03;
private String server04;
private String server05;
private String server06;
private String server07;
private String server08;
private String server09; private Map<String, String> urlsMap = new HashMap<>(); @PostConstruct
public void initUrlList() {
urlsMap.put("server00", server00);
urlsMap.put("server01", server01);
urlsMap.put("server02", server02);
urlsMap.put("server03", server03);
urlsMap.put("server04", server04);
urlsMap.put("server05", server05);
urlsMap.put("server06", server06);
urlsMap.put("server07", server07);
urlsMap.put("server08", server08);
urlsMap.put("server09", server09);
} }
3、指定动态数据源的关键类
import com.xxx.common.bean.BaseShardedBean;
import com.xxx.config.datasource.prop.ShardingProperties;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils; import java.util.HashMap;
import java.util.Map; /**
* 动态数据源切换
* 方式:配置多个数据源加到动态数据源对象中,根据实际的情况动态的切换到相应的数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource { private static Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class); @Autowired
private ShardingProperties shardingProperties; /**
* 连接池分配的最大连接数
*/
private int maxActive = GenericObjectPool.DEFAULT_MAX_ACTIVE; /**
* 空闲时保留的最大连接数
*/
private int maxIdle = GenericObjectPool.DEFAULT_MAX_IDLE; /**
* 空闲时保留的最少连接数
*/
private int minIdle = GenericObjectPool.DEFAULT_MIN_IDLE; /**
* 当连接池没有空闲时等待的最大毫秒数,小于0表示无限期等待
*/
private long maxWait = GenericObjectPool.DEFAULT_MAX_WAIT;
/**
* 连接池的初始连接数
*/
private int initialSize = 0; /**
* 返回数据源dataSource的key值,实现数据源切换的关键方法
* 在determineTargetDataSource方法中根据key获取目标连接数据源
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
Object value = CurentDataSourceHolder.getCurentDataSource();
if (StringUtils.isEmpty(value)) {
value = BaseShardedBean.defaultKey();
}
return value;
} /**
* 启动时初始化所有的目标数据源
*/
@Override
public void afterPropertiesSet() {
super.setTargetDataSources(buildDataSource());
super.afterPropertiesSet();
} /**
* 根据配置文件的配置构建数据源
*
* @return
*/
private Map<Object, Object> buildDataSource() {
Map<Object, Object> dataSources = new HashMap<>(16);
Map<String, String> urls = shardingProperties.getUrlsMap();
if (MapUtils.isEmpty(urls)) {
LOGGER.error("获取配置的数据库urls为空");
return dataSources;
}
String driver = shardingProperties.getDriver();
String username = shardingProperties.getUsername();
String password = shardingProperties.getPassword();
urls.forEach((key, value) -> {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl(value);
dataSource.setMaxWait(maxWait);
dataSource.setMaxIdle(maxIdle);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSources.put(key, dataSource);
});
return dataSources;
} /**
* 数据源存放对象,用于动态切换用
*/
public static class CurentDataSourceHolder { private static ThreadLocal<Object> holder = new ThreadLocal<>(); /**
* 设置当前线程的数据源
*
* @param source
*/
public static void setCurentDataSource(Object source) {
holder.set(source);
} /**
* 获取当前线程的数据源
*
* @return
*/
public static Object getCurentDataSource() {
return holder.get();
} /**
* 清楚数据源
*/
public static void remove() {
holder.remove();
}
} public int getMaxActive() {
return maxActive;
} public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
} public int getMaxIdle() {
return maxIdle;
} public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
} public int getMinIdle() {
return minIdle;
} public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
} public long getMaxWait() {
return maxWait;
} public void setMaxWait(long maxWait) {
this.maxWait = maxWait;
} public int getInitialSize() {
return initialSize;
} public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
} }
afterPropertiesSet()方法初始化了所有的目标数据源
determineCurrentLookupKey()方法用来获取当前线程变量中指定的当前数据源的key,若没有,则使用默认数据源
AbstractRoutingDataSource :
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { @Nullable
private Map<Object, Object> targetDataSources; // 多个目标数据源 @Nullable
private Object defaultTargetDataSource; // 默认的目标数据源 private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); @Nullable
private Map<Object, DataSource> resolvedDataSources; @Nullable
private DataSource resolvedDefaultDataSource; /**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
} /**
* Specify the default target DataSource, if any.
* <p>The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>This DataSource will be used as target if none of the keyed
* {@link #setTargetDataSources targetDataSources} match the
* {@link #determineCurrentLookupKey()} current lookup key.
*/
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
} /**
* Specify whether to apply a lenient fallback to the default DataSource
* if no specific DataSource could be found for the current lookup key.
* <p>Default is "true", accepting lookup keys without a corresponding entry
* in the target DataSource map - simply falling back to the default DataSource
* in that case.
* <p>Switch this flag to "false" if you would prefer the fallback to only apply
* if the lookup key was {@code null}. Lookup keys without a DataSource
* entry will then lead to an IllegalStateException.
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
} /**
* Set the DataSourceLookup implementation to use for resolving data source
* name Strings in the {@link #setTargetDataSources targetDataSources} map.
* <p>Default is a {@link JndiDataSourceLookup}, allowing the JNDI names
* of application server DataSources to be specified directly.
*/
public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
} @Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
} /**
* Resolve the given lookup key object, as specified in the
* {@link #setTargetDataSources targetDataSources} map, into
* the actual lookup key to be used for matching with the
* {@link #determineCurrentLookupKey() current lookup key}.
* <p>The default implementation simply returns the given key as-is.
* @param lookupKey the lookup key object as specified by the user
* @return the lookup key as needed for matching
*/
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
} /**
* Resolve the specified data source object into a DataSource instance.
* <p>The default implementation handles DataSource instances and data source
* names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}).
* @param dataSource the data source value object as specified in the
* {@link #setTargetDataSources targetDataSources} map
* @return the resolved DataSource (never {@code null})
* @throws IllegalArgumentException in case of an unsupported value type
*/
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource) dataSource;
}
else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String) dataSource);
}
else {
throw new IllegalArgumentException(
"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
} @Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
} @Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
} @Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this)) {
return (T) this;
}
return determineTargetDataSource().unwrap(iface);
} @Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
} /**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
} /**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey(); }
4、Spring容器中数据源配置类
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager; /**
* 数据源配置文件
*
* @author yangyongjie
* @date 2019/9/25
* @desc
*/
@Configuration
public class ShardingDataSourceConfig { private static final Logger LOGGER = LoggerFactory.getLogger(ShardingDataSourceConfig.class); /**
* 数据源
*
* @return
*/
@Bean
public DynamicDataSource shardingDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setMaxActive(8);
dynamicDataSource.setMaxIdle(2);
dynamicDataSource.setMinIdle(1);
dynamicDataSource.setMaxWait(60000);
return dynamicDataSource;
} @Bean
public SqlSessionFactory shardingSqlSessionFactory() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 扫描相关mapper文件
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
SqlSessionFactory sqlSessionFactory = null;
try {
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mapper/**/*Mapper.xml"));
sqlSessionFactoryBean.setDataSource(shardingDataSource());
// sqlSessionFactoryBean.setConfigLocation();
sqlSessionFactory = sqlSessionFactoryBean.getObject();
} catch (Exception e) {
LOGGER.error("创建SqlSessionFactory:"+e.getMessage(),e);
}
return sqlSessionFactory;
} @Bean
public SqlSessionTemplate shardingSqlSessionTemplate() {
return new SqlSessionTemplate(shardingSqlSessionFactory());
} /**
* sharding数据源事务管理器
*
* @return
*/
@Bean
public DataSourceTransactionManager shardingTransactionManager() {
return new DataSourceTransactionManager(shardingDataSource());
} }
5、指定了默认数据源,提供 generateShardingKey 方法,用来根据当前bean分配库名,并保存在线程本地变量中,以及生成表名,为bean的tableIndex属性赋值
public abstract class BaseShardedBean { static String DEFAULT_SHARDING_SCHAME = "server";
static String DEFAULT_SHARDING_INDEX = "00"; public abstract Object dbKey(); public abstract String tableKey(); /**
* 子类必须指定分片的key
*/
public abstract Object genShardedKey(); /**
* 子类必须根据bean的映射选择表名
*/
public abstract String genTableName(); /**
* 子类必须制定业务数据库类型
*/
public abstract String genDbSchema(); /**
* 默认的库名,server00
*
* @return
*/
public static Object defaultKey() {
return DEFAULT_SHARDING_SCHAME + DEFAULT_SHARDING_INDEX;
} /**
* 生成库名和表名
*/
public void generateShardingKey() {
Object key = dbKey();
key = StringUtils.isEmpty(key) ? defaultKey() : key;
DynamicDataSource.CurentDataSourceHolder.setCurentDataSource(key);
tableKey();
} }
6、具体的库表映射实体,继承了BaseShardedBean ,用户实体需继承它,以及顶一个公共的分库分表规则,即所有的实体类对应的分库分表规则都相同,不同的是库名前缀和表名前缀
**
* 支持分片的Bean,只支持单表操作 required ${genShardedKey} 指定类型的分片key ${genTableName} 指定类型映射的表名
**/
public abstract class ShardedMapperBean extends BaseShardedBean { @JsonIgnore
protected String orderByClause;
@JsonIgnore
protected boolean distinct;
@JsonIgnore
protected String tableName;
@JsonIgnore
private String tableIndex;
@JsonIgnore
protected Object shardKey; public String getOrderByClause() {
return orderByClause;
} public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
} public boolean isDistinct() {
return distinct;
} public void setDistinct(boolean distinct) {
this.distinct = distinct;
} public String getTableName() {
return tableName;
} public void setTableName(String tableName) {
this.tableName = tableName;
} public String getTableIndex() {
return tableIndex;
} public void setTableIndex(String tableIndex) {
this.tableIndex = tableIndex;
} @Override
public Object dbKey() {
String key = genShardedKey() == null ? null : String.valueOf(genShardedKey());
if (StringUtils.isEmpty(key)) {
return null;
}
return genDbSchema() + "0" + Integer.parseInt(getLastSub4to2(key)) / 10; // 库名为key倒数三四位除10取整,即倒数第四位的值,一共分10个库
} @Override
public String tableKey() {
this.setTableName(genTableName());
String key = genShardedKey() == null ? null : String.valueOf(genShardedKey());
if (StringUtils.isEmpty(key)) {
setTableIndex(genTableName());
return null;
} else {
String tableIndex = genTableName() + "_" + getLastSub2(key); // 表名为倒数一二位的值,一共分100张表
setTableIndex(tableIndex);
return tableIndex;
}
} private static String getLastSub4to2(String key) {
if (StringUtils.isEmpty(key) || key.length() < 4) {
return "";
}
int keyLen = key.length();
return key.substring(keyLen - 4, keyLen - 2);
} private static String getLastSub2(String key) {
if (StringUtils.isEmpty(key) || key.length() < 2) {
return "";
}
int keyLen = key.length();
return key.substring(keyLen - 2);
}
}
如具体的实体继承ShardedMapperBean 之后重写的方法,自定义库名前缀和表名前缀,以及用什么作为分库分表的key
@Override
public Object genShardedKey() {
return id;
} @Override
public String genTableName() {
return "order";
} @Override
public String genDbSchema() {
return "databaseNamePrefix";
}
7、最后使用AOP拦截方法的执行,给DynamicDataSource中的静态内部类CurentDataSourceHolder中的线程本地变量指定实际连接的数据源即可。
import com.xxx.common.bean.BaseShardedBean;
import com.xxx.config.datasource.DynamicDataSource;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
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 org.springframework.transaction.annotation.Transactional; import java.lang.reflect.Method; /**
* 事务注解的拦截器,为事务管理器为shardingTransactionManager的方法指定数据源
*
* @author yangyongjie
* @date 2019/9/30
* @desc
*/
@Aspect
@Component
public class TransactionAspect { private static final String SHARD_TRANSACTION_MANAGER = "shardingTransactionManager"; @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
private void transactionbPointCut() {
// donothing
} /**
* 方法执行前的拦截,指定数据源
* 因此如果需要动态指定数据源,则方法参数bean需要继承ShardedMapperBean并重写genShardedKey()、genTableName()和genDbSchema()
*
* @param joinPoint
*/
@Before(value = "transactionbPointCut()")
public void beforeTransaction(JoinPoint joinPoint) {
// 获取方法上的注解
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method pointCutMethod = methodSignature.getMethod();
Transactional ann = pointCutMethod.getAnnotation(Transactional.class);
if (!StringUtils.equals(ann.value(), SHARD_TRANSACTION_MANAGER)) {
return;
}
// 获取方法参数
Object[] methodArgs = joinPoint.getArgs();
for (Object args : methodArgs) {
if (args instanceof BaseShardedBean) {
((BaseShardedBean) args).generateShardingKey();
break;
}
} } /**
* 方法执行后删除存放数据源key的线程本地变量
*/
@After(value = "transactionbPointCut()")
public void afterTransaction() {
DynamicDataSource.CurentDataSourceHolder.remove();
}
}
三、水平拆分数据分片规则
1、根据数值范围
按照时间区间或者ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。
2、根据数值取模
一般采用hash取模mod的切分方式,如将Order表根据userId字段切分到10个库中,余数为0的放到第一个库,余数为1的放到第二个库,依此类推。这样同一个用户的数据会分散到同一个库中。
end.