《日经亚洲》报道,来自八个国家14个学术机构的研究人员被发现在论文中隐藏提示,以操纵AI审稿人给出正面评价,引发了关于学术诚信和AI在同行评审中作用的激烈辩论。
Go|如何判断两个IP是否在同一个网段
本文字数: 2.6k 阅读时长 ≈ 2 分钟
介绍如何判断两个IP地址是否属于同一个网段,解释其底层原理,并提供Go语言的实现代码。
MySQL|为什么同样的sql多个where条件,排序就不一样了
下面的sql查询同一张表的数据,差异仅在where条件不同,但是同样是active =true的记录在返回的结果中,排序顺序不同,这是为什么
1 | SELECT id,name FROM `tag` WHERE corp_id =4 ORDER BY active DESC LIMIT 10; |
这两条SQL语句返回的数据顺序不一致的原因在于第一条SQL语句中 active
列可能存在除了 true
(或 1) 以外的其他值,比如 false
(或 0) 或者 NULL
。
让我们更详细地分析一下:
SQL 1:
1 | SELECT id, name FROM `tag` WHERE corp_id = 4 ORDER BY active DESC LIMIT 10; |
- WHERE
corp_id
= 4: 首先过滤出corp_id
为 4 的所有行。 - ORDER BY
active
DESC: 然后按照active
列降序排序。 这意味着:true
(或 1) 的行会排在前面。false
(或 0) 的行会排在true
之后。NULL
的行会排在false
之前,但具体位置取决于数据库实现。 MySQL会默认把 NULL 值放在最前面
- LIMIT 10: 最后只返回排序后的前10行。
SQL 2:
1 | SELECT id, name FROM `tag` WHERE (corp_id = 4) AND active = true ORDER BY active DESC LIMIT 10; |
- WHERE
corp_id
= 4 ANDactive
= true: 首先过滤出corp_id
为 4 并且active
为true
的所有行。 - ORDER BY
active
DESC: 然后按照active
列降序排序。 由于WHERE
子句已经确保了所有行的active
都是true
,所以这个排序实际上是多余的,因为所有行的active
值都相同。 - LIMIT 10: 最后只返回排序后的前10行。
关键区别在于WHERE子句的过滤条件:
- 第一条SQL语句包含了所有
corp_id = 4
的行,然后根据active
进行排序,NULL值可能在最前面。 - 第二条SQL语句只包含了
corp_id = 4
并且active = true
的行。 排序没有实际作用。
举例说明:
假设你的 tag
表有以下数据(简化):
id | name | corp_id | active |
---|---|---|---|
1 | A | 4 | true |
2 | B | 4 | false |
3 | C | 4 | NULL |
4 | D | 4 | true |
5 | E | 4 | false |
6 | F | 4 | NULL |
7 | G | 4 | true |
8 | H | 4 | false |
9 | I | 4 | NULL |
10 | J | 4 | true |
11 | K | 4 | false |
SQL 1 的结果 (可能):
假设 MySQL 把 NULL 放在最前面排序
id = 3, name = C
(active = NULL)id = 6, name = F
(active = NULL)id = 9, name = I
(active = NULL)id = 1, name = A
(active = true)id = 4, name = D
(active = true)id = 7, name = G
(active = true)id = 10, name = J
(active = true)id = 2, name = B
(active = false)id = 5, name = E
(active = false)id = 8, name = H
(active = false)
SQL 2 的结果:
id = 1, name = A
(active = true)id = 4, name = D
(active = true)id = 7, name = G
(active = true)id = 10, name = J
(active = true)
… 以及其他active = true
的行,直到达到10行。 因为排序没意义,结果具有不确定性,取决于数据库内部的行存储顺序。
总结:
- 如果
active
列只包含true
和false
,并且没有 NULL 值,那么这两条SQL语句的结果在id
和name
上可能相同,但在结果排序上可能不同(第一条语句明确排序了,第二条排序实质上无效)。 - 如果
active
列包含NULL
值,这是最可能导致结果不同的原因,因为第一条语句会受到NULL值排序的影响。
如何解决:
如果你的意图是只获取 active
为 true
的记录,那么第二条SQL语句是正确的选择。
如果你想要获取所有 corp_id = 4
的记录,并让 active
为 true
的记录排在最前面,可以使用以下SQL语句(假设你想把 NULL 值放在最后):
1 | SELECT id, name |
或者,更简单的使用 IFNULL
函数(MySQL 特性):
1 | SELECT id, name |
这条语句将 NULL
视为 0
(false),从而将 true
的行放在最前面。 如果你想将 NULL 视为 True, 将 0 改为 1 即可.
或者,增加排序字段,用id和active同时排序:
1 | SELECT id,name |
选择哪种方法取决于你想要如何处理 active
列中的 NULL
值。 最重要的是理解你的数据和你想达到的结果,然后选择正确的SQL语句。
ES|PUT修改索引新增字段时,如果添加重复的会怎么样
本文字数: 1k 阅读时长 ≈ 1 分钟
在 Elasticsearch 中,向已有索引的 mapping 里新增字段时,如果你尝试添加一个已经存在的字段(即字段名重复),会出现以下情况:
不能修改已存在字段的类型:Elasticsearch 不允许修改已存在字段的类型或映射配置。如果你试图用不同的类型或属性重新定义已存在字段,操作会失败并报错,因为字段映射一旦确定,不能更改[3][5]。
如果新增字段名和已有字段完全一致且映射相同,则相当于“重复添加”,这通常不会有实际影响,但也不会做任何修改,mapping 保持不变。
如果新增字段名重复但映射不同,Elasticsearch 会拒绝更新 mapping,返回错误提示,防止数据索引混乱[3][5]。
新增字段时,必须保证字段名唯一且映射合理,否则需要新建索引并通过 Reindex API 迁移数据来实现字段类型变更[3][5]。
总结:
操作场景 | 结果说明 |
---|---|
新增字段名不存在 | 成功添加字段到 mapping |
新增字段名已存在且映射相同 | 无变化,mapping 不会重复添加 |
新增字段名已存在但映射不同 | 报错,更新失败,不能修改字段类型 |
因此,新增字段时如果字段名重复且映射不同,ES 会拒绝更新 mapping 并报错,你需要通过新建索引和重新索引数据来变更字段类型。
这是 Elasticsearch 设计的限制,保证倒排索引结构的稳定性和数据一致性[3][5]。
[1] https://codeshellme.github.io/2021/02/es-mappings/
[2] https://blog.csdn.net/weixin_48990070/article/details/120342866
[3] http://masikkk.com/article/Elasticsearch-Mapping/
[4] http://www.zbpblog.com/blog-458.html
[5] https://www.cnblogs.com/wupeixuan/p/12514843.html
[6] https://www.cnblogs.com/shoufeng/p/10648835.html
[7] https://blog.csdn.net/yxd179/article/details/82907796
[8] https://scsundefined.gitbooks.io/elasticsearch-reference-cn/content/s12/00_mapping.html
ES|索引中text和keyword的区别
本文字数: 2.3k 阅读时长 ≈ 2 分钟
Elasticsearch 中 text
和 keyword
是两种常用的字符串字段类型,它们的主要区别在于是否进行分词,进而影响索引和查询行为。
1. text
与 keyword
的区别
特性 | text |
keyword |
---|---|---|
是否分词 | 会分词,进行全文分析 | 不分词,整体作为一个词项索引 |
适用场景 | 需要全文检索、模糊查询、相关度排序 | 需要精确匹配、过滤、排序、聚合 |
支持的查询类型 | match 、match_phrase 等全文查询 |
term 、terms 精确查询 |
支持聚合/排序 | 不支持(性能差且不合理) | 支持 |
存储限制 | 无字符长度限制 | 默认最大长度256字符,超过不索引(可配置) |
典型用途 | 文章内容、评论、描述等长文本 | 用户名、邮箱、标签、状态、ID等 |
2. 使用案例
2.1 Mapping 示例(含 multi-fields)
通常为了兼顾全文检索和精确匹配,字段会定义成 text
类型,同时添加一个 keyword
子字段:
1 | PUT my_index |
这样,title
字段既可以全文检索,也可以做精确匹配和聚合。
2.2 查询示例
- 全文检索(text 字段)
1 | GET my_index/_search |
- 精确匹配(keyword 字段)
1 | GET my_index/_search |
- 聚合示例(keyword 字段)
1 | GET my_index/_search |
2.3 修改 Mapping
Elasticsearch 不支持直接修改已有字段的类型。如果想给已有索引新增 keyword
子字段,需要使用 动态模板或在创建索引时定义好,或者新建索引并重建数据。
示例:新增字段时定义 multi-fields
1 | PUT my_index/_mapping |
如果字段已存在且类型不同,修改会失败,需要新建索引。
3. 总结
text
适合全文检索,支持分词和相关度评分,不能用于聚合和排序。keyword
适合精确匹配、过滤、排序和聚合,不分词。- 多数字符串字段建议用
text
+keyword
多字段映射,兼顾两种需求。 - 查询时全文检索用
match
查询text
字段,精确匹配用term
查询keyword
字段。
以上内容基于 Elasticsearch 官方设计理念及社区实践总结[1][2][3][4][6]。如果需要,我可以帮你写具体的 mapping 和查询模板。
[1] https://www.cnblogs.com/hahaha111122222/p/12177377.html
[2] https://blog.csdn.net/UbuntuTouch/article/details/128904528
[3] https://cloud.tencent.com/developer/article/2357713
[4] https://bbs.huaweicloud.com/blogs/410730
[5] https://www.cnblogs.com/Rawls/p/10069670.html
[6] https://blog.csdn.net/weixin_41860630/article/details/126471632
[7] https://blog.51cto.com/u_15730090/5510216
[8] https://blog.51cto.com/u_15278282/2933670
ES|常用字段类型
本文字数: 2.1k 阅读时长 ≈ 2 分钟
Elasticsearch 中字段类型繁多,合理选择字段类型对索引效率、查询性能和存储空间都有重要影响。以下是常用字段类型的全面介绍及区别解析,结合核心、复合、地理和特殊类型,帮助你理解它们的作用和应用场景。
1. 核心数据类型
类型 | 说明 | 适用场景与特点 |
---|---|---|
text | 用于全文检索的字符串字段,会经过分词器拆分成词项并建立倒排索引。 | 适合长文本、描述、文章内容等需要模糊匹配、分词查询的场景。不支持排序和精确聚合。 |
keyword | 不分词的字符串字段,整体作为一个词项索引。 | 适合存储结构化短文本,如用户名、邮箱、标签、状态码等。支持精确匹配、过滤、排序和聚合。 |
long | 64位整数 | 存储时间戳、ID、计数等大范围整数。支持范围查询、排序、聚合。 |
integer | 32位整数 | 存储较小范围的整数,如数量、等级等。支持范围查询、排序、聚合。 |
float/double | 单精度/双精度浮点数 | 存储金额、权重、评分等带小数的数值。支持范围查询、排序、聚合。 |
boolean | 布尔值,true 或 false | 存储二元状态,如开关、是否激活等。支持过滤和聚合。 |
date | 日期时间类型 | 存储日期时间,支持多种格式。方便时间范围查询、排序和时间聚合。 |
binary | 二进制数据,不支持检索和聚合 | 存储图片、文件等二进制内容,主要用于存储,不用于搜索。 |
2. 复合数据类型
类型 | 说明 | 适用场景与特点 |
---|---|---|
object | JSON 对象,包含多个字段 | 存储结构化数据,字段之间无独立索引,数组中对象匹配可能出现跨对象匹配问题。 |
nested | 嵌套对象数组,每个对象独立索引 | 解决数组中对象字段交叉匹配问题,适合复杂数组结构,支持嵌套查询。 |
array | Elasticsearch 不单独定义数组类型,字段可直接存储数组值 | 支持存储同类型多个值,数组中元素类型由字段类型决定。 |
3. 地理空间类型
类型 | 说明 | 适用场景与特点 |
---|---|---|
geo_point | 经纬度坐标点 | 存储地理位置点,支持基于距离的查询和排序。 |
geo_shape | 复杂地理形状,如多边形、线等 | 适合存储区域边界、路径等复杂地理信息,支持空间关系查询。 |
4. 特殊类型
类型 | 说明 | 适用场景与特点 |
---|---|---|
ip | IPv4 或 IPv6 地址 | 存储IP地址,支持范围查询。 |
completion | 自动补全建议字段 | 用于实现搜索自动补全功能。 |
token_count | 统计字符串中词条数量 | 用于分析文本长度或复杂度。 |
murmur3 | 哈希值字段 | 用于快速哈希计算和索引。 |
percolator | 存储查询以便反向匹配文档 | 实现基于查询的索引反向匹配。 |
5. 字符串类型的区别详解
类型 | 分词情况 | 支持排序/聚合 | 适用查询类型 | 典型用途 |
---|---|---|---|---|
text | 分词 | 不支持排序 | match、全文检索、模糊查询 | 文章内容、评论、描述等长文本 |
keyword | 不分词 | 支持排序/聚合 | term、精确匹配、过滤 | 用户名、标签、状态、ID等 |
Elasticsearch 5.x 以后,
string
类型被拆分为text
和keyword
,分别满足全文检索和精确匹配需求[2][3][5]。
6. 数值类型的选择
根据数值大小和精度选择合适类型:
类型 | 说明 | 典型范围/用途 |
---|---|---|
byte | 8位整数 | -128 到 127 |
short | 16位整数 | -32,768 到 32,767 |
integer | 32位整数 | -2^31 到 2^31-1 |
long | 64位整数 | 大整数,如时间戳、ID |
float | 单精度浮点数 | 金额、评分等带小数数据 |
double | 双精度浮点数 | 高精度小数 |
scaled_float | 通过缩放因子存储浮点数 | 节省存储空间,适合定点数存储 |
总结
- 全文检索用
text
,结构化精确匹配用keyword
。 - 数值类型根据范围和精度选择,保证存储和查询效率。
- 日期类型专门处理时间,支持多种格式和时间操作。
- 复杂结构用
object
或nested
,后者支持嵌套查询,避免字段跨对象匹配错误。 - 地理数据和特殊需求有专门类型支持,满足多样化业务场景。
合理选择字段类型,是 Elasticsearch 索引设计的关键,直接影响查询性能和存储效率[1][2][3][4][5]。
[1] https://cloud.tencent.com/developer/article/2357713
[2] https://xiaoxiami.gitbook.io/elasticsearch/ji-chu/mapping/zi-duan-de-shu-ju-lei-xing
[3] https://www.cnblogs.com/tanghaorong/p/16323253.html
[4] https://cloud.tencent.com/developer/article/2260312
[5] https://developer.aliyun.com/article/969878
[6] https://blog.csdn.net/aben_sky/article/details/121515175
[7] https://www.cnblogs.com/shoufeng/p/10692113.html
[8] https://blog.csdn.net/ZYC88888/article/details/83059040
[9] https://developer.aliyun.com/article/707773
mysql| enum 字段不要加索引( status 、type...)是正确的吗,为什么
这是一个非常经典并且常常被误解的数据库优化问题。
简短的回答是:这个说法不完全正确。 更准确地说,它是一个需要根据具体情况分析的优化建议,而不是一个绝对的规则。
下面我将详细解释“为什么会有这个说法”以及“什么时候应该或不应该加索引”。
为什么会有“不要给ENUM/低基数字段加索引”的说法?
这个建议的核心在于一个概念:索引的基数(Cardinality)。
- 基数:指一个列中不重复值的数量。
- 高基数列:列中的值几乎都是唯一的,比如
user_id
,email
。 - 低基数列:列中只有很少几个固定的、重复值,比如
status
(‘pending’, ‘approved’, ‘rejected’),gender
(‘male’, ‘female’, ‘other’),is_active
(true, false)。ENUM 和 BOOLEAN 类型天生就是低基数的。
低基数列作为索引的主要问题是“筛选效率低下”:
索引选择性差 (Poor Selectivity)
数据库优化器在决定是否使用索引时,会评估其“选择性”。一个好的索引应该能帮你快速排除掉大部分不符合条件的数据。举个例子:
假设一个orders
表有1000万条记录,其中status
列有3个值:’processing’, ‘shipped’, ‘completed’。- 如果你执行
SELECT * FROM orders WHERE status = 'shipped';
- 数据库会估算,
status = 'shipped'
的记录可能占了总数的三分之一,也就是大约333万条。 - 在这种情况下,优化器很可能会认为:通过索引找到这333万条记录的地址,然后再逐一回表(回到主表去读取完整的行数据),这个过程的随机I/O成本可能比直接全表扫描(从头到尾读一遍表)还要高。
- 因此,即使你建了索引,数据库也可能放弃使用它,导致索引白建了。
- 如果你执行
索引维护成本 (Maintenance Overhead)
索引不是免费的。每次对表进行INSERT
,UPDATE
,DELETE
操作时,如果动到了索引列,数据库也需要同步更新索引本身。对于一个写操作频繁的表,一个几乎用不上的低效索引反而会拖慢整体的写入性能。
结论: 当一个索引不能有效地区分数据时,它的存在就弊大于利。这就是“不要给低基数列加索引”这个建议的根本原因。
那么,什么时候应该给ENUM/低基数列加索引?
虽然低基数索引通常是低效的,但在以下几种情况下,给它们建立索引却是非常明智和高效的:
数据分布极不均衡 (Skewed Data Distribution)
这是最常见也最重要的例外情况。虽然列的基数很低,但如果某个值的出现频率极低,为它建索引就非常有用。举例:
一个users
表有1亿用户,有一个is_banned
(true/false) 字段。- 99.99% 的用户
is_banned
是false
。 - 只有 0.01% 的用户
is_banned
是true
(被封禁的用户)。 - 此时,
SELECT * FROM users WHERE is_banned = true;
这个查询的选择性就极高。索引可以快速定位到那一小部分被封禁的用户,性能会远超全表扫描。
- 99.99% 的用户
作为覆盖索引 (Covering Index) 的一部分
当一个查询所需的所有列都包含在索引中时,数据库引擎就无需回表,可以直接从索引中获取所有数据返回。这被称为“覆盖索引”。举例:
你想统计每种订单状态的数量:SELECT status, COUNT(*) FROM orders GROUP BY status;
- 如果你只在
status
列上建立一个索引INDEX(status)
。 - 数据库可以只扫描这个小得多的
status
索引,而完全不需要触碰庞大的主表数据,就能完成统计。这会带来巨大的性能提升。
- 如果你只在
作为复合索引 (Composite Index) 的一部分
当低基数列是复合索引的非前导列时,它通常是非常有用的。举例:
你的查询条件通常是WHERE corp_id = ? AND status = ?
。corp_id
是一个高基数列(有很多公司)。status
是一个低基数列。- 建立一个复合索引
INDEX(corp_id, status)
是非常高效的。 - 查询时,索引会首先通过
corp_id
快速筛选出很少一部分数据,然后再在这个小范围内根据status
进行查找。此时status
的低基数问题就不再是瓶颈。
总结与建议
场景 | 是否建议加索引 | 原因 |
---|---|---|
单独查询,且数据均匀分布 | 不建议 | 索引选择性差,优化器可能放弃使用,且有维护成本。 |
单独查询,但数据分布极不均衡 | 强烈建议 | 对稀有值的查询效率极高。 |
查询只涉及该列和少数几列 | 建议 | 可以创建覆盖索引,避免回表,极大提升性能。 |
作为复合索引的一部分,与其他高选择性列组合 | 强烈建议 | 复合索引可以大大提高多条件查询的效率。 |
实践法则:
- 不要盲从规则:先分析你的业务场景和查询模式。
- 检查数据分布:使用
SELECT status, COUNT(*) FROM your_table GROUP BY status;
来了解值的分布情况。 - 使用
EXPLAIN
:在你认为需要加索引的查询前加上EXPLAIN
,看看数据库的执行计划。确认它是否会使用你创建的索引(key
列),以及扫描的行数(rows
列)是否显著减少。 - 优先考虑复合索引和覆盖索引:在设计索引时,优先考虑如何让索引服务于一类查询,而不仅仅是单个列。
总而言之,“不要给ENUM/低基数列加索引”是一个有用的“经验法则”,它提醒我们警惕低基数列的索引陷阱。但它绝不是一条必须遵守的铁律。 真正的数据库优化需要结合具体业务场景、查询模式和数据分布来进行综合判断。
mysql| MySQL表要加业务唯一性约束,这对吗
MySQL表添加业务唯一性约束(Unique Constraint)通常是正确且非常推荐的做法。
这不仅仅是数据库层面的一个设置,更是保证业务逻辑严谨性和数据完整性的关键手段。
为什么强烈推荐这样做 (好处)
- 保证数据完整性 (Data Integrity)
- 这是最主要的原因。通过数据库约束,可以从根本上杜绝脏数据的产生。例如,可以防止两个用户使用同一个邮箱注册,或者系统中出现两个
编码完全相同的商品。
- 这是最主要的原因。通过数据库约束,可以从根本上杜绝脏数据的产生。例如,可以防止两个用户使用同一个邮箱注册,或者系统中出现两个
- 简化应用层逻辑 (Simplifies Application Logic)
- 如果没有唯一性约束,你需要在应用代码中实现“先查询是否存在,再插入/更新”的逻辑。这种逻辑在并发场景下很容易出错(即“竞态条件”,
Race Condition),可能导致在极短的时间差内,两个请求都查询到数据不存在,然后都成功插入,最终还是产生了重复数据。 - 有了数据库约束,应用层可以大胆地直接执行 INSERT 或 UPDATE,然后捕获并处理可能抛出的“唯一键冲突”异常。这种方式更简单、更可靠。
- 如果没有唯一性约束,你需要在应用代码中实现“先查询是否存在,再插入/更新”的逻辑。这种逻辑在并发场景下很容易出错(即“竞态条件”,
- 提升查询性能 (Improves Query Performance)
- 当你添加一个 UNIQUE 约束时,MySQL 会自动为其创建一个唯一索引(Unique Index)。
- 当你的查询条件(WHERE 子句)涉及到这个唯一性字段时,数据库可以利用这个索引进行极速查找,性能远高于没有索引的全表扫描。
- 明确业务规则 (Clarifies Business Rules)
- 数据库的表结构(Schema)本身就是一种文档。当其他开发者看到这个表有一个唯一性约束时,他们能立刻理解这个字段(或字段组合)在业
务上是不允许重复的,这使得代码更易于维护。
- 数据库的表结构(Schema)本身就是一种文档。当其他开发者看到这个表有一个唯一性约束时,他们能立刻理解这个字段(或字段组合)在业
需要注意的点 (Considerations)
- 对
NULL
值的处理- 在MySQL中,UNIQUE 约束的列可以包含多个
NULL
值。因为在索引层面,NULL 被认为是一个不确定的值,NULL 不等于任何值,包括另一个NULL。如果你希望某个字段要么唯一,要么为空,那么 UNIQUE 约束是合适的。
- 在MySQL中,UNIQUE 约束的列可以包含多个
- 写入性能开销
- 每次 INSERT 或 UPDATE 唯一键字段时,数据库都需要检查索引以确保其唯一性,这会带来微小的性能开销。但在绝大多数场景下,这点开销与它带来的数据完整性、查询性能提升相比,是完全值得的。只有在写入极其密集(例如,每秒数十万次)的特定场景下,才需要特殊考量。
- 联合唯一约束 (Composite Unique Constraint)
- 你可以对多个列组合起来设置唯一性约束。例如,一个用户在同一个社区内只能对一篇文章点赞一次,你可以对 (user_id, post_id)
这两个字段组合建立唯一约束。
- 你可以对多个列组合起来设置唯一性约束。例如,一个用户在同一个社区内只能对一篇文章点赞一次,你可以对 (user_id, post_id)
总结
结论是:应该加。在设计表结构时,应该主动思考哪些字段或字段组合是业务上的“天然唯一标识”,并为它们添加 UNIQUE 约束。
最佳实践
- 核心业务标识:如用户邮箱、手机号、订单号、商品SKU等,必须添加唯一约束。
- 并发安全:依赖数据库约束来保证唯一性,而不是在应用层做“查-写”操作。
- 错误处理:应用层代码必须准备好捕获和优雅地处理唯一键冲突的错误,并向用户返回友好的提示(例如:“该邮箱已被注册”)。
redis|为什么大 key 应该用二级缓存优化
简单来说,为 Redis 大 Key(Big Key)增加一个二级缓存(通常是本地进程内缓存),核心目标是用内存换取CPU和网络资源,以数量级的方式减少对 Redis 的访问压力,并避免大 Key 带来的潜在风险。
下面我们来详细拆解这个问题。
首先,为什么 Redis 大 Key 是个问题?
“大 Key” 通常指一个 Key 对应的 Value 非常大(例如超过 100KB),或者一个 Key 包含的元素非常多(例如一个有数百万成员的 Hash 或 ZSET)。
大 Key 会带来一系列严重问题:
- 网络IO瓶颈:一次 GET 一个 5MB 的大 Key,就会在网卡上产生 5MB 的流量。在高并发下,这会迅速占满服务器带宽,导致网络延迟剧增,影响所有其他服务。
- Redis 阻塞风险:Redis 的命令处理是单线程的。当 Redis 处理一个大 Key 时(例如对其进行序列化、反序列化、网络传输),会消耗更长的时间。在这期间,其他所有请求都必须排队等待,导致 Redis 的 QPS (每秒查询率)急剧下降,甚至出现服务“卡死”的现象。特别是删除一个大 Key (DEL a_big_key),可能会造成秒级的阻塞。
- 内存分配不均:在 Redis Cluster 模式下,一个大 Key 会导致某个特定节点的内存使用量远超其他节点,造成数据倾斜和资源分配不均,难以扩容。
- 缓存淘汰效率低:当内存不足需要淘汰数据时,如果淘汰了一个大 Key,会瞬间释放大量内存,但下一次请求这个 Key时,又会造成一次成本极高的“缓存穿透”,直接打到数据库上。
什么是二级缓存(本地缓存)?
在这里,“二级缓存”特指位于应用程序进程内部的缓存(In-Process Cache)。
一级缓存:远程的分布式缓存,如 Redis、Memcached。
二级缓存:本地内存中的缓存,如 Java 的 Caffeine/Guava Cache,Go 的 sync.Map 或 freecache,Python 的 functools.lru_cache 或 dict。
这个二级缓存的特点是:
速度极快:直接在内存中访问,没有任何网络开销,速度是访问 Redis 的百倍甚至千倍。
容量有限:受限于应用服务器的内存,容量远小于 Redis。
为什么用二级缓存优化大 Key 效果拔群?
现在,我们将大 Key 的问题和二级缓存的优势结合起来看,答案就显而易见了。
假设我们有一个 5MB 的大 Key,它存储了某个不常变化但读取频繁的配置信息。
优化前的工作流程:
- 客户端请求数据。
- 应用服务向 Redis 发送 GET big_key 命令。
- Redis 读取 5MB 数据,通过网络发送给应用服务。
- 应用服务接收 5MB 数据,处理后返回给客户端。
- 问题:每次请求都要消耗 Redis 的 CPU 和大量的网络带宽。
优化后的工作流程:
- 客户端请求数据。
- 应用服务首先检查本地缓存中是否存在 big_key。
- 缓存命中 (Hit):
- 直接从本地内存中读取数据(速度极快,无网络IO)。
- 处理后返回给客户端。
- 效果:整个过程完全不涉及 Redis,Redis 和网络带宽的压力被彻底消除。
- 缓存未命中 (Miss):
- 本地缓存没有数据,此时才向 Redis 发送 GET big_key 命令。
- Redis 读取 5MB 数据,通过网络发送给应用服务。
- 应用服务接收到数据后,先将数据写入本地缓存(并设置一个较短的过期时间,如1分钟)。
- 处理后返回给客户端。
- 效果:只有在第一次请求或本地缓存过期后的第一次请求,才会访问 Redis。后续大量请求都由本地缓存响应。
优势:
- 极大降低网络开销:避免了每次请求都传输大 Key 的数据。只有在缓存失效时才会有一次网络传输。
- 极大降低 Redis 负载:绝大多数读请求被本地缓存拦截,Redis 不再需要为这些请求消耗 CPU 进行数据查找和序列化。
- 避免 Redis 阻塞:由于访问 Redis 的频率大大降低,由大 Key 引起的阻塞风险也随之降低。
- 提升应用性能和响应速度:从本地内存读取数据的速度远快于远程读取,用户感受到的延迟更低。
关键的注意事项:数据一致性
引入二级缓存带来的最大挑战是数据一致性。当大 Key 的源数据在数据库中被更新后,你需要一种机制来同时或先后地让 Redis 和所有应用服务器的本地缓存都失效。
- Redis 中的数据如何更新?
- 通常采用“缓存旁路模式”(Cache-Aside Pattern):更新数据库后,直接 DEL Redis 中的 Key。下次请求时会重新从数据库加载并写入 Redis。
- 所有服务器的本地缓存如何更新?
- 这是一个难题,因为一个应用通常部署在多个服务器上。
- 常用解决方案:使用消息队列(如 RabbitMQ, Kafka, Redis Pub/Sub)。
- 当数据在数据库中被更新时。
- 发送一个“缓存失效”消息到消息队列的特定主题(Topic)。
- 所有订阅了该主题的应用服务实例都会收到这个消息。
- 收到消息后,每个应用服务实例都删除自己本地缓存中的对应 Key。
总结
为大 Key 添加二级(本地)缓存是一种典型的空间换时间和分级缓存思想的应用。它通过在应用端牺牲一小部分内存,来保护下游更宝贵、更容易成为瓶颈的分布式缓存和网络资源。
这种模式尤其适用于“读多写少”的大 Key 场景,例如:
- 商品详情页的完整信息。
- 系统的基础配置。
- 一个热点新闻的完整内容。
对于频繁更新的数据,使用二级缓存需要非常谨慎地处理一致性问题,成本会更高。
转载 | [官方优惠] 免费兑换一年 jetbrain all products,IDEA,GoLand,Clion,PyCharm
本文字数: 356 阅读时长 ≈ 1 分钟
Have you received a 100% off promo code for a complimentary subscription? Complete a short form below to redeem your coupon. Once your order is confirmed, you will receive a confirmation email and further instructions on how to activate your Product.
兑换码:DataGrip2025
兑换地址: https://www.jetbrains.com/store/redeem/
如果已经有订阅了会续订一年
兑换码已失效,兑换完毕,额度用完(最后兑换时间 2025-07-01 12:00 左右)