百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Redis客户端缓存的几种实现方式

nanshan 2025-05-11 17:25 25 浏览 0 评论

前言:

Redis作为当今最流行的内存数据库和缓存系统,被广泛应用于各类应用场景。然而,即使Redis本身性能卓越,在高并发场景下,应用于Redis服务器之间的网络通信仍可能成为性能瓶颈。

所以客户端缓存非常重要,客户端缓存指在应该程序内存中维护一份redis数据的本地副本,以较少网络请求与redis服务器的交互,降低延迟。下面介绍几种方式。

一:本地内存缓存

本地内存缓存是最直接的客户端缓存实现方式,它在应用程序内存中使用数据结构(如HashMap、ConcurrentHashMap或专业缓存库如Caffeine、Guava Cache等)存储从Redis获取的数据。这种方式完全由应用程序自己管理,与Redis服务器无关。

以下是使用Spring Boot和Caffeine实现的简单本地缓存示例

@Service
public class RedisLocalCacheService {
    
    private final StringRedisTemplate redisTemplate;
    private final Cache<String, String> localCache;
    
    public RedisLocalCacheService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        
        // 配置Caffeine缓存
        this.localCache = Caffeine.newBuilder()
                .maximumSize(10_000)  // 最大缓存条目数
                .expireAfterWrite(Duration.ofMinutes(5))  // 写入后过期时间
                .recordStats()  // 记录统计信息
                .build();
    }
    
    public String get(String key) {
        // 首先尝试从本地缓存获取
        String value = localCache.getIfPresent(key);
        
        if (value != null) {
            // 本地缓存命中
            return value;
        }
        
        // 本地缓存未命中,从Redis获取
        value = redisTemplate.opsForValue().get(key);
        
        if (value != null) {
            // 将从Redis获取的值放入本地缓存
            localCache.put(key, value);
        }
        
        return value;
    }
    
    public void set(String key, String value) {
        // 更新Redis
        redisTemplate.opsForValue().set(key, value);
        
        // 更新本地缓存
        localCache.put(key, value);
    }
    
    public void delete(String key) {
        // 从Redis中删除
        redisTemplate.delete(key);
        
        // 从本地缓存中删除
        localCache.invalidate(key);
    }
    
    // 获取缓存统计信息
    public Map<String, Object> getCacheStats() {
        CacheStats stats = localCache.stats();
        Map<String, Object> statsMap = new HashMap<>();
        
        statsMap.put("hitCount", stats.hitCount());
        statsMap.put("missCount", stats.missCount());
        statsMap.put("hitRate", stats.hitRate());
        statsMap.put("evictionCount", stats.evictionCount());
        
        return statsMap;
    }
}

优点

  • 实现简单,易于集成
  • 无需额外的服务器支持
  • 可完全控制缓存行为(大小、过期策略等)
  • 显著减少网络请求次数
  • 对Redis服务器完全透明

缺点

  • 缓存一致性问题:当Redis数据被其他应用或服务更新时,本地缓存无法感知变化
  • 内存占用:需要消耗应用程序的内存资源
  • 冷启动问题:应用重启后缓存需要重新预热
  • 分布式环境下多实例之间的缓存不一致

二:Redis服务器辅助的客户端缓存

Redis 6.0引入了服务器辅助的客户端缓存功能,也称为跟踪模式(Tracking)。在这种模式下,Redis服务器会跟踪客户端请求的键,当这些键被修改时,服务器会向客户端发送失效通知。这种机制确保了客户端缓存与Redis服务器之间的数据一致性。

Redis提供了两种跟踪模式:

  1. 默认模式:服务器精确跟踪每个客户端关注的键
  2. 广播模式:服务器广播所有键的变更,客户端过滤自己关心的键

使用Lettuce(Spring Boot Redis的默认客户端)实现服务器辅助的客户端缓存:

@Service
public class RedisTrackingCacheService {
    
    private final StatefulRedisConnection<String, String> connection;
    private final RedisCommands<String, String> commands;
    private final Map<String, String> localCache = new ConcurrentHashMap<>();
    private final Set<String> trackedKeys = ConcurrentHashMap.newKeySet();
    
    public RedisTrackingCacheService(RedisClient redisClient) {
        this.connection = redisClient.connect();
        this.commands = connection.sync();
        
        // 配置客户端缓存失效监听器
        connection.addListener(message -> {
            if (message instanceof PushMessage) {
                PushMessage pushMessage = (PushMessage) message;
                if ("invalidate".equals(pushMessage.getType())) {
                    List<Object> invalidations = pushMessage.getContent();
                    handleInvalidations(invalidations);
                }
            }
        });
        
        // 启用客户端缓存跟踪
        commands.clientTracking(ClientTrackingArgs.Builder.enabled());
    }
    
    public String get(String key) {
        // 首先尝试从本地缓存获取
        String value = localCache.get(key);
        
        if (value != null) {
            return value;
        }
        
        // 本地缓存未命中,从Redis获取
        value = commands.get(key);
        
        if (value != null) {
            // 启用跟踪后,Redis服务器会记录这个客户端正在跟踪这个键
            localCache.put(key, value);
            trackedKeys.add(key);
        }
        
        return value;
    }
    
    public void set(String key, String value) {
        // 更新Redis
        commands.set(key, value);
        
        // 更新本地缓存
        localCache.put(key, value);
        trackedKeys.add(key);
    }
    
    private void handleInvalidations(List<Object> invalidations) {
        if (invalidations != null && invalidations.size() >= 2) {
            // 解析失效消息
            String invalidationType = new String((byte[]) invalidations.get(0));
            
            if ("key".equals(invalidationType)) {
                // 单个键失效
                String invalidatedKey = new String((byte[]) invalidations.get(1));
                localCache.remove(invalidatedKey);
                trackedKeys.remove(invalidatedKey);
            } else if ("prefix".equals(invalidationType)) {
                // 前缀失效
                String prefix = new String((byte[]) invalidations.get(1));
                Iterator<Map.Entry<String, String>> it = localCache.entrySet().iterator();
                while (it.hasNext()) {
                    String key = it.next().getKey();
                    if (key.startsWith(prefix)) {
                        it.remove();
                        trackedKeys.remove(key);
                    }
                }
            }
        }
    }
    
    // 获取缓存统计信息
    public Map<String, Object> getCacheStats() {
        Map<String, Object> stats = new HashMap<>();
        stats.put("cacheSize", localCache.size());
        stats.put("trackedKeys", trackedKeys.size());
        return stats;
    }
    
    // 清除本地缓存但保持跟踪
    public void clearLocalCache() {
        localCache.clear();
    }
    
    // 关闭连接并清理资源
    @PreDestroy
    public void cleanup() {
        if (connection != null) {
            connection.close();
        }
    }
}

优点

  • 自动维护缓存一致性,无需手动同步
  • Redis服务器能感知客户端缓存状态
  • 显著减少网络请求数量
  • 支持细粒度(键级别)的缓存控制
  • 实时感知数据变更,数据一致性保证强

缺点

  • 需要Redis 6.0以上版本支持
  • 增加Redis服务器内存占用(跟踪状态)
  • 客户端连接必须保持活跃
  • 服务器广播模式可能产生大量失效消息
  • 实现复杂度高于简单本地缓存

三:基于过期时间的缓存失效策略

基于过期时间(Time-To-Live,TTL)的缓存失效策略是一种简单有效的客户端缓存方案。它为本地缓存中的每个条目设置一个过期时间,过期后自动删除或刷新。

使用Spring Cache和Caffeine实现TTL缓存:

@Configuration
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeineSpec(CaffeineSpec.parse(
                "maximumSize=10000,expireAfterWrite=300s,recordStats"));
        return cacheManager;
    }
}

