企业级架构师综合能力项目案例二(项目性能优化方案JVM+数据库+缓存+代码JUC+消息中间件架构+服务熔断降级)

  1. 确立基线: 使用压测工具(JMeter)对当前系统压测,得到QPS(每秒处理的请求数)、RT(响应时间Response Time)、错误率等基线数据。

    系统在保证可接受性能(如 RT < 200ms, 错误率 < 0.01%)的前提下,最大能支撑多少 QPS?

    系统在特定压力(如日常峰值的 1.5 倍)下,持续运行 12/24 小时,是否会出现内存泄漏、RT 缓慢增长、错误率上升等问题?

    找到系统的性能瓶颈所在(CPU、内存、磁盘 I/O、网络 I/O、数据库、外部依赖等)

  2. 监控分析:

    复制代码
     使用APM和jstat等工具定位瓶颈(是CPU、内存、IO还是数据库?)。
指标 描述 如何获取JMeter 基线含义
QPS 每秒处理的请求数 Aggregate Report 中的 Throughput 系统处理能力的核心指标。基线:在目标 RT 和错误率下能达到的最大值。
RT 响应时间 (Response Time) Aggregate Report 中的 Average, 90% Line (更重要), 95% Line, 99% Line 用户体验的直接体现。90% Line 更具参考价值。基线:P90 RT < [预定目标],如 200ms。
错误率 失败请求的百分比 Aggregate Report 中的 Error % 系统稳定性的体现。基线:错误率 < 0.1% (根据业务要求调整)。
吞吐量 单位时间内处理的字节数 Aggregate Report 中的 Received KB/sec 网络带宽规划参考
并发用户数 同时发出请求的虚拟用户数 线程组配置数 系统能支撑的最大并发数。
  1. 分层优化:

    复制代码
     JVM层: 调整堆大小、选择合适的GC器、分析GC日志。
    
     数据库层: 分析慢SQL、优化索引、调整连接池、考虑批量操作。ES优化、MongoDB优化
    
     缓存层: 根据场景引入本地或分布式缓存,注意缓存穿透、击穿、雪崩问题。
    
     代码层: review代码,避免内存泄漏、使用高效的数据结构和算法。
    
     架构层: 考虑读写分离、分库分表、异步化(MQ)、服务熔断降级等。
  2. 验证效果: 再次压测,对比优化前后的指标,确认优化是否有效。无效则回到第2步。

JVM调优四步法

java性能优化

核心在于监控->分析->假设->验证的闭环。企业中90%的JVM问题通过分析GC日志和堆转储文件都能找到根源,剩下的10%则需要结合代码、中间件和系统架构进行更深层次的优化。

一、监控与诊断

①、开启GC日志 (强制必须)

bash 复制代码
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-XX:+PrintGCTimeStamps 
-XX:+PrintGCCause 
-Xloggc:/path/to/your/gc-%t.log 
-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=5 
-XX:GCLogFileSize=10M

使用 GCeasy (在线分析工具)、GCE Viewer 等工具上传GC日志文件,它们会自动生成可视化报告,指出问题所在。

  • YoungGC频率/耗时: 是否过于频繁?平均耗时是否正常(几十毫秒内)?
  • FullGC频率/耗时: 绝对重点! 是否有FullGC?FullGC次数越多、耗时越长(秒级),问题越严重。FullGC是STW(Stop-The-World)的,会直接导致服务暂停、RT飙升。
  • 堆内存使用率: 各区域(Eden, Survivor, Old Gen)的使用情况是否合理?

②、使用APM工具进行代码级诊断

Arthas (阿里开源,必备神器)、SkyWalking、Pinpoint工具,当发现某个接口RT很长,但不确定是JVM问题还是代码问题

Arthas常用命令:

  • dashboard: 整体系统状态,实时查看堆内存、GC、线程。
  • thread <线程ID>: 查看某个线程的栈,定位卡顿线程。
  • trace <类全路径> <方法名>: 追踪方法内部调用路径,并输出每个节点的耗时。这是定位"慢方法"的神器。
  • jad <类全路径>: 反编译线上代码,确认最新版本已部署。

③、系统级监控

jstat (命令行利器): jstat -gcutil 1s (每秒钟查看一次GC和内存情况)

关注 YGC/YGCT(YoungGC次数/耗时),FGC/FGCT(FullGC次数/耗时),OUE(老年代使用率)。

二、常见问题模式与调优策略

问题模式 症状 监控证据 调优策略 (以G1GC为例)
YoungGC过于频繁 YoungGC时间短,但频率极高,可能每分钟几次。 Eden区增长飞快,很快占满。 增大堆内存: -Xms4g -Xmx4g (设成一样,避免伸缩开销)。增大Eden区: -XX:NewRatio=2 (老年代/新生代=2/1) 或直接 -Xmn1.5g 设置新生代大小。
FullGC频繁或耗时过长 最严重问题。服务定期卡顿,RT飙高。 GC日志中出现大量FullGC记录,Old区在FullGC后回收不多。 1. 根源可能是内存泄漏: 用 jmap -histo:live 或 jmap -dump:live,format=b,file=heap.hprof 导出堆转储,用MAT工具分析泄漏对象。2. 调整G1GC参数:- 增大堆大小- 降低触发MixedGC阈值: -XX:InitiatingHeapOccupancyPercent=45 (默认45,可尝试降低,更早开始回收老年代)- 加快回收速度: -XX:ConcGCThreads=? (增加并发标记线程数)
MetaSpace (元空间)溢出 报 OutOfMemoryError: Metaspace 设置MetaSpace大小并开启压缩:-XX:MaxMetaspaceSize=256m-XX:+UseCompressedClassPointers-XX:+UseCompressedOops
对象晋升年龄不当 短命对象进入老年代,引发FullGC。 Survivor区使用率100%,年龄分布不均。 调整晋升阈值: -XX:MaxTenuringThreshold=15 调整Survivor区比例: -XX:SurvivorRatio=8 (Eden/Survivor=8/1)

三、参数调整与压测验证

  1. 编写调优脚本: 将调整的JVM参数写入应用的启动脚本(如 java -server -Xms4g -Xmx4g -XX:+UseG1GC
    ... -jar your-app.jar)。
  2. 重启应用: 部署到压测环境。
  3. 重新压测: 使用完全相同的JMeter脚本和压力策略进行压测。
  4. 收集数据: 再次收集GC日志、系统指标和性能指标(QPS, RT)。

四、对比分析与迭代

对比基线: 将新的GC日志分析报告和性能数据与第一步的基线进行对比。

  • FullGC次数是否减少甚至消除?
  • YoungGC耗时和频率是否可接受?
  • P90/P99 RT是否下降?
  • QPS是否提升?

得出结论:

  • 如果有效: 保留参数,将其作为新的基线,并考虑是否进行下一轮优化(如进一步优化代码或数据库)。
  • 如果无效甚至恶化: 回退参数,重新分析日志,尝试其他策略。

五、企业级项目实战案例

场景: 一个电商订单核心服务,在每晚高峰期,监控系统发现RT周期性飙高,同时伴随大量GC告警。

①、诊断 (第一步):

查看GC日志 (通过公司统一的日志平台): 使用GCeasy分析发现,每隔5-10分钟就有一次长达2-3秒的Full GC。

使用Arthas连接线上服务(在低峰期): 使用 thread 命令未发现大量阻塞线程。使用 trace 命令追踪订单生成方法,发现内部有一个深度的递归调用比较耗时,但并非主要矛盾。

使用jstat监控: jstat -gcutil 1s 发现老年代使用率在FullGC后从98%降到65%,但之后又缓慢上升,直到下次FullGC。

结论: 存在轻微的内存泄漏,或者生存期较长的缓存对象过多,导致老年代对象缓慢积累,最终触发FullGC。

②、策略与调整 (第二步):

第一步(紧急止血): 由于是G1GC,先尝试更早地启动混合回收(Mixed GC),让G1更积极一些。

将 -XX:InitiatingHeapOccupancyPercent 从45下调到35。

第二步(根因治理): 在凌晨低峰期,使用 jmap -dump 命令dump堆内存,下载到本地。

使用 Eclipse MAT (Memory Analyzer Tool) 分析堆转储文件。

点击 Leak Suspects Report(泄漏嫌疑报告)。

MAT显示有一个自定义的本地缓存 LocalCache 对象持有大量订单查询对象,且这个缓存没有大小限制和过期策略。

③、验证与迭代 (第三、四步):

代码修复: 开发团队修复代码,为 LocalCache 添加了LRU淘汰策略和TTL过期时间。

参数调整: 将 -XX:InitiatingHeapOccupancyPercent 调回45(因为代码修复后,老年代增长变慢)。

部署上线后重新监控: 下一个高峰期,FullGC现象消失,YoungGC频率正常,RT曲线变得平滑。调优成功。

六、推荐的标准G1GC参数模板

对于大多数企业级Java应用(8核CPU,16G内存级别),可以从这个模板开始,然后根据上述步骤微调:

bash 复制代码
# 必选:堆内存大小,根据机器内存设置,通常设成一样
-Xms4g -Xmx4g

# 必选:使用G1垃圾收集器 (JDK9+默认)
-XX:+UseG1GC

# 重要:GC日志记录,便于排查
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintGCCause
-Xloggc:/opt/your_app/logs/gc-%t.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M

# 重要:Metaspace大小,避免溢出
-XX:MaxMetaspaceSize=256m
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops

# 可选:目标最大GC暂停时间,根据你的SLA要求设置(毫秒)
-XX:MaxGCPauseMillis=200

# 可选:并行GC线程数,一般无需设置,默认根据CPU核数计算
#-XX:ParallelGCThreads=8
# 可选:并发GC线程数,一般为ParallelGCThreads的1/4
#-XX:ConcGCThreads=2

# 重要:触发Mixed GC的堆占用率阈值
-XX:InitiatingHeapOccupancyPercent=45

# 重要:启用JVM对外内存泄漏检查(如Netty等NIO框架常用)
-XX:NativeMemoryTracking=detail

数据库层调优

①、SQL语句与索引优化 (占比约70%的问题)

  • 开启慢查询日志 (Slow Query Log): 抓出执行缓慢的SQL语句。
  • 使用 EXPLAIN 分析执行计划: 理解数据库如何执行某条SQL,这是优化的核心技能。
  • 避免常见反模式:
    SELECT *
    在 WHERE 子句中对字段进行函数操作或运算
    使用 %keyword% 前导通配符模糊查询
    隐式类型转换
  • 索引优化策略:
    最左前缀匹配原则
    避免冗余和重复索引
    使用覆盖索引 (Covering Index) 减少回表
    对区分度高的字段建索引

场景: 用户订单查询页面缓慢。

Ⅰ、定位慢SQL

首先在MySQL配置中开启慢查询日志(通常在 my.cnf 中)

ini 复制代码
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 2   # 定义超过2秒的查询为"慢查询"
log_queries_not_using_indexes = 1 # 记录未使用索引的查询

重启MySQL后,通过工具(如 mysqldumpslow, pt-query-digest)分析慢日志文件。

Ⅱ、分析执行计划(EXPLAIN)

假设分析后找到一条慢SQL:

sql 复制代码
SELECT * FROM orders 
WHERE user_id = 123 
AND create_time > '2023-01-01' 
ORDER BY total_amount DESC 
LIMIT 10;

使用 EXPLAIN 分析:

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE user_id = 123 
AND create_time > '2023-01-01' 
ORDER BY total_amount DESC 
LIMIT 10;

可能的结果与问题:

  • type: ALL (最坏的情况,全表扫描)
  • key: NULL (没有使用索引)
  • Extra: Using filesort (在磁盘上进行排序,非常耗时)

Ⅲ、优化和添加索引(以下两个方案)

问题在于 WHERE 和 ORDER BY 用的字段不同。根据最左前缀原则,一个索引无法同时高效过滤和排序。

方案一:(常用): 创建联合索引 (user_id, create_time)。这样能快速定位到某个用户在某段时间内的订单。但排序 total_amount 依然需要 filesort。

sql 复制代码
CREATE INDEX idx_userid_createtime ON orders(user_id, create_time);

方案二: 如果业务总是按金额排序,可以创建覆盖索引 (user_id, total_amount, create_time)。这样索引本身就可以完成过滤和排序,无需回表(如果SELECT的字段都在索引中)和 filesort。

sql 复制代码
-- 删除旧索引(如果需要)
DROP INDEX idx_userid_createtime ON orders;
-- 创建新索引
CREATE INDEX idx_userid_amount_createtime ON orders(user_id, total_amount, create_time);

再次使用 EXPLAIN 验证,会发现:

  • type: ref 或 range (索引查找)
  • key: idx_userid_amount_createtime
  • Extra: Using where; Using index (理想状态,使用了覆盖索引)

Ⅳ、改写SQL

如果查询必须用 SELECT *,但表中有 text/blob 等大字段,回表代价很大。应考虑是否需要所有字段,或者进行分页查询。

sql 复制代码
-- 分页查询,减少单次数据量
SELECT id, order_sn, total_amount, status, create_time -- 只查询需要的字段
FROM orders 
WHERE user_id = 123 
AND create_time > '2023-01-01' 
ORDER BY total_amount DESC 
LIMIT 0, 10; -- 第一页

