Redis+Caffeine 实现二级缓存的优势与应用场景

658 阅读11分钟

缓存设计模式有哪些?

缓存设计模式在软件开发中用于提升数据访问效率和系统性能。常见的缓存设计模式包括:

  1. Cache Aside(旁路缓存)模式:应用程序直接与缓存和数据库交互。
    • 读操作流程
      1. 应用程序先从缓存读取数据。
      2. 如果缓存命中,返回数据。
      3. 如果缓存未命中,从数据库读取数据,将数据写入缓存,然后返回数据。
    • 写操作流程
      1. 应用程序更新数据库中的数据。
      2. 然后删除或更新缓存中的旧数据。

image.png

  1. Read-Through 模式:应用程序只与缓存交互,缓存系统负责从数据库加载数据。
    • 操作流程
      1. 应用程序从缓存读取数据。
      2. 如果缓存命中,返回数据。
      3. 如果缓存未命中,缓存系统从数据库加载数据,写入缓存,然后返回数据。

image.png

  1. Write-Through 模式:应用程序只与缓存交互,缓存系统负责同步更新数据库。
    • 操作流程
      1. 应用程序更新缓存中的数据。
      2. 缓存系统同步将数据写入数据库,确保数据一致性。

image.png

  1. Write-Behind(Write-Back)模式:应用程序更新缓存,缓存系统异步批量更新数据库。
    • 操作流程
      1. 应用程序更新缓存中的数据。
      2. 缓存系统在后台异步批量将数据写入数据库。

image.png


本地缓存的优缺点

本地缓存是一种将缓存数据存储在应用程序内存中的方式,主要通过工具(如 CaffeineGuava)实现。以下是本地缓存的主要优缺点:


优点

1. 访问速度快

  • 原因:数据直接存储在应用程序的内存中,避免了网络传输和外部系统调用。
  • 适用场景:访问频率高、实时性要求强的场景(如热点数据会话数据)。

2. 实现简单

  • 原因:本地缓存依赖轻量级工具,无需部署独立的缓存服务(如 Redis)。
  • 适用场景:中小型应用或对性能需求适中的场景。

3. 降低外部依赖

  • 原因:数据在本地内存中,减少对数据库和分布式缓存系统的访问,降低外部系统的压力。
  • 适用场景:分布式缓存系统性能瓶颈的场景。

4. 支持复杂数据结构

  • 原因:许多本地缓存工具(如 Caffeine)支持复杂的数据存储和操作(如缓存过期策略、统计功能)。
  • 适用场景:需要定制缓存策略或操作复杂数据的场景。

缺点

1. 数据量有限

  • 原因:受限于服务器内存,缓存数据量不能过大,否则可能导致内存溢出。
  • 适用建议:适合小数据量的热点数据,无法缓存大规模数据。

2. 分布式场景不适用

  • 原因:本地缓存只适用于单实例,无法在分布式环境中共享数据。
  • 适用建议:分布式应用需要引入分布式缓存(如 Redis、Memcached)。

3. 数据一致性问题

  • 原因:缓存与数据库数据可能不同步,尤其在数据更新频繁时,本地缓存难以实时更新。
  • 适用建议:对一致性要求较高的场景需要谨慎使用。

4. 缓存命中率依赖初始化

  • 原因:本地缓存的初始状态为空,需要逐步填充数据,可能导致首次访问命中率低。
  • 适用建议:可以在应用启动时预加载常用数据。

5. 增加内存压力

  • 原因:缓存占用内存资源过多可能影响其他业务逻辑的运行。
  • 适用建议:设置合理的缓存大小和过期策略,防止缓存滥用。

总结对比

优点缺点
访问速度快数据量有限,受内存限制
实现简单,依赖轻不适合分布式环境
减少外部系统压力数据一致性问题
支持复杂数据结构和策略缓存初始命中率低

场景适用建议

  1. 适合场景

    • 热点数据(如商品分类、配置数据、用户会话等)。
    • 中小型系统或单节点服务。
    • 数据更新频率低、读取频率高的场景。
  2. 不适合场景

    • 分布式系统,需要共享缓存数据。
    • 数据量特别大,内存无法承载。
    • 对数据一致性要求极高的场景。

本地缓存的应用

本地缓存是指将缓存数据存储在应用程序的内存中,具有高速访问、简单实现等优点,常用于以下场景:


1. 热点数据缓存

  • 场景描述:访问频率极高的数据(如热门商品、用户会话信息等),对性能要求高。
  • 适用原因:热点数据的访问次数多,存储在本地缓存中可以减少网络调用和数据库压力。

Java 示例代码(使用 Caffeine 缓存实现):

@Service
public class HotDataService {

    private final Cache<Long, String> hotDataCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    public String getHotData(Long id) {
        return hotDataCache.get(id, this::fetchFromDatabase);
    }

    private String fetchFromDatabase(Long id) {
        // 模拟从数据库获取数据
        return "HotData-" + id;
    }
}

2. 配置数据缓存

  • 场景描述:应用程序的配置数据(如特定阈值、特性开关)更新频率较低,但读取频率高。
  • 适用原因:配置数据的更新不频繁,本地缓存可以快速提供访问而无需每次请求配置服务或数据库。

Java 示例代码

@Service
public class ConfigService {

    private final Cache<String, String> configCache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build();

    public String getConfigValue(String key) {
        return configCache.get(key, this::fetchConfigFromDb);
    }

    private String fetchConfigFromDb(String key) {
        // 模拟从数据库获取配置
        return "ConfigValue-" + key;
    }
}

3. 会话数据缓存

  • 场景描述:需要频繁访问的会话数据(如登录用户的会话状态)。
  • 适用原因:会话数据通常与请求绑定,使用本地缓存可以避免频繁查询分布式存储或数据库。

Java 示例代码

@Service
public class SessionService {

    private final Cache<String, String> sessionCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterAccess(30, TimeUnit.MINUTES)
            .build();

    public void storeSession(String sessionId, String userInfo) {
        sessionCache.put(sessionId, userInfo);
    }

    public String getSession(String sessionId) {
        return sessionCache.getIfPresent(sessionId);
    }

    public void removeSession(String sessionId) {
        sessionCache.invalidate(sessionId);
    }
}

缓存设计模式结合场景应用及示例

1. Cache Aside 模式(旁路缓存) 应用于热点数据

  • 场景:电商平台的热门商品详情页访问量高。
  • 适用原因:Cache Aside 模式灵活,允许缓存和数据库数据不同步的短暂窗口,适合热点数据。

Java 示例代码

@Service
public class ProductService {

    private final Cache<Long, Product> productCache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();

    public Product getProduct(Long productId) {
        return productCache.get(productId, this::fetchProductFromDb);
    }

    private Product fetchProductFromDb(Long productId) {
        // 从数据库获取数据
        return productRepository.findById(productId).orElse(null);
    }

    public void updateProduct(Product product) {
        // 更新数据库
        productRepository.save(product);
        // 删除缓存中的数据
        productCache.invalidate(product.getId());
    }
}

2. Read-Through 模式 应用于配置数据

  • 场景:系统启动时加载配置数据,用户访问时直接从缓存读取。
  • 适用原因:配置数据通常从缓存读取,更新较少,Read-Through 模式简化了缓存操作。

Java 示例代码

@Service
public class ConfigReadThroughService {

    private final Cache<String, String> configCache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build();

    public String getConfig(String key) {
        return configCache.get(key, this::fetchConfigFromSource);
    }

    private String fetchConfigFromSource(String key) {
        // 从配置中心或数据库获取配置
        return "ConfigValue-" + key;
    }
}

3. Write-Through 模式 应用于用户权限数据

  • 场景:后台管理系统中,管理员更新用户权限时,同时更新缓存和数据库。
  • 适用原因:Write-Through 模式保证数据一致性,适合敏感数据的读写操作。

Java 示例代码

@Service
public class PermissionService {

    private final Cache<Long, List<String>> permissionCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    public List<String> getUserPermissions(Long userId) {
        return permissionCache.get(userId, this::fetchPermissionsFromDb);
    }

    private List<String> fetchPermissionsFromDb(Long userId) {
        // 从数据库获取用户权限
        return permissionRepository.findPermissionsByUserId(userId);
    }

