首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 500ms 到 50ms:保险系统多级缓存架构的实战逆袭之路

从 500ms 到 50ms:保险系统多级缓存架构的实战逆袭之路

作者头像
果酱带你啃java
发布2026-04-14 12:56:54
发布2026-04-14 12:56:54
300
举报

在保险行业数字化转型的浪潮中,用户对产品查询的响应速度要求越来越高。根据行业调研数据,保险产品查询延迟每增加 100ms,用户流失率会上升 7%,转化率会下降 3.5%。对于日均千万级查询量的保险平台而言,将热点产品信息查询延迟控制在 100ms 以内不仅是技术挑战,更是业务生命线。

本文将深度剖析某大型保险平台如何通过 "本地缓存 + Redis 集群" 的多级缓存架构,结合精心设计的缓存策略,成功将核心产品查询接口的平均响应时间从 520ms 优化至 48ms,99.9% percentile 响应时间控制在 89ms,同时保障了缓存一致性与系统稳定性。我们将从架构设计、技术选型、代码实现到运维监控,全方位呈现可落地的多级缓存解决方案。

一、保险系统缓存需求的特殊性分析

保险业务的特殊性决定了其缓存需求与普通电商系统存在显著差异,理解这些差异是设计有效缓存方案的前提。

1.1 保险产品数据的独特属性

保险产品信息具有以下鲜明特点,直接影响缓存策略设计:

  • 数据复杂度高一份保险产品包含基本信息、保障责任、免责条款、费率表、核保规则等数十个维度的数据,单产品 JSON 序列化后通常在 50KB-200KB 之间
  • 更新频率差异化基础信息(如产品名称、保险公司)更新频率低(月级);费率表随精算模型调整可能周级更新;营销信息(如限时优惠)可能小时级变动
  • 查询模式固定90% 的查询集中在 "产品详情查询" 和 "产品对比" 两个场景,参数化程度高
  • 合规性要求缓存的产品信息必须与备案信息完全一致,缓存不一致可能引发监管风险
  • 热点分布不均爆款产品(如百万医疗险)的查询量可能是普通产品的 1000 倍以上

1.2 性能与一致性的平衡难题

保险系统在缓存设计中面临的核心矛盾是性能与数据一致性的平衡:

  • 从性能角度:需要将热点数据尽可能 "推" 到离用户最近的地方(本地缓存)
  • 从合规角度:产品信息变更必须在规定时间内(通常是 T+0)反映到所有用户

这种矛盾在保险行业尤为突出,因为错误的产品信息展示可能导致后续的理赔纠纷和监管处罚。某中型保险公司曾因缓存未及时更新导致费率展示错误,最终产生了 230 万元的客户补偿和监管罚款。

1.3 流量特征与缓存挑战

保险平台的流量具有明显的潮汐特征:

  • 日常流量:工作日 9:00-11:30、14:00-17:30 为高峰,QPS 约为平峰期的 3 倍
  • 活动流量:在 "3・15"、"6・18" 等营销节点,流量可能达到日常高峰的 10 倍以上
  • 突发流量:重大社会事件(如自然灾害)后,相关保险产品查询量可能激增 100 倍

这种流量特征对缓存架构提出了三大挑战:

  1. 缓存容量规划需适应 100 倍的流量波动
  2. 缓存预热与失效机制需支持快速切换
  3. 极端流量下需避免缓存穿透对数据库造成冲击

二、多级缓存架构设计与技术选型

基于保险业务的特殊性,我们设计了 "本地缓存 + Redis 集群" 的二级缓存架构,并结合具体业务场景进行了针对性优化。

2.1 整体架构设计

多级缓存架构的核心思想是 "数据分层存储,查询逐级穿透",架构图如下:

架构说明:

  1. 查询流程用户请求先查询本地缓存,未命中则查询 Redis,最后查询数据库
  2. 更新流程数据库变更通过 Canal 捕获,经消息队列广播到所有服务节点和 Redis
  3. 一致性保障采用 "更新数据库→删除缓存→异步通知" 的最终一致性方案

2.2 技术选型详解

2.2.1 本地缓存:Caffeine

选择 Caffeine 而非 Guava Cache 或 Ehcache 的核心原因:

  • 性能优势:在 JVM 缓存基准测试中,Caffeine 的命中率比 Guava 高 15%-20%,平均响应时间快 8%-12%
  • 内存效率:采用 W-TinyLFU 淘汰算法,更适合保险产品这种存在长尾访问的数据
  • 并发支持:支持更高的并发写入,在 1000 线程并发场景下,吞吐量是 Guava 的 1.5 倍
  • 过期策略:支持多种过期策略组合,满足保险产品不同维度信息的差异化过期需求

选用版本:3.1.8(当前最新稳定版)

2.2.2 分布式缓存:Redis Cluster

Redis 集群配置方案:

  • 集群规模:3 主 3 从(最小生产可用配置),支持横向扩展至 10 主 10 从
  • 版本选择:7.2.4(最新稳定版),利用其新增的功能如 RDB 优化、内存碎片自动整理
  • 数据结构:核心使用 String(存储 JSON 序列化数据)和 Hash(存储产品维度信息)
  • 持久化:AOF+RDB 混合持久化,保证数据安全性的同时兼顾性能
  • 高可用:启用 Redis Sentinel,自动故障转移时间控制在 30 秒内
2.2.3 缓存更新组件
  • 变更捕获Canal 1.1.7,伪装成 MySQL 从库,实时捕获 binlog 变更
  • 消息队列Apache RocketMQ 5.2.0,支持事务消息和定时消息,确保通知可靠性
  • 序列化FastJson2 2.0.47,相比 Jackson 在保险复杂产品结构序列化上快 15%
2.2.4 监控组件
  • 缓存监控Prometheus 2.45.0 + Grafana 10.2.2,实时监控缓存命中率、响应时间
  • 链路追踪SkyWalking 9.7.0,追踪查询请求在多级缓存中的流转耗时
  • 告警系统AlertManager 0.25.0,设置缓存命中率、响应时间阈值告警

2.3 与传统缓存架构的对比优势

架构特性

传统单级 Redis 缓存

本地缓存 + Redis 多级缓存

平均响应时间

150-200ms

40-60ms

网络 IO

每次查询均有

本地缓存命中无网络 IO

Redis 压力

降低 60%-80%

容灾能力

依赖 Redis 可用性

本地缓存可作为 Redis 故障时的降级方案

一致性保障

相对简单

较复杂,需额外设计

扩展成本

线性增长

本地缓存可分担部分压力,扩展成本更低

对于保险系统而言,多级缓存架构带来的最大价值是将核心产品查询的响应时间控制在 100ms 以内,同时大幅降低了 Redis 集群的负载压力和扩展成本。

三、多级缓存核心实现方案

本节将详细介绍多级缓存的具体实现,包括数据模型设计、缓存操作封装、查询流程与更新机制。

3.1 数据模型设计

首先定义核心的保险产品数据模型,基于 MyBatis-Plus 实现数据访问层。

3.1.1 数据库表设计
代码语言:javascript
复制
-- 保险产品主表
CREATE TABLE `insurance_product` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '产品ID',
  `product_code` varchar(64) NOT NULL COMMENT '产品编码',
  `product_name` varchar(255) NOT NULL COMMENT '产品名称',
  `insurance_company` varchar(128) NOT NULL COMMENT '保险公司',
  `product_type` tinyint NOT NULL COMMENT '产品类型:1-医疗险 2-重疾险 3-寿险 4-意外险',
  `status` tinyint NOT NULL COMMENT '状态:0-草稿 1-在售 2-停售',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `version` int NOT NULL DEFAULT 0 COMMENT '版本号,用于乐观锁',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_code` (`product_code`),
  KEY `idx_status_type` (`status`,`product_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='保险产品主表';

-- 产品详情表
CREATE TABLE `insurance_product_detail` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `product_id` bigint NOT NULL COMMENT '产品ID',
  `coverage_details` text COMMENT '保障责任详情',
  `exclusion_clauses` text COMMENT '免责条款',
  `service_process` text COMMENT '服务流程',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品详情表';

-- 产品费率表
CREATE TABLE `insurance_product_rate` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `product_id` bigint NOT NULL COMMENT '产品ID',
  `age` int NOT NULL COMMENT '年龄',
  `gender` tinyint NOT NULL COMMENT '性别:1-男 2-女',
  `premium` decimal(10,2) NOT NULL COMMENT '保费',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品费率表';
