当前位置: 代码网 > it编程>数据库>Redis > Redis+Caffeine如何构建高性能二级缓存

Redis+Caffeine如何构建高性能二级缓存

2025年05月12日 Redis 我要评论
二级缓存架构的技术背景1. 基础缓存架构在现代分布式系统设计中,缓存是优化服务性能的核心组件。标准实现方案采用远程缓存(如redis/memcached)作为数据库前置层,通过以下机制提升性能:读写策

二级缓存架构的技术背景

1. 基础缓存架构

在现代分布式系统设计中,缓存是优化服务性能的核心组件。标准实现方案采用远程缓存(如redis/memcached)作为数据库前置层,通过以下机制提升性能:

  • 读写策略:遵循cache-aside模式,仅当缓存未命中时查询数据库
  • 核心价值:
  • 将平均响应时间从数据库的10-100ms级别降至1-10ms
  • 降低数据库负载50%-80%(根据命中率变化)

2. 架构演进动因

当系统面临以下场景时,纯远程缓存方案显现局限性:

问题类型

表现特征

典型案例

超高并发读取

redis带宽成为瓶颈

热点商品详情页访问

超低延迟要求

网络往返耗时不可忽略

金融行情数据推送

成本控制需求

高频访问导致redis扩容

用户基础信息查询

3. 二级缓存解决方案

引入本地缓存构建两级缓存体系:

  • 一级缓存:caffeine(高性能本地缓存)
  • 二级缓存:redis cluster(高可用远程缓存)

协同机制:

  • 本地缓存设置短ttl(秒级)
  • 远程缓存设置长ttl(分钟级)
  • 通过pubsub实现跨节点失效

为什么选择本地缓存?

1. 极速访问

内存级响应:本地缓存直接存储在应用进程的内存中(如java堆内),访问速度通常在纳秒级(如caffeine的读写性能可达每秒千万次),而远程缓存(如redis)需要网络通信,延迟在毫秒级。

技术选型

响应时长

本地缓存

~100ns

redis远程缓存

~1ms(受网络影响可能更高)

数据库查询

~10ms 甚至更长。

2. 减少网络io

  • 避免远程调用:每次访问redis都需要经过网络i/o(序列化、传输、反序列化),本地缓存完全绕过这一过程。
  • 适用场景:高频访问的热点数据(如商品详情、用户基础信息),通过本地缓存可减少90%以上的redis请求。

3. 降低远程缓存和数据库压力

  • 保护redis:大量请求直接命中本地缓存,避免redis成为瓶颈(尤其在高并发场景下,如秒杀、热点查询)。
  • 减少穿透风险:本地缓存可设置短期过期时间,避免缓存失效时大量请求直接冲击数据库。

4. 提升系统吞吐量

  • 减少线程阻塞:远程缓存访问会阻塞线程(如redis的同步调用),本地缓存无此问题,尤其适合高并发服务。
  • 案例:某电商系统引入caffeine后,qps从1万提升到5万,redis负载下降60%。

5. 功能灵活

本地缓存支持丰富的特性,满足不同业务需求:

  • 淘汰策略:lru(最近最少使用)、lfu(最不经常使用)、fifo等。
  • 过期控制:支持基于时间(写入后过期、访问后过期)或容量触发淘汰。
  • 原子操作:如get-if-absent-compute(查不到时自动加载),避免并发重复查询。

本地内存具备的功能

1. 基本读写

功能:基础的键值存储与原子操作。

cache<string, string> cache = caffeine.newbuilder().build();

// 写入缓存
cache.put("user:1", "alice");

// 读取缓存(若不存在则自动计算)
string value = cache.get("user:1", key -> fetchfromdb(key));

2. 缓存淘汰策略

功能:限制缓存大小并淘汰数据。

算法

描述

适用场景

代码示例(caffeine)

lru

淘汰最久未访问的数据

热点数据分布不均匀

.maximumsize(100).build()

lfu

淘汰访问频率最低的数据

长期稳定的热点数据

.maximumsize(100).build()

(w-tinylfu)

fifo

按写入顺序淘汰

数据顺序敏感的场景

需自定义实现

3. 过期时间控制

功能:自动清理过期数据。

caffeine.newbuilder()
.expireafterwrite(10, timeunit.minutes) // 写入后10分钟过期
.expireafteraccess(5, timeunit.minutes) // 访问后5分钟过期
.build();

4. 缓存加载与刷新

功能:自动加载数据并支持后台刷新。

