微服务拆分治理最佳实践

作者:京东零售 徐强 黄威 张均杰

背景

部门中维护了一个老系统,功能都耦合在一个单体应用中(300+接口),表也放在同一个库中(200+表),导致系统存在很多风险和缺陷。经常出现问题:如数据库的单点、性能问题,应用的扩展受限,复杂性高等问题。

从下图可见。各业务相互耦合无明确边界,调用关系错综复杂。

image.png

随着业务快速发展,各种问题越来越明显,急需对系统进行微服务改造优化。经过思考,整体改造将分为三个阶段进行:

  • 数据库拆分:数据库按照业务垂直拆分。

  • 应用拆分:应用按照业务垂直拆分。

  • 数据访问权限收口:数据权限按照各自业务领域,归属到各自的应用,应用与数据库一对一,禁止交叉访问。

2.png

数据库拆分

单体数据库的痛点:未进行业务隔离,一个慢SQL易导致系统整体出现问题;吞吐量高,读写压力大,性能下降;

数据库改造

640 (1).png

根据业务划分,我们计划将数据库拆分为9个业务库。数据同步方式采用主从复制的方式,并且通过binlog过滤将对应的表和数据同步到对应的新数据库中。

代码改造方案

如果一个接口中操作了多张表,之前这些表属于同一个库,数据库拆分后可能会分属于不同的库。所以需要针对代码进行相应的改造。

目前存在问题的位置:

  • 数据源选择:系统之前是支持多数据源切换的,在service上添加注解来选择数据源。数据库拆分后出现的情况是同一个service中操作的多个mapper从属于不同的库。

  • 事务:事务注解目前是存在于service上的,并且事务会缓存数据库链接,一个事务内不支持同时操作多个数据库。

改造点梳理:

  • 同时写入多个库,且是同一事务的接口6个:需改造数据源,需改造事务,需要关注分布式事务;

  • 同时写入多个库,且不是同一事务的接口50+:需改造数据源,需改造事务,无需关注分布式事务;

  • 同时读取多个库 或 读取一个库写入另一个库的接口200+:需改造数据源,但无需关注事务;

  • 涉及多个库的表的联合查询8个:需进行代码逻辑改造

梳理方式:

采用部门中的切面工具,抓取入口和表的调用关系(可识别表的读/写操作),找到一个接口中操作了多个表,并且多个表分属于不同业务库的情况;

分布式事务:

进行应用拆分和数据收口之后,是不存在分布式事务的问题的,因为操作第二个库会调用对应系统的RPC接口进行操作。所以本次不会正式支持分布式事务,而是采用代码逻辑保证一致性的方式来解决;

方案一

640 (1) (1).png

将service中分别操作多个库的mapper,抽取成多个Service。分别添加切换数据源注解和事务注解。

问题:改动位置多,涉及改动的每个方法都需要梳理历史业务;service存在很多嵌套调用的情况,有时难以理清逻辑;修改200+位置改动工作量大,风险高;

方案二

640 (2).png
如图所示,方案二将数据源注解移动到Mapper上,并使用自定义的事务实现来处理事务。

将多数据源注解放到Mapper上的好处是,不需要梳理代码逻辑,只需要在Mapper上添加对应数据源名称即可。但是这样又有新的问题出现,

  • 问题1:如上图,事务的是配置在Service层,当事务开启时,数据源的连接并没有获取到,因为真正的数据源配置在Mapper上。所以会报错,这个错误可以通过多数据源组件的默认数据源功能解决。

  • 问题2:mybatis的事务实现会缓存数据库链接。当第一次缓存了数据库链接后,后续配置在mapper上的数据源注解并不会重新获取数据库链接,而是直接使用缓存起来的数据库链接。如果后续的mapper要操作其余数据库,会出现找不到表的情况。鉴于以上问题,我们开发了一个自定义的事务实现类,用来解决这个问题。

下面将对方案中出现的两个组件进行简要说明原理。

多数据源组件

多数据源组件是单个应用连接多个数据源时使用的工具,其核心原理是通过配置文件将数据库链接在程序启动时初始化好,在执行到存在注解的方法时,通过切面获取当前的数据源名称来切换数据源,当一次调用涉及多个数据源时,会利用栈的特性解决数据源嵌套的问题。

/**
 * 切面方法
 */
public Object switchDataSourceAroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        //获取数据源的名字
        String dsName = getDataSourceName(pjp);
        boolean dataSourceSwitched = false;
        if (StringUtils.isNotEmpty(dsName)
                && !StringUtils.equals(dsName, StackRoutingDataSource.getCurrentTargetKey())) {
            // 见下一段代码
            StackRoutingDataSource.setTargetDs(dsName);
            dataSourceSwitched = true;
        }
        try {
            // 执行切面方法
            return pjp.proceed();
        } catch (Throwable e) {
            throw e;
        } finally {
            if (dataSourceSwitched) {
                StackRoutingDataSource.clear();
            }
        }
    }

