这是我参与「第五届青训营」伴学笔记创作活动的第 17 天
用户信息与评论信息分离
经过自己的初步实践和业务的分析,再看过了一个评论区缓存的生产环境案例的分享文章,我决定为用户信息单独分配一部分缓存,专门用来更新和查询用户的关注数和粉丝数。我使用的是新添加每位用户的关注数和粉丝数的本地缓存,这部分缓存的装入一般不是单独请求数据库得到的,如果你对一个视频的评论进行了数据库请求那么由于表连接就会得到评论的用户相应的数据,那么你可能会问这样的话引入新的缓存又有什么意义呢。答案是这部分数据主要是用来在用户更新自己的关注关系时,不需要被迫使整个缓存中的评论数据作废。只需要每次读取到本地缓存的评论时,从缓存中再取出评论用户的数据进行用户数据的更新覆写即可。
用户关注关系缓存
之前每次做已登录用户的评论读取时,都要额外访问数据库中的following表,返回用户的关注人群。这样仍有一次数据库调用,既然现在为了用户的关注数加入了缓存,并且决定在关注关系改变时修改缓存,那么就一并将用户的关注列表也一起缓存起来同步就好了,这样通过本地缓存又可以节省一次数据库访问。这部分的优化也是本次改造带来优化效果最大的。
检查视频是否存在时利用本地缓存
之前我提到过,每次对视频的评论进行读取或操作,都必须先保证视频存在,而验证视频存在的一个方法就是读取数据库中的视频表通过视频ID检索是否有对应行,但是这样势必会带来对数据库的读取,为了减少这一访问,可以通过在本地缓存中先看一下有没有相应视频ID的缓存存在,如果存在就可以直接返回合法,如果不存在,就降级为数据库查询,不过这种访问次数应该不多,因为一般对视频评论的操作一般都会先看到视频的评论,这一步就会有缓存存在了。
PitFall:锁不可重入
在编写和测试代码时我发现了一个问题,当运行到一个读取评论列表的请求后发现整个程序就不继续运行了,也不响应读取评论的http请求也报错。这让我很困惑,不过经过我看代码和log进行排查,最后我发现是在一处读取缓存或从MySQL读取的部分对另一个函数进行了调用,并且这个函数又进行了一次Lock操作,但是我的代码中Lock和Unlock都是成对出现的,难道是有在一些意想不到的地方返回了导致Unlock没有按期望执行?
后来我才发现原来是在调用的函数中占有锁的Lock函数阻塞了,由此我才发现原来sync.Mutex是不可重入的而且这个在Go程序设计语言中就提到了(中文),并给出了理由。他们认为可重入锁本身就是一个无法保证代码安全性的设计,因为它会让你1不知道自己到底在哪里调用和释放了锁,会互斥量的控制变得不可控了。但是我觉得可重入锁增加了代码的可复用性,并且加锁与解锁本来就可以通过defer语句达成一定程度的保护。
结论
经过上面和前面的一系列优化,我成功将读取列表访问(评论数1000),从原来的50ms以上的响应时间下降到1ms以内,内存分配和回收次数也减少了,因为每次gorm调用又需要为新生成的结果分配空间。当然由于加入本地缓存带来了业务代码复杂度的极大提高,因此当前的初步测试还不能验证我代码的完全正确性,还需要进一步的测试和修改。
从这一次优化我认识到了,大部分性能瓶颈还是出现在远程调用和数据库访问上,通过引入缓存空间换时间是比较划算的。同时,不能忘记数据的encode和decode行为也会带来计算负担和内存分配,减少这部分开销也是通过引入缓存带来的好处。