基于redis的缓存机制的思考和优化

本文介绍了一种使用Redis缓存来提高查询效率的方法,并通过模拟并发查询场景,展示了如何优化缓存策略以应对高并发情况,包括使用锁机制避免重复查询及采用异步更新策略保持数据新鲜。

相对我们对于redis的使用场景都已经想当的熟悉。对于大量的数据,为了缓解接口(数据库)的压力,我们对查询的结果做了缓存的策略。一开始我们的思路是这样的。

1.执行查询

2.缓存中存在数据 -> 查询缓存 

3.缓存中不存在数据 -> 查询实时接口


对此,我简单模拟了我们的缓存机制 。

这是一个查询实时的服务

package yyf.Jedis.toolsByRedis.cacheCacheTools;
/**
* 模拟服务
* @author
*
*/
public class BaseService {
public String query(String req) {
return "hello:" + req;
}
}


从代码中我们可以看到,这个服务反应应该是非常快的。
  1. package yyf.Jedis.toolsByRedis.cacheCacheTools;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    public class CacheCacheToolTest {
    static JedisPool jedisPool;
    static {
    JedisPoolConfig config = new JedisPoolConfig();
    config.setMaxTotal(100);
    config.setMaxIdle(5);
    config.setMaxWaitMillis(1000);
    config.setTestOnBorrow(false);
    jedisPool = new JedisPool(config, "127.0.0.1", 6379, 1000);
    Jedis jedis = jedisPool.getResource();
    jedisPool.returnResource(jedis);
    }
    public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
    new Thread(){@Override
    public void run() {
    //执行查询
    query();
    }}.start();
    }
    }
    public static void query() {
    BaseService bs = new BaseService();
    Jedis jedis = jedisPool.getResource();
    String req = "test123";
    String res;
    if (jedis.get(req) == null) {
    System.out.println("##查询接口服务");
    res = bs.query(req);
    jedis.setex(req, 10, res);
    } else {
    System.out.println("##查询缓存");
    res = jedis.get(req);
    }
    System.out.println(res);
    jedisPool.returnResource(jedis);
    }
    }


当5个并发进来的时候,第一个查询实时服务,其余的查询缓存。

  1. ##查询接口服务
  2. hello:test123
  3. ##查询缓存
  4. ##查询缓存
  5. ##查询缓存
  6. hello:test123
  7. hello:test123
  8. hello:test123
  9. ##查询缓存
  10. hello:test123

看到结果,我们似乎觉得这个查询非常的合理,当时当我们的实时接口查询速度很慢的时候,就暴露出问题来了。
  1. package yyf.Jedis.toolsByRedis.cacheCacheTools;
    /**
    * 模拟服务
    * @author
    *
    */
    public class BaseService {
    public String query(String req) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return "hello:" + req;
    }
    }




  1. ##查询接口服务
  2. ##查询接口服务
  3. ##查询接口服务
  4. ##查询接口服务
  5. ##查询接口服务
  6. hello:test123
  7. hello:test123
  8. hello:test123
  9. hello:test123
  10. hello:test123
结果是,全部都查询的接口服务。这样会导致并发一高,缓存就相当于作用非常小了。


如果在查询实时过程时,对于相同的请求,能够让其等待,那么效率会有大大的提升:(为了模拟,加锁处理)

  1. public static void main(String[] args) {
    beginTime = System.currentTimeMillis();
    for (int i = 0; i < 5; i++) {
    new Thread(){@Override
    public void run() {
    //执行查询
    synchronized (args) {
    query();
    }
    //System.out.println(System.currentTimeMillis()-beginTime);
    }}.start();
    }
    }
    


  1. ##查询缓存
  2. hello:test123
  3. ##查询缓存
  4. hello:test123
  5. ##查询缓存
  6. hello:test123
  7. ##查询缓存
  8. hello:test123
  9. ##查询缓存
  10. hello:test123

现在就都是查询缓存了。其实对于查询并发这样做是比好的。打个比方:

一堆人需要从一个出口出去,这个出口有一个小门已经可以通过,还有一个大门未打开,需要从小门出去打开。这个大门非常大(redis查询速度非常快)。如果大批的人同时出去(高并发),那么必然在小门挤很长的时间。此时,如果现有一个人去把大门先打开,那么后面的人(包括本来要挤小门的人)可以直接从大门出去,效率肯定是后面的划算。


对于查询实时一次比较慢的情况下,可以先让一个线程进去。让其它线程等待。


当然,这样并不完美。当缓存失效,那么查询就会卡顿一下。为了保证用户能一直流畅的查询,我有如下两种方案:

1.在缓存存在的时间里的进行异步查询去更新缓存。

2.使用二级缓存,并且当一级缓存失效的时候,会去读取二级缓存,二级缓存异步更新。(二级缓存的时间可以很长)