asyncloadingcache<string, string> cache = caffeine.newbuilder()
.refreshafterwrite(1, timeunit.minutes) // 1分钟后后台刷新
.buildasync(key -> fetchfromdb(key));

// 获取数据(若需刷新,不会阻塞请求)
completablefuture<string> future = cache.get("user:1");

5. 并发控制

功能:线程安全与击穿保护。

// 自动合并并发请求(同一key仅一次加载)
loadingcache<string, string> cache = caffeine.newbuilder()
    .build(key -> {
        system.out.println("仅执行一次: " + key);
        return fetchfromdb(key);
    });

// 并发测试(输出1次日志)
intstream.range(0, 100).parallel().foreach(
    i -> cache.get("user:1")
);

6. 统计与监控

功能:记录命中率等指标。

cache<string, string> cache = caffeine.newbuilder()
.recordstats() // 开启统计
.build();

cache.get("user:1");
cachestats stats = cache.stats();
system.out.println("命中率: " + stats.hitrate());

7. 持久化

功能:缓存数据持久化到磁盘。

// 使用caffeine + rocksdb(需额外依赖)
cache<string, byte[]> cache = caffeine.newbuilder()
    .maximumsize(100)
    .writer(new cachewriter<string, byte[]>() {
        @override public void write(string key, byte[] value) {
            rocksdb.put(key.getbytes(), value); // 同步写入磁盘
        }
        @override public void delete(string key, byte[] value, removalcause cause) {
            rocksdb.delete(key.getbytes());
        }
    })
    .build();

8. 事件监听

功能:监听缓存变更事件。

cache<string, string> cache = caffeine.newbuilder()
    .removallistener((key, value, cause) -> 
        system.out.println("移除事件: " + key + " -> " + cause))
    .evictionlistener((key, value, cause) -> 
        system.out.println("驱逐事件: " + key + " -> " + cause))
    .build();

本地缓存方案选型

1. concurrenthashmap

concurrenthashmap是java集合框架中提供的线程安全哈希表实现,首次出现在jdk1.5中。它采用分段锁技术(jdk8后改为cas+synchronized优化),通过将数据分成多个段(segment),每个段独立加锁,实现了高并发的读写能力。

作为juc(java.util.concurrent)包的核心组件,它被广泛应用于需要线程安全哈希表的场景。

  • 原生jdk支持,零外部依赖
  • 读写性能接近非同步的hashmap
  • 完全线程安全,支持高并发
  • 提供原子性复合操作(如computeifabsent)
import java.util.concurrent.*;
import java.util.function.function;

public class chmcache<k,v> {
    private final concurrenthashmap<k,v> map = new concurrenthashmap<>(16, 0.75f, 32);
    private final scheduledexecutorservice cleaner = executors.newsinglethreadscheduledexecutor();
    
    // 基础操作
    public void put(k key, v value) {
        map.put(key, value);
    }
    
    // 带ttl的put
    public void put(k key, v value, long ttl, timeunit unit) {
        map.put(key, value);
        cleaner.schedule(() -> map.remove(key), ttl, unit);
    }
    
    // 自动加载
    public v get(k key, function<k,v> loader) {
        return map.computeifabsent(key, loader);
    }
    
    // 批量操作
    public void putall(map<? extends k, ? extends v> m) {
        map.putall(m);
    }
    
    // 清空缓存
    public void clear() {
        map.clear();
    }
}

2. guava cache

guava cache是google guava库中的缓存组件,诞生于2011年。作为concurrenthashmap的增强版,它添加了缓存特有的特性。guava项目本身是google内部java开发的标准库,经过大规模生产环境验证,稳定性和性能都有保障。guava cache广泛应用于各种需要本地缓存的java项目中。

  • google背书,质量有保证
  • 丰富的缓存特性
  • 良好的api设计
  • 完善的文档和社区支持
<dependency>
  <groupid>com.google.guava</groupid>
  <artifactid>guava</artifactid>
  <version>31.1-jre</version>
</dependency>
import com.google.common.cache.*;
import java.util.concurrent.timeunit;

