需求设计
随着业务体量的增加,原有的技术设计无法满足现有的需求和规模,分库分表是一种重要的数据库优化技术,通过将数据分散到多个数据库和表中,可以显著提高系统的性能、扩展性、可用性和数据安全性。在设计大型系统时,分库分表是一个值得考虑的重要策略。
方案设计
使用Springboot进行开发,基于aop,Spring动态数据源切换,扰动函数实现整体功能。
实现
读取配置信息
编写DataSourceAutoConfig类,它实现了 EnvironmentAware 接口,
这意味着 Spring 会在配置类被初始化时通过setEnvironment方法注入当前的 Environment,允许类访问配置文件中的属性。
@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {
//创建一个map集合存放数据源
private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();
private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();
private int dbCount; //分库数
private int tbCount; //分表数
//实例化一个DBRouterConfig并返回读取到的dbCount,tbCount值
@Bean
public DBRouterConfig dbRouterConfig() {
return new DBRouterConfig(dbCount, tbCount);
}
//通过 setEnvironment 方法读取配置文件中的属性。
@Override
public void setEnvironment(Environment environment) {
String prefix = "router.jdbc.datasource.";
//读取 dbCount 和 tbCount 作为数据库路由的配置信息
//2和4
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
//db01 ,db02
String dataSources = environment.getProperty(prefix + "list");
for (String dbInfo : dataSources.split(",")) {
Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
dataSourceMap.put(dbInfo, dataSourceProps);
}
}
}
//被读取的application.yml
# 路由配置
router:
jdbc:
datasource:
dbCount: 2
tbCount: 4
list: db01,db02
db01:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/cool_01?useUnicode=true
username: root
password: 123456
db02:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/cool_02?useUnicode=true
username: root
password: 123456
通过以上方法的编写,配置文件中的数据库被读取,并存入dataSourceMap中。
创建数据源
通过上一步获取到的数据库配置,编写new DriverManagerDataSource()用来创建数据源,再放入DynamicDataSource类中,这个类继承了AbstractRoutingDataSource,完成数据源s
@Bean
public DataSource dataSource() {
// 根据读取的多个数据库信息创建数据源
Map<Object, Object> targetDataSources = new HashMap<>();
for (String dbInfo : dataSourceMap.keySet()) {
Map<String, Object> objMap = dataSourceMap.get(dbInfo);
//取到了数据源的url,userName,password等信息。
targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));
}
// 设置数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//DynamicDataSource继承了AbstractRoutingDataSource,这样完成了多个数据源的映射。
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
//DynamicDataSource类,继承了AbstractRoutingDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
}
编写注解
@Documented
@Retention(RetentionPolicy.RUNTIME)// 表示该注解的生命周期将保留到运行时,也就是说,在运行时可以通过反射机制获取并使用该注解。
@Target({ElementType.TYPE, ElementType.METHOD}) //表示该注解可以应用于类和方法上。
public @interface DBRouter {
String key() default "";
}
基于HashMap实现
通过aop切面拦截注解的方式,实现将使用注解的方法拦截,获取key值,对其进行扰动函数的计算,算出库表各自的索引,存入ThreadLocal中。ThreadLocal 是一种线程局部变量的实现,它为每个线程提供了独立的副本。在本例中,DBContextHolder 使用 ThreadLocal 来为每个线程存储表索引。
@Around("aopPoint() && @annotation(dbRouter)")
//定义了一个环绕通知方法 doRouter,它将在匹配的方法执行前后执行。
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
String dbKey = dbRouter.key();
if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");
// 计算路由
//deKeyAttr代表的就是UserId
String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 库表索引
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
// 设置到 ThreadLocal
DBContextHolder.setDBKey(String.format("%02d", dbIdx));
DBContextHolder.setTBKey(String.format("%02d", tbIdx));
logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);
// 返回结果
try {
return jp.proceed();
} finally {
DBContextHolder.clearDBKey();
DBContextHolder.clearTBKey();
}
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
public String getAttrValue(String attr, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
//BeanUtils.getProperty(arg, attr) 方法尝试从 arg 对象中获取名为 attr 的属性值。
//举例,此时arg 就是 user 对象,attr 就是 "userId"。
if (StringUtils.isNotBlank(filedValue)) break;
filedValue = BeanUtils.getProperty(arg, attr);
} catch (Exception e) {
logger.error("获取路由属性值失败 attr:{}", attr, e);
}
}
return filedValue;
}
分库分表索引计算
为什么要使用扰动函数计算?因为hashCode太大了,hashCode的取值范围是[-2147483648, 2147483647],数组初始化做不到这么大,内存也放不下。在此组件中我通过一次扰动计算,(dbKeyAttr.hashCode() >>> 16)。把哈希值右移16位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。再通过 (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16))进行与运算得到了idx,而此处idx的最大值为 size-1。如果 size 为 8,那么 idx 的最大值为 7。如果 size 为 16,那么 idx 的最大值为 15。
那么分库分表的索引怎么计算得来的呢?以八个表为例:把计算出的索引位置 idx 分摊到8个表中,这个时候不需要在使用其他模数,只需要按照HashMap的数据结构分散到库表中即可。 2. 比如:idx = 3,那么它就是在1库到3表、idx = 7 那么它就是在2库的3表,因为1库4个+2库3个正好是7 。
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 数据库,表索引的计算,基于idx
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
数据源选择
当进行数据库操作时,上一步中计算好的库表索引就派上用场了,
当一个带有 DBRouter 注解的方法被调用时,aop会拦截方法,根据注解的配置计算出当前请求需要使用的数据库索引,并将其设置到 DBContextHolder 中。当需要执行数据库操作时,DynamicDataSource 的 determineCurrentLookupKey 方法会被调用。根据 DBContextHolder 中的数据库索引, determineCurrentLookupKey 方法返回一个键,如 "db01"。
AbstractRoutingDataSource 会根据返回的键从 targetDataSources 映射中选择对应的数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//获取数据库索引
return "db" + DBContextHolder.getDBKey();
}
}
同时mybatis也通过DBContextHolder获取到tbIdx数据表的具体索引并进行具体的操作
<insert id="insertUser" parameterType="cn.bugstack.middleware.test.infrastructure.po.User">
insert into user_${tbIdx} (id, userId, userNickName, userHead, userPassword,createTime, updateTime)
values (#{id},#{userId},#{userNickName},#{userHead},#{userPassword},now(),now())
</insert>
基于以上的方法实现,就可以成功通过注解实现动态的分库分表