学习笔记5:缓存穿透

avatar
作者
猴君
阅读量:0

缓存穿透

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

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

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

缓存穿透的问题在于:

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

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!