编程笔记

lifelong learning & practice makes perfect

在 Redis 中,BGREWRITEAOF 是一个用于重写 AOF(Append Only File)文件的后台操作,目的是压缩 AOF 文件,去除冗余命令,从而减小文件体积并提升恢复速度。然而,在执行 BGREWRITEAOF 期间,主进程仍在处理客户端请求,可能会产生新的写入操作。这就带来了一个潜在问题:主进程与子进程(BGREWRITEAOF 子进程)之间可能出现数据不一致

问题本质:主进程与子进程的数据不一致

  • 主进程:正常处理写操作,将命令追加到 AOF 缓冲区,并可能写入 AOF 文件。
  • 子进程(BGREWRITEAOF):在某一时刻(通常是 BGREWRITEAOF 命令执行时)启动,它会读取当前 Redis 的完整数据集(内存中的键值对),并将其以命令形式写入一个新的 AOF 文件
  • 问题:在子进程执行期间,主进程可能已经接收并处理了新的写操作,这些写操作没有被子进程看到,导致新 AOF 文件中缺少这部分数据。

Redis 如何解决这个问题?

Redis 采用了一种称为 “AOF 重写缓冲区”(AOF rewrite buffer) 的机制来解决这一问题。

✅ 核心机制:AOF 重写缓冲区(AOF Rewrite Buffer)

  1. 子进程启动时

    • Redis 主进程会创建一个新的 AOF 文件(临时文件,如 temp-rewriteaof-bg-*.aof)。
    • 子进程开始读取内存中的数据集,并将所有键值对以命令形式写入这个新文件。
  2. 主进程在子进程运行期间

    • 所有新的写操作(如 SET key value)不仅写入 AOF 缓冲区,还会额外写入一个特殊的缓冲区 —— AOF 重写缓冲区
    • 这个缓冲区是独立的,用于记录子进程运行期间的增量操作。
  3. 子进程完成重写后

    • 子进程退出。
    • 主进程将 AOF 重写缓冲区中的所有命令追加写入到新 AOF 文件末尾
    • 然后将新 AOF 文件原子替换为旧的 AOF 文件(通过 rename 系统调用)。
  4. 最终结果

    • 新的 AOF 文件包含了:
      • 子进程重写时的完整数据集(即快照)。
      • 子进程运行期间所有新增的写操作。
    • 因此,新 AOF 文件是完整且一致的,能完整恢复 Redis 的状态。

关键点

机制 作用
AOF 重写缓冲区 缓存子进程运行期间的增量写操作
子进程读内存快照 生成 AOF 的基础数据(无冗余命令)
主进程追加缓冲区内容 确保不丢失子进程运行期间的写操作
原子替换文件 保证 AOF 文件切换的原子性和一致性

注意事项

  1. AOF 重写缓冲区大小有限

    • 如果缓冲区溢出(例如写入太频繁),Redis 会阻塞主进程,直到子进程完成或缓冲区清空。
    • 可通过配置 aof-rewrite-incremental-fsyncaof-rewrite-buffer-size 调优。
  2. 性能影响

    • BGREWRITEAOF 是 CPU 密集型操作,可能影响主进程性能。
    • 建议在低峰期执行,或使用 auto-aof-rewrite 自动触发。
  3. AOF 重写期间主进程仍可服务

    • 重写过程是异步的,主进程不阻塞(除了缓冲区满时可能阻塞)。

示例流程

1
2
3
4
5
6
7
8
# 主进程执行 BGREWRITEAOF
BGREWRITEAOF

# 1. 子进程启动,读取内存快照,写入 temp-rewriteaof-bg-*.aof
# 2. 主进程开始写入 AOF 文件 + AOF 重写缓冲区
# 3. 子进程完成,退出
# 4. 主进程将重写缓冲区内容追加到新 AOF 文件
# 5. 原 AOF 文件被 rename 为 backup,新文件替换它

✅ 总结

Redis 通过 AOF 重写缓冲区 机制,完美解决了 BGREWRITEAOF 与主进程数据不一致的问题:

