shop-back-end/doc/缓存系统指南.md

12 KiB
Raw Permalink Blame History

缓存系统指南

概述

本项目采用多级缓存架构将原有的Redis缓存替换为基于Caffeine的本地缓存以提升性能并减少对外部Redis服务的依赖。

缓存架构

多级缓存设计

应用层缓存
├── Caffeine缓存 (本地内存缓存)
│   ├── 用户缓存
│   ├── 角色缓存
│   ├── 岗位缓存
│   └── 登录用户缓存
├── Guava缓存 (配置缓存)
│   ├── 系统配置
│   ├── 字典数据
│   └── 部门数据
└── Redis缓存 (分布式缓存,可选)
    ├── 会话缓存
    ├── 验证码缓存
    └── 分布式锁

Caffeine缓存迁移

1. 迁移概述

本项目已将原有的Redis缓存替换为基于Caffeine的本地缓存主要变更包括

  • 新增依赖: Caffeine 2.9.3
  • 新增组件: AbstractCaffeineCacheTemplate、CaffeineCacheService
  • 缓存中心更新: CacheCenter使用Caffeine缓存替换Redis缓存

2. 缓存配置

2.1 Caffeine缓存配置

@Configuration
public class CaffeineCacheConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                // 初始容量
                .initialCapacity(128)
                // 最大容量
                .maximumSize(1024)
                // 写入后过期时间
                .expireAfterWrite(60, TimeUnit.MINUTES)
                // 写入后刷新时间
                .refreshAfterWrite(5, TimeUnit.MINUTES)
                // 并发级别
                .concurrencyLevel(16)
                // 使用软引用
                .softValues()
                .build();
    }
}

2.2 缓存服务配置

@Service
public class CaffeineCacheService {

    // 验证码缓存
    @Bean
    public AbstractCaffeineCacheTemplate<String> captchaCache() {
        return new AbstractCaffeineCacheTemplate<String>("captcha") {
            @Override
            protected String loadData(String key) {
                // 从数据库加载数据的逻辑
                return null;
            }
        };
    }

    // 登录用户缓存
    @Bean
    public AbstractCaffeineCacheTemplate<LoginUser> loginUserCache() {
        return new AbstractCaffeineCacheTemplate<LoginUser>("loginUser") {
            @Override
            protected LoginUser loadData(String key) {
                // 从数据库加载用户信息
                return userService.getLoginUserByToken(key);
            }
        };
    }
}

缓存使用

1. 获取缓存数据

1.1 从缓存中获取用户数据

// 从缓存中获取用户数据
SysUserEntity user = CacheCenter.userCache.get("1");

// 从缓存中获取角色数据
SysRoleEntity role = CacheCenter.roleCache.get("1");

// 从缓存中获取岗位数据
SysPostEntity post = CacheCenter.postCache.get("1");

1.2 获取登录用户信息

public LoginUser getLoginUser(String token) {
    LoginUser loginUser = CacheCenter.loginUserCache.get(token);
    if (loginUser == null) {
        throw new ApiException("用户登录状态已过期");
    }
    return loginUser;
}

2. 使缓存失效

2.1 使单个缓存失效

// 使单个用户缓存失效
CacheCenter.userCache.invalidate("1");

// 使单个登录用户缓存失效
CacheCenter.loginUserCache.invalidate(token);

2.2 使所有缓存失效

// 使所有用户缓存失效
CacheCenter.userCache.invalidateAll();

// 使所有登录用户缓存失效
CacheCenter.loginUserCache.invalidateAll();

3. 更新缓存数据

@Service
@RequiredArgsConstructor
public class UserService {

    public void updateUser(UpdateUserCommand command) {
        // 1. 更新数据库
        userModel.update(command);

        // 2. 使缓存失效
        CacheCenter.userCache.invalidate(command.getUserId().toString());

        // 3. 如果用户正在登录,也需要更新登录用户缓存
        String token = getTokenByUserId(command.getUserId());
        if (token != null) {
            CacheCenter.loginUserCache.invalidate(token);
        }
    }
}

缓存模板

1. AbstractCaffeineCacheTemplate

public abstract class AbstractCaffeineCacheTemplate<T> {

    protected final Cache<String, T> cache;
    protected final String cacheName;

    public AbstractCaffeineCacheTemplate(String cacheName) {
        this.cacheName = cacheName;
        this.cache = Caffeine.newBuilder()
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.MINUTES)
                .refreshAfterWrite(5, TimeUnit.MINUTES)
                .recordStats()
                .build();
    }

    /**
     * 从缓存获取数据,如果不存在则从数据库加载
     */
    public T get(String key) {
        try {
            return cache.get(key, k -> loadData(k));
        } catch (Exception e) {
            log.error("从缓存{}获取数据失败: {}", cacheName, e.getMessage(), e);
            return null;
        }
    }

    /**
     * 从数据库加载数据的抽象方法
     */
    protected abstract T loadData(String key);

    /**
     * 使缓存失效
     */
    public void invalidate(String key) {
        cache.invalidate(key);
    }

    /**
     * 使所有缓存失效
     */
    public void invalidateAll() {
        cache.invalidateAll();
    }

    /**
     * 获取缓存统计信息
     */
    public String getStats() {
        CacheStats stats = cache.stats();
        return String.format(
            "缓存%s统计: 命中率=%.2f%%, 加载次数=%d, 加载成功次数=%d, 加载异常次数=%d, 总加载时间=%dms",
            cacheName,
            stats.hitRate() * 100,
            stats.loadCount(),
            stats.loadSuccessCount(),
            stats.loadExceptionCount(),
            stats.totalLoadTime()
        );
    }
}

缓存监控

1. 通过API接口监控

访问缓存统计信息:

GET /monitor/caffeine/stats

响应:

{
  "code": 200,
  "msg": "success",
  "data": {
    "userCache": "缓存user统计: 命中率=95.23%, 加载次数=100, 加载成功次数=98, 加载异常次数=2, 总加载时间=150ms",
    "roleCache": "缓存role统计: 命中率=98.76%, 加载次数=50, 加载成功次数=50, 加载异常次数=0, 总加载时间=80ms",
    "loginUserCache": "缓存loginUser统计: 命中率=92.45%, 加载次数=200, 加载成功次数=195, 加载异常次数=5, 总加载时间=300ms"
  }
}

2. 通过代码监控

@Service
@RequiredArgsConstructor
public class CacheMonitorService {

    @Autowired
    private CaffeineCacheService caffeineCacheService;

    public void printCacheStats() {
        String stats = caffeineCacheService.getCacheStats();
        System.out.println(stats);
    }

    @Scheduled(fixedRate = 300000) // 每5分钟执行一次
    public void monitorCachePerformance() {
        log.info("缓存性能监控: {}", caffeineCacheService.getCacheStats());
    }
}

性能对比

Redis vs Caffeine 性能对比

特性 Redis Caffeine
访问延迟 网络延迟 (0.1-1ms) 内存访问 (<0.01ms)
数据一致性 分布式一致 本地不一致
内存使用 独立服务内存 应用进程内存
适用场景 分布式缓存 本地高速缓存
部署复杂度 需要独立部署 内置在应用中

适用场景建议

使用Caffeine缓存的场景

  • 用户基本信息
  • 角色权限数据
  • 系统配置数据
  • 字典数据
  • 登录用户会话

使用Redis缓存的场景

  • 分布式会话
  • 分布式锁
  • 消息队列
  • 跨服务数据共享

迁移注意事项

1. 数据一致性

Caffeine是本地缓存在多实例部署时需要注意

@Service
public class CacheConsistencyService {