②、数据库架构优化

当单实例数据库无法满足性能需求时,必须考虑架构扩展。

措施与方案:

  • 读写分离: 主库负责写操作,多个从库负责读操作。适用于读多写少的场景。
  • 分库分表: 分为垂直分库(按业务模块拆分)和水平分表(将一个大表的数据分到多个物理表中)。
  • 引入缓存: 使用Redis等缓存热点数据,减轻数据库压力。

场景: orders 表数据量已达亿级,查询和插入都非常缓慢。

方案: 对 orders 表进行水平分表,按 user_id 取模分到4个表中。

xml 复制代码
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>

Spring Boot 配置 (application.yml):

yml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0 # 这里先用单个数据源演示分表
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db?useUnicode=true
        username: root
        password: root
    rules:
      sharding:
        tables:
          orders: # 逻辑表名
            actual-data-nodes: ds0.orders_$->{0..3} # 映射的实际物理表
            key-generator: # 分布式主键生成策略
              column: id
              type: SNOWFLAKE
            database-strategy: # 分库策略,这里未配置
            table-strategy: # 分表策略
              standard:
                sharding-column: user_id
                sharding-algorithm-name: orders_table_mod
        sharding-algorithms:
          orders_table_mod:
            type: MOD
            props:
              sharding-count: 4 # 分4张表
    props:
      sql-show: true # 开发环境开启,显示实际路由的SQL

应用程序代码:

无需修改! 这是分库分中间件的最大优势。你仍然像操作单表一样编写SQL。

java 复制代码
@Mapper
public interface OrderMapper {
    // ShardingSphere会根据 user_id = 123 的值,计算并路由到具体的物理表
    // 例如 123 % 4 = 3,这条记录会插入到 orders_3 表
    @Insert("INSERT INTO orders (user_id, amount, ...) VALUES (#{userId}, #{amount}, ...)")
    int insert(Order order);

    // 查询时同样,根据传入的 userId 路由到正确的表
    @Select("SELECT * FROM orders WHERE user_id = #{userId} AND order_sn = #{orderSn}")
    Order selectOneByUserIdAndSn(@Param("userId") Long userId, @Param("orderSn") String orderSn);
}

注意: 对于不带 user_id 的查询(如 SELECT * FROM orders WHERE order_sn = 'ABC'),ShardingSphere会执行广播查询(查询所有分表),效率低下。因此,分表后查询条件应尽量包含分片键。

③、连接池与应用层优化

  • 使用高性能连接池: 如 HikariCP,并正确配置参数(maximum-pool-size 不是越大越好!通常等于 (核心数 * 2)
  • 磁盘数)。
  • 批处理 (Batch Processing): 对于大批量插入/更新操作。
  • 避免 N+1 查询问题: 使用MyBatis等框架的联合查询或批量查询功能。
java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderMapper orderMapper;
    private final SqlSessionFactory sqlSessionFactory;

    // 低效:循环单条插入
    public void createOrdersBad(List<Order> orders) {
        for (Order order : orders) {
            orderMapper.insert(order); // 每次插入都是一次网络IO+数据库事务
        }
    }

    // 高效:使用批处理
    public void createOrdersBatch(List<Order> orders) {
        try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            OrderMapper mapper = session.getMapper(OrderMapper.class);
            for (int i = 0; i < orders.size(); i++) {
                mapper.insert(orders.get(i));
                // 每500条提交一次,避免批量过大
                if (i % 500 == 0 && i > 0) {
                    session.commit();
                    session.clearCache(); // 清空缓存,防止OOM
                }
            }
            session.commit(); // 提交剩余的数据
        } // try-with-resources 自动关闭session
    }
}

④、数据库服务本身优化

  • 参数调优: 调整
    innodb_buffer_pool_size(通常设置为机器物理内存的50%-70%),innodb_log_file_size 等。
  • 硬件升级: 使用SSD硬盘,增加内存容量。
  • 操作系统优化: 调整文件系统的挂载参数、内核参数等。

总结: 数据库调优是一个自上而下、由表及里的过程。优先从SQL和索引入手,这能解决大部分问题。当单实例性能达到极限时,再考虑读写分离、分库分表等架构方案。同时,配合应用层的批处理和缓存,以及数据库本身的参数调优,才能构建一个高性能、高可用的企业级数据存储层。

Elasticsearch (ES) 优化

ES优化的核心目标是:提升查询速度、降低写入延迟、保证集群稳定性和资源利用率

①、合理设置分片 (Shards) 和副本 (Replicas)

分片数: 一旦创建索引,主分片数不可更改。设置需谨慎。

  • 过大弊端: 每个分片都有开销(CPU、内存)。分片过多会导致查询性能下降(需要合并更多分片的结果)、集群恢复变慢。
  • 过小弊端: 无法水平扩展,单个分片过大影响性能且迁移困难。
  • 黄金法则: 建议单个分片大小控制在 10GB - 50GB 之间。可以通过 总数据量(1+副本数) / 50GB
    来预估初始分片数。对于时序数据,通常按天/周创建索引,分片数可固定为一个较小值(如3-5)。

副本数: 提供高可用和提升读取吞吐量。可以在创建索引后动态调整。

  • 生产环境至少设置为 1。在写入压力大时,可以临时设置为 0,写入完成后再恢复,最后用 _forcemerge 合并段。

案例:创建一个商品索引,预计有1TB数据,设置1个副本

json 复制代码
PUT /products
{
  "settings": {
    "number_of_shards": 15,   // (1000GB * 2) / 50GB ≈ 40, 但40可能过多。可先试15,预留未来2-3倍增长。
    "number_of_replicas": 1,
    "refresh_interval": "30s" // 写入优化:降低刷新频率,默认1s
  },
  "mappings": { ... }
}

②、映射 (Mapping) 与查询优化

  • 避免动态映射,明确定义字段类型:防止ES自动推断出不合适的类型(如将数字推断为text)。
  • 慎用 keyword 和 text:
    keyword:用于精确匹配、聚合、排序。无需分词。
    text:用于全文检索。会被分词,占用资源更多。
  • 禁用不需要索引的字段:对于仅用于存储、从不用于查询的字段,设置 "index": false,大幅节省磁盘和内存。
  • 使用 runtime_fields:对于查询时才计算的字段,避免索引开销。

案例:一个商品映射优化

