java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java本地缓存

本地缓存在Java中的实现过程

作者:找不到、了

这篇文章主要介绍了本地缓存在Java中的实现过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

本地缓存是Java应用中常用的性能优化手段。如下图所示:

在分布式系统中,同一个应用部署有多个,这些应用的本地缓存仅限于本地应用内部,是互不相通的,在负载均衡中,分配到处理的各个应用读取本地缓存的结果可能会存在不一致。

注意:本地缓存是jvm层面的缓存,一旦该应用重启或停止了,缓存也消失了。

1、介绍

引入缓存,主要用于实现系统的高性能,高并发。如下图所示:

将数据库查询出来的数据放入缓存服务中,因为缓存是存储在内存中的,内存的读写性能远超磁盘的读写性能,所以访问的速度非常快。

注意:

但是电脑重启后,内存中的数据会全部清除,而磁盘中的数据虽然读写性能很差,但是数据不会丢失。

2、实现方式

2.1、HashMap

最简单的方式是使用ConcurrentHashMap实现线程安全的缓存。

代码示例如下:

import java.util.concurrent.ConcurrentHashMap;

public class SimpleCache<K, V> {
    private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
    
    public void put(K key, V value) {
        cache.put(key, value);
    }
    
    public V get(K key) {
        return cache.get(key);
    }
    
    public void remove(K key) {
        cache.remove(key);
    }
    
    public void clear() {
        cache.clear();
    }
}

适用场景

优点

缺点

如下所示:

// 简单的配置项缓存
private static final Map<String, String> CONFIG_CACHE = new ConcurrentHashMap<>();

public String getConfig(String key) {
    return CONFIG_CACHE.computeIfAbsent(key, k -> loadConfigFromDB(k));
}

2.2、LinkedHashMap

利用LinkedHashMap的访问顺序特性实现LRU(最近最少使用)缓存。

如下图所示:

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxSize;
    
    public LRUCache(int maxSize) {
        super(maxSize, 0.75f, true);
        this.maxSize = maxSize;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxSize;
    }
}

适用场景

优点

缺点

如下所示:

// 最近访问的用户基本信息缓存
private static final int MAX_ENTRIES = 1000;
private static final Map<Long, UserInfo> USER_CACHE = 
    Collections.synchronizedMap(new LinkedHashMap<Long, UserInfo>(MAX_ENTRIES, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > MAX_ENTRIES;
        }
    });

2.3、Guava Cache

Guava Cache是JVM层面的缓存,服务停掉或重启便消失了,在分布式环境中也有其局限性。

因此,比较好的缓存方案是Guava Cache+Redis双管齐下。先查询Guava Cache,命中即返回,未命中再查redis。

引入依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.2-jre</version> <!-- 使用最新版本 -->
</dependency>

代码如下所示:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class GuavaCacheExample {
    public static void main(String[] args) {
       //建造者模式
        Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(100) // 最大缓存数量
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
            .build();
        
        // 放入缓存
        cache.put("key1", "value1");
        
        // 获取缓存
        String value = cache.getIfPresent("key1");
        System.out.println(value);
        
        // 移除缓存
        cache.invalidate("key1");
    }
}

适用场景

优点

缺点

示例如下:

// 商品详情缓存,30分钟自动过期,最大10000条
LoadingCache<Long, Product> productCache = CacheBuilder.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(30, TimeUnit.MINUTES)
    .recordStats()
    .build(new CacheLoader<Long, Product>() {
        @Override
        public Product load(Long id) {
            return productDao.findById(id);
        }
    });

// 使用
Product product = productCache.get(123L);

2.4、Caffeine Cache

Caffeine是Guava Cache的现代替代品,性能更好。

引入依赖:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version> <!-- 使用最新版本 -->
</dependency>

代码示例如下:

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class CaffeineCacheExample {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();
        
        cache.put("key1", "value1");
        String value = cache.getIfPresent("key1");
        System.out.println(value);
    }
}

适用场景

优点

缺点

示例如下:

// 高性能的秒杀商品库存缓存
Cache<Long, AtomicInteger> stockCache = Caffeine.newBuilder()
    .maximumSize(100_000)
    .expireAfterWrite(10, TimeUnit.SECONDS) // 库存信息短期有效
    .refreshAfterWrite(1, TimeUnit.SECONDS) // 1秒后访问自动刷新
    .build(id -> new AtomicInteger(queryStockFromDB(id)));

// 使用
int remaining = stockCache.get(productId).decrementAndGet();

2.5、Ehcache

Ehcache是一个成熟的Java缓存框架:功能更强大,支持磁盘持久化、分布式缓存等。

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
</dependency>
import org.ehcache.Cache;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.core.config.DefaultConfiguration;
import org.ehcache.core.spi.service.LocalPersistenceService;
import org.ehcache.impl.config.persistence.DefaultPersistenceConfiguration;
import org.ehcache.impl.persistence.DefaultLocalPersistenceService;

public class EhcacheExample {
    public static void main(String[] args) {
        // 配置持久化到磁盘
        LocalPersistenceService persistenceService = new DefaultLocalPersistenceService(
            new DefaultPersistenceConfiguration(new File("cache-data")));

        // 创建缓存管理器
        DefaultConfiguration config = new DefaultConfiguration(
            persistenceService, ResourcePoolsBuilder.heap(100).build());

        Cache<String, String> cache = CacheConfigurationBuilder.newCacheConfigurationBuilder(
                String.class, String.class,
                ResourcePoolsBuilder.newResourcePoolsBuilder()
                    .heap(100, MemoryUnit.MB)    // 堆内内存
                    .disk(1, MemoryUnit.GB)     // 磁盘持久化
            ).buildConfig(String.class);

        // 写入数据
        cache.put("key1", "value1");

        // 读取数据
        String value = cache.get("key1");
        System.out.println("Value: " + value); // 输出 Value: value1

        // 关闭资源
        persistenceService.close();
    }
}

适用场景

优点

缺点

示例如下:

<!-- ehcache.xml -->
<cache name="financialDataCache"
       maxEntriesLocalHeap="10000"
       timeToLiveSeconds="3600"
       memoryStoreEvictionPolicy="LFU">
    <persistence strategy="localTempSwap"/>
</cache>
// 金融数据缓存,需要持久化
@Cacheable(value = "financialDataCache", 
           key = "#symbol + '_' + #date.format(yyyyMMdd)")
public FinancialData getFinancialData(String symbol, LocalDate date) {
    // 从外部API获取数据
}

2.6、使用Spring Cache注解

Spring框架提供了缓存抽象。关于cache的常用注解如下:

1、引入依赖

<dependencies>
    <!-- Spring Boot Starter Cache -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    <!-- 使用Caffeine作为缓存实现 -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
</dependencies>

2、使用缓存配置类

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.caffeine.CaffeineCacheManager;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CaffeineCacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(500)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .recordStats());
        return cacheManager;
    }


    @Bean
    @Primary
    public CacheManager productCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("products");
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(1, TimeUnit.HOURS));
        return cacheManager;
    }
    
    @Bean
    public CacheManager userCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users");
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(500)
                .expireAfterAccess(30, TimeUnit.MINUTES));
        return cacheManager;
    }
}

注意:在设置缓存配置类的时候,可以配置多个。

然后在服务类中指定使用哪个缓存管理器:

@Service
public class UserService {
    
    @Cacheable(value = "users", cacheManager = "userCacheManager")
    public User getUserById(Long id) {
        // ...
    }
}

