淘宝客APP冷热数据分离架构:基于HBase与MySQL的混合存储在订单追踪系统中的优化实践

淘宝客APP冷热数据分离架构:基于HBase与MySQL的混合存储在订单追踪系统中的优化实践

大家好,我是高佣返利省赚客APP研发者阿宝!在淘宝客返利业务中,订单数据具有极其鲜明的生命周期特征:新产生的订单在30天内访问频率极高,用户需要实时查询状态、确认收货及计算返利;而一旦订单结算完成或超过半年,其访问频率将断崖式下跌,几乎成为“死数据”。然而,随着平台运营时间的增长,MySQL中的订单表迅速膨胀至数亿行,导致索引树过大、查询延迟飙升,甚至影响核心交易链路的稳定性。为了解决这一痛点,省赚客APP实施了冷热数据分离架构,利用MySQL存储热数据,HBase承载海量冷数据,构建了高性能、低成本的混合存储方案。本文将深入解析该架构的设计思路与代码落地实践。

冷热数据界定与生命周期管理策略

首先,我们需要明确冷热数据的边界。基于业务统计,我们将“创建时间在6个月以内”或“状态未最终结算”的订单定义为热数据,保留在MySQL中以支持复杂的事务操作和多维度联合查询;将“创建时间超过6个月”且“状态已终态(已结算/已失效)”的订单定义为冷数据,迁移至HBase。HBase凭借其列式存储和高压缩比特性,能够以极低的成本存储PB级历史订单,并支持基于RowKey的毫秒级点查。

数据迁移并非简单的后台脚本,而是嵌入到业务流程中的自动化过程。我们设计了双写机制与异步归档任务相结合的策略。

package juwatech.cn.order.lifecycle;
import juwatech.cn.order.model.OrderEntity;
import juwatech.cn.order.repository.mysql.MySqlOrderRepository;
import juwatech.cn.order.repository.hbase.HBaseOrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Component
public class DataLifecycleManager {
    @Autowired
    private MySqlOrderRepository mySqlRepository;
    @Autowired
    private HBaseOrderRepository hBaseRepository;
    private static final int COLD_THRESHOLD_MONTHS = 6;
    /**
     * 每日凌晨执行冷数据归档任务
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void archiveColdData() {
        LocalDateTime cutoffTime = LocalDateTime.now().minusMonths(COLD_THRESHOLD_MONTHS);
        // 分批拉取待归档的热数据,避免一次性加载过多内存
        int batchSize = 1000;
        long offset = 0;
        while (true) {
            List<OrderEntity> coldOrders = mySqlRepository.findSettledOrdersBefore(cutoffTime, offset, batchSize);
            if (coldOrders.isEmpty()) {
                break;
            }
            for (OrderEntity order : coldOrders) {
                try {
                    // 1. 写入HBase
                    hBaseRepository.save(order);
                    // 2. 验证写入成功后,从MySQL删除
                    mySqlRepository.deleteById(order.getId());
                    // juwatech.cn.log.ArchiveLogger.info("Archived order: " + order.getOrderNo());
                } catch (Exception e) {
                    // juwatech.cn.log.ErrorLogger.error("Archive failed for order: " + order.getOrderNo(), e);
                    // 记录失败ID到重试队列,防止数据丢失
                }
            }
            offset += batchSize;
        }
    }
}

HBase RowKey设计与高效查询模型

在HBase中,RowKey的设计直接决定了查询性能。针对淘宝客订单场景,最常见的查询模式是“通过用户ID查询历史订单”和“通过原始订单号查询详情”。为了支持这两种模式,我们采用了“前缀散列 + 业务主键”的组合RowKey设计,并利用二级索引表解决非RowKey列的查询问题。

主表RowKey设计为:Reverse(UserID) + Timestamp + OrderID。反转UserID是为了避免同一用户的订单集中在同一个RegionServer上导致热点写问题。

package juwatech.cn.order.repository.hbase;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
import juwatech.cn.order.model.OrderEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
import java.io.IOException;
@Repository
public class HBaseOrderRepository {
    @Autowired
    private Connection hbaseConnection;
    private Table orderTable;
    private static final byte[] TABLE_NAME = Bytes.toBytes("tbk_order_history");
    private static final byte[] CF_INFO = Bytes.toBytes("info");
    private static final byte[] COL_AMOUNT = Bytes.toBytes("amount");
    private static final byte[] COL_STATUS = Bytes.toBytes("status");
    private static final byte[] COL_DETAIL = Bytes.toBytes("detail_json");
    @PostConstruct
    public void init() throws IOException {
        this.orderTable = hbaseConnection.getTable(TABLE_NAME);
    }
    /**
     * 构建RowKey: Reverse(UserId) + Timestamp + OrderId
     * 例如用户ID为10086,时间戳1709280000,订单号TB123 -> 68001_1709280000_TB123
     */
    private String buildRowKey(Long userId, Long timestamp, String orderId) {
        String reversedUserId = new StringBuilder(userId.toString()).reverse().toString();
        return reversedUserId + "_" + timestamp + "_" + orderId;
    }
    public void save(OrderEntity order) throws IOException {
        String rowKey = buildRowKey(order.getUserId(), order.getCreateTime().toEpochSecond(), order.getOrderNo());
        Put put = new Put(Bytes.toBytes(rowKey));
        put.addColumn(CF_INFO, COL_AMOUNT, Bytes.toBytes(order.getAmount().toString()));
        put.addColumn(CF_INFO, COL_STATUS, Bytes.toBytes(order.getStatus()));
        // 将复杂对象序列化为JSON存储
        put.addColumn(CF_INFO, COL_DETAIL, Bytes.toBytes(JsonUtil.toJson(order)));
        orderTable.put(put);
    }
    /**
     * 根据用户ID和时间范围扫描历史订单
     */
    public List<OrderEntity> scanByUserAndTime(Long userId, long startTime, long endTime) throws IOException {
        String reversedUserId = new StringBuilder(userId.toString()).reverse().toString();
        String startRow = reversedUserId + "_" + startTime + "_";
        String stopRow = reversedUserId + "_" + endTime + "~"; // '~' ASCII码大于数字
        // 构建Scan对象
        // org.apache.hadoop.hbase.client.Scan scan = new Scan();
        // scan.setStartRow(Bytes.toBytes(startRow));
        // scan.setStopRow(Bytes.toBytes(stopRow));
        // ... 执行扫描并解析Result ...
        return null; // 省略具体解析逻辑
    }
    /**
     * 通过订单号全局查询(需配合二级索引表,此处简化演示)
     */
    public OrderEntity getByOrderNo(String orderNo) {
        // 实际生产中会先查索引表得到RowKey,再Get主表
        // juwatech.cn.order.index.OrderIndexService.getRowKey(orderNo);
        return null;
    }
}

