一、读写分离
1.1 背景
面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。
读写分离的数据节点中的数据内容是一致的,而水平分片的每个数据节点的数据内容却并不相同。将水平分片和读写分离联合使用,能够更加有效的提升系统性能。
1.2 挑战
读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题。 这包括多个主库之间的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。 下图展现了将数据分片与读写分离一同使用时,应用程序与数据库集群之间的复杂拓扑关系。
1.3 应用场景
许多系统通过采用主从数据库架构的配置来提高整个系统的吞吐量,但是主从的配置也给业务的使用带来了一定的复杂性。接入 ShardingSphere
,可以利用读写分离功能管理主从数据库,实现透明化的读写分离功能,让用户像使用一个数据库一样使用主从架构的数据库。
1.4 核心概念
1.4.1 主库
添加、更新以及删除数据操作所使用的数据库,目前仅支持单主库。(可支持单主库多表数据分片)
1.4.2 从库
查询数据操作所使用的数据库,可支持多从库。
1.4.3 主从同步
将主库的数据异步的同步到从库的操作。 由于主从同步的异步性,从库与主库的数据会短时间内不一致。
1.4.4 负载均衡策略
通过负载均衡策略将查询请求疏导至不同从库。
1.5 使用限制
- 不处理主库和从库的数据同步(数据同步需自行实现)
- 不处理主库和从库的数据同步延迟导致的数据不一致
- 不支持主库多写
- 不处理主从库间的事务一致性。主从模型中,事务中的数据读写均用主库。
二、Mysql主从数据库同步
针对 1.5章节 提到的使用限制,自行实现主库和从库的数据同步,配置详情请见往期章节:Mysql8 数据库安装及主从配置 | Spring Cloud 2
三、读写分离示例示例
3.1 读写分开总体结构
3.1.1 主库数据源write_ds1
数据库地址 | 数据源名称 | 真实表名 | 逻辑表名称 | 业务描述 |
---|---|---|---|---|
192.168.0.35 | write_ds1 | t_goods_0 | t_goods | 商品表-分表 |
192.168.0.35 | write_ds1 | t_goods_1 | t_goods | 商品表-分表 |
3.1.2 从库数据源read_ds1
数据库地址 | 数据源名称 | 真实表名 | 逻辑表名称 | 业务描述 |
---|---|---|---|---|
192.168.0.45 | read_ds1 | t_goods_0 | t_goods | 商品表-分表 |
192.168.0.45 | read_ds1 | t_goods_1 | t_goods | 商品表-分表 |
3.1.3 从库数据源read_ds2
数据库地址 | 数据源名称 | 真实表名 | 逻辑表名称 | 业务描述 |
---|---|---|---|---|
192.168.0.46 | read_ds2 | t_goods_0 | t_goods | 商品表-分表 |
192.168.0.46 | read_ds1 | t_goods_1 | t_goods | 商品表-分表 |
3.1.1 逻辑商品表 t_goods
-- ----------------------------
-- Table structure for t_goods_0
-- ----------------------------
DROP TABLE IF EXISTS `t_goods_0`;
CREATE TABLE `t_goods_0` (
`goods_id` bigint NOT NULL,
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商品名称',
`main_class` bigint NULL DEFAULT NULL COMMENT '商品大类数据字典',
`sub_class` bigint NULL DEFAULT NULL COMMENT '商品小类数据字典',
PRIMARY KEY (`goods_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_goods_1
-- ----------------------------
DROP TABLE IF EXISTS `t_goods_1`;
CREATE TABLE `t_goods_1` (
`goods_id` bigint NOT NULL,
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商品名称',
`main_class` bigint NULL DEFAULT NULL COMMENT '商品大类数据字典',
`sub_class` bigint NULL DEFAULT NULL COMMENT '商品小类数据字典',
PRIMARY KEY (`goods_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
3.2 项目结构
3.2.1 项目总体结构
3.2.2 Maven 依赖
shading-sphere/shading-readwrite/pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>shading-sphere</artifactId>
<groupId>com.gm</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shading-readwrite</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
</dependencies>
</project>
-
shardingsphere-jdbc-core-spring-boot-starter
使用版本5.2.1
-
JDBC
的ORM
框架选用mybatis-plus
3.2.3 配置文件
server:
port: 8844
spring:
application:
name: @artifactId@
shardingsphere:
# 数据源配置
datasource:
names: write_ds1,read_ds1,read_ds2
write_ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.0.35:3306/db1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: '1qaz@WSX'
read_ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.0.45:3306/db1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: '1qaz@WSX'
read_ds2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.0.46:3306/db1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: '1qaz@WSX'
# 定义规则
rules:
sharding:
# 数据分片规则配置
tables:
# 指定某个表的分片配置,逻辑表名
t_goods:
# 这个配置是告诉sharding有多少个库和多少个表及所在实际的数据库节点,由数据源名 + 表名组成(参考 Inline 语法规则)
actual-data-nodes: readwrite_ds.t_goods_$->{0..1} # 此处使用下方定义的读写分离数据源
# 配置表分片策略
table-strategy:
# 用于单分片键的标准分片场景
standard:
# 分片列名称
sharding-column: main_class
# 分片算法名称
sharding-algorithm-name: t_goods_table_inline
# 分布式序列策略
key-generate-strategy:
# 自增列名称,缺省表示不使用自增主键生成器
column: goods_id
# 分布式序列算法名称
key-generator-name: snowflake
# 分片算法配置
sharding-algorithms:
# 分片算法名称
t_goods_table_inline:
# 分片算法类型
type: INLINE
# 分片算法属性配置
props:
algorithm-expression: t_goods_${main_class % 2}
# 分布式序列算法配置(如果是自动生成的,在插入数据的sql中就不要传id,null也不行,直接插入字段中就不要有主键的字段)
keyGenerators:
# 分布式序列算法名称
snowflake:
# 分布式序列算法类型
type: SNOWFLAKE
readwrite-splitting:
dataSources:
readwrite_ds: # 此处定义的数据源名称在上分分表中使用
staticStrategy:
writeDataSourceName: write_ds1
readDataSourceNames:
- read_ds1
- read_ds2
# dynamicStrategy:
loadBalancerName: myBalancer
load-balancers:
myBalancer:
type: RANDOM
props:
transactionalReadQueryStrategy: PRIMARY
props:
sql-show: true #显示sql
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
配置简要说明:
商品逻辑表
t_goods
:
- 按照商品大类取模分片算法进行分表
- 使用
RANDOM
算法进行读写分离配置
读写分离规则中定义的数据源名称
readwrite_ds
,在逻辑表t_goods
的数据分片规则中得用应用(actual-data-nodes
属性)
ShardingSphere-JDBC
读写分离实现包括:静态读写分离
和动态读写分离
两种方式,示例采用静态读写分离
。
关于动态读写分离使用配置,请见官网:
https://shardingsphere.apache.org/document/5.2.1/cn/user-manual/shardingsphere-jdbc/yaml-config/rules/ha/
https://shardingsphere.apache.org/document/5.2.1/cn/user-manual/shardingsphere-jdbc/spring-boot-starter/rules/ha/
ShardingSphere
内置提供了多种负载均衡算法,请见官网:
https://shardingsphere.apache.org/document/5.2.1/cn/user-manual/common-config/builtin-algorithm/load-balance/
3.2.4 实体定义
com/gm/shading/readwrite/entity/Goods.java
:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("t_goods")
public class Goods {
@TableId(type = IdType.ASSIGN_ID)
private Long goodsId;
private String goodsName;
private Long mainClass;
private Long subClass;
}
3.2.5 定义mapper
com/gm/shading/readwrite/mapper/GoodsMapper.java
:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.shading.readwrite.entity.Goods;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface GoodsMapper extends BaseMapper<Goods> {
void save(Goods goods);
}
src/main/resources/mapper/GoodsMapper.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2020 mttsmart4cloud Authors. All Rights Reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gm.shading.readwrite.mapper.GoodsMapper">
<insert id="save" parameterType="com.gm.shading.readwrite.entity.Goods">
insert into t_goods (goods_name, main_class, sub_class)
values (#{goodsName}, #{mainClass}, #{subClass})
</insert>
</mapper>
3.2.6 单元测试
src/test/java/com/gm/shading/readwrite/ShadingReadWriteApplicationTests.java
:
package com.gm.shading.readwrite;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gm.shading.readwrite.entity.Goods;
import com.gm.shading.readwrite.mapper.GoodsMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Random;
@SpringBootTest
public class ShadingReadWriteApplicationTests {
@Autowired
GoodsMapper goodsMapper;
// 分布式队列不生效,使用数据库递增
@Test
void addGoods() throws InterruptedException {
for (int i = 1; i <= 10; i++) {
Goods goods = new Goods();
Random random = new Random();
int mainClass = random.nextInt(100) + 1;
int subClass = random.nextInt(100) + 1;
goods.setMainClass((long) mainClass);
goods.setSubClass((long) subClass);
goods.setGoodsName("商品" + i);
goodsMapper.insert(goods);
}
Thread.sleep(2000);
for (int i = 1; i <= 10; i++) {
QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
List<Goods> list = goodsMapper.selectList(queryWrapper.orderByAsc("goods_id"));
for (Goods goods : list) {
System.out.println(goods.toString());
}
}
}
// 分布式队列生效,使用雪花算法生成ID
@Test
void addGoods2() throws InterruptedException {
for (int i = 1; i <= 10; i++) {
Goods goods = new Goods();
Random random = new Random();
int mainClass = random.nextInt(100) + 1;
int subClass = random.nextInt(100) + 1;
goods.setMainClass((long) mainClass);
goods.setSubClass((long) subClass);
goods.setGoodsName("商品" + i);
goodsMapper.save(goods);
}
Thread.sleep(2000);
for (int i = 1; i <= 10; i++) {
QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
List<Goods> list = goodsMapper.selectList(queryWrapper.orderByAsc("goods_id"));
for (Goods goods : list) {
System.out.println(goods.toString());
}
}
}
}
3.2.7 数据分片及读写分离效果