一、完整代码实现

步骤 1:引入 Maven 依赖(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>two-level-cache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>two-level-cache</name>
    
    <dependencies>
        <!-- Spring Boot Web(提供接口测试) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Redis 分布式缓存 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <!-- 连接池(提升Redis性能) -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        
        <!-- Caffeine 本地缓存(性能最优) -->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.8</version>
        </dependency>
        
        <!-- FastJSON(序列化/反序列化) -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
        
        <!-- Lombok(简化代码) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
步骤 2:配置文件(application.yml)
server:
  port: 8080  # 服务端口

spring:
  # Redis 配置
  redis:
    host: 127.0.0.1  # 替换为你的Redis地址
    port: 6379
    password:  # 你的Redis密码(无则留空)
    database: 0
    lettuce:
      pool:
        max-active: 10  # 最大连接数
        max-idle: 5     # 最大空闲连接
        min-idle: 2     # 最小空闲连接
        max-wait: 3000ms # 连接等待时间

# 自定义缓存配置
cache:
  # Caffeine本地缓存配置
  caffeine:
    initial-capacity: 100  # 初始缓存大小
    maximum-size: 1000     # 最大缓存数量
    expire-after-write: 5m # 写入后5分钟过期(本地缓存过期时间短于Redis)
  # Redis缓存配置
  redis:
    expire-after-write: 1h # 写入后1小时过期
    null-value-expire: 60s # 空值缓存60秒(防穿透)
步骤 3:核心配置类(缓存配置)
package com.example.twolevelcache.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * 二级缓存配置类:初始化Caffeine本地缓存和Redis模板
 */
@Configuration
public class CacheConfig {

    // 从配置文件读取Caffeine参数
    @Value("${cache.caffeine.initial-capacity}")
    private int initialCapacity;

    @Value("${cache.caffeine.maximum-size}")
    private long maximumSize;

    @Value("${cache.caffeine.expire-after-write}")
    private long expireAfterWrite;

    /**
     * 初始化Caffeine本地缓存
     */
    @Bean
    public com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(initialCapacity)       // 初始容量
                .maximumSize(maximumSize)               // 最大缓存数
                .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) // 过期时间
                .recordStats()                          // 记录缓存统计(可选)
                .build();
    }

    /**
     * 二级缓存管理器(封装本地+Redis缓存逻辑)
     */
    @Bean
    public TwoLevelCacheManager twoLevelCacheManager(StringRedisTemplate stringRedisTemplate,
                                                     com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache) {
        return new TwoLevelCacheManager(stringRedisTemplate, caffeineCache);
    }
}
步骤 4:二级缓存核心实现类
package com.example.twolevelcache.config;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 二级缓存管理器:封装本地缓存(Caffeine)+ Redis缓存的核心逻辑
 */
@Slf4j
@Component
public class TwoLevelCacheManager {

    private final StringRedisTemplate stringRedisTemplate;
    private final com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache;

    // Redis缓存过期时间
    @Value("${cache.redis.expire-after-write}")
    private long redisExpireTime;

    // 空值缓存过期时间
    @Value("${cache.redis.null-value-expire}")
    private long nullValueExpireTime;

    public TwoLevelCacheManager(StringRedisTemplate stringRedisTemplate,
                                com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.caffeineCache = caffeineCache;
    }

    // ==================== 读取缓存 ====================
    /**
     * 读取二级缓存(先本地,后Redis)
     */
    public <T> T get(String key, Class<T> clazz) {
        // 1. 先查本地缓存
        String value = caffeineCache.getIfPresent(key);
        if (value != null) {
            log.info("本地缓存命中,key:{}", key);
            // 空值处理
            if (value.isEmpty()) {
                return null;
            }
            return JSON.parseObject(value, clazz);
        }
        log.info("本地缓存未命中,key:{}", key);

        // 2. 再查Redis缓存
        value = stringRedisTemplate.opsForValue().get(key);
        if (value != null) {
            log.info("Redis缓存命中,key:{}", key);
            // 将Redis数据回写到本地缓存
            caffeineCache.put(key, value);
            // 空值处理
            if (value.isEmpty()) {
                return null;
            }
            return JSON.parseObject(value, clazz);
        }
        log.info("Redis缓存未命中,key:{}", key);

        // 3. 两级缓存都未命中
        return null;
    }

    // ==================== 写入缓存 ====================
    /**
     * 写入二级缓存(本地+Redis)
     */
    public <T> void put(String key, T value) {
        if (value == null) {
            // 缓存空值(防穿透)
            caffeineCache.put(key, "");
            stringRedisTemplate.opsForValue().set(key, "", nullValueExpireTime, TimeUnit.SECONDS);
            return;
        }

        String jsonValue = JSON.toJSONString(value);
        // 1. 写入本地缓存
        caffeineCache.put(key, jsonValue);
        // 2. 写入Redis缓存(设置过期时间)
        stringRedisTemplate.opsForValue().set(key, jsonValue, redisExpireTime, TimeUnit.HOURS);
        log.info("写入二级缓存成功,key:{}", key);
    }

    // ==================== 删除缓存 ====================
    /**
     * 删除二级缓存(本地+Redis)
     */
    public void delete(String key) {
        // 1. 删除本地缓存
        caffeineCache.invalidate(key);
        // 2. 删除Redis缓存
        stringRedisTemplate.delete(key);
        log.info("删除二级缓存成功,key:{}", key);
    }
}
步骤 5:实体类(示例:用户)
package com.example.twolevelcache.entity;

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

