Tio Boot DocsTio Boot Docs
Home
文档导航
  • java-db
  • api-table
  • jooq
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • java-openai
  • ai_agent
  • knowlege_base
  • voice-agent
  • ai-search
  • ai-coding
  • ai-browser
  • 案例
Abount
AI 检索
  • Github
  • Gitee
Home
文档导航
  • java-db
  • api-table
  • jooq
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • java-openai
  • ai_agent
  • knowlege_base
  • voice-agent
  • ai-search
  • ai-coding
  • ai-browser
  • 案例
Abount
AI 检索
  • Github
  • Gitee
  • 入门

    • 01 · 入门

      • 01_introduction
      • tio-boot:新一代高性能 Java Web 开发框架
      • tio-boot 入门示例
      • Tio-Boot 配置 : 现代化的配置方案
      • tio-boot 整合 hotswap-classloader 实现热加载
      • 自行编译 tio-boot
      • 最新版本
      • 开发规范
  • 部署与日志

    • 02 · 部署

      • 02_deployment
      • 使用 Maven Profile 实现分环境打包 tio-boot 项目
      • Maven 项目配置详解:依赖与 Profiles 配置
      • tio-boot 打包成 FatJar
      • 使用 GraalVM 构建 tio-boot Native 程序
      • 使用 Docker 部署 tio-boot
      • 部署到 Fly.io
      • 部署到 AWS Lambda
      • 到阿里云云函数
      • 使用 Deploy 工具部署
      • 使用Systemctl启动项目
      • 使用 Jenkins 部署 Tio-Boot 项目
      • 使用 Nginx 反向代理 Tio-Boot
      • 使用 Supervisor 管理 Java 应用
      • 历史部署页与替代方案
      • 胖包与瘦包的打包与部署
      • 使用 JDK 8 构建基础库与验证运行环境
    • 03 · 日志

      • 日志
      • tio-boot 整合 Logback
  • 核心开发

    • 04 · 配置

      • 04_configuration
      • 配置参数
      • 服务器监听器
      • 内置缓存系统 AbsCache
      • 使用 Redis 作为内部 Cache
      • 静态文件处理器
      • 基于域名的静态资源隔离
      • DecodeExceptionHandler
      • 开启虚拟线程(Virtual Thread)
      • 框架级错误通知
    • 05 · JSON

      • 05_json
      • Json
      • 接受 JSON 和响应 JSON
      • 响应实体类
    • 06 · Web 开发

      • Web 开发
      • 路由与参数

        • 概述
        • 添加 Controller
        • handler入门
        • HttpRequestFunction:直接注册 Service 方法
        • Handler 的请求方法与错误响应
        • 接收请求参数
        • 接收日期参数
        • 接收数组参数
        • HttpRequest
        • 请求校验与响应转换
      • 响应与文件

        • 返回字符串
        • 返回文本数据
        • 返回网页
        • 请求和响应字节
        • 文件上传
        • 文件下载
        • 返回视频文件并支持断点续传
        • HttpResponse
        • Resps
        • RespBodyVo
        • 动态 返回 CSS 实现
        • 返回图片
        • 返回 multipart
        • 零拷贝
        • 大文件导出:零拷贝发送 Excel、Word、PDF、PPT
        • 分片上传
        • WebJars
      • 会话与请求控制

        • http Session
        • Cookie 读取与设置
        • 重定向和转发
        • Controller拦截器
        • 请求拦截器
        • LoggingInterceptor
        • 全局异常处理器
        • 跨域
        • 自定义 Handler 转发请求
        • 使用 HttpForwardHandler 转发所有请求
        • HTTP Basic 认证
        • Http响应加密
      • 异步与流式输出

        • 异步处理
        • Transfer-Encoding: chunked 实时音频播放
        • Server-Sent Events (SSE)
      • 工具与监控

        • 常用工具类
        • 接口访问统计
        • 接口请求和响应数据记录
        • JProtobuf
        • 测速
        • Gzip Bomb:使用压缩炸弹防御恶意爬虫
    • 07 · 参数校验

      • 07_validation
      • 数据紧校验规范
      • 参数校验
    • 08 · WebSocket 应用开发

      • 08_websocket
      • 使用 tio-boot 搭建 WebSocket 服务
      • WebSocket 聊天室项目示例
    • 09 · AOP

      • 09_aop
      • JFinal-aop
      • Aop 工具类
      • 配置
      • 独立使用 JFinal Aop
      • @AImport
      • 自定义注解拦截器
      • 原理解析
    • 10 · 国际化

      • 10_i18n
      • i18n
    • 11 · Enjoy 模板

      • 11_enjoy
      • tio-boot 整合 Enjoy 模版引擎文档
      • Tio-Boot 整合 Java-DB 与 Enjoy 模板引擎示例
      • 引擎配置
      • 表达式
      • 指令
      • 注释
      • 原样输出
      • Shared Method 扩展
      • Shared Object 扩展
      • Extension Method 扩展
      • Spring boot 整合
      • 独立使用 Enjoy
      • tio-boot enjoy 自定义指令 localeDate
      • PromptEngine
      • Enjoy 入门示例-擎渲染大模型请求体
      • Tio Boot + Enjoy:分页与 SEO 实战指南
      • TioBoot + Enjoy 生成 robots.txt 与 sitemap.xml:实战与SEO指南
      • Enjoy 使用示例
    • 12 · 定时任务

      • 12_scheduling
      • Quartz 定时任务集成指南
      • 分布式定时任务 xxl-jb
      • cron4j 使用指南
    • 13 · 测试

      • 13_testing
      • TioBootTest:环境与 AOP 初始化
      • 真实 HTTP 集成测试
      • 数据库集成测试与隔离
    • 14 · tio-utils

      • 14_tio-utils
      • tio-utils
      • EnvUtils 配置工具
      • Notification
      • Email
      • JSON
      • File
      • Base64
      • 上传和下载
      • Http
      • Telegram
      • RsaUtils
      • HttpUtils
      • ByteBufferUtils
      • 系统监控
      • 线程
      • 虚拟线程
      • 毫秒并发 ID (MCID) 生成方案
      • Base64 输入与锁等待
      • Gzip 与字节缓冲区
      • 字符串查找与随机整数
      • 日期转换与文件名解析
      • JSON 泛型转换与输出配置
      • JWT 签发与校验
      • 集合分页 PageUtils
      • URL 与流复制工具
  • 数据库与数据访问

    • 15 · java-db

      • 15_java-db
      • Db 工具类
      • java‑db
      • 操作数据库入门示例
      • SQL 模板 (SqlTemplates)
      • 数据源配置与使用
      • ActiveRecord
      • Db 工具类
      • 批量操作
      • Model
      • Model生成器
      • 注解
      • 异常处理
      • 数据库事务处理
      • Cache 缓存
      • Dialect 多数据库支持
      • 表关联操作
      • 复合主键
      • Oracle 支持
      • Enjoy SQL 模板
      • 整合 Enjoy 模板最佳实践
      • 多数据源支持
      • 独立使用 ActiveRecord
      • 调用存储过程
      • java-db 整合 Guava 的 Striped 锁优化
      • 生成 SQL
      • 通过实体类操作数据库
      • java-db 读写分离
      • Spring Boot 整合 Java-DB
      • like 查询
      • 常用操作示例
      • Druid 监控集成指南
      • SQL 统计
      • Db 与 PostgreSQL 业务实践
      • 查询资源自动管理
      • Row 展示转换与表格导出
      • 支付回执与权益事务
      • 商业库存与按量履约事务
    • 16 · api-table

      • 16_api-table
      • ApiTable 概述
      • 使用 ApiTable 连接 SQLite
      • 使用 ApiTable 连接 Mysql
      • 使用 ApiTable 连接 Postgres
      • 使用 ApiTable 连接 TDEngine
      • 使用 api-table 连接 oracle
      • 使用 api-table 连接 mysql and tdengine 多数据源
      • EasyExcel 导出
      • EasyExcel 导入
      • ApiTable 的权限与业务边界
      • ApiTable 联调与故障定位
      • ApiTable 实现增删改查
      • 数组类型
      • 单独使用 ApiTable
      • TQL(Table SQL)前端输入规范
    • 17 · MyBatis

      • 17_mybatis
      • Tio-Boot 整合 MyBatis
      • 使用配置类方式整合 MyBatis
      • 整合数据源
      • 使用 mybatis-plus 整合 tdengine
      • 整合 mybatis-plus
    • 18 · jOOQ

      • 18_jooq
      • 使用配置类方式整合 jOOQ
      • tio-boot + jOOQ 事务管理
      • 批量操作与性能优化
      • 整合agroal
      • 代码生成与类型安全
      • 基于 Record / POJO 增删改查
      • UPSERT、批量更新、返回主键与高级 SQL
      • 的多表关联查询、DTO 投影、聚合统计与视图封装
      • 的窗口函数、CTE、JSON 查询与 PostgreSQL 高级 SQL 实战
      • tio-boot + jOOQ 的审计字段、乐观锁、数据权限与企业级 Repository 设计
      • 测试策略、SQL 日志、性能诊断与生产排障
      • 多租户、读写分离与多数据源设计
      • 代码生成治理、数据库迁移与团队协作规范实战
    • 19 · PostgreSQL

      • 19_postgresql
      • PostgreSQL 安装
      • PostgreSQL 主键自增
      • PostgreSQL 日期类型
      • Postgresql 金融类型
      • PostgreSQL 数组类型
      • 索引
      • PostgreSQL 查询优化
      • 获取字段类型
      • PostgreSQL 全文检索
      • PostgreSQL 向量
      • PostgreSQL 优化向量查询
      • PostgreSQL 其他
    • 20 · MySQL

      • 20_mysql
      • 使用 Docker 运行 MySQL
      • 常见问题
    • 21 · OceanBase

      • 21_oceanbase
      • 快速体验 OceanBase 社区版
      • 快速上手 OceanBase 数据库单机部署与管理
      • 诊断集群性能
      • 优化 SQL 性能指南
      • 待定
    • 22 · Oracle

      • 22_oracle
      • Oracle
    • 23 · SQL Server

      • 23_sqlserver
      • SQL Server
    • 24 · SQLite

      • 24_sqlite
      • SQLite
    • 25 · MongoDB

      • 25_mongodb
      • tio-boot 使用 mongo-java-driver 操作 mongodb
    • 26 · Elasticsearch

      • 26_elasticsearch
      • Elasticsearch
      • JavaDB 整合 ElasticSearch
      • Elastic 工具类使用指南
      • Elastic-search 注意事项
      • ES 课程示例文档
  • 缓存与消息队列

    • 27 · Cache

      • 27_cache
      • Caffeine
      • CacheUtils 工具类
      • 使用 java-db 整合 ehcache
    • 28 · Redis

      • 28_redis
      • 使用 Docker 安装 Redis
      • 使用 java-db 整合 Redis
      • Java DB Redis 相关 Api
      • redis 使用示例
      • 和 RedisTemplate 协作
      • 使用 Jedis 连接池接入 Redis
      • hutool RedisDS
      • Redisson
      • Caffeine 与 Redis 两级缓存
      • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
    • 29 · 消息队列

      • 29_mq
      • Mica-mqtt
      • EMQX
      • Disruptor
      • RocketMQ 安装(Docker Compose)
      • tio-boot 整合 RocketMQ
      • RocketMQ 即时通讯实战
    • 30 · Kafka

      • 30_kafka
      • Kafka
      • AWS MSK
  • 认证与账号体系

    • 31 · 认证与权限

      • 31_authentication
      • FixedTokenInterceptor
      • TokenManager
      • 数据表
      • 匿名登录
      • 个人中心
      • 权限校验注解
      • Sa-Token
      • sa-token 登录注册
      • StpUtil.isLogin() 源码解析
    • 32 · 第三方登录注册

      • 32_third-party-auth
      • 邮箱登录和注册
      • 邮箱重置密码
      • 腾讯云短信登录注册
      • 腾讯云短信重置密码
      • 阿里云短信登录和注册
      • 阿里云短信重置密码
      • 微信登录与绑定手机号
      • 支付宝登录与绑定手机号
      • 微信小程序手机号快捷登录
      • Google登录
      • 阿里云邮件推送验证邮箱
  • 网络通信

    • 33 · AIO

      • 33_aio
      • ByteBuffer
      • AIO HTTP 服务器
      • 自定义和线程池和池化 ByteBuffer
      • AioHttpServer 应用示例 IP 属地查询
      • 手写 AIO Http 服务器
      • Java 21 中的虚拟线程与 AIO
    • 34 · t-io

      • 34_tio
      • 认识 t-io

        • t-io 核心优势与应用价值
        • t-io 消息处理流程
      • 快速上手

        • TioBootServer
        • 独立端口启动 TCP 服务器
        • 内置 TCP 处理器
        • 独立启动 UDPServer
        • 使用内置 UDPServer
      • 核心概念

        • TioConfig
        • ChannelContext
        • Packet
        • Tio 工具类
      • 消息与文件传输

        • 发送数据
        • HTTP 长连接与高效文件传输
        • 使用 AsynchronousSocketChannel 响应数据
      • 连接管理

        • 业务数据绑定
        • 业务数据解绑
        • 关闭连接
        • 资源共享
        • 成员排序
        • 拉黑 IP
      • 加密通信

        • SSL
        • Https建立连接过程
      • 心跳与监控

        • 监控: 心跳
        • 监控: 客户端的流量数据
        • 监控: 单条 TCP 连接的流量数据
        • 监控: 端口的流量数据
        • 单条通道统计: ChannelStat
        • 所有通道统计: GroupStat
      • 深入原理

        • tio-运行原理详解
        • DecodeRunnable
        • t-io 稳定性设计与资源管理
        • 深入解析 Tio 源码:构建高性能 Java 网络应用
        • HTTP、WebSocket 与 TCP 的缓冲区复用
    • 35 · tio-http-server

      • 35_tio-http-server
      • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
      • tio-boot 添加 HttpRequestHandler
      • 在 Android 上使用 tio-boot 运行 HTTP 服务
      • tio-http-server-native
      • handler 常用操作
      • tio-http-server 与 tio-boot 的使用边界
    • 36 · tio-websocket

      • 36_tio-websocket
      • WebSocket 服务器
      • WebSocket Client
      • TCP 数据转发
    • 37 · Netty

      • 37_netty
      • Netty TCP Server
      • Netty Web Socket Server
      • 使用 protoc 生成 Java 包文件
      • Netty WebSocket Server 二进制数据传输
      • Netty 组件详解
    • 38 · netty-boot

      • 38_netty-boot
      • Netty-Boot
      • 原理解析
      • 整合 Hot Reload
      • 整合 数据库
      • 整合 Redis
      • 整合 Elasticsearch
      • 整合 Dubbo
      • Listener
      • 文件上传
      • 拦截器
      • Spring Boot 整合 Netty-Boot
      • SSL 配置指南
      • ChannelInitializer
      • Reserve
  • 集成与扩展

    • 39 · 第三方集成

      • 39_integrations
      • 整合 okhttp
      • 整合 GrpahQL
      • 集成 Mailjet
      • 整合 ip2region
      • 整合 GeoLite 离线库
      • 整合 Lark 机器人指南
      • 集成 Lark Mail 实现邮件发送
      • Thymeleaf
      • Swagger
      • Clerk 验证
      • 集成datadog
    • 40 · Magic Script

      • 40_magic-script
      • tio-boot 与 magic-script 集成指南
    • 41 · Groovy

      • 41_groovy
      • tio-boot 整合 Groovy
      • 调试常用脚本
    • 42 · 爬虫

      • 42_crawling
      • jsoup
      • 爬取 z-lib.io 数据
      • 整合 WebMagic
      • WebMagic 示例:爬取学校课程数据
      • Playwright
      • Flexmark (Markdown 处理器)
      • tio-boot 整合 Playwright
      • 缓存网页数据
    • 43 · Dubbo

      • 43_dubbo
      • 概述
      • dubbo 2.6.0
      • dubbo 2.6.0 调用过程
      • dubbo 3.2.0
    • 44 · Spring

      • 44_spring
      • Spring Boot Web 整合 Tio Boot
      • spring-boot-starter-webflux 整合 tio-boot
      • tio-boot 整合 spring-boot-starter
      • Tio Boot 整合 Spring Boot Starter db
      • Tio Boot 整合 Spring Boot Starter Data Redis 指南
    • 45 · Spring Cloud

      • 45_spring-cloud
      • tio-boot spring-cloud
    • 46 · Quarkus

      • 46_quarkus
      • Quarkus(无 HTTP)整合 tio-boot(有 HTTP)
      • tio-boot + Quarkus + Hibernate ORM Panache
      • tio-boot + Quarkus + Hibernate ORM Panache + jOOQ 整合方案
    • 47 · Telegram4J

      • 47_telegram4j
      • 数据库设计
      • 基于 HTTP 协议开发 Telegram 翻译机器人
      • 基于 MTProto 协议开发 Telegram 翻译机器人
      • 过滤旧消息
      • 保存机器人消息
      • 定时推送
      • 增加命令菜单
      • 使用 telegram-Client
      • 使用自定义 StoreLayout
      • 延迟测试
      • Reactor 错误处理
      • Telegram4J 常见错误处理指南
      • 处理回调查询
      • Reactor
      • 文档翻译
      • 使用 Tio-Boot 整合 tdlight
      • tio-boot 整合 TelegramBots
      • tio-boot 整合 Telegram-Bot-Utils
      • Telegram-Bot-Utils 使用指南
    • 48 · Telegram Bots

      • 48_telegram-bots
      • TelegramBots 入门指南
      • 使用工具库 telegram-bot-base 开发翻译机器人
    • 49 · 文件存储

      • 49_file-storage
      • 文件上传数据表
      • 本地存储
      • 存储到 亚马逊 S3
      • 存储到 Cloudflare R2
      • 存储到 腾讯 COS
      • 上传文件到阿里云 OSS
      • 上传文件到 MinIO
    • 50 · 支付

      • 支付集成
      • 微信小程序支付:普通支付
      • 微信支付:Native 扫码支付(PC 网页扫码)
      • 支付宝:电脑网站支付接入指南
      • 支付宝:使用 page.pay 二维码前置模式实现网页内扫码支付
  • Firebase 与 Clerk

    • 51 · Firebase

      • 51_firebase
      • 整合 google firebase
      • Firebase Storage
      • Firebase Authentication
      • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
      • 导出用户
      • 登录注册
      • 注册回调
    • 52 · Clerk

      • 52_clerk
      • Clerk
  • 多媒体

    • 53 · 音视频处理

      • 53_media
      • JAVE 提取视频中的声音
      • Jave 提取视频中的图片
      • 待定
    • 54 · 语音识别

      • 54_asr
      • Whisper-JNI
    • 55 · 语音合成

      • 55_tts
    • 56 · 文字识别

      • 56_ocr
    • 57 · Native Media

      • 57_native-media
      • java-native-media
      • JNI 入门示例
      • mp3 拆分
      • mp4 转 mp3
      • 使用 libmp3lame 实现高质量 MP3 编码
      • Linux 编译
      • macOS 编译
      • 从 JAR 包中加载本地库文件
      • 支持的音频和视频格式
      • 任意格式转为 mp3
      • 通用格式转换
      • 通用格式拆分
      • 视频合并
      • VideoToHLS
      • split_video_to_hls 支持其他语言
      • 持久化 HLS 会话
      • 获取视频长度
      • 保存视频的最后一帧
      • 添加水印
      • linux版本
    • 58 · 计算机视觉

      • 58_computer-vision
      • 使用 Java 运行 YOLOv8 ONNX 模型进行目标检测
      • tio-boot整合yolo
      • ONNX Runtime 推理说明
      • Paddle Structure
      • tio-boot 整合 Paddle Structure
      • tio-boot整合Paddle Structure 提取图片
      • U2Net 图片去背景原理
      • tio-boot 整合 U2Net 实现图片去背景
  • AI 开发

    • 59 · java-openai

      • 59_java-openai
      • 简介
      • 流式生成
      • 图片多模态输入
      • Google Gemini接入
      • google Vertex AI 接入
      • 模型查询
      • 按业务档位选模型
      • OpenAI、Claude 与 DeepSeek 联网搜索
      • 为单次生成配置独立传输策略
      • GiteeClient 文档解析与图片 OCR
      • WhisperClient 语音识别
      • Perplexity API
      • SupadataClient 获取视频字幕
      • BailianTTSClient 语音合成
      • UniChatClient 与调用方 HTTP 客户端
    • 60 · AI Agent

      • 60_ai-agent
      • 数据库设计
      • 示例问题管理
      • 会话管理
      • 历史记录
      • 意图识别
      • 智能问答
      • 文件上传与解析文档
      • 翻译
      • 名人搜索功能实现
      • Ai studio gemini youbue 问答使用说明
      • 自建 YouTube 字幕问答系统
      • 自建 获取 youtube 字幕服务
      • 使用 OpenAI ASR 实现语音识别接口(Java 后端示例)
      • 定向搜索
      • 16
      • 17
      • 18
      • 在 tio-boot 应用中整合 ai-agent
      • 接口文档
      • 自定义 ChatAskService
      • 请求记录
      • 限流和错误处理
      • 增强检索(RAG)
      • 结构化数据检索
      • AI 问答
      • 连接代码执行器
      • 待定
      • 模型编程能力评测
      • 音频会话 SDP 示例
    • 61 · 知识库

      • Java MossKB 知识库文档
      • 学术论文
      • 数据库设计
      • 用户登录实现
      • 模型管理
      • 知识库管理
      • 文档拆分
      • 片段向量
      • 命中测试
      • 文档管理
      • 片段管理
      • 问题管理
      • 应用管理
      • 向量检索
      • 推理问答
      • 问答模块
      • 统计分析
      • 用户管理
      • VL模型文档解析方案和费用对比
      • 文档解析模型实测
      • 文档解析优化
      • 片段汇总
      • 段落分块与检索
      • 多文档解析
      • 检索性能优化
      • 对话日志
      • 创建独立用户与账号资源管理
      • api 管理
      • 独立对话页
      • 嵌入第三方web系统
      • 分段预览
      • 文档预览
      • 多轮检索
      • 上下文压缩
      • 支持自定义模型
      • 爬取网页数据
      • 隔离 Python 执行环境配置
      • 隔离 Python 执行器
      • 函数库与调用接口
      • Windows 与 Linux 部署
      • 离线运行向量模型
      • Milvus
      • 存储文件到云存储
      • 文档解析异步化
    • 62 · AI 搜索

      • 62_ai-search
      • ai-search 项目简介
      • ai-search 数据库文档
      • ai-search SearxNG 搜索引擎
      • ai-search Jina Reader API
      • ai-search Jina Search API
      • ai-search 搜索、重排与读取内容
      • ai-search PDF 文件处理
      • ai-search 推理问答
      • Google Custom Search JSON API
      • ai-search 意图识别
      • ai-search 问题重写
      • ai-search 系统 API 接口 WebSocket 版本
      • ai-search 搜索代码实现 WebSocket 版本
      • ai-search 生成建议问
      • ai-search 生成问题标题
      • ai-search 历史记录
      • Discover API
      • 翻译
      • Tavily Search API 文档
      • 对接 Tavily Search
      • 火山引擎 DeepSeek
      • 对接 火山引擎 DeepSeek
      • ai-search 搜索代码实现 SSE 版本
      • jar 包部署
      • Docker 部署
      • 爬取一个静态网站的所有数据
      • 网页数据预处理
      • 网页数据检索与问答流程整合
    • 63 · 语音 Agent

      • 63_voice-agent
      • 整合Gemini realtime模型
      • Voice Agent 前端接入接口文档
      • 整合千问realtime模型
      • 打断支持
      • 主动介入
      • eleven labs
      • 基于 tio-boot + ElevenLabs 构建实时语音 Agent(支持打断与主动介入)
    • 64 · AI Coding

      • 64_ai-coding
      • Cline 提示词
      • Cline 提示词-中文版本
      • tio-boot 调用 Agent CLI:agent-cli-sdk
    • 65 · AI Browser

      • deepseek-browser-use:从入门到源码
      • deepseek-browser-use:概念与学习路线
      • 安装、启动与健康检查
      • 第一个任务:打开页面、读取结果与关闭
      • 客户端:dsb 命令行与结构化留档
      • 统一命令接口与人机协作
      • 浏览器、profile 与登录态
      • 窗口尺寸与页面视口
      • 接入智能体:观察、执行与验证
      • 表单与多层弹窗排障:防止重复提交
      • 调用追踪、页面留档与文件上传
      • 站点配方、技能与异步作业
      • Windows OCR:本地图片与页面文字识别
      • 配置项与运维自省
      • 命令清单
      • 源码教程:从 HTTP 请求到命令执行
      • DOM 原理:前端如何生成可交互快照
      • DOM 原理:Java 模型与跨 Frame 索引
      • 正文提取与上层结构化处理
      • 源码教程:生命周期、导航与页签
      • 源码教程:DOM、页面状态与元素读取
      • 源码教程:点击、输入、键盘与鼠标
      • 源码教程:等待条件与 JavaScript 执行
      • 源码教程:文件上传、截图、PDF 与 OCR
      • 源码教程:Cookie、存储与页面设置
      • 源码教程:网络记录、请求拦截与控制台
      • 源码教程:原生对话框、DOM 弹窗与人机协作
      • 源码教程:批量、配方、后台作业与维护
      • 源码教程:Chrome 走 CDP 与 CDP 客户端
      • 请求响应关联与线程约束
      • DeepSeek Harness 插件开发入门:从入口到第一个工具
      • 开发 dsh-plugin-deepseek-browser-use:HTTP 客户端与基础工具
      • 开发 dsh-plugin-deepseek-browser-use:会话、作业、文件与图片
      • 把 dsh-plugin-deepseek-browser-use 构建并安装到 DeepSeek Harness
      • DeepSeek Harness 原生浏览器插件:安装、配置与使用
      • DeepSeek Harness 原生浏览器插件:源码、生命周期与测试
      • 后端自动管理:安装 Java 与 Git、更新源码、编译和启动
      • 页面正在导航与主 Frame 缺失:PAGE_NAVIGATING
      • dsb 客户端:Go 版本、多目标与后端服务管理
      • 可靠的数据采集:上下文校验、UTF-8 留档与证据口径
      • 浏览器 profile:列表、克隆与选择
  • 项目实战

    • 66 · java-uni-ai-server

      • 66_java-uni-ai-server
      • 语音合成系统
      • Fish.audio TTS 接口说明文档与 Java 客户端封装
      • 整合 fishaudio 到 java-uni-ai-server 项目
      • 待定
    • 67 · java-llm-proxy

      • 67_java-llm-proxy
      • 使用tio-boot搭建多模型LLM代理服务
    • 68 · java-kit-server

      • 68_java-kit-server
      • Java 执行 python 代码
      • 通过大模型执行 Python 代码
      • 执行 Python (Manim) 代码
      • 待定
      • 待定
      • 待定
      • 视频下载增加水印说明文档
    • 69 · tio-im

      • 69_tio-im
      • 通讯协议文档
      • ChatPacket.proto 文档
      • java protobuf
      • 数据表设计
      • 创建工程
      • 登录
      • 历史消息
      • 发消息
    • 70 · tio-mail-wing

      • 70_tio-mail-wing
      • tio-mail-wing简介
      • 任务1:实现POP3系统
      • 使用 getmail 验证 tio-mail-wing POP3 服务
      • 任务2:实现 SMTP 服务
      • 数据库初始化文档
      • 用户管理
      • 邮件管理
      • 任务3:实现 SMTP 服务 数据库版本
      • 任务4:实现 POP3 服务(数据库版本)
      • IMAP 协议
      • 拉取多封邮件
      • 任务5:实现 IMAP 服务(数据库版本)
      • IMAP实现讲解
      • IMAP 手动测试脚本
      • IMAP 认证机制
      • 主动推送
      • namesapce
      • CONDSTORE and QRESYNC
    • 71 · tio-mcp-server

      • 71_tio-mcp-server
      • 实现 MCP Server 开发指南
      • MCP 协议
      • /zh/71_tio-mcp-server/11.html
    • 72 · tio-log-server

      • 72_tio-log-server
      • 简介
      • 收集 docker 日志
      • 入库
    • 73 · tio-sip

      • 73_tio-sip
      • SIP Server 第一版原理说明
      • SIP Server 第一版实战
      • 一、Windows 平台测试
      • SIP Server 第二版实战
      • SIP Server 第三版实战
      • 性能优化
      • 基于 MediaProcessor 对接 Realtime 模型说明
      • 对接大语言模型
      • 支持 G722 宽带语音
      • G722编码和解码
      • 会话级采样率转换
      • 增加 9196 回声测试分机
      • 语音系统链路说明
      • 一、Gemini Realtime 的打断机制
    • 74 · tio-boot-admin

      • 74_tio-boot-admin
      • 入门指南:使用框架内置配置
      • 手动初始化数据库
      • 配置职责、生效条件与扩展边界
      • 整合数据库
      • 与前端集成
      • 文件上传
      • 网络请求
      • 单图片管理(只读模式)
      • 多图片管理
      • 布尔值管理
      • 字段联动
      • Word 管理
      • PDF 管理
      • 文章管理
      • 富文本编辑器
      • 整合 Enjoy 模版引擎
      • 历史可选方案:Token 存储与 Sa-Token
      • 业务 API 与 H5 / 小程序联调
      • 方法路由与业务鉴权
      • 整合 Redis
      • 整合 Elasticsearch
      • 后端开发规范:tio-boot、java-db 与 Kv
      • 多表实现文件数据存储
      • 受限事项与原操作闭环
      • 具名业务后台与受控命令集成
      • 固定用途查询、受控导出与删除证明
      • 登录与认证
    • 75 · 案例

      • 75_examples
      • 封装 IP 查询服务
      • tio-boot 案例 - 全局异常捕获与企业微信群通知
      • tio-boot 案例 - 文件上传和下载
      • tio-boot 案例 - 整合 ant design pro 增删改查
      • tio-boot 案例 - 流失响应
      • tio-boot 案例 - 增强检索
      • tio-boot 案例 - 整合 function call
      • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
      • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
      • tio-boot 案例 - 执行 shell 命令
      • /zh/75_examples/11.html
      • /zh/75_examples/12.html
      • /zh/75_examples/13.html
  • 性能、原理与源码

    • 76 · 性能测试

      • 76_performance
      • tio-boot 为什么运行更快:从请求链路到内存复用
      • 压力测试 - tio-http-serer
      • 压力测试 - tio-boot
      • 压力测试 - tio-boot-native
      • 压力测试 - netty-boot
      • 性能测试对比
      • TechEmpower FrameworkBenchmarks
      • 压力测试 - tio-boot 12 C 32G
      • HTTP/1.1 Pipelining 性能测试报告
      • tio-boot vs Quarkus 性能对比测试报告
    • 77 · 原理

      • 77_internals
      • 生命周期
      • 请求处理流程
      • 重要的类
    • 78 · 源码解析

      • 78_source-code
      • 源码阅读入口
      • Swagger 整合到 Tio-Boot 中的指南
      • 启动与关闭生命周期
      • HTTP 请求分发与路由优先级
      • 高性能网络编程中的 ByteBuffer 分配与回收策略
      • TioBootServerHandler 源码解析
      • HTTP 请求与连接生命周期

