摸鱼鱼塘 2.0 版本发布
https://printf520.com/hot.html欢迎体验新版
更新说明
https://printf520.com/single.html?id=69之前一直打算优化热榜的历史架构问题,在讲优化点前,先看一看热榜遗留的历史问题:
热榜数据由一整个 json 串组成,造成的问题就是后期无法对热榜的历史数据查看,和每一条数据后期进行条件性筛选。
高强度的零散数据写入和读取,关系型数据库在表结构的扩展和数据库的写入造成问题
一次性获取几百条热榜数据,api 接口需要传输接近 20kb 的数据量,在用户高峰时期造成了热榜的卡顿
解决问题一:
对于造成上面三点的历史原因分析在于在初期创立热榜这个页面,基于快捷开发,产品先上,用户体验优化后期跟进的原则。所以遗留了这些问题,目前正是后期的优化时间点。先看第一个点,前期为了快速开发,数据的存储放在 mysql,并且由表的一个字段存储。表的结构如下图 1.0:
1.0 表结构图
可以看到在之前的设计里面每次 api 返回的数据其实就是一个 str 字段,当然这样的设计对于数据的读取和写入都是十分方便的,但是如果后期想要对热榜单独的每一条数据进行分类,这个 json 数据就显得十分的不方便了,加之综合第二点关系型数据库并不适合这样零散的数据,所以为了解决这一点,我选择了 nosql 领域比较成熟的一款数据库软件——mangodb。
解决问题二:
其实若不是第二点的原因,想要单独的存储每个数据不一致的热榜信息,mysql 也是可以胜任的,不过主要是因为不同的网站热榜所带的参数不一致,所以这个时候 mangodb 的面向文档存储就很适用了。对于文档型存储,其数据是用二进制的 Json 格式 Bson 存储的。数据就像 Ruby 的 hashes,或者 Python 的字典,或者 PHP 的数组,适用这样的数据类型完全可以满足每个热榜的数据差异性,并且 mangodb 没有固定的表结构,不用为了修改表结构而进行数据迁移,比如如果业务需要在原有的表增加一个字段用于记录每行数据的点击量,如果使用 mysql 我们需要修改整个表结构,然后再更新整个字段,但是对于 mangodb 来说,基于文档的存储可以不用修改表结构,直接把带有用户点击量的字段插入到表里即可。
解决问题三:
之前有不少的用户反馈一次性获取 200 条热榜数据,会响应一段时间,接到这个问题后,发现其中一个原因就是因为 api 每次需要返回的数据量过大,在不增加服务器带宽和使用加速 CDN 的情况下,我使用的解决方式是开启 API 的 gzip 模式,开启 gzip 后 api 返回的不再是原滋原味的原生数据,而是通过服务器端压缩后传输的 gzip 压缩数据,带来最直接的效果就是数据被压缩了不少,对比图如下:
3.0 压缩后的 api 数据大小
3.1 压缩前的 api 数据大小
从 2 张图的对比可以看到,开启 gzip 后的 api 返回数据明显比未开启前小了不少,数据越小传输速度越快,当然压缩带来的成本是 API 在每次返回数据前 CPU 会执行压缩算法,不过在当前带宽不足,而 CPU 足余的情况下可以忽略不计,并且压缩后的数据可以直接存放在 redis 的缓存里面,避免二次压缩。值得一提的是 Golang 语言并不需要使用 Nginx 和 tomcat 的服务器软件,所以开启 Golang 的 http gzip 需要由自己实现,下面贴出 Golang 开启 Gzip 的关键代码段:
package main
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func makeGzipHandler(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
fn(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
fn(gzr, r)
}
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
jsonStr := `{"Code":0,"Message":"获取数据成功","Data":"热榜数据","CreateTime":1569673821}]}`
w.Write([]byte(jsonStr))
}
func main() {
http.HandleFunc("/a", makeGzipHandler(handler)) // 设置访问的路由
http.ListenAndServe(":1113", nil)
}
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
https://www.v2ex.com/t/605629
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.