Redis-Cluster-的数据分片机制

上一篇《分布式数据缓存中的一致性哈希算法》文章中讲述了一致性哈希算法的基本原理和实现,今天就以 Redis Cluster 为例,详细讲解一下分布式数据缓存中的数据分片,上线下线时数据迁移以及请求重定向等操作。 Redis 集群简介 Redis Cluster 是 Redis 的分布式解决方案,在 3.0 版本正式推出,有效地解决了 Redis 分布式方面的需求。 Redis Cluster 一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群,其中三个为主节点,三个为从节点。三个主节点会分配槽,处理客户端的命令请求,而从节点可用在主节点故障后,顶替主节点。 图片来源 redislabs 如上图所示,该集群中包含 6 个 Redis 节点,3主3从,分别为M1,M2,M3,S1,S2,S3。除了主从 Redis 节点之间进行数据复制外,所有 Redis 节点之间采用 Gossip 协议进行通信,交换维护节点元数据信息。 一般来说,主 Redis 节点会处理 Clients 的读写操作,而从节点只处理读操作。 数据分片策略分布式数据存储方案中最为重要的一点就是数据分片,也就是所谓的 Sharding。为了使得集群能够水平扩展,首要解决的问题就是如何将整个数据集按照一定的规则分配到多个节点上,常用的数据分片的方法有:范围分片,哈希分片,一致性哈希算法,哈希槽等。 范围分片假设数据集是有序,将顺序相临近的数据放在一起,可以很好的支持遍历操作。范围分片的缺点是面对顺序写时,会存在热点。比如日志类型的写入,一般日志的顺序都是和时间相关的,时间是单调递增的,因此写入的热点永远在最后一个分片。 对于关系型的数据库,因为经常性的需要表扫描或者索引扫描,基本上都会使用范围的分片策略。 哈希分片和一致性哈希算法在上一篇文章中已经学习过了,感兴趣的同学可以去了解一下《分布式数据缓存中的一致性哈希算法》。我们接下来主要来看Redis 的虚拟哈希槽策略。 Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式:slot = CRC16(key) & 16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。 Redis 虚拟槽分区的特点: 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据支持节点、槽和键之间的映射查询,用于数据路由,在线集群伸缩等场景。 Redis 集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。可以说,槽是 Redis 集群管理数据的基本单位,集群伸缩就是槽和数据在节点之间的移动。 下面我们就先来看一下 Redis 集群伸缩的原理。然后再了解当 Redis 节点数据迁移过程中或者故障恢复时如何保证集群可用。 ...

May 25, 2019 · 2 min · jiezi

SpringBoot + MyBatisPlus + ShardingJDBC 分库分表读写分离整合