    public void updatePermissions(Long userId, List<String> permissions) {
        // 同步更新数据库和缓存
        permissionRepository.updatePermissions(userId, permissions);
        permissionCache.put(userId, permissions);
    }
}

  • 本地缓存的典型应用场景:热点数据、配置数据、会话数据。
  • 缓存设计模式的结合场景
    • Cache Aside:适合灵活处理数据一致性的场景。
    • Read-Through:适合读取频繁、更新较少的场景。
    • Write-Through:适合对一致性要求高的数据。

二级缓存是指在系统中设置两级缓存:一级缓存通常是本地缓存(如 Caffeine),位于应用程序内部,提供快速访问;二级缓存通常是分布式缓存(如 Redis),用于共享持久化数据。

二级缓存的优缺点

  • 优点

    • 提高数据访问速度,减少数据库压力。
    • 本地缓存减少网络延迟,提升性能。
    • 分布式缓存提供数据一致性和持久性。
  • 缺点

    • 增加系统复杂度,需要处理缓存同步和一致性问题。
    • 可能出现缓存不一致的情况,需要设计有效的失效策略。

Redis+Caffeine 实现两级缓存

在这种架构中,Caffeine 作为一级缓存,存储于应用程序内部,提供快速访问;Redis 作为二级缓存,存储于分布式系统中,提供共享和持久化。

应用场景

  1. 商品详情页缓存:电商平台的商品详情页访问频繁,使用两级缓存可以提高响应速度,减少数据库压力。

  2. 用户信息缓存:社交平台的用户信息查询频繁,采用两级缓存可以提升查询效率,改善用户体验。

  3. 配置数据缓存:系统的配置数据读取频繁且更新较少,使用两级缓存可以提高读取速度,确保配置的及时性。

代码示例

以下是一个使用 Spring BootCaffeineRedis 实现两级缓存的示例,应用于查询商品详情的场景。

  1. 引入依赖(在 pom.xml 中添加):

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
    
  2. 配置缓存

    @Configuration
    public class CacheConfig extends CachingConfigurerSupport {
    
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
            // 配置 Redis 缓存
            RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
            RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
                    .cacheDefaults(redisCacheConfiguration)
                    .build();
    
            // 配置 Caffeine 缓存
            CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
            caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
                    .initialCapacity(100)
                    .maximumSize(1000)
                    .expireAfterWrite(60, TimeUnit.SECONDS));
    
            // 将两者结合
            CompositeCacheManager cacheManager = new CompositeCacheManager(caffeineCacheManager, redisCacheManager);
            cacheManager.setFallbackToNoOpCache(false);
            return cacheManager;
        }
    }
    
  3. 使用缓存

    @Service
    public class ProductService {
    
        @Cacheable(value = "productCache", key = "#productId")
        public Product getProductById(Long productId) {
            // 从数据库获取商品信息的逻辑
            return productRepository.findById(productId).orElse(null);
        }
    
        @CacheEvict(value = "productCache", key = "#product.id")
        public void updateProduct(Product product) {
            // 更新数据库中的商品信息
            productRepository.save(product);
        }
    
        @CacheEvict(value = "productCache", key = "#productId")
        public void deleteProduct(Long productId) {
            // 删除数据库中的商品信息
            productRepository.deleteById(productId);
        }
    }
    

二级缓存流程

以下是 Redis + Caffeine 两级缓存的流程图,展示了查询和写入数据的操作过程。

  1. 查询操作流程

    • 先从 Caffeine 一级缓存中查找数据。
    • 如果命中,直接返回数据。
    • 如果未命中,从 Redis 二级缓存中查找。
    • 如果 Redis 命中,数据加载到 Caffeine 缓存,同时返回数据。
    • 如果 Redis 未命中,从数据库读取数据,数据加载到 Redis 和 Caffeine 缓存,然后返回数据。
  2. 写入操作流程

    • 更新数据库中的数据。
    • 同步更新或失效 Redis 和 Caffeine 缓存中的数据。

image.png

  1. 写入操作

image.png


常见应用场景

1. 新闻热点缓存

  • 新闻热点的访问频率很高且更新及时,适合使用两级缓存架构。

Java 示例

@Cacheable(value = "newsCache", key = "#newsId")
public News getNewsById(Long newsId) {
    return newsRepository.findById(newsId).orElse(null);
}