json 复制代码
PUT /products/_mapping
{
  "properties": {
    "product_id": {
      "type": "keyword"  // 精确查找、Term聚合
    },
    "product_name": {
      "type": "text",    // 全文搜索
      "fields": {
        "keyword": {
          "type": "keyword" // 同时提供一个用于精确聚合的字段
        }
      }
    },
    "price": {
      "type": "scaled_float", // 优于float,存储更高效
      "scaling_factor": 100
    },
    "description": {
      "type": "text",
      "index": false // 商品描述很长,只存储,从不参与搜索
    },
    "create_time": {
      "type": "date"
    }
  }
}

③、写入优化

  • 使用批量请求 (Bulk API):单条写入开销极大,必须批量。
  • 调整刷新间隔 (refresh_interval):写入阶段临时设置为 -1(关闭)或 "30s",减少Segment生成和合并的压力。
  • 调整事务日志 (translog) 策略:对于可容忍少量数据丢失的场景,设置 "index.translog.durability":
    "async" 和 "index.translog.sync_interval": "5s",减少磁盘IO。

案例:大批量导入商品数据的最佳实践

bash 复制代码
# 1. 创建索引时禁用刷新和副本
PUT /temp_products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 0,    // 写入时先禁用副本
    "refresh_interval": "-1"    // 写入时先禁用刷新
  }
}

# 2. 使用Bulk API进行大量写入
curl -s -H "Content-Type: application/x-ndjson" -XPOST "localhost:9200/temp_products/_bulk" --data-binary "@bulk_data.json"

# 3. 写入完成后,恢复设置
PUT /temp_products/_settings
{
  "number_of_replicas": 1,
  "refresh_interval": "1s"
}

# 4. (可选) 强制合并段,优化查询性能
POST /temp_products/_forcemerge?max_num_segments=1

④、查询优化

  • 避免深度分页:from + size 方式超过 10000 会性能骤降。使用 search_after 或 scroll API。
  • 使用过滤器上下文 (Filter Context):filter 子句会缓存结果,速度远快于 must(评分查询)。
  • 限制返回字段:使用 _source 过滤,避免传输大量无用数据。
  • 使用异步搜索 (Async Search):对于非常耗时的查询,避免阻塞。

案例:一个高效的商品搜索查询

json 复制代码
GET /products/_search
{
  "_source": ["product_id", "product_name", "price"], // 只返回需要的字段
  "query": {
    "bool": {
      "must": [ // 必须匹配,参与评分
        { "match": { "product_name": "手机" } }
      ],
      "filter": [ // 过滤,不参与评分,结果可缓存
        { "range": { "price": { "gte": 1000, "lte": 5000 } } },
        { "term": { "category": "electronics" } }
      ]
    }
  },
  "sort": [ // 按价格排序
    { "price": { "order": "asc" } }
  ],
  "from": 0,
  "size": 20
}

MongoDB 优化

MongoDB优化的核心是:正确使用索引、优化数据模型、高效使用硬件资源。

①、数据模型设计优化

嵌入式 vs 引用式

  • 嵌入式: 适用于"一对一"或"一对少"且子文档不频繁独立查询的场景。优先选择,因为它能通过一次查询获取所有数据。

    案例:用户(User)文档中嵌入地址(Address)文档数组。

  • 引用式: 适用于"一对多"或"多对多"且子文档经常被独立查询或更新的场景。

    案例:文章(Post) 和 评论(Comment) 用 ObjectId 关联。

避免使用大型数组:数组增长会导致文档移动,影响性能。如果数组可能无限增长,应将其建模为单独的集合。

②、索引优化 (与RDBMS思路类似但更灵活)

  • 复合索引顺序:遵循 ESR原则:精确匹配(Equal)字段 -> 排序(Sort)字段 -> 范围(Range)字段。
  • 创建适合查询模式的索引:通过 db.collection.explain().find(...)
    分析查询计划,确认是否使用索引(IXSCAN)而非全表扫描(COLLSCAN)。
  • 覆盖查询 (Covered Query):如果查询只需返回索引中包含的字段,可以无需回表,极快。
  • 使用部分索引 (Partial Index):只为满足条件的文档创建索引,节省空间。
  • TTL索引:自动过期删除数据,非常适合日志、会话等场景。

案例:优化一个订单查询

js 复制代码
// 查询:查找某个用户状态为"已完成"的订单,按创建时间倒序排列
db.orders.find({
  user_id: 12345,
  status: "completed"
}).sort({ create_time: -1 })

// 创建复合索引:Equal -> Sort -> Range (这里没有Range,所以ES)
db.orders.createIndex({ 
  "user_id": 1,   // Equal first
  "create_time": -1 // Sort next (注意方向:1为升序,-1为降序,与sort一致最佳)
})

// 更优的索引:覆盖查询
// 假设我们只需要返回 order_id 和 create_time
db.orders.createIndex(
  { "user_id": 1, "status": 1, "create_time": -1 },
  { "partialFilterExpression": { "status": "completed" } } // 部分索引,只索引已完成订单
)
// 查询改为只投影需要的字段
db.orders.find({
  user_id: 12345,
  status: "completed"
}, { order_id: 1, create_time: 1, _id: 0 }) // _id:0 排除默认返回的_id
).sort({ create_time: -1 })
// 这个查询可以被我们创建的索引完全覆盖,性能极佳。

③、写入优化

  • 有序 vs 无序写入:ordered: false 的批量插入更快,因为可以并行执行,但无法保证顺序。
  • 调整写关注 (Write Concern):根据业务对数据安全性的要求调整。
    w: 1 (默认):写主节点确认。性能好。
    w: majority:写大多数节点确认。数据安全,但延迟高。
  • 批量插入:使用 insertMany() 而非循环 insertOne()。

案例:批量插入日志数据(可容忍少量丢失)

js 复制代码
db.app_log.insertMany(
  [ ... ], // 巨大的日志文档数组
  {
    ordered: false, // 无序写入,提升速度
    writeConcern: { w: 0 } // 不等待确认,速度最快,但可靠性最低
  }
)

④、分片集群优化 (应对海量数据)

选择合适的分片键 (Shard Key):这是最重要的决策。

  • 要求: 基数大、频率高、写分布均匀。避免单调递增的分片键(如时间戳、自增ID),会导致"热分片"和写瓶颈。
  • 策略: 使用复合分片键(如 {customer_id: 1, timestamp: -1})或基于哈希的分片({_id:
    "hashed"})。

预分裂 (Pre-Splitting):在大规模导入数据前,手动预先分割分片,避免集群自动平衡带来的开销。

缓存层优化(本地缓存+分布式缓存)

企业级项目通常采用多级缓存架构,最大化减少对分布式缓存的访问延迟和压力。

L1: 本地缓存 (Local Cache):

  • 特点: 速度极快(内存访问),与应用进程共生,无网络开销。
  • 缺点: 容量有限,数据不一致(不同节点间缓存可能不同)。
  • 适用场景: 极热点的、数据量小、更新不频繁的数据(如字典数据、配置信息)。

