手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(一) – 介绍
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(二) – 数据库设计
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(三) – 项目初始化
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(四) – 日志 & 跨域配置
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(五) – MyBatis-Plus & 代码生成器集成与配置
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(六) – 本地缓存 Caffeine 和 分布式缓存 Redis 集成与配置
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(七) – Elasticsearch 8.2 集成与配置
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(八) – XXL-JOB 集成与配置
手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(九) – Spring AMQP 集成与配置
背景
传统的将数据集中存储至单一节点的解决方案,在性能、可用性和运维成本这三方面已经难于满足海量数据的场景。
从性能方面来说,由于关系型数据库大多采用 B+ 树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降; 同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
从可用性的方面来讲,服务化的无状态性,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。 而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。
从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于 DBA 的运维压力就会增大。 数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例的数据的阈值在 1TB 之内,是比较合理的范围。
数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。
小说数据有着内容多、增长速度快的特点,一本主流的完结小说一般所需存储空间大概在 5MB 以上。一个主流的小说网站在发展中后期,数据量是远远超过单一数据库实例的阈值的,所以我们对小说内容进行分库分表存储是非常有必要的。在发展初期,我们的数据量还不是很大,可以先将小说内容分表存储以减轻数据库单表压力以及为后期的数据库分库做准备。等数据量即将超过阈值时,再迁移到不同的数据库实例上。
注:数据分片分为按照业务将表进行归类,分布到不同的数据库中的垂直分片和通过某个字段(或某几个字段)按照某种规则将数据分散至多个库或表中的水平分片。
Apache ShardingSphere 介绍
Apache ShardingSphere 产品定位为 Database Plus,它关注如何充分合理地利用数据库的计算和存储能力,而并非实现一个全新的数据库。ShardingSphere 站在数据库的上层视角,关注他们之间的协作多于数据库自身,由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的基于数据库作为存储节点的增量功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
ShardingSphere-Proxy 定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。
ShardingSphere-Sidecar 定位为 Kubernetes 的云原生数据库代理,以 Sidecar 的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的啮合层,即 Database Mesh,又可称数据库网格。
连接、增量 和 可插拔 是 Apache ShardingSphere 的核心概念:
-
连接:通过对数据库协议、SQL 方言以及数据库存储的灵活适配,快速的连接应用与多模式的异构数据库;
-
增量:获取数据库的访问流量,并提供流量重定向(数据分片、读写分离、影子库)、流量变形(数据加密、数据脱敏)、流量鉴权(安全、审计、权限)、流量治理(熔断、限流)以及流量分析(服务质量分析、可观察性)等透明化增量功能;
-
可插拔:项目采用微内核 + 三层可插拔模型,使内核、功能组件以及生态对接完全能够灵活的方式进行插拔式扩展,开发者能够像使用积木一样定制属于自己的独特系统。
Apache ShardingSphere 的数据分片模块透明化了分库分表所带来的影响,让使用方尽量像使用一个数据库一样使用水平分片之后的数据库集群。
集成步骤
- MySQL 执行以下的数据迁移脚本:
DROP PROCEDURE IF EXISTS createBookChapterTable; -- 创建小说章节表的存储过程 CREATE PROCEDURE createBookChapterTable ( ) BEGIN -- 定义变量 DECLARE i INT DEFAULT 0; DECLARE tableName CHAR ( 13 ) DEFAULT NULL; WHILE i < 10 DO SET tableName = concat( 'book_chapter', i ); SET @stmt = concat( 'create table ', tableName, '( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `book_id` bigint(20) unsigned NOT NULL COMMENT /'小说ID/', `chapter_num` smallint(5) unsigned NOT NULL COMMENT /'章节号/', `chapter_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT /'章节名/', `word_count` int(10) unsigned NOT NULL COMMENT /'章节字数/', `is_vip` tinyint(3) unsigned NOT NULL DEFAULT /'0/' COMMENT /'是否收费;1-收费 0-免费/', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_bookId_chapterNum` (`book_id`,`chapter_num`) USING BTREE, UNIQUE KEY `pk_id` (`id`) USING BTREE, KEY `idx_bookId` (`book_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=/'小说章节/'' ); PREPARE stmt FROM @stmt; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET i = i + 1; END WHILE; END; CALL createBookChapterTable ( ); DROP PROCEDURE IF EXISTS createBookContentTable; -- 创建小说内容表的存储过程 CREATE PROCEDURE createBookContentTable ( ) BEGIN -- 定义变量 DECLARE i INT DEFAULT 0; DECLARE tableName CHAR ( 13 ) DEFAULT NULL; WHILE i < 10 DO SET tableName = concat( 'book_content', i ); SET @stmt = concat( 'create table ', tableName, '( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT /'主键/', `chapter_id` bigint(20) unsigned NOT NULL COMMENT /'章节ID/', `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT /'小说章节内容/', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_chapterId` (`chapter_id`) USING BTREE, UNIQUE KEY `pk_id` (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=/'小说内容/'' ); PREPARE stmt FROM @stmt; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET i = i + 1; END WHILE; END; CALL createBookContentTable ( ); DROP PROCEDURE IF EXISTS copyBookChapterData; -- 迁移小说章节数据的存储过程 CREATE PROCEDURE copyBookChapterData ( ) BEGIN -- 定义变量 DECLARE s INT DEFAULT 0; DECLARE chapterId BIGINT; DECLARE bookId BIGINT; DECLARE chapterNum SMALLINT; DECLARE chapterName VARCHAR ( 100 ); DECLARE wordCount INT DEFAULT 0; DECLARE isVip TINYINT ( 64 ) DEFAULT 0; DECLARE createTime datetime DEFAULT NULL; DECLARE updateTime datetime DEFAULT NULL; DECLARE tableNumber INT DEFAULT 0; DECLARE tableName CHAR ( 13 ) DEFAULT NULL; -- 定义游标 DECLARE report CURSOR FOR SELECT id, book_id, chapter_num, chapter_name, word_count, is_vip, create_time, update_time FROM book_chapter; -- 声明当游标遍历完后将标志变量置成某个值 DECLARE CONTINUE HANDLER FOR NOT FOUND SET s = 1; -- 打开游标 OPEN report; -- 将游标中的值赋值给变量,注意:变量名不要和返回的列名同名,变量顺序要和sql结果列的顺序一致 FETCH report INTO chapterId, bookId, chapterNum, chapterName, wordCount, isVip, createTime, updateTime; -- 循环遍历 WHILE s <> 1 DO -- 执行业务逻辑 SET tableNumber = bookId % 10; SET tableName = concat( 'book_chapter', tableNumber ); SET @stmt = concat( 'insert into ', tableName, '(`id`, `book_id`, `chapter_num`, `chapter_name`, `word_count`, `is_vip`, `create_time`, `update_time`) VALUES (', chapterId, ', ', bookId, ', ', chapterNum, ', /'', chapterName, '/', ', wordCount, ', ', isVip, ', /'', createTime, '/', /'', updateTime, '/')' ); PREPARE stmt FROM @stmt; EXECUTE stmt; DEALLOCATE PREPARE stmt; FETCH report INTO chapterId, bookId, chapterNum, chapterName, wordCount, isVip, createTime, updateTime; END WHILE; -- 关闭游标 CLOSE report; END; CALL copyBookChapterData ( ); DROP PROCEDURE IF EXISTS copyBookContentData; -- 迁移小说内容数据的存储过程 CREATE PROCEDURE copyBookContentData ( ) BEGIN -- 定义变量 DECLARE s INT DEFAULT 0; DECLARE contentId BIGINT; DECLARE chapterId BIGINT; DECLARE bookContent MEDIUMTEXT; DECLARE createTime datetime DEFAULT NULL; DECLARE updateTime datetime DEFAULT NULL; DECLARE tableNumber INT DEFAULT 0; DECLARE tableName CHAR ( 13 ) DEFAULT NULL; -- 定义游标 DECLARE report CURSOR FOR SELECT id, chapter_id, content, create_time, update_time FROM book_content; -- 声明当游标遍历完后将标志变量置成某个值 DECLARE CONTINUE HANDLER FOR NOT FOUND SET s = 1; -- 打开游标 OPEN report; -- 将游标中的值赋值给变量,注意:变量名不要和返回的列名同名,变量顺序要和sql结果列的顺序一致 FETCH report INTO contentId, chapterId, bookContent, createTime, updateTime; -- 循环遍历 WHILE s <> 1 DO -- 执行业务逻辑 SET tableNumber = chapterId % 10; SET tableName = concat( 'book_content', tableNumber ); SET bookContent = REPLACE ( bookContent, '/'', "//'" ); SET @stmt = concat( 'insert into ', tableName, '(`id`, `chapter_id`, `content`) VALUES (', contentId, ', ', chapterId, ',/'', bookContent, '/')' ); PREPARE stmt FROM @stmt; EXECUTE stmt; DEALLOCATE PREPARE stmt; FETCH report INTO contentId, chapterId, bookContent, createTime, updateTime; END WHILE; -- 关闭游标 CLOSE report; END; CALL copyBookContentData ( );
- 引入 ShardingSphere-JDBC 官方提供的 Spring Boot Starter 依赖:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.1</version> </dependency>
- application.yml 中添加 ShardingSphere-JDBC 的配置:
spring: shardingsphere: # 是否开启 shardingsphere enabled: false props: # 是否在日志中打印 SQL sql-show: true # 模式配置 mode: # 单机模式 type: Standalone repository: # 文件持久化 type: File props: # 元数据存储路径 path: .shardingsphere # 使用本地配置覆盖持久化配置 overwrite: true # 数据源配置 datasource: names: ds_0 ds_0: type: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: test123456 # 规则配置 rules: # 数据分片 sharding: tables: # book_content 表 book_content: # 数据节点 actual-data-nodes: ds_$->{0}.book_content$->{0..9} # 分表策略 table-strategy: standard: # 分片列名称 sharding-column: chapter_id # 分片算法名称 sharding-algorithm-name: bookContentSharding sharding-algorithms: bookContentSharding: # 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持 type: INLINE props: # 分片算法的行表达式 algorithm-expression: book_content$->{chapter_id % 10}
配置是 ShardingSphere-JDBC 中唯一与应用开发者交互的模块,通过它可以快速清晰的理解 ShardingSphere-JDBC 所提供的功能。
-
模式配置: Apache ShardingSphere 提供的 3 种运行模式分别是适用于集成测试的环境启动,方便开发人员在整合功能测试中集成 Apache ShardingSphere 而无需清理运行痕迹
内存模式
、能够将数据源和规则等元数据信息持久化,但无法将元数据同步至多个 Apache ShardingSphere 实例,无法在集群环境中相互感知的单机模式
和提供了多个 Apache ShardingSphere 实例之间的元数据共享和分布式场景下状态协调能力的集群模式
。 -
数据源配置:包括使用本地数据源配置(本项目中)和使用 JNDI 数据源的配置。如果计划使用 JNDI 配置数据库,在应用容器(如 Tomcat)中使用 ShardingSphere-JDBC 时, 可使用 spring.shardingsphere.datasource.${datasourceName}.jndiName 来代替数据源的一系列配置。
-
规则配置:规则是 Apache ShardingSphere 面向可插拔的一部分,包括数据分片、读写分离、高可用、数据加密、影子库、SQL 解析、混合规则等。
以下是数据分片的配置项说明:
# 标准分片表配置 spring.shardingsphere.rules.sharding.tables.<table-name>.actual-data-nodes= # 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况 # 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一 # 用于单分片键的标准分片场景 spring.shardingsphere.rules.sharding.tables.<table-name>.database-strategy.standard.sharding-column= # 分片列名称 spring.shardingsphere.rules.sharding.tables.<table-name>.database-strategy.standard.sharding-algorithm-name= # 分片算法名称 # 用于多分片键的复合分片场景 spring.shardingsphere.rules.sharding.tables.<table-name>.database-strategy.complex.sharding-columns= # 分片列名称,多个列以逗号分隔 spring.shardingsphere.rules.sharding.tables.<table-name>.database-strategy.complex.sharding-algorithm-name= # 分片算法名称 # 用于 Hint 的分片策略 spring.shardingsphere.rules.sharding.tables.<table-name>.database-strategy.hint.sharding-algorithm-name= # 分片算法名称 # 分表策略,同分库策略 spring.shardingsphere.rules.sharding.tables.<table-name>.table-strategy.xxx= # 省略 # 自动分片表配置 spring.shardingsphere.rules.sharding.auto-tables.<auto-table-name>.actual-data-sources= # 数据源名 spring.shardingsphere.rules.sharding.auto-tables.<auto-table-name>.sharding-strategy.standard.sharding-column= # 分片列名称 spring.shardingsphere.rules.sharding.auto-tables.<auto-table-name>.sharding-strategy.standard.sharding-algorithm-name= # 自动分片算法名称 # 分布式序列策略配置 spring.shardingsphere.rules.sharding.tables.<table-name>.key-generate-strategy.column= # 分布式序列列名称 spring.shardingsphere.rules.sharding.tables.<table-name>.key-generate-strategy.key-generator-name= # 分布式序列算法名称 spring.shardingsphere.rules.sharding.binding-tables[0]= # 绑定表规则列表 spring.shardingsphere.rules.sharding.binding-tables[1]= # 绑定表规则列表 spring.shardingsphere.rules.sharding.binding-tables[x]= # 绑定表规则列表 spring.shardingsphere.rules.sharding.broadcast-tables[0]= # 广播表规则列表 spring.shardingsphere.rules.sharding.broadcast-tables[1]= # 广播表规则列表 spring.shardingsphere.rules.sharding.broadcast-tables[x]= # 广播表规则列表 spring.shardingsphere.sharding.default-database-strategy.xxx= # 默认数据库分片策略 spring.shardingsphere.sharding.default-table-strategy.xxx= # 默认表分片策略 spring.shardingsphere.sharding.default-key-generate-strategy.xxx= # 默认分布式序列策略 spring.shardingsphere.sharding.default-sharding-column= # 默认分片列名称 # 分片算法配置 spring.shardingsphere.rules.sharding.sharding-algorithms.<sharding-algorithm-name>.type= # 分片算法类型 spring.shardingsphere.rules.sharding.sharding-algorithms.<sharding-algorithm-name>.props.xxx= # 分片算法属性配置 # 分布式序列算法配置 spring.shardingsphere.rules.sharding.key-generators.<key-generate-algorithm-name>.type= # 分布式序列算法类型 spring.shardingsphere.rules.sharding.key-generators.<key-generate-algorithm-name>.props.xxx= # 分布式序列算法属性配置
其中,分片算法分为包含取模分片、哈希取模分片、基于分片容量的范围分片、基于分片边界的范围分片、自动时间段分片在内的自动分片算法
和包含行表达式分片、时间范围分片在内的标准分片算法
以及复合分片算法
和Hint 分片算法
。我们还可以自定义类分片算法,通过配置分片策略类型和算法类名,实现自定义扩展。
分布式序列算法包括雪花算法和 UUID。