线程池

一、什么是线程池?

线程池 = 提前创建好的一堆线程 + 任务队列 + 调度器

你可以把它理解成:

一个固定人数的“工人小组”,专门等着处理你丢过来的“任务”。

  • 不用每次来任务都新招工人(创建线程)
  • 任务做完工人不辞退(不销毁线程)
  • 工人闲着就等着,来任务马上干活
  • 任务太多排排队,不会一下子把系统压垮

这就是线程池


二、为什么要用线程池?(核心作用)

如果不用线程池,你每次处理任务都 new Thread(),会有 3 个致命问题:

1. 频繁创建/销毁线程 → 非常耗性能

创建线程、销毁线程是重量级操作,比执行普通代码慢得多。
线程池让线程复用,避免反复创建销毁。

2. 无限制创建线程 → 系统崩溃

如果你来了 1000 个任务,就开 1000 个线程:

  • CPU 疯狂切换
  • 内存爆掉
  • 程序直接卡死

线程池可以限制最大线程数量,保证系统稳定。

3. 线程管理混乱

手动创建线程很难统一管理:

  • 怎么控制并发数?
  • 怎么拒绝过载任务?
  • 怎么监控任务状态?

线程池自带管理、监控、拒绝策略、定时任务等功能。


三、线程池的核心好处(总结版)

  1. 降低资源消耗:复用线程,不反复创建销毁
  2. 提高响应速度:任务来了,直接用空闲线程,不用等创建
  3. 控制并发数量:防止线程太多把系统压崩
  4. 统一管理线程:可监控、可定时、可拒绝、可优化

一句话:
线程池让多线程变得高效、安全、可控。


四、生活类比(最容易懂)

  • 不用线程池:来一个客人,招一个服务员;客人走了,辞退服务员。
  • 用线程池:提前招 5 个服务员站着等;来客人马上服务;客人走了继续等下一个;客人太多就排队。

是不是一下就明白了?


五、Java 里最常见的线程池(简单提一下)

Java 内置好的线程池,直接用:

1
2
3
4
Executors.newFixedThreadPool(5); // 固定大小线程池
Executors.newCachedThreadPool(); // 自动扩容
Executors.newSingleThreadExecutor(); // 单线程
Executors.newScheduledThreadPool(3); // 定时任务

总结

  • 线程池:预先创建的线程集合,负责复用线程执行任务。
  • 作用省资源、提速度、控并发、好管理
  • 本质:用空间换时间,用管理换稳定。

在 Go 里其实没有传统意义上的线程池,因为 Go 自带了GMP 调度器 + goroutine(轻量级线程),goroutine 极轻,一般直接 go func() 就行。

面试/工程里依然常考“Go 实现线程池”,本质是:控制同时运行的 goroutine 数量,防止瞬间爆几十万 goroutine 把内存/CPU 打满。

下面给你一个最标准、最常用的 Go 协程池(线程池)实现


五、Go 线程池

在 Go 里其实没有传统意义上的线程池,因为 Go 自带了GMP 调度器 + goroutine(轻量级线程),goroutine 极轻,一般直接 go func() 就行。
但面试 / 工程里依然常考 “Go 实现线程池”,本质是:控制同时运行的 goroutine 数量,防止瞬间爆几十万 goroutine 把内存 / CPU 打满。
下面给你一个最标准、最常用的 Go 协程池(线程池)实现。

1. 用 channel 实现简易协程池

思路:

  • 用一个带缓冲的 channel 当作“池”
  • 缓冲大小 = 最大并发数
  • 任务来了先占一个槽位,执行完释放
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"
"time"
)

// 线程池(协程池)执行任务
func worker(id int, jobs <-chan int, results chan<- string) {
// 不断从任务队列取任务
for job := range jobs {
fmt.Printf("worker %d 开始执行任务 %d\n", id, job)
// 模拟业务耗时
time.Sleep(1 * time.Second)
results <- fmt.Sprintf("任务 %d 完成", job)
}
}

func main() {
// 任务数量
const jobCount = 50
// 最大并发协程数(线程池大小)
const workerCount = 5

// 任务队列
jobs := make(chan int, jobCount)
// 结果队列
results := make(chan string, jobCount)

// 启动固定数量的 worker(协程池)
for w := 1; w <= workerCount; w++ {
go worker(w, jobs, results)
}

// 投递任务
for j := 1; j <= jobCount; j++ {
jobs <- j
}
close(jobs) // 关闭任务通道,表示没有新任务

// 收集结果
for a := 1; a <= jobCount; a++ {
fmt.Println(<-results)
}

close(results)
fmt.Println("所有任务执行完毕")
}

效果

  • 同时最多只有 5 个 goroutine 在跑
  • 任务排队执行,不会暴增并发
  • 这就是 Go 版线程池/协程池

2. 更常用的简化版:用 semaphore 风格(控制并发)

实际项目里经常这么写,更简洁:

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

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

func main() {
var wg sync.WaitGroup
// 线程池大小:最多 5 个并发
pool := make(chan struct{}, 5)

// 100 个任务
for i := 1; i <= 100; i++ {
// 占一个槽位
pool <- struct{}{}
wg.Add(1)

go func(idx int) {
defer func() {
// 释放槽位
<-pool
wg.Done()
}()

fmt.Printf("执行任务 %d\n", idx)
time.Sleep(1 * time.Second)
}(i)
}

wg.Wait()
fmt.Println("全部完成")
}
  • pool := make(chan struct{}, 5) 就是线程池
  • 控制同时运行的 goroutine 不超过 5
  • 简单、高效、Go 标准写法

3. 为什么 Go 很少提“线程池”?

  • Java/C++ 线程重,必须池化复用
  • Go 的 goroutine 是轻量级协程(KB 级),可以轻松开几万个
  • Go runtime 自己调度,不用你管理“线程复用”

依然需要协程池的场景:

  • 爬虫/大量 IO 请求(防止瞬间打崩目标网站)
  • 数据库连接有限(控制并发 SQL)
  • 避免无限制创建 goroutine 导致 OOM

一句话总结

  • Go 没有 JVM 那种线程池,但可以用 channel + goroutine 实现协程池
  • 作用:限制并发数量、保护系统资源、稳定可控
  • 标准写法:带缓冲 channel 当作令牌桶

HashMap

HashMap底层采用数组+链表/红黑树结构,通过哈希算法确定元素存储位置。默认初始容量16,负载因子0.75,当元素数量超过(容量×负载因子)时触发扩容。扩容时创建双倍容量新数组,通过高位运算重新计算节点位置(JDK8优化为无需重新hash),原数据通过尾插法迁移到新数组。链表长度超过8且数组长度≥64时会转为红黑树,提升查询效率。

Redis Bitmaps(位图)

是基于 String 类型 实现的位级操作方案,用 1bit 存 0/1 表示二值状态,空间极省、读写极快


一、核心原理

  • 本质:不是独立数据类型,是对 String(SDS)的位操作封装。
  • 存储:每个 bit 存 0/1,offset 从 0 开始;最大支持 2³²−1 位(≈42.9 亿),对应 512MB。
  • 定位规则
    • 字节位置 = offset / 8
    • 位位置 = 7 − (offset % 8)
  • 优势
    • 空间效率极高:1 亿用户状态仅需 12.5MB(1e8 / 8)。
    • 原子操作、O(1) 读写,适合海量二值统计。

二、常用命令

1. 基础操作

  • SETBIT key offset value
    设置第 offset 位为 0/1,返回旧值。
    1
    SETBIT user:sign:1001 5 1  # 用户1001第5天签到
  • GETBIT key offset
    获取第 offset 位的值(0/1)。
    1
    GETBIT user:sign:1001 5  # 1
  • BITCOUNT key [start end] [BIT]
    统计值为 1 的位数;默认按字节,加 BIT 按位。
    1
    2
    BITCOUNT user:sign:1001        # 总签到天数
    BITCOUNT user:sign:1001 0 2 BIT # 0-2位中1的个数
  • BITPOS key value [start end]
    查找第一个值为 value(0/1)的 offset。

2. 位运算(BITOP)

对多个 Bitmap 做 AND/OR/XOR/NOT,结果存到新 key。

1
2
3
# 3天均活跃的用户(AND)
BITOP AND active:3days active:20260329 active:20260330 active:20260331
BITCOUNT active:3days

3. 位段操作(BITFIELD)

批量读写指定位宽的整数,适合复杂位存储。

1
2
# 从offset 0开始取4位,转为无符号整数
BITFIELD user:status GET u4 0

三、典型应用场景

1. 用户签到/活跃统计

  • 按用户:user:sign:{uid},offset=日期,1=签到。
  • 按日期:active:{date},offset=uid,1=活跃。
  • 统计:
    • 总签到:BITCOUNT user:sign:1001
    • 连续3天活跃:BITOP AND ...

2. 权限/状态标记

  • 一个 key 存多状态:offset 0=绑定手机、1=会员、2=通知开关。
  • 示例:
    1
    2
    SETBIT user:status:1001 0 1  # 已绑手机
    SETBIT user:status:1001 1 1 # 是会员

3. 去重/计数

  • 日活、周活、月活:用 OR 合并多天 Bitmap,再 BITCOUNT。
  • 布隆过滤器(Bloom Filter)底层常用 Bitmaps 实现。