RocketMQ 即时通讯实战

本文用一个可运行的工程,把 tio-boot 整合 RocketMQ 的各项能力串成即时通讯(IM)的消息链路。即时通讯后端通常分成"业务服务"和"WebSocket 连接节点"两部分:业务服务负责落库和计算推送对象,连接节点负责把消息写到用户的长连接上。两者之间用 RocketMQ 解耦以后,业务服务可以水平扩容,连接节点也可以随在线人数增减,互不影响。

本文实现的链路:

  1. 发送消息:HTTP 接口生成消息 ID,发到 im_msg_send_topic(带 Key,方便按消息 ID 查询)。
  2. 扇出:MsgSendListener 集群消费,计算接收者所在节点,把推送发到该节点专属的 im_push_{节点ID}。
  3. 定向推送:每个连接节点只订阅自己的 im_push_${app.node-id},收到后写到本地连接。
  4. ACK 与重推:推送同时登记"在途",并发送一条延迟消息;3 秒后检查,仍未 ACK 的用户再推一次,最多 3 次。
  5. 上线广播:上线事件用广播消费,每个节点都能更新"用户在哪个节点"的路由表。
  6. 租户透传:发送钩子和消费钩子把租户 ID 放在消息属性中,跨多跳自动传递。
  7. 可靠发送:消息和本地消息表在同一个事务内写入,提交后发送;发送失败由定时任务重试。

