likes
comments
collection
share

数据读写分离方案调研

作者站长头像
站长
· 阅读数 16

读写分离方案调研

数据读写分离方案调研

背景

随着数据量的快速增长,单台数据库实例的 TPS & QPS 都增长的很高,对数据库造成的压力比较大,特别是多个应用都连接同一个数据库实例的场景。虽然我们可以对数据库进行调优,比如针对 MySQL 提高最大连接数,增加 buffer pool size 的内存大小。但是随着数据量的持续增加这种针对单台实例的调优就会达到上限,这时候我们可能会选择购买性能配置更优的机器,但是始终还是受制于机器的性能配置。于是我们转变一下思路,可以从上述的垂直扩展的思路转换到水平扩展。采用分流的思想,将请求分发到多台数据库实例上来分摊压力。这样系统整体瓶颈不再受制于单点的数据库实例。而水平扩展让我们之后的扩展变得更为灵活,在我们规划好扩展策略后,随着数据的持续增加我们只需要增加新的数据库实例即可。虽然这个方案解决了我们的问题,但是也引入了新的问题,如在读写分离场景下数据同步的及时性问题。在数据分片场景下,垮数据库之间的事务问题。以及查询场景下数据的查询结果归并问题。

鉴于目前我们先不考虑对数据进行分片,所以这里只讨论读写分离的场景。

MySQL 主从复制

数据读写分离方案调研

为了提高系统整体可用性,降低由于单点故障导致的系统整体事故, MySQL 可以选择主从复制的方案,主服务器 Master 负责写,从服务器 Slave 负责读。

MySQL 主从复制过程

数据读写分离方案调研

binlog 格式

  • master 必须要开启 binlog 日志
  • 留意 binlog 日志的格式,ROW , MiXED , Statement
  • Statement 记录的是修改数据的 SQL 语句, 日志量小但是有主从数据不一致的风险(语句中使用了一些函数的场景)。
  • ROW 记录的是被修改的行数据,产生的日志量较大。
  • MiXEDStatementROW 的混合模式,一般语句修改使用 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]

这样我们就以最低的代价(代码层面)实现了读写分离,并可以为之后的数据分片做准备。