MySQL性能优化

MySQL 性能优化不是看到慢 SQL 就立刻加索引。真正有效的优化过程,应该先确认问题范围,再定位瓶颈,再选择成本最低、收益最稳定的方案。很多线上问题不是某一条 SQL 写得差,而是数据量、索引设计、查询模型、分页方式、事务范围和业务流程共同导致的。

在供应链系统里,订单、库存、采购、出入库、结算这些表增长很快。系统刚上线时几万条数据跑得很快,半年后变成几千万条,原来的查询就可能从几十毫秒变成几秒。优化 MySQL 的关键是让数据库少扫描、少排序、少回表、少锁等待。

MySQL SQL 优化步骤流程

第一步:先确认是不是数据库慢

当用户反馈订单列表慢时,不要直接打开 SQL 改写。应该先确认耗时发生在哪里。

一次请求的耗时可能来自:

  • 网关排队。
  • 应用线程池不足。
  • 远程服务调用慢。
  • Redis 缓存超时。
  • MySQL 查询慢。
  • 返回数据过大导致网络传输慢。
  • 前端渲染慢。

可以通过接口日志、链路追踪、APM、慢查询日志来拆分耗时。如果接口总耗时 3 秒,其中 MySQL 查询只用了 80 毫秒,那优化 SQL 不是当前重点。

在 Java 应用里,建议每个核心接口记录 traceId、SQL 耗时、调用下游耗时、返回条数。没有这些数据,优化容易变成猜。

第二步:找到真实慢 SQL

确认瓶颈在数据库后,再打开 MySQL 慢查询日志或使用监控平台找慢 SQL。

慢 SQL 的判断不能只看单次耗时,还要看调用频率。一个报表 SQL 偶尔跑 5 秒不一定最急;一个订单列表 SQL 每次 300 毫秒、每分钟调用几千次,可能对系统压力更大。

常用观察指标包括:

  • 平均耗时。
  • P95、P99 耗时。
  • 扫描行数。
  • 返回行数。
  • 执行次数。
  • 是否频繁创建临时表。
  • 是否频繁 filesort。
  • 锁等待时间。

供应链系统中常见慢 SQL 包括订单多条件查询、库存流水查询、采购单明细关联、应收应付报表、仓库作业统计。

第三步:使用 EXPLAIN 分析执行计划

拿到慢 SQL 后,必须用 EXPLAIN 看执行计划。执行计划告诉我们 MySQL 准备怎么执行这条 SQL。

重点关注这些字段:

  • type:访问类型,至少应尽量达到 rangeref,避免大表 ALL
  • key:实际使用的索引。
  • rows:预估扫描行数。
  • Extra:是否出现 Using temporaryUsing filesort
  • filtered:过滤比例。

比如订单表 scm_order 有 3000 万行,页面按客户、状态、创建时间查询:

1
2
3
4
5
6
7
SELECT id, order_no, customer_id, status, created_at, total_amount
FROM scm_order
WHERE customer_id = 10086
AND status = 'WAIT_DELIVERY'
AND created_at >= '2024-08-01'
ORDER BY created_at DESC
LIMIT 20;

如果只在 customer_id 上有单列索引,MySQL 可能先找出该客户所有订单,再过滤状态和时间,并排序。客户订单量大时,这个查询就会慢。

第四步:按查询模式设计联合索引

索引不是越多越好。索引会提升查询,也会增加写入成本和存储成本。优化时应该根据高频查询模式设计联合索引。

上面的订单查询可以考虑:

1
2
CREATE INDEX idx_customer_status_created
ON scm_order(customer_id, status, created_at);

这样 MySQL 可以先按客户过滤,再按状态过滤,再按时间范围扫描。由于排序字段也在索引里,ORDER BY created_at DESC LIMIT 20 的成本会明显降低。

联合索引设计要考虑最左前缀原则。等值条件通常放在前面,范围条件和排序字段放在后面。对于不同查询,不要为了每个页面都创建一个索引,而是合并相近查询,找最核心的访问路径。

供应链系统里的常见索引组合包括:

  • 订单:customer_id + status + created_at
  • 采购单:supplier_id + status + created_at
  • 库存流水:sku_id + warehouse_id + created_at
  • 出库单:warehouse_id + status + planned_ship_time
  • 财务单据:tenant_id + bill_type + bill_date

第五步:避免 SELECT *

很多列表接口习惯写 SELECT *,这在数据量小时问题不明显,数据量大后会导致更多 IO、更多网络传输,也更容易触发回表。

列表页应该只查询展示需要的字段。详情页再根据主键查询完整信息。

例如订单列表只需要订单号、客户、状态、金额、创建时间,就不要把备注、扩展 JSON、收货地址、审批意见一起查出来。供应链系统里很多表有大字段,比如合同备注、物流轨迹、扩展属性,这些字段对列表查询非常不友好。

如果查询字段都在索引里,还可能形成覆盖索引,减少回表。

第六步:优化分页方式

深分页是 MySQL 性能问题的高发点。