所有代码都在 tio-boot 中实际运行过,结果见文末"运行验证"。RocketMQ 服务端安装见 RocketMQ 安装(Docker Compose)。

依赖与配置

  <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.3.3</version>
  </dependency>
  <dependency>
    <groupId>nexus.io</groupId>
    <artifactId>tio-boot</artifactId>
    <version>${tio-boot.version}</version>
  </dependency>
  <dependency>
    <groupId>nexus.io</groupId>
    <artifactId>tio-boot-admin-base</artifactId>
    <version>${tio-boot-admin.version}</version>
  </dependency>
  <dependency>
    <groupId>nexus.io</groupId>
    <artifactId>jfinal-aop</artifactId>
    <version>${jfinal-aop.version}</version>
  </dependency>
  <dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.52</version>
  </dependency>
  <dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>5.5.0</version>
  </dependency>
  <dependency>
    <groupId>nexus.io</groupId>
    <artifactId>java-db</artifactId>
    <version>${java-db.version}</version>
  </dependency>
  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.5.0</version>
  </dependency>

app.properties:

server.port=8088
app.node-id=1

rocketmq.name-server=127.0.0.1:9876
rocketmq.producer.group=im_producer_group
rocketmq.producer.send-message-timeout=3000
rocketmq.producer.retry-times-when-send-failed=2
rocketmq.producer.compress-message-body-threshold=4096
rocketmq.producer.max-message-size=4194304

