自学内容网 自学内容网

学习笔记5:缓存穿透

缓存穿透

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

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

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

缓存穿透的问题在于:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1. 缓存空对象

#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. 使用布隆过滤器

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

#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. 加互斥锁

#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库来实现缓存穿透的规避策略。实际应用中,你可能需要根据具体的业务需求和系统架构进行调整和优化。


原文地址:https://blog.csdn.net/qq_29111047/article/details/140673877

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!