MyBatis拦截器分表
西瓜猿 5/29/2024 工作工作经验
MyBatis拦截器分表-优化数据库性能
# 背景
部门内有一些亿级别核心业务表增速非常快,增量日均100W,但线上业务只依赖近一周的数据。随着数据量的迅速增长,慢SQL频发,数据库性能下降,系统稳定性受到严重影响。本篇文章,将分享如何使用MyBatis拦截器低成本的提升数据库稳定性。
# 大表业界常见处理方案
针对冷数据多的大表,常用的策略有以2种:
删除/归档旧数据。
分表。
# 归档/删除旧数据
定期将冷数据移动到归档表或者冷存储中,或定期对表进行删除,以减少表的大小。此策略逻辑简单,只需要编写一个JOB定期执行SQL删除数据。我们开始也是用这种方案,但此方案也有一些副作用:
数据删除会影响数据库性能,引发慢sql,多张表并行删除,数据库压力会更大。
频繁删除数据,会产生数据库碎片,影响数据库性能,引发慢SQL。
综上,此方案有一定风险,为了规避这种风险,我们决定采用另一种方案:分表。
# 分表方案
考虑数据增长方式和区分,我们决定按日期对表进行横向拆分,实现让系统每周生成一张周期表,表内只存近一周的数据,规避单表过大带来的风险。
# 分表方案选型
经调研,考虑两种分表方案:Sharding-JDBC
、利用Mybatis
自带的拦截器
。
经过对比后,决定采用Mybatis拦截器来实现分表,原因如下:
1.JAVA
生态中很常用的分表框架是Sharding-JDBC
,虽然功能强大,但需要一定的接入成本,并且很多功能暂时用不上。
2.系统本身已经在使用Mybatis
了,只需要添加一个Mybatis
的拦截器,把SQL表名替换为新的周期表就可以了,没有接入新框架的成本,开发成本也不高。
# 分表具体实现代码
分表配置对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShardingProperty {
// 分表周期天数,配置7,就是7一周一分
private Integer days;
// 分表开始日期,需要用这个日期计算周期表名
private Date beginDate;
// 需要分表的表名
private String tableName;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分表配置类
import java.util.concurrent.ConcurrentHashMap;
public class ShardingPropertyConfig {
public static final ConcurrentHashMap<String, ShardingProperty> SHARDING_TABLE = new ConcurrentHashMap<>();
static {
ShardingProperty orderInfoShardingConfig = new ShardingProperty(15, DateUtils.string2Date("20231117"), "order_info");
ShardingProperty userInfoShardingConfig = new ShardingProperty(7, DateUtils.string2Date("20231117"), "user_info");
SHARDING_TABLE.put(orderInfoShardingConfig.getTableName(), orderInfoShardingConfig);
SHARDING_TABLE.put(userInfoShardingConfig.getTableName(), userInfoShardingConfig);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
拦截器
@Slf4j
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class ShardingTableInterceptor implements Interceptor {
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
private static final String MAPPED_STATEMENT = "delegate.mappedStatement";
private static final String BOUND_SQL = "delegate.boundSql";
private static final String ORIGIN_BOUND_SQL = "delegate.boundSql.sql";
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private ConfigUtils configUtils = SpringContextHolder.getBean(ConfigUtils.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
boolean shardingSwitch = configUtils.getBool("sharding_switch", false);
// 没开启分表 直接返回老数据
if (!shardingSwitch) {
return invocation.proceed();
}
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue(MAPPED_STATEMENT);
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue(BOUND_SQL);
String originSql = (String) metaStatementHandler.getValue(ORIGIN_BOUND_SQL);
if (StringUtils.isBlank(originSql)) {
return invocation.proceed();
}
// 获取表名
String tableName = TemplateMatchService.matchTableName(boundSql.getSql().trim());
ShardingProperty shardingProperty = ShardingPropertyConfig.SHARDING_TABLE.get(tableName);
if (shardingProperty == null) {
return invocation.proceed();
}
// 新表
String shardingTable = getCurrentShardingTable(shardingProperty, new Date());
String rebuildSql = boundSql.getSql().replace(shardingProperty.getTableName(), shardingTable);
metaStatementHandler.setValue(ORIGIN_BOUND_SQL, rebuildSql);
if (log.isDebugEnabled()) {
log.info("rebuildSQL -> {}", rebuildSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {}
public static String getCurrentShardingTable(ShardingProperty shardingProperty, Date createTime) {
String tableName = shardingProperty.getTableName();
Integer days = shardingProperty.getDays();
Date beginDate = shardingProperty.getBeginDate();
Date date;
if (createTime == null) {
date = new Date();
} else {
date = createTime;
}
if (date.before(beginDate)) {
return null;
}
LocalDateTime targetDate = SimpleDateFormatUtils.convertDateToLocalDateTime(date);
LocalDateTime startDate = SimpleDateFormatUtils.convertDateToLocalDateTime(beginDate);
LocalDateTime intervalStartDate = DateIntervalChecker.getIntervalStartDate(targetDate, startDate, days);
LocalDateTime intervalEndDate = intervalStartDate.plusDays(days - 1);
return tableName + "_" + intervalStartDate.format(FORMATTER) + "_" + intervalEndDate.format(FORMATTER);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82