jdbc.url=jdbc:postgresql://127.0.0.1:5433/im_demo
jdbc.user=postgres
jdbc.pswd=your-password

多节点部署时,每个连接节点配置不同的 app.node-id。

工程结构:

com.example.im
├── ImApp.java
├── config      RocketMqConfiguration、HttpRouteConfiguration、GlobalExceptionHandler、DbConfiguration、OutboxRetryConfiguration
├── handler     SendMessageHandler、ReliableSendMessageHandler、AckHandler、OnlineHandler、InboxHandler
├── hook        TenantContext、TenantSendMessageHook、TenantConsumeMessageHook
├── listener    MsgSendListener、NodePushListener、RetryPushListener、AckListener、UserOnlineListener
├── model       ImTopics、ChatMessage、NodePush、AckMessage、OnlineEvent
├── service     ChatService、PushService、OnlineService、OutboxService、ReliableChatService
└── utils       ParameterValidationUtils

启动类

package com.example.im;

import nexus.io.annotation.AComponentScan;
import nexus.io.tio.boot.TioApplication;

@AComponentScan
public class ImApp {
  public static void main(String[] args) {
    TioApplication.run(ImApp.class, args);
  }
}

Topic 与消息模型

Topic 名称集中定义。推送 Topic 只定义前缀,节点 ID 在注解中用占位符补全:

package com.example.im.model;

public interface ImTopics {
  String MSG_SEND = "im_msg_send_topic";
  String MSG_SEND_GROUP = "im_msg_send_group";

  String MSG_ACK = "im_msg_ack_topic";
  String MSG_ACK_GROUP = "im_msg_ack_group";

  /** One topic per WebSocket node: im_push_1, im_push_2 ... */
  String PUSH_PREFIX = "im_push_";
  String PUSH_GROUP_PREFIX = "im_push_group_";

  String PUSH_RETRY = "im_push_retry_topic";
  String PUSH_RETRY_GROUP = "im_push_retry_group";

  String USER_ONLINE = "im_user_online_topic";
  String USER_ONLINE_GROUP = "im_user_online_group";
}

ChatMessage 是客户端发来的聊天消息:

package com.example.im.model;

import java.util.List;

public class ChatMessage {
  private Long msgId;
  private Long roomId;
  private Long fromUid;
  private List<Long> toUids;
  private String content;

  public Long getMsgId() {
    return msgId;
  }

  public void setMsgId(Long msgId) {
    this.msgId = msgId;
  }

  public Long getRoomId() {
    return roomId;
  }

  public void setRoomId(Long roomId) {
    this.roomId = roomId;
  }

  public Long getFromUid() {
    return fromUid;
  }

  public void setFromUid(Long fromUid) {
    this.fromUid = fromUid;
  }

  public List<Long> getToUids() {
    return toUids;
  }

  public void setToUids(List<Long> toUids) {
    this.toUids = toUids;
  }

  public String getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = content;
  }
}

NodePush 是发给某个节点的推送,attempt 记录第几次推送:

package com.example.im.model;

import java.util.List;

public class NodePush {
  private Long msgId;
  private List<Long> uids;
  private String content;
  private int attempt;

  public NodePush() {
  }

  public NodePush(Long msgId, List<Long> uids, String content, int attempt) {
    this.msgId = msgId;
    this.uids = uids;
    this.content = content;
    this.attempt = attempt;
  }

  public Long getMsgId() {
    return msgId;
  }

  public void setMsgId(Long msgId) {
    this.msgId = msgId;
  }

  public List<Long> getUids() {
    return uids;
  }

  public void setUids(List<Long> uids) {
    this.uids = uids;
  }

  public String getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = content;
  }

  public int getAttempt() {
    return attempt;
  }

  public void setAttempt(int attempt) {
    this.attempt = attempt;
  }
}

AckMessage 是客户端回执:

package com.example.im.model;

public class AckMessage {
  private Long msgId;
  private Long uid;

  public Long getMsgId() {
    return msgId;
  }

  public void setMsgId(Long msgId) {
    this.msgId = msgId;
  }

  public Long getUid() {
    return uid;
  }

  public void setUid(Long uid) {
    this.uid = uid;
  }
}

OnlineEvent 是上线事件:

package com.example.im.model;

public class OnlineEvent {
  private Long uid;
  private String nodeId;

  public OnlineEvent() {
  }

  public OnlineEvent(Long uid, String nodeId) {
    this.uid = uid;
    this.nodeId = nodeId;
  }

  public Long getUid() {
    return uid;
  }

  public void setUid(Long uid) {
    this.uid = uid;
  }

  public String getNodeId() {
    return nodeId;
  }

  public void setNodeId(String nodeId) {
    this.nodeId = nodeId;
  }
}

租户上下文与钩子

接口入口把请求头里的租户 ID 放进 TenantContext,发送钩子写入消息属性,消费钩子在监听器执行前还原。监听器中再发出的消息会被发送钩子继续带上,所以"接口 → 扇出 → 节点推送"三跳都能拿到同一个租户。

package com.example.im.hook;

public class TenantContext {
  private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();

  public static void set(String tenantId) {
    TENANT_ID.set(tenantId);
  }

  public static String get() {
    return TENANT_ID.get();
  }

  public static void clear() {
    TENANT_ID.remove();
  }
}
package com.example.im.hook;

import org.apache.rocketmq.client.hook.SendMessageContext;
import org.apache.rocketmq.client.hook.SendMessageHook;

/** Copies the tenant id of the current thread into every outgoing message. */
public class TenantSendMessageHook implements SendMessageHook {

  public static final String HEADER_TENANT_ID = "tenantId";

  @Override
  public String hookName() {
    return "TenantSendMessageHook";
  }

  @Override
  public void sendMessageBefore(SendMessageContext context) {
    String tenantId = TenantContext.get();
    if (tenantId != null) {
      context.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId);
    }
  }

  @Override
  public void sendMessageAfter(SendMessageContext context) {
  }
}
package com.example.im.hook;

import org.apache.rocketmq.client.hook.ConsumeMessageContext;
import org.apache.rocketmq.client.hook.ConsumeMessageHook;
import org.apache.rocketmq.common.message.MessageExt;

/** Restores the tenant id before the listener runs and clears it afterwards. */
public class TenantConsumeMessageHook implements ConsumeMessageHook {

  @Override
  public String hookName() {
    return "TenantConsumeMessageHook";
  }

  @Override
  public void consumeMessageBefore(ConsumeMessageContext context) {
    MessageExt message = context.getMsgList().get(0);
    String tenantId = message.getUserProperty(TenantSendMessageHook.HEADER_TENANT_ID);
    if (tenantId != null) {
      TenantContext.set(tenantId);
    }
  }

  @Override
  public void consumeMessageAfter(ConsumeMessageContext context) {
    TenantContext.clear();
  }
}

注册监听器

TioAdminRocketMqConfig 读取 rocketmq.* 配置,注册钩子和监听器并启动;没有配置 rocketmq.name-server 时直接跳过。

package com.example.im.config;

import com.example.im.hook.TenantConsumeMessageHook;
import com.example.im.hook.TenantSendMessageHook;
import com.example.im.listener.AckListener;
import com.example.im.listener.MsgSendListener;
import com.example.im.listener.NodePushListener;
import com.example.im.listener.RetryPushListener;
import com.example.im.listener.UserOnlineListener;

import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.boot.admin.config.TioAdminRocketMqConfig;

@AConfiguration
public class RocketMqConfiguration {

  @Initialization
  public void config() {
    new TioAdminRocketMqConfig()
        .addSendMessageHook(new TenantSendMessageHook())
        .addConsumeMessageHook(new TenantConsumeMessageHook())
        .addListener(new MsgSendListener())
        .addListener(new NodePushListener())
        .addListener(new RetryPushListener())
        .addListener(new AckListener())
        .addListener(new UserOnlineListener())
        .config();
  }
}

发送消息

ChatService 生成雪花 ID,把消息 ID 设为 Key 发送。接口很快返回,后续扇出与推送都在消费端异步完成:

package com.example.im.service;

import com.example.im.model.ChatMessage;
import com.example.im.model.ImTopics;

import nexus.io.tio.boot.extension.rocketmq.RocketMq;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessage;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

public class ChatService {

  /** Saves nothing in this demo; publishes the message and returns its id. */
  public long send(ChatMessage message) {
    long msgId = SnowflakeIdUtils.id();
    message.setMsgId(msgId);
    RocketMq.getTemplate().send(ImTopics.MSG_SEND, RocketMqMessage.of(message).keys(msgId));
    return msgId;
  }
}

扇出到节点

MsgSendListener 集群消费,多个业务服务实例分摊处理。它把消息转成 NodePush,发到接收者所在节点的 Topic,并安排重推检查。示例只有一个节点;多节点时先通过上线路由表查出每个用户所在的节点,再按节点分组发送。

package com.example.im.listener;

import com.example.im.model.ChatMessage;
import com.example.im.model.ImTopics;
import com.example.im.model.NodePush;
import com.example.im.service.PushService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.extension.rocketmq.RocketMqListener;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessageListener;

@RocketMqMessageListener(topic = ImTopics.MSG_SEND, consumerGroup = ImTopics.MSG_SEND_GROUP)
public class MsgSendListener implements RocketMqListener<ChatMessage> {