/**
 * 用户实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String phone;
}
步骤 6:模拟数据库 DAO 层
package com.example.twolevelcache.dao;

import com.example.twolevelcache.entity.User;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;

/**
 * 模拟数据库操作(实际项目替换为MyBatis/MyBatis-Plus/JPA)
 */
@Repository
public class UserDAO {

    // 模拟数据库表
    private static final Map<Long, User> USER_DB = new HashMap<>();

    // 初始化测试数据
    static {
        USER_DB.put(1L, new User(1L, "张三", 25, "13800138000"));
        USER_DB.put(2L, new User(2L, "李四", 30, "13900139000"));
        USER_DB.put(3L, new User(3L, "王五", 28, "13700137000"));
    }

    /**
     * 根据ID查询用户
     */
    public User getById(Long id) {
        // 模拟数据库查询耗时(50ms)
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return USER_DB.get(id);
    }

    /**
     * 更新用户
     */
    public void update(User user) {
        USER_DB.put(user.getId(), user);
    }

    /**
     * 删除用户
     */
    public void deleteById(Long id) {
        USER_DB.remove(id);
    }
}
步骤 7:业务服务层
package com.example.twolevelcache.service;

import com.example.twolevelcache.config.TwoLevelCacheManager;
import com.example.twolevelcache.dao.UserDAO;
import com.example.twolevelcache.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 用户业务服务:封装二级缓存的核心使用逻辑
 */
@Slf4j
@Service
public class UserService {

    @Resource
    private UserDAO userDAO;

    @Resource
    private TwoLevelCacheManager twoLevelCacheManager;

    // 缓存key前缀(避免key冲突)
    private static final String CACHE_KEY_PREFIX = "user:";

    /**
     * 查询用户(二级缓存核心逻辑)
     */
    public User getUserById(Long userId) {
        String cacheKey = CACHE_KEY_PREFIX + userId;

        // 1. 查二级缓存
        User user = twoLevelCacheManager.get(cacheKey, User.class);
        if (user != null) {
            return user;
        }

        // 2. 缓存未命中:查数据库
        user = userDAO.getById(userId);

        // 3. 回写二级缓存(包括空值)
        twoLevelCacheManager.put(cacheKey, user);

        return user;
    }

    /**
     * 更新用户(先更库,再删缓存)
     */
    public void updateUser(User user) {
        // 1. 先更新数据库
        userDAO.update(user);

        // 2. 删除二级缓存(而非更新,避免并发脏数据)
        String cacheKey = CACHE_KEY_PREFIX + user.getId();
        twoLevelCacheManager.delete(cacheKey);
    }

    /**
     * 删除用户(先删库,再删缓存)
     */
    public void deleteUser(Long userId) {
        // 1. 先删除数据库数据
        userDAO.deleteById(userId);

        // 2. 删除二级缓存
        String cacheKey = CACHE_KEY_PREFIX + userId;
        twoLevelCacheManager.delete(cacheKey);
    }
}
步骤 8:控制器(接口测试)
package com.example.twolevelcache.controller;

import com.example.twolevelcache.entity.User;
import com.example.twolevelcache.service.UserService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * 用户接口控制器:提供测试接口
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    /**
     * 查询用户
     * 示例:http://localhost:8080/user/1
     */
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    /**
     * 更新用户
     * 示例:POST http://localhost:8080/user
     * Body:{"id":1,"name":"张三-update","age":26,"phone":"13800138001"}
     */
    @PostMapping
    public String updateUser(@RequestBody User user) {
        userService.updateUser(user);
        return "更新成功";
    }

    /**
     * 删除用户
     * 示例:DELETE http://localhost:8080/user/1
     */
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return "删除成功";
    }
}
步骤 9:启动类
package com.example.twolevelcache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TwoLevelCacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(TwoLevelCacheApplication.class, args);
    }
}

二、测试验证

  1. 启动项目:确保 Redis 已启动,运行TwoLevelCacheApplication
  2. 第一次访问:http://localhost:8080/user/1
    • 日志会显示:本地缓存未命中 → Redis 缓存未命中 → 查数据库 → 写入二级缓存
    • 响应耗时约 50ms(模拟数据库耗时)
  3. 第二次访问:http://localhost:8080/user/1
    • 日志会显示:本地缓存命中
    • 响应耗时 < 1ms(极致速度)
  4. 更新用户:POST 请求更新用户后,再次查询会重新查库并更新缓存
  5. 删除用户:DELETE 请求后,缓存会被清空,再次查询返回 null

三、生产级优化建议(代码已预留扩展点)

  1. 本地缓存更新通知:多实例部署时,可通过 Redis Pub/Sub 广播缓存删除消息,保证所有实例本地缓存一致
  2. 分布式锁:热点 key 查询时加 Redis 锁,避免缓存击穿
  3. 缓存统计:Caffeine 的recordStats()可统计缓存命中率,便于优化缓存策略
  4. Redis 集群:生产环境替换为 Redis 主从 + 哨兵 / Cluster,保证高可用

总结

  1. 核心流程:二级缓存遵循「本地缓存优先 → Redis 缓存兜底 → 数据库最终源」的查询逻辑,更新 / 删除遵循「先操作数据库,再删除缓存」的原则;
  2. 关键配置:本地缓存(Caffeine)过期时间短于 Redis,避免本地缓存数据不一致;
  3. 防坑要点:缓存空值防穿透、删除缓存而非更新缓存防脏数据、本地缓存 + Redis 组合兼顾性能与分布式一致性。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