技术选型: Caffeine (高性能,推荐), Guava Cache, Ehcache。

L2: 分布式缓存 (Distributed Cache):

  • 特点: 容量大,可扩展,所有应用节点共享同一份视图,保证数据一致性。
  • 缺点: 有网络开销,速度慢于本地缓存。
  • 适用场景: 共享的、大量的热点数据。

技术选型: Redis (最主流), Memcached。

应用 -> 本地缓存 -> (未命中) -> 分布式缓存 -> (未命中) -> 数据库

缓存模式 (Cache Pattern)

  • Cache-Aside (旁路缓存): 最常用模式。应用代码直接负责读写缓存和数据库。
  • Read/Write-Through: 缓存提供商负责同步读写缓存和数据库,对应用透明。实现复杂,较少用。
  • Write-Behind: 异步写入数据库,性能最好,但有数据丢失风险。

以最经典的 Cache-Aside 模式为例,结合 Spring Boot + Caffeine + Redis 进行说明

①、缓存穿透

查询一个根本不存在的数据。请求会穿过缓存,直接访问数据库。如果有人恶意攻击,大量请求不存在的key,会导致数据库压力巨大。

  • 缓存空对象 (Cache Null): 即使从DB没查到,也将一个空值(或特殊标记)写入缓存,并设置一个较短的过期时间(如30秒)
  • 布隆过滤器 (Bloom Filter):在缓存之前加一层布隆过滤器。它能够以极小的空间代价判断一个key是否一定不存在于数据库中。对于不存在的key,直接拦截返回。
java 复制代码
@Service
@RequiredArgsConstructor
public class ProductServcie{
	
	private final ProductMapper productMapper;
	private final RedisTemplate<String,Object> redisTemplate;

	private final CacheManager cacheManager;

	private static final String PRODUCT_CACHE = "products";
	private static final String NULL_PLACEHOLDER = "NULL";//空值标记

	public Product getProductById(){
		
		// 1. 构造缓存Key
        String cacheKey = "product:" + id;

        // 2. 先从缓存查询 (这里直接使用RedisTemplate演示,也可用@Cacheable)
        Object value = redisTemplate.opsForValue().get(cacheKey);
        
        // 3. 如果缓存命中,且不是空标记,则直接返回
        if (value != null) {
            if (NULL_PLACEHOLDER.equals(value)) {
                // 命中的是空值,防止穿透,直接返回null,无需查库
                return null;
            }
            return (Product) value;
        }

        // 4. 缓存未命中,查询数据库
        Product product = productMapper.selectById(id);

        // 5. 数据库也不存在,缓存空对象(设置短过期时间,如30秒)
        if (product == null) {
            redisTemplate.opsForValue().set(cacheKey, NULL_PLACEHOLDER, 30, TimeUnit.SECONDS);
            return null;
        }

        // 6. 数据库存在,写入缓存(设置正常过期时间,如1小时)
        redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
        return product;
	}
}

②、缓存击穿

某个热点key在缓存过期的瞬间,有大量并发请求进来,导致所有请求都落到数据库上。

  • 互斥锁 (Mutex Lock): 只允许一个请求去重建缓存,其他请求等待或返回旧数据。
  • 逻辑过期 (Logical Expiration):
    不给缓存设置物理TTL,而是定义一个逻辑过期字段。当发现逻辑过期时,另起线程去异步更新缓存,当前请求返回旧数据。
java 复制代码
public Product getProductByIdWithLock(Long id) {
    String cacheKey = "product:" + id;
    String lockKey = "lock:product:" + id; // 锁的Key
    Product product;

    // 1. 尝试从缓存获取
    product = (Product) redisTemplate.opsForValue().get(cacheKey);
    if (product != null) {
        return product;
    }

    // 2. 缓存未命中,尝试获取分布式锁
    // SET lockKey currentTime NX PX 3000 -> 原子操作:不存在时设置,并过期时间3秒
    String token = UUID.randomUUID().toString();
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, token, 3, TimeUnit.SECONDS);

    try {
        if (Boolean.TRUE.equals(isLock)) {
            // 3. 获取锁成功,再次检查缓存(Double Check),因为可能前面的请求已经重建好了
            product = (Product) redisTemplate.opsForValue().get(cacheKey);
            if (product != null) {
                return product;
            }

            // 4. 查询数据库
            product = productMapper.selectById(id);
            if (product == null) {
                // 防止穿透
                redisTemplate.opsForValue().set(cacheKey, NULL_PLACEHOLDER, 30, TimeUnit.SECONDS);
                return null;
            }
            // 5. 写入缓存
            redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
        } else {
            // 6. 获取锁失败,说明有其他线程在重建缓存,休眠一下再重试(自旋)
            Thread.sleep(50);
            return getProductByIdWithLock(id); // 递归重试
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("Interrupted while acquiring lock", e);
    } finally {
        // 7. 释放锁:使用Lua脚本保证原子性,判断token是否还是自己的再删除
        // 避免误删其他线程的锁
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), 
                             Collections.singletonList(lockKey), 
                             token);
    }
    return product;
}

③、缓存雪崩

大量缓存key在同一时间点(或时间段)大面积失效,导致所有请求都落到数据库上,造成数据库瞬时压力过大。

  • 差异化过期时间: 给缓存设置过期时间时,增加一个随机值,避免同时失效。TTL = baseTime + random.nextInt(0, 300) // 例如基础1小时 + 随机5分钟内
  • 缓存永不过期,后台更新: 缓存不设TTL,而是由后台任务或定时任务定期异步更新缓存。
  • 构建高可用缓存集群: 如Redis Cluster,防止单点故障导致整个缓存层不可用。
  • 服务熔断与降级: 使用Hystrix或Sentinel等组件,当数据库压力过大时,对请求进行熔断,直接返回降级信息(如"服务繁忙,请稍后再试")。
java 复制代码
// 基础过期时间
long baseExpireTime = TimeUnit.HOURS.toSeconds(1);
// 随机增加0-5分钟的随机时间
long randomExpire = ThreadLocalRandom.current().nextLong(0, TimeUnit.MINUTES.toSeconds(5));
long finalExpireTime = baseExpireTime + randomExpire;

redisTemplate.opsForValue().set(cacheKey, product, finalExpireTime, TimeUnit.SECONDS);

本地缓存与分布式缓存协同 (Caffeine + Redis)

yml 复制代码
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=10000,expireAfterWrite=60s # 本地缓存:1万条,60秒过期
    redis:
      time-to-live: 3600s # 分布式缓存:1小时过期(基础值,代码中会加随机)
      use-key-prefix: true
      cache-null-values: true # 允许缓存空值,解决穿透
