一、面试复习:通信、锁与代理
这部分是前几天面试题的展开讲解,也是写高并发程序的基础。
1.1 什么是 IPC,如何进行进程间通信
IPC 是 Inter-Process Communication(进程间通信)的缩写。之所以需要进程间通信,是因为进程之间数据是隔离的。
两种情况:
- 同一台机器上的两个进程通信(如同一个程序开启的多个进程,
Python多进程)。 - 不同机器上的两个进程通信(跨主机通信)。
如何通信:
| 方式 | 说明 |
|---|---|
| 共享内存 / 共享变量 | 线程间变量天然共享,进程间需借助共享内存 |
| 队列 / 管道 | 如 Python multiprocessing.Queue、Pipe |
| 消息队列 | Redis、RabbitMQ、Kafka 等 |
| Socket 套接字 | 服务与服务的接口调用(HTTP)、RPC、MySQL socket 自定协议 |
| 文件方式 | 通过文件交换数据 |
1.2 线程间通信与锁(重点补充)
为什么需要锁? 多线程共享同一个变量时,多个线程同时读写就会产生数据竞争,导致数据错乱,因此需要加锁保证数据安全。
互斥锁(Lock):
python
from threading import Thread, Lock
def work():
global n
lock.acquire() # 加锁保证同一时刻只有一个线程修改共享数据
temp = n
time.sleep(0.001)
n = temp - 1
lock.release()
join 与锁的区别:
- 在
start()后立即join(),会让 100 个任务整体串行执行,虽然数据安全,但效率很低。 - 加锁只是把"修改共享数据"的那部分代码串行化,不加锁的部分仍能并发,效率更高。
死锁: 两个或两个以上的进程/线程执行时,因争夺资源而互相等待,若无外力干预都无法继续推进。典型如"科学家吃面"问题:每个科学家需要同时拿两把叉子才能吃面,若各拿一把又互相等待另一把,就会死锁。
递归锁(RLock / 可重入锁): 允许同一个线程多次 acquire,内部维护一个计数器记录获取次数,同一次数释放后其他线程才能获取。
python
from threading import RLock
lock = RLock()
lock.acquire()
lock.acquire() # 同一线程可以再次获取,不会死锁
print(123)
lock.release()
lock.release()
信号量(Semaphore): 可以理解为"多把锁",同时允许多个线程执行某段代码。例如 Semaphore(5) 表示最多 5 个线程同时进入。
事件(Event): 某些线程需要等其他线程执行到某个节点后再继续。例如女神者等待对方 event.set() 后,多个备胎线程 event.wait() 再启动。
GIL 与普通互斥锁的区别:
- GIL 是解释器级别的锁,保证同一时刻只有一个线程执行 Python 字节码。
- 普通互斥锁是应用层面的锁,用来保护共享数据。GIL 无法替代数据保护,所以仍需互斥锁。
I/O 密集型与计算密集型的选型:
- 单核 CPU:无论 I/O 密集还是计算密集,都用线程(或协程)。
- 多核 CPU:计算密集型用多进程,I/O 密集型用多线程(I/O 等待不占用 CPU)。
1.3 正向代理与反向代理
代理本质是一个中介:原本 A 和 B 可以直连,中间插入一个 C,C 就是代理。
正向代理 ------ 代理的是客户端:
- 访问原本无法访问的资源,如 Google。
- 做缓存,加速访问。
- 对客户端访问授权、上网认证。
- 记录用户访问记录(上网行为管理),对外隐藏用户身份。典型如 VPN、爬虫代理池。
反向代理 ------ 代理的是服务端:
- 保护内网安全,阻止 Web 攻击。大型网站通常把反向代理作为公网访问入口,Web 服务器放在内网。
- 负载均衡:通过反向代理服务器优化网站负载。典型如 nginx、apache。
项目上线升级常结合反向代理做灰度发布与平滑升级。
1.4 什么是粘包
粘包是 TCP 流式协议 的现象:TCP 没有消息边界,客户端发送的多个数据包会像水流一样流向服务端,多个包"粘"在一起,无法区分是几个数据包。
解决思路:
- 每个包设置结束标志(如 HTTP 用
\r\n\r\n)。 - 每个包设置固定大小的头,头里包含包的大小,再按长度读取。
二、补充:表单表设计(结合业务)
动态表单可以用"两张表 + 一个提交内容字段"来设计:
表单表(定义一个表单)
| id | 创建人 | 创建时间 |
|---|---|---|
| 1 | 用户 id | 时间 |
表单详情表(定义表单字段)
| id | 表单 id | 类型 | key | 是否必填 | 值 |
|---|---|---|---|---|---|
| 1 | 1 | 单行文本 | 申请人 | true | |
| 2 | 1 | 单选框 | 用途 | true | 测试, 生产 |
| 3 | 1 | 单选框 | 是否第一次 | true | 是, 否 |
| 4 | 1 | 多选框 | 云厂商 | true | 腾讯云, 阿里云, xx云 |
填写表单(用户提交记录)
| id | 表单 id | 用户 id | 内容 |
|---|---|---|---|
| 1 | 1 | 1 | {1:lqz, 2:测试, 3:是, 4:腾讯云, 阿里云} |
| 2 | 2 | 4 | {1:是, 2:测试, 3:是, 4:腾讯云, 阿里云} |
内容字段直接用 JSON 存储,灵活但不易汇总统计,适合字段不确定的动态表单场景。