1
2
3
4
5
SELECT id, order_no, created_at
FROM scm_order
WHERE status = 'FINISHED'
ORDER BY created_at DESC
LIMIT 100000, 20;

这条 SQL 需要先跳过 100000 行,再返回 20 行。页码越深,成本越高。

更好的方式是基于游标分页:

1
2
3
4
5
6
SELECT id, order_no, created_at
FROM scm_order
WHERE status = 'FINISHED'
AND created_at < '2024-08-24 14:00:00'
ORDER BY created_at DESC
LIMIT 20;

页面保存上一页最后一条记录的创建时间和 id,下一页从这个位置继续查。对于订单流水、库存流水、操作日志这类按时间滚动查看的数据,游标分页更稳定。

如果必须支持跳页,可以用延迟关联优化:先通过索引查出主键,再回表查详情。

第七步:减少大表 JOIN 和复杂统计

供应链系统经常有多表关联:订单表、客户表、商品表、库存表、仓库表、结算表。开发时写一个大 JOIN 很方便,但数据量上来后很难优化。

优化思路包括:

  • 把高频展示字段冗余到主表,例如客户名称、供应商名称、仓库名称。
  • 明细表和主表分开查询,避免列表页一次 JOIN 大量明细。
  • 报表类查询走宽表、汇总表或数仓,不要压在交易库上。
  • 对低频复杂查询做异步导出。

例如订单列表需要展示客户名称,可以在订单表冗余 customer_name_snapshot。客户改名时不影响历史订单展示,也减少订单列表 JOIN 客户表。

第八步:控制事务范围和锁

SQL 慢不一定是扫描慢,也可能是锁等待。

供应链系统里库存扣减、采购入库、出库确认都可能更新同一批库存行。如果事务里先调用外部接口,再更新库存,就会把锁持有时间拉长,导致并发请求排队。

优化原则是:

  • 事务内只做必要的数据库操作。
  • 外部接口调用放在事务外。
  • 固定多表更新顺序,减少死锁。
  • 库存扣减使用条件更新,避免先查后改的并发漏洞。
  • 对热点库存考虑分仓、分批次、分桶。

库存扣减可以写成:

1
2
3
4
5
6
UPDATE scm_inventory
SET available_qty = available_qty - 10,
locked_qty = locked_qty + 10
WHERE sku_id = 123
AND warehouse_id = 8
AND available_qty >= 10;

然后根据影响行数判断是否锁定成功。这比先查询库存再更新更安全。

第九步:用业务模型减少查询压力

有些性能问题不能只靠 SQL 调优解决,而要调整业务模型。

例如 ERP 首页要展示今天订单数、待发货数、库存预警数、采购待审数。如果每次打开首页都实时扫订单表、库存表、采购表,数据库压力会很大。

更合理的方式是把这些指标做成统计表:

1
2
3
4
5
6
7
scm_dashboard_metric
- metric_date
- tenant_id
- wait_delivery_count
- low_stock_sku_count
- pending_purchase_count
- updated_at

业务事件发生后异步更新统计表,首页直接读统计结果。这样牺牲几秒一致性,换来稳定的查询性能。

第十步:验证优化效果

优化完成后,必须验证,而不是只看执行计划。

验证内容包括:

  • 优化前后 SQL 耗时对比。
  • 扫描行数是否下降。
  • 是否仍然出现临时表和 filesort。
  • 高并发下连接池、CPU、IO 是否改善。
  • 写入性能是否被新增索引拖慢。
  • 结果是否和优化前一致。

对于核心 SQL,建议把优化结论记录下来:原 SQL、问题原因、改造方案、索引变更、回滚方式、压测结果。这样后续团队维护时不会重复踩坑。

总结

MySQL 性能优化的步骤可以概括为:确认瓶颈、定位慢 SQL、分析执行计划、设计索引、改写 SQL、优化分页、减少大 JOIN、控制事务、调整业务模型、验证效果。

在供应链系统里,性能优化不能只看数据库技巧,还要理解业务访问模式。订单列表、库存流水、采购审批、财务报表的查询目标不同,优化策略也不同。真正稳定的优化方案,是让 SQL、索引和业务模型匹配起来。

分布式系统CAP理论:供应链业务中的一致性取舍

CAP 理论是理解分布式系统取舍的基础。它经常被概括为“一致性、可用性、分区容错性三选二”,这个说法便于记忆但不够严谨,也容易在真实系统设计中被误用。

更实用的理解是:当系统出现网络分区时,分布式系统必须在一致性和可用性之间做取舍。平时网络正常时,一个系统可以同时做到比较好的一致性和可用性;真正考验架构选择的,是网络异常、节点超时、跨机房通信失败、消息延迟这些情况。

CAP 在供应链系统中的取舍流程

CAP 分别是什么意思

CAP 包含三个概念。

Consistency,一致性,在 CAP 语境下接近线性一致性:一次写入成功后,后续读取应表现得像访问同一份按顺序更新的数据,不能从其他节点读到更旧的值。它不同于数据库 ACID 中的 Consistency。