public static void setTargetDs(String dbName) {
    if (dbName == null) {
	throw new NullPointerException();
    }
    if (contextHolder.get() == null) {
        contextHolder.set(new Stack<String>());
    }
    contextHolder.get().push(dbName);
    log.debug("set current datasource is " + dbName);
}

StackRoutingDataSource继承 AbstractRoutingDataSource类,AbstractRoutingDataSource是spring-jdbc包提供的一个了AbstractDataSource的抽象类,它实现了DataSource接口的用于获取数据库链接的方法。

自定义事务实现

从方案二的图中可以看到默认的事务实现使用的是mybatis的SpringManagedTransaction。

640 (3).png
如上图,Transaction和SpringManagedTransaction都是mybatis提供的类,他提供了接口供SqlSession使用,处理事务操作。
通过下边的一段代码可以看到,事务对象中存在connection变量,首次获得数据库链接后,后续当前事务内的所有数据库操作都不会重新获取数据库链接,而是会使用现有的数据库链接,从而无法支持跨库操作。


public class SpringManagedTransaction implements Transaction {

  private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);

  private final DataSource dataSource;

  private Connection connection;

  private boolean isConnectionTransactional;

  private boolean autoCommit;

  public SpringManagedTransaction(DataSource dataSource) {
    notNull(dataSource, "No DataSource specified");
    this.dataSource = dataSource;
  }
  // 下略
}

MultiDataSourceManagedTransaction是我们自定义的事务实现,继承自SpringManagedTransaction类,并在内部支持维护多个数据库链接。每次执行数据库操作时,会根据数据源名称判断,如果当前数据源没有缓存的链接则重新获取链接。这样,service上的事务注解其实控制了多个单库事务,且作用域范围相同,一起进行提交或回滚。

代码如下:

public class MultiDataSourceManagedTransaction extends SpringManagedTransaction {
    private DataSource dataSource;

    public ConcurrentHashMap<String, Connection> CON_MAP = new ConcurrentHashMap<>();


    public MultiDataSourceManagedTransaction(DataSource dataSource) {
        super(dataSource);
        this.dataSource = dataSource;
    }

    @Override
    public Connection getConnection() throws SQLException {
        Method getCurrentTargetKey;
        String dataSourceKey;
        try {
            getCurrentTargetKey = dataSource.getClass().getDeclaredMethod("getCurrentTargetKey");
            getCurrentTargetKey.setAccessible(true);
            dataSourceKey = (String) getCurrentTargetKey.invoke(dataSource);
        } catch (Exception e) {
            log.error("MultiDataSourceManagedTransaction invoke getCurrentTargetKey 异常", e);
            return null;
        }

        if (CON_MAP.get(dataSourceKey) == null) {
            Connection connection = dataSource.getConnection();
            if (!TransactionSynchronizationManager.isActualTransactionActive()) {
                connection.setAutoCommit(true);
            } else {
                connection.setAutoCommit(false);
            }
            CON_MAP.put(dataSourceKey, connection);
            return connection;
        }

        return CON_MAP.get(dataSourceKey);
    }

    @Override
    public void commit() throws SQLException {
        if (CON_MAP == null || CON_MAP.size() == 0) {
            return;
        }
        Set<Map.Entry<String, Connection>> entries = CON_MAP.entrySet();
        for (Map.Entry<String, Connection> entry : entries) {
            Connection value = entry.getValue();
            if (!value.isClosed() && !value.getAutoCommit()) {
                value.commit();
            }
        }
    }

    @Override
    public void rollback() throws SQLException {
        if (CON_MAP == null || CON_MAP.size() == 0) {
            return;
        }
        Set<Map.Entry<String, Connection>> entries = CON_MAP.entrySet();
        for (Map.Entry<String, Connection> entry : entries) {
            Connection value = entry.getValue();
            if (value == null) {
                continue;
            }
            if (!value.isClosed() && !value.getAutoCommit()) {
                entry.getValue().rollback();
            }
        }
    }

    @Override
    public void close() throws SQLException {
        if (CON_MAP == null || CON_MAP.size() == 0) {
            return;
        }
        Set<Map.Entry<String, Connection>> entries = CON_MAP.entrySet();
        for (Map.Entry<String, Connection> entry : entries) {
            DataSourceUtils.releaseConnection(entry.getValue(), this.dataSource);
        }
        CON_MAP.clear();
    }
}

注:上面并不是分布式事务。在数据收口之前,它只存在于同一个JVM中。如果项目允许,可以考虑使用Atomikos和Mybatis整合的方案。

数据安全性

本次进行了很多代码改造,如何保证数据安全,保证数据不丢失,我们的机制如下,分为三种情况进行讨论:

  • 跨库事务:6处,采用了代码保证一致性的改造方式;上线前经过重点测试,保证逻辑无问题;

  • 单库事务:依赖于自定义事务实现,针对自定义事务实现这一个类进行充分测试即可,测试范围小,安全性有保障;

  • 其余单表操作:相关修改是在mapper上添加了数据源切换注解,改动位置几百处,几乎是无脑改动,但也存在遗漏或错改的可能;测试同学可以覆盖到核心业务流程,但边缘业务可能会遗漏;我们添加了线上监测机制,当出现找不到表的错误时(说明数据源切换注解添加错误),记录当前执行sql并报警,我们进行逻辑修复与数据处理。