@CacheEvict(value = "newsCache", key = "#news.id")
public void updateNews(News news) {
    newsRepository.save(news);
}

@CacheEvict(value = "newsCache", key = "#newsId")
public void deleteNews(Long newsId) {
    newsRepository.deleteById(newsId);
}

2. 金融交易缓存

  • 金融系统中的交易记录查询需求高,但写入频率较低,可使用两级缓存提高查询效率。

Java 示例

@Cacheable(value = "transactionCache", key = "#transactionId")
public Transaction getTransactionById(Long transactionId) {
    return transactionRepository.findById(transactionId).orElse(null);
}

@CacheEvict(value = "transactionCache", key = "#transaction.id")
public void updateTransaction(Transaction transaction) {
    transactionRepository.save(transaction);
}

@CacheEvict(value = "transactionCache", key = "#transactionId")
public void deleteTransaction(Long transactionId) {
    transactionRepository.deleteById(transactionId);
}

3. 电商购物车缓存

  • 用户的购物车数据需要频繁读取和更新,适合使用两级缓存保证性能。

Java 示例

@Cacheable(value = "cartCache", key = "#userId")
public ShoppingCart getCartByUserId(Long userId) {
    return shoppingCartRepository.findByUserId(userId);
}

@CacheEvict(value = "cartCache", key = "#userId")
public void updateCart(Long userId, ShoppingCart cart) {
    shoppingCartRepository.save(cart);
}

4. 商品分类缓存

场景:商品分类信息更新少,但读取频繁。适合使用两级缓存减少分类查询对数据库的压力。

Java 代码实现

@Service
public class CategoryService {

    @Cacheable(value = "categoryCache", key = "#categoryId")
    public Category getCategoryById(Long categoryId) {
        // 从数据库查询分类信息
        return categoryRepository.findById(categoryId).orElse(null);
    }

    @CacheEvict(value = "categoryCache", key = "#category.id")
    public void updateCategory(Category category) {
        // 更新数据库中的分类信息
        categoryRepository.save(category);
    }

    @CacheEvict(value = "categoryCache", key = "#categoryId")
    public void deleteCategory(Long categoryId) {
        // 删除数据库中的分类信息
        categoryRepository.deleteById(categoryId);
    }
}

5. 文章详情缓存

场景:博客系统中的文章详情读取频繁,使用两级缓存加速访问。

Java 代码实现

@Service
public class ArticleService {

    @Cacheable(value = "articleCache", key = "#articleId")
    public Article getArticleById(Long articleId) {
        // 从数据库查询文章详情
        return articleRepository.findById(articleId).orElse(null);
    }

    @CacheEvict(value = "articleCache", key = "#article.id")
    public void updateArticle(Article article) {
        // 更新数据库中的文章详情
        articleRepository.save(article);
    }

    @CacheEvict(value = "articleCache", key = "#articleId")
    public void deleteArticle(Long articleId) {
        // 删除数据库中的文章详情
        articleRepository.deleteById(articleId);
    }
}

6. 用户权限缓存

场景:后台系统中用户权限查询频繁,且权限更新较少,适合使用两级缓存提高查询效率。

Java 代码实现

@Service
public class UserPermissionService {

    @Cacheable(value = "permissionCache", key = "#userId")
    public List<Permission> getPermissionsByUserId(Long userId) {
        // 从数据库查询用户权限信息
        return permissionRepository.findPermissionsByUserId(userId);
    }

    @CacheEvict(value = "permissionCache", key = "#userId")
    public void updatePermissions(Long userId, List<Permission> permissions) {
        // 更新用户权限信息
        permissionRepository.updatePermissions(userId, permissions);
    }

    @CacheEvict(value = "permissionCache", key = "#userId")
    public void deletePermissions(Long userId) {
        // 删除用户权限信息
        permissionRepository.deletePermissionsByUserId(userId);
    }
}

总结:

使用 Redis + Caffeine 构建两级缓存,能够满足多种高频读操作场景(如商品、用户、配置等)的性能需求,同时减少数据库压力。通过合理设计缓存失效策略与数据同步机制,可以有效规避缓存不一致问题。