--- title: 场景 description: 复习用 categories: [bagu] --- ## 短链接系统实现 1. 如何保证生成短链接不重复 2. 如何存储短链接 3. 用302(临时)还是301(永久)重定向 ## 秒杀 使用redis(保证秒杀效率)的lua脚本(保证原子性)进行库存扣减,使用分布式事务的二阶段消息解决事务数据一致性。**二阶段消息适用于无需回滚的这一类数据一致性问题**,主要是为了保证第一阶段操作执行成功后,后续阶段一定能感知并执行。 二阶段消息的回查操作,主要还是依赖事务中第一阶段使用的数据库,来保证第一阶段整体操作的原子性以及幂等。 ![flash-sales](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/flash-sales.0f022cdb.svg) 无论是请求执行lua脚本的服务端宕机,还是redis服务本身宕机,lua脚本都保证原子性,即写操作均无效 库存扣减lua脚本: ```lua -- KEYS[1] 库存 -- KEYS[2] 事务当前操作 -- KEYS[3] 如果事务当前操作是回滚操作,则为回滚所对应的操作 local v = redis.call('GET', KEYS[1]) -- 库存 local e1 = redis.call('GET', KEYS[2]) -- 事务当前操作的状态 if v == false or v + ARGV[1] < 0 then -- 库存不足 return 'FAILURE' end if e1 ~= false then -- 当前状态不为空,幂等退出 return 'DUPLICATE' end -- 设置当前操作为已完成 redis.call('SET', KEYS[2], 'op', 'EX', ARGV[3]) if ARGV[2] ~= '' then local e2 = redis.call('GET', KEYS[3]) if e2 == false then -- 如果是回滚操作,将回滚对应操作的状态设置为已回滚 redis.call('SET', KEYS[3], 'rollback', 'EX', ARGV[3]) return end end -- 库存扣减 redis.call('INCRBY', KEYS[1], ARGV[1]) ``` 回查lua脚本: ```lua local v = redis.call('GET', KEYS[1]) -- 扣减库存操作的状态 if v == false then -- 为空则直接回滚 redis.call('SET', KEYS[1], 'rollback', 'EX', ARGV[1]) v = 'rollback' end -- 如果阶段1是回滚,直接返回事务失败 if v == 'rollback' then return 'FAILURE' end -- 如果不是回滚,说明事务成功 ``` 以下是可能出现的各个场景。 1. **AP在prepare后并且在提交脚本到redis前马上宕机**:此时库存扣减脚本没有执行,后续超时执行回查脚本,全局事务直接置为失败 2. **AP在提交脚本后,AP宕机**:即使AP与redis断连,redis依然会执行脚本,后续超时执行回查脚本,事务成功 3. **AP在提交脚本后,redis宕机**:若redis执行脚本时宕机,则脚本中的任意写操作都不会落盘,因此脚本的写操作要么都成功要么都不成功。若脚本成功落盘,并且AP由于与redis断连进行重试,脚本的幂等处理会返回DUPLICATE 4. **AP在prepare后,DTM整个服务不可用**:DTM需要在恢复之后查询所有未完成事务,并执行回查,推进事务完成 ## 缓存一致性 基于先更新数据库再删除缓存的方案,为了避免更新数据库后删除缓存失败,并且数次同步地重试导致返回结果的延迟。因此引入二阶段消息,类似于MQ的方案,将更新数据库与删除缓存解耦,让后者异步地进行。 ```go // 注册事务,当更新完数据库后,TM会异步地触发删除缓存 msg := dtmcli.NewMsg(DtmServer, gid). Add(busi.Busi+"/DeleteRedis", &Req{Key: key1}) err := msg.DoAndSubmitDB(busi.Busi+"/QueryPrepared", db, func(tx *sql.Tx) error { // 更新数据库 }) ``` ## 海量文本去重 simhash - ## feed流设计 - 写扩散/读扩散 ### 读扩散/拉模式 特点:用户每次读feed流,发生读扩散(读每一个关注的人发布的帖子),以及聚合操作(按时间线或者其他规则进行排序)。适用于有大V、用户关注的数量少、用户不活跃的场景 优点:存储简单,没有空间浪费。发布帖子发布简单,只需把帖子写入数据库即可 缺点:当关注的人比较多,每次刷feed都进行读扩散+聚合,造成系统开销大、用户响应延迟高。如果feed数据量大,那么聚合操作还会给分页带来麻烦。 ### 写扩散/推模式 背景:自己发帖子的用户比刷feed的用户数量少得多,因此宁愿让发帖效率低一些,复杂一点,也尽量不影响用户的刷feed体验。适用于没有大V、用户活跃的场景 特点:用户发帖子时,发生写扩散(将帖子推给所有关注者的“收件箱”)。为了提高发帖子效率,还可以将写扩散异步处理,比如由MQ消费者慢慢写。 优点:解决了用户刷feed时需要比较重的读扩散 缺点:对于大V用户来说,发一次帖子就需要推给上亿个用户,系统负载十分大,而且由于消费有延迟,有一部用户可能迟迟看不到他发的帖子。另外还会有数据冗余(一篇帖子会存在于每个用户的收件箱中),并且如果在写扩散后,帖子被删除的话,还要注意去删除收件箱中的帖子,或者在读帖子的时候懒删除 ### 推拉结合模式 特点:根据实际运行的情况选择使用写扩散或读扩散。具体来说: - 大V发帖:对于活跃用户使用写扩散,对于非活跃用户的则读扩散(即不推送) - 普通用户发帖:直接写扩散 当刷feed时: - 活跃用户:直接读收件箱的内容即可 - 非活跃用户:读取收件箱+读扩散关注的大V用户 ### 分页 无论是推还是拉模式,刷feed都难以避免分页的需求,一般使用last id的方式,从最后一条feed的id的下一条开始加载下一页,这样可以避免当按时间线有新的feed刷进来的时候,第二页前面的内容包含了第一页后面的内容。 ### 场景 - 直播feed 所谓直播feed,就是将朋友圈/微博的帖子换成直播,比如b站主页就有直播板块,里面刷到的是直播feed。直播feed与帖子不同之处在于,可能会有多种状态(预告中、直播中、回放),我们希望结合这些状态与时间线进行feed排序,比如展示的先后顺序为: 1. 直播中(最晚开播优先) 2. 预告中(最早开播优先) 3. 回放(最晚结束优先) 而普通的帖子只要发出来后,除了删除变成不可见之外不会有其他状态变化(被删除的帖子不会出现在feed流中)。因此直播feed实际上可以抽象为带状态流转的帖子,并且状态影响feed排序。 如果只采用写扩散方案,每次直播状态变更都触发写扩散,逻辑复杂并且由于主播的粉丝一般很多,导致粉丝接收到这个消息的延迟也比较大(因为是直播,所以希望最好能实时知道是否在直播)。 如果只采用读扩散的方案,又会带来读扩散本身的缺点。 因此: - 对回放采取写扩散,因为回放是直播feed的最终状态 - 对于预告中和直播中这两个易变状态可以采取读扩散,以每次都读到直播最新的状态,简化逻辑,或者采取推拉结合的方式,易变状态对活跃粉丝采取写扩散,对普通粉丝采取读扩散。 最后还有关于直播feed分页的问题,见参考链接。 ## 接口调用失败排查 1. 检查调用超时设置,可能超时设得过短 2. 检查是否有代理和防火墙等对请求拦截 3. ping测试节点连通性 4. 检查服务本身是否有在正常运行,有无崩溃重启等情况 5. 检查服务的资源:cpu(top/ps -r)/内存(top/ps -m)/磁盘(df/iostat)/网络(ifconfig/sar/netstat/)等情况,是否资源耗尽导致无法响应 - `ps aux`的列含义:**VSZ虚存大小**、**RSS物存大小**、**STAT(R运行 S睡眠 D不可中断睡眠/通常是IO T停止 Z僵尸)**、**TIME进程占用CPU时间** **STARTED程序启动时间** ## 慢SQL 1. 查看慢查询日志,确定慢SQL是哪些 2. 执行explain查看慢sql的执行计划,重点关注指标有: - type连接类型:比如const, eq_ref, ref等,避免all全表扫描 - possible_keys和key:使用了哪些索引 - rows:预计要扫描的行数 - extra:是否有文件排序using filesort、临时表using temporary等 3. 让查询走索引 4. 避免select *查询所有字段,减少网络IO 5. limit限制返回行数 6. 可能是因为锁竞争引起的慢SQL,检查锁等待情况 7. 纵向扩容:增加硬件资源 8. 横向扩容:分库分表 9. 业务手段:使用缓存 ### 参考 - - - -