@Service
public class RedisTtlCacheService {
    
    private final StringRedisTemplate redisTemplate;
    
    @Autowired
    public RedisTtlCacheService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Cacheable(value = "redisCache", key = "#key")
    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    @CachePut(value = "redisCache", key = "#key")
    public String set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
        return value;
    }
    
    @CacheEvict(value = "redisCache", key = "#key")
    public void delete(String key) {
        redisTemplate.delete(key);
    }
    
    // 分层缓存 - 不同过期时间的缓存
    @Cacheable(value = "shortTermCache", key = "#key")
    public String getWithShortTtl(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    @Cacheable(value = "longTermCache", key = "#key")
    public String getWithLongTtl(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    // 在程序逻辑中手动控制过期时间
    public String getWithDynamicTtl(String key, Duration ttl) {
        // 使用LoadingCache,可以动态设置过期时间
        Cache<String, String> dynamicCache = Caffeine.newBuilder()
                .expireAfterWrite(ttl)
                .build();
        
        return dynamicCache.get(key, k -> redisTemplate.opsForValue().get(k));
    }
    
    // 定期刷新缓存
    @Scheduled(fixedRate = 60000) // 每分钟执行
    public void refreshCache() {
        // 获取需要刷新的键列表
        List<String> keysToRefresh = getKeysToRefresh();
        
        for (String key : keysToRefresh) {
            // 触发重新加载,会调用被@Cacheable注解的方法
            this.get(key);
        }
    }
    
    private List<String> getKeysToRefresh() {
        // 实际应用中,可能从配置系统或特定的Redis set中获取
        return Arrays.asList("config:app", "config:features", "daily:stats");
    }
    
    // 使用二级缓存模式,对热点数据使用更长的TTL
    public String getWithTwoLevelCache(String key) {
        // 首先查询本地一级缓存(短TTL)
        Cache<String, String> l1Cache = Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(Duration.ofSeconds(10))
                .build();
        
        String value = l1Cache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 查询本地二级缓存(长TTL)
        Cache<String, String> l2Cache = Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(Duration.ofMinutes(5))
                .build();
        
        value = l2Cache.getIfPresent(key);
        if (value != null) {
            // 提升到一级缓存
            l1Cache.put(key, value);
            return value;
        }
        
        // 查询Redis
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            // 更新两级缓存
            l1Cache.put(key, value);
            l2Cache.put(key, value);
        }
        
        return value;
    }
}


优点

  • 实现简单,易于集成到现有系统
  • 不依赖Redis服务器特殊功能
  • 适用于任何Redis版本
  • 内存占用可控,过期的缓存会自动清理
  • 通过调整TTL可以在一致性和性能之间取得平衡

缺点

  • 无法立即感知数据变更,存在一致性窗口期
  • TTL设置过短会导致缓存效果不佳
  • TTL设置过长会增加数据不一致的风险
  • 所有键使用统一TTL策略时缺乏灵活性
  • 可能出现"缓存风暴"(大量缓存同时过期导致突发流量)

总结

Redis客户端缓存是提升应用性能的强大工具,通过减少网络请求和数据库访问,可以显著降低延迟并提高吞吐量。
通过正确应用客户端缓存技术,可以在保持数据一致性的同时,显著提升系统性能和用户体验。

相关推荐