  @Override
  public void onMessage(ChatMessage message) {
    PushService pushService = Aop.get(PushService.class);
    NodePush push = new NodePush(message.getMsgId(), message.getToUids(), message.getContent(), 1);
    // This demo runs one node; a cluster looks up the node of each user and groups by node id.
    pushService.pushToNode(pushService.currentNodeId(), push);
    pushService.trackAndScheduleRetry(push);
  }
}

节点定向推送

每个节点只订阅自己的 Topic。${app.node-id} 在启动时从配置中解析,节点 1 订阅 im_push_1,节点 2 订阅 im_push_2,推送不会发到无关的节点:

package com.example.im.listener;

import com.example.im.model.ImTopics;
import com.example.im.model.NodePush;
import com.example.im.service.PushService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.extension.rocketmq.RocketMqListener;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessageListener;

/** Every node only subscribes to its own push topic, e.g. im_push_1. */
@RocketMqMessageListener(topic = ImTopics.PUSH_PREFIX + "${app.node-id}",
    consumerGroup = ImTopics.PUSH_GROUP_PREFIX + "${app.node-id}", maxReconsumeTimes = 3)
public class NodePushListener implements RocketMqListener<NodePush> {

  @Override
  public void onMessage(NodePush push) {
    Aop.get(PushService.class).deliver(push);
  }
}

推送、ACK 与延迟重推

PushService 负责三件事:

  • pushToNode:发到节点 Topic。
  • trackAndScheduleRetry:为每个接收者登记"在途",再用延迟级别 2(Broker 配置 messageDelayLevel=1s 3s 5s ... 时为 3 秒)发一条检查消息。
  • retryIfNotAcked:检查时间到,仍在途的用户再推一次,并重新安排检查;达到 3 次后放弃,等客户端上线后拉取离线消息。

示例的在途表和收件箱放在内存中。多实例部署时在途表要放到 Redis 这类共享存储中,各节点看到同一份状态。

package com.example.im.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.example.im.hook.TenantContext;
import com.example.im.model.ImTopics;
import com.example.im.model.NodePush;

import nexus.io.tio.boot.extension.rocketmq.RocketMq;
import nexus.io.tio.utils.environment.EnvUtils;

public class PushService {
  private static final Logger log = LoggerFactory.getLogger(PushService.class);

  /** Delay level 2 = 3s with messageDelayLevel=1s 3s 5s ... */
  private static final int RETRY_DELAY_LEVEL = 2;
  private static final int MAX_ATTEMPTS = 3;

  // Demo storage. Use Redis sets in production so every node sees the same state.
  private static final Map<String, Boolean> pending = new ConcurrentHashMap<>();
  private static final Map<Long, List<String>> inbox = new ConcurrentHashMap<>();

  public String currentNodeId() {
    return EnvUtils.get("app.node-id", "1");
  }

  /** Sends a push to the topic of the node that holds the users' connections. */
  public void pushToNode(String nodeId, NodePush push) {
    RocketMq.getTemplate().send(ImTopics.PUSH_PREFIX + nodeId, push);
  }

  /** Marks the message as in flight and schedules a check after the delay. */
  public void trackAndScheduleRetry(NodePush push) {
    for (Long uid : push.getUids()) {
      pending.put(key(uid, push.getMsgId()), Boolean.TRUE);
    }
    RocketMq.getTemplate().syncSend(ImTopics.PUSH_RETRY, push, 3000, RETRY_DELAY_LEVEL);
  }

  /** Called by the retry listener: pushes again to users that have not acked yet. */
  public void retryIfNotAcked(NodePush push) {
    List<Long> notAcked = new ArrayList<>();
    for (Long uid : push.getUids()) {
      if (pending.containsKey(key(uid, push.getMsgId()))) {
        notAcked.add(uid);
      }
    }
    if (notAcked.isEmpty()) {
      log.info("msgId={} acked by all users, no retry", push.getMsgId());
      return;
    }
    if (push.getAttempt() >= MAX_ATTEMPTS) {
      log.warn("msgId={} still not acked by {} after {} attempts, give up", push.getMsgId(), notAcked,
          push.getAttempt());
      return;
    }
    NodePush retry = new NodePush(push.getMsgId(), notAcked, push.getContent(), push.getAttempt() + 1);
    log.info("msgId={} not acked by {}, push again, attempt={}", push.getMsgId(), notAcked, retry.getAttempt());
    pushToNode(currentNodeId(), retry);
    trackAndScheduleRetry(retry);
  }

  /** Delivers to local connections. A real server writes to the WebSocket channel here. */
  public void deliver(NodePush push) {
    for (Long uid : push.getUids()) {
      inbox.computeIfAbsent(uid, k -> new CopyOnWriteArrayList<>())
          .add(push.getMsgId() + ":" + push.getContent() + "#" + push.getAttempt());
    }
    log.info("node={} delivered msgId={} to {}, attempt={}, tenant={}", currentNodeId(), push.getMsgId(),
        push.getUids(), push.getAttempt(), TenantContext.get());
  }

  public void ack(Long uid, Long msgId) {
    pending.remove(key(uid, msgId));
  }

  public List<String> inbox(Long uid) {
    List<String> list = inbox.get(uid);
    if (list == null) {
      return new ArrayList<>();
    }
    return new ArrayList<>(list);
  }

  private static String key(Long uid, Long msgId) {
    return uid + ":" + msgId;
  }
}

延迟检查的消费者:

package com.example.im.listener;

import com.example.im.model.ImTopics;
import com.example.im.model.NodePush;
import com.example.im.service.PushService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.extension.rocketmq.RocketMqListener;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessageListener;

@RocketMqMessageListener(topic = ImTopics.PUSH_RETRY, consumerGroup = ImTopics.PUSH_RETRY_GROUP, maxReconsumeTimes = 3)
public class RetryPushListener implements RocketMqListener<NodePush> {

  @Override
  public void onMessage(NodePush push) {
    Aop.get(PushService.class).retryIfNotAcked(push);
  }
}

客户端收到消息后调用 ACK 接口,ACK 经 MQ 回到业务服务,删除在途记录:

package com.example.im.listener;

import com.example.im.model.AckMessage;
import com.example.im.model.ImTopics;
import com.example.im.service.PushService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.extension.rocketmq.RocketMqListener;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessageListener;

@RocketMqMessageListener(topic = ImTopics.MSG_ACK, consumerGroup = ImTopics.MSG_ACK_GROUP)
public class AckListener implements RocketMqListener<AckMessage> {

  @Override
  public void onMessage(AckMessage ack) {
    Aop.get(PushService.class).ack(ack.getUid(), ack.getMsgId());
  }
}

上线广播

上线事件用广播消费,每个节点都收到,各自维护"用户 → 节点"的路由表。消费组相同、实例不同的节点都会收到同一条事件:

package com.example.im.service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.example.im.model.ImTopics;
import com.example.im.model.OnlineEvent;

import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.extension.rocketmq.RocketMq;

public class OnlineService {

  // uid -> node id, filled on every node by the broadcasting listener.
  private static final Map<Long, String> userNodes = new ConcurrentHashMap<>();

  public void online(Long uid) {
    String nodeId = Aop.get(PushService.class).currentNodeId();
    RocketMq.getTemplate().send(ImTopics.USER_ONLINE, new OnlineEvent(uid, nodeId));
  }

  public void onOnline(OnlineEvent event) {
    userNodes.put(event.getUid(), event.getNodeId());
  }

  public String nodeOf(Long uid) {
    return userNodes.get(uid);
  }
}
package com.example.im.listener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.example.im.model.ImTopics;
import com.example.im.model.OnlineEvent;
import com.example.im.service.OnlineService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.extension.rocketmq.RocketMqListener;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessageListener;
import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;

/** BROADCASTING: every node of the group receives every online event. */
@RocketMqMessageListener(topic = ImTopics.USER_ONLINE, consumerGroup = ImTopics.USER_ONLINE_GROUP,
    messageModel = MessageModel.BROADCASTING)
public class UserOnlineListener implements RocketMqListener<OnlineEvent> {
  private static final Logger log = LoggerFactory.getLogger(UserOnlineListener.class);

  @Override
  public void onMessage(OnlineEvent event) {
    Aop.get(OnlineService.class).onOnline(event);
    log.info("user {} is online on node {}", event.getUid(), event.getNodeId());
  }
}

HTTP 接口

参数校验封装在工具类中,内部使用 ParameterValidator:

package com.example.im.utils;

import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.utils.validator.ParameterValidator;

public class ParameterValidationUtils {

  public static String requestBody(HttpRequest request, int maxLength) {
    String body = request.getBodyString();
    ParameterValidator.require(body != null && !body.isEmpty(), "请求体不能为空");
    ParameterValidator.require(body.length() <= maxLength, "请求体过大");
    return body;
  }

  public static void required(Object value, String message) {
    ParameterValidator.require(value != null, message);
  }
}

