必赢365net手机版简单总结,分库分表

必赢365net手机版 9

前言

我在上篇博客 “Spring Boot
的实践与思考”
中比对不同规范的 ORM
框架应用场景的时候提到过主从与读写分离,本篇随笔将针对此和分库分表进行更深入地探讨。
必赢365net手机版 1

前言

常用的数据有oracle和mysql;

oracle费用高,性能高,一个oracle相当于10到30个mysql;

但是面临海量数据,oracle仍不够,分库分表的难度大;

分库分表针对于mysql;

 

解决性能问题,需将数据或操作分离,mysql官方提供了读写分离的插件:proxyg;

读写分离,日志同步,解决了高并发问题,单没有解决高可用;

 

1. 漫谈

在进入正题之前,我想先随意谈谈对架构的拓展周期的想法(仅个人观点)。首先,我认为初期规划不该太复杂或者庞大,无论项目的中长期可能会发展地如何如何,前期都应该以灵活为优先,像分库分表等操作不应该在开始的时候就考虑进去。其次,我认为需求变更是非常正常的,这点在我等开发的圈子里吐槽的最多,其中自然有
“领导们”
在业务方面欠缺整体考虑的因素,但我们也不该局限在一个观点内,市场中变则通,不变则死,前期更是如此,因此在前几版的架构中我们必须要考虑较高的可扩展性。最后,当项目经过几轮市场的洗礼和迭代开发,核心业务趋于稳定了,此时我们再结合中长期的规划给系统来一次重构,细致地去划分领域边界,该解耦的解耦,该拆分的拆分。

必赢365net手机版,单库单表

单库单表是最常见的数据库设计,例如,有一张用户(user)表放在数据库db中,所有的用户都可以在db库中的user表中查到。

 

2. 分库分表

单库多表

随着用户数量的增加,user表的数据量会越来越大,当数据量达到一定程度的时候对user表的查询会渐渐的变慢,从而影响整个DB的性能。如果使用mysql, 还有一个更严重的问题是,当需要添加一列的时候,mysql会锁表,期间所有的读写操作只能等待。

可以通过某种方式将user进行水平的切分,产生两个表结构完全一样的user_0000,user_0001等表,user_0000

  • user_0001 + …的数据刚好是一份完整的数据。

 

2.1 概述

当数据库达到一定规模后(比如说大几千万以上),切分是必须要考虑的。一般来说我们首先要进行垂直切分,即按业务分割,比如说用户相关、订单相关、统计相关等等都可以单独成库。图片来源

必赢365net手机版 2

但仅仅如此这是完全不够的,垂直切分虽然剥离了一定的数据,但每个业务还是那个数量级,因此我们还得采取水平切分进一步分散数据,这也是本节论述的重点。
必赢365net手机版 3

分库分表的优点相信上述两图都一目了然了,一个是专库专用,业务更集中,另一个是提升数据库服务的负载能力。But
there are always two sides to a coin。
从此以后你要接受你的系统复杂度将提升一个档次,迭代、迁移、运维等都不再容易。

多库多表

         随着数据量增加也许单台DB的存储空间不够,随着查询量的增加单台数据库服务器已经没办法支撑。这个时候可以再对数据库进行水平区分。

 

2.2 切分策略

垂直切分在实现上就是一个多数据源的问题,没啥好讲的。以下 Demo
为水平切分,基于 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: 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。

User:

@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:
必赢365net手机版 4
必赢365net手机版 5

以上做了一个简单的循环插入,可以看到数据已经按策略分库存储,结果符合我们的预期。

分库之后在查询方面要比之前更加谨慎,既然按策略去切了,那最好就是按策略去查,否则…比如我水平切分了
100个库,若不按策略去查询 LIMIT 100000, 10
这么一组数据,那最后扫描的数量级别是 100 * (100000 + 10)
这是比较恐怖的,虽然 Sharding-JDBC
做了一些优化,比如他不是一次性去查询到内存中,而是采用流式处理 +
归并排序的方式,但仍然比较耗资源,能避免还是尽量去避免吧。

分库分表规则

         设计表的时候需要确定此表按照什么样的规则进行分库分表。例如,当有新用户时,程序得确定将此用户信息添加到哪个表中;同理,当登录的时候我们得通过用户的账号找到数据库中对应的记录,所有的这些都需要按照某一规则进行。

2.3 分布式事务

在任何系统中事务都是顶要紧的事情,面对已分库的系统更是如此,保证夸库事务的安全从来不容易。分布式事务的场景有两种,一个是在分布式服务中,这个后续有机会再探讨,本节重点关注夸库事务。

Sharding-JDBC
自动包含了弱XA事务支持,即能够保证逻辑上的事务安全,但因网络或硬件导致的异常无法回滚,实现上与一般事务无异:

@Test
@Transactional
public void insertTest() {
    userService.insert();
    int error = Integer.parseInt("I want error");
    userService.insert();
}

必赢365net手机版 6

可以看到夸库事务已回滚,除此之外 Sharding-JDBC
还提供了最大努力送达型柔性事务(将执行过程记录到日志中,失败重试,成功后删除,若最终还是失败则保留事务日志,供人工干预),虽然安全性更高,但无法保证时效,限制也很多,这里留个待续吧,后续有空再深入探讨(主要是比较晚了,想早点写完休息😁)。

路由

         通过分库分表规则查找到对应的表和库的过程。如分库分表的规则是user_id
mod 4的方式,当用户新注册了一个账号,账号id的123,我们可以通过id mod
4的方式确定此账号应该保存到User_0003表中。当用户123登录的时候,我们通过123
mod 4后确定记录在User_0003中。

3. 主从与读写分离

分库分表产生的问题,及注意事项

  1.   分库分表维度的问题

假如用户购买了商品,需要将交易记录保存取来,如果按照用户的纬度分表,则每个用户的交易记录都保存在同一表中,所以很快很方便的查找到某用户的购买情况,但是某商品被购买的情况则很有可能分布在多张表中,查找起来比较麻烦。反之,按照商品维度分表,可以很方便的查找到此商品的购买情况,但要查找到买人的交易记录比较麻烦。

 

所以常见的解决方式有:

     a.通过扫表的方式解决,此方法基本不可能,效率太低了。

     b.记录两份数据,一份按照用户纬度分表,一份按照商品维度分表。

     c.通过搜索引擎解决,但如果实时性要求很高,又得关系到实时搜索。

 

  1.   联合查询的问题

联合查询基本不可能,因为关联的表有可能不在同一数据库中。

 

  1.   避免跨库事务

避免在一个事务中修改db0中的表的时候同时修改db1中的表,一个是操作起来更复杂,效率也会有一定影响。

 

  1.   尽量把同一组数据放到同一DB服务器上

例如将卖家a的商品和交易信息都放到db0中,当db1挂了的时候,卖家a相关的东西可以正常使用。也就是说避免数据库中的数据依赖另一数据库中的数据。

 

3.1 概述

为什么要做主从?我们先来探讨以下这几个场景:

  • 我们知道每台数据库服务器有他的最大连接数和
    IOPS,若有一天他无法再满足我们的业务需求,那相比于在单台服务器上去做性能堆叠,是是否横向去扩展几台
    Slave 去分担 Master 的压力更加合理。
  • 如果服务对数据库的需求是 IO
    密集型的,那可能会经常遇到行锁等待等问题,若要鱼与熊掌兼得,读写分离是否是更好的选择。
  • 如果我们的系统需要做很多报表,或者统计和数据分析,这些业务往往相当地耗费资源但又不是很重要,那针对此,我们是否应该开几台
    Slave,让他们去小黑屋里慢慢执行,别来影响我处理核心业务的效率。

我大致能想到这么几点,欢迎各位继续留言补充。

一主多备

在实际的应用中,绝大部分情况都是读远大于写。Mysql提供了读写分离的机制,所有的写操作都必须对应到Master,读操作可以在Master和Slave机器上进行,Slave与Master的结构完全一样,一个Master可以有多个Slave,甚至Slave下还可以挂Slave,通过此方式可以有效的提高DB集群的QPS.                                                       

所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

此外,可以看出Master是集群的瓶颈,当写操作过多,会严重影响到Master的稳定性,如果Master挂掉,整个集群都将不能正常工作。

所以,1. 当读压力很大的时候,可以考虑添加Slave机器的分式解决,但是当Slave机器达到一定的数量就得考虑分库了。 2. 当写压力很大的时候,就必须得进行分库操作。

 

         另外,可能会因为种种原因,集群中的数据库硬件配置等会不一样,某些性能高,某些性能低,这个时候可以通过程序控制每台机器读写的比重,达到负载均衡。

 

3.2 主从部署

我以 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, 从编排配置文件到最后启动服务整个过程不到一分钟:
必赢365net手机版 7

接下来就是配置主从关系:

docker exec -it cluster_mysql-master mysql -p

CREATE USER 'reader'@'%' IDENTIFIED BY 'youclk';
GRANT REPLICATION SLAVE ON *.* TO 'reader'@'%';

show master statusG

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 statusG

Test:
必赢365net手机版 8
上图中左边连的是 Master, 右边为 Slave, 我在 Master 中执行
create database youclk_0; 可以看到 Slave 中也生成了
youclk_0,至此主从配置测试完成。

3.3 读写分离

基于 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:
必赢365net手机版 9

跟踪 MySQL 的日志可以发现主从库分别执行了插入与查询,实现了读写分离。

Leave a Comment.