Log4j2异步打印可变对象的问题

现象

应用代码如下:

        Test test = new Test();
        test.setA(1);
        test.setB("1");
        log.info("before modification: {} \t ",test);
        test.setA(2);
        test.setB("2");
        log.info("after modification: {} \t ",test);

问题应用的日志控制台输出和文件输出为:

2023-11-18 20:45:32.391 INFO  [main]  before modification: ExpressServerApplication.Test(a=2, b=2) 	 
2023-11-18 20:45:32.391 INFO  [main] after modification: ExpressServerApplication.Test(a=2, b=2) 	 

修改前后打的日志都是改之后的内容a=2, b=2。

但是同样的依赖版本和Log4j2配置 新起了一个Spring Boot 应用进行自测,日志输出确又是正常的:

2023-11-18 21:19:28.750 INFO  [main] before modification: DemoLoggingApplication.Test(a=1, b=1) 	 
2023-11-18 21:21:21.029 INFO  [main] after modification: DemoLoggingApplication.Test(a=2, b=2) 	 

pom依赖和版本如下:


<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.7</version>
</dependency>
        // 异步日志支持
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.0</version>
</dependency>
        // 配置文件是YAML或YML时需添加该依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

日志门面使用Slf4j;日志实现使用Log4j2,使用混合异步方式,其配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <property name="baseLogDir">./app/log</property>
        <property name="logPattern">%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%logger{0}:%line][%thread] %X{logger_id} -
            [TID: %X{EagleEye-TraceID}] %msg%n
        </property>
    </Properties>

    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="${logPattern}"/>
            <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
        </Console>

        <RollingFile name="business" fileName="${baseLogDir}/app.log"
                     filePattern="${baseLogDir}/main/%d{yyyy-MM-dd}/app-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="${logPattern}"/>
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--            滚动策略 -->
            <Policies>
                <!--                时间间隔-->
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="200M"/>
            </Policies>

            <DefaultRolloverStrategy max="99999">
                <Delete basePath="${baseLogDir}/main" maxDepth="3">
                    <IfFileName glob="app-20*"/>
                    <IfLastModified age="30d"/>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

        <RollingFile name="error" fileName="${baseLogDir}/error.log"
                     filePattern="${baseLogDir}/error/%d{yyyy-MM-dd}/error-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="${logPattern}"/>
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="200M"/>
            </Policies>

            <DefaultRolloverStrategy max="99999">
                <Delete basePath="${baseLogDir}/error" maxDepth="3">
                    <IfFileName glob="error-20*"/>
                    <IfLastModified age="30d"/>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </Appenders>

    <Loggers>
        <AsyncRoot level="info" includeLocation="true">
            <AppenderRef ref="console"/>
            <AppenderRef ref="business"/>
            <AppenderRef ref="error"/>
        </AsyncRoot>
    </Loggers>
</Configuration>

原因剖析

异步基本原理

Log4j2异步日志基于 LMAX Disruptor 实现,借助该并发框架,在多线程场景下,Asynchronous Loggers 相比于Log4j1.x和Logback在吞吐量上提升18倍且在延迟上低了几个数量级。

首先简单介绍一下 Disruptor 框架。

Disruptor

LMAX是一家交易平台,交易系统需要达到 低延时高吞吐 两个目标。而阻碍Java系统以上目标的两个原因:

  • CPU缓存行失效
  • Locks需要进入内核态,代价大

由此诞生无锁的Disruptor并发框架。

Disruptor 借助Ring Buffer数据结构,Ring Buffer用于存放线程间通信的内容.

详见:
高性能队列——Disruptor
Log4j 2 Async Logging 的原理和实现

Log4j2异步日志

异步日志的优点:

  • 高吞吐
  • log方法耗时短,低延时

异步日志的缺点:

  • 异步打日志时错误处理

  • 打印可变内容可能打印错误内容:In some rare cases, care must be taken with mutable messages. Most of the time you don’t
    need to worry about this (这句话不合事实) . Log4 will ensure that log messages like logger.debug(“My object is {}”, myObject) will use the state of the myObject parameter at the time of the call to logger.debug(). The log
    message will not change even if myObject is modified later. It is safe to asynchronously log mutable objects because most Message implementations built-in to Log4j take a snapshot of the parameters. There are some exceptions however: MapMessage and StructuredDataMessage are mutable by design (事实还包含ParameterizedMessage): fields can be added to these messages after the message object was created. These messages should not be modified after they are logged with asynchronous loggers or asynchronous appenders; you may or may not see the modifications in the resulting log output. Similarly, custom Message implementations should be designed with asynchronous use in mind, and either take a snapshot of their parameters at construction time, or document their thread-safety characteristics.

  • 不利于CPU资源少的运行环境,因为需要额外后台线程进行磁盘IO

  • 如果打印日志比刷到磁盘还要快时,会导致队列排满,此时默认策略为使用当前线程执行append操作(类似线程池的CallerRun拒绝策略)。

详见:Asynchronous Loggers for Low-Latency Logging

现象原因

经过调试发现问题应用和自测应用传入Ring Buffer的LoggerEvent类型不同:

  • 问题应用:Log4jLogEvent
  • 自测应用:MutableLogEvent

Log4j2是否开启LocalThreads配置项影响了LogEventFactory的实现类,从而生成了不同的LoggerEvent
。关键代码就在org.apache.logging.log4j.util.Constants中,:

package org.apache.logging.log4j.util;

public final class Constants {
    // 是否是WEB,问题应用是WEB,自测应用不是WEB,
    public static final boolean IS_WEB_APP = PropertiesUtil.getProperties().getBooleanProperty("log4j2.is.webapp", isClassAvailable("javax.servlet.Servlet"));
    // 问题应用关闭了ThreadLocals,自测应用开启了ThreadLocals
    public static final boolean ENABLE_THREADLOCALS;
    // ...
    static {
        ENABLE_THREADLOCALS = !IS_WEB_APP && PropertiesUtil.getProperties().getBooleanProperty("log4j2.enable.threadlocals", true);
    }
}

是否开启ThreadLocals直接影响了LoggerEventFacotry的实现类:

package org.apache.logging.log4j.core.config;
public class LoggerConfig extends AbstractFilterable {
       // ...
       static {
       // 是否指定了Log4jLogEventFactory
        String factory = PropertiesUtil.getProperties().getStringProperty("Log4jLogEventFactory");
        if (factory != null) {
            try {
                Class<?> clazz = LoaderUtil.loadClass(factory);
                if (clazz != null && LogEventFactory.class.isAssignableFrom(clazz)) {
                    LOG_EVENT_FACTORY = (LogEventFactory)clazz.newInstance();
                }
            } catch (Exception var2) {
                LOGGER.error("Unable to create LogEventFactory {}", factory, var2);
            }
        }
        // 没有指定,则看ThreadLocals是否开启
        // 开启则 ReusableLogEventFactory,会生成 MutableLogEvent,如问题应用
        // 关闭则 DefaultLogEventFactory,会生成 Log4jLogEvent,如自测应用
        if (LOG_EVENT_FACTORY == null) {
            LOG_EVENT_FACTORY = (LogEventFactory)(Constants.ENABLE_THREADLOCALS ? new ReusableLogEventFactory() : new DefaultLogEventFactory());
        }

    }
}

在自测应用中禁用ThreadLocals,复现了错误打印可变对象内容的现象。

由此,产生3个疑问:

  1. 开启Threadlocal时的ReusableLogEventFactory有什么优势?
  2. 为什么WEBAPP必须关闭ThreadLocals?
  3. 为什么从Log4jLogEvent打印可变对象的日志会错?
Garbage-free Steady State Logging

之前的许多日志框架会生成很多临时对象,比如log event objects, Strings, char arrays, byte arrays,这会造成GC压力。针对GC问题,2.6版本的Log4j2后提供2个模式:

  • garbage free mode: objects and buffers are reused and no temporary objects are allocated as much as possible
  • low garbage mode: not completely garbage free but does not use ThreadLocal fields, it’s the default mode when Log4j
    detects it is running in a web application.

所以ReusableLogEventFactory利用ThreadLocals提供LogEvent的重用,避免频繁GC。

WEBAPP的Log4j2必须禁用ThreadLocals

To avoid causing memory leaks, Log4j will not use these ThreadLocals when it detects that it is used in a web
application (when the javax.servlet.Servlet class is in the classpath, or when system property log4j2.is.webapp is set
to “true”).

WEBAPP能在不重启Servlet容器的情况下重新加载应用,如果使用ThreadLocals,共用的线程池的线程的ThreadLocals会引用老版本的LogEvent,这样LogEvent不会被GC到,导致内存泄漏。

如果保证线程池不会被公用且重新加载应用时线程池也会关闭再重启,就可以开启Garbage-free

详见:
Why WEBAPP not using ThreadLocals
why log4j gc free logging is not suitable for webapp

关于这里的ThreadLocal,可以看下京东的技术文章:记一次疑似JVM内存泄漏的排查过程

格式化日志内容的时机

org.apache.logging.log4j.core.LogEvent中会携带org.apache.logging.log4j.message.Message信息,比如:

  • WEBAPP中Log4jLogEvent携带ParameterizedMessage
  • MutableLogEvent携带ReusableParameterizedMessage

不同类型的Message对于占位符{}的字符串填充对象的时机不同,填充时占位符的对象内容才是真正被log到appender的内容。

  • 对于ReusableParameterizedMessage,在org.apache.logging.log4j.core.impl.ReusableLogEventFactory#createEvent
    方法中调用MutableLogEvent#setMessage时会填充{},该过程在进入RingBuffer之前由log的线程在log.info方法调用中 同步进行。
  • 对于ParameterizedMessage,填充{}却是由Disruptor的handler线程池(只有一个线程,日志多时处理LogEvent延迟大)
    来完成,如下图所示,在handler填充{}对象时,log的线程可能已经把该对象改变内容了。

在这里插入图片描述

因此,WEBAPP打印日志时,传入的对象应为"不可变对象":

  • 要么类似String这种类型(String.valueOf(…)或转JSON)
  • 要么确保对象后续不再变更。

org.apache.logging.log4j.message.Message的注释说明也应证了这一点:

Note: Message objects should not be considered to be thread safe nor should they be assumed to be safely reusable even
on the same thread. The logging system may provide information to the Message objects and the Messages might be queued
for asynchronous delivery. Thus, any modifications to a Message object by an application should by avoided after
the Message has been passed as a parameter on a Logger method.

或者自定义LogEventFacotry,在createEvent时同步填充{}

  1. 创建自定义LogEventFacotry
package com.example.demologging;

import java.lang.reflect.Field;
import java.util.List;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.core.impl.LogEventFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.PropertiesUtil;

/**
 * {@link AbstractStringLayout}
 * @author: baotingyu
 * @date: 2023/11/19
 **/
public class SyncFormatLog4jLogEventFactory implements LogEventFactory {
    protected static final int MAX_STRING_BUILDER_SIZE = Math.max(1024, size("log4j.layoutStringBuilder.maxSize", 2048));
    private static final ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();

    private static int size(String property, int defaultValue) {
        return PropertiesUtil.getProperties().getIntegerProperty(property, defaultValue);
    }
    protected static StringBuilder getStringBuilder() {
        StringBuilder result = threadLocal.get();
        if (result == null) {
            result = new StringBuilder(1024);
            threadLocal.set(result);
        }

        trimToMaxSize(result);
        result.setLength(0);
        return result;
    }

    protected static void trimToMaxSize(StringBuilder stringBuilder) {
        if (stringBuilder.length() > MAX_STRING_BUILDER_SIZE) {
            stringBuilder.setLength(MAX_STRING_BUILDER_SIZE);
            stringBuilder.trimToSize();
        }

    }
    public LogEvent createEvent(String loggerName, Marker marker, String fqcn, Level level, Message data, List<Property> properties, Throwable t) {
        if(data instanceof ParameterizedMessage){
            try {
                // ThreadLocal重用StringBuilder
                StringBuilder builder = getStringBuilder();
                ((ParameterizedMessage) data).formatTo(builder);
                Field formattedMessage = data.getClass().getDeclaredField("formattedMessage");
                formattedMessage.setAccessible(true);
                formattedMessage.set(data,builder.toString());
            } catch (NoSuchFieldException | IllegalAccessException e) {
                System.err.println(this.getClass().getSimpleName()+"fail to createEvent:"+e.getMessage());
            }

        }
        return new Log4jLogEvent(loggerName, marker, fqcn, level, data, properties, t);
    }
}

  1. 指定自定义LogEventFacotry:在resources目录下创建log4j2.component.properties
    文件(log4j2按文件名称加载),内容为Log4jLogEventFactory=com.example.demologging.SyncFormatLogEventFactory

Java日志体系

顺便记录一下Java日志体系。

最初,Java并没有日志模块,都是通过System.outSystem.err输出。1996年,Ceki Gülcü 主导编写了 Log4j
日志框架;2002年,Java官方推出了一个日志模块JUL(Java Util Logging)
,但是这时候Log4j已经成了事实日志框架,用的人更多;Apache在日志框架上抽象了一套标准,即一套日志门面 JCL (Jakarta Commons Logging)JCL
日志门面可以兼容Log4j日志实现和JUL日志实现;但是JCL问题很多,Ceki
Gülcü再次发功,另写了一套日志门面Slf4j(Simple Logging Facade for Java),但是这个协议如何兼容之前的Log4jJUL
呢?通过桥接包做适配,那之前用JCL门面的怎么用Slf4j门面呢?通过桥接包做适配;接入日志实现还得加桥接包,太麻烦,不如自己搞个新的日志实现,于是Ceki
Gülcü又发功了,写了日志实现 Logback,直接接入Slf4j,且针对之前的Log4j进行了改进;Apache看Logback
改进很多,也“借鉴”Logback开发了新的日志实现 Log4j2,更甚者的Log4j2也搞了分离的设计,分化成log4j-api
log4j-corelog4j-api也是日志门面,log4j-core才是日志实现,包名没有2,我真服了。

在这里插入图片描述

以上,我们在选择日志时,可以引入多个日志门面但只能引入一个日志实现。代码中打日志都用日志门面的api打印,而日志配置文件根据你的日志实现不同而不同。

桥接包实现主要分为两部分(以log4j-slf4-impl为例)

  • LoggerFactory: org.slf4j.ILoggerFactory 生成 Log4j2的 org.apache.logging.log4j.Logger
  • Logger: org.slf4j.Logger 桥接到 org.apache.logging.log4j.Logger

桥接类分别为:

  • LoggerFactory: log4j-slf4-impl包中的org.apache.logging.slf4j.Log4jLoggerFactory ,该工厂直接返回 log4j-slf4-impl
    org.apache.logging.slf4j.Log4jLogger
  • Logger: log4j-slf4-impl包中的org.apache.logging.slf4j.Log4jLogger

org.apache.logging.log4j.Logger,使用组合的方式在字段中组合了Log4j2的org.apache.logging.log4j.spi.ExtendedLogger
,很简单的实现。

org.apache.logging.slf4j.Log4jLoggerFactory作为桥接类,就有些技巧了,其UML图如下:

在这里插入图片描述

注意,上层的两个接口函数签名(函数名+参数列表)是一样的,也就是最下面的Log4jLoggerFactory只要实现该方法,就能满足两个接口的接口方法调用,而该方法又没有直接在Log4jLoggerFacotry中实现,而是在父抽象类AbstractLoggerAdapter中实现。

参考:Java日志系统历史从入门到崩溃

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/730315.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

怎么添加网页到桌面快捷方式?

推荐用过最棒的学习网站&#xff01;https://offernow.cn 添加网页到桌面快捷方式&#xff1f; 很简单&#xff0c;仅需要两步&#xff0c;接下来以chrome浏览器为例。 第一步 在想要保存的网页右上角点击设置。 第二步 保存并分享-创建快捷方式&#xff0c;保存到桌面即可…

使用VisualBox+Vagrant搭建Centos虚拟机环境

1.下载并安装VisualBox&#xff1b; 2.下载并安装Vagrant; 3.打开cmd窗口&#xff0c;执行命令vagrant init centos/7&#xff0c;初始化centos环境&#xff0c;该步骤受网络带宽影响&#xff0c;可能挂级30分钟到1个小时&#xff1b; 4.启动虚拟机&#xff1a;vagrant up&…

C# yolov8 OpenVINO 同步、异步接口视频推理

C# yolov8 OpenVINO 同步、异步接口视频推理 目录 效果 项目 代码 下载 效果 同步推理效果 异步推理效果 项目 代码 using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Windows.Form…

慢阻肺患者为何容易营养不良?朗格力教你轻松改善

#肺科营养#朗格力#班古营养#复合营养素#肺部营养#肺部健康# 慢阻肺是我国常见的、高患病率的慢性呼吸系统疾病,会对肺结构和功能产生影响,从而引起各种不良反应,其中营养不良是常见并发症之一。慢阻肺为什么会发生营养不良?营养不良又是怎么伤害慢阻肺的呢?为什么像班古精准…

鸿蒙 登录界面示例

1.b站登录界面 我的b站教学视频&#xff1a;https://www.bilibili.com/video/BV1LQgQexEGm/?spm_id_from333.999.0.0&vd_sourced0ea58f1127eed138a4ba5421c577eb1 最终实现效果&#xff1a; 需要准备2张图片&#xff0c;分别是向下和向右边的图标 代码&#xff1a; En…

(2024,Vision-RWKV,线性复杂度双向注意力,四向标记移位)通过类似 RWKV 的架构实现高效且可扩展的视觉感知

Vision-RWKV: Efficient and Scalable Visual Perception with RWKV-Like Architectures 公和众与号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 2. 特征聚合机制 3. Vision-RWKV 3.…

一问搞懂Linux信号【上】

Linux信号在Linux系统中的地位仅此于进程间通信&#xff0c;其重要程度不言而喻。本文我们将从信号产生&#xff0c;信号保存&#xff0c;信号处理三个方面来讲解信号。 &#x1f6a9;结合现实认识信号 在讲解信号产生之前&#xff0c;我们先做些预备的工作。 现实生活中信号…

2024.6最最新版MySQL数据库安装(保姆级教程,不懂你捶我)

1.MySQL数据库下载 1.打开MySQL官网 如下页面 2.下翻网页到最底部,找到Download,点击第一个MySQL Community Server 3.选择自己需要的版本以及系统的MySQL: 4.跳转页面会有一个登录/注册页面,这里我们不鸟他,直接开始下载 2.MySQL数据库安装 1.双击我们刚刚下载的安装包 2.勾…

音乐管理系统

摘 要 现如今&#xff0c;在信息快速发展的时代&#xff0c;互联网已经成了人们在日常生活中进行信息交流的重要平台。看起来&#xff0c;听歌只是一种消遣和消遣&#xff0c;其实&#xff0c;只要你选对了曲子&#xff0c;就会产生许多不同的作用。音乐能舒缓身心&#xff0c…

上海交大阿里巴巴推出虚拟试衣新里程碑式工作——AnyFit:任意场景、任意组合!

文章链接&#xff1a;https://arxiv.org/pdf/2405.18172 工程链接&#xff1a;https://colorful-liyu.github.io/anyfit-page/ 今天和大家一起学习的是一种名为AnyFit的新型虚拟试穿系统&#xff0c;旨在解决现有技术在处理不同场景和服饰组合时出现的衣物风格不匹配和质量下…

量化系统--开源强大的qmt交易系统,提供源代码

经过的3天终于写完了qmt_trader的文档了开源直接使用我开源了全部源代码 文档地址 https://gitee.com/li-xingguo11111/qmt_trader 源代码from qmt_trader.qmt_trader import qmt_trader from qmt_trader.xtquant.xttype import StockAccountfrom qmt_trader.xtquant import …

opencascade AIS_InteractiveContext源码学习2

AIS_InteractiveContext 前言 交互上下文&#xff08;Interactive Context&#xff09;允许您在一个或多个视图器中管理交互对象的图形行为和选择。类方法使这一操作非常透明。需要记住的是&#xff0c;对于已经被交互上下文识别的交互对象&#xff0c;必须使用上下文方法进行…

C语言练习03-字符串

一、遍历字符 #include<stdio.h>int main() {char str[100];//录入字符串printf("请输入一串字符&#xff1a;\n");scanf("%s",str);//遍历字符串char* p str;while(1){char c *p;if(c \0){//如果遍历到结束标记&#xff0c;则循环结束break;}//…

雷池社区版自动SSL

正常安装雷池&#xff0c;并配置站点&#xff0c;暂时不配置ssl 不使用雷池自带的证书申请。 安装&#xff08;acme.sh&#xff09;&#xff0c;使用域名验证方式生成证书 先安装git yum install git 或者 apt-get install git 安装完成后使用 git clone https://gitee.com/n…

应用案例 | 冷藏集装箱基于云的WiFi无线温度监测系统COMET Cloud

一、集装箱的作用和分类 集装箱运输是国际贸易货物多式联运过程中的重要运输方式。由于集装箱运输具有标准化高、密封性好&#xff0c;破损率低、集约化、规模化、班轮化、成本低、质量好等优点&#xff0c;大大提高了货物运输的安全和效率。 集装箱种类很多&#xff0c;按所…

C++类基本常识

文章目录 一、类的默认方法二、类的成员变量初始化1 类的成员变量有三种初始化方法&#xff1a;2 成员变量初始化顺序3 const和static的初始化 三、C内存区域四、const和static 一、类的默认方法 C的类都会有8个默认方法 默认构造函数默认拷贝构造函数默认析构函数默认重载赋…

mongodb嵌套聚合

db.order.aggregate([{$match: {// 下单时间"createTime": {$gte: ISODate("2024-05-01T00:00:00Z"),$lte: ISODate("2024-05-31T23:59:59Z")}// 商品名称,"goods.productName": /美国皓齿/,//订单状态 2:待发货 3:已发货 4:交易成功…

最火AI角色扮演流量已达谷歌搜索20%!每秒处理2万推理请求,Transformer作者公开优化秘诀

卡奥斯智能交互引擎是卡奥斯基于海尔近40年工业生产经验积累和卡奥斯7年工业互联网平台建设的最佳实践&#xff0c;基于大语言模型和RAG技术&#xff0c;集合海量工业领域生态资源方优质产品和知识服务&#xff0c;旨在通过智能搜索、连续交互&#xff0c;实时生成个性化的内容…

springboot3 连接 oceanbase + logproxy数据同步到redis

我这用的是 社区版的 单机&#xff0c; rocky liunx 安装oceanbase 注意事项&#xff1a; logproxy 是 CDC 模式 &#xff0c; springboot 可以直接订阅 canal 是 binlog模式&#xff0c; canal 订阅 logproxy&#xff0c; springboot 订阅 canal logproxy 也可以转 bi…

何在 Vue3 中使用 Cytoscape 创建交互式网络图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 Vue.js 中加载 Cytoscape.js 的技术实现 应用场景 Cytoscape.js 是一个用于创建交互式网络的可视化库。在生物信息学、社会网络分析和药物发现等领域中得到了广泛应用。 基本功能 本代码片段演示了如何在 V…