综上,通过对三种情况的处理来保证数据的安全性。

应用拆分

系统接近单体架构,存在以下风险:

  1. 系统性风险:一个组件缺陷会导致整个进程崩溃,如内存泄漏、死锁。

  2. 复杂性高:系统代码繁多,每次修改代码都心惊胆战,任何一个bug都可能导致整个系统崩溃,不敢优化代码导致代码可读性也越来越差。

  3. 测试环境冲突,测试效率低:业务都耦合在一个系统,只要有需求就会出现环境抢占,需要额外拉分支合并代码。

拆分方案

与数据库拆分相同,系统拆分也是根据业务划分拆成9个新系统。

方案一:搭建空的新系统,然后将老系统的相关代码挪到新系统。

  • 优点:一步到位。

  • 缺点:需要主观挑选代码,然后挪到新系统,可视为做了全量业务逻辑的变动,需要全量测试,风险高,周期长。

方案二:从老系统原样复制出9个新系统,然后直接上线,通过流量路由将老系统流量转发到新系统,后续再对新系统的冗余代码做删减。

  • 优点:拆分速度快, 首次上线前无业务逻辑改动,风险低;后续删减代码时依据接口调用量情况来判定,也可视为无业务逻辑的改动,风险较低,并且各系统可各自进行,无需整体排期, 较为灵活。

  • 缺点:分为了两步,拆分上线和删减代码

拆分方案对比

我们在考虑拆分风险和拆分效率后,最终选择了方案二。

方案二原理

拆分实践

1、搭建新系统

直接复制老系统代码,修改系统名称,部署即可

2、流量路由

路由器是拆分的核心,负责分发流量到新系统,同时需要支持识别测试流量,让测试同学可以提前在线上测试新系统。我们这边用filter来作为路由器的,源码见下方。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    HttpServletRequest servletRequest = (HttpServletRequest) request;
    HttpServletResponse servletResponse = (HttpServletResponse) response;

    // 路由开关(0-不路由, 1-根据指定请求头路由, 2-全量路由)
    final int systemRouteSwitch = configUtils.getInteger("system_route_switch", 1);
    if (systemRouteSwitch == 0) {
        filterChain.doFilter(request, response);
        return;
    }
    // 只路由测试流量
    if (systemRouteSwitch == 1) {
        // 检查请求头是否包含测试流量标识 包含才进行路由
        String systemRoute = ((HttpServletRequest) request).getHeader("systemRoute");
        if (systemRoute == null || !systemRoute.equals("1")) {
            filterChain.doFilter(request, response);
            return;
        }
    }

    String systemRouteMapJsonStr = configUtils.getString("route.map", "");
    Map<String, String> map = JSONObject.parseObject(systemRouteMapJsonStr, Map.class);
    String rootUrl = map.get(servletRequest.getRequestURI());

    if (StringUtils.isEmpty(rootUrl)) {
        log.error("路由失败,本地服务内部处理。原因:请求地址映射不到对应系统, uri : {}", servletRequest.getRequestURI());
        filterChain.doFilter(request, response);
        return;
    }

    String targetURL = rootUrl + servletRequest.getRequestURI();
    if (servletRequest.getQueryString() != null) {
        targetURL = targetURL + "?" + servletRequest.getQueryString();
    }
    RequestEntity<byte[]> requestEntity = null;
    try {
        log.info("路由开始 targetURL = {}", targetURL);
        requestEntity = createRequestEntity(servletRequest, targetURL);
        ResponseEntity responseEntity = restTemplate.exchange(requestEntity, byte[].class);
        if (requestEntity != null && requestEntity.getBody() != null && requestEntity.getBody().length > 0) {
            log.info("路由完成-请求信息: requestEntity = {}, body = {}", requestEntity.toString(), new String(requestEntity.getBody()));
        } else {
            log.info("路由完成-请求信息: requestEntity = {}", requestEntity != null ? requestEntity.toString() : targetURL);
        }

        HttpHeaders headers = responseEntity.getHeaders();
        String resp = null;
        if (responseEntity.getBody() != null && headers != null && headers.get("Content-Encoding") != null && headers.get("Content-Encoding").contains("gzip")) {
            byte[] bytes = new byte[30 * 1024];
            int len = new GZIPInputStream(new ByteArrayInputStream((byte[]) responseEntity.getBody())).read(bytes, 0, bytes.length);
            resp = new String(bytes, 0, len);
        }

        log.info("路由完成-响应信息: targetURL = {}, headers = {}, resp = {}", targetURL, JSON.toJSONString(headers), resp);
        if (headers != null && headers.containsKey("Location") && CollectionUtils.isNotEmpty(headers.get("Location"))) {
            log.info("路由完成-需要重定向到 {}", headers.get("Location").get(0));
            ((HttpServletResponse) response).sendRedirect(headers.get("Location").get(0));
        }
        addResponseHeaders(servletRequest, servletResponse, responseEntity);
        writeResponse(servletResponse, responseEntity);
    } catch (Exception e) {
        if (requestEntity != null && requestEntity.getBody() != null && requestEntity.getBody().length > 0) {
            log.error("路由异常-请求信息: requestEntity = {}, body = {}", requestEntity.toString(), new String(requestEntity.getBody()), e);
        } else {
            log.error("路由异常-请求信息: requestEntity = {}", requestEntity != null ? requestEntity.toString() : targetURL, e);
        }
        response.setCharacterEncoding("UTF-8");
        ((HttpServletResponse) response).addHeader("Content-Type", "application/json");
        response.getWriter().write(JSON.toJSONString(ApiResponse.failed("9999", "网络繁忙哦~,请您稍后重试")));
    }
}

3、接口抓取&归类

路由filter是根据接口路径将请求分发到各个新系统的,所以需要抓取一份接口和新系统的映射关系。
我们这边自定义了一个注解@TargetSystem,用注解标识接口应该路由到的目标系统域名,

@TargetSystem(value = "http://order.demo.com")
@GetMapping("/order/info")
public ApiResponse orderInfo(String orderId) {
    return ApiResponse.success();
}

然后遍历获取所有controller根据接口地址和注解生成路由映射关系map

    /**
     * 生成路由映射关系MAP
     * key:接口地址 ,value:路由到目标新系统的域名
     */
    public Map<String, String> generateRouteMap() {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
        Set<Map.Entry<RequestMappingInfo, HandlerMethod>> entries = handlerMethods.entrySet();
        Map<String, String> map = new HashMap<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entries) {
            RequestMappingInfo key = entry.getKey();
            HandlerMethod value = entry.getValue();
            Class declaringClass = value.getMethod().getDeclaringClass();
            TargetSystem targetSystem = (TargetSystem) declaringClass.getAnnotation(TargetSystem.class);
            String targetUrl = targetSystem.value();
            String s1 = key.getPatternsCondition().toString();
            String url = s1.substring(1, s1.length() - 1);
            map.put(url, targetUrl);
        }
        return map;
    }

路由映射关系MAP

4、测试流量识别

测试可以用利用抓包工具charles,为每个请求都添加固定的请求头,也就是测试流量标识,路由器拦截请求后判断请求头内是否包含测试流量标,包含就路由到新系统,不包含就是线上流量留在老系统执行。

路由流程

5、需求代码合并

执行系统拆分的过程中,还是有需求正在并行开发,并且需求代码是写在老系统的,系统拆分完成上线后,需要将这部分需求的代码合并到新系统,同时要保证git版本记录不能丢失,那应该怎么做呢?

我们利用了git可以添加多个多个远程仓库来解决需求合并的痛点,命令:git remote add origin 仓库地址,把新系统的git仓库地址添加为老系统git的远程仓库,老系统的git变动就可以同时push到所有新系统的仓库内,新系统pull下代码后进行合并。

需求代码合并方案

6、上线风险

风险一:JOB在新老系统并行执行。新系统是复制的老系统,JOB也会复制过来,导致新老系统有相同的JOB,如果这时候上线新系统,新系统的JOB就会执行,老系统的JOB也一直在run,这样一个JOB就会执行2次。新系统刚上线还没经过测试验证,这时候执行JOB是有可能失败的。以上2种情况都会引起线上Bug,影响系统稳定性。

风险二:新系统提前消费MQ。和风险一一样,新系统监听和老系统一样的topic,如果新系统直接上线,消息是有可能被新系统消费的,新系统刚上线还没经过测试验证,消费消息有可能会出异常,造成消息丢失或其他问题,影响系统稳定性。

如何解决以上2个上线风险呢?

我们用“动态开关”解决了上述风险,为新老系统的JOB和MQ都加了开关,用开关控制JOB和MQ在新/老系统执行。上线后新系统的JOB和MQ都是关掉的,待QA测试通过后,把老系统的JOB和MQ关掉,把新系统的JOB和MQ打开就可以了。

上线风险解决方案

系统瘦身

拆分的时候已经梳理出了一份“入口映射关系map”,每个新系统只需要保留自己系统负责的接口、JOB、MQ代码就可以了,除此之外都可以进行删除。

拆分带来的好处

  1. 系统架构更合理,可用性更高:即使某个服务挂了也不会导致整个系统崩溃

  2. 复杂性可控:每个系统都是单一职责,系统逻辑清晰

  3. 系统性能提升上限大:可以针对每个系统做优化,如加缓存

  4. 测试环境冲突的问题解决,不会因为多个系统需求并行而抢占环境

数据访问权限收口

问题介绍

数据访问权限未收口:一个业务的数据库被其余业务应用直接访问,未通过rpc接口将数据访问权限收口到数据拥有方自己的应用。数据访问逻辑分散,存在业务耦合,阻碍后续迭代和优化。

问题产生的背景:之前是单体应用和单体数据库,未进行业务隔离。在进行数据库拆分和系统拆分时,为解决系统稳定性的问题需快速上线,所以未优化拆分后跨业务访问数据库的情况。本阶段是对数据库拆分和应用拆分的延伸和补充。

640 (10).png

改造过程

  1. RPC接口统计(如图一)

进行比对,如程序入口归类和调用的业务DB归类不一致,则认为Dao方法需提供RPC接口

640 (11).png

经统计,应用访问非本业务数据库的位置有260+。由于涉及位置多,人工改造成本高、效率较低,且有错改和漏掉的风险,我们采用了开发工具,用工具进行代码生成和批量修改的方式进行改造。

  1. RPC接口生成(如图二)
  • 读取需要生成RPC接口的Dao文件,进行解析

  • 获取文件名称,Dao方法列表,import导包列表等,放入ClassContext上下文

  • 匹配api、rpc文件模板,从classContext内取值替换模板变量,通过package路径生成java文件到指定服务内

  • 批量将服务内Dao名称后缀替换为Rpc服务名,减少人工改动风险,例:SettleRuleDao -> SettleRuleRpc

640 (12).png

名词解释:

  • ftl:Freemarker模板的文件后缀名,FreeMarker是一个模版引擎,一个基于文本的模板输出工具。

  • interfaceName:用存放api文件名称

  • className:用于存放serviceImpl文件名称

  • methodList:用于存放方法列表,包含入参、出参、返回值等信息

  • importList:用于存放api和impl文件内其他引用实体的导包路径

  • apiPackage:用于存放生成的Api接口类包名

  • implPackage:用于存放生成的Api实现类包名

  • rpcPackage:用于存放生成的rpc调用类包名

640 (13).png

640 (14).png

  1. 灰度方案(如图三)
  • 数据操作统一走RPC层处理,初期阶段RPC层兼顾RPC调用,也有之前的DAO调用,使用开关切换。

  • RPC层进行双读,进行Api层和Dao层返回结果的比对,前期优先返回Dao层结果,验证无问题后,在全量返回RPC的结果,清除其他业务数据库连接。

  • 支持开关一键切换,按流量进行灰度,降低数据收口风险

640 (15).png

收益

  1. 业务数据解耦,数据操作统一由各自垂直系统进行,入口统一

  2. 方便后续在接口粒度上增加缓存和降级处理

总结

以上,是我们对单体系统的改造过程,经过了三步优化、上线,将单体系统平滑过渡到了微服务结构。解决了数据库的单点问题、性能问题,应用业务得到了简化,更利于分工,迭代。并且可以针对各业务单独进行优化升级,扩容、缩容,提升了资源的利用率。

相关推荐

AI 会写代码之后,交付为什么还没变快?

AI 写代码,只是这场变革的起点,而不是终点。真正决定一个组织能否吃到 AI 红利的,不是"能不能用 AI 写出代码",而是敢不敢用 AI 重新设计交付流程、重新划分角色分工、重新组织团队结构。研发视角能把工具和个人推到极致,却会在组织的边界前止步;只有站上组织视角,把AI × 工具一路推进到AI × 组织,那些被 AI 照出来的摩擦,才会真正被抹平。国际技术部 Multi AI Native 想做的,正是这件事——让 Agent 成为你可靠的数字战友,让每一次迭代交付都快一个量级;

京东科技开发者 246

OxygenData-Vibe搭建:用 AI Coding重新定义数据看板搭建

长期以来,数据看板开发主要有两种方式:一种是使用标准化 BI 工具,交付快、成本低,但图表样式、交互体验和功能扩展都受限于 BI 本身的能力;另一种是自研定制开发,能够充分满足复杂和个性化需求,但需要投入大量研发资源,交付周期也更长。比如一个通过 BI 只需要 1 天就能搭建出来的看板,如果采用自研方式,可能需要投入 30 PD 的研发人力。既然 BI 交付这么快,为什么不把所有看板需求都交给 BI?原因在于,业务某些场景需要的不只是把数据展示出来。

京东科技开发者 309

当我谈数据库高可用时我谈些什么

大家都经常听到说数据库的双1,那双1指的是什么呢?参数=1(最安全)≠1(更快但可能丢)每次提交都把 redo 刷盘0/2:攒着刷,崩溃丢最近 1 秒每次提交都把 binlog 刷盘0/N:攒着刷,崩溃丢最近若干条"双 1"(都设为 1)= 单机不丢已提交事务的前提。生产核心库必须双 1;代价是每次提交两次 fsync。小结:单机层用 WAL 提速、用 2PC 保证 redo/binlog 一致、用崩溃恢复裁决半成品事务、用双 1 兜底持久性。

京东科技开发者 271

ScoAI:面向供应链的运筹原生决策智能

ScoAI 关注的并非 AI 能否进入供应链,而是 AI 参与供应链决策之后,哪些内容适合通过数据学习,哪些关系必须显式计算,以及哪些结果需要依据执行记录进行验证。其基本分工是:供应链业务定义问题、资源与行动后果,运筹学表达目标、约束和协同关系,AI 提供可复用的估计与判断。真正用于改进后续决策的,不仅是模型生成的初始结果,还包括实际采取的行动及其产生的业务反馈。

