目录
一、NoSQL 是什么?为什么我们需要 NoSQL?
• NoSQL 的本质:非关系型数据库的核心理念 • 关系型数据库的局限性:高并发、灵活 schema、海量数据存储的挑战 • NoSQL 的优劣对比:高性能 vs 弱一致性、灵活扩展 vs 事务支持
二、Redis 入门与核心机制
• Redis 的核心数据结构:String/Hash/List/Set/ZSet • Java 整合实战:Spring Boot + Lettuce/Redisson 客户端配置 • 第一个 Redis 项目:实现分布式 Session 管理
三、Redis 进阶:企业级应用场景
• 缓存穿透/击穿/雪崩解决方案:布隆过滤器、空值缓存、熔断机制 • 分布式锁实现:Redisson 的 RLock
与看门狗机制 • 热 Key 处理:本地缓存 + 随机过期时间
四、Redis 高可用与生产实战
• 持久化机制:RDB 快照 vs AOF 日志的选型策略 • 集群方案:Redis Cluster 分片与数据迁移 • 生产问题排查:内存分析(memory usage
)、慢查询日志
五、MongoDB 入门与文档模型
• 文档数据库核心概念:BSON 格式、集合与文档 • Java 整合实战:Spring Data MongoDB + @Document
注解 • 第一个 MongoDB 项目:电商商品详情存储
六、MongoDB 进阶:查询与聚合
• 复杂查询:嵌套文档查询、数组操作($elemMatch
) • 聚合管道:$match
/$group
/$project
实战 • 索引优化:覆盖索引、TTL 索引自动清理
七、MongoDB 高可用与分片策略
• 副本集原理:选举机制与数据同步 • 分片集群配置:哈希分片 vs 范围分片 • 生产调优:连接池配置、写入关注(Write Concern)
八、Elasticsearch 入门与搜索原理
• 倒排索引机制:全文检索的核心原理 • Java 整合实战:Spring Data Elasticsearch + @Query
注解 • 第一个搜索项目:商品多条件筛选实现
九、Elasticsearch 进阶:分析与优化
• 中文分词:IK 分词器配置与自定义词典 • 聚合分析:指标聚合(avg/max)与分桶聚合(terms) • 性能调优:分片策略、_source
字段控制
十、NoSQL 与关系型数据库协同架构
• 混合架构设计:MySQL + Redis 缓存加速 • 数据同步方案:Canal 监听 binlog 同步至 Elasticsearch • 一致性保障:本地消息表 + 最大努力通知
十一、NoSQL 面试题精选
• Redis • 如何用 Redis 实现分布式锁?有哪些注意事项? • Redis 集群数据分片原理是什么? • MongoDB • 分片键的选择标准是什么? • 如何设计嵌套文档避免数据冗余? • Elasticsearch • 倒排索引和正排索引的区别? • 深分页问题如何解决?(Search After vs Scroll API)
附录:工具与资源推荐
• 开发工具 • Redis 可视化:RedisInsight • Elasticsearch 调试:Kibana Dev Tools • 学习资源 • 书籍:《Redis 设计与实现》 • 课程:慕课网《Elasticsearch 核心技术与实战》 • 云服务方案 • 阿里云 Redis(Tair 持久内存版) • 腾讯云 MongoDB 分片集群
一、NoSQL 是什么?为什么我们需要 NoSQL?
NoSQL 的本质
定义:NoSQL(Not Only SQL)是非关系型数据库的统核心目标是解决关系型数据库(如 MySQL)在高并发、海量数据、灵活数据结构场景下的局限性。 核心理念:
-
去 schema 化:无需预先定义表结构(如 MongoDB 的文档动态扩展)。
-
分布式架构:天然支持水平扩展(如 Redis Cluster 自动分片)。
-
场景驱动设计:针对特定场景优化(如 Redis 内存优先、Elasticsearch 全文检索)。
Java 开发者视角: • 用 Redis 缓存热点数据(如商品详情),减少 MySQL 压力。 • 用 MongoDB 存储动态 JSON 数据(如用户行为日志),避免频繁修改表结构。
关系型数据库的局限性
1. 高并发下的性能瓶颈
• 案例:电商秒杀场景中,MySQL 的行级锁和事务开销导致吞吐量骤降。 • 解决方案: • 用 Redis 预减库存(内存操作,TPS 可达 10万+/秒)。
// Spring Boot + Redis 实现秒杀扣库存 public boolean seckill(Long productId) {String key = "stock:" + productId;Long stock = redisTemplate.opsForValue().decrement(key);return stock != null && stock >= 0; }
2. 海量数据存储与扩展难题
• 问题:单表数据过亿时,MySQL 查询性能指数级下降,分库分表复杂且易出错。 • 对比方案: • MongoDB 分片集群:自动拆分数据到多个节点,写入可线性扩展。 • Elasticsearch 分布式索引:TB 级日志数据实时检索。
3. 动态 Schema 的维护成本
• 场景:用户画像系统需频繁增减字段(如新增“兴趣爱好”标签)。 • 关系型痛点:ALTER TABLE
修改表结构导致锁表、服务停机。 • NoSQL 优势:MongoDB 直接插入新字段文档,无需停机。
NoSQL 的优劣对比
维度 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
性能 | 内存操作(Redis)或分布式架构(MongoDB)实现高吞吐 | 弱一致性(如 MongoDB 默认最终一致性) | 缓存、计数器、实时统计 |
扩展性 | 水平扩展简单(加节点即可扩容) | 跨节点事务支持弱(需依赖外部方案) | 大数据存储(用户日志) |
数据结构灵活性 | 支持文档、图、键值等多样化数据模型 | 复杂关联查询能力弱(如 MongoDB 无 JOIN) | 动态 Schema(商品属性) |
事务支持 | 部分支持(如 Redis 6.0 的 MULTI-EXEC) | 无法完全替代关系型数据库的 ACID 事务 | 金融核心系统仍需 MySQL |
为什么 Java 后端必须掌握 NoSQL?
-
企业架构标配: • 互联网大厂架构 = MySQL + Redis + Elasticsearch(如订单系统用 MySQL 存储,Redis 抗并发,ES 做搜索)。
-
面试高频考点: • Redis 的分布式锁、缓存穿透/击穿/雪崩解决方案(90% 的面试会问)。 • MongoDB 的副本集选举机制、Elasticsearch 的倒排索引原理。
-
性能优化刚需: • 单靠 MySQL 无法支撑万级 QPS,必须通过 Redis 缓存、MongoDB 分片分流。
二、Redis 入门与核心机制
1. Redis 的核心数据结构与 Java 操作
Redis 的战斗力源于其 5 大核心数据结构,Java 开发者需掌握其特性与 API 用法:
数据结构 | 特性与场景 | Java 操作示例(Spring Boot) |
---|---|---|
String | 文本、计数器(阅读量/库存) | redisTemplate.opsForValue().set("key", 100) |
Hash | 对象存储(用户信息、购物车) | redisTemplate.opsForHash().put("user:1", "name", "Jack") |
List | 消息队列(LPUSH+BRPOP 实现阻塞队列) | redisTemplate.opsForList().leftPush("queue", task) |
Set | 去重集合(共同关注/抽奖黑名单) | redisTemplate.opsForSet().add("blacklist", "userA") |
ZSet | 排行榜(分数排序) | redisTemplate.opsForZSet().add("rank", "player1", 95) |
面试考点: • ZSet 底层结构:跳跃表(SkipList) + 哈希表,实现 O(logN复杂度插入与查询。 • List 做消息队列的缺陷:无 ACK 机制,需自行实现消息可靠性(推荐改用 RabbitMQ/Kafka)。
2. Spring Boot 整合 Redis 实战
步骤 1:依赖与配置
<!-- pom.xml --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId> <!-- 默认使用 Lettuce 客户端 --> </dependency>
# application.yml spring:redis:host: 127.0.0.1port: 6379password: 123456lettuce:pool:max-active: 8 # 连接池最大连接数(根据 CPU 核数调整)
步骤 2:注入 RedisTemplate
@Autowired private RedisTemplate<String, Object> redisTemplate; // 设置序列化方式(默认 JDK 序列化可读性差,建议改为 JSON) @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);template.setKeySerializer(RedisSerializer.string());template.setValueSerializer(RedisSerializer.json());return template; }
步骤 3:分布式 Session 实战
// 启用 Redis 存储 Session @Configuration @EnableRedisHttpSession public class RedisSessionConfig { } // 登录时存储用户信息到 Session @PostMapping("/login") public String login(HttpSession session,) {session.setAttribute("currentUser", user);return "Login Success!"; } // 其他服务读取 Session(微服务架构共享 Session) @GetMapping("/profile") public User profile(HttpSession session) {return (User) session.getAttribute("currentUser"); }
生产经验: • Session 过期时间:通过 maxInactiveIntervalInSeconds
配置(默认 30 分钟)。 • 安全建议:敏感信息(如密码)避免存入 Session,改用 Token 机制。
3. Redis 核心机制解析
3.1 单线程模型
• 为何单线程还能高性能?
-
纯内存操作(纳秒级响应)。
-
非阻塞 I/O 多路复用(epoll 机制)。
-
无锁竞争(避免线程切换开销)。
• 单线程的副作用: • 长命令阻塞:如 KEYS *
扫描全库,需用 SCAN
替代。 • CPU 瓶颈:多核服务器建议部署多个 Redis 实例。
3.2 持久化机制
机制 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
RDB | 定时生成内存快照(二进制文件) | 文件小,恢复速度快 | 可能丢失最后一次快照后的数据 | 灾备恢复、主从复制 |
AOF | 记录所有写操作命令(追加日志) | 数据丢失少(可配置同步策略) | 文件大,恢复慢 | 高数据安全要求场景 |
配置建议(redis.conf):
# 开启混合持久化(Redis 4.0+) aof-use-rdb-preamble yes # RDB 每 5 分钟至少 1 次修改触发 save 300 1 # AOF 每秒同步 appendfsync everysec
4. 生产级避坑指南
• 内存优化: • 避免存储大 Value(如 10MB 的 String),拆分多个小 Key。 • 使用 HASH
替代多个独立 String
(减少 Key 数量)。 • 连接池配置: • Lettuce 连接数公式:max-active = 最大并发数 / 平均命令耗时
。 • 监控指标:redisTemplate.getRequiredConnectionFactory().getMetrics()
。
三、Redis 进阶:企业级应用场景
1. 缓存穿透/击穿/雪崩解决方案
定义与区别:
问题类型 | 触发场景 | 核心解决思路 | Java 实现方案 |
---|---|---|---|
缓存穿透 | 大量请求查询不存在的数据(如非法ID) | 拦截无效请求 | 布隆过滤器(Redisson RBloomFilter ) |
缓存击穿 | 热点 Key 过期后瞬间高并发请求 | 防止并发重建缓存 | 分布式锁(Redisson RLock ) + 互斥重建 |
缓存雪崩 | 大量 Key 同时过期或 Redis 宕机 | 分散过期时间 + 降级策略 | 随机过期时间 + 熔断机制(Hystrix/Sentinel) |
实战代码示例:
// 布隆过滤器拦截非法请求 public boolean checkBloomFilter(String key) { RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("productFilter"); bloomFilter.tryInit(100000L, 0.01); // 预期数据量 10万,误判率 1% return bloomFilter.contains(key); } // 分布式锁互斥重建缓存 public String getData(String key) { String value = redisTemplate.opsForValue().get(key); if (value == null) { RLock lock = redissonClient.getLock(key + ":lock"); try { if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { value = db.query(key); // 查询数据库 redisTemplate.opsForValue().set(key, value, 300 + (int)(Math.random() * 60), TimeUnit.SECONDS); } } finally { lock.unlock(); } } return value; }
避坑指南: • 布隆过滤器误判率:需根据数据量调整参数,避免误判导致正常请求被拦截。 • 锁超时时间:锁自动释放时间需大于业务执行时间,防止并发漏洞。
2. 分布式锁实现(Redisson 核心机制)
Redisson 分布式锁特性: • 自动续期:看门狗线程每隔 10 秒检查锁状态并续期(默认锁过期时间 30 秒)。 • 可重入性:同一线程可重复获取锁,避免死锁。 • 高可用:支持 Redis 单机、哨兵、集群模式。
Java 实现秒杀扣库存:
public boolean seckill(Long productId) { String lockKey = "seckill:lock:" + productId; RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待 100ms,锁自动释放时间 30s if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) { int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock:" + productId)); if (stock > 0) { redisTemplate.opsForValue().decrement("stock:" + productId); return true; } } } finally { lock.unlock(); } false; }
生产问题排查: • 看门狗线程阻塞:避免在锁代码块内执行耗时操作(如复杂计算或)。 • 网络分区风险:Redis 集群脑裂时可能导致锁失效,需配合 Redlock 算法(争议较大,谨慎使用)。
3. 热 Key 处理方案
热 Key 检测手段: • Redis 内置命令:redis-cli --hotkeys
(需开启 maxmemory-policy
为 LFU)。 • 监控工具:阿里云 Redis 控制台、自研监控脚本(统计 Key 访问频率)。
解决方案:
-
本地缓存:使用 Caffeine 缓存热点数据,降低 Redis 压力。
// Spring Boot 整合 Caffeine @Bean public Cache<String, Object> localCache() { return Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) // 短暂过期,避免脏数据 .maximumSize(1000) .build(); } // 查询逻辑 public Object getData(String key) { Object value = localCache.getIfPresent(key); if (value == null) { value = redisTemplate.opsForValue().get(key); localCache.put(key, value); } return value; }
-
随机过期时间:分散 Key 过期时间,避免集中失效。
redisTemplate.opsForValue().set(key, value, 30 + (int)(Math.random() * 10), TimeUnit.SECONDS);
-
读写分离:Redis Cluster 中从节点分担读压力(需开启
readFrom=REPLICA
)。
面试考点: • 本地缓存一致性:如何保证本地缓存与 Redis 数据一致?(答案:设置短过期时间 + 监听 Redis 更新事件) • 热 Key 限流:结合 Sentinel 或网关对热 Key 请求限流。
4. 企业级方案总结
场景 | 技术选型 | 注意事项 |
---|---|---|
缓存穿透 | 布隆过滤器 + 空值缓存 | 空值需设置短暂过期时间(如 60 秒) |
高并发锁竞争 | Redisson 可重入锁 | 锁粒度控制(避免锁整个大对象) |
热 Key 性能瓶颈 | 本地缓存 + 集群分片 | 本地缓存需考虑 JVM 内存压力 |
三、Redis 进阶:企业级应用场景
1. 缓存穿透/击穿/雪崩解决方案
定义与区别
问题类型 | 触发场景 | 核心解决思路 | Java 实现方案 |
---|---|---|---|
缓存穿透 | 大量请求查询不存在的数据(如非法ID) | 拦截无效请求 | 布隆过滤器(Redisson RBloomFilter ) |
缓存击穿 | 热点 Key 过期后瞬间高并发请求 | 防止并发重建缓存 | 分布式锁(Redisson RLock ) + 互斥重建 |
缓存雪崩 | 大量 Key 同时过期或 Redis 宕机 | 分散过期时间 + 降级策略 | 随机过期时间 + 熔断机制(Hystrix/Sentinel) |
实战代码示例
// 布隆过滤器拦截非法请求 public boolean checkBloomFilter(String key) { RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("productFilter"); bloomFilter.tryInit(100000L, 0.01); // 预期数据量 10万,误判率 1% return bloomFilter.contains(key); } // 分布式锁互斥重建缓存 public String getData(String key) { String value = redisTemplate.opsForValue().get(key); if (value == null) { RLock lock = redissonClient.getLock(key + ": try { if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { value = db.query(key); // 查询数据库 redisTemplate.opsForValue().set(key, value, 300 + (int)(Math.random() * 60), TimeUnit.SECONDS); } } finally { lock.unlock(); } } return value; }
避坑指南
• 布隆过滤器误判率:需根据数据量调整参数,避免误判导致正常请求被拦截。 • 锁超时时间:锁自动释放时间需大于业务执行时间,防止并发漏洞。
2. 分布式锁实现(Redisson 核心机制)
Redisson 分布式锁特性
• 自动续期:看门狗线程每隔 10 秒检查锁状态并续期(默认锁过期时间 30 秒)。 • 可重入性:同一线程可重复获取锁,避免死锁。 • 高可用:支持 Redis 单机、哨兵、集群模式。
Java 实现秒杀扣库存
public boolean seckill(Long productId) { String lockKey = "seckill:lock:" + productId; RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待 100ms,锁自动释放时间 30s if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) { int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock:" + productId)); if (stock > 0) { redisTemplate.opsForValue().decrement("stock:" + productId); return true; } } } finally { lock.unlock(); } return false; }
生产问题排查
• 看门狗线程阻塞:避免在锁代码块内执行耗时操作(如复杂计算或同步 IO)。 • 网络分区风险:Redis 集群脑裂时可能导致锁失效,需配合 Redlock 算法(争议较大,谨慎使用)。
3. 热 Key 处理方案
热 Key 检测手段
• Redis 内置命令:redis-clikeys
(需开启 maxmemory-policy
为 LFU)。 • 监控工具:阿里云 Redis 控制台、自研监控脚本(统计 Key 访问频率)。
解决方案
-
本地缓存:使用 Caffeine 缓存热点数据,降低 Redis 压力。
// Spring Boot 整合 Caffeine @Bean public Cache<String, Object> localCache() { return Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) // 短暂过期,避免脏数据 .maximumSize(1000) .build(); } // 查询逻辑 public Object getData(String key) { Object value = localCache.getIfPresent(key); if (value == null) { value = redisTemplate.opsForValue().get(key); localCache.put(key, value); } return value; }
-
随机过期时间:分散 Key 过期时间,避免集中失效。
redisTemplate.opsForValue().set(key, value, 30 + (int)(Math.random() * 10), TimeUnit.SECONDS);
-
读写分离:Redis Cluster 中从节点分担读压力(需开启
readFrom=REPLICA
)。
面试考点
• 本地缓存一致性:如何保证本地缓存与 Redis 数据一致?(答案:设置短过期时间 + 监听 Redis 更新事件) • 热 Key 限流:结合 Sentinel 或网关对热 Key 请求限流。
4. 企业级方案总结
场景 | 技术选型 | 注意事项 |
---|---|---|
缓存穿透 | 布隆过滤器 + 空值缓存 | 空值需设置短暂过期时间(如 60 秒) |
高并发锁竞争 | Redisson 可重入锁 | 锁粒度控制(避免锁整个大对象) |
热 Key 性能瓶颈 | 本地缓存 + 集群分片 | 本地缓存需考虑 JVM 内存压力 |
四、Redis 高可用与生产实战
1. 持久化机制:RDB vs AOF
核心机制对比
维度 | RDB | AOF |
---|---|---|
原理 | 定时生成内存快照(二进制文件) | 记录所有写操作命令(追加日志) |
优点 | 文件小、恢复速度快 | 数据丢失少(可配置同步策略) |
缺点 | 可能丢失最后一次快照后的数据 | 文件大、恢复慢 |
配置触发条件 | save 900 1 (15分钟至少1次修改) | appendfsync everysec (每秒同步) |
生产选型 | 灾备恢复、主从复制 | 金融级数据安全要求场景 |
Java 开发者配置建议(redis.conf):
# 开启混合持久化(Redis 4.0+ 推荐) aof-use-rdb-preamble yes # 每小时生成 RDB 快照 save 3600 1 # AOF 文件重写阈值(避免文件过大) auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
避坑指南: • 禁用 save ""
:若不需要 RDB,需明确禁用而非留空配置。 • AOF 重写阻塞:主进程重写期间可能阻塞写入,建议在从节点执行 BGREWRITEAOF
。
2. 集群方案:Redis Cluster 分片与迁移
核心原理
• 数据分片: • 16384 个哈希槽(Hash Slot)分配到多个节点。 • Key 通过 CRC16(key) % 16384
计算所属槽位。 • 高可用: • 每个分片由主节点 + 至少1个从节点组成。 • 主节点宕机时,从节点自动升级为主节点(需至少半数主节点存活)。
Java 客户端配置(Spring Boot + Lettuce):
spring: redis: cluster: nodes: 192.168.1.101:7001,192.168.1.102:7002 max-redirects: 3 # 最大重定向次数 lettuce: pool: max-active: 16
数据迁移实战:
# 将槽位 1000 从节点 A 迁移到节点 B redis-cli --cluster reshard 192.168.1.101:7001 # 输入目标节点 ID、迁移槽位数、源节点 ID(all 表示所有节点分摊)
生产经验: • 节点数量:至少3主3从,避免集群脑裂(分区容忍性)。 • 槽位分配:避免单节点负载过高,可通过 redis-cli --cluster rebalance
调整。
3. 生产问题排查:内存与性能分析
3.1 内存分析
关键命令:
# 查看 Key 内存占用 MEMORY USAGE user:1001 # 统计大 Key(单值 > 10KB) redis-cli --bigkeys # 分析内存碎片率 INFO memory | grep mem_fragmentation_ratio
内存优化方案: • 大 Key 拆分:Hash 拆分为多个子 Key,List 分页存储。 • 过期策略:主动清理 + 随机过期时间(避免集中淘汰)。
3.2 慢查询日志
配置与查看:
# 设置慢查询阈值(单位:微秒) CONFIG SET slowlog-log-slower-than 10000 # 查看最近10条慢查询 SLOWLOG GET 10
日志字段解析:
1) 1) (integer) 12 # 日志ID 2) (integer) 1690000000 # 时间戳 3) (integer) 12000 # 执行耗时(微秒) 4) 1) "KEYS" # 命令 2) "*"
典型问题处理: • KEYS/FLUSHALL:禁用高危命令,使用 SCAN
渐进式遍历。 • 复杂 Lua 脚本:避免执行超过 1 秒的脚本(阻塞主线程)。
4. 高可用架构选型总结
方案 | 适用场景 | 优缺点 | 国内云服务参考 |
---|---|---|---|
主从复制 | 读写分离、数据备份 | 简单易用,无自动故障转移 | 阿里云 Redis 基础版 |
哨兵模式 | 自动故障转移(高可用) | 额外资源消耗,配置复杂 | 腾讯云 Redis 哨兵版 |
Redis Cluster | 海量数据、水平扩展 | 原生分布式,运维成本高 | 华为云 GeminiDB Redis 接口 |
5. 面试高频考点
-
Redis 持久化如何选择? • 综合场景:混合持久化(RDB+AOF)。
-
集群脑裂问题如何解决? • 配置
min-slaves-to-write 1
(主节点至少需1个从节点同步)。 -
线上 Redis 内存突然飙升,如何排查? • 步骤:
INFO memory
→--bigkeys
→ 分析业务代码(是否存在循环写入)。
五、MongoDB 入门与文档模型
1. 文档数据库核心概念
1.1 BSON 格式
• 定义:BSON(Binary JSON)是 MongoDB 的存储格式,在 JSON 基础上扩展支持更多数据类型(如日期、二进制数据)。 • 对比 JSON:
// JSON { "price": 99.9, "created_at": "2023-07-20" } // BSON { "price": NumberDecimal("99.9"), "created_at": ISODate("2023-07-20T00:00:00Z") }
• Java 映射:通过 org.bson.Document
类或 Spring Data 的 @Document
注解实现 POJO 转换。
1.2 集合与文档
概念 | 关系型对应 | MongoDB 特性 |
---|---|---|
文档 | 行记录 | 动态 Schema(不同文档结构可不同) |
集合 | 表 | 无需预定义结构,文档自动归属集合 |
数据库 | 数据库 | 多个集合的容器 |
适用场景: • 电商商品详情(不同类目商品属性差异大)。 • 用户行为日志(动态增减埋点字段)。
2. Spring Boot 整合 MongoDB 实战
2.1 依赖与配置
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>
# application.yml spring: data: mongodb: uri: mongodb://user:password@127.0.0.1:27017/ecommerce # 国内云服务示例(阿里云) # uri: mongodb://user:password@docdb-xxx.mongodb.rds.aliyuncs.com:3717/admin?replicaSet=mgset-xxx
2.2 实体类与 Repository
// 商品详情实体类 @Document(collection = "products") public class Product { @Id private String id; private String name; private Map<String, Object> specs; // 动态规格参数(如颜色、尺寸) // Getter/Setter } // Repository 接口 public interface ProductRepository extends MongoRepository<Product, String> { List<Product> findByName(String name); // 自动生成查询方法 }
2.3 基础 CRUD 操作
@Autowired private ProductRepository productRepository; // 插入文档 public void saveProduct(Product product) { productRepository.save(product); } // 查询文档 public Product getProduct(String id) { return productRepository.findById(id).orElse(null); } // 动态查询 public List<Product> searchProducts(String keyword) { Query query = new Query(); query.addCriteria(Criteria.where("name").regex(keyword, "i")); return mongoTemplate.find(query, Product.class); }
3. 电商商品详情存储实战
3.1 文档设计示例
// 手机商品 { "name": "iPhone 14", "specs": { "color": "黑色", "storage": "256GB", "network": ["5G", "4G"] } } // 服装商品 { "name": "男士T恤", "specs": { "size": "XL", "material": "纯棉" } }
3.2 动态查询与索引优化
查询示例:检索所有支持 5G 的手机
// 使用 MongoDB 的 $elemMatch 查询数组 Query query = new Query(); query.addCriteria(Criteria.where("specs.network").elemMatch(Criteria.where("$eq").is("5G"))); List<Product> products = mongoTemplate.find(query, Product.class);
索引优化:
// 为 specs.network 字段创建索引 @CompoundIndex(def = "{'specs.network': 1}") public class Product { /* ... */ } // 通过注解自动生成索引(需在启动类添加 @EnableMongoAuditing)
4. 生产经验与面试考点
4.1 生产避坑指南
• 连接池配置:
spring: data: mongodb: uri: mongodb://host:port/db?maxPoolSize=50&minPoolSize=10
• 慢查询分析:
# 开启慢查询日志(执行时间超过 100ms) db.setProfilingLevel(1, 100) # 查看慢查询记录 db.system.profile.find().sort({ ts: -1 }).limit(10)
4.2 面试高频问题
-
MongoDB 与 MySQL 如何选型? • 答:动态 Schema 用 MongoDB,强事务用 MySQL。
-
文档嵌套层级过深有什么问题? • 答:查询性能下降,建议嵌套不超过 3 层,复杂关系拆分为引用。
六、MongoDB 进阶:查询与聚合
1. 复杂查询:嵌套文档与数组操作
1.1 嵌套文档查询
场景:电商订单中嵌套用户地址信息,需根据地址查询订单。 文档结构:
{ "orderId": "O1001", "user": { "name": "张三", "address": { "city": "北京", "street": "朝阳区" } } }
Java 查询代码:
// 查询北京地区的所有订单 Query query = new Query(); query.addCriteria(Criteria.where("user.address.city").is("北京")); List<Order> orders = mongoTemplate.find(query, Order.class); // 使用字段投影(只返回用户地址) query.fields().include("user.address");
1.2 数组操作($elemMatch
)
场景:商品评论中包含标签数组,需筛选同时包含“质量好”和“物流快”的评论。 文档结构:
{ "productId": "P100", "comments": [ { "userId": "U1", "tags": ["质量好", "物流快"] }, { "userId": "U2", "tags": ["质量好"] } ] }
Java 查询代码:
// 使用 $elemMatch 匹配数组元素 Query query = new Query(); query.addCriteria(Criteria.where("comments").elemMatch( Criteria.where("tags").all("质量好", "物流快") )); List<Product> products = mongoTemplate.find(query, Product.class);
避坑指南: • $all
vs $and
:$all
表示同时包含,$and
需满足多个条件(可能跨不同数组元素)。 • 性能优化:为数组字段添加索引(如 db.products.createIndex({"comments.tags": 1})
)。
2. 聚合管道实战
2.1 聚合阶段解析
阶段 | 作用 | 示例 |
---|---|---|
$match | 过滤数据(类似 SQL WHERE) | { $match: { status: "PAID" } } |
$group | 分组统计(类似 SQL GROUP BY) | { $group: { _id: "$category", total: { $sum: "$price" } } } |
$project | 字段重塑(类似 SQL SELECT) | { $project: { productName: 1, price: 1 } } |
$sort | 排序结果 | { $sort: { total: -1 } } |
2.2 电商用户行为分析案例
需求:统计每个用户的订单总金额和平均订单价。 聚合管道:
Aggregation aggregation = Aggregation.newAggregation( Aggregation.match(Criteria.where("status").is("PAID")), // 筛选已支付订单 Aggregation.group("userId") .sum("totalPrice").as("totalAmount") .avg("totalPrice").as("avgAmount"), Aggregation.sort(Sort.Direction.DESC, "totalAmount") ); AggregationResults<UserOrderStats> results = mongoTemplate.aggregate(aggregation, "orders", UserOrderStats.class);
输出结果:
[ { "_id": "U1001", "totalAmount": 1500, "avgAmount": 500 }, { "_id": "U1002", "totalAmount": 800, "avgAmount": 400 } ]
3. 索引优化策略
3.1 覆盖索引(Covered Index)
原理:索引包含查询所需的所有字段,无需回表查询文档。 **:
// 创建覆盖索引 db.orders.createIndex({ userId: 1, status: 1 }); // 查询使用索引 Query query = new Query(); query.addCriteria(Criteria.where("userId").is("U1001").and("status").is("PAID")); query.fields().include("userId").include("status"); // 只返回索引字段 List<Order> orders = mongoTemplate.find(query, Order.class);
性能对比: • 无覆盖索引:需扫描文档(COLLSCAN)。 • 有覆盖索引:仅扫描索引(IXSCAN)。
3.2 TTL 索引(自动清理过期数据)
场景:自动删除 30 天前的用户登录日志。 Java 实现:
@Document(collection = "login_logs") public class LoginLog { @Id private String id; @Indexed(expireAfterSeconds = 2592000) // 30天过期 private Date loginTime; }
生产经验: • 误差范围:TTL 清理任务每分钟运行一次,数据可能延迟删除。 • 复合 TTL 索引:仅支持单字段,若需多字段组合需在代码逻辑处理。
4. 面试高频考点
-
聚合管道执行顺序对性能的影响? • 答:优先使用
$match
和$project
减少数据处理量。 -
如何选择索引类型? • 答:高频查询用复合索引,排序用有序索引,数组用多键索引。
-
$group
的_id
字段为null
的作用? • 答:将所有文档视为一个分组(类似 SQL 的全局聚合)。
七、MongoDB 高可用与分片策略
1. 副本集(Replica Set)核心原理
1.1 副本集架构与选举机制
• 节点角色:
角色 | 作用 |
---|---|
Primary | 处理所有写操作和读请求(默认) |
Secondary | 异步复制主节点数据,可处理读请求 |
Arbiter | 不存储数据,仅参与选举投票 |
• 选举触发条件: • 主节点宕机(心跳超时)。 • 超过半数节点无法通信(网络分区)。 • 强制重新选举(rs.stepDown()
)。
Java 客户端配置:
spring: data: mongodb: uri: mongodb://主节点IP:27017,从节点IP:27017/dbname?replicaSet=rs0&readPreference=secondaryPreferred
• readPreference
参数: • primary
:默认,只从主节点读。 • secondaryPreferred
:优先从从节点读,主节点不可用时切回。
2. 分片集群(Sharding)实战
2.1 分片集群组成
组件 | 作用 |
---|---|
Shard | 存储实际数据的分片节点(每个分片可以是副本集) |
Config Server | 存储集群元数据(分片键、路由信息) |
Mongos | 路由节点,对外提供统一访问入口 |
2.2 分片策略选择
分片类型 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
哈希分片 | 数据均匀分布(如用户ID) | 负载均衡,写入扩展性强 | 范围查询效率低 |
范围分片 | 范围查询频繁(如时间序列数据) | 查询性能高,易于冷热归档 | 可能导致数据不均 |
分片键选择原则: • 基数大(如用户ID而非性别)。 • 写操作分布均匀(避免热点分片)。 • 匹配查询模式(常用条件字段如时间、地域)。
2.3 分片集群搭建步骤
-
启动 Config Server(3节点副本集):
mongod --configsvr --replSet configRs --port 27019
-
启动 Shard 节点(每个分片为副本集):
mongod --shardsvr --replSet shard1Rs --port 27018
-
启动 Mongos:
mongos --configdb configRs/配置服务器IP:27019
-
添加分片并启用分片:
# 连接到 Mongos mongo --port 27017 # 添加分片 sh.addShard("shard1Rs/分片节点IP:27018") # 启用数据库分片 sh.enableSharding("ecommerce # 选择分片键 sh.shardCollection("ecommerce.orders", { "userId": "hashed" })
3. 生产调优与避坑指南
3.1 连接池配置
# Spring Boot 连接池配置(阿里云推荐) spring: data: mongodb: uri: mongodb://mongos1:27017,mongos2:27017/dbname? maxPoolSize=50&minPoolSize=10&maxIdleTimeMS=30000
参数建议: • maxPoolSize
:按 (总QPS / 单个连接QPS) * 1.2
计算。 • maxIdleTimeMS
:30秒~5分钟,避免长空闲连接超时。
3.2 写入关注(Write Concern)
// Java 代码设置写入关注级别 MongoTemplate mongoTemplate = new MongoTemplate(...); mongoTemplate.setWriteConcern(WriteConcern.MAJORITY); // 确保数据写入多数节点
级别 | 描述 | 数据安全性 | 性能影响 |
---|---|---|---|
ACKNOWLEDGED | 默认,确认写入主节点 | 低 | 低 |
MAJORITY | 确认写入多数副本集节点 | 高 | 中 |
JOURNALED | 确认写入磁盘日志 | 最高 | 高 |
4. 国内大厂解决方案
云服务商 | MongoDB 服务 | 核心特性 |
---|---|---|
阿里云 | 云数据库 MongoDB 版 | 全托管分片集群、跨可用区容灾 |
腾讯云 | 云数据库 MongoDB | 一键扩缩容、审计日志与慢查询分析 |
华为云 | 文档数据库 DDS | 兼容 MongoDB API、备份恢复自动化 |
5. 面试高频考点
-
如何解决分片集群数据分布不均的问题? • 答:调整分片键(增加散列性)、手动迁移块(
sh.moveChunk()
)。 -
副本集选举过程中如何避免脑裂? • 答:配置
多数节点在同一机房
或通过priority
调整节点权重。 -
分片键选择错误如何补救? • 答:唯一方案是创建新集合并重新分片,旧数据迁移(无在线修改分片键功能)。
八、Elasticsearch 入门与搜索原理
1. Elasticsearch 核心概念
1.1 倒排索引机制
• 原理: • 正排索引:文档ID → 文档内容(类似数据库行记录)。 • 倒排索引:关键词 → 文档ID列表(快速定位包含关键词的文档)。 • 示例:
文档1: "Java工程师" 文档2: "Python工程师" 倒排索引: "Java" → [文档1] "工程师" → [文档1, 文档2] "Python" → [文档2]
Java 开发者视角: • 优势:全文检索速度远超 MySQL 的 LIKE
查询。 • 场景:商品搜索、日志分析、实时数据分析。
2. Spring Boot 整合 Elasticsearch
2.1 依赖与配置
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
# application.yml spring: elasticsearch: uris: http://localhost:9200 # 阿里云服务配置示例 # uris: https://es-cn-xxx.elasticsearch.aliyuncs.com:9200 # username: elastic # password: your_password
2.2 实体类与 Repository
@Document(indexName = "products") public class Product { @Id private String id; @Field(type = FieldType.Text, analyzer = "ik_max_word") private String name; @Field(type = FieldType.Double) private Double price; // Getter/Setter } public interface ProductRepository extends ElasticsearchRepository<Product, String> { List<Product> findByName(String name); }
2.3 基础 CRUD 操作
@Autowired private ProductRepository productRepository; // 插入文档 public void saveProduct(Product product) { productRepository.save(product); } // 搜索商品 public List<Product> search(String keyword) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchQuery("name", keyword)) .build(); return elasticsearchOperations.search(query, Product.class) .getSearchHits() .stream() .map(SearchHit::getContent) .collect(Collectors.toList()); }
3. 搜索与聚合实战
3.1 中文分词与 IK 分词器
配置 IK 分词器:
-
下载 IK 插件并放入 Elasticsearch 的
plugins
目录。 -
重启 Elasticsearch,实体类字段指定分词器:
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private String description;
搜索示例:
// 使用 multi_match 跨字段搜索 NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.multiMatchQuery(keyword, "name", "description")) .build();
3.2 聚合分析
统计每个类目的商品数量与平均价格:
TermsAggregationBuilder aggregation = AggregationBuilders .terms("category_agg").field("category.keyword") .subAggregation(AggregationBuilders.avg("avg_price").field("price")); NativeSearchQuery query = new NativeSearchQueryBuilder() .addAggregation(aggregation) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class);
输出结果:
"aggregations": { "category_agg": { "buckets": [ { "key": "手机", "doc_count": 100, "avg_price": 2999 }, { "key": "笔记本", "doc_count": 50, "avg_price": 6999 } ] } }
4. 集群与高可用架构
4.1 分片与副本机制
• 分片(Shard):数据水平拆分的单元(默认每个索引5个主分片)。 • 副本(Replica):每个分片的拷贝(默认1个副本,提升容错与读性能)。
Java 客户端配置:
spring: elasticsearch: uris: http://node1:9200,http://node2:9200,http://node3:9200
4.2 集群状态监控
# 查看集群健康状态 GET /_cluster/health # 查看节点分片分布 GET /_cat/shards ---## **5. 生产调优与面试考点** ### **5.1 性能优化** • **分片策略**: • 单个分建议 10GB~50GB(阿里云推荐)。 • 避免过度分片(每个分片消耗资源)。 • **内存配置**: • JVM 堆内存不超过 32GB(避免指针压缩失效)。 • 预留 50% 内存给操作系统文件缓存。 ### **5.2 高频面试题1. **倒排索引为什么比 B+ 树快**? • 答:倒排索引直接定位关键词,B+ 树需从根节点逐层查找。 2. **如何解决深分页性能问题**? • 答:改用 `search_after` 分页(游标分页),避免 `from + size` 深分页。 3. **集群脑裂如何预防**? • 答:配置 `discovery.zen.minimum_master_nodes = (节点数/2 + 1)`。
九、Elasticsearch 进阶:分析与优化
1. 索引设计与 Mapping 优化
1.1 索引生命周期管理(ILM)
场景:电商日志按天滚动存储,自动删除30天前数据。 Java 配置索引模板:
// 定义 ILM 策略(Rollover + Delete) String policyJson = """ { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_size": "50GB", "max_age": "30d" } } }, "delete": { "min_age": "30d", "actions": { "delete": {} } } } } } """; // 创建索引模板关联 ILM IndexTemplateRequest request = new IndexTemplateRequest("logs-template"); request.patterns(List.of("logs-*")); request.settings(Settings.builder().put("index.lifecycle.name", "logs-policy")); client.indices().putTemplate(request, RequestOptions.DEFAULT);
1.2 字段类型优化
字段类型 | 适用场景 | 优化技巧 |
---|---|---|
Keyword | 精确匹配(如状态码、分类ID) | 避免对长文本使用(占用内存) |
Text | 全文检索(如商品描述) | 结合 fields 实现多分词策略 |
Date | 时间范围查询 | 指定 format 避免解析歧义 |
Nested | 对象数组独立查询 | 控制嵌套层级(不超过3层) |
Java 实体类优化示例:
@Document(indexName = "products") public class Product { @Field(type = FieldType.Text, analyzer = "ik_max_word", fields = { @Field(type = FieldType.Keyword, name = "raw") // 精确匹配子字段 }) private String name; @Field(type = FieldType.Nested) // 嵌套类型 private List<Spec> specs; }
2. 查询性能优化
2.1 DSL 查询优化技巧
• 避免通配符查询:
// 错误示例:通配符导致全索引扫描 QueryBuilders.wildcardQuery("name", "*手机*"); // 正确方案:改用分词后的全文检索 QueryBuilders.matchQuery("name", "智能手机");
• 过滤器上下文(Filter Context):
// 使用 filter 不计算相关性分数,提升性能 BoolQueryBuilder query = QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery("status", "ON_SALE")) .must(QueryBuilders.matchQuery("name", "手机"));
2.2 聚合查询优化
Terms 聚合 Cardinality 控制:
// 限制返回桶数量,避免内存溢出 TermsAggregationBuilder agg = AggregationBuilders.terms("category_agg") .field("category.keyword") .size(100); // 默认10,按需调整 // 使用 show_term_doc_count_error 跳过低频词条 agg.showTermDocCountError(true);
Pipeline 聚合优化:
// 使用 moving_avg 减少计算开销 AggregationBuilder movingAvg = PipelineAggregatorBuilders .movingAvg("price_trend", "price");
3. 分片与集群管理
3.1 分片策略最佳实践
• 分片数量公式:
总分片数 = 数据总量 / 单个分片推荐大小(30GB)
• 动态扩容: • 增加节点后,Elasticsearch 自动平衡分片。 • 手动迁移:POST _cluster/reroute
调整分片分布。
3.2 集群监控与告警
关键指标监控: • 节点健康:GET _cluster/health
• 资源瓶颈:CPU、内存、磁盘IO(通过 Prometheus + Grafana 可视化)。
Java 集成告警:
// 使用 Elasticsearch Java Client 查询集群状态 ClusterHealthRequest request = new ClusterHealthRequest(); ClusterHealthResponse response = client.cluster().health(request, RequestOptions.DEFAULT); if (response.getStatus() == ClusterHealthStatus.RED) { // 触发告警通知 }
4. 生产问题排查
4.1 慢查询日志分析
启用慢日志:
PUT /products/_settings { "index.search.slowlog.threshold.query.warn": "10s", "index.search.slowlog.threshold.fetch.debug": "500ms" }
日志解读:
[2023-07-20T10:00:00] took[15s], took_millis[15000], types[], stats[], search_type[QUERY_THEN_FETCH], total_shards[100], extra[]
4.2 内存与 GC 调优
JVM 参数建议:
# jvm.options -Xms16g -Xmx16g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
堆内存分配原则: • 不超过物理内存的 50%,且不超过 32GB(压缩指针优势)。 • 预留 50% 内存给 Lucene 文件缓存。
5. 面试高频考点
-
如何设计一个支持千万级商品搜索的系统? • 分片策略:按类目哈希分片 + 时间范围别名。 • 查询优化:禁用
_source
字段 + 路由分片查询。 -
Elasticsearch 如何保证数据一致性? • 写入:主分片同步复制(
consistency=quorum
)。 • 读取:preference=_primary
强制读主分片。 -
Terms 聚合的精度问题如何解决? • 方案:
execution_hint=map
提升速度,或使用composite
分页聚合。
十、NoSQL 与关系型数据库协同架构
1. 混合架构设计:MySQL + Redis 缓存加速
1.1 典型场景:电商首页性能优化
• 架构设计: • Redis:缓存商品详情、秒杀库存、用户会话。 • MySQL:存储订单、用户账户等强一致性数据。 • Elasticsearch:商品搜索与聚合分析。
• Java 实现缓存逻辑:
@Cacheable(value = "product", key = "#productId") public Product getProduct(Long productId) { // 缓存未命中时查询数据库 return productRepository.findById(productId).orElse(null); } @CacheEvict(value = "product", key = "#productId") public void updateProduct(Product product) { productRepository.save(product); // 可选:延迟双删(防缓存不一致) redisTemplate.delete("product:" + product.getId()); }
生产经验: • 缓存击穿防护:热点 Key 永不过期 + 互斥锁重建。 • 数据一致性:监听 MySQL binlog(如 Canal)同步删除缓存。
2. 数据同步方案:Canal → Elasticsearch
2.1 Canal 工作原理
• 核心流程:
-
Canal 伪装为 MySQL 从库,接收 binlog。
-
解析 binlog 为结构化数据(JSON/ProtoBuf)。
-
推送变更事件到 Kafka/RocketMQ。
-
消费者(Java 服务)将数据写入 Elasticsearch。
Java 监听示例:
@KafkaListener(topics = "canal.product") public void syncProductToES(String message) { CanalMessage<Product> canalMessage = JSON.parseObject(message, new TypeReference<>() {}); if (canalMessage.getType() == CanalMessage.EventType.UPDATE) { elasticsearchTemplate.save(canalMessage.getData()); } }
2.2 同步一致性保障
• 最终一致性方案: • 本地消息表:将同步任务写入数据库,确保事务原子性。 java @Transactional public void createOrder(Order order) { orderRepository.save(order); // 写入本地消息表 eventRepository.save(new Event("order_created", order.getId())); }
• 最大努力通知:定时任务重试失败事件(如每5分钟)。 java @Scheduled(fixedRate = 300000) public void retryFailedEvents() { List<Event> failedEvents = eventRepository.findByStatus(EventStatus.FAILED); failedEvents.forEach(event -> { if (retrySync(event)) { event.setStatus(EventStatus.SUCCESS); } }); }
3. 一致性保障:本地消息表 + 最大努力通知
3.1 分布式事务挑战
• 场景:用户支付成功后,需更新订单状态(MySQL)并发送消息通知(Redis/ES)。 • 难点:跨数据库事务无法保证原子性。
3.2 解决方案对比
方案 | 原理 | 适用场景 | Java 实现复杂度 |
---|---|---|---|
本地消息表 | 数据库事务 + 异步重试 | 中低频业务(如订单通知) | 低 |
TCC 事务 | Try-Confirm-Cancel 阶段补偿 | 高频高一致性(如账户扣款) | 高 |
Seata AT 模式 | 全局锁 + 反向 SQL 回滚 | 简单事务,强依赖数据库支持 | 中 |
本地消息表 Java 实现:
public void payOrder(Long orderId) { // 1. 开启事务 TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { // 2. 更新订单状态 orderRepository.updateStatus(orderId, PAID); // 3. 写入本地消息表 eventRepository.save(new Event("order_paid", orderId)); // 4. 提交事务 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } }
4. 生产级架构设计案例
4.1 电商订单系统协同架构
• 组件分工: • MySQL:订单表、用户表(ACID 事务)。 • Redis:库存扣减(原子操作)、订单状态缓存。 • Elasticsearch:订单历史搜索(按时间、商品名检索)。 • Kafka:订单创建事件通知(积分发放、物流调度)。
• 数据流向:
-
用户下单 → MySQL 写入订单 → Canal 同步到 ES。
-
支付成功 → Redis 清除库存缓存 → 本地消息表触发积分服务。
4.2 容灾与降级策略
• 缓存故障:熔断策略(直接读 DB,记录日志后续补偿)。 • ES 同步延迟:本地缓存近期订单,提供降级查询接口。
5. 面试高频考点
-
如何保证缓存与数据库的一致性? • 答:延迟双删 + binlog 监听失效缓存。
-
Canal 同步数据时,如何处理删除操作? • 答:解析 binlog 的 DELETE 事件,触发 ES 的 deleteByQuery。
-
最大努力通知可能造成消息重复消费,如何解决? • 答:消费者接口幂等设计(如唯一流水号 + 数据库去重表)。
附录:工具与资源推荐
• 数据同步工具: • 阿里云 Canal(开源版) • Debezium(Kafka Connect 生态) • 学习资源: • 《凤凰架构》(本地消息表与 TCC 详解) • Elastic 官方文档《Cross-cluster replication》
十一、NoSQL 面试题精选
Redis
1. 如何用 Redis 实现分布式锁?有哪些注意事项?
答案:
-
核心实现: • 加锁:
SET key unique_value NX PX 30000
(NX 表示不存在时设置,PX 设置过期时间)。 • 解锁:Lua 脚本保证原子性(校验值 + 删除 Key)。if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
-
注意事项: • 锁续期:使用 Redisson 的看门狗机制(默认每 10 秒续期)。 • 网络分区风险:集群脑裂可能导致锁失效,需结合业务重试机制。 • 锁粒度:避免锁住大对象(如锁整个用户表,改为锁用户ID)。
大厂考点: • RedLock 算法的争议:需半数以上节点加锁成功,但网络分区时仍可能失效。 • GC 停顿影响:JVM 长时间 GC 可能导致锁过期后业务仍在执行。
2. Redis 集群数据分片原理是什么?
答案: • 虚拟槽分区: • 预分配 16384 个槽(Slot),每个 Key 通过 CRC16(key) % 16384
计算所属槽。 • 节点负责部分槽,集群扩容时通过 redis-cli --cluster reshard
迁移槽数据。 • 客户端路由: • 客户端缓存槽与节点的映射关系,直接定位目标节点(Moved/Ask 重定向优化)。
生产经验: • 热点 Key:同一槽的 Key 可能集中在某节点,可通过 Hash Tag 强制分配(如 {user100}.order
)。 • 迁移阻塞:避免在迁移过程中执行 KEYS
或 FLUSHDB
等高危命令。
MongoDB
1. 分片键的选择标准是什么?
答案:
-
基数大:如用户ID(唯一性高)优于性别(基数低)。
-
写分布均匀选择单调递增字段(如时间戳),导致写入集中在最后一个分片。
-
匹配查询模式:高频查询条件字段(如地域、类目)。
分片策略对比:
类型 | 优点 | 缺点 |
---|---|---|
**哈希分 | 数据均匀分布 | 范围查询需跨分片 |
范围分片 | 高效范围查询 | 可能导致数据倾斜 |
案例: • 电商订单表按 用户ID
哈希分片,用户行为日志按 时间范围
分片。
2. 如何设计嵌套文档避免数据冗余?
答案: • 嵌套层级:不超过 3 层(如 用户 → 订单 → 商品
)。 • 引用设计:高频查询的字段内嵌,低频字段用 DBRef
或手动关联。
// 内嵌常用字段 { "orderId": "O1001", "user": { "name": "张三", "userId": "U1001" // 用于关联查询用户详情 } }
避坑指南: • 数组膨胀:避免单个文档的数组无限增长(如评论列表),超过 16MB 文档限制时需分页存储。
Elasticsearch
1. 倒排索引和正排索引的区别?
答案:
索引类型 | 数据结构 | 应用场景 |
---|---|---|
倒排索引 | 词项 → 文档列表(快速定位文档) | 全文检索、关键词过滤 |
正排索引 | 文档ID → 字段值(存储原始数据) | 排序、聚合、高亮显示 |
技术细节: • Doc Values:Elasticsearch 的正排索引实现,列式存储优化聚合性能。 • 联合查询:倒排索引筛选文档,正排索引计算排序/聚合。
2. 深分页问题如何解决?(Search After vs Scroll API)
答案: • 问题根源:from + size
深分页时,协调节点需汇总所有分片数据,内存消耗大。 • 解决方案:
方案 | 原理 | 适用场景 |
---|---|---|
Search After | 基于上一页排序值定位 | 用户实时翻页(如下一页按钮) |
Scroll API | 创建快照,游标遍历(非实时) | 数据导出、离线分析 |
Java 实现 Search After:
SearchRequest request = new SearchRequest("products"); SearchSourceBuilder source = new SearchSourceBuilder() .size(10) .sort("price", SortOrder.ASC) .sort("_id", SortOrder.ASC); // 避免排序字段值重复 // 第二页开始设置 Search After if (lastPagePrices != null) { source.searchAfter(lastPagePrices); }
生产经验: • Scroll API 限制:快照占用堆内存,不适合高并发场景。 性能对比**:Search After 比 Scroll 快 5 倍以上(无快照开销)。
总结:NoSQL 面试需结合原理、生产避坑经验、框架源码(如 Redisson 锁实现),回答时优先提供 落地解决方案 而非纯理论,展现工程化思维。