学习笔记5:缓存穿透

缓存穿透

缓存穿透是指在缓存系统中,当一个请求的查询结果为空时,这个请求会直接穿透缓存系统,访问后端的数据库。如果这种情况频繁发生,会对数据库造成较大的压力,甚至可能导致数据库崩溃。

在正常情况下,缓存系统会将经常访问的数据存储在内存中,以便快速响应用户的请求。当用户请求某个数据时,系统首先检查缓存中是否存在该数据。如果存在,就直接从缓存中获取数据并返回;如果不存在,则向数据库查询数据,并将查询结果存储到缓存中,然后再返回给用户。

然而,如果某个数据在数据库中也不存在,那么缓存中同样不会有这个数据。这时,每次请求这个数据都会直接访问数据库,而不会经过缓存。这种情况就是缓存穿透。

缓存穿透的问题在于:

  1. 增加数据库压力:大量的空查询会增加数据库的负担,可能导致数据库响应变慢或崩溃。
  2. 降低系统性能:由于缓存系统没有发挥作用,系统的响应速度和吞吐量都会受到影响。

为了解决缓存穿透问题,可以采取以下一些策略:

  • 缓存空值:将查询结果为空的数据也缓存起来,但设置一个较短的过期时间。
  • 使用布隆过滤器:在缓存之前,使用布隆过滤器判断数据是否存在,从而避免对数据库的无效查询。
  • 互斥锁:当缓存中没有数据时,使用互斥锁确保只有一个线程去查询数据库并更新缓存。
  • 限流:对某些频繁查询的请求进行限流,减少对数据库的访问。
  • 数据库优化:优化数据库查询语句,减少查询时间,减轻数据库压力。

通过这些方法,可以有效地减少缓存穿透对系统的影响,提高系统的稳定性和性能。

缓存穿透是指当用户请求的数据在缓存中不存在时,请求会直接穿透到数据库。这不仅增加了数据库的负担,还可能导致数据库崩溃。以下是一些常见的解决方案:

  1. 使用布隆过滤器

    布隆过滤器是一种空间效率很高的数据结构,可以判断一个元素是否在一个集合中。在缓存穿透的情况下,可以使用布隆过滤器存储所有缓存的键,当请求到达时,先在布隆过滤器中判断是否存在,如果不存在,则直接返回,避免对数据库的查询。

  2. 缓存空值

    当查询数据库发现没有数据时,可以将空值也缓存起来,并设置一个较短的过期时间。这样,下次查询时,可以直接从缓存中获取,避免再次查询数据库。

  3. 使用互斥锁

    当缓存中没有数据时,可以使用互斥锁来控制多个线程同时访问数据库。当一个线程开始查询数据库时,其他线程会等待,直到数据库查询完成并更新缓存后,其他线程再继续执行。

  4. 设置过期时间

    对于缓存中的数据,可以设置一个合理的过期时间,这样可以避免缓存中的数据长时间占用内存,同时也能减少缓存穿透的风险。

  5. 增加随机因子

    在缓存空值时,可以增加一个随机因子,使得缓存的过期时间有一定的随机性,这样可以避免大量请求同时到达缓存失效的情况。

  6. 使用分布式缓存

    使用分布式缓存可以提高缓存的可用性和扩展性,同时也能更好地应对高并发的请求。

  7. 数据库查询优化

    优化数据库查询语句,减少查询时间,可以降低缓存穿透对数据库的压力。

  8. 限流

    对用户的请求进行限流,可以减少短时间内大量请求对数据库的冲击。

通过这些方法,可以有效地减少缓存穿透对系统的影响,提高系统的稳定性和性能。

如果系统本身就是依靠缓存来对数据库查询进行分流,那么缓存穿透问题可能不会那么明显,因为缓存的主要作用是减少对数据库的直接访问,提高系统的响应速度和吞吐量。即使在这种情况下,缓存穿透仍然是一个需要关注的问题,因为它可能会导致以下问题:

  1. 数据库压力增加:尽管缓存可以分流,但如果大量请求穿透到数据库,数据库的压力仍然会增加。
  2. 缓存效率降低:缓存的主要目的是减少对数据库的访问,如果大量请求直接访问数据库,缓存的效率就会降低。
  3. 系统稳定性风险:数据库的压力增加可能会影响系统的稳定性,尤其是在高并发场景下。

