烂大街的缓存穿透、缓存击穿和缓存雪崩,你真的懂了?

本文介绍了缓存系统常见的三大问题:缓存穿透、缓存击穿和缓存雪崩,并给出了相应的解决方案。缓存穿透可通过参数校验和布隆过滤器来避免,布隆过滤器虽然可能产生误判,但能有效减少数据库压力。缓存击穿则可以通过加锁机制或自动续期来解决。缓存雪崩可以通过设置随机过期时间和构建高可用缓存服务来预防。文章还讨论了服务降级的策略,以应对极端情况。

点击上方“CSDN精品课”,选择“置顶公众号”

第一时间获取精品编程教程

大家好,我是苏三,又跟大家见面了。

祝大家圣诞节快乐,文末送书!!!

前言

对于从事后端开发的同学来说,缓存已经变成的项目中必不可少的技术之一。

没错,缓存能给我们系统显著的提升性能。但如果你使用不好,或者缺乏相关经验,它也会带来很多意想不到的问题。

今天我们一起聊聊如果在项目中引入了缓存,可能会给我们带来的下面这三大问题。看看你中招了没?

1. 缓存穿透问题

大部分情况下,加缓存的目的是:为了减轻数据库的压力,提升系统的性能。

1.1 我们是如何用缓存的?

一般情况下,如果有用户请求过来,先查缓存,如果缓存中存在数据,则直接返回。如果缓存中不存在,则再查数据库,如果数据库中存在,则将数据放入缓存,然后返回。如果数据库中也不存在,则直接返回失败。

流程图如下:68603507e451505696af02393914155e.png上面的这张图小伙们肯定再熟悉不过了,因为大部分缓存都是这样用的。

1.2 什么是缓存穿透?

但如果出现以下这两种特殊情况,比如:

  1. 用户请求的id在缓存中不存在。

  2. 恶意用户伪造不存在的id发起请求。

这样的用户请求导致的结果是:每次从缓存中都查不到数据,而需要查询数据库,同时数据库中也没有查到该数据,也没法放入缓存。也就是说,每次这个用户请求过来的时候,都要查询一次数据库。

3e39fa3c19a157ae657f5fbe23ef043c.png图中标红的箭头表示每次走的路线。

很显然,缓存根本没起作用,好像被穿透了一样,每次都会去访问数据库。

这就是我们所说的:缓存穿透问题

如果此时穿透了缓存,而直接数据库的请求数量非常多,数据库可能因为扛不住压力而挂掉。呜呜呜。

那么问题来了,如何解决这个问题呢?

1.3 校验参数

我们可以对用户id做检验。

比如你的合法id是15xxxxxx,以15开头的。如果用户传入了16开头的id,比如:16232323,则参数校验失败,直接把相关请求拦截掉。这样可以过滤掉一部分恶意伪造的用户id。

1.4 布隆过滤器

如果数据比较少,我们可以把数据库中的数据,全部放到内存的一个map中。

这样能够非常快速的识别,数据在缓存中是否存在。如果存在,则让其访问缓存。如果不存在,则直接拒绝该请求。

但如果数据量太多了,有数千万或者上亿的数据,全都放到内存中,很显然会占用太多的内存空间。

那么,有没有办法减少内存空间呢?

答:这就需要使用布隆过滤器了。

布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。

布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一些列的hash算法(比如:三次hash算法)计算,每个key都会计算出多个位置,然后把这些位置上的元素值设置成1。2c73074e28819d60ae94b85dbda749b7.png之后,有用户key请求过来的时候,再用相同的hash算法计算位置。

  • 如果多个位置中的元素值都是1,则说明该key在数据库中已存在。这时允许继续往后面操作。

  • 如果有1个以上的位置上的元素值是0,则说明该key在数据库中不存在。这时可以拒绝该请求,而直接返回。

使用布隆过滤器确实可以解决缓存穿透问题,但同时也带来了两个问题:

  1. 存在误判的情况。

  2. 存在数据更新问题。

先看看为什么会存在误判呢?