Availability,可用性,指每个非故障节点都能在合理时间内返回响应。注意这里强调的是返回响应,不代表一定返回最新数据。

Partition Tolerance,分区容错性,指系统在网络分区发生时仍然能够继续运行。网络分区可以理解为节点之间无法通信,或者通信延迟大到系统认为对方已经不可用。

在单机系统里,不需要讨论 P,因为没有跨节点通信。但 Java 分布式系统天然存在多个进程、多个节点、多个数据库副本、多个机房,P 几乎不可避免。只要系统是分布式的,就必须默认网络会出问题。

为什么不是简单的三选二

CAP 经常被简化成 C、A、P 三选二,但工程上更准确的说法是:在发生网络分区时,如果继续对外提供服务,就可能牺牲强一致性;如果坚持强一致性,就可能拒绝部分请求,从而牺牲可用性。

也就是说,P 不是一个可以随便放弃的选项。对于真实分布式系统,网络分区一定要考虑。选择更多发生在 CP 和 AP 之间。

CP 系统优先保证一致性。网络异常时,它宁可让部分请求失败,也不返回可能错误的数据。

AP 系统优先保证可用性。网络异常时,它允许部分节点继续提供服务,但可能出现短时间数据不一致,后续通过补偿、同步、校验来恢复一致。

供应链业务里的库存扣减例子

假设有一个供应链系统,核心业务链路是:客户下单、锁定库存、生成出库任务、仓库拣货、物流发货。

其中库存是最容易体现 CAP 取舍的对象。系统有两个机房:上海机房和北京机房。为了提高访问速度,两个机房都部署了订单服务和库存服务,并且都有库存数据副本。

现在有一个 SKU:A1001,可售库存只剩 1 件。上海客户和北京客户几乎同时下单。

如果系统选择强一致性,库存扣减必须由能够形成单一提交顺序的权威节点或共识协议确认。分布式锁只有在具备正确的租约、故障转移和陈旧请求隔离机制时才能参与这一过程,不能仅凭“加了一把锁”就宣称获得强一致性。只有一个订单能成功锁定库存,另一个订单会失败或进入等待。这样可以避免并发超卖,但如果上海和北京之间网络断开,无法确认权威库存的一侧就要拒绝下单或提示系统繁忙。

这就是 CP 取舍:宁可不可用,也不能卖出不存在的库存。

如果系统选择高可用,上海机房和北京机房可以在网络分区时各自接单。两个机房都认为库存还有 1 件,于是两个订单都成功。等网络恢复后,系统发现库存被多扣了,需要做补偿:取消后下单的订单、通知客服、给用户补偿券,或者从其他仓库调拨。

这就是 AP 取舍:先保证用户能下单,再通过业务补偿处理不一致。

哪些供应链场景更适合 CP

不是所有业务都需要强一致性,但有些场景不应该轻易牺牲一致性。

第一类是库存最后一件商品的锁定。对于稀缺商品、定制件、批次严格的物料,如果超卖会带来严重履约风险,库存锁定应该偏 CP。

第二类是财务结算。供应商应付、客户应收、账期核销、发票金额,这些数据一旦错了,后续修正成本很高,甚至会影响审计。财务主账通常要选择强一致或准强一致。

第三类是审批状态。采购订单从待审到已审、已驳回、已作废,状态流转必须清晰。不能一个节点显示已审批,另一个节点还允许继续修改明细。

第四类是唯一性约束。例如采购单号、出库单号、批次号,不能因为网络分区在两个节点生成重复编号。

这些场景的共同点是:错误数据比短暂不可用更可怕。系统可以提示稍后重试,但不能让错误状态进入主流程。

哪些供应链场景可以偏 AP

也有很多场景更适合优先保证可用性。

第一类是商品基础信息展示。商品名称、图片、描述、类目、品牌等信息短时间不一致,通常不会造成严重后果。读多写少的数据可以通过缓存和异步同步提升可用性。

第二类是报表统计。当天订单量、仓库作业量、供应商履约率这些指标允许分钟级延迟。报表更关心趋势和分析,不一定要求每一次刷新都读到最新事务数据。

第三类是搜索索引。订单搜索、商品搜索、供应商搜索往往使用 Elasticsearch 等搜索引擎,索引延迟几秒或几十秒通常可以接受。

第四类是消息通知。订单创建后发送短信、站内信、邮件,如果短时间失败,可以重试或补发,不应该阻塞主交易链路。

这些场景的共同点是:短时间不一致可以被用户接受,系统可以通过最终一致性修复。

Java 系统里常见的 CAP 落地方式

在 Java 分布式系统里,CAP 不是只体现在数据库选型上,也体现在服务设计、缓存策略、消息机制和降级策略里。

对于偏 CP 的场景,单库内可以依赖事务、唯一索引、条件更新和行锁建立确定的提交顺序;跨节点则需要理解所用数据库或协调组件的复制与共识语义,例如基于 Raft 的多数派提交。普通异步主从复制不能自动等同于强一致。比如库存锁定时以 sku_id + warehouse_id 唯一标识库存行,通过带可用量条件的原子更新完成扣减,避免并发请求把库存扣成负数。