子进程负责“快照”,主进程负责“增量”,最后合并,确保 AOF 文件完整、一致、可恢复。

sync.RWMutex 的基本特性

  • 值类型: sync.RWMutex 是一个 struct。这意味着如果你直接传递它,你将得到一个副本。对副本的锁定操作不会影响原始的 mutex,这会导致同步失效。
  • 不应在第一次使用后复制: Go 官方文档明确指出,sync.Mutex (以及 sync.RWMutex) 不应在第一次使用(即调用 Lock/UnlockRLock/RUnlock)后进行复制。复制会导致未定义的行为,通常是死锁或 panic。
  • 零值可用: sync.RWMutex{} (零值) 是一个有效的、可直接使用的 mutex。

在局部函数中创建并返回 *sync.RWMutex 的情况

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"sync"
"time"
)

// 情况 A: 函数内部创建 RWMutex 并返回其指针
func createAndReturnRWMutex() *sync.RWMutex {
var mu sync.RWMutex // 局部变量,Go的逃逸分析会将其分配到堆上
fmt.Println("Mutex created at address:", &mu)
return &mu
}

func main() {
// 获取由 createAndReturnRWMutex 函数创建的 mutex 指针
mu1 := createAndReturnRWMutex()
mu2 := createAndReturnRWMutex() // 再次调用,会得到一个新的 mutex

fmt.Println("Returned mu1 address:", mu1)
fmt.Println("Returned mu2 address:", mu2)

// 使用 mu1
mu1.RLock()
fmt.Println("mu1 RLocked")
go func() {
mu1.Lock() // 这个地方会等待上面的 RLock 释放
fmt.Println("mu1 Locked by goroutine")
time.Sleep(100 * time.Millisecond)
mu1.Unlock()
fmt.Println("mu1 Unlocked by goroutine")
}()
time.Sleep(50 * time.Millisecond) // 等待 goroutine 尝试加锁
mu1.RUnlock()
fmt.Println("mu1 RUnlocked")

// 确保 goroutine 完成
time.Sleep(200 * time.Millisecond)

// 使用 mu2 (与 mu1 独立)
mu2.Lock()
fmt.Println("mu2 Locked")
mu2.Unlock()
fmt.Println("mu2 Unlocked")
}

分析:

  1. 技术上可行: Go 的逃逸分析会识别到局部变量 mu 的地址被返回了函数外部,因此它会被分配到堆上(heap),而不是栈上。所以,返回的指针是有效的,不会指向已被销毁的内存。
  2. 不常见,且通常不是好的设计模式:
    • 封装性差: sync.RWMutex 的核心作用是保护共享数据。它几乎总是作为某个数据结构(struct)的字段而存在,由该数据结构的方法来管理其锁定和解锁,以保护该结构体内部的数据。
    • 职责不清: 如果一个函数仅仅返回一个 *sync.RWMutex,那么这个 mutex 是为了保护什么数据?这个信息并没有被封装起来,使用者需要自己去追踪这个 mutex 的用途,这增加了代码的复杂性和出错的风险。
    • 容易误用: 调用者可能会不清楚这个 mutex 应该保护什么数据,或者在不适当的时候加锁/解锁,导致死锁、竞争条件等问题。
    • 代码可读性差: 相比于一个明确的结构体(例如 SafeCache)包含一个 RWMutex,仅仅返回一个 RWMutex 指针会让代码意图模糊。

在局部函数中返回已存在*sync.RWMutex 指针

另一种情况是,函数不是创建新的 RWMutex,而是返回一个指向已经存在的 RWMutex 的指针,比如从一个全局变量或者传入参数的结构体中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "sync"

var globalMu sync.RWMutex

type MyContainer struct {
Mu sync.RWMutex
Data int
}

// 情况 B: 返回全局 RWMutex 的指针
func getGlobalRWMutex() *sync.RWMutex {
return &globalMu
}

