商品服务架构实战:多数据源切换与分级缓存设计全解析

商品服务架构实战:多数据源切换与分级缓存设计全解析

在电商商品服务中,高并发、海量数据、复杂查询是三大核心挑战。本文将结合真实生产经验,从多数据源切换商品详情页分级缓存两个维度,深入剖析实现方案、痛点与最佳实践。内容涵盖:三种多数据源切换方式对比、SPU/SKU 数据模型设计、本地+Redis+DB 三级缓存架构、缓存三大问题(穿透/击穿/雪崩)的解决方案,以及基于双删、MQ、Canal 的最终一致性保障。

📌 本文代码示例基于 Spring Boot + MyBatis-Plus + Redis + Caffeine,适用于电商商品服务、内容详情页等读多写少场景。


一、商品服务数据模型与多数据源场景

1.1 核心数据模型

商品服务通常涉及以下实体:

实体 说明 关联关系
品牌 (Brand) 商品所属品牌 1 对多关联 SPU
分类 (Category) 商品分类(树形结构) 1 对多关联 SPU
SPU (Standard Product Unit) 标准产品单元(如 iPhone 14) 关联品牌、分类
SKU (Stock Keeping Unit) 库存量单元(如 iPhone 14 黑色 128G) 属于某个 SPU,拥有规格属性
规格属性 (Spec/Attribute) 规格名(颜色、内存)与规格值 多对多关联 SKU/SPU

典型数据库设计简图:
has
belongs
contains
has
defines
BRAND
SPU
CATEGORY
SKU
SPEC_VALUE
SPEC_NAME

1.2 为什么需要多数据源?

  • 读写分离:主库处理写操作(下单、库存扣减),从库处理读操作(商品详情、列表查询),降低主库压力。
  • 分库分表:商品数据量大,可按品类或哈希分库。
  • 多业务隔离:商品核心库、搜索库、报表库分离。

下面介绍三种主流多数据源切换实现方式。


二、多数据源切换的三种实现方案

2.1 方案一:自定义注解 + AOP(轻量级)

原理 :通过 @DataSource 注解标记方法或类,AOP 切面在方法执行前动态设置 ThreadLocal 中的数据源标识,最后在 AbstractRoutingDataSource 中路由。

实现步骤

  1. 定义注解 @DataSource("slave")
  2. AOP 切面:@Around@Before,将注解值存入 DataSourceContextHolder
  3. 继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 返回当前线程的数据源 key。
  4. 配置多个数据源。
java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "master";
}

@Component
@Aspect
public class DataSourceAspect {
    @Before("@annotation(dataSource)")
    public void switchDataSource(JoinPoint point, DataSource dataSource) {
        DataSourceContextHolder.setDataSource(dataSource.value());
    }
    @After("@annotation(dataSource)")
    public void restoreDataSource(DataSource dataSource) {
        DataSourceContextHolder.clear();
    }
}

优点 :灵活、无侵入、轻量。
缺点 :需要手动维护 ThreadLocal,事务环境下数据源切换可能失效(需注意事务传播)。

2.2 方案二:MyBatis-Plus 动态数据源(官方推荐)

MyBatis-Plus 提供了 dynamic-datasource 组件,只需配置多数据源并使用 @DS 注解即可。

配置示例

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master
      strict: false
      datasource:
        master:
          url: jdbc:mysql://master:3306/product
          username: root
        slave_1:
          url: jdbc:mysql://slave1:3306/product
          username: root
        slave_2:
          url: jdbc:mysql://slave2:3306/product

使用

java 复制代码
@DS("slave_1")
@Mapper
public interface SkuMapper extends BaseMapper<Sku> {
    // 读从库
}

优点 :配置简单、支持 Seata 分布式事务、内置负载均衡(轮询/随机等)。
缺点:依赖 MyBatis-Plus 生态,非 MyBatis-Plus 项目需引入额外依赖。

2.3 方案三:ShardingJDBC(最强悍)

Apache ShardingJDBC 不仅支持读写分离,还支持分库分表、分布式事务。适用于超大规模商品数据。