四、内存计算

  • 1 个 offset 占 1bit。
  • 公式:内存(字节) = (最大offset + 1) / 8
  • 示例:4 亿用户 → (4e8 + 1)/8 ≈ 50MB

五、注意事项

  • offset 从 0 开始,最大 2³²−1
  • 跨字节操作自动扩容,会占用内存。
  • 遍历所有 1 的 offset 效率低,适合统计而非枚举。

Redis 持久化

Redis提供两种持久化策略:RDB和AOF。RDB通过定时生成数据快照实现,适合快速恢复但可能丢失部分数据;AOF记录所有写操作命令,数据完整性更高但文件较大。此外,Redis支持混合持久化模式(AOF+RDB),结合两者优势实现高效备份与恢复。

缓存穿透

缓存穿透是指查询的数据在缓存中不存在,而数据库中也没有该数据的数据,导致缓存穿透。缓存击穿是热点数据过期导致瞬间高并发请求压垮数据库;缓存雪崩是大量缓存同时失效引发数据库连锁崩溃。区别在于穿透因数据不存在,击穿因单点热点失效,雪崩因大规模缓存失效。

数据库缓存一致性

为确保Redis与数据库双写一致性,常用方法包括:先更新数据库再删缓存(Cache-Aside)、延迟双删(更新前后均删缓存并延迟二次删除)、基于消息队列异步同步、监听数据库binlog触发缓存更新,以及设置缓存过期时间兜底。需结合业务场景选择策略,配合重试机制确保最终一致性。

Angular Commit 规范

feat: 新功能
fix: 修复 bug
docs: 仅文档修改
style: 格式、空格、缩进、分号,不影响逻辑
refactor: 重构(既不新增功能,也不修 bug)
perf: 性能优化
test: 测试用例、测试代码修改
chore: 构建/工具/依赖/杂项修改
build: 构建系统、打包配置修改
ci: CI/CD 配置、脚本修改
revert: 回滚某次提交
Redis Zset(有序集合)采用双编码动态切换实现:小数据用 listpack/ziplist 省内存,大数据用 skiplist+dict 保性能。


Redis Zset(有序集合)


一、双编码策略(核心)

Redis 根据数据规模自动选择底层结构,不可逆升级

编码 触发条件(默认) 核心作用
listpack/ziplist 元素数 < 128 且 member 长度 < 64 字节 紧凑存储、省内存
skiplist+dict 不满足上述任一条件 O(logN) 排序、范围查询、O(1) 查 score

二、listpack/ziplist 编码(小数据)

  • 结构:连续内存块,按 score 升序 存储 [member, score] 对。
  • 优点:无指针开销、缓存友好、内存占用极低。
  • 缺点:插入/删除最坏 O(N)(连锁更新)。
  • 适用:排行榜前N、小范围统计等场景。

三、skiplist+dict 编码(标准实现)

1. dict(哈希表)

  • 结构member → score 映射。
  • 作用O(1) 实现 ZSCOREZREM 等单点操作。

2. skiplist(跳表,核心)

  • 结构:多层有序链表,节点包含:
    • score(排序键)、member(值)
    • backward(反向指针,支持逆序遍历)
    • 多层 forward(正向指针)+ span(跨度,计算排名)
  • 层数:随机生成(最多 32 层),高层节点稀疏,加速跳跃查找。
  • 时间复杂度:平均 O(logN) 增删改查、范围查询。
  • 排序规则:先按 score 升序;score 相同按 member 字典序。

四、编码转换流程

  1. 初始创建 Zset 时默认用 listpack/ziplist
  2. 每次 ZADD 检查:若元素数 ≥128 或 member 长度 ≥64 字节,立即升级为 skiplist+dict
  3. 升级不可逆:删除元素后也不会降级回 listpack/ziplist。

五、核心命令实现原理

  • ZADD:dict 查 member 存在性;skiplist 按 score 插入并更新索引。
  • ZRANGE:skiplist 从表头遍历,按层级快速定位范围。
  • ZSCORE:dict 直接 O(1) 查找。
  • ZRANK:skiplist 累加 span 计算排名。

六、总结

Zset 用 listpack/ziplist 优化小数据内存,用 skiplist+dict 保障大数据性能,兼顾内存与效率,是排行榜、延迟队列、范围统计的首选结构。

B+ 树节点大小与磁盘页大小匹配,减少磁盘 I/O 操作。

监控并优化慢 SQL 可以通过启用慢查询日志、分析查询计划、优化索引和调整查询结构等方法实现。

MySQL 提供四种事务隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read,MySQL 默认) 和 串行化(Serializable),它们从低到高依次增强数据一致性,但并发性能递减。