一、Redis 核心问题
1. 为什么使用 Redis?
| 优势维度 | 具体说明 |
|---|---|
| 高性能 | 内存级操作,响应速度达微秒级,有效缓解数据库查询压力 |
| 多功能 | 支持 String/Hash/List/ZSet 等丰富数据结构,适配缓存、计数器、分布式锁等场景 |
| 高可用 | 主从复制 + 哨兵 + 集群架构,保障服务不间断运行 |
| 原子性 | 天然支持 INCR、SET NX 等原子操作,无需额外加锁实现并发控制 |
2. Redis 为什么快?
- 内存存储:核心数据驻留内存,无磁盘 IO 延迟;
- 单线程模型:避免多线程上下文切换、锁竞争等开销;
- IO 多路复用:基于 epoll(Linux)/kqueue(BSD),单线程处理海量客户端连接;
- 高效数据结构:String 用动态字符串、Hash 用压缩列表,操作复杂度低;
- 异步 IO:持久化、网络发送等 IO 操作异步执行,不阻塞主线程。
3. 追问 1:Redis 的 IO 操作包含哪些?
| IO 类型 | 具体操作 |
|---|---|
| 网络 IO | 客户端连接(accept)、请求读取(read)、响应发送(write) |
| 磁盘 IO | 持久化(RDB 快照写入、AOF 日志追加)、数据加载(启动读 RDB/AOF)、过期键删除 |
4. 追问 2:Redis 用什么让 IO 更快?
| IO 类型 | 优化手段 |
|---|---|
| 网络 IO | 1. IO 多路复用(epoll+ET 模式);2. 非阻塞 IO;3. 批量处理请求 |
| 磁盘 IO | 1. 异步刷盘(AOF 分每秒刷/事务提交刷/不刷三种策略);2. 内存映射(mmap)减少 RDB 拷贝;3. 顺序写(AOF/RDB 均为顺序写,比随机写快 10 倍以上) |
二、Go & Java 对比
1. 优势对比
| 维度 | Go | Java |
|---|---|---|
| 性能 | 编译型语言,接近 C,无 JVM 开销 | 解释型(JIT 编译),性能略低 |
| 并发 | 轻量级协程(Goroutine),百万级并发 | 重量级线程(依赖 OS 线程),受线程数限制 |
| 语法 | 简洁,少封装,易上手 | 面向对象完善,生态丰富 |
| 生态 | 云原生/中间件(Docker/K8s)强 | 全场景生态(电商/金融/企业级) |
| 部署 | 编译为单二进制文件,无依赖 | 需 JRE,依赖多 |
2. 跨平台实现
- Go:编译时通过跨平台编译器,指定 GOOS/GOARCH 生成对应系统(Linux/Windows/Mac)的二进制文件,运行时无依赖;
- Java:一次编译生成字节码(.class),JVM 针对不同系统实现字节码解释器/编译器,实现"一次编译,到处运行"。
三、场景题
1. 扫码登录(防篡改)
核心流程
| 步骤 | 操作说明 |
|---|---|
| 生成二维码 | 1. 服务端生成 UUID 作为二维码 ID,关联"未登录状态+5 分钟过期时间"存入 Redis;2. 二维码内容包含:二维码 ID + 服务端域名 + 时间戳 |
| 用户扫码授权 | 1. 手机端扫码获取二维码 ID,携带用户 token 调用登录接口;2. 服务端验证 token,更新 Redis 中二维码 ID 状态为"已授权"并关联用户 ID |
| PC 端轮询验证 | 1. PC 端每秒轮询查询二维码 ID 状态;2. 状态为"已授权"时生成 PC 端登录 token 并返回 |
| 登录完成 | PC 端携带 token 访问资源,服务端验证 token 有效性 |
防篡改措施
- 二维码加签:HMAC-SHA256 对"二维码 ID+时间戳+密钥"签名,手机端扫码后验签;
- 传输加密:全程 HTTPS 防止中间人篡改;
- 一次性验证:二维码 ID 登录成功后立即失效;
- 防重放:时间戳+RSA 非对称加密,服务端验证 30 秒超时。
2. 社区关注/被关注功能设计
核心模块
| 模块名 | 核心职责 |
|---|---|
| 关系管理模块 | 1. 关注/取消关注;2. 查询关注列表/粉丝列表;3. 检查是否已关注 |
| 存储模块 | 1. 数据库:user_follow(id, user_id, follow_id, create_time);2. Redis 缓存关注数/粉丝数(ZSet/HSet) |
| 通知模块 | 关注后基于 MQ 异步推送"XX 关注了你"通知 |
| 权限模块 | 限制高频操作(如 1 分钟最多关注 10 人),防止刷量 |
| 统计模块 | Redis 计数器(INCR/DECR)实时统计关注数,定时同步到 DB |
设计细节
- 数据库:user_follow 表建立复合索引(user_id+follow_id),防止重复关注;
- 性能优化:关注/粉丝列表分页查询,Redis 缓存前 100 条数据;
- 高并发:关注操作加分布式锁,避免重复写入。
四、Redis GEO
1. 核心原理
基于 GeoHash 算法,将经纬度编码为字符串,支持距离计算与范围查询。
2. 核心命令
| 命令格式 | 功能 | 示例 |
|---|---|---|
| GEOADD key lon lat member | 添加地理位置 | GEOADD driver:location 116.403874 39.914885 driver1 |
| GEODIST key m1 m2 [unit] | 计算成员距离 | GEODIST driver:location driver1 driver2 km |
| GEORADIUS key lon lat radius unit [WITHCOORD] [WITHDIST] [COUNT] | 按坐标查范围内成员 | GEORADIUS driver:location 116.403874 39.914885 5 km WITHCOORD WITHDIST COUNT 10 |
| GEORADIUSBYMEMBER key member radius unit ... | 按成员查范围内其他成员 | GEORADIUSBYMEMBER driver:location driver1 3 km COUNT 5 |
| GEOHASH key member... | 获取 GeoHash 编码 | GEOHASH driver:location driver1 |
3. 实战场景
- 滴滴打车:GEORADIUS 查询用户 5 公里内可用司机;
- 外卖配送:GEODIST 计算骑手与商家/用户距离;
- 本地生活:GEORADIUSBYMEMBER 查询店铺 3 公里内用户。
4. 注意事项
- 精度:GeoHash 精度 1-10 米,不适合高精度定位;
- 数据结构:底层是 Sorted Set,删除用 ZREM 命令;
- 性能:单 Key 成员数≤10 万,超量按区域拆分(如 driver:location:beijing)。
五、数据库相关
1. MySQL & MongoDB 区别
| 维度 | MySQL(关系型) | MongoDB(文档型 NoSQL) |
|---|---|---|
| 数据模型 | 结构化(表+行+列),预定义 Schema | 非结构化(BSON 文档),Schema 灵活 |
| 事务支持 | 支持 ACID(InnoDB),强一致性 | 4.0+ 支持多文档事务,默认最终一致性 |
| 查询能力 | 支持复杂 SQL(JOIN/子查询/聚合) | 支持简单聚合,不支持复杂 JOIN |
| 索引类型 | 主键/普通/联合/唯一/全文索引 | 单字段/复合/地理空间/文本索引 |
| 适用场景 | 结构化数据(订单/用户/财务),需强事务 | 非结构化数据(日志/商品详情/用户画像),高写入 |
| 扩展性 | 垂直扩展为主,水平扩展需分库分表 | 原生支持分片集群,水平扩展灵活 |
2. 关系型数据库命名原因
- 数据存储为二维表结构,表间通过主键-外键建立"关系",符合集合论中"关系"的数学定义;
- 遵循 ACID 事务特性,保证数据可靠性;
- 支持 SQL 结构化查询,标准化操作数据。
3. 并发事务问题(脏读/不可重复读/幻读)
| 问题类型 | 定义 | 示例 |
|---|---|---|
| 脏读 | 读取到未提交事务的数据,回滚后失效 | 事务 A 改余额为 1000(未提交),事务 B 读取,A 回滚后 B 读的是脏数据 |
| 不可重复读 | 同一事务内多次读同一数据,结果不一致 | 事务 A 首次读余额 500,事务 B 改余额为 800 并提交,A 再次读为 800 |
| 幻读 | 同一事务内多次范围查询,行数不一致 | 事务 A 查余额>500 的用户有 3 人,事务 B 插入 1 人并提交,A 再次查为 4 人 |
4. 可重复读(MySQL 默认隔离级别)
核心定义
同一事务内多次读取同一数据/范围数据,结果始终一致,不受其他并发事务修改影响。
实现细节
- MVCC 多版本控制 :事务启动生成 Read View,仅能看到 Read View 生成前已提交的数据;
- 示例:事务 A(ID=100)生成 Read View,事务 B(ID=101)修改数据并提交,A 仍读旧数据;
- 锁机制:行锁防止修改,间隙锁(范围查询时)防止插入,解决大部分幻读问题。
场景验证
sql
-- 事务 A
BEGIN;
SELECT balance FROM user WHERE id=1; -- 结果 500
-- 事务 B
BEGIN;
UPDATE user SET balance=800 WHERE id=1;
COMMIT;
-- 事务 A
SELECT balance FROM user WHERE id=1; -- 结果仍为 500
COMMIT;
SELECT balance FROM user WHERE id=1; -- 结果 800
六、编程题
32 位有符号整数反转
java
public class ReverseInteger {
public int reverse(int x) {
int res = 0;
while (x != 0) {
// 弹出最后一位
int digit = x % 10;
x = x / 10;
// 预判溢出
if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && digit > 7)) {
return 0;
}
if (res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && digit < -8)) {
return 0;
}
// 拼接数字
res = res * 10 + digit;
}
return res;
}
// 测试用例
public static void main(String[] args) {
ReverseInteger solution = new ReverseInteger();
System.out.println(solution.reverse(123)); // 321
System.out.println(solution.reverse(-123)); // -321
System.out.println(solution.reverse(120)); // 21
System.out.println(solution.reverse(2147483647)); // 0
}
}
代码解释
- 弹出数字:
x%10取最后一位,x/10去除最后一位(负数除法仍为负); - 溢出预判:通过
res > MAX/10或res < MIN/10提前判断,避免直接溢出; - 拼接数字:无溢出则更新结果,循环至
x=0。
岛屿数量(Go BFS 版)
go
func numIslands(grid [][]byte) int {
if len(grid) == 0 {
return 0
}
count := 0
rows, cols := len(grid), len(grid[0])
dirs := [][]int{{-1,0}, {1,0}, {0,-1}, {0,1}} // 上下左右
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
if grid[i][j] == '1' {
count++
// BFS 队列
queue := [][]int{{i,j}}
grid[i][j] = '0' // 标记已访问
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
// 遍历四个方向
for _, dir := range dirs {
x, y := curr[0]+dir[0], curr[1]+dir[1]
if x >=0 && y >=0 && x < rows && y < cols && grid[x][y] == '1' {
queue = append(queue, []int{x,y})
grid[x][y] = '0'
}
}
}
}
}
}
return count
}
面试加分点
- 时间复杂度:O(M*N)(M 行 N 列,每个格子仅访问一次);
- 空间复杂度:DFS 为 O(M*N)(递归栈),BFS 为 O(min(M,N))(队列大小);
- 拓展:超大网格可改用并查集,避免递归栈溢出。