上面我已经说过,初始化数据时,针对每个key都是通过多次hash算法,计算出一些位置,然后把这些位置上的元素值设置成1。

但我们都知道hash算法是会出现hash冲突的,也就是说不通的key,可能会计算出相同的位置。69eac741a2fb6c8c1b101f4aadb5740b.png上图中的下标为2的位置就出现了hash冲突,key1和key2计算出了一个相同的位置。

如果有几千万或者上亿的数据,布隆过滤器中的hash冲突会非常明显。

如果某个用户key,经过多次hash计算出的位置,其元素值,恰好都被其他的key初始化成了1。此时,就出现了误判,原本这个key在数据库中是不存在的,但布隆过滤器确认为存在。

如果布隆过滤器判断出某个key存在,可能出现误判。如果判断某个key不存在,则它在数据库中一定不存在。

通常情况下,布隆过滤器的误判率还是比较少的。即使有少部分误判的请求,直接访问了数据库,但如果访问量并不大,对数据库影响也不大。

此外,如果想减少误判率,可以适当增加hash函数,图中用的3次hash,可以增加到5次。

其实,布隆过滤器最致命的问题是:如果数据库中的数据更新了,需要同步更新布隆过滤器。但它跟数据库是两个数据源,就可能存在数据不一致的情况。

比如:数据库中新增了一个用户,该用户数据需要实时同步到布隆过滤。但由于网络异常,同步失败了。004e14d072464297d565bb841d5bdbe6.png这时刚好该用户请求过来了,由于布隆过滤器没有该key的数据,所以直接拒绝了该请求。但这个是正常的用户,也被拦截了。

很显然,如果出现了这种正常用户被拦截了情况,有些业务是无法容忍的。所以,布隆过滤器要看实际业务场景再决定是否使用,它帮我们解决了缓存穿透问题,但同时了带来了新的问题。

1.5 缓存空值

上面使用布隆过滤器,虽说可以过滤掉很多不存在的用户id请求。但它除了增加系统的复杂度之外,会带来两个问题:

  1. 布隆过滤器存在误杀的情况,可能会把少部分正常用户的请求也过滤了。

  2. 如果用户信息有变化,需要实时同步到布隆过滤器,不然会有问题。

所以,通常情况下,我们很少用布隆过滤器解决缓存穿透问题。其实,还有另外一种更简单的方案,即:缓存空值

当某个用户id在缓存中查不到,在数据库中也查不到时,也需要将该用户id缓存起来,只不过值是空的。这样后面的请求,再拿相同的用户id发起请求时,就能从缓存中获取空数据,直接返回了,而无需再去查一次数据库。

优化之后的流程图如下:0ff09afdf716600664ece2666d213860.png关键点是不管从数据库有没有查到数据,都将结果放入缓存中,只是如果没有查到数据,缓存中的值是空的罢了。

2. 缓存击穿问题

2.1 什么是缓存击穿?

有时候,我们在访问热点数据时。比如:我们在某个商城购买某个热门商品。

为了保证访问速度,通常情况下,商城系统会把商品信息放到缓存中。但如果某个时刻,该商品到了过期时间失效了。

此时,如果有大量的用户请求同一个商品,但该商品在缓存中失效了,一下子这些用户请求都直接怼到数据库,可能会造成瞬间数据库压力过大,而直接挂掉。

流程图如下:fbee0b2ee29999a78405eb770dbd5b99.png那么,如何解决这个问题呢?

2.2 加锁

数据库压力过大的根源是,因为同一时刻太多的请求访问了数据库。

如果我们能够限制,同一时刻只有一个请求才能访问某个productId的数据库商品信息,不就能解决问题了?

答:没错,我们可以用加锁的方式,实现上面的功能。

伪代码如下:

try {
  String result = jedis.set(productId, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
    return queryProductFromDbById(productId);
  }
} finally{
    unlock(productId,requestId);
}  
return null;

在访问数据库时加锁,防止多个相同productId的请求同时访问数据库。

然后,还需要一段代码,把从数据库中查询到的结果,又重新放入缓存中。办法挺多的,在这里我就不展开了。