public class guavacachedemo {
    public static void main(string[] args) {
        loadingcache<string, string> cache = cachebuilder.newbuilder()
            .maximumsize(1000) // 最大条目数
            .expireafterwrite(10, timeunit.minutes) // 写入后过期时间
            .expireafteraccess(30, timeunit.minutes) // 访问后过期时间
            .concurrencylevel(8) // 并发级别
            .recordstats() // 开启统计
            .removallistener(notification -> 
                system.out.println("removed: " + notification.getkey()))
            .build(new cacheloader<string, string>() {
                @override
                public string load(string key) throws exception {
                    return loadfromdb(key);
                }
            });
        
        try {
            // 自动加载
            string value = cache.get("user:1001");
            
            // 手动操作
            cache.put("config:timeout", "5000");
            cache.invalidate("user:1001");
            
            // 打印统计
            system.out.println(cache.stats());
        } catch (executionexception e) {
            e.printstacktrace();
        }
    }
    
    private static string loadfromdb(string key) {
        // 模拟数据库查询
        return "db_result_" + key;
    }
}

3. caffeine

caffeine是guava cache作者的新作品,发布于2015年。它专为现代java应用设计,采用window-tinylfu淘汰算法,相比传统lru有更高的命中率。caffeine充分利用java 8特性(如completablefuture),在性能上大幅超越guava cache(3-5倍提升),是目前性能最强的java本地缓存库。

  • 超高性能
  • 更高的缓存命中率
  • 异步刷新机制
  • 精细的内存控制
<dependency>
  <groupid>com.github.ben-manes.caffeine</groupid>
  <artifactid>caffeine</artifactid>
  <version>2.9.3</version>
</dependency>
import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.timeunit;

public class caffeinedemo {
    public static void main(string[] args) {
        // 同步缓存
        cache<string, data> cache = caffeine.newbuilder()
            .maximumsize(10_000)
            .expireafterwrite(5, timeunit.minutes)
            .expireafteraccess(10, timeunit.minutes)
            .refreshafterwrite(1, timeunit.minutes)
            .recordstats()
            .build();
        
        // 异步加载缓存
        asyncloadingcache<string, data> asynccache = caffeine.newbuilder()
            .maximumweight(100_000)
            .weigher((string key, data data) -> data.size())
            .expireafterwrite(10, timeunit.minutes)
            .buildasync(key -> loadfromdb(key));
        
        // 使用示例
        data data = cache.getifpresent("key1");
        completablefuture<data> future = asynccache.get("key1");
        
        // 打印统计
        system.out.println(cache.stats());
    }
    
    static class data {
        int size() { return 1; }
    }
    
    private static data loadfromdb(string key) {
        // 模拟数据库加载
        return new data();
    }
}

4. encache

eehcache是terracotta公司开发的企业级缓存框架,始于2003年。它是jsr-107标准实现之一,支持从本地缓存扩展到分布式缓存。ehcache的特色在于支持多级存储(堆内/堆外/磁盘),适合需要缓存持久化的企业级应用。

最新版本ehcache 3.x完全重构,提供了更现代的api设计。

  • 企业级功能支持
  • 多级存储架构
  • 完善的监控管理
  • 良好的扩展性
<dependency>
  <groupid>org.ehcache</groupid>
  <artifactid>ehcache</artifactid>
  <version>3.9.7</version>
</dependency>
import org.ehcache.*;
import org.ehcache.config.*;
import org.ehcache.config.builders.*;
import java.time.duration;

public class ehcachedemo {
    public static void main(string[] args) {
        // 1. 配置缓存管理器
        cachemanager cachemanager = cachemanagerbuilder.newcachemanagerbuilder()
            .with(cachemanagerbuilder.persistence("/tmp/ehcache-data"))
            .build();
        cachemanager.init();
        
        // 2. 配置缓存
        cacheconfiguration<string, string> config = cacheconfigurationbuilder
            .newcacheconfigurationbuilder(
                string.class, 
                string.class,
                resourcepoolsbuilder.newresourcepoolsbuilder()
                    .heap(1000, entryunit.entries)  // 堆内
                    .offheap(100, memoryunit.mb)    // 堆外
                    .disk(1, memoryunit.gb, true)   // 磁盘
            )
            .withexpiry(expirypolicybuilder.timetoliveexpiration(duration.ofminutes(10)))
            .build();
        
        // 3. 创建缓存
        cache<string, string> cache = cachemanager.createcache("mycache", config);
        
        // 4. 使用缓存
        cache.put("key1", "value1");
        string value = cache.get("key1");
        system.out.println(value);
        
        // 5. 关闭
        cachemanager.close();
    }
}

方案对比

特性

concurrenthashmap

guava cache

caffeine

ehcache

基本缓存功能

过期策略

淘汰算法

lru