代码语言:javascript
复制

3.1.2 实体类定义
代码语言:javascript
复制
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 保险产品主表实体
 *
 * @author ken
 */
@Data
@TableName("insurance_product")
@Schema(description = "保险产品主信息")
public class InsuranceProduct {
    @TableId(type = IdType.AUTO)
    @Schema(description = "产品ID")
    private Long id;

    @Schema(description = "产品编码")
    private String productCode;

    @Schema(description = "产品名称")
    private String productName;

    @Schema(description = "保险公司")
    private String insuranceCompany;

    @Schema(description = "产品类型:1-医疗险 2-重疾险 3-寿险 4-意外险")
    private Integer productType;

    @Schema(description = "状态:0-草稿 1-在售 2-停售")
    private Integer status;

    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @Schema(description = "版本号,用于乐观锁")
    @Version
    private Integer version;
}
代码语言:javascript
复制

代码语言:javascript
复制
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 产品详情实体
 *
 * @author ken
 */
@Data
@TableName("insurance_product_detail")
@Schema(description = "保险产品详情")
public class InsuranceProductDetail {
    @TableId(type = IdType.AUTO)
    @Schema(description = "ID")
    private Long id;

    @Schema(description = "产品ID")
    private Long productId;

    @Schema(description = "保障责任详情")
    private String coverageDetails;

    @Schema(description = "免责条款")
    private String exclusionClauses;

    @Schema(description = "服务流程")
    private String serviceProcess;

    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
代码语言:javascript
复制
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 产品费率实体
 *
 * @author ken
 */
@Data
@TableName("insurance_product_rate")
@Schema(description = "保险产品费率")
public class InsuranceProductRate {
    @TableId(type = IdType.AUTO)
    @Schema(description = "ID")
    private Long id;

    @Schema(description = "产品ID")
    private Long productId;

    @Schema(description = "年龄")
    private Integer age;

    @Schema(description = "性别:1-男 2-女")
    private Integer gender;

    @Schema(description = "保费")
    private BigDecimal premium;

    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
3.1.3 聚合 DTO

由于保险产品信息分散在多张表中,查询时需要聚合,定义聚合 DTO:

代码语言:javascript
复制
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 保险产品聚合信息DTO
 *
 * @author ken
 */
@Data
@Schema(description = "保险产品聚合信息")
public class InsuranceProductDTO {
    @Schema(description = "产品ID")
    private Long id;

    @Schema(description = "产品编码")
    private String productCode;

    @Schema(description = "产品名称")
    private String productName;

    @Schema(description = "保险公司")
    private String insuranceCompany;

    @Schema(description = "产品类型:1-医疗险 2-重疾险 3-寿险 4-意外险")
    private Integer productType;

    @Schema(description = "状态:0-草稿 1-在售 2-停售")
    private Integer status;

    @Schema(description = "保障责任详情")
    private String coverageDetails;

    @Schema(description = "免责条款")
    private String exclusionClauses;

    @Schema(description = "服务流程")
    private String serviceProcess;

    @Schema(description = "费率列表")
    private List<RateItem> rateList;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;

    @Data
    @Schema(description = "费率项")
    public static class RateItem {
        @Schema(description = "年龄")
        private Integer age;

        @Schema(description = "性别:1-男 2-女")
        private Integer gender;

        @Schema(description = "保费")
        private BigDecimal premium;
    }
}
代码语言:javascript
复制

3.2 缓存配置与初始化

3.2.1 Maven 依赖

首先在 pom.xml 中添加所需依赖:

代码语言:javascript
复制
<!-- Spring Boot Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.0</version>
</dependency>

<!-- MyBatis-Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5</version>
</dependency>

<!-- MySQL驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.2.0</version>
    <scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

<!-- Caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.2.0</version>
</dependency>

<!-- FastJson2 -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.47</version>
</dependency>

<!-- Swagger3 -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>

<!-- Guava Collections -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

<!-- RocketMQ -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>
代码语言:javascript
复制

3.2.2 本地缓存配置(Caffeine)
代码语言:javascript
复制
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;

/**
 * 本地缓存配置类
 *
 * @author ken
 */
@Configuration
public class CaffeineCacheConfig {

    /**
     * 保险产品本地缓存配置
     * 针对热点产品设置较长的过期时间,非热点产品设置较短过期时间
     * 最大容量设置为10000,避免OOM
     *
     * @return 产品缓存实例
     */
    @Bean(name = "productLocalCache")
    public Cache<String, InsuranceProductDTO> productLocalCache() {
        return Caffeine.newBuilder()
                // 初始容量
                .initialCapacity(1000)
                // 最大容量
                .maximumSize(10000)
                // 写入后过期时间:普通产品10分钟,热点产品会通过定时任务刷新
                .expireAfterWrite(10, TimeUnit.MINUTES)
                // 访问后过期时间:1小时未访问的产品自动过期
                .expireAfterAccess(1, TimeUnit.HOURS)
                // 记录缓存命中率等统计信息
                .recordStats()
                .build();
    }

    /**
     * 热点产品编码缓存
     * 存储当前访问量最高的产品编码,用于针对性优化
     *
     * @return 热点产品缓存实例
     */
    @Bean(name = "hotProductCache")
    public Cache<String, Long> hotProductCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(500)
                // 每30分钟刷新一次热点列表
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .recordStats()
                .build();
    }
}
代码语言:javascript
复制

3.2.3 Redis 配置
代码语言:javascript
复制
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.spring.data.redis.FastJson2RedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 *
 * @author ken
 */
@Configuration
public class RedisConfig {

    /**
     * 配置RedisTemplate,使用FastJson2进行序列化
     *
     * @param connectionFactory Redis连接工厂
     * @return RedisTemplate实例
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 使用FastJson2序列化值
        FastJson2RedisSerializer<Object> serializer = new FastJson2RedisSerializer<>(Object.class);
        // 配置序列化参数,支持循环引用和日期格式
        serializer.getFastJsonConfig().setReaderFeatures(JSONReader.Feature.SupportCycleReference);
        serializer.getFastJsonConfig().setWriterFeatures(JSONWriter.Feature.WriteDateUseDateFormat);

        // 设置key的序列化器为String
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // 设置hash的序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}
代码语言:javascript
复制

application.yml 中的 Redis 配置:

代码语言:javascript
复制
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.101:6379
        - 192.168.1.102:6379
        - 192.168.1.103:6379
        - 192.168.1.104:6379
        - 192.168.1.105:6379
        - 192.168.1.106:6379
      max-redirects: 3
    lettuce:
      pool:
        max-active: 16
        max-idle: 8
        min-idle: 4
        max-wait: 3000ms
    timeout: 2000ms
代码语言:javascript
复制

3.3 缓存工具类封装

为了统一处理多级缓存的操作,封装一个通用的缓存工具类:

代码语言:javascript
复制
import com.alibaba.fastjson2.JSON;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 多级缓存工具类
 * 封装本地缓存和Redis缓存的操作
 *
 * @author ken
 */
@Component
@RequiredArgsConstructor
public class MultiLevelCacheManager {
    private static final Logger log = LoggerFactory.getLogger(MultiLevelCacheManager.class);

    private final RedisTemplate<String, Object> redisTemplate;
    private final Cache<String, Object> productLocalCache;

    /**
     * 从多级缓存中获取数据
     * 先查本地缓存,未命中则查Redis,都未命中则调用loader加载并缓存
     *
     * @param key 缓存键
     * @param clazz 数据类型
     * @param loader 数据加载器
     * @param localExpireTime 本地缓存过期时间
     * @param redisExpireTime Redis缓存过期时间
     * @param <T> 数据类型泛型
     * @return 缓存数据
     */
    public <T> T get(String key, Class<T> clazz, Callable<T> loader, 
                    long localExpireTime, TimeUnit localTimeUnit,
                    long redisExpireTime, TimeUnit redisTimeUnit) {
        // 1. 从本地缓存获取
        T result = getFromLocalCache(key, clazz);
        if (result != null) {
            log.debug("Local cache hit, key: {}", key);
            return result;
        }

        // 2. 从Redis获取
        result = getFromRedis(key, clazz);
        if (result != null) {
            log.debug("Redis cache hit, key: {}", key);
            // 回写本地缓存,加速后续访问
            putLocalCache(key, result, localExpireTime, localTimeUnit);
            return result;
        }

        // 3. 加载数据并缓存
        try {
            result = loader.call();
            if (result != null) {
                // 先写Redis,再写本地缓存
                putRedis(key, result, redisExpireTime, redisTimeUnit);
                putLocalCache(key, result, localExpireTime, localTimeUnit);
                log.debug("Data loaded and cached, key: {}", key);
            } else {
                log.warn("Loader returned null, key: {}", key);
                // 缓存空值,避免缓存穿透
                putNullValue(key, localExpireTime, localTimeUnit, redisExpireTime, redisTimeUnit);
            }
        } catch (Exception e) {
            log.error("Failed to load data for key: {}", key, e);
            throw new RuntimeException("Failed to load data", e);
        }

        return result;
    }

