首页 游戏 软件 资讯 排行榜 专题
首页
数据库
Redis旁路缓存深度解析

Redis旁路缓存深度解析

热心网友
73
转载
2026-04-30

为什么需要缓存

在互联网应用里,数据库往往是整个系统的性能瓶颈。想象一下,当海量并发读取请求瞬间涌来时,如果每次都直接去敲数据库的门,会发生什么?

结果通常不太美妙:数据库连接池被迅速榨干,复杂查询带来的响应延迟动辄几十甚至几百毫秒,用户体验直线下降。更棘手的是,想通过简单增加数据库实例来扩容,成本高昂且收效未必显著。

这时候,缓存的价值就凸显出来了。它的思路很直接:把那些被频繁访问的“热点”数据,提前搬到内存里。内存的访问速度是微秒级的,相比磁盘I/O,这简直是降维打击。

旁路缓存核心原理

在众多缓存应用模式中,旁路缓存(Cache-Aside Pattern)堪称经典中的经典。它的核心哲学非常清晰:由应用程序主动管理缓存,数据库是绝对的数据源头,缓存只是它的一个高效“副本”

读操作流程

public Product getProduct(Long id) {
    // 1. 先查缓存
    Product cachedProduct = redis.get("product:" + id);
    if (cachedProduct != null) {
        // 缓存命中,直接返回
        return cachedProduct;
    }
    // 2. 缓存未命中,查询数据库
    Product product = productMapper.findById(id);
    if (product != null) {
        // 3. 将数据写入缓存,设置过期时间
        redis.set("product:" + id, product, 3600);
    }
    return product;
}

读操作的逻辑链条非常直观:

  1. 第一步,先去Redis里看看有没有。
  2. 如果有(缓存命中),皆大欢喜,直接返回。
  3. 如果没有(缓存未命中),那就只能老老实实去查数据库。
  4. 从数据库拿到数据后,别忘了“回填”到缓存里,并给它设置一个合理的过期时间,方便后续请求快速获取。

写操作流程

public void updateProduct(Product product) {
    // 1. 先更新数据库
    productMapper.update(product);
    // 2. 再删除缓存
    redis.del("product:" + product.getId());
}

写操作采用了先更新数据库,再删除缓存的策略。为什么是“删除”而不是“更新”缓存?这里面有几个关键的考量:

  • 保证数据最终一致性:即使缓存删除这一步失败了,数据库里的数据也已经是最新的。下次读取时,缓存未命中会触发数据库查询,并将新数据重新加载到缓存中,最终达到一致。
  • 规避并发难题:在并发写场景下,更新缓存的时序很难控制,容易导致缓存与数据库数据不一致。直接删除缓存则简单粗暴且有效。
  • 简化系统复杂度:不需要去维护一套缓存与数据库之间强同步的复杂机制。

缓存三大经典问题

1. 缓存穿透

问题描述:查询一个根本不存在的数据。因为缓存和数据库里都没有,所以每次请求都会穿透缓存,直接打到数据库上。

危害:这可不是小问题。如果被恶意攻击者利用,持续用大量不存在的数据ID发起请求,数据库很可能被压垮。

解决方案

public Product getProduct(Long id) {
    // 参数校验
    if (id == null || id <= 0) {
        return null;
    }
    // 1. 先查缓存
    Product cachedProduct = redis.get("product:" + id);
    if (cachedProduct != null) {
        return cachedProduct;
    }
    // 2. 缓存未命中,查询数据库
    Product product = productMapper.findById(id);
    if (product == null) {
        // 3. 缓存空值,防止穿透
        redis.set("product:" + id, null, 60); // 短过期时间
        return null;
    }
    // 4. 写入缓存
    redis.set("product:" + id, product, 3600);
    return product;
}

额外防护措施:对于穿透风险极高的场景,可以引入布隆过滤器(Bloom Filter)作为前置屏障。

// 使用布隆过滤器
private BloomFilter bloomFilter;

public Product getProduct(Long id) {
    // 布隆过滤器检查
    if (!bloomFilter.mightContain(id)) {
        return null; // 一定不存在
    }
    // 正常查询流程
    Product product = getProductFromCacheOrDB(id);
    return product;
}