java 复制代码
@Configuration
@EnableCaching
public class CacheConfig {
    // Caffeine配置已通过yaml完成
}

@Service
public class ProductService {

    // 此方法会自动使用两级缓存:先查Caffeine,未命中再查Redis,再未命中才执行方法体查DB。
    // 查到的结果会依次写回Redis和Caffeine。
    @Cacheable(value = "products", key = "#id", unless = "#result == null")
    public Product getProductById(Long id) {
        // 防止击穿:在方法上加分布式锁的逻辑需要自己实现,注解无法自动完成。
        // 或者使用逻辑过期方案,对注解返回的结果进行包装。
        Product product = productMapper.selectById(id);
        // 如果查不到,因为配置了`cache-null-values: true`,Spring会缓存null值
        return product;
    }

    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        productMapper.updateById(product);
        return product; // 更新缓存
    }

    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        productMapper.deleteById(id);
    }
}

缓存一致性

策略:

  • 先更新数据库,再删除缓存 (推荐): updateDB(); invalidateCache();
    这是最常用的模式,出现不一致的概率较低。即使第二步失败,也只是导致一次脏读,下次读取时会纠正。
  • 通过 Canal 监听数据库Binlog,异步更新/删除缓存: 解耦应用和缓存更新逻辑,可靠性高,是大型互联网公司的首选方案。

监控与告警

  • 监控指标: 缓存命中率、慢查询、内存使用率、网络流量、Key数量。
  • 工具: Redis自带的 INFO 命令、RedisExporter + Prometheus + Grafana。
  • 设置告警规则:当缓存命中率过低(如低于80%)或内存使用率过高(如超过80%)时触发告警。

总结:

  • 架构上: 采用多级缓存(Caffeine + Redis)分担压力。
  • 策略上: 严格使用 Cache-Aside 模式,并配套解决三大问题:
    穿透: 缓存空对象 + 布隆过滤器。
    击穿: 互斥锁(分布式锁)或逻辑过期。
    雪崩: 差异化过期时间 + 高可用集群。
    缓存工具类封装
  • 实现上: 优先使用成熟的框架(Spring Cache)简化开发,但对核心热点数据访问路径要有能力进行手动精细控制(如自己实现锁逻辑)。
  • 运维上: 完善的监控和告警是保证缓存层长期稳定运行的基石。

具体内容:缓存具体介绍

代码层的优化(异步+JUC等)

JUC并发包参考:具体JUC内容介绍

监控先行: 使用APM工具和Micrometer监控线程池状态、锁竞争情况、队列长度。

选择合适的工具:

  • 计算任务: ForkJoinPool
  • IO任务: 自定义 ThreadPoolExecutor
  • 缓存: Caffeine / Guava Cache
  • 映射表: ConcurrentHashMap
  • 计数器: LongAdder

避免常见陷阱:

  • 线程池: 使用有界队列和合理的拒绝策略。
  • 锁: 尽量缩小锁范围,使用读写锁减少竞争。
  • CompletableFuture: 注意异常处理和线程池选择。

性能测试: 使用JMeter等工具对优化前后的代码进行压测,用数据证明优化效果。

一、线程池优化

Java高并发(线程创建以及线程池,异步调用,CompletableFuture)详解

SpringBoot整合定时任务+定时任务启用异步线程池

  • CPU密集型任务: 线程数 ≈ CPU核数 + 1
  • IO密集型任务: 线程数 ≈ CPU核数 * (1 + 平均等待时间/平均计算时间)

①、使用合适的线程池

java 复制代码
@Service
public class OrderProcessingService {

    // IO密集型任务(如调用外部API、数据库操作),假设CPU为8核
    // 最佳线程数 = 8 * (1 + 等待时间/计算时间) ≈ 8 * (1 + 10) ≈ 80
    private final ThreadPoolExecutor ioBoundExecutor = new ThreadPoolExecutor(
        50, // 核心线程数:维持的常驻线程,避免频繁创建销毁
        100, // 最大线程数:突发流量的应对能力
        60L, TimeUnit.SECONDS, // 空闲线程存活时间
        new LinkedBlockingQueue<>(1000), // 任务队列,需设置有界队列防止OOM
        new ThreadFactoryBuilder().setNameFormat("order-io-pool-%d").build(), // 命名线程,便于监控
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行,是一种降级
    );

    // CPU密集型任务(如计算、数据处理)
    private final ExecutorService cpuBoundExecutor = Executors.newWorkStealingPool(Runtime.getRuntime().availableProcessors());

    public CompletableFuture<Order> processOrderAsync(Order order) {
        return CompletableFuture.supplyAsync(() -> {
            // 模拟IO操作
            enrichOrderDetails(order); // 调用外部服务
            calculateOrderPrice(order); // CPU计算
            saveOrderToDB(order); // 数据库操作
            return order;
        }, ioBoundExecutor); // 指定使用IO线程池
    }

    // 优雅关闭,在应用关闭时调用
    @PreDestroy
    public void shutdown() {
        ioBoundExecutor.shutdown();
        cpuBoundExecutor.shutdown();
    }
}

②、监控线程池状态:

使用Micrometer监控线程池指标,便于调优。

java 复制代码
@Configuration
public class ThreadPoolMonitorConfig {

    @Autowired
    public void registerMetrics(ThreadPoolExecutor executor, MeterRegistry registry) {
        Gauge.builder("thread.pool.core.size", executor, ThreadPoolExecutor::getCorePoolSize)
                .tags("poolName", "order-io-pool")
                .register(registry);
        Gauge.builder("thread.pool.active.count", executor, ThreadPoolExecutor::getActiveCount)
                .tags("poolName", "order-io-pool")
                .register(registry);
        Gauge.builder("thread.pool.queue.size", executor, e -> e.getQueue().size())
                .tags("poolName", "order-io-pool")
                .register(registry);
    }
}

二、并发数据结构

使用线程安全的集合类替代 synchronized 包装类,性能提升显著。

①、高并发读、少量写的场景:使用 CopyOnWriteArrayList

java 复制代码
@Service
public class ProductConfigService{
	
	//产品配置列表,读多写少(写只在每天凌晨刷新一次)
	private final CopyOnWriteArrayList<ProductConfig> configList = CopyOnWriteArrayList<>();

	//并发读:性能极高,无需加锁
	public ProductConfig getConfigById(String id){
		return configList.stream()
                        .filter(config -> config.getId().equals(id))
                        .findFirst()
                        .orElse(null);
	}