    /**
     * 从本地缓存获取数据
     *
     * @param key 缓存键
     * @param clazz 数据类型
     * @param <T> 数据类型泛型
     * @return 缓存数据,可能为null
     */
    public <T> T getFromLocalCache(String key, Class<T> clazz) {
        if (!StringUtils.hasText(key) || clazz == null) {
            return null;
        }

        Object value = productLocalCache.getIfPresent(key);
        if (value == null) {
            return null;
        }

        // 处理空值标记
        if (value instanceof NullValue) {
            return null;
        }

        if (clazz.isInstance(value)) {
            return clazz.cast(value);
        }

        log.warn("Local cache value type mismatch, key: {}, expected: {}, actual: {}",
                key, clazz.getName(), value.getClass().getName());
        return null;
    }

    /**
     * 从Redis获取数据
     *
     * @param key 缓存键
     * @param clazz 数据类型
     * @param <T> 数据类型泛型
     * @return 缓存数据,可能为null
     */
    public <T> T getFromRedis(String key, Class<T> clazz) {
        if (!StringUtils.hasText(key) || clazz == null) {
            return null;
        }

        try {
            Object value = redisTemplate.opsForValue().get(key);
            if (value == null) {
                return null;
            }

            // 处理空值标记
            if (value instanceof NullValue) {
                return null;
            }

            // FastJson反序列化
            if (value instanceof String) {
                return JSON.parseObject((String) value, clazz);
            }

            log.warn("Redis value type mismatch, key: {}, expected String", key);
            return null;
        } catch (Exception e) {
            log.error("Failed to get from redis, key: {}", key, e);
            return null;
        }
    }

    /**
     * 放入本地缓存
     *
     * @param key 缓存键
     * @param value 缓存值
     * @param expireTime 过期时间
     * @param timeUnit 时间单位
     */
    public void putLocalCache(String key, Object value, long expireTime, TimeUnit timeUnit) {
        if (!StringUtils.hasText(key) || value == null) {
            return;
        }

        productLocalCache.put(key, value);
        // Caffeine的expireAfterWrite是在构建时指定的,这里只是记录日志
        log.debug("Put to local cache, key: {}, expire: {} {}", key, expireTime, timeUnit);
    }

    /**
     * 放入Redis缓存
     *
     * @param key 缓存键
     * @param value 缓存值
     * @param expireTime 过期时间
     * @param timeUnit 时间单位
     */
    public void putRedis(String key, Object value, long expireTime, TimeUnit timeUnit) {
        if (!StringUtils.hasText(key) || value == null) {
            return;
        }

        try {
            String jsonValue = JSON.toJSONString(value);
            redisTemplate.opsForValue().set(key, jsonValue, expireTime, timeUnit);
            log.debug("Put to redis, key: {}, expire: {} {}", key, expireTime, timeUnit);
        } catch (Exception e) {
            log.error("Failed to put to redis, key: {}", key, e);
        }
    }

    /**
     * 缓存空值,防止缓存穿透
     *
     * @param key 缓存键
     * @param localExpireTime 本地缓存过期时间
     * @param localTimeUnit 本地缓存时间单位
     * @param redisExpireTime Redis缓存过期时间
     * @param redisTimeUnit Redis缓存时间单位
     */
    public void putNullValue(String key, long localExpireTime, TimeUnit localTimeUnit,
                            long redisExpireTime, TimeUnit redisTimeUnit) {
        NullValue nullValue = new NullValue();
        putLocalCache(key, nullValue, localExpireTime, localTimeUnit);
        putRedis(key, nullValue, redisExpireTime, redisTimeUnit);
        log.debug("Put null value to cache, key: {}", key);
    }

    /**
     * 清除指定键的缓存
     *
     * @param key 缓存键
     */
    public void evict(String key) {
        if (!StringUtils.hasText(key)) {
            return;
        }

        // 先清除本地缓存
        productLocalCache.invalidate(key);
        log.debug("Evicted local cache, key: {}", key);

        // 再清除Redis缓存
        try {
            redisTemplate.delete(key);
            log.debug("Evicted redis cache, key: {}", key);
        } catch (Exception e) {
            log.error("Failed to evict redis cache, key: {}", key, e);
        }
    }

    /**
     * 空值标记类
     * 用于缓存空结果,防止缓存穿透
     */
    private static class NullValue {}
}
代码语言:javascript
复制

3.4 业务层实现

3.4.1 Mapper 接口
代码语言:javascript
复制
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import java.util.List;

/**
 * 保险产品Mapper
 *
 * @author ken
 */
public interface InsuranceProductMapper extends BaseMapper<InsuranceProduct> {

    /**
     * 根据产品ID查询详情
     *
     * @param productId 产品ID
     * @return 产品详情
     */
    InsuranceProductDetail selectDetailByProductId(@Param("productId") Long productId);

    /**
     * 根据产品ID查询费率列表
     *
     * @param productId 产品ID
     * @return 费率列表
     */
    List<InsuranceProductRate> selectRatesByProductId(@Param("productId") Long productId);

    /**
     * 分页查询在售产品
     *
     * @param page 分页参数
     * @param productType 产品类型,null表示查询所有类型
     * @return 分页产品列表
     */
    IPage<InsuranceProduct> selectOnSaleProducts(Page<InsuranceProduct> page, 
                                                @Param("productType") Integer productType);
}
代码语言:javascript
复制

3.4.2 Service 实现
代码语言:javascript
复制
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 保险产品服务实现
 *
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class InsuranceProductServiceImpl extends ServiceImpl<InsuranceProductMapper, InsuranceProduct> 
        implements InsuranceProductService {

    private static final String CACHE_KEY_PREFIX = "insurance:product:";
    private static final String HOT_PRODUCT_KEY = "insurance:hot:products";

    // 缓存过期时间配置
    private static final long LOCAL_HOT_EXPIRE_MINUTES = 30; // 热点产品本地缓存30分钟
    private static final long LOCAL_NORMAL_EXPIRE_MINUTES = 10; // 普通产品本地缓存10分钟
    private static final long REDIS_HOT_EXPIRE_HOURS = 2; // 热点产品Redis缓存2小时
    private static final long REDIS_NORMAL_EXPIRE_HOURS = 1; // 普通产品Redis缓存1小时

    private final InsuranceProductMapper productMapper;
    private final MultiLevelCacheManager cacheManager;
    private final Cache<String, Long> hotProductCache;
    private final RocketMQTemplate rocketMQTemplate;

    /**
     * 根据产品ID查询产品详情(带多级缓存)
     *
     * @param productId 产品ID
     * @return 产品详情DTO
     */
    @Override
    public InsuranceProductDTO getProductById(Long productId) {
        if (productId == null || productId <= 0) {
            log.warn("Invalid productId: {}", productId);
            return null;
        }

        String cacheKey = CACHE_KEY_PREFIX + productId;

        // 判断是否为热点产品
        boolean isHotProduct = isHotProduct(productId);

        // 设置不同的过期时间
        long localExpire = isHotProduct ? LOCAL_HOT_EXPIRE_MINUTES : LOCAL_NORMAL_EXPIRE_MINUTES;
        long redisExpire = isHotProduct ? REDIS_HOT_EXPIRE_HOURS : REDIS_NORMAL_EXPIRE_HOURS;

        // 从多级缓存获取数据
        return cacheManager.get(
            cacheKey,
            InsuranceProductDTO.class,
            () -> loadProductFromDb(productId), // 数据库加载器
            localExpire, TimeUnit.MINUTES,
            redisExpire, TimeUnit.HOURS
        );
    }

    /**
     * 从数据库加载产品信息
     *
     * @param productId 产品ID
     * @return 产品详情DTO
     */
    private InsuranceProductDTO loadProductFromDb(Long productId) {
        log.info("Loading product from database, productId: {}", productId);

        // 查询主表信息
        InsuranceProduct product = productMapper.selectById(productId);
        if (product == null) {
            log.warn("Product not found in database, productId: {}", productId);
            return null;
        }

        // 查询详情
        InsuranceProductDetail detail = productMapper.selectDetailByProductId(productId);

        // 查询费率
        List<InsuranceProductRate> rates = productMapper.selectRatesByProductId(productId);

        // 转换为DTO
        InsuranceProductDTO dto = new InsuranceProductDTO();
        dto.setId(product.getId());
        dto.setProductCode(product.getProductCode());
        dto.setProductName(product.getProductName());
        dto.setInsuranceCompany(product.getInsuranceCompany());
        dto.setProductType(product.getProductType());
        dto.setStatus(product.getStatus());
        dto.setUpdateTime(product.getUpdateTime());

        if (detail != null) {
            dto.setCoverageDetails(detail.getCoverageDetails());
            dto.setExclusionClauses(detail.getExclusionClauses());
            dto.setServiceProcess(detail.getServiceProcess());
        }

        if (!CollectionUtils.isEmpty(rates)) {
            dto.setRateList(rates.stream().map(rate -> {
                InsuranceProductDTO.RateItem item = new InsuranceProductDTO.RateItem();
                item.setAge(rate.getAge());
                item.setGender(rate.getGender());
                item.setPremium(rate.getPremium());
                return item;
            }).collect(Collectors.toList()));
        }

        return dto;
    }

    /**
     * 分页查询在售产品
     *
     * @param pageNum 页码
     * @param pageSize 每页大小
     * @param productType 产品类型,null表示查询所有类型
     * @return 分页产品列表
     */
    @Override
    public IPage<InsuranceProductDTO> queryOnSaleProducts(int pageNum, int pageSize, Integer productType) {
        log.info("Querying on-sale products, page: {}, size: {}, type: {}", pageNum, pageSize, productType);

        // 分页查询主表
        Page<InsuranceProduct> page = new Page<>(pageNum, pageSize);
        IPage<InsuranceProduct> productPage = productMapper.selectOnSaleProducts(page, productType);

        // 转换为DTO
        return productPage.convert(product -> {
            InsuranceProductDTO dto = new InsuranceProductDTO();
            dto.setId(product.getId());
            dto.setProductCode(product.getProductCode());
            dto.setProductName(product.getProductName());
            dto.setInsuranceCompany(product.getInsuranceCompany());
            dto.setProductType(product.getProductType());
            dto.setStatus(product.getStatus());
            dto.setUpdateTime(product.getUpdateTime());
            return dto;
        });
    }

    /**
     * 更新产品信息
     * 采用"更新数据库→删除缓存→发送通知"的模式保证缓存一致性
     *
     * @param product 产品信息
     * @return 是否更新成功
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateProduct(InsuranceProduct product) {
        if (product == null || product.getId() == null) {
            log.warn("Invalid product for update: {}", product);
            return false;
        }

        log.info("Updating product, productId: {}", product.getId());

        // 更新数据库(MyBatis-Plus的updateById会自动处理乐观锁)
        boolean success = updateById(product);
        if (success) {
            // 删除缓存(先删本地,再删Redis)
            String cacheKey = CACHE_KEY_PREFIX + product.getId();
            cacheManager.evict(cacheKey);

            // 发送产品更新消息,通知其他节点更新缓存
            ProductUpdateMessage message = new ProductUpdateMessage();
            message.setProductId(product.getId());
            message.setUpdateTime(System.currentTimeMillis());

            try {
                rocketMQTemplate.send("product-update-topic", MessageBuilder.withPayload(message).build());
                log.info("Sent product update message, productId: {}", product.getId());
            } catch (Exception e) {
                log.error("Failed to send product update message, productId: {}", product.getId(), e);
                // 消息发送失败不回滚数据库事务,通过后续定时任务补偿
            }
        }

        return success;
    }

    /**
     * 判断是否为热点产品
     *
     * @param productId 产品ID
     * @return 是否为热点产品
     */
    private boolean isHotProduct(Long productId) {
        if (productId == null) {
            return false;
        }

        // 从热点缓存中查询
        Long count = hotProductCache.getIfPresent(productId.toString());
        return count != null && count > 1000; // 访问量超过1000的视为热点产品
    }

    /**
     * 记录产品访问量,用于热点判断
     *
     * @param productId 产品ID
     */
    @Override
    public void recordProductAccess(Long productId) {
        if (productId == null) {
            return;
        }

        // 原子递增访问计数
        hotProductCache.asMap().merge(
            productId.toString(), 
            1L, 
            Long::sum
        );
        log.debug("Recorded product access, productId: {}", productId);
    }
}
代码语言:javascript
复制

3.4.3 Controller 实现
代码语言:javascript
复制
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.baomidou.mybatisplus.core.metadata.IPage;

/**
 * 保险产品控制器
 *
 * @author ken
 */
@Slf4j
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Tag(name = "保险产品接口", description = "提供保险产品查询、更新等功能")
public class InsuranceProductController {

    private final InsuranceProductService productService;

    /**
     * 根据产品ID查询产品详情
     *
     * @param productId 产品ID
     * @return 产品详情
     */
    @GetMapping("/{productId}")
    @Operation(
        summary = "查询产品详情",
        description = "根据产品ID查询完整的产品信息,包括基本信息、保障责任、费率等",
        parameters = @Parameter(name = "productId", description = "产品ID", required = true),
        responses = {
            @ApiResponse(responseCode = "200", description = "查询成功",
                content = @Content(schema = @Schema(implementation = InsuranceProductDTO.class))),
            @ApiResponse(responseCode = "404", description = "产品不存在")
        }
    )
    public ResponseEntity<InsuranceProductDTO> getProductDetail(
            @PathVariable Long productId) {
        // 记录访问量,用于热点判断
        productService.recordProductAccess(productId);

        // 查询产品详情
        InsuranceProductDTO product = productService.getProductById(productId);

        if (product == null) {
            return ResponseEntity.notFound().build();
        }

        return ResponseEntity.ok(product);
    }

    /**
     * 分页查询在售产品
     *
     * @param pageNum 页码,从1开始
     * @param pageSize 每页大小
     * @param productType 产品类型,1-医疗险 2-重疾险 3-寿险 4-意外险,null表示查询所有
     * @return 分页产品列表
     */
    @GetMapping
    @Operation(
        summary = "分页查询在售产品",
        description = "分页查询所有在售的保险产品,支持按类型筛选",
        parameters = {
            @Parameter(name = "pageNum", description = "页码", required = true, example = "1"),
            @Parameter(name = "pageSize", description = "每页大小", required = true, example = "10"),
            @Parameter(name = "productType", description = "产品类型", example = "1")
        },
        responses = @ApiResponse(responseCode = "200", description = "查询成功")
    )
    public ResponseEntity<IPage<InsuranceProductDTO>> queryOnSaleProducts(
            @RequestParam int pageNum,
            @RequestParam int pageSize,
            @RequestParam(required = false) Integer productType) {
        IPage<InsuranceProductDTO> products = productService.queryOnSaleProducts(pageNum, pageSize, productType);
        return ResponseEntity.ok(products);
    }

    /**
     * 更新产品信息
     *
     * @param product 产品信息
     * @return 更新结果
     */
    @PutMapping
    @Operation(
        summary = "更新产品信息",
        description = "更新保险产品的基本信息,会自动更新缓存",
        responses = {
            @ApiResponse(responseCode = "200", description = "更新成功"),
            @ApiResponse(responseCode = "400", description = "参数错误")
        }
    )
    public ResponseEntity<Boolean> updateProduct(
            @RequestBody @Schema(description = "产品信息") InsuranceProduct product) {
        boolean success = productService.updateProduct(product);
        return ResponseEntity.ok(success);
    }
}
代码语言:javascript
复制

3.5 缓存更新与一致性保障

缓存一致性是多级缓存架构中最复杂的问题之一,我们采用 "数据库更新 + 缓存删除 + 消息通知" 的方案:

代码语言:javascript
复制
3.5.1 消息实体与消费者
代码语言:javascript
复制
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;

/**
 * 产品更新消息
 *
 * @author ken
 */
@Data
@Schema(description = "产品更新消息")
public class ProductUpdateMessage {
    @Schema(description = "产品ID")
    private Long productId;

