数据读写分离方案调研
读写分离方案调研
背景
随着数据量的快速增长,单台数据库实例的 TPS & QPS 都增长的很高,对数据库造成的压力比较大,特别是多个应用都连接同一个数据库实例的场景。虽然我们可以对数据库进行调优,比如针对 MySQL 提高最大连接数,增加 buffer pool size 的内存大小。但是随着数据量的持续增加这种针对单台实例的调优就会达到上限,这时候我们可能会选择购买性能配置更优的机器,但是始终还是受制于机器的性能配置。于是我们转变一下思路,可以从上述的垂直扩展的思路转换到水平扩展。采用分流的思想,将请求分发到多台数据库实例上来分摊压力。这样系统整体瓶颈不再受制于单点的数据库实例。而水平扩展让我们之后的扩展变得更为灵活,在我们规划好扩展策略后,随着数据的持续增加我们只需要增加新的数据库实例即可。虽然这个方案解决了我们的问题,但是也引入了新的问题,如在读写分离场景下数据同步的及时性问题。在数据分片场景下,垮数据库之间的事务问题。以及查询场景下数据的查询结果归并问题。
鉴于目前我们先不考虑对数据进行分片,所以这里只讨论读写分离的场景。
MySQL 主从复制
为了提高系统整体可用性,降低由于单点故障导致的系统整体事故, MySQL 可以选择主从复制的方案,主服务器 Master 负责写,从服务器 Slave 负责读。
MySQL 主从复制过程
binlog 格式
- master 必须要开启 binlog 日志
- 留意 binlog 日志的格式,
ROW
,MiXED
,Statement
Statement
记录的是修改数据的 SQL 语句, 日志量小但是有主从数据不一致的风险(语句中使用了一些函数的场景)。ROW
记录的是被修改的行数据,产生的日志量较大。MiXED
是Statement
和ROW
的混合模式,一般语句修改使用Statement
格式保存binlog,如一些函数,Statement
无法完成主从复制的操作,则采用Row
格式保存 binlog。
数据同步策略
- 同步策略:Master会等待所有的Slave都回应后才会提交
- 半同步策略:Master至少会等待一个Slave回应后提交
- 异步策略:Master不用等待Slave回应就可以提交
- 延迟策略:Slave要落后于Master指定的时间
my.cnf 文件配置
# 开启二进制日志
log_bin = /usr/local/mysql-5.7.21/log/mysql-bin.log
# mysql清除过期日志的时间,默认值0,不自动清理,而是使用滚动循环的方式。
expire_logs_days = 0
# 日志每达到设定大小后,会使用新的bin log日志
max_binlog_size = 1000M
# binlog的格式也有三种:STATEMENT,ROW,MIXED。mysql 5.7.7后,默认值从 MIXED 改为 ROW
binlog_format = row
# 等于 0 当事务提交之后,MySQL不做fsync之类的磁盘同步指令刷新binlog_cache中的信息到磁盘,而让Filesystem自行决定什么时候来做同步,或者cache满了之后才同步到磁盘. 大于 1 时当每进行n次事务提交之后,MySQL将进行一次fsync之类的磁盘同步指令来将binlog_cache中的数据强制写入磁盘。所以这个参数很重要会直接影响到主从同步的效果。
# sync_binlog = 1
# 查 binlog 格式
show global variables like '%binlog_format%' ;
# 查 binlog 落盘配置
show global variables like '%sync_binlog%' ;
基于 MySQL 主从实现读写分离
代理方式
在数据库实例和客户端之前设置一层代理,对数据的分片,查询结果归并,事务控制,读写分离都在代理层控制,对客户端是透明的,对应用零侵入。但是系统的瓶颈转移到了代理层,所以代理层必须要保证是高可用的,否则代理层出现故障会导致整个数据访问层不可用。
可选产品
- Cobar: 阿里巴巴产品,实现MySQL协议,不支持读写分离;
- Altas: 360产品,基于 MySQL Proxy 实现,不能实现分布式分表,所有的子表必须在同一台DB的同一个;
- Heisenberg: 百度产品,实现MySQL协议,基于 Cobar,不活跃;
- Mycat: 开源产品,实现MySQL协议,但是不建议使用,有烂尾风险;mycatone.top/
- 阿里云 RDS MySQL 高可用版或集群版;help.aliyun.com/document_de…
- ShardingSphere-Proxy:国人开发,实现MySQL协议,阿帕奇顶级项目; shardingsphere.apache.org/
- Vitess: YouTube 产品,集群基于ZooKeeper管理,通过RPC方式进行数据处理,可支撑高流量,它还添加了一个连接池,具有基于行的高速缓存,重写SQL查询,与原生MySQL高度兼容。Vitess 的不提供默认的读写分离功能,但是提供在 SQL 中通过 @指定的方式进行从库的查询; vitess.io/
- proxysql:使用C++开发性能较好; proxysql.com/
还有其他同类型产品不一一列举了。
应用层依赖方式
应用层依赖方式通过提供一个 jar 包方式实现数据分片,读写分离等,特点是使用起来较为简单,技术投入较少。缺点也很明显没法处理通过命令行,或者图形化界面工具操作数据库的场景。比如需要执行某个 SQL 需要操作者根据分片策略计算得出该在那个库或者表上执行命令,对运维环节十分的不友好。
可选产品
- TDDL: github上TDDL处于停滞状态,部分功能不开源,TDDL 必须要依赖 diamond 配置中心( diamond 是淘宝内部使用的一个管理持久配置的系统,目前淘宝内部绝大多数系统的配置)。
- dynamic-datasource-spring-boot-starter: 一个基于springboot的快速集成多数据源的启动器,对业务系统代码的侵入性较强。只能实现简单的读写分离,不支持数据水平拆分。 www.kancloud.cn/tracy5546/d…
- ShardingSphere-JDBC: 国人开源,阿帕奇顶级项目,社区活跃。shardingsphere.apache.org
dynamic-datasource-spring-boot-starter 对业务系统代码的侵入。
@Service
@DS("master")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("db1")
public List<Map<String, Object>> selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
ShardingSphere-JDBC
ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。
特性 | 定义 |
---|---|
数据分片 | 数据分片,是应对海量数据存储与计算的有效手段。ShardingSphere 基于底层数据库提供分布式数据库解决方案,可以水平扩展计算和存储。 |
分布式事务 | 事务能力,是保障数据库完整、安全的关键技术,也是数据库的核心技术。基于 XA 和 BASE 的混合事务引擎,ShardingSphere 提供在独立数据库上的分布式事务功能,保证跨数据源的数据安全。 |
读写分离 | 读写分离,是应对高压力业务访问的手段。基于对 SQL 语义理解及对底层数据库拓扑感知能力,ShardingSphere 提供灵活的读写流量拆分和读流量负载均衡。 |
高可用 | 高可用,是对数据存储计算平台的基本要求。ShardingSphere 提供基于原生或 Kubernetes 环境下数据库集群的分布式高可用能力。 |
数据迁移 | 数据迁移,是打通数据生态的关键能力。ShardingSphere 提供跨数据源的数据迁移能力,并可支持重分片扩展。 |
联邦查询 | 联邦查询,是面对复杂数据环境下利用数据的有效手段。ShardingSphere 提供跨数据源的复杂查询分析能力,实现跨源的数据关联与聚合。 |
数据加密 | 数据加密,是保证数据安全的基本手段。ShardingSphere 提供完整、透明、安全、低成本的数据加密解决方案。 |
影子库 | 在全链路压测场景下,ShardingSphere 支持不同工作负载下的数据隔离,避免测试数据污染生产环境。 |
ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC; 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, HikariCP 等; 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 访问的数据库。
ShardingSphere-JDBC Demo
引入依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.3.2</version>
</dependency>
<!-- 如果存在冲突需要指定版本,否则 shardingsphere 无法正常工作 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
编写 ShardingSphere Yaml 配置
mode:
type: Standalone
repository:
type: JDBC
dataSources:
write_ds:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://example.org:3306/test_1?useUnicode=true&useSSL=false
username: root
password: root
read_ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://example.org:3306/test_2?useUnicode=true&useSSL=false&allowMultiQueries=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
rules:
- !READWRITE_SPLITTING
dataSources:
readwrite_ds:
staticStrategy:
writeDataSourceName: write_ds
readDataSourceNames:
- read_ds_0
props:
sql-show: true
spring 配置文件
# 配置 DataSource Driver
spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
# 指定 YAML 配置文件
spring.datasource.url=jdbc:shardingsphere:classpath:shardingsphere.yaml
mybatis-plus.configuration.aggressive-lazy-loading=false
mybatis-plus.configuration.lazy-loading-enabled=true
mybatis-plus.global-config.banner=false
##自动驼峰命名转换
mybatis.configuration.mapUnderscoreToCamelCase=true
spring.h2.console.enabled=false
logging.level.root=debug
代码
@Mapper
public interface FaceSheetRewriteLogMapper extends BaseMapper<FaceSheetRewriteLog> {
}
@Service
@AllArgsConstructor
public class MyService {
private final FaceSheetRewriteLogMapper faceSheetRewriteLogMapper;
private final JdbcTemplate jdbcTemplate;
@Transactional(rollbackFor = Throwable.class)
public Map<String , Object> save(FaceSheetRewriteLog log) {
faceSheetRewriteLogMapper.insert(log);
FaceSheetRewriteLog faceSheetRewriteLog = faceSheetRewriteLogMapper.selectById(log.getId());
Map<String , Object> payload = Maps.newHashMap();
payload.put("msg" , "ok");
payload.put("payload" , faceSheetRewriteLog);
return payload;
}
public Map<String , Object> getById(Serializable id) {
Map<String , Object> payload = Maps.newHashMap();
payload.put("msg" , "ok");
payload.put("log" , faceSheetRewriteLogMapper.selectById(id));
return payload;
}
}
调用 save 方法,在事务中为了保障及时性写在主库上 (write_ds),读数据依然会从主库 (write_ds) 中查询。
2023-05-04 19:08:49.433 INFO 20960 --- [nio-8080-exec-3] ShardingSphere-SQL : Logic SQL: INSERT INTO t_face_sheet_rewrite_log ( succeed_order_id_arr,
failed_order_id_arr,
count ) VALUES ( ?,
?,
? )
2023-05-04 19:08:49.435 INFO 20960 --- [nio-8080-exec-3] ShardingSphere-SQL : Actual SQL: write_ds ::: INSERT INTO t_face_sheet_rewrite_log ( succeed_order_id_arr,
failed_order_id_arr,
count ) VALUES ( ?,
?,
? ) ::: [[1 , 2 , 3], [4], 4]
2023-05-04 19:08:49.653 INFO 20960 --- [nio-8080-exec-3] ShardingSphere-SQL : Logic SQL: SELECT id,succeed_order_id_arr,failed_order_id_arr,count,created_time,last_update_time FROM t_face_sheet_rewrite_log WHERE id=?
2023-05-04 19:08:49.654 INFO 20960 --- [nio-8080-exec-3] ShardingSphere-SQL : Actual SQL: write_ds ::: SELECT id,succeed_order_id_arr,failed_order_id_arr,count,created_time,last_update_time FROM t_face_sheet_rewrite_log WHERE id=? ::: [7]
调用 getById 方法, 在我们配置的读库 (read_ds_0) 上进行查询。
2023-05-04 19:11:06.733 INFO 20960 --- [nio-8080-exec-6] ShardingSphere-SQL : Logic SQL: SELECT id,succeed_order_id_arr,failed_order_id_arr,count,created_time,last_update_time FROM t_face_sheet_rewrite_log WHERE id=?
2023-05-04 19:11:06.733 INFO 20960 --- [nio-8080-exec-6] ShardingSphere-SQL : Actual SQL: read_ds_0 ::: SELECT id,succeed_order_id_arr,failed_order_id_arr,count,created_time,last_update_time FROM t_face_sheet_rewrite_log WHERE id=? ::: [2]
这样我们就以最低的代价(代码层面)实现了读写分离,并可以为之后的数据分片做准备。
转载自:https://juejin.cn/post/7229272943313109048