diff --git a/notes/系统设计基础.md b/notes/系统设计基础.md index cc7135e6..c326f242 100644 --- a/notes/系统设计基础.md +++ b/notes/系统设计基础.md @@ -478,7 +478,9 @@ WHERE id = 70001 AND user_id = 1001 AND status = 0 AND valid_end > NOW(); 抽中限量的奖品后,再用与秒杀相同的 Lua 脚本扣减这个奖品的库存。扣减失败说明奖品已经发完,这时应降级为“谢谢参与”或兜底奖品,而不是重新抽一次。这是因为重新抽相当于把一等奖的 1% 按比例分给其它奖品,二等奖的实际概率会从 9% 变为 9 ÷ 99 ≈ 9.09%,发出的奖品超出预算。 -每人每天的抽奖次数可以在 Redis 中用 INCR 计数并按天设置过期时间,返回值超过上限就拒绝。“每人最多中一次大奖”这类限制也按降级处理,因此实际中奖率会低于配置值,上线前可以用大量模拟统计各奖品的频率来核对。 +每人每天的抽奖次数可以在 Redis 中用 INCR 计数并按天设置过期时间,返回值超过上限就拒绝。“每人最多中一次大奖”这类限制也按降级处理,因此实际中奖率会低于配置值。 + +核对抽奖的代码时,可以把 0~9999 每个随机数都跑一遍,统计每个奖品落到的格数,应当正好等于权重;用随机模拟的话,一等奖差一格(0.01%)这样的边界错误,按 3 倍标准差估算约需 900 万次才能分辨出来。上线后再按天统计各奖品实际发放的数量,与配置比较。 #### 3. 抢红包 @@ -546,6 +548,8 @@ WHERE id = 8001 AND remain_num > 0 AND remain_amount >= 276; 应用在脚本执行成功之后、消息发出之前宕机时,这条领取只记录在 Redis 中。因此红包抢完或过期后,要用 Redis 中的领取记录与数据库对账,补发缺失的消息。例如每条消息都投递两次、其中一个用户的消息全部丢失:18 条消息插入 9 条、冲突 9 条,对账补发后剩余金额为 0、领取记录 10 条,合计正好 100 元(MySQL 8.0 实测,见[附录 A16](#a16-并发抢红包与异步落库))。 +需要注意的是,这种对账只能补回应用宕机丢失的消息。Redis 主节点在复制之前宕机、从节点被提升时,已经返回成功的领取在 Redis 中也可能没有了,以 Redis 的领取记录为准的对账发现不了。执行脚本后在同一个连接上再执行 WAIT,等至少一个从节点确认后才返回成功,只能缩小这个窗口(见 [Sentinel](Redis.md#十二sentinel));资金类的场景也可以不在缓存中预扣,把同一个红包的请求排队后在数据库事务中扣减。 + 红包一般有有效期,例如 24 小时。到期后先在 Redis 中停止领取(例如删除金额列表,之后的脚本 LPOP 取不到金额,直接返回已抢完),再对账并补发缺失的消息,确认这些领取都已写入数据库后,把数据库中的剩余金额退回给发送者。顺序反过来时,对账之后仍可能有人抢到,退回的金额就多了。 ## 七、海量数据处理 @@ -636,7 +640,7 @@ Base62 区分大小写,code 列要用区分大小写的排序规则。MySQL 8. 短链读多写少,热门链接的访问又很集中,适合用 Redis 缓存短码到长链接的映射。不存在的短码会穿透到数据库,可以缓存空值或者使用布隆过滤器,见 [缓存穿透](缓存.md#缓存穿透)。点击统计不要在跳转时同步写数据库,而是把点击事件写入消息队列,异步汇总。 -设置了有效期的短链,跳转时先判断 expire_at,过期的不再跳转,返回失效提示;写缓存时让过期时间与 expire_at 对齐(例如 SET ... EXAT 时间戳),后台任务再按 expire_at 索引分批删除过期的记录。 +设置了有效期的短链,跳转时先判断 expire_at,过期的不再跳转,返回失效提示;写缓存时让过期时间与 expire_at 对齐(例如 SET ... EXAT 时间戳),后台任务再按 expire_at 索引分批删除过期的记录。用哈希或随机方法生成、靠唯一约束判重的短码,删掉记录后就可能再分配给新的链接,旧短信、海报上的短链会跳到别人的地址,所以删除时保留短码本身(例如只清空长链接),不再分配。 ## 九、Feed 流 🔥 @@ -1287,7 +1291,7 @@ CREATE TABLE conv_member ( 服务端的限制主要是: -- 文件描述符:每个连接占用一个,要调大进程能打开的描述符数,在 shell 中用 ulimit -n,由 systemd 启动的服务不继承登录 shell 的设置,默认软限制 1024、硬限制 524288(systemd 240 起),要在 unit 文件中设置 LimitNOFILE,见 [epoll](Socket.md#epoll); +- 文件描述符:每个连接占用一个,要调大进程能打开的描述符数,在 shell 中用 ulimit -n,由 systemd 启动的服务不继承登录 shell 的设置,默认软限制 1024、硬限制 524288(systemd 240 起),要在 unit 文件中设置 LimitNOFILE,Java 进程则会在启动时把软限制提到硬限制(HotSpot 的 MaxFDLimit 默认开启),见 [epoll](Socket.md#epoll); - 内存:10 万个空闲连接,Java 进程的内存增加约 120 MB,内核中每个套接字约 4 KB,合计每个连接约 5 KB,100 万个连接约需 5 GB; - CPU 和带宽:取决于消息量,空闲的连接只需要处理心跳。