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

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

在电商商品服务中,高并发、海量数据、复杂查询是三大核心挑战。本文将结合真实生产经验,从多数据源切换商品详情页分级缓存两个维度,深入剖析实现方案、痛点与最佳实践。内容涵盖:三种多数据源切换方式对比、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
消息队列

相关推荐
豆豆1 小时前
2026年主流CMS技术选型对比:从架构特性到适用场景的深度解析
ai·架构·cms·建站系统·建站平台·内容管理系统·网站管理系统
easy_coder3 小时前
云产品诊断架构设计:路由 + 分层加载方案实践
人工智能·架构·云计算
达达尼昂3 小时前
Claude 多 Agent 系统:从零搭建一个 4 Agent 团队
前端·架构·ai编程
189228048614 小时前
H27QCG8T2ELR-BCF海力士H27QCG8UDBIR-BCB
大数据·服务器·人工智能·科技·缓存
xcLeigh5 小时前
Python开篇:撬动未来的万能钥匙 —— 从入门到架构的全链路指南
数据库·python·架构·教程·应用·网页
小小王app小程序开发5 小时前
海外盲盒小程序开发解析:跨境潮玩商业模式、功能架构与避坑方案
大数据·架构
IronMurphy5 小时前
Redis拷打第一讲
数据库·redis·缓存
Filwaod5 小时前
Java面试:AIGC场景下的技术深度拷问-谢飞机篇
spring boot·缓存·微服务·消息队列·aigc·java面试·ai技术
楠枬6 小时前
Redis 事务
数据库·redis·缓存