基于 Linux 快速搭建企业级 DNS 服务器(Bind9 ...

一、引言在大型企业网络或自建系统中,搭建一套高可用、自控的DNS解析服务器至关重要。本文将带你基于Linux环境,从零搭建企业级DNS服务平台,采用Bind9实战配置,确保解析稳定、安...

Linux无法解析域名的解决办法(linux无法解析域名的解决办法有哪些)

如果由于误操作,删除了系统原有的dhcp相关设置就无法正常解析域名。  此时,需要手动修改配置文件:  /etc/resolv.conf  将域名解析服务器手动添加到配置文件中  该文件是DNS域名解...

在centos7 创建基于域名的虚拟主机nginx服务器

直接用ip地址访问首先是不安全,其次不太容易记住,如果你的服务器上的项目有很多个,你创建多个基于Ip的虚拟主机,很容易导致公网ip冲突或乱用的情况。这时候我们就可以选择基于域名的虚拟主机。第一步、安装...

Linux之DNS服务(linux dnsserver)

一、学习路线如下二、DNS介绍1.域名的概念域名由特定的格式组成,用来表示互联网中某一台计算机或者计算机组的名称,能够使人更方便的访问互联网,而不用记住能够被机器直接读取的IP地址。2.DNS(dom...

Linux环境下DNS服务器配置图文详细教程

测试环境为vmware虚拟机下,linux系统为RedHatEnterpriseLinuxServer6.0(Santiago),内核版本Linux2.6.32-71.el6.i686...

构建基于 Linux 的高性能 DNS 服务器

在现代网络架构中,DNS(域名解析)是访问互联网的关键环节。搭建一个高性能、低延迟、可缓存加速的私有DNS服务器,不仅可以提升访问速度,还能增强网络隐私和安全性。本文将基于Linux系统,详细...

从运维的角度带你初识neo4j图形数据库的安装及配置

前言随着公司业务架构的改变,以前我部署环境的时候,一般只是部署Mysql,jdk,tomcat即可,现在还要部署一些nosql,如redis,neo4j,在之前从来没了解过,随着学习的深入而做了一些笔...

[超全整理] Java 程序员必备的 100 条 Linux 命令大全

一、基础操作(10条)#1.ls-查看目录内容ls-l#长格式显示文件和目录ls-a#显示隐藏文件ls-lh#带单位显示文件大小#2.cd-切换目录...

软件测试|一文教你轻松搭建docker环境

前言Docker提供轻量的虚拟化,你能够从Docker获得一个额外抽象层,你能够在单台机器上运行多个Docker微容器,而每个微容器里都有一个微服务或独立应用,例如你可以将Tomcat运行在一个Do...

docker基础知识/尚硅谷docker学习笔记

最近看了好多docker的资料,找了一些尚硅谷docker的教学视频,大概总结了一下前前后后的学习笔记。分享给大家。安装Docker的基本组成镜像Docker镜像(Image)就是一个只读的模板。镜...

前端_react项目从windows部署到centos

前言:从工程角度来讲,本地开发完就要把项目部署到生产环境,此过程的快慢也直接影响着整体的效率。所以也有很多人做持续集成的工作,例如:CI/CD/一键部署。但对于个人开发者而言,如果能有工具支撑是最好的...

Springboot项目使用docker部署(docker中运行springboot项目)

环境:SpringBoot2.2.10.RELEASE+Docker+Centos7+JDK8安装配置Dockeryum包更新到最新yumupdate卸载旧版本dockeryumre...

Spring Boot 3.x + Redis 7.x,轻松掌握Redisson分布式锁实战技巧

大家好,我是袁庭新。在分布式环境中,确保数据的一致性和正确性是至关重要的。对于需要高性能、高并发和分布式数据存储的应用程序来说,Redisson是一个很好的选择。同时,Redisson提供的分布式锁功...

Docker篇(二):Docker实战,命令解析

大家好,我是杰哥上周我们通过几个问题,让大家对于Docker有了一个全局的认识。然而,说跟练往往是两个概念。从学习的角度来说,理论知识的学习,往往只是第一步,只有经过实战,才能真正掌握一门技术所以,本...

新手快速入门Docker,轻松掌握Docker安装与使用

安装使用官方安装脚本自动安装curl-fsSLhttps://get.docker.com|bash-sdocker--mirrorAliyun手动安装CentOS7(使用yum进...

取消回复欢迎 发表评论: