一、NoSQL选型的核心误区
很多开发者对NoSQL的认知停留在「替代MySQL」的层面,实际生产中却频繁踩坑:用Redis存海量冷数据导致内存成本飙升,用MongoDB做多表关联查询性能崩盘,用Elasticsearch做高频事务写入导致集群雪崩。
本质问题在于,NoSQL的核心是Not Only SQL,它是关系型数据库的补充,而非替代品。不同类型的NoSQL有着完全不同的设计目标、底层逻辑和能力边界,选错数据库的代价,往往是后期架构重构的巨额成本。
二、先搞懂:NoSQL的四大分类与核心定位
NoSQL数据库按照数据模型和设计目标,分为四大核心类别,本文聚焦的三款产品分别对应其中三类:
- 键值型(KV)数据库:以键值对为核心数据模型,极致追求读写性能与低延迟,代表产品为Redis。
- 文档型数据库:以半结构化文档为核心数据模型,兼顾灵活的schema与丰富的查询能力,代表产品为MongoDB。
- 搜索引擎型数据库:以倒排索引为核心,极致优化全文检索与多维聚合分析能力,代表产品为Elasticsearch。
- 列存储型数据库:以列族为核心数据模型,面向海量离线数据分析场景,代表产品为HBase,本文不做展开。
三、深度拆解三大主流NoSQL
3.1 Redis:内存级KV存储的王者
Redis是一款开源的、基于内存的高性能键值存储系统,是目前互联网行业应用最广泛的NoSQL产品。
3.1.1 底层核心架构
Redis的核心设计围绕「极致性能」展开,核心特性如下:
- 核心命令单线程执行:所有读写命令的执行都在单线程中完成,彻底避免了多线程的上下文切换与锁竞争开销,保证了命令执行的原子性。网络IO、持久化、集群同步、懒删除等非核心操作由多线程处理,6.0版本引入的多线程IO仅负责网络数据读写与协议解析,不影响核心命令的串行执行。
- IO多路复用模型:基于epoll/kqueue实现IO多路复用,单线程可处理数万级并发连接,支撑超高QPS。
- 内存存储+持久化机制:所有数据默认存储在内存中,读写延迟可达微秒级;同时提供RDB快照、AOF日志两种持久化方式,默认开启混合持久化,平衡数据安全性与性能。
- 高效的数据结构实现:所有内置数据结构都做了极致的底层优化,比如String类型基于SDS(简单动态字符串)实现,避免了C语言原生字符串的缓冲区溢出问题,同时支持O(1)复杂度获取字符串长度;ZSet基于跳表实现,保证范围查询的高性能。
3.1.2 核心数据模型
Redis以键值对为基础,支持丰富的数据结构,覆盖绝大多数业务场景:
- 基础结构:String、Hash、List、Set、SortedSet(ZSet)
- 高级结构:Bitmap、HyperLogLog、Geo、Stream、BloomFilter、JSON
3.1.3 适用场景
- 分布式缓存:核心场景,用于缓存热点数据,降低数据库压力,解决缓存穿透、击穿、雪崩等问题。
- 分布式锁:基于SET NX PX原子命令与Lua脚本,实现高性能、高可靠的分布式锁,解决分布式系统的并发竞争问题。
- 计数器与限流:基于INCR/DECR原子命令,实现接口限流、UV/PV统计、商品库存计数等场景。
- 排行榜:基于ZSet实现,支持海量数据的实时排序与范围查询,适用于电商热销榜、游戏排行榜等场景。
- 会话存储:存储分布式系统的用户Session、Token等数据,支持过期自动删除。
- 轻量级消息队列:基于List/Stream实现,支持发布订阅、消息持久化,适用于简单的异步解耦场景。
3.1.4 绝对禁忌场景
- 大规模冷数据持久化存储:内存存储的单位成本远高于磁盘,海量冷数据存储会导致成本失控。
- 复杂的关联查询与事务处理:Redis不支持SQL,无关联查询能力,事务能力仅能满足简单场景,无法支撑核心金融级事务。
- 频繁的大范围数据扫描:Keys、HGetAll等全量扫描命令会阻塞主线程,导致集群性能急剧下降。
- 大数据量的离线分析:无聚合分析能力,无法支撑复杂的数据分析场景。
3.2 MongoDB:文档型数据库的标杆
MongoDB是一款开源的、面向文档的分布式数据库,核心设计目标是平衡关系型数据库的强能力与NoSQL的灵活性,是目前最流行的文档型数据库。
3.2.1 底层核心架构
MongoDB的核心设计围绕「灵活的文档模型」与「分布式扩展能力」展开:
- WiredTiger存储引擎:默认存储引擎,基于页式存储,使用B+树作为默认索引结构,支持MVCC(多版本并发控制)、文档级别的写锁、写时复制(COW)、数据压缩,兼顾读写性能与数据安全性。默认缓存大小为主机内存的50%减去1GB,最大化利用内存提升查询性能。
- BSON文档模型:基于二进制JSON格式,支持动态Schema、嵌套文档、数组结构,无需提前定义表结构,字段可随时扩展,完美适配敏捷开发的需求变化。单个文档最大支持16MB,满足绝大多数业务场景。
- 原生分布式能力:支持副本集实现高可用(1主多从+仲裁节点,自动故障转移),支持分片集群实现水平扩展,可支撑PB级别的数据存储。
- 完整的事务支持:单文档操作天然具备原子性,4.0版本之后支持跨文档、跨分片的分布式事务,支持读已提交、快照、可序列化隔离级别,满足绝大多数业务的事务需求。
3.2.2 核心数据模型
MongoDB的核心是BSON文档,对应关系型数据库的「行」,集合(Collection)对应关系型数据库的「表」。文档支持任意层级的嵌套与数组,无需分表即可实现一对多、多对多的关系映射,比如订单与商品数据可直接嵌套在一个文档中,一次查询即可获取完整数据,无需关联查询。
3.2.3 适用场景
- 内容管理系统(CMS):文章、商品、用户画像等半结构化数据,字段灵活多变,嵌套文档模型可完美适配,无需频繁修改表结构。
- 物联网(IoT)数据存储:设备元数据、事件上报数据,数据量大、字段不固定,MongoDB的动态Schema与分片集群可轻松支撑。
- 电商业务系统:商品、订单、购物车等数据,嵌套文档可减少关联查询,提升接口性能,同时支持快速迭代业务需求。
- 游戏玩家数据存储:玩家属性、道具、战绩等数据,每个玩家的字段差异大,动态Schema可完美适配,同时支持高并发读写。
- 敏捷开发的创业项目:业务需求变化快,无需提前设计表结构,可快速迭代开发,降低前期架构设计成本。
3.2.4 绝对禁忌场景
- 核心金融级强事务系统:虽然支持分布式事务,但性能与可靠性远不如MySQL,不适合转账、支付等核心金融场景。
- 复杂的多表关联查询:MongoDB不擅长关联查询,$lookup操作的性能极差,频繁的关联查询会导致系统性能崩盘。
- 数据仓库与离线分析:无完善的OLAP能力,不适合海量数据的离线分析场景。
- 需要严格Schema约束与数据校验的系统:动态Schema的灵活性,也带来了数据一致性的风险,不适合对数据格式有严格要求的系统。
3.3 Elasticsearch:全文检索与分析引擎的天花板
Elasticsearch(简称ES)是一款开源的、基于Lucene的分布式全文检索与分析引擎,是ELK/EFK技术栈的核心,目前是全文检索、日志分析场景的绝对主流。
3.3.1 底层核心架构
ES的核心设计围绕「全文检索」与「分布式聚合分析」展开:
- Lucene内核与倒排索引:底层基于Apache Lucene实现,核心是倒排索引。正排索引是「文档ID→内容」的映射,而倒排索引是「分词后的词条(Term)→包含该词条的文档ID列表」的映射,通过倒排索引可实现毫秒级的全文检索。同时支持BKD树数值索引,优化数值类型的范围查询。
- 近实时(NRT)检索:数据写入后,默认1秒刷新一次内存缓冲区,生成新的Segment分段,才能被检索到,因此是近实时而非实时检索。Segment会在后台定期合并,减少磁盘碎片,提升查询性能。
- 原生分布式架构:天然支持分布式,节点分为主节点、数据节点、协调节点、 ingest节点,通过分片(Shard)实现数据水平拆分,通过副本(Replica)实现高可用与读写分离,可轻松支撑PB级别的数据与每秒数十万的查询请求。
- 丰富的分析能力:内置完善的聚合分析框架,支持指标聚合、桶聚合、管道聚合,可实现多维统计、用户行为分析、时序数据监控等场景。
3.3.2 核心数据模型
ES以JSON文档为基础,索引(Index)对应关系型数据库的「库」,文档(Document)对应「行」,字段(Field)对应「列」。每个字段可配置不同的类型与分词器,Text类型会被分词并建立倒排索引,用于全文检索;Keyword类型不会被分词,用于精确匹配与聚合排序。
3.3.3 适用场景
- 全文检索场景:站内搜索、电商商品搜索、资讯内容搜索、文档检索等,支持分词、高亮、相关性排序、模糊匹配,是目前最成熟的全文检索解决方案。
- 日志分析与监控:ELK/EFK技术栈的核心,用于收集、存储、检索、分析海量的服务日志、系统指标、安全审计日志,是互联网行业运维监控的标配。
- 用户行为分析:存储用户的点击、浏览、下单等行为数据,通过聚合分析实现用户画像、留存分析、转化漏斗分析等场景。
- 安全审计与风险防控:存储海量的操作日志、访问日志,支持实时检索与异常行为匹配,实现安全审计与风险预警。
- 时序数据监控:存储系统、应用、设备的指标数据,支持实时聚合与可视化,配合Grafana实现监控告警。
3.3.4 绝对禁忌场景
- 高频OLTP事务系统:ES无原生ACID事务支持,写操作是标记删除+新增文档,频繁的单条数据增删改会导致段合并压力剧增,写放大严重,性能急剧下降。
- 强数据一致性场景:ES是最终一致性,数据写入后需要等待刷新才能被检索到,主从同步存在延迟,不适合对数据一致性有强要求的场景。
- 小数据量的简单查询:ES的启动成本与资源开销高,小数据量的简单查询用MySQL即可,无需杀鸡用牛刀。
- 海量冷数据的归档存储:ES的索引占用磁盘空间大,压缩比低,海量冷数据存储会导致成本过高,性能下降。
四、生产级核心维度全对比
| 对比维度 | Redis | MongoDB | Elasticsearch |
| 核心定位 | 内存级KV存储/缓存 | 分布式文档型数据库 | 分布式全文检索与分析引擎 |
| 数据模型 | 键值对,支持多数据结构 | BSON文档,动态Schema,支持嵌套 | JSON文档,分Text/Keyword字段类型 |
| 底层存储 | 内存为主,支持RDB/AOF持久化 | 磁盘存储,WiredTiger引擎页式管理 | 磁盘存储,Lucene分段存储 |
| 核心索引结构 | 哈希表、跳表 | B+树(默认),支持地理空间、全文索引 | 倒排索引,BKD树数值索引 |
| 一致性模型 | 可配置,默认最终一致性,支持强一致 | 可配置读写策略,支持强一致到最终一致 | 最终一致性,近实时检索 |
| 事务支持 | 单命令原子性,支持Multi/Exec事务、Lua脚本原子执行 | 单文档原子性,支持跨文档/分片分布式事务 | 仅单文档操作原子性,无原生ACID事务 |
| 水平扩展能力 | 主从+哨兵,Redis Cluster分片集群 | 副本集高可用,分片集群水平扩展 | 原生分布式架构,分片+副本水平扩展 |
| 读写性能 | 读写延迟微秒级,单节点QPS可达10万+ | 读写延迟毫秒级,单节点QPS可达数万级 | 读延迟毫秒级,写延迟数十毫秒级,高吞吐聚合分析 |
| 存储成本 | 内存存储,单位成本高 | 磁盘存储,压缩比高,单位成本低 | 磁盘存储,索引占用空间大,单位成本中等 |
| 高可用方案 | 哨兵模式、Cluster集群 | 副本集(1主多从+仲裁节点) | 多节点集群,分片副本机制 |
五、选型决策树:一分钟选对NoSQL
六、代码实战
6.1 项目环境与依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>nosql-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nosql-demo</name>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.2.1-jre</guava.version>
<springdoc.version>2.6.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
6.2 Redis实战:分布式锁与计数器实现
6.2.1 分布式锁工具类
package com.jam.demo.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* Redis分布式锁工具类
*
* @author ken
*/
@Slf4j
@Component
public class RedisDistributedLock {
private final StringRedisTemplate stringRedisTemplate;
private static final Long RELEASE_SUCCESS = 1L;
private static final String UNLOCK_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的唯一键
* @param requestId 请求唯一标识,用于标识锁的持有者
* @param expireTime 锁过期时间,单位毫秒
* @return 加锁成功返回true,失败返回false
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
if (!org.springframework.util.StringUtils.hasText(lockKey)) {
log.error("锁键不能为空");
return false;
}
if (!org.springframework.util.StringUtils.hasText(requestId)) {
log.error("请求标识不能为空");
return false;
}
if (expireTime <= 0) {
log.error("过期时间必须大于0");
return false;
}
Boolean result = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
return !ObjectUtils.isEmpty(result) && result;
}
/**
* 释放分布式锁
*
* @param lockKey 锁的唯一键
* @param requestId 请求唯一标识,必须与加锁时的标识一致
* @return 释放成功返回true,失败返回false
*/
public boolean releaseLock(String lockKey, String requestId) {
if (!org.springframework.util.StringUtils.hasText(lockKey) || !org.springframework.util.StringUtils.hasText(requestId)) {
log.error("锁键或请求标识不能为空");
return false;
}
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);
return RELEASE_SUCCESS.equals(result);
}
}
6.2.2 计数器工具类
package com.jam.demo.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* Redis计数器工具类
*
* @author ken
*/
@Slf4j
@Component
public class RedisCounter {
private final StringRedisTemplate stringRedisTemplate;
public RedisCounter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 计数器递增
*
* @param key 计数器键
* @return 递增后的值
*/
public Long increment(String key) {
return stringRedisTemplate.opsForValue().increment(key);
}
/**
* 带过期时间的计数器递增
*
* @param key 计数器键
* @param expireTime 过期时间
* @param timeUnit 时间单位
* @return 递增后的值
*/
public Long incrementWithExpire(String key, long expireTime, TimeUnit timeUnit) {
Long count = stringRedisTemplate.opsForValue().increment(key);
if (count != null && count == 1) {
stringRedisTemplate.expire(key, expireTime, timeUnit);
}
return count;
}
/**
* 计数器递减
*
* @param key 计数器键
* @return 递减后的值
*/
public Long decrement(String key) {
return stringRedisTemplate.opsForValue().decrement(key);
}
/**
* 获取计数器当前值
*
* @param key 计数器键
* @return 当前计数值
*/
public Long getCount(String key) {
String value = stringRedisTemplate.opsForValue().get(key);
return value == null ? 0L : Long.parseLong(value);
}
/**
* 重置计数器
*
* @param key 计数器键
*/
public void reset(String key) {
stringRedisTemplate.delete(key);
}
}
6.3 MongoDB实战:文档CRUD与聚合查询
6.3.1 订单文档实体
package com.jam.demo.mongodb.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单文档实体
*
* @author ken
*/
@Data
@Document(collection = "order_info")
@Schema(description = "订单信息实体")
public class OrderInfo {
@Id
@Schema(description = "订单ID")
private String id;
@Field("user_id")
@Schema(description = "用户ID")
private Long userId;
@Field("order_no")
@Schema(description = "订单编号")
private String orderNo;
@Field("order_amount")
@Schema(description = "订单金额")
private BigDecimal orderAmount;
@Field("order_status")
@Schema(description = "订单状态:0-待付款 1-已付款 2-已发货 3-已完成 4-已取消")
private Integer orderStatus;
@Field("goods_list")
@Schema(description = "商品列表")
private List<GoodsItem> goodsList;
@Field("create_time")
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Field("update_time")
@Schema(description = "更新时间")
private LocalDateTime updateTime;
/**
* 商品子项
*/
@Data
@Schema(description = "订单商品子项")
public static class GoodsItem {
@Field("goods_id")
@Schema(description = "商品ID")
private Long goodsId;
@Field("goods_name")
@Schema(description = "商品名称")
private String goodsName;
@Field("goods_price")
@Schema(description = "商品单价")
private BigDecimal goodsPrice;
@Field("goods_num")
@Schema(description = "商品数量")
private Integer goodsNum;
}
}
6.3.2 订单服务实现
package com.jam.demo.mongodb.service;
import com.jam.demo.mongodb.entity.OrderInfo;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 订单服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class OrderService {
private final MongoTemplate mongoTemplate;
public OrderService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
/**
* 新增订单
*
* @param orderInfo 订单信息
* @return 新增后的订单
*/
public OrderInfo saveOrder(OrderInfo orderInfo) {
if (ObjectUtils.isEmpty(orderInfo)) {
log.error("订单信息不能为空");
return null;
}
orderInfo.setCreateTime(LocalDateTime.now());
orderInfo.setUpdateTime(LocalDateTime.now());
return mongoTemplate.save(orderInfo);
}
/**
* 根据ID查询订单
*
* @param id 订单ID
* @return 订单信息
*/
public OrderInfo getOrderById(String id) {
if (!org.springframework.util.StringUtils.hasText(id)) {
log.error("订单ID不能为空");
return null;
}
return mongoTemplate.findById(id, OrderInfo.class);
}
/**
* 根据用户ID查询订单列表
*
* @param userId 用户ID
* @return 订单列表
*/
public List<OrderInfo> getOrderByUserId(Long userId) {
if (ObjectUtils.isEmpty(userId)) {
log.error("用户ID不能为空");
return List.of();
}
Query query = new Query(Criteria.where("user_id").is(userId));
return mongoTemplate.find(query, OrderInfo.class);
}
/**
* 更新订单状态
*
* @param id 订单ID
* @param orderStatus 订单状态
* @return 更新结果
*/
public UpdateResult updateOrderStatus(String id, Integer orderStatus) {
if (!org.springframework.util.StringUtils.hasText(id) || ObjectUtils.isEmpty(orderStatus)) {
log.error("订单ID或状态不能为空");
return null;
}
Query query = new Query(Criteria.where("_id").is(id));
Update update = new Update().set("order_status", orderStatus).set("update_time", LocalDateTime.now());
return mongoTemplate.updateFirst(query, update, OrderInfo.class);
}
/**
* 删除订单
*
* @param id 订单ID
* @return 删除结果
*/
public DeleteResult deleteOrder(String id) {
if (!org.springframework.util.StringUtils.hasText(id)) {
log.error("订单ID不能为空");
return null;
}
Query query = new Query(Criteria.where("_id").is(id));
return mongoTemplate.remove(query, OrderInfo.class);
}
/**
* 统计用户订单总金额
*
* @param userId 用户ID
* @return 订单总金额
*/
public BigDecimal sumUserOrderAmount(Long userId) {
if (ObjectUtils.isEmpty(userId)) {
log.error("用户ID不能为空");
return BigDecimal.ZERO;
}
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("user_id").is(userId)),
Aggregation.group("user_id").sum("order_amount").as("total_amount")
);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, OrderInfo.class, Map.class);
List<Map> mappedResults = results.getMappedResults();
if (CollectionUtils.isEmpty(mappedResults)) {
return BigDecimal.ZERO;
}
Object totalAmount = mappedResults.get(0).get("total_amount");
return totalAmount == null ? BigDecimal.ZERO : new BigDecimal(totalAmount.toString());
}
}
6.4 Elasticsearch实战:全文检索与多维聚合
6.4.1 商品文档实体
package com.jam.demo.elasticsearch.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品文档实体
*
* @author ken
*/
@Data
@Document(indexName = "goods_info", createIndex = true)
@Schema(description = "商品信息实体")
public class GoodsInfo {
@Id
@Schema(description = "商品ID")
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
@Schema(description = "商品名称")
private String goodsName;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
@Schema(description = "商品描述")
private String goodsDesc;
@Field(type = FieldType.Keyword)
@Schema(description = "商品分类")
private String category;
@Field(type = FieldType.Double)
@Schema(description = "商品价格")
private BigDecimal goodsPrice;
@Field(type = FieldType.Integer)
@Schema(description = "商品库存")
private Integer stock;
@Field(type = FieldType.Date, format = {}, pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "上架时间")
private LocalDateTime shelfTime;
}
6.4.2 商品检索服务实现
package com.jam.demo.elasticsearch.service;
import com.jam.demo.elasticsearch.entity.GoodsInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* 商品检索服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class GoodsSearchService {
private final ElasticsearchOperations elasticsearchOperations;
public GoodsSearchService(ElasticsearchOperations elasticsearchOperations) {
this.elasticsearchOperations = elasticsearchOperations;
}
/**
* 新增商品文档
*
* @param goodsInfo 商品信息
* @return 新增后的商品
*/
public GoodsInfo saveGoods(GoodsInfo goodsInfo) {
if (ObjectUtils.isEmpty(goodsInfo)) {
log.error("商品信息不能为空");
return null;
}
return elasticsearchOperations.save(goodsInfo);
}
/**
* 根据ID查询商品
*
* @param id 商品ID
* @return 商品信息
*/
public GoodsInfo getGoodsById(Long id) {
if (ObjectUtils.isEmpty(id)) {
log.error("商品ID不能为空");
return null;
}
return elasticsearchOperations.get(id, GoodsInfo.class);
}
/**
* 商品全文检索
*
* @param keyword 检索关键词
* @param pageNum 页码
* @param pageSize 每页条数
* @return 商品分页结果
*/
public List<GoodsInfo> searchGoods(String keyword, int pageNum, int pageSize) {
if (!org.springframework.util.StringUtils.hasText(keyword)) {
log.error("检索关键词不能为空");
return List.of();
}
Criteria criteria = new Criteria("goodsName").matches(keyword)
.or("goodsDesc").matches(keyword);
Query query = new CriteriaQuery(criteria)
.setPageable(PageRequest.of(pageNum - 1, pageSize));
SearchHits<GoodsInfo> searchHits = elasticsearchOperations.search(query, GoodsInfo.class);
return searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 区间检索与分类过滤
*
* @param category 商品分类
* @param minPrice 最低价格
* @param maxPrice 最高价格
* @return 商品列表
*/
public List<GoodsInfo> searchGoodsByRange(String category, BigDecimal minPrice, BigDecimal maxPrice) {
Criteria criteria = new Criteria();
if (org.springframework.util.StringUtils.hasText(category)) {
criteria.and("category").is(category);
}
if (!ObjectUtils.isEmpty(minPrice)) {
criteria.and("goodsPrice").greaterThanEqual(minPrice);
}
if (!ObjectUtils.isEmpty(maxPrice)) {
criteria.and("goodsPrice").lessThanEqual(maxPrice);
}
Query query = new CriteriaQuery(criteria);
SearchHits<GoodsInfo> searchHits = elasticsearchOperations.search(query, GoodsInfo.class);
return searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 删除商品文档
*
* @param id 商品ID
* @return 删除结果
*/
public String deleteGoods(Long id) {
if (ObjectUtils.isEmpty(id)) {
log.error("商品ID不能为空");
return null;
}
return elasticsearchOperations.delete(id.toString(), GoodsInfo.class);
}
}
七、生产环境避坑指南
7.1 Redis避坑要点
- 禁止使用Keys、FlushAll、FlushDB等高危命令,生产环境必须rename-command禁用或限制权限。
- 缓存必须设置过期时间,避免内存溢出,同时给过期时间添加随机值,避免缓存雪崩。
- 避免大Key与热Key问题,单个Key的value大小建议不超过10KB,热Key可通过本地缓存、分片打散的方式解决。
- 持久化配置需平衡性能与数据安全,混合持久化是最优解,避免AOF刷盘策略设置为always导致性能急剧下降。
- 集群模式下,避免跨槽位的批量操作,比如MGet、MSet,会导致请求路由到多个节点,性能下降。
7.2 MongoDB避坑要点
- 必须为常用查询字段建立索引,全表扫描会导致性能极差,同时避免索引过多导致写入性能下降。
- 分片键的选择是核心,必须选择高基数、分布均匀、查询频繁的字段,分片键一旦设置无法修改。
- 避免使用大文档与深层嵌套,单个文档最大16MB,嵌套层级建议不超过3层,否则会导致查询与更新性能下降。
- 分布式事务仅用于必要场景,频繁的分布式事务会导致性能大幅下降,优先使用单文档原子操作。
- WiredTiger缓存大小建议设置为主机内存的50%,避免与其他服务抢占内存导致OOM。
7.3 Elasticsearch避坑要点
- 避免频繁的更新与删除操作,ES的更新是标记删除,频繁操作会导致大量段碎片,段合并压力剧增,性能下降。
- 分片数量设置要合理,单个分片大小建议在20GB-50GB之间,每个节点的分片数不超过3个,分片数一旦设置无法修改。
- Text类型与Keyword类型要严格区分,不需要全文检索的字段必须设置为Keyword,避免索引膨胀。
- 深度分页问题,避免使用from+size做深度分页,建议使用search_after滚动查询,from+size的深度越深,内存占用越高,性能越差。
- 生产环境必须关闭自动创建索引,避免错误的字段类型导致索引失效,同时严格控制字段数量,避免字段爆炸。
八、NoSQL选型的核心原则
NoSQL选型的本质,是用合适的工具解决合适的问题,没有万能的数据库,只有适配业务场景的最优解。
核心选型原则只有三条:
- 优先明确核心需求:先搞清楚业务的核心诉求是低延迟读写、灵活的schema,还是全文检索与聚合分析,核心需求决定了数据库的选型。
- 不要用单一数据库解决所有问题:互联网行业的成熟架构,都是MySQL+Redis+ES/MongoDB的组合,各司其职,发挥各自的优势。
- 不要过度设计:小数据量、简单业务场景,优先使用MySQL即可,无需为了技术而技术,引入不必要的复杂度。