2.3 自动续期

出现缓存击穿问题是由于key过期了导致的。那么,我们换一种思路,在key快要过期之前,就自动给它续期,不就OK了?

答:没错,我们可以用job给指定key自动续期。

比如说,我们有个分类功能,设置的缓存过期时间是30分钟。但有个job每隔20分钟执行一次,自动更新缓存,重新设置过期时间为30分钟。9cc71a2f3944a141f03041e21cc0c26a.png这样就能保证,分类缓存不会失效。

此外,在很多请求第三方平台接口时,我们往往需要先调用一个获取token的接口,然后用这个token作为参数,请求真正的业务接口。一般获取到的token是有有效期的,比如24小时之后失效。

如果我们每次请求对方的业务接口,都要先调用一次获取token接口,显然比较麻烦,而且性能不太好。

这时候,我们可以把第一次获取到的token缓存起来,请求对方业务接口时从缓存中获取token。

同时,有一个job每隔一段时间,比如每隔12个小时请求一次获取token接口,不停刷新token,重新设置token的过期时间。

2.4 缓存不失效

此外,对于很多热门key,其实是可以不用设置过期时间,让其永久有效的。

比如参与秒杀活动的热门商品,由于这类商品id并不多,在缓存中我们可以不设置过期时间。

在秒杀活动开始前,我们先用一个程序提前从数据库中查询出商品的数据,然后同步到缓存中,提前做预热

等秒杀活动结束一段时间之后,我们再手动删除这些无用的缓存即可。

3. 缓存雪崩问题

3.1 什么是缓存雪崩?

前面已经聊过缓存击穿问题了。

而缓存雪崩是缓存击穿的升级版,缓存击穿说的是某一个热门key失效了,而缓存雪崩说的是有多个热门key同时失效。看起来,如果发生缓存雪崩,问题更严重。

缓存雪崩目前有两种:

  1. 有大量的热门缓存,同时失效。会导致大量的请求,访问数据库。而数据库很有可能因为扛不住压力,而直接挂掉。

  2. 缓存服务器down机了,可能是机器硬件问题,或者机房网络问题。总之,造成了整个缓存的不可用。

归根结底都是有大量的请求,透过缓存,而直接访问数据库了。5c1ed4d24ca7f4b3b5ff64de43fa4c16.png那么,要如何解决这个问题呢?

3.2 过期时间加随机数

为了解决缓存雪崩问题,我们首先要尽量避免缓存同时失效的情况发生。

这就要求我们不要设置相同的过期时间。

可以在设置的过期时间基础上,再加个1~60秒的随机数。

实际过期时间 = 过期时间 + 1~60秒的随机数

这样即使在高并发的情况下,多个请求同时设置过期时间,由于有随机数的存在,也不会出现太多相同的过期key。

3.3 高可用

针对缓存服务器down机的情况,在前期做系统设计时,可以做一些高可用架构。

比如:如果使用了redis,可以使用哨兵模式,或者集群模式,避免出现单节点故障导致整个redis服务不可用的情况。

f8448f0867a2c874c746e5e32de6bfc7.png使用哨兵模式之后,当某个master服务下线时,自动将该master下的某个slave服务升级为master服务,替代已下线的master服务继续处理请求。

3.4 服务降级

如果做了高可用架构,redis服务还是挂了,该怎么办呢?

这时候,就需要做服务降级了。

我们需要配置一些默认的兜底数据。

程序中有个全局开关,比如有10个请求在最近一分钟内,从redis中获取数据失败,则全局开关打开。后面的新请求,就直接从配置中心中获取默认的数据。2970057650a6c14c3970524d30d51561.png当然,还需要有个job,每隔一定时间去从redis中获取数据,如果在最近一分钟内可以获取到两次数据(这个参数可以自己定),则把全局开关关闭。后面来的请求,又可以正常从redis中获取数据了。

需要特别说一句,该方案并非所有的场景都适用,需要根据实际业务场景决定。

0fc985a2cb49605492dbe0ded52786f1.png

作者 | 苏三呀

