Linux百度网盘优化三板斧

thbcm阅读(213)

终端执行
cpulimit -e baidunetdisk -l 100进行限制,后面的数值越大,允许占用CPU越高。如果太小,又会降低下载速度,可以根据自己设备和带宽情况灵活设置。

这个指令会在后台持续监控,如果百度网盘退出仍会监测,大约每秒一次动态控制。

蚁群算法的原理及实现示例

thbcm阅读(236)

  蚂蚁在觅食时会释放信息素(Pheromone),其他蚂蚁通过感知信息素浓度选择路径,形成群体协作:

    
正反馈:路径上经过的蚂蚁越多,信息素浓度越高,吸引更多蚂蚁。

    
负反馈:信息素会随时间挥发,避免算法陷入局部最优。

    
概率选择:蚂蚁以一定概率选择路径,平衡探索(新路径)和利用(信息素高的路径)。

如何构建 AI 智能体(2025 完全指南)

thbcm阅读(335)

如何构建 AI 智能体(2025 完全指南)

AI 智能体革命才刚刚开始。虽然炒作是真实的,但机遇也是真实的。通过遵循本指南并避免常见陷阱,你今天就可以构建出能够交付真正价值的智能体,同时为即将到来的自主未来做好准备。

内容提要

AI 智能体是能够自主决策并采取行动以完成任务的系统。与聊天机器人不同,它们不遵循预定义的工作流程——它们会进行推理、规划、使用工具并动态适应。本指南将通过真实示例和代码,向你具体展示如何使用如 LangChain 和 AutoGen 等现代框架来构建可工作的智能体。


2025 年正被誉为”AI 智能体之年”,其在企业中的应用正在加速。微软 CEO 萨提亚·纳德拉称其为一根本性转变:”请将智能体视为 AI 时代的应用。”但问题在于——大多数教程向你展示的都是伪装成智能体的聊天机器人,或者更糟的是,那些在演示中有效但在生产环境中失败的复杂系统。
在构建了多个生产级智能体并分析了最新框架之后,我将确切地向你展示如何创建真正有效的 AI 智能体。不掺水分,不搞噱头——只有由真实代码和经过验证的架构支持的实践实现细节。

AI 智能体与聊天机器人有何不同?

让我们立刻澄清这一点。智能体没有预定义的工作流程——它不仅仅是遵循第一步、第二步、第三步。相反,它会在不确定的步骤数量中动态做出决策,并根据需要进行调整。

特性 传统聊天机器人 AI 智能体
决策能力 遵循预定义规则 自主决策
工作流程 固定的、线性的步骤 动态的、自适应的规划
记忆 仅限于会话 跨任务持久化
工具使用 无或硬编码 动态选择和使用工具
错误处理 失败或请求帮助 尝试替代方法

真实示例: 要求一个聊天机器人”预订下周二飞往纽约的航班”,它要么会失败,要么会向你询问更多信息。而一个智能体会检查你的日历、搜索航班、比较价格,甚至处理预订——根据发现的情况调整其方法。

每个 AI 智能体所需的 5 个核心组件

基于广泛的研究和生产部署,每个可工作的 AI 智能体都需要以下五个组件:

1. 大语言模型 – 大脑

LLM 充当推理引擎。在 2025 年,你有多种优秀选择(参见我们的详细比较):

  • Claude 4 Opus: 最适合复杂推理和扩展思考
  • GPT-4.1: 在编码和工具使用方面表现出色,拥有 100 万令牌上下文
  • Gemini 2.5 Pro: 强大的多模态能力

专业提示: 不要默认使用最昂贵的模型。对于智能体任务,每百万令牌 2 美元的 GPT-4.1-mini 通常表现不俗,尤其是在结合良好提示的情况下。

2. 记忆系统 – 上下文

由于 LLM 默认是无状态的,你需要管理它们的历史和上下文。现代框架提供几种记忆类型:

# 示例:LangChain 记忆实现
from langchain.memory import ConversationSummaryBufferMemory
memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=2000,
    return_messages=True
)
# 智能体现在可以跨多次交互进行记忆

记忆类型:

  • 缓冲区记忆: 存储原始对话历史
  • 摘要记忆: 压缩长对话
  • 实体记忆: 跟踪特定实体及其属性
  • 知识图谱记忆: 构建概念间的关系

3. 工具 – 双手

工具允许你的智能体与外部世界交互。正确的工具配置与提示工程同等重要。

# 示例:定义一个用于网络搜索的工具
from langchain.tools import Tool
def search_web(query: str) -> str:
    """搜索网络以获取最新信息。"""
    # 此处为实现代码
    return search_results
web_search_tool = Tool(
    name="WebSearch",
    func=search_web,
    description="搜索网络以获取最新信息。当你需要最新数据时使用。"
)

关键点: 你的工具描述直接影响智能体性能。要具体说明何时以及如何使用每个工具。模糊的描述会导致工具选择不当。

4. 规划系统 – 策略

智能体必须能够提前规划和思考。2025 年最成功的方法是 ReAct 范式(推理 + 行动)

# ReAct 风格智能体循环
while not task_complete:
    # 1. 观察当前状态
    observation = get_current_state()
   
    # 2. 思考下一步行动
    thought = llm.think(f"给定 {observation},我下一步该做什么?")
   
    # 3. 决定行动
    action = llm.decide_action(thought, available_tools)
   
    # 4. 执行行动
    result = execute_action(action)
   
    # 5. 反思结果
    reflection = llm.reflect(result)
   
    # 更新状态并继续

5. 执行循环 – 引擎

执行循环负责协调一切。现代框架以不同方式处理此问题:

  • LangChain/LangGraph: 使用基于图的执行模型
  • AutoGen: 实现事件驱动的参与者模型
  • CrewAI: 专注于基于角色的智能体协作

逐步指南:构建你的第一个可工作智能体

让我们构建一个能够研究主题并撰写报告的实用智能体。此示例展示了所有五个核心组件的实际运作。

步骤 1:设置环境

# 安装所需的包
pip install langchain langchain-openai tavily-python
# 设置环境变量
export OPENAI_API_KEY="你的密钥"
export TAVILY_API_KEY="你的密钥"

步骤 2:初始化核心组件

from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain.memory import ConversationBufferMemory
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.tools import Tool
from langchain import hub
# 1. 初始化 LLM
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
# 2. 设置记忆
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)
# 3. 配置工具
search = TavilySearchResults(max_results=5)
tools = [
    Tool(
        name="Search",
        func=search.run,
        description="搜索关于任何主题的最新信息。返回相关结果。"
    )
]
# 4. 加载 ReAct 提示(处理规划)
prompt = hub.pull("hwchase17/react")

步骤 3:创建智能体

# 创建 ReAct 智能体
agent = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=prompt
)
# 5. 设置执行循环
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    verbose=True,  # 查看智能体的思考过程
    handle_parsing_errors=True,
    max_iterations=10  # 防止无限循环
)

步骤 4:运行你的智能体

# 示例:研究并报告 AI 智能体
result = agent_executor.invoke({
    "input": "研究 2025 年 7 月 AI 智能体的最新发展,并撰写一份简要报告,重点介绍前 3 大趋势。"
})
print(result["output"])

成功关键: 该智能体将自主搜索多次,综合信息,并生成连贯的报告。它并非遵循脚本——而是根据发现的内容动态决定搜索什么。

真正提升性能的高级技巧

在分析了数千次智能体交互后,以下是真正能提高智能体性能的技巧:

1. 问题分解优于角色扮演

  • 无效的方法: 角色提示(例如,”你是一位专家研究员……”)对准确性影响甚微或没有影响。
  • 有效的方法: 要求智能体将问题分解为子任务:
decomposition_prompt = """
    将此任务分解为更小的步骤:
      1. 首先,识别关键组成部分
      2. 然后,分别处理每个组成部分
      3. 最后,综合结果
    任务:{task}
"""

2. 自我批评与反思

添加自我批评步骤能显著提高输出质量:

reflection_prompt = """
审查你之前的回应并识别:
1. 任何逻辑错误或不一致之处
2. 遗漏的重要信息
3. 可以更清晰的领域
之前的回应:{response}
"""

3. 上下文重于指令

上下文的重要性被严重低估。仅仅提供更多相关的背景信息,比复杂的提示技术更能提高性能:

# 效果较差
prompt = "写一份关于 AI 智能体的报告"
# 效果更好
prompt = """写一份关于 AI 智能体的报告。
上下文:AI 智能体是可以规划并执行任务的自主系统。
它们与聊天机器人的不同之处在于做出动态决策而非遵循脚本。
关键框架包括 LangChain、AutoGen 和 CrewAI。
该报告面向熟悉 AI 概念的技术读者。
"""

导致 AI 智能体失效的常见错误

以下是我反复看到的常见错误:

1. 无限循环且无限制

始终设置 max_iterations: 智能体可能陷入循环。设置合理的限制并实现超时处理。

2. 工具描述不清

# 差:描述模糊
Tool(name="search", description="搜索东西")
# 好:包含用例的具体描述
Tool(
    name="WebSearch",
    description="搜索网络以获取最新信息。用于:近期新闻、时事、事实数据、公司信息。返回 5 个最相关的结果。"
)

3. 忽略错误状态

智能体会遇到错误。要为它们做好计划:

try:
    result = agent_executor.invoke({"input": user_query})
