活动系统中的幂等补偿与最终一致性
这类链路真正难的不是失败,而是失败后状态不清楚活动系统一旦走到异步履约,链路通常就不会再是单服务闭环。
比较典型的一条链路会拆成:
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 ...
接口幂等常用实践
先把问题说清幂等这个词,很多系统都在说,但真正落到代码里,经常只剩一句“加个唯一索引就好了”。
唯一索引当然有用,但它解决的是一部分问题,不是全部问题。
业务里更常见的场景是下面这几类:
1、前端重复点击,接口被重复提交
2、网关超时重试,请求被打了多次
3、消息重复投递,消费逻辑重复执行
4、补偿任务重跑,同一笔数据被再次处理
这篇只讲 Java 服务里常见的幂等落地方式,不展开讲概念定义。
先分清防重和幂等很多接口把这两个概念混在一起,最后实现出来就会很别扭。
防重复提交,重点是短时间内不要让同一个动作进来两次。
业务幂等,重点是同一笔业务就算来两次,最终结果也只能落一次。
例如创建订单:
1234@PostMapping("/order/create")public OrderCreateResponse create(@RequestBody OrderCreateRequest request) { return orderAppService.create(request);}
如果只是前端双击,你可以先用 to ...
消息消费积压、手动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 ...
Drools规则引擎学习
为什么要学 Drools规则一旦开始长到这个程度,普通 if/else 就会越来越难收:
1、条件组合越来越多
2、规则优先级开始互相影响
3、产品总想临时改条件、加条件、下线条件
4、线上出了问题,很难说清楚到底是哪条规则命中了
所以 Drools 真正解决的,不是“把判断写到配置里”,而是把规则判断从业务主流程里拆出来。
如果只是两三个固定分支,没必要上规则引擎;但一旦规则开始高频变更、组合变多,Drools 确实比长串分支更容易维护。
先认识几个核心概念Drools 真正常用的概念其实不多,先把下面几个抓住就够用了:
1、Fact:送进规则引擎的一份事实数据
2、Rule:一条规则,包含条件和动作
3、KieBase:规则定义集合
4、KieSession:一次可执行的规则会话
5、AgendaGroup:规则分组,用来控制当前执行哪一组
6、Salience:规则优先级,数字越大越先执行
如果把它翻成更接近业务代码的话,其实就是:
1、先准备一份干净的输入
2、再挑一组规则执行
3、最后把命中结果收回来
执行时到底怎么跑Drools 执行时,大致就是这条 ...
一次线上慢SQL与接口超时问题排查
问题现象这是一次典型的后台列表接口超时。
接口除了查主表,还会补下游状态和展示字段。高峰期一上来就开始抖:
1、列表接口平时大约 200ms ~ 300ms
2、高峰期偶发升到 3s ~ 5s
3、数据库连接占用上升,但应用 CPU 并不高
4、网关有超时报警,应用日志里经常看到分页查询耗时偏长
这类问题不能只盯一层。表面是接口超时,实际往往是分页 SQL、事务边界和补充查询叠在一起。
第一轮排查先看应用日志先补分段耗时,结果很快就出来了:主要时间都耗在列表查询。
123long start = System.currentTimeMillis();List<ActivityEntry> entryList = activityEntryMapper.selectByExample(req.toExample());log.info("queryPage cost={}ms", System.currentTimeMillis() - start);
到这一步已经能先排掉 JSON 序列化和返回组装,问题在数据库查询。
...
CompletableFuture常用实践
适用场景CompletableFuture 在业务代码里最常见的用法,主要是下面这几类:
1、一批相互独立的任务并发执行,最后统一收口
2、先按任务类型分类,再分别并发处理
3、批量过滤、批量校验、批量补充信息
4、异步线程里补 TraceId、兜底超时和异常
只收常用实践,不展开 API 说明。
常见实践一:一批任务并发执行,最后统一收口核心写法就是:先把每个请求包装成 Future,再统一 allOf 等待。
1234567891011List<CompletableFuture<TaskResult<R>>> futures = new ArrayList<>();for (Q request : requestList) { CompletableFuture<TaskResult<R>> future = new CompletableFuture<>(); CompletableFuture.supplyAsync(() -> query(reque ...
后台列表查询分层与分页治理
列表变慢先别急着加索引后台列表变慢,先看查询模型,不要先加索引。
这类列表一开始通常都很简单:
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 ...



