欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

MyBatis攔截器分表實(shí)踐分享

 更新時(shí)間:2024年01月26日 14:00:43   作者:京東云開發(fā)者  
部門內(nèi)有一些億級(jí)別核心業(yè)務(wù)表增速非常快,增量日均100W,但線上業(yè)務(wù)只依賴近一周的數(shù)據(jù),隨著數(shù)據(jù)量的迅速增長,慢SQL頻發(fā),數(shù)據(jù)庫性能下降,系統(tǒng)穩(wěn)定性受到嚴(yán)重影響,本篇文章,將分享如何使用MyBatis攔截器低成本的提升數(shù)據(jù)庫穩(wěn)定性,需要的朋友可以參考下

背景

部門內(nèi)有一些億級(jí)別核心業(yè)務(wù)表增速非???,增量日均100W,但線上業(yè)務(wù)只依賴近一周的數(shù)據(jù)。隨著數(shù)據(jù)量的迅速增長,慢SQL頻發(fā),數(shù)據(jù)庫性能下降,系統(tǒng)穩(wěn)定性受到嚴(yán)重影響。本篇文章,將分享如何使用MyBatis攔截器低成本的提升數(shù)據(jù)庫穩(wěn)定性。

業(yè)界常見方案

針對(duì)冷數(shù)據(jù)多的大表,常用的策略有以2種:

1. 刪除/歸檔舊數(shù)據(jù)。

2. 分表。

歸檔/刪除舊數(shù)據(jù)

定期將冷數(shù)據(jù)移動(dòng)到歸檔表或者冷存儲(chǔ)中,或定期對(duì)表進(jìn)行刪除,以減少表的大小。此策略邏輯簡(jiǎn)單,只需要編寫一個(gè)JOB定期執(zhí)行SQL刪除數(shù)據(jù)。我們開始也是用這種方案,但此方案也有一些副作用:

1.數(shù)據(jù)刪除會(huì)影響數(shù)據(jù)庫性能,引發(fā)慢sql,多張表并行刪除,數(shù)據(jù)庫壓力會(huì)更大。

2.頻繁刪除數(shù)據(jù),會(huì)產(chǎn)生數(shù)據(jù)庫碎片,影響數(shù)據(jù)庫性能,引發(fā)慢SQL。

綜上,此方案有一定風(fēng)險(xiǎn),為了規(guī)避這種風(fēng)險(xiǎn),我們決定采用另一種方案:分表。

分表

我們決定按日期對(duì)表進(jìn)行橫向拆分,實(shí)現(xiàn)讓系統(tǒng)每周生成一張周期表,表內(nèi)只存近一周的數(shù)據(jù),規(guī)避單表過大帶來的風(fēng)險(xiǎn)。

分表方案選型

經(jīng)調(diào)研,考慮2種分表方案:Sharding-JDBC、利用Mybatis自帶的攔截器特性。

經(jīng)過對(duì)比后,決定采用Mybatis攔截器來實(shí)現(xiàn)分表,原因如下:

1.JAVA生態(tài)中很常用的分表框架是Sharding-JDBC,雖然功能強(qiáng)大,但需要一定的接入成本,并且很多功能暫時(shí)用不上。

2.系統(tǒng)本身已經(jīng)在使用Mybatis了,只需要添加一個(gè)mybaits攔截器,把SQL表名替換為新的周期表就可以了,沒有接入新框架的成本,開發(fā)成本也不高。

分表具體實(shí)現(xiàn)代碼

分表配置對(duì)象

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShardingProperty {
    // 分表周期天數(shù),配置7,就是一周一分
    private Integer days;
    // 分表開始日期,需要用這個(gè)日期計(jì)算周期表名
    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);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        boolean shardingSwitch = configUtils.getBool("sharding_switch", false);
        // 沒開啟分表 直接返回老數(shù)據(jù)
        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);
    }
}


臨界點(diǎn)數(shù)據(jù)不連續(xù)問題

分表方案有1個(gè)難點(diǎn)需要解決:周期臨界點(diǎn)數(shù)據(jù)不連續(xù)。舉例:假設(shè)要對(duì)operate_log(操作日志表)大表進(jìn)行橫向分表,每周一張表,分表明細(xì)可看下面表格。

