本地缓存一时爽,数据修改修罗场

01 引言

在追求高性能系统的征途中,本地缓存(Local Cache)扮演着关键角色。它将数据存储在应用进程内存中,省去了网络开销和磁盘I/O,提供近乎瞬时的数据访问。

由于本地缓存的高效性,备受广大开发者喜爱,但是本地缓存的内存和程序进程共享一个JVM,缓存的大小直接影响到程序进程的内存,若使用不当,这把“利器”也可能伤及自身。

02 常用的本地缓存

本地缓存就是数据存储在应用程序进程的内存中,每个应用实例拥有自己独立的缓存副本。

2.1 Caffeine

当前性能最优、功能最丰富的 Java 本地缓存库。API 友好,支持多种淘汰策略、异步加载、刷新等。

Maven

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>${latest.version}</version>
</dependency>

案例

Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)                 // 最大条目数
    .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后过期时间
    .recordStats()                      // 开启统计
    .build();

// 取值(自动加载):取值后自动放入缓存
User user = cache.get(userId, id -> userDao.getUser(id));

2.2 Guava Cache

Google 出品,成熟稳定,功能强大,曾是主流选择,现在常被 Caffeine 替代(性能更好)。

Maven

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>${latest.version}</version>
</dependency>

案例

// 使用Guava Cache加载全局配置
LoadingCache<String, Config> configCache = CacheBuilder.newBuilder()
    .refreshAfterWrite(5, TimeUnit.MINUTES)
    .build(new CacheLoader<>() {
        @Override
        public Config load(String key) {
            return configService.loadConfig(key);
        }
    });

// 获取配置(数万次/秒)
Config config = configCache.get("system_params");

2.3 ConcurrentHashMap

ConcurrentHashMap + 自定义逻辑,以最基础的方式,需要手动处理过期、淘汰、并发等问题,不推荐用于复杂场景。

ConcurrentHashMap cache = new ConcurrentHashMap();

简单来说,就是提供了一个存储数据的集合容器,一个加了分段锁的Map集合而已,里面的过期、淘汰等都需要自行设计。Spring的三级缓存也使用的是ConcurrentHashMap

2.4 对比

CaffeineGuava Cache都是非常优秀的本地缓存框架,有丰富的API,已经满足我们日常的需求。既有过期策略,又有淘汰机制还有权重以及缓存命中统计等功能。使用者任选其一即可。

ConcurrentHashMap就是一个普通的Map,仅适用于简单的场景。

03 修罗场

本地缓存的使用非常简单,但在使用过程中会出现问题呢?看看你有没有被击中!

3.1 内存溢出

由于本地缓存都是和程序进程共享的一个JVM内存,如果不设置容量的上线,随着缓存的增大,程序进程的内存被严重压缩,内存溢出就像定时炸弹一样,随时都有可能引爆。

排查问题难度增大。因为随着程序的终止,重启之后内存被重新分配,几乎没有留下任何线索。不知道是缓存的导致的还是程序进程导致的内存溢出。

知道了问题的所在,解决方案也就变的简单了。

  • 必须设置本地缓存的容量上限(maximumSize/maximumWeight),以确保缓存占用的内存不影响程序进程的内存。
  • 必须定义过期策略(expireAfterWrite/expireAfterAccess),确保缓存的内存及时释放,给更需要的缓存腾出空间。

3.2 集群一致性

生产的节点一般都不是一个节点而是多节点部署,多节点部署中,每一个节点都会拥有独属于自己的本地缓存,而且多节点之间的缓存并不可见。

还记得之前遇到的生产问题,因为线上需要修改数据,修改数据之后发现并不生效,排查下来是因为使用了本地缓存导致的。本地缓存无法及时更新,设置的有效期时间又偏长,为了使修改的数据生效,只能重启应用重置本地缓存才解决。

本地缓存不像Redis等分布式缓存一样,可以通过客户端统一修改,以达到最终数据一致性。为了解决这一问题,我们特意为应用程序留了后门以便操作本地缓存,但是由于多节点的部署,需要手动更新每一个节点的缓存才会生效。

最终的解决方案来了,通过广播通知的方式,通知每一个节点更新缓存。这里广播通知的技术有很多。

  • Redis的发布订阅模,
  • MQ消息队列的方式(Kafka、ActiveMQ、RocketMQ等)

3.3 内存监控

监控是一个项目架构设计的重要指标,缓存的使用情况、命中情况,内存的使用情况等可以协助我们了解缓存使用的合理不合理。但是,日常开发中可能大家更聚焦于缓存的使用,至于缓存的命中率、使用的效果可能并不关注。

而本地缓存本身实现了缓存的使用情况,通过对缓存的分析,及时调整参数和冷热数据,让缓存发挥自己最大的效果。

04 小结

Java本地缓存是把双刃剑,合理使用可使QPS提升10倍以上,滥用则会导致服务崩溃。大家在使用本地缓存的时候,有没有遇到过这些问题呢?又是怎么解决的呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值