except Exception as e:
    # 不要只是失败 - 帮助智能体恢复
    recovery_prompt = f"先前的操作因错误而失败:{e}。请尝试另一种方法。"
    result = agent_executor.invoke({"input": recovery_prompt})

4. 忽视令牌成本

智能体可能快速消耗令牌。需监控并优化:

  • 尽可能使用较小的模型(GPT-4.1-mini vs GPT-4.1)
  • 对长对话实施摘要记忆
  • 缓存工具结果以避免重复调用

生产就绪的智能体架构

对于生产系统,根据你的需求选择架构:

框架 最适合 架构 关键优势
LangChain + LangGraph 复杂的单一智能体 基于图的执行 模块化,工具丰富
AutoGen 多智能体系统 事件驱动的参与者 智能体协作
CrewAI 基于团队的工作流 基于角色的智能体 自然的团队动态
自定义 特定需求 你的选择 完全控制

LangChain + LangGraph 架构

LangChain 已发展成为单智能体系统的事实标准。2025 年 LangGraph 的加入带来了复杂的状态管理:

from langgraph.graph import StateGraph, State
from typing import TypedDict
class AgentState(TypedDict):
    messages: list
    current_task: str
    completed_tasks: list
# 定义图
workflow = StateGraph(AgentState)
# 为不同的智能体能力添加节点
workflow.add_node("researcher", research_node)
workflow.add_node("writer", writing_node)
workflow.add_node("reviewer", review_node)
# 定义流程
workflow.add_edge("researcher", "writer")
workflow.add_edge("writer", "reviewer")

AutoGen 多智能体架构

微软的 AutoGen 在你需要多个专业智能体协同工作时表现出色:

import autogen
# 定义专业智能体
researcher = autogen.AssistantAgent(
    name="Researcher",
    system_message="你是一名研究专家。查找并验证信息。"
)
writer = autogen.AssistantAgent(
    name="Writer",
    system_message="你是一名技术文档工程师。创建清晰、准确的内容。"
)
critic = autogen.AssistantAgent(
    name="Critic",
    system_message="你审查工作的准确性和清晰度。要有建设性但要彻底。"
)

可工作的 AI 智能体真实案例

让我们看看当今在生产环境中实际使用的智能体(查看更多真实可工作的 AI 智能体示例):

1. 客户服务智能体(电子商务)

该智能体自主处理完整的客户交互(在我们的客户服务自动化指南中了解更多):

  • 在数据库中检查订单状态
  • 处理退货和退款
  • 更新送货地址
  • 将复杂问题升级给人工处理
  • 关键创新: 根据客户需求动态选择使用多个专业工具(数据库查询、支付处理、运输 API)。

2. 代码审查智能体(软件开发)

自动审查拉取请求:

  • 分析代码变更
  • 运行安全扫描
  • 提出改进建议
  • 检查是否符合编码标准

3. 研究助手智能体(内容创作)

进行综合研究:

  • 搜索多个来源
  • 事实核查信息
  • 综合发现
  • 生成引用

AI 智能体的安全考量

关键警告: 基于智能体的 AI 系统比聊天机器人更容易受到攻击。随着智能体开始预订航班、发送邮件和执行代码,风险呈指数级增长。

基本安全措施

  • 工具权限: 为每个工具实施细粒度权限
  • 操作验证: 对不可逆操作要求确认
  • 提示注入防御: 验证并清理所有输入
  • 审计日志: 记录每个操作以确保可追溯性
  • 人工监督: 维持紧急停止开关和审批工作流
# 示例:安全的工具执行
def execute_with_permission(action, requires_approval=True):
    if requires_approval and action.risk_level == "high":
        approval = request_human_approval(action)
        if not approval:
            return "操作被安全策略拒绝"
   
    # 记录操作
    audit_log.record(action, user, timestamp)
   
    # 带超时执行
    return execute_with_timeout(action, timeout=30)

测试和调试 AI 智能体

测试智能体需要不同于传统软件的方法:

1. 基于场景的测试

# 测试各种场景
test_scenarios = [
    {
        "input": "预订明天飞往纽约的航班",
        "expected_tools": ["calendar_check", "flight_search", "price_compare"],
        "expected_outcome": "flight_options"
    },
    {
        "input": "取消我的订阅并退还上个月的费用",
        "expected_tools": ["account_lookup", "subscription_cancel", "refund_process"],
        "expected_outcome": "confirmation"
    }
]
for scenario in test_scenarios:
    result = agent_executor.invoke({"input": scenario["input"]})
    assert all(tool in result["tool_calls"] for tool in scenario["expected_tools"])

2. 调试工具

启用详细日志记录以查看智能体的决策过程:

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,  # 显示思考过程
    return_intermediate_steps=True  # 返回所有步骤
)

5 个即用型智能体系统提示

以下是为常见智能体类型准备的、经过实战检验的系统提示:

1. 研究智能体

你是一个可以访问网络搜索和文档分析工具的研究智能体。
对于每个研究任务:

  1. 将主题分解为关键问题
  2. 从多个来源搜索信息
  3. 通过交叉引用验证事实
  4. 将发现综合成连贯的摘要
  5. 为所有主张包含引用
    始终优先考虑近期信息和权威来源。

2. 客户支持智能体

你是一个客户支持智能体,帮助用户处理他们的账户和订单。

可用工具: order_lookup, refund_process, ticket_create, knowledge_base_search
指南:

  • 在访问账户信息前始终验证客户身份
  • 在升级前搜索知识库
  • 要有同理心并以解决方案为导向
  • 遇到以下情况升级至人工支持:法律问题、威胁或超出你工具范围的请求

切勿对你无法直接实现的功能做出承诺。

3. 数据分析智能体

你是一个专长于商业智能的数据分析智能体。
对于每个分析请求:

  1. 澄清业务问题
  2. 识别相关数据源
  3. 使用适当的统计方法执行分析
  4. 可视化关键发现
  5. 提供可操作的建议

始终在你的分析中注明数据局限性和置信水平。

4. 代码助手智能体

你是一个可以访问文件系统和执行工具的代码助手智能体。
能力:

  • 阅读和分析代码
  • 提出改进建议
  • 实施更改
  • 运行测试
  • 调试问题

切勿:

  • 未经明确许可删除文件
  • 修改系统文件
  • 执行可能有害的命令
  • 在代码中存储凭证

在进行重大更改前始终创建备份。

5. 内容创作智能体

你是一个专注于病毒式内容策略的内容创作智能体。

流程:

  1. 研究指定领域的趋势话题
  2. 分析成功的内容模式
  3. 生成多个内容创意
  4. 创建带有吸引点的详细内容
  5. 建议分发策略

关注真实性和价值,而非点击诱饵。

未来:AI 智能体的下一步是什么?

基于当前轨迹和内部知识,以下是将要发生的事情:

近期(未来 6 个月)

  • 视觉智能体: 能够查看并与 UI 交互的智能体
  • 语音优先智能体: 自然对话取代文本界面
  • 智能体市场: 针对特定行业的预构建智能体
  • 改进的安全性: 内置沙盒和权限系统

中期(2026 年)

  • 物理世界智能体: 与机器人技术集成
  • 监管框架: 为智能体行为设定法律边界
  • 智能体间经济: 智能体雇佣其他智能体
  • 个人 AI 操作系统: 管理整个数字生活的智能体

关键要点

构建真正有效的 AI 智能体需要理解五个核心组件:用于推理的 LLM、用于上下文的记忆、用于行动的工具、用于策略的规划以及一个健壮的执行循环。与聊天机器人的关键区别在于自主决策和动态适应。
从经过验证的框架开始,如用于单智能体的 LangChain 或用于多智能体系统的 AutoGen。专注于清晰的工具描述、适当的错误处理和全面的测试。最重要的是,记住上下文和问题分解比复杂的提示技巧更重要。

AI 智能体革命才刚刚开始。虽然炒作是真实的,但机遇也是真实的。通过遵循本指南并避免常见陷阱,你今天就可以构建出能够交付真正价值的智能体,同时为即将到来的自主未来做好准备。


【注】本文译自:How to Build AI Agents (Complete 2025 Guide) – Superprompt.com

posted @
2025-11-05 13:11 
码者无疆  阅读(
375)  评论(
1)   
收藏 
举报

使用数据库工具进行高效数据查询的 10 大 IntelliJ IDEA 快捷方式

thbcm阅读(312)

使用数据库工具进行高效数据查询的 10 大 IntelliJ IDEA 快捷方式

使用数据库工具进行高效数据查询的 10 大 IntelliJ IDEA 快捷方式

引言

在现代软件开发中,数据库操作是不可或缺的一部分。作为Java开发者,我们经常需要与各种数据库进行交互,编写和执行SQL查询。IntelliJ IDEA Ultimate版本内置了强大的Database Tools,它提供了一个专用的SQL查询控制台,允许开发者在不离开IDE的情况下,轻松修改和提取连接到Java应用程序的任何数据库中的数据。

查询控制台配备了SQL语句特定的代码片段库、代码补全、实时错误检测和各种实用快捷方式,所有这些功能都旨在帮助开发者保持高效的工作流程。本文将重点介绍10个最实用的快捷方式,它们能够显著提升您在IntelliJ IDEA中使用数据库工具进行数据查询时的效率。

正文内容

1. 打开新控制台:⌘⇧L | Ctrl+Shift+Q

