一种轻量分表方案-MyBatis拦截器分表实践|京东零售技术实践

背景

部门内有一些亿级别核心业务表增速非常快,增量日均100W,但线上业务只依赖近一周的数据。随着数据量的迅速增长,慢SQL频发,数据库性能下降,系统稳定性受到严重影响。本篇文章,将分享如何使用MyBatis拦截器低成本的提升数据库稳定性。

业界常见方案

针对冷数据多的大表,常用的策略有以2种:

1. 删除/归档旧数据。

2. 分表。

归档/删除旧数据

定期将冷数据移动到归档表或者冷存储中,或定期对表进行删除,以减少表的大小。此策略逻辑简单,只需要编写一个JOB定期执行SQL删除数据。我们开始也是用这种方案,但此方案也有一些副作用:

1.数据删除会影响数据库性能,引发慢sql,多张表并行删除,数据库压力会更大。

2.频繁删除数据,会产生数据库碎片,影响数据库性能,引发慢SQL。

综上,此方案有一定风险,为了规避这种风险,我们决定采用另一种方案:分表。

分表

我们决定按日期对表进行横向拆分,实现让系统每周生成一张周期表,表内只存近一周的数据,规避单表过大带来的风险。

分表方案选型

经调研,考虑2种分表方案:Sharding-JDBC、利用Mybatis自带的拦截器特性。

经过对比后,决定采用Mybatis拦截器来实现分表,原因如下:

1.JAVA生态中很常用的分表框架是Sharding-JDBC,虽然功能强大,但需要一定的接入成本,并且很多功能暂时用不上。

2.系统本身已经在使用Mybatis了,只需要添加一个mybaits拦截器,把SQL表名替换为新的周期表就可以了,没有接入新框架的成本,开发成本也不高。

简易架构图

分表具体实现代码

分表配置对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShardingProperty {// 分表周期天数,配置7,就是一周一分private Integer days;// 分表开始日期,需要用这个日期计算周期表名private Date beginDate;// 需要分表的表名private String tableName;
}
分表配置类
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);}
}
拦截器
import lombok.extern.slf4j.Slf4j;
import o2o.aspect.platform.function.template.service.TemplateMatchService;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.springframework.stereotype.Component;import java.sql.Connection;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Properties;@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 static final String SHARDING_MAPPER = "com.jd.o2o.inviter.promote.mapper.ShardingMapper";private ConfigUtils configUtils = SpringContextHolder.getBean(ConfigUtils.class);@Overridepublic 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();}@Overridepublic Object plugin(Object target) {if (target instanceof StatementHandler) {return Plugin.wrap(target, this);}return target;}@Overridepublic 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个难点需要解决:周期临界点数据不连续。举例:假设要对operate_log(操作日志表)大表进行横向分表,每周一张表,分表明细可看下面表格。

第一周(operate_log_20240107_20240108)第二周(operate_log_20240108_20240114)第三周(operate_log_20240115_20240121)
1月1号 ~ 1月7号的数据1月8号 ~ 1月14号的数据1月15号 ~ 1月21号的数据

1月8号就是分表临界点,8号需要切换到第二周的表,但8号0点刚切换的时候,表内没有任何数据,这时如果业务需要查近一周的操作日志是查不到的,这样就会引发线上问题。

我决定采用数据冗余的方式来解决这个痛点。每个周期表都冗余一份上个周期的数据,用双倍数据量实现数据滑动的效果,效果见下面表格。

第一周(operate_log_20240107_20240108)第二周(operate_log_20240108_20240114)第三周(operate_log_20240115_20240121)
12月25号 ~ 12月31号的数据1月1号 ~ 1月7号的数据1月8号 ~ 1月14号的数据
1月1号 ~ 1月7号的数据1月8号 ~ 1月14号的数据1月15号 ~ 1月21号的数据

注:表格内第一行数据就是冗余的上个周期表的数据。

