别再瞎选 NoSQL 了!Redis、MongoDB、ES 场景边界、底层原理与生产选型全解

0 阅读15分钟

一、NoSQL选型的核心误区

很多开发者对NoSQL的认知停留在「替代MySQL」的层面,实际生产中却频繁踩坑:用Redis存海量冷数据导致内存成本飙升,用MongoDB做多表关联查询性能崩盘,用Elasticsearch做高频事务写入导致集群雪崩。

本质问题在于,NoSQL的核心是Not Only SQL,它是关系型数据库的补充,而非替代品。不同类型的NoSQL有着完全不同的设计目标、底层逻辑和能力边界,选错数据库的代价,往往是后期架构重构的巨额成本。

二、先搞懂:NoSQL的四大分类与核心定位

NoSQL数据库按照数据模型和设计目标,分为四大核心类别,本文聚焦的三款产品分别对应其中三类:

  1. 键值型(KV)数据库:以键值对为核心数据模型,极致追求读写性能与低延迟,代表产品为Redis。
  2. 文档型数据库:以半结构化文档为核心数据模型,兼顾灵活的schema与丰富的查询能力,代表产品为MongoDB。
  3. 搜索引擎型数据库:以倒排索引为核心,极致优化全文检索与多维聚合分析能力,代表产品为Elasticsearch。
  4. 列存储型数据库:以列族为核心数据模型,面向海量离线数据分析场景,代表产品为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的索引占用磁盘空间大,压缩比低,海量冷数据存储会导致成本过高,性能下降。

四、生产级核心维度全对比

对比维度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 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避坑要点

  1. 禁止使用Keys、FlushAll、FlushDB等高危命令,生产环境必须rename-command禁用或限制权限。
  2. 缓存必须设置过期时间,避免内存溢出,同时给过期时间添加随机值,避免缓存雪崩。
  3. 避免大Key与热Key问题,单个Key的value大小建议不超过10KB,热Key可通过本地缓存、分片打散的方式解决。
  4. 持久化配置需平衡性能与数据安全,混合持久化是最优解,避免AOF刷盘策略设置为always导致性能急剧下降。
  5. 集群模式下,避免跨槽位的批量操作,比如MGet、MSet,会导致请求路由到多个节点,性能下降。

7.2 MongoDB避坑要点

  1. 必须为常用查询字段建立索引,全表扫描会导致性能极差,同时避免索引过多导致写入性能下降。
  2. 分片键的选择是核心,必须选择高基数、分布均匀、查询频繁的字段,分片键一旦设置无法修改。
  3. 避免使用大文档与深层嵌套,单个文档最大16MB,嵌套层级建议不超过3层,否则会导致查询与更新性能下降。
  4. 分布式事务仅用于必要场景,频繁的分布式事务会导致性能大幅下降,优先使用单文档原子操作。
  5. WiredTiger缓存大小建议设置为主机内存的50%,避免与其他服务抢占内存导致OOM。

7.3 Elasticsearch避坑要点

  1. 避免频繁的更新与删除操作,ES的更新是标记删除,频繁操作会导致大量段碎片,段合并压力剧增,性能下降。
  2. 分片数量设置要合理,单个分片大小建议在20GB-50GB之间,每个节点的分片数不超过3个,分片数一旦设置无法修改。
  3. Text类型与Keyword类型要严格区分,不需要全文检索的字段必须设置为Keyword,避免索引膨胀。
  4. 深度分页问题,避免使用from+size做深度分页,建议使用search_after滚动查询,from+size的深度越深,内存占用越高,性能越差。
  5. 生产环境必须关闭自动创建索引,避免错误的字段类型导致索引失效,同时严格控制字段数量,避免字段爆炸。

八、NoSQL选型的核心原则

NoSQL选型的本质,是用合适的工具解决合适的问题,没有万能的数据库,只有适配业务场景的最优解。

核心选型原则只有三条:

  1. 优先明确核心需求:先搞清楚业务的核心诉求是低延迟读写、灵活的schema,还是全文检索与聚合分析,核心需求决定了数据库的选型。
  2. 不要用单一数据库解决所有问题:互联网行业的成熟架构,都是MySQL+Redis+ES/MongoDB的组合,各司其职,发挥各自的优势。
  3. 不要过度设计:小数据量、简单业务场景,优先使用MySQL即可,无需为了技术而技术,引入不必要的复杂度。