当您需要在数据库工具窗口打开一个新的查询控制台时,只需选择一个数据源,然后按下这三个键:⌘⇧L(macOS)或Ctrl+Shift+Q(Windows/Linux)。这个快捷方式可以快速创建一个新的查询环境,让您立即开始编写SQL语句。

需要注意的是,这个快捷方式可能与Google Chrome的快捷方式冲突。如果遇到冲突,您可以通过系统设置的键盘快捷键选项将其停用。

2. 执行语句:⌘↩ | Ctrl+Enter

要快速执行整个SQL语句或选定的代码部分,请使用⌘↩(macOS)或Ctrl+Enter(Windows/Linux)快捷方式。这一功能对于测试查询片段或验证复杂SQL语句的特定部分特别有用。

当您选择了多个语句时,可以从”Statements”弹出菜单中选择要执行的特定语句。所选语句会在查询编辑器中突出显示,方便您查看和确认选择。

3. 调用意图操作:⌥↩ | Alt+Enter

IntelliJ IDEA会持续分析您的代码,提供优化建议,检测潜在问题并提供修复方案。要获取这些智能建议,只需按⌥↩(macOS)或Alt+Enter(Windows/Linux)。

这个功能特别强大,因为它不仅能识别语法错误,还能根据数据库结构提供智能建议,比如自动补全表名、列名,甚至能根据上下文建议更高效的查询方式。

4. 打开实时模板列表:⌘J | Ctrl+J

IntelliJ IDEA内置了丰富的SQL代码模板库,可以显著加快SQL查询的编写速度。按⌘J(macOS)或Ctrl+J(Windows/Linux)可以访问这些实时模板。

您还可以通过”Settings | Editor | Live Templates”添加自定义模板,只需按⌘N(macOS)或Ctrl+N(Windows/Linux)即可创建新模板。这对于团队中常用的标准查询模式特别有用。

5. 最近文件:⌘E | Ctrl+E

如果您不小心关闭了包含复杂SQL查询的标签页,不必担心。只需按⌘E(macOS)或Ctrl+E(Windows/Linux),就能从”Recent files”列表中快速重新打开文件。

这个功能对于在多个查询之间切换或找回意外关闭的重要查询非常实用,能节省大量重新编写查询的时间。

6. 重新格式化代码:⌥⌘L | Ctrl+Alt+L

整洁有序的代码对于高效开发至关重要。要在查询控制台中对选定的代码片段或整个文件进行重新格式化,请使用⌥⌘L(macOS)或Ctrl+Alt+L(Windows/Linux)快捷方式。

您可以通过”Settings | Editor | Code Style | SQL”自定义SQL格式化规则,设置适合您编码风格的语句、表、表达式等格式规则。

7. 从剪贴板历史记录粘贴:⇧⌘V | Ctrl+Shift+V

IntelliJ IDEA会保存您复制的所有代码或文本片段,在整个工作会话期间都可以访问。按⇧⌘V(macOS)或Ctrl+Shift+V(Windows/Linux)可以从剪贴板历史记录中选择并粘贴项目。

这对于需要在多个查询之间复制代码片段,或者需要重用之前复制的SQL语句部分时特别方便。

8. 重命名重构:⇧F6 | Shift+F6

将光标放在要重命名的对象上,然后按⇧F6(macOS)或Shift+F6(Windows/Linux)可以调出”重命名”对话框。在这里,您不仅可以重命名对象,还可以调整更改范围,在实际重命名前预览效果。

这个功能对于重构大型SQL脚本或修改表/列别名时特别有价值,能确保所有相关引用都同步更新。

9. 生成代码片段:⌘N | Alt+Insert

在查询控制台中按⌘N(macOS)或Alt+Insert(Windows/Linux)可以调用”Generate”弹出窗口。这里提供了大量预定义的SQL语句片段可供选择。

从简单的SELECT语句到复杂的JOIN操作,这个功能可以帮助快速搭建查询框架,然后根据具体需求进行定制。

10. 使用AI生成代码:⌘\ | Ctrl+\

IntelliJ IDEA的AI助手可以直接在查询控制台中为您生成SQL查询。在编辑区域按⌘\(macOS)或Ctrl+\(Windows/Linux)调用AI助手,输入您想要的查询描述,然后按Enter提交请求。

这个创新的功能对于快速创建复杂查询或当您不熟悉特定数据库语法时特别有帮助。AI会根据您的自然语言描述生成符合语法规范的SQL代码。

结论

熟练掌握这10个IntelliJ IDEA的数据库工具快捷方式,可以显著提升您处理SQL查询的效率和工作流畅度。从快速执行查询到智能代码生成,这些功能涵盖了数据库开发的各个环节。

将这些快捷方式集成到您的日常工作中,可以节省大量时间,让您专注于解决业务问题而非工具操作。IntelliJ IDEA的数据库工具不断进化,提供更多智能化功能来简化开发者的工作流程。


本文是由葡萄城技术开发团队发布,转载请注明出处:葡萄城官网

posted @
2025-11-05 14:30 
葡萄城技术团队  阅读(
140)  评论(
1)   
收藏 
举报



刷新页面
返回顶部

Redis 基础入门与核心概念【第一部分】

thbcm阅读(329)

Redis 基础入门与核心概念【第一部分】

前面将多线程、锁、线程安全部分内容完整了解了一遍,接下来准备点啥呢?发现最开始Redis部分的内容写的有些凌乱,想着趁着这次机会把Redis部分从头到尾梳理一遍,如果想了解或者梳理Redis知识的小伙伴,看这个系列的几篇Redis文章就够了~

第1章:Redis 简介与概览

1.1 什么是Redis?

如果把传统的关系型数据库(如MySQL)比作一个”大型仓库”——数据存放有序但存取较慢,那么Redis就是一个”超高速工作台”——所有常用工具都摆在手边,随取随用!

Redis(Remote Dictionary Server) 是一个开源的、基于内存的键值对存储系统。它不仅仅支持简单的Key-Value,还提供了丰富的数据结构,可以用作数据库、缓存和消息中间件。

1.2 Redis为什么这么快?核心优势解析

1. 基于内存操作

就像从书架上拿书(硬盘) vs 从桌面上拿便签(内存),Redis基于内存的操作让它拥有惊人的速度:

# 速度对比
硬盘读取速度:    约 100MB/s
内存读取速度:    约 10GB/s  
Redis读取速度:   约 100,000次/秒

2. 单线程架构

听起来反直觉?但正是Redis的”独门绝技”!想象一下:银行只有一个超级高效的柜员,但他能同时处理多个窗口的客户请求,没有内部协调的混乱。这就是Redis的单线程+IO多路复用模型:

  • 避免了线程切换和锁竞争的开销
  • 一个线程同时处理多个客户端请求

3. 高效的数据结构

Redis不是简单的Key-Value,而是为不同场景量身定制的数据结构服务器,就像多功能工具箱,每种数据结构都是不同的专用工具。

1.3 Redis vs 其他数据库:什么时候该用Redis?

场景 推荐使用 不推荐使用
热点数据缓存 完美适合
会话存储(Session) 完美适合
排行榜/计数器 完美适合
交易记录存储 用MySQL
复杂关联查询 用MySQL

简单判断法则:如果你的数据需要快速读写不要求100%持久化安全,就用Redis!


第2章:Redis 安装与配置

2.1 Linux (Ubuntu) 安装

# 1. 更新包管理器
sudo apt update

# 2. 安装Redis
sudo apt install redis-server

# 3. 启动Redis服务
sudo systemctl start redis-server

# 4. 设置开机自启
sudo systemctl enable redis-server

# 5. 检查状态
sudo systemctl status redis-server

# 6. 测试连接
redis-cli ping
# 如果返回 PONG,恭喜安装成功!

2.2 macOS 安装

# 1. 使用Homebrew安装
brew install redis

# 2. 启动Redis服务(后台运行)
brew services start redis

# 3. 或者手动启动(前台运行,方便调试)
redis-server /usr/local/etc/redis.conf

# 4. 测试连接
redis-cli ping

2.3 Windows 安装

注意:官方不支持Windows,但有以下选择:

方案一:WSL2(推荐)

# 在Windows Terminal中开启WSL Ubuntu,然后按照Linux步骤安装

方案二:Microsoft维护的Windows版本

  1. 访问:https://github.com/microsoftarchive/redis/releases
  2. 下载 Redis-x64-3.2.100.msi
  3. 双击安装,Redis会作为Windows服务运行

2.4 Docker 安装(跨平台通用)

# 1. 拉取最新Redis镜像
docker pull redis:latest

# 2. 运行Redis容器
docker run -d --name my-redis \
  -p 6379:6379 \
  -v /path/on/host:/data \
  redis:latest
  
# 参数解释:
# -d : 后台运行
# --name : 容器名称
# -p 6379:6379 : 端口映射(主机:容器)
# -v : 数据卷挂载,持久化数据

# 3. 进入容器执行命令
docker exec -it my-redis redis-cli

# 4. 或者直接在主机连接
redis-cli -h 127.0.0.1 -p 6379

2.5 核心配置文件解读

Redis的”大脑”是redis.conf文件,让我们看看关键配置:

# 找到配置文件位置
find / -name redis.conf 2>/dev/null
# 通常位置:/etc/redis/redis.conf

# 查看关键配置
cat /etc/redis/redis.conf | grep -v "^#" | grep -v "^$"

重要配置项详解

