1、场景描述
比如说我们要做一款 APP,需要通过 api 接口给 app 提供数据。假设我们是做商城,比如我们卖书的。我们可以想象下这个 APP 大概有哪些内容:
1)首页:banner 区域(可以是一些热门书籍的图片做推广)、本周热卖书籍区域、本月好评书籍区域、活动打折的书籍区域。。。
2)排行榜:比如第一季度热销榜、新书版。。。
3)书单:管理后台运营添加的书单,比如《程序员从入门到放弃》系列书单。。。
4)用户相关的:比如用户个人信息设置、订单管理、消息管理、收藏的书籍。。。
数据是保存在数据库中,考虑到高并发数据库的瓶颈,采用 DB + 缓存的服务器架构。
2、重要接口汇总
看似简单的一个 app,需要调用的 api 接口是非常多的,总结下大概有这几类接口:
1)列表接口:比如书单里面的书籍列表、排行榜的书籍列表;
2)详情接口:书籍的详细信息;
3)评论接口:书籍评论(这里可能要求购买了的才能评论)、星标;
4)点赞接口:给书籍点赞、给书单点赞;
5)收藏接口:收藏书籍、收藏书单;
6)“相关” 接口:比如书籍《php 从入门到放弃》相关的有哪些书籍;
7)关注接口:关注某本书或者书籍作者,一旦某本书有打折或者作者有新书,会推消息等等。或者是用户间互相关注;
8)发布接口:比如用户可以发布书单。A 用户发布了书单,B 用户可以关注 A 用户,A 用户再发布新书单,会给 B 用户推消息等等;
9)搜索接口:查询书籍、查询书单、查询用户等等
3、后台管理系统
1)书籍信息的来源:爬虫抓取的数据、运营人员在管理后台添加;
2)书籍的价格、库存等信息,是可以在后台设置;
3)书籍的分类、书籍的标签,像爬虫如果抓取不到的,运营可以手动设置;
4)书籍的排序:在管理后台设置,用于在 app 上展示的先后顺序;
5)书单管理:运营可以添加、可以设置展示 / 隐藏;用户提交的书单,在后台进行审核;
6)用户的评论:在后台审核;
备注:管理系统还有很多功能,简单先写这几个。
4、api 接口设计
接口设计要遵循的一个想法:可以先从缓存读取,读取不到,再去数据库读取,然后写回缓存。
1)banner 数据: key-value 类型(string 类型)
redis key 名: index:banners 这个 key 是首页上面的 banner 可以设置一个缓存时间:1 小时
里面保存的数据是从数据库获取出来的 banner 数据,json 之后保存的字符串。
2)实时性问题
比如数据是在管理设置的,比如 banner 数据添加了一条,或者某一个 banner 数据被删除了或者改为不展示了,但是缓存里面还没变化,怎么处理?
【方案一】不做处理,等待缓存自己过期,然后再有请求获取的就是新的数据。缓存要有个过期时间,这个要看业务需要设置相应的缓存过期时间,比如 5 分钟。
【方案二】管理后台对 banner 有添加删除修改操作时,修改成功的同时把缓存中的数据也更新。这个方案不太赞成,因为这样子数据就有 2 个地方维护了,
一个是 api 接口访问的时候,如果缓存过期会回源写缓存,一个是管理后台;
【方案三】解决方案二的缺点,就是管理后台有数据增加删除修改的时候,把对应的缓存删掉,这样子 api 接口从缓存读取不到 key 就会重新生成缓存。这样也是比较实时的。
3)详情数据
详情信息可以放在缓存里面,key-value 类型,设置一个过期时间
redis key 名: book:detail:{book_id}
4)列表数据:key-value 类型
【方案一】把每一页的数据缓存起来,这样 api 接口来获取的时候,直接把一页的数据从缓存取出来然后返回。
redis key 名:index:book_list:{page} 缓存时间根据实际业务设置
假设每一页 20 条数据,这里保存的就是 20 条数据的 json 后的字符串,key 名里面有页码,每一页的数据用不同的 key 名缓存起来。
这个方案有个问题,比如管理后台删除一条数据,刚好缓存重新生成了,那么第一页的 20 条数据肯定是最新的,但是第二页如果缓存没有重新生成的,
那么新的第一页和旧的第二页会有一条数据重复。当然,如果新增数据那么第一页的缓存和第二页的缓存数据中间有一条数据遗漏了。。。
【方案二】把 id 放入有序集合,每次获取列表,从集合中获取出 20 个 id,然后去缓存中获取详情数据,然后返回。
管理后台如果有新增删除,那么就在集合里面新增或删除。 这里相比方案一,就是每次要去缓存获取 20 条详情信息,虽然缓存中获取很快但是也有一定的开销,但是数据最实时。
还有一个问题,感觉就是有点耦合,就是万一集合失效,那么要重建集合,假如数据很多,比如有几十万几百万个 id,那么要去数据库获取,然后写入缓存,有这种隐患。
【方案三】不用缓存,采用像 spinx、es 这些搜索引擎。通常有些业务需要很多排序规则,比如 sort 字段、时间字段、销量字段等等等,这个时候如果硬是要用方案二,
就必须是把排好序的数据保存如有序集合,比如 id=1,序号 = 1;id=15,序号 = 2;id=8,序号 = 3,每次有新增、删除,就要重新生成集合,很不好维护。
使用搜索系统来辅助,可以应该解决方案一和方案二的问题。当然了,看业务,如果不要紧的,方案一即可。像新浪博客、今日头条,之前也看到他们有些分页数据有重复的。
5)点赞接口
根据不同的业务要求,有些业务是只有点赞,没有取消点赞,我们就讨论这个有取消点赞的。
首先,假设我们有个业务是对书籍点赞,点赞之后可以取消点赞。比如说 APP 首页的列表里面需要点赞数,而且要实时,怎么处理?
我们肯定有个数据表来存储点赞信息,比如 book_like 表,里面记录了用户对书籍的点赞信息。
鉴于点赞数如果每次用户接口请求都去数据库请求,会给数据库增加压力,我们使用 redis 来减压。这里使用 hash 类型,下面会讲为何。
redis key 名: book:like:{hash_id} 内容: field =》 value
这里的 hash_id 是 book_id / 1000 的结果
field 的值是 book_id % 1000 的结果
value 的值是点赞数
为何这种设计:hash 相比 string 类型有个优势,在满足 2 个条件的时候会进行压缩,就是内存的占用会小很多。
这两个条件是 hash 里面的 field 的数量小于指定数量(貌似默认是 512 还是 1024),另外一个条件是 value 值的大小要小于 64 字节(这个也是可以调整的,配置信息里面改)。
举个例子:
book_id = 100, 那么 hash_id 就是 0,field 是 100,值是点赞数
book_id = 1001, 那么 hash_id 就是 1,field 是 1,值是点赞数
列表接口里面有点赞数,可以从 hash 中获取
多次 redis 的 get 请求(get、hget 等),可以用管道(pipeline)来一次性获取,也会提高速度
点赞的时候,数据肯定是要写入数据库的,有 2 种方案处理:
【方案一】
书籍表里面比如有个点赞数字段,每次点赞 / 取消点赞,就是往里面加 1 减 1。
点赞表就是往里面增加或删除数据(可以是软删除)。
像 php 的 laravel 的 eloquent 模型是支持直接给字段 +-1 这样,不需要先取出数据再加。
【方案二】
先写入缓存,然后定期把缓存的数据取出来然后写入数据库。这个方案的话有点耦合了,比如需要定时脚本去写回数据库。
【方案三】
不使用缓存,数据直接更新回 book 表,或者 book 表不需要这个字段,需要点赞数的时候去 book_like 表计算,如果是有搜索引擎系统,那么应该不成问题。
6)相关接口
比如根据分类关联书籍。《php 从入门到放弃》和《php 从入门到奔溃》关联。
数据库里面可以根据分类 id 查找。简单的业务可以通过集合存储分类 id 下有哪些书籍。
如果复杂的业务,比如书籍有多个分类,比如分类 id=1 , 分类名称:php,分类 id=2 ,分类名称:程序开发,假设这 2 个分类是同级的,
那么可能有一些书是同时属于这 2 个分类的。
又或者有些业务下,是根据标签来关联,一个书籍可以有多个标签,标签和书籍是多对多关系。
此时又要来一句搜索系统可以处理。。。缓存也不是不能处理,感觉有些场景缓存处理起来很费劲。
以上只是一些很肤浅的看法,肯定有更好的处理方案,待研究补充上。
5、api 接口设计总结
考虑到高并发时数据库的瓶颈,所以需要把请求的结果缓存起来。这里的策略是:
1)从缓存中获取数据,缓存中有数据则直接返回,或者做简单处理然后返回;
2)缓存没有数据,则从 DB 中查找数据,然后写回缓存;这种情况叫做 “回源”。
3)缓存的 key 是需要设置过期时间的,避免数据一直占用内存;
4)过期时间的设计,最好是打乱,避免同一时间有大量的 key 过期导致请求集中去 DB 请求导致雪崩;
5)根据业务需求,“回源” 的时候考虑申请到缓存锁的请求去数据库获取数据并更新缓存,其他请求则 sleep 一段时间(比如 5 毫秒 10 毫秒)然后再去缓存请求,如果请求还是没数据,可以继续等待或者直接返回空数据。
6)没有必要缓存起来的字段,不要缓存;
7)APP 不需要用到的字段没有必要返回给 APP,比如评论的审核时间、审核者,返回给 APP 是没用的,因为这些数据不需要展示出来,不会被用到;
8)减少 api 请求的次数,比如多次请求,要看下能否合并,比如首页,有好几个区域,每个区域都有几条数据。当然多次请求一样可以实现,但是请求次数少,则服务器可以接受更多客户端的请求。