利用AI与设计原则,对复杂性业务抽象的一次实战
关于AI的一些想法
AI时代之下,工具类问题或者唯一解问题将不再成为程序员发展的桎梏。正在的技术核心优势在于:对复杂业务场景的建模能力、风险预判能力、价值权衡、领域驱动等方面。如何权衡选择较优解,成功关键。
共生效应的讨论
基于GitHub的Copilot生产力报告、麦肯锡2023年开发者效率调研,验证了以下的场景:纯AI驱动模式容易形成高风险技术债务(上下文缺失,模式复制陷阱),纯人工模式存在认知负荷和效率瓶颈,帕累托最优场景下是人机互相协作。
这里引用Linux之父Linus Torvalds所言:“好的程序员知道写什么代码,伟大的程序员知道不写什么代码。”
这种共生模式本质上重构了软件工程的价值链——AI承担「加速器」,人类专注「方向盘」和「刹车系统」
业务场景(裂变)
业务上有多个来源单据,分为基础类档案,计算公式参数类档案,成本调整单。然后当这些档案发生变化的时候,要去找到他所影响的业务单据(成本制定单+成本调整单),再对业务单据进行重算(可以理解为执行相关业务规则)。然后数据会进行冲洗到价格表,价格发生变化之后,再次触达基础档案的变化,再次去寻找影响面,依次循环。
而我们将这个业务过程称为裂变。(因为他很像原子核裂变,我就用这个名字代替业务场景了)
业务建模
从业务场景上,可以提炼以下业务点:
- 业务触达源抽象(不同的业务表单)
- 初始影响因子获取(业务表单变更的值)
- 影响面获取(变化量引起的BOM最短链路获取)
- 获取实时影响业务对象(业务对象可能匹配多个单据,需要匹配最新活跃数据)
- 特殊化业务单据赋值(子类特定化实现,基于SDP思想)
- 调用业务重算(复用思想,此步骤会冲洗价格表,导致二次裂变)
- 业务数据记录(归档、回溯)
- 业务任务分流处理(考虑服务器压力)
- 日志任务消息触达(业务人员触达)
这个过程我认为就是一个线下业务场景的抽象能力。这也是在前面提到的在AI时代,自己的技术护城河能力。
当我入职第一家公司的时候,有一次的技术分享会,一位大佬两小时,全程画了两张图。详细解释了一下国内外标准供应链的业务线下模式,以及现有系统的单据流如何承载这些业务场景。当时惊为天人,后面才知道,这叫领域专家。
而作为一名开发,或者一名立志做架构的开发,我们需要做的是如何在领域专家的建议下,划分模块边界,制定业务规则,明确方案设计,统筹兼顾后续的扩展点,性能点。而这也是DDD领域驱动模型设计的雏形。
DDD简单规则
领域专家–>>DDD专家: 提供业务规则和流程
DDD专家–>>架构师: 交付领域模型和上下文映射
架构师–>>开发团队: 发布架构规范和接口
开发团队–>>领域专家: 验证实现是否符合业务意图
一张草图
这是当时画的草图,实际在做的过程中,业务又加了两三个版本的需求吧,但是大体骨架是不变的。
每次画流程图,我都拉着需求讨论,但是她好像有点痛苦。emmmm
业务关键技术点
事件驱动模型
关于为什么选择事件发布模型。
首先我整个项目服务是微服务的,而整个裂变过程是横跨多个业务模块的,我希望组件之间不再耦合。
其次,很明显整个业务过程中,我是链式反应的。一张单子作为触达源,引起N多张其他单据的变化,进而又引起其他基础档案的变化。
然后整体的业务功能,我这边要求的是异步,不要影响我的主流程的RT。
最终,不排除我有炫技的可能性。当然最后我没有选择Spring的事件发布,我自己手写了。
事件总线
这里我设计了一个事件总线(BUS),用于关于所有的事件类型与事件监听器。
大致代码如下:
@Component
public class BusinessEventPublisher implements IBusinessEventPublisher {
private final Map<Class<?>, IBizActuationListener> listeners = new HashMap<>();
}
这里主要是声明了一个listeners的容器对象,用于存储触达的业务源对象和影响触达的事件监听器
当然这里注意一下:listeners并不需要ConcurrentHashMap,并不是所有的公共访问对象都需要考虑并发,很明显他是Bean加载的时候塞的
动态注解&注册反转
在最初一版的时候,我遵循发布订阅原则,在业务代码发布者的时候进行了事件的注册。大概代码如下:
public class PlannedPriceAfterConvertInfoServiceImpl extends ServiceImpl<PlannedPriceAfterConvertInfoMapper, PlannedPriceAfterConvertInfo> implements IPlannedPriceAfterConvertInfoService, InitializingBean {
@Autowired
private IBusinessEventPublisher publisher;
@Override
public void afterPropertiesSet() throws Exception {
PriceConfirmListener priceConfirmListener = SpringUtils.getBean(PriceConfirmListener.class);
publisher.register(priceConfirmListener, PriceEvent.class);
}
}
然后我遇到了一个业务需求的拓展
对于当天定时类执行的任务,我的业务代码触达源是所有的Listener监听
那么这就要求我在业务代码内进行多个监听器的注册
大概代码长成这样
@Override
public void afterPropertiesSet() {
CoefficientDensityListener coefficientDensityListener = SpringUtils.getBean(CoefficientDensityListener.class);
EnamelledWireWasteListener enamelledWireWasteListener = SpringUtils.getBean(EnamelledWireWasteListener.class);
MachiningAllowanceListener machiningAllowanceListener = SpringUtils.getBean(MachiningAllowanceListener.class);
RawMaterialListener rawMaterialListener = SpringUtils.getBean(RawMaterialListener.class);
ThreadCoefficientListener threadCoefficientListener = SpringUtils.getBean(ThreadCoefficientListener.class);
WorkingHourConfirmListener workingHourConfirmListener = SpringUtils.getBean(WorkingHourConfirmListener.class);
PriceConfirmListener priceConfirmListener = SpringUtils.getBean(PriceConfirmListener.class);
RecycleMaterialListener recycleMaterialListener = SpringUtils.getBean(RecycleMaterialListener.class);
publisher.register(coefficientDensityListener, CoefficientDensityEvent.class);
publisher.register(enamelledWireWasteListener, EnamelledWireWasteEvent.class);
publisher.register(machiningAllowanceListener, MachiningAllowanceEvent.class);
publisher.register(rawMaterialListener, RawMaterialEvent.class);
publisher.register(threadCoefficientListener, ThreadCoefficientEvent.class);
publisher.register(workingHourConfirmListener, WorkingHourEvent.class);
publisher.register(priceConfirmListener, PriceEvent.class);
publisher.register(recycleMaterialListener, RecycleMaterialEvent.class);
}
在这种情况下,不是说程序不能跑,程序是可以跑的,但是我觉得不够优雅。
大量的注册事件埋点在各个业务端,我觉得不合适。于是我整了事件总线,同时定义了自己的注解。
/**
* @author shengjie.tang
* @version 1.0.0
* @description: 事件触达
* @date 2025/4/1 15:59
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBusinessListener {
Class<?> eventType();
}
@PostConstruct
public void init() {
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(EventBusinessListener.class);
for (Object bean : beansWithAnnotation.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
EventBusinessListener annotation = targetClass.getAnnotation(EventBusinessListener.class);
if (annotation != null) {
Class<?> eventType = annotation.eventType();
listeners.put(eventType, (IBizActuationListener) bean);
}
}
}
这里需要注意,我拿的是Target代理对象,那么为什么会拿Target对象呢?因为CGLib会生成一个 子类(如 YourService$$EnhancerByCGLIB$$xxxx
),并重写方法。而默认情况下,子类不会继承父类的注解
/**
* @author shengjie.tang
* @version 1.0.0
* @description: 调整确认监听
* @date 2025/2/25 14:41
*/
@Component
@EventBusinessListener(eventType = AdjustConfirmListener.class)
public class AdjustConfirmListener extends AdjustBusinessTouchHandle {
@Override
public void doOperation(BaseEvent event) {
super.doAction(event);
}
@Override
protected String getOriginBaseCode(BaseEvent event) {
AdjustEvent adjustEvent = (AdjustEvent) event;
return adjustEvent.getBillCode();
}
@Override
protected String getOriginBaseName(BaseEvent event) {
AdjustEvent adjustEvent = (AdjustEvent) event;
return adjustEvent.getBillName();
}
}
对了业务代码就像这样,是不是侵入性很低。
对象适配&简单工厂方法
在进入我们核心调用链之前,我觉得先要有两个抽象对象,一个是业务事件的抽象触达源,一个是Event的事件的抽象
所以大致的代码如下:
public static BaseEvent buildEvent(ThirdAbstractBusinessDTO businessDTO, BatchUserInfo userInfo) {
SyncFormEnum syncFormEnum = SyncFormEnum.getEnumByClazzName(businessDTO.getClass().getName());
BaseEvent event;
switch (Objects.requireNonNull(syncFormEnum)) {
case CoefficientDensityInfo:
event = CoefficientDensityEvent.adapter(businessDTO);
break;
case EnamelledWireWaste:
event = EnamelledWireWasteEvent.adapter(businessDTO);
break;
case MachiningAllowanceInfo:
event = MachiningAllowanceEvent.adapter(businessDTO);
break;
case RawMaterialInfo:
event = RawMaterialEvent.adapter(businessDTO);
break;
case ThreadCoefficientInfo:
event = ThreadCoefficientEvent.adapter(businessDTO);
break;
case WorkHourRateDoc:
event = WorkingHourEvent.adapter(businessDTO);
break;
case PlannedPriceFromOtherSys:
event = PriceEvent.adapter((PlannedPriceAfterConvertInfo) businessDTO);
break;
case RecycleMaterialDoc:
event = RecycleMaterialEvent.adapter(businessDTO);
break;
default:
throw new IllegalArgumentException("Unsupported business type: " + syncFormEnum);
}
event.setBillUserId(userInfo.getCreatorId());
event.setBillDepartId(userInfo.getDepartId());
return event;
}
public static AbstractFormulaEvent adapter(ThirdAbstractBusinessDTO businessDTO) {
EnamelledWireWasteEvent enamelledWireWasteEvent = new EnamelledWireWasteEvent(businessDTO);
enamelledWireWasteEvent.setBizId(businessDTO.getId());
enamelledWireWasteEvent.setFlowStatus(BaseEvent.APPROVE_FLOW_STATUS);
enamelledWireWasteEvent.setVersion(businessDTO.getVersion());
return enamelledWireWasteEvent;
}
这段代码的核心思想就是:
将 ThirdAbstractBusinessDTO
(被适配对象)转换成 BaseEvent
(目标接口),解决两者接口不兼容的问题。
然后注意编译期与运行期的差异,这里的调度,在运行期,我都是具体的子类,只是在这里我将其抽象了父类。所以你能看到,我对(PlannedPriceAfterConvertInfo) businessDTO进行了强转。
当然这里的switch-case是可以优化的,比如在枚举类中定义clazzName,进行反射获取,这种我后面代码写了很多。这里就没有整合,因为感觉没有必要。
这又提到设计模式的另外一个原则,你没有必要为了一个Hello Word去进行设计模式。
再往外扩展一点,在架构层面,这个原则类似于YAGNI。
再谈VO PO DTO对象
adpater本意是结构对象的转化,这里我想再次扩展描述一下PO,VO,DTO的概念。
一般来说:
单体服务:应该有个PO和VO,与Controller的应该是VO
分布式/微服务内:DAO层使用PO,服务间通讯使用DTO,前端使用VO
复杂业务场景:需要多个DTO与VO,我这边会增加一个叫做BizModel的业务对象。
这样划分的依据与好处
- 安全考虑:避免PO敏感字段的暴露
- 性能考虑:避免无关字段的序列化与反序列化
- 解耦:PO,VO,DTO各自不同的场景,有着不同的更新速度曲线
- 灵活性:不同场景可以使用不同的数据结构
所以,请不要偷懒。
三天三变的算法逻辑
此处的算法逻辑 主要是业务建模内的第三点:影响面获取(变化量引起的BOM最短链路获取)
首先我需要先提前说明一下,整个BOM结构不是一个简单链表结构,他是一个网状结构。一个子件可以找到多个父件,一个父件有可以下挂N多个子件。子件可以作为顶层,也可以是另外一个更大的父件的子件。(比如业务上的一个鼠标,可以单独看做一个顶层,但他也是电脑的子件)
核心逻辑分为分为以下几点:
- 通过递归算法,选择传入的物料编码,查找从给定物料编码到其所有父级物料的所有路径
- 合并重复链路,将子集路径去除,比如:若存在A->B->C与 A->B,选择最长路径即可,避免重复运算。
- 结构反转:按路径末端(顶层BOM)分组并合并路径,这样获取到的数据结构 key是顶层物料,同时合并重复路径 (A->B->C与A->B->D 变化为A->B-C、D,其中A为顶层)
- 再次去除重复路径,通过父子件作为唯一key进行去除,保证最后一步数据筛选的干净
- 通过广度优先遍历算法,对所有node节点,增加虚拟字段level,注意需要支持一个父节点下多个子节点(主要用于批量多任务的时候,进行顺序执行)
public Map<String, List<BOMSimpleInfo>> getTopLinkBomInfos(List<String> materialCodes) {
Map<String, List<BOMSimpleInfo>> resultMap = new HashMap<>();
if (CollectionUtils.isEmpty(materialCodes)) {
return resultMap;
}
List<Bom> allBom = (List<Bom>) basicCacheService.doGetCache(SyncFormEnum.BomTable.getSyncCacheType());
if (CollectionUtils.isEmpty(allBom)) {
return resultMap;
}
Map<String, List<Bom>> bomMapping = allBom.stream().collect(Collectors.groupingBy(Bom::getMaterialCode));
Map<String, List<List<BOMSimpleInfo>>> allBomInfos = new HashMap<>();
List<List<BOMSimpleInfo>> allLinkInfos = new ArrayList<>();
for (String materialCode : materialCodes) {
List<List<BOMSimpleInfo>> allPaths = findAllPaths(materialCode, bomMapping);
allBomInfos.put(materialCode, allPaths);
List<List<BOMSimpleInfo>> coverBomLinks = filterSubsetPaths(allPaths);
allLinkInfos.addAll(coverBomLinks);
}
Map<String, List<BOMSimpleInfo>> topLinkBomInfos = mergePaths(allLinkInfos);
topLinkBomInfos = removeRepeatBom(topLinkBomInfos);
buildLevel(topLinkBomInfos);
return topLinkBomInfos;
}
具体代码细节不提供(我相信没有人愿意看的)
业务触控监听体系架构设计
架构设计理念
代码整体结构
三级抽象体系
classDiagram
class AbstractBusinessTouchHandle {
<<abstract>>
+ResponseResult doAction(BaseEvent event)*
#doValidate()
#getInitialImpactFactor()
#reBuildBomLevels()
#calibration()
}
class BasicBusinessTouchHandle {
<<abstract>>
+getInitialImpactFactor()
#buildBusinessQueryInfo()*
}
class BasicFormulaTouchListener {
<<abstract>>
+FormulaHelper
+RemoteCalculationService
#reBuildFormulaDetail()
}
class FundingConfirmListener {
+doOperation()
+buildBusinessQueryInfo()
+getOriginBaseCode()
}
AbstractBusinessTouchHandle <|-- BasicBusinessTouchHandle
AbstractBusinessTouchHandle <|-- BasicFormulaTouchListener
BasicBusinessTouchHandle <|-- FundingConfirmListener
核心流程模板
// 模板方法模式典型实现
public ResponseResult<Object> doAction(BaseEvent event) {
// 1-5 基础校验与数据准备(不可变流程)
doValidate() → getImpactFactor → processBom → rebuildLevel → collectTouchInfo
// 6-10 业务定制扩展点(可变部分)
doBusinessLog() → archive() → calibration() → filterSyncData()
// 11 最终同步操作(固定收尾)
return doSyncOperation()
}
SOLID原则实践
1. 单一职责原则(SRP)
类/方法 | 单一职责体现 |
---|---|
doValidate() | 仅负责业务事件的基础校验 |
reBuildFormulaDetail() | 专注公式重组逻辑 |
FundingConfirmListener | 处理资金确认特定业务场景 |
2. 开闭原则(OCP)
// 扩展案例:新增环保材料监听器
public class EcoMaterialListener extends BasicBusinessTouchHandle {
@Override
protected CostTotalReportQueryInfo buildBusinessQueryInfo(BaseEvent event) {
// 实现新的查询参数构建逻辑
EcoMaterial material = (EcoMaterial) event.getSource();
return buildEcoQuery(material);
}
}
3.里氏替换原则(LSP)
// 父类引用可透明替换为子类实现
AbstractBusinessTouchHandle handler = new FundingConfirmListener();
ResponseResult result = handler.doAction(event); // 行为保持一致
核心代码结构解析
1. 模板方法控制流
public ResponseResult<Object> doAction(BaseEvent businessEvent) {
// [固定流程] 阶段1-5
preValidate → 获取初始影响源 → 构建BOM层级 → 收集触达信息
// [扩展点] 阶段6-10
doBusinessLog() // 可重写日志策略
calibration() // 可定制校准逻辑
// [固定流程] 最终同步
return doSyncOperation();
}
2.二级抽象实现
@Component
public abstract class BasicBusinessTouchHandle extends AbstractBusinessTouchHandle {
@Override
protected List<TouchModifyBusinessInfo> getInitialImpactFactor(BaseEvent originEvent) {
// 通用Redis数据获取实现
String redisKey = remoteService.getKey(buildQueryInfo(originEvent));
return redissonClient.getList(redisKey);
}
protected abstract CostTotalReportQueryInfo buildBusinessQueryInfo(BaseEvent event);
}
3.三级具体实现
public class FundingConfirmListener extends BasicBusinessTouchHandle {
@Override
protected CostTotalReportQueryInfo buildBusinessQueryInfo(BaseEvent event) {
// 实现资金业务特有的查询参数构建
FundingInformation funding = (FundingInformation) event.getSource();
return new QueryInfo()
.setProdUnitCodes(funding.getUnitCodes())
.setCostPeriod(getActivityPeriod());
}
@Override
protected String getOriginBaseCode(BaseEvent event) {
return ((FundingInformation)event.getSource()).getBusinessCode();
}
}
架构效益分析
指标 | 传统实现 | 当前架构 | 提升效果 |
---|---|---|---|
新功能开发周期 | 5人日/场景 | 1人日/场景 | 80%↓ |
核心流程变更影响 | 需修改所有实现类 | 仅修改抽象层 | 影响范围缩小90% |
代码重复率 | 60%+ | <15% | 4倍复用率提升 |
系统稳定性 | 各场景实现差异大 | 统一流程保障一致性 | 故障率降低70% |
CAS思想
我始终觉得,那些曾经觉得晦涩难懂的,在旁人眼中毫无意义的理论知识。随着时间的沉淀,知识面的拓展,终究会成长,破土,倔强而又美丽。
原子化业务操作模型
首先业务基础表单内可能有多个变更字段,我需要比较所有的变更字段(compare)
其次这些字段我需要备份,然后在将新的字段覆盖值到影响的业务对象上。(这一步是为了SDP,我不想动核心卷积算法的逻辑,而算法又依赖于部分业务表单的字段,swap)
最后,我们调用了核算的逻辑,不管成功与否,业务表单进行回退,恢复到原本的业务状态。(rollBack)
在整体过程中,增加链路日志,进行业务归档追踪(Trace business log)
核心设计原则
原则 | 实现方式 |
---|---|
稳定依赖原则(SDP) | 卷积算法与业务变更处理解耦 |
开闭原则(OCP) | 通过扩展FormulaAdjustInfo支持新业务类型 |
单一职责原则(SRP) | Compare/Swap阶段分离处理 |
稳定依赖的架构实现
架构层级 | 变更频率 | 技术特征 | 保护机制 |
---|---|---|---|
卷积计算层 | 低频 | 复杂数学逻辑 | 数据沙箱隔离 |
业务裂变层 | 高频 | 业务规则组合 | 流程原子化 |
数据持久层 | 中频 | 状态存储 | 事务补偿机制 |
部分关键核心代码
public void cas(List<ThirdAbstractBusinessDTO> thirdAbstractInfos, String type) {
if (CollectionUtils.isEmpty(thirdAbstractInfos) && !SyncFormEnum.isFormula(type)) {
return;
}
SyncFormEnum syncFormEnum = SyncFormEnum.getSyncEnumByType(type);
List<? extends ThirdAbstractBusinessDTO> allFormulaCacheInfos = basicCacheService.doGetCache(syncFormEnum.getSyncCacheType());
Map<String, ThirdAbstractBusinessDTO> formulaCacheMap = allFormulaCacheInfos.stream().collect(Collectors.groupingBy(ThirdAbstractBusinessDTO::getCompareVal,
Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(ThirdAbstractBusinessDTO::getVersion)), optional -> optional.orElseThrow(() -> new NoSuchElementException("No value present")))));
for (ThirdAbstractBusinessDTO business : thirdAbstractInfos) {
String compareVal = business.getCompareVal();
ThirdAbstractBusinessDTO cacheInfo = formulaCacheMap.get(compareVal);
if (cacheInfo == null) {
continue;
}
doCompare(business, cacheInfo, syncFormEnum.getSyncCacheType());
}
}
private void doCompare(ThirdAbstractBusinessDTO business, ThirdAbstractBusinessDTO cacheInfo, String syncCacheType) {
List<Field> allFields = getAllFields(business.getClass());
Map<String, BigDecimal> diffMap = compare(allFields, business, cacheInfo);
if (CollectionUtils.isEmpty(diffMap)) {
return;
}
String compareField = business.comparingFields().get(0);
if (!StringUtils.hasText(compareField)) {
return;
}
Object dataValue = getValue(compareField, allFields, business);
FormulaAdjustInfo formulaAdjustInfo = FormulaAdjustInfo.build(syncCacheType, compareField, business.getId(), dataValue, diffMap);
String key = formulaAdjustInfo.getType() + "::" + formulaAdjustInfo.getFormulaFieldCode() + "::" + business.getId();
RBucket<Object> formulaBucket = redissonClient.getBucket(key);
formulaBucket.set(formulaAdjustInfo, 1, TimeUnit.HOURS);
}
这里面的实现细节,无非是运行期,编译期,业务抽象对象,反射思想等
SDP稳定依赖原则
软件模块的依赖方向应该指向更稳定的方向
稳定性量化指标
稳定性量化指标
稳定性(I) = 传出依赖数(out) / (传入依赖数(in) + 传出依赖数(out))
其中:
- 传入依赖(in):其他模块依赖该模块的数量
- 传出依赖(out):该模块依赖其他模块的数量
分层架构实践
SOFT-HARD分层模型
我再看架构整洁之道的时候 ,模块之间硬与软就是通过更新曲线来决定,同时更新曲线能够区别不同的模块。
层级 | 变更频率 | 稳定性(I) | 典型组件 |
---|---|---|---|
SOFT层 | 天/周级 | 0.1-0.3 | 业务规则、UI交互 |
MID层 | 月级 | 0.4-0.6 | 领域服务、适配器 |
HARD层 | 年/版本级 | 0.7-1.0 | 数学算法、核心框架 |
架构防腐策略
抽象隔离层(Anti-Corruption Layer)
SDP的辩证思考
- 稳定≠僵化
“没有绝对稳定的系统,只有相对稳定的抽象”- 通过抽象接口隔离变化
- 使用策略模式应对多变性
- 动态平衡艺术
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "架构演进平衡点",
"data": {"values": [
{"x": 0, "y": 100, "label": "过度灵活"},
{"x": 50, "y": 50, "label": "最佳平衡"},
{"x": 100, "y": 0, "label": "过度固化"}
]},
"mark": "point",
"encoding": {
"x": {"field": "x", "type": "quantitative", "title": "稳定性"},
"y": {"field": "y", "type": "quantitative", "title": "灵活性"},
"text": {"field": "label", "type": "nominal"}
}
}
3.演进式架构建议
- 每季度评估模块稳定性指标
- 当I值变化超过±0.2时重构依赖关系
- 保持HARD层年变更率<5%,SOFT层可接受30-50%
该原则指导我们构建"柔性的稳定"系统——在保持核心稳固的同时,为变化保留充分空间。正如Martin Fowler所言:“架构的本质是决定哪些东西应该难以改变,而不是追求不可改变。”
关于业务事件消费
就是在我所有的任务进行业务消费的时候,这里我主要改了两版。
第一版本就是一个redis的Deque的队列,加锁。
大概长的这个样子
public void consumer() {
RLock lock = redissonClient.getLock(LOCK_NAME);
RDeque<CostTouchInfoTable> handleQueue = redissonClient.getDeque(HANDLE_QUEUE);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
while (!Thread.currentThread().isInterrupted()) {
try {
doHandle(handleQueue);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
然后每天晚上跑自动任务的时候,就会把服务器搞崩,分析了一下,感觉是锁的问题。又分析了一个感觉双端队列也没有必要啊。
我pull数据的时候,依赖的是reids的io模块,他不是有序的吗。
于是第二版本就是这个样子了
/**
* 此时并发过来 我应该也是对handleQueue获取 我只拿redis队列的最右侧元素 不关心这个元素是不是当前线程塞进去的
* 同时其他线程的获取 也不关心是不是自己线程的元素
* 最终只要保证整个RDeque的元素 线性有序的执行 就可以了 而这个依赖于redis队列的IO单线程
* <p>
* v2版本 修改队列为List 增加业务创建时间+level 按照业务顺序获取数据
*/
public void consumer() {
RLock lock = redissonClient.getLock(LOCK_NAME);
List<CostTouchInfoTable> handleQueue = redissonClient.getList(HANDLE_QUEUE);
this.handleByLock(lock, handleQueue);
}
private void doHandle(List<CostTouchInfoTable> handleQueue) {
// 这里已经分布式锁过了 我认为你就算此时有并发 将handleQueue数据增加 此时我也不需要考虑 考虑当前的数据集合就行 再说了新的现场又进不来
//按照创建时间增序 层级倒序排序
List<CostTouchInfoTable> sortedList = handleQueue.stream()
.sorted(Comparator.comparing(CostTouchInfoTable::getCreatedTime)
.thenComparing(Comparator.comparing(CostTouchInfoTable::getLevel).reversed()))
.collect(Collectors.toList());
// 遍历排序后的列表,但删除操作针对原始 Redis 列表
Iterator<CostTouchInfoTable> iterator = sortedList.iterator();
Set<String> handleTaskIds = new HashSet<>();
while (iterator.hasNext()) {
CostTouchInfoTable task = iterator.next();
log.info("handle log id : {}", task.getId());
try {
BaseEvent event = taskAdapter.adapter(task);
businessEventPublisher.doOperation(event);
} finally {
handleTaskIds.add(task.getId());
}
}
List<CostTouchInfoTable> itemsToRemove = handleQueue.stream().filter(item -> handleTaskIds.contains(item.getId())).collect(Collectors.toList());
//不能使用removeIf RList没有实现它 这里套一层remove元素 他的内存id是一致的
handleQueue.removeAll(itemsToRemove);
}
这里面给我的提示就是,我始终是认为无锁状态是优于锁竞争的(参考 synchronized锁膨胀的第一阶段,无锁状态->偏向锁->轻量锁->重量锁)
所以能不加锁,就不加锁。
异常逻辑处理
通篇看来,整体业务流程是复杂的,所以越是复杂流程,越是需要注意异常逻辑处理。
异常防御体系全景图
分层防御策略
防御层级 | 技术手段 | 业务价值 | 实现案例 |
---|---|---|---|
前置校验 | 参数校验/状态检查 | 过滤80%非法请求 | @Valid 注解校验 |
过程防护 | 事务隔离/重试机制 | 保证操作原子性 | Spring @Transactional |
事后补偿 | TCC/Saga模式 | 最终一致性保障 | 补偿任务队列 |
应急恢复 | 快照回滚/流量控制 | 系统快速自愈 | Redis快照恢复 |
TCC事务补偿实现
在异常发生之后,可以通过TCC,达到BASE。但是需要注意,错误重试应该有阈值防护,避免大量错误无效请求,挤占CPU资源
可观测性建立
- 基础设施层,建立Zabbix监控服务器状态
- 应用服务层,建立Prometheus监控
- 接口调用层,建立SkyWalking链路追踪
- 业务逻辑层,建立各种业务报表,过程日志(ELK+BI)
当然你要是有能力的话,可以做一个企业级别的预警平台,我老想做这个了。
可以内置一些固定校验规则,再增加预留埋点接口,给不同的系统去填充他们的校验接口,当然格式会统一,然后业务进行触达。
再做个看板,后面集成zabbix,监听服务器状态。emmm,可惜没有机会。
弹性处理策略
这个主要是电商当时做的,流量紧急削峰,容灾,降级等等策略。
他是建立在程序自动识别异常的时候,进行操作,避免事故进一步扩大。