go并发之美·redis篇·实现实时排行榜

    科技2022-08-19  101

    国庆假期第五天快结束了。今天你够浪了么?

    个人原创,欢迎阅读~

    初始化其client

    import ( "context" "github.com/go-redis/redis" ) var Rdb *redis.Client func InitClient() (err error) { Rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) _, err = Rdb.Ping(context.TODO()).Result() if err != nil { return err } return nil }

    初始化一些文章,测试链接是否正常,再分别获取全量文章浏览量、top5浏览量看看:

    type Article struct { ID int // 文章ID Title string Views int64 // 浏览量 Content string } var ZSetKey = "article" /* 初始化一些新文章 */ func DoCreateArticle() { insertList := []*redis.Z{ &redis.Z{Score: 1, Member: 11}, &redis.Z{Score: 1, Member: 12}, &redis.Z{Score: 1, Member: 13}, &redis.Z{Score: 1, Member: 14}, &redis.Z{Score: 3, Member: 15}, &redis.Z{Score: 1, Member: 16}, &redis.Z{Score: 1, Member: 17}, &redis.Z{Score: 1, Member: 18}, } num, err := utils.Rdb.ZAdd(context.TODO(), ZSetKey, insertList...).Result() if err != nil { fmt.Printf("添加失败, err:%v\n", err) return } fmt.Printf("添加成功 %d.\n", num) for i := 11; i <= 18; i++ { article := Article{ ID: i, Title: "文章" + strconv.Itoa(i) + "的标题", Views: 1, Content: "时维九月,序属三秋。潦水尽而寒潭清,烟光凝而暮山紫。地势极而南溟深,天柱高而北辰远。", } articleMap.Store(strconv.Itoa(i), article) } // 获取所有 ret, err := utils.Rdb.ZRevRangeWithScores(context.TODO(), ZSetKey, 0, -1).Result() if err != nil { fmt.Printf("获取所有失败, err:%v\n", err) return } for k, z := range ret { fmt.Println(fmt.Sprintf("键=%v,值:分数=%v,文章id=%v,文章标题=%v。", k, z.Score, z.Member, articleMap[z.Member.(string)].Title)) } fmt.Println("----------------") // 获取前5 top5Ret, err := utils.Rdb.ZRevRangeWithScores(context.TODO(), ZSetKey, 0, 4).Result() if err != nil { fmt.Printf("获取所有失败, err:%v\n", err) return } for k, z := range top5Ret { fmt.Println(fmt.Sprintf("键=%v,值:分数=%v,文章id=%v,文章标题=%v。", k, z.Score, z.Member, articleMap[z.Member.(string)].Title)) } } === RUN TestRedisZeg 添加成功 8. 键=0,值:分数=3,文章id=15,文章标题=文章15的标题。 键=1,值:分数=1,文章id=18,文章标题=文章18的标题。 键=2,值:分数=1,文章id=17,文章标题=文章17的标题。 键=3,值:分数=1,文章id=16,文章标题=文章16的标题。 键=4,值:分数=1,文章id=14,文章标题=文章14的标题。 键=5,值:分数=1,文章id=13,文章标题=文章13的标题。 键=6,值:分数=1,文章id=12,文章标题=文章12的标题。 键=7,值:分数=1,文章id=11,文章标题=文章11的标题。 ---------------- 键=0,值:分数=3,文章id=15,文章标题=文章15的标题。 键=1,值:分数=1,文章id=18,文章标题=文章18的标题。 键=2,值:分数=1,文章id=17,文章标题=文章17的标题。 键=3,值:分数=1,文章id=16,文章标题=文章16的标题。 键=4,值:分数=1,文章id=14,文章标题=文章14的标题。 --- PASS: TestRedisZeg (0.03s) PASS

    好,到这里前序步骤先走通了,接下来操作下文章持续被阅读,以及持续获取top5:

    // 用map模拟持久化存储,存放每篇文章 var articleMap sync.Map /* 初始化一些新文章 */ func DoCreateArticle() { insertList := []*redis.Z{ &redis.Z{Score: 1, Member: 11}, &redis.Z{Score: 1, Member: 12}, &redis.Z{Score: 1, Member: 13}, &redis.Z{Score: 1, Member: 14}, &redis.Z{Score: 3, Member: 15}, &redis.Z{Score: 1, Member: 16}, &redis.Z{Score: 1, Member: 17}, &redis.Z{Score: 1, Member: 18}, } num, err := utils.Rdb.ZAdd(context.TODO(), ZSetKey, insertList...).Result() if err != nil { fmt.Printf("添加失败, err:%v\n", err) return } fmt.Printf("添加成功 %d条记录.\n", num) for i := 11; i <= 18; i++ { article := Article{ ID: i, Title: "文章" + strconv.Itoa(i) + "的标题", Views: 1, Content: "时维九月,序属三秋。潦水尽而寒潭清,烟光凝而暮山紫。地势极而南溟深,天柱高而北辰远。", } articleMap.Store(strconv.Itoa(i), article) } } /* 随机新增任意文章的浏览量,模拟文章持续被阅读 这里用sleep主要为了观察日志 */ func DoInCer() { for { time.Sleep(time.Second * 3) rand.Seed(time.Now().UnixNano()) num := rand.Intn(8) + 11 // 给id为num的文章增加浏览量 inCer := rand.Intn(3) + 1 newScore, err := utils.Rdb.ZIncrBy(context.TODO(), ZSetKey, float64(inCer), strconv.Itoa(num)).Result() if err != nil { fmt.Printf("给id为%v的文章增加浏览量失败, err:%v", num, err) return } fmt.Println("给id为",num,"的文章增加了",inCer,",该文章当前浏览量为",newScore,"。") } } /* 持续更新文章的浏览量到持久化存储 */ func UpdateViews() { ticker := time.NewTicker(time.Second * 5) for { select { case <-ticker.C: // 从redis遍历所有的文章,拿到id和浏览量,对articleMap进行更新 ret, err := utils.Rdb.ZRevRangeWithScores(context.TODO(), ZSetKey, 0, -1).Result() if err != nil { fmt.Printf("获取所有失败, err:%v\n", err) return } for _, z := range ret { if data, ok := articleMap.Load(z.Member.(string)); ok { obj := data.(Article) obj.Views = int64(z.Score) articleMap.Store(z.Member.(string), obj) fmt.Println(fmt.Sprintf("id为%v的文章已更新到map,当前浏览量为%v.", z.Member.(string), obj.Views)) } } } } } /* 持续获取top3的文章标题及浏览量 */ func GetArticleTopN(n int) { for { time.Sleep(time.Second * 2) top5Ret, err := utils.Rdb.ZRevRangeWithScores(context.TODO(), ZSetKey, 0, int64(n)).Result() if err != nil { fmt.Printf("获取top%v失败, err:%v\n", n, err) return } fmt.Println("------------------start top3------------------") for _, z := range top5Ret { if obj, ok := articleMap.Load(z.Member.(string)); ok { fmt.Println(fmt.Sprintf("文章id=%v,分数=%v,文章标题=%v。", z.Member, z.Score, obj.(Article).Title)) } } fmt.Println("-------------------end top3-------------------") } } func TestRedisTopIng(t *testing.T) { utils.InitClient() DoCreateArticle() go GetArticleTopN(2) go DoInCer() go UpdateViews() for !false { fmt.Print() } } speed running: 添加成功 8条记录. ------------------start top3------------------ 文章id=15,分数=3,文章标题=文章15的标题。 文章id=18,分数=1,文章标题=文章18的标题。 文章id=17,分数=1,文章标题=文章17的标题。 -------------------end top3------------------- 给id为 18 的文章增加了 2 ,该文章当前浏览量为 3 。 ------------------start top3------------------ 文章id=18,分数=3,文章标题=文章18的标题。 文章id=15,分数=3,文章标题=文章15的标题。 文章id=17,分数=1,文章标题=文章17的标题。 -------------------end top3------------------- id为18的文章已更新到map,当前浏览量为3. id为15的文章已更新到map,当前浏览量为3. id为17的文章已更新到map,当前浏览量为1. id为16的文章已更新到map,当前浏览量为1. id为14的文章已更新到map,当前浏览量为1. id为13的文章已更新到map,当前浏览量为1. id为12的文章已更新到map,当前浏览量为1. id为11的文章已更新到map,当前浏览量为1. 给id为 11 的文章增加了 3 ,该文章当前浏览量为 4 。 ------------------start top3------------------ 文章id=11,分数=4,文章标题=文章11的标题。 文章id=18,分数=3,文章标题=文章18的标题。 文章id=15,分数=3,文章标题=文章15的标题。 -------------------end top3------------------- ------------------start top3------------------ 文章id=11,分数=4,文章标题=文章11的标题。 文章id=18,分数=3,文章标题=文章18的标题。 文章id=15,分数=3,文章标题=文章15的标题。 -------------------end top3------------------- 给id为 13 的文章增加了 2 ,该文章当前浏览量为 3 。 id为11的文章已更新到map,当前浏览量为4. id为18的文章已更新到map,当前浏览量为3. id为15的文章已更新到map,当前浏览量为3. id为13的文章已更新到map,当前浏览量为3. id为17的文章已更新到map,当前浏览量为1. id为16的文章已更新到map,当前浏览量为1. id为14的文章已更新到map,当前浏览量为1. id为12的文章已更新到map,当前浏览量为1. ------------------start top3------------------ 文章id=11,分数=4,文章标题=文章11的标题。 文章id=18,分数=3,文章标题=文章18的标题。 文章id=15,分数=3,文章标题=文章15的标题。 -------------------end top3------------------- 给id为 12 的文章增加了 3 ,该文章当前浏览量为 4 。 ------------------start top3------------------ 文章id=12,分数=4,文章标题=文章12的标题。 文章id=11,分数=4,文章标题=文章11的标题。 文章id=18,分数=3,文章标题=文章18的标题。 -------------------end top3------------------- ------------------start top3------------------ 文章id=12,分数=4,文章标题=文章12的标题。 文章id=11,分数=4,文章标题=文章11的标题。 文章id=18,分数=3,文章标题=文章18的标题。 -------------------end top3------------------- id为12的文章已更新到map,当前浏览量为4. id为11的文章已更新到map,当前浏览量为4. id为18的文章已更新到map,当前浏览量为3. id为15的文章已更新到map,当前浏览量为3. id为13的文章已更新到map,当前浏览量为3. id为17的文章已更新到map,当前浏览量为1. id为16的文章已更新到map,当前浏览量为1. id为14的文章已更新到map,当前浏览量为1. 给id为 11 的文章增加了 1 ,该文章当前浏览量为 5 。 ------------------start top3------------------ 文章id=11,分数=5,文章标题=文章11的标题。 文章id=12,分数=4,文章标题=文章12的标题。 文章id=18,分数=3,文章标题=文章18的标题。 -------------------end top3------------------- 给id为 18 的文章增加了 1 ,该文章当前浏览量为 4 。 ------------------start top3------------------ 文章id=11,分数=5,文章标题=文章11的标题。 文章id=18,分数=4,文章标题=文章18的标题。 文章id=12,分数=4,文章标题=文章12的标题。 -------------------end top3------------------- id为11的文章已更新到map,当前浏览量为5. id为18的文章已更新到map,当前浏览量为4. id为12的文章已更新到map,当前浏览量为4. id为15的文章已更新到map,当前浏览量为3. id为13的文章已更新到map,当前浏览量为3. id为17的文章已更新到map,当前浏览量为1. id为16的文章已更新到map,当前浏览量为1. id为14的文章已更新到map,当前浏览量为1. ------------------start top3------------------ 文章id=11,分数=5,文章标题=文章11的标题。 文章id=18,分数=4,文章标题=文章18的标题。 文章id=12,分数=4,文章标题=文章12的标题。 -------------------end top3------------------- 给id为 16 的文章增加了 1 ,该文章当前浏览量为 2 。 ------------------start top3------------------ 文章id=11,分数=5,文章标题=文章11的标题。 文章id=18,分数=4,文章标题=文章18的标题。 文章id=12,分数=4,文章标题=文章12的标题。 -------------------end top3------------------- 给id为 13 的文章增加了 3 ,该文章当前浏览量为 6 。 ------------------start top3------------------ 文章id=13,分数=6,文章标题=文章13的标题。 文章id=11,分数=5,文章标题=文章11的标题。 文章id=18,分数=4,文章标题=文章18的标题。 -------------------end top3------------------- id为13的文章已更新到map,当前浏览量为6. id为11的文章已更新到map,当前浏览量为5. id为18的文章已更新到map,当前浏览量为4. id为12的文章已更新到map,当前浏览量为4. id为15的文章已更新到map,当前浏览量为3. id为16的文章已更新到map,当前浏览量为2. id为17的文章已更新到map,当前浏览量为1. id为14的文章已更新到map,当前浏览量为1. ... ... ...

    上面大多数sleep主要是为了观察验证逻辑,要验证功能的都ok了。下面我们做这样2个优化:

    1,让文章浏览量增加和从redis读取topN、更新最新浏览量到持久化等操作进行实时同步进行;

    2,上面的逻辑每次更新到持久化存储时不论哪篇文章是否被阅读都是全量更新,实际上大部分可能阅读量并未变化,无需更新,这里优化为只更新变动的部分,即更新方收到的通知只是要更新的部分。

    好的,go,上代码:

    /* 随机新增任意文章的浏览量,模拟文章持续被阅读 批量统计增加的文章id和当前浏览量(为方便观察效果,暂定一批3条),然后批量操作map,否则当阅读次数频繁时需要每次频繁操作持久化存储 */ func DoInCer(ingCh chan []ChData, ch chan struct{}) { count := 0 // 批量计数器 var batchList []ChData //m := make(map[string]int) for { count++ rand.Seed(time.Now().UnixNano()) num := rand.Intn(8) + 11 // 给id为num的文章增加浏览量 inCer := rand.Intn(3) + 1 newScore, err := utils.Rdb.ZIncrBy(context.TODO(), ZSetKey, float64(inCer), strconv.Itoa(num)).Result() if err != nil { fmt.Printf("给id为%v的文章增加浏览量失败, err:%v", num, err) return } //fmt.Println("发现文章", num, "的浏览量新增了", inCer) batchList = append(batchList, ChData{ID: num, IncerCount: inCer}) //if data, ok := m[strconv.Itoa(num)]; ok { // m[strconv.Itoa(num)] = inCer + data //} else { // m[strconv.Itoa(num)] = inCer //} fmt.Println("给id为", num, "的文章增加了", inCer, ",该文章当前浏览量为", newScore, "。") ch <- air if count >= 3 { select { case ingCh <- batchList: fmt.Println("本次", count, "条记录已批量加入。") count = 0 //for k := range batchList { // delete(m, strconv.Itoa(k)) //} batchList = []ChData{} } } } } /* 持续更新文章的浏览量到持久化存储,收到需要更新的通知就进行更新 */ func UpdateViews(ingCh chan []ChData) { for { select { case batchListData := <-ingCh: // 可以只用文章ID,最新浏览量从redis取;也可以先读取库中该文章的原浏览量进行加值,此处采取前者ZSCORE key member // 一次update一批持久化存储中只发生浏览量变化的文章记录,阅读量未变化的不操作 succCount := 0 var idList []string for _, updateObj := range batchListData { score, err := utils.Rdb.ZScore(context.TODO(), ZSetKey, strconv.Itoa(updateObj.ID)).Result() if err != nil { fmt.Printf("获取文章[%v]的分数失败, err:%v\n", updateObj.ID, err) continue } idList = append(idList, strconv.Itoa(updateObj.ID)) if data, ok := articleMap.Load(strconv.Itoa(updateObj.ID)); ok { obj := data.(Article) obj.Views = int64(score) // 最新值 articleMap.Store(strconv.Itoa(updateObj.ID), obj) fmt.Println(fmt.Sprintf("id为%v的文章已更新到map,当前浏览量为%v.", updateObj.ID, obj.Views)) succCount++ } else { // 进入失败队列,所增加阅读量对应的文章不存在 } } fmt.Println("本批", len(batchListData), "条变动记录[", strings.Join(idList, ","), "]已更新完毕!成功更新", succCount, "条。") } } } /* 只要收到浏览量变动,就触发获取top3的文章标题及浏览量 可以想象下直播时的礼物贡献榜topN实时显示的情景 */ func GetArticleTopN(n int, ch chan struct{}) { for { select { case <-ch: topNRet, err := utils.Rdb.ZRevRangeWithScores(context.TODO(), ZSetKey, 0, int64(n)).Result() if err != nil { fmt.Printf("获取top%v失败, err:%v\n", n, err) return } fmt.Println("------------------start top3------------------") for _, z := range topNRet { if obj, ok := articleMap.Load(z.Member.(string)); ok { fmt.Println(fmt.Sprintf("文章id=%v,分数=%v,文章标题=%v。", z.Member, z.Score, obj.(Article).Title)) } } fmt.Println("-------------------end top3-------------------") } } } //v2.0 func TestRedisTopIng2(t *testing.T) { utils.InitClient() ch := make(chan struct{}) batchIngCh := make(chan []ChData) DoCreateArticle() go GetArticleTopN(2, ch) go DoInCer(batchIngCh, ch) go UpdateViews(batchIngCh) for !false { fmt.Print() } }

    看看效果如何:

    添加成功 8条记录. 给id为 16 的文章增加了 1 ,该文章当前浏览量为 2 。 给id为 17 的文章增加了 1 ,该文章当前浏览量为 2 。 ------------------start top3------------------ 文章id=15,分数=3,文章标题=文章15的标题。 文章id=17,分数=2,文章标题=文章17的标题。 文章id=16,分数=2,文章标题=文章16的标题。 -------------------end top3------------------- ------------------start top3------------------ 文章id=15,分数=3,文章标题=文章15的标题。 文章id=17,分数=2,文章标题=文章17的标题。 文章id=16,分数=2,文章标题=文章16的标题。 -------------------end top3------------------- 给id为 15 的文章增加了 1 ,该文章当前浏览量为 4 。 本次 3 条记录已批量加入。 给id为 14 的文章增加了 1 ,该文章当前浏览量为 2 。 ------------------start top3------------------ 文章id=15,分数=4,文章标题=文章15的标题。 文章id=17,分数=2,文章标题=文章17的标题。 文章id=16,分数=2,文章标题=文章16的标题。 -------------------end top3------------------- ------------------start top3------------------ 文章id=15,分数=4,文章标题=文章15的标题。 文章id=17,分数=2,文章标题=文章17的标题。 文章id=16,分数=2,文章标题=文章16的标题。 -------------------end top3------------------- 给id为 16 的文章增加了 3 ,该文章当前浏览量为 5 。 给id为 18 的文章增加了 1 ,该文章当前浏览量为 2 。 ------------------start top3------------------ 文章id=16,分数=5,文章标题=文章16的标题。 文章id=15,分数=4,文章标题=文章15的标题。 文章id=18,分数=2,文章标题=文章18的标题。 -------------------end top3------------------- ------------------start top3------------------ 文章id=16,分数=5,文章标题=文章16的标题。 文章id=15,分数=4,文章标题=文章15的标题。 文章id=18,分数=2,文章标题=文章18的标题。 -------------------end top3------------------- id为16的文章已更新到map,当前浏览量为5. id为17的文章已更新到map,当前浏览量为2. id为15的文章已更新到map,当前浏览量为4. 本批 3 条变动记录[ 16,17,15 ]已更新完毕!成功更新 3 条。 本次 3 条记录已批量加入。 id为14的文章已更新到map,当前浏览量为2. id为16的文章已更新到map,当前浏览量为5. 给id为 15 的文章增加了 1 ,该文章当前浏览量为 5 。 id为18的文章已更新到map,当前浏览量为2. 本批 3 条变动记录[ 14,16,18 ]已更新完毕!成功更新 3 条。 ------------------start top3------------------ 文章id=16,分数=5,文章标题=文章16的标题。 文章id=15,分数=5,文章标题=文章15的标题。 文章id=13,分数=4,文章标题=文章13的标题。 -------------------end top3------------------- 给id为 13 的文章增加了 3 ,该文章当前浏览量为 4 。 ------------------start top3------------------ 文章id=16,分数=5,文章标题=文章16的标题。 文章id=15,分数=5,文章标题=文章15的标题。 文章id=13,分数=4,文章标题=文章13的标题。 -------------------end top3------------------- 给id为 11 的文章增加了 3 ,该文章当前浏览量为 4 。 本次 3 条记录已批量加入。 给id为 14 的文章增加了 1 ,该文章当前浏览量为 3 。 ------------------start top3------------------ 文章id=16,分数=5,文章标题=文章16的标题。 文章id=15,分数=5,文章标题=文章15的标题。 文章id=13,分数=4,文章标题=文章13的标题。 -------------------end top3------------------- id为15的文章已更新到map,当前浏览量为5. 给id为 17 的文章增加了 3 ,该文章当前浏览量为 5 。 ------------------start top3------------------ 文章id=16,分数=5,文章标题=文章16的标题。 文章id=15,分数=5,文章标题=文章15的标题。 文章id=13,分数=4,文章标题=文章13的标题。 -------------------end top3------------------- id为13的文章已更新到map,当前浏览量为4. 给id为 15 的文章增加了 3 ,该文章当前浏览量为 8 。 ------------------start top3------------------ 文章id=15,分数=8,文章标题=文章15的标题。 文章id=17,分数=5,文章标题=文章17的标题。 文章id=16,分数=5,文章标题=文章16的标题。 -------------------end top3------------------- id为11的文章已更新到map,当前浏览量为4. 本批 3 条变动记录[ 15,13,11 ]已更新完毕!成功更新 3 条。 本次 3 条记录已批量加入。 ------------------start top3------------------ 文章id=15,分数=8,文章标题=文章15的标题。 文章id=17,分数=5,文章标题=文章17的标题。 文章id=16,分数=5,文章标题=文章16的标题。 -------------------end top3------------------- id为14的文章已更新到map,当前浏览量为3. 给id为 16 的文章增加了 3 ,该文章当前浏览量为 8 。 给id为 18 的文章增加了 1 ,该文章当前浏览量为 3 。 id为17的文章已更新到map,当前浏览量为5. ------------------start top3------------------ 文章id=16,分数=8,文章标题=文章16的标题。 文章id=15,分数=8,文章标题=文章15的标题。 文章id=17,分数=5,文章标题=文章17的标题。 -------------------end top3------------------- id为15的文章已更新到map,当前浏览量为8. 本批 3 条变动记录[ 14,17,15 ]已更新完毕!成功更新 3 条。 ------------------start top3------------------ 文章id=16,分数=8,文章标题=文章16的标题。 文章id=15,分数=8,文章标题=文章15的标题。 文章id=17,分数=5,文章标题=文章17的标题。 -------------------end top3------------------- ... ... ...

    经过仔细观察日志结果没有问题,美滋滋!!!后续在这个基础上还能继续更新,这次先写到这吧。

    在代码中寻找快乐,写完代码对自己笑一笑。

    随风随浪飘荡

    随着一生里的浪

    你我在重叠的那一刻

    顷刻各在一方...

     

     

     

    Processed: 0.008, SQL: 9