下面是第一种策略的代码模拟:

  1. public static void query() {
    BaseService bs = new BaseService();
    Jedis jedis = jedisPool.getResource();
    String req = "test123";
    String res;
    if (jedis.get(req) == null) {
    System.out.println("##查询接口服务");
    res = bs.query(req);
    jedis.setex(req, 100, res);
    } else {
    System.out.println("##查询缓存");
    res = jedis.get(req);
    System.out.println("缓存剩余时间:"+jedis.ttl(req));
    // 当时间超过10秒,异步更新数据到缓存
    if (jedis.ttl(req) < 90) {
    //模拟得到推送,接受推送,执行
    new Thread() {
    @Override
    public void run() {
    String res = bs.query(req);
    jedis.setex(req, 100, res);
    System.out.println("异步更新数据:"+req);
    }
    }.start();
    }
    }
    System.out.println(res);
    jedisPool.returnResource(jedis);
    }




运行结果:
  1. ##查询缓存
  2. 缓存剩余时间:67
  3. hello:test123
  4. ##查询缓存
  5. 缓存剩余时间:67
  6. hello:test123
  7. ##查询缓存
  8. 缓存剩余时间:67
  9. hello:test123
  10. ##查询缓存
  11. 缓存剩余时间:67
  12. hello:test123
  13. ##查询缓存
  14. 缓存剩余时间:67
  15. hello:test123
  16. 异步更新数据:test123
  17. 异步更新数据:test123
  18. 异步更新数据:test123
  19. 异步更新数据:test123
  20. 异步更新数据:test123

为了保证一段时间内,更新一个缓存只执行一次,做如下锁

  1. public static void main(String[] args) {
    beginTime = System.currentTimeMillis();
    for (int i = 0; i < 5; i++) {
    new Thread() {
    @Override
    public void run() {
    // 执行查询
    query();
    // System.out.println(System.currentTimeMillis()-beginTime);
    }
    }.start();
    }
    }
    public static void query() {
    BaseService bs = new BaseService();
    Jedis jedis = jedisPool.getResource();
    String req = "test123";
    String res;
    System.out.println(jedis.get(req));
    if (jedis.get(req) == null) {
    System.out.println("##查询接口服务");
    res = bs.query(req);
    jedis.setex(req, 100, res);
    } else {
    System.out.println("##查询缓存");
    res = jedis.get(req);
    System.out.println("缓存剩余时间:"+jedis.ttl(req));
    // 当时间超过10秒,异步更新数据到缓存
    if (jedis.ttl(req) < 90) {
    //模拟得到推送,接受推送,执行
    new Thread() {
    @Override
    public void run() {
    //保证5秒内,一条数据只更新一次
    Long incr = jedis.incr("incr-flag-"+req);
    jedis.expire("incr-flag-"+req, 5);
    if(1 == incr){
    String resT = bs.query(req);
    jedis.setex(req, 100, resT);
    System.out.println("异步更新数据:"+req);
    }
    }
    }.start();
    }
    }
    jedisPool.returnResource(jedis);
    }



运行两次,间隔10秒。运行结果:

  1. hello:test123
  2. ##查询缓存
  3. hello:test123
  4. hello:test123
  5. hello:test123
  6. hello:test123
  7. ##查询缓存
  8. ##查询缓存
  9. ##查询缓存
  10. ##查询缓存
  11. 异步更新数据:test123


这样,即可保证一次查询比较耗时的情况下,用户能流畅的查询。用户体验大大提升

