Mongo错误修复 - WiredTiger metadata corruption detected
免责声明本文提供的所有信息仅供参考，作者不对使用本文所含信息而导致的任何直接或间接损失负责。在进行MongoDB数据恢复操作之前，请务必备份您的数据并仔细阅读MongoDB官方文档。数据恢复操作具有风险，可能导致数据的永久丢失或损坏。作者建议在执行任何数据恢复步骤之前，先在测试环境中进行尝试，并咨询有经验的数据库管理员或技术专家。背景系统: macOS 13.6Mongo: 5.0.3 (Docker 容器部署)原因: 由于Docker Desktop在Mac上运行消耗资源非常高，所以又安装了OrbStack进行docker管理。 在OrbStack运行时，意外的打开了Docker Desktop（噩耗的开始）。 刚开始没在意，中午吃饭发现系统刷新失败了！？ 果然还是出问题了。782c0bd0dfb4 mongo:5.0.3 "docker-entrypoint.s…" 6 weeks ago Restarting (14) 46 seconds ago mongodbmongodb的容器一直在重启，于是看看log:{"t":{"$date":"2024-08-01T06:15:22.352+00:00"},"s":"I", "c":"NETWORK", "id":4915701, "ctx":"-","msg":"Initialized wire specification","attr":{"spec":{"incomingExternalClient":{"minWireVersion":0,"maxWireVersion":13},"incomingInternalClient":{"minWireVersion":0,"maxWireVersion":13},"outgoing":{"minWireVersion":0,"maxWireVersion":13},"isInternalClient":true}}} {"t":{"$date":"2024-08-01T06:15:22.352+00:00"},"s":"I", "c":"CONTROL", "id":23285, "ctx":"-","msg":"Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'"} {"t":{"$date":"2024-08-01T06:15:22.354+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"} {"t":{"$date":"2024-08-01T06:15:22.355+00:00"},"s":"I", "c":"NETWORK", "id":4648601, "ctx":"main","msg":"Implicit TCP FastOpen unavailable. If TCP FastOpen is required, set tcpFastOpenServer, tcpFastOpenClient, and tcpFastOpenQueueSize."} {"t":{"$date":"2024-08-01T06:15:22.355+00:00"},"s":"W", "c":"ASIO", "id":22601, "ctx":"main","msg":"No TransportLayer configured during NetworkInterface startup"} {"t":{"$date":"2024-08-01T06:15:22.355+00:00"},"s":"I", "c":"REPL", "id":5123008, "ctx":"main","msg":"Successfully registered PrimaryOnlyService","attr":{"service":"TenantMigrationDonorService","ns":"config.tenantMigrationDonors"}} {"t":{"$date":"2024-08-01T06:15:22.355+00:00"},"s":"I", "c":"REPL", "id":5123008, "ctx":"main","msg":"Successfully registered PrimaryOnlyService","attr":{"service":"TenantMigrationRecipientService","ns":"config.tenantMigrationRecipients"}} {"t":{"$date":"2024-08-01T06:15:22.356+00:00"},"s":"I", "c":"CONTROL", "id":4615611, "ctx":"initandlisten","msg":"MongoDB starting","attr":{"pid":1,"port":27017,"dbPath":"/data/db","architecture":"64-bit","host":"2f51c27fb1c7"}} {"t":{"$date":"2024-08-01T06:15:22.356+00:00"},"s":"I", "c":"CONTROL", "id":23403, "ctx":"initandlisten","msg":"Build Info","attr":{"buildInfo":{"version":"5.0.3","gitVersion":"657fea5a61a74d7a79df7aff8e4bcf0bc742b748","openSSLVersion":"OpenSSL 1.1.1f 31 Mar 2020","modules":[],"allocator":"tcmalloc","environment":{"distmod":"ubuntu2004","distarch":"x86_64","target_arch":"x86_64"}}}} {"t":{"$date":"2024-08-01T06:15:22.356+00:00"},"s":"I", "c":"CONTROL", "id":51765, "ctx":"initandlisten","msg":"Operating System","attr":{"os":{"name":"Ubuntu","version":"20.04"}}} {"t":{"$date":"2024-08-01T06:15:22.356+00:00"},"s":"I", "c":"CONTROL", "id":21951, "ctx":"initandlisten","msg":"Options set by command line","attr":{"options":{"net":{"bindIp":"*"}}}} {"t":{"$date":"2024-08-01T06:15:22.357+00:00"},"s":"I", "c":"STORAGE", "id":22270, "ctx":"initandlisten","msg":"Storage engine to use detected by data files","attr":{"dbpath":"/data/db","storageEngine":"wiredTiger"}} {"t":{"$date":"2024-08-01T06:15:22.357+00:00"},"s":"I", "c":"STORAGE", "id":22315, "ctx":"initandlisten","msg":"Opening WiredTiger","attr":{"config":"create,cache_size=3455M,session_max=33000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),builtin_extension_config=(zstd=(compression_level=6)),file_manager=(close_idle_time=600,close_scan_interval=10,close_handle_minimum=250),statistics_log=(wait=0),verbose=[recovery_progress,checkpoint_progress,compact_progress],"}} {"t":{"$date":"2024-08-01T06:15:22.776+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:776094][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 637: unable to read root page from file:WiredTiger.wt: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.776+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:776189][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 643: WiredTiger has failed to open its metadata: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.776+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:776210][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 646: This may be due to the database files being encrypted, being from an older version or due to corruption on disk: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.776+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:776232][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 649: You should confirm that you have opened the database with the correct options including all encryption and compression options: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.783+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:783621][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 637: unable to read root page from file:WiredTiger.wt: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.783+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:783671][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 643: WiredTiger has failed to open its metadata: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.783+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:783683][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 646: This may be due to the database files being encrypted, being from an older version or due to corruption on disk: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.783+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:783688][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 649: You should confirm that you have opened the database with the correct options including all encryption and compression options: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.790+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:790000][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 637: unable to read root page from file:WiredTiger.wt: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.790+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:790045][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 643: WiredTiger has failed to open its metadata: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.790+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:790057][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 646: This may be due to the database files being encrypted, being from an older version or due to corruption on disk: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.790+00:00"},"s":"E", "c":"STORAGE", "id":22435, "ctx":"initandlisten","msg":"WiredTiger error","attr":{"error":-31802,"message":"[1722492922:790063][1:0x7f18c7605c80], file:WiredTiger.wt, connection: __wt_btree_tree_open, 649: You should confirm that you have opened the database with the correct options including all encryption and compression options: WT_ERROR: non-specific WiredTiger error"}} {"t":{"$date":"2024-08-01T06:15:22.791+00:00"},"s":"W", "c":"STORAGE", "id":22347, "ctx":"initandlisten","msg":"Failed to start up WiredTiger under any compatibility version. This may be due to an unsupported upgrade or downgrade."} {"t":{"$date":"2024-08-01T06:15:22.791+00:00"},"s":"W", "c":"STORAGE", "id":22348, "ctx":"initandlisten","msg":"WiredTiger metadata corruption detected"} {"t":{"$date":"2024-08-01T06:15:22.791+00:00"},"s":"F", "c":"STORAGE", "id":50944, "ctx":"initandlisten","msg":"Please read the documentation for starting MongoDB with --repair here: http://dochub.mongodb.org/core/repair"} {"t":{"$date":"2024-08-01T06:15:22.791+00:00"},"s":"F", "c":"-", "id":23091, "ctx":"initandlisten","msg":"Fatal assertion","attr":{"msgid":50944,"file":"src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp","line":685}} {"t":{"$date":"2024-08-01T06:15:22.791+00:00"},"s":"F", "c":"-", "id":23092, "ctx":"initandlisten","msg":"\n\n***aborting after fassert() failure\n\n"} 关键信息： Failed to start up WiredTiger under any compatibility version. This may be due to an unsupported upgrade or downgrade. WiredTiger metadata corruption detected Please read the documentation for starting MongoDB with --repair here: http://dochub.mongodb.org/core/repair1、"WiredTiger metadata corruption detected" 原因清楚了，存储引擎的元数据坏掉了。在 MongoDB 中，WiredTiger 是一个用于存储引擎的名字。WiredTiger 引擎提供了高效的数据压缩和并发处理能力，从 MongoDB 3.2 版本开始成为默认的存储引擎。WiredTiger 文件实际上包含了数据文件和索引文件。下面是一些有关 WiredTiger 文件的关键点： 存储引擎：WiredTiger 是 MongoDB 的存储引擎，它负责管理数据库的数据存储、索引、并发控制和缓存等方面。 数据文件：在 MongoDB 数据库目录中，WiredTiger 存储引擎会创建一系列文件，用于存储数据和索引。这些文件包括 .wt 后缀的文件。 配置文件：WiredTiger.wt 是一个主要的配置文件，它包含了有关存储引擎配置和元数据的信息。 日志文件：WiredTiger 还使用一组日志文件（如 WiredTigerLog 开头的文件）来进行写入操作的日志记录，以便在发生崩溃时进行数据恢复。 压缩和并发：WiredTiger 提供了数据压缩功能，可以显著减少存储空间需求。此外，它还支持多线程并发写入，从而提高数据库性能。 简而言之，WiredTiger 是 MongoDB 用于管理和存储数据的核心组件，提供了高效的数据存储和管理能力。2、"http://dochub.mongodb.org/core/repair"问题发生时系统没有数据写入，所以我们数据应该没问题，根据提示我们进行修复:mongod --dbpath /data/db --repair修复由于在容器中部署的Mongo，不可能在这个容器中进行修复，所以重新起一个同版本的容器进行修复。步骤：备份数据确定Mongo 版本启动修复Mongo的容器"fix-mongo"拷贝数据到"fix-mongo"并修复从"fix-mongo"导出修复好的数据数据备份zip -r bad_mongodata_yyyyMMdd.zip /your/mount/path/mongodata确定Mongo版本 docker inspect your_container |grep 'MONGO_VERSION' "MONGO_VERSION=5.0.3"我这里是"5.0.3"启动修复Mongo的容器"fix-mongo"docker run --name fix-mongo -d mongo:your_mongo_version拷贝数据到"fix-mongo"并修复将整个数据目录拷贝到"fix-mongo"的"/tmp"## 拷贝数据到容器 docker cp /your/mount/path/mongodata fix-mongo:/tmp ## 修复 - 等待结束 mongod --dbpath /tmp/mongodata --repair ## 修复的部分log ... {"t":{"$date":"2024-08-01T06:47:54.435+00:00"},"s":"I", "c":"STORAGE", "id":22319, "ctx":"initandlisten","msg":"Finished shutting down session sweeper thread"} {"t":{"$date":"2024-08-01T06:47:54.451+00:00"},"s":"I", "c":"STORAGE", "id":4795902, "ctx":"initandlisten","msg":"Closing WiredTiger","attr":{"closeConfig":"leak_memory=true,"}} {"t":{"$date":"2024-08-01T06:47:54.693+00:00"},"s":"I", "c":"STORAGE", "id":4795901, "ctx":"initandlisten","msg":"WiredTiger closed","attr":{"durationMillis":242}} {"t":{"$date":"2024-08-01T06:47:54.693+00:00"},"s":"I", "c":"STORAGE", "id":22279, "ctx":"initandlisten","msg":"shutdown: removing fs lock..."} {"t":{"$date":"2024-08-01T06:47:54.693+00:00"},"s":"I", "c":"-", "id":4784931, "ctx":"initandlisten","msg":"Dropping the scope cache for shutdown"} {"t":{"$date":"2024-08-01T06:47:54.693+00:00"},"s":"I", "c":"FTDC", "id":20626, "ctx":"initandlisten","msg":"Shutting down full-time diagnostic data capture"} {"t":{"$date":"2024-08-01T06:47:54.693+00:00"},"s":"I", "c":"CONTROL", "id":20565, "ctx":"initandlisten","msg":"Now exiting"} {"t":{"$date":"2024-08-01T06:47:54.693+00:00"},"s":"I", "c":"CONTROL", "id":23138, "ctx":"initandlisten","msg":"Shutting down","attr":{"exitCode":0}} ## 退出容器 exit从"fix-mongo"导出修复好的数据## 移除修复前的数据，确保已做好数据备份 rm -rf /your/mount/path/mongodata ## 取回修复好的数据 docker cp fix-mongo:/tmp/mongodata /your/mount/path/mongodata ## 重启你的容器 docker restart your_container结束！数据勤备份、数据勤备份、数据勤备份
摘要Mongo支持与Mysql类似的关联表查询，但比较麻烦。通过与Mysql实现进行对比，便于理解Mongo如何实现。MysqlOrder表关联Product表的id, 实现查询订单支持搜索产品名称。# Order CREATE TABLE `order` ( `id` bigint(20) DEFAULT NULL, `no` varchar(255) DEFAULT NULL, `amount` int(11) DEFAULT NULL COMMENT '单位- 分\n', `productId` bigint(20) DEFAULT NULL, `count` int(11) DEFAULT NULL, `userId` bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of product -- ---------------------------- INSERT INTO `order` (`id`, `no`, `amount`, `productId`, `count`, `userId`) VALUES (1, '2024062701', 10000, 1, 1, 1); INSERT INTO `order` (`id`, `no`, `amount`, `productId`, `count`, `userId`) VALUES (1, '2024062702', 20000, 1, 2, 1); # Product CREATE TABLE `product` ( `id` bigint(20) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `price` int(11) DEFAULT NULL COMMENT '单位-分' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of product -- ---------------------------- INSERT INTO `product` (`id`, `name`, `price`) VALUES (1, '测试产品', 10000); 关联查询查询订单、产品名称，通过left join搜索产品名称SELECT o,p.name from `order` as o left JOIN `product` as p on o.productId = p.id where p.name like '%测试%'; ## 结果 #id no amount productId count userId name 1 2024062701 10000 1 1 1 测试产品 1 2024062702 20000 1 2 1 测试产品Mongo同样在mongo中准备Order和Product的集合## insert to product db.product.insertOne({ name: '测试产品', price: 10000 }); ## result { "_id" : ObjectId("667d7fbe04cfea555206351f"), "name" : "测试产品", "price" : 10000 } ## insert to orders db.order.insertMany([ { no: '2024062701', amount: 10000, productId: "667d7fbe04cfea555206351f", count: 1, userId: 1 }, { no: '2024062702', amount: 20000, productId: "667d7fbe04cfea555206351f", count: 2, userId: 1 } ]); # result { "_id" : ObjectId("667d80b804cfea5552063520"), "no" : "2024062701", "amount" : 10000, "productId" : "667d7fbe04cfea555206351f", "count" : 1, "userId" : 1 } { "_id" : ObjectId("667d80b804cfea5552063521"), "no" : "2024062702", "amount" : 20000, "productId" : "667d7fbe04cfea555206351f", "count" : 2, "userId" : 1 }关联查询使用Mongo提供的$lookup来实现关联查询{ "$lookup": { "from": { "connectionName": "<registered-atlas-connection>", "db": "<registered-database-name>", "coll": "<atlas-collection-name>" }, "localField": "<field-in-source-messages>", "foreignField": "<field-in-from-collection>", "as": "<output-array-field>" } }那么我们使用Mongo查询，应该这样写db.order.aggregate( { $lookup: { from: "product", # 需要关联的product表 localField: "proudctId", # order中的productId foreignField: '_id', # 与proudct中的_id关联 as: "p" # product的别名，出现在查询结果中，默认是一个数组 } }, { $unwind: "$p" }, # 将p拉平，如果p是多个，那么会出现多条记录 # 此时得到 order + p:{} { $match: { "p.name": { # 过滤p.name $regex: ".*测试.*", # 使用正则，模糊查询= '%测试%' $options: "i" # 忽略大小写 } } }) 但是，没有查询到结果！？因为 在order中productId是String,而product._id是ObjectId,他俩不是同一个类型，所以是不等的。解决他俩类型不一样，那么把他俩类型弄成一样再比较！db.order.aggregate( { $lookup: { from: "product", let: { pid: "$productId" }, ## 取order.productId到变量pid中 pipeline: [ ## 在product表操作，将pid变成ObjectId后与order._id进行等于比较 { $match: { $expr: { $eq: ["$_id", { $toObjectId: "$$pid" }] } } } ], as: "p" } }, { $unwind: "$p" }, { $match: { "p.name": { $regex: ".*测试.*", $options: "i" } } }) ## Result // collection: order { "_id" : ObjectId("667d80b804cfea5552063520"), "no" : "2024062701", "amount" : 10000, "productId" : "667d7fbe04cfea555206351f", "count" : 1, "userId" : 1, "p" : { "_id" : ObjectId("667d7fbe04cfea555206351f"), "name" : "测试产品", "price" : 10000 } } // collection: order { "_id" : ObjectId("667d80b804cfea5552063521"), "no" : "2024062702", "amount" : 20000, "productId" : "667d7fbe04cfea555206351f", "count" : 2, "userId" : 1, "p" : { "_id" : ObjectId("667d7fbe04cfea555206351f"), "name" : "测试产品", "price" : 10000 } } lookup 解释1、将order.productId声明一个新的变量：pid2、将order._id与 pid比较，此时的pid转换成了obejctIdAI 解释这段代码是MongoDB聚合管道($aggregate)中的一部分，使用了$lookup阶段来实现集合之间的关联查询。这里是将当前集合（假设为订单或其他与产品相关的集合）与"product"集合进行关联，以获取与每个文档中productId字段对应的产品详细信息。我将逐步解释每一部分的含义： $lookup: 这是MongoDB聚合管道的一个操作符，用于在当前集合的文档中连接来自另一个集合的文档。它基于指定的键执行类似SQL中的左外连接。 - from: "product": 指定要连接的另一个集合名为"product"。这是你想要获取额外数据的集合。 - let: { pid: "$productId" }: 定义了一个变量pid，其值为当前文档中的productId字段。这在后续的pipeline中作为查找依据。 - pipeline: 这是一个数组，包含了在"product"集合上执行的聚合管道操作。这里仅有一个操作： - $match: 用于过滤"product"集合中的文档。它只选择那些满足特定条件的文档。 - $expr: 允许在$match中使用聚合表达式。 - $eq: 检查两个表达式是否相等。在这个例子中，检查"product"集合文档的_id字段是否等于通过$toObjectId转换后的$$pid（即来自外部文档的productId，已经转换为ObjectId类型）。 - as: "p": 指定将连接结果存储在当前文档的新字段p中，作为数组形式存在，包含匹配到的所有"product"集合的文档。 Spring Data Mongo Tempalte 实现 Pageable pageable= "Spring data Pageable'; String name= "测试" // Template不支持let,所以自己实现 AggregationOperation l = context -> new Document("$lookup", new Document("from", "product") .append("let", new Document("pid", "$projectId")) .append("pipeline", Arrays.asList( new Document("$match", new Document("$expr", new Document("$eq", Arrays.asList("$_id", new Document("$toObjectId", "$$pid")))) ) )) .append("as", "p")); UnwindOperation unwindOperation = Aggregation.unwind("p"); Criteria c = new Criteria(); if (StringUtils.isNotBlank(name)) { c.and("p.name").regex(".*" + name + ".*", "i"); } Aggregation aggregation = Aggregation.newAggregation( l, unwindOperation, Aggregation.match(c), Aggregation.count().as("c") ); Document count = MongoUtils.template(dbName).aggregate(aggregation, Order.class, Document.class) .getUniqueMappedResult(); // 没有搜索到结果，这里的count会是null if (count == null) { return PageResult.empty(); } Aggregation pageAggregation = Aggregation.newAggregation( // 这有顺序关联： 先跳过，在limit l, unwindOperation, Aggregation.match(c), Aggregation.skip(pageable.getOffset()), Aggregation.limit(pageable.getPageSize()) ); List<Order> data = MongoUtils.template(dbName).aggregate(pageAggregation, Order.class, Order.class) .getMappedResults(); return Page.of(count.getInteger("c"), data);
遗愿消息 - LWT（Last Will and Testament）根据MQTT协议，在创建链接时可设置遗愿消息（Will Message）。 它属于链接的一个配置信息，当链接意外断开时将发送该消息。包含但不限于以下情况将发送该信息：服务端发现I/O或网络错误；Client未在Keep Alive time规定时间内进行通信；Client关闭网络链接时未发送0x00代码的“DISCONNECT”包 （非正常关闭）；Server 关闭网络链接时未收到0x00代码的“DISCONNECT”包（非正常关闭）；遗愿被储存在服务端并与Session进行关联。遗愿消息存储在CONNECT的payload中，由三部分组成：Will Properties : 遗愿属性，一个自定义map。Will Topic : 遗愿主题。Will Payload fields: 遗愿payload，与普通message的payload一样，可自定义。保留消息 -Retain Message保留消息是指发布Message的retain标识为1，当服务端收到一条保留消息：服务端替换该Topic已有的保留消息；订阅者会立即收到消息；未来的订阅者订阅后会立即收到该消息。即Topic只会保存最新的一条保留信息，已订阅的会立即收到，未来订阅的会在订阅时收到该保留消息。清除Topic中的保留消息:如果保留消息的Payload为空(0字节)，服务端会该Topic中的保留消息进行移除，并且Topic将不会存储该保留消息。遗愿消息+保留消息实现设备状态监测1、订阅者首先启动，订阅device/status2、设备创建连接时设置保留遗愿消息：{"status":"offline"}3、设备链接成功后，立即发送保留消息：{"status":"online"}此时，订阅者会立即收到消息：{"status":"online"} 设备上线场景1: 订阅者重启1、订阅者会立即收到保留消息：{"status":"online"} 此时设备时在线的。场景2: 设备重启1、设备断开，触发遗愿消息：{"status":"offline"} 设备离线2、设备连接，收到上线的保留消息：{"status":"online"}至此，完成设备状态监测。源码 - JavaDevice:import com.alibaba.fastjson2.JSONObject; import com.hivemq.client.mqtt.MqttClient; import com.hivemq.client.mqtt.MqttClientBuilder; import com.hivemq.client.mqtt.MqttClientState; import com.hivemq.client.mqtt.datatypes.MqttQos; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5WillPublish; import java.nio.charset.StandardCharsets; import java.util.Map; /** * publish message * * @author chenchen * @Date 2023/11/17 */ public class Publisher { public static void main(String[] args) { MqttClientBuilder clientBuilder = MqttClient.builder() .identifier("cc-status-monitor") .addConnectedListener((context) -> { MqttClientState clientState = context.getClientConfig().getState(); System.out.println(clientState.isConnected()); }) .addDisconnectedListener(context -> System.out.println(context.getCause().getMessage())) .serverHost("broker.hivemq.com") .serverPort(1883); // will message with retain Mqtt5WillPublish offline = Mqtt5WillPublish.builder() .topic("device/status") .qos(MqttQos.AT_LEAST_ONCE) .retain(false) .payload(JSONObject.toJSONString(Map.of("status", "offline")).getBytes(StandardCharsets.UTF_8)) .build(); // set will message when create connect Mqtt5BlockingClient client = clientBuilder.useMqttVersion5() .willPublish(offline) .buildBlocking(); // connect client.connect(); // success connect, publish a retain massage of online if (client.getState().isConnected()) { Mqtt5WillPublish online = Mqtt5WillPublish.builder() .topic("device/status") .qos(MqttQos.AT_LEAST_ONCE) .retain(true) .payload(JSONObject.toJSONString(Map.of("status", "online")).getBytes(StandardCharsets.UTF_8)) .build(); client.publish(online); } } } Subscribe:import cn.hutool.core.util.StrUtil; import com.hivemq.client.mqtt.MqttClient; import com.hivemq.client.mqtt.MqttClientState; import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient; import com.hivemq.client.mqtt.mqtt5.message.subscribe.Mqtt5Subscribe; import com.hivemq.client.mqtt.mqtt5.message.subscribe.Mqtt5Subscription; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Optional; /** * subscribe * @author chenchen * @Date 2023/11/17 */ public class Subscriber { public static void main(String[] args) { Mqtt5AsyncClient client = MqttClient.builder() .addConnectedListener((context) -> { MqttClientState clientState = context.getClientConfig().getState(); System.out.println(clientState.isConnected()); }) .addDisconnectedListener(context -> System.out.println(context.getCause().getMessage())) .serverHost("broker.hivemq.com") .serverPort(1883) .useMqttVersion5() .buildAsync(); // subscribe topic Mqtt5Subscribe subscribe = Mqtt5Subscribe.builder() .addSubscription(Mqtt5Subscription.builder() .topicFilter("device/status") .build()) .build(); // connect client.connect(); // handle message client.subscribe(subscribe, mqtt5Publish -> { Optional<ByteBuffer> payload = mqtt5Publish.getPayload(); payload.ifPresent(byteBuffer -> System.out.println("receive message with payload:" + StrUtil.str(byteBuffer, StandardCharsets.UTF_8))); }); } }引用《物联网系统开发：从0到1构建IoT平台》付强Will : https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901042Return: https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901104hivemq : https://github.com/hivemq/hivemq-mqtt-clientMqtt Server: broker.hivemq.com
“我认为，过去就是过去了，没有回忆，没有遗憾，没有放不下的人，没有忘不掉的旅行。既往不恋。人之所以感到痛苦，很多时候是因为拿以往和现在做比较。”
背景我们一直从事企业服务，早在2020年通过服务国内的客户，我们就敏感的发现交互技术的变革即将发生，新的交互时代即将来临。我们把自身的专业知识、经验和能力集成到了我们的产品中，希望能帮助客户获得相应的能力，能更好的面对这个伟大的时代！PromptAI为快速对话设计和部署⽽构建的⽆代码开发环境。它基于RASA，但⼜不需要特定的RASA 知识来设计和部署聊天机器⼈。使⽤⾃然语⾔来设计⾃然语⾔对话，它最⼤限度地减少了标注和编码⼯作，旨在⼏分钟内发布企业级聊天机器⼈。有哪些特点？使用友好无需编程, 近似自然语言的方式编辑对话，能写word就能上手免费且拥有所有功能，可上线两个独立的chatbot [真-无任何费用]解决80%以上的对话需求，24小时在线服务开发友好：Web无侵入式部署，移动端完美适配FAQ、多轮、变量提取、Form、Webhook、Goto...免费下载⽣成的RASA源码ChatGPT支持私有部署 [免费版已推出，欢迎联系]适合那些用户？个人: 对chatbot/Rasa感兴趣个人开发者: 提供免费机器人服务，无任何运行及运维成本，一行代码无侵入式集成到您的应用中/小企业用户: 无需专门组建客服团队，已有客户团队的情况下极大降低客服团队工作量。其他: 欢迎联系我们获取专属解决方案.预览近似自然语言的方式编辑对话一键发布，无侵入式部署完美兼容移动端如何注册?https://www.promptai.cn联系我们:email: info@promptai.cnWechat: 官网左下角微信扫码加入群聊我们把我们产品最基本的部分贡献出来，方便企业用户开发、设计交互式对话系统，并在这个基础之上，我们提供了许多针对企业的咨询服务和解决方案。如果想更多的了解我们的产品和服务，请访问我们的网站，期待你的光临！
背景使用联想天逸510S mini(十代 10400)安装黑苹果作为日常生产力工具。 本文参考的天逸510s Mini兼macOS BigSur安装教程，然后记录一些细节问题。配置：(仅贴出与官方配置有差异的地方)类型具体型号内存玖合(JUHOR) 48GB DDR4 3200 笔记本内存条PCIe梵想（FANXIANG）1TB SSD固态硬盘 M.2接口PCIe 4.0 x4SATA移速 金钱豹系列-256GB网卡博通BCM94360Z4准备// U盘 推荐 32 GB // Ventura 镜像 微信公众号 黑果小兵的部落阁 // Ventura 镜像刻录工具 balenaetcher https://www.balena.io/etcher // 小兵开源的EFI https://github.com/daliansky/Lenovo-TianYi-510S-Mini-Hackintosh安装修改Bios配置参考github中的信息.注：如果要关闭CPU的“CFG Lock”，需要先安装仓库中提供的BIOS版本，先进入Windows运行该程序完成BIOS版本替换（我先安装了Windows，然后进行替换，未在PE系统中测试。）制作启动U盘使用balenaetcher将下载的Ventura安装镜像刻录到U盘。 安装过程略。替换U盘中的EFI从小兵开源的EFI仓库Release页面下载最新EFI。 下载完成后，解压替换启动盘中的EFI目录。这一步必须操作，否则无法进入安装界面。注意：目前release中的版本是v2.3.0，从日志看不支持Ventura。 可以直接clone项目，然后手动拷贝EFI目录。[2023/5/3]分区将硬盘格式化为GUID分区，将在ESP分区中创建EFI文件夹，将U盘替换后的EFI拷贝进去，否则无法脱离U盘使用。 -- 这一步一定要在安装前做，实际测试安装后替换无效。安装插入U盘启动进入安装界面，选择“Install macOS Ventura”进行安装：等待一会，顺利的话会进入安装界面：首先选择“磁盘工具-Disk Utility”格式化硬盘格式为“APFS”，如果格式化失败，先格式化为“Mac OS Extended (Hournaled)”，再格式化为“APFS”.接下来退出Disk Utility，选择“Install macOs Ventura”进行安装,选中格式化好的磁盘,进行安装。安装过程会几次重启,等待即可。完成顺利的话会进入配置界面，创建用户等操作，后续即可拔掉U盘使用。总结安装过程折腾了好几天，不过好在有小兵这样的大神提供完整的EFI，减少了很多折腾的时间。非常感谢！这个机器是20年发布的到现在已三年左右，CPU已是前三代的产品。不过作为日常写写代码已满足使用，比起二手的Mac迷你仍有不小的性价比。再加上目前内存、硬盘价格非常划算，300块可买32GB内存条。 比起苹果升级太香了！这个机器购买起来比较折腾，先在淘宝买的“准系统”，然后自购CPU、内存、硬盘及网卡，整个过程还是比较有意思。目前使用下来还体验还不错，Wi-Fi、蓝牙、接力等等都可正常使用，比目前使用的15款的Macbook Pro体验好得多。说一说问题：1、使用Ipad Pro当扩展屏，拖动的时候界面不会刷新，当操作完成后才用响应。2、在SATA固态上安装的Win11，目前开机后Core0会占用100%，解决方案是拔掉所有USB，等待一会再插上去即可恢复。3、网卡天线的安装对网速、蓝牙影响，如果自购准系统，建议让卖家帮忙安装天线。其他如果你折腾这个设备遇到了问题，可以联系我提供一些帮助（如果你啥经验都没有，建议直接买成品吧）。联系我：Email: idkpengc@gmail.com文章底部留言。引用https://blog.daliansky.net/Lenovo-Tianyi-510s-Mini-and-macOS-BigSur-Installation-Tutorial.html https://github.com/daliansky/Lenovo-TianYi-510S-Mini-Hackintosh
背景在使用ChatGPT时，通过http.Post进行调用耗时非常高，体验较差。 OpenAI提供了Stream(即SSE)的返回模式,不用等到响应生成完后才给到客户端响应。使用Stream API方式时，那么后端与前端也需要使用SSE的方式进行API交互。源代码在文末。Server-Sent Events（SSE）是一种在Web浏览器和服务器之间实现服务器推送事件的技术。它允许服务器向客户端推送事件流，这些事件可以是简单的文本消息或具有自定义格式的复杂数据。SSE 是一种基于 HTTP 的协议，使用长轮询或 HTTP 流来实现服务器推送。 SSE 可以用于各种用例，例如实时更新股票报价、即时聊天应用程序、新闻或社交媒体网站上的实时更新等等。与传统的轮询或 WebSocket 不同，SSE 不需要客户端发起请求，而是服务器始终保持连接打开并推送数据，因此 SSE 可以更高效地使用网络带宽和服务器资源。 在前端，可以使用 JavaScript API 来监听服务器发送的事件，以及处理和显示这些事件的数据。 引用：## open ai chat api https://platform.openai.com/docs/api-reference/chat/create ## Request ChatGPT API Using Spring boot https://www.taogenjia.com/2023/03/16/The-Guide-to-Call-ChatGPT-3-5-Event-Stream-API-in-Spring-Boot/ ## Doc https://developer.mozilla.org/en-US/docs/web/api/server-sent_events/using_server-sent_events https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource ## nginx https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering理解常见的HTTP请求是服务端把命令执行完成过后给到一个结果，那么客户端必须等待，大部分场景下这是没问题的。 那么在处理时间比较高的场景下，客户端可以实时接收部分处理结果，增加用户体验。我们从另一个角度看Http,HttpServerRequest: 有输入流，包含请求的数据HttpServletResponse:有输出流，可以写响应数据。那么我们处理Http请求时，不断的向Response的 outputStream写入数据，那么在没有处理完成响应的情况下也可以收到一部分数据。 @GetMapping("test") public void test(HttpServletRequest request, HttpServletResponse response) throws Exception { // 设置响应的Content-Type为text/event-stream response.setContentType("text/event-stream"); response.setStatus(200); OutputStream outputStream = response.getOutputStream(); int i = 10; while (i >= 0) { String message = "i : " + i + "\n"; outputStream.write(message.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); try { Thread.sleep(1000); } catch (Exception ignored) {} i--; } }我们打开浏览器访问这个api时，界面会间隔一秒输出一行：i : 10 i : 9 i : 8 i : 7 i : 6 i : 5 i : 4 i : 3 i : 2 i : 1 i : 0点击查看效果有了以上理解，我们只需要按着SSE要求的格式不停的向写入数据即可。SSE 格式1、id event dataid: xxx \n event: message \n data: hello from cc\n\n2、event dataevent: message \n data: hello from cc\n\n3、data onlydata: hello from cc\n\n其中： event: 事件名称，可以自己定义，默认是message id、event: 可选4、结束推送 如果使用sse，需要服务端进行关闭，如果不关闭前端会不停的重试（retry）自定义事件event: close \n data:\n\n前端订阅然后close，然后关闭sse const sse = new EventSource('/api/sse'); /* * 自定义订阅 */ sse.addEventListener("close", function(e) { sse.close(); }) /* This will listen only for events * similar to the following: * * event: notice * data: useful data * id: someid * */ sse.addEventListener("notice", function(e) { console.log(e.data) }) /* Similarly, this will listen for events * with the field `event: update` */ sse.addEventListener("update", function(e) { console.log(e.data) }) /* The event "message" is a special case, as it * will capture events without an event field * as well as events that have the specific type * `event: message` It will not trigger on any * other event type. */ sse.addEventListener("message", function(e) { console.log(e.data) })发送特殊标记的datadata: [DONE]\n\n处理 '[DONE]'的message const sse = new EventSource('/api/sse'); /* The event "message" is a special case, as it * will capture events without an event field * as well as events that have the specific type * `event: message` It will not trigger on any * other event type. */ sse.addEventListener("message", function(e) { var data = e.data; if (data.contains('[DONE]')) { sse.close() } else { console.log(data) } })踩坑1、使用Nginx部署后，消息没有一个一个收到，而是在处理完成后整个返回修改 nginxlocation / { proxy_pass http://127.0.0.1:xxx/; ... # When buffering is disabled, the response is passed to a client synchronously, immediately as # it is received. nginx will not try to read the whole response from the proxied server. The # maximum size of the data that nginx can receive from the server at a time is set by the # proxy_buffer_size directive. proxy_buffering off; ... }2、如果使用了WebFiler，需要开启asyncSupported @WebFilter(urlPatterns = "*",asyncSupported = true) public class SomeFilter implements Filter { .... }源代码https://gitee.com/pchenc/sse-demo
“我想我能变成动物，和它们为伴，它们是那么恬静那么矜持，我站着，久久的望着它们。它们不为儿女作牛马，也不为几女哀号，它们不在黑暗里睁眼失眠，为了它们的罪过啼泣，它们不喋喋讨论对上帝的责任，使我头晕脑胀，没有一个不满，没有一个为了占有欲而癲狂，没有一个向另一个屈膝，也不对几千年前的袓先跪拜，在整个的地球上没有一个有什么身份，也没有一个忧郁哀伤。——惠特曼Walt Whitman”摘录来自幸福之路罗素此材料可能受版权保护。“而过事诛求的结果，一定使你连应得的一份都落空。一个人能凭藉一些真正的兴趣，例如德朗会议或星辰史等，而忘记他的烦虑的话，当他从无人格的世界上旅行回来时，定将发觉自己觅得了均衡与宁静，使他能用最高明的手段去对付他的烦虑，而同时也尝到了真正的、即使是暂时的幸福。 幸福底秘诀是：让你的兴趣尽量的扩大，让你对人对物的反应，尽量的倾向于友善。”摘录来自: 罗素. “幸福之路 (傅雷经典译文全集).” Apple Books. 此材料受版权保护。
背景在系统开发过程中，以迭代功能为主，在使用过程中遇到性能瓶颈再进行针对性优化。本次遇到的问题是在导入3000+FAQ时导入时间大概500S左右，用户端提示导入失败，结果数据在导入中。文章出现的代码经过处理，不包含任何具体隐私信息，仅还原场景概念：FAQ：一般指常见问题解答。常见问题解答（frequently-asked questions，简称FAQ）即：一个问题对应一个答案。他的结构大概是一组相关的问题和一个答案。比如“你是谁？”这个问题，他会有不同的问法：“你叫什么？你叫啥？”，然后对应一个答案："我是xxx"意图/Intent：即FAQ中的“你是谁”目标1、在3000条数据时，导入在时间优到5秒内（而用户能够忍受的最长等待时间在6～8秒之间）；2、优化查询，返回时间在1S内；步骤1、找出瓶颈点；2、确定优化措施；3、实施；这三个步骤看似简单，其实需要在开发过程中逐步积累出自己的套路及经验。FAQ批量导入这里从系统csv文件中读取出意图和答案，再将数据保存到数据库，然后做一些检查，把错误的信息打上标签。解析数据保存校验、更新找出瓶颈点首先，我们发现API慢时，我们要知道哪些步骤慢。 这里我们用到google的Stopwatch进行“打桩”，给个操作计时。如 public static void main(String[] args) { // 创建后可多次输出耗时 Stopwatch stopwatch = Stopwatch.createStarted(); ThreadUtil.sleep(1000); System.out.println("消耗时间：" + stopwatch.elapsed(TimeUnit.MILLISECONDS)); ThreadUtil.sleep(2000); System.out.println("消耗时间：" + stopwatch.elapsed(TimeUnit.MILLISECONDS)); } 消耗时间：1001 消耗时间：3009回到我们的导入FAQ代码 public void save(String faqId, MultipartFile file) throws Exception { Stopwatch stopwatch = Stopwatch.createStarted(); // 1、从faq文件解析出数据 List<List<String>> rows = _readRows(file); LOG.info("[import faq - finish check used:]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); // 获取一个链接 Connection conn = getConn(); // 一个FAQ由一个User、一个Bot组成 // 转换成faq pair 对 List<Pair<User, Bot>> details = _convertRows(rows, faqId); LOG.info("[import faq - convert: ]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); // 保存到数据库 for (Pair<User, Bot> p : details) { ComponentDao.add(conn, p.getKey().toComponent()); ComponentDao.add(conn, p.getRight().toComponent()); } LOG.info("[import faq - save 2 db: ]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); conn.commit(); LOG.info("[import faq - finish commit 2: ]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); _validate(faqId, conn); conn.commit(); } private void _validate(String faqId, Connection conn, String dbName) throws Exception { Stopwatch stopwatch = Stopwatch.createStarted(); // 1、创建一个上下文，携带一些信息 ComponentValidatorContext context = _buildContext(componentId, conn); LOG.debug("[validate faq - build context:{}]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); // 2、 query 当前节点所在的validator，将user、bot转换成校验器 Pair<ComponentValidator /*Faq Validator*/, List<ComponentValidator>/*children validators*/> validatorPair = _validators(context); LOG.debug("[validate faq - build validate pair:{}]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); // 3、遍历节点，更新错误信息 LOG.debug("[validate faq - start validate with length:{}, spend:{}]", validators.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); int writeDbCount = 0; boolean hasError = false; for (ComponentValidator validator : validators) { StatusCodes statusCodes = validator.validate(context); if (statusCodes != null) { hasError = true; } // 节点错误 boolean updated = updateComponentError(validator); if (updated) { ComponentDao.update(conn, validator.getComponent()); writeDbCount++; } } LOG.debug("[validate faq - finish validate with write 2 db count:{}, spend:{}]", writeDbCount, stopwatch.elapsed(TimeUnit.MILLISECONDS)); // 3、更新FAQ上的标记 _updateError(faqId, hasError); LOG.debug("[validate used in millis:{}]", stopwatch.elapsed(TimeUnit.MILLISECONDS)); }运行结果[import faq - finish check used:147] [import faq - convert:241] [import faq - finish save 2 db:50870] [import faq - finish commit:50878] [start validate] [validate faq - build context:33486] [validate faq - build validate pair:35203] [validate faq - start validate with length:6795, spend:35203] [validate faq - finish validate with write 2 db count:6795, spend:361809] [validate used in millis:361818]到此，我们可以从打桩信息中得出瓶颈：保存到数据库，花费50s+构建上下文：33s+检验所有节点：326s+确定优化措施1、保存到数据库，花费50s+代码： // 保存到数据库 for (Pair<User, Bot> p : details) { ComponentDao.add(conn, p.getKey().toComponent()); ComponentDao.add(conn, p.getRight().toComponent()); }这里有3397个FAQ，6794个User、Bot节点；那么就执行6794次save操作。方案：结合以往经验，这里我们做批量保存。减少db写入次数， 批量写入;mysql批量写入优化;1、改为批量写入 : 写入db次数变成了 14次private void _saveBach(List<Pair<RestProjectComponentUser, RestProjectComponentBot>> data, Connection conn, String dbName)throws SQLException { String sql = "INSERT INTO tableName (id, xx, xx) VALUES (?, ?, ?) "; PreparedStatement ps = conn.prepareStatement(sql); for (int i = 0; i < data.size(); i++) { Pair<User, Bot> item = data.get(i); Component user = item.getKey().toComponent(); Component bot = item.getValue().toComponent(); _add2Bach(ps, user); _add2Bach(ps, bot); // 每500次提交一下 if (i % 500 == 0) { ps.executeBatch(); conn.commit(); ps.clearBatch(); } } ps.executeBatch(); conn.commit(); ps.clearBatch(); } private void _add2Bach(PreparedStatement ps, Component component) throws SQLException{ ps.setString(1, component.getId()); ps.setString(2, component.getXX()); ps.setString(3, component.getXx()); ps.addBatch(); } 2、mysql批量写入优化修改链接信息，重写批量写入jdbc:mysql://127.0.0.1:3305?rewriteBatchedStatements=true验证## 时间从50S+到1s+ [import faq - finish save 2 db:1586]总结： 这里原来每次保存都要写入数据库，大的数据量下这效率必然是低下的。经过批量保存和重写批量insert的sql，这减少了写入db次数，提高了sql批量写入效率# 之前：6794次insert insert into xx (xx,xx) values (xx,xx) # 之后：14次批量insert INSERT INTO xx (xx,xx) values ('xxx','xxx'),('xxx','xxx')...2、构建上下文：33s+针对构建上下文，我们在再次打桩，查找瓶颈点[build - context start build validate context] [build - convert2Rest - start] [build - convert2Rest - find root & query all projects spend:652] [build - convert2Rest - load children:10113] [build - convert2Rest - covert children:22067] [build - build - context convert2Rest pair spend:22072] [build - build - context spend:32382]这里大概的逻辑：查询顶级节点、查询当前项目下所有的节点信息还原关联关系从db对象转换成Rest对象筛选一些节点优化的点多次查询当前项目的所有节点-这数据量大，多次重复操作这在只查询一次，其他方法用到所有节点时通过参数传入还原关联关系这用了递归，实现简单数据量大时效率不如 while /for 循环 -- 改为while + for从db对象转换成Rest对象-在循环中查询整个项目数据减少重复查询db循环中使用stream，筛选/转换少量 - 这种场景下效率不如for筛选一些节点减少db查询递归转while /for验证[build - context start build validate context] [build - convert2Rest - start] [build - convert2Rest - find root & query all projects spend:3] [build - convert2Rest - load children:15] [build - convert2Rest - covert children:124] [build - context convert2Rest pair spend:128] [build - context spend:214]总结：减少db查询合理使用stream递归转while / for实现这优化还是很明显，从32S+到0.2S。3、检验所有节点：326s+这是最耗时的地方，先分析下代码：for循环所有节点，执行校验逻辑 - 验证意图（Intent，用户节点的输入需要唯一）不能重复如果有错误，将错误信息更新并同步数据库finish validate with write 2 db count:6795, spend:361809这提示总共写了6795次db，这显然不合理，因为在测试的时候是全新的数据，不会重复。 int writeDbCount = 0; boolean hasError = false; for (ComponentValidator validator : validators) { StatusCodes statusCodes = validator.validate(context); if (statusCodes != null) { hasError = true; } // 节点错误 boolean updated = updateComponentError(validator); if (updated) { ComponentDao.update(conn, validator.getComponent()); writeDbCount++; } }这里结合上面的套路优化stream修复更新db的bugfinish validate with write 2 db count:0, spend:108427 这总时间从361s+到108s+,提升非常大。剩余的代码检查没有操作db，但仍然有如此高的耗时，这很不合理。那么108427 / 6794 约等于16毫秒，校验一个节点需要这么久，显然不正常。我们再次使用Stopwatch打点状，输出下校验的耗时 for (ComponentValidator validator : validators) { Stopwatch sw = Stopwatch.createStarted(); StatusCodes statusCodes = validator.validate(context); if (statusCodes != null) { hasError = true; } long usedInMillis = sw.elapsed(TimeUnit.MILLISECONDS); if (usedInMillis > 20) { LOG.debug("[validate usedInMillis:{}]", usedInMillis) } .... }... [validate component usedInMillis:199] ...这就发现了问题，部分节点耗时异常的高。排查后发现这些节点的intent数量很大，intent越大越耗时。 // 经过排查发现， 在校验User.Intent时会判断是否与数据库已存在的意图重复。 // 那么这里有很大的计算量: // 这样判断扩展问重复从 example.size * db.example.size private boolean intentRepeated() { List<String> exampleTexts = getIntents(); //user的example不能重复，如果是引用的摸板则允许重复 List<User> users = componets.stream() // filter .collect(Collectors.toList()); for (User item : users) { List<String> examples =item.getIntents(); for (String text : examples) { if (exampleTexts.contains(text)) { return true; } } } return false; } 我们来看看这里的问题：循环中有stream， 这部分在筛选db中的User这里所有节点都是一样的，不用每次都计算，将结果在context中缓存起来，只计算一次，后续直接用for * for计算量大修改实现1、User从context获取，不在这进行计算 - 计算次数减少了6000次+ List<User> users= null; public List<User> getUsers() { if (users == null) { synchronized (this) { if (users == null) { users = componets.stream() // ... filter .collect(Collectors.toList()); } } } return users; }2、减少计算次数这里需要一定的经验积累，比如在第二个for循环中，我们改成map.containsKey去判断，就去掉了一个for循环。Tip：在最好的情况下，map.containsKey(key)的时间复杂度时o(1)Context中构建db中存在的Intents,只在第一次用到时计算，其他后续节点直接使用即可； private boolean intentRepeated() { List<String> exampleTexts = getIntents(); Map<String> exitsIntents = context.intentsMap(); for (String text : examples) { if (exitsIntents.containsKey(text)) { return true; } } return false; }这样以来，我们的计算量变：从 example.size db.example.size 变成 example.size 1 次；验证：[start validate with length:6795] [finish validate with write 2 db count:0, spend:227]总结：合理使用stream缓存结果，减少计算次数减少计算次数结论至此，完成了本次优化，同样数据导入时间控制在了5s左右。减少db读写次数、批量操作；合理使用stream，在循环且单个集合数量较小的情况下，使用循环；合理使用递归修复Bug合适使用Context，将结果缓存，减少重复计算减少计算量经过此次调整，其中重要的是如何发现问题，解决方案可以有很多。同时要知道调整后，多少是符合预期的，不能盯着小的优化点去弄，这种优化空间太低了，反而会花很多时间效果也不会好。如何找到瓶颈点预期优化效果在开发过程中，我们还是以满足实现为主，在时间可能的情况下合理保证代码效率，可以在后续碰到瓶颈时单独处理即可。打个广告：如果你是RASA开发者： PromptAI支持通过脑图的方式编辑对话流程，编辑完成后可一键下载Rasa Project文件，可在原生Rasa直接训练[部分高级功能需要配合应用使用] - 功能: FAQ、多轮、变量提取（单个/多个）、form、webhook及自定义Action等高级功能； - 编辑: 支持拖拽、回收站、收藏、撤销、重做及复制等便捷功能； - 调试: 编辑完成后可一键调试、发布，最大程度减少开发时间。我们提供高性能GPU（3090），让你的效率起飞！ - 其他: 支持私有部署，如果你有需求可联系我们！ 如果你是普通用户，不会开发，但是想用AI对话： PromptAI对普通用户/企业极度友好，无需了解编程/AI的相关知识，也可快速上手，比Excel使用还简单，没有复杂繁琐的操作及公式！ 几分钟就能拥有自己的ChatBot！ 免费使用：https://www.promptai.cn 注册过程：免费试用 -> 填写试用信息 -> 审核通过 -> 即可上手！
背景由于Ngrok开源版本已停止维护，经过调研后Frp比较合适:服务端/客户端无需编译支持多种客户端...引用：// github https://github.com/fatedier/frp // doc https://gofrp.org/docs/环境服务端：Centos 7客户端：Linux、Mac...Frp: 0.45其他：supervisord要求：服务器有公网ip+自有域名域名解析可以使用子域名，如: frp.xx.com; 可工作的地址: xx.frp.xx.com这里需要配置两个域名解析frp.xx.com ip *.frp.xx.com ip （泛域名解析）安装 - 服务端下载目前在最新版本为0.45，下载对应的Linux版本frp_0.45.0_linux_amd64.tar.gzzip包解压后有对应的服务端、客户端及对应的配置. ├── LICENSE ├── frpc # 客户端 ├── frpc.ini # 客户端简化配置 ├── frpc_full.ini # 客户端完整配置 ├── frps # 服务端 ├── frps.ini # 服务端简化配置 └── frps_full.ini # 服务完整化配置 0 directories, 7 files准备这里我使用的目录：/usr/local/frp将frpc、frpc.ini拷贝到此目录中配置文件frps.inibind_port = 9443 # 客户端链接的端口 # if you want to support virtual host, you must set the http port for listening (optional) # Note: http port and https port can be same with bind_port # vhost_https_port = 443 vhost_http_port = 9443 subdomain_host = frp.xx.com # 配置子域名 # authentication_method specifies what authentication method to use authenticate frpc with frps. # If "token" is specified - token will be read into login message. # If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token". authentication_method = token # 这里选择token进行验证，可参考文档进行调整 # auth token token = changeit # 这里是配置的token，客户端需要保持一致 # enable_prometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} in /metrics api. enable_prometheus = true 启动防止经进程挂掉，这里使用supervisor对frps进行管理supervisor 配置 [program:frserver] directory=/usr/local/frp command=/usr/local/frp/frps -c /usr/local/frp/frps.ini autostart=true autorestart=true startsecs=10 user=root stderr_logfile=/usr/local/frp/log/frp_stderr.log stdout_logfile=/usr/local/frp/log/frp_stdout.log redirect_stderr=true 注意，supervisor使用的目录要预先创建好，比如这里的log目录配置好后，执行supervisord reload supervisord status # 结果 [root@cc etc]# supervisorctl status frserver RUNNING pid 12464, uptime 1 days, 22:47:02 安装 - 客户端以Mac环境进行演示下载与服务端版本一致，下载对应的Mac版本frp_0.45.0_darwin_amd64.tar.gzzip包解压后有对应的服务端、客户端及对应的配置. ├── LICENSE ├── frpc # 客户端 ├── frpc.ini # 客户端简化配置 ├── frpc_full.ini # 客户端完整配置 ├── frps # 服务端 ├── frps.ini # 服务端简化配置 └── frps_full.ini # 服务完整化配置 0 directories, 7 files准备进入解压后的目录，修改客户端配置配置文件frpc.ini[common] authentication_method = token # 与服务端一致 server_port = 9443 # 与服务端一致 server_addr = frp.xx.com # 与服务端 subdomain_host 配置一致 token = changeit # 与服务端一致 [cc] # 子域名, 这里可自定义 local_port = 80 # 需要转发的本地端口，可自定义 subdomain = cc # 子域名, 这里可自定义 type = http启动执行./frpc -c frpc.ini 2022/12/10 13:25:23 [I] [service.go:349] [4773c37ef35b5761] login to server success, get run id [4773c37ef35b5761], server udp port [0] 2022/12/10 13:25:23 [I] [proxy_manager.go:144] [4773c37ef35b5761] proxy added: [cc] 2022/12/10 13:25:23 [I] [control.go:181] [4773c37ef35b5761] [cc] start proxy success结束此时可通过客户端配置域名进行访问： http://cc.frp.xx.com:9443