一、分库分表操作主要有垂直拆分和水平拆分
垂直拆分: 按照业务将表分布到不同的数据库上,这样也就将数据的压力分担到不同的库上面。最终一个数据库由很多表的构成,每个表对应着不同的业务,也就是专库专用。
水平拆分: 垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而用户策略计算结果表需要实现的水平拆分,是把同一个表拆到不同的数据库中。
遇到下面几种场景可以考虑分库分表:
- 单表的数据达到千万级别以上,数据库读写速度比较缓慢。(一般B+树索引高度是2~3层最佳,如果数据量千万级别,可能高度就变4层了,性能就会明显变慢了。不过业界流传,一般500万数据就要考虑分表了。)
- 数据库中的数据占用的空间越来越大,备份时间越来越长。
- 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)。
项目中使用分库分表因为:
- 不能等到系统到了200万数据才拆,那样工作量会非常大。因为有成熟方案,所以前期就分库分表了。
- 但,为了解释服务器空间。所以把分库分表的库,用服务器虚拟出来机器安装。这样既不过多的占用服务器资源,也方便后续数据量真的上来了,好拆分。
- 同时,抽奖系统是瞬时峰值较高的系统,历史数据不一定多。因为抽奖这东西,push发完,基本就1~3分钟结束,10分钟人都没了。所以我们希望,用户可以快速的检索到个人数据,做最优响应。
主要用到的技术点包括:散列算法、数据源切换、AOP切面、SpringBoot Starter 开发等。
二、自研组件的原因:
- 维护性;市面的路由组件比如 shardingsphere 但过于庞大,还需要随着版本做一些升级。而我们需要更少的维护成本。
- 扩展性;结合自身的业务需求,我们的路由组件可以分库分表、自定义路由协议,扫描指定库表数据等各类方式。研发扩展性好,简单易用。
- 安全性;自研的组件更好的控制了安全问题,不会因为一些额外引入的jar包,造成安全风险。
三、设计实现 1)定义路由注解:
首先我们需要自定义一个注解,用于放置在需要被数据库路由的方法上。
它的使用方式是通过方法配置注解,就可以被我们指定的 AOP 切面进行拦截,拦截后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上。
2)解析路由配置
使用到 org.springframework.context.EnvironmentAware 接口,来获取配置文件并提取需要的配置信息。
使用时,需要在yml中进行多数据源路由配置。
3)数据源切换
在结合 SpringBoot 开发的 Starter 中,提供一个 DataSource 的实例化对象,将这个对象我们就放在 DataSourceAutoConfig 来实现,使得提供的数据源是可以动态变换的,也就是支持动态切换数据源。
4)切面拦截
在 AOP 的切面拦截中需要完成;数据库路由计算、扰动函数加强散列、计算库表索引、设置到 ThreadLocal 传递数据源。
l 首先我们提取了库表乘积的数量,把它当成 HashMap 一样的长度进行使用。
l 接下来使用和 HashMap 一样的扰动函数逻辑,让数据分散的更加散列。
l 当计算完总长度上的一个索引位置后,还需要把这个位置折算到库表中,看看总体长度的索引因为落到哪个库哪个表。
l 最后是把这个计算的索引信息存放到 ThreadLocal 中,用于传递在方法调用过程中可以提取到索引信息。
路由策略如下:
5)Mybatis 拦截器处理分表
最开始考虑直接在Mybatis对应的表 INSERT INTO user_strategy_export_${tbIdx} 添加字段的方式处理分表。但这样看上去并不优雅,不过也并不排除这种使用方式,仍然是可以使用的。
l 基于 Mybatis 拦截器进行处理,通过拦截 SQL 语句动态修改添加分表信息,再设置回 Mybatis 执行 SQL 中。
l 实现 Interceptor 接口的 intercept 方法,获取StatementHandler、通过自定义注解判断是否进行分表操作、获取SQL并替换SQL表名 USER 为 USER_03、最后通过反射修改SQL语句。
l 此处会用到正则表达式拦截出匹配的sql。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicMybatisPlugin implements Interceptor {
/**
* 正则表达式模式,用于匹配SQL语句中的表名。匹配from、into、update关键字后面的表名。
*/
private Pattern pattern = Pattern.compile("(from|into|update)[\s]{1,}(\w{1,})", Pattern.CASE_INSENSITIVE);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 获取自定义注解判断是否进行分表操作
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
Class<?> clazz = Class.forName(className);
DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
return invocation.proceed();
}
// 获取SQL
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// 替换SQL表名 USER 为 USER_03
Matcher matcher = pattern.matcher(sql);
String tableName = null;
if (matcher.find()) {
tableName = matcher.group().trim();
}
assert null != tableName;
String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());
// 通过反射修改SQL语句
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, replaceSql);
field.setAccessible(false);
return invocation.proceed();
}
}
四、AOP切面
在多数据源配置中,使用AOP(Aspect-Oriented Programming,面向切面编程)来拦截数据源选择是一个常见的做法。通过切面,你可以在方法执行前后进行动态的数据源切换,从而实现分库分表的需求。
实现步骤
- 创建一个持有当前数据源信息的上下文:
DBContextHolder,这是一个持有当前数据源信息的类,使用ThreadLocal来确保每个线程都有自己的数据源副本。 - 编写切面拦截类:
DBRouterJoinPoint,使用Spring AOP在方法执行前后进行数据源切换。 - 配置切面:包括定义切面类(使用
@Aspect注解标识,并用@Component注册成Spring的Bean)、定义切点(Pointcut) (使用@Pointcut注解定义哪些方法需要被切面拦截)、定义通知(Advice) (使用@Before,@After,@Around等注解在切点方法执行前后添加额外的逻辑)。
五、SpringBoot Starter开发 db-router-spring-boot-starter
Spring Boot Starter 是一组便捷的依赖描述符,提供了一种简化配置的方式,使开发者能够快速将这些功能集成到 Spring Boot 项目中。
特点:
- 自动配置与依赖管理: 每个 Starter 都对应一个功能或技术栈,结合Spring Boot的自动配置功能,Starters能够自动配置Spring组件,无需手动配置和管理多个相关依赖项。
- 快速入门:使开发人员可以专注于业务逻辑,而不必花大量时间在项目的初始化配置上。
- 模块化:涵盖了从Web开发、数据访问、消息传递到安全性等不同的领域。开发人员可以根据需要选择相应的Starter。
步骤:
- 创建项目:
创建一个新的Maven项目,并设置基本的项目结构和Pom文件。 - 配置pom.xml:
在pom.xml中添加必要的依赖,并正确配置了项目的打包方式。 - 创建自动配置类:
编写一个配置信息类,使用@Configuration注解,并定义需要初始化的Bean。 - 创建业务逻辑:
在项目中编写你希望封装的核心代码逻辑。 - 创建Spring Factories文件:
在src/main/resources/META-INF目录下创建spring.factories文件,并将自动配置类添加进去。
PS: 在用户领取活动中,扣减个人参与次数和插入领取活动信息需要在同一个事务下,连续操作不同的DAO操作,那么就会涉及到在 DAO 上使用注解 @DBRouter(key = "uId") 反复切换路由的操作。虽然都是一个数据源,但这样切换后,事务就没法处理了。
这里选择了一个较低的成本的解决方案,就是把数据源的切换放在事务处理前,而事务操作也通过编程式编码进行处理。