色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

一種輕量分表方案-MyBatis攔截器分表實踐

京東云 ? 來源:京東零售 張均杰 ? 作者:京東零售 張均杰 ? 2025-01-23 17:38 ? 次閱讀

作者:京東零售 張均杰

背景

部門內有一些億級別核心業務表增速非常快,增量日均100W,但線上業務只依賴近一周的數據。隨著數據量的迅速增長,慢SQL頻發,數據庫性能下降,系統穩定性受到嚴重影響。本篇文章,將分享如何使用MyBatis攔截器低成本的提升數據庫穩定性。

業界常見方案

針對冷數據多的大表,常用的策略有以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 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);

    @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個難點需要解決:周期臨界點數據不連續。舉例:假設要對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數據雙寫注意點

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;

    @Autowired
    private JmqProducerService jmqProducerService;


    @Override
    public void onMessage(List messages) throws Exception {
        if (messages == null || messages.isEmpty()) {
            return;
        }
        List 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 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對比。

審核編輯 黃宇

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • SQL
    SQL
    +關注

    關注

    1

    文章

    773

    瀏覽量

    44223
  • 京東
    +關注

    關注

    2

    文章

    1000

    瀏覽量

    48662
  • mybatis
    +關注

    關注

    0

    文章

    62

    瀏覽量

    6734
收藏 人收藏

    評論

    相關推薦

    介紹一種OpenAtom OpenHarmony系統適配方案

    本文在不改變原有系統基礎框架的基礎上, 介紹了一種OpenAtom OpenHarmony(以下簡稱“OpenHarmony”)系統適配方案
    的頭像 發表于 03-05 09:24 ?1259次閱讀
    介紹<b class='flag-5'>一種</b>OpenAtom OpenHarmony<b class='flag-5'>輕</b><b class='flag-5'>量</b>系統適配<b class='flag-5'>方案</b>

    輸入毫伏

    輸入毫伏--見附件
    發表于 06-19 00:07

    三豐千分表通訊方案

    套能夠讓電腦每隔0.1秒讀取次千分表讀數的方案,有意者電聯***
    發表于 06-19 14:11

    分庫是什么?怎么實現?

    數據庫分庫、讀寫分離的原理實現,使用場景
    發表于 10-25 17:24

    mybatis支持數據庫兼容的方案

    方案, 令mybatis支持數據庫兼容
    發表于 04-09 17:44

    原理及實際應用

    覆蓋率。  1.1功原理  功全稱功率分配器,是一種路輸入信號能量分成兩路或多路輸出
    發表于 12-03 15:00

    介紹一種串行接口方案

    折疊式手機面臨哪些問題?一種滿足手機高速圖像數據傳輸的差串行接口方案
    發表于 06-01 06:51

    動能攔截器六自由度仿真建模研究

    仿真建模技術是動能攔截器制導律研究中的重要技術,文中主要建立動能攔截器的軌道運動動力學以及姿態運動動力學模型,并建立完整的制導控制系統數學模型。文末,以某型
    發表于 08-07 08:50 ?14次下載

    利用Mycat實現MySQL讀寫分離、分庫最佳實踐

    利用Mycat實現MySQL讀寫分離、分庫最佳實踐
    發表于 09-08 10:20 ?14次下載
    利用Mycat實現MySQL讀寫分離、分庫<b class='flag-5'>分</b><b class='flag-5'>表</b>最佳<b class='flag-5'>實踐</b>

    springmvc 自定義攔截器實現未登錄用戶的攔截

    springmvc自定義攔截器實現未登錄用戶的攔截
    發表于 11-25 14:44 ?2524次閱讀
    springmvc 自定義<b class='flag-5'>攔截器</b>實現未登錄用戶的<b class='flag-5'>攔截</b>

    分表和千分表的區別是什么

    分表:百分表是利用精密齒條齒輪機構制成的式通用長度測量工具。通常由測頭、桿、防震彈簧、齒條、齒輪、游絲、圓表盤及指針等組成。
    發表于 01-02 18:16 ?8.9w次閱讀
    百<b class='flag-5'>分表</b>和千<b class='flag-5'>分表</b>的區別是什么

    什么是分庫?為什么分庫?什么情況下會用分庫呢?

    分庫是由分庫和這兩個獨立概念組成的,只不過通常分庫與的操作會同時進行,以至于我們習慣
    的頭像 發表于 11-30 09:37 ?7598次閱讀

    springboot過濾器和攔截器哪個先執行

    的概念、用途、執行順序以及實際使用中的注意事項。 、過濾器和攔截器的概念和用途 過濾器(Filter) 過濾器是Java Web應用程序中的一種組件,它用于攔截客戶端請求并對其進行預
    的頭像 發表于 12-03 15:00 ?2638次閱讀

    使用go語言實現個grpc攔截器

    在開發grpc服務時,我們經常會遇到些通用的需求,比如:日志、鏈路追蹤、鑒權等。這些需求可以通過grpc攔截器來實現。本文使用go語言來實現個 grpc元模式(Unary)
    的頭像 發表于 12-18 10:13 ?727次閱讀
    使用go語言實現<b class='flag-5'>一</b>個grpc<b class='flag-5'>攔截器</b>

    分庫后復雜查詢的應對之道:基于DTS實時性ES寬構建技術實踐

    1 問題域 業務發展的初期,我們的數據庫架構往往是單庫單,外加讀寫分離來快速的支撐業務,隨著用戶和訂單的增加,數據庫的計算和存儲往往會成為我們系統的瓶頸,業界的實踐多數采用分而治
    的頭像 發表于 06-25 18:30 ?902次閱讀
    分庫<b class='flag-5'>分</b><b class='flag-5'>表</b>后復雜查詢的應對之道:基于DTS實時性ES寬<b class='flag-5'>表</b>構建技術<b class='flag-5'>實踐</b>
    主站蜘蛛池模板: 欧美阿v天堂视频在99线 | 国产超碰AV人人做人人爽 | 亚洲国产韩国欧美在线不卡 | 国产美女裸身网站免费观看视频 | 99亚洲精品自拍AV成人软件 | 亚洲欧美国产综合在线 | 性欧美video另类hd高清 | 亚洲中文久久久久久国产精品 | 亚洲高清无在码在线无弹窗 | 亚洲免费无l码中文在线视频 | 91亚洲精品福利在线播放 | 色婷婷亚洲精品天天综合影院 | 国产午夜在线观看视频 | 校园刺激全黄H全肉细节文 校园纯肉H教室第一次 | 秋霞伦理高清视频在线 | 手机看片国产日韩欧美 | 99视频在线免费观看 | 国产99视频在线观看 | 岛国大片在线观看完整版 | 欧美XXXX69学生HD | 高龄熟女50P | 动漫美女被羞羞动漫怪物 | 久久久久国产一级毛片高清片 | 尹人综合网| 超级最爽的乱淫片免费 | 久久偷拍免费2017 | 一二三四电影完整版免费观看 | 一受多攻高h | 日韩丰满少妇无码内射 | 蜜臀AV99无码精品国产专区 | 91伊人久久大香线蕉 | 国产一区二区三区内射高清 | 18禁止看的免费污网站 | 欧美506070| 亚洲mv在线观看 | 久草热8精品视频在线观看 久草青青在线 | 午夜片神马影院福利 | 亚洲精品无码AAAAAA片 | 亚洲欧美中文字幕5发布 | 啪啪后入内射日韩 | AV72啪啪网站|