分库分表理论
常见问题
为什么要分库分表?
- 我们分库分表用的非常熟。但不能为了等到系统到了200万数据,才拆。那么工作量会非常大
- 我们的做法是,因为有成熟方案,所以前期就分库分表了。但,为了解释服务器空间。所以把分库分表的库,用服务器虚拟出来机器安装。这样即不过多的占用服务器资源,也方便后续数据量真的上来了,好拆分。
- 同时,抽奖系统,是瞬时峰值较高的系统,历史数据不一定多。所以我们希望,用户可以快速的检索到个人数据,做最优响应。因为大家都知道,抽奖这东西,push发完,基本就1~3分钟结束,10分钟人都没了。所以我们这也是做了分库分表的理由。
Q1:为什么要引入分库分表路由组件? 在分布式应用中,分库分表是一种非常常用的数据存储方案。它的好处在于
- 可以将数据和请求分散到多个数据库实例中,从而分摊负载,提高性能。
- 如果后期应用数据量和访问量上升时,可以容易进行水平扩展。
- 避免单表数据容量过大,影响查询效率
- 可以定义规则进行数据分片和路由,简化数据管理和维护
Q2 我早期没有那么多的数据库资源,有必要分库分表吗?
- 对于早期上线的系统而言,如果评估以后没有那么大的体谅,可以使用虚拟机的方案安装数据库。
- 当然暂时不分库分表也是可以的,因为这也是节省资源和成本的一种方式。
- 但是早期设计为单库单表的,那么后期再想扩展为分库分表则会有非常大的数据迁移和工程改造成本。
Q3:它的查询逻辑是什么样的?(它是如何路由的?)
- 数据库操作针对于 用户类行为,根据 「用户 id」 进行路由,将数据分配到 x 库 y 表中
- 注解切面拦截,拦截字段 「用户 id」
- 通过获取「用户 ID(userId)」值进行 哈希索引计算。哈希值 & 2 从 n 次幂数量的库表 - 1 得到一个值,在根据这个值计算应该分配到哪个库表上去。
- threadlocal 存储 分库分表值 5. 在真正的数据操作执行之前,获取「路由」,并通过 「mybatis plugin」得到分表
Sharding-JDBC
概述
官方定位: ShareSphere-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
说人话: sharding-jdbc是以jar的形式,在本地应用层重写的jdbc原生的方法,实现数据库分片形式,主要是基于AOP原理,在webapp本地进行sql的拦截,解析,改写,路由和结果归集处理。 它直接嵌入我们的应用程序之中,基于JDBC对上层服务提供分库分表以及路由的能力,(对比另一个分库分表组件mycat,mycat是独立于服务之外的一个数据库中间件,类似于tomcat容器或相关的web中间件。)
技术选型
参考文章:Mycat VS ShardingSphere 为什么选用sharding-jdbc而不是mycat?
数据库分库分表中间件,常用的就是mycat和shardingsphere了。两者的定位也是不一样的,一个本地拦截处理(sharding-jdbc),一个是服务器端拦截处理(mycat)。
Mycat
MyCat 属于服务器端的数据库中间件,可以**通过代码直连数据库**,通过改写SQL分发,以保证数据的安全。它是**基于Proxy**,它复写了MyCat协议,将MyCat server**伪装成一个 MyCat 数据库**。它的这种操作,就会导致它的**效率偏低,损耗略高**,**mycat只能多库单表**,对于本项目来说使用Sharding-jdbc实现多分库分表的场景更为适用。 不过,MyCat无须修改代码,很方便!
ShardingSphere
它是一个本地数据库中间件框架,是以jar的形式,在本地应用层重写的jdbc原生的方法,实现数据库分片形式,主要是基于AOP原理,在程序层面进行sql的拦截,解析,改写,路由和结果归集处理。效率高,可是使用Sharding-jdbc时需要修改代码,对应用入侵性比较强。
sharding分库分表过程
![[Pasted image 20241011185216.png]]
SQL解析
SQL优化
SQL路由
SQL改写
执行
结果归并
Sharding-jdbc分片算法
参考文章:
事务管理
参考文章:
事务管理器类型
#### LOCAL 本地事务管理,适用于单个数据源的操作,不支持分布式事务。
本地事务方式也就是使用Spring的@Transaction注解来进行配置。传统的本地事务是不具备分布式事务特性的,但是ShardingSphere对本地事务进行了增强。在ShardingSphere中,LOCAL本地事务已经完全支持由于逻辑异常导致的分布式事务问题。不过这种本地事务模式IBU支持因网络、硬件导致的跨库事务。例如同一个事务中,跨两个库更新,更新完毕后,提交之前,第一个库宕机了,则只有第二个库数据提交。
本地事务原理默认本地事务是在不开启任何分布式事务管理器的前提下,让每个数据节点各自管理自己的事务。 它们之间没有协调以及通信的能力,也并不互相知晓其他数据节点事务的成功与否。 本地事务在性能方面无任何损耗,但在强一致性以及最终一致性方面则力不从心。开启事务管理器后,shardingJDBC采取其实是一种弱XA事务,也是采用二阶段提交方式。
【为什么说local事务管理是一种弱XA型事务呢?】 可以看到在二阶段提交过程中对于抛错只是进行catch处理,所以对于服务器宕机或者是网络原因引起的提交失败,本地事务无法处理,可以看到官网介绍LOCAL事务管理特征:
- 完全支持非跨库事务,例如:仅分表,或分库但是路由的结果在单库中。
- 完全支持因逻辑异常导致的跨库事务。例如:同一事务中,跨两个库更新。更新完毕后,抛出空指针,则两个库的内容都能回滚。
- 不支持因网络、硬件异常导致的跨库事务。例如:同一事务中,跨两个库更新,更新完毕后、未提交之前,第一个库死机,则只有第二个库数据提交。
![[Pasted image 20241012203045.png]]
XA
支持分布式事务,适用于需要跨多个数据源进行原子性操作的场景。通过XA协议实现两阶段提交。 todo
BASE柔性事务
一种最终一致性模型,适用于对性能要求较高的场景。它不保证立即一致性,而是通过异步方式确保最终一致性。 todo
使用方式
- 项目添加适合依赖(分布式事务需要) ```xml
2. 配置事务管理器(本地事务不需要)
```java
@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {
@Bean
public PlatformTransactionManager txManager(final DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
//如果不使用jdbctemplate就可以不注入。
@Bean
public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
- 在需要事务的方法或类中直接添加注解相关注解即可。注意:
@ShardingTransactionType
需要同Spring的@Transactional
配套使用,事务才会生效。@ShardingTransactionType(TransactionType.LOCAL) @Transactional
或分布式XA事务
@ShardingTransactionType(TransactionType.XA) @Transactional
自研分库分表路由组件
参考:
- xfg知识星球
- 散列算法答疑
- 面试答疑
设计思路
- 自定义注解
- 定义分库分表配置信息,定义数据源信息
- 使用
EnvironmentAware
读取配置文件中的数据源信息 - 动态生成数据源
- AOP切面拦截
- 拦截
@dbRouter
注解的方法,获取user_id(dbKey) - 在切面中进行库表匹配计算,将定位到的库表信息存入ThreadLocal中
- 拦截
- Mybatis拦截器拦截,修改SQL语句
- 判断是否为自定义注解方法,即是否需要改写该SQL,不是则放行
- 如果是需要分库分表的语句则使用反射进行修改SQL语句
![[Pasted image 20241011191231.png]]
具体实现
在参考文章中
项目实现(使用Sharding-JDBC)
参考文章:
分库分表规则
目的
本项目中对两张表进行水平分库,一张表进行分库分表。 实现对表user_take_activity(用户参与活动记录表)、user_take_activity_count(用户参与次数表)和user_strategy_export(用户抽奖结果表,每个用户同一次活动后不同的抽奖的记录都会存在该表中)的CRUD操作。 ![[Pasted image 20241011191702.png]]
实现
- 准备两个数据库lottery0,lottery1。两个数据源lottery0,lottery1。
- 每个数据库下方都有user_take_activity0和user_take_activity1即可。
- 数据库规则,uid对2取模选择放入0、1库中。
- 数据表规则:uid取模结果为0放入lottery0库,其余放入lottery1库。
路由部分配置部分
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 参数配置,显示sql
props:
sql:
show: true
sharding:
# 默认数据源,主要用于写,注意一定要配置读写分离 ,注意:如果不配置,那么就会把三个节点都当做从slave节点,新增,修改和删除会出错。
default-data-source-name: lottery
# 配置分表的规则
tables:
# -----------------仅分库------------------
# user_take_activity 逻辑表名
user_take_activity:
# 数据节点:数据源$->{0..N}.逻辑表名$->{0..N}
actual-data-nodes: lottery$->{0..1}.user_take_activity
# 拆分库策略,也就是什么样子的数据放入放到哪个数据库中。
database-strategy:
inline:
sharding-column: uid # 分片字段(分片键)
algorithm-expression: lottery$->{uid % 2} # 分片算法表达式
# -----------------仅分库------------------
# user_take_activity_count 逻辑表名
user_take_activity_count:
# 数据节点:数据源$->{0..N}.逻辑表名$->{0..N}
actual-data-nodes: lottery$->{0..1}.user_take_activity_count
# 拆分库策略,也就是什么样子的数据放入放到哪个数据库中。
database-strategy:
inline:
sharding-column: uid # 分片字段(分片键)
algorithm-expression: lottery$->{uid % 2} # 分片算法表达式
# -----------------分库分表------------------
# user_strategy_export 逻辑表名
user_strategy_export:
# 数据节点:数据源$->{0..N}.逻辑表名$->{0..N}
actual-data-nodes: lottery$->{0..1}.user_strategy_export$->{0..3}
# 拆分库策略,也就是什么样子的数据放入放到哪个数据库中。
database-strategy:
inline:
sharding-column: uid # 分片字段(分片键)
algorithm-expression: lottery$->{uid % 2} # 分片算法表达式
# 拆分表策略,也就是什么样子的数据放入放到哪个数据表中。
table-strategy:
inline:
sharding-column: uid # 分片字段(分片键)
algorithm-expression: user_strategy_export$->{uid % 2} # 分片算法表达式
数据源配置部分
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 参数配置,显示sql
props:
sql:
show: true
# 配置数据源
datasource:
# 给每个数据源取别名,下面的lottery_01,lottery_02任意取名字
names: lottery1,lottery2
# 给master-ds1每个数据源配置数据库连接信息
lottery0:
# 配置druid数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.115.94.78:3306/lottery_01?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置ds1-slave
lottery1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://114.215.145.201:3306/lottery_02?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置默认数据源ds0
sharding:
# ...
# 整合mybatis的配置XXXXX
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.lottery.shardingjdbc.entity