即使在以缓存分流为主要目的的系统中,也可以采取以下措施来预防或减轻缓存穿透的影响:

  1. 缓存空值:对于数据库查询结果为空的情况,仍然可以将这个空结果缓存起来,并设置一个合理的过期时间。这样,后续的相同请求就可以直接从缓存中获取结果,而不需要再次查询数据库。

  2. 使用布隆过滤器:布隆过滤器可以快速判断一个元素是否在一个集合中,使用它可以在查询数据库之前快速判断数据是否存在,从而避免无效的数据库查询。

  3. 设置合理的缓存策略:根据数据的访问模式和更新频率,设置合理的缓存策略,比如缓存热点数据、设置不同的过期时间等。

  4. 数据库查询优化:优化数据库的查询性能,比如使用索引、优化查询语句等,可以减少数据库处理查询的时间,从而降低缓存穿透的影响。

  5. 限流和降级:在系统压力较大时,通过限流和降级策略来保护系统,避免因为大量请求导致系统崩溃。

  6. 使用分布式缓存:分布式缓存可以提供更好的扩展性和容错性,有助于应对高并发的查询请求。

  7. 监控和报警:对系统进行监控,当检测到缓存穿透的迹象时,及时发出报警,以便采取相应的措施。

通过这些措施,即使在以缓存分流为主要目的的系统中,也可以有效地预防和减轻缓存穿透的影响,确保系统的稳定性和性能。

要使用hiredis来规避缓存穿透,可以通过以下伪代码示例来实现几种常见的策略:

1. 缓存空对象

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <hiredis/hiredis.h>

// 连接到Redis
redisContext* context = redisConnect("127.0.0.1", 6379);
if (context == NULL || context->err) {
    if (context) {
        printf("Error: %s\n", context->errstr);
        redisFree(context);
    }
    return;
}

// 定义一个函数来处理缓存穿透
void handleCacheMiss(const char* key) {
    redisReply* reply;
    char* cacheKey = "cache:shop:";
    char* cacheValue = NULL;

    // 拼接完整的缓存键
    char* fullKey = malloc(strlen(cacheKey) + strlen(key) + 1);
    strcpy(fullKey, cacheKey);
    strcat(fullKey, key);

    // 检查缓存中是否存在数据
    reply = redisCommand(context, "GET %s", fullKey);
    if (reply != NULL && reply->type == REDIS_REPLY_STRING) {
        cacheValue = reply->str;
    } else {
        // 数据库查询
        reply = redisCommand(context, "SELECT * FROM shop WHERE id = %s", key);
        if (reply != NULL && reply->type == REDIS_REPLY_ARRAY) {
            // 假设查询到数据,将其转换为字符串并缓存
            cacheValue = strdup("dummy_value"); // 这里应替换为实际查询结果
            redisCommand(context, "SET %s %s EX 30", fullKey, cacheValue); // 缓存30秒
        } else {
            // 缓存空值
            redisCommand(context, "SET %s '' EX 30", fullKey); // 缓存空值30秒
        }
        freeReplyObject(reply);
    }
    freeReplyObject(reply);
    free(fullKey);
}

int main() {
    const char* shopId = "123";

    // 处理缓存穿透
    handleCacheMiss(shopId);

    redisFree(context);
    return 0;
}

2. 使用布隆过滤器

布隆过滤器的实现较为复杂,通常需要在应用层实现。以下是一个简化的伪代码示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <hiredis/hiredis.h>
#include "bloom_filter.h" // 假设已经实现了布隆过滤器

// 连接到Redis
redisContext* context = redisConnect("127.0.0.1", 6379);
if (context == NULL || context->err) {
    if (context) {
        printf("Error: %s\n", context->errstr);
        redisFree(context);
    }
    return;
}

// 初始化布隆过滤器
BloomFilter* filter = bloomFilterCreate(10000, 0.01); // 假设有10000个元素,误判率为1%