京东科技开发者 367

拆解海博 AI-Native 落地保障:海博团队 AI 知识库能力建设

绕了一大圈,回头看,我们其实一直在解开头那三个结:AI 读不懂系统、影响面靠人脑记、测试预期没处查。做到这里,本质上是把"AI 用不好"这个问题,从"是不是该换个更强的模型"重新定义成了"怎么把上下文经营好"。顺着这个思路,我们攒下了这么几样东西:•。

京东科技开发者 518

从零构建一个生产级记忆型 AI Agent —— AgentScope 项目全景技术与学习指南

domain 层端口接口(domain)职责适配器实现(infra)对话流水账读写经验记忆检索/落库请求级幂等四态会话元数据 CRUD会话级 Skill 选择态Skill 元数据HITL 权限网关(domain 内含逻辑,见第四篇)StreamSink技术中立的流式下沉(client 层桥接)以幂等端口为例,头部注释明确写道:实现 domain 端口,持有实体不出 infra 边界,对外只暴露嵌套视图GateResult。

京东科技开发者 571

数据涅槃:Agentic 重塑搜推数据生产体系

当前大模型已经能够理解需求、生成代码和解释错误,但数据生产的难点并不只是“写出一段 SQL”。一个需求还要经过数据源确认、口径澄清、加工编排、资源规划、任务提交、调度运行、结果交付和故障处置。任何一步缺少都可能让“看起来正确”的任务变成生产事故。

京东科技开发者 337

81.8 秒的视频,我们改了 15 个版本:一次纯 Codex 驱动的 AI-native 视频实践

81.8 秒、15 个版本,这些数字真正想说明什么?视频制作正在从一组专业软件里的复杂操作,变成一场围绕目标、参考和反馈展开的持续对话。AI 已经可以接住从脚本到交付的大量工作。你可以把精力留给更重要的部分:理解观众、选择故事、判断好坏。过去停在 PPT 和脑海里的想法,现在有机会被快速做成一支能看、能改、也愿意转发的视频。如果明天就要介绍你手头的项目,你最想先把哪一步交给 AI?

京东科技开发者 433

Codex 驱动的移动端AI全栈开发:从 Relay 原型图到可交付链路

AI 编程工具降低了跨端开发门槛,但“让 AI 写页面”并不等于“完成移动端交付”。PC 运营端页面通常以功能闭环为中心,设计约束较弱,研发可以基于组件库快速搭建;移动端 C 端页面直接面对用户,Relay 原型图和 UI 设计稿不仅是视觉参考,更是交互、信息层级、状态反馈和业务信任感的综合契约。

京东科技开发者 566

Claude Code vs Codex 记忆体系深度对比

Claude Code —— 分层自动记忆系统四层分层架构(CLAUDE.md / Auto Memory / Session Memory / AutoDream)构成自优化闭环,系统主动捕获、分类、去冗并跨会话接力经验。走的是自动化优先路线——让机器替人记忆、替人整理,以少量可控性的不确定换取强大的经验积累能力。Codex —— 声明式契约系统两层扁平架构(AGENTS.md 静态声明层 + 会话上下文层),记忆完全由人显式订立、显式修改,系统只读契约、绝不擅自变更。走的是可控性优先。

京东科技开发者 605

上游给空、下游拿到 -1,我把故障一直追到了 commons-beanutils 的构造函数

这篇手记是一份事后复盘,写在我们已经把修复推上线、把测试用例跑绿、把约束文档沉淀进团队库之后。非研发同事,可以直接从第八章开始看,前七章都是讲排障和代码分析。故障本身不复杂,修复也不复杂,但这次排查真正让我想写下来的,不是技术本身——而是从问题提报到解决全程AI的作用:问题定位:业务方反馈了一个异常单号说「下发失败」,一线运维同事直接在日志系统捞到相关的系统日志,用IDE打开下游代码库,把异常信息给到JoyCode——AI 直接指向了问题是serialType值非法,系统只接受0和1,而当前上游给的是-1。

京东科技开发者 703

SKill测评:谁才是真正的Skill之王?

巧合的是,阿里近期开源了一款名为 Skill-Up 的测评工具——它专门面向 Agent Skill 开发者,支持在 Claude Code、Codex、Qoder CLI 等真实 Agent Engine 环境中验证 Skill 的功能正确性,并可将失败用例转化为精准的修复指引,同时支持本地与 CI 环境的持续回归。2026年,Skill 的热度不减,已成为 AI 辅助编程领域的标配。在日常研发中,无论是沉淀个人经验,还是将团队的优秀范本标准化,将知识“蒸馏”为 Skill 已是大家的常规操作。

京东科技开发者 618

让AI真正参与工作!JoyDesk已上线京东云

JoyDesk提供覆盖AI资产管理、权限控制、安全策略、审批审计、运营监控等企业级治理能力,支持按组织、角色和项目进行精细化权限管理,并支持私有化部署,确保企业数据边界清晰、AI行为全程可追溯、安全策略灵活可配置,满足金融、政企等行业对数据安全和合规治理的要求,让AI真正成为企业可信赖的生产力。JoyDesk支持MCP协议、标准API、技能等多种开放接入方式,可快速连接企业现有业务系统,将业务能力封装为AI可调用的技能和专家,让AI能够理解业务流程、调用业务系统、完成业务任务,而无需改变企业原有IT架构。