配置示例(读写分离 + 分片):

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: master,slave1,slave2
      master:
        type: HikariDataSource
        url: jdbc:mysql://master:3306/product
      # ... slave 配置
    rules:
      readwrite-splitting:
        data-sources:
          product_ds:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave1,slave2
            load-balancer-name: round_robin
    props:
      sql-show: true

优点 :功能最全(分片、加密、影子库),对业务代码零侵入。
缺点:学习曲线较陡,部分复杂 SQL 可能需改写。

2.4 方案对比与选择建议

方案 适用场景 学习成本 功能丰富度
自定义注解+AOP 简单读写分离,团队技术掌控力强
MyBatis-Plus @DS 基于 MyBatis-Plus 的项目,希望快速集成
ShardingJDBC 分库分表 + 读写分离,数据量极大

商品服务建议:初期可用 MyBatis-Plus 动态数据源,后期数据量增长至亿级时迁移到 ShardingJDBC。


三、商品详情页分级缓存架构

商品详情页是典型的高并发读场景,请求量可达数万 QPS。我们采用 本地缓存(Caffeine)→ Redis → DB 三级缓存,结构如下:
命中
未命中
命中
未命中
客户端请求详情页
Caffeine 本地缓存
返回 HTML / JSON
Redis 集群
回填 Caffeine
查询 MySQL
回填 Redis + Caffeine

3.1 关键问题:毛刺现象(Thundering Herd)

当本地缓存设置了固定过期时间(如 30 秒),大量 Key 同时失效时,瞬间请求会穿透到 Redis,造成网络 IO 拥堵,RT 从 1ms 飙升至 20ms+,形成"毛刺"。

解决方案(引用前文门户服务的双缓存机制)

  • 主缓存(Caffeine):永不过期,由后台定时任务从 Redis 刷新。
  • 备份缓存 (Caffeine):expireAfterAccess(5min),仅作为防穿透兜底,不主动回填主缓存。
  • 定时任务:每隔 1 分钟拉取热点 Key 的最新数据,更新主缓存。

具体流程参见《门户服务缓存优化》一文,此处不再赘述。

3.2 Redis 三大缓存问题及对策

问题 描述 商品服务中的解法
缓存穿透 查询不存在的数据(如恶意请求不存在的 SKU ID),请求直达 DB ① 布隆过滤器(Bloom Filter)预判 ID 有效性;② 空结果缓存(TTL 短,如 1 分钟)
缓存击穿 某个热点 Key 在 Redis 失效瞬间,大量并发请求打到 DB ① 互斥锁(Redis setnx);② 逻辑过期(主动刷新,不设物理 TTL)
缓存雪崩 大量 Key 同时到期,或 Redis 宕机,请求全部压垮 DB ① 随机 TTL(基础 TTL ± 随机值);② 多级缓存(本地兜底);③ Redis 集群高可用

互斥锁解决击穿代码示例

java 复制代码
public SkuDetail getSkuDetail(Long skuId) {
    String cacheKey = "sku:" + skuId;
    // 1. 查 Redis
    SkuDetail detail = redisTemplate.opsForValue().get(cacheKey);
    if (detail != null) return detail;

    // 2. 尝试获取分布式锁
    String lockKey = "lock:sku:" + skuId;
    Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(3));
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 3. 查询 DB(只让一个线程执行)
            detail = skuMapper.selectDetail(skuId);
            if (detail != null) {
                redisTemplate.opsForValue().set(cacheKey, detail, 
                    randomTtl(), TimeUnit.SECONDS);
            } else {
                // 缓存空对象(防穿透)
                redisTemplate.opsForValue().set(cacheKey, "", 
                    Duration.ofSeconds(60));
            }
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        // 4. 未获得锁的线程:短暂休眠后重试
        try { Thread.sleep(50); } catch (InterruptedException e) {}
        return getSkuDetail(skuId);
    }
    return detail;
}

四、Redis 与 DB 数据一致性方案

商品服务中,库存、价格、上下架状态等数据会频繁更新。如何保证 Redis 缓存与 MySQL 最终一致,同时不影响业务性能?以下是三种主流方案。

4.1 延时双删(简单,适合低并发)