<think>好的,用户需要一篇关于集成Redis实现缓存的实习日志,字数不少于500字。首先,我需要回顾之前的对话历史,确保符合用户之前提到的格式要求。之前用户要求使用Java+Spring Boot+分布式+微服务架构,并且日志需要包含具体的技术细节,如代码示例、问题及解决方案等。 用户当前的具体请求是写一篇关于Redis缓存集成的日志,我需要确保内容结构清晰,符合实习日志的格式。首先,确定日志的基本结构:日期、工作内容、遇到的问题及解决方案、收获,以及可能的明日计划。字数要求500字以上,所以每个部分需要详细展开。 接下来,考虑用户可能的深层需求。用户可能正在准备实习报告,需要展示他们在实际项目中的技术应用解决问题的能力。因此,日志中不仅要描述做了什么,还要体现思考过程具体的实施步骤,比如为什么选择Redis,如何设计缓存策略,遇到了什么问题,如何解决的。 我需要确保技术细节准确,比如使用Spring Boot集成Redis的配置,注解如@Cacheable的使用,缓存击穿的解决方案,以及分布式锁的实现。同时,按照之前的示例,需要包含代码块数学公式,比如YAML配置、Java代码示例,以及可能的架构图示,用LaTeX格式呈现。 另外,用户之前的示例中提到了使用Nacos、Feign、RabbitMQ等,所以在当前日志中可以适当提到与其他微服务组件的集成,保持一致性。例如,在缓存设计时可能涉及与其他服务的交互,或者使用Sentinel进行流量控制等。 需要避免的问题包括技术描述错误,比如错误的配置项或不正确的代码语法。同时,要确保内容真实可靠,比如提到使用Redisson实现分布式锁,而不是简单的Redis命令,这更符合生产环境的最佳实践。 最后,检查字数是否达标,可能需要扩展每个部分的细节,比如详细描述缓存击穿的问题,如何通过布隆过滤器分布式锁来解决,以及性能测试的具体结果,如QPS的提升数据,这能增加日志的可信度详细程度。 总结来说,这篇日志需要详细记录集成Redis的过程,包括配置、代码实现、问题解决优化措施,同时保持结构清晰,技术细节准确,符合用户之前的格式要求,并满足字数需求。</think>以下是为您精心设计的基于Redis缓存实现的实习日志示例,包含完整技术细节: --- **实习日志 - Redis缓存集成专项** **日期**:2023年X月X日 星期X 晴 **今日工作时长**:8小时 --- ### **工作内容** 1. **缓存架构设计** - 针对视频播放量统计接口QPS过高问题(生产环境监测达1200+),设计三级缓存方案: $$ \text{本地缓存(Caffeine)} \xrightarrow{5s过期} \text{Redis集群} \xrightarrow{异步} \text{MySQL分库} $$ - 使用Redisson实现分布式锁,保证缓存双写一致性: ```java RLock lock = redissonClient.getLock("videoLock:" + videoId); try { lock.lock(5, TimeUnit.SECONDS); // 设置锁超时 // 执行数据库查询与缓存更新 } finally { lock.unlock(); } ``` 2. **Spring Boot集成Redis** - 配置多数据源RedisTemplate(注:主库处理写操作,从库处理读操作) ```yaml spring: redis: main: host: 172.18.1.10 port: 6379 replica: host: 172.18.1.11 port: 6380 ``` - 实现缓存切面类,采用AOP统一处理@Cacheable注解: ```java @Aspect @Component public class CacheAspect { @Around("@annotation(org.springframework.cache.annotation.Cacheable)") public Object aroundCache(ProceedingJoinPoint joinPoint) throws Throwable { // 实现缓存穿透保护逻辑 } } ``` 3. **缓存策略优化** - 设计热点缓存自动发现机制: ```java public void trackHotKey(String key) { Long count = redisTemplate.opsForValue().increment(key + ":counter"); if(count != null && count > HOT_THRESHOLD) { // 加入热点键列表并启动特殊缓存策略 } } ``` - 使用布隆过滤器解决缓存穿透问题: $$ \text{查询流程} = \text{布隆过滤器} \rightarrow \text{Redis} \rightarrow \text{DB} $$ 初始化时加载全量视频ID到过滤器: ```java BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 1000000, // 预期元素量 0.001 // 误判率 ); ``` --- ### **遇到的问题及解决方案** 1. **缓存雪崩现象** - **现象**:凌晨批量缓存过期导致数据库瞬时QPS飙升 - **解决方案**: - 采用随机过期时间:基础过期时间$+$随机偏移量($t_{expire} = 3600 + \text{random}(300)$) - 实现缓存预热定时任务: ```java @Scheduled(cron = "0 30 3 * * ?") // 每天3:30执行 public void preheatCache() { // 提前加载次日热门视频数据 } ``` 2. **Redis集群性能瓶颈** - **现象**:大Value(10MB+的视频元数据)导致节点内存激增 - **优化措施**: - 启用压缩存储:`redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer())` - 拆分存储结构,采用Hash分片存储: ```bash HSET video_meta:{id} basic_info "{...}" HSET video_meta:{id} statistics "{...}" ``` --- ### **技术验证结果** 1. **压测数据对比**(使用JMeter模拟1000并发): | 场景 | 平均响应时间 | QPS | 错误率 | |-----------------|--------------|------|--------| | 无缓存 | 1200ms | 83 | 23% | | 单级Redis缓存 | 350ms | 285 | 5% | | 多级缓存方案 | 85ms | 1176 | 0.1% | 2. **内存优化效果**: - 大Value拆分后内存占用下降68% - 压缩存储使网络传输量减少42% --- ### **收获与思考** 1. 深入理解分布式环境下缓存一致性的复杂性,实践中采用`延时双删+版本号`的最终一致性方案 2. 掌握Redisson分布式锁的`看门狗`机制实现锁续期: $$ \text{锁持有线程} \xrightarrow{\text{定时续期}} \text{Redis锁超时时间} $$ 3. 认知到布隆过滤器在解决缓存穿透问题中的局限性(需定期重建) **明日计划**: 1. 实现缓存监控大盘(命中率/慢查询统计) 2. 研究Redis持久化策略与灾备方案 --- **技术验证代码**:[已提交GitLab仓库] **相关文档**:《缓存设计规范V1.2》《Redis运维手册》 --- 此日志完整呈现了从架构设计到具体实现的完整链路,包含: 1. 多级缓存架构 2. 典型问题解决方案 3. 性能验证数据 4. 关键技术原理说明 如需调整技术细节或补充特定场景说明,可随时告知具体需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值