京东科技开发者 652

【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手【最终版】

这个项目展示了AI Agent 在垂直内容创作领域端到端自动化:从内容理解到播客输出,全流程 AI 驱动多模态融合:文字、图片、音频、视频统一处理专业级音频质量:音色设计 + 智能混音,输出广播级品质低门槛使用:可视化配置 + 自然语言交互,无需技术背景。

京东科技开发者 556

【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手(高级篇)

这是一个 AI 驱动的播客创作助手,专注于从文字/视频/音频内容识别到播客制作的全流程。你可以用自然语言与它对话,也可以上传视频和音频,它会理解意图、调用相应的语音合成工具,把他们变成真实的播客音频。

京东科技开发者 720

【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手(进阶篇)

这是一个AI 驱动的音频内容创作助手。你可以用自然语言与它对话,它会理解意图、调用相应的语音合成工具,把文字变成真实的播客音频,并保存在本地。关于项目的详细介绍见第一篇文章:第一个 AI Agent 项目:从零搭建 AI 音频创作助手(入门篇)这篇文章更多介绍进阶篇添加的功能。

京东科技开发者 545

【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手(入门篇)

这个项目让我第一次把前端、后端、AI Agent 三块内容独立串联起来。技术选型上每一块都是当下主流的方案,实际跑通整条链路之后对"全栈"有了更具体的感受。框架版本要锁定:LangChain 这类快速迭代的框架,版本差异带来的问题远比你想象的多先跑通链路,再追求完美:新手阶段能把用户输入 → AI 处理 → 结果展示这条链跑通,比深入某个技术点更有价值多看官方文档:相比博客和教程,官方文档更新及时,特别是 LangGraph 这种新框架后续计划继续迭代,欢迎交流!

京东科技开发者 602

ICLR 2026 | 面向序列决策的贝叶斯集成方法

在这篇工作中,我们关注的是。

京东科技开发者 294

ICML 2026|NaviAgent:面向 Oxygen 智能体的可扩展工具编排

大型语言模型(LLM)越来越多地作为函数调用代理,调用外部工具来处理超出其静态知识范围的任务。然而,它们通常一次只调用一个工具,缺乏对任务结构的全局视图。由于工具之间往往相互依赖,这会导致错误累积和可扩展性差,尤其是在扩展到数百或数千个工具时。为了解决这些局限性,我们提出了NaviAgent,一种显式的双层架构,它通过基于图的工具关系建模将任务规划与工具执行解耦。在规划层,基于 LLM 的代理决定是直接响应、澄清意图,还是检索并执行一个独立于工具间复杂性的工具链。

京东科技开发者 383

实测 9 款 AI 架构图工具:从 Mermaid 美化到 GPT-Image2,一份选型清单

支持 5 类图(架构图、工作流图、时序图、数据流图、生命周期/状态机图),主题 4 种(Classic、Signal Flow、Blueprint、Editorial)+ 3 种色彩模式(Dark / Light / System Auto),输出格式含 HTML、PNG、SVG、WebM、Share Card PNG、JSON IR。它把提示词图库、图像生成案例、参考图编辑流程和命令行工具整合在一起,让 AI Agent 能直接调用 GPT Image 2 模型完成文生图、图生图、参考图编辑等任务。

京东科技开发者 895

别再守着 Claude Code 了——学会指挥它自主干活

回到开头那句:别再一句一句地喂它、然后守着屏幕。真正的用法是——把一件事想清楚、划好边界、给它一个能自我验证的目标,然后交出去。你会发现,省下来的时间不是一点半点。把重复的判断和执行还给机器,把真正需要你的判断留给自己。

京东科技开发者 584

CVPR 2026|Oxygen XR 团队在原生鱼眼高斯泼溅技术上带来新突破

京东零售技术团队联合清华大学提出DirectFisheye-GS框架,创新性地将鱼眼相机模型原生嵌入3D高斯泼溅管线,通过跨视角联合优化策略直接处理鱼眼图像,避免了传统去畸变预处理导致的信息损失。该方案在多个公开数据集上达到SOTA性能,有效解决了单视角优化引起的几何畸变和视角不一致问题,同时保持与现有3DGS工具链的兼容性。技术已应用于电商3D重建和4D体积视频等场景,显著提升了沉浸式体验。相关论文被CVPR 2026接收,为宽视场场景重建提供了高效解决方案。

京东科技开发者 353

拆解海博 AI-Native 落地保障:Harness、双 Loop、知识库与技能自主迭代实践

海博研发RoadMap 的落地,彻底终结了 AI 编程“对话式玄学”的旧模式,真正落地了AI Native 研发工程化。它证明 AI 规模化赋能研发的核心,不在于堆砌大模型能力,而在于用工程思维重构 AI 的工作范式:用结构化流程替代随机对话,用固化规则约束模型行为,用持续沉淀实现能力复利,用分层架构支撑规模化落地。

