前言
微服务架构已成为当今软件开发领域的主流趋势,而 Dubbo 作为一种优秀的微服务框架,其在性能优化方面有着独到的见解。然而,随着服务规模的增长,微服务架构中的性能问题也变得日益突出。就像一辆车需要油来运转一样,微服务也需要一种有效的机制来提高其性能,而 Dubbo 的缓存机制正是为此而生!
LRU缓存机制
LRU 缓存机制:
LRU(Least Recently Used)是一种常见的缓存淘汰算法,其工作原理是根据数据的访问历史来淘汰最近最少使用的数据,以保证缓存中的数据始终是最热门的数据。
工作原理和算法:
LRU 缓存机制基于数据的访问时间进行淘汰。当缓存空间满时,新加入的数据会替换掉最久未被访问的数据。LRU 缓存通常使用链表和哈希表实现。哈希表用于快速查询缓存中的数据,链表用于记录数据的访问顺序。
LRU 缓存算法的核心思想是维护一个有序的访问历史列表,当有新数据访问时,将数据移动到列表头部,当缓存空间满时,淘汰列表尾部的数据。这样可以保证频繁访问的数据总是位于列表的头部,最少访问的数据总是位于列表的尾部。
优点和局限性:
- 优点:
- 简单易实现:LRU 缓存算法思路清晰,实现相对简单。
- 适用范围广:LRU 缓存适用于各种场景,特别是对于访问模式比较集中的情况。
- 局限性:
- 缓存污染:当数据访问模式发生变化时,LRU 缓存可能会导致缓存污染问题,即缓存中的数据可能已经不再是热门数据,但仍然被保留在缓存中。
- 实现复杂度:LRU 缓存需要维护访问历史列表,当数据量较大时,可能会带来较高的实现复杂度和性能开销。
最佳实践和使用场景:
- 最佳实践:
- 合理设置缓存大小:根据系统的实际情况和性能需求,合理设置缓存的大小,避免缓存空间过大或过小。
- 定期清理缓存:定期清理过期或者不再使用的缓存数据,以释放内存资源,避免缓存的无效占用。
- 考虑缓存预热:在系统启动或者高峰时段之前,通过预热缓存的方式,将热门数据提前加载到缓存中,提高系统的响应速度。
- 使用场景:
- 高频热点数据缓存:对于访问频率较高、热点数据集中的场景,LRU 缓存可以有效地提高系统的响应速度和性能。
- 高并发访问场景:对于需要处理大量并发请求的场景,LRU 缓存可以降低系统的负载,提高系统的并发处理能力。
- 数据访问模式较稳定的场景:LRU 缓存适用于数据访问模式相对稳定、变化较少的场景,对于访问模式频繁变化的场景,可能需要考虑其他缓存策略。
ThreadLocal缓存机制
ThreadLocal 缓存机制:
ThreadLocal 是一种在多线程环境下使用的特殊缓存机制,它可以在每个线程中保存数据副本,保证每个线程访问的是自己的数据,从而避免了线程安全问题。
如何在线程级别缓存调用结果:
ThreadLocal 缓存机制的使用非常简单,只需要在每个线程中创建一个 ThreadLocal 对象,然后将需要缓存的数据存储在 ThreadLocal 中。这样就可以保证每个线程独立访问自己的数据副本,而不会影响其他线程。
public class MyCache { private static ThreadLocal<Map<String, Object>> cache = ThreadLocal.withInitial(HashMap::new); public static void put(String key, Object value) { cache.get().put(key, value); } public static Object get(String key) { return cache.get().get(key); } }
适用场景和注意事项:
- 适用场景:
- 线程级别的数据共享:适用于需要在每个线程中共享数据,但又不希望数据被其他线程访问的场景。
- 线程池环境下的数据传递:适用于线程池环境下,需要将数据从任务提交线程传递到任务执行线程的场景。
- 注意事项:
- 内存泄漏风险:ThreadLocal 使用静态的弱引用来保存线程本地变量,如果不及时清理 ThreadLocal,可能会导致内存泄漏问题。
- 线程安全问题:虽然 ThreadLocal 可以保证线程之间的数据隔离,但在多线程访问同一个 ThreadLocal 变量时,仍可能存在线程安全问题,需要注意同步控制。
与其他缓存机制的对比:
- 与LRU缓存对比:
- LRU 缓存适用于全局的数据共享,通过淘汰最近最少使用的数据来保证缓存的命中率。
- ThreadLocal 缓存适用于线程级别的数据共享,可以避免线程安全问题,但每个线程只能访问自己的数据副本。
- 与JCache缓存对比:
- JCache 是一种标准的缓存规范,提供了统一的API和功能,支持多种缓存实现。
- ThreadLocal 缓存是一种简单的线程级别缓存,适用于特定的线程间数据共享场景,不具备跨线程的能力。
ThreadLocal 缓存机制适用于需要在线程级别共享数据的场景,可以有效避免线程安全问题,并提高系统的性能和并发能力。但在使用过程中需要注意内存泄漏和线程安全问题,以确保缓存的有效性和稳定性。
JCache缓存机制
JCache 缓存机制:
JCache 是一种标准的缓存规范,旨在提供统一的缓存API和功能,使得开发人员可以在不同的缓存实现之间进行切换和替换。JCache 的最新版本为 JSR-107,它定义了一组缓存接口和相关的规范,为缓存的实现和使用提供了标准化的方法。
JCache标准规范和实现:
- JCache 标准规范:
- JCache 规范定义了一组缓存相关的接口和注解,包括 Cache、CacheManager、CacheEntry、ExpiryPolicy 等。
- 这些接口和注解提供了一套标准化的缓存操作方法,包括数据的存储、检索、过期处理等。
- JCache 实现:
- JCache 是一个标准规范,并不是一个具体的实现。根据 JCache 规范,各种缓存提供商(如 Ehcache、Hazelcast、Infinispan 等)都可以实现自己的 JCache 缓存产品。
- JCache 规范的实现通常会提供一套标准的 API,以及相应的配置和管理工具,使得开发人员可以方便地在不同的缓存产品之间切换和替换。
与JSR-107的关系:
JSR-107 是 Java Community Process 中关于缓存规范的一个标准,定义了 JCache 规范的内容。JCache 是 JSR-107 的具体实现之一,它基于 JSR-107 规范提供了一套标准的缓存API和功能。因此,JCache 可以看作是 JSR-107 规范的一种实现。
在Dubbo中的应用实践和配置方法:
在 Dubbo 中,可以通过配置 JCache 缓存来提高服务的性能和稳定性。以下是在 Dubbo 中应用 JCache 缓存的实践和配置方法:
- 引入 JCache 实现库:
- 首先需要引入具体的 JCache 缓存实现库,如 Ehcache、Hazelcast、Infinispan 等。可以通过 Maven 等依赖管理工具引入相应的库。
- 配置 JCache 缓存管理器:
- 在 Dubbo 的配置文件中,配置 JCache 缓存管理器,指定具体的 JCache 实现和相应的配置参数。可以配置缓存的大小、过期时间、淘汰策略等。
- 在服务接口上添加缓存注解:
- 在需要缓存的服务接口或方法上,添加 JCache 缓存相关的注解,如 @CacheResult、@CachePut、@CacheRemove 等。这些注解用于定义缓存的行为和策略。
- 启用 JCache 缓存功能:
- 在 Dubbo 的配置文件中,启用 JCache 缓存功能,指定缓存管理器和相应的缓存配置。可以通过配置文件或者代码方式启用缓存功能。
通过以上配置和实践,可以在 Dubbo 中使用 JCache 缓存机制,提高服务的性能和稳定性,同时实现缓存的统一管理和标准化。 JCache 提供了一种灵活、标准化的缓存解决方案,适用于各种 Dubbo 项目的缓存需求。
Expiring机制
Expiring 缓存机制:
Expiring 缓存机制是一种基于时间过期的缓存策略,它允许开发人员为缓存中的数据设置生命周期,当数据的生命周期到期时,自动将数据从缓存中移除,以确保缓存中的数据是最新的。
基于时间过期的缓存策略:
Expiring 缓存策略基于时间设置缓存的生命周期,通常使用时间单位(如秒、分钟、小时)来指定数据在缓存中的存储时间。当数据存储时间超过指定的生命周期时,数据将被自动从缓存中淘汰。
如何设置缓存的生命周期:
在 Expiring 缓存策略中,开发人员可以通过以下方式来设置缓存的生命周期:
- 在缓存存储数据时设置过期时间:
- 在将数据存储到缓存中时,同时指定数据的过期时间,通常以时间戳或相对时间(如5分钟后过期)的方式来设置。
- 在缓存配置中指定默认的过期时间:
- 在缓存配置中,可以指定默认的过期时间,所有存储到缓存中的数据都将使用该默认过期时间。这样可以简化数据存储时的设置。
- 动态调整缓存的生命周期:
- 在实际应用中,可能需要根据业务需求动态调整缓存的生命周期。开发人员可以根据业务场景和需求,灵活地调整缓存的过期时间。
处理缓存过期的方式和策略选择:
在 Expiring 缓存策略中,缓存过期时的处理方式通常有以下几种:
- 自动淘汰:
- 当数据的生命周期到期时,缓存系统会自动将数据从缓存中淘汰,不再提供给客户端使用。这是最常见的缓存过期处理方式。
- 手动刷新:
- 当数据的生命周期到期时,缓存系统不会立即将数据从缓存中淘汰,而是等待客户端下一次访问时重新加载数据。这种方式可以减少缓存的刷新频率,降低系统负载。
- 异步刷新:
- 当数据的生命周期到期时,缓存系统会异步地从数据源重新加载数据,并更新缓存。这种方式可以减少对客户端的影响,提高系统的响应速度。
策略选择:
在选择缓存过期处理策略时,需要根据业务需求和系统性能要求进行权衡:
- 如果数据的更新频率较低,且缓存数据的即时性要求不高,可以选择自动淘汰的方式。
- 如果数据的更新频率较高,且需要保证缓存数据的及时更新,可以选择手动刷新或异步刷新的方式。
- 在实际应用中,还可以根据缓存数据的重要性和访问频率,灵活选择合适的过期处理策略。
通过合理设置缓存的生命周期和选择适当的过期处理策略,可以有效提高系统的性能和稳定性,优化用户的使用体验。 Expiring 缓存机制是一种灵活、高效的缓存策略,适用于各种类型的应用场景。
缓存的配置方式
在 Dubbo 中,可以通过 @EnableDubbo
注解或者 XML 配置文件来配置缓存。以下是 Dubbo 中配置缓存的方式:
通过 @EnableDubbo 注解配置:
- 在 Spring Boot 项目中使用 @EnableDubbo 注解:
@SpringBootApplication @EnableDubbo(cache = "lru") public class DubboApplication { public static void main(String[] args) { SpringApplication.run(DubboApplication.class, args); } }
- 通过 XML 配置文件配置缓存:
<dubbo:consumer cache="lru" /> <dubbo:provider cache="lru" />
在上述示例中,cache
属性用于指定 Dubbo 缓存的类型,可以设置为 lru
、threadlocal
、jcache
或 expiring
,分别对应 LRUCache、ThreadLocalCache、JCache 和 ExpiringCache 缓存实现。
通过 Dubbo.properties 文件配置:
在 Dubbo.properties 文件中,可以使用 dubbo.cache
属性来配置缓存类型:
# Consumer 缓存类型 dubbo.consumer.cache=lru # Provider 缓存类型 dubbo.provider.cache=lru
以上是在 Dubbo 中配置缓存的几种方式。根据项目的实际情况和需求,选择合适的配置方式来配置 Dubbo 缓存,以提高系统的性能和稳定性。
极客时间何辉老师引用
- 改造方案的数据是存储在JVM内存中,可能会撑爆内存
- 如果某些用户的权限发生变更,变更完成到使用新数据容忍时间间隔,如何完成内存数据的刷新操作?
lru
的底层
// 过滤器被触发调用的入口 org.apache.dubbo.cache.filter.CacheFilter#invoke ↓ // 根据 invoker.getUrl() 获取缓存容器 org.apache.dubbo.cache.support.AbstractCacheFactory#getCache ↓ // 若缓存容器没有的话,则会自动创建一个缓存容器 org.apache.dubbo.cache.support.lru.LruCacheFactory#createCache ↓ // 最终创建的是一个 LruCache 对象,该对象的内部使用的 LRU2Cache 存储数据 org.apache.dubbo.cache.support.lru.LruCache#LruCache // 存储调用结果的对象 private final Map<Object, Object> store; public LruCache(URL url) { final int max = url.getParameter("cache.size", 1000); this.store = new LRU2Cache<>(max); } ↓ // LRU2Cache 的带参构造方法,在 LruCache 构造方法中,默认传入的大小是 1000 org.apache.dubbo.common.utils.LRU2Cache#LRU2Cache(int) public LRU2Cache(int maxCapacity) { super(16, DEFAULT_LOAD_FACTOR, true); this.maxCapacity = maxCapacity; this.preCache = new PreCache<>(maxCapacity); } // 若继续放数据时,若发现现有数据个数大于 maxCapacity 最大容量的话 // 则会考虑抛弃掉最古老的一个,也就是会抛弃最早进入缓存的那个对象 @Override protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) { return size() > maxCapacity; } ↓ // JDK 中的 LinkedHashMap 源码在发生节点插入后 // 给了子类一个扩展删除最旧数据的机制 java.util.LinkedHashMap#afterNodeInsertion void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
所以容忍时间间隔不确定,刷新的时效也是不确定的
- threadlocal,使用的是 ThreadLocalCacheFactory 工厂类,类名中 ThreadLocal 是本地 线程的意思,而 ThreadLocal 最终还是使用的是 JVM 内存。
jcache,使用的是 JCacheFactory 工厂类,是提供 javax-spi 缓存实例的工厂类,既然是 一种 spi 机制,可以接入很多自制的开源框架。
expiring,使用的是 ExpiringCacheFactory 工厂类,内部的 ExpiringCache 中还是使用 的 Map 数据结构来存储数据,仍然使用的是 JVM 内存。 - 实现
jcache
<!--加入解决NoClassDefFoundError报错问题--> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency> <!--解决没有CachingProvider的实现类--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.18.0</version> </dependency>
- 解决
Default configuration hasn't been specified!
{ "singleServerConfig": { "address": "redis://127.0.0.1:6379" } }