三、通用命令
bash
info # 查看内存、CPU、主从信息,可用于写 Redis 监控
client list # 查看正在连接的会话
client kill <ip>:<port> # 断开某个客户端
dbsize # 统计总共有多少个 key
flushall # 清空所有数据库(危险,慎用)
flushdb # 只清空当前数据库
select <数字> # 选择某个库,默认共 16 个库(0~15)
monitor # 记录/监控操作日志,会一直输出,注意会夯住终端
生产环境请谨慎使用
flushall、flushdb、monitor。其中monitor会持续打印所有命令,影响性能且占用终端。
管理 Redis 实例的 Web 项目(可选参考):
- https://gitee.com/bijingrui/repoll
- https://gitee.com/dromara/mayfly-go
- https://gitee.com/careyjike_173/redis_web_client
- https://gitee.com/Alcex/QMySQLAdmin
- https://gitee.com/CloudWise/OMP
- https://gitee.com/lustlost/ubackup
四、字符串(String)类型
String 是 Redis 最基本也最常用的数据类型,value 是二进制安全的(可以存图片、序列化对象等)。
4.1 基本使用 get / set / del
bash
set name lqz # 设置 key 的 value,O(1)
get name # 获取 key 的 value,O(1)
del name # 删除 key,O(1)
4.2 自增自减 incr / decr / incrby / decrby
bash
incr age # value 自增 1
decr age # value 自减 1
incrby age 10 # value 增加 10
decrby age 10 # value 减 10
应用场景:
- 统计网站访问量:Redis 单线程执行命令,天然无竞争,适合做计数器。
- 分布式 ID 生成:多台机器并发生成也不会重复。
说明:
INCR/DECR要求 value 是整数字符串,否则会报错。这类命令用于计数非常高效。
4.3 setnx / setxx
bash
set name lqz # 不管 key 是否存在,都设置
setnx name lqz # key 不存在时才设置(新增)
set name lqz nx # 等价于 SETNX
set name lqz xx # key 存在时才设置(更新)
分布式锁: 用 SET key value NX EX 秒数 实现"不存在才设置 + 自动过期",是分布式锁的常见做法。
bash
set lock_code 1 NX EX 10 # 加锁,最多持有 10 秒
del lock_code # 释放锁
注意:
SETNX与EXPIRE分开写不是原子的,可能出现"已 setnx 但还没 expire 时进程崩溃,锁永不释放"的问题,因此更推荐SET key value NX EX。
4.4 批量操作 mget / mset
bash
mget key1 key2 key3 # 批量获取,O(n)
mset key1 value1 key2 value2 key3 value3 # 批量设置,O(n)
n 次 get 与一次 mget 的差别:
n次get= n 次命令时间 + n 次网络往返时间。- 一次
mget= 1 次网络往返 + n 次命令时间,省去大量网络开销。
4.5 getset / append / strlen
bash
getset name lqznb # 设置新值并返回旧值,O(1)
append name 666 # 把 value 追加到旧值后面,O(1)
strlen name # 计算字符串长度(注意中文按 UTF-8 字节数计算),O(1)
4.6 incrbyfloat / getrange / setrange
bash
incrbyfloat age 3.5 # value 自增 3.5,传负值表示自减,O(1)
getrange key 0 3 # 获取指定下标范围的子串,O(1)
setrange key 2 xyz # 从指定下标开始覆盖写入 value,O(1)
4.7 String 的其他要点
- 底层编码 :String 底层为
SDS(简单动态字符串),根据长度有int、embstr、raw三种编码,embstr用于较短字符串,raw用于较长字符串。 - 常用场景:缓存、计数器、分布式锁、会话(Session)、短信验证码、限流等。
五、哈希(Hash)类型
Hash 类似字典,一个 key 对应多个 field-value,适合存对象或一行的字段。
5.1 基本操作 hget / hset / hdel
bash
hset key field value # 设置 hash key 对应的 field 的 value,O(1)
hget key field # 获取 hash key 对应的 field 的 value,O(1)
hdel key field # 删除 hash key 对应的 field,O(1)
示例:
bash
hset user:1:info age 23
hget user:1:info age
hset user:1:info name lqz
hgetall user:1:info
hdel user:1:info age
5.2 判断与计数 hexists / hlen
bash
hexists user:1:info name # 判断 field 是否存在,O(1)
hlen user:1:info # 获取 field 数量,O(1)
5.3 批量操作 hmget / hmset
bash
hmget key field1 field2 ... fieldN # 批量获取,O(n)
hmset key field1 value1 field2 value2 # 批量设置,O(n)
5.4 全量操作 hgetall / hvals / hkeys
bash
hgetall key # 返回所有 field 和 value,O(n)
hvals key # 返回所有 field 的 value,O(n)
hkeys key # 返回所有 field,O(n)
小心使用
hgetall:它返回全部字段,当字段很多时会阻塞 Redis,生产环境建议用hscan(游标式)代替。
两个典型应用:
- 统计网站每个用户主页的访问量:
hincrby user:1:info pageview count。 - 缓存 MySQL 的一条记录:把行字段作为 field 存入 hash,如
hmset user:1 name lqz age 23。
5.5 hsetnx / hincrby / hincrbyfloat
bash
hsetnx key field value # field 不存在才设置,O(1)
hincrby key field intCounter # field 的 value 自增,O(1)
hincrbyfloat key field floatCounter # field 的 value 自增浮点数,O(1)
小结:Hash 适合存"一个对象的多个字段",比把整个对象序列化成字符串更省内存、方便单字段更新(如只更新 age)。
六、算法与数据结构复杂度
6.1 为什么重要
算法和数据结构是编程的基石:
- 算法 :代码的执行逻辑和流程,如
for、if,例如排序。 - 数据结构 :变量的组织结构,如
list、map、set。
写 Redis 时,理解每条命令的时间复杂度,能避免在数据量巨大时把服务"打停"。
6.2 大 O 表示法
| 记号 | 含义 | 例子 |
|---|---|---|
O(1) |
常数时间,与数据量无关 | GET、SET、HSET、LPUSH、SADD |
O(log n) |
对数时间 | ZSet 的 ZADD / ZRANGEBYSCORE(底层跳表) |
O(n) |
线性时间,随数据量线性增长 | KEYS、SMEMBERS、HGETALL、LRANGE(全量) |
O(n²) |
平方时间 | 某些嵌套遍历算法 |
时间复杂度 :消耗的时间;空间复杂度:消耗的内存。
6.3 生产环境注意点
keys、smembers、hgetall等全量命令是O(n),key 很多时会阻塞 Redis。- 用
scan、hscan、sscan代替全量命令,逐批游标式遍历,不阻塞。 - 单条命令尽可能快;批量操作
mget、mset、pipeline减少网络往返。
想刷题可参考力扣(LeetCode),难度分简单、中等、困难;排序等算法可查阅 Python 常用排序教程。
七、列表(List)类型
List 是有序的字符串列表,可用来实现队列、栈、消息队列等。
7.1 插入操作
bash
rpush key value1 value2 ... valueN # 从右侧插入,O(1~n)
lpush key value1 value2 ... valueN # 从左侧插入,O(1~n)
linsert key before|after value newValue # 在 value 前/后插入,O(n),需遍历列表
示例:
bash
linsert listkey before b java
linsert listkey after b php
7.2 删除操作
bash
lpop key # 从左侧弹出一个 item,O(1)
rpop key # 从右侧弹出一个 item,O(1)
lrem key count value # 按 count 删除 value,O(n)
ltrim key start end # 按索引范围修剪列表,O(n)
lrem 的 count 规则:
count > 0:从左到右,删除最多 count 个 value 相等的项。count < 0:从右到左,删除最多Math.abs(count)个 value 相等的项。count = 0:删除所有 value 相等的项。
示例:
bash
lrem listkey 0 a # 删除列表中所有值 a
lrem listkey -1 c # 从右侧删除 1 个 c
ltrim listkey 1 4 # 只保留下标 1~4 的元素
7.3 查询操作
bash
lrange key start end # 获取指定索引范围所有 item(包含 end),O(n)
lindex key index # 获取指定索引的 item,O(n)
llen key # 获取列表长度,O(1)
示例:
bash
lrange listkey 0 2 # 下标 0~2
lrange listkey 1 -1 # 从第 1 个到倒数第 1 个
lindex listkey 0
lindex listkey -1
7.4 修改操作
bash
lset key index newValue # 设置指定索引的值,O(n)
lset listkey 2 ppp # 把第 2 个位置设为 ppp
7.5 阻塞式操作 blpop / brpop
bash
blpop key timeout # LPOP 的阻塞版本,timeout 为超时时间,0 表示一直阻塞
brpop key timeout # RPOP 的阻塞版本,timeout 为超时时间,0 表示一直阻塞
7.6 实战:用 List 组合出常见结构
bash
# 实现栈(先进后出):lpush + lpop
# 实现队列(先进先出):lpush + rpop
# 固定大小的列表(只保留最近 N 条):lpush + ltrim
# 阻塞式消息队列(生产者-消费者):lpush + brpop
实现时间轴(timeLine) :把关注的人的微博按时间顺序 lpush 进列表,即可按时间轴展示。
八、集合(Set)类型
Set 是无序、去重的字符串集合,适合做标签、点赞、抽奖、共同好友等。
8.1 基础命令
bash
sadd key element # 向集合添加元素,重复添加失败,O(1)
srem key element # 移除元素,O(1)
scard key # 计算集合大小
sismember key element # 判断元素是否在集合中
srandmember key count # 随机取出 count 个元素,不破坏集合
spop key # 随机弹出一个元素并移除
smembers key # 获取所有元素(无序),O(n),小心使用会阻塞
8.2 集合运算
bash
sdiff key1 key2 # 差集:在 key1 中但不在 key2 中
sinter key1 key2 # 交集:同时在两个集合中
sunion key1 key2 # 并集:两个集合所有元素
把结果保存到新集合:
bash
sdiffstore dest key1 key2 # 差集存入 dest
sinterstore dest key1 key2 # 交集存入 dest
sunionstore dest key1 key2 # 并集存入 dest
8.3 集合典型应用
- 抽奖系统 :
spop弹出用户 id;活动取消直接删除集合。 - 点赞 / 点踩 / 喜欢:用户点了赞,就把用户 id 放到该条记录的集合中。
- 标签 :给用户/文章打标签
sadd user:1:tags 标签1 标签2;也可反向保存"关注某标签的人有哪些"。 - 共同好友:使用集合的交集 / 并集 / 差集计算。
总结:
sadd:标签相关。spop/srandmember:随机数相关(抽奖、随机推荐)。sadd/sinter:社交相关(共同好友、共同关注)。
九、面试题与参考答案
1. HTTP 协议详情、版本、请求头
HTTP 协议:基于 TCP 的"请求-响应"式无状态应用层协议,客户端发起请求,服务端返回响应。
版本:
HTTP/1.0:短连接,每次请求都要新建连接。HTTP/1.1:持久连接(keep-alive)、管线化、chunked分块传输;解决队头阻塞能力有限。HTTP/2:二进制分帧、多路复用、头部压缩(HPACK)、服务端推送,解决队头阻塞。HTTP/3:基于 QUIC(底层 UDP),进一步降低连接建立延时。
常见请求头:
text
Host # 请求主机
User-Agent # 客户端标识
Accept # 可接受的响应类型
Accept-Encoding # 可接受的压缩编码
Content-Type # 请求体类型(如 application/json)
Content-Length # 请求体长度
Authorization # 认证信息 / Token
Cookie # 会话 Cookie
Cache-Control # 缓存策略
Connection # 连接控制(keep-alive / close)
Referer # 来源页面
Origin # 跨域来源
X-Forwarded-For # 经过代理的客户端 IP
常见响应头 :Content-Type、Content-Length、Set-Cookie、Cache-Control、Location、Server、Date 等。
2. 如何实现服务器给客户端发送消息,WebSocket 是什么
普通的 HTTP 是"客户端主动请求、服务端被动响应",无法直接由服务端主动推消息。实现服务端推送的常见方式:
- 轮询(Polling):客户端定时请求,浪费资源。
- 长轮询(Long Polling):服务端挂起请求直到有新消息,减少请求次数。
- SSE(Server-Sent Events):服务端单向推送,基于 HTTP。
- WebSocket:推荐方案。
WebSocket 是什么 :一种基于 TCP 的全双工 通信协议。客户端通过 HTTP 发起握手(Upgrade: websocket),升级后建立一条持久连接,双方可随时互相发消息,适合聊天、实时通知、在线游戏、实时行情等场景。
常见实现:Django Channels、FastAPI WebSocket、Node.js 的 Socket.IO 等。
3. 悲观锁和乐观锁,如何实现
- 悲观锁 :假设一定会发生冲突,访问数据前先加锁,其他线程/事务被阻塞。实现:MySQL
SELECT ... FOR UPDATE、Redis 分布式锁(SET NX)。 - 乐观锁 :假设很少冲突,不加锁,提交时检查版本号是否变化,变了则重试。实现:MySQL 版本号字段(
version)、Redis 使用WATCH+MULTI+EXEC的乐观事务。
在 Redis 中的实现方式:
bash
# 悲观锁(分布式锁):key 不存在才写入,并设置过期时间
SET lock:order 1 NX EX 5
DEL lock:order
# 乐观锁:WATCH 监视 key,在事务期间若被修改,则 EXEC 返回失败
WATCH stock:1
MULTI
DECR stock:1
EXEC
UNWATCH