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
相关推荐
2601_962071172 小时前
八. Spring Boot2 整合连接 Redis(超详细剖析)
spring boot·redis·后端
程序员夏洛4 小时前
Redis 数据过期后的删除策略是什么?
数据库·redis·缓存
鱼鳞_4 小时前
热门八股-Redis
数据库·redis·缓存
程序员夏洛5 小时前
Redis 的持久化机制有哪些?
java·数据库·redis
hweiyu006 小时前
Redis命令:MIGRATE
redis·缓存
A.说学逗唱的Coke7 小时前
【人工智能专题】Redis 杀进 AI 数据层:向量搜索、语义缓存与 Agent 记忆工程从入门到踩坑
人工智能·redis·缓存
XiYang-DING8 小时前
地图城市缓存优化:ApplicationReadyEvent + Caffeine + Redis + Redisson 分布式锁
redis·分布式·缓存
爱和冰阔落10 小时前
【Linux】多线程打印为什么会乱?pthread 创建、等待、退出、取消与分离全实战
linux·运维·c++·redis
一只小bit13 小时前
Redis哨兵机制、主从复制与集群搭建与维护
数据库·redis·bootstrap