笔者最近在做一个互联网的“类SNS”应用,应用中用户数量巨大(约4000万)左右,因此,简单的使用传统单一数据库存储肯定是不行的。 参考了业内广泛使用的分库分表,以及使用DAL数据访问层等的做法,笔者决定使用一种最简单的数据源路由选择方式来解决问题。 严格的说,目前的实现不能算是一个解决方案,只能是一种思路的简易实现,笔者也仅花了2天时间来完成(其中1.5天是在看资料和Spring/ibatis的源码)。这里也只是为各位看官提供一个思路参考,顺便给自己留个笔记 2.系统的设计前提 我们的系统使用了16个数据库实例(目前分布在2台物理机器上,后期将根据系统负荷的增加,逐步移库到16台物理机器上)。16个库是根据用户的UserID进行简单的hash分配。这里值得一说的是,我们既然做了这样的横向切分设计,就已经考虑了系统需求的特性,
1.不会发生经常性的跨库访问。
2.主要的业务逻辑都是围绕UserID为核心的,在一个单库事务内即可完成。
在系统中,我们使用Spring和iBatis。Spring负责数据库的事务管理AOP,以及Bean间的IOC。选择iBatis的最大原因是对Sql的性能优化,以及后期如果有分表要求的时,可以很容易实现对sql表名替换。 3.设计思路 首先,要说明一下笔者的思路,其实很简单,即“在每次数据库操作前,确定当前要选择的数据库对象”而后就如同访问单库一样的访问当前选中的数据库即可。 其次,要在每次DB访问前选择数据库,需要明确几个问题,1.iBatis在什么时候从DataSource中取得具体的数据库Connection 的,2.对取得的Connection,iBatis是否进行缓存,因为在多库情况下Connection被缓存就意味着无法及时改变数据库链接选择。 3.由于我们使用了Spring来管理DB事务,因此必须搞清Spring对DB Connction的开关拦截过程是否会影响多DataSource的情况。 幸运的是,研究源码的结果发现,iBatis和Spring都是通过标准的DataSource接口来控制 Connection的,这就为我们省去了很多的麻烦,只需要实现一个能够支持多个数据库的DataSource,就能达到我们的目标。 4.代码与实现 多数据库的DataSource实现:MultiDataSource.class
Java代码
[java] view plaincopy
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;
/**
* 复合多数据源(Alpha)
* @author linliangyi2005@gmail.com
* Jul 15, 2010
*/
public class MultiDataSource implements DataSource {
static Logger logger = Logger.getLogger(MultiDataSource.class);
//当前线程对应的实际DataSource
private ThreadLocal<DataSource> currentDataSourceHolder = new ThreadLocal<DataSource>();
//使用Key-Value映射的DataSource
private Map<String , DataSource> mappedDataSources;
//使用横向切分的分布式DataSource
private ArrayList<DataSource> clusterDataSources;
public MultiDataSource(){
mappedDataSources = new HashMap<String , DataSource>(4);
clusterDataSources = new ArrayList<DataSource>(4);
}
/**
* 数据库连接池初始化
* 该方法通常在web 应用启动时调用
*/
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<DataSource> 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;
}
}
}
}
}
/**
* 获取当前线程绑定的DataSource
* @return
*/
public DataSource getCurrentDataSource() {
//如果路由策略存在,且更新过,则根据路由算法选择新的DataSource
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<DataSource> getClusterDataSources() {
return clusterDataSources;
}
public void setClusterDataSources(ArrayList<DataSource> clusterDataSources) {
this.clusterDataSources = clusterDataSources;
}
/**
* 使用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);
}
/**
* 使用取模算法,在群集数据源中做路由选择
* @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)
* 该接口方法since 1.6
* 不是所有的DataSource都实现有这个方法
* @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)
* 该接口方法since 1.6
* 不是所有的DataSource都实现有这个方法
* @see java.sql.Wrapper#unwrap(java.lang.Class)
*/
public <T> T unwrap(Class<T> iface) throws SQLException {
// if(getCurrentDataSource() != null){
// return getCurrentDataSource().unwrap(iface);
// }
return null;
}
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;
/**
* 复合多数据源(Alpha)
* @author linliangyi2005@gmail.com
* Jul 15, 2010
*/
public class MultiDataSource implements DataSource {
static Logger logger = Logger.getLogger(MultiDataSource.class);
//当前线程对应的实际DataSource
private ThreadLocal<DataSource> currentDataSourceHolder = new ThreadLocal<DataSource>();
//使用Key-Value映射的DataSource
private Map<String , DataSource> mappedDataSources;
//使用横向切分的分布式DataSource
private ArrayList<DataSource> clusterDataSources;
public MultiDataSource(){
mappedDataSources = new HashMap<String , DataSource>(4);
clusterDataSources = new ArrayList<DataSource>(4);
}
/**
* 数据库连接池初始化
* 该方法通常在web 应用启动时调用
*/
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<DataSource> 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;
}
}
}
}
}
/**
* 获取当前线程绑定的DataSource
* @return
*/
public DataSource getCurrentDataSource() {
//如果路由策略存在,且更新过,则根据路由算法选择新的DataSource
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<DataSource> getClusterDataSources() {
return clusterDataSources;
}
public void setClusterDataSources(ArrayList<DataSource> clusterDataSources) {
this.clusterDataSources = clusterDataSources;
}
/**
* 使用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);
}
/**
* 使用取模算法,在群集数据源中做路由选择
* @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)
&nbs
小编还为您整理了以下内容,可能对您也有帮助:
数据库水平分库和垂直分库有什么区别
常见的分库方式有水平性和垂直性。一般来说,就是按照用户属性(地市或者ID的hash)进行分库,或者按照业务功能块进行分库。
水平分库方式主要根据用户属性(如地市)拆分物理数据库。一种常见的方式是将全省划分为个大区。
垂直分库方式:根据业务维度和数据的访问量等,进行数据的分离,剥离为多个数据库。例如,将一些公用的配置信息存储到一个数据库中进行单独维护。
数据库水平分库和垂直分库有什么区别
常见的分库方式有水平性和垂直性。一般来说,就是按照用户属性(地市或者ID的hash)进行分库,或者按照业务功能块进行分库。
水平分库方式主要根据用户属性(如地市)拆分物理数据库。一种常见的方式是将全省划分为个大区。
垂直分库方式:根据业务维度和数据的访问量等,进行数据的分离,剥离为多个数据库。例如,将一些公用的配置信息存储到一个数据库中进行单独维护。
浅谈mysql数据库分库分表那些事-亿级数据存储方案
mysql分库分表一般有如下场景
其中1,2相对较容易实现,本文重点讲讲水平拆表和水平拆库,以及基于mybatis插件方式实现水平拆分方案落地。
在 《聊一聊扩展字段设计》 一文中有讲解到基于KV水平存储扩展字段方案,这就是非常典型的可以水平分表的场景。主表和kv表是一对N关系,随着主表数据量增长,KV表最大N倍线性增长。
这里我们以分KV表水平拆分为场景
对于kv扩展字段查询,只会根据id + key 或者 id 为条件的方式查询,所以这里我们可以按照id 分片即可
分512张表(实际场景具体分多少表还得根据字段增加的频次而定)
分表后表名为kv_000 ~ kv_511
id % 512 = 1 .... 分到 kv_001,
id % 512 = 2 .... 分到 kv_002
依次类推!
水平分表相对比较容易,后面会讲到基于mybatis插件实现方案
场景:以下我们基于博客文章表分库场景来分析
目标:
表结构如下(节选部分字段):
按照user_id sharding
假如分1024个库,按照user_id % 1024 hash
user_id % 1024 = 1 分到db_001库
user_id % 1024 = 2 分到db_002库
依次类推
目前是2个节点,假如后期达到瓶颈,我们可以增加至4个节点
最多可以增加只1024个节点,性能线性增长
对于水平分表/分库后,非shardingKey查询首先得考虑到
基于mybatis分库分表,一般常用的一种是基于spring AOP方式, 另外一种基于mybatis插件。其实两种方式思路差不多。
为了比较直观解决这个问题,我分别在Executor 和StatementHandler阶段2个
实现动态数据源获取接口
测试结果如下
由此可知,我们需要在Executor阶段 切换数据源
对于分库:
原始sql:
目标sql:
其中定义了三个注解
@useMaster 是否强制读主
@shardingBy 分片标识
@DB 定义逻辑表名 库名以及分片策略
1)编写entity
Insert
select
以上顺利实现mysql分库,同样的道理实现同时分库分表也很容易实现。
此插件具体实现方案已开源: https://github.com/bytearch/mybatis-sharding
目录如下:
mysql分库分表,首先得找到瓶颈在哪里(IO or CPU),是分库还是分表,分多少?不能为了分库分表而拆分。
原则上是尽量先垂直拆分 后 水平拆分。
以上基于mybatis插件分库分表是一种实现思路,还有很多不完善的地方,
例如:
数据库水平拆分是在单库中拆出多个表,每个表是表名+规则。还是分出多个库,每个库是库名+规则,表名不变
1 基本思想之什么是分库分表?
从字面上简单理解,就是把原本存储于一个库的数据分块存储到多个库上,把原本存储于一个表的数据分块存储到多个表上。
2 基本思想之为什么要分库分表?
数据库中的数据量不一定是可控的,在未进行分库分表的情况下,随着时间和业务的发展,库中的表会越来越多,表中的数据量也会越来越大,相应地,数据操作,增删改查的开销也会越来越大;另外,由于无法进行分布式式部署,而一台服务器的资源(CPU、磁盘、内存、IO等)是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。
3 分库分表的实施策略。
分库分表有垂直切分和水平切分两种。
3.1 何谓垂直切分,即将表按照功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立定义数据库workDB、商品数据库payDB、用户数据库userDB、日志数据库logDB等,分别用于存储项目数据定义表、商品定义表、用户数据表、日志数据表等。
3.2 何谓水平切分,当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,例如userID散列,进行划分,然后存储到多个结构相同的表,和不同的库上。例如,我们的userDB中的用户数据表中,每一个表的数据量都很大,就可以把userDB切分为结构相同的多个userDB:part0DB、part1DB等,再将userDB上的用户数据表userTable,切分为很多userTable:userTable0、userTable1等,然后将这些表按照一定的规则存储到多个userDB上。
3.3 应该使用哪一种方式来实施数据库分库分表,这要看数据库中数据量的瓶颈所在,并综合项目的业务类型进行考虑。
如果数据库是因为表太多而造成海量数据,并且项目的各项业务逻辑划分清晰、低耦合,那么规则简单明了、容易实施的垂直切分必是首选。
而如果数据库中的表并不多,但单表的数据量很大、或数据热度很高,这种情况之下就应该选择水平切分,水平切分比垂直切分要复杂一些,它将原本逻辑上属于一体的数据进行了物理分割,除了在分割时要对分割的粒度做好评估,考虑数据平均和负载平均,后期也将对项目人员及应用程序产生额外的数据管理负担。
在现实项目中,往往是这两种情况兼而有之,这就需要做出权衡,甚至既需要垂直切分,又需要水平切分。我们的游戏项目便综合使用了垂直与水平切分,我们首先对数据库进行垂直切分,然后,再针对一部分表,通常是用户数据表,进行水平切分。