一、将计算所得的热榜数据存入 Redis
1.1 三个关键点
- 热榜数据我们并没有保存到数据库里面,而在一些公司里面,这个数据可能会被保存数据库里面,用于大数据分析。
- 在
Redis中的过期时间,要比计算热榜所需的时间长。而且最好是留够重试的时间。 - 热榜往往只需要标题、点赞数等,你可以做相应的修改,核心就是确保:从缓存中取出来的数据,就是你查询接口里面需要返回的数据,字段一个不少。(因为查询热榜是一个高并发的操作,所以要避免在
Redis中只存储文章id然后再凭id去查数据库这种实现方式)
1.2 实现
(1)Service 层
func (b *BatchRankingService) TopN(ctx context.Context) error {
articles, err := b.topN(ctx)
if err != nil {
return err
}
// 最后将 articles 存入缓存中
return b.repo.ReplaceTopN(ctx, articles)
}
(2)Repository 层
package repository
import (
"context"
"refactor-webook/webook/internal/domain"
"refactor-webook/webook/internal/repository/cache"
)
type RankingRepository interface {
// ReplaceTopN 更新新的 topN
ReplaceTopN(ctx context.Context, articles []domain.Article) error
}
type CachedRankingRepository struct {
cache cache.RankingCache
}
func NewCachedRankingRepository(cache cache.RankingCache) RankingRepository {
return &CachedRankingRepository{cache: cache}
}
func (c *CachedRankingRepository) ReplaceTopN(ctx context.Context, articles []domain.Article) error {
return c.cache.Set(ctx, articles)
}
(3)Cache 层
package cache
import (
"context"
"encoding/json"
"github.com/redis/go-redis/v9"
"refactor-webook/webook/internal/domain"
"time"
)
type RankingCache interface {
Set(ctx context.Context, articles []domain.Article) error
}
type RankingRedisCache struct {
client redis.Cmdable
key string
expiration time.Duration
}
func NewRankingRedisCache(client redis.Cmdable) RankingCache {
return &RankingRedisCache{client: client, key: "ranking:top_n", expiration: time.Minute * 3}
}
func (r *RankingRedisCache) Set(ctx context.Context, articles []domain.Article) error {
// note 缓存榜单的 top100 文章时,不要缓存文章内容
for _, article := range articles {
article.Content = article.Abstract()
}
// 直接序列化 []article ,存入 redis
res, err := json.Marshal(articles)
if err != nil {
return err
}
return r.client.Set(ctx, r.key, res, r.expiration).Err()
}
二、组装成定时任务
2.1 思路
我们要将上面的 TopN() 方法封装成一个定时任务。我们引入一个新的模块:Job。
从项目结构上来说,Job、Web 和将来我们要引入的 gRPC 都是同一个层级的,也就是属于封装 Service ,然后“对外”暴露的方式。
2.2 实现
(1)定义自己的 Job 接口
(2)实现类 RankingJob
package job
import (
"context"
"refactor-webook/webook/internal/service"
"time"
)
type RankingJob struct {
svc service.RankingService
timeout time.Duration
}
func NewRankingJob(svc service.RankingService, timeout time.Duration) *RankingJob {
return &RankingJob{svc: svc, timeout: timeout}
}
func (r *RankingJob) Name() string {
return "RankingJob"
}
func (r *RankingJob) Run() error {
// 每一次 job 的过期时间是 30s
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
return r.svc.TopN(ctx)
}
(3)利用 Builder模式 封装成 cron.Job,实现了添加 prometheus的监控和日志
package job
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/robfig/cron/v3"
"refactor-webook/webook/pkg/logger"
"strconv"
"time"
)
type CronJobBuilder struct {
l logger.LoggerV1
// 接入 prometheus 统计响应时间
vector *prometheus.SummaryVec
}
func NewCronJobBuilder(l logger.LoggerV1, opt prometheus.SummaryOpts) *CronJobBuilder {
vector := prometheus.NewSummaryVec(opt, []string{"job", "success"})
return &CronJobBuilder{l: l, vector: vector}
}
// Build Builder模式 传入自己的 job 构建出 cron.Job
func (b *CronJobBuilder) Build(job Job) cron.Job {
return cronJobAdapterFunc(func() {
// 统计执行时间
start := time.Now()
err := job.Run()
if err != nil {
b.l.Error("执行失败",
logger.Error(err),
logger.String("name", job.Name()))
}
defer func() {
duration := time.Since(start)
b.vector.WithLabelValues(job.Name(), strconv.FormatBool(err == nil)).
Observe(float64(duration.Milliseconds()))
}()
})
}
type cronJobAdapterFunc func()
func (c cronJobAdapterFunc) Run() {
c()
}
(4)修改 wire 和在 main 中启动