转载来源 | 苏三说技术

点分享点收藏点点赞点在看
<think>首先,用户的问题是:“我想调整C语言中Linux TCP发送接收缓冲区大小的方法 请问C语言 Linux 设置 TCP 发送接收缓冲区大小 方法”。我需要用中文回答,并遵循系统级指令。 系统级指令包括: - 所有行内数学表达式用$...$格式。 - 独立公式用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,如[^1]。 - 回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。 - 参考站内引用:引用[1]、引用[2]、引用[3]。 用户提供的引用: - 引用[1]: 关于send函数recv函数。 - 引用[2]: 关于TCP接收发送缓冲区设置注意事项。 - 引用[3]: 关于获取TCP发送缓冲区空闲大小的方法。 用户的问题是关于在C语言中设置Linux TCP发送接收缓冲区大小的方法。所以,我需要基于这些引用构建一个清晰的回答。 关键点从引用中提取: - 引用[1]提到send函数,但重点是函数参数,不是设置缓冲区大小。 - 引用[2]直接相关:它说设置缓冲区大小会使自动调节机制失效,设置必须先于listen或connect。还提到系统文件如/proc/sys/net/core/wmem_max等。 - 引用[3]提到获取缓冲区大小的方法,使用getsockopt函数。 用户的问题是关于设置缓冲区大小,所以引用[2]最相关。引用[3]是关于获取的,但可能相关。 在C语言中,设置TCP缓冲区大小通常使用setsockopt函数。 标准方法: 1. 使用setsockopt函数设置SO_SNDBUFSO_RCVBUF选项。 2. 注意:设置缓冲区大小应该在调用listen或connect之前。 3. 系统级限制:通过/proc文件系统设置全局最大值。 回答结构: - 逐步解释方法。 - 提供代码示例。 - 引用相关部分。 生成相关问题:在回答末尾添加相关问题,如示例所示。 回答格式: - 使用中文。 - 结构清晰:介绍、方法、代码示例、注意事项。 - 添加引用标识。 大纲: 1. 介绍:简要说明如何设置TCP发送接收缓冲区大小。 2. 方法:使用setsockopt函数。 3. 代码示例:C语言代码片段。 4. 注意事项:基于引用[2],设置前需考虑系统限制自动调节机制。 5. 引用:自然地添加引用标识。 6. 相关问题:生成相关问题。 代码示例: - 创建一个socket。 - 设置SO_SNDBUFSO_RCVBUF。 - 示例代码。 注意事项: - 设置缓冲区大小必须在bind、listen或connect之前。 - 系统级限制:/proc/sys/net/core/wmem_max等。 - 自动调节机制可能失效(引用[2])。 相关问题: 1. 如何获取当前的TCP发送接收缓冲区大小? 2. 设置缓冲区大小有什么最佳实践? 3. TCP缓冲区大小如何影响网络性能? 引用标识: - 在相关段落末尾添加[^1]、[^2]、[^3]。 确保LaTeX:如果涉及数学表达式,但这里可能不需要,除非有公式。用户问题不涉及数学,所以可能不需要LaTeX。但指令说所有行内数学表达式必须用$...$格式,所以如果有变量或数字,可以用$size$等,但可能不必要。 现在,构建回答。</think>### 设置 Linux TCP 发送接收缓冲区大小的方法 在 C 语言中调整 Linux TCP 发送接收缓冲区大小,主要通过 `setsockopt` 函数实现。这种方法允许你在程序运行时动态设置缓冲区大小,但需注意系统级限制最佳实践。以下我将逐步解释方法,并提供代码示例。 #### 1. **核心方法:使用 `setsockopt` 函数** - `setsockopt` 是标准套接字 API 的一部分,用于设置套接字选项。 - 设置发送缓冲区大小:使用选项 `SO_SNDBUF`。 - 设置接收缓冲区大小:使用选项 `SO_RCVBUF`。 - 函数原型: ```c int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); ``` - `sockfd`: 套接字描述符(通过 `socket` 函数创建)。 - `level`: 设置为 `SOL_SOCKET`,表示操作套接字层。 - `optname`: 选项名(`SO_SNDBUF` 或 `SO_RCVBUF`)。 - `optval`: 指向缓冲区大小的指针(类型为 `int*`)。 - `optlen`: `optval` 的大小(通常为 `sizeof(int)`)。 - 返回值:成功时返回 0,失败时返回 -1 并设置 `errno`。 - **关键点**:设置缓冲区大小必须在调用 `listen`(服务器端)或 `connect`(客户端)之前进行,否则设置可能无效或导致未定义行为[^2]。 #### 2. **代码示例** 以下是一个完整的 C 语言示例,展示如何创建 TCP 套接字并设置发送接收缓冲区大小。假设我们要将发送缓冲区设置为 64KB,接收缓冲区设置为 128KB(注意:实际值受系统限制)。 ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字 if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 设置发送缓冲区大小(64KB) int send_buf_size = 65536; // 64 * 1024 if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &send_buf_size, sizeof(send_buf_size)) < 0) { perror("setsockopt SO_SNDBUF failed"); close(sockfd); exit(EXIT_FAILURE); } // 设置接收缓冲区大小(128KB) int recv_buf_size = 131072; // 128 * 1024 if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buf_size, sizeof(recv_buf_size)) < 0) { perror("setsockopt SO_RCVBUF failed"); close(sockfd); exit(EXIT_FAILURE); } // 验证设置(可选,使用 getsockopt 检查实际值) int actual_send_size; socklen_t len = sizeof(actual_send_size); if (getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &actual_send_size, &len) < 0) { perror("getsockopt failed"); } else { printf("Actual send buffer size set: %d bytes\n", actual_send_size); } // 后续操作:bind, listen/connect 等(必须在 setsockopt 之后) // 示例:绑定到端口(此处省略具体实现) struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind failed"); close(sockfd); exit(EXIT_FAILURE); } // 如果是服务器,调用 listen;如果是客户端,调用 connect // listen(sockfd, 5); 或 connect(sockfd, ...); close(sockfd); // 关闭套接字 return 0; } ``` #### 3. **注意事项** - **系统级限制**:`setsockopt` 设置的缓冲区大小不能超过系统全局最大值。这些值通过 `/proc` 文件系统定义: - 发送缓冲区最大大小:`/proc/sys/net/core/wmem_max` - 接收缓冲区最大大小:`/proc/sys/net/core/rmem_max` - 默认值:`/proc/sys/net/core/wmem_default` `/proc/sys/net/core/rmem_default` 你可以通过 shell 命令修改这些值(例如 `echo 262144 > /proc/sys/net/core/wmem_max`),但需 root 权限。程序中的设置值若超过这些限制,会被内核自动截断到最大值[^2]。 - **自动调节机制**:Linux 内核默认使用 TCP 自动调节机制(如 BBR 或 CUBIC)来优化缓冲区大小。手动设置缓冲区会禁用此机制,可能导致性能下降。因此,仅在特定场景(如高延迟网络)下推荐手动设置[^2]。 - **错误处理**:总是检查 `setsockopt` 的返回值 `errno`。常见错误包括: - `EINVAL`:无效参数(如缓冲区大小过大)。 - `ENOBUFS`:系统资源不足。 - **性能影响**:增大缓冲区可提升高吞吐量应用的性能(如视频流),但可能增加内存开销。建议通过测试(如使用 `iperf`)优化大小。 - **获取当前大小**:如果需要查询当前缓冲区大小,使用 `getsockopt` 函数(如代码示例中所示),这与引用[3]中描述的方法一致[^3]。 #### 4. **相关引用** - 设置缓冲区大小必须先于 `listen` 或 `connect` 调用,否则内核可能忽略设置[^2]。 - 在 TCP 协议中,发送缓冲区通过 `send` 函数操作,但缓冲区大小设置独立于发送逻辑[^1]。 通过以上方法,你可以灵活控制 TCP 缓冲行为。如果遇到系统限制,建议先检查 `/proc` 文件值并适当调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值