尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信 4.x 本地数据库设计拆解:SQLCipher 加密、会话级分表与中文全文检索是怎么实现的

微信 4.x 本地数据库设计拆解:SQLCipher 加密、会话级分表与中文全文检索是怎么实现的 基于一台 Windows 微信客户端本地数据的完整观测客户端版本微信 Windows 4.1.15.13存储引擎WCDBSQLite 3 分支 SQLCipher 4观测规模20 个加密库 / 972.8 MB / 1697 张表 / 约 102 万行目标读者中高级后端与客户端开发者预计阅读22 分钟摘要本文基于对一台 Windows 机器上微信 4.1.15.13客户端本地数据目录的完整观测与全量迁移实测拆解其本地持久层的设计为什么一个聊天软件要拆 20 个加密数据库、为什么要按会话建 994 张表、FTS5 如何做中文分词与拼音索引、4MB 固定大小的 WAL 意味着什么以及这套设计给普通业务系统带来的启发。文中所有数字均来自实机测量路径已脱敏会话名已匿名化。微信 4.x 的本地持久层不是一个大数据库而是20 个按业务域拆分的 SQLCipher 4 加密库运行于腾讯开源的 WCDBSQLite 分支之上。核心结论按会话分表而非按行message_0.db内含Msg_md5(会话ID)表994 张删除会话等价于DROP TABLE毫秒级完成。消息量极端长尾994 张表共 14.5 万行中位数仅16 行最大7104 行前 1% 的表占掉29.3%的消息量。检索与主表彻底解耦message_fts.db用 8 张 FTS5 虚表4 文本 4 图片承接搜索MMFtsTokenizer自定义分词器通过disable_pinyin/disable_origin开关实现一份内容、两份索引。异地分隔的资源索引图片/视频/文件的去重信息放在hardlink.db摘要索引放在message_resource.db主消息表只存结构化字段。可靠性有边界WAL 恒定封顶 4MB自动 checkpoint优雅退出才能拿到一致快照崩溃会留下少量坏页我们实测到biz_message_0存在真实的行级损坏。一、实验环境与数据来源项值客户端版本微信 Windows 4.1.15.13存储引擎WCDBSQLite 3 分支 SQLCipher 4数据根目录用户文档\xwechat_files\账号哈希\db_storagedb_storage总体量972.8 MB含所有附属文件完全解读的库14 个message_0 / biz_message_0 / message_fts / contact / contact_fts / sns / session / favorite / favorite_fts / message_resource / hardlink / head_image / general / emoticon未能解读的库6 个media_0 / bizchat / chatbot_message / solitaire / third_app_icon / weclaw分析工具Python 3.13sqlite3 读取、页面级二进制解析 PyMySQL本文所有结论均建立在本机自有数据的分析之上重点讨论其工程设计方案。涉及密钥的部分只做架构层面的说明不提供任何可用于获取他人数据的复现步骤。二、全局架构20 个库是怎么切分的先看清物理布局再回答一个关键问题为什么不用一个大库。2.1 目录按业务域组织与很多应用把所有表塞进一个main.db不同微信在db_storage下先按业务域建子目录再放同名数据库db_storage/ ├── message/ # 主消息域 │ ├── message_0.db 138.91 MB │ ├── biz_message_0.db 552.23 MB ← 公众号/服务号消息 │ ├── message_fts.db 35.66 MB ← 消息全文索引 │ ├── media_0.db 66.99 MB ← 媒体索引 │ └── message_resource.db 9.93 MB ← 资源摘要索引 ├── contact/ contact.db 18.62 MB contact_fts.db 6.01 MB ├── session/ session.db 2.06 MB ├── favorite/ favorite.db 7.74 MB favorite_fts.db 1.07 MB ├── sns/ sns.db 32.50 MB ├── hardlink/ hardlink.db 2.91 MB ├── head_image/ head_image.db 45.25 MB ├── general/ general.db 5.09 MB ├── emoticon/ emoticon.db 0.41 MB ├── chatbot/ chatbot_message.db ├── bizchat/ solitaire/ third_app_icon/ MMKV/每个库旁边都规整地跟着-wal预写日志与-shm共享内存文件。命名后缀_fts的三个库是纯索引库db_storage/MMKV/则是腾讯 MMKV 键值文件的地盘——一个典型的结构化用 SQLite、小配置用 MMKV的分层。2.2 各库的职责边界库体积表数行数职责message_0138.91 MB1003141,161单聊与群聊消息正文biz_message_0552.23 MB50379,985公众号/服务号消息含大量富媒体message_fts35.66 MB60343,771消息全文索引8 张 FTS5 虚表contact18.62 MB1647,837联系人、群成员、陌生人、标签contact_fts6.01 MB37153,733联系人检索含拼音索引head_image45.25 MB11,936头像二进制缓存message_resource9.93 MB692,830消息内资源的摘要索引hardlink2.91 MB820,295图片/视频/文件的去重与路径索引sns32.50 MB1228,001朋友圈时间线、草稿、发布任务session2.06 MB712,045会话列表最近联系人/群favorite7.74 MB88,669收藏general5.09 MB222,004转账/红包/撤回等跨领域杂项emoticon0.41 MB71,125表情包元数据表数/行数为迁入 MySQL 后的实测值与目标端一致6 个未解读库不计入。2.3 拆这么多库图什么从工程角度看这个粒度至少换来五件事写锁隔离。SQLite 是库级写锁。朋友圈后台刷新、表情包同步、头像更新如果和正在打字发消息争同一把锁卡顿会被用户直接感知。拆库之后它们各写各的。故障域收敛。我们实测到biz_message_0存在行级页损坏——如果它和主聊天库是同一个文件一次坏页就意味着整个资料不可用。按需加载。这是最有意思的一点微信并不会在启动时打开所有库。生命周期差异。头像、视频封面是可以随时丢弃重建的缓存聊天记录不是。放在不同文件里才能用不同的清理策略删文件 vs 表内 DELETE。迁移与扩容粒度。.db之间可以独立做迁移、压缩或搬移。代价也很真实按需加载意味着没被打开的库对用户/分析者完全不可见。本次实验中media_067 MB、chatbot_message、solitaire等 6 个库始终没被本次会话触达因此连一处可读取的素材都拿不到。这是一把双刃剑。三、存储层WCDB SQLCipher 4 的页格式20 个库共用同一套加密页格式本节把它拆到字节级。3.1 加密页布局微信 4.x 走了SQLCipher 4默认配置AES-256-CBC 加密 HMAC-SHA512 完整性校验 PBKDF2-HMAC-SHA512 密钥派生256000 次迭代。每个 4096 字节的页面扣除80 字节尾部保留区16 字节 IV 64 字节 HMAC实际可用负载4016 字节第 1 页: [0:16] salt 明文 [16:4016] 密文 [4016:4032] AES IV [4032:4096] HMAC-SHA512 其它页: [0:4016] 密文 [4016:4032] AES IV [4032:4096] HMAC-SHA512用 Python 解析一份库文件的页头可以确认 page_size 一致保持 4096import struct head open(message_0.db, rb).read(24) page_size struct.unpack(H, head[16:18])[0] # 4096 print(head[:16]) # bSQLite format 3\x00 print(head[16:24].hex()) # 10000101004020204096 - 4016 80正好等于 IV 与 HMAC 的长度之和。这也是为什么 SQLCipher 加密库的 SQLite 头部reserve字段必须是 80少了它页尾的身份校验一定失败。3.2 一库一盐每个库一把独立密钥关键设计在于salt 存在第 1 页头部、每个库各不相同。同一份 passphrase 经过不同 salt 派生得到的是互不相同的 32 字节 raw key。带来的直接后果是20 个库 20 个独立的加密域破一个不等于破全部密钥只有在该库真正被打开时才会在进程内存中出现进程退出即消失磁盘上不存在可直接使用的明文密钥key_info.db里key_md5列为空真正的 key material 以加密 BLOB 形式存放。从安全工程看这是合格的做法它防的是整盘拷贝后离线解析防不住本机已登录状态下的任意进程读取。这是一个被很多客户端软件共享的威胁模型边界值得在做敏感数据本地存储时想清楚。3.3 体积分布磁盘都花在哪了biz_message_0.db552 MB 远超message_0.db的 139 MB——公众号文章消息天然携带大量富文本与媒体。而message_fts.db只有 35.66 MB、却撑起了 message_0 里 14.5 万条消息的全文检索见第八节说明索引结构设计得相当克制——注意 message_fts 的 MySQL 端行数有 34 万那是因为 FTS5 的 content / docsize / idx 等影子表各自占行并非每条消息对应一行。四、会话级分表Msg_md5的设计取舍这是整套方案里最反直觉、也最值得借鉴的一环一个会话一张表。4.1 命名规则在message_0.db里会话名到表名的映射关系是确定的表名 Msg_ || lower(hex(md5(会话 username)))例如会话 ID12345678901chatroom已脱敏其消息表为Msg_74b1e878969e017cf5dc78d154c898b8。这个 md5 前缀策略至少有三个好处定长表名永远 36 字符不会因为昵称长短影响sqlite_master的 b-tree 平衡无需转义username 里可能含、_、中文直接当表名要做大量引号处理md5 后一律是[0-9a-f]不可逆表文件即使被单独导出也无法从中读出这是谁的聊天当然配合Name2Id表可以反查。message_0里并非只有消息表还有一套元数据层见第六节。按SELECT name FROM sqlite_master的分类统计类别数量Msg_md5会话表994元数据/系统表Name2Id/TimeStamp/DeleteInfo/SendInfo/MessageGroupTimeInfo/DeleteResInfo/HistorySysMsgInfo/HistoryAddMsgInfo/wcdb_builtin_compression_record9sqlite_sequence1合计1004biz_message_0.db同构Msg_md5表 498 张 6 张元数据表。4.2 实测分布极端长尾对message_0.db的 994 张会话表逐张COUNT(*)指标值会话表总数994消息总行数144,939平均每表145.8 行中位数16 行最大表7,104 行行数 1000 的表33 张3.3%前 1%9 张表消息占比29.3%空表0中位数 16 行、平均 145 行——这意味着绝大多数会话只是有过几句话的浅层接触真正的内容集中在极少数会话里TOP 10 会话的体量已匿名化从 7104 到 2465 不等。这个分布对我们的启示是如果你的数据也有这种长尾按实体建表而不是一张大表加索引能获得极低的常数开销。4.3 收益与代价维度按会话分表微信做法大表 索引常规做法删除单个会话DROP TABLE毫秒级空间立即释放大范围DELETE触发 VACUUM 才回收空间单会话扫描直达 B-tree无索引回跳需走WHERE chat_id ?索引跨会话搜索必须依赖外部 FTS 库一条 SQL 即可但会把主表拖垮冷启动sqlite_master装载 1000 条目单表装载快DDL 运维1000 张表的迁移/加列成本高一次 ALTER 搞定混合读写热点天然隔离页竞争更明显微信选择了前者并用一个独立的 FTS 库补上它的最大短板跨会话搜索。这是一个完整的 trade-off 闭环先承认方案缺陷再用另一个子系统设计补偿而不是硬在同一张表里揉又能改又能查。五、消息表 Schema16 列里藏了什么剥掉分表的外壳看字段16 列里藏着一条完整的多端同步设计线索。以Msg_74b1e878969e017cf5dc78d154c898b8为例这是从解密后的库中读出的原始 DDLCREATE TABLE Msg_74b1e878969e017cf5dc78d154c898b8( local_id INTEGER PRIMARY KEY AUTOINCREMENT, server_id INTEGER, local_type INTEGER, sort_seq INTEGER, real_sender_id INTEGER, create_time INTEGER, status INTEGER, upload_status INTEGER, download_status INTEGER, server_seq INTEGER, origin_source INTEGER, source TEXT, message_content TEXT, compress_content TEXT, packed_info_data BLOB, WCDB_CT_message_content INTEGER DEFAULT NULL, WCDB_CT_source INTEGER DEFAULT NULL )配套四个索引CREATE INDEX ..._SENDERID ON ...(real_sender_id) CREATE INDEX ..._SERVERID ON ...(server_id) CREATE INDEX ..._SORTSEQ ON ...(sort_seq) CREATE INDEX ..._TYPE_SEQ ON ...(local_type, sort_seq)5.1 三个 ID、三个时间维度这是整张表最值得学习的地方——它明确区分了三种身份字段含义为什么必须存在local_id本地自增主键离线也能发消息本地必须先有主键。是 UI 滚动、撤回定位的锚点。server_id服务端消息 ID用于幂等同一条消息被重复推送时靠它去重。sort_seq服务端单调序列号才是真正的排序键。本地时间不可信跨设备一致性靠它。很多 IM 自研方案会直接用create_time排序然后在多端登录、消息乱序到达时手忙脚乱。微信把排序这件事外包给服务端单调序列是一个干净的做法。5.2 三份 payloadsource/message_content/compress_contentsource原始推送内容通常是带有 XML 结构的完整数据包包含所有可能被下游功能用到的字段message_content渲染所需的可读文本compress_content压缩后的内容大文本消息的落地形式。同一份数据按用途存三份换取的是渲染时不用解析 XML、解析时不用解压、容量与速度各占一边。这在设计上叫读写路径分离在存储成本可接受时非常划算。5.3WCDB_CT_列WCDB 的列压缩每个可被压缩的 TEXT/BLOB 列都配一个WCDB_CT_列名的整型标记列这里是WCDB_CT_message_content、WCDB_CT_source用于记录该行此列当前使用的压缩算法类型。整个库还有一张注册表CREATE TABLE wcdb_builtin_compression_record( tableName TEXT PRIMARY KEY, columns TEXT NOT NULL, rowid INTEGER ) WITHOUT ROWID即压缩是列级、行级可判定的——同一列里小数据存原文、大数据压缩后存储读取时按WCDB_CT_标记决定要不要解压。对比整列统一压缩或整个字段一起 gzip这个粒度能避免在短文本上白白付出 CPU。六、元数据层message_0里的数据字典message_0.db的 9 张非消息表构成了一个轻量元数据层其中最关键的是字符串整数化CREATE TABLE Name2Id(user_name TEXT PRIMARY KEY, is_session INTEGER) CREATE TABLE TimeStamp(timestamp INTEGER) CREATE TABLE DeleteInfo(chat_name_id INTEGER, delete_table_name TEXT, CONSTRAINT UNIQUE_CHAT_DELETE UNIQUE(chat_name_id, delete_table_name)) CREATE TABLE SendInfo(chat_name_id INTEGER, msg_local_id INTEGER) CREATE TABLE MessageGroupTimeInfo(chatname_id INTEGER, group_id TEXT, create_time INTEGER, initial_sort_seq INTEGER, birth_time INTEGER)表作用Name2Id把 username → 整数 id。让 994 张消息表内部只出现整数大幅缩小行体积TimeStamp单值行记录本地同步水位DeleteInfo记录某会话的某张分表已被删除用于多端同步时的已删除判定SendInfo待发送/已发送队列与 ACK 关联MessageGroupTimeInfo会话内的时间分组聊天卡片上今天/昨天那种分组的锚点把字典抽出来、让数据表里只存整数是 SQLite 上很有效的一招INT 是变长编码5 字节能表示到 2^42而一个xxxchatroom形式的账号串要 15 字节;乘以每条消息一行、几万行差距显著。七、会话列表为什么是宽表反范式的正确性session.db只有 1542 行但SessionTable有 20 列CREATE TABLE SessionTable( username TEXT UNIQUE, type INTEGER, unread_count INTEGER, unread_first_msg_srv_id INTEGER, is_hidden INTEGER, summary TEXT, draft TEXT, status INTEGER, last_timestamp INTEGER, sort_timestamp INTEGER, last_msg_locald_id INTEGER, last_msg_type INTEGER, last_msg_sub_type INTEGER, last_msg_sender TEXT, last_sender_display_name TEXT, last_msg_ext_type INTEGER, ... )它把会话的最后一条消息摘要、草稿、未读数、时间戳全部冗余进同一行。这在 OLTP 建模课上是反范式但在IM 列表页这个场景下是对的会话列表是每次进 App 必读的一屏数据最多几十行如果规范化的话每个会话都要回各自的Msg_md5表做一次取最后一条1000 张表的MAX(sort_seq)是灾难换成写消息时顺手更新会话表就把 N 次读摊平成 1 次写。session.db还配套了SessionDeleteTable、SessionUnreadListTable_1、SessionDraft、Name2Id、SessionNoContactInfoTable等把删除未读列表草稿这类会反复修改的字段再拆出去避免行内热点竞争。按读写频率差异二次拆分是这套设计里贯穿始终的思路。八、中文全文检索FTS5 自定义分词器 分片中文没有空格标准分词器会直接失效——微信的解法是自定义分词器加分片索引。8.1message_fts.db8 张虚表虚表用途message_fts_v4_0~v4_3消息文本索引4 个分片ImgFts0V0~ImgFts3V0图片相关索引4 个分片消息索引虚表的定义CREATE VIRTUAL TABLE message_fts_v4_0 USING fts5( tokenize MMFtsTokenizer disable_pinyin, acontent, message_local_id UNINDEXED, sort_seq UNINDEXED, local_type UNINDEXED, session_id UNINDEXED, sender_id UNINDEXED, create_time UNINDEXED )要点只有acontent参与建索引其余 6 列全部UNINDEXED——它们只是随索引一起存放的载荷供命中后回填不再占用倒排表空间。分词器是自定义扩展MMFtsTokenizer不是 FTS5 自带的unicode61。中文没有空格标准分词器只会把整句当成一个 tokenMMFtsTokenizer负责实现词典/子串切分并且通过参数开关把 tokenizer 参数化。分成 4 个 shard外加 4 个图片 shard。分片的好处是索引构建/增量合并可以分片并行单个 shard 损坏不致命也便于按会话 hash 路由到 shard。每张文本虚表都配一张 aux 表CREATE TABLE message_fts_v4_aux_0( message_local_id INTEGER, sort_seq INTEGER, session_id INTEGER, CONSTRAINT sessionId_localId_sortseq PRIMARY KEY(session_id, message_local_id, sort_seq) )它的主键(session_id, message_local_id, sort_seq)用来保证一条消息在索引里只出现一次也支持按会话快速删除索引条目。另有message_fts_v4_range、message_fts_v4_session_delete_info、table_info三张台账表。8.2 一份原始内容两份索引外部内容表contact_fts.db里有 6 张虚表其中一对尤其漂亮CREATE VIRTUAL TABLE wa_contact_fts_v1 USING fts5( tokenize MMFtsTokenizer disable_pinyin enable_special_char, search_key, time UNINDEXED) CREATE VIRTUAL TABLE wa_contact_fts_pinyin_v1 USING fts5( tokenize MMFtsTokenizer disable_origin enable_special_char, contentwa_contact_fts_v1, -- 外部内容表复用同一份content search_key, time UNINDEXED)同一份联系人文本wa_contact_fts_v1用原始分词建索引搜汉字wa_contact_fts_pinyin_v1用拼音分词建索引搜 zhangsan 就能命中张三。后者通过contentwa_contact_fts_v1声明外部内容表——content 只存一份倒排索引存两份。三个开关的组合非常克制开关含义disable_pinyin不生成拼音 token汉字索引用它disable_origin不生成原始 token拼音索引用它enable_special_char允许、-这类符号进入索引便于精确匹配账号、邮箱等contact_fts.db总共 37 张表、15.3 万行撑起了联系人、群成员、表情搜索词典三类检索。把检索当成一个独立产品域来建模而不是给主表加 LIKE是这套架构最值得借鉴的地方。九、资源去重与摘要hardlink与message_resource图片、视频、文件不进消息表而是另开两处索引承接。9.1hardlink.dbMD5 去重image_hardlink_info_v4 / file_hardlink_info_v4 / video_hardlink_info_v4 索引: xxx_v4_DIR1 / xxx_v4_MD5_HASH / xxx_v4_MODIFY_TIME dir2id -- 目录名 → 整数 id file_checkpoint_v4 / video_checkpoint_v4 / talker_checkpoint_v4三类资源各自一张表均建有MD5_HASH索引——同一个文件被反复转发、反复出现在不同会话时物理文件只存一份逻辑引用靠 MD5 关联。*_checkpoint_v4表是增量扫描的水位线talker_checkpoint_v4甚至按MONTH_ID建索引用于下次扫描只处理新增部分。这套哈希去重 扫描水位的组合在本地相册/网盘类应用里几乎是标准答案。9.2message_resource.db把富消息摘要外移ChatName2Id / SenderName2Id -- 又一次字符串整数化 MessageResourceInfo -- CC/CS/SENDER/TIME/TYPE 五个索引 MessageResourceDetail -- ATIME/CTIME/MID/SIZE/TYPE 五个索引 FtsRange / FtsDeleteInfo -- 与 FTS 库的联动台账6 张表、9.3 万行。它的存在让统计某人发了多少张图按大小清理某个会话的文件这类查询完全不需要触碰 139 MB 的message_0.db。再一次按访问模式拆表。十、可靠性WAL 为什么恒定封顶 4MB10.1 观测事实扫描所有-wal文件可以看到一个整齐的现象库主库体积-wal体积message_0138.91 MB4.00 MBbiz_message_0552.23 MB4.00 MBcontact18.62 MB4.00 MBcontact_fts6.01 MB4.00 MBmessage_fts35.66 MB4.00 MBsession2.06 MB4.00 MB全部卡在 4.00 MB。这显然不是巧合而是设置了 WAL 自动 checkpoint 的大小阈值SQLite 的journal_size_limit/ WCDB 的自动 checkpoint 配置一旦 WAL 增长到 4MB 就立刻把已提交页回写主库并截断把未落盘数据量控制在一个可预测的上限内。这对终端用户体验很关键崩溃最多丢最近几 MB 的写入而不是让你回到上个月。10.2 一致性优雅退出 vs 强杀我们在取快照时对比过两种方式方式结果直接结束进程取快照WAL 残留最多 4MB 未 checkpoint 数据部分表读出来database disk image is malformed发送关闭请求让客户端正常退出进程清空、WAL 回写完毕可直接得到一致快照更要紧的一点SQLCipher 会改写加密 WAL 的帧头。我们逐帧解析时发现同一个 WAL 文件内各帧的 salt 字段并不一致按标准 SQLite 的盐必须匹配头部来判定会把 998/1018 个有效帧误判为垃圾但反过来这些帧的提交点页数小于主库当前页数说明它们其实是上一周期残留——贸然应用反而会把数据库回退成旧版本并造成损坏。结论对着一个正在运行的、加密的 SQLite 做离线分析时这个帧要不要应用必须同时用解密后是否合理和提交点是否比主库新双重判定。这也是我们把分析流程改成先优雅退出、再快照、再逐页解密的原因。10.3 仍有真实损坏即便如此biz_message_0里仍有少部分Msg_表存在行级损坏COUNT(*)能跑、SELECT *会在某些行上失败。这说明SQLite WAL 能保证单个事务原子不能保证在进程被粗暴终止时任何页都不坏。我们在迁移时改成按 rowid 分批读取、坏批逐行重试最终只丢弃了真正损坏的少数行。十一、这套设计对普通业务系统的启发把上面的观察抽象成可以直接抄的七条按变化频率 访问模式切库不按业务模块拍脑袋。微信切的是可以被批次性丢弃的和绝对不能丢的、每秒写一次的和每天写一次的。长尾数据用分表别用大表加索引。中位 16 行的数据分布下分表让删一个实体从全表扫描变成DROP TABLE。代价交给外部索引库去补。把排序权交给服务端单调序列。本地时间戳在多端同步场景下永远不可信。字符串一律整数化。Name2Id这种字典表是 SQLite 上性价比最高的优化之一。检索是独立子系统。用 FTS5 自定义 tokenizer 外部内容表做到一份 content、多份倒排。不要在主表上写LIKE %x%。给每条持久化路径一个可预测的上限。4MB 的 WAL 意味着最坏情况丢多少是已知量。列压缩做到行列级可判定。WCDB_CT_col这种标记列只有 1 字节左右的成本换来的是压缩策略的灵活度。十二、附录 B完整数据清单库源 SQLite表数行数MySQL 端体积message_fts60343,77160.38 MBcontact_fts37153,73320.03 MBmessage_01003141,161236.41 MBmessage_resource692,83016.72 MBbiz_message_050379,985781.42 MBcontact1647,83720.67 MBsns1228,00115.22 MBhardlink820,2955.50 MBsession712,0453.08 MBfavorite88,6696.19 MBfavorite_fts73,0480.72 MBgeneral222,0045.00 MBhead_image11,9369.67 MBemoticon71,1250.45 MB_meta_tables自建元数据11,6970.48 MB合计1698≈ 1,023,920≈ 1.18 GB边界声明本文分析对象为作者本人设备上自己账号产生的本地数据属于个人数据自主范畴文中会话名、联系人均已匿名化。本文的目的是理解优秀的客户端本地存储工程实践不涉及、也不提供任何读取他人数据的手段。涉及密钥的部分仅做架构层面的讨论。微信各版本的库结构会变化本文实测于 4.1.15.13迁移到其他版本前请以实际sqlite_master为准。元信息目标读者中高级后端/客户端开发者尤其是正在设计本地数据存储、IM、离线优先应用的同学前置知识SQLite 基础WAL、B-tree、FTS5、对称加密基本概念、SQL 调优常识技术栈版本微信 Windows 4.1.15.13 / SQLCipher 4 / SQLite 3 / MySQL 8.4.7 / Python 3.13预计阅读时间22 分钟关键词SQLiteSQLCipherWCDBFTS5数据库设计本地存储微信分表策略本文所有数据均来自本机微信客户端自有数据的离线分析会话名、群名、联系人等已做匿名化处理。文中涉及的加密分析仅用于理解本地存储架构不提供任何读取他人数据的手段。
返回列表