Go 原生 map 不是并发安全的,多协程同时读写会panic: concurrent map read/write。

解决方法

互斥锁

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
package main

import "sync"

func main() {
var mu sync.Mutex
m := make(map[int]int)

go func() {
for i := 0; i < 10000; i++ {
mu.Lock() // 加锁
m[i] = i
mu.Unlock() // 解锁
}
}()

go func() {
for i := 0; i < 10000; i++ {
mu.Lock()
_ = m[i]
mu.Unlock()
}
}()

select {}
}

读写锁

1
2
3
4
5
6
7
8
9
10
11
var mu sync.RWMutex

// 读:用 RLock()
mu.RLock()
val := m[key]
mu.RUnlock()

// 写:用 Lock()
mu.Lock()
m[key] = val
mu.Unlock()

性能高于互斥锁

go原生并发安全map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "sync"

func main() {
m := &sync.Map{} // 直接用

// 写
go func() {
for i := 0; i < 10000; i++ {
m.Store(i, i)
}
}()

// 读
go func() {
for i := 0; i < 10000; i++ {
m.Load(i)
}
}()

select {}
}

已关闭channel读写

往已经关闭的 channel 写数据
→ 直接 panic(panic: send on closed channel)
从已经关闭的 channel 读数据
→ 不会 panic

防击穿

方案 1:互斥锁(mutex lock)

只允许一个请求去数据库查数据并重建缓存,其他请求等待。

优点:简单、安全、无数据不一致
缺点:高并发下有少量阻塞

1
2
3
4
5
6
7
8
9
10
11
if 缓存存在 {
return 缓存
} else {
加锁
if 缓存不存在 { // 二次检查!
查数据库
写回缓存
}
解锁
return 数据
}

方案 2:热 Key 永不过期

热 Key 不设置过期时间,彻底杜绝失效。

更新方式

  • 后台异步线程定时刷新缓存
  • 或数据变化时主动更新

优点:完全不会击穿
缺点:需要额外维护刷新逻辑

方案 3:提前异步刷新 Key

在 Key 快要过期前,主动异步刷新。

流程

  1. 给 Key 设置过期时间
  2. 每次访问时判断:是否快过期了?
  3. 快过期 → 异步去 DB 拉新数据重新写入缓存

优点:无感知、无阻塞
缺点:实现稍复杂

方案 4:本地缓存(Caffeine)+ 二级缓存

热 Key 同时存放于:

  • Redis(分布式缓存)
  • 本地缓存(应用内内存)

Redis 失效 → 还能读本地缓存,不会直接打数据库。

优点:性能极高、抗击穿
缺点:要注意数据一致性

RDB vs AOF

对比项 RDB AOF
原理 全量快照 记录写命令
文件内容 二进制数据(紧凑) 文本命令(可读)
恢复速度 极快 慢(要重放命令)
数据安全性 低(可能丢最后几分钟数据) 高(最多丢 1 秒)
文件大小 大(会膨胀)
性能影响 高(fork 子进程,阻塞瞬间) 低(异步刷盘)
适用场景 冷备、全量恢复 实时持久化、高可靠

SLICE不是线程安全的

因为slice 底层结构不是原子的
包含:指针 + 长度(len) + 容量(cap)
多协程同时修改这三个值,没有任何锁保护。

进程线程协程

进程:操作系统资源分配的最小单位(独立房子)
线程:CPU 调度的最小单位(房子里的工人)
协程:用户态轻量级执行单元(工人手里的小任务)

如果Zset做排行榜时要获取某个分数范围内的用户,应该使用哪个命令

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

两协程交替打印1-100

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
package main

import (
"fmt"
"sync"
)

func main() {
ch := make(chan struct{}) // 空channel,只做通知
var wg sync.WaitGroup
wg.Add(2)

// 协程1:打印奇数
go func() {
defer wg.Done()
for i := 1; i <= 100; i += 2 {
<-ch // 等待通知
fmt.Println(i)
ch <- struct{}{} // 通知另一个协程
}
}()

// 协程2:打印偶数
go func() {
defer wg.Done()
for i := 2; i <= 100; i += 2 {
<-ch
fmt.Println(i)
ch <- struct{}{}
}
}()

// 启动:先通知奇数协程
ch <- struct{}{}

wg.Wait()
close(ch)
}

jwt相关

1. JWT 的组成部分

JWT 由三部分组成,用 . 分隔:

  1. Header(头部):声明加密算法、token 类型
  2. Payload(载荷):存放业务数据(用户信息)
  3. Signature(签名):防篡改校验串

格式:
Header.Payload.Signature


2. Payload 部分存什么?

存非敏感的公开业务信息,标准字段 + 自定义字段:

  • 标准:sub(用户ID)、iss(签发者)、exp(过期时间)
  • 自定义:用户名、角色、权限、手机号(不存密码、密钥

3. 其他用户能看到 Payload 吗?

能!完全能!

  • Payload 只是 Base64 编码不是加密
  • 任何人都可以直接解码看到明文
  • 绝对不能存敏感信息(密码、身份证、密钥)

总结

  1. JWT = 头部 + 载荷 + 签名
  2. Payload 存公开用户信息,不存敏感数据
  3. Payload 可被任何人查看,仅防篡改,不保密

m3u8

早期完整视频是单个 MP4 文件,缺点:
大文件加载慢、卡顿;
无法动态切换清晰度;
防盗链、防盗录难度低。
于是把完整视频切成很多小段 TS 分片(通常每片几秒),再用 m3u8 文件记录所有分片的 URL、播放顺序。
播放器 / 下载器先读取 m3u8,再按顺序逐个加载 TS 片段,拼接播放。

约束解码

用于强制让llm输出标准结构的输出
模型每次算下一个 Token 的概率分布(logits),约束解码在采样前加一层 “掩码(Mask)”:
用 JSON Schema / Pydantic 类定义合法结构
实时跑一个语法解析器(有限状态机 FSM / 上下文无关文法 CFG)
只允许符合当前语法状态的 Token 参与采样;非法 Token 概率直接设为 -∞(永远不会被选)

千问不支持。

goMap

Go 语言中的 map 是无序的!
遍历的时候每次顺序都不一样,不保证插入顺序,也不保证排序顺序。
为了防止开发者依赖遍历顺序
Go runtime 在遍历的时候故意做了随机化