数据库的分库分表访问,原理上很简单。对于一条sql来说,就是确定表名称,对于操作来说,就是要确定数据源。因此,我要对数据源与表名进行分析。
在spring中对于单数据源的配置,非常简单,相信大家也都会配置。那么对于多数据源来说有两种方式:
1,静态数据源选择方式,只需要在dao中注入对应数据源。这种也没什么好说的,但是如果存在事物的话,需要注意,一旦在 service的方法中操作不同数据源的dao应该如何处理。
2,动态数据源选择方式。动态的方式一般会在程序中通过一定的条件来选择数据源。所以对于在spring中配置数据源就有了小小改变。目前我使用的方式是实现自己的一个数据源,这个数据源的特点就是有一个map,保存了真正需要配置的数据员,然后给每个数据源分配一个key
示例配置
- <bean id="dataSource" class="halo.dao.sql.HaloDataSourceWrapper">
- <property name="dataSourceMap">
- <map>
- <entry key="mysql_test0">
- <bean class="com.mchange.v2.c3p0.ComboPooledDataSource">
- ....
- </bean>
- </entry>
- <entry key="mysql_test1">
- <bean class="com.mchange.v2.c3p0.ComboPooledDataSource">
- .....
- </bean>
- </entry>
- </map>
- </property>
- </bean>
通过这种配置方式,程序就有机会根据条件来选择相应的数据源。那么,在程序的什么位置进行数据源选择才合适呢。个人认为这属于数据访问层的职责,因此,决定数据源的选择问题交给dao来处理。对dao注入自定的数据源。然后在所有的dao的方法中,肯定会多一个参数,这个参数就为了选择数据源所使用。
示例代码
- public int count(Object key, String where, Object[] params)
现在选择数据源的条件有了,下面要做的就是如何根据条件选择数据源,这时,我们可以专门写一个类,来做数据源的选择以及真实表名的确定。
示例代码
-
-
-
-
-
- public class PartitionTableInfo {
-
-
-
-
- private String dsKey;
-
-
-
-
- private String tableName;
-
-
-
-
- private String aliasName;
-
- public PartitionTableInfo() {
- }
-
- public PartitionTableInfo(String dsKey, String tableName) {
- this.dsKey = dsKey;
- this.tableName = tableName;
- }
-
- public String getDsKey() {
- return dsKey;
- }
-
-
-
-
-
-
- public void setDsKey(String dsKey) {
- this.dsKey = dsKey;
- }
-
- public String getTableName() {
- return tableName;
- }
-
-
-
-
-
-
- public void setTableName(String tableName) {
- this.tableName = tableName;
- }
-
-
-
-
-
-
- public void setAliasName(String aliasName) {
- this.aliasName = aliasName;
- }
-
- public String getAliasName() {
- return aliasName;
- }
- }
数据源表名分析的抽象类
示例代码
-
-
-
-
-
- public abstract class DbPartitionHelper {
-
-
-
-
-
-
-
-
-
-
- public abstract PartitionTableInfo parse(String tableLogicName,
- Map<String, Object> ctxMap);
- }
数据源的选择实现类
示例代码
- public class TestUserDbPartitionHelper extends DbPartitionHelper {
-
- @Override
- public PartitionTableInfo parse(String tableLogicName,
- Map<String, Object> ctxMap) {
-
- long userid = (Long) ctxMap.get("userid");
-
- String lastChar = this.get01(userid);
- PartitionTableInfo partitionTableInfo = new PartitionTableInfo();
-
- partitionTableInfo.setAliasName(tableLogicName);
-
- partitionTableInfo.setTableName("testuser" + lastChar);
-
- partitionTableInfo.setDsKey("mysql_test" + lastChar);
- return partitionTableInfo;
- }
- }
这样dao的方法就获得了真正的数据源key和真实的表名称
调用举例代码
-
- TestUserDbPartitionHelper dbPartitionHelper = new TestUserDbPartitionHelper();
- Map<String, Object> ctxMap = new HashMap<String, Object>();
- ctxMap.put("userid", 123);
- PartitionTableInfo info = dbPartitionHelper.parse("testuser", ctxMap);
- String dsKey = info.getDsKey();
- String realTableName = info.getTableName();
通过这种调用,我么获得了数据源的key以及表的真实名称
这样dao里面的方法就可以拼装sql了。
那么数据源的key如何使用呢?
由于我们对dao都注入了自定义的datasource,这个key我们需要在datasource中通过map.get(String name)获得真实的datasource,一个简单的方式就是我们吧数据源key放到threadlocal中,让datasource在获得connection的方法中调用
保存数据源key的代码示例
-
-
-
-
-
- public class DataSourceStatus {
-
- private static final ThreadLocal<String> currentDsKey = new ThreadLocal<String>();
-
- public static void setCurrentDsKey(String dsKey) {
- currentDsKey.set(dsKey);
- }
-
- public static String getCurrentDsKey() {
- return currentDsKey.get();
- }
- }
自定义的datasource代码示例
-
-
-
-
-
- public class MyDataSourceWrapper implements DataSource {
-
- private Map<String, DataSource> dataSourceMap;
-
- private PrintWriter logWriter;
-
- private int loginTimeout = 3;
-
- public DataSource getCurrentDataSource() {
- DataSource ds = this.dataSourceMap.get(DataSourceStatus
- .getCurrentDsKey());
- if (ds == null) {
- throw new RuntimeException("no datasource [ "
- + DataSourceStatus.getCurrentDsKey() + " ]");
- }
- return ds;
- }
-
- public void setDataSourceMap(Map<String, DataSource> dataSourceMap) {
- this.dataSourceMap = dataSourceMap;
- }
-
- @Override
- public Connection getConnection() throws SQLException {
- return this.getCurrentDataSource().getConnection();
- }
-
- @Override
- public Connection getConnection(String username, String password)
- throws SQLException {
- throw new SQLException("only support getConnection()");
- }
-
- @Override
- public PrintWriter getLogWriter() throws SQLException {
- return this.logWriter;
- }
-
- @Override
- public int getLoginTimeout() throws SQLException {
- return this.loginTimeout;
- }
-
- @Override
- public void setLogWriter(PrintWriter out) throws SQLException {
- this.logWriter = out;
- }
-
- @Override
- public void setLoginTimeout(int seconds) throws SQLException {
- this.loginTimeout = seconds;
- }
-
- @Override
- public boolean isWrapperFor(Class<?> iface) throws SQLException {
- return this.getCurrentDataSource().isWrapperFor(iface);
- }
-
- @Override
- public <T> T unwrap(Class<T> iface) throws SQLException {
- return this.getCurrentDataSource().unwrap(iface);
- }
- }
到目前为止,我们就可以使用spring jdbcTemplate来进行分库分表的sql操作了。
在上述的示例代码中很多的部分可以,进行数据库路由的分析写了不少的代码,其实这些代码可以通过配置的方式来解决,不需要通过手写代码来解决。我的一个思路就是对于与数据表对应的一个实体class配置一个路由规则的标示和表的别名,然后写一段程序来对这个配置进行解析,来实现上面分库分表选择的功能
上述方法解决了分库分表功能,但是没有解决单库的事务问题。由于数据库的选择是在dao层决定,那么对于一个service的方法就无法获得数据库,并开启事务。为了解决这种情况,我们可以对connection进行改造,然后再对自定义的datasource再次改造。我们在使用spring数据库事务的使用,大多情况都是在service的方法上加上事务,这样对于这个方法里面的dao调用都具有了事务操作。这样就必须在service方法运行之前就决定数据源是什么。
其实spring的事务方法只需要一个数据源,并获得connection然后进行connection.setAutoCommit等操作。spring并不关心你的connection是什么,是哪个数据源的。所以我们就可以写一个与数据源没有直接关系的自定义connection,让他来沉承担选择数据源之前对connection的所有操作。
自定义数据源示例代码
接口
-
-
-
-
-
- public interface ConnectionProxy extends Connection {
-
-
-
-
-
-
-
- Connection getCurrentConnection();
- }
实现
- public class ConnectionProxyImpl implements ConnectionProxy {
-
-
-
-
- private final Map<String, Connection> conMap = new HashMap<String, Connection>();
-
- private boolean autoCommit;
-
- private int transactionIsolation;
-
- private int holdability;
-
- private boolean readOnly;
-
-
-
-
- private HkDataSourceWrapper cloudDataSourceWrapper;
-
- public ConnectionProxyImpl(HkDataSourceWrapper cloudDataSourceWrapper)
- throws SQLException {
- this.cloudDataSourceWrapper = cloudDataSourceWrapper;
- this.setAutoCommit(true);
- }
-
- @Override
- public void clearWarnings() throws SQLException {
- this.getCurrentConnection().clearWarnings();
- }
-
- @Override
- public void close() throws SQLException {
- Collection<Connection> c = this.conMap.values();
- for (Connection con : c) {
- con.close();
- }
- DataSourceStatus.setCurrentDsKey(null);
- }
-
- @Override
- public void commit() throws SQLException {
- Collection<Connection> c = this.conMap.values();
- for (Connection con : c) {
- con.commit();
- }
- }
-
- @Override
- public Statement createStatement() throws SQLException {
- return this.getCurrentConnection().createStatement();
- }
-
- @Override
- public Connection getCurrentConnection() {
- String name = DataSourceStatus.getCurrentDsKey();
- Connection con = this.conMap.get(name);
- if (con == null) {
- try {
- con = this.cloudDataSourceWrapper.getCurrentDataSource()
- .getConnection();
- this.initCurrentConnection(con);
- this.conMap.put(name, con);
- }
- catch (SQLException e) {
- throw new RuntimeException(e);
- }
- }
- return con;
- }
-
- private void initCurrentConnection(Connection con) throws SQLException {
- con.setAutoCommit(this.getAutoCommit());
- if (this.getTransactionIsolation() != 0) {
- con.setTransactionIsolation(this.getTransactionIsolation());
- }
- con.setHoldability(this.getHoldability());
- con.setReadOnly(this.isReadOnly());
- }
-
- @Override
- public Statement createStatement(int resultSetType, int resultSetConcurrency)
- throws SQLException {
- return this.getCurrentConnection().createStatement(resultSetType,
- resultSetConcurrency);
- }
-
- @Override
- public Statement createStatement(int resultSetType,
- int resultSetConcurrency, int resultSetHoldability)
- throws SQLException {
- return this.getCurrentConnection().createStatement(resultSetType,
- resultSetConcurrency, resultSetHoldability);
- }
-
- @Override
- public boolean getAutoCommit() throws SQLException {
- return this.autoCommit;
- }
-
- @Override
- public int getHoldability() throws SQLException {
- return this.holdability;
- }
-
- @Override
- public DatabaseMetaData getMetaData() throws SQLException {
- return this.getCurrentConnection().getMetaData();
- }
-
- @Override
- public int getTransactionIsolation() throws SQLException {
- return this.transactionIsolation;
- }
-
- @Override
- public Map<String, Class<?>> getTypeMap() throws SQLException {
- return this.getCurrentConnection().getTypeMap();
- }
-
- @Override
- public SQLWarning getWarnings() throws SQLException {
- return this.getCurrentConnection().getWarnings();
- }
-
- @Override
- public boolean isClosed() throws SQLException {
- return this.getCurrentConnection().isClosed();
- }
-
- @Override
- public boolean isReadOnly() throws SQLException {
- return this.readOnly;
- }
-
- @Override
- public String nativeSQL(String sql) throws SQLException {
- return this.getCurrentConnection().nativeSQL(sql);
- }
-
- @Override
- public CallableStatement prepareCall(String sql) throws SQLException {
- return this.getCurrentConnection().prepareCall(sql);
- }
-
- @Override
- public CallableStatement prepareCall(String sql, int resultSetType,
- int resultSetConcurrency) throws SQLException {
- return this.getCurrentConnection().prepareCall(sql, resultSetType,
- resultSetConcurrency);
- }
-
- @Override
- public CallableStatement prepareCall(String sql, int resultSetType,
- int resultSetConcurrency, int resultSetHoldability)
- throws SQLException {
- return this.getCurrentConnection().prepareCall(sql, resultSetType,
- resultSetConcurrency, resultSetHoldability);
- }
-
- @Override
- public PreparedStatement prepareStatement(String sql) throws SQLException {
- return this.getCurrentConnection().prepareStatement(sql);
- }
-
- @Override
- public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
- throws SQLException {
- return this.getCurrentConnection().prepareStatement(sql,
- autoGeneratedKeys);
- }
-
- @Override
- public PreparedStatement prepareStatement(String sql, int[] columnIndexes)
- throws SQLException {
- return this.getCurrentConnection().prepareStatement(sql, columnIndexes);
- }
-
- @Override
- public PreparedStatement prepareStatement(String sql, String[] columnNames)
- throws SQLException {
- return this.getCurrentConnection().prepareStatement(sql, columnNames);
- }
-
- @Override
- public PreparedStatement prepareStatement(String sql, int resultSetType,
- int resultSetConcurrency) throws SQLException {
- return this.getCurrentConnection().prepareStatement(sql, resultSetType,
- resultSetConcurrency);
- }
-
- @Override
- public PreparedStatement prepareStatement(String sql, int resultSetType,
- int resultSetConcurrency, int resultSetHoldability)
- throws SQLException {
- return this.getCurrentConnection().prepareStatement(sql, resultSetType,
- resultSetConcurrency, resultSetHoldability);
- }
-
- @Override
- public void rollback() throws SQLException {
- Collection<Connection> c = conMap.values();
- for (Connection con : c) {
- con.rollback();
- }
- }
-
- @Override
- public void setAutoCommit(boolean autoCommit) throws SQLException {
- this.autoCommit = autoCommit;
- Collection<Connection> c = conMap.values();
- for (Connection con : c) {
- con.setAutoCommit(autoCommit);
- }
- }
-
- @Override
- public void setCatalog(String catalog) throws SQLException {
- this.getCurrentConnection().setCatalog(catalog);
- }
-
- @Override
- public String getCatalog() throws SQLException {
- return this.getCurrentConnection().getCatalog();
- }
-
- @Override
- public void setHoldability(int holdability) throws SQLException {
- this.holdability = holdability;
- }
-
- @Override
- public void setReadOnly(boolean readOnly) throws SQLException {
- this.readOnly = readOnly;
- }
-
- @Override
- public void setTransactionIsolation(int level) throws SQLException {
- this.transactionIsolation = level;
- Collection<Connection> c = conMap.values();
- for (Connection con : c) {
- con.setTransactionIsolation(level);
- }
- }
-
- @Override
- public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
- this.getCurrentConnection().setTypeMap(map);
- }
-
- @Override
- public void releaseSavepoint(Savepoint savepoint) throws SQLException {
- throw new SQLException("do not support savepoint");
- }
-
- @Override
- public void rollback(Savepoint savepoint) throws SQLException {
- throw new SQLException("do not support savepoint");
- }
-
- @Override
- public Savepoint setSavepoint() throws SQLException {
- throw new SQLException("do not support savepoint");
- }
-
- @Override
- public Savepoint setSavepoint(String name) throws SQLException {
- throw new SQLException("do not support savepoint");
- }
-
- @Override
- public Array createArrayOf(String typeName, Object[] elements)
- throws SQLException {
- return this.getCurrentConnection().createArrayOf(typeName, elements);
- }
-
- @Override
- public Blob createBlob() throws SQLException {
- return this.getCurrentConnection().createBlob();
- }
-
- @Override
- public Clob createClob() throws SQLException {
- return this.getCurrentConnection().createClob();
- }
-
- @Override
- public NClob createNClob() throws SQLException {
- return this.getCurrentConnection().createNClob();
- }
-
- @Override
- public SQLXML createSQLXML() throws SQLException {
- return this.getCurrentConnection().createSQLXML();
- }
-
- @Override
- public Struct createStruct(String typeName, Object[] attributes)
- throws SQLException {
- return this.getCurrentConnection().createStruct(typeName, attributes);
- }
-
- @Override
- public Properties getClientInfo() throws SQLException {
- return this.getCurrentConnection().getClientInfo();
- }
-
- @Override
- public String getClientInfo(String name) throws SQLException {
- Connection con = this.getCurrentConnection();
- return con.getClientInfo(name);
- }
-
- @Override
- public boolean isValid(int timeout) throws SQLException {
- return this.getCurrentConnection().isValid(timeout);
- }
-
- @Override
- public void setClientInfo(Properties properties)
- throws SQLClientInfoException {
- this.getCurrentConnection().setClientInfo(properties);
- }
-
- @Override
- public void setClientInfo(String name, String value)
- throws SQLClientInfoException {
- this.getCurrentConnection().setClientInfo(name, value);
- }
-
- @Override
- public boolean isWrapperFor(Class<?> iface) throws SQLException {
- return this.getCurrentConnection().isWrapperFor(iface);
- }
-
- @Override
- public <T> T unwrap(Class<T> iface) throws SQLException {
- return this.getCurrentConnection().unwrap(iface);
- }
- }
然后我们对自定义的datasource再次进行改造,新的datasource代码如下
-
-
-
-
-
- public class HkDataSourceWrapper implements DataSource, InitializingBean {
-
- public static final String DEFAULT_DBKEY = "defaultdbkey";
-
- private Map<String, DataSource> dataSourceMap;
-
- private PrintWriter logWriter;
-
- private int loginTimeout = 3;
-
- private boolean debugConnection;
-
- public void setDebugConnection(boolean debugConnection) {
- this.debugConnection = debugConnection;
- }
-
- public boolean isDebugConnection() {
- return debugConnection;
- }
-
- public DataSource getCurrentDataSource() {
- DataSource ds = this.dataSourceMap.get(DataSourceStatus
- .getCurrentDsKey());
- if (ds == null) {
- throw new RuntimeException("no datasource");
- }
- return ds;
- }
-
- public void setDataSourceMap(Map<String, DataSource> dataSourceMap) {
- this.dataSourceMap = dataSourceMap;
- }
-
- @Override
- public Connection getConnection() throws SQLException {
- return new ConnectionProxyImpl(this);
- }
-
- @Override
- public Connection getConnection(String username, String password)
- throws SQLException {
- throw new SQLException("only support getConnection()");
- }
-
- @Override
- public PrintWriter getLogWriter() throws SQLException {
- return this.logWriter;
- }
-
- @Override
- public int getLoginTimeout() throws SQLException {
- return this.loginTimeout;
- }
-
- @Override
- public void setLogWriter(PrintWriter out) throws SQLException {
- this.logWriter = out;
- }
-
- @Override
- public void setLoginTimeout(int seconds) throws SQLException {
- this.loginTimeout = seconds;
- }
-
- @Override
- public boolean isWrapperFor(Class<?> iface) throws SQLException {
- return this.getCurrentDataSource().isWrapperFor(iface);
- }
-
- @Override
- public <T> T unwrap(Class<T> iface) throws SQLException {
- return this.getCurrentDataSource().unwrap(iface);
- }
-
- @Override
- public void afterPropertiesSet() throws Exception {
- if (this.dataSourceMap.size() == 1) {
- this.dataSourceMap.put(DEFAULT_DBKEY, this.dataSourceMap.values()
- .iterator().next());
- }
- }
- }
其中最主要的部分就是
- @Override
- public Connection getConnection() throws SQLException {
- return new ConnectionProxyImpl(this);
- }
就是这部分返回了一个虚假的connection让spring进行事务开启等操作,那么既然spring进行了事务等设置,如何反应到真实的connection上呢,最住院哦的代码部分就是
- private void initCurrentConnection(Connection con) throws SQLException {
- con.setAutoCommit(this.getAutoCommit());
- if (this.getTransactionIsolation() != 0) {
- con.setTransactionIsolation(this.getTransactionIsolation());
- }
- con.setHoldability(this.getHoldability());
- con.setReadOnly(this.isReadOnly());
- }
这部分代码会在获得真正的connection的时候进行对connection的初始化。这样就解决了事务问题。
分享到:
相关推荐
ShardingJDBC作为一个轻量级的Java库,能够在不修改现有数据库架构和业务代码的情况下,仅通过配置即可实现分库分表。它具备良好的兼容性,可以与任何Java应用无缝集成,包括但不限于Spring、MyBatis等。在本项目中...
Sharding-JDBC教程:Spring Boot整合Sharding-JDBC实现分库分表+读写分离 Sharding-JDBC是阿里巴巴开源的关系型数据库中间件,提供了数据库分库分表、读写分离、数据库路由等功能。本教程将指导读者使用Sharding-...
在本教程中,我们将深入探讨如何使用SpringBoot与Sharding-JDBC进行集成,以实现自定义的数据库分库分表策略。Sharding-JDBC是Apache软件基金会下的一个轻量级Java框架,它允许开发者在不改变原有业务代码和数据库...
本案例基于Spring、MyBatis和Sharding-JDBC 1.3.1版本,提供了一个可以直接运行的分库分表实现,帮助开发者快速理解和实践这一技术。 首先,我们要理解什么是分库分表。分库是指将一个大型数据库拆分为多个小型...
.NET Core 实现分表分库、读写分离的通用 Repository 功能是指使用 FreeSql.Repository 库来实现通用的仓储层功能,实现了基础的仓储层(CURD),并且支持分表分库、读写分离等功能。 FreeSql.Repository 库是基于 ...
通过Sharding-JDBC和MyBatis实现数据库分片,结合Logstash将MySQL数据同步到Elasticsearch,解决了分库分表后的联合查询难题,同时利用Elasticsearch的高性能搜索能力,实现了高效的数据筛选。这样的设计思路在大...
本课程是新一代分库分表 Sharding-JDBC 最佳实践专题课程。课程内容丰富,涵盖了 MySQL 架构演变升级、分库分表的优缺点、常见策略及中间件介绍等多个方面。 课程首先介绍了分库分表的背景,包括 MySQL 数据库架构...
1、shardingsphere 并不直接支持达梦数据库,需要实现部分接口逻辑。 2、本demo并不完全支持达梦sql 3、包里面含有test demo可以直接测试 4、感谢shardingsphere 团队。 5、具体如何实现的 请查看我的博文 ...
通过这个项目,你可以深入理解Spring全家桶和Mybatis如何协同工作,以及如何在实际项目中实现数据库的分库分表策略。同时,博文链接提供的详细教程会进一步帮助你理解和实践这些概念。在学习过程中,务必动手实践,...
在IT行业中,数据库扩展是解决高并发、大数据量问题的关键技术之一。Sharding-JDBC作为阿里巴巴开源的一款轻量级...在实际操作中,我们需要结合业务场景和数据库特性,合理地设计分片策略,以实现最佳的分库分表效果。
"Python+MySQL分表分库实战"的主题,正是探讨如何结合这两者,以解决大数据存储和查询中的挑战。 分表分库,也称为数据库水平扩展,是应对海量数据的常用策略。当单个数据库表的数据量过大时,会导致查询效率降低,...
"spring动态数据源+mybatis分库分表"是一个针对大型数据库场景的解决方案,它利用Spring框架的动态数据源功能和MyBatis的SQL映射能力,实现数据库的透明化分片。以下是这个主题的详细知识点: 1. **Spring动态数据...
标题中的“mycat+mysql+jdbc实现根据手机号尾号分库分表存储”涉及的是分布式数据库中间件Mycat与MySQL数据库以及Java JDBC接口的结合使用。Mycat是一款开源的分布式数据库系统,用于解决大数据量、高并发的场景下的...
**Spring Boot 整合 ShardingSphere (Sharding JDBC) 5.2.0 分库分表实战** 在现代企业级应用开发中,随着业务量的增长,数据库的压力也随之增大,这时就需要进行数据库的分库分表操作来提升系统性能。Spring Boot ...
17、ShardingJDBC分库分表实战指南_ev.rar17、ShardingJDBC分库分表实战指南_ev.rar17、ShardingJDBC分库分表实战指南_ev.rar17、ShardingJDBC分库分表实战指南_ev.rar17、ShardingJDBC分库分表实战指南_ev.rar17、...
标题"sharding-jdbc分表分库接口版、配置版"提到了"sharding-jdbc",这是一个开源的Java框架,用于解决大数据量下的数据库分库分表问题。"接口版"可能指的是通过API来操作数据库,而"配置版"可能指的是通过配置文件...
Sharding-JDBC 是当当网开源的适用于微服务的分布式数据访问基础类库,完整的实现了分库分表,读写分离和分布式主键功能,并初步实现了柔性事务。从 2016 年开源至今,在经历了整体架构的数次精炼以及稳定性打磨后,...
本文将深入探讨如何利用SpringBoot的AOP(面向切面编程)特性,结合MyBatis的多数据源配置,实现动态表名的分库分表查询。 首先,我们需要理解SpringBoot的核心概念。SpringBoot是Spring框架的简化版本,它预设了...
作为一款高性能、易用性高的数据库水平分片框架,Sharding-JDBC在设计上力求简单高效,它通过直接封装JDBC协议,实现了对传统数据库操作的高度兼容,使得开发者能够在几乎不改动现有代码的基础上完成数据分库分表的...