| 第一周(operate_log_20240107_20240108) | 第二周(operate_log_20240108_20240114) | 第三周(operate_log_20240115_20240121) | | 1月1號(hào) ~ 1月7號(hào)的數(shù)據(jù) | 1月8號(hào) ~ 1月14號(hào)的數(shù)據(jù) | 1月15號(hào) ~ 1月21號(hào)的數(shù)據(jù) |

1月8號(hào)就是分表臨界點(diǎn),8號(hào)需要切換到第二周的表,但8號(hào)0點(diǎn)剛切換的時(shí)候,表內(nèi)沒有任何數(shù)據(jù),這時(shí)如果業(yè)務(wù)需要查近一周的操作日志是查不到的,這樣就會(huì)引發(fā)線上問題。

我決定采用數(shù)據(jù)冗余的方式來解決這個(gè)痛點(diǎn)。每個(gè)周期表都冗余一份上個(gè)周期的數(shù)據(jù),用雙倍數(shù)據(jù)量實(shí)現(xiàn)數(shù)據(jù)滑動(dòng)的效果,效果見下面表格。

| 第一周(operate_log_20240107_20240108) | 第二周(operate_log_20240108_20240114) | 第三周(operate_log_20240115_20240121) | | 12月25號(hào) ~ 12月31號(hào)的數(shù)據(jù) | 1月1號(hào) ~ 1月7號(hào)的數(shù)據(jù) | 1月8號(hào) ~ 1月14號(hào)的數(shù)據(jù) | | 1月1號(hào) ~ 1月7號(hào)的數(shù)據(jù) | 1月8號(hào) ~ 1月14號(hào)的數(shù)據(jù) | 1月15號(hào) ~ 1月21號(hào)的數(shù)據(jù) |

注:表格內(nèi)第一行數(shù)據(jù)就是冗余的上個(gè)周期表的數(shù)據(jù)。

思路有了,接下來就要考慮怎么實(shí)現(xiàn)雙寫(數(shù)據(jù)冗余到下個(gè)周期表),有2種方案:

1.在SQL執(zhí)行完成返回結(jié)果前添加邏輯(可以用AspectJ 或 mybatis攔截器),如果SQL內(nèi)的表名是當(dāng)前周期表,就把表名替換為下個(gè)周期表,然后再次執(zhí)行SQL。此方案對(duì)業(yè)務(wù)影響大,相當(dāng)于串行執(zhí)行了2次SQL,有性能損耗。

2.監(jiān)聽增量binlog,京東內(nèi)部有現(xiàn)成的數(shù)據(jù)訂閱中間件DRC,讀者也可以使用cannal等開源中間件來代替DRC,原理大同小異,此方案對(duì)業(yè)務(wù)無影響。

方案對(duì)比后,選擇了對(duì)業(yè)務(wù)性能損耗小的方案二。

監(jiān)聽binlog并雙寫流程圖

監(jiān)聽binlog數(shù)據(jù)雙寫注意點(diǎn)

1.提前上線監(jiān)聽程序,提前把老表數(shù)據(jù)同步到新的周期表。分表前只監(jiān)聽老表binlog就可以,分表前只需要把老表數(shù)據(jù)同步到新表。

2.切換到新表的臨界點(diǎn),為了避免丟失積壓的老表binlog,需要同時(shí)處理新表binlog和老表binlog,這樣會(huì)出現(xiàn)死循環(huán)同步的問題,因?yàn)槔媳硇枰叫卤恚卤碛中枰p寫老表。為了打破循環(huán),需要先把雙寫老表消費(fèi)堵上讓消息暫時(shí)積壓,切換新表成功后,再打開雙寫消費(fèi)。

監(jiān)聽binlog數(shù)據(jù)雙寫代碼

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

/**
 * 監(jiān)聽binlog ,分表雙寫,解決數(shù)據(jù)臨界問題
*/
@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<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 {
        // 根據(jù)binlog內(nèi)的表名,獲取需要同步的表
        // 3種情況:
        // 1、老表:需要同步當(dāng)前周期表,和下個(gè)周期表。
        // 2、當(dāng)前周期表:需要同步下個(gè)周期表,和老表。
        // 3、下個(gè)周期表:不需要同步。
        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);
                // 雙寫老表發(fā)Q,為了避免出現(xiàn)同步死循環(huán)問題
                if (ShardingPropertyConfig.SHARDING_TABLE.containsKey(syncTable)) {
                    Long primaryKey = getPrimaryKey(rowData.getAfterColumnsList());
                    sendDoubleWriteMsg(insertSql, primaryKey);
                    continue;
                }
                mysqlConnection.executeSql(insertSql);
            }
            continue;
        }
    }