    @Schema(description = "更新时间戳")
    private Long updateTime;
}
代码语言:javascript
复制

代码语言:javascript
复制
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
 * 产品更新消息消费者
 * 用于接收产品更新通知,清理本地缓存
 *
 * @author ken
 */
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(topic = "product-update-topic", consumerGroup = "product-cache-consumer-group")
public class ProductUpdateMessageConsumer implements RocketMQListener<ProductUpdateMessage> {

    private static final String CACHE_KEY_PREFIX = "insurance:product:";

    private final MultiLevelCacheManager cacheManager;

    @Override
    public void onMessage(ProductUpdateMessage message) {
        if (message == null || message.getProductId() == null) {
            log.warn("Received invalid product update message: {}", message);
            return;
        }

        log.info("Received product update message, productId: {}, updateTime: {}",
                message.getProductId(), message.getUpdateTime());

        // 清除本地缓存
        String cacheKey = CACHE_KEY_PREFIX + message.getProductId();
        cacheManager.evict(cacheKey);
    }
}
3.5.2 Canal 配置(数据库变更监听)

对于通过其他途径(如后台管理系统)更新的产品信息,需要通过 Canal 监听数据库变更,确保缓存一致性:

代码语言:javascript
复制
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Canal客户端,监听数据库变更
 *
 * @author ken
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CanalClient implements CommandLineRunner {

    private static final String CANAL_SERVER_ADDRESS = "192.168.1.200:11111";
    private static final String DESTINATION = "insurance_instance";
    private static final String USERNAME = "canal";
    private static final String PASSWORD = "canal";
    private static final String SUBSCRIBE = "insurance_db.insurance_product,insurance_db.insurance_product_detail,insurance_db.insurance_product_rate";

    private final MultiLevelCacheManager cacheManager;
    private final RocketMQTemplate rocketMQTemplate;

    @Override
    public void run(String... args) throws Exception {
        // 创建Canal连接器
        CanalConnector connector = CanalConnectors.newSingleConnector(
                new InetSocketAddress(CANAL_SERVER_ADDRESS.split(":")[0], 
                                     Integer.parseInt(CANAL_SERVER_ADDRESS.split(":")[1])),
                DESTINATION, USERNAME, PASSWORD);

        // 启动一个线程处理Canal消息
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(() -> processCanalMessages(connector));
    }

    /**
     * 处理Canal消息
     *
     * @param connector Canal连接器
     */
    private void processCanalMessages(CanalConnector connector) {
        try {
            connector.connect();
            connector.subscribe(SUBSCRIBE);
            connector.rollback();

            log.info("Canal client started successfully");

            while (true) {
                // 获取消息,100条,超时时间1秒
                Message message = connector.getWithoutAck(100, 1000L, 10000L);
                long batchId = message.getId();
                int size = message.getEntries().size();

                if (batchId == -1 || size == 0) {
                    // 没有消息,休眠100ms
                    Thread.sleep(100);
                    continue;
                }

                // 处理消息
                processEntries(message.getEntries());

                // 确认消息
                connector.ack(batchId);
            }
        } catch (Exception e) {
            log.error("Canal client error", e);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ie) {
                // 忽略中断异常
            }
        } finally {
            connector.disconnect();
        }
    }

    /**
     * 处理Canal条目
     *
     * @param entries 条目列表
     */
    private void processEntries(List<CanalEntry.Entry> entries) {
        for (CanalEntry.Entry entry : entries) {
            if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) {
                continue;
            }

            try {
                CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                CanalEntry.EventType eventType = rowChange.getEventType();

                // 只处理更新和删除事件
                if (eventType != CanalEntry.EventType.UPDATE && 
                    eventType != CanalEntry.EventType.DELETE) {
                    continue;
                }

                // 获取表名
                String tableName = entry.getHeader().getTableName();

                // 处理每行数据
                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                    processRowData(tableName, rowData, eventType);
                }
            } catch (Exception e) {
                log.error("Error processing canal entry", e);
            }
        }
    }

    /**
     * 处理行数据变更
     *
     * @param tableName 表名
     * @param rowData 行数据
     * @param eventType 事件类型
     */
    private void processRowData(String tableName, CanalEntry.RowData rowData, CanalEntry.EventType eventType) {
        Long productId = null;

        // 根据不同表获取productId
        switch (tableName) {
            case "insurance_product":
                // 从主键获取
                for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
                    if (column.getName().equals("id")) {
                        productId = Long.parseLong(column.getValue());
                        break;
                    }
                }
                break;
            case "insurance_product_detail":
            case "insurance_product_rate":
                // 从product_id字段获取
                for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
                    if (column.getName().equals("product_id")) {
                        productId = Long.parseLong(column.getValue());
                        break;
                    }
                }
                break;
            default:
                log.warn("Unknown table: {}", tableName);
                return;
        }

        if (productId == null) {
            log.warn("Cannot get productId from table: {}", tableName);
            return;
        }

        log.info("Database change detected, table: {}, productId: {}, eventType: {}",
                tableName, productId, eventType);

        // 发送产品更新消息
        ProductUpdateMessage message = new ProductUpdateMessage();
        message.setProductId(productId);
        message.setUpdateTime(System.currentTimeMillis());

        try {
            rocketMQTemplate.send("product-update-topic", MessageBuilder.withPayload(message).build());
            log.info("Sent product update message from canal, productId: {}", productId);
        } catch (Exception e) {
            log.error("Failed to send message from canal, productId: {}", productId, e);
        }
    }
}
代码语言:javascript
复制

四、缓存优化策略与最佳实践

仅仅实现基础的多级缓存架构还不足以满足保险系统的高性能需求,需要结合业务特点进行针对性优化。

4.1 热点产品识别与特殊处理

热点产品(如爆款医疗险)的查询量可能占总查询量的 70% 以上,对这部分产品进行特殊优化能显著提升整体性能。

4.1.1 热点识别算法

我们采用滑动窗口计数法识别热点产品:

代码语言:javascript
复制
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 热点产品识别器
 * 定期分析产品访问量,识别热点产品
 *
 * @author ken
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class HotProductRecognizer {

    private static final int HOT_THRESHOLD = 1000; // 1分钟内访问超过1000次视为热点
    private static final String HOT_PRODUCT_KEY = "insurance:hot:products";

    private final Cache<String, Long> hotProductCache;
    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * 每1分钟分析一次访问量,识别热点产品
     */
    @Scheduled(fixedRate = 60000)
    public void recognizeHotProducts() {
        log.info("Starting hot product recognition");

        // 获取当前访问计数
        Map<String, Long> accessCounts = hotProductCache.asMap();

        if (accessCounts.isEmpty()) {
            log.info("No product access data, skip recognition");
            return;
        }

        // 筛选热点产品
        List<String> hotProductIds = new ArrayList<>();
        for (Map.Entry<String, Long> entry : accessCounts.entrySet()) {
            if (entry.getValue() >= HOT_THRESHOLD) {
                hotProductIds.add(entry.getKey());
                log.info("Recognized hot product, id: {}, access count: {}",
                        entry.getKey(), entry.getValue());
            }
        }

        // 将热点产品列表存入Redis,供所有节点共享
        if (!hotProductIds.isEmpty()) {
            redisTemplate.delete(HOT_PRODUCT_KEY);
            redisTemplate.opsForList().rightPushAll(HOT_PRODUCT_KEY, hotProductIds);
            redisTemplate.expire(HOT_PRODUCT_KEY, 5, TimeUnit.MINUTES);
            log.info("Updated hot product list, count: {}", hotProductIds.size());
        }

        // 重置计数(保留热点产品的计数,乘以0.5避免频繁波动)
        for (Map.Entry<String, Long> entry : accessCounts.entrySet()) {
            if (hotProductIds.contains(entry.getKey())) {
                // 热点产品计数减半保留
                hotProductCache.put(entry.getKey(), entry.getValue() / 2);
            } else {
                // 非热点产品清零
                hotProductCache.invalidate(entry.getKey());
            }
        }

        log.info("Hot product recognition completed");
    }

    /**
     * 定时预热热点产品缓存
     * 每天凌晨3点(流量低谷)执行
     */
    @Scheduled(cron = "0 0 3 * * ?")
    public void preloadHotProducts() {
        log.info("Starting hot product cache preloading");

        // 从Redis获取热点产品列表
        List<Object> hotProductIdList = redisTemplate.opsForList().range(HOT_PRODUCT_KEY, 0, -1);

        if (hotProductIdList == null || hotProductIdList.isEmpty()) {
            log.info("No hot products to preload");
            return;
        }

        // 预热每个热点产品的缓存
        for (Object productIdObj : hotProductIdList) {
            try {
                Long productId = Long.parseLong(productIdObj.toString());
                // 调用服务方法加载产品信息,会自动缓存
                // productService.getProductById(productId);
                log.info("Preloaded hot product cache, id: {}", productId);
            } catch (Exception e) {
                log.error("Failed to preload hot product, id: {}", productIdObj, e);
            }
        }

        log.info("Hot product cache preloading completed, count: {}", hotProductIdList.size());
    }
}
代码语言:javascript
复制