# 网络相关
bind 127.0.0.1                    # 只允许本地连接,远程访问改为 0.0.0.0
port 6379                         # 默认端口号
protected-mode yes                # 保护模式,生产环境建议yes

# 持久化相关
dir /var/lib/redis               # 数据存储目录
dbfilename dump.rdb              # RDB文件名

# 内存管理
maxmemory 100mb                  # 最大内存限制
maxmemory-policy allkeys-lru     # 内存满时的淘汰策略

# 安全相关
requirepass your_strong_password # 设置访问密码

第3章:Redis 核心数据结构 (上) – 基础五虎将

数据结构总览

Redis不是简单的Key-Value,而是数据结构服务器!就像一个多功能工具箱,每种数据结构都是不同的工具:

数据结构 比喻 典型应用
String 便利贴 缓存、计数器
Hash 表格 用户信息、对象存储
List 书架 消息队列、最新列表
Set 集合 标签、共同好友
Sorted Set 排行榜 排行榜、延迟队列

3.1 String(字符串) – 万能选手

就像办公室的便利贴,简单直接,什么都能记!

# ========== 基础操作 ==========

# 设置键值对
SET username "redis_learner"
#  结果:OK
#  使用建议:最简单的缓存用法
# ️ 雷点:如果key已存在会覆盖,慎用!

# 获取值
GET username  
#  结果:"redis_learner"
# ️ 雷点:如果key不存在返回nil,注意空值处理

# 设置并获取原值(原子操作)
GETSET username "new_learner"
#  结果:"redis_learner" (返回旧值)
#  现在username的值是:"new_learner"

# ========== 数字操作 ==========

# 设置数字
SET page_views 100
# 递增
INCR page_views
#  结果:101
#  使用建议:完美用于计数器,原子操作不怕并发

# 增加指定数值
INCRBY page_views 5
#  结果:106

# 递减
DECR page_views
#  结果:105

# ========== 批量操作 ==========

# 批量设置
MSET user:1000:name "Alice" user:1000:age 25 user:1000:city "Beijing"
#  结果:OK
#  使用建议:减少网络开销,提升性能

# 批量获取
MGET user:1000:name user:1000:age user:1000:city
#  结果:1) "Alice" 2) "25" 3) "Beijing"

# ========== 生存时间 ==========

# 设置值并指定10秒后过期
SETEX session_token 10 "abc123"
#  结果:OK
#  使用建议:Session管理、验证码场景

# 设置key的生存时间(秒)
EXPIRE username 60
#  结果:(integer) 1 (设置成功)

# 查看剩余生存时间
TTL username
#  结果:(integer) 57 (剩余57秒)

String使用场景

  • 缓存HTML片段、API响应
  • 文章阅读量、点赞数计数器
  • Session存储、临时令牌
  • ⏰ 验证码、限流器

3.2 Hash(哈希) – 对象存储器

就像Excel表格,一个key对应多个字段,完美存储对象!

# ========== 基本操作 ==========

# 设置单个字段
HSET user:1000 name "Alice"
#  结果:(integer) 1 (新增字段数)

# 同时设置多个字段
HSET user:1000 age 25 city "Beijing" profession "Engineer"
#  结果:(integer) 3

# 获取单个字段
HGET user:1000 name
#  结果:"Alice"

# 获取所有字段和值
HGETALL user:1000
#  结果:
# 1) "name"
# 2) "Alice" 
# 3) "age"
# 4) "25"
# 5) "city"
# 6) "Beijing"
# 7) "profession"
# 8) "Engineer"
#  使用建议:适合存储对象,但字段不宜过多(建议<1000)
# ️ 雷点:字段太多会占用大量内存,考虑分拆

# ========== 批量操作 ==========

# 批量获取指定字段
HMGET user:1000 name age
#  结果:1) "Alice" 2) "25"

# ========== 数字操作 ==========

# 字段值递增
HINCRBY user:1000 age 1
#  结果:(integer) 26
#  使用建议:用户年龄更新、积分变动等

# ========== 查询操作 ==========

# 获取所有字段名
HKEYS user:1000
#  结果:1) "name" 2) "age" 3) "city" 4) "profession"

# 获取所有字段值
HVALS user:1000  
#  结果:1) "Alice" 2) "26" 3) "Beijing" 4) "Engineer"

# 获取字段数量
HLEN user:1000
#  结果:(integer) 4

# 检查字段是否存在
HEXISTS user:1000 email
#  结果:(integer) 0 (不存在)

Hash使用场景

  • 用户信息存储
  • 购物车商品信息
  • 对象属性缓存

3.3 List(列表) – 有序队列

就像排队的人群,可以从队头或队尾加入,保持顺序!

# ========== 从左侧操作 ==========

# 从左侧插入(类似队列头部)
LPUSH tasks "task1"
#  结果:(integer) 1
LPUSH tasks "task2" "task3"
#  结果:(integer) 3
#  当前列表:["task3", "task2", "task1"]

# 从左侧弹出
LPOP tasks
#  结果:"task3"
#  剩余列表:["task2", "task1"]

# ========== 从右侧操作 ==========

# 从右侧插入(类似队列尾部)
RPUSH tasks "task4"
#  结果:(integer) 3  
#  当前列表:["task2", "task1", "task4"]

# 从右侧弹出
RPOP tasks
#  结果:"task4"
#  剩余列表:["task2", "task1"]

# ========== 查询操作 ==========

# 获取列表长度
LLEN tasks
#  结果:(integer) 2

# 获取指定范围的元素
LRANGE tasks 0 -1  # 0到-1表示获取所有
#  结果:1) "task2" 2) "task1"

LRANGE tasks 0 0   # 获取第一个元素
#  结果:1) "task2"

# ========== 高级操作 ==========

# 阻塞式弹出(等待任务,超时时间5秒)
BLPOP new_tasks 5
#  使用建议:消息队列场景,队列为空时等待
#  结果:如果5秒内有元素返回元素,否则返回nil

# 修剪列表,只保留指定范围
LPUSH numbers 1 2 3 4 5
LTRIM numbers 0 2  # 只保留前3个元素
LRANGE numbers 0 -1
#  结果:1) "5" 2) "4" 3) "3"

List使用场景

  • 消息队列(LPUSH + BRPOP)
  • 最新文章列表
  • 操作日志记录

3.4 Set(集合) – 无序唯一

就像数学里的集合,元素无序但唯一,适合做关系运算!

# ========== 基本操作 ==========

# 添加元素
SADD tags "redis" "database" "cache"
#  结果:(integer) 3

# 添加重复元素(自动去重)
SADD tags "redis" "new_tag"
#  结果:(integer) 1 (只新增了1个)

# 获取所有元素
SMEMBERS tags
#  结果:1) "cache" 2) "database" 3) "redis" 4) "new_tag"
# ️ 雷点:元素无序!不要依赖返回顺序

# 检查元素是否存在
SISMEMBER tags "redis"
#  结果:(integer) 1 (存在)

# 获取集合大小
SCARD tags
#  结果:(integer) 4

# 随机弹出一个元素
SPOP tags
#  结果:"new_tag" (随机)
#  使用建议:抽奖场景

# ========== 集合运算 ==========

# 创建两个集合
SADD group_A "user1" "user2" "user3"
SADD group_B "user3" "user4" "user5"

# 交集 - 共同好友
SINTER group_A group_B
#  结果:1) "user3"

# 并集 - 所有用户
SUNION group_A group_B  
#  结果:1) "user1" 2) "user2" 3) "user3" 4) "user4" 5) "user5"

# 差集 - A有B没有
SDIFF group_A group_B
#  结果:1) "user1" 2) "user2"

# 将交集存储到新集合
SINTERSTORE common_users group_A group_B
SMEMBERS common_users
#  结果:1) "user3"

Set使用场景

  • ️ 文章标签系统
  • 社交网络共同好友
  • 随机抽奖、唯一值存储

3.5 Sorted Set(有序集合) – 带分队的集合

就像游戏排行榜,每个玩家都有分数,可以按分数排序!

# ========== 基本操作 ==========

# 添加带分数的成员
ZADD leaderboard 1000 "Alice"
ZADD leaderboard 850 "Bob" 1200 "Charlie" 900 "David"
#  结果:(integer) 3

# 按分数升序获取(从小到大)
ZRANGE leaderboard 0 -1 WITHSCORES
#  结果:
# 1) "Bob"    2) "850"
# 3) "David"  4) "900" 
# 5) "Alice"  6) "1000"
# 7) "Charlie" 8) "1200"

# 按分数降序获取(从大到小)
ZREVRANGE leaderboard 0 -1 WITHSCORES
#  结果:
# 1) "Charlie" 2) "1200"
# 3) "Alice"   4) "1000"
# 5) "David"   6) "900"
# 7) "Bob"     8) "850"

# ========== 分数操作 ==========

# 增加成员分数
ZINCRBY leaderboard 50 "Bob"
#  结果:"900"
#  使用建议:实时更新排行榜分数

# 获取成员分数
ZSCORE leaderboard "Alice"
#  结果:"1000"

# 获取成员排名(从0开始,按分数升序)
ZRANK leaderboard "Alice"
#  结果:(integer) 2 (第三名)

# 获取成员排名(按分数降序)
ZREVRANK leaderboard "Alice"  
#  结果:(integer) 1 (第二名)

# ========== 范围查询 ==========

# 按分数范围查询
ZRANGEBYSCORE leaderboard 900 1100 WITHSCORES
#  结果:
# 1) "David" 2) "900"
# 3) "Alice" 4) "1000"

# 查询分数大于1000的成员
ZRANGEBYSCORE leaderboard (1000 +inf WITHSCORES
#  结果:1) "Charlie" 2) "1200"

# 查询前3名
ZREVRANGE leaderboard 0 2 WITHSCORES
#  结果:
# 1) "Charlie" 2) "1200"
# 3) "Alice"   4) "1000" 
# 5) "David"   6) "900"

# ========== 统计操作 ==========

# 统计成员数量
ZCARD leaderboard
#  结果:(integer) 4

# 统计分数范围内的成员数量
ZCOUNT leaderboard 800 1000
#  结果:(integer) 3

Sorted Set使用场景

  • 游戏排行榜、热度排名
  • ⏰ 延迟队列(用时间戳作为分数)
  • 带权重的任务调度

本章总结

Redis的”基础五虎将”各有绝活:

  1. String – 简单直接,万能型选手
  2. Hash – 对象存储,结构化专家
  3. List – 有序队列,消息传递高手
  4. Set – 无序唯一,关系运算大师
  5. Sorted Set – 带分排序,排行榜王者

选择数据结构的心法

  • 要存单个值? → String
  • 要存对象? → Hash
  • 要维护顺序列表? → List
  • 要保证唯一性? → Set
  • 要按分数排序? → Sorted Set

记住:选择合适的数据结构,性能提升立竿见影!

在接下来的章节中,我们将深入Redis的持久化、高可用等高级特性。准备好迎接更精彩的Redis世界吧!

️ 如果你喜欢这篇文章,请点赞支持! 同时欢迎关注我的博客,获取更多精彩内容!

本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/19193827

posted @
2025-11-05 15:38 
佛祖让我来巡山  阅读(
166)  评论(
1)   
收藏 
举报

Chat2DB测试体验

thbcm阅读(341)

Chat2DB测试体验

最近因为整理数据类的管理工具和平台,想起来上半年曾经测试使用过的Chat2db。这个工具最早就是因为博客园的推广看到的。上半年deepseek火了之后,大家都开始关注AI的应用了。作为数据工程师,首先想到就是AI在数据上的应用。在关注学习ChatBI类的工具时,偶然间发现了Chat2DB这个工具,于是就测试试用一下。

首先Chat2DB分为本地版和在线版。本地版可以连接公司测试环境的数据库,可以基于实际项目的数据做一些测试。下载安装比较简单,之后提示要输入激活码,否则只能试用14天。这个时间相对比较短,如果超过时间了,可以另外找一台机器下载安装测试了。

数据源接入这块,基本支持各类主流的数据库,测试了下Oracle,能正常连接。大数据这块,支持hive、impala引擎,还缺一个Spark的支持。连接好数据源之后,查看表、查看数据、写SQL等常规操作与其他数据库客户端基本差不多。以下是安装后,连上数据源,进行SQL编写的界面。

这个工具强调其是AI first,因此想必AI是其最大的特色。在SQL编辑器中,按下“/”键,可以随时调用打开自然语言输入浮动对话框,输入一段文本,即可生成想要的SQL。比如实际项目中经常遇到的,生成一段日期临时表。

生成完之后可以随时关闭浮动框,继续编写SQL代码。这个体验还基本流畅。

自然语言查询支持各类常用大模型,如Qwen、deepseek等,用户可以自行选择。除了生成一些通用的SQL外(如各类SQL函数),数据开发中最常见的就是基于库表写一些SQL查询。这块翻译生成效果的好坏主要依赖元数据是否完整准确,如表注释是否清晰,字段是否都加了注释。在AI类演示系统中一般都会选择业务简单,标签齐全的表,但是真实的系统中,一大半表可能都没有表注释,在这样的情况下,大模型生成的SQL往往是胡说八道。Chat2DB的自然语言对话框中输入@,可以指定库表中的表,这样就限制了SQL表的范围,一定程度上控制了大模型幻觉。

 除此之外,Chat2DB中还有一个AI数据集的概念。将数据源中的表添加至AI数据集,然后将该集合的表进行标准化的标注。这样后续用户在自然语言查询时,可以手动选择基于AI数据集进行SQL生成。这样一来,生成SQL的质量就很高了。看完介绍视频中提到这个功能,我眼睛一亮,于是就实操测试了下。AI数据集的创建还是比较清晰方便的,但是在AI对话时,怎么也找不到选择自定义AI数据集的入口,只能选择某个库表(离线版和在线版都找不到)。这个让我很奇怪,按说这么重要的功能不会只停留在宣传片中,希望下次有机会再实际测试一下这个功能。

另外一个特色功能就是AI辅助DDL/DML功能,Chat2DB将其称为Table Copilot功能。具体来说,在表结构管理页面中,用户可以通过自然语言描述各类DDL工作,比如增加一个字段***,将***字段长度扩展到***,删除字段等,Copilot收到后会翻译成SQL,然后返回给用户进行确认,若有必要,用户可进行手工微调,然后点击执行,进行表结构更新。因为现在数据库多达十来种,各类数据库的SQL方言又有些差异,比如字符串类型:有VARCHAR/VARCHAR2/STRING/TEXT等。通过自然语言方式进行交互,确确实实能提高数据开发工程师日常工作的效率,这块实测下来是完全可用的。

有一点不足,目前Copilot功能不支持自定义词根。在实际测试中发现经常同一个词根被翻译成不同的英文。比如证券,有时会被翻译成sec,有时会翻译成security,这样就很容易造成同一个库同一系列表出现不同的表名单词缩写,或者同一张表中同一系列字段出现不同的字段单词缩写。以下是实测中连续两次增加字段:

如果支持用户自定义上传词根,大模型在生成SQL时优先匹配词根,则可以控制表名字段名的命名规范,这个在大型多人合作开发项目中尤为重要。

其他功能就是可以快速对数据做一些展示,如折线图、柱状图等。这个其实就是BI的功能,我猜测既然主要定位是Chat,那么就不局限于DB了,跟数据相关的功能都可以包含进来了。

在线版和本地版页面基本一样,暂时还没看出其主要应用场景在哪里(可能只是给用户提供一个测试demo环境)。另外还有个本地化部署版,部署完成之后效果看着像是CloudQuery一样,作为一个后台各类数据库访问的统一的入口,这些与AI都不太沾边,就不测试了。

在AI使用限制方面,Local版本只能免费试用AI功能一年,每月3000次AI服务。因为AI服务大模型占用资源,按照调用次数或者token数量收费,这个也可以理解。在企业环境中,有些企业已经部署了本地大模型,如果Chat2DB可以开放入口,可以让用户选择企业自有大模型引擎,这样的话可能会有更多的企业受众。

整体来说,Chat2DB作为数据库客户端还是能满足数据库工程师日常的各种需求的,但是目前来看,还不足以吸引我放弃已经使用习惯的数据库客户端(如使用中的PLSQL develepor/DBeaver),可能不收费我就用了吧。希望其AI特色功能未来越来越好用,以后有机会会再试用一下。

posted on 2025-11-05 15:26  davawang  阅读(139)  评论(0)    收藏  举报

实现 json path 来评估函数式解析器的损耗

thbcm阅读(313)

实现 json path 来评估函数式解析器的损耗

目的

之前就考虑评估过 函数式解析器 在dotnet这些面向对象语言上有着一些损耗,虽然表意性很强,与ABNF范式结合使用,维护性大大提升

不过由于性能考虑(以及之前认为也或许没有太多机会实现解析器),就没打算继续深究

不过现实确实有多次需要实现解析器,每次从0手写,太费时间了,比较很多实现对于解析性能并不需极致性能,(特别现在ai code疯狂的时代,老板哪会给那么多时间让你慢慢扣性能,老板们都常说ai几秒钟的事情,你个辣鸡还花几天)

所以为了血压和身体健康,只要性能将就,还是对于解析不太在意性能的场景以后就这样偷懒吧 (比如今年搞得 VKProxy 里面得动态条件和简单模板替换, 路子可行也许后面还是换掉吧,维护多累呀)

当然也有高手搞过类似的库,比如 Parlot , 不过其不支持 stream, 理想情况还是想stream 也能支持, 所以现在先简单搞一波:参考 Parlot 简单实现 string 解析器底层以及 json path 简单版

然后和 Newtonsoft.json 在简单的json path 场景比较,如果打不过,那怕没有继续的必要了

简要说明函数式解析器

这里以最简单 json path “$.Name” 举例,

我们所需要解析即为 $ 开头, . 之后字符为属性名

换成代码大致为 Parser = Char('$').And(Char('.')).And(AnyExclude("[]().,\" '\r\n@$!=<\\?&|*:")).Eof()

像 Char And 这些方法在面向对象中我们会让方法生成解析算子实例,以达到与函数式相同效果, 这也是性能损耗的一个大点

比如 Char 方法

 public static Parser<char> Char(char c) => new CharLiteral(c);

具体算子实现

public class CharLiteral : Parser<char>
{
    private SearchValues<char> c;

    public string Value { get; private set; }

    public CharLiteral(char c)
    {
        this.c = SearchValues.Create(new char[] { c });
        Value = c.ToString();
    }

    public CharLiteral(string c)
    {
        this.c = SearchValues.Create(c);
        Value = c;
    }

    public override bool Parse(CharParseContext context, ref ParseResult<char> result)
    {
        context.EnterParser(this);
        var cursor = context.Cursor;
        if (!cursor.Eof && c.Contains(cursor.Current))
        {
            var c = cursor.Current;
            var start = cursor.Offset;
            cursor.Advance();
            result.Set(start, cursor.Offset, c);

            context.ExitParser(this);
            return true;
        }
        context.ExitParser(this);
        return false;
    }
}

好了,借用经典话语 这是功,这是防,这是boss 去吧

按照 [rfc9535](https://www.rfc-editor.org/rfc/rfc9535.html) 标准, 最终 json path 解析器大致如下:


public class JsonPathParser
{
    //public static Parser<char> B = Char(new char[]
    //{ (char)0x20, // Space
    //  (char)0x09, //Horizontal tab
    //  (char)0x0A, // Line feed or New line
    //  (char)0x0D // Carriage return
    //});

    public static readonly Parser<char> RootIdentifier = Char('$').Name(nameof(RootIdentifier));
    public static readonly Parser<int> Int = Int().Name(nameof(Int));
    public static readonly Parser<char> DoubleQuoted = Char('"').Name(nameof(DoubleQuoted));
    public static readonly Parser<char> SingleQuoted = Char('\'').Name(nameof(SingleQuoted));
    public static readonly Parser<IStatement> WildcardSelector = Char('*').Then<IStatement>(static x => new WildcardSelectorStatment()).Name(nameof(WildcardSelector));
    public static readonly Parser<IStatement> IndexSelector = Int.Then<IStatement>(static x => new IndexSelectorStatment() { Index = x }).Name(nameof(IndexSelector));
    public static readonly Parser<TextSpan> StringLiteral = Between(DoubleQuoted, ZeroOrOne(Any("\"", mustHasEnd: true, escape: '\\')), DoubleQuoted).Or(Between(SingleQuoted, ZeroOrOne(Any("'", mustHasEnd: true, escape: '\\')), SingleQuoted)).Name(nameof(StringLiteral));
    public static readonly Parser<IStatement> NameSelector = StringLiteral.Then<IStatement>(static x => new Member() { Name = x.Span.ToString() }).Name(nameof(NameSelector));
    public static readonly Parser<int> Start = Int;
    public static readonly Parser<int> End = Int;
    public static readonly Parser<int> Step = Int;

    public static readonly Parser<Nothing> S = IgnoreChar(new char[]
    { (char)0x20, // Space
      (char)0x09, //Horizontal tab
      (char)0x0A, // Line feed or New line
      (char)0x0D // Carriage return
    }).Name(nameof(S));

    public static readonly Parser<char> CurrentNodeIdentifier = Char('@').Name(nameof(CurrentNodeIdentifier));
    public static readonly Parser<char> LogicalNotOp = Char('!').Name(nameof(LogicalNotOp));
    public static readonly Parser<string> ComparisonOp = Text("==").Or(Text("!=")).Or(Text("<=")).Or(Text(">=")).Or(Text("<")).Or(Text(">")).Name(nameof(ComparisonOp));
    public static readonly Parser<IStatement> Num = Decimal(NumberOptions.Float).Then<IStatement>(static x => new NumberValue(x)).Name(nameof(Num));
    public static readonly Parser<IStatement> True = Text("true").Then<IStatement>(static x => BoolValue.True).Name(nameof(True));
    public static readonly Parser<IStatement> False = Text("false").Then<IStatement>(static x => BoolValue.False).Name(nameof(False));
    public static readonly Parser<IStatement> Null = Text("null").Then<IStatement>(static x => NullValue.Value).Name(nameof(Null));

    private const string name = "[]().,\" '\r\n@$!=<\\?&|*:";

    //public static Parser<char> LCALPHA = Char('a', 'z');
    //public static Parser<char> DIGIT = Char('0', '9');
    //public static Parser<char> ALPHA = Char((char)0x41, (char)0x5A).Or(Char((char)0x61, (char)0x7A));
    public static readonly Parser<IStatement> MemberNameShorthand = AnyExclude(name).Then<IStatement>(static x => new Member { Name = x.Span.ToString() }).Name(nameof(MemberNameShorthand));

    //public static Parser<char> NameFirst = ALPHA.Or(Char('_')).Or(Char((char)0x80, (char)0xD7FF)).Or(Char((char)0xE000, (char)0xFFFF));
    //public static Parser<char> NameChar = NameFirst.Or(DIGIT);
    //public static Parser<char> FunctionNameFirst = LCALPHA;

    //public static Parser<char> FunctionNameChar = FunctionNameFirst.Or(Char('_')).Or(DIGIT);
    //public static Parser<string> FunctionName = FunctionNameFirst.And(ZeroOrMany(FunctionNameChar)).Then<string>(static x => throw new NotImplementedException());
    public static readonly Parser<string> FunctionName = AnyExclude(name).Then<string>(static x => x.Span.ToString()).Name(nameof(FunctionName));

    public static readonly Parser<IStatement> SliceSelector = Optional<int?>(Start.And(S).Then<int?>(static x => x.Item1), null).And(Char(':')).And(S).And(Optional<int?>(End.And(S).Then<int?>(static x => x.Item1), null)).And(Optional<int?>(Char(':').And(Optional<int?>(S.And(Step).Then<int?>(static x => x.Item2), null)).Then<int?>(static x => x.Item2))).Then<IStatement>(static x => new SliceStatement() { Start = x.Item1, End = x.Item4, Step = x.Item5 })
        .Name(nameof(SliceSelector));

    public static readonly Deferred<IStatement> LogicalExpr = Deferred<IStatement>(nameof(LogicalExpr));

    public static readonly Parser<IStatement> FilterSelector = Char('?').And(S).And(LogicalExpr).Then<IStatement>(static x => new FilterSelectorStatement()
    {
        Statement = x.Item3
    }).Name(nameof(FilterSelector));

    public static readonly Parser<IStatement> Selector = NameSelector.Or(WildcardSelector).Or(SliceSelector).Or(IndexSelector).Or(FilterSelector).Name(nameof(Selector));

    public static readonly Parser<IStatement> ParenExpr = Optional(LogicalNotOp.And(S)).And(Char('(')).And(S).And(LogicalExpr).And(S).And(Char(')'))
        .Then<IStatement>(static x => new UnaryOperaterStatement()
        {
            Operator = x.Item1.Item1 == '!' ? "!" : "(",
            Statement = x.Item4
        }).Name(nameof(ParenExpr));

    public static readonly Deferred<IReadOnlyList<(Nothing, IStatement)>> Segments = Deferred<IReadOnlyList<(Nothing, IStatement)>>(nameof(Segments));

    public static readonly Deferred<IStatement> FunctionExpr = Deferred<IStatement>(nameof(FunctionExpr));
    public static readonly Deferred<IStatement> JsonPathQuery = Deferred<IStatement>(nameof(JsonPathQuery));
    public static readonly Parser<IStatement> RelQuery = CurrentNodeIdentifier.And(Segments).Then<IStatement>(static x => new CurrentNode() { Child = ConvertSegments(x.Item2) }).Name(nameof(RelQuery));
    public static readonly Parser<IStatement> Literal = Num.Or(StringLiteral.Then<IStatement>(static x => new StringValue(x.Span.ToString()))).Or(True).Or(False).Or(Null).Name(nameof(Literal));
    public static readonly Parser<IStatement> NameSegment = Char('[').And(NameSelector).And(Char(']')).Then<IStatement>(static x => x.Item2).Or(Char('.').And(MemberNameShorthand).Then<IStatement>(static x => x.Item2)).Name(nameof(NameSegment));
    public static readonly Parser<IStatement> IndexSegment = Char('[').And(IndexSelector).And(Char(']')).Then<IStatement>(static x => x.Item2).Name(nameof(IndexSegment));

    public static readonly Parser<IStatement> SingularQuerySegments = ZeroOrMany(S.And(NameSegment.Or(IndexSegment))).Then<IStatement>(ConvertSegments).Name(nameof(SingularQuerySegments));

    public static readonly Parser<IStatement> RelSingularQuery = CurrentNodeIdentifier.And(SingularQuerySegments).Then<IStatement>(static x => new CurrentNode() { Child = x.Item2 }).Name(nameof(RelSingularQuery));
    public static readonly Parser<IStatement> AbsSingularQuery = RootIdentifier.And(SingularQuerySegments).Then<IStatement>(static x => new RootNode() { Child = x.Item2 }).Name(nameof(AbsSingularQuery));
    public static readonly Parser<IStatement> SingularQuery = RelSingularQuery.Or(AbsSingularQuery).Name(nameof(SingularQuery));
    public static readonly Parser<IStatement> Comparable = Literal.Or(SingularQuery).Or(FunctionExpr).Name(nameof(Comparable));
    public static readonly Parser<IStatement> ComparisonExpr = Comparable.And(S).And(ComparisonOp).And(S).And(Comparable).Then<IStatement>(static x => new OperatorStatement() { Left = x.Item1, Operator = x.Item3, Right = x.Item5 }).Name(nameof(ComparisonExpr));
    public static readonly Parser<IStatement> FilterQuery = RelQuery.Or(JsonPathQuery).Name(nameof(FilterQuery));
    public static readonly Parser<IStatement> FunctionArgument = FilterQuery.Or(LogicalExpr).Or(FunctionExpr).Or(Literal).Name(nameof(FunctionArgument));
    public static readonly Parser<IStatement> TestExpr = Optional(LogicalNotOp.And(S)).And(FilterQuery.Or(FunctionExpr)).Then<IStatement>(static x => x.Item1.Item1 == '!' ? new UnaryOperaterStatement() { Operator = "!", Statement = x.Item2 } : x.Item2).Name(nameof(TestExpr));
    public static readonly Parser<IStatement> BasicExpr = ParenExpr.Or(ComparisonExpr).Or(TestExpr).Name(nameof(BasicExpr));

    public static readonly Parser<IStatement> LogicalAndExpr = BasicExpr.And(ZeroOrMany(S.And(Text("&&")).And(S).And(BasicExpr))).Then<IStatement>(static x =>
    {
        IStatement current = x.Item1;
        if (x.Item2 != null && x.Item2.Count > 0)
        {
            foreach (var item in x.Item2)
            {
                current = new AndStatement() { Left = current, Right = item.Item4 };
            }
        }
        return current;
    }).Name(nameof(LogicalAndExpr));

    public static readonly Parser<IStatement> LogicalOrExpr = LogicalAndExpr.And(ZeroOrMany(S.And(Text("||")).And(S).And(LogicalAndExpr))).Then<IStatement>(static x =>
    {
        IStatement current = x.Item1;
        if (x.Item2 != null && x.Item2.Count > 0)
        {
            foreach (var item in x.Item2)
            {
                current = new OrStatement() { Left = current, Right = item.Item4 };
            }
        }
        return current;
    }).Name(nameof(LogicalOrExpr));

    public static readonly Parser<IStatement> BracketedSelection = Char('[').And(S).And(Selector).And(ZeroOrMany(S.And(Char(',')).And(S).And(Selector))).And(S).And(Char(']'))
        .Then<IStatement>(static x =>
    {
        var list = new List<IStatement> { x.Item3 };
        if (x.Item4 != null)
            list.AddRange(x.Item4.Select(y => y.Item4));
        if (list.Count == 0)
            return null;
        return list.Count == 1 ? list[0] : new UnionSelectionStatement(list);
    }).Name(nameof(BracketedSelection));

    public static readonly Parser<IStatement> ChildSegment = BracketedSelection.Or(Char('.').And(WildcardSelector.Or(MemberNameShorthand)).Then<IStatement>(static x => x.Item2)).Name(nameof(ChildSegment));

    public static readonly Parser<IStatement> DescendantSegment = Char('.').And(Char('.')).And(BracketedSelection.Or(WildcardSelector).Or(MemberNameShorthand)).Then<IStatement>(static x => new WildcardSelectorStatment() { Child = x.Item3 }).Name(nameof(DescendantSegment));
    public static readonly Parser<IStatement> Segment = ChildSegment.Or(DescendantSegment).Name(nameof(Segment));

    public static readonly Parser<IStatement> Parser;

    static JsonPathParser()
    {
        LogicalExpr.Parser = LogicalOrExpr;
        Segments.Parser = ZeroOrMany(S.And(Segment));
        //MemberNameShorthand.Parser = NameFirst.And(ZeroOrMany(NameChar)).Then<IStatement>(static x => new Member { Name = x.Item1 + new string(x.Item2.ToArray()) });
        FunctionExpr.Parser = FunctionName.And(Char('(')).And(S).And(Optional(FunctionArgument.And(ZeroOrMany(S.And(Char(',')).And(S).And(FunctionArgument))))).And(S).And(Char(')')).Then<IStatement>(static x =>
        {
            var args = new List<IStatement>();
            if (x.Item4.Item1 != null)
            {
                args.Add(x.Item4.Item1);
            }
            if (x.Item4.Item2 != null)
            {
                args.AddRange(x.Item4.Item2.Select(y => y.Item4));
            }
            var func = new FunctionStatement()
            {
                Name = x.Item1,
                Arguments = args.Count == 0 ? Array.Empty<IStatement>() : args.ToArray()
            };

            return func;
        });
        JsonPathQuery.Parser = RootIdentifier.And(Segments).Then<IStatement>(static x => new RootNode() { Child = ConvertSegments(x.Item2) });
        Parser = JsonPathQuery.Eof().Name(nameof(Parser));
    }

    private static IStatement ConvertSegments(IReadOnlyList<(Nothing, IStatement)> x)
    {
        if (x == null || x.Count == 0)
        {
            return null;
        }
        else if (x.Count == 1)
            return x[0].Item2;
        else
        {
            var current = x.Last().Item2;
            for (int i = x.Count - 2; i >= 0; i--)
            {
                if (x[i].Item2 is IParentStatement p)
                {
                    var pp = p;
                    while (pp.Child != null)
                    {
                        var pc = p.Child as IParentStatement;
                        if (pc is null)
                            throw new NotSupportedException($"Cannot set child for statement of type {p.GetType().FullName}");
                        pp = pc;
                    }
                    pp.Child = current;
                    current = p;
                }
                else
                {
                    throw new NotSupportedException($"Cannot set child for statement of type {x[i].Item2.GetType().FullName}");
                }
            }
            return current;
        }
    }
}

性能测试

测试代码

[MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public class JsonPathBenchmarks
{
    private object data = new
    {
        Num = -3.4,
        Nu = null as string,
        Array = new object[]
        {
            new { Name = "Alice", Age = 30 },
            new { Name = "Bob", Age = 25 },
            new { Name = "Charlie", Age = 35 }
        },
    };

    private string path = "$.Array[1]['Name','Age']";

    private string json;
    private IStatement cache;
    private readonly JsonPath pc;

    public JsonPathBenchmarks()
    {
        json = JsonSerializer.Serialize(data);
        JsonPathParser.Parser.TryParseResult(path, out var result, out var error);
        cache = result.Value;
        pc = JsonPath.Parse(path);
    }

    [Benchmark]
    public object CacheTest()
    {
        return cache.EvaluateJson(json);
    }

    [Benchmark]
    public object NoCacheTest()
    {
        JsonPathParser.Parser.TryParseResult(path, out var result, out var error);
        return result.Value.EvaluateJson(json);
    }

    private Newtonsoft.Json.Linq.JToken testTo = Newtonsoft.Json.Linq.JToken.Parse("null");

    [Benchmark]
    public object NewtonsoftOnlyParseTest()
    {
        return testTo.SelectTokens(path);
    }

    [Benchmark]
    public object NewtonsoftTest()
    {
        Newtonsoft.Json.Linq.JToken token = Newtonsoft.Json.Linq.JToken.Parse(json);
        return token.SelectTokens(path);
    }

    [Benchmark]
    public object JsonPathNetTest()
    {
        var p = JsonPath.Parse(path);
        var instance = JsonNode.Parse(json);
        return p.Evaluate(instance);
    }

    [Benchmark]
    public object JsonPathNetCacheTest()
    {
        var instance = JsonNode.Parse(json);
        return pc.Evaluate(instance);
    }

    [Benchmark]
    public object JsonPathNetOnlyParseTest()
    {
        return JsonPath.Parse(path);
    }

    [Benchmark]
    public object OnlyParseTest()
    {
        JsonPathParser.Parser.TryParseResult(path, out var result, out var error);
        return result.Value;
    }
}

效果


BenchmarkDotNet v0.15.4, Windows 11 (10.0.26100.6584/24H2/2024Update/HudsonValley)
Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.304
  [Host]     : .NET 9.0.8 (9.0.8, 9.0.825.36511), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 9.0.8 (9.0.8, 9.0.825.36511), X64 RyuJIT x86-64-v3


Method Mean Error StdDev Gen0 Gen1 Allocated
CacheTest 1,488.1 ns 15.42 ns 14.42 ns 0.2537 2136 B
NoCacheTest 2,650.5 ns 11.44 ns 10.14 ns 0.4196 3528 B
NewtonsoftOnlyParseTest 204.0 ns 2.65 ns 2.35 ns 0.1128 944 B
NewtonsoftTest 3,039.1 ns 33.40 ns 26.07 ns 0.9651 0.0191 8088 B
JsonPathNetTest 1,266.9 ns 25.03 ns 35.90 ns 0.2728 2288 B
JsonPathNetCacheTest 855.0 ns 9.16 ns 7.65 ns 0.1469 1232 B
JsonPathNetOnlyParseTest 346.3 ns 3.19 ns 2.66 ns 0.1259 1056 B
OnlyParseTest 1,057.0 ns 5.24 ns 4.90 ns 0.1659 1392 B

嗯,在 system.text.json 上解析 比 Newtonsoft.json 还是快了点,具体实现都是最基本的,没有像 jsonpath 那些做优化设计,性能感觉还是可以的

所有 实现放在 https://github.com/fs7744/Lmzzz

后续看什么时候有空搞搞 stream ,能行以后就这样偷懒搞吧

posted @
2025-11-05 17:28 
victor.x.qu  阅读(
52)  评论(
0)   
收藏 
举报

告别漫长GC停顿:深入解析G1如何实现可预测的毫秒级响应

thbcm阅读(332)

告别漫长GC停顿:深入解析G1如何实现可预测的毫秒级响应

G1(Garbage-First)垃圾回收器是一款面向服务端应用、为大内存和多处理器系统设计的革命性垃圾回收器。G1的核心设计目标是在满足高吞吐量的同时,建立一个“可预测的停顿时间模型”(Pause-Time Model),让使用者可以明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间大概率不超过N毫秒。这一特性是它与之前回收器(如CMS)最本质的区别。
在JDK 9发布之后,G1凭借其在性能、停顿时间控制和未来发展潜力上的综合优势,正式取代了经典的Parallel Scavenge + Parallel Old组合,成为服务端模式下的默认垃圾回收器,而一度辉煌的CMS回收器则被正式声明为不推荐使用(Deprecated),并在后续版本中被移除。
G1的出现,标志着Java虚拟机垃圾回收技术进入了一个更加注重延迟和用户体验的新时代。

停顿时间模型
G1垃圾回收器颠覆了传统分代回收器将堆内存划分为连续的新生代和老年代的物理布局。它将整个Java堆划分为多个大小相等、不要求物理连续的独立区域(Region)。每个Region的大小可以1MB到32MB之间,且必须是2的N次幂。G1会根据堆的初始大小和目标,自动选择最合适的Region大小。每个Region在运行时可以扮演不同的角色:Eden区、Survivor区,或者Old区。对于体积超过一个Region容量一半的超大对象,G1会将其视为巨型对象(Humongous Object),并直接分配到特殊的Humongous区。
在G1之前的垃圾回收器,无论是CMS还是Parallel Scavenge,其回收范围要么是整个新生代(Minor GC),要么是整个老年代(Major GC)或整个Java堆(Full GC)。这意味着一旦触发老年代回收,其扫描和清理范围就是整个老年代空间,停顿时间会随着老年代空间的增大而线性增加,难以控制。
G1则彻底打破了这一限制,它回收的最小单元是Region。G1可以根据预设的停顿时间目标,自由地选择任意数量、任意分代的Region组成一个回收集(Collection Set,CSet)来进行回收。G1衡量回收哪个Region的唯一标准,不再是“这个Region属于哪个分代”,而是“回收这个Region的收益有多高”。这个收益指的是花费最小的时间,回收出最大的空间。这正是“Garbage-First”名称的由来:优先回收垃圾最多的Region。
为了实现这一目标,G1引入了停顿预测模型(Pause Prediction Model)。这个模型基于衰减均值(Decaying Average)理论。衰减均值是一种加权平均的统计方法,它给予近期的数据更高的权重,而历史久远的数据权重则会逐渐衰减。这种方法能更精确地反映系统当前的动态行为。G1在后台会持续监控和记录每一次回收中每个Region的各项指标,例如回收一个Region平均需要多长时间(回收成本)、Region内有多少存活对象(回收收益)等。
当需要进行垃圾回收时,G1会利用这个模型进行预测:它会从所有候选Region中,按照回收收益从高到低排序,然后开始模拟选择。它会挑选收益最高的Region加入回收集,并累加预估的回收时间,然后继续挑选下一个,直到累加的预估时间即将触及用户设定的停顿时间目标(通过 -XX:MaxGCPauseMillis 参数设定)为止。通过这种方式,G1可以在满足停顿时间目标的前提下,实现垃圾回收效率的最大化,达到了性能与延迟的精妙平衡。

记忆集
为了支持对任意Region集合进行独立的回收,G1必须解决一个关键问题:如何高效地处理跨Region的对象引用。例如,当回收Region A时,必须知道是否有其他Region(如Region B、C)中的对象正引用着Region A中的对象。如果存在这样的引用,那么Region A中的被引用对象就不能被回收。为了跟踪这些跨Region引用,G1为每个Region都维护了一个名为记忆集(Remembered Set,RSet)的数据结构。

在记忆集的设计中,通常有两种视角:一种是记录“谁指向我”(points-into),另一种是记录“我指向谁”(points-out)。G1的RSet采用的是前者,即每个Region的RSet记录了“有哪些其他的Region中的对象引用了当前Region中的对象”。具体实现上,G1的底层仍然依赖于卡表。
如上图所示,Region1和Region3中的对象都引用了Region2中的对象,因此在Region2首先使用points-into的RSet记录了这两个引用。这个RSet其实是一个哈希表结构,key是别的Region的起始地址,value是一个集合,里面的元素是卡表的index。
举例来说,如果Region 2的RSet里有一项的key是Region 1,value里有index为1234的卡页,它的意思就是Region 1的一个卡页里有引用指向Region 2。所以对Region 2来说,该RSet记录的是points-into的关系;而卡表仍然记录了points-out的关系。
当回收Region2时,只需扫描其RSet,就能快速找到所有需要保留的存活对象,而无需全堆扫描。
这种设计的代价是显著的内存开销。堆中每一个Region,无论新旧,都必须配备一份自己的卡表和RSet。这导致G1的记忆集以及相关的辅助数据结构可能会占据整个堆容量的20%甚至更多。这是G1为实现可预测停顿时间所付出的空间成本。

Young GC And Mixed GC
G1垃圾回收器提供了两种垃圾回收模式:Young GC和Mixed GC,这两种模式都会暂停应用线程。
1)Young GC:此模式的回收范围仅限于所有年轻代的Region(Eden + Survivor)。当Eden区被占满,无法为新对象分配空间时,就会触发一次Young GC。G1会根据停顿时间目标动态地调整年轻代Region的数量(即年轻代内存的大小),从而间接控制Young GC的耗时。回收过程会将存活对象复制到Survivor区或晋升到Old区,与传统的Minor GC类似。
2)Mixed GC:这是G1最具特色的回收模式。它不仅会回收所有年轻代的Region,还会额外回收一部分老年代的Region。Mixed GC的触发时机通常是在老年代的堆占用比例达到某个阈值(由 -XX:InitiatingHeapOccupancyPercent参数控制,默认为45%)之后,由全局并发标记(Global Concurrent Marking)阶段统计出回收收益最高的一批老年代Region。在执行时,Mixed GC会根据用户设定的停顿时间目标,在新生代之外,优先选择那些垃圾最多的老年代Region加入回收集(CSet),一起进行回收。
必须强调,Mixed GC不等于Full GC。它是一种增量式的、部分回收老年代的机制,旨在通过多次、小规模的回收来逐步清理老年代,避免出现长时间的Full GC停顿。然而,如果应用程序的内存分配速率过快,导致Mixed GC的清理速度跟不上对象晋升到老年代的速度,最终老年代被填满,G1将不得不放弃其优雅的回收方式,触发一次后备的、单线程的Full GC(Serial Old GC) 来整理整个堆。

工作过程
G1的完整工作过程可以划分为以下几个核心阶段,其中并发标记是与应用程序并发执行的,而其他阶段则需要暂停应用线程。
1)初始标记(Initial Marking):一个短暂的停顿阶段。这个阶段仅标记出从GC Roots(如栈上的本地变量、静态变量等)能直接关联到的对象。它的耗时非常短。
2)并发标记(Concurrent Marking):此阶段与应用程序并发执行,不产生停顿。G1会从初始标记阶段找到的对象出发,开始递归遍历整个堆中的对象图,找出所有存活的对象。G1采用原始快照算法,保证在并发标记开始时存活的对象,无论在标记过程中引用关系如何变化,最终都会被认为是存活的。这种机制的副作用是可能产生一些浮动垃圾,这些垃圾只能等到下一次回收周期才能被清理。
3)最终标记(Final Marking):一个短暂的停顿阶段。由于并发标记阶段应用线程仍在运行,可能会修改对象的引用关系,原始快照算法会将这些变化记录下来。此阶段的目的就是处理这些在并发标记期间产生的日志,对标记结果进行修正。
4)筛选回收(Live Data Counting and Evacuation):一个核心的停顿阶段,负责真正的垃圾清理。在此阶段,G1首先会对各个Region的存活对象数量和回收价值进行精确统计(Live Data Counting)。然后,基于其停顿时间预测模型,选择一组回收价值最高的Region组成回收集(CSet)。最后,G1会将CSet中所有Region里的存活对象通过复制算法拷贝到新的、空闲的Region中,并清空CSet中的所有旧Region。这个复制的过程天然地完成了内存碎片的整理,一举两得。