2. 缓存击穿

问题描述:注意,这和“穿透”不同。击穿指的是某个热点Key在缓存过期的瞬间,大量并发请求同时发现缓存失效,于是这些请求像洪水一样全部涌向数据库。

解决方案

方案一:互斥锁

private boolean lock = false;
public Product getProduct(Long id) {
    Product product = redis.get("product:" + id);
    if (product != null) {
        return product;
    }
    // 获取锁
    if (tryLock("lock:product:" + id)) {
        try {
            // Double Check
            product = redis.get("product:" + id);
            if (product != null) {
                return product;
            }
            // 查询数据库
            product = productMapper.findById(id);
            redis.set("product:" + id, product, 3600);
        } finally {
            unlock("lock:product:" + id);
        }
    } else {
        // 等待后重试
        Thread.sleep(100);
        return getProduct(id);
    }
    return product;
}

方案二:逻辑过期

public Product getProduct(Long id) {
    // 1. 查缓存
    Product product = redis.get("product:" + id);
    if (product == null) {
        // 缓存为空,尝试获取锁重建缓存
        if (tryLock("lock:product:" + id)) {
            Product newProduct = productMapper.findById(id);
            redis.set("product:" + id, newProduct, 3600);
            unlock("lock:product:" + id);
            return newProduct;
        }
        // 等待后重试
        Thread.sleep(100);
        return getProduct(id);
    }
    // 2. 检查是否逻辑过期
    if (isLogicalExpired(product)) {
        // 异步重建缓存,不阻塞请求
        if (tryLock("lock:product:" + id)) {
            threadPool.execute(() -> {
                Product newProduct = productMapper.findById(id);
                redis.set("product:" + id, newProduct, 3600);
                unlock("lock:product:" + id);
            });
        }
    }
    return product;
}

3. 缓存雪崩

问题描述:大量缓存数据在同一时间过期失效,导致所有请求在那一刻都去查询数据库,数据库压力瞬间激增,可能引发连锁故障。

解决方案

方案一:随机过期时间

// 设置过期时间添加随机值
int baseExpire = 3600;
int randomExpire = ThreadLocalRandom.current().nextInt(300);
redis.set("product:" + id, product, baseExpire + randomExpire);

方案二:多级缓存

public Product getProduct(Long id) {
    // 1. 先查本地缓存
    Product product = localCache.get(id);
    if (product != null) {
        return product;
    }
    // 2. 查 Redis
    product = redis.get("product:" + id);
    if (product != null) {
        // 回填本地缓存
        localCache.put(id, product, 300);
        return product;
    }
    // 3. 查数据库
    product = productMapper.findById(id);
    redis.set("product:" + id, product, 3600);
    return product;
}

方案三:服务降级

public Product getProduct(Long id) {
    try {
        // 1. 查缓存
        Product product = redis.get("product:" + id);
        if (product != null) {
            return product;
        }
        // 2. 缓存未命中,降级处理
        return getProductFromBackup(id);
    } catch (Exception e) {
        // Redis 异常,降级到数据库
        log.error("Redis error, fallback to DB", e);
        return productMapper.findById(id);
    }
}

数据一致性方案

延迟双删

public void updateProduct(Product product) {
    // 1. 删除缓存
    redis.del("product:" + product.getId());
    // 2. 更新数据库
    productMapper.update(product);
    // 3. 延迟删除缓存
    threadPool.execute(() -> {
        try {
            Thread.sleep(1000);
            redis.del("product:" + product.getId());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

延迟双删策略主要用在写操作非常频繁,且对数据一致性要求极高的场景。第二次延迟删除,是为了清理掉在“更新数据库”这个极短时间窗口内,可能被其他线程读请求重新写入缓存的旧数据。

订阅 Binlog + Canal

// Canal 配置
@CanalMessageListener(topic = "product_db.product")
public void onMessage(CanalEntry.Entry entry) {
    CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
    for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
        if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
            // 更新操作
            for (Column column : rowData.getBeforeColumnsList()) {
                if ("id".equals(column.getName())) {
                    Long id = Long.parseLong(column.getValue());
                    redis.del("product:" + id);
                }
            }
        }
    }
}

优势

  • 完全解耦:缓存更新异步化,完全不干扰主业务逻辑。
  • 保证最终一致性:基于数据库的Binlog,能可靠地捕获所有数据变更。
  • 适合大型系统:在复杂的分布式架构中,这种方案的可维护性和扩展性更好。

缓存策略最佳实践

缓存 key 设计

// 好的设计
String key = "product:info:" + categoryId + ":" + productId;
String key = "user:profile:" + userId;
String key = "order:summary:" + dateStr;

// 避免的设计
String key = "product_" + productId;           // 缺少命名空间,易冲突
String key = getComplexKey(product);            // 包含复杂计算,影响性能
String key = "temp:" + System.currentTimeMillis(); // 使用时效性数据,难以管理

缓存 Value 设计

// 序列化配置
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
    return productMapper.findById(id);
}

// JSON 序列化
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator());
        serializer.setObjectMapper(mapper);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        return template;
    }
}