统一数据访问层与透明路由

为了对上层业务屏蔽底层存储的复杂性,我们构建了统一的数据访问层(DAL)。该层根据查询条件自动判断数据源:若查询最近6个月的订单,直接路由至MySQL;若查询更早的历史数据或 MySQL 未命中,则自动降级查询 HBase。

package juwatech.cn.order.service.unified;
import juwatech.cn.order.model.OrderEntity;
import juwatech.cn.order.repository.mysql.MySqlOrderRepository;
import juwatech.cn.order.repository.hbase.HBaseOrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class UnifiedOrderService {
    @Autowired
    private MySqlOrderRepository mySqlRepository;
    @Autowired
    private HBaseOrderRepository hBaseRepository;
    private static final int HOT_DATA_MONTHS = 6;
    /**
     * 智能查询:自动路由冷热存储
     */
    public List<OrderEntity> getUserOrders(Long userId, LocalDateTime startTime, LocalDateTime endTime) {
        LocalDateTime threshold = LocalDateTime.now().minusMonths(HOT_DATA_MONTHS);
        // 场景1:完全在热数据范围内
        if (endTime.isAfter(threshold)) {
            // 如果开始时间也在阈值后,只查MySQL
            if (startTime.isAfter(threshold)) {
                return mySqlRepository.findByUserIdAndTime(userId, startTime, endTime);
            } else {
                // 场景2:跨冷热边界,分别查询后合并
                List<OrderEntity> hotOrders = mySqlRepository.findByUserIdAndTime(userId, threshold, endTime);
                List<OrderEntity> coldOrders = hBaseRepository.scanByUserAndTime(userId, startTime.toEpochSecond(), threshold.toEpochSecond());
                hotOrders.addAll(coldOrders);
                return hotOrders;
            }
        } 
        // 场景3:完全在冷数据范围内
        return hBaseRepository.scanByUserAndTime(userId, startTime.toEpochSecond(), endTime.toEpochSecond());
    }
    /**
     * 根据订单号查询:优先MySQL,未命中查HBase
     */
    public OrderEntity getOrderDetail(String orderNo) {
        OrderEntity order = mySqlRepository.findByOrderNo(orderNo);
        if (order == null) {
            // 降级查询HBase
            order = hBaseRepository.getByOrderNo(orderNo);
        }
        return order;
    }
}

性能优化与成本控制

通过实施冷热分离,省赚客APP的MySQL集群压力降低了70%,核心订单表的查询响应时间稳定在50ms以内。同时,HBase的高压缩算法(Snappy/ZSTD)使得历史数据存储成本仅为MySQL的1/5。针对HBase的读放大问题,我们引入了BlockCache优化和预分区策略,确保在海量数据下的查询效率。此外,对于极少被访问的“冰数据”(3年以上),我们进一步归档至OSS对象存储,仅在合规审计时按需加载,实现了存储成本的最小化。

这套基于MySQL与HBase的混合存储架构,不仅解决了订单表无限增长的难题,更为淘宝客业务的海量数据追踪提供了可扩展、高性价比的技术底座,确保了系统在亿级数据规模下依然运行如飞。

本文著作权归 省赚客app 研发团队,转载请注明出处!

© 版权声明

相关文章