260331笔记
线程池
一、什么是线程池?
线程池 = 提前创建好的一堆线程 + 任务队列 + 调度器
你可以把它理解成:
一个固定人数的“工人小组”,专门等着处理你丢过来的“任务”。
- 不用每次来任务都新招工人(创建线程)
- 任务做完工人不辞退(不销毁线程)
- 工人闲着就等着,来任务马上干活
- 任务太多排排队,不会一下子把系统压垮
这就是线程池。
二、为什么要用线程池?(核心作用)
如果不用线程池,你每次处理任务都 new Thread(),会有 3 个致命问题:
1. 频繁创建/销毁线程 → 非常耗性能
创建线程、销毁线程是重量级操作,比执行普通代码慢得多。
线程池让线程复用,避免反复创建销毁。
2. 无限制创建线程 → 系统崩溃
如果你来了 1000 个任务,就开 1000 个线程:
- CPU 疯狂切换
- 内存爆掉
- 程序直接卡死
线程池可以限制最大线程数量,保证系统稳定。
3. 线程管理混乱
手动创建线程很难统一管理:
- 怎么控制并发数?
- 怎么拒绝过载任务?
- 怎么监控任务状态?
线程池自带管理、监控、拒绝策略、定时任务等功能。
三、线程池的核心好处(总结版)
- 降低资源消耗:复用线程,不反复创建销毁
- 提高响应速度:任务来了,直接用空闲线程,不用等创建
- 控制并发数量:防止线程太多把系统压崩
- 统一管理线程:可监控、可定时、可拒绝、可优化
一句话:
线程池让多线程变得高效、安全、可控。
四、生活类比(最容易懂)
- 不用线程池:来一个客人,招一个服务员;客人走了,辞退服务员。
- 用线程池:提前招 5 个服务员站着等;来客人马上服务;客人走了继续等下一个;客人太多就排队。
是不是一下就明白了?
五、Java 里最常见的线程池(简单提一下)
Java 内置好的线程池,直接用:
1 | Executors.newFixedThreadPool(5); // 固定大小线程池 |
总结
- 线程池:预先创建的线程集合,负责复用线程执行任务。
- 作用:省资源、提速度、控并发、好管理。
- 本质:用空间换时间,用管理换稳定。
在 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 | package main |
效果
- 同时最多只有 5 个 goroutine 在跑
- 任务排队执行,不会暴增并发
- 这就是 Go 版线程池/协程池
2. 更常用的简化版:用 semaphore 风格(控制并发)
实际项目里经常这么写,更简洁:
1 | package main |
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
2BITCOUNT 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 | # 3天均活跃的用户(AND) |
3. 位段操作(BITFIELD)
批量读写指定位宽的整数,适合复杂位存储。
1 | # 从offset 0开始取4位,转为无符号整数 |
三、典型应用场景
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
2SETBIT 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) 实现
ZSCORE、ZREM等单点操作。
2. skiplist(跳表,核心)
- 结构:多层有序链表,节点包含:
score(排序键)、member(值)backward(反向指针,支持逆序遍历)- 多层
forward(正向指针)+span(跨度,计算排名)
- 层数:随机生成(最多 32 层),高层节点稀疏,加速跳跃查找。
- 时间复杂度:平均 O(logN) 增删改查、范围查询。
- 排序规则:先按
score升序;score相同按member字典序。
四、编码转换流程
- 初始创建 Zset 时默认用 listpack/ziplist。
- 每次
ZADD检查:若元素数 ≥128 或 member 长度 ≥64 字节,立即升级为 skiplist+dict。 - 升级不可逆:删除元素后也不会降级回 listpack/ziplist。
五、核心命令实现原理
- ZADD:dict 查 member 存在性;skiplist 按 score 插入并更新索引。
- ZRANGE:skiplist 从表头遍历,按层级快速定位范围。
- ZSCORE:dict 直接 O(1) 查找。
- ZRANK:skiplist 累加
span计算排名。
六、总结
Zset 用 listpack/ziplist 优化小数据内存,用 skiplist+dict 保障大数据性能,兼顾内存与效率,是排行榜、延迟队列、范围统计的首选结构。