本文描述在本地数据库模拟分库分表、读写分离的整合实现,假定会员数据按照 ID 取模进行分库分表,分为 2 个主库,每个库分配一个读库,累计 100 张表。如下表所示:库主/从表user_1主t_user_00 ~ t_user_49user_slave_1从t_user_00 ~ t_user_49user_2主t_user_50 ~ t_user_99user_slave_2从t_user_50 ~ t_user_99本文主要展示核心代码,部分如 Controller、Service 层的测试代码实现非常简单,故而省略这部分代码。依赖版本<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc</artifactId> <version>3.0.0.M1</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version></dependency><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.41</version></dependency>数据准备use user_1;CREATE TABLE t_user_00 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_01 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_02 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_2;CREATE TABLE t_user_50 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_51 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_52 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_slave_1;CREATE TABLE t_user_00 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_01 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_02 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_slave_2;CREATE TABLE t_user_50 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_51 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_52 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;代码实现数据源配置server: port: 23333spring: application: name: pt-framework-demo datasource: type: com.alibaba.druid.pool.DruidDataSourcedatasource: default: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user: master: user1: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user2: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true slave: user1: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_slave_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user2: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_slave_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true主从、读写分离/** * Created by Captain on 01/03/2019. /@Configuration@MapperScan(basePackages = {“com.xxxx.framework.usermapper”}, sqlSessionFactoryRef = “userShardingSqlSessionFactory”)public class UserShardingDBConfiguration { @Value("${spring.datasource.type}") private Class<? extends DataSource> dataSourceType; private static final String USER_1_MASTER = “dsUser1Master”; private static final String USER_1_SLAVE = “dsUser1Slave”; private static final String USER_2_MASTER = “dsUser2Master”; private static final String USER_2_SLAVE = “dsUser2Slave”; private static final String USER_SHARDING_1 = “dsMasterSlave1”; private static final String USER_SHARDING_2 = “dsMasterSlave2”; private static final String USER_SHARDING_DATA_SOURCE = “userSharding”; @Bean(USER_1_MASTER) @ConfigurationProperties(prefix = “datasource.user.master.user1”) public DataSource dsUser1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_2_MASTER) @ConfigurationProperties(prefix = “datasource.user.master.user2”) public DataSource dsUser2(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_1_SLAVE) @ConfigurationProperties(prefix = “datasource.user.slave.user1”) public DataSource dsUserSlave1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } /* * user_2 * @return / @Bean(USER_2_SLAVE) @ConfigurationProperties(prefix = “datasource.user.slave.user2”) public DataSource dsUserSlave2(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_SHARDING_1) public DataSource masterSlave1(@Qualifier(USER_1_MASTER) DataSource dsUser1,@Qualifier(USER_1_SLAVE) DataSource dsUserSlave1) throws Exception { Map<String,DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(USER_1_MASTER, dsUser1); dataSourceMap.put(USER_1_SLAVE, dsUserSlave1); MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration(“dsUser1”, USER_1_MASTER, Lists.newArrayList(USER_1_SLAVE)); return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>()); } @Bean(USER_SHARDING_2) public DataSource masterSlave2(@Qualifier(USER_2_MASTER) DataSource dsUser2,@Qualifier(USER_2_SLAVE) DataSource dsUserSlave2) throws Exception { Map<String,DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(USER_2_MASTER, dsUser2); dataSourceMap.put(USER_2_SLAVE, dsUserSlave2); MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration(“dsUser2”, USER_2_MASTER, Lists.newArrayList(USER_2_SLAVE)); return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>()); } @Bean(USER_SHARDING_DATA_SOURCE) public DataSource dsUser(@Qualifier(USER_SHARDING_1) DataSource dsUser1, @Qualifier(USER_SHARDING_2) DataSource dsUser2) throws Exception { Map<String, DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(“dsUser1”, dsUser1); dataSourceMap.put(“dsUser2”, dsUser2); ShardingRuleConfiguration userRule = getUserRule(); userRule.setDefaultDataSourceName(“dsUser”); return ShardingDataSourceFactory.createDataSource(dataSourceMap, userRule, new ConcurrentHashMap<>(), new Properties()); } /* * 配置分片规则 * @return / private ShardingRuleConfiguration getUserRule(){ ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”, new MemberIdShardingSchemeAlgorithm())); shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”,new MemberIdShardingTableAlgorithm())); shardingRuleConfig.getBindingTableGroups().add(“t_user”); return shardingRuleConfig; } @Bean(“userShardingSqlSessionFactory”) public SqlSessionFactory userSqlSessionFactory(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource) throws Exception{ MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:usermapper/.xml”)); return sqlSessionFactoryBean.getObject(); } @Bean(“userTransaction”) public DataSourceTransactionManager userTransactionManager(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource){ return new DataSourceTransactionManager(dataSource); }}分库策略/** * CoreUser 分库策略 * Created by Captain on 01/03/2019. /public class MemberIdShardingSchemeAlgorithm implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { for ( String str : availableTargetNames ){ int index = shardingValue.getValue() % 100; return str + (index > 49 ? “2” : “1”); } return null; }}分表策略/* * 会员信息分表策略,按照 id 分表 * Created by Captain on 04/03/2019. /public class MemberIdShardingTableAlgorithm implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { int index = shardingValue.getValue() % 100; return shardingValue.getLogicTableName() + “_” + (index < 10 ? “0” + index : index + “”); }}实体类/* * Created by Captain on 01/03/2019. /@TableName(“t_user”)public class User { @TableId(type = IdType.INPUT) private Integer id; private String name; private Integer age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; }}Mapper/* * Created by Captain on 04/03/2019. */public interface UserMapper extends BaseMapper<User> {}测试预期模拟过程没有实际做主从同步,写入“主库”中的数据并不能自动同步至“从库”,因此,插入数据后,需要手动写入数据至对应的从库,并且可对数据进行差异写入,测试查询时可根据差异来判断读写分离是否生效。测试用例预期结果插入数据 id 指定为 8902user_1 中数据写入成功插入数据 id 指定为 8952user_2 中数据写入成功查询 id 为 8902 的数据查询到 user_slave_1 中的结果查询 id 为 8952 的数据查询到 user_slave_2 中的结果 ...

March 4, 2019 · 5 min · jiezi

“分库分表 ?选型和流程要慎重,否则会失控

