编程笔记

lifelong learning & practice makes perfect

下面的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 左右)

原文

本文讨论了 Go 语言中 `[]Item` 类型的参数如何传递给函数,阐明了是使用副本还是指向同一数据的指针。当一个 `[]Item` 切片传递给函数时,切片的头部信息(包含指针、长度和容量)会被复制,但底层数组不会。这意味着在函数内部修改切片的元素会影响原始切片,因为它们共享同一个底层数组。但是,如果函数追加元素时超过了切片的容量,将会创建一个新的底层数组,而原始切片保持不变。文章提供示例代码来演示这些概念,展示了元素修改和在容量范围内追加如何影响原始切片,而超出容量的追加则不会。总结建议在将切片传递给函数时要小心,并建议创建切片副本以避免修改原始切片。This Go article discusses how `[]Item` type parameters are passed to functions, clarifying whether a copy or a pointer to the same data is used. When a `[]Item` slice is passed to a function, the slice header (containing a pointer, length, and capacity) is copied, but the underlying array is not. This means modifications to the slice's elements within the function will affect the original slice because they share the same underlying array. However, if the function appends elements exceeding the slice's capacity, a new underlying array is created, and the original slice remains unchanged. The article provides example code to demonstrate these concepts, showing how element modification and appending within capacity affect the original slice, while appending beyond capacity does not. It concludes by advising caution when passing slices to functions and suggests creating a slice copy if modifications are to be avoided.

阅读全文 »