别再瞎选 NoSQL 了!Redis、MongoDB、ES 场景边界、底层原理与生产选型全解
一、NoSQL选型的核心误区很多开发者对NoSQL的认知停留在「替代MySQL」的层面实际生产中却频繁踩坑用Redis存海量冷数据导致内存成本飙升用MongoDB做多表关联查询性能崩盘用Elasticsearch做高频事务写入导致集群雪崩。本质问题在于NoSQL的核心是Not Only SQL它是关系型数据库的补充而非替代品。不同类型的NoSQL有着完全不同的设计目标、底层逻辑和能力边界选错数据库的代价往往是后期架构重构的巨额成本。二、先搞懂NoSQL的四大分类与核心定位NoSQL数据库按照数据模型和设计目标分为四大核心类别本文聚焦的三款产品分别对应其中三类键值型KV数据库以键值对为核心数据模型极致追求读写性能与低延迟代表产品为Redis。文档型数据库以半结构化文档为核心数据模型兼顾灵活的schema与丰富的查询能力代表产品为MongoDB。搜索引擎型数据库以倒排索引为核心极致优化全文检索与多维聚合分析能力代表产品为Elasticsearch。列存储型数据库以列族为核心数据模型面向海量离线数据分析场景代表产品为HBase本文不做展开。三、深度拆解三大主流NoSQL3.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、SortedSetZSet高级结构Bitmap、HyperLogLog、Geo、Stream、BloomFilter、JSON3.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的索引占用磁盘空间大压缩比低海量冷数据存储会导致成本过高性能下降。四、生产级核心维度全对比对比维度RedisMongoDBElasticsearch核心定位内存级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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.0/version relativePath/ /parent groupIdcom.jam/groupId artifactIdnosql-demo/artifactId version0.0.1-SNAPSHOT/version namenosql-demo/name properties java.version17/java.version mybatis-plus.version3.5.7/mybatis-plus.version fastjson2.version2.0.52/fastjson2.version guava.version33.2.1-jre/guava.version springdoc.version2.6.0/springdoc.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.34/version scopeprovided/scope /dependency dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version${fastjson2.version}/version /dependency dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version${guava.version}/version /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version${springdoc.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project6.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; } DefaultRedisScriptLong 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 ListGoodsItem 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 ListOrderInfo 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) ); AggregationResultsMap results mongoTemplate.aggregate(aggregation, OrderInfo.class, Map.class); ListMap 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 ListGoodsInfo 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)); SearchHitsGoodsInfo searchHits elasticsearchOperations.search(query, GoodsInfo.class); return searchHits.getSearchHits().stream() .map(SearchHit::getContent) .collect(Collectors.toList()); } /** * 区间检索与分类过滤 * * param category 商品分类 * param minPrice 最低价格 * param maxPrice 最高价格 * return 商品列表 */ public ListGoodsInfo 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); SearchHitsGoodsInfo 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避免索引膨胀。深度分页问题避免使用fromsize做深度分页建议使用search_after滚动查询fromsize的深度越深内存占用越高性能越差。生产环境必须关闭自动创建索引避免错误的字段类型导致索引失效同时严格控制字段数量避免字段爆炸。八、NoSQL选型的核心原则NoSQL选型的本质是用合适的工具解决合适的问题没有万能的数据库只有适配业务场景的最优解。核心选型原则只有三条优先明确核心需求先搞清楚业务的核心诉求是低延迟读写、灵活的schema还是全文检索与聚合分析核心需求决定了数据库的选型。不要用单一数据库解决所有问题互联网行业的成熟架构都是MySQLRedisES/MongoDB的组合各司其职发挥各自的优势。不要过度设计小数据量、简单业务场景优先使用MySQL即可无需为了技术而技术引入不必要的复杂度。