分库分表

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

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

在这里插入图片描述

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

在这里插入图片描述

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

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_KaTeX parse error: Expected group after '_' at position 83: …a-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" ?>
<sql id="Base_Column_List">
    id, number, createTime
</sql>

<insert id="insert">
    INSERT INTO user (
      id, number
    )
    VALUES (
        uuid(),
        #{number,jdbcType=INTEGER}
    )
</insert>
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:

在这里插入图片描述
在这里插入图片描述

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

分库之后在查询方面要比之前更加谨慎,既然按策略去切了,那最好就是按策略去查,否则…比如我水平切分了 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();
}
在这里插入图片描述

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

  1. 主从与读写分离
    3.1 概述
    为什么要做主从?我们先来探讨以下这几个场景:

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

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, 从编排配置文件到最后启动服务整个过程不到一分钟:

在这里插入图片描述

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

docker exec -it cluster_mysql-master mysql -p

CREATE USER ‘reader’@’%’ IDENTIFIED BY ‘youclk’;
GRANT REPLICATION SLAVE ON . TO ‘reader’@’%’;

show master status\G
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 status\G
Test:
在这里插入图片描述

上图中左边连的是 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:
在这里插入图片描述

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值