String、Hash、List、Set 四大数据结构与通用命令

一、面试复习:通信、锁与代理

这部分是前几天面试题的展开讲解,也是写高并发程序的基础。

1.1 什么是 IPC,如何进行进程间通信

IPC 是 Inter-Process Communication(进程间通信)的缩写。之所以需要进程间通信,是因为进程之间数据是隔离的

两种情况:

  • 同一台机器上的两个进程通信(如同一个程序开启的多个进程,Python 多进程)。
  • 不同机器上的两个进程通信(跨主机通信)。

如何通信:

方式 说明
共享内存 / 共享变量 线程间变量天然共享,进程间需借助共享内存
队列 / 管道 如 Python multiprocessing.QueuePipe
消息队列 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              # 记录/监控操作日志,会一直输出,注意会夯住终端

生产环境请谨慎使用 flushallflushdbmonitor。其中 monitor 会持续打印所有命令,影响性能且占用终端。

管理 Redis 实例的 Web 项目(可选参考):


四、字符串(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               # 释放锁

注意:SETNXEXPIRE 分开写不是原子的,可能出现"已 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)

nget 与一次 mget 的差别:

  • nget = 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(简单动态字符串),根据长度有 intembstrraw 三种编码,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 为什么重要

算法和数据结构是编程的基石:

  • 算法 :代码的执行逻辑和流程,如 forif,例如排序。
  • 数据结构 :变量的组织结构,如 listmapset

写 Redis 时,理解每条命令的时间复杂度,能避免在数据量巨大时把服务"打停"。

6.2 大 O 表示法

记号 含义 例子
O(1) 常数时间,与数据量无关 GETSETHSETLPUSHSADD
O(log n) 对数时间 ZSetZADD / ZRANGEBYSCORE(底层跳表)
O(n) 线性时间,随数据量线性增长 KEYSSMEMBERSHGETALLLRANGE(全量)
O(n²) 平方时间 某些嵌套遍历算法

时间复杂度 :消耗的时间;空间复杂度:消耗的内存。

6.3 生产环境注意点

  • keyssmembershgetall 等全量命令是 O(n),key 很多时会阻塞 Redis。
  • scanhscansscan 代替全量命令,逐批游标式遍历,不阻塞。
  • 单条命令尽可能快;批量操作 mgetmsetpipeline 减少网络往返。

想刷题可参考力扣(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)

lremcount 规则:

  • 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 集合典型应用

  1. 抽奖系统spop 弹出用户 id;活动取消直接删除集合。
  2. 点赞 / 点踩 / 喜欢:用户点了赞,就把用户 id 放到该条记录的集合中。
  3. 标签 :给用户/文章打标签 sadd user:1:tags 标签1 标签2;也可反向保存"关注某标签的人有哪些"。
  4. 共同好友:使用集合的交集 / 并集 / 差集计算。

总结:

  • 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-TypeContent-LengthSet-CookieCache-ControlLocationServerDate 等。

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
相关推荐
IT大白鼠1 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz1 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙1 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除1 天前
redis--018
redis
两锭子1 天前
Redis基础
redis
烟沙九洲1 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索1 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞1 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路2 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog2 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis