场景
复习用
短链接系统实现
- 如何保证生成短链接不重复
- 如何存储短链接
- 用302(临时)还是301(永久)重定向
https://cloud.tencent.com/developer/article/1858351
https://blog.csdn.net/codejas/article/details/106102452
https://juejin.cn/post/7312353213348741132
秒杀
使用redis(保证秒杀效率)的lua脚本(保证原子性)进行库存扣减,使用分布式事务的二阶段消息解决事务数据一致性。二阶段消息适用于无需回滚的这一类数据一致性问题,主要是为了保证第一阶段操作执行成功后,后续阶段一定能感知并执行。
二阶段消息的回查操作,主要还是依赖事务中第一阶段使用的数据库,来保证第一阶段整体操作的原子性以及幂等。
无论是请求执行lua脚本的服务端宕机,还是redis服务本身宕机,lua脚本都保证原子性,即写操作均无效
库存扣减lua脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 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脚本:
1
2
3
4
5
6
7
8
9
10
11
12
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
-- 如果不是回滚,说明事务成功
以下是可能出现的各个场景。
- AP在prepare后并且在提交脚本到redis前马上宕机:此时库存扣减脚本没有执行,后续超时执行回查脚本,全局事务直接置为失败
- AP在提交脚本后,AP宕机:即使AP与redis断连,redis依然会执行脚本,后续超时执行回查脚本,事务成功
- AP在提交脚本后,redis宕机:若redis执行脚本时宕机,则脚本中的任意写操作都不会落盘,因此脚本的写操作要么都成功要么都不成功。若脚本成功落盘,并且AP由于与redis断连进行重试,脚本的幂等处理会返回DUPLICATE
- AP在prepare后,DTM整个服务不可用:DTM需要在恢复之后查询所有未完成事务,并执行回查,推进事务完成
缓存一致性
基于先更新数据库再删除缓存的方案,为了避免更新数据库后删除缓存失败,并且数次同步地重试导致返回结果的延迟。因此引入二阶段消息,类似于MQ的方案,将更新数据库与删除缓存解耦,让后者异步地进行。
1
2
3
4
5
6
// 注册事务,当更新完数据库后,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 - https://cloud.tencent.com/developer/article/1379302?from=14588
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排序,比如展示的先后顺序为:
- 直播中(最晚开播优先)
- 预告中(最早开播优先)
- 回放(最晚结束优先)
而普通的帖子只要发出来后,除了删除变成不可见之外不会有其他状态变化(被删除的帖子不会出现在feed流中)。因此直播feed实际上可以抽象为带状态流转的帖子,并且状态影响feed排序。
如果只采用写扩散方案,每次直播状态变更都触发写扩散,逻辑复杂并且由于主播的粉丝一般很多,导致粉丝接收到这个消息的延迟也比较大(因为是直播,所以希望最好能实时知道是否在直播)。
如果只采用读扩散的方案,又会带来读扩散本身的缺点。
因此:
- 对回放采取写扩散,因为回放是直播feed的最终状态
- 对于预告中和直播中这两个易变状态可以采取读扩散,以每次都读到直播最新的状态,简化逻辑,或者采取推拉结合的方式,易变状态对活跃粉丝采取写扩散,对普通粉丝采取读扩散。
最后还有关于直播feed分页的问题,见参考链接。
接口调用失败排查
- 检查调用超时设置,可能超时设得过短
- 检查是否有代理和防火墙等对请求拦截
- ping测试节点连通性
- 检查服务本身是否有在正常运行,有无崩溃重启等情况
- 检查服务的资源: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
- 查看慢查询日志,确定慢SQL是哪些
- 执行explain查看慢sql的执行计划,重点关注指标有:
- type连接类型:比如const, eq_ref, ref等,避免all全表扫描
- possible_keys和key:使用了哪些索引
- rows:预计要扫描的行数
- extra:是否有文件排序using filesort、临时表using temporary等
- 让查询走索引
- 避免select *查询所有字段,减少网络IO
- limit限制返回行数
- 可能是因为锁竞争引起的慢SQL,检查锁等待情况
- 纵向扩容:增加硬件资源
- 横向扩容:分库分表
- 业务手段:使用缓存