思路有了,接下来就要考虑怎么实现双写(数据冗余到下个周期表),有2种方案:

1.在SQL执行完成返回结果前添加逻辑(可以用AspectJ 或 mybatis拦截器),如果SQL内的表名是当前周期表,就把表名替换为下个周期表,然后再次执行SQL。此方案对业务影响大,相当于串行执行了2次SQL,有性能损耗。

2.监听增量binlog,京东内部有现成的数据订阅中间件DRC,读者也可以使用cannal等开源中间件来代替DRC,原理大同小异,此方案对业务无影响。

方案对比后,选择了对业务性能损耗小的方案二。

监听binlog并双写流程图



监听binlog数据双写注意点

1.提前上线监听程序,提前把老表数据同步到新的周期表。分表前只监听老表binlog就可以,分表前只需要把老表数据同步到新表。

2.切换到新表的临界点,为了避免丢失积压的老表binlog,需要同时处理新表binlog和老表binlog,这样会出现死循环同步的问题,因为老表需要同步新表,新表又需要双写老表。为了打破循环,需要先把双写老表消费堵上让消息暂时积压,切换新表成功后,再打开双写消费。

监听binlog数据双写代码

注:下面代码不能直接用,只提供基本思路

/*** 监听binlog ,分表双写,解决数据临界问题
*/
@Slf4j
@Component
public class BinLogConsumer implements MessageListener {private MessageDeserialize deserialize = new JMQMessageDeserialize();private static final String TABLE_PLACEHOLDER = "%TABLE%";@Value("${mq.doubleWriteTopic.topic}")private String doubleWriteTopic;@Autowiredprivate JmqProducerService jmqProducerService;@Overridepublic void onMessage(List<Message> messages) throws Exception {if (messages == null || messages.isEmpty()) {return;}List<EntryMessage> entryMessages = deserialize.deserialize(messages);for (EntryMessage entryMessage : entryMessages) {try {syncData(entryMessage);} catch (Exception e) {log.error("sharding sync data error", e);throw e;}}}private void syncData(EntryMessage entryMessage) throws JMQException {// 根据binlog内的表名,获取需要同步的表// 3种情况:// 1、老表:需要同步当前周期表,和下个周期表。// 2、当前周期表:需要同步下个周期表,和老表。// 3、下个周期表:不需要同步。List<String> syncTables = getSyncTables(entryMessage.tableName, entryMessage.createTime);if (CollectionUtils.isEmpty(syncTables)) {log.info("table {} is not need sync", tableName);return;}if (entryMessage.getHeader().getEventType() == WaveEntry.EventType.INSERT) {String insertTableSqlTemplate = parseSqlForInsert(rowData);for (String syncTable : syncTables) {String insertSql = insertTableSqlTemplate.replaceAll(TABLE_PLACEHOLDER, syncTable);// 双写老表发Q,为了避免出现同步死循环问题if (ShardingPropertyConfig.SHARDING_TABLE.containsKey(syncTable)) {Long primaryKey = getPrimaryKey(rowData.getAfterColumnsList());sendDoubleWriteMsg(insertSql, primaryKey);continue;}mysqlConnection.executeSql(insertSql);}continue;}}

数据对比

为了保证新表和老表数据一致,需要编写对比程序,在上线前进行数据对比,保证binlog同步无问题。

具体实现代码不做展示,思路:新表查询一定量级数据,老表查询相同量级数据,都转换成JSON,equals对比。

作者:京东零售业务研发 张均杰

来源:京东零售技术 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/660482.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微软Azure-OpenAI 测试调用及说明

本文是公司在调研如何集成Azure-openAI时&#xff0c;调试测试用例得出的原文&#xff0c;原文主要基于官方说明文档简要整理实现 本文已假定阅读者申请部署了模型&#xff0c;已获取到所需的密钥和终结点 变量名称值ENDPOINT从 Azure 门户检查资源时&#xff0c;可在“密钥和…

Advanced CNN

文章目录 回顾Google NetInception1*1卷积Inception模块的实现网络构建完整代码 ResNet残差模块 Resedual Block残差网络的简单应用残差实现的代码 练习 回顾 这是一个简单的线性的卷积神经网络 然而有很多更为复杂的卷积神经网络。 Google Net Google Net 也叫Inception V…

5、应急响应-拒绝服务钓鱼识别DDOS压力测试邮件反制分析应用日志

目录 前言&#xff1a; 1、#内网应急-日志分析-爆破&横向&数据库 2、#红队APT-钓鱼邮件识别-内容&发信人&附件 3、#拒绝服务攻击-DDOS&CC-代理&防火墙防御 用途&#xff1a;个人学习笔记&#xff0c;欢迎指正&#xff01; 前言&#xff1a; 了解和…

OkHttp完全解读

一&#xff0c;概述 OkHttp作为android非常流行的网络框架&#xff0c;笔者认为有必要剖析此框架实现原理&#xff0c;抽取并理解此框架优秀的设计模式。OkHttp有几个重要的作用&#xff0c;如桥接、缓存、连接复用等&#xff0c;本文笔者将从使用出发&#xff0c;解读源码&am…

深度视频恢复软件推荐,轻松恢复视频文件!

“我在电脑上保存了一些视频&#xff0c;但在清理时却不小心将这些视频删除了&#xff0c;有什么方法可以恢复删除的视频吗&#xff1f;希望大家给我推荐一些好用的方法。” 随着科技的飞速发展&#xff0c;数字媒体已经成为了我们生活中不可或缺的一部分。然而&#xff0c;数字…

国图公考:考公和考编一样吗?

公务员&#xff1a;是指在各级机关中&#xff0c;符合规定&#xff0c;行使职权&#xff0c;执行公务的人员 事业单位&#xff1a;事业单位是指由国家或社会组织举办&#xff0c;从事教育、科学、文化、卫生、体育等社会公益事业的单位。 公务员和事业编都是有编制的&#xf…

dataframe 列按指定字符截取

创建一个示例 import pandas as pd data {Column1: [1~2, 21~3, 3~41, 411~5], } test_df pd.DataFrame(data) print(test_df) 截取 ’~ ‘前、后的值 test_df[Column1_left] test_df[Column1].apply(lambda x: x.split(~)[0] if pd.notnull(x) else np.nan) test_df[…

某赛通电子文档安全管理系统 PolicyAjax SQL注入漏洞复现

0x01 产品简介 某赛通电子文档安全管理系统(简称:CDG)是一款电子文档安全加密软件,该系统利用驱动层透明加密技术,通过对电子文档的加密保护,防止内部员工泄密和外部人员非法窃取企业核心重要数据资产,对电子文档进行全生命周期防护,系统具有透明加密、主动加密、智能…

推荐系统|排序_MMOE

MMOE MMOE是指Multi-gate Mixture-of-Experts 注意看Expert后面加了s&#xff0c;说明了有多个专家。 而在MMOE中专家是指用来对输入特征计算的神经网络&#xff0c;每个神经网络根据输入计算出来的向量都会有所不同。 MMOE的低层 MMOE的上一层 通过MMOE的低层算出的向量和权…

Markdown 图片尺寸对齐等详细使用

✍️作者简介&#xff1a;小北编程&#xff08;专注于HarmonyOS、Android、Java、Web、TCP/IP等技术方向&#xff09; &#x1f433;博客主页&#xff1a; 开源中国、稀土掘金、51cto博客、博客园、知乎、简书、慕课网、CSDN &#x1f514;如果文章对您些帮助请&#x1f449;关…

全链路压测的关键点是什么?

全链路压测是一种重要的性能测试方法&#xff0c;用于评估应用程序或系统在真实生产环境下的性能表现。通过模拟真实用户行为和流量&#xff0c;全链路压测能够全面评估系统在不同负载下的稳定性和性能表现。本文将介绍全链路压测的关键点&#xff0c;以帮助企业更好地理解和应…

【第二十二课】最短路:dijkstra算法 ( acwing849 / acwing850 / c++ 代码)

目录 dijkstra算法求最短距离步骤 朴素的dijkstra算法---acwing-849 代码如下 代码思路 堆优化版的dijkstra算法---acwing-850 代码如下 关于最短路问题分有好几种类型 &#xff1a; 单源就是指&#xff1a;只求从一个顶点到其他各顶点 多源是指&#xff1a;要求每个顶…

SD-WAN和MPLS的区别以及如何选择?

网络连接技术的选择对企业来说至关重要。SD-WAN&#xff08;软件定义广域网&#xff09;和MPLS&#xff08;多协议标签交换&#xff09;是两种备受关注的网络连接方案。它们在架构、带宽、成本和管理等方面存在显著区别&#xff0c;企业应了解清楚这些区别再进行选择。 SD-WAN采…

AI算力专题:从超微电脑创新高看AI算力产业链高景气

今天分享的是AI算力系列深度研究报告&#xff1a;《AI算力专题&#xff1a;从超微电脑创新高看AI算力产业链高景气》。 &#xff08;报告出品方&#xff1a;太平洋证券&#xff09; 报告共计&#xff1a;10页 海外巨头指引 Al 算力产业链高景气 超微电脑业绩指引大幅上调反映…

三子棋游戏小课堂

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 今天的主菜是&#xff0c;C语言实现的三子棋小游戏&#xff0c; 所属专栏&#xff1a; C语言知识点 主厨的主页&#xff1a;Chef‘s blog 前言&…

机器学习 | 掌握逻辑回归在实践中的应用

目录 初识逻辑回归 逻辑回归实操 分类评估方法 初识逻辑回归 逻辑回归&#xff08;LogisticRegression&#xff09;是机器学习中的一种分类模型&#xff0c;逻辑回归是一种分类算法&#xff0c;虽然名字中带有回归&#xff0c;但是它与回归之间有一定的联系。由于算法的简单…

【Spark系列2】Spark编程模型RDD

RDD概述 RDD最初的概述来源于一片论文-伯克利实验室的Resilient Distributed Datasets&#xff1a;A Fault-Tolerant Abstraction for In-Memory Cluster Computing。这篇论文奠定了RDD基本功能的思想 RDD实际为Resilient Distribution Datasets的简称&#xff0c;意为弹性分…

【大厂AI课学习笔记】1.3 人工智能产业发展(2)

&#xff08;注&#xff1a;腾讯AI课学习笔记。&#xff09; 1.3.1 需求侧 转型需求&#xff1a;人口红利转化为创新红利。 场景丰富&#xff1a;超大规模且多样的应用场景。主要是我们的场景大&#xff0c;数据资源丰富。 抗疫加速&#xff1a;疫情常态化&#xff0c;催生新…

Windows11通过Hyper-V创建VM,然后通过vscode连接vm进行开发

这边需要在win11上建立vm来部署docker(这边不能用windows版本的docker destop)&#xff0c;学习了下&#xff0c;记录。 下载系统镜像 首先下载系统镜像&#xff1a;https://releases.ubuntu.com/focal/ 这边使用的是ubuntu20.04.6 LTS (Focal Fossa) &#xff0c;Server inst…

CIFAR-10数据集详析:使用卷积神经网络训练图像分类模型

1.数据集介绍 CIFAR-10 数据集由 10 个类的 60000 张 32x32 彩色图像组成&#xff0c;每类 6000 张图像。有 50000 张训练图像和 10000 张测试图像。 数据集分为5个训练批次和1个测试批次&#xff0c;每个批次有10000张图像。测试批次正好包含从每个类中随机选择的 1000 张图像…