对于偏 AP 的场景,可以使用缓存、消息队列、异步任务、重试补偿、定时对账、事件溯源、最终一致性表等方式。比如订单创建后发送库存变更事件,报表系统异步消费事件生成统计结果。

关键是不要把所有业务都设计成一种取舍。供应链系统里,库存、财务、审批偏一致性;报表、搜索、通知偏可用性;订单主流程则通常是分段取舍:核心状态强一致,周边动作最终一致。

一个简单的 Java 策略 Demo

在代码里可以把不同业务能力的 CAP 取舍显式表达出来,避免所有服务都套同一种降级逻辑:

1
2
3
4
5
6
7
8
9
10
11
public enum ConsistencyMode {
CP_REQUIRED, // 失败就拒绝,不能返回可能错误的数据
AP_ACCEPTABLE // 先返回可用结果,后续通过补偿修正
}

public record CapabilityPolicy(
String capability,
ConsistencyMode mode,
Duration timeout
) {
}

库存锁定可以配置成偏 CP:

1
2
3
4
5
CapabilityPolicy reserveInventory = new CapabilityPolicy(
"reserve-inventory",
ConsistencyMode.CP_REQUIRED,
Duration.ofMillis(800)
);

消息通知可以配置成偏 AP:

1
2
3
4
5
CapabilityPolicy notifyCustomer = new CapabilityPolicy(
"notify-customer",
ConsistencyMode.AP_ACCEPTABLE,
Duration.ofMillis(200)
);

调用方根据策略决定失败处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public OrderResult createOrder(CreateOrderCommand command) {
inventoryService.reserve(command.skuId(), command.qty()); // CP:失败则订单不能创建

Order order = orderRepository.save(command.toOrder());

try {
notificationService.sendOrderCreated(order); // AP:失败不阻塞主链路
} catch (Exception ex) {
outboxRepository.save(NotificationEvent.from(order)); // 后续重试补偿
}

return OrderResult.success(order.getOrderNo());
}

这段代码表达了一个重要原则:不是所有异常都应该用同一种方式处理。库存锁定失败会影响主交易正确性,所以必须失败返回;通知失败只影响用户触达,可以通过 outbox、消息队列和定时任务补发。

一个完整的订单链路取舍

可以把下单流程拆成几个步骤:

  1. 创建订单草稿。
  2. 校验客户、价格、合同。
  3. 锁定库存。
  4. 写入订单主表。
  5. 发送订单创建事件。
  6. 生成出库任务。
  7. 通知客户下单成功。

其中第 2、3、4 步属于主交易链路,应该更重视一致性。第 5、6、7 步可以更多使用消息和补偿,保证主链路响应速度和可用性。

如果库存服务暂时不可用,系统可以拒绝创建正式订单,保留草稿并提示稍后重试。如果短信服务不可用,订单仍然可以创建成功,短信后续重试。这就是按业务重要性拆分 CAP 取舍。

监控和补偿不能省略

偏 AP 的系统不能只说“最终一致”,必须设计最终怎么一致。至少需要三类能力:

  1. 幂等:同一个订单事件重复消费时,不能重复生成出库任务或重复扣减库存。
  2. 对账:定时比较订单、库存流水、出库任务之间的状态差异。
  3. 告警:补偿失败次数、消息堆积时间、库存差异数量超过阈值时要通知研发和业务。

例如订单创建事件已经发送,但出库任务生成失败,系统应该能通过对账任务发现“已支付订单没有出库任务”,并自动补偿或进入人工处理队列。没有这些能力,AP 设计就会从“短暂不一致”变成“永久脏数据”。

总结

CAP 理论的价值不是让我们背诵一致性、可用性、分区容错性,而是提醒我们:分布式系统一定会遇到网络异常,异常发生时必须提前决定哪些数据不能错,哪些流程不能停。

供应链系统的设计原则可以总结成一句话:核心交易数据宁可慢一点,也不要错;辅助查询和通知可以快一点,再通过最终一致性修正。

真正成熟的 Java 分布式系统,不会简单宣称自己是 CP 或 AP,而是会在不同业务边界上做不同取舍,并且把补偿、对账、幂等和监控一起设计进去。

Spring Cloud核心组件:服务治理与供应链微服务实践

Spring Cloud 不是一个单独的框架,而是一组围绕微服务落地的工程组件。它解决的不是某一个业务功能,而是微服务拆分之后必然出现的一组基础问题:服务在哪里、请求怎么进来、服务之间怎么调用、配置怎么管理、故障怎么隔离、链路怎么追踪。

如果把一个供应链系统拆成订单服务、库存服务、采购服务、仓储服务、结算服务,每个服务都可以独立部署和扩容。拆分之后的好处是边界清晰、扩展灵活,但随之而来的问题是系统从一个进程变成了多个进程的协作。Spring Cloud 常用组件就是为了处理这些协作问题。

Spring Cloud 常用组件协作流程

服务注册与发现:Eureka、Nacos、Consul

