编程笔记

lifelong learning & practice makes perfect

《日经亚洲》报道,来自八个国家14个学术机构的研究人员被发现在论文中隐藏提示,以操纵AI审稿人给出正面评价,引发了关于学术诚信和AI在同行评审中作用的激烈辩论。

阅读全文 »

下面的sql查询同一张表的数据,差异仅在where条件不同,但是同样是active =true的记录在返回的结果中,排序顺序不同,这是为什么

1
2
3
SELECT id,name FROM `tag` WHERE corp_id =4 ORDER BY active DESC LIMIT 10;

SELECT id,name FROM `tag` WHERE (corp_id =4) AND active =true 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 AND active = true: 首先过滤出 corp_id 为 4 并且 activetrue 的所有行。
  • 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 放在最前面排序

  1. id = 3, name = C (active = NULL)
  2. id = 6, name = F (active = NULL)
  3. id = 9, name = I (active = NULL)
  4. id = 1, name = A (active = true)
  5. id = 4, name = D (active = true)
  6. id = 7, name = G (active = true)
  7. id = 10, name = J (active = true)
  8. id = 2, name = B (active = false)
  9. id = 5, name = E (active = false)
  10. id = 8, name = H (active = false)

SQL 2 的结果:

  1. id = 1, name = A (active = true)
  2. id = 4, name = D (active = true)
  3. id = 7, name = G (active = true)
  4. id = 10, name = J (active = true)
    … 以及其他 active = true 的行,直到达到10行。 因为排序没意义,结果具有不确定性,取决于数据库内部的行存储顺序。

总结:

  • 如果 active 列只包含 truefalse,并且没有 NULL 值,那么这两条SQL语句的结果在 idname 上可能相同,但在结果排序上可能不同(第一条语句明确排序了,第二条排序实质上无效)。
  • 如果 active 列包含 NULL 值,这是最可能导致结果不同的原因,因为第一条语句会受到NULL值排序的影响。

如何解决:

如果你的意图是只获取 activetrue 的记录,那么第二条SQL语句是正确的选择。

如果你想要获取所有 corp_id = 4 的记录,并让 activetrue 的记录排在最前面,可以使用以下SQL语句(假设你想把 NULL 值放在最后):

1
2
3
4
5
6
7
8
9
10
SELECT id, name
FROM `tag`
WHERE corp_id = 4
ORDER BY
CASE
WHEN active IS NULL THEN 1 -- 把 NULL 放在最后
ELSE 0 -- 把 true 和 false 放在前面
END,
active DESC -- 然后按照 true > false 排序
LIMIT 10;

或者,更简单的使用 IFNULL 函数(MySQL 特性):

1
2
3
4
5
SELECT id, name
FROM `tag`
WHERE corp_id = 4
ORDER BY IFNULL(active, 0) DESC
LIMIT 10;

这条语句将 NULL 视为 0 (false),从而将 true 的行放在最前面。 如果你想将 NULL 视为 True, 将 0 改为 1 即可.

或者,增加排序字段,用id和active同时排序:

1
2
3
4
5
6
7
8
SELECT id,name 
FROM `tag`
WHERE corp_id =4
ORDER BY id,active DESC LIMIT 10;

SELECT id,name
FROM `tag` WHERE (corp_id =4) AND active =true
ORDER BY id,active DESC LIMIT 10;

选择哪种方法取决于你想要如何处理 active 列中的 NULL 值。 最重要的是理解你的数据和你想达到的结果,然后选择正确的SQL语句。

在 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

Elasticsearch 中 textkeyword 是两种常用的字符串字段类型,它们的主要区别在于是否进行分词,进而影响索引和查询行为。

1. textkeyword 的区别

特性 text keyword
是否分词 会分词,进行全文分析 不分词,整体作为一个词项索引
适用场景 需要全文检索、模糊查询、相关度排序 需要精确匹配、过滤、排序、聚合
支持的查询类型 matchmatch_phrase 等全文查询 termterms 精确查询
支持聚合/排序 不支持(性能差且不合理) 支持
存储限制 无字符长度限制 默认最大长度256字符,超过不索引(可配置)
典型用途 文章内容、评论、描述等长文本 用户名、邮箱、标签、状态、ID等

2. 使用案例

2.1 Mapping 示例(含 multi-fields)

通常为了兼顾全文检索和精确匹配,字段会定义成 text 类型,同时添加一个 keyword 子字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}

这样,title 字段既可以全文检索,也可以做精确匹配和聚合。

2.2 查询示例

  • 全文检索(text 字段)