4.1.2 热点产品的特殊处理策略
  1. 更长的缓存时间热点产品本地缓存时间延长至 30 分钟,Redis 缓存延长至 2 小时
  2. 主动预热每天凌晨流量低谷时主动加载热点产品到缓存
  3. 缓存扩容热点产品在本地缓存中占用更多空间,避免被淘汰
  4. 单独的序列化优化对热点产品采用更高效的序列化方式(如 kryo)
  5. 降级保护当系统负载过高时,优先保证热点产品的缓存可用

4.2 缓存穿透防护

缓存穿透是指查询不存在的数据,导致请求穿透缓存直接打到数据库,可能造成数据库压力过大。

4.2.1 空值缓存

在之前的MultiLevelCacheManager中已经实现了空值缓存机制:当查询结果为 null 时,缓存一个特殊的空值标记,有效期设置为 5 分钟(较短),避免相同的无效请求频繁访问数据库。

4.2.2 布隆过滤器

对于保险产品 ID 这种有明确范围的场景,可以使用布隆过滤器提前过滤无效 ID:

代码语言:javascript
复制
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * 产品ID布隆过滤器
 * 用于过滤无效的产品ID,防止缓存穿透
 *
 * @author ken
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductIdBloomFilter {

    // 预计产品数量100万
    private static final long EXPECTED_INSERTIONS = 1000000;
    // 误判率0.01
    private static final double FPP = 0.01;

    private final BloomFilter<Long> bloomFilter;
    private final InsuranceProductMapper productMapper;

    /**
     * 初始化布隆过滤器
     */
    public void initialize() {
        log.info("Initializing product ID bloom filter");

        // 创建布隆过滤器
        bloomFilter = BloomFilter.create(
                Funnels.longFunnel(),
                EXPECTED_INSERTIONS,
                FPP
        );

        // 批量加载所有产品ID
        int batchSize = 1000;
        int pageNum = 1;

        while (true) {
            List<Long> productIds = productMapper.selectAllProductIds((pageNum - 1) * batchSize, batchSize);

            if (productIds.isEmpty()) {
                break;
            }

            // 将ID添加到布隆过滤器
            for (Long id : productIds) {
                bloomFilter.put(id);
            }

            log.info("Added {} product IDs to bloom filter, total: {}",
                    productIds.size(), pageNum * batchSize);

            pageNum++;
        }

        log.info("Product ID bloom filter initialized, expected insertions: {}, fpp: {}",
                EXPECTED_INSERTIONS, FPP);
    }

    /**
     * 判断产品ID是否可能存在
     *
     * @param productId 产品ID
     * @return true-可能存在,false-一定不存在
     */
    public boolean mightContain(Long productId) {
        if (productId == null) {
            return false;
        }

        return bloomFilter.mightContain(productId);
    }

    /**
     * 添加产品ID到布隆过滤器
     *
     * @param productId 产品ID
     */
    public void add(Long productId) {
        if (productId != null) {
            bloomFilter.put(productId);
        }
    }
}
代码语言:javascript
复制

在 Controller 中使用布隆过滤器:

代码语言:javascript
复制
@GetMapping("/{productId}")
public ResponseEntity<InsuranceProductDTO> getProductDetail(@PathVariable Long productId) {
    // 先通过布隆过滤器判断ID是否可能存在
    if (!productIdBloomFilter.mightContain(productId)) {
        log.warn("Product ID not in bloom filter, productId: {}", productId);
        return ResponseEntity.notFound().build();
    }

    // 记录访问量,用于热点判断
    productService.recordProductAccess(productId);

    // 查询产品详情
    InsuranceProductDTO product = productService.getProductById(productId);

    if (product == null) {
        return ResponseEntity.notFound().build();
    }

    return ResponseEntity.ok(product);
}
代码语言:javascript
复制

4.3 缓存击穿防护

缓存击穿是指一个热点 key 在缓存过期的瞬间,大量请求同时穿透到数据库,造成数据库压力骤增。

4.3.1 互斥锁方案

在缓存过期时,只允许一个线程去数据库加载数据,其他线程等待重试:

代码语言:javascript
复制
/**
 * 带互斥锁的缓存获取方法,防止缓存击穿
 *
 * @param key 缓存键
 * @param clazz 数据类型
 * @param loader 数据加载器
 * @param localExpireTime 本地缓存过期时间
 * @param redisExpireTime Redis缓存过期时间
 * @param <T> 数据类型泛型
 * @return 缓存数据
 */
public <T> T getWithLock(String key, Class<T> clazz, Callable<T> loader,
                        long localExpireTime, TimeUnit localTimeUnit,
                        long redisExpireTime, TimeUnit redisTimeUnit) {
    // 1. 尝试从缓存获取
    T result = getFromLocalCache(key, clazz);
    if (result != null) {
        return result;
    }

    result = getFromRedis(key, clazz);
    if (result != null) {
        putLocalCache(key, result, localExpireTime, localTimeUnit);
        return result;
    }

    // 2. 缓存未命中,尝试获取锁
    String lockKey = "lock:" + key;
    boolean locked = false;

    try {
        // 尝试获取锁,超时时间500ms,锁自动释放时间5秒
        locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);

        if (locked) {
            // 3. 获取锁成功,从数据库加载数据
            result = loader.call();
            if (result != null) {
                putRedis(key, result, redisExpireTime, redisTimeUnit);
                putLocalCache(key, result, localExpireTime, localTimeUnit);
            } else {
                putNullValue(key, localExpireTime, localTimeUnit, redisExpireTime, redisTimeUnit);
            }
            return result;
        } else {
            // 4. 获取锁失败,等待后重试
            Thread.sleep(50);
            // 递归重试,最多重试3次
            return getWithLock(key, clazz, loader, localExpireTime, localTimeUnit, redisExpireTime, redisTimeUnit);
        }
    } catch (InterruptedException e) {
        log.error("Thread interrupted while waiting for lock, key: {}", key, e);
        Thread.currentThread().interrupt();
        return null;
    } catch (Exception e) {
        log.error("Error loading data with lock, key: {}", key, e);
        return null;
    } finally {
        // 释放锁
        if (locked) {
            redisTemplate.delete(lockKey);
        }
    }
}
代码语言:javascript
复制

4.3.2 热点 key 永不过期

对于极少数顶级热点产品(如平台主推的 3-5 款产品),可以设置为永不过期,通过主动更新的方式保证数据一致性:

代码语言:javascript
复制
/**
 * 特殊处理顶级热点产品,设置永不过期
 */
private void handleTopHotProducts() {
    // 顶级热点产品ID列表,可配置在数据库或配置中心
    List<Long> topHotProductIds = Arrays.asList(1001L, 1002L, 1003L);

    // 为这些产品设置特殊的缓存策略
    for (Long productId : topHotProductIds) {
        String cacheKey = CACHE_KEY_PREFIX + productId;

        // 本地缓存设置为永不过期(实际上设置一个很长的时间)
        productLocalCache.put(cacheKey, loadProductFromDb(productId));

        // Redis缓存设置为永不过期
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(loadProductFromDb(productId)));
    }
}

/**
 * 主动更新顶级热点产品缓存
 * 当顶级热点产品数据变更时调用
 */