// 情况 C: 返回结构体字段 RWMutex 的指针
func getContainerRWMutex(c *MyContainer) *sync.RWMutex {
return &c.Mu
}

func main() {
gMu := getGlobalRWMutex()
gMu.Lock()
// ...
gMu.Unlock()

container := &MyContainer{}
cMu := getContainerRWMutex(container)
cMu.RLock()
// ...
cMu.RUnlock()
}

分析:

  • 技术上可行: 同样,返回的指针是有效的。
  • 合适性存疑:
    • 暴露内部实现: 这种做法打破了封装性。一个结构体通常应该通过其方法来提供对数据(包括同步机制)的受控访问,而不是直接暴露其内部的 sync.RWMutex 字段。
    • 耦合性增加: 外部代码直接操作内部 mutex 会增加模块间的耦合,使得结构体的内部实现更难修改。
    • 潜在的API风险: 如果 MyContainer 的设计者希望控制对 Data 的访问方式,而调用者直接使用 c.Mu 进行锁操作,可能会绕过 MyContainer 提供的方法,导致不一致或错误的状态。

推荐做法

在 Go 中,管理 sync.RWMutex惯用和最佳实践是将其嵌入到一个结构体中,并通过该结构体的方法来管理锁定和解锁,从而保护结构体自身的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"sync"
)

// SafeCounter 是一个线程安全的计数器
type SafeCounter struct {
mu sync.RWMutex // RWMutex 作为结构体的字段
count int
}

// Inc 增加计数器的值
func (sc *SafeCounter) Inc() {
sc.mu.Lock() // 方法内部管理锁
defer sc.mu.Unlock()
sc.count++
}

// Value 返回当前计数器的值
func (sc *SafeCounter) Value() int {
sc.mu.RLock() // 方法内部管理读锁
defer sc.mu.RUnlock()
return sc.count
}

// NewSafeCounter 是 SafeCounter 的构造函数
func NewSafeCounter() *SafeCounter {
// RWMutex 的零值是可用的,无需额外初始化
return &SafeCounter{}
}

func main() {
counter := NewSafeCounter() // 返回的是 *SafeCounter,而不是 *sync.RWMutex

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc() // 通过 SafeCounter 的方法访问计数器,由方法处理同步
}()
}

wg.Wait()
fmt.Println("Final Counter Value:", counter.Value())
}

总结

在Go语言中,一般不推荐在局部函数中创建 sync.RWMutex 并单独返回其指针。这通常表示设计上存在封装性不足的问题。

更推荐的做法是:

  1. sync.RWMutex 作为你希望保护的结构体的字段。
  2. 通过该结构体的方法来封装对数据的访问和 sync.RWMutex 的锁定/解锁操作。
  3. 如果需要提供一个同步的实例,你的构造函数应该返回一个指向该结构体实例的指针(例如 *SafeCounter),而不是单独的 *sync.RWMutex 指针。

这样做可以提高代码的封装性、可读性和健壮性,减少误用的可能性。

Go 语言自 1.18 版本开始支持泛型(Generics)。泛型允许我们为函数和数据结构定义类型参数,增强代码的复用性和类型安全性。然而,泛型并不是在所有情况下都适用,正确地使用泛型能带来代码清晰性和性能提升,错误使用则可能使代码复杂或降低性能。

本文旨在帮助你了解在什么情况下应该考虑使用泛型。

泛型的优点

  1. 代码复用性:泛型允许用一种实现支持多种类型,减少代码重复。
  2. 类型安全:在编译时检查类型,防止运行时类型错误。
  3. 抽象能力:抽象地处理数据结构和算法,提升代码表达力。
  4. 性能:避免因为使用interface{}类型带来的类型断言,从而减少运行时开销。

何时使用泛型?

泛型适用于以下场景:

  • 你有一组算法或数据结构逻辑,它们在不同的类型上运作,但逻辑完全相同。
  • 你想写高度通用的代码来简化代码库,同时保持类型安全。
  • 你的代码需要同时处理多种类型,但不想牺牲性能。
  • 你希望避免冗余代码,减少维护工作量。