流程

  1. 更新数据库。
  2. 删除 Redis 缓存。
  3. 休眠 500ms(业务容忍时间)。
  4. 再次删除 Redis 缓存。

作用:避免并发请求在第一次删除后、第二次删除前将旧数据重新载入。

java 复制代码
@Transactional
public void updatePrice(Long skuId, BigDecimal newPrice) {
    skuMapper.updatePrice(skuId, newPrice);
    redisTemplate.delete("sku:" + skuId);
    try { Thread.sleep(500); } catch (InterruptedException ignored) {}
    redisTemplate.delete("sku:" + skuId);
}

缺点:休眠时间难以精确控制,对高延时敏感业务不友好。

4.2 MQ 异步补偿(可靠)

架构
失败
更新 DB
发送 MQ 消息
MQ Broker
消费者
删除 Redis 或更新缓存
重试队列

优点 :解耦、保证最终成功。
缺点:引入消息队列,增加运维成本。

4.3 Canal 监听 Binlog(最强一致性)

Canal 伪装成 MySQL 从库,解析 binlog 变更事件,然后异步更新 Redis。
binlog
发送变更消息
消费
MySQL
Canal
MQ
RedisCacheUpdater
Redis

优点 :完全解耦业务代码,无侵入;数据一致性近乎实时。
缺点:部署 Canal 组件,对 DBA 能力有要求。

4.4 方案对比

方案 实时性 业务侵入性 复杂度 推荐场景
延时双删 ~ms 低并发,可容忍短时不一致
MQ 异步 ~100ms 常规读写分离场景
Canal 实时 核心数据强一致性,多服务依赖同一数据源

商品服务建议 :价格、库存对一致性要求较高,采用 MQ 方案Canal;商品描述、图片等对一致性要求低,可用延时双删。


五、总结与最佳实践

问题 解决方案 核心要点
多数据源切换 MyBatis-Plus @DS 或 ShardingJDBC 读写分离,从库负载均衡
本地缓存毛刺 主缓存(永不过期)+ 备份缓存 + 定时刷新 避免批量失效
缓存穿透 布隆过滤器 + 空对象缓存 提前拦截非法 ID
缓存击穿 互斥锁 / 逻辑过期 防止热点 Key 重建 DB
缓存雪崩 随机 TTL + 多级缓存 + Redis 集群 打散过期时间
最终一致性 延时双删(简单)/ MQ(可靠)/ Canal(最强) 根据业务要求选择

商品服务整体架构图
一致性组件
数据层
缓存层
应用层
接入层
负载均衡
API网关
商品服务
Caffeine 本地缓存
动态数据源路由
Redis 集群
MySQL 主库
MySQL 从库1
MySQL 从库2
MySQL
Canal
消息队列

相关推荐
凌虚21 小时前
面向 MySQL 用户的 PostgreSQL 快速上手指南
数据库·后端·架构
努力进修1 天前
KFS 全链路并行架构:异构增量同步的 TB 级性能突破
架构·kfs
ai_coder_ai1 天前
分布式缓存架构设计与实践
分布式·缓存
老王以为1 天前
走进 AI Agent
前端·人工智能·架构
SLD_Allen1 天前
大模型推理引擎架构深度解析:vLLM、SGLang与TensorRT-LLM的技术路线对决
架构·vllm·sglang
cxr8281 天前
Graphify vs GitNexus vs CodeGraph — 三工具架构深度对比
开发语言·架构·知识图谱
奈斯先生Vector1 天前
把 Midjourney 二次编辑做成生产系统:customId 能力令牌、Action Graph 与 WebUI 精修工作台
数据库·人工智能·架构·aigc·音视频·midjourney
隔窗听雨眠1 天前
生产级Kubernetes集群部署完全指南:从kubeadm到高可用架构落地
容器·架构·kubernetes
IT小白杨1 天前
2026亚马逊多店铺运营的环境隔离架构与合规实践
经验分享·架构·业界资讯·指纹浏览器
&Hello Code1 天前
3.解决STM32工程混乱:一套可复用六层架构,GPIO 、LED 实战演示
stm32·嵌入式硬件·架构