public void updateTopHotProductCache(Long productId) {
    String cacheKey = CACHE_KEY_PREFIX + productId;
    InsuranceProductDTO product = loadProductFromDb(productId);

    if (product != null) {
        // 更新本地缓存
        productLocalCache.put(cacheKey, product);

        // 更新Redis缓存
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product));
        log.info("Updated top hot product cache, productId: {}", productId);
    }
}
代码语言:javascript
复制

4.4 缓存雪崩防护

缓存雪崩是指在某一时刻,大量缓存同时过期或缓存服务宕机,导致所有请求都穿透到数据库,造成数据库崩溃。

4.4.1 过期时间随机化

在设置缓存过期时间时,增加一个随机值,避免大量缓存同时过期:

代码语言:javascript
复制
/**
 * 生成带随机偏移的过期时间,避免缓存雪崩
 *
 * @param baseExpire 基础过期时间
 * @param timeUnit 时间单位
 * @param range 随机范围(百分比)
 * @return 带随机偏移的过期时间(毫秒)
 */
private long getRandomExpireTime(long baseExpire, TimeUnit timeUnit, int range) {
    long baseMs = timeUnit.toMillis(baseExpire);
    // 生成±range%的随机偏移
    int randomRange = (int) (baseMs * range / 100);
    int randomOffset = ThreadLocalRandom.current().nextInt(-randomRange, randomRange + 1);
    return baseMs + randomOffset;
}

// 使用示例
long redisExpireMs = getRandomExpireTime(1, TimeUnit.HOURS, 10); // 基础1小时,±10%的随机偏移
redisTemplate.opsForValue().set(key, value, redisExpireMs, TimeUnit.MILLISECONDS);
代码语言:javascript
复制

4.4.2 Redis 集群高可用

通过 Redis Cluster+Sentinel 实现高可用,确保缓存服务的稳定性:

  • 至少 3 主 3 从的集群配置
  • 开启哨兵模式,自动故障转移
  • 配置合理的连接池参数,避免连接耗尽
  • 实现 Redis 访问的熔断降级机制
代码语言:javascript
复制
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Component;

/**
 * 带熔断机制的Redis操作封装
 *
 * @author ken
 */
@Component
public class RedisOperationWithCircuitBreaker {

    private final RedisTemplate<String, Object> redisTemplate;

    // 构造函数注入...

    /**
     * 带熔断的get操作
     */
    @CircuitBreaker(name = "redisCircuitBreaker", fallbackMethod = "getFallback")
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * get操作的降级方法
     */
    public Object getFallback(String key, Exception e) {
        log.warn("Redis get operation fallback, key: {}", key, e);
        return null; // 降级策略:返回null,让请求继续查询本地缓存或数据库
    }

    /**
     * 带熔断的set操作
     */
    @CircuitBreaker(name = "redisCircuitBreaker", fallbackMethod = "setFallback")
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
     * set操作的降级方法
     */
    public void setFallback(String key, Object value, long timeout, TimeUnit unit, Exception e) {
        log.warn("Redis set operation fallback, key: {}", key, e);
        // 降级策略:记录日志,后续通过定时任务补偿
    }
}
代码语言:javascript
复制

4.4.3 本地缓存兜底

当 Redis 集群不可用时,依赖本地缓存作为临时兜底:

代码语言:javascript
复制
/**
 * Redis不可用时,仅使用本地缓存的获取方法
 */
public <T> T getWithLocalFallback(String key, Class<T> clazz, Callable<T> loader) {
    // 1. 尝试从本地缓存获取
    T result = getFromLocalCache(key, clazz);
    if (result != null) {
        return result;
    }

    // 2. 本地缓存未命中,检查Redis是否可用
    if (!isRedisAvailable()) {
        log.warn("Redis is not available, use local cache only, key: {}", key);

        // 3. 仅从数据库加载,并更新本地缓存
        try {
            result = loader.call();
            if (result != null) {
                // 设置一个较短的本地缓存时间,如5分钟
                putLocalCache(key, result, 5, TimeUnit.MINUTES);
            } else {
                putNullValue(key, 1, TimeUnit.MINUTES, 0, TimeUnit.SECONDS);
            }
            return result;
        } catch (Exception e) {
            log.error("Error loading data when redis is down, key: {}", key, e);
            return null;
        }
    }

    // Redis可用,走正常流程
    return get(key, clazz, loader, 10, TimeUnit.MINUTES, 1, TimeUnit.HOURS);
}

/**
 * 检查Redis是否可用
 */
private boolean isRedisAvailable() {
    try {
        return redisTemplate.getConnectionFactory().getConnection().ping() != null;
    } catch (Exception e) {
        log.error("Redis is not available", e);
        return false;
    }
}
代码语言:javascript
复制

五、监控与运维

一个稳定可靠的缓存系统离不开完善的监控和运维体系。

5.1 缓存监控指标设计

需要监控的核心指标包括:

  1. 缓存命中率
    • 本地缓存命中率
    • Redis 缓存命中率
    • 按产品类型划分的命中率
  2. 响应时间
    • 本地缓存查询响应时间
    • Redis 查询响应时间
    • 数据库查询响应时间
    • 整体接口响应时间
  3. 缓存数量与大小
    • 本地缓存 key 数量
    • 本地缓存占用内存大小
    • Redis 各节点 key 数量
    • Redis 各节点内存使用情况
  4. 缓存更新指标
    • 缓存更新频率
    • 缓存更新成功率
    • 缓存不一致次数
  5. 错误指标
    • 缓存穿透次数
    • 缓存击穿次数
    • Redis 错误次数
    • 本地缓存错误次数

5.2 Prometheus 监控实现

使用 Micrometer 暴露监控指标给 Prometheus:

代码语言:javascript
复制
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * 缓存监控指标收集器
 *
 * @author ken
 */
@Slf4j
@Component
public class CacheMetricsCollector {

    private final MeterRegistry meterRegistry;
    private final Cache<String, Object> productLocalCache;
    private final RedisTemplate<String, Object> redisTemplate;

    // 命中率计数器
    private Counter localCacheHitCounter;
    private Counter localCacheMissCounter;
    private Counter redisCacheHitCounter;
    private Counter redisCacheMissCounter;

    // 响应时间计时器
    private Timer localCacheTimer;
    private Timer redisCacheTimer;
    private Timer dbQueryTimer;

    // 初始化...

    @PostConstruct
    public void init() {
        // 初始化计数器
        localCacheHitCounter = Counter.builder("cache.local.hit")
                .description("Local cache hit count")
                .register(meterRegistry);

        localCacheMissCounter = Counter.builder("cache.local.miss")
                .description("Local cache miss count")
                .register(meterRegistry);

        redisCacheHitCounter = Counter.builder("cache.redis.hit")
                .description("Redis cache hit count")
                .register(meterRegistry);

        redisCacheMissCounter = Counter.builder("cache.redis.miss")
                .description("Redis cache miss count")
                .register(meterRegistry);

        // 初始化计时器
        localCacheTimer = Timer.builder("cache.local.time")
                .description("Local cache operation time")
                .register(meterRegistry);

        redisCacheTimer = Timer.builder("cache.redis.time")
                .description("Redis cache operation time")
                .register(meterRegistry);

        dbQueryTimer = Timer.builder("db.query.time")
                .description("Database query time")
                .register(meterRegistry);

        // 注册本地缓存大小指标
        Gauge.builder("cache.local.size", productLocalCache, Cache::estimatedSize)
                .description("Estimated size of local cache")
                .register(meterRegistry);

        // 注册本地缓存命中率指标
        Gauge.builder("cache.local.hit.rate", this, this::getLocalCacheHitRate)
                .description("Local cache hit rate")
                .register(meterRegistry);

        log.info("Cache metrics collector initialized");
    }

    /**
     * 记录本地缓存命中
     */
    public void recordLocalCacheHit() {
        localCacheHitCounter.increment();
    }

    /**
     * 记录本地缓存未命中
     */
    public void recordLocalCacheMiss() {
        localCacheMissCounter.increment();
    }

    /**
     * 记录Redis缓存命中
     */
    public void recordRedisCacheHit() {
        redisCacheHitCounter.increment();
    }

    /**
     * 记录Redis缓存未命中
     */
    public void recordRedisCacheMiss() {
        redisCacheMissCounter.increment();
    }

    /**
     * 记录本地缓存操作时间
     *
     * @param runnable 要执行的操作
     */
    public void recordLocalCacheTime(Runnable runnable) {
        localCacheTimer.record(runnable);
    }