數(shù)據(jù)對(duì)比

為了保證新表和老表數(shù)據(jù)一致,需要編寫對(duì)比程序,在上線前進(jìn)行數(shù)據(jù)對(duì)比,保證binlog同步無問題。

具體實(shí)現(xiàn)代碼不做展示,思路:新表查詢一定量級(jí)數(shù)據(jù),老表查詢相同量級(jí)數(shù)據(jù),都轉(zhuǎn)換成JSON,equals對(duì)比。

以上就是MyBatis攔截器分表實(shí)踐分享的詳細(xì)內(nèi)容,更多關(guān)于MyBatis攔截器分表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 圖文詳解Java中的字節(jié)輸入與輸出流

    圖文詳解Java中的字節(jié)輸入與輸出流

    在Java中所有數(shù)據(jù)都是使用流讀寫的,流是一組有序的數(shù)據(jù)序列,將數(shù)據(jù)從一個(gè)地方帶到另一個(gè)地方,這篇文章主要給大家介紹了關(guān)于Java中字節(jié)輸入與輸出流的相關(guān)資料,需要的朋友可以參考下
    2021-08-08
  • Java調(diào)用.dll文件的方法

    Java調(diào)用.dll文件的方法

    因?yàn)轫?xiàng)目的需求,要在JAVA項(xiàng)目中調(diào)用Windows的Dll(動(dòng)態(tài)鏈接庫)文件,之前用Jni調(diào)用過C寫的Dll文件,比較麻煩,這里不多說,網(wǎng)上也有很多這方面的文檔。在網(wǎng)上找到一個(gè)開源的組件JNative,使用后感覺比較方便
    2013-04-04
  • java封裝前端查詢條件通用版

    java封裝前端查詢條件通用版

    這篇文章主要為大家詳細(xì)介紹了java封裝前端查詢條件的通用版,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-05-05
  • 關(guān)于maven打包出錯(cuò)的解決方案

    關(guān)于maven打包出錯(cuò)的解決方案

    這篇文章主要介紹了關(guān)于maven打包出錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • java 對(duì)象數(shù)組排序

    java 對(duì)象數(shù)組排序

    當(dāng)遇到數(shù)組排序時(shí),我們經(jīng)常會(huì)使用學(xué)過的幾種排序方法,而java 本身提供了Arrays.sort,在數(shù)據(jù)元素較少或者對(duì)效率要求不是抬高時(shí),直接使用Arrays.sort來的更容易。查看一下源碼后Arrays.sort 本身采用的是快速排序。
    2015-04-04
  • Spring Cache @Cacheable 緩存在部分Service中不生效的解決辦法

    Spring Cache @Cacheable 緩存在部分Service中不生效的解決辦法

    這篇文章主要介紹了Spring Cache @Cacheable 緩存在部分Service中不生效的解決辦法
    2023-10-10
  • java web實(shí)現(xiàn)自動(dòng)登錄

    java web實(shí)現(xiàn)自動(dòng)登錄

    這篇文章主要為大家詳細(xì)介紹了java web實(shí)現(xiàn)自動(dòng)登錄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • java &與&&的區(qū)別及實(shí)例

    java &與&&的區(qū)別及實(shí)例

    這篇文章主要介紹了java &與&&的區(qū)別的相關(guān)資料,并附簡(jiǎn)單實(shí)例,幫助大家學(xué)習(xí)理解這部分知識(shí),需要的朋友可以參考下
    2016-10-10
  • 深入探究SpringBoot中的Elasticsearch自動(dòng)配置原理及用法

    深入探究SpringBoot中的Elasticsearch自動(dòng)配置原理及用法

    SpringBoot中的Elasticsearch自動(dòng)配置為我們提供了一種快速集成Elasticsearch的方式,使我們可以在SpringBoot應(yīng)用程序中輕松地使用Elasticsearch,本文將介紹Spring Boot中的Elasticsearch自動(dòng)配置的作用、原理和使用方法
    2023-07-07
  • Java面試題 從源碼角度分析HashSet實(shí)現(xiàn)原理

    Java面試題 從源碼角度分析HashSet實(shí)現(xiàn)原理

    這篇文章主要介紹了Java面試題 從源碼角度分析HashSet實(shí)現(xiàn)原理?,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-07-07

最新評(píng)論