POST /api/im/send,从请求头 x-tenant-id 取租户:

package com.example.im.handler;

import com.example.im.hook.TenantContext;
import com.example.im.model.ChatMessage;
import com.example.im.service.ChatService;
import com.example.im.utils.ParameterValidationUtils;

import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.json.JsonUtils;

/** POST /api/im/send */
public class SendMessageHandler {
  public HttpResponse handle(HttpRequest request) {
    String bodyString = ParameterValidationUtils.requestBody(request, 16384);
    ChatMessage input = JsonUtils.parse(bodyString, ChatMessage.class);
    ParameterValidationUtils.required(input.getToUids(), "toUids 不能为空");
    ParameterValidationUtils.required(input.getContent(), "content 不能为空");

    TenantContext.set(request.getHeader("x-tenant-id"));
    try {
      ChatService chatService = Aop.get(ChatService.class);
      long msgId = chatService.send(input);
      return TioRequestContext.getResponse().respond(RespBodyVo.ok(String.valueOf(msgId)));
    } finally {
      TenantContext.clear();
    }
  }
}

POST /api/im/ack:

package com.example.im.handler;

import com.example.im.model.AckMessage;
import com.example.im.model.ImTopics;
import com.example.im.utils.ParameterValidationUtils;

import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.extension.rocketmq.RocketMq;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.json.JsonUtils;

/** POST /api/im/ack — the client confirms that a message arrived. */
public class AckHandler {
  public HttpResponse handle(HttpRequest request) {
    String bodyString = ParameterValidationUtils.requestBody(request, 1024);
    AckMessage ack = JsonUtils.parse(bodyString, AckMessage.class);
    ParameterValidationUtils.required(ack.getMsgId(), "msgId 不能为空");
    ParameterValidationUtils.required(ack.getUid(), "uid 不能为空");
    RocketMq.send(ImTopics.MSG_ACK, ack);
    return TioRequestContext.getResponse().respond(RespBodyVo.ok());
  }
}

POST /api/im/online:

package com.example.im.handler;

import com.example.im.service.OnlineService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.validator.ParameterValidator;

/** POST /api/im/online?uid=1 */
public class OnlineHandler {
  public HttpResponse handle(HttpRequest request) {
    long uid = ParameterValidator.id(request.getParam("uid"), "uid");
    Aop.get(OnlineService.class).online(uid);
    return TioRequestContext.getResponse().respond(RespBodyVo.ok());
  }
}

GET /api/im/inbox,查看某个用户在本节点收到的推送,用于观察效果:

package com.example.im.handler;

import com.example.im.service.OnlineService;
import com.example.im.service.PushService;

import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.validator.ParameterValidator;

/** GET /api/im/inbox?uid=1 — what has been pushed to this user on this node. */
public class InboxHandler {
  public HttpResponse handle(HttpRequest request) {
    long uid = ParameterValidator.id(request.getParam("uid"), "uid");
    String node = Aop.get(OnlineService.class).nodeOf(uid);
    String data = JsonUtils.toJson(Aop.get(PushService.class).inbox(uid));
    return TioRequestContext.getResponse().respond(RespBodyVo.ok("node=" + node + " " + data));
  }
}

全局异常处理器把 ParameterValidationException 转成 400:

package com.example.im.config;

import nexus.io.model.body.RespBodyVo;
import nexus.io.model.exception.ParameterValidationException;
import nexus.io.tio.boot.exception.TioBootExceptionHandler;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.websocket.common.WebSocketRequest;

public class GlobalExceptionHandler implements TioBootExceptionHandler {

  @Override
  public Object handler(HttpRequest request, Throwable e) {
    if (e instanceof ParameterValidationException) {
      TioRequestContext.getResponse().setStatus(400);
      return RespBodyVo.fail(e.getMessage());
    }
    return null;
  }

  @Override
  public Object wsTextHandler(WebSocketRequest webSocketRequest, String text, ChannelContext channelContext,
      HttpRequest httpRequest, Throwable e) {
    return null;
  }

  @Override
  public Object wsBytesHandler(WebSocketRequest webSocketRequest, byte[] bytes, ChannelContext channelContext,
      HttpRequest httpRequest, Throwable e) {
    return null;
  }
}

注册路由:

package com.example.im.config;

import com.example.im.handler.AckHandler;
import com.example.im.handler.InboxHandler;
import com.example.im.handler.OnlineHandler;
import com.example.im.handler.ReliableSendMessageHandler;
import com.example.im.handler.SendMessageHandler;

import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.router.HttpRequestRouter;

@AConfiguration
public class HttpRouteConfiguration {

  @Initialization
  public void config() {
    TioBootServer.me().setExceptionHandler(new GlobalExceptionHandler());
    HttpRequestRouter r = TioBootServer.me().getRequestRouter();
    r.post("/api/im/send", new SendMessageHandler()::handle);
    r.post("/api/im/send-reliable", new ReliableSendMessageHandler()::handle);
    r.post("/api/im/ack", new AckHandler()::handle);
    r.post("/api/im/online", new OnlineHandler()::handle);
    r.get("/api/im/inbox", new InboxHandler()::handle);
  }
}

事务提交后发送:本地消息表

"先写库再发 MQ"有两个问题:在事务内发送,事务回滚了消息却已经发出;在事务后发送,发送失败消息就丢了。本地消息表(Outbox)的做法是把待发送的消息和业务数据写在同一个事务里,提交后立即发送,发送成功删除记录,失败的由定时任务重试。只要事务提交成功,消息最终一定会发出。

数据源与表结构:

package com.example.im.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.db.activerecord.ActiveRecordPlugin;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.dialect.PostgreSqlDialect;
import nexus.io.hook.HookCan;
import nexus.io.tio.utils.environment.EnvUtils;

@AConfiguration
public class DbConfiguration {

  private static final String SCHEMA = """
      CREATE TABLE IF NOT EXISTS im_message (
        id BIGINT PRIMARY KEY,
        room_id BIGINT,
        from_uid BIGINT,
        content TEXT NOT NULL,
        create_time TIMESTAMP NOT NULL DEFAULT now()
      );
      CREATE TABLE IF NOT EXISTS mq_outbox (
        id BIGINT PRIMARY KEY,
        topic VARCHAR(128) NOT NULL,
        msg_keys VARCHAR(128),
        body TEXT NOT NULL,
        retry_count INT NOT NULL DEFAULT 0,
        next_retry_time TIMESTAMP NOT NULL,
        create_time TIMESTAMP NOT NULL DEFAULT now()
      );
      """;

  @Initialization
  public void config() {
    String jdbcUrl = EnvUtils.get("jdbc.url");
    if (jdbcUrl == null) {
      return;
    }
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(jdbcUrl);
    config.setUsername(EnvUtils.get("jdbc.user"));
    config.setPassword(EnvUtils.get("jdbc.pswd"));
    config.setMaximumPoolSize(EnvUtils.getInt("jdbc.MaximumPoolSize", 4));
    HikariDataSource dataSource = new HikariDataSource(config);
    HookCan.me().addDestroyMethod(dataSource::close);

    ActiveRecordPlugin arp = new ActiveRecordPlugin(dataSource);
    arp.setDialect(new PostgreSqlDialect());
    arp.start();
    HookCan.me().addDestroyMethod(arp::stop);

    Db.update(SCHEMA);
  }
}

OutboxService 写入、发送和重试本地消息。消息体已经是 JSON 字符串,用 String 原样发送,消费端按 ChatMessage 解析:

package com.example.im.service;

import java.sql.Timestamp;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.tio.boot.extension.rocketmq.RocketMq;
import nexus.io.tio.boot.extension.rocketmq.RocketMqMessage;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

/**
 * Transactional outbox: the outbox row is written in the business transaction, the message is
 * sent after commit, and rows that failed to send are retried by a scheduled task.
 */
public class OutboxService {
  private static final Logger log = LoggerFactory.getLogger(OutboxService.class);

  private static final int MAX_RETRY = 10;
  private static final long RETRY_INTERVAL_MILLIS = 60_000L;

  /** Must be called inside Db.tx(...). Returns the outbox id. */
  public long add(String topic, Object keys, Object payload) {
    long id = SnowflakeIdUtils.id();
    Row row = Row.by("id", id)
        .set("topic", topic)
        .set("msg_keys", keys == null ? null : keys.toString())
        .set("body", JsonUtils.toJson(payload))
        .set("next_retry_time", new Timestamp(System.currentTimeMillis() + RETRY_INTERVAL_MILLIS));
    Db.save("mq_outbox", row);
    return id;
  }