// 定义一个函数来处理缓存穿透
void handleCacheMiss(const char* key) {
    redisReply* reply;
    char* cacheKey = "cache:shop:";
    char* fullKey = malloc(strlen(cacheKey) + strlen(key) + 1);
    strcpy(fullKey, cacheKey);
    strcat(fullKey, key);

    // 检查布隆过滤器
    if (bloomFilterCheck(filter, fullKey)) {
        printf("Key already checked, skip.\n");
        free(fullKey);
        return;
    }

    // 检查缓存中是否存在数据
    reply = redisCommand(context, "GET %s", fullKey);
    if (reply != NULL && reply->type == REDIS_REPLY_STRING) {
        printf("Cache hit.\n");
        freeReplyObject(reply);
    } else {
        // 数据库查询
        reply = redisCommand(context, "SELECT * FROM shop WHERE id = %s", key);
        if (reply != NULL && reply->type == REDIS_REPLY_ARRAY) {
            printf("Database hit, update cache.\n");
            redisCommand(context, "SET %s %s EX 30", fullKey, reply->str); // 缓存30秒
        } else {
            // 缓存空值
            redisCommand(context, "SET %s '' EX 30", fullKey); // 缓存空值30秒
            bloomFilterAdd(filter, fullKey); // 添加到布隆过滤器
        }
        freeReplyObject(reply);
    }
    free(fullKey);
}

int main() {
    const char* shopId = "123";

    // 处理缓存穿透
    handleCacheMiss(shopId);

    redisFree(context);
    bloomFilterDestroy(filter); // 销毁布隆过滤器
    return 0;
}

3. 加互斥锁

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <hiredis/hiredis.h>
#include <pthread.h>

// 连接到Redis
redisContext* context = redisConnect("127.0.0.1", 6379);
if (context == NULL || context->err) {
    if (context) {
        printf("Error: %s\n", context->errstr);
        redisFree(context);
    }
    return;
}

// 互斥锁
pthread_mutex_t lock;

// 定义一个函数来处理缓存穿透
void handleCacheMiss(const char* key) {
    redisReply* reply;
    char* cacheKey = "cache:shop:";
    char* fullKey = malloc(strlen(cacheKey) + strlen(key) + 1);
    strcpy(fullKey, cacheKey);
    strcat(fullKey, key);

    // 获取互斥锁
    pthread_mutex_lock(&lock);

    // 再次检查缓存中是否存在数据
    reply = redisCommand(context, "GET %s", fullKey);
    if (reply != NULL && reply->type == REDIS_REPLY_STRING) {
        printf("Cache hit.\n");
        freeReplyObject(reply);
    } else {
        // 数据库查询
        reply = redisCommand(context, "SELECT * FROM shop WHERE id = %s", key);
        if (reply != NULL && reply->type == REDIS_REPLY_ARRAY) {
            printf("Database hit, update cache.\n");
            redisCommand(context, "SET %s %s EX 30", fullKey, reply->str); // 缓存30秒
        } else {
            // 缓存空值
            redisCommand(context, "SET %s '' EX 30", fullKey); // 缓存空值30秒
        }
        freeReplyObject(reply);
    }

    // 释放互斥锁
    pthread_mutex_unlock(&lock);

    free(fullKey);
}

int main() {
    const char* shopId = "123";

    // 初始化互斥锁
    pthread_mutex_init(&lock, NULL);

    // 处理缓存穿透
    handleCacheMiss(shopId);

    redisFree(context);
    pthread_mutex_destroy(&lock); // 销毁互斥锁
    return 0;
}

这些伪代码示例展示了如何在C语言中使用hiredis库来实现缓存穿透的规避策略。实际应用中,你可能需要根据具体的业务需求和系统架构进行调整和优化。

相关推荐
刘婉晴5 分钟前
【信息安全工程师备考笔记】第三章 密码学基本理论
笔记·安全·密码学
球求了23 分钟前
C++:继承机制详解
开发语言·c++·学习
时光追逐者1 小时前
MongoDB从入门到实战之MongoDB快速入门(附带学习路线图)
数据库·学习·mongodb
一弓虽1 小时前
SpringBoot 学习
java·spring boot·后端·学习
头顶秃成一缕光1 小时前
Redis的主从模式和哨兵模式
数据库·redis·缓存
观无2 小时前
Redis安装及入门应用
数据库·redis·缓存
晓数2 小时前
【硬核干货】JetBrains AI Assistant 干货笔记
人工智能·笔记·jetbrains·ai assistant
我的golang之路果然有问题2 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
genggeng不会代码2 小时前
用于协同显著目标检测的小组协作学习 2021 GCoNet(总结)
学习
lwewan2 小时前
26考研——存储系统(3)
c语言·笔记·考研