3、服务类使用缓存

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    
    // 根据ID获取产品,如果缓存中有则直接返回
    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        // 模拟数据库查询
        System.out.println("查询数据库获取产品: " + id);
        return findProductInDB(id);
    }
    
    // 更新产品信息,并更新缓存
    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        // 模拟数据库更新
        System.out.println("更新数据库中的产品: " + product.getId());
        return updateProductInDB(product);
    }
    
    // 删除产品,并清除缓存
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        // 模拟数据库删除
        System.out.println("从数据库删除产品: " + id);
    }
    
    // 清除所有产品缓存
    @CacheEvict(value = "products", allEntries = true)
    public void clearAllCache() {
        System.out.println("清除所有产品缓存");
    }
    
    // 模拟数据库查询方法
    private Product findProductInDB(Long id) {
        // 实际项目中这里应该是数据库操作
        return new Product(id, "产品" + id, 100.0);
    }
    
    // 模拟数据库更新方法
    private Product updateProductInDB(Product product) {
        // 实际项目中这里应该是数据库操作
        return product;
    }
}

4、实体类

public class Product {
    private Long id;
    private String name;
    private double price;
    
    // 构造方法、getter和setter省略
    // 实际项目中应该包含这些方法
}

5、控制器示例:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/products")
public class ProductController {
    
    private final ProductService productService;
    
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productService.getProductById(id);
    }
    
    @PutMapping
    public Product updateProduct(@RequestBody Product product) {
        return productService.updateProduct(product);
    }
    
    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
    }
    
    @PostMapping("/clear-cache")
    public void clearCache() {
        productService.clearAllCache();
    }
}

适用场景

优点

缺点

如下所示:

// 多级缓存配置:本地缓存+Redis
@Configuration
@EnableCaching
public class CacheConfig {
    
    // 本地一级缓存
    @Bean
    @Primary
    public CacheManager localCacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(30, TimeUnit.MINUTES));
        return manager;
    }
    
    // Redis二级缓存
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory)
                .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                        .entryTtl(Duration.ofHours(2))
                        .disableCachingNullValues())
                .build();
    }
}

// 服务层使用
@Service
public class ProductService {
    
    @Cacheable(cacheNames = "products", 
               cacheManager = "localCacheManager") // 先用本地缓存
    @Cacheable(cacheNames = "products", 
               cacheManager = "redisCacheManager", 
               unless = "#result == null") // 再用Redis缓存
    public Product getProduct(Long id) {
        return productRepository.findById(id);
    }
}

3、性能对比

1.合理设置缓存大小

根据可用内存设置上限。使用weigher对大型对象特殊处理。

2.选择合适的过期策略

// 根据业务场景选择
.expireAfterWrite(10, TimeUnit.MINUTES)  // 写入后固定时间过期
.expireAfterAccess(30, TimeUnit.MINUTES) // 访问后延长有效期
.refreshAfterWrite(1, TimeUnit.MINUTES)  // 写入后定时刷新

3.监控缓存命中率

CacheStats stats = cache.stats();
double hitRate = stats.hitRate();      // 命中率
long evictionCount = stats.evictionCount(); // 淘汰数量

4.避免缓存污染

// 不缓存null或空值
.build(key -> {
    Value value = queryFromDB(key);
    return value == null ? Optional.empty() : value;
});

@Cacheable(unless = "#result == null || #result.isEmpty()")

5.考虑使用软引用(内存敏感场景):

.softValues() // 内存不足时自动回收

根据您的具体业务需求、数据规模和性能要求,选择最适合的缓存方案,并持续监控和优化缓存效果。

4、使用建议

简单小规模缓存ConcurrentHashMapLinkedHashMap

中等规模通用缓存Guava CacheCaffeine

高性能大规模缓存Caffeine

企业级复杂需求Ehcache

Spring项目Spring Cache + Caffeine

多级缓存架构Caffeine + Redis

总结

内存管理‌:设置合理的 maximumSize 或 expireAfterWrite,避免内存溢出(OOM)。

并发安全‌:Guava/Caffeine/Ehcache 均为线程安全,直接使用即可。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文