    /**
     * 在多实例环境下,需要同步缓存失效
     */
    public void invalidateUserCache(Long userId) {
        // 1. 使本地缓存失效
        CacheCenter.userCache.invalidate(userId.toString());

        // 2. 发布缓存失效事件(如果有多实例)
        applicationEventPublisher.publishEvent(new UserCacheEvictEvent(userId));
    }
}

2. 内存使用监控

@Service
public class MemoryMonitorService {

    @Scheduled(fixedRate = 60000) // 每1分钟执行一次
    public void monitorMemoryUsage() {
        Runtime runtime = Runtime.getRuntime();
        long usedMemory = runtime.totalMemory() - runtime.freeMemory();
        long maxMemory = runtime.maxMemory();
        double usageRate = (double) usedMemory / maxMemory * 100;

        if (usageRate > 80) {
            log.warn("内存使用率过高: {}%, 建议调整缓存配置", String.format("%.2f", usageRate));
        }
    }
}

3. 缓存配置调优

根据业务需求调整缓存参数:

@Configuration
public class CustomCacheConfig {

    @Bean
    public AbstractCaffeineCacheTemplate<SysUserEntity> userCache() {
        return new AbstractCaffeineCacheTemplate<SysUserEntity>("user") {
            @Override
            protected SysUserEntity loadData(String key) {
                return userService.getById(Long.valueOf(key));
            }
        } {
            @Override
            protected Cache<String, SysUserEntity> createCache() {
                return Caffeine.newBuilder()
                        .maximumSize(500)  // 用户数据较多,限制最大数量
                        .expireAfterWrite(30, TimeUnit.MINUTES)  // 用户数据变化较少
                        .build();
            }
        };
    }

    @Bean
    public AbstractCaffeineCacheTemplate<LoginUser> loginUserCache() {
        return new AbstractCaffeineCacheTemplate<LoginUser>("loginUser") {
            @Override
            protected LoginUser loadData(String key) {
                return userService.getLoginUserByToken(key);
            }
        } {
            @Override
            protected Cache<String, LoginUser> createCache() {
                return Caffeine.newBuilder()
                        .maximumSize(1000)  // 登录用户较多
                        .expireAfterWrite(2, TimeUnit.HOURS)  // 登录会话较长
                        .build();
            }
        };
    }
}

故障排查

1. 常见问题

1.1 缓存不生效

  • 检查 @PostConstruct 方法是否正确初始化
  • 验证缓存键是否正确
  • 检查缓存配置参数

1.2 内存溢出

  • 调整 maximumSize 参数
  • 缩短 expireAfterWrite 时间
  • 监控内存使用情况

1.3 数据不一致

  • 确保数据更新时正确调用 invalidate 方法
  • 在多实例环境下实现缓存同步
  • 定期清理过期缓存

2. 调试方法

// 查看缓存统计
System.out.println(CacheCenter.userCache.getStats());

// 手动触发缓存加载
CacheCenter.userCache.get("test-key");

// 查看缓存内容(仅开发环境)
CacheCenter.userCache.asMap().forEach((key, value) -> {
    System.out.println(key + ": " + value);
});

回滚方案

如需回滚到Redis缓存

  1. 恢复 CacheCenter 中的Redis缓存引用
  2. 移除Caffeine相关依赖
  3. 重启应用
<!-- 移除Caffeine依赖 -->
<!-- <dependency> -->
<!--     <groupId>com.github.ben-manes.caffeine</groupId> -->
<!--     <artifactId>caffeine</artifactId> -->
<!--     <version>2.9.3</version> -->
<!-- </dependency> -->

最佳实践

1. 缓存键设计

  • 使用有意义的键名
  • 避免过长的键名
  • 统一命名规范

2. 缓存失效策略

  • 写操作时立即失效相关缓存
  • 读操作时设置合理的过期时间
  • 定期清理无用缓存

3. 性能监控

  • 监控缓存命中率
  • 监控内存使用情况
  • 监控缓存加载时间

4. 安全考虑

  • 避免缓存敏感数据
  • 设置合理的缓存大小
  • 实现缓存数据加密