1
2
3
4
5
6
7
8
GET my_index/_search
{
"query": {
"match": {
"title": "Elasticsearch tutorial"
}
}
}
  • 精确匹配(keyword 字段)
1
2
3
4
5
6
7
8
GET my_index/_search
{
"query": {
"term": {
"title.keyword": "Elasticsearch tutorial"
}
}
}
  • 聚合示例(keyword 字段)
1
2
3
4
5
6
7
8
9
10
11
GET my_index/_search
{
"size": 0,
"aggs": {
"titles": {
"terms": {
"field": "title.keyword"
}
}
}
}

2.3 修改 Mapping

Elasticsearch 不支持直接修改已有字段的类型。如果想给已有索引新增 keyword 子字段,需要使用 动态模板或在创建索引时定义好,或者新建索引并重建数据。

示例:新增字段时定义 multi-fields

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT my_index/_mapping
{
"properties": {
"new_field": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}

如果字段已存在且类型不同,修改会失败,需要新建索引。

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

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 类型被拆分为 textkeyword,分别满足全文检索和精确匹配需求[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
  • 数值类型根据范围和精度选择,保证存储和查询效率
  • 日期类型专门处理时间,支持多种格式和时间操作
  • 复杂结构用 objectnested,后者支持嵌套查询,避免字段跨对象匹配错误
  • 地理数据和特殊需求有专门类型支持,满足多样化业务场景

合理选择字段类型,是 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

这是一个非常经典并且常常被误解的数据库优化问题。

简短的回答是:这个说法不完全正确。 更准确地说,它是一个需要根据具体情况分析的优化建议,而不是一个绝对的规则。

下面我将详细解释“为什么会有这个说法”以及“什么时候应该或不应该加索引”。


为什么会有“不要给ENUM/低基数字段加索引”的说法?

这个建议的核心在于一个概念:索引的基数(Cardinality)

  • 基数:指一个列中不重复值的数量。
  • 高基数列:列中的值几乎都是唯一的,比如 user_id, email
  • 低基数列:列中只有很少几个固定的、重复值,比如 status (‘pending’, ‘approved’, ‘rejected’),gender (‘male’, ‘female’, ‘other’),is_active (true, false)。ENUM 和 BOOLEAN 类型天生就是低基数的。

低基数列作为索引的主要问题是“筛选效率低下”:

  1. 索引选择性差 (Poor Selectivity)
    数据库优化器在决定是否使用索引时,会评估其“选择性”。一个好的索引应该能帮你快速排除掉大部分不符合条件的数据。

    举个例子:
    假设一个 orders 表有1000万条记录,其中 status 列有3个值:’processing’, ‘shipped’, ‘completed’。

    • 如果你执行 SELECT * FROM orders WHERE status = 'shipped';
    • 数据库会估算,status = 'shipped' 的记录可能占了总数的三分之一,也就是大约333万条。
    • 在这种情况下,优化器很可能会认为:通过索引找到这333万条记录的地址,然后再逐一回表(回到主表去读取完整的行数据),这个过程的随机I/O成本可能比直接全表扫描(从头到尾读一遍表)还要高
    • 因此,即使你建了索引,数据库也可能放弃使用它,导致索引白建了。
  2. 索引维护成本 (Maintenance Overhead)
    索引不是免费的。每次对表进行 INSERT, UPDATE, DELETE 操作时,如果动到了索引列,数据库也需要同步更新索引本身。对于一个写操作频繁的表,一个几乎用不上的低效索引反而会拖慢整体的写入性能。

结论: 当一个索引不能有效地区分数据时,它的存在就弊大于利。这就是“不要给低基数列加索引”这个建议的根本原因。


那么,什么时候应该给ENUM/低基数列加索引?

虽然低基数索引通常是低效的,但在以下几种情况下,给它们建立索引却是非常明智和高效的:

  1. 数据分布极不均衡 (Skewed Data Distribution)
    这是最常见也最重要的例外情况。虽然列的基数很低,但如果某个值的出现频率极低,为它建索引就非常有用。

    举例:
    一个 users 表有1亿用户,有一个 is_banned (true/false) 字段。

    • 99.99% 的用户 is_bannedfalse
    • 只有 0.01% 的用户 is_bannedtrue(被封禁的用户)。
    • 此时,SELECT * FROM users WHERE is_banned = true; 这个查询的选择性就极高。索引可以快速定位到那一小部分被封禁的用户,性能会远超全表扫描。
  2. 作为覆盖索引 (Covering Index) 的一部分
    当一个查询所需的所有列都包含在索引中时,数据库引擎就无需回表,可以直接从索引中获取所有数据返回。这被称为“覆盖索引”。

    举例:
    你想统计每种订单状态的数量:SELECT status, COUNT(*) FROM orders GROUP BY status;

    • 如果你只在 status 列上建立一个索引 INDEX(status)
    • 数据库可以只扫描这个小得多的 status 索引,而完全不需要触碰庞大的主表数据,就能完成统计。这会带来巨大的性能提升。
  3. 作为复合索引 (Composite Index) 的一部分
    当低基数列是复合索引的非前导列时,它通常是非常有用的。

    举例:
    你的查询条件通常是 WHERE corp_id = ? AND status = ?

    • corp_id 是一个高基数列(有很多公司)。
    • status 是一个低基数列。
    • 建立一个复合索引 INDEX(corp_id, status) 是非常高效的。
    • 查询时,索引会首先通过 corp_id 快速筛选出很少一部分数据,然后再在这个小范围内根据 status 进行查找。此时 status 的低基数问题就不再是瓶颈。

总结与建议

场景 是否建议加索引 原因
单独查询,且数据均匀分布 不建议 索引选择性差,优化器可能放弃使用,且有维护成本。
单独查询,但数据分布极不均衡 强烈建议 对稀有值的查询效率极高。
查询只涉及该列和少数几列 建议 可以创建覆盖索引,避免回表,极大提升性能。
作为复合索引的一部分,与其他高选择性列组合 强烈建议 复合索引可以大大提高多条件查询的效率。

实践法则:

  1. 不要盲从规则:先分析你的业务场景和查询模式。
  2. 检查数据分布:使用 SELECT status, COUNT(*) FROM your_table GROUP BY status; 来了解值的分布情况。
  3. 使用 EXPLAIN:在你认为需要加索引的查询前加上 EXPLAIN,看看数据库的执行计划。确认它是否会使用你创建的索引(key 列),以及扫描的行数(rows 列)是否显著减少。
  4. 优先考虑复合索引和覆盖索引:在设计索引时,优先考虑如何让索引服务于一类查询,而不仅仅是单个列。

总而言之,“不要给ENUM/低基数列加索引”是一个有用的“经验法则”,它提醒我们警惕低基数列的索引陷阱。但它绝不是一条必须遵守的铁律。 真正的数据库优化需要结合具体业务场景、查询模式和数据分布来进行综合判断。

MySQL表添加业务唯一性约束(Unique Constraint)通常是正确且非常推荐的做法。

这不仅仅是数据库层面的一个设置,更是保证业务逻辑严谨性和数据完整性的关键手段。

为什么强烈推荐这样做 (好处)

  1. 保证数据完整性 (Data Integrity)
    • 这是最主要的原因。通过数据库约束,可以从根本上杜绝脏数据的产生。例如,可以防止两个用户使用同一个邮箱注册,或者系统中出现两个
      编码完全相同的商品。
  1. 简化应用层逻辑 (Simplifies Application Logic)
    • 如果没有唯一性约束,你需要在应用代码中实现“先查询是否存在,再插入/更新”的逻辑。这种逻辑在并发场景下很容易出错(即“竞态条件”,
      Race Condition),可能导致在极短的时间差内,两个请求都查询到数据不存在,然后都成功插入,最终还是产生了重复数据。
    • 有了数据库约束,应用层可以大胆地直接执行 INSERT 或 UPDATE,然后捕获并处理可能抛出的“唯一键冲突”异常。这种方式更简单、更可靠。
  1. 提升查询性能 (Improves Query Performance)
    • 当你添加一个 UNIQUE 约束时,MySQL 会自动为其创建一个唯一索引(Unique Index)。
    • 当你的查询条件(WHERE 子句)涉及到这个唯一性字段时,数据库可以利用这个索引进行极速查找,性能远高于没有索引的全表扫描。
  1. 明确业务规则 (Clarifies Business Rules)
    • 数据库的表结构(Schema)本身就是一种文档。当其他开发者看到这个表有一个唯一性约束时,他们能立刻理解这个字段(或字段组合)在业
      务上是不允许重复的,这使得代码更易于维护。

需要注意的点 (Considerations)

  1. NULL 值的处理
    • 在MySQL中,UNIQUE 约束的列可以包含多个 NULL 值。因为在索引层面,NULL 被认为是一个不确定的值,NULL 不等于任何值,包括另一个NULL。如果你希望某个字段要么唯一,要么为空,那么 UNIQUE 约束是合适的。
  1. 写入性能开销
    • 每次 INSERT 或 UPDATE 唯一键字段时,数据库都需要检查索引以确保其唯一性,这会带来微小的性能开销。但在绝大多数场景下,这点开销与它带来的数据完整性、查询性能提升相比,是完全值得的。只有在写入极其密集(例如,每秒数十万次)的特定场景下,才需要特殊考量。
  1. 联合唯一约束 (Composite Unique Constraint)
    • 你可以对多个列组合起来设置唯一性约束。例如,一个用户在同一个社区内只能对一篇文章点赞一次,你可以对 (user_id, post_id)
      这两个字段组合建立唯一约束。

总结

结论是:应该加。在设计表结构时,应该主动思考哪些字段或字段组合是业务上的“天然唯一标识”,并为它们添加 UNIQUE 约束。

最佳实践

  • 核心业务标识:如用户邮箱、手机号、订单号、商品SKU等,必须添加唯一约束。
  • 并发安全:依赖数据库约束来保证唯一性,而不是在应用层做“查-写”操作。
  • 错误处理:应用层代码必须准备好捕获和优雅地处理唯一键冲突的错误,并向用户返回友好的提示(例如:“该邮箱已被注册”)。

简单来说,为 Redis 大 Key(Big Key)增加一个二级缓存(通常是本地进程内缓存),核心目标是用内存换取CPU和网络资源,以数量级的方式减少对 Redis 的访问压力,并避免大 Key 带来的潜在风险。

下面我们来详细拆解这个问题。

首先,为什么 Redis 大 Key 是个问题?

“大 Key” 通常指一个 Key 对应的 Value 非常大(例如超过 100KB),或者一个 Key 包含的元素非常多(例如一个有数百万成员的 Hash 或 ZSET)。

大 Key 会带来一系列严重问题:

  1. 网络IO瓶颈:一次 GET 一个 5MB 的大 Key,就会在网卡上产生 5MB 的流量。在高并发下,这会迅速占满服务器带宽,导致网络延迟剧增,影响所有其他服务。
  2. Redis 阻塞风险:Redis 的命令处理是单线程的。当 Redis 处理一个大 Key 时(例如对其进行序列化、反序列化、网络传输),会消耗更长的时间。在这期间,其他所有请求都必须排队等待,导致 Redis 的 QPS (每秒查询率)急剧下降,甚至出现服务“卡死”的现象。特别是删除一个大 Key (DEL a_big_key),可能会造成秒级的阻塞。
  3. 内存分配不均:在 Redis Cluster 模式下,一个大 Key 会导致某个特定节点的内存使用量远超其他节点,造成数据倾斜和资源分配不均,难以扩容。
  4. 缓存淘汰效率低:当内存不足需要淘汰数据时,如果淘汰了一个大 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,它存储了某个不常变化但读取频繁的配置信息。

优化前的工作流程:

  1. 客户端请求数据。
  2. 应用服务向 Redis 发送 GET big_key 命令。
  3. Redis 读取 5MB 数据,通过网络发送给应用服务。
  4. 应用服务接收 5MB 数据,处理后返回给客户端。
  • 问题:每次请求都要消耗 Redis 的 CPU 和大量的网络带宽。

优化后的工作流程:

  1. 客户端请求数据。
  2. 应用服务首先检查本地缓存中是否存在 big_key。
  3. 缓存命中 (Hit):
    • 直接从本地内存中读取数据(速度极快,无网络IO)。
    • 处理后返回给客户端。
    • 效果:整个过程完全不涉及 Redis,Redis 和网络带宽的压力被彻底消除。
  4. 缓存未命中 (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)。
      1. 当数据在数据库中被更新时。
      2. 发送一个“缓存失效”消息到消息队列的特定主题(Topic)。
      3. 所有订阅了该主题的应用服务实例都会收到这个消息。
      4. 收到消息后,每个应用服务实例都删除自己本地缓存中的对应 Key。

总结

为大 Key 添加二级(本地)缓存是一种典型的空间换时间和分级缓存思想的应用。它通过在应用端牺牲一小部分内存,来保护下游更宝贵、更容易成为瓶颈的分布式缓存和网络资源。

这种模式尤其适用于“读多写少”的大 Key 场景,例如:

  • 商品详情页的完整信息。
  • 系统的基础配置。
  • 一个热点新闻的完整内容。

对于频繁更新的数据,使用二级缓存需要非常谨慎地处理一致性问题,成本会更高。

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.

free

兑换码:DataGrip2025

兑换地址: https://www.jetbrains.com/store/redeem/

如果已经有订阅了会续订一年

兑换码已失效,兑换完毕,额度用完(最后兑换时间 2025-07-01 12:00 左右)

原文