	// 写操作:加锁,复制整个数组,成本高。适合低频写。
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点刷新
    public void refreshConfig() {
        List<ProductConfig> newConfigs = loadConfigsFromDB();
        configList.clear();
        configList.addAll(newConfigs);
    }
}

②、高并发计数器:使用 LongAdder (替代 AtomicLong)

java 复制代码
@Service
public class MetricsService {

    // 统计API调用次数。LongAdder在高并发下性能远优于AtomicLong。
    private final LongAdder apiCallCounter = new LongAdder();
    private final LongAdder errorCounter = new LongAdder();

    public void recordApiCall() {
        apiCallCounter.increment();
    }

    public void recordError() {
        errorCounter.increment();
    }

    public long getApiCallCount() {
        return apiCallCounter.sum();
    }

    public long getErrorCount() {
        return errorCounter.sum();
    }
}

③、高并发映射表:使用 ConcurrentHashMap

java 复制代码
@Service
public class UserSessionManager {

    // 存储用户会话信息,key: userId, value: SessionInfo
    private final ConcurrentHashMap<Long, SessionInfo> sessionMap = new ConcurrentHashMap<>();

    // 使用 computeIfAbsent 保证原子性的"如果不存在则计算"
    public SessionInfo getSession(Long userId) {
        return sessionMap.computeIfAbsent(userId, id -> {
            // 只有当key不存在时,这个函数才会被执行
            SessionInfo newSession = createNewSession(id);
            return newSession;
        });
    }

    // 使用 forEach 进行并发遍历(弱一致性)
    public void expireOldSessions(Duration duration) {
        long cutoffTime = System.currentTimeMillis() - duration.toMillis();
        sessionMap.forEach((userId, session) -> {
            if (session.getLastAccessTime() < cutoffTime) {
                sessionMap.remove(userId, session); // 使用 remove(key, value) 避免误删
            }
        });
    }
}

三、锁优化

减少锁竞争是提升并发性能的核心。

①、读写锁 (ReentrantReadWriteLock):读多写少的场景

java 复制代码
@Service
public class ProductInventoryService {

