后台列表查询分层与分页治理
列表变慢先别急着加索引后台列表变慢,先看查询模型,不要先加索引。
这类列表一开始通常都很简单:
1、按条件查表
2、分页返回
3、前端展示几列字段
但做着做着就会长出很多额外需求:
1、多条件筛选
2、多种排序
3、列表里直接带关联信息
4、导出和列表共用一套查询
5、详情页也复用同一个查询对象
问题往往不是单条 SQL 慢,而是一个查询承担了太多职责。
先看典型 SQL这类列表 SQL 往往长这样:
12345select *from t_partnerwhere status = ? and create_time > ? and update_time < ?order by update_time desclimit ${offset}, ${rows}
请求量上来以后,问题基本集中在三块:
1、orderByClause 动态开放过大
2、深分页 limit offset 越翻越慢
3、查询字段越来越多
查询是怎么一步步变重的下面这个例子只说明问题,不代表固定耗时。
初始版本:
1234select * fr ...
发奖接口防重与失败处理
发奖接口先处理重复请求发奖接口先看重复发奖。
这类接口一般有几个共性:
1、上游会重试
2、用户会重复点击
3、下游结果可能返回慢
4、同一笔业务可能从多个入口打进来
核心不是怎么发,而是怎么保证只发一次。
入口顺序不要写反入口顺序直接定死:
1、先做签名或合法性校验
2、再看这笔业务是否已经成功处理过
3、再做并发互斥
4、最后才真正调发奖能力
123456789101112131415checkSendPrizeSign(req);req.setUnionId(groupService.getGroupUnionId(req));String sentKey = RedisKey.getQuestionSendPrizeIsHaveKey(req.getPrizeId(), req.getBizNo());if (StringUtils.isNotBlank(redisUtil.get(sentKey))) { return alreadySent();}String lockKey = RedisKey.getLockKey(req.getPr ...
链路追踪、耗时拆分与无侵入监控实践
日志先要能回答问题很多线上慢请求不是没有日志,而是日志只能告诉你:
1、这次失败了
2、总耗时很高
3、线程池里有异常
但当你继续追问“慢在哪一段”“是不是同一条请求”“异步线程里到底发生了什么”时,日志就答不上来了。
所以可观测性的第一步不是上多复杂的平台,而是先让日志结构能回答问题。
traceId 先解决串链如果一次请求里会经过主线程、异步线程、下游调用,没有统一的 trace 标识,最后看到的只是一堆散日志。
比较实用的做法还是在入口统一生成或透传 traceId,异步线程里显式放回 MDC:
123456789String traceId = MDC.get("traceId");executor.execute(() -> { MDC.put("traceId", traceId); try { handleTask(); } finally { MDC.clear(); }});
这里最容易漏的是 finally 里的清理。不清理时 ...
异步任务与线程池治理实践
线程池问题最容易伪装成“偶发超时”很多系统里的异步能力不是一次设计出来的,而是慢慢叠上去的:
1、先有一个公共线程池
2、后面补批量任务
3、再加补偿、消息处理、外部调用
4、最后所有异步任务都往里面塞
这时线上最常见的表现不是线程池报错,而是接口慢、任务堆、偶发超时、日志里还看不出来是谁把池子打满了。
真正该先拆的,是任务模型,不是线程数我现在更倾向按职责拆池,而不是按“项目里有异步”拆池。
比较实用的拆法通常是:
1、规则计算线程池
2、批量查询线程池
3、补偿任务线程池
4、消息消费后处理线程池
5、外部调用线程池
因为这几类任务的资源模型完全不同:
1、有的偏 CPU
2、有的偏 IO
3、有的容忍堆积
4、有的必须及时完成
如果混在一个池子里,出了问题基本只能看到“这个池满了”,但看不到是谁干的。
一套能进生产的线程池配置,至少要把边界定清楚1234567891011@Bean("asyncExecutor")public ThreadPoolTaskExecutor asyncExecutor() { ThreadPoolT ...
配置开关、灰度放量与快速回滚
线上开关真正解决的,不是配置问题,而是变更风险问题很多能力到了线上以后,真正的上线方式已经不是“发版打开”,而是:
1、代码先上线
2、默认关闭或小范围开启
3、通过配置逐步放量
4、出问题时快速回滚
所以开关如果只被写成一个简单 if,通常还是不够。
我更习惯先把开关按用途分层如果所有开关都混成一堆布尔值,后面非常难维护。
我更常见的分法是:
1、保护性开关:出问题时立即止血,比如关闭某个消费者入口
2、策略开关:控制某段业务逻辑是否生效,比如签名校验、代理缓存、规则代理
3、参数配置:灰度比例、超时、缓存时间、批量大小
4、白名单配置:只对部分活动、渠道、用户放量
只有先把用途分开,后面灰度和回滚才好做。
保护性开关最重要的是默认值和关闭态一个保护性开关如果没有默认值,环境切换时就容易出问题。
我更喜欢这种写法:
1234if (omsOrderConfig.isConsumerClose()) { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); retu ...
活动系统中的幂等补偿与最终一致性
这类链路真正难的不是失败,而是失败后状态不清楚活动系统一旦走到异步履约,链路通常就不会再是单服务闭环。
比较典型的一条链路会拆成:
1、入口受理请求
2、写用户事件或业务记录
3、资格判断或规则执行
4、调用下游履约能力
5、失败补偿或重放
这类链路最怕的不是某一步失败,而是失败后回答不了三个问题:
1、到底做没做
2、做了一半还是全做完了
3、再来一次会不会重复执行
这就是为什么幂等、补偿、最终一致性必须一起设计。
幂等要先落在入口,而不是补偿任务里补救幂等最先解决的是重复入口问题:
1、上游重试
2、消息重复消费
3、人工补发
4、超时后再次请求
最简单的实现还是稳定幂等键 + 唯一约束:
12INSERT INTO t_idempotent_record (idempotent_id, biz_id, create_time)VALUES (#{idempotentId}, #{bizId}, now());
如果插入成功,说明当前请求第一次进入;如果插入失败,就按已处理或重复请求处理。
幂等键到底该怎么选我自己的标准很简单 ...
MySQL 索引与执行计划
B+ 树与索引结构为什么用 B+ 树索引为什么最后落到 B+ 树,不是为了概念好看,而是它更贴近磁盘页和范围查询的实际需求:
二叉树:深度太深,每次查询需要多次磁盘IO
哈希表:范围查询无法用(只能精确查询),不支持排序
B+树:一个节点可以存多个key,减少树深度;所有data都在叶子节点,范围查询和排序都高效
常见业务表在索引设计还算正常的前提下,树高一般不会太夸张,所以查找成本通常比很多人想得低。这里更该关注的是回表、扫描范围和是否命中合适索引。
叶子节点有序结构B+ 树的叶子节点天然有序,所以范围查询和按索引字段排序时更容易顺着扫下去:
1[10-20-30-40-50-60-70-80-90] <- 叶子节点按大小排序
执行SELECT * FROM user WHERE age BETWEEN 20 AND 60时:
定位到20这个节点
沿着链表顺序扫到60
无需回到根节点再查其他范围
索引设计联合索引顺序同一张表的多个字段建联合索引时,顺序很关键:
12345-- 方案A:(status, create_time)SELECT * FRO ...
消息消费积压、手动ACK与补偿重放
真正把消费链路拖死的,往往不是吞吐不够,而是失败模型不清楚线上看消息积压,我现在第一反应通常不是“多加几个消费者”,而是先问:
1、是不是固定 offset 反复失败
2、ack 时机是不是和业务成功没对齐
3、失败消息有没有被正确旁路
4、补偿重放是不是又把同样的问题打回来
如果这四件事没理清,直接加线程数通常只是把问题放大。
手动 ACK 的意义,是把位点提交和业务成功对齐批量消费里我更常见的写法是手动 ack,而不是自动提交:
12345678910111213@KafkaListener(topics = "xxx", containerFactory = "batchFactory")public void receive(List<ConsumerRecord<String, String>> msgArray, Acknowledgment ack) { for (int i = 0; i < msgArray.size(); i++) { ConsumerRe ...
分布式系统设计基础
CAP 与一致性CAP 的取舍分布式系统最多只能同时保证三者中的两个:
**一致性(C)**:所有节点在同一时刻看到相同数据
**可用性(A)**:系统对请求的响应不会失败
**分区容错性(P)**:节点间网络分区时系统继续运行
实际里 P 基本没法回避,所以讨论更多是在分区出现时,系统更偏向保一致还是保可用:
CP系统:优先数据一致,网络分区时拒绝服务(ZK、etcd)
AP系统:优先可用,允许临时不一致,最终一致(Dynamo、Cassandra)
大多数互联网业务不会把所有链路都做成强一致,常见做法是核心扣减、扣款类链路偏保守,外围状态同步和通知链路接受短暂不一致。
BASE 与最终一致性BASE是对CAP的实用补充:
**基本可用(BA)**:服务基本可用,可能存在性能下降
**软状态(S)**:允许存在中间状态,不要求强一致
**最终一致性(E)**:经过一段时间后,所有副本达到一致状态
落到工程实现里,通常就是主流程先落库,后续再通过消息、补偿任务和对账任务把状态收平。
分布式锁Redis 锁常见写法是用 SET NX EX 把加锁动作收成一步: ...
Kafka学习与使用
先把几个核心概念学明白我自己真正把 Kafka 用顺,是先把下面四个概念和业务含义对上:
1、topic:消息类别,不等于业务模块
2、partition:并行度和顺序边界
3、groupId:消费位点隔离
4、offset:消费进度,不代表业务成功
这几个概念如果只停留在“知道名字”,后面一遇到重复消费、积压、重试就很容易乱。
生产端真正要配的不是 KafkaTemplate,而是发送语义最小可用的生产端不复杂,但我更关心的是:
1、key 怎么定
2、失败能不能观察到
3、是否需要重试或落失败记录
123456789101112131415@Beanpublic KafkaTemplate<String, String> kafkaTemplate() { KafkaTemplate<String, String> kafkaTemplate = new KafkaTemplate<>(producerFactory()); kafkaTemplate.setProducerListener(new Kafka ...



