cobarclient分库的基本原理(如何实现分库?)
一:通过配置hashFuction如何找到相应要访问的数据库分片
1.通过笔者的第一篇文章(cobarclient分库的基本配置),但凡你看了那篇文章,你就大概知道它可能是通过下面这个配置来路由到相应的数据库分片的。
classpath:spring/namespace-rules.xml
classpath:spring/no-namespace-rules.xml
那这个SimpleRouterFactoryBean主要是干什么的?先在这提前告诉大家,不瞒你说,它就是用来实例化并初始化的一个类型为SimpleRouter对象的,但是它又是如何初始化的?顾名思义的SimpleRouter对象在路由的过程又是扮演着怎样的角色?来往下看源码,你就会很清楚了,
2.SimpleRouterFactoryBean实现了FactoryBean
public class SimpleRouterFactoryBean implements FactoryBean, InitializingBean {
//路由规则配置文件的Resource
private Resource configLocation;
//路由规则配置文件的Resource集合
private Resource[] configLocations;
//简单的路由器对象(上面id="router"的bean配置的真实对象)
private SimpleRouter router = null;
//对应上面配置文件的属性name="functions"
private Map functions;
//对应上面配置文件的属性name="shards"
private Set shards;
public SimpleRouterFactoryBean() {
}
/**
* 1.解析路由规则配置文件====》allRules,2.解析allRules集合===》简单的路由器对象router
*/
public void afterPropertiesSet() throws Exception {
//1.1路由规则对象集合
List allRules = new ArrayList();
//1.2解析路由规则配置文件
if (this.getConfigLocation() != null) {
List rules = this.loadRules(this.configLocation);
if (!CollectionUtils.isEmpty(rules)) {
allRules.addAll(rules);
}
}
//1.3解析路由规则配置文件
if (!ObjectUtils.isEmpty(this.getConfigLocations())) {
Resource[] arr$ = this.getConfigLocations();
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; ++i$) {
Resource res = arr$[i$];
List rules = this.loadRules(res);
if (!CollectionUtils.isEmpty(rules)) {
allRules.addAll(rules);
}
}
}
if (!CollectionUtils.isEmpty(allRules)) {
//2.1把所有的数据库分片set集合shards转换为Map形式保存,key=Shard的id,value=Shard对象
Map shardMap = this.convertShardMap(this.shards);
//2.2路由路线对象集合
Set routes = new LinkedHashSet();
Route route;
//2.3遍历路由规则对象集合allRules填充路由路线对象集合routes
for(Iterator i$ = allRules.iterator(); i$.hasNext(); routes.add(route)) {
InternalRule rule = (InternalRule)i$.next();
String sqlmap = rule.getSqlmap();
if (sqlmap == null || sqlmap.equals("")) {
//2.4 sqlmap的值,如果路由规则配置文件中没有sqlmap节点,则会取namespace节点的值
//如果俩个都有则会取sqlmap节点的值
sqlmap = rule.getNamespace();
}
//2.5按逗号切分路由规则对象中的shards字符串,(由数据库分片对象的唯一标志id组成)
String[] shardArr = rule.getShards().split(",");
//2.6用于填充路由路线对象route中的“shards”属性(数据库分片集合)
Set subShard = new LinkedHashSet();
String[] arr$ = shardArr;
int len$ = shardArr.length;
for(int i$ = 0; i$ < len$; ++i$) {
String shardId = arr$[i$];
//2.7 根据shardId找到对应的Shard对象
Shard tempShard = (Shard)shardMap.get(shardId);
if (tempShard == null) {
throw new NullPointerException("shard:" + shardId + " is not exists");
}
//2.8 保存Shard对象
subShard.add(tempShard);
}
//2.9 根据路由规则对象中的shardingExpression,填充路由路线对象route中的“expression”属性
if (null != rule.getShardingExpression()) {
if (null == this.functions) {
//可能存在表达式不为空,但是functions为空的情况,这时this.functions = new HashMap(),
//比如下面这种情况:
//表达式如果不使用自定义路由规则函数,而是直接使用 appType%2==0 这种的话就不用在配置文件
//中配置 了
this.functions = new HashMap();
}
//2.10 根据生成的sqlmap,rule.getShardingExpression,this.functions,subShard 实例化路由路线对象route
route = new Route(sqlmap, new MVELExpression(rule.getShardingExpression(), this.functions), subShard);
} else {
route = new Route(sqlmap, (Expression)null, subShard);
}
}
//2.11 利用有参数的构造函数实例化简单的路由器对象router,,它的实例化又做了什么事?
this.router = new SimpleRouter(routes);
} else {
//2.13 没有路由规则配置文件或者配置文件里面没有规则
this.router = new SimpleRouter((Set)null);
}
}
private Map convertShardMap(Set shards) {
Map shardMap = new HashMap();
Iterator i$ = shards.iterator();
while(i$.hasNext()) {
Shard shard = (Shard)i$.next();
shardMap.put(shard.getId(), shard);
}
return shardMap;
}
//利用XStream 解析路由规则配置文件====》List
private List loadRules(Resource configLocation) throws Exception {
XStream xstream = new XStream();
xstream.alias("rules", InternalRules.class);
xstream.alias("rule", InternalRule.class);
xstream.addImplicitCollection(InternalRules.class, "rules");
xstream.useAttributeFor(InternalRule.class, "merger");
InternalRules internalRules = (InternalRules)xstream.fromXML(configLocation.getInputStream());
return internalRules.getRules();
}
public Router getObject() throws Exception {
return this.router;
}
public Class getObjectType() {
return Router.class;
}
public boolean isSingleton() {
return true;
}
public Resource getConfigLocation() {
return this.configLocation;
}
public void setConfigLocation(Resource configLocation) {
this.configLocation = configLocation;
}
public Resource[] getConfigLocations() {
return this.configLocations;
}
public void setConfigLocations(Resource[] configLocations) {
this.configLocations = configLocations;
}
public Map getFunctions() {
return this.functions;
}
public void setFunctions(Map functions) {
this.functions = functions;
}
public Set getShards() {
return this.shards;
}
public void setShards(Set shards) {
this.shards = shards;
}
}
3.路由规则对象
public class InternalRule {
private String namespace;
private String sqlmap;
private String shardingExpression;
//其实就是多个数据库分片对象Shard的唯一id(可以为多个,之间用逗号隔开)
private String shards;
private String merger;
.........
}
4.数据库分片对象
public class Shard {
//数据库分片唯一标志id
private String id;
//数据库分片的数据源
private DataSource dataSource;
//数据库分片的描述(非必填)
private String description;
......
}
5.简单的路由器对象
public class SimpleRouter implements Router {
protected Logger logger = Logger.getLogger("SimpleRouter");
private Map routes = new HashMap();
private Set EMPTY_SHARD_SET = new HashSet();
public SimpleRouter(Set routeSet) {
if (routeSet != null && !routeSet.isEmpty()) {
//1.解析routeSet===》初始化routes
Iterator i$ = routeSet.iterator();
while(i$.hasNext()) {
Route route = (Route)i$.next();
if (!this.routes.containsKey(route.getSqlmap())) {
//2.key=route中的sqlmap属性,value=RouteGroup对象,如果有两个sqlmap相同,且都不含表达式的路由规则,有一个会被覆盖
this.routes.put(route.getSqlmap(), new RouteGroup());
}
if (route.getExpression() == null) {
//3.不带expression的Route对象,决定其路由到数据库分片的属性只有sqlmap
((RouteGroup)this.routes.get(route.getSqlmap())).setFallbackRoute(route);
} else {
//4.带expression的Route对象,决定其路由到数据库分片的属性有sqlmap和expression
((RouteGroup)this.routes.get(route.getSqlmap())).getSpecificRoutes().add(route);
}
}
}
}
**为什么会有3点和4点
GeneralMallUser
hash.generalApply(appType) == 1
master12
GeneralMallUser
hash.generalApply(appType) == 2
master13
GeneralMallUser
hash.generalApply(appType) == 3
master15
**
/**
* 找到数据库分片的主要方法
* action:=AllPlatformUser.insertAllPlatformUser
**/
public Set route(String action, Object argument) {
Route resultRoute = this.findRoute(action, argument);
if (resultRoute == null && action != null) {
String namespace = action.substring(0, action.lastIndexOf("."));
resultRoute = this.findRoute(namespace, argument);
}
return resultRoute == null ? this.EMPTY_SHARD_SET : resultRoute.getShards();
}
/**
* 根据action(sqlmap的id)和参数argument 找到指定的Route对象
**/
protected Route findRoute(String action, Object argument) {
if (this.routes.containsKey(action)) {
RouteGroup routeGroup = (RouteGroup)this.routes.get(action);
//1.先遍历routeGroup中的specificRoutes属性(expression表达式不为null的路由路线对象的集合)从这点可以看出带表达式的规则会优先于不带表达式的规则
Iterator i$ = routeGroup.getSpecificRoutes().iterator();
while(i$.hasNext()) {
Route route = (Route)i$.next();
if (route.apply(action, argument)) {
return route;
}
}
//2.再routeGroup中的fallbackRoute属性
if (routeGroup.getFallbackRoute() != null && routeGroup.getFallbackRoute().apply(action, argument)) {
return routeGroup.getFallbackRoute();
}
}
return null;
}
}
public interface Router {
Set route(String action, Object argument);
}
6.路由路线对象(暂且这么叫吧)
public class Route {
private String sqlmap;
private Expression expression;
private Set shards;
public Route(String sqlmap, Expression expression, Set shards) {
this.sqlmap = sqlmap;
this.expression = expression;
this.shards = shards;
}
/**
* 用于判断该action(sqlmap)所代表的sql语句是否走这条Route(这个方法很重要)
*/
public boolean apply(String action, Object argument) {
if (this.sqlmap == null) {
return false;
} else if (!this.sqlmap.equals(action)) {
return false;
} else if (this.expression == null) {
return true;
} else {
return this.expression != null && argument != null && this.expression.apply(argument);
}
}
}
7.路由路线组对象
public class RouteGroup {
//expression表达式为null的路由路线对象
private Route fallbackRoute = null;
//expression表达式不为null的路由路线对象的集合
private Set specificRoutes = new HashSet();
public RouteGroup() {
}
public RouteGroup(Route fallbackRoute, Set specificRoutes) {
this.fallbackRoute = fallbackRoute;
if (specificRoutes != null && !specificRoutes.isEmpty()) {
this.specificRoutes.addAll(specificRoutes);
}
}
public Route getFallbackRoute() {
return this.fallbackRoute;
}
public void setFallbackRoute(Route fallbackRoute) {
this.fallbackRoute = fallbackRoute;
}
public Set getSpecificRoutes() {
return this.specificRoutes;
}
public void setSpecificRoutes(Set specificRoutes) {
this.specificRoutes = specificRoutes;
}
}
8.通过上面的源码分析,我们可以很清楚的知道SimpleRouterFactoryBean的作用:1.解析路由规则配置文件====》allRules,2.解析allRules集合===》简单的路由器对象router。
而路由器对象SimpleRouter中的route方法,route方法通过俩个重要的参数action和argument可以准确找到相应数据库分片,此方法在数据访问中寻找相应数据库分片的过程中起着重要的作用。
二:cobarclient的MysdalSqlMapClientTemplate如何使用路由器对象SimpleRouter来实现分库?
1.配置文件中的如何配置?
classpath:ibatis/sqlmap-config.xml
classpath*:/ibatis/sqlmap/*.xml
classpath*:/ibatis/sqlmap/*/*-sqlmap.xml
2.从配置中可以看到sqlMapClientTemplate使用的是cobarclient重新对ibatis的SqlMapClientTemplate封装过的MysdalSqlMapClientTemplate。下面就来看看源码,它对原生的SqlMapClientTemplate做了哪些封装。
public class MysdalSqlMapClientTemplate extends SqlMapClientTemplate implements DisposableBean {
//保存当前线程中所有SqlMapClientTemplate实例,一个数据库分片对应一个SqlMapClientTemplate实例
protected Map CURRENT_THREAD_SQLMAP_CLIENT_TEMPLATES = new HashMap();
private Set shards;
//简单的路由器对象SimpleRouter
private Router router;
private long timeout = 5000L;
private boolean useDefaultExecutor = false;
private ExecutorService executor = Executors.newFixedThreadPool(20);
public MysdalSqlMapClientTemplate() {
}
//填充CURRENT_THREAD_SQLMAP_CLIENT_TEMPLATES。
public void afterPropertiesSet() {
super.afterPropertiesSet();
if (this.shards != null && !this.shards.isEmpty()) {
if (this.router == null) {
throw new IllegalArgumentException("'router' argument is required");
} else {
if (this.executor == null) {
this.useDefaultExecutor = true;
this.executor = Executors.newCachedThreadPool(new ThreadFactory() {
public Thread newThread(Runnable runnable) {
return new Thread(runnable, "MysdalSqlMapClientTemplate executor thread");
}
});
}
Iterator i$ = this.shards.iterator();
while(i$.hasNext()) {
Shard shard = (Shard)i$.next();
this.CURRENT_THREAD_SQLMAP_CLIENT_TEMPLATES.put(shard.getId(), new SqlMapClientTemplate(shard.getDataSource(), this.getSqlMapClient()));
}
}
} else {
throw new IllegalArgumentException("'shards' argument is required.");
}
}
public void destroy() throws Exception {
if (this.useDefaultExecutor) {
this.executor.shutdown();
}
}
public boolean isHasShard(String statementName, Object parameterObject) {
Set shards = this.getRouter().route(statementName, parameterObject);
return shards.size() == 1;
}
public List queryForList(final String statementName, final Object parameterObject) throws DataAccessException {
Set shards = this.getRouter().route(statementName, parameterObject);
if (shards.isEmpty()) {
return super.queryForList(statementName, parameterObject);
} else {
return shards.size() == 1 ? ((SqlMapClientTemplate)this.CURRENT_THREAD_SQLMAP_CLIENT_TEMPLATES.get(((Shard)shards.iterator().next()).getId())).queryForList(statementName, parameterObject) : this.queryForListBase(shards, new SqlMapClientCallback() {
public List doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
return executor.queryForList(statementName, parameterObject);
}
});
}
}
.............
}
三:MysdalBaseDao对原生ibatis的SqlMapClientDaoSupport做了怎样的封装?又是如何使用到它自己封装的MysdalSqlMapClientTemplate来实现分库?
先看如下类的关系图:
在看具体源码如下:
1.
public class MysdalBaseDao extends MysdalCobarSqlMapClientDaoSupport {
@Resource(name = "sqlMapClient")
private SqlMapClient sqlMapClient;
private int defaultBatchSize = 1000;
public MysdalBaseDao() {
}
@PostConstruct
public void initSqlMapClient() {
super.setSqlMapClient(this.sqlMapClient);
}
.......
}
2.
public class MysdalCobarSqlMapClientDaoSupport extends BaseSqlMapClientDaoSupport {
public MysdalCobarSqlMapClientDaoSupport() {
}
public int batchInsert(String statementName, List> entities) throws DataAccessException {
if (CollectionUtils.isEmpty(entities)) {
return 0;
} else if (this.isPartitionBehaviorEnabled()) {
MysdalSqlMapClientTemplate template = (MysdalSqlMapClientTemplate)this.getSqlMapClientTemplate();
return template.isHasShard(statementName, entities.get(0)) ? template.batchInsert(statementName, entities) : super.batchInsert(statementName, entities);
} else {
return super.batchInsert(statementName, entities);
}
}
protected boolean isPartitionBehaviorEnabled() {
return this.getSqlMapClientTemplate() instanceof MysdalSqlMapClientTemplate;
}
....
}
3.
public class BaseSqlMapClientDaoSupport extends SqlMapClientDaoSupport {
@Autowired
private SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate();
public BaseSqlMapClientDaoSupport() {
}
@PostConstruct
public void init() {
this.setSqlMapClientTemplate(this.sqlMapClientTemplate);
}
}
4.
public abstract class SqlMapClientDaoSupport extends DaoSupport {
private SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate();
private boolean externalTemplate = false;
public SqlMapClientDaoSupport() {
}
public final void setDataSource(DataSource dataSource) {
if (!this.externalTemplate) {
this.sqlMapClientTemplate.setDataSource(dataSource);
}
}
public final DataSource getDataSource() {
return this.sqlMapClientTemplate.getDataSource();
}
public final void setSqlMapClient(SqlMapClient sqlMapClient) {
if (!this.externalTemplate) {
this.sqlMapClientTemplate.setSqlMapClient(sqlMapClient);
}
}
public final SqlMapClient getSqlMapClient() {
return this.sqlMapClientTemplate.getSqlMapClient();
}
public final void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) {
Assert.notNull(sqlMapClientTemplate, "SqlMapClientTemplate must not be null");
this.sqlMapClientTemplate = sqlMapClientTemplate;
this.externalTemplate = true;
}
public final SqlMapClientTemplate getSqlMapClientTemplate() {
return this.sqlMapClientTemplate;
}
protected final void checkDaoConfig() {
if (!this.externalTemplate) {
this.sqlMapClientTemplate.afterPropertiesSet();
}
}
}
四:总结
1.cobarclient支持不同分库相同表的路由,不支持同库的分表路由
从源码的角度分析:只是对数据库做了分片配置,简单来说,它对整个系统的数据源都放一起了来集中管理,一个数据源对应一个SqlMapClientTemplate,然后通过相应参数路由到相应数据源上,再拿到相应的SqlMapClientTemplate执行sql返回结果。这个切分的细粒度对整个系统数据源的切分,而对于更细粒度的同数据库下的表的切分是没有做处理的,我们这边是通过代码具体指定一个表名来访问到相应的表。
2.不配置路由规则文件,默认访问的数据源是dataSource。
MysdalSqlMapClientTemplate本身就是SqlMapClientTemplate,它继承了父类dataSource属性,所以它本身就有自己的数据源dataSource
这个dataSource就是默认的MysdalSqlMapClientTemplate数据源