    private final Map<Long, Integer> inventoryMap = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 多个线程可以同时读库存
    public Integer getInventory(Long productId) {
        rwLock.readLock().lock();
        try {
            return inventoryMap.getOrDefault(productId, 0);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 写库存时独占锁
    public void updateInventory(Long productId, Integer quantity) {
        rwLock.writeLock().lock();
        try {
            inventoryMap.put(productId, quantity);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

②、分段锁(Striped Lock):优化热点资源竞争

java 复制代码
@Service
public class StripedLockOrderService {

    // 按订单ID进行分段锁,减少锁竞争
    private static final int STRIPE_COUNT = 16;
    private final Lock[] stripes = new ReentrantLock[STRIPE_COUNT];

    public StripedLockOrderService() {
        for (int i = 0; i < STRIPE_COUNT; i++) {
            stripes[i] = new ReentrantLock();
        }
    }

    private Lock getLock(Long orderId) {
        // 简单的哈希取模,将不同订单散列到不同的锁上
        return stripes[(int) (orderId % STRIPE_COUNT)];
    }

    public void processOrder(Long orderId) {
        Lock lock = getLock(orderId);
        lock.lock();
        try {
            // 处理订单业务,同一订单串行,不同订单并行
            doProcessOrder(orderId);
        } finally {
            lock.unlock();
        }
    }
}

③、自旋锁与 Atomic 字段更新器:极致性能场景

java 复制代码
public class CompactCounter {

    private volatile long value;
    private static final AtomicLongFieldUpdater<CompactCounter> UPDATER =
            AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "value");

    public long increment() {
        return UPDATER.incrementAndGet(this);
    }
    // 比 AtomicLong 节省内存,性能相近
}

四、异步编排与并发工具

①、CompletableFuture 异步编排:优化串行远程调用

java 复制代码
@Service
public class UserDetailService {

    private final ExecutorService taskExecutor = ...;

    public CompletableFuture<UserProfile> getUserProfileAsync(Long userId) {
        // 并行调用多个外部服务,最后合并结果
        CompletableFuture<UserInfo> userInfoFuture = CompletableFuture.supplyAsync(
            () -> userService.getUserInfo(userId), taskExecutor);
        CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(
            () -> orderService.getUserOrders(userId), taskExecutor);
        CompletableFuture<Account> accountFuture = CompletableFuture.supplyAsync(
            () -> accountService.getAccount(userId), taskExecutor);

        // 等所有任务完成,然后组合结果
        return CompletableFuture.allOf(userInfoFuture, ordersFuture, accountFuture)
                .thenApplyAsync(v -> {
                    try {
                        UserInfo userInfo = userInfoFuture.get();
                        List<Order> orders = ordersFuture.get();
                        Account account = accountFuture.get();
                        return new UserProfile(userInfo, orders, account);
                    } catch (Exception e) {
                        throw new CompletionException(e);
                    }
                }, taskExecutor);
    }
}

②、CountDownLatch / CyclicBarrier:并行任务栅栏

java 复制代码
@Service
public class DataInitializationService {

    public void initAllData() throws InterruptedException {
        int taskCount = 3;
        CountDownLatch latch = new CountDownLatch(taskCount);
        ExecutorService executor = Executors.newFixedThreadPool(taskCount);

        executor.submit(() -> { initUserData(); latch.countDown(); });
        executor.submit(() -> { initProductData(); latch.countDown(); });
        executor.submit(() -> { initOrderData(); latch.countDown(); });

        // 等待所有初始化任务完成,最多等10分钟
        if (!latch.await(10, TimeUnit.MINUTES)) {
            throw new RuntimeException("Data initialization timeout");
        }
        executor.shutdown();
    }
}

③、Semaphore:控制资源并发访问数

java 复制代码
@Service
public class RateLimitedHttpClient {

    // 限制对某个外部API的并发调用数不超过10个
    private final Semaphore semaphore = new Semaphore(10);

    public String callExternalApi(String url) {
        if (!semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
            throw new RuntimeException("API call limit exceeded");
        }
        try {
            return httpClient.get(url);
        } finally {
            semaphore.release();
        }
    }
}

架构层的优化(中间件)

异步化 (Asynchronization) - 消息队列 (MQ)

识别场景: 找出适合异步化的场景,如:

  • 耗时操作: 发送短信/邮件、生成报表、上传大文件。
  • 非核心业务: 用户行为日志记录、积分更新、消息推送。
  • 流量削峰: 秒杀、抢购等场景,将请求先存入MQ,后端服务慢慢消费。

引入MQ依赖: 在项目中引入MQ客户端依赖。

配置MQ连接: 在配置文件中配置MQ的NameServer/Broker地址、生产者组、消费者组等。

编写生产者: 在需要发送消息的地方,注入MQ模板类,发送消息。

编写消费者: 创建监听器类,消费指定Topic的消息,并实现业务逻辑。

考虑事务消息: 对于需要保证业务操作和消息发送一致性的场景(如下单后发券),使用事务消息。

SpringBoot整合消息中间件

RocketMQ多种消费模式

①、添加依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>

②、配置文件

yml 复制代码
rocketmq:
  name-server: 127.0.0.1:9876 # RocketMQ nameserver 地址
  producer:
    group: my-producer-group # 生产者组名

③、生产者服务

java 复制代码
@Service
public class OrderService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;
	
	public void createOrder(){
		
		//1.本地数据库事务,创建订单
		orderDao.insert(order);

		// 2. 订单创建成功后,发送一个异步消息(例如:发放优惠券)
        // 使用异步消息确保即使消息发送失败,也不会回滚订单事务(最终一致性)
        String topic = "Topic-OrderCreated";
        String payload = "OrderID:" + order.getId();

		rocketMQTemplate.asyncSend(topic, MessageBuilder.withPayload(payload).build(), new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("消息发送成功:{}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                log.error("消息发送失败,订单ID:{}", order.getId(), e);
                // 可以在这里加入重试逻辑或记录补偿日志
            }
        });
        //3.立即返回结果给用户
	}

	
}

④、消费者

java 复制代码
@Service
@RocketMQMessageListener(topic = "Topic-OrderCreated", consumerGroup = "my-consumer-group-coupon")
public class OrderCreatedCouponListener implements RocketMQListener<String> {

    @Autowired
    private CouponService couponService;

    @Override
    public void onMessage(String message) {
        // 解析消息,获取订单ID
        String orderId = message.split(":")[1];
        log.info("收到订单创建消息,开始为订单 {} 发放优惠券", orderId);
        
        try {
            couponService.grantCouponByOrderId(orderId);
        } catch (Exception e) {
            log.error("发放优惠券失败,订单ID: {}", orderId, e);
            // 注意:RocketMQ消费者默认会自动重试(重试16次后进入死信队列)
            // 根据业务决定是否需要抛出异常以触发重试
            throw e; 
        }
    }
}

服务熔断与降级

防止分布式系统出现"雪崩效应",当某个服务出现故障或延迟时,提供备用方案,保证核心链路的可用性。

企业级解决方案: Resilience4j, Sentinel (Alibaba), Hystrix (Netflix,已停维护,推荐前两者)。

优化步骤 (以 Resilience4j 为例):

①、添加依赖

xml 复制代码
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

②、配置文件application.yml

配置熔断器/降级规则: 在配置文件中为特定方法设置规则(失败率、滑动窗口大小、半开状态等待时间等)

yaml 复制代码
resilience4j:
  circuitbreaker:
    instances:
      userServiceCB: # 熔断器实例名称
        registerHealthIndicator: true
        slidingWindowSize: 10 # 滑动窗口大小
        minimumNumberOfCalls: 5 # 最小调用次数,低于此数则不开启熔断计算
        waitDurationInOpenState: 5s # 熔断开启后,等待多久进入半开状态
        failureRateThreshold: 50 # 失败率阈值,超过50%则熔断
        permittedNumberOfCallsInHalfOpenState: 3 # 半开状态下允许的调用次数

③、Service层注解

java 复制代码
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class OrderService {

    @Autowired
    private UserServiceClient userServiceClient; // 一个Feign客户端,用于调用远程用户服务

    /**
     * 获取用户订单详情
     * 需要调用用户服务获取用户信息
     */
    @CircuitBreaker(name = "userServiceCB", fallbackMethod = "getUserOrderDetailFallback")
    // 可以组合多个注解,比如再加一个 @TimeLimiter 做超时控制
    public UserOrderDetail getUserOrderDetail(String orderId, String userId) {
        // 1. 本地查询订单
        Order order = orderDao.selectById(orderId);
        
        // 2. 【远程调用】- 可能失败或高延迟的点
        User user = userServiceClient.getUserById(userId); 

        // 3. 组装数据
        return new UserOrderDetail(order, user);
    }

    /**
     * Fallback 方法
     * 参数和返回值必须与原方法一致,最后可以多加一个 Throwable 参数用来接收异常
     */
    private UserOrderDetail getUserOrderDetailFallback(String orderId, String userId, Throwable t) {
        log.warn("调用用户服务失败,进入降级逻辑。订单ID: {}, 用户ID: {}", orderId, userId, t);
        
        // 降级策略:
        // 1. 返回一个兜底数据
        Order order = orderDao.selectById(orderId);
        User dummyUser = new User();
        dummyUser.setId(userId);
        dummyUser.setName("用户信息暂不可用");
        
        return new UserOrderDetail(order, dummyUser);
        
        // 2. 也可以返回一个空对象、默认值,或者从本地缓存中获取旧数据
        // 3. 根据业务场景选择最合适的策略
    }

    // 【异步熔断示例】- 使用 CompletableFuture
    @CircuitBreaker(name = "asyncServiceCB", fallbackMethod = "asyncFallback")
    @TimeLimiter(name = "asyncServiceCB") // 组合使用超时控制
    public CompletableFuture<String> asyncCall() {
        return CompletableFuture.supplyAsync(() -> {
            // 模拟一个耗时的远程调用
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return "Success";
        });
    }

    public CompletableFuture<String> asyncFallback(Throwable t) {
        return CompletableFuture.completedFuture("Fallback result due to: " + t.getMessage());
    }
}
相关推荐
你我约定有三12 小时前
面试tips--JVM(4)--Minor GC & Major GC & Full GC
jvm·面试·职场和发展
zhz521412 小时前
基于SQLite索引的智能图片压缩存储系统设计与实现
数据库·sqlite·图片压缩·压缩
Li_yizYa12 小时前
JVM:内存区域划分、类加载的过程、垃圾回收机制
java·jvm
周小码12 小时前
Turso数据库:用Rust重构的下一代SQLite——轻量级嵌入式数据库的未来选择
数据库·重构·rust
ByteBlossom12 小时前
JVM核心机制:类加载与内存结构详解
jvm
善我12 小时前
JVM中产生OOM(内存溢出)的8种典型情况及解决方案
jvm
程序员江鸟12 小时前
Java面试实战系列【JVM篇】- JVM内存结构与运行时数据区详解(共享区域)
java·jvm·面试
showmethetime13 小时前
Ubuntu平台查看.gz格式压缩文件内容以及利用grep命令过滤搜索内容
数据库·ubuntu·postgresql
ybq1951334543113 小时前
RabbitMinQ(模拟实现消息队列项目)02
jvm