  /** Sends one outbox row; deletes it on success, pushes the next retry time on failure. */
  public boolean trySend(long outboxId) {
    Row row = Db.findById("mq_outbox", outboxId);
    if (row == null) {
      return true;
    }
    try {
      RocketMq.getTemplate().send(row.getStr("topic"),
          RocketMqMessage.of(row.getStr("body")).keys(row.getStr("msg_keys")));
      Db.deleteById("mq_outbox", outboxId);
      return true;
    } catch (Exception e) {
      int retryCount = row.getInt("retry_count") + 1;
      long next = System.currentTimeMillis() + RETRY_INTERVAL_MILLIS * retryCount;
      String sql = """
          UPDATE mq_outbox SET retry_count = ?, next_retry_time = ? WHERE id = ?
          """;
      Db.update(sql, retryCount, new Timestamp(next), outboxId);
      log.warn("outbox {} send failed, retry {}", outboxId, retryCount, e);
      return false;
    }
  }

  /** Called periodically: resends rows whose retry time has come. */
  public int retryDue() {
    String sql = """
        SELECT id FROM mq_outbox
        WHERE next_retry_time <= now() AND retry_count < ?
        ORDER BY next_retry_time
        LIMIT 100
        """;
    List<Row> rows = Db.find(sql, MAX_RETRY);
    int sent = 0;
    for (Row row : rows) {
      if (trySend(row.getLong("id"))) {
        sent++;
      }
    }
    return sent;
  }
}

业务服务在同一个 Db.tx 中写消息表和本地消息表,提交后立即尝试发送:

package com.example.im.service;

import com.example.im.model.ChatMessage;
import com.example.im.model.ImTopics;

import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

public class ReliableChatService {

  /** Stores the message and its outbox row in one transaction, then sends after commit. */
  public long send(ChatMessage message) {
    OutboxService outboxService = Aop.get(OutboxService.class);
    long msgId = SnowflakeIdUtils.id();
    message.setMsgId(msgId);
    long[] outboxId = new long[1];
    Db.tx(() -> {
      Row row = Row.by("id", msgId)
          .set("room_id", message.getRoomId())
          .set("from_uid", message.getFromUid())
          .set("content", message.getContent());
      Db.save("im_message", row);
      outboxId[0] = outboxService.add(ImTopics.MSG_SEND, msgId, message);
      return true;
    });
    // Committed: the message is either sent now or later by the retry task.
    outboxService.trySend(outboxId[0]);
    return msgId;
  }
}

定时重试:

package com.example.im.config;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.example.im.service.OutboxService;

import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.hook.HookCan;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.utils.environment.EnvUtils;

@AConfiguration
public class OutboxRetryConfiguration {
  private static final Logger log = LoggerFactory.getLogger(OutboxRetryConfiguration.class);

  @Initialization
  public void config() {
    if (EnvUtils.get("jdbc.url") == null) {
      return;
    }
    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.scheduleWithFixedDelay(() -> {
      try {
        int sent = Aop.get(OutboxService.class).retryDue();
        if (sent > 0) {
          log.info("outbox resent {} messages", sent);
        }
      } catch (Exception e) {
        log.error("outbox retry failed", e);
      }
    }, 10, 10, TimeUnit.SECONDS);
    HookCan.me().addDestroyMethod(scheduler::shutdown);
  }
}

POST /api/im/send-reliable:

package com.example.im.handler;

import com.example.im.model.ChatMessage;
import com.example.im.service.ReliableChatService;
import com.example.im.utils.ParameterValidationUtils;

import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.json.JsonUtils;

/** POST /api/im/send-reliable */
public class ReliableSendMessageHandler {
  public HttpResponse handle(HttpRequest request) {
    String bodyString = ParameterValidationUtils.requestBody(request, 16384);
    ChatMessage input = JsonUtils.parse(bodyString, ChatMessage.class);
    ParameterValidationUtils.required(input.getToUids(), "toUids 不能为空");
    ParameterValidationUtils.required(input.getContent(), "content 不能为空");
    long msgId = Aop.get(ReliableChatService.class).send(input);
    return TioRequestContext.getResponse().respond(RespBodyVo.ok(String.valueOf(msgId)));
  }
}

消费端可能因为重试收到同一条消息两次,业务处理按消息 ID 做幂等即可。

运行验证

先用 mqadmin 创建 5 个 Topic(im_msg_send_topic、im_push_1、im_push_retry_topic、im_msg_ack_topic、im_user_online_topic),再启动应用。启动日志中能看到生产者和 5 个消费者,im_push_${app.node-id} 已解析成 im_push_1,上线事件是广播模式:

RocketMQ producer started, group=im_producer_group, nameServer=127.0.0.1:9876
RocketMQ consumer started, topic=im_msg_send_topic, group=im_msg_send_group, model=CLUSTERING, mode=CONCURRENTLY, listener=com.example.im.listener.MsgSendListener
RocketMQ consumer started, topic=im_push_1, group=im_push_group_1, model=CLUSTERING, mode=CONCURRENTLY, listener=com.example.im.listener.NodePushListener
RocketMQ consumer started, topic=im_push_retry_topic, group=im_push_retry_group, model=CLUSTERING, mode=CONCURRENTLY, listener=com.example.im.listener.RetryPushListener
RocketMQ consumer started, topic=im_msg_ack_topic, group=im_msg_ack_group, model=CLUSTERING, mode=CONCURRENTLY, listener=com.example.im.listener.AckListener
RocketMQ consumer started, topic=im_user_online_topic, group=im_user_online_group, model=BROADCASTING, mode=CONCURRENTLY, listener=com.example.im.listener.UserOnlineListener

上线广播

curl -X POST 'http://127.0.0.1:8088/api/im/online?uid=1001'
user 1001 is online on node 1

推送、ACK 与重推

用户 1001 给 2001 和 2002 发消息,租户 88:

curl -X POST http://127.0.0.1:8088/api/im/send \
  -H 'x-tenant-id: 88' -H 'Content-Type: application/json' \
  -d '{"roomId":1,"fromUid":1001,"toUids":[2001,2002],"content":"hello"}'
{"msg":null,"error":null,"data":"706430829423689728","code":1,"ok":true}

只让 2001 回执:

curl -X POST http://127.0.0.1:8088/api/im/ack -H 'Content-Type: application/json' \
  -d '{"msgId":706430829423689728,"uid":2001}'

日志:

17:03:54.609 node=1 delivered msgId=706430829423689728 to [2001, 2002], attempt=1, tenant=88
17:03:57.643 msgId=706430829423689728 not acked by [2002], push again, attempt=2
17:03:57.648 node=1 delivered msgId=706430829423689728 to [2002], attempt=2, tenant=88
17:04:00.748 msgId=706430829423689728 not acked by [2002], push again, attempt=3
17:04:00.753 node=1 delivered msgId=706430829423689728 to [2002], attempt=3, tenant=88
17:04:03.852 WARN msgId=706430829423689728 still not acked by [2002] after 3 attempts, give up

可以看到:

  • 消息经过"接口 → 扇出 → 节点推送"三跳,租户 88 始终存在。
  • 延迟级别 2 的检查消息恰好 3 秒后到达。
  • 已回执的 2001 不再重推,未回执的 2002 重推到第 3 次后停止。

收件箱:

curl 'http://127.0.0.1:8088/api/im/inbox?uid=2002'
{"msg":null,"error":null,"data":"node=null [\"706430829423689728:hello#1\",\"706430829423689728:hello#2\",\"706430829423689728:hello#3\"]","code":1,"ok":true}

参数缺失时返回 400:

curl -X POST http://127.0.0.1:8088/api/im/send -H 'Content-Type: application/json' -d '{"content":"x"}'
{"msg":"toUids 不能为空","error":null,"data":null,"code":0,"ok":false}

在控制台按 Key 查询

消息 ID 就是 Key。在控制台 Message 页选择 Message Key,输入 Topic 和消息 ID 即可查到:

按 Key 查询消息

消息详情中能看到 KEYS、发送钩子写入的 tenantId=88、JSON 消息体,以及 im_msg_send_group 已消费:

消息详情

Broker 不可用时的可靠发送

先停止 Broker,再调用可靠发送接口:

docker stop rocketmq-broker
curl -X POST http://127.0.0.1:8088/api/im/send-reliable -H 'Content-Type: application/json' \
  -d '{"roomId":2,"fromUid":1001,"toUids":[3002],"content":"survives-broker-down"}'

接口照常返回消息 ID,消息已入库,本地消息表留下一条待发送记录:

outbox rows|1|1

重新启动 Broker:

docker start rocketmq-broker

到达重试时间后,定时任务补发,消息正常推送,本地消息表清空:

17:08:42.687 outbox 706432037784834048 send failed, retry 1
17:09:42.851 outbox resent 1 messages
17:09:42.853 node=1 delivered msgId=706432037780639744 to [3002], attempt=1, tenant=null
outbox rows|0
Edit this page
Last Updated: 11/1/26, 4:26 AM
Contributors: Tong Li
Prev
tio-boot 整合 RocketMQ