过期时间策略

数据类型 过期时间 原因
热点商品 24 小时 数据相对稳定
用户会话 30 分钟 安全性考虑
排行榜数据 5 分钟 更新频繁
配置信息 1 小时 变更不频繁
计数器 不过期 需要持久化

容量规划

// 预估缓存容量

// 假设每秒 10000 次查询,缓存 10000 条数据

// 每条数据 1KB

// 需要的内存 = 10000 * 1KB = 10MB

// 实际规划需要预留 20-30% 冗余

// 还需要考虑 Redis 本身的内存开销

监控告警

# 监控指标
- alert: RedisHighMemoryUsage
  expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Redis 内存使用率过高"

- alert: RedisHighHitMissRatio
  expr: redis_keyspace_hits_total / (redis_keyspace_hits_total + redis_keyspace_misses_total) < 0.8
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Redis 缓存命中率过低"

性能优化技巧

批量操作

// 批量查询
public Map getProducts(List ids) {
    List keys = ids.stream()
        .map(id -> "product:" + id)
        .collect(Collectors.toList());
    List products = redis.mGet(keys);
    Map result = new HashMap<>();
    for (int i = 0; i < ids.size(); i++) {
        if (products.get(i) != null) {
            result.put(ids.get(i), products.get(i));
        }
    }
    // 批量回补缓存
    result.forEach((id, product) ->
         redis.set("product:" + id, product, 3600)
    );
    return result;
}

Pipeline 批量写入

public void batchWriteProducts(List products) {
    redis.executePipelined((RedisCallback) connection -> {
        for (Product product : products) {
            String key = "product:" + product.getId();
            byte[] value = serializationUtils.serialize(product);
            connection.setEx(key.getBytes(), 3600, value);
        }
        return null;
    });
}

缓存预热

@PostConstruct
public void warmupCache() {
    // 系统启动时预加载热点数据
    log.info("Start cache warmup...");
    List hotProductIds = productService.getHotProductIds();
    for (Long id : hotProductIds) {
        Product product = productMapper.findById(id);
        redis.set("product:" + id, product, 3600);
    }
    log.info("Cache warmup completed, {} products loaded", hotProductIds.size());
}

常见错误与规避

错误一:缓存与数据库双写不一致

// 错误写法:先更新缓存,再更新数据库
public void updateProduct(Product product) {
    redis.set("product:" + product.getId(), product);  // 先更新缓存
    productMapper.update(product);                       // 后更新数据库
    // 并发时可能缓存是旧数据
}

// 正确写法:先删缓存,再更新数据库
public void updateProduct(Product product) {
    redis.del("product:" + product.getId());  // 先删缓存
    productMapper.update(product);             // 后更新数据库
}

错误二:缓存频繁更新

// 错误写法:每次访问都更新缓存
public Product getProduct(Long id) {
    Product product = redis.get("product:" + id);
    if (product == null) {
        product = productMapper.findById(id);
    }
    // 每次都更新,浪费资源
    redis.set("product:" + id, product, 3600);
    return product;
}

// 正确写法:只在缓存不存在时更新
public Product getProduct(Long id) {
    Product product = redis.get("product:" + id);
    if (product == null) {
        product = productMapper.findById(id);
        if (product != null) {
            redis.set("product:" + id, product, 3600);
        }
    }
    return product;
}

