
一、一条通知两万条记录去年汛期的一次强降雨预警镇里要求在半小时内把撤离提示发到全镇八个村。我们照着老办法做了一次全量推送触达常住人口两万三千人其中党员六百四十人、帮扶对象三百一十二人。事后统计发现光是这一条通知产生的已读记录就有两万多行因为系统给每个接收人都预先写了一条未读记录人有没有点开先记上再说。问题在第二天暴露。那个星期镇上连着发了十一条通知已读明细表一下子涨到一百四十万行后台看阅读率的报表直接跑到十二秒还没出结果超时被网关掐断了。更要命的是村干部问某个组到底有多少人看了我们只能给出一个全量数字拆不出村组维度因为当时的推送范围是整镇一把梭标签这个概念压根没进系统。二、范围是集合运算已读是去重问题把需求拆开看第一件事是范围计算。镇干部描述接收人时用的是一串条件比如三组的党员、全体村民代表、加上帮扶对象这些条件有的是并集有的是交集还有偶尔要用到的差集。把它抽象出来就是标签的集合运算每个标签对应一批成员通知的目标是一棵表达式树叶子是标签节点是交并差三种运算。第二件事是统计口径。一条通知的阅读率等于已读人数除以触达人数分子要按人去重同一个人用手机看了一次、用网页又看了一次只能算一个人。这里最容易踩的是分母触达人数不能在出报表时现算因为人员会流动半年后有人迁出现算出来的分母会比发布当天小阅读率被人为抬高。所以触达人数必须在发布的那一刻做一次快照之后不再变。三、正向记账还是反向记账第一条路是正向记录也就是我们原来那套发布时给每个接收人写一条未读记录点开后把状态改成已读。这条路的好处是查询简单一条 SQL 就能数出未读人数代价是写入量等于触达人数乘以通知数两万三千人一天发三条就是七万行而且绝大部分记录到过期都不会被读纯属负担。第二条路是反向记录只写已读未读用触达快照减去已读集合算出来。代价是每次统计要做一次差集还有人退订、注销之后已读集合会比快照多出几个需要额外处理。我们最后选了第二条理由是基层推送的打开率并不高实测在四成左右也就是说超过一半的预写记录从生到死都不会被改动为了这一半的垃圾数据付出全量写入成本不划算。四、把范围表达式收进通知模块推送体系建在万村乐数字乡村的通知模块上标签体系和通知发布共用同一套范围表达式。发布页面上镇干部勾选条件前端拼出一棵表达式树提交到后端后由范围服务编译成一条成员标识集合再把集合的基数写进通知表作为快照。整个过程对外只暴露一个动作给我这批人。标签本身分成三类。第一类是村组标签由行政区划码前缀自动生成不用人工维护第二类是身份标签党员、村民代表、帮扶对象这些由业务系统在成员信息变更时反向刷新第三类是自定义标签镇里自己建的临时分组比如参加某次环境整治的志愿者。三类标签放在同一张表里靠一个来源字段区分查询时行为一致维护方式各管各的。五、表结构与回执去重核心是四张表。标签表 t_tag 存标签名、来源类型和刷新方式关系表 t_tag_member 存标签与成员的对应主键是标签加成员通知表 t_notice 存标题、正文、范围表达式、触达快照和状态回执表 t_notice_read 存通知标识、成员标识、首次阅读时间、阅读渠道和设备序号主键是通知加成员这一条主键就是去重的全部依据。回执上报走批量接口客户端在弱网下会把同一批回执重发三次。为了挡住重放我们让客户端带上一个单调递增的设备序号服务端写入时用插入忽略的语义冲突了不报错也不覆盖首次阅读时间保持最早的那一次。阅读率的查询直接数回执表里有几条不去关联成员表这样即使某个成员后来注销了历史阅读率也不会跟着变。六、三个坑都在数据量上第一个坑是写入放大也是促使我们重构的直接原因。老办法一天写两万条未读记录一周一百四十万行一个月就能把明细表顶到六百万行备份和查询都开始变慢。改成反向记录之后同样两万三千人的通知按四成打开率算一天只写九千多条行数降了六成报表从十二秒压到一点四秒。第二个坑是多端重复计数。有一批通知的阅读率算出来是一点一八超过了整一百村干部拿着截图来问是谁看了两次。根因是客户端在手机端和网页端各上报了一次而当时的回执表没有主键约束两条都写进去了。改法是给回执表加上通知加成员的复合主键上报接口改成插入忽略语义客户端再补一个设备序号用于幂等同一个人的多条回执只会留下最早的那条。第三个坑是撤回之后的统计。有一次通知写错了内容发布三分钟后被撤回按说阅读率应该定格可我们发现它还在慢慢往上涨因为有人从历史消息里点开了旧链接回执照样上报。改法是把通知状态和统计状态分开撤回时同时冻结统计并记下冻结时间回执接口遇到已撤回的通知仍然入库用于审计但不再参与阅读率计算。这样既保住了原始数据也保住了对外口径的稳定。七、这套标签算不出来的事标签只能描述已经知道的属性推不出哪些人可能会关心。比如一条关于养殖补贴的通知理论上应该发给养殖户可如果入户登记时没把这项职业信息录进去系统就永远选不中这批人。我们在发布页上加了一个手动补人的入口允许村干部在系统算出的集合上临时加减几个成员这个动作会记进操作日志方便事后复盘为什么这条通知发给了名单外的人。短信兜底那一路也只能证明到达证明不了阅读。对于没有智能终端的老人我们只能走短信或大喇叭短信能拿到运营商的到达回执但老人有没有真的看谁也说不清。所以对外口径上我们把这类通知单列不计入阅读率的分母避免把到达率混成阅读率报上去。还有一条是阅读率本身不等于看懂了。通知发出去百分之多少的人点开过这个数字好统计内容有没有被理解、有没有按提示撤离得靠村委会逐户回访。报表只能告诉镇上哪些组还没看不能替他们做动员这一层我们从来没打算用系统替代人的工作。八、小结三个汛期下来万村乐数字乡村的标签从最初的五个长到四十六个推送范围从单个村扩到四个乡镇中间没有再改过表结构只加过一个用于统计冻结的状态位。回头看省下来的其实不是存储而是每一次发布时的写入压力以及由此带来的报表响应时间。如果你的系统也在做类似的范围推送建议先想清楚两件事一是接收范围能不能抽象成标签运算二是触达人数到底在哪一刻固化。这两件事想明白了剩下的写入路径、回执去重、撤回处理都是它们的自然推论想不明白就先别急着建表。