文章

场景

复习用

场景

短链接系统实现

  1. 如何保证生成短链接不重复
  2. 如何存储短链接
  3. 用302(临时)还是301(永久)重定向

https://cloud.tencent.com/developer/article/1858351

https://blog.csdn.net/codejas/article/details/106102452

https://juejin.cn/post/7312353213348741132

秒杀

使用redis(保证秒杀效率)的lua脚本(保证原子性)进行库存扣减,使用分布式事务的二阶段消息解决事务数据一致性。二阶段消息适用于无需回滚的这一类数据一致性问题,主要是为了保证第一阶段操作执行成功后,后续阶段一定能感知并执行。

二阶段消息的回查操作,主要还是依赖事务中第一阶段使用的数据库,来保证第一阶段整体操作的原子性以及幂等。

flash-sales

无论是请求执行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

-- 如果不是回滚,说明事务成功

以下是可能出现的各个场景。

  1. AP在prepare后并且在提交脚本到redis前马上宕机:此时库存扣减脚本没有执行,后续超时执行回查脚本,全局事务直接置为失败
  2. AP在提交脚本后,AP宕机:即使AP与redis断连,redis依然会执行脚本,后续超时执行回查脚本,事务成功
  3. AP在提交脚本后,redis宕机:若redis执行脚本时宕机,则脚本中的任意写操作都不会落盘,因此脚本的写操作要么都成功要么都不成功。若脚本成功落盘,并且AP由于与redis断连进行重试,脚本的幂等处理会返回DUPLICATE
  4. 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排序,比如展示的先后顺序为:

  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. 业务手段:使用缓存

参考

本文由作者按照 CC BY 4.0 进行授权