服务注册中心负责回答一个最基础的问题:服务在哪里。

在单体系统里,方法调用发生在同一个进程内,不需要知道网络地址。拆成微服务之后,订单服务调用库存服务,需要知道库存服务当前有哪些实例、实例的 IP 和端口是什么、哪些实例健康、哪些实例已经下线。

注册中心的工作方式通常是:

  1. 服务启动时,把自己的服务名、地址、端口、健康状态注册到注册中心。
  2. 服务运行期间,持续向注册中心发送心跳。
  3. 调用方从注册中心拉取可用实例列表。
  4. 注册中心根据心跳和健康检查剔除不可用实例。

Eureka 是 Spring Cloud 早期常用的注册中心,强调 AP 思路,在短时间网络抖动时更倾向于保证服务发现可用。Nacos 同时支持服务发现和配置管理,在国内项目里使用较多。Consul 提供服务发现、健康检查和 KV 存储,也常用于基础设施统一管理。

在供应链系统里,库存服务可能部署 6 个实例。订单服务不应该写死某一个库存服务地址,而是通过服务名 inventory-service 找到健康实例,再把请求分发过去。

配置中心:Spring Cloud Config、Nacos Config

配置中心负责回答另一个问题:配置怎么统一管理。

微服务数量多了以后,如果每个服务都把数据库地址、开关配置、限流阈值、第三方接口地址写在本地配置文件里,修改一次配置就要重新打包和发布多个服务,风险很高。

配置中心的工作原理是把配置从应用包里抽出来,集中存储和下发。服务启动时从配置中心读取配置,运行期间也可以通过刷新机制拿到最新配置。

典型配置包括:

  • 数据库连接信息。
  • Redis、MQ、搜索服务地址。
  • 业务开关,例如是否启用新的库存锁定策略。
  • 限流、超时、熔断阈值。
  • 不同环境的差异配置。

例如采购系统要临时关闭自动补货功能,只需要在配置中心修改 autoReplenish.enabled=false,再让采购服务刷新配置,不需要重新发布服务。

网关:Spring Cloud Gateway、Zuul

网关是微服务系统的统一入口。

如果没有网关,前端要直接知道订单服务、库存服务、报表服务、用户服务的地址,这会让前端和后端部署结构强耦合。网关把外部请求统一接进来,再根据路由规则转发到内部服务。