    /**
     * 记录Redis缓存操作时间
     *
     * @param runnable 要执行的操作
     */
    public void recordRedisCacheTime(Runnable runnable) {
        redisCacheTimer.record(runnable);
    }

    /**
     * 记录数据库查询时间
     *
     * @param runnable 要执行的操作
     */
    public void recordDbQueryTime(Runnable runnable) {
        dbQueryTimer.record(runnable);
    }

    /**
     * 计算本地缓存命中率
     */
    private double getLocalCacheHitRate() {
        double hits = localCacheHitCounter.count();
        double misses = localCacheMissCounter.count();
        double total = hits + misses;

        return total == 0 ? 0 : hits / total;
    }
}
代码语言:javascript
复制

在缓存工具类中集成监控:

代码语言:javascript
复制
// 修改getFromLocalCache方法,添加监控
public <T> T getFromLocalCache(String key, Class<T> clazz) {
    return cacheMetricsCollector.recordLocalCacheTime(() -> {
        // 原有逻辑...

        if (value != null) {
            cacheMetricsCollector.recordLocalCacheHit();
            // ...
        } else {
            cacheMetricsCollector.recordLocalCacheMiss();
        }

        // ...
    });
}
代码语言:javascript
复制

5.3 Grafana 监控面板

通过 Grafana 创建缓存监控面板,包含以下关键图表:

  1. 缓存命中率趋势图展示本地缓存和 Redis 缓存的命中率变化趋势
  2. 响应时间对比图对比本地缓存、Redis、数据库的响应时间
  3. 缓存大小变化图展示本地缓存和 Redis 缓存的大小变化
  4. 热点产品访问 Top10展示访问量最高的 10 个产品
  5. 错误指标统计展示缓存穿透、击穿等错误的数量统计

5.4 告警配置

设置关键指标的告警阈值:

  1. 本地缓存命中率低于 80% 时告警
  2. Redis 缓存命中率低于 90% 时告警
  3. 接口平均响应时间超过 100ms 时告警
  4. 99 分位响应时间超过 200ms 时告警
  5. Redis 错误率超过 1% 时告警
  6. 缓存穿透次数 5 分钟内超过 1000 次时告警

六、性能测试与优化效果

为了验证多级缓存架构的效果,我们进行了全面的性能测试,并与优化前的单级 Redis 缓存架构进行了对比。

6.1 测试环境与方案

6.1.1 测试环境
  • 应用服务器:4 台 8 核 16G 云服务器
  • Redis 集群:3 主 3 从,每节点 4 核 8G
  • 数据库:MySQL 8.0 主从架构,主库 16 核 32G
  • 压测工具:JMeter 5.6
  • 监控工具:Prometheus + Grafana
6.1.2 测试方案
  1. 基准测试单用户单请求,测量最小响应时间
  2. 负载测试逐步增加并发用户数(100→500→1000→2000),观察系统表现
  3. 压力测试设置 3000 并发用户,持续压测 30 分钟
  4. 热点测试针对 3 款热点产品,模拟 80% 的请求集中访问
  5. 容灾测试模拟 Redis 集群部分节点宕机,观察系统表现

6.2 测试结果对比

指标

优化前(单级 Redis)

优化后(多级缓存)

提升比例

平均响应时间

520ms

48ms

90.8%

95 分位响应时间

850ms

72ms

91.5%

99 分位响应时间

1200ms

89ms

92.6%

最大响应时间

3500ms

156ms

95.5%

吞吐量(QPS)

1800

12500

594.4%

Redis 平均负载

70%

25%

降低 64.3%

数据库查询次数

180 次 / 秒

35 次 / 秒

降低 80.6%

6.3 热点场景专项测试

针对热点产品的测试结果:

指标

优化前

优化后

提升比例

热点产品平均响应时间

380ms

22ms

94.2%

热点产品 99 分位响应时间

950ms

45ms

95.3%

热点产品缓存命中率

92%

99.6%

提升 8.3%

6.4 容灾测试结果

当 Redis 集群中 1 个主节点宕机时:

指标

优化前

优化后

差异

故障期间平均响应时间

2800ms

185ms

优化后响应时间仅为优化前的 6.6%

故障期间错误率

15%

0.3%

优化后错误率大幅降低

故障恢复时间

45 秒

28 秒

恢复速度提升 37.8%

七、总结与展望

通过在保险系统中实施 "本地缓存 + Redis 集群" 的多级缓存架构,我们成功将热点产品信息查询延迟控制在 100ms 以内,同时大幅提升了系统的吞吐量和稳定性。这一架构不仅满足了用户对快速查询的需求,也为业务的持续增长提供了坚实的技术支撑。

7.1 关键经验总结

  1. 多级缓存不是简单叠加需要根据数据特性设计合理的缓存策略,如热点产品的特殊处理
  2. 一致性是核心挑战采用 "更新数据库→删除缓存→消息通知" 的方案,结合定时任务补偿,实现最终一致性
  3. 监控是运维基础完善的监控体系能及时发现缓存问题,避免故障扩大
  4. 预防优于补救提前设计缓存穿透、击穿、雪崩的防护措施,而非事后处理
  5. 性能测试必不可通过全面的性能测试验证缓存策略的有效性,发现潜在问题

7.2 未来优化方向

  1. 智能缓存策略基于 AI 算法预测产品热度变化,动态调整缓存策略
  2. 分布式本地缓存引入如 Hazelcast 等分布式本地缓存,进一步提升集群性能
  3. 边缘缓存在 CDN 或边缘节点缓存静态产品信息,减少中心服务压力
  4. 缓存与计算结合将简单的产品费率计算逻辑下沉到 Redis,减少应用服务计算压力
  5. 混沌工程实践定期模拟缓存故障,验证系统的容错能力

多级缓存架构的实施是一个持续优化的过程,需要结合业务发展和技术进步不断调整。对于保险系统而言,在保证数据准确性和合规性的前提下,通过技术创新提升用户体验,将是长期的努力方向。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、保险系统缓存需求的特殊性分析
    • 1.1 保险产品数据的独特属性
    • 1.2 性能与一致性的平衡难题
    • 1.3 流量特征与缓存挑战
  • 二、多级缓存架构设计与技术选型
    • 2.1 整体架构设计
    • 2.2 技术选型详解
      • 2.2.1 本地缓存:Caffeine
      • 2.2.2 分布式缓存:Redis Cluster
      • 2.2.3 缓存更新组件
      • 2.2.4 监控组件
    • 2.3 与传统缓存架构的对比优势
  • 三、多级缓存核心实现方案
    • 3.1 数据模型设计
      • 3.1.1 数据库表设计
      • 3.1.2 实体类定义
      • 3.1.3 聚合 DTO
    • 3.2 缓存配置与初始化
      • 3.2.1 Maven 依赖
      • 3.2.2 本地缓存配置(Caffeine)
      • 3.2.3 Redis 配置
    • 3.3 缓存工具类封装
    • 3.4 业务层实现
      • 3.4.1 Mapper 接口
      • 3.4.2 Service 实现
      • 3.4.3 Controller 实现
    • 3.5 缓存更新与一致性保障
      • 3.5.1 消息实体与消费者
      • 3.5.2 Canal 配置(数据库变更监听)
  • 四、缓存优化策略与最佳实践
    • 4.1 热点产品识别与特殊处理
      • 4.1.1 热点识别算法
      • 4.1.2 热点产品的特殊处理策略
    • 4.2 缓存穿透防护
      • 4.2.1 空值缓存
      • 4.2.2 布隆过滤器
    • 4.3 缓存击穿防护
      • 4.3.1 互斥锁方案
      • 4.3.2 热点 key 永不过期
    • 4.4 缓存雪崩防护
      • 4.4.1 过期时间随机化
      • 4.4.2 Redis 集群高可用
      • 4.4.3 本地缓存兜底
  • 五、监控与运维
    • 5.1 缓存监控指标设计
    • 5.2 Prometheus 监控实现
    • 5.3 Grafana 监控面板
    • 5.4 告警配置
  • 六、性能测试与优化效果
    • 6.1 测试环境与方案
      • 6.1.1 测试环境
      • 6.1.2 测试方案
    • 6.2 测试结果对比
    • 6.3 热点场景专项测试
    • 6.4 容灾测试结果
  • 七、总结与展望
    • 7.1 关键经验总结
    • 7.2 未来优化方向
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档