当前位置: 首页 > news >正文

利用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的辩证思考
  1. 稳定≠僵化
    “没有绝对稳定的系统,只有相对稳定的抽象”
    • 通过抽象接口隔离变化
    • 使用策略模式应对多变性
  2. 动态平衡艺术
{
  "$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,可惜没有机会。

弹性处理策略

这个主要是电商当时做的,流量紧急削峰,容灾,降级等等策略。

他是建立在程序自动识别异常的时候,进行操作,避免事故进一步扩大。

http://www.dtcms.com/a/108890.html

相关文章:

  • Linux文件系统选择指南:XFS or EXT4
  • MCP Servers是什么?
  • 数学复习(12)关于三角函数极限和求导
  • Jackson 处理 JSON 生成与解析指南
  • 逆透视投影 torch
  • 全面解析 Mybatis 与 Mybatis-Plus:深入原理、实践案例与高级特性对比
  • XXS漏洞零基础学习到入门
  • 通用的动态定时任务系统
  • 【动态规划】二分优化最长上升子序列
  • 34、web前端开发之JavaScript(三)
  • 将图表和表格导出为PDF的功能
  • ThreadLocalMap的作用和特点
  • cobbler自动最小化安装centos,并配置地址
  • springboot+easyexcel实现下载excels模板下拉选择
  • Spring Boot 的配置文件
  • 网络空间安全(50)JavaScript基础语法
  • C#:重构(refactoring)
  • 【Spring Cloud Alibaba】:Nacos 使用全详解
  • CExercise04_1位运算符_1 用位运算符判断某个整数是否为奇数
  • 购物车(V2装饰器)
  • 算法:优选(1)
  • RK3568驱动 SPI主/从 配置
  • 基于微信小程序的医院挂号预约系统设计与实现
  • Apache Doris 2025 Roadmap:构建 GenAI 时代实时高效统一的数据底座
  • WRF-Chem 中出现real.exe错误(psfc 计算问题)- MOZART
  • Apache BookKeeper Ledger 的底层存储机制解析
  • 配置单区域OSPF
  • ARM—LED,看门狗关闭,按钮,时钟,PWM定时器,蜂鸣器
  • 【前端扫盲】postman介绍及使用
  • 走向多模态AI之路(三):多模态 AI 的挑战与未来