网关常见职责包括:

  • 路由转发:把 /api/orders/** 转发到订单服务。
  • 鉴权认证:统一校验 token、登录态、权限。
  • 限流降级:对高频接口做访问控制。
  • 日志审计:记录请求来源、耗时、状态码。
  • 协议适配:对外暴露统一 HTTP 接口,对内转发到多个服务。

Spring Cloud Gateway 基于响应式模型,性能和扩展性比早期 Zuul 1 更适合新项目。一个常见做法是把所有供应链后台请求都先打到网关,网关校验用户身份后,再转到订单、库存、采购等服务。

服务调用:OpenFeign、RestTemplate、WebClient

服务调用组件负责让一个服务更方便地调用另一个服务。

OpenFeign 的价值是把远程 HTTP 调用写得像本地接口调用。开发者只需要定义接口、声明服务名和路径,Feign 会根据服务发现结果、负载均衡策略和 HTTP 编码规则发起请求。

例如订单服务创建订单时,需要调用库存服务锁定库存:

1
2
3
4
5
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@PostMapping("/inventory/lock")
LockResult lock(@RequestBody LockInventoryCommand command);
}

它背后的核心流程是:根据服务名找到实例列表,选择一个实例,组装 HTTP 请求,发送请求,解析响应,异常时交给重试、熔断或降级逻辑处理。

RestTemplate 是较早的同步 HTTP 客户端,WebClient 支持响应式调用。对于大多数普通业务服务,OpenFeign 的声明式调用更容易维护。

负载均衡:Ribbon、Spring Cloud LoadBalancer

负载均衡负责决定请求发给哪一个服务实例。

当库存服务有多个实例时,订单服务不能永远调用同一个实例,否则这个实例会成为热点。客户端负载均衡会从注册中心拿到实例列表,然后根据策略选择一个实例。

常见策略包括:

  • 轮询:请求依次分配给不同实例。
  • 随机:随机选择一个实例。
  • 权重:性能更强或权重更高的实例承担更多请求。
  • 最少连接或响应时间优先:尽量选择当前压力较小的实例。

Ribbon 是早期常见组件,新版本 Spring Cloud 更推荐 Spring Cloud LoadBalancer。它们的核心目标一致:让调用方不用关心某个服务具体部署了多少实例。

熔断与限流:Hystrix、Resilience4j、Sentinel

熔断和限流负责防止局部故障拖垮整个系统。

在分布式系统里,一个服务慢并不可怕,可怕的是调用它的服务都被拖慢。比如库存服务数据库异常,订单服务大量线程卡在库存锁定接口上,随后订单服务也无法响应,最终网关、前端和其他服务都受到影响。

熔断器的工作原理类似电路保护:

  1. 统计一段时间内的失败率、慢调用比例、异常数量。
  2. 当指标超过阈值时,打开熔断器。
  3. 熔断打开后,请求不再真实调用下游,而是直接失败或走降级结果。
  4. 过一段时间进入半开状态,放少量请求试探下游是否恢复。
  5. 如果试探成功,关闭熔断;如果失败,继续熔断。

Hystrix 曾经是 Spring Cloud 里最常见的熔断组件,但已经进入维护状态。新项目更常见的是 Resilience4j 或 Sentinel。Sentinel 在限流、热点参数控制、系统自适应保护方面更完整。

消息总线和消息驱动:Spring Cloud Bus、Stream

微服务之间不一定都要同步调用。对于不要求立即返回结果的业务,异步消息更适合。

Spring Cloud Stream 把消息中间件抽象成绑定模型,让业务代码不直接依赖 Kafka、RabbitMQ 等具体实现。Spring Cloud Bus 常用于配置刷新、服务间事件通知。

供应链系统里的典型场景是订单创建成功后发送 OrderCreatedEvent。库存服务订阅事件后扣减可售库存,仓储服务订阅事件后准备出库任务,积分服务订阅事件后累计客户积分。这样订单服务不需要同步等待所有下游服务完成。

异步消息的关键不是简单地把请求丢到 MQ,而是要处理幂等、重复消费、消息顺序、失败重试和最终一致性。

链路追踪:Sleuth、Zipkin、Micrometer Tracing

链路追踪负责回答线上排查时最痛苦的问题:一次请求到底经过了哪些服务,慢在哪里。

当用户点击创建采购单,可能经过网关、采购服务、供应商服务、库存服务、审批服务、消息队列。没有链路追踪时,只能分别查多个服务日志,很难把它们串起来。

链路追踪的工作方式是给每次请求生成 traceId,并在服务之间透传。每个服务再生成自己的 spanId,记录当前服务处理了什么、耗时多少、是否异常。Zipkin 或其他追踪平台负责收集和展示完整调用链。

Spring Cloud Sleuth 曾经负责自动注入 traceId 和 spanId。新版本生态里,Micrometer Tracing 是更主流的观测方案。

监控与治理:Actuator、Admin、Prometheus

服务能跑起来只是第一步,更重要的是知道它跑得怎么样。

Spring Boot Actuator 提供健康检查、指标、线程、环境变量等端点。Spring Boot Admin 可以集中查看服务状态。Prometheus 和 Grafana 常用于采集指标和展示监控面板。

供应链系统至少应该关注:

  • 接口成功率和 P95、P99 响应时间。
  • 数据库连接池使用率。
  • MQ 积压数量。
  • 服务实例健康状态。
  • 线程池队列长度。
  • 熔断、限流、降级次数。

总结

Spring Cloud 常用组件本质上是在处理微服务运行期治理问题。注册中心解决服务在哪里,配置中心解决配置如何统一,网关解决入口治理,Feign 解决服务调用,负载均衡解决实例选择,熔断限流解决故障隔离,消息组件解决异步解耦,链路追踪和监控解决可观测性。

真正落地时不要为了组件而组件。系统规模小的时候,可以先从注册中心、网关、配置中心、Feign、基础监控开始。等调用链复杂、流量变大、故障影响扩大时,再逐步引入熔断限流、链路追踪、消息驱动和更细的治理能力。

Redis分布式锁:正确实现、续期与故障边界

分布式锁解决什么问题

Redis 锁是很多 Java 项目都会遇到的话题。比如供应链系统里,多个服务同时扣减库存;ERP 里同一张单据不能被重复审核;定时任务在多台机器上部署,但同一批数据只能被一个节点处理。这些场景都需要某种“互斥”。

但 Redis 锁也是容易写错的地方。很多人第一版会写成 setnx lock 1,看起来能用,线上一遇到超时、宕机、重试、主从切换,就会暴露问题。分布式锁不是“抢到 key 就完事”,它要考虑 owner、过期时间、释放原子性、业务幂等和异常恢复。

这篇笔记按常见 Redis 锁类型梳理:简单 SETNX 锁、带过期时间的锁、带唯一标识和 Lua 释放的锁、可重入锁、Redisson 看门狗锁、RedLock 思路。重点还是落到项目里怎么避免锁丢失和死锁。

Redis分布式锁安全流程

正确性依赖哪些条件

一个相对安全的 Redis 锁,至少要满足五个条件。

第一,加锁要原子。不能先 SETNX 再单独 EXPIRE,因为中间如果进程宕机,锁可能永不过期。

第二,锁要有唯一 owner。释放锁时必须确认锁是自己加的,不能误删别人的锁。

第三,释放要原子。检查 owner 和删除 key 要放在 Lua 脚本里执行,避免检查后锁过期、别人重新加锁、自己又误删。

第四,业务要幂等。分布式环境里很难只靠锁保证绝对一次执行,业务本身也要能处理重复请求。

第五,下游要能拒绝陈旧持有者。客户端发生长时间 GC、网络暂停或进程挂起时,锁可能已经过期并被其他客户端获得,原持有者恢复后却仍继续写数据。对正确性要求高的资源,应使用单调递增的 fencing token,让数据库或下游服务拒绝较旧 token 的写入。

实现方式与选择建议

最朴素的锁是 SETNX

1
SETNX order:lock:1001 1

它的优点是简单,缺点也明显:如果没有过期时间,持锁进程挂了就可能死锁。

稍微好一点的写法是原子设置锁和值和过期时间:

1
SET order:lock:1001 requestId NX PX 30000

这里 NX 表示 key 不存在才设置,PX 30000 表示 30 秒过期,requestId 表示锁的持有者。这个写法解决了加锁和设置过期时间的原子性问题。

释放锁时不要直接:

1
DEL order:lock:1001

因为你的业务可能执行超过 30 秒,锁已经过期,另一个线程重新拿到锁。如果你这时直接删除,就会删掉别人的锁。

更安全的释放方式是 Lua:

1
2
3
4
5
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

这段脚本把“判断 owner”和“删除 key”放到 Redis 单线程执行里,避免中间被打断。

如果业务执行时间不确定,可以使用带看门狗机制的锁。Redisson 的思路是:加锁成功后,如果业务线程还活着,就定期延长锁过期时间;业务结束后主动释放。这样可以避免业务正常运行但锁提前过期。

不过看门狗也不是万能的。如果任务本身可能跑几分钟甚至几十分钟,我更倾向于拆小任务,或者使用任务状态表控制流程,而不是让一个 Redis 锁长时间占着。

可重入锁适合一个线程在同一调用链里重复进入同一把锁的场景。它一般会记录线程标识和重入次数。优点是符合 Java ReentrantLock 的使用习惯,缺点是实现复杂,依赖客户端框架管理。

RedLock 是一种尝试在多个 Redis 节点上获取多数锁的算法。它的目标是减少单点 Redis 故障对锁安全性的影响。但它实现和时钟假设更复杂,业务上是否需要要谨慎评估。多数普通后台系统,用单 Redis 主节点加合理业务幂等,或者直接用数据库唯一约束/状态机,反而更容易解释和维护。

需要强调的是,自动续期只能降低锁在正常执行期间提前过期的概率,不能证明持锁者始终拥有资源。只要业务跨越 Redis 和数据库两个系统,就应把数据库条件更新、唯一约束或 fencing token 作为最终写入保护。

供应链防重复处理案例

以供应链库存预占为例。用户下单后,系统要对 SKU 做库存预占,不能让两个请求同时把同一批库存扣成负数。

可以按 SKU 加锁:

1
stock:lock:sku1001

流程是:

  1. 请求生成唯一 requestId
  2. 使用 SET key requestId NX PX 30000 加锁。
  3. 查询库存可用量。
  4. 写入库存流水和订单预占记录。
  5. 更新库存。
  6. 提交数据库事务。
  7. 用 Lua 判断 owner 后释放锁。

这里还要做一层幂等:订单号和 SKU 的预占记录要有唯一约束。如果请求重试,即使再次进入,也不会重复预占。

再比如 ERP 单据审核,同一张采购单不能同时被两个人审核。可以用 po:audit:lock:{poNo} 做锁,但最终仍然要在数据库里校验单据状态:只有 待审核 才能流转到 已审核。Redis 锁减少并发冲突,数据库状态机保证最终正确。

失效、续期与主从切换风险

第一个坑,是没有过期时间。没有 TTL 的锁,遇到进程宕机就可能永远释放不了。

第二个坑,是锁过期时间太短。业务还没执行完,锁先没了,其他线程进来就会并发执行。过期时间要结合业务耗时和最大抖动设置。

第三个坑,是直接删除锁。释放前不判断 owner,很容易误删别人的锁。

第四个坑,是只依赖锁不做幂等。网络重试、消费者重复消费、接口超时重放都可能导致同一业务重复进入。锁只是降低并发概率,幂等才是最后防线。

第五个坑,是大锁。比如用一个 global:lock 锁住所有库存操作,会严重影响吞吐量。锁粒度要尽量贴近业务资源,比如按 SKU、订单号、仓库维度拆分。

总结

Redis 锁常见形态从简单 SETNX 到带 TTL、带 owner、Lua 释放、可重入锁和 Redisson 看门狗锁。项目里最推荐的底线写法是:SET key requestId NX PX ttl 加锁,Lua 判断 owner 后释放,业务做好幂等和状态校验。

锁不是越复杂越安全。更可靠的方案通常包括短锁、合理 TTL、原子释放、业务幂等、陈旧请求隔离和监控告警。对于库存、结算等高风险写入,Redis 锁只负责降低竞争,最终正确性仍应由数据库约束或可验证的状态机保证。

Redis高可用与集群模式:主从、Sentinel和Cluster

为什么要区分高可用与分片

Redis 一开始通常是从单机用起的:缓存商品详情、存登录 token、做计数器、做分布式锁。等系统访问量上来以后,问题就来了:单机内存不够怎么办?主节点挂了怎么办?读压力太高怎么办?业务需要更高可用时怎么做?

这时就要考虑 Redis 的集群模式。这里的“集群”不只是 Redis Cluster 一种,而是一组部署方式:主从复制、哨兵模式、Redis Cluster、代理分片,以及云托管 Redis。每种方式解决的问题不一样,优缺点也不一样。

项目选型时不要先问“哪种最先进”,而要先问:我的数据量有多大?QPS 有多高?能不能接受短暂不可用?团队有没有 Redis 运维经验?业务是否依赖跨 key 操作?

Redis集群模式选型流程

四种部署形态及其能力边界

Redis 常见部署方式可以按能力逐层理解。

单机模式最简单,适合开发环境、小系统、低风险缓存。但单点故障明显,内存和吞吐也受单机限制。

主从复制解决的是读扩展和数据备份。主节点负责写,从节点复制数据,可以承担部分读请求。但主节点挂了以后,是否自动切换取决于外部机制。

哨兵模式解决的是高可用。Sentinel 会监控主从节点,主节点异常时自动选举新的主节点,并通知客户端切换。它适合数据量还没有大到必须分片,但又需要自动故障转移的系统。

Redis Cluster 解决的是水平扩展。它把 key 映射到 16384 个 hash slot,再把 slot 分布到多个主节点上。容量和吞吐可以通过增加节点扩展,但客户端、跨 slot 操作和运维复杂度都会提高。

代理分片和云托管 Redis 解决的是工程复杂度。代理可以屏蔽后端分片,云托管可以减少运维成本。但要关注命令兼容性、网络延迟、成本和故障透明度。

选型方法与迁移路径

如果是一个普通后台系统,Redis 只用来缓存字典、商品详情、用户权限,数据量不大,故障后可以短暂回源数据库,那么主从或哨兵模式通常就够了。

例如 ERP 系统里,商品基础资料、供应商列表、组织架构权限可以放 Redis 缓存。即使 Redis 短暂不可用,也可以降级查数据库,只是慢一点。这种场景不一定要上 Redis Cluster。

如果系统对可用性要求高,但数据量还在单机内存范围内,可以选择 Sentinel。比如登录 token、会话信息、限流计数这类数据,业务希望节点故障后能自动切换,哨兵模式就比较合适。

如果数据量和访问量都明显增长,比如电商商品缓存、库存热点、活动库存、推荐结果缓存,单机内存和 QPS 已经吃紧,就要考虑 Redis Cluster。Cluster 的好处是容量可以横向扩展,坏处是开发要注意 hash slot。

比如下面这种多 key 操作:

1
MGET product:1001 product:1002 product:1003

在 Cluster 下,如果这些 key 落在不同 slot,可能无法像单机那样直接执行。可以通过 hash tag 把相关 key 放到同一个 slot:

1
2
stock:{sku1001}:available
stock:{sku1001}:locked

但 hash tag 也不能乱用。如果所有 key 都强行放到一个 slot,分片就失去意义。

如果团队 Redis 运维能力弱,又不想自己处理扩容、备份、故障切换,可以考虑云托管 Redis。云服务能减少大量运维工作,但成本、规格限制、网络链路、版本兼容要提前评估。

我做选型时一般按这个顺序问:

  1. Redis 只是缓存,还是承载关键状态?
  2. 单机内存能不能装下未来一年数据?
  3. 读写 QPS 是否需要水平扩展?
  4. 是否大量使用 Lua、多 key、事务、pipeline?
  5. 团队是否有能力处理故障切换和扩容?
  6. 可接受的成本和可用性目标是什么?

运维风险与一致性限制

第一个坑,是一上来就上 Cluster。Cluster 能扩容,但复杂度也高。小系统如果只是缓存几万条基础资料,用 Cluster 反而会增加开发和运维负担。

第二个坑,是把主从当高可用。主从复制不等于自动故障切换。没有哨兵或外部切换机制,主节点挂了仍然需要人工处理。

第三个坑,是忽略数据一致性。Redis 复制通常是异步的,主节点刚写入还没同步给从节点时发生故障,可能会丢一小段数据。关键业务不能把 Redis 当唯一事实来源。

第四个坑,是跨 slot 操作太多。Cluster 选型前要盘点代码里是否大量使用 MGETMSET、Lua、事务和批量删除。否则上线后会发现很多命令不再好用。

第五个坑,是只看 QPS 不看热点 key。即使是 Cluster,如果大量请求集中在一个超级热点 key 上,也可能打爆单个节点。热点数据要考虑本地缓存、拆 key、限流或预计算。

总结

Redis 集群模式没有绝对最好,只有适合当前阶段。数据量小、风险低,用单机或主从;需要自动故障切换,用 Sentinel;容量和吞吐需要横向扩展,用 Redis Cluster;团队想减少运维,可以考虑云托管。

选型时最重要的是把 Redis 在系统里的角色想清楚。它是缓存、锁、队列、会话,还是关键状态存储?角色不同,容灾、持久化、扩容和一致性要求也完全不同。