京东科技开发者 633

让 AI 快速「读懂」你的代码仓:Joy-Code-Graph 云端图谱服务的三次进化

代码知识图谱不是要取代 AI 的智能,而是要补齐它对代码全局关系的认知盲区。当 AI 能一眼看清「谁调用了谁、改动会波及哪里」,它写出的代码才真正靠谱;当这份认知安全地留在自己手里、随时被 Joygen 调用、并且永久沉淀在代码仓里——AI 编程的体验,才算真正上了一个台阶。这,就是我们做 Joy-Code-Graph 云端服务的初衷。能力共享,数据自主;一次建图,长期受用。

京东科技开发者 751

【Coding生态】从代码托管到 AI 能力底座:与Coding一起共建 AI 研发生态

前两种框架能够很好地支撑对话式能力和预设流程式能力。它们适合基于提示词或预设工作流完成研发问答、知识查询、流程编排等任务。但在更复杂的研发场景中,仅仅“回答问题”还不够。当用户业务逻辑复杂时,一旦场景进入真实落地阶段,例如下载代码、修改文件、执行命令、安装依赖、运行测试、读取完整代码文件,或者与自有系统联动、将结果渲染到 Coding 页面、推送到京 ME,AI 应用需要的不只是知识能力,还需要真实的代码库操作能力和独立可执行环境。

京东科技开发者 768

GPT-5.6、ChatGPT Work 与 Codex 更新食用指南

三档模型当前都提供 1,050,000 tokens 上下文、922,000 tokens 最大输入和 128,000 tokens 最大输出,接受文本与图像输入、输出文本,并支持 web search、file search、Computer Use、MCP、Tool Search、Skills、Apply Patch 等工具。Work 可以读取你提供的文件,通过插件连接 Slack、Google Drive、SharePoint、邮箱、日历、CRM 或项目系统,调用获批工具,并在执行中展示进度。

京东科技开发者 860

把业务流程沉淀成高质量 Skill 的实践路径

使用Record & Replay创建的Skill,尽管创建流程简单,但它的本质实际是让 AI 去操作页面,这不但有可能影响我们对电脑的操作,还有可能无法稳定复现。创建Skill时,可以使用 Codex 的 Record & Replay 插件,把一次手工操作录下来,再让 AI 生成可复用流程。刚开始切换时,很容易遇到接口报错,可以让Codex排查原因进行优化,直到可以稳定执行;补充相应业务背景,说明这件事为什么要做,谁会用,输入从哪来,输出给谁看。电脑休眠、关机、软件没开时,本地任务可能无法执行。

京东科技开发者 835

政企前台AI-Agent的架构实践:打造可复用的 AI 服务平台-【灯塔计划】

不止是爆火的龙虾,还是各种Skills,MCP、Agent,脱离的具体的场景谈方案,最后只会是一场空。这一套可复用的 AI 服务平台,截止目前,通过前后端对接规范文档,多项能力已快速接入到各个场域的不同端平台,大幅降低研发成本, 并且各业务线新场景的接入从原来的 “开发驱动” 转为 “配置驱动”,接入周期从天维度缩短至小时级。

京东科技开发者 860

京东 Oxygen xLLM 大模型推理引擎正式捐赠开放原子开源基金会,共建国产 AI Infra 生态

下一阶段 AI 基础设施的核心命题,不是简单的算力堆叠,而是让工程本身变得智能——让调度系统能自主感知负载特征并动态优化,让推理引擎能根据模型结构和硬件特性自动生成最优执行方案,让整个 AI 工程链路具备自感知、自决策、自优化的能力。未来,Oxygen xLLM 的项目官网、社区沟通渠道及微信社区将陆续向开发者开放,诚邀芯片厂商、模型厂商、云厂商、企业用户及个人开发者共同参与,让国产大模型推理引擎在开放生态中加速成长。硬件层,原生支持 GPU / NPU / MLU 多类 AI 芯片,深度适配国产芯片。

京东科技开发者 1000

【京东云】全球首个!京东全栈开源JoyAI-VL-Interaction,让大模型从“一问一答”走向“边看边说”

开源一套系统,而不只是一个模型在实时视频流中,JoyAI-VL-Interaction每秒都会做一次判断,比如,继续观察、保持沉默,发现关键事件、主动回应,遇到复杂任务,交给后台Agent处理。一个好的AI 助手,不应该一直打扰用户,而应该知道什么时候该出现,什么时候该安静,以及什么时候自己解决,什么时候交由agent解决。深耕零售、物流、健康、工业等实体产业二十余年,京东拥有全球领先的物理世界运营网络,覆盖仓储、配送、门店、直播、客服、售后海量真实场景,每天都在发生人、货、场的实时互动。

京东科技开发者 1029
上一篇: 搜索EE场景排序链路升级
下一篇: 风险洞察之事件总线的探索与演进
京东云开发者
京东云开发者 企业官方账号 企业官方账号
博客等级 码龄8年 1万+粉丝 2139原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值