w-tinylfu

lru/lfu

自动加载

异步加载

持久化支持

多级存储

命中率统计

基本

详细

详细

分布式支持

内存占用

本地缓存问题及解决

1. 数据一致性

两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。

1.1. 解决方案1: 失效广播机制

通过redis pubsub或rabbit mq等消息中间件实现跨节点通知

  • 优点:实时性较好,能快速同步变更
  • 缺点:增加了系统复杂度,网络分区时可能失效

如果你不想在你的业务代码发送mq消息,还可以适用近几年比较流行的方法:订阅数据库变更日志,再操作缓存。canal 订阅mysql的 binlog日志,当发生变化时向mq发送消息,进而也实现数据一致性。

1.2. 解决方案2:版本号控制

实现原理:

  • 在数据库表中增加版本号字段(version)
  • 缓存数据时同时存储版本号
  • 查询时比较缓存版本与数据库版本
// 版本号校验示例
public product getproduct(long id) {
cacheentry entry = localcache.get(id);
if (entry != null) {
    int dbversion = db.query("select version from products where id=?", id);
    if (entry.version == dbversion) {
        return entry.product; // 版本一致,返回缓存
    }
}
// 版本不一致或缓存不存在,从数据库加载
product product = db.loadproduct(id);
localcache.put(id, new cacheentry(product, product.getversion()));
return product;
}

2. 内存管理问题

2.1. 解决方案1:分层缓存架构

// 组合堆内与堆外缓存
cache<string, object> multilevelcache = caffeine.newbuilder()
.maximumsize(10_000) // 一级缓存(堆内)
.buildasync(key -> {
    object value = offheapcache.get(key); // 二级缓存(堆外)
    if(value == null) value = loadfromdb(key);
    return value;
});
  • 使用window-tinylfu算法自动识别热点
  • 对top 1%的热点数据单独配置更大容量

2.2. 解决方案2:智能淘汰策略

策略类型

适用场景

配置示例

基于大小

固定数量的小对象

maximumsize(10_000)

基于权重

大小差异显著的对象

maximumweight(1gb).weigher()

基于时间

时效性强的数据

expireafterwrite(5min)

基于引用

非核心数据

softvalues()

3. gc压力

3.1. gc压力问题的产生原因

缓存对象生命周期特征:

  • 本地缓存通常持有大量长期存活对象(如商品信息、配置数据)
  • 与传统短期对象(如http请求作用域对象)不同,这些对象会持续晋升到老年代
  • 示例:1gb的本地缓存意味着老年代常驻1gb可达对象

内存结构影响:

// 典型缓存数据结构带来的内存开销
concurrenthashmap<string, product> cache = new concurrenthashmap<>();
// 实际内存占用 = 键对象 + 值对象 + 哈希表entry对象(约额外增加40%开销)

gc行为变化表现:

  • full gc频率上升:从2次/天 → 15次/天(如问题描述)
  • 停顿时间增长:stw时间从120ms → 可能达到秒级(取决于堆大小)
  • 晋升失败风险:当缓存大小接近老年代容量时,容易触发concurrent mode failure

3.2. 解决方案1:堆外缓存(off-heap cache)

// 使用ohc(off-heap cache)示例
ohcache<string, product> ohcache = ohcachebuilder.newbuilder()
.keyserializer(new stringserializer())
.valueserializer(new productserializer())
.capacity(1, unit.gb)
.build();

优势:

  • 完全绕过jvm堆内存管理
  • 不受gc影响,内存由操作系统直接管理
  • 可突破jvm堆大小限制(如缓存50gb数据)

代价:

  • 需要手动实现序列化/反序列化
  • 读取时存在内存拷贝开销(比堆内缓存慢约20-30%)

3.3. 方案2:分区域缓存

// 按业务划分独立缓存实例
public class cacheregistry {
    private static loadingcache<string, product> productcache = ...;  // 商品专用
    private static loadingcache<integer, userprofile> usercache = ...; // 用户专用

    // 独立配置各缓存参数
    static {
        productcache = caffeine.newbuilder()
        .maximumsize(10_000)
        .build(...);

        usercache = caffeine.newbuilder()
        .maximumweight(100mb)
        .weigher(...)
        .build(...);
    }
}

效果:

  • 避免单一超大缓存域导致全局gc压力
  • 可针对不同业务设置差异化淘汰策略

总结

通过以上的分析和实现,可以通过redis+caffeine实现高性能二级缓存实现。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com