青训营项目中期总结(4)| 青训营

74 阅读3分钟

gozero api服务调用rpc服务,rpc服务返回值为一个数据类型指针,一个error,踩坑,将两个值都设成nil返回时,在api中接收到的是nil+一个非nil的报错信息(deadline),因此,如果rpc未查到信息,正确返回值设置为数据类型的指针(其中的数据设为空数组之类的)+nil

在查询用户数据时,将该用户粉丝数和关注数存入redis,并设置过期时间为50分加10分内的一个随机值,防止缓存雪崩,在有关注操作时,判断redis中是否有对应的数据,有的话对redis数据进行更新,并刷新过期时间。

今日工作:

  • 编写rpc-获取用户作品数点赞数(简单的查询数据库count),但要在user服务中新增content数据库的配置,不然访问不到数据库

  • 修复获赞数不兼容用户作品为空的bug,修复评论列表不兼容评论列表为空的bug,这两个不兼容为空的bug都一样,就是在sql有in操作,但是为空的情况下,in()中没有值,sql报错,这块判断最开始列表为空就直接返回了,解决了bug还顺便优化了性能。

  • 编写用户信息redis宕机容灾,考虑到业务比较简单,多考虑一些极端情况,当redis服务宕机了之后(由于没做哨兵机制),数据要从数据库中取,取到后再回写redis(兼容暂时宕机)。

关注任务,之前考虑如果并发太高,采用开启协程用time.NewTicker的方式延迟执行,后来发现采用go关键字开启协程的方式不可行,因为协程往往要等待延迟的时间后才会结束,而主线程早就执行完毕,协程被迫死亡

go func(userId, followId string) { //新开协程执行延迟写的操作
   randomInterval := time.Duration(rand.Int63n(int64(10 * time.Minute))) //10分钟内的随机时间
   ticker := time.NewTicker(randomInterval)                              //延迟执行写入数据库
OuterLoop:
   for {
      select {
      case _ = <-ticker.C:
         _, err := l.svcCtx.UserRpcClient.AddFollows(l.ctx, &pb.AddFollowsReq{
            UserId:   userId,
            FollowId: followId,
         })
         if err == nil { //确实写进去了再结束
            ticker.Stop()
            break OuterLoop
         }
      }
   }
}(strconv.Itoa(int(logger.UserID)), strconv.FormatInt(req.ToUserId, 10))

改用time.AfterFunc的方式,不阻塞并延迟执行,同时使用time.ticker,用于写入失败后的重试

time.AfterFunc(randomInterval, func() {
   userId := strconv.Itoa(int(logger.UserID))
   followId := strconv.FormatInt(req.ToUserId, 10)
   _, err := l.svcCtx.UserRpcClient.AddFollows(l.ctx, &pb.AddFollowsReq{
      UserId:   userId,
      FollowId: followId,
   })
   if err != nil { //没写进去则隔一段时间重试
      randomInterval := time.Duration(rand.Int63n(int64(20 * time.Second))) //20s内的随机时间
      ticker := time.NewTicker(randomInterval)                              //延迟执行写入数据库
   OuterLoop:
      for {
         select {
         case _ = <-ticker.C:
            _, err := l.svcCtx.UserRpcClient.AddFollows(l.ctx, &pb.AddFollowsReq{
               UserId:   userId,
               FollowId: followId,
            })
            if err == nil { //确实写进去了再结束
               ticker.Stop()
               break OuterLoop
            }
         }
      }
   }
})

time.AfterFuncgo关键字用于不同的目的,有以下区别:

  1. 功能不同:time.AfterFunc用于在指定的时间间隔后执行一个函数,而go关键字用于开启一个新的并发协程来执行函数。
  2. 阻塞行为不同:time.AfterFunc是非阻塞的,即它会创建一个定时器并立即返回,不会阻塞当前的程序流程,而go关键字会启动一个新的并发协程,会在协程的函数执行完成之前阻塞当前的程序流程。
  3. 控制能力不同:time.AfterFunc提供了对定时器的控制,可以通过返回的*Timer类型值调用Stop()方法来取消定时器。而go关键字启动的协程在启动后就无法对其进行直接的控制,除非使用一些其他的同步机制,如通道、互斥锁等。
  4. 使用场景不同:time.AfterFunc适用于需要在一定时间后执行某个操作的场景,例如定时任务、延迟执行等。而go关键字适用于需要并发执行的场景,可以同时执行多个函数或任务。

综上所述,time.AfterFuncgo关键字用于不同的场景和目的,选择使用哪个取决于具体的需求。如果需要在指定时间后执行某个函数,可以使用time.AfterFunc;如果需要并发执行函数或任务,可以使用go关键字开启协程。