更多文章关注微信公众号《小姐姐味道》 https://mp.weixin.qq.com/s?__…数据库中间件之分库分表恭喜你,贵公司终于成长到一定规模,需要考虑高可用,甚至分库分表了。但你是否知道分库分表需要哪些要素?拆分过程是复杂的,提前计划,不要等真正开工,各种意外的工作接踵而至,以至失控。本文意图打开数据库中间件的广度,而不考虑实现深度,至于库表垂直和水平分的概念和缘由,不做过多解释。所以此文面向的是有一定研发经验,正在寻找选型和拆分流程的专业人士。切入层次以下,范围界定在JAVA和MySQL中。我们首先来看一下分库分表切入的层次。① 编码层在同一个项目中创建多个数据源,采用if else的方式,直接根据条件在代码中路由。Spring中有动态切换数据源的抽象类,具体参见AbstractRoutingDataSource。如果项目不是很庞大,使用这种方式能够快速的进行分库。但缺点也是显而易见的,需要编写大量的代码,照顾到每个分支。当涉及跨库查询、聚合,需要循环计算结果并合并的场景,工作量巨大。如果项目裂变,此类代码大多不能共用,大多通过拷贝共享。长此以往,码将不码。② 框架层这种情况适合公司ORM框架统一的情况,但在很多情况下不太现实。主要是修改或增强现有ORM框架的功能,在SQL中增加一些自定义原语或者hint来实现。通过实现一些拦截器(比如Mybatis的Interceptor接口),增加一些自定义解析来控制数据的流向,效果虽然较好,但会改变一些现有的编程经验。很多情况要修改框架源码,不推荐。③ 驱动层基于在编码层和框架层切入的各种缺点,真正的数据库中间件起码要从驱动层开始。什么意思呢?其实就是重新编写了一个JDBC的驱动,在内存中维护一个路由列表,然后将请求转发到真正的数据库连接中。像TDDL、ShardingJDBC等,都是在此层切入。包括Mysql Connector/J的Failover协议(具体指“load balancing”、“replication”、“farbic”等),也是直接在驱动上进行修改。请求流向一般是这样的:④ 代理层代理层的数据库中间件,将自己伪装成一个数据库,接受业务端的链接。然后负载业务端的请求,解析或者转发到真正的数据库中。像MySQL Router、MyCat等,都是在此层切入。请求流向一般是这样的:⑤ 实现层SQL特殊版本支持,如Mysql cluster本身就支持各种特性,mariadb galera cluster支持对等双主,Greenplum支持分片等。需要换存储,一般是解决方案,就不在讨论之列了。技术最终都会趋于一致,选择任何一种、都是可行的。但最终选型,受开发人员熟悉度、社区活跃度、公司切合度、官方维护度、扩展性,以及公司现有的数据库产品等多方位因素影响。选择或开发一款合适的,小伙伴们会幸福很多。驱动层和代理层对比通过以上层次描述,很明显,我们选择或开发中间件,就集中在驱动层和代理层。在这两层,能够对数据库连接和路由进行更强的控制和更细致的管理。但它们的区别也是明显的。驱动层特点仅支持JAVA,支持丰富的DB驱动层中间件仅支持Java一种开发语言,但支持所有后端关系型数据库。如果你的开发语言固定,后端数据源类型丰富,推荐使用此方案。占用较多的数据库连接驱动层中间件要维护很多数据库连接。比如一个分了10个 库 的表,每个java中的Connection要维护10个数据库连接。如果项目过多,则会出现连接爆炸(我们算一下,如果每个项目6个实例,连接池中minIdle等于5,3个项目的连接总数是 1065*3 = 900 个)。像Postgres这种每个连接对应一个进程的数据库,压力会很大。数据聚合在业务实例执行数据聚合,比如count sum等,是通过多次查询,然后在业务实例的内存中进行聚合。路由表存在于业务方实例内存中,通过轮询或者被动通知的途径更新路由表即可。集中式管理所有集群的配置管理都集中在一个地方,运维负担小,DBA即可完成相关操作。典型实现代理层特点异构支持,DB支持有限代理层中间件正好相反。仅支持一种后端关系型数据库,但支持多种开发语言。如果你的系统是异构的,并且都有同样的SLA要求,则推荐使用此方案。运维负担大代理层需要维护数据库连接数量有限(MySQL Router那种粘性连接除外)。但作为一个独立的服务,既要考虑单独部署,又要考虑高可用,会增加很多额外节点,更别提用了影子节点的公司了。另外,代理层是请求唯一的入口,稳定性要求极高,一旦有高耗内存的聚合查询把节点搞崩溃了,都是灾难性的事故。典型实现共同点篇幅有限,不做过多讨论。访问各中间件宣传页面,能够看到长长的Feature列表,也就是白名单;也能看到长长的限制列表,也就是黑名单。限定了你怎么玩,在增强了分布式能力后,分库分表本身就是一个阉割的数据库。使用限制确保数据均衡 拆分数据库的数据尽量均匀,比如按省份分user库不均匀,按userid取模会比较均匀不用深分页 不带切分键的深分页,会取出所有库所取页数之前的所有数据在内存排序计算。容易造成内存溢出。减少子查询 子查询会造成SQL解析紊乱,解析错误的情况,尽量减少SQL的子查询。事务最小原则 尽量缩小单机事务涉及的库范围,即尽可能减少夸库操作,将同类操作的库/表分在一起数据均衡原则 拆分数据库的数据尽量均匀,比如按省份分user库不均匀,按userid取模会比较均匀特殊函数 distinct、having、union、in、or等,一般不被支持。或者被支持,使用之后会增加风险,需要改造。产品建议聚焦在MyCat和ShardingJDBC上。另外,还有大量其他的中间件,不熟悉建议不要妄动。数据库中间件不好维护,你会发现大量半死不活的项目。以下列表,排名不分先后,有几个是只有HA功能,没有拆分功能的:Atlas、Kingshard、DBProxy、mysql router、MaxScale、58 Oceanus、ArkProxy、Ctrip DAL、Tsharding、Youtube vitess、网易DDB、Heisenberg、proxysql、Mango、DDAL、Datahekr、MTAtlas、MTDDL、Zebra、Cobar、Cobar汗、几乎每个大厂都有自己的数据库中间件(还发现了几个喜欢拿开源组件加公司前缀作为产品的),只不过不给咱用罢了。流程解决方案无论是采用哪个层面切入进行分库分表,都面临以下工作过程。信息收集统计影响的业务和项目项目范围越大,分库难度越高。有时候,一句复杂的SQL能够涉及四五个业务方,这种SQL都是需要重点关注的。确定分库分表的规模,是只分其中的几张表,还是全部涉及。分的越多,工作量越大,几乎是线性的。还有一些项目是牵一发动全身的。举个例子,下面这个过程,影响的链路就不仅是分库这么简单了。确定参与人员除了分库分表组件的技术支持人员,最应该参与的是对系统、对现有代码最熟悉的几个人。只有他们能够确定哪些SQL该废弃掉、SQL的影响面等。确定分库分表策略确定分库分表的维度和切分键。切分键(就是路由数据的column)一旦确定,是不允许修改的,所以在前期架构设计上,应该首先将其确立下来,才能进行后续的工作;数据维度多意味着有不同的切分键,达到不同条件查询的效果。这涉及到数据的冗余(多写、数据同步),会更加复杂。前期准备数据规整库表结构不满足需求,需要提前规整。比如,切分键的字段名称不同或者类型各异。在实施分库分表策略时,这些个性会造成策略过大不好维护。扫描所有SQL将项目中所有的SQL扫描出来,逐个判断是否能够按照切分键正常运行。在判断过程中肯定会有大量不合规的SQL,则都需要给出改造方案,这是主要的工作量之一。验证工具支持直接在原有项目上进行改动和验证是可行的,但会遇到诸多问题,主要是效率太低。我倾向于首先设计一些验证工具,输入要验证的SQL或者列表,然后打印路由信息和结果进行判断。技术准备建议以下提到的各个点,都找一个例子体验一下,然后根据自己的团队预估难度。以下:中间件所有不支持的SQL类型整理容易造成崩溃的注意事项不支持的SQL给出处理方式考虑一个通用的主键生成器考虑没有切分键的SQL如何处理考虑定时任务等扫全库的如何进行遍历考虑跨库跨表查询如何改造准备一些工具集实施阶段数据迁移分库分表会重新影响数据的分布,无论是全量还是增量,都会涉及到数据迁移,所以Databus是必要的。一种理想的状态是所有的增删改都是消息,可以通过订阅MQ进行双写。但一般情况下,仍然需要去模拟这个状态,比如使用Canal组件。怎么保证数据安全的切换,我们分其他章节进行讨论。充足的测试分库分表必须经过充足的测试,每一句SQL都要经过严格的验证。如果有单元测试或者自动化测试工具,完全的覆盖是必要的。一旦有数据进行了错误的路由,尤其是增删改,将会创造大量的麻烦。在测试阶段,将验证过程输出到单独的日志文件,充足测试后review日志文件是否有错误的数据流向。SQL复验强烈建议统一进行一次SQL复验。主要是根据功能描述,确定SQL的正确性,也就是通常说的review。演练在非线上环境多次对方案进行演练,确保万无一失。制定新的SQL规范分库分表以后,项目中的SQL就加了枷锁,不能够随意书写了。很多平常支持的操作,在拆分环境下就可能运行不了了。所以在上线前,涉及的SQL都应该有一个确认过程,即使已经经过了充足的测试。题外话没有支持的活别接,干不成。分库分表是战略性的技术方案,很多情况无法回退或者回退方案复杂。如果要拆分的库表涉及多个业务方,公司技术人员复杂,CTO要亲自挂帅进行协调,并有专业仔细的架构师进行监督。没有授权的协调人员会陷入尴尬的境地,导致流程失控项目难产。真正经历过的人,会知道它的痛!

December 5, 2018 · 1 min · jiezi