欢迎光临
我们一直在努力

Java深入解析篇三十二之分布式缓存

分布式缓存详解

本文深入讲解分布式缓存核心原理与实战,涵盖 Redis 数据结构、持久化、集群、缓存策略、三大问题、分布式锁、本地缓存、多级缓存架构。


目录

  • 分布式缓存概述
  • Redis核心数据结构
  • Redis持久化
  • Redis集群
  • 缓存策略
  • 缓存穿透
  • 缓存击穿
  • 缓存雪崩
  • 缓存一致性
  • 分布式锁
  • 本地缓存
  • 多级缓存架构
  • Spring Cache抽象
  • Redis在Java中的使用
  • 最佳实践

  • 一、分布式缓存概述

    1.1 为什么需要分布式缓存

    在高并发系统中,数据库是最常见的性能瓶颈。分布式缓存通过将热点数据存储在内存中,大幅降低数据库压力:

    指标数据库(MySQL)分布式缓存(Redis)
    响应时间 5~50ms 0.1~1ms
    QPS(单节点) 1,000~5,000 100,000+
    数据容量 TB级(磁盘) GB级(内存)
    并发模型 连接池/线程 单线程IO多路复用

    1.2 缓存的核心价值

    • 降低延迟:内存访问速度是磁盘的 10万倍
    • 提升吞吐:Redis 单节点可支撑 10W+ QPS
    • 保护数据库:削峰填谷,避免DB被打垮
    • 降低成本:减少数据库扩容需求

    1.3 缓存的代价

    • 数据一致性:缓存与DB数据可能不一致
    • 系统复杂度:需处理穿透/击穿/雪崩
    • 运维成本:Redis集群的部署与监控
    • 内存成本:热点数据占用大量内存

    二、Redis核心数据结构

    2.1 String(字符串)

    最基础的数据类型,可存储字符串、整数、浮点数。

    @Service
    public class StringCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 缓存用户Token
    */

    public void cacheToken(Long userId, String token) {
    String key = "user:token:" + userId;
    redisTemplate.opsForValue().set(key, token, 30, TimeUnit.MINUTES);
    }

    /**
    * 分布式计数器(文章浏览量)
    */

    public Long incrementViewCount(Long articleId) {
    String key = "article:views:" + articleId;
    return redisTemplate.opsForValue().increment(key);
    }

    /**
    * 分布式锁基础操作
    */

    public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
    Boolean result = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(result);
    }

    /**
    * 批量获取(Pipeline减少网络往返)
    */

    public List<String> batchGet(List<String> keys) {
    return redisTemplate.opsForValue().multiGet(keys);
    }
    }

    2.2 Hash(哈希)

    适合存储对象,支持字段级别的独立操作。

    @Service
    public class HashCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 缓存用户对象(字段级更新)
    */

    public void cacheUser(User user) {
    String key = "user:info:" + user.getId();
    Map<String, String> userMap = new HashMap<>();
    userMap.put("name", user.getName());
    userMap.put("email", user.getEmail());
    userMap.put("age", String.valueOf(user.getAge()));
    redisTemplate.opsForHash().putAll(key, userMap);
    redisTemplate.expire(key, 1, TimeUnit.HOURS);
    }

    /**
    * 更新单个字段(无需读取整个对象)
    */

    public void updateUserName(Long userId, String newName) {
    String key = "user:info:" + userId;
    redisTemplate.opsForHash().put(key, "name", newName);
    }

    /**
    * 获取单个字段
    */

    public String getUserName(Long userId) {
    String key = "user:info:" + userId;
    Object name = redisTemplate.opsForHash().get(key, "name");
    return name != null ? name.toString() : null;
    }

    /**
    * 购物车(商品ID -> 数量)
    */

    public void addToCart(Long userId, Long productId, int quantity) {
    String key = "cart:" + userId;
    redisTemplate.opsForHash().increment(key, productId.toString(), quantity);
    }
    }

    2.3 List(列表)

    双端链表结构,适合实现消息队列和最新列表。

    @Service
    public class ListCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 最新消息列表(保留最新100条)
    */

    public void pushNews(String news) {
    String key = "news:latest";
    redisTemplate.opsForList().leftPush(key, news);
    redisTemplate.opsForList().trim(key, 0, 99); // 只保留100条
    }

    /**
    * 获取最新N条消息
    */

    public List<String> getLatestNews(int count) {
    String key = "news:latest";
    return redisTemplate.opsForList().range(key, 0, count 1);
    }

    /**
    * 简单消息队列(阻塞式消费)
    */

    public String consumeMessage(String queueKey, long timeoutSeconds) {
    return redisTemplate.opsForList().rightPop(queueKey, timeoutSeconds, TimeUnit.SECONDS);
    }

    /**
    * 生产者
    */

    public void produceMessage(String queueKey, String message) {
    redisTemplate.opsForList().leftPush(queueKey, message);
    }
    }

    2.4 Set(集合)

    无序不重复集合,支持交并差集运算。

    @Service
    public class SetCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 用户点赞(去重)
    */

    public boolean likeArticle(Long articleId, Long userId) {
    String key = "article:likes:" + articleId;
    return Boolean.TRUE.equals(redisTemplate.opsForSet().add(key, userId.toString()));
    }

    /**
    * 共同关注(交集)
    */

    public Set<String> getCommonFollowers(Long userA, Long userB) {
    String keyA = "user:following:" + userA;
    String keyB = "user:following:" + userB;
    return redisTemplate.opsForSet().intersect(keyA, keyB);
    }

    /**
    * 抽奖(随机弹出)
    */

    public String drawLottery(String lotteryKey) {
    return redisTemplate.opsForSet().pop(lotteryKey);
    }

    /**
    * 判断用户是否已参与
    */

    public boolean hasParticipated(String lotteryKey, Long userId) {
    return Boolean.TRUE.equals(
    redisTemplate.opsForSet().isMember(lotteryKey, userId.toString()));
    }
    }

    2.5 ZSet(有序集合)

    每个元素关联一个分数,按分数排序。

    @Service
    public class ZSetCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 排行榜(分数越高排名越前)
    */

    public void updateScore(String rankKey, String member, double score) {
    redisTemplate.opsForZSet().add(rankKey, member, score);
    }

    /**
    * 获取Top N
    */

    public Set<ZSetOperations.TypedTuple<String>> getTopN(String rankKey, int n) {
    return redisTemplate.opsForZSet().reverseRangeWithScores(rankKey, 0, n 1);
    }

    /**
    * 获取用户排名
    */

    public Long getUserRank(String rankKey, String member) {
    Long rank = redisTemplate.opsForZSet().reverseRank(rankKey, member);
    return rank != null ? rank + 1 : null; // 排名从1开始
    }

    /**
    * 延迟队列(score为执行时间戳)
    */

    public void addDelayTask(String task, long executeTimeMillis) {
    redisTemplate.opsForZSet().add("delay:queue", task, executeTimeMillis);
    }

    /**
    * 消费到期任务
    */

    public Set<String> pollDueTasks() {
    long now = System.currentTimeMillis();
    Set<String> tasks = redisTemplate.opsForZSet()
    .rangeByScore("delay:queue", 0, now);
    if (tasks != null && !tasks.isEmpty()) {
    redisTemplate.opsForZSet().remove("delay:queue", tasks.toArray());
    }
    return tasks;
    }
    }

    2.6 Bitmap(位图)

    基于String的位操作,适合布尔状态存储。

    @Service
    public class BitmapCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 用户签到(offset为一年中的第几天)
    */

    public void signIn(Long userId, int dayOfYear) {
    String key = "signin:" + userId + ":" + LocalDate.now().getYear();
    redisTemplate.opsForValue().setBit(key, dayOfYear, true);
    }

    /**
    * 判断某天是否签到
    */

    public boolean hasSignedIn(Long userId, int dayOfYear) {
    String key = "signin:" + userId + ":" + LocalDate.now().getYear();
    Boolean bit = redisTemplate.opsForValue().getBit(key, dayOfYear);
    return Boolean.TRUE.equals(bit);
    }

    /**
    * 统计连续在线天数(BITCOUNT)
    */

    public Long countSignDays(Long userId) {
    String key = "signin:" + userId + ":" + LocalDate.now().getYear();
    // 使用Redis命令 BITCOUNT
    return redisTemplate.execute((RedisCallback<Long>)
    connection -> connection.bitCount(key.getBytes()));
    }
    }

    2.7 HyperLogLog(基数统计)

    概率型数据结构,用极小内存估算集合基数(误差约0.81%)。

    @Service
    public class HyperLogLogService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 记录页面UV(独立访客)
    */

    public void recordVisit(String pageId, String userId) {
    String key = "uv:" + pageId + ":" + LocalDate.now();
    redisTemplate.opsForHyperLogLog().add(key, userId);
    }

    /**
    * 获取UV数量
    */

    public Long getUVCount(String pageId) {
    String key = "uv:" + pageId + ":" + LocalDate.now();
    return redisTemplate.opsForHyperLogLog().size(key);
    }

    /**
    * 合并多天UV(去重)
    */

    public Long mergeWeeklyUV(String pageId, List<String> dailyKeys) {
    String destKey = "uv:weekly:" + pageId;
    byte[][] keys = dailyKeys.stream()
    .map(k -> k.getBytes())
    .toArray(byte[][]::new);
    redisTemplate.execute((RedisCallback<Long>) connection -> {
    connection.pfMerge(destKey.getBytes(), keys);
    return connection.pfCount(destKey.getBytes());
    });
    return redisTemplate.opsForHyperLogLog().size(destKey);
    }
    }


    三、Redis持久化

    3.1 RDB(Redis Database)

    RDB 通过 fork 子进程生成某一时刻的全量快照。

    触发方式:

    • SAVE:阻塞主进程(生产禁用)
    • BGSAVE:fork子进程,不阻塞
    • 配置自动触发:save 900 1(900秒内至少1次修改)

    配置示例(redis.conf):

    save 900 1
    save 300 10
    save 60 10000
    dbfilename dump.rdb
    dir /var/lib/redis
    rdbcompression yes

    3.2 AOF(Append Only File)

    AOF 记录每条写命令,重启时重放命令恢复数据。

    同步策略:

    策略说明数据安全性能
    always 每条命令都fsync 最高 最低
    everysec(推荐) 每秒fsync一次 最多丢1秒 均衡
    no 由OS决定flush 可能丢较多 最高

    配置示例:

    appendonly yes
    appendfsync everysec
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb

    3.3 混合持久化(Redis 4.0+)

    AOF 重写时,前半段用 RDB 格式,后半段追加 AOF 命令,兼顾恢复速度和数据完整性。

    aof-use-rdb-preamble yes

    3.4 Java中验证持久化状态

    @Service
    public class RedisPersistenceMonitor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 获取持久化状态信息
    */

    public Map<String, String> getPersistenceInfo() {
    Properties info = redisTemplate.getConnectionFactory()
    .getConnection().info("persistence");
    Map<String, String> result = new HashMap<>();
    if (info != null) {
    result.put("rdb_last_bgsave_status",
    info.getProperty("rdb_last_bgsave_status"));
    result.put("rdb_last_save_time",
    info.getProperty("rdb_last_save_time"));
    result.put("aof_enabled",
    info.getProperty("aof_enabled"));
    result.put("aof_last_write_status",
    info.getProperty("aof_last_write_status"));
    }
    return result;
    }

    /**
    * 手动触发BGSAVE
    */

    public void triggerBgSave() {
    redisTemplate.execute((RedisCallback<String>) connection -> {
    connection.bgSave();
    return "BGSAVE triggered";
    });
    }
    }


    四、Redis集群

    4.1 主从复制

    主节点处理写请求,从节点异步复制数据并处理读请求。

    核心流程:

  • 从节点发送 PSYNC 命令
  • 主节点执行 BGSAVE 生成RDB发送给从节点
  • 主节点将缓冲区中的增量命令发送给从节点
  • 后续写命令实时同步
  • 4.2 哨兵(Sentinel)

    哨兵集群监控主节点健康状态,自动完成故障转移。

    故障转移流程:

  • 哨兵通过 PING 检测主节点下线(主观下线 → 客观下线)
  • 哨兵之间通过 Raft 选举 Leader
  • Leader 从从节点中选出新主(优先级 → 复制偏移量 → runid)
  • 通知其他从节点复制新主,通知客户端切换
  • Spring Boot 哨兵配置:

    spring:
    data:
    redis:
    sentinel:
    master: mymaster
    nodes: 192.168.1.10:26379,192.168.1.11:26379,192.168.1.12:26379
    password: ${REDIS_PASSWORD}
    lettuce:
    pool:
    max-active: 16
    max-idle: 8
    min-idle: 4

    4.3 Redis Cluster

    去中心化分片集群,数据按 CRC16(key) % 16384 分配到不同槽。

    核心特性:

    • 16384个哈希槽分布在多个主节点
    • 客户端收到 MOVED 重定向到正确节点
    • 每个主节点有从节点做故障备份
    • 支持在线扩缩容(槽迁移)

    Spring Boot Cluster配置:

    spring:
    data:
    redis:
    cluster:
    nodes: 192.168.1.10:7000,192.168.1.11:7001,192.168.1.12:7002
    max-redirects: 3
    password: ${REDIS_PASSWORD}
    lettuce:
    pool:
    max-active: 32
    max-idle: 16
    min-idle: 8

    4.4 集群方案Java配置类

    @Configuration
    public class RedisClusterConfig {

    @Bean
    @ConditionalOnProperty(name = "redis.mode", havingValue = "cluster")
    public RedisConnectionFactory clusterConnectionFactory(
    @Value("${spring.data.redis.cluster.nodes}") String nodes,
    @Value("${spring.data.redis.password}") String password) {

    String[] nodeArray = nodes.split(",");
    List<RedisNode> redisNodes = Arrays.stream(nodeArray)
    .map(node -> {
    String[] parts = node.split(":");
    return new RedisNode(parts[0], Integer.parseInt(parts[1]));
    })
    .collect(Collectors.toList());

    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
    clusterConfig.setClusterNodes(redisNodes);
    clusterConfig.setPassword(RedisPassword.of(password));
    clusterConfig.setMaxRedirects(3);

    GenericObjectPoolConfig<?> poolConfig = new GenericObjectPoolConfig<>();
    poolConfig.setMaxTotal(32);
    poolConfig.setMaxIdle(16);
    poolConfig.setMinIdle(8);

    LettucePoolingClientConfiguration clientConfig =
    LettucePoolingClientConfiguration.builder()
    .poolConfig(poolConfig)
    .commandTimeout(Duration.ofMillis(3000))
    .build();

    return new LettuceConnectionFactory(clusterConfig, clientConfig);
    }
    }


    五、缓存策略

    5.1 Cache Aside(旁路缓存)

    最常用的缓存模式,应用程序直接管理缓存。

    读流程:先读缓存 → 命中则返回 → 未命中则读DB → 写入缓存 → 返回

    写流程:先更新DB → 再删除缓存

    @Service
    public class CacheAsideService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    private static final long CACHE_TTL_MINUTES = 30;

    /**
    * 读:Cache Aside模式
    */

    public User getUserById(Long userId) {
    String key = "user:" + userId;

    // 1. 先读缓存
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    // 2. 缓存未命中,读数据库
    User user = userMapper.selectById(userId);
    if (user == null) {
    return null;
    }

    // 3. 写入缓存(加随机偏移防雪崩)
    long ttl = CACHE_TTL_MINUTES + ThreadLocalRandom.current().nextInt(5);
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user), ttl, TimeUnit.MINUTES);

    return user;
    }

    /**
    * 写:先更新DB,再删除缓存
    */

    @Transactional
    public void updateUser(User user) {
    // 1. 更新数据库
    userMapper.updateById(user);

    // 2. 删除缓存(下次读时重建)
    String key = "user:" + user.getId();
    redisTemplate.delete(key);
    }
    }

    5.2 Read Through(读穿透)

    缓存层作为数据访问的统一入口,miss时自动从DB加载。

    @Service
    public class ReadThroughService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    /**
    * Read Through:缓存层自动加载
    * 对调用方透明,只需访问缓存层
    */

    public User getUser(Long userId) {
    String key = "user:" + userId;

    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    // 缓存层自动从DB加载(对调用方透明)
    User user = loadFromDBAndCache(key, userId);
    return user;
    }

    private synchronized User loadFromDBAndCache(String key, Long userId) {
    // 双重检查
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    User user = userMapper.selectById(userId);
    if (user != null) {
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    30, TimeUnit.MINUTES);
    }
    return user;
    }
    }

    5.3 Write Through(写穿透)

    写操作由缓存层代理,同步写入DB。

    @Service
    public class WriteThroughService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    /**
    * Write Through:缓存层代理写入
    * 保证缓存和DB的同步更新
    */

    @Transactional
    public void saveUser(User user) {
    String key = "user:" + user.getId();

    // 1. 更新数据库
    userMapper.updateById(user);

    // 2. 同步更新缓存(而非删除)
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    30, TimeUnit.MINUTES);
    }
    }

    5.4 Write Behind(异步写回)

    只写缓存,异步批量刷回数据库,适合写密集场景。

    @Service
    public class WriteBehindService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    private final BlockingQueue<User> writeQueue = new LinkedBlockingQueue<>(10000);

    /**
    * 写操作只写缓存,异步入队
    */

    public void updateUser(User user) {
    String key = "user:" + user.getId();
    // 1. 立即更新缓存(保证读一致性)
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    30, TimeUnit.MINUTES);
    // 2. 放入异步写队列
    writeQueue.offer(user);
    }

    /**
    * 异步批量刷DB(定时任务)
    */

    @Scheduled(fixedDelay = 1000)
    public void flushToDatabase() {
    List<User> batch = new ArrayList<>();
    writeQueue.drainTo(batch, 100); // 每次最多100条

    if (!batch.isEmpty()) {
    // 批量更新数据库
    userMapper.batchUpdate(batch);
    }
    }
    }


    六、缓存穿透

    6.1 问题描述

    查询一个不存在的数据,缓存中没有,DB中也没有,导致每次请求都穿透到数据库。恶意攻击时可导致DB崩溃。

    6.2 方案一:布隆过滤器

    在缓存前加一层布隆过滤器,快速判断数据是否可能存在。

    @Service
    public class BloomFilterService {

    private final BloomFilter<Long> bloomFilter;

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;

    public BloomFilterService() {
    // 预期100万元素,误判率1%
    this.bloomFilter = BloomFilter.create(
    Funnels.longFunnel(), 1_000_000, 0.01);
    }

    /**
    * 系统启动时预热布隆过滤器
    */

    @PostConstruct
    public void warmUp() {
    List<Long> allUserIds = userMapper.selectAllUserIds();
    allUserIds.forEach(bloomFilter::put);
    }

    /**
    * 带布隆过滤器的查询
    */

    public User getUserById(Long userId) {
    // 1. 布隆过滤器判断(不存在则一定不存在)
    if (!bloomFilter.mightContain(userId)) {
    return null; // 直接拦截,不穿透到DB
    }

    // 2. 正常缓存查询逻辑
    String key = "user:" + userId;
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    User user = userMapper.selectById(userId);
    if (user != null) {
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    30, TimeUnit.MINUTES);
    }
    return user;
    }

    /**
    * 新增用户时同步更新布隆过滤器
    */

    public void addUser(User user) {
    userMapper.insert(user);
    bloomFilter.put(user.getId());
    }
    }

    6.3 方案二:空值缓存

    对查询结果为空的Key也进行缓存,设置较短TTL。

    @Service
    public class NullValueCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    private static final String NULL_PLACEHOLDER = "NULL";
    private static final long NULL_TTL_SECONDS = 60; // 空值缓存60秒

    public User getUserById(Long userId) {
    String key = "user:" + userId;
    String cached = redisTemplate.opsForValue().get(key);

    // 命中空值标记,直接返回null
    if (NULL_PLACEHOLDER.equals(cached)) {
    return null;
    }

    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    // 查询数据库
    User user = userMapper.selectById(userId);
    if (user == null) {
    // 缓存空值,防止反复穿透
    redisTemplate.opsForValue().set(key, NULL_PLACEHOLDER,
    NULL_TTL_SECONDS, TimeUnit.SECONDS);
    return null;
    }

    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    30, TimeUnit.MINUTES);
    return user;
    }
    }


    七、缓存击穿

    7.1 问题描述

    某个热点Key在过期瞬间,大量并发请求同时穿透到数据库,造成DB瞬时压力暴增。

    7.2 方案一:互斥锁重建缓存

    只允许一个线程去重建缓存,其他线程等待或返回旧数据。

    @Service
    public class MutexLockCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    private static final long LOCK_TTL_SECONDS = 10;
    private static final long CACHE_TTL_MINUTES = 30;

    public User getUserById(Long userId) {
    String key = "user:" + userId;
    String lockKey = "lock:user:" + userId;

    // 1. 尝试读缓存
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    // 2. 缓存未命中,尝试获取互斥锁
    String requestId = UUID.randomUUID().toString();
    Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, requestId, LOCK_TTL_SECONDS, TimeUnit.SECONDS);

    if (Boolean.TRUE.equals(locked)) {
    try {
    // 3. 双重检查(可能其他线程已重建)
    cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    return JSON.parseObject(cached, User.class);
    }

    // 4. 查询DB并重建缓存
    User user = userMapper.selectById(userId);
    if (user != null) {
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    CACHE_TTL_MINUTES, TimeUnit.MINUTES);
    }
    return user;
    } finally {
    // 5. 释放锁(Lua保证原子性)
    releaseLock(lockKey, requestId);
    }
    } else {
    // 6. 未获取到锁,短暂等待后重试
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    return getUserById(userId); // 递归重试
    }
    }

    private void releaseLock(String lockKey, String requestId) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
    "then return redis.call('del', KEYS[1]) else return 0 end";
    redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
    Collections.singletonList(lockKey), requestId);
    }
    }

    7.3 方案二:逻辑过期(永不真正过期)

    缓存永不设置TTL,在Value中存储逻辑过期时间,过期后异步重建。

    @Data
    public class CacheData<T> {
    private T data;
    private LocalDateTime expireTime; // 逻辑过期时间
    }

    @Service
    public class LogicalExpireCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    private final ExecutorService cacheRebuildExecutor =
    Executors.newFixedThreadPool(4);

    /**
    * 预热缓存(设置逻辑过期时间,不设TTL)
    */

    public void warmUpUser(Long userId) {
    User user = userMapper.selectById(userId);
    if (user != null) {
    CacheData<User> cacheData = new CacheData<>();
    cacheData.setData(user);
    cacheData.setExpireTime(LocalDateTime.now().plusMinutes(30));

    String key = "user:" + userId;
    redisTemplate.opsForValue().set(key, JSON.toJSONString(cacheData));
    // 注意:不设置TTL,永不真正过期
    }
    }

    public User getUserById(Long userId) {
    String key = "user:" + userId;
    String cached = redisTemplate.opsForValue().get(key);
    if (cached == null) {
    return null;
    }

    CacheData<User> cacheData = JSON.parseObject(cached,
    new TypeReference<CacheData<User>>() {});

    // 判断逻辑过期
    if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
    // 未过期,直接返回
    return cacheData.getData();
    }

    // 已过期,尝试异步重建(当前请求仍返回旧数据)
    String lockKey = "lock:user:" + userId;
    Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

    if (Boolean.TRUE.equals(locked)) {
    cacheRebuildExecutor.submit(() -> {
    try {
    warmUpUser(userId); // 重建缓存
    } finally {
    redisTemplate.delete(lockKey);
    }
    });
    }

    // 返回旧数据(保证可用性)
    return cacheData.getData();
    }
    }


    八、缓存雪崩

    8.1 问题描述

    大量缓存Key在同一时间过期,或Redis节点宕机,导致大量请求直接打到数据库。

    8.2 方案一:随机过期时间

    在基础TTL上增加随机偏移,打散过期时间。

    @Service
    public class RandomTtlCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final int BASE_TTL_MINUTES = 30;
    private static final int RANDOM_RANGE_MINUTES = 10;

    /**
    * 设置带随机偏移的过期时间
    */

    public void setWithRandomTtl(String key, String value) {
    int randomOffset = ThreadLocalRandom.current().nextInt(RANDOM_RANGE_MINUTES);
    long ttl = BASE_TTL_MINUTES + randomOffset;
    redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.MINUTES);
    }

    /**
    * 批量预热缓存(打散过期时间)
    */

    public void batchWarmUp(Map<String, String> dataMap) {
    dataMap.forEach((key, value) -> {
    int randomOffset = ThreadLocalRandom.current().nextInt(RANDOM_RANGE_MINUTES);
    long ttl = BASE_TTL_MINUTES + randomOffset;
    redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.MINUTES);
    });
    }
    }

    8.3 方案二:多级缓存

    本地缓存作为第一道防线,即使Redis不可用也能扛住部分流量。

    @Service
    public class MultiLevelFallbackService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    // 本地缓存作为降级方案
    private final Cache<Long, User> localCache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .build();

    public User getUserById(Long userId) {
    // L1: 本地缓存
    User user = localCache.getIfPresent(userId);
    if (user != null) {
    return user;
    }

    // L2: Redis(带降级)
    try {
    String key = "user:" + userId;
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    user = JSON.parseObject(cached, User.class);
    localCache.put(userId, user); // 回填L1
    return user;
    }
    } catch (Exception e) {
    // Redis不可用时降级到DB
    }

    // L3: 数据库
    user = userMapper.selectById(userId);
    if (user != null) {
    localCache.put(userId, user);
    try {
    redisTemplate.opsForValue().set("user:" + userId,
    JSON.toJSONString(user), 30, TimeUnit.MINUTES);
    } catch (Exception ignored) {
    // Redis不可用时忽略
    }
    }
    return user;
    }
    }

    8.4 方案三:限流降级

    当DB压力过大时,通过限流保护数据库。

    @Service
    public class RateLimitCacheService {

    @Autowired
    private UserMapper userMapper;

    // 使用Guava RateLimiter限制DB访问频率
    private final RateLimiter dbRateLimiter = RateLimiter.create(1000); // 每秒1000次

    public User getUserFromDB(Long userId) {
    // 尝试获取令牌(非阻塞)
    if (!dbRateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
    // 限流:返回降级数据或抛出异常
    throw new ServiceException("系统繁忙,请稍后重试");
    }
    return userMapper.selectById(userId);
    }
    }


    九、缓存一致性

    9.1 延迟双删

    先删缓存 → 更新DB → 延迟一段时间 → 再删缓存,解决并发读写不一致。

    @Service
    public class DelayDoubleDeleteService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    private final ScheduledExecutorService scheduler =
    Executors.newScheduledThreadPool(2);

    private static final long DELAY_MILLIS = 500; // 延迟500ms

    @Transactional
    public void updateUser(User user) {
    String key = "user:" + user.getId();

    // 1. 第一次删除缓存
    redisTemplate.delete(key);

    // 2. 更新数据库
    userMapper.updateById(user);

    // 3. 延迟第二次删除(解决并发读导致的脏数据)
    scheduler.schedule(() -> {
    redisTemplate.delete(key);
    }, DELAY_MILLIS, TimeUnit.MILLISECONDS);
    }
    }

    9.2 Canal监听Binlog

    通过Canal伪装为MySQL从节点,监听Binlog变更,异步更新/删除缓存。

    @Component
    public class CanalCacheSyncListener {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 监听Canal消息(通过MQ消费)
    * Canal将Binlog变更发送到RocketMQ/Kafka
    */

    @RocketMQMessageListener(
    topic = "canal-user-topic",
    consumerGroup = "cache-sync-group"
    )
    public void onMessage(CanalMessage message) {
    if ("UPDATE".equals(message.getType()) || "DELETE".equals(message.getType())) {
    String tableName = message.getTable();
    if ("t_user".equals(tableName)) {
    String userId = message.getData().get("id");
    String key = "user:" + userId;
    redisTemplate.delete(key);
    // 也可以主动重建缓存
    }
    }
    }
    }

    @Data
    public class CanalMessage {
    private String database;
    private String table;
    private String type; // INSERT / UPDATE / DELETE
    private Map<String, String> data;
    private Map<String, String> old; // 更新前的值
    }


    十、分布式锁

    10.1 SETNX基础实现

    @Service
    public class SimpleDistributedLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 加锁(SET key value NX EX)
    */

    public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
    Boolean result = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(result);
    }

    /**
    * 释放锁(Lua脚本保证原子性:判断+删除)
    */

    public boolean releaseLock(String lockKey, String requestId) {
    String script =
    "if redis.call('get', KEYS[1]) == ARGV[1] " +
    "then return redis.call('del', KEYS[1]) " +
    "else return 0 end";

    Long result = redisTemplate.execute(
    new DefaultRedisScript<>(script, Long.class),
    Collections.singletonList(lockKey),
    requestId);
    return Long.valueOf(1L).equals(result);
    }

    /**
    * 使用示例:秒杀扣库存
    */

    public boolean seckill(Long productId) {
    String lockKey = "lock:seckill:" + productId;
    String requestId = UUID.randomUUID().toString();

    if (!tryLock(lockKey, requestId, 5)) {
    return false; // 获取锁失败
    }

    try {
    // 扣减库存(临界区)
    String stockKey = "product:stock:" + productId;
    Long stock = redisTemplate.opsForValue().decrement(stockKey);
    return stock != null && stock >= 0;
    } finally {
    releaseLock(lockKey, requestId);
    }
    }
    }

    10.2 Redisson分布式锁(生产推荐)

    Redisson 提供可重入、自动续期(看门狗)的分布式锁。

    @Service
    public class RedissonLockService {

    @Autowired
    private RedissonClient redissonClient;

    /**
    * 可重入锁(自动续期)
    */

    public void transferMoney(Long fromAccount, Long toAccount, BigDecimal amount) {
    // 按ID排序避免死锁
    String lockKey1 = "lock:account:" + Math.min(fromAccount, toAccount);
    String lockKey2 = "lock:account:" + Math.max(fromAccount, toAccount);

    RLock lock1 = redissonClient.getLock(lockKey1);
    RLock lock2 = redissonClient.getLock(lockKey2);

    try {
    // 尝试获取锁(等待3秒,持有10秒后自动释放)
    boolean acquired = lock1.tryLock(3, 10, TimeUnit.SECONDS);
    if (!acquired) {
    throw new BusinessException("操作频繁,请稍后重试");
    }

    boolean acquired2 = lock2.tryLock(3, 10, TimeUnit.SECONDS);
    if (!acquired2) {
    throw new BusinessException("操作频繁,请稍后重试");
    }

    try {
    // 执行转账业务逻辑
    doTransfer(fromAccount, toAccount, amount);
    } finally {
    lock2.unlock();
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new BusinessException("操作被中断");
    } finally {
    if (lock1.isHeldByCurrentThread()) {
    lock1.unlock();
    }
    }
    }

    /**
    * 看门狗自动续期(不指定leaseTime)
    * 默认30秒过期,每10秒自动续期
    */

    public void longRunningTask(String taskId) {
    RLock lock = redissonClient.getLock("lock:task:" + taskId);

    try {
    lock.lock(); // 看门狗自动续期,直到unlock
    // 执行耗时任务(无需担心锁过期)
    processTask(taskId);
    } finally {
    if (lock.isHeldByCurrentThread()) {
    lock.unlock();
    }
    }
    }

    /**
    * 公平锁(按请求顺序获取)
    */

    public void fairLockExample(String resource) {
    RLock fairLock = redissonClient.getFairLock("fair:lock:" + resource);
    try {
    fairLock.lock();
    // 按顺序执行
    } finally {
    fairLock.unlock();
    }
    }

    /**
    * 读写锁
    */

    public void readWriteLockExample(String configKey) {
    RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw:lock:" + configKey);

    // 读操作
    try {
    rwLock.readLock().lock();
    // 多个读线程可同时持有
    } finally {
    rwLock.readLock().unlock();
    }

    // 写操作
    try {
    rwLock.writeLock().lock();
    // 独占
    } finally {
    rwLock.writeLock().unlock();
    }
    }

    private void doTransfer(Long from, Long to, BigDecimal amount) {
    // 转账逻辑
    }

    private void processTask(String taskId) {
    // 任务处理逻辑
    }
    }

    10.3 RedLock(多节点容错)

    当Redis主节点宕机且锁未同步到从节点时,单节点锁可能失效。RedLock通过多数节点加锁解决。

    @Service
    public class RedLockService {

    @Autowired
    private RedissonClient redissonClient;

    /**
    * RedLock:向多个独立Redis节点加锁
    * 需要在多个Redis实例上分别创建RedissonClient
    */

    public void criticalOperation(String resourceId) {
    // 实际生产中需要配置多个独立的RedissonClient
    RLock lock1 = redissonClient.getLock("lock:" + resourceId);
    // RLock lock2 = redissonClient2.getLock("lock:" + resourceId);
    // RLock lock3 = redissonClient3.getLock("lock:" + resourceId);

    // Redisson 3.x 已废弃RedLock,推荐使用MultiLock替代
    RLock multiLock = new RedissonMultiLock(lock1);
    // RLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);

    try {
    boolean acquired = multiLock.tryLock(5, 30, TimeUnit.SECONDS);
    if (acquired) {
    // 执行关键操作
    doCriticalWork(resourceId);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    } finally {
    multiLock.unlock();
    }
    }

    private void doCriticalWork(String resourceId) {
    // 关键业务逻辑
    }
    }


    十一、本地缓存

    11.1 Caffeine(高性能本地缓存)

    Caffeine 使用 W-TinyLFU 算法,命中率优于 LRU,是 Spring Boot 默认本地缓存实现。

    @Configuration
    public class CaffeineCacheConfig {

    /**
    * 手动创建Caffeine缓存
    */

    @Bean
    public Cache<String, Object> manualCache() {
    return Caffeine.newBuilder()
    .maximumSize(10_000) // 最大容量
    .expireAfterWrite(10, TimeUnit.MINUTES) // 写后过期
    .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后过期
    .recordStats() // 开启统计
    .removalListener((key, value, cause) -> {
    // 移除监听
    System.out.println("Cache removed: " + key + ", cause: " + cause);
    })
    .build();
    }

    /**
    * 自动加载缓存(LoadingCache)
    */

    @Bean
    public LoadingCache<Long, User> userLoadingCache(UserMapper userMapper) {
    return Caffeine.newBuilder()
    .maximumSize(5_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .refreshAfterWrite(5, TimeUnit.MINUTES) // 异步刷新
    .recordStats()
    .build(userId -> userMapper.selectById(userId)); // 加载函数
    }

    /**
    * 异步加载缓存
    */

    @Bean
    public AsyncLoadingCache<Long, User> asyncUserCache(UserMapper userMapper) {
    return Caffeine.newBuilder()
    .maximumSize(5_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .buildAsync(userId -> userMapper.selectById(userId));
    }
    }

    @Service
    public class CaffeineCacheService {

    @Autowired
    private LoadingCache<Long, User> userLoadingCache;

    /**
    * 使用LoadingCache(自动加载)
    */

    public User getUser(Long userId) {
    return userLoadingCache.get(userId); // miss时自动调用加载函数
    }

    /**
    * 手动操作
    */

    public void invalidateUser(Long userId) {
    userLoadingCache.invalidate(userId);
    }

    /**
    * 获取缓存统计
    */

    public CacheStats getStats() {
    return userLoadingCache.stats();
    // hitRate(), hitCount(), missCount(), evictionCount()
    }
    }

    11.2 Guava Cache

    @Service
    public class GuavaCacheService {

    private final com.google.common.cache.LoadingCache<String, String> configCache;

    @Autowired
    private ConfigMapper configMapper;

    public GuavaCacheService(ConfigMapper configMapper) {
    this.configMapper = configMapper;
    this.configCache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .recordStats()
    .build(new CacheLoader<String, String>() {
    @Override
    public String load(String key) {
    return configMapper.getValueByKey(key);
    }
    });
    }

    public String getConfig(String key) {
    try {
    return configCache.get(key);
    } catch (ExecutionException e) {
    throw new RuntimeException("加载配置失败: " + key, e);
    }
    }

    public void refreshConfig(String key) {
    configCache.refresh(key); // 异步刷新
    }

    public void invalidateAll() {
    configCache.invalidateAll();
    }
    }


    十二、多级缓存架构

    12.1 架构设计

    请求 → L1(Caffeine) → L2(Redis) → L3(MySQL)
    ↑ 回填 ↑ 回填

    更新 → 更新DB → 删Redis → 发MQ → 各节点失效L1

    12.2 完整实现

    @Service
    public class MultiLevelCacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisMessageTemplate messageTemplate;

    // L1: 本地缓存
    private final Cache<Long, User> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(2, TimeUnit.MINUTES)
    .build();

    private static final long REDIS_TTL_MINUTES = 30;

    /**
    * 多级缓存读取
    */

    public User getUserById(Long userId) {
    // L1: 本地缓存
    User user = localCache.getIfPresent(userId);
    if (user != null) {
    return user;
    }

    // L2: Redis
    String key = "user:" + userId;
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
    user = JSON.parseObject(cached, User.class);
    localCache.put(userId, user); // 回填L1
    return user;
    }

    // L3: 数据库
    user = userMapper.selectById(userId);
    if (user != null) {
    // 回填L2
    long ttl = REDIS_TTL_MINUTES + ThreadLocalRandom.current().nextInt(5);
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
    ttl, TimeUnit.MINUTES);
    // 回填L1
    localCache.put(userId, user);
    }
    return user;
    }

    /**
    * 更新数据 + 多级缓存失效
    */

    @Transactional
    public void updateUser(User user) {
    // 1. 更新数据库
    userMapper.updateById(user);

    // 2. 删除Redis缓存
    String key = "user:" + user.getId();
    redisTemplate.delete(key);

    // 3. 发布消息,通知所有节点失效本地缓存
    messageTemplate.convertAndSend("cache:invalidate",
    "user:" + user.getId());
    }

    /**
    * 监听缓存失效消息(每个节点都监听)
    */

    @RedisMessageListener(channel = "cache:invalidate")
    public void onCacheInvalidate(String message) {
    // 解析key,失效本地缓存
    if (message.startsWith("user:")) {
    Long userId = Long.parseLong(message.substring(5));
    localCache.invalidate(userId);
    }
    }
    }


    十三、Spring Cache抽象

    13.1 基本配置

    @Configuration
    @EnableCaching
    public class SpringCacheConfig {

    /**
    * Redis缓存管理器
    */

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
    // 序列化配置
    GenericJackson2JsonRedisSerializer jsonSerializer =
    new GenericJackson2JsonRedisSerializer();

    RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
    .defaultCacheConfig()
    .entryTtl(Duration.ofMinutes(30))
    .serializeKeysWith(RedisSerializationContext.SerializationPair
    .fromSerializer(new StringRedisSerializer()))
    .serializeValuesWith(RedisSerializationContext.SerializationPair
    .fromSerializer(jsonSerializer))
    .disableCachingNullValues();

    // 不同缓存空间不同TTL
    Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
    cacheConfigs.put("user", defaultConfig.entryTtl(Duration.ofMinutes(30)));
    cacheConfigs.put("config", defaultConfig.entryTtl(Duration.ofHours(2)));
    cacheConfigs.put("hotData", defaultConfig.entryTtl(Duration.ofMinutes(5)));

    return RedisCacheManager.builder(factory)
    .cacheDefaults(defaultConfig)
    .withInitialCacheConfigurations(cacheConfigs)
    .transactionAware()
    .build();
    }
    }

    13.2 注解使用

    @Service
    @CacheConfig(cacheNames = "user")
    public class UserCacheService {

    @Autowired
    private UserMapper userMapper;

    /**
    * 查询缓存(key使用SpEL表达式)
    */

    @Cacheable(key = "#userId", unless = "#result == null")
    public User getUserById(Long userId) {
    return userMapper.selectById(userId);
    }

    /**
    * 条件缓存(只缓存活跃用户)
    */

    @Cacheable(key = "#userId", condition = "#userId > 0")
    public User getActiveUser(Long userId) {
    return userMapper.selectActiveById(userId);
    }

    /**
    * 更新后删除缓存
    */

    @CacheEvict(key = "#user.id")
    @Transactional
    public void updateUser(User user) {
    userMapper.updateById(user);
    }

    /**
    * 更新并同步缓存
    */

    @CachePut(key = "#user.id")
    @Transactional
    public User updateUserAndCache(User user) {
    userMapper.updateById(user);
    return user; // 返回值将更新到缓存
    }

    /**
    * 删除所有缓存
    */

    @CacheEvict(allEntries = true)
    public void clearAllUserCache() {
    // 清空user缓存空间
    }

    /**
    * 多缓存空间操作
    */

    @Caching(
    evict = {
    @CacheEvict(cacheNames = "user", key = "#userId"),
    @CacheEvict(cacheNames = "userList", allEntries = true)
    }
    )
    public void deleteUser(Long userId) {
    userMapper.deleteById(userId);
    }
    }

    13.3 自定义缓存Key生成器

    @Component
    public class CustomKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
    // 类名:方法名:参数
    StringBuilder sb = new StringBuilder();
    sb.append(target.getClass().getSimpleName());
    sb.append(":").append(method.getName());
    for (Object param : params) {
    sb.append(":").append(param != null ? param.toString() : "null");
    }
    return sb.toString();
    }
    }


    十四、Redis在Java中的使用

    14.1 Jedis(同步客户端)

    @Configuration
    public class JedisConfig {

    @Bean
    public JedisPool jedisPool(
    @Value("${redis.host}") String host,
    @Value("${redis.port}") int port,
    @Value("${redis.password}") String password) {

    JedisPoolConfig poolConfig = new JedisPoolConfig();
    poolConfig.setMaxTotal(32);
    poolConfig.setMaxIdle(16);
    poolConfig.setMinIdle(8);
    poolConfig.setMaxWaitMillis(3000);
    poolConfig.setTestOnBorrow(true);

    return new JedisPool(poolConfig, host, port, 3000, password);
    }
    }

    @Service
    public class JedisService {

    @Autowired
    private JedisPool jedisPool;

    public String get(String key) {
    try (Jedis jedis = jedisPool.getResource()) {
    return jedis.get(key);
    }
    }

    public void set(String key, String value, int expireSeconds) {
    try (Jedis jedis = jedisPool.getResource()) {
    jedis.setex(key, expireSeconds, value);
    }
    }

    /**
    * Pipeline批量操作
    */

    public List<Object> pipelineGet(List<String> keys) {
    try (Jedis jedis = jedisPool.getResource()) {
    Pipeline pipeline = jedis.pipelined();
    keys.forEach(pipeline::get);
    return pipeline.syncAndReturnAll();
    }
    }

    /**
    * Lua脚本(原子操作)
    */

    public Long decrStock(String stockKey) {
    String script =
    "local stock = tonumber(redis.call('get', KEYS[1])) " +
    "if stock > 0 then " +
    " redis.call('decr', KEYS[1]) " +
    " return 1 " +
    "else " +
    " return 0 " +
    "end";
    try (Jedis jedis = jedisPool.getResource()) {
    return (Long) jedis.eval(script,
    Collections.singletonList(stockKey),
    Collections.emptyList());
    }
    }
    }

    14.2 Lettuce(异步/响应式客户端)

    @Service
    public class LettuceService {

    @Autowired
    private RedisConnectionFactory connectionFactory;

    /**
    * 异步操作
    */

    public CompletableFuture<String> asyncGet(String key) {
    RedisConnection connection = connectionFactory.getConnection();
    // Lettuce底层使用Netty,天然支持异步
    return CompletableFuture.supplyAsync(() -> {
    byte[] value = connection.get(key.getBytes());
    return value != null ? new String(value) : null;
    });
    }

    /**
    * 响应式操作(Reactive)
    */

    public Mono<String> reactiveGet(String key) {
    ReactiveRedisTemplate<String, String> reactiveTemplate =
    new ReactiveRedisTemplate<>(connectionFactory,
    RedisSerializationContext.string());
    return reactiveTemplate.opsForValue().get(key);
    }
    }

    14.3 RedisTemplate(Spring封装)

    @Configuration
    public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
    RedisConnectionFactory factory) {

    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);

    // Key序列化
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());

    // Value序列化(JSON)
    GenericJackson2JsonRedisSerializer jsonSerializer =
    new GenericJackson2JsonRedisSerializer();
    template.setValueSerializer(jsonSerializer);
    template.setHashValueSerializer(jsonSerializer);

    template.afterPropertiesSet();
    return template;
    }
    }

    @Service
    public class RedisTemplateService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
    * 通用缓存操作
    */

    public void set(String key, Object value, long timeout, TimeUnit unit) {
    redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public <T> T get(String key, Class<T> clazz) {
    Object value = redisTemplate.opsForValue().get(key);
    return clazz.cast(value);
    }

    /**
    * 批量删除(使用SCAN避免阻塞)
    */

    public void deleteByPattern(String pattern) {
    Set<String> keys = new HashSet<>();
    ScanOptions options = ScanOptions.scanOptions()
    .match(pattern)
    .count(1000)
    .build();

    try (Cursor<String> cursor = redisTemplate.scan(options)) {
    while (cursor.hasNext()) {
    keys.add(cursor.next());
    if (keys.size() >= 1000) {
    redisTemplate.delete(keys);
    keys.clear();
    }
    }
    }

    if (!keys.isEmpty()) {
    redisTemplate.delete(keys);
    }
    }

    /**
    * 执行Redis命令(通用)
    */

    public Object executeCommand(String command, String... args) {
    return redisTemplate.execute((RedisCallback<Object>) connection -> {
    byte[][] rawArgs = Arrays.stream(args)
    .map(String::getBytes)
    .toArray(byte[][]::new);
    return connection.execute(command, rawArgs);
    });
    }
    }


    十五、最佳实践

    15.1 Key设计规范

    /**
    * 缓存Key常量类(统一管理)
    */

    public final class CacheKeys {

    private CacheKeys() {}

    private static final String SEPARATOR = ":";

    /** 用户信息: user:info:{userId} */
    public static String userInfo(Long userId) {
    return "user:info" + SEPARATOR + userId;
    }

    /** 商品库存: product:stock:{productId} */
    public static String productStock(Long productId) {
    return "product:stock" + SEPARATOR + productId;
    }

    /** 分布式锁: lock:{business}:{resourceId} */
    public static String lock(String business, String resourceId) {
    return "lock" + SEPARATOR + business + SEPARATOR + resourceId;
    }

    /** 排行榜: rank:{type}:{date} */
    public static String rank(String type, String date) {
    return "rank" + SEPARATOR + type + SEPARATOR + date;
    }
    }

    15.2 大Key处理

    @Service
    public class BigKeyHandler {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * Hash大Key分片存储
    * 原始: user:detail:{userId} (可能包含上百个字段)
    * 分片: user:detail:{userId}:0, user:detail:{userId}:1 …
    */

    public void saveLargeHash(Long userId, Map<String, String> fields) {
    int shardSize = 50; // 每个分片50个字段
    List<Map.Entry<String, String>> entries = new ArrayList<>(fields.entrySet());

    for (int i = 0; i < entries.size(); i += shardSize) {
    int end = Math.min(i + shardSize, entries.size());
    String shardKey = "user:detail:" + userId + ":" + (i / shardSize);

    Map<String, String> shard = new HashMap<>();
    for (int j = i; j < end; j++) {
    shard.put(entries.get(j).getKey(), entries.get(j).getValue());
    }
    redisTemplate.opsForHash().putAll(shardKey, shard);
    }
    }

    /**
    * 异步删除大Key(UNLINK代替DEL)
    */

    public void asyncDeleteBigKey(String key) {
    redisTemplate.unlink(key); // Redis 4.0+ 非阻塞删除
    }
    }

    15.3 热Key处理

    @Service
    public class HotKeyHandler {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 本地缓存分散热Key压力
    private final Cache<String, String> localHotCache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(1, TimeUnit.SECONDS) // 极短过期
    .build();

    /**
    * 热Key多副本打散
    * 将 hot:key 拆分为 hot:key:0, hot:key:1, …, hot:key:N
    */

    public String getHotKey(String baseKey) {
    // 先查本地缓存
    String localValue = localHotCache.getIfPresent(baseKey);
    if (localValue != null) {
    return localValue;
    }

    // 随机选择一个副本
    int replicaCount = 4;
    int replicaIndex = ThreadLocalRandom.current().nextInt(replicaCount);
    String replicaKey = baseKey + ":" + replicaIndex;

    String value = redisTemplate.opsForValue().get(replicaKey);
    if (value != null) {
    localHotCache.put(baseKey, value);
    }
    return value;
    }

    /**
    * 写入所有副本
    */

    public void setHotKey(String baseKey, String value, long ttlSeconds) {
    int replicaCount = 4;
    for (int i = 0; i < replicaCount; i++) {
    redisTemplate.opsForValue().set(
    baseKey + ":" + i, value, ttlSeconds, TimeUnit.SECONDS);
    }
    }
    }

    15.4 缓存预热

    @Component
    public class CacheWarmUpRunner implements ApplicationRunner {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ProductMapper productMapper;

    private final ExecutorService warmUpExecutor = Executors.newFixedThreadPool(4);

    @Override
    public void run(ApplicationArguments args) {
    // 系统启动时异步预热热点数据
    warmUpExecutor.submit(this::warmUpHotProducts);
    }

    private void warmUpHotProducts() {
    List<Product> hotProducts = productMapper.selectHotProducts(1000);
    for (Product product : hotProducts) {
    String key = "product:" + product.getId();
    long ttl = 30 + ThreadLocalRandom.current().nextInt(10);
    redisTemplate.opsForValue().set(key,
    JSON.toJSONString(product), ttl, TimeUnit.MINUTES);
    }
    }
    }

    15.5 监控与告警

    @Service
    public class RedisMonitorService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
    * 获取Redis运行状态
    */

    public Map<String, String> getRedisHealth() {
    Properties info = redisTemplate.getConnectionFactory()
    .getConnection().info();
    Map<String, String> health = new HashMap<>();
    if (info != null) {
    health.put("used_memory_human", info.getProperty("used_memory_human"));
    health.put("connected_clients", info.getProperty("connected_clients"));
    health.put("keyspace_hits", info.getProperty("keyspace_hits"));
    health.put("keyspace_misses", info.getProperty("keyspace_misses"));
    health.put("instantaneous_ops_per_sec",
    info.getProperty("instantaneous_ops_per_sec"));

    // 计算命中率
    long hits = Long.parseLong(info.getProperty("keyspace_hits", "0"));
    long misses = Long.parseLong(info.getProperty("keyspace_misses", "0"));
    double hitRate = (hits + misses) > 0
    ? (double) hits / (hits + misses) * 100 : 0;
    health.put("hit_rate", String.format("%.2f%%", hitRate));
    }
    return health;
    }

    /**
    * 慢查询日志
    */

    public List<Object> getSlowLog(int count) {
    return redisTemplate.execute((RedisCallback<List<Object>>)
    connection -> connection.slowLogGet(count));
    }
    }

    15.6 生产环境CheckList

    检查项建议
    所有Key必须设置TTL 防止内存无限增长
    禁止使用 KEYS * 使用 SCAN 替代
    避免大Key(>10KB) 拆分或使用Hash分片
    连接池配置 Lettuce: 共享连接; Jedis: maxTotal=32
    序列化方式 JSON(可读)或 Protobuf(性能)
    淘汰策略 maxmemory-policy allkeys-lru
    持久化 主节点AOF(everysec) + 从节点RDB
    监控 内存/命中率/连接数/慢查询
    安全 设置密码 + 禁用危险命令 + 绑定IP
    热Key 本地缓存 + 多副本
    雪崩防护 随机TTL + 多级缓存 + 限流
    一致性 先更新DB再删缓存 + 延迟双删/Canal

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Java深入解析篇三十二之分布式缓存
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址