├── .gitignore ├── README.md ├── docker-compose.yml ├── pom.xml └── src ├── main ├── java │ └── codehole │ │ └── shardino │ │ ├── DataSourceAdapter.java │ │ ├── DebugSQLInterceptor.java │ │ ├── Holder.java │ │ ├── MySQLBuilder.java │ │ ├── MySQLConfig.java │ │ ├── MySQLGroupBuilder.java │ │ ├── MySQLGroupStore.java │ │ ├── MySQLMasterBuilder.java │ │ ├── MySQLOperation.java │ │ ├── MySQLSlaveBuilder.java │ │ ├── MySQLStore.java │ │ ├── RandomWeightedDataSource.java │ │ ├── WeightedAddr.java │ │ └── sample │ │ ├── Application.java │ │ ├── Charsets.java │ │ ├── PartitionConfig.java │ │ ├── Post.java │ │ ├── PostMapper.java │ │ ├── PostMySQL.java │ │ └── RepoConfig.java └── resources │ └── application.properties └── test └── java └── codehole └── shardino └── sample ├── AppTestBase.java └── PostMySQLTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | *.class 3 | .settings 4 | .classpath 5 | target 6 | .idea 7 | *.iml 8 | .DS_Store 9 | taiji-common/src/main/java/com/zhangyue/taiji/common/shardmysql 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Principles 2 | -- 3 | Explicit is better than Implicit! 4 | 5 | 6 | ```java 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Qualifier; 9 | import org.springframework.stereotype.Repository; 10 | import codehole.shardino.Holder; 11 | import codehole.shardino.MySQLGroupStore; 12 | 13 | @Repository 14 | public class PostMySQL { 15 | 16 | @Autowired 17 | private PartitionConfig partitions; 18 | 19 | @Autowired 20 | @Qualifier("post") 21 | private MySQLGroupStore mysql; 22 | 23 | public void createTables() { 24 | for (int i = 0; i < partitions.post(); i++) { 25 | int k = i; 26 | mysql.master(k).execute(session -> { 27 | PostMapper mapper = session.getMapper(PostMapper.class); 28 | mapper.createTable(k); 29 | }); 30 | } 31 | } 32 | 33 | public void dropTables() { 34 | for (int i = 0; i < partitions.post(); i++) { 35 | int k = i; 36 | mysql.master(k).execute(session -> { 37 | PostMapper mapper = session.getMapper(PostMapper.class); 38 | mapper.dropTable(k); 39 | }); 40 | } 41 | } 42 | 43 | public Post getPostFromMaster(String userId, String id) { 44 | Holder holder = new Holder<>(); 45 | int partition = this.partitionFor(userId); 46 | mysql.master(partition).execute(session -> { 47 | PostMapper mapper = session.getMapper(PostMapper.class); 48 | holder.value(mapper.getPost(partition, id)); 49 | }); 50 | return holder.value(); 51 | } 52 | 53 | public Post getPostFromSlave(String userId, String id) { 54 | Holder holder = new Holder<>(); 55 | int partition = this.partitionFor(userId); 56 | mysql.slave(partition).execute(session -> { 57 | PostMapper mapper = session.getMapper(PostMapper.class); 58 | holder.value(mapper.getPost(partition, id)); 59 | }); 60 | return holder.value(); 61 | } 62 | 63 | public void savePost(Post post) { 64 | int partition = this.partitionFor(post); 65 | mysql.master(partition).execute(session -> { 66 | PostMapper mapper = session.getMapper(PostMapper.class); 67 | Post curPost = mapper.getPost(partition, post.getId()); 68 | if (curPost != null) { 69 | mapper.updatePost(partition, post); 70 | } else { 71 | mapper.insertPost(partition, post); 72 | } 73 | }); 74 | } 75 | 76 | public void deletePost(String userId, String id) { 77 | int partition = this.partitionFor(userId); 78 | mysql.master(partition).execute(session -> { 79 | PostMapper mapper = session.getMapper(PostMapper.class); 80 | mapper.deletePost(partition, id); 81 | }); 82 | } 83 | 84 | private int partitionFor(Post post) { 85 | return Post.partitionFor(post.getUserId(), partitions.post()); 86 | } 87 | 88 | private int partitionFor(String userId) { 89 | return Post.partitionFor(userId, partitions.post()); 90 | } 91 | 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | master-0: 5 | image: 'bitnami/mariadb:latest' 6 | ports: 7 | - '3306:3306' 8 | environment: 9 | - MARIADB_REPLICATION_MODE=master 10 | - MARIADB_REPLICATION_USER=replication 11 | - MARIADB_REPLICATION_PASSWORD=123456 12 | - MARIADB_ROOT_PASSWORD=123456 13 | - MARIADB_USER=sample 14 | - MARIADB_PASSWORD=123456 15 | - MARIADB_DATABASE=sample 16 | slave-0-0: 17 | image: 'bitnami/mariadb:latest' 18 | ports: 19 | - '3307:3306' 20 | depends_on: 21 | - master-0 22 | environment: 23 | - MARIADB_REPLICATION_MODE=slave 24 | - MARIADB_REPLICATION_USER=replication 25 | - MARIADB_REPLICATION_PASSWORD=123456 26 | - MARIADB_MASTER_HOST=master-0 27 | - MARIADB_MASTER_PORT_NUMBER=3306 28 | - MARIADB_MASTER_ROOT_PASSWORD=123456 29 | slave-0-1: 30 | image: 'bitnami/mariadb:latest' 31 | ports: 32 | - '3308:3306' 33 | depends_on: 34 | - master-0 35 | environment: 36 | - MARIADB_REPLICATION_MODE=slave 37 | - MARIADB_REPLICATION_USER=replication 38 | - MARIADB_REPLICATION_PASSWORD=123456 39 | - MARIADB_MASTER_HOST=master-0 40 | - MARIADB_MASTER_PORT_NUMBER=3306 41 | - MARIADB_MASTER_ROOT_PASSWORD=123456 42 | master-1: 43 | image: 'bitnami/mariadb:latest' 44 | ports: 45 | - '3309:3306' 46 | environment: 47 | - MARIADB_REPLICATION_MODE=master 48 | - MARIADB_REPLICATION_USER=replication 49 | - MARIADB_REPLICATION_PASSWORD=123456 50 | - MARIADB_ROOT_PASSWORD=123456 51 | - MARIADB_USER=sample 52 | - MARIADB_PASSWORD=123456 53 | - MARIADB_DATABASE=sample 54 | slave-1-0: 55 | image: 'bitnami/mariadb:latest' 56 | ports: 57 | - '3310:3306' 58 | depends_on: 59 | - master-1 60 | environment: 61 | - MARIADB_REPLICATION_MODE=slave 62 | - MARIADB_REPLICATION_USER=replication 63 | - MARIADB_REPLICATION_PASSWORD=123456 64 | - MARIADB_MASTER_HOST=master-1 65 | - MARIADB_MASTER_PORT_NUMBER=3306 66 | - MARIADB_MASTER_ROOT_PASSWORD=123456 67 | slave-1-1: 68 | image: 'bitnami/mariadb:latest' 69 | ports: 70 | - '3311:3306' 71 | depends_on: 72 | - master-1 73 | environment: 74 | - MARIADB_REPLICATION_MODE=slave 75 | - MARIADB_REPLICATION_USER=replication 76 | - MARIADB_REPLICATION_PASSWORD=123456 77 | - MARIADB_MASTER_HOST=master-1 78 | - MARIADB_MASTER_PORT_NUMBER=3306 79 | - MARIADB_MASTER_ROOT_PASSWORD=123456 80 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | codehole 6 | shardino 7 | 0.0.1-SNAPSHOT 8 | 9 | 10 | 1.8 11 | UTF-8 12 | 13 | 14 | 15 | 16 | org.mybatis 17 | mybatis 18 | 3.5.0 19 | 20 | 21 | mysql 22 | mysql-connector-java 23 | 6.0.6 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 2.0.5.RELEASE 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | 2.0.5.RELEASE 34 | test 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 3.8.0 44 | 45 | ${java.version} 46 | ${java.version} 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/DataSourceAdapter.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.io.PrintWriter; 4 | import java.sql.SQLException; 5 | import java.sql.SQLFeatureNotSupportedException; 6 | import java.util.logging.Logger; 7 | import javax.sql.DataSource; 8 | 9 | public abstract class DataSourceAdapter implements DataSource { 10 | 11 | @Override 12 | public PrintWriter getLogWriter() throws SQLException { 13 | throw new UnsupportedOperationException("getLogWriter"); 14 | } 15 | 16 | @Override 17 | public void setLogWriter(PrintWriter out) throws SQLException { 18 | throw new UnsupportedOperationException("setLogWriter"); 19 | } 20 | 21 | @Override 22 | public void setLoginTimeout(int seconds) throws SQLException { 23 | throw new UnsupportedOperationException("setLoginTimeout"); 24 | } 25 | 26 | @Override 27 | public int getLoginTimeout() throws SQLException { 28 | throw new UnsupportedOperationException("getLoginTimeout"); 29 | } 30 | 31 | @Override 32 | public Logger getParentLogger() throws SQLFeatureNotSupportedException { 33 | return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); 34 | } 35 | 36 | @SuppressWarnings("unchecked") 37 | @Override 38 | public T unwrap(Class iface) throws SQLException { 39 | if (iface.isInstance(this)) { 40 | return (T) this; 41 | } 42 | throw new SQLException("DataSource of type [" + getClass().getName() 43 | + "] cannot be unwrapped as [" + iface.getName() + "]"); 44 | } 45 | 46 | @Override 47 | public boolean isWrapperFor(Class iface) throws SQLException { 48 | return iface.isInstance(this); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/DebugSQLInterceptor.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.util.Properties; 4 | import org.apache.ibatis.executor.Executor; 5 | import org.apache.ibatis.mapping.MappedStatement; 6 | import org.apache.ibatis.plugin.Interceptor; 7 | import org.apache.ibatis.plugin.Intercepts; 8 | import org.apache.ibatis.plugin.Invocation; 9 | import org.apache.ibatis.plugin.Plugin; 10 | import org.apache.ibatis.plugin.Signature; 11 | import org.apache.ibatis.session.ResultHandler; 12 | import org.apache.ibatis.session.RowBounds; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | @Intercepts({ 17 | @Signature(method = "query", type = Executor.class, 18 | args = {MappedStatement.class, Object.class, RowBounds.class, 19 | ResultHandler.class}), 20 | @Signature(method = "update", type = Executor.class, 21 | args = {MappedStatement.class, Object.class})}) 22 | public class DebugSQLInterceptor implements Interceptor { 23 | private final static Logger LOG = LoggerFactory.getLogger(DebugSQLInterceptor.class); 24 | 25 | private boolean showSQL; 26 | 27 | public DebugSQLInterceptor(boolean showSQL) { 28 | this.showSQL = showSQL; 29 | } 30 | 31 | @Override 32 | public Object intercept(Invocation invocation) throws Throwable { 33 | if (showSQL) { 34 | Object[] args = invocation.getArgs(); 35 | MappedStatement stmt = (MappedStatement) args[0]; 36 | if (args.length > 2) { 37 | RowBounds bounds = (RowBounds) args[2]; 38 | LOG.info("SQL:{}; Using:{}; Offset:{}; Limit:{}", 39 | stmt.getBoundSql(args[1]).getSql(), args[1], bounds.getOffset(), 40 | bounds.getLimit()); 41 | } else { 42 | LOG.info("SQL:{}; Using:{}", stmt.getBoundSql(args[1]).getSql(), args[1]); 43 | } 44 | } 45 | return invocation.proceed(); 46 | } 47 | 48 | @Override 49 | public Object plugin(Object target) { 50 | return Plugin.wrap(target, this); 51 | } 52 | 53 | @Override 54 | public void setProperties(Properties properties) { 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/Holder.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | public class Holder { 4 | 5 | private T value; 6 | 7 | public Holder() {} 8 | 9 | public Holder(T value) { 10 | this.value = value; 11 | } 12 | 13 | public void value(T value) { 14 | this.value = value; 15 | } 16 | 17 | public T value() { 18 | return value; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLBuilder.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import org.springframework.core.env.Environment; 4 | 5 | public abstract class MySQLBuilder { 6 | 7 | private MySQLConfig readConfig(Environment env, String configPrefix) { 8 | MySQLConfig config = new MySQLConfig(); 9 | String addrsProp = String.format("mysql.%s.%s.addrWeights", configPrefix, mode()); 10 | String addrsWeightRaw = env.getRequiredProperty(addrsProp); 11 | config.setAddrWeights(WeightedAddr.parse(addrsWeightRaw)); 12 | String dbProp = String.format("mysql.%s.%s.db", configPrefix, mode()); 13 | config.setDb(env.getProperty(dbProp, MySQLConfig.DEFAULT_DB)); 14 | String userProp = String.format("mysql.%s.%s.user", configPrefix, mode()); 15 | config.setUser(env.getProperty(userProp, MySQLConfig.DEFAULT_USER)); 16 | String passwdProp = String.format("mysql.%s.%s.password", configPrefix, mode()); 17 | config.setPasswd(env.getProperty(passwdProp, MySQLConfig.DEFAULT_PASSWD)); 18 | String poolSizeProp = String.format("mysql.%s.%s.poolSize", configPrefix, mode()); 19 | config.setPoolSize(env.getProperty(poolSizeProp, Integer.class, 20 | MySQLConfig.DEFAULT_POOL_SIZE)); 21 | return config; 22 | } 23 | 24 | public abstract String mode(); 25 | 26 | public MySQLStore buildStore(Environment env, String configPrefix) { 27 | MySQLConfig config = this.readConfig(env, configPrefix); 28 | MySQLStore store = new MySQLStore(config); 29 | return store; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLConfig.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class MySQLConfig { 7 | 8 | public final static String DEFAULT_DB = "test"; 9 | public final static String DEFAULT_USER = "root"; 10 | public final static String DEFAULT_PASSWD = ""; 11 | public final static int DEFAULT_POOL_SIZE = 16; 12 | 13 | private List addrWeights = new ArrayList(); 14 | 15 | private String db = "test"; 16 | private String user = DEFAULT_USER; 17 | private String passwd = DEFAULT_PASSWD; 18 | private int poolSize = DEFAULT_POOL_SIZE; 19 | 20 | public String getDb() { 21 | return db; 22 | } 23 | 24 | public void setDb(String db) { 25 | this.db = db; 26 | } 27 | 28 | public List getAddrWeights() { 29 | return addrWeights; 30 | } 31 | 32 | public void setAddrWeights(List addrWeights) { 33 | this.addrWeights = addrWeights; 34 | } 35 | 36 | public String getUser() { 37 | return user; 38 | } 39 | 40 | public void setUser(String user) { 41 | this.user = user; 42 | } 43 | 44 | public String getPasswd() { 45 | return passwd; 46 | } 47 | 48 | public void setPasswd(String passwd) { 49 | this.passwd = passwd; 50 | } 51 | 52 | public int getPoolSize() { 53 | return poolSize; 54 | } 55 | 56 | public void setPoolSize(int poolSize) { 57 | this.poolSize = poolSize; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLGroupBuilder.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import org.springframework.core.env.Environment; 4 | 5 | public class MySQLGroupBuilder { 6 | 7 | private MySQLMasterBuilder masterBuilder = new MySQLMasterBuilder(); 8 | private MySQLSlaveBuilder slaveBuilder = new MySQLSlaveBuilder(); 9 | 10 | public MySQLGroupStore buildStore(Environment env, String configPrefix) { 11 | String nodesProp = String.format("mysqlgroup.%s.nodes", configPrefix); 12 | String[] nodes = env.getProperty(nodesProp, String[].class); 13 | String slaveEnabledProp = String.format("mysqlgroup.%s.slaveEnabled", configPrefix); 14 | boolean slaveEnabled = env.getProperty(slaveEnabledProp, Boolean.class, false); 15 | MySQLGroupStore store = new MySQLGroupStore(); 16 | for (String node : nodes) { 17 | MySQLStore master = masterBuilder.buildStore(env, node); 18 | MySQLStore slave = null; 19 | if (slaveEnabled) { 20 | slave = slaveBuilder.buildStore(env, node); 21 | } 22 | store.append(master, slave); 23 | } 24 | return store; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLGroupStore.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Consumer; 6 | import org.apache.ibatis.session.SqlSessionFactory; 7 | 8 | public class MySQLGroupStore { 9 | 10 | static class Pair { 11 | MySQLStore master; 12 | MySQLStore slave; 13 | 14 | public Pair(MySQLStore master, MySQLStore slave) { 15 | this.master = master; 16 | this.slave = slave; 17 | } 18 | } 19 | 20 | private List pairs = new ArrayList(); 21 | 22 | public MySQLGroupStore append(MySQLStore master, MySQLStore slave) { 23 | this.pairs.add(new Pair(master, slave)); 24 | return this; 25 | } 26 | 27 | public MySQLStore master(int partition) { 28 | return pairs.get(partition % pairs.size()).master; 29 | } 30 | 31 | public MySQLStore slave(int partition) { 32 | return pairs.get(partition % pairs.size()).slave; 33 | } 34 | 35 | public MySQLStore master() { 36 | return pairs.get(0).master; 37 | } 38 | 39 | public MySQLStore slave() { 40 | return pairs.get(0).slave; 41 | } 42 | 43 | public MySQLStore db() { 44 | return master(); 45 | } 46 | 47 | public void prepare(Consumer consumer) { 48 | for (Pair pair : pairs) { 49 | pair.master.prepare(consumer); 50 | if (pair.slave != null) 51 | pair.slave.prepare(consumer); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLMasterBuilder.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | public class MySQLMasterBuilder extends MySQLBuilder { 4 | 5 | @Override 6 | public String mode() { 7 | return "master"; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLOperation.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.sql.SQLException; 4 | 5 | @FunctionalInterface 6 | public interface MySQLOperation { 7 | 8 | void accept(T t) throws SQLException; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLSlaveBuilder.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | public class MySQLSlaveBuilder extends MySQLBuilder { 4 | 5 | @Override 6 | public String mode() { 7 | return "slave"; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/MySQLStore.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.sql.SQLException; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.function.Consumer; 9 | import javax.sql.DataSource; 10 | import org.apache.ibatis.datasource.pooled.PooledDataSource; 11 | import org.apache.ibatis.mapping.Environment; 12 | import org.apache.ibatis.session.Configuration; 13 | import org.apache.ibatis.session.SqlSession; 14 | import org.apache.ibatis.session.SqlSessionFactory; 15 | import org.apache.ibatis.session.SqlSessionFactoryBuilder; 16 | import org.apache.ibatis.transaction.TransactionFactory; 17 | import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | public class MySQLStore { 22 | 23 | private final static Logger LOG = LoggerFactory.getLogger(MySQLStore.class); 24 | 25 | private MySQLConfig config; 26 | private RandomWeightedDataSource ds; 27 | private SqlSessionFactory factory; 28 | 29 | private List> prepareCallbacks = new ArrayList<>(); 30 | 31 | public MySQLStore(MySQLConfig config) { 32 | this.config = config; 33 | this.ds = buildDataSource(config); 34 | this.factory = this.buildFactory(ds); 35 | } 36 | 37 | private static RandomWeightedDataSource buildDataSource(MySQLConfig config) { 38 | Map sources = new HashMap<>(); 39 | for (WeightedAddr aw : config.getAddrWeights()) { 40 | String driver = "com.mysql.cj.jdbc.Driver"; 41 | String url = String.format( 42 | "jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=UTF8", 43 | aw.getHost(), aw.getPort(), config.getDb()); 44 | PooledDataSource ds = 45 | new PooledDataSource(driver, url, config.getUser(), config.getPasswd()); 46 | ds.setPoolMaximumActiveConnections(config.getPoolSize()); 47 | ds.setPoolPingEnabled(true); 48 | ds.setPoolPingQuery("select 1"); 49 | ds.setPoolPingConnectionsNotUsedFor(10000); 50 | sources.put(ds, aw.getWeight()); 51 | } 52 | return new RandomWeightedDataSource(sources); 53 | } 54 | 55 | private SqlSessionFactory buildFactory(DataSource ds) { 56 | TransactionFactory trxFactory = new JdbcTransactionFactory(); 57 | Environment env = new Environment("wtf", trxFactory, ds); 58 | Configuration c = new Configuration(env); 59 | c.setCacheEnabled(false); 60 | SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(c); 61 | for (Consumer prepare : this.prepareCallbacks) { 62 | prepare.accept(factory); 63 | } 64 | return factory; 65 | } 66 | 67 | public void prepare(Consumer prepareCallback) { 68 | this.prepareCallbacks.add(prepareCallback); 69 | prepareCallback.accept(factory); 70 | } 71 | 72 | public MySQLConfig getConfig() { 73 | return this.config; 74 | } 75 | 76 | public void execute(MySQLOperation consumer) { 77 | this.execute(consumer, true); 78 | } 79 | 80 | public void executeWithMapper(Class mapperClass, MySQLOperation consumer) { 81 | this.executeWithMapper(mapperClass, consumer, true); 82 | } 83 | 84 | public void executeWithMapper(Class mapperClass, MySQLOperation consumer, 85 | boolean autocommit) { 86 | this.execute(session -> { 87 | T mapper = session.getMapper(mapperClass); 88 | consumer.accept(mapper); 89 | }, autocommit); 90 | } 91 | 92 | public void execute(MySQLOperation consumer, boolean autocommit) { 93 | SqlSession session; 94 | try { 95 | session = factory.openSession(autocommit); 96 | } catch (Exception e) { 97 | LOG.error("connect mysql error", e); 98 | throw new RuntimeException("connect mysql error", e); 99 | } 100 | try { 101 | consumer.accept(session); 102 | } catch (SQLException e) { 103 | if (!autocommit) 104 | session.rollback(); 105 | LOG.error("access mysql error", e); 106 | throw new RuntimeException("access mysql error", e); 107 | } finally { 108 | session.close(); 109 | } 110 | } 111 | 112 | public void close() { 113 | this.ds.close(); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/RandomWeightedDataSource.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | import java.util.HashMap; 6 | import java.util.HashSet; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | import java.util.Set; 10 | import java.util.concurrent.ThreadLocalRandom; 11 | import org.apache.ibatis.datasource.pooled.PooledDataSource; 12 | 13 | public class RandomWeightedDataSource extends DataSourceAdapter { 14 | 15 | private int totalWeight; 16 | private Set sources; 17 | private Map sourceMap; 18 | 19 | public RandomWeightedDataSource(Map srcs) { 20 | this.sources = new HashSet<>(); 21 | this.sourceMap = new HashMap<>(); 22 | for (Entry entry : srcs.entrySet()) { 23 | // 权重值不宜过大 24 | int weight = Math.min(10000, entry.getValue()); 25 | for (int i = 0; i < weight; i++) { 26 | sourceMap.put(totalWeight, entry.getKey()); 27 | totalWeight++; 28 | } 29 | this.sources.add(entry.getKey()); 30 | } 31 | } 32 | 33 | private PooledDataSource getDataSource() { 34 | return this.sourceMap.get(ThreadLocalRandom.current().nextInt(totalWeight)); 35 | } 36 | 37 | public void close() { 38 | for (PooledDataSource ds : sources) { 39 | ds.forceCloseAll(); 40 | } 41 | } 42 | 43 | @Override 44 | public Connection getConnection() throws SQLException { 45 | return getDataSource().getConnection(); 46 | } 47 | 48 | @Override 49 | public Connection getConnection(String username, String password) throws SQLException { 50 | return getDataSource().getConnection(username, password); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/WeightedAddr.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class WeightedAddr { 7 | private String host; 8 | private int port; 9 | private int weight; 10 | 11 | public WeightedAddr() {} 12 | 13 | public WeightedAddr(String addr, int weight) { 14 | String[] splits = addr.split(":"); 15 | this.host = splits[0]; 16 | this.port = Integer.parseInt(splits[1]); 17 | this.weight = weight; 18 | } 19 | 20 | public WeightedAddr(String host, int port, int weight) { 21 | this.host = host; 22 | this.port = port; 23 | this.weight = weight; 24 | } 25 | 26 | public String getAddr() { 27 | return String.format("%s:%d", host, port); 28 | } 29 | 30 | public String getHost() { 31 | return host; 32 | } 33 | 34 | public void setHost(String host) { 35 | this.host = host; 36 | } 37 | 38 | public int getPort() { 39 | return port; 40 | } 41 | 42 | public void setPort(int port) { 43 | this.port = port; 44 | } 45 | 46 | public int getWeight() { 47 | return weight; 48 | } 49 | 50 | public void setWeight(int weight) { 51 | this.weight = weight; 52 | } 53 | 54 | public static List parse(String s) { 55 | String[] splits = s.split("&"); 56 | List addrs = new ArrayList<>(); 57 | for (String split : splits) { 58 | String[] parts = split.split("="); 59 | String addr = parts[0]; 60 | int weight = 100; 61 | if (parts.length > 1) { 62 | weight = Integer.parseInt(parts[1]); 63 | } 64 | addrs.add(new WeightedAddr(addr, weight)); 65 | } 66 | return addrs; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/Application.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 7 | 8 | @SpringBootApplication 9 | @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) 10 | public class Application { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/Charsets.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import java.nio.charset.Charset; 4 | 5 | public class Charsets { 6 | 7 | public final static Charset UTF8 = Charset.forName("utf-8"); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/PartitionConfig.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | 5 | @Configuration 6 | public class PartitionConfig { 7 | 8 | private int post = 64; 9 | 10 | public int post() { 11 | return post; 12 | } 13 | 14 | public void post(int post) { 15 | this.post = post; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/Post.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import java.util.Date; 4 | import java.util.zip.CRC32; 5 | 6 | public class Post { 7 | private String id; 8 | 9 | private String userId; 10 | 11 | private String title; 12 | 13 | private String content; 14 | 15 | private Date createTime; 16 | 17 | public Post() {} 18 | 19 | public Post(String id, String userId, String title, String content, Date createTime) { 20 | super(); 21 | this.id = id; 22 | this.userId = userId; 23 | this.title = title; 24 | this.content = content; 25 | this.createTime = createTime; 26 | } 27 | 28 | public String getId() { 29 | return id; 30 | } 31 | 32 | public void setId(String id) { 33 | this.id = id; 34 | } 35 | 36 | public String getUserId() { 37 | return userId; 38 | } 39 | 40 | public void setUserId(String userId) { 41 | this.userId = userId; 42 | } 43 | 44 | public String getTitle() { 45 | return title; 46 | } 47 | 48 | public void setTitle(String title) { 49 | this.title = title; 50 | } 51 | 52 | public String getContent() { 53 | return content; 54 | } 55 | 56 | public void setContent(String content) { 57 | this.content = content; 58 | } 59 | 60 | public Date getCreateTime() { 61 | return createTime; 62 | } 63 | 64 | public void setCreateTime(Date createTime) { 65 | this.createTime = createTime; 66 | } 67 | 68 | public int partitionFor(int num) { 69 | return partitionFor(userId, num); 70 | } 71 | 72 | public static int partitionFor(String userId, int num) { 73 | CRC32 crc = new CRC32(); 74 | crc.update(userId.getBytes(Charsets.UTF8)); 75 | return (int) (Math.abs(crc.getValue()) % num); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/PostMapper.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import org.apache.ibatis.annotations.Delete; 4 | import org.apache.ibatis.annotations.Insert; 5 | import org.apache.ibatis.annotations.Param; 6 | import org.apache.ibatis.annotations.Result; 7 | import org.apache.ibatis.annotations.Results; 8 | import org.apache.ibatis.annotations.Select; 9 | import org.apache.ibatis.annotations.Update; 10 | 11 | public interface PostMapper { 12 | 13 | @Update("create table if not exists post_#{partition}(id varchar(128) primary key not null, user_id varchar(1024) not null, title varchar(1024) not null, content text, create_time timestamp not null) engine=innodb") 14 | public void createTable(int partition); 15 | 16 | @Update("drop table if exists post_#{partition}") 17 | public void dropTable(int partition); 18 | 19 | @Results({@Result(property = "createTime", column = "create_time"), 20 | @Result(property = "userId", column = "user_id")}) 21 | @Select("select id, user_id, title, content, create_time from post_#{partition} where id=#{id}") 22 | public Post getPost(@Param("partition") int partition, @Param("id") String id); 23 | 24 | @Insert("insert into post_#{partition}(id, user_id, title, content, create_time) values(#{p.id}, ${p.userId}, #{p.title}, #{p.content}, #{p.createTime})") 25 | public void insertPost(@Param("partition") int partition, @Param("p") Post post); 26 | 27 | @Update("update post_#{partition} set title=#{p.title}, content=#{p.content}, create_time=#{p.createTime} where id=#{p.id}") 28 | public void updatePost(@Param("partition") int partition, @Param("p") Post post); 29 | 30 | @Delete("delete from post_#{partition} where id=#{id}") 31 | public void deletePost(@Param("partition") int partition, @Param("id") String id); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/PostMySQL.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.stereotype.Repository; 6 | import codehole.shardino.Holder; 7 | import codehole.shardino.MySQLGroupStore; 8 | 9 | @Repository 10 | public class PostMySQL { 11 | 12 | @Autowired 13 | private PartitionConfig partitions; 14 | 15 | @Autowired 16 | @Qualifier("post") 17 | private MySQLGroupStore mysql; 18 | 19 | public void createTables() { 20 | for (int i = 0; i < partitions.post(); i++) { 21 | int k = i; 22 | mysql.master(k).executeWithMapper(PostMapper.class, mapper -> { 23 | mapper.createTable(k); 24 | }); 25 | } 26 | } 27 | 28 | public void dropTables() { 29 | for (int i = 0; i < partitions.post(); i++) { 30 | int k = i; 31 | mysql.master(k).executeWithMapper(PostMapper.class, mapper -> { 32 | mapper.dropTable(k); 33 | }); 34 | } 35 | } 36 | 37 | public Post getPostFromMaster(String userId, String id) { 38 | Holder holder = new Holder<>(); 39 | int partition = this.partitionFor(userId); 40 | mysql.master(partition).executeWithMapper(PostMapper.class, mapper -> { 41 | holder.value(mapper.getPost(partition, id)); 42 | }); 43 | return holder.value(); 44 | } 45 | 46 | public Post getPostFromSlave(String userId, String id) { 47 | Holder holder = new Holder<>(); 48 | int partition = this.partitionFor(userId); 49 | mysql.slave(partition).executeWithMapper(PostMapper.class, mapper -> { 50 | holder.value(mapper.getPost(partition, id)); 51 | }); 52 | return holder.value(); 53 | } 54 | 55 | public void savePost(Post post) { 56 | int partition = this.partitionFor(post); 57 | mysql.master(partition).executeWithMapper(PostMapper.class, mapper -> { 58 | Post curPost = mapper.getPost(partition, post.getId()); 59 | if (curPost != null) { 60 | mapper.updatePost(partition, post); 61 | } else { 62 | mapper.insertPost(partition, post); 63 | } 64 | }); 65 | } 66 | 67 | public void deletePost(String userId, String id) { 68 | int partition = this.partitionFor(userId); 69 | mysql.master(partition).executeWithMapper(PostMapper.class, mapper -> { 70 | mapper.deletePost(partition, id); 71 | }); 72 | } 73 | 74 | private int partitionFor(Post post) { 75 | return Post.partitionFor(post.getUserId(), partitions.post()); 76 | } 77 | 78 | private int partitionFor(String userId) { 79 | return Post.partitionFor(userId, partitions.post()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/codehole/shardino/sample/RepoConfig.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.env.Environment; 8 | import codehole.shardino.DebugSQLInterceptor; 9 | import codehole.shardino.MySQLGroupBuilder; 10 | import codehole.shardino.MySQLGroupStore; 11 | 12 | @Configuration 13 | public class RepoConfig { 14 | 15 | @Autowired 16 | private Environment env; 17 | 18 | private MySQLGroupBuilder mysqlGroupBuilder = new MySQLGroupBuilder(); 19 | 20 | @Bean 21 | @Qualifier("post") 22 | public MySQLGroupStore replyMySQLGroupStore() { 23 | MySQLGroupStore store = mysqlGroupBuilder.buildStore(env, "post"); 24 | store.prepare(factory -> { 25 | factory.getConfiguration().addMapper(PostMapper.class); 26 | factory.getConfiguration().addInterceptor(new DebugSQLInterceptor(true)); 27 | }); 28 | return store; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | debug=false 2 | 3 | server.tomcat.accept-count=128 4 | server.tomcat.max-connections=1024 5 | server.tomcat.max-threads=256 6 | server.tomcat.min-spare-threads=16 7 | server.tomcat.uri-encoding=UTF-8 8 | server.servlet.context-path=/ 9 | server.port=8000 10 | 11 | mysql.post0.master.addrWeights=localhost:3306 12 | mysql.post0.master.db=sample 13 | mysql.post0.master.user=sample 14 | mysql.post0.master.password=123456 15 | mysql.post0.master.poolSize=10 16 | 17 | mysql.post0.slave.addrWeights=localhost:3307=100&localhost:3308=100 18 | mysql.post0.slave.db=sample 19 | mysql.post0.slave.user=sample 20 | mysql.post0.slave.password=123456 21 | mysql.post0.slave.poolSize=10 22 | 23 | mysql.post1.master.addrWeights=localhost:3309 24 | mysql.post1.master.db=sample 25 | mysql.post1.master.user=sample 26 | mysql.post1.master.password=123456 27 | mysql.post1.master.poolSize=10 28 | 29 | mysql.post1.slave.addrWeights=localhost:3310=100&localhost:3311=100 30 | mysql.post1.slave.db=sample 31 | mysql.post1.slave.user=sample 32 | mysql.post1.slave.password=123456 33 | mysql.post1.slave.poolSize=10 34 | 35 | mysqlgroup.post.nodes=post0,post1 36 | mysqlgroup.post.slaveEnabled=true -------------------------------------------------------------------------------- /src/test/java/codehole/shardino/sample/AppTestBase.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | 9 | @RunWith(SpringRunner.class) 10 | @SpringBootTest 11 | @AutoConfigureMockMvc 12 | public abstract class AppTestBase { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/codehole/shardino/sample/PostMySQLTest.java: -------------------------------------------------------------------------------- 1 | package codehole.shardino.sample; 2 | 3 | import java.util.Date; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | public class PostMySQLTest extends AppTestBase { 10 | 11 | @Autowired 12 | private PostMySQL pm; 13 | 14 | @Before 15 | public void setUp() { 16 | pm.dropTables(); 17 | pm.createTables(); 18 | } 19 | 20 | private static void sleep(int seconds) { 21 | try { 22 | Thread.sleep(seconds * 1000); 23 | } catch (InterruptedException e) { 24 | } 25 | } 26 | 27 | @Test 28 | public void savePost() throws Exception { 29 | Post p = pm.getPostFromMaster("2222", "1111"); 30 | Assertions.assertThat(p).isNull(); 31 | pm.savePost(new Post("1111", "2222", "test-title", "test-content", new Date())); 32 | p = pm.getPostFromMaster("2222", "1111"); 33 | Assertions.assertThat(p).isNotNull(); 34 | Assertions.assertThat(p.getTitle()).isEqualTo("test-title"); 35 | // waiting for replication 36 | sleep(2); 37 | p = pm.getPostFromSlave("2222", "1111"); 38 | Assertions.assertThat(p).isNotNull(); 39 | Assertions.assertThat(p.getTitle()).isEqualTo("test-title"); 40 | } 41 | 42 | @Test 43 | public void saveMultiPost() throws Exception { 44 | for (int i = 0; i <= 1000; i++) { 45 | int j = i / 10; 46 | Post p = pm.getPostFromMaster("" + j, "" + i); 47 | Assertions.assertThat(p).isNull(); 48 | pm.savePost(new Post("" + i, "" + j, "test-title", "test-content", new Date())); 49 | p = pm.getPostFromMaster("" + j, "" + i); 50 | Assertions.assertThat(p).isNotNull(); 51 | Assertions.assertThat(p.getTitle()).isEqualTo("test-title"); 52 | } 53 | sleep(2); 54 | for (int i = 0; i <= 1000; i++) { 55 | int j = i / 10; 56 | Post p = pm.getPostFromSlave("" + j, "" + i); 57 | Assertions.assertThat(p).isNotNull(); 58 | Assertions.assertThat(p.getTitle()).isEqualTo("test-title"); 59 | } 60 | } 61 | 62 | } 63 | --------------------------------------------------------------------------------