何时不适合使用泛型?

泛型不是万金油,某些情况下反而带来负担:

  • 只有单一具体类型需求时,泛型会增加代码复杂度。
  • 泛型代码过于复杂,导致可读性变差,增加维护难度。
  • 性能占优且简单逻辑可以接受轻微重复代码时,不必强制用泛型。
  • 当泛型参数变得过于复杂(过多约束、嵌套)时,可能适得其反。

不要把interface类型替换为类型参数

Go语言有interface类型,interface支持某种意义上的泛型编程。

举个例子,被广泛使用的io.Reader接口提供了一种泛型机制用于读取数据,比如支持从文件和随机数生成器里读取数据。

如果你对某些类型的变量的操作只是调用该类型的方法,那就直接使用interface类型,不要使用类型参数。io.Reader从代码角度易于阅读且高效,没必要使用类型参数。

举个例子,有人可能会把下面第1个基于interface类型的ReadSome版本修改为第2个基于类型参数的版本。

1
2
3
func ReadSome(r io.Reader) ([]byte, error)

func ReadSome[T io.Reader](r T) ([]byte, error)

不要做这种修改,使用第1个基于interface的版本会让函数更容易编写和阅读,并且函数执行效率也几乎一样。

注意:尽管可以使用不同的方式来实现泛型,并且泛型的实现可能会随着时间的推移而发生变化,但是Go 1.18中泛型的实现在很多情况下对于类型为interface的变量和类型为类型参数的变量处理非常相似。这意味着使用类型参数通常并不会比使用interface快,所以不要单纯为了程序运行速度而把interface类型修改为类型参数,因为它可能并不会运行更快。

用例示例

例如处理一组不同类型的切片排序、查找操作,可以使用泛型函数避免重复编写针对不同类型的代码。

非泛型版本

1
2
func IntContains(slice []int, val int) bool { ... }
func StringContains(slice []string, val string) bool { ... }

上述重复很多类似逻辑。

泛型版本

1
func Contains[T comparable](slice []T, val T) bool { ... }

案例2: Go内置的容器类型

入参是一个map,要返回该map的所有key组成的slice,key的类型可以是map支持的任意key类型。

1
2
3
4
5
6
7
8
9
// MapKeys returns a slice of all the keys in m.
// The keys are not returned in any particular order.
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {
s := make([]Key, 0, len(m))
for k := range m {
s = append(s, k)
}
return s
}

这段代码没有对map里key的类型做任何限定,并且没有用map里的value,因此这段代码适用于所有的map类型。这就是使用类型参数的一个很好的示例。

这种场景下,也可以使用反射(reflection),但是反射是一种比较别扭的编程模型,在编译期没法做静态类型检查,并且会导致运行期的速度变慢。

结语

泛型是强大的工具,但需用时权衡复杂度与收益。建议代码在泛型和具体类型之间平衡。合适时用泛型,实现代码复用和类型安全;不合适则回归简单可理解方案。

如果你正在考虑是否使用泛型,问自己:

  • 这段代码是否适用于多种类型?
  • 是否存在类型重复代码?

参考

redis

“两大维度”就是指系统维度和应用维度,“三大主线”也就是指高性能、高可靠和高可扩展(可以简称为“三高”)

系统维度

需要了解 Redis 的各项关键技术的设计原理

三大主线

  • 高性能: 线程模型、数据结构、持久化、网络框架;
  • 高可靠: 主从复制、哨兵机制;
  • 高可扩展: 数据分片、负载均衡。

常见问题

redis

文章指出特斯拉利用激光雷达车队在奥斯汀等地收集数据,并非放弃纯视觉路线,而是为Robotaxi建立高精度的地面实况(Ground Truth)。通过时间戳同步技术,将激光雷达的精确测量值与摄像头图像自动配对,高效生成海量训练数据,从而“教导”纯视觉系统独立完成距离和速度估算,加速自动驾驶的实现。

阅读全文 »

《日经亚洲》报道,来自八个国家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