G1的优势主要体现在停顿时间的可预测性和空间的高效整合。G1的停顿时间与回收集的大小有关,而非整个Java堆的大小,这使得停顿时间更加可控。同时,G1在全局范围内采用整理算法,在局部Region之间采用复制算法,这两种算法都能有效避免内存碎片的产生,从而杜绝了因碎片过多而触发的耗时Full GC。
然而,G1也存在一些缺点。首先,G1在运行过程中需要执行复杂的并发操作(如并发标记)和维护写屏障,这些都会消耗额外的处理器资源。其次,为了实现高效的垃圾回收,G1需要维护Region、RSet、卡表等复杂数据结构,这会占用一部分内存空间。最后,如果垃圾产生的速度远远超过G1的回收速度(尤其是Mixed GC的回收速度),可能会导致堆迅速填满,最终退化为非常缓慢的Full GC。
根据业界的实践经验,对于小内存应用(例如堆大小小于4GB),传统的CMS甚至Parallel GC在吞吐量上的表现往往优于G1。然而,当Java堆大小超过一个临界点时,G1在控制停顿时间上的优势便开始凸显。这个优劣势的转换点通常在6GB至8GB之间。

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦

本文来自博客园,作者:poemyang,转载请注明原文链接:https://www.cnblogs.com/poemyang/p/19194401

posted on 2025-11-05 18:27  poemyang  阅读(46)  评论(0)    收藏  举报

联系我们