错误三:大对象缓存

// 错误写法:缓存整个列表
public List getAllProducts() {
    List products = redis.get("all_products");
    if (products == null) {
        products = productMapper.findAll();
        redis.set("all_products", products, 300);
    }
    return products;
}

// 正确写法:分页缓存或使用压缩
public List getProducts(int page, int size) {
    String key = "products:" + page + ":" + size;
    return redis.get(key);
}

总结

旁路缓存确实是提升系统读取性能的一把利器,但要想用好它,有几个关键点必须牢牢把握:

  1. 设计要合理:没有放之四海而皆准的策略,必须根据业务的数据访问模式、一致性要求来量身定制。
  2. 一致性是根本:根据业务对一致性的容忍度,在“延迟双删”和“Binlog订阅”等方案中做出明智选择。
  3. 容错不能忘:穿透、击穿、雪崩这“三兄弟”的防护措施,是线上系统的安全底线。
  4. 监控是眼睛:缓存命中率、内存使用量、响应时间,这些关键指标必须纳入实时监控和告警体系。
  5. 预案需完备:系统启动时的缓存预热,异常发生时的服务降级,这些预案能让系统更稳健。

说到底,正确且深入地理解并应用旁路缓存,往往能让系统性能实现数量级的提升。这不仅是优化技巧,更是现代后端开发者必须掌握的核心架构能力之一。

来源:https://www.jb51.net/database/3620213rq.htm
免责声明: 游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

最新APP

宝宝过生日
宝宝过生日
应用辅助 04-07
台球世界
台球世界
体育竞技 04-07
解绳子
解绳子
休闲益智 04-07
骑兵冲突
骑兵冲突
棋牌策略 04-07
三国真龙传
三国真龙传
角色扮演 04-07

热门推荐

NeuroStream视觉数据底座实测报告发布性能与应用解析
科技数码
NeuroStream视觉数据底座实测报告发布性能与应用解析

随着人工智能大模型与机器视觉技术的深度融合与产业升级,一个根本性的挑战愈发关键:底层视觉数据基础设施的能效水平,直接决定了上层AI应用的成本边界与识别精度的上限。近期,Robo ai (NASDAQ: AIIO) 旗下专注于AI基础设施的Neurovia AI,在第九届国际安全与国家风险防范展(IS

热心网友
05.27
安全出币技巧指南:掌握高效交易与资金保障的关键
web3.0
安全出币技巧指南:掌握高效交易与资金保障的关键

数字货币成功变现需掌握关键技巧:理解市场动态与主流币种联动,选择安全高流动性平台,制定明确风险目标和交易策略,严格执行止损与分散投资。市场持续变化,保持学习与适应能力是长期稳健交易的基础。

热心网友
05.27
华硕电竞显示器618选购指南 高性价比双子星推荐
科技数码
华硕电竞显示器618选购指南 高性价比双子星推荐

618购物节是电竞玩家升级装备的良机。华硕TUFGaming系列的战杀27与小金刚显示器凭借FastIPS面板、高刷新率、精准色彩及丰富电竞功能,以高性价比满足不同玩家对帧率与画质的追求,成为热门选择。

热心网友
05.27
2026年二战飞行游戏推荐:空战模拟与对战佳作盘点
游戏资讯
2026年二战飞行游戏推荐:空战模拟与对战佳作盘点

移动端二战空战游戏以机械浪漫与硬核操作吸引玩家。多款作品各具特色:或精细还原战机与基地经营,或重现太平洋战场任务,或融合弹幕射击与昼夜战术,或侧重战机收集养成,或提供割草式爽快体验。它们以历史氛围带玩家重返决定历史的天空。

热心网友
05.27
和平精英安V收车币如何革新游戏经济与玩家交易生态
web3.0
和平精英安V收车币如何革新游戏经济与玩家交易生态

《和平精英》中,“安V收车币”作为一种新兴交易方式,为玩家获取稀有车辆皮肤提供了安全便捷的渠道。它满足了玩家个性化需求,提升了游戏体验与沉浸感。参与交易需选择正规平台,合理规划消费并遵守官方规定,以保障自身权益。这一模式活跃了游戏经济,丰富了玩家的资源选择。

热心网友
05.27