一、要求
设计一个高性能方案,要求:
- 综合考虑可以怎么利用缓存,包括
Redis和本地缓存,设计方案有无亮点? - 考虑怎样的业务折中?【一般由产品经理决定能接受怎样的折中方案】
- 绘制
UML序列图,至少包含两张图:缓存命中与未命中的场景(评论区请你来实现 doge) - 使用
wrk输入不同参数进行性能测试(评论区请你来实现 doge)
二、方案
利用 zset 实现 LikeTop() 方法,主要使用其 ZINCRBY 和 ZRangeWithScores 方法
(1)repo 层
在 IncrLike() 中调用 IncrRankingIfPresent() 方法
func (c *CachedInteractiveRepository) IncrLike(ctx context.Context, biz string, bizId int64, uid int64) error {
// note 命名:区别于阅读数 +1 的命名,因为 dao 层除了实现点赞数 +1,还要标记已赞状态
err := c.dao.InsertLikeInfo(ctx, biz, bizId, uid)
if err != nil {
return err
}
err = c.cache.IncrLikeCntIfExist(ctx, biz, bizId)
if err != nil {
return err
}
return c.cache.IncrRankingIfPresent(ctx, biz, bizId)
}
(2)cache 层
LikeTop 方法返回的就是按照点赞数 top100 的数据
func (c *RedisInteractiveCache) LikeTop(ctx context.Context, biz string) ([]domain.Interactive, error) {
var start int64 = 0
var end int64 = 99
res, err := c.client.ZRangeWithScores(ctx, c.rankingKey(biz), start, end).Result()
if err != nil {
return nil, err
}
interactives := make([]domain.Interactive, 0, len(res))
for _, z := range res {
val, _ := strconv.ParseInt(z.Member.(string), 10, 64)
interactives = append(interactives, domain.Interactive{
BizId: val,
Biz: biz,
LikeCnt: int64(z.Score),
})
}
return interactives, nil
}
某数据点赞数增加对应的 IncrRankingIfExist() 方法
func (c *RedisInteractiveCache) IncrRankingIfExist(ctx context.Context, biz string, bizId int64) error {
_, err := c.client.Eval(ctx, luaRankingCnt, []string{c.rankingKey(biz)}, bizId).Result()
return err
}
对应的 luaRankingCnt lua 脚本
local zsetName = KEY[1]
local member2incr = ARGV[1]
-- 判断redis中该key是否存在
local exist = redis.call("EXISTS", zsetName)
if exist == 1 then
newScore = redis.call("ZINCRBY", zsetName, 1, member2incr)
return 1
else
return 0
end
设置某数据在 redis 中对应的 zset 缓存方法:
func (c *RedisInteractiveCache) SetRankingScore(ctx context.Context, biz string, bizId int64, score int64) error {
_, err := c.client.Eval(ctx, luaRankingSet, []string{c.rankingKey(biz)}, bizId, score).Result()
return err
}
对应的 luaRankingSet lua 脚本
local zsetName = KEY[1]
local member2incr = ARGV[1]
local newScore = ARGV[2] -- 指定的分数,如果未指定则默认为1
local exists = redis.call("EXISTS", zsetName)
if exists == 0 then
redis.call("ZADD", zsetName, newScore, member2incr)
end
-- 获取指定元素的当前分数
local currentScore = redis.call("ZSCORE", zsetName, member2incr)
if currentScore then
-- 如果元素存在,将分数加1
local newScore = newScore + 1
redis.call("ZADD", zsetName, newScore, member2incr)
else
-- 如果元素不存在,将分数设置为指定的分数
redis.call("ZADD", zsetName, newScore, member2incr)
end
批量将数据加载到 zset 中的方法
// BatchSetRankingScore 将所有 interactive 存进 zset 中
func (c *RedisInteractiveCache) BatchSetRankingScore(ctx context.Context, biz string, interactives []domain.Interactive) error {
zs := make([]redis.Z, 0, len(interactives))
for _, interactive := range interactives {
zs = append(zs, redis.Z{
Score: float64(interactive.LikeCnt),
Member: interactive.BizId,
})
}
return c.client.ZAdd(ctx, c.rankingKey(biz), zs...).Err()
}
三、补充方案(进阶)
(1)引入本地缓存,并定时刷新,比如说每 5s 调用 LikeTop(),放入本地缓存。也可以考虑不全局排序,仅定时计算 top1000,然后借助 zset 来实时维护 top1000 的 score ,每次选 top100 时都从这 1000 名中选,每 30min 再更新前 1000 名的 score。
(2)如果有一亿个数据,无法全部放入 zset,该怎么办?可以只维护近期(如近三天)的点赞数(业务折中),也可以 分key ,分成 100 * 1000,000个数据,存入 100 个 key中。