在进入正题以前,我想先随意谈谈对架构的拓展周期的想法(仅我的观点)。首先,我认为初期规划不应太复杂或者庞大,不管项目的中长期可能会发展地如何如何,前期都应该以灵活为优先,像分库分表等操做不该该在开始的时候就考虑进去。其次,我认为需求变动是很是正常的,这点在我等开发的圈子里吐槽的最多,其中天然有 “领导们” 在业务方面欠缺总体考虑的因素,但咱们也不应局限在一个观点内,市场中变则通,不变则死,前期更是如此,所以在前几版的架构中咱们必需要考虑较高的可扩展性。最后,当项目通过几轮市场的洗礼和迭代开发,核心业务趋于稳定了,此时咱们再结合中长期的规划给系统来一次重构,细致地去划分领域边界,该解耦的解耦,该拆分的拆分。java
当数据库达到必定规模后(好比说大几千万以上),切分是必需要考虑的。通常来讲咱们首先要进行垂直切分,即按业务分割,好比说用户相关、订单相关、统计相关等等均可以单独成库。图片来源 →node
但仅仅如此这是彻底不够的,垂直切分虽然剥离了必定的数据,但每一个业务仍是那个数量级,所以咱们还得采起水平切分进一步分散数据,这也是本节论述的重点。mysql
分库分表的优势相信上述两图都一目了然了,一个是专库专用,业务更集中,另外一个是提高数据库服务的负载能力。But there are always two sides to a coin。 今后之后你要接受你的系统复杂度将提高一个档次,迭代、迁移、运维等都再也不容易。sql
垂直切分在实现上就是一个多数据源的问题,没啥好讲的。如下 Demo 为水平切分,基于 Sharding-JDBC 中间件,我只作逻辑上的陈述,有关其更详细的信息和配置请移步 “官方文档”。docker
首先,咱们得在配置文件中定义分片策略,application.yml:数据库
server: port: 8001 mybatis: config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mappers/*.xml sharding: jdbc: datasource: names: youclk_0,youclk_1 youclk_0: type: org.apache.commons.dbcp.BasicDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://mysql:3306/youclk_0?useSSL=false username: root password: youclk youclk_1: type: org.apache.commons.dbcp.BasicDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://mysql:3306/youclk_1?useSSL=false username: root password: youclk config: sharding: default-database-strategy: inline: sharding-column: number algorithm-expression: youclk_${number % 2} tables: user: actual-data-nodes: youclk_${0..1}.user
具体每一个参数的含义在官方文档有详细解释,其实看名称也能理解个大概了,我定义将 number 为偶数的数据存入 youclk_0,奇数存入 youclk_1。express
User:apache
@Data public class User { private String id; private Integer number; private Date createTime; }
UserRepository:编程
@Mapper public interface UserRepository { void insert(User user); }
UserMapper.xml:安全
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.youclk.data.repository.UserRepository"> <resultMap id="BaseResultMap" type="com.youclk.data.entity.User"> <id column="id" property="id" jdbcType="CHAR"/> <result column="number" property="number" jdbcType="INTEGER"/> <result column="createTime" property="create_time" jdbcType="DATE"/> </resultMap> <sql id="Base_Column_List"> id, number, createTime </sql> <insert id="insert"> INSERT INTO user ( id, number ) VALUES ( uuid(), #{number,jdbcType=INTEGER} ) </insert> </mapper>
UserService:
@Service public class UserService { @Resource private UserRepository userRepository; public void insert() { for (int i = 0; i < 10; i++) { User user = new User(); user.setNumber(i); userRepository.insert(user); } } }
Result:
以上作了一个简单的循环插入,能够看到数据已经按策略分库存储,结果符合咱们的预期。
分库以后在查询方面要比以前更加谨慎,既然按策略去切了,那最好就是按策略去查,不然...好比我水平切分了 100个库,若不按策略去查询 LIMIT 100000, 10
这么一组数据,那最后扫描的数量级别是 100 * (100000 + 10)
, 这是比较恐怖的,虽然 Sharding-JDBC 作了一些优化,好比他不是一次性去查询到内存中,而是采用流式处理 + 归并排序的方式,但仍然比较耗资源,能避免仍是尽可能去避免吧。
在任何系统中事务都是顶要紧的事情,面对已分库的系统更是如此,保证夸库事务的安全历来不容易。分布式事务的场景有两种,一个是在分布式服务中,这个后续有机会再探讨,本节重点关注夸库事务。
Sharding-JDBC 自动包含了弱XA事务支持,即可以保证逻辑上的事务安全,但因网络或硬件致使的异常没法回滚,实现上与通常事务无异:
@Test @Transactional public void insertTest() { userService.insert(); int error = Integer.parseInt("I want error"); userService.insert(); }
能够看到夸库事务已回滚,除此以外 Sharding-JDBC 还提供了最大努力送达型柔性事务(将执行过程记录到日志中,失败重试,成功后删除,若最终仍是失败则保留事务日志,供人工干预),虽然安全性更高,但没法保证时效,限制也不少,这里留个待续吧,后续有空再深刻探讨(主要是比较晚了,想早点写完休息😁)。
为何要作主从?咱们先来探讨如下这几个场景:
我大体能想到这么几点,欢迎各位继续留言补充。
我以 MySQL 为例,通常部署架构为一台 Master 和 n 台 Slave,Master 的主责为写,并将数据同步至 Slave,Slave 主要提供查询功能。
为了测试方便,我直接使用 Docker 来部署,首先建立主从的配置文件,master.cnf:
[mysqld] server_id = 1 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-storage-engine=INNODB #Optimize omit sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES log-bin = /var/lib/mysql/binlog log_bin_trust_function_creators=1 binlog_format = ROW expire_logs_days = 99 sync_binlog = 0 slow-query-log=1 slow-query-log-file=/var/log/mysql/slow-queries.log long_query_time = 3 log-queries-not-using-indexes
slave.cnf:
[mysqld] server_id = 2 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-storage-engine=INNODB #Optimize omit sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES log-bin = /var/lib/mysql/binlog log_bin_trust_function_creators=1 binlog_format = ROW expire_logs_days = 99 sync_binlog = 0 relay_log=slave-relay-bin log-slave-updates=1 slave-skip-errors=all slow-query-log=1 slow-query-log-file=/var/log/mysql/slow-queries.log long_query_time = 3
而后进行 compose 编排,加入 warm 集群,docker-compose.yml:
version: '3.5' services: mysql-master: image: mysql ports: - 3301:3306 networks: - proxy - youclk volumes: - /Users/Jermey/Documents/data/db/cluster/master/mysql:/var/lib/mysql - /Users/Jermey/Documents/data/db/cluster/master/conf.d:/etc/mysql/conf.d environment: MYSQL_ROOT_PASSWORD: youclk mysql-slave: image: mysql ports: - 3302:3306 networks: - proxy - youclk volumes: - /Users/Jermey/Documents/data/db/cluster/slave/mysql:/var/lib/mysql - /Users/Jermey/Documents/data/db/cluster/slave/conf.d:/etc/mysql/conf.d environment: MYSQL_ROOT_PASSWORD: youclk networks: proxy: external: true youclk: external: true
再次感激 Docker, 从编排配置文件到最后启动服务整个过程不到一分钟:
接下来就是配置主从关系:
docker exec -it cluster_mysql-master mysql -p CREATE USER 'reader'@'%' IDENTIFIED BY 'youclk'; GRANT REPLICATION SLAVE ON *.* TO 'reader'@'%'; show master status\G
docker exec -it cluster_mysql-slave mysql -p CHANGE MASTER TO \ MASTER_HOST='mysql-master',\ MASTER_PORT=3306,\ MASTER_USER='reader',\ MASTER_PASSWORD='youclk',\ MASTER_LOG_FILE='binlog.000004',\ MASTER_LOG_POS=154; start slave; show slave status\G
Test:
上图中左边连的是 Master, 右边为 Slave, 我在 Master 中执行 create database youclk_0;
能够看到 Slave 中也生成了 youclk_0,至此主从配置测试完成。
基于 Sharding-JDBC 的读写分离实现很是简单,改一下配置文件,其他几乎是无感知的,application.yml:
server: port: 8001 mybatis: config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mappers/*.xml sharding: jdbc: datasource: names: ds_master,ds_slave ds_master: type: org.apache.commons.dbcp.BasicDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://mysql:3301/youclk_0?useSSL=false username: root password: youclk ds_slave: type: org.apache.commons.dbcp.BasicDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://mysql:3302/youclk_0?useSSL=false username: root password: youclk config: masterslave: load-balance-algorithm-type: round_robin name: ds_ms master-data-source-name: ds_master slave-data-source-names: ds_slave sharding: props: sql.show: true
Test:
@Test public void selectAndInsertTest() { userService.selectAll(); userService.insert(); }
Result:
跟踪 MySQL 的日志能够发现主从库分别执行了插入与查询,实现了读写分离。