背景痛点
- 状态丢失:HTTP长轮询或WebSocket断线后,服务端若未持久化会话上下文,用户再次接入时会出现“从头开始”的尬聊。
- 消息追溯困难:运营需要按关键词搜索历史记录,而裸存JSON文件或单表粗暴追加,导致全表扫描,延迟随数据量线性放大。
- 高并发写入冲突:热门直播间同一时间可能产生上万条消息,单行记录频繁update造成InnoDB行锁热点,TPS骤降。
- 冷热不均:最近7天数据占查询量95%,但全量保留365天消息,存储成本与索引内存占用同步膨胀。
技术选型
- MySQL 8.0优势:ACID严格、MVCC快照读、二级索引丰富,适合需要复杂查询、事务回滚的客服场景。
- MongoDB 5.0优势:WiredTiger行级锁、文档嵌套减少JOIN,单文档原子性,写入吞吐量大,适合IM类追加写。
- 基准数据(4C8G容器,单表5000万消息):
- MySQL:纯写入6k TPS,读写混合3k TPS,CPU先跑满。
- MongoDB:纯写入18k TPS,读写混合8k TPS,IO先跑满。
- 选型结论:对一致性要求>性能选MySQL;对追加写>一致性选MongoDB;也可MySQL存“会话”+Mongo存“消息”混合部署。
MySQL实现
符合3NF的表设计
-- 会话主表 CREATE TABLE chat_session ( session_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, bot_id BIGINT UNSIGNED NOT NULL, status TINYINT DEFAULT 1 COMMENT '1活跃 2关闭', created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3), updated_at DATINY(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (session_id), -- 业务常用索引 KEY idx_user ((user_id, updated_at DESC), KEY idx_status_ctime(status, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 消息子表 CREATE TABLE chat_message ( msg_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, session_id BIGINT UNSIGNED NOT NULL, seq BIGINT UNSIGNED NOT NULL, sender ENUM('user','bot') NOT NULL, msg_type TINYINT NOT NULL COMMENT '1文本 2图片', content TEXT NOT NULL, created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (msg_id), UNIQUE KEY uk_session_seq (session_id, seq), -- 保证同一session内seq唯一 KEY idx_ctime (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;分库分表策略(按user_id哈希)
-- 假设分8库×16表,共128张 db_index = hash(user_id) % 8 tbl_index = hash(user_id) >> 8 % 16 表名:chat_session_0_0 ... chat_session_7_15 好处:单行查询落到单表,避免分布式事务;坏处:聚合分析需离线同步至数仓。读写分离
- 写:指向InnoDB主库;读:指向只读实例,利用MVCC快照避免锁等待。
- 若开启binlog级联,延迟<200ms,对聊天场景可接受。
MongoDB实现
文档结构(单文档<=16MB,消息数组上限1万条,超过则归档)
{ "_id": ObjectId("..."), "uid": 123456, "botId": 42, "status": "active", "msg": [ { "seq": 1, "from": "user", "txt": "你好", "ts": ISODate("...") }, { "seq": 2, "from": "bot", "txt": "在呢", "ts": ISODate("...") } ], "lastSeq": 2, "ttl": ISODate("...") // TTL索引字段 }原子追加消息
db.chat_session.updateOne( { _id: sid, lastSeq: expected }, { $inc: { lastSeq: 1 }, $push: { msg: { $each: [ { seq: nextSeq, from: "bot", txt: "答复", ts: new Date() } ], $slice: -1000 // 保持数组长度,防爆炸 } }, $set: { ttl: new Date(Date.now()+7*24*3600*1000) } }, { upsert: false } );索引
db.chat_session.createIndex({ uid: 1, botId: 1 }) db.chat_session.createIndex({ ttl: 1 }, { expireAfterSeconds: 0 })
避坑指南
- TTL索引不是精确秒级,后台清理间隔60s,敏感场景需业务侧二次校验。
- 消息数组勿无限增长,采用$slice或“滚动归档”策略,超1万条即新建archive文档。
- 分布式节点时钟不同步会导致seq乱序,建议统一用数据库自增或Snowflake。
- MySQL大字段TEXT需把content拆到垂直子表或对象存储,避免页分裂。
- 热点会话读大于写时,可在应用侧用Redis缓存last50条,写时双写,读时先读缓存。
性能验证
- JMeter脚本要点
- 线程组:模拟1万用户,每用户每2s发1条消息。
- 断言:检查seq连续、msg_id递增。
- 结果(128张表/分片,SSD云盘)
- MySQL:99th延迟120ms,CPU 85%,索引命中率99.2%。
- MongoDB:99th延迟45ms,wiredTiger cache命中率98%。
- 慢查定位
- MySQL:打开performance_schema,按
events_statement.*聚合,查看rows_examined>1000的语句。 - MongoDB:db.setProfilingLevel(1,50),然后
db.system.profile.find({"millis":{$gt:50}}).
- MySQL:打开performance_schema,按
延伸思考
- 冷热分层:Redis缓存当日会话+MySQL热分片+对象存储冷归档,可节省60%磁盘。
- 多模态扩展:图片、语音先存OSS,消息表只保留URL与元数据,避免大字段拖慢索引。
- 实时检索:利用MongoDB Atlas Search或ES同步oplog,实现分词搜索历史消息。
- 多云容灾:MySQL采用Group Replication跨可用区,MongoDB采用Shard+Replica Set,保证机房级故障RPO<30s。
把会话表设计好后,Chatbot的“记忆”就有了可靠底座。想进一步让AI不仅能打字,还能开口说话?我顺手体验了从0打造个人豆包实时通话AI动手实验,跟着步骤把ASR、LLM、TTS串成一条低延迟语音管道,半小时就能在浏览器里跟虚拟角色语音聊天。实验把火山引擎的豆包系列模型都封装成了可调用接口,连WebRTC推拉流也给了示例,比自己东拼西凑省了不少时间。